Está en la página 1de 10

10/9/23, 17:18 desarrollo_actividad_1.

ipynb - Colaboratory

Actividad HDFS, Spark SQL y MLlib

Integrantes:
Diego Armando
Jorge Ayala
Diego Serrano

Instrucciones para funcionamiento del desarrollo


Cargar archivo flights_act1.csv a la carpeta raiz del directorio de Google Colab que se encuentra en la aprte izquierda de esta pantalla en
el icono de "Carpeta"
Clic derecho y seleccionar "Subir",
Seleccionar el archivo flights_act1.csv

Indice de los pasos desarrollados


1. Transformación y limpieza del dataframe

2. Ejercicio 1 - Seleccion y conteo

3. Ejercicio 2 - Calculo

4. Ejercicio 3 - Preparación y Entrenamiento de modelo

5. Conclusiones

....

Transformación y limpieza del dataframe

1.Paso Se Debe Realizar Instalacion De Spark Y Su Ecosistema Hadoop, Por Ultimo Se Realiza Instalacion De Pyspark Como Interprete De
Codigo

"""
!apt-get install openjdk-8-jdk-headless -qq > /dev/null
!wget -q http://archive.apache.org/dist/spark/spark-3.1.1/spark-3.1.1-bin-hadoop3.2.tgz
!tar xf spark-3.1.1-bin-hadoop3.2.tgz
!pip install -q findspark
!pip install pyspark
"""

'\n!apt-get install openjdk-8-jdk-headless -qq > /dev/null\n!wget -q http://archive.apache.org/dist/spark/spark-3.1.


1/spark-3.1.1-bin-hadoop3.2.tgz\n!tar xf spark-3.1.1-bin-hadoop3.2.tgz\n!pip install -q findspark\n!pip install pyspa
rk\n'

2. Importar las librerias requeridas y que se utilizaran durante el desarrollo

https://colab.research.google.com/drive/1VVkPUNnwYYHQ4Z7OSht_TRBWgxj7Xr39#scrollTo=GZpY0seut128&printMode=true 1/10
10/9/23, 17:18 desarrollo_actividad_1.ipynb - Colaboratory

from pyspark.sql import SparkSession


from pyspark.sql import functions as F
from pyspark.sql.functions import col, sum, count, countDistinct, avg, round
from pyspark.sql.types import IntegerType, DoubleType
from pyspark.ml.feature import StringIndexer
from pyspark.ml.feature import VectorAssembler
from pyspark.ml.feature import Binarizer
from pyspark.ml.classification import DecisionTreeClassifier
from pyspark.ml import Pipeline

3. Se crea la sesión de Spark en la cual se desarrollara la actividad

1. Se realiza copypath de la ruta en donde se encuentra el fichero que se requiere importar


2. Se inicializa variable flightsDF con valor None
3. Se realiza lectura del archivo spark por medio del comando spark.read con las opciones complementarias para traer encabezados y el
esquema
4. Se indica el formato en el cual se encuentra el archivo a importar y detro del parentesis se especifica la ruta_hdfs declada en el punto 1

requiere importar 3.2 la variable flightsDF

spark = SparkSession.builder.appName("ActividadSpark").getOrCreate()

ruta_hdfs = "/content/flights_act1.csv"
flightsDF = None

flightsDF = spark.read\
.option("header", "true")\
.option("inferSchema", "true")\
.csv(ruta_hdfs)

4. Conocer el tipo de datos a traves del comando .printSchema()

flightsDF.printSchema()

root
|-- year: integer (nullable = true)
|-- month: integer (nullable = true)
|-- day: integer (nullable = true)
|-- dep_time: string (nullable = true)
|-- dep_delay: string (nullable = true)
|-- arr_time: string (nullable = true)
|-- arr_delay: string (nullable = true)
|-- carrier: string (nullable = true)
|-- tailnum: string (nullable = true)
|-- flight: integer (nullable = true)
|-- origin: string (nullable = true)
|-- dest: string (nullable = true)
|-- air_time: string (nullable = true)
|-- distance: integer (nullable = true)
|-- hour: string (nullable = true)
|-- minute: string (nullable = true)

https://colab.research.google.com/drive/1VVkPUNnwYYHQ4Z7OSht_TRBWgxj7Xr39#scrollTo=GZpY0seut128&printMode=true 2/10
10/9/23, 17:18 desarrollo_actividad_1.ipynb - Colaboratory

5. Conocer la cantidad de registros que existen en el dataframe de Spark a traves del comando .count()

flightsDF.count()

162049

6. Mostrar los primeros 5 registros del dataframe flightDF

flightsDF.show(5)

+----+-----+---+--------+---------+--------+---------+-------+-------+------+------+----+--------+--------+----+------+
|year|month|day|dep_time|dep_delay|arr_time|arr_delay|carrier|tailnum|flight|origin|dest|air_time|distance|hour|minute|
+----+-----+---+--------+---------+--------+---------+-------+-------+------+------+----+--------+--------+----+------+
|2014| 1| 1| 1| 96| 235| 70| AS| N508AS| 145| PDX| ANC| 194| 1542| 0| 1|
|2014| 1| 1| 4| -6| 738| -23| US| N195UW| 1830| SEA| CLT| 252| 2279| 0| 4|
|2014| 1| 1| 8| 13| 548| -4| UA| N37422| 1609| PDX| IAH| 201| 1825| 0| 8|
|2014| 1| 1| 28| -2| 800| -23| US| N547UW| 466| PDX| CLT| 251| 2282| 0| 28|
|2014| 1| 1| 34| 44| 325| 43| AS| N762AS| 121| SEA| ANC| 201| 1448| 0| 34|
+----+-----+---+--------+---------+--------+---------+-------+-------+------+------+----+--------+--------+----+------+
only showing top 5 rows

7. Se realiza recuento de los valores NA que existen para la columna dep_time **texto en negrita

cuantos_NA = flightsDF\
.where(F.col("dep_time") == "NA")\
.count()
cuantos_NA

857

Otra Alternativa para conocer el total de "NA" por cada columna y optimizar mejor el codigo seria haciendo una sumatoria de los valores NA
por cada columna del dataframe flightsDF, se debe realiza un CAST para poder realizar el conteo

na_por_columna = flightsDF.select([sum((col(c) == "NA").cast("int")).alias(c) for c in flightsDF.columns])


na_por_columna.show()

+----+-----+----+--------+---------+--------+---------+-------+-------+------+------+----+--------+--------+----+------+
|year|month| day|dep_time|dep_delay|arr_time|arr_delay|carrier|tailnum|flight|origin|dest|air_time|distance|hour|minute|
+----+-----+----+--------+---------+--------+---------+-------+-------+------+------+----+--------+--------+----+------+
|null| null|null| 857| 857| 988| 1301| 0| 0| null| 0| 0| 1301| null| 857| 857|
+----+-----+----+--------+---------+--------+---------+-------+-------+------+------+----+--------+--------+----+------+

8. Se realiza filtrado de datos para aquellos valores que no tienen un valor de "NA"

columnas_limpiar = ["dep_time", "dep_delay", "arr_time", "arr_delay", "air_time", "hour", "minute"]

flightsLimpiado = flightsDF
for nombreColumna in columnas_limpiar: # para cada columna, nos quedamos con las filas que no tienen NA en esa columna
flightsLimpiado = flightsLimpiado.where(F.col(nombreColumna) != "NA")

https://colab.research.google.com/drive/1VVkPUNnwYYHQ4Z7OSht_TRBWgxj7Xr39#scrollTo=GZpY0seut128&printMode=true 3/10
10/9/23, 17:18 desarrollo_actividad_1.ipynb - Colaboratory
flightsLimpiado.cache()
flightsLimpiado.count()

160748

9. Se realiza conversión de las columnas de tipo de dato String a Double "dep_time", "dep_delay", "arr_time", "arr_delay", "air_time", "hour",
"minute" , necesario para los pasos posteriores donde ajustaremos un modelo predictivo.

flightsConvertido = flightsLimpiado

for c in columnas_limpiar:
flightsConvertido = flightsConvertido.withColumn(c, col(c).cast(IntegerType()))

flightsConvertido = flightsConvertido.withColumn("dep_time", F.col("dep_time").cast(DoubleType()))\


.withColumn("dep_delay", F.col("dep_delay").cast(DoubleType()))\
.withColumn("arr_time", F.col("arr_time").cast(DoubleType()))\
.withColumn("air_time", F.col("air_time").cast(DoubleType()))\
.withColumn("hour", F.col("hour").cast(DoubleType()))\
.withColumn("minute", F.col("minute").cast(DoubleType()))\
.withColumn("arr_delay", col("arr_delay").cast("double"))

flightsConvertido.cache()
flightsConvertido.printSchema()
flightsConvertido.show(5)

root
|-- year: integer (nullable = true)
|-- month: integer (nullable = true)
|-- day: integer (nullable = true)
|-- dep_time: double (nullable = true)
|-- dep_delay: double (nullable = true)
|-- arr_time: double (nullable = true)
|-- arr_delay: double (nullable = true)
|-- carrier: string (nullable = true)
|-- tailnum: string (nullable = true)
|-- flight: integer (nullable = true)
|-- origin: string (nullable = true)
|-- dest: string (nullable = true)
|-- air_time: double (nullable = true)
|-- distance: integer (nullable = true)
|-- hour: double (nullable = true)
|-- minute: double (nullable = true)

+----+-----+---+--------+---------+--------+---------+-------+-------+------+------+----+--------+--------+----+------+
|year|month|day|dep_time|dep_delay|arr_time|arr_delay|carrier|tailnum|flight|origin|dest|air_time|distance|hour|minute|
+----+-----+---+--------+---------+--------+---------+-------+-------+------+------+----+--------+--------+----+------+
|2014| 1| 1| 1.0| 96.0| 235.0| 70.0| AS| N508AS| 145| PDX| ANC| 194.0| 1542| 0.0| 1.0|
|2014| 1| 1| 4.0| -6.0| 738.0| -23.0| US| N195UW| 1830| SEA| CLT| 252.0| 2279| 0.0| 4.0|
|2014| 1| 1| 8.0| 13.0| 548.0| -4.0| UA| N37422| 1609| PDX| IAH| 201.0| 1825| 0.0| 8.0|
|2014| 1| 1| 28.0| -2.0| 800.0| -23.0| US| N547UW| 466| PDX| CLT| 251.0| 2282| 0.0| 28.0|
|2014| 1| 1| 34.0| 44.0| 325.0| 43.0| AS| N762AS| 121| SEA| ANC| 201.0| 1448| 0.0| 34.0|
+----+-----+---+--------+---------+--------+---------+-------+-------+------+------+----+--------+--------+----+------+
only showing top 5 rows

Ejercicio 1 - Seleccion y conteo


https://colab.research.google.com/drive/1VVkPUNnwYYHQ4Z7OSht_TRBWgxj7Xr39#scrollTo=GZpY0seut128&printMode=true 4/10
10/9/23, 17:18 desarrollo_actividad_1.ipynb - Colaboratory
Aeropuertos de Origen Únicos: Se seleccionan los valores únicos de la columna "origin" del DataFrame flightsConvertido utilizando la función
distinct(). Esto genera una lista de los aeropuertos de origen únicos y se almacena en el DataFrame aeropuertosOrigenDF.

Conteo de Aeropuertos de Origen: Se cuenta la cantidad de aeropuertos de origen únicos utilizando la función count() en el DataFrame
aeropuertosOrigenDF, y el resultado se almacena en la variable n_origen.

Rutas Distintas: Se seleccionan las columnas "origin" y "dest" del DataFrame flightsConvertido y se eliminan las filas duplicadas utilizando la
función distinct(). Esto proporciona una lista de rutas distintas y se almacena en el DataFrame rutasDistintasDF. Además, las rutas se ordenan
en orden ascendente según el aeropuerto de origen y el destino utilizando orderBy.

Conteo de Rutas Distintas: Se cuenta la cantidad de rutas distintas utilizando la función count() en el DataFrame rutasDistintasDF, y el
resultado se almacena en la variable n_rutas.

Dataframes resultantes

El DataFrame aeropuertosOrigenDF contiene una lista de aeropuertos de origen


n_origen almacena el número total de aeropuertos de origen únicos.
El DataFrame rutasDistintasDF contiene una lista de rutas distintas ordenadas por aeropuerto de origen y destino.
n_rutas almacena el número total de rutas distintas con valores unicos.

Haz doble clic (o pulsa Intro) para editar

aeropuertosOrigenDF = flightsConvertido.select("origin").distinct()
n_origen = aeropuertosOrigenDF.select("origin").count()
rutasDistintasDF = flightsConvertido.select("origin","dest")\
.orderBy("origin","dest") \
.distinct()
n_rutas = rutasDistintasDF.count()

aeropuertosOrigenDF.show()
print("n_origen: ",n_origen)
rutasDistintasDF.show()
print("n_rutas: ",n_rutas)

assert(n_origen == 2)
assert(n_rutas == 115)
assert(aeropuertosOrigenDF.count() == n_origen)
assert(rutasDistintasDF.count() == n_rutas)

+------+
|origin|
+------+
| SEA|
| PDX|
+------+

n_origen: 2
+------+----+
|origin|dest|
+------+----+
| SEA| RNO|
| SEA| DTW|
| SEA| CLE|

https://colab.research.google.com/drive/1VVkPUNnwYYHQ4Z7OSht_TRBWgxj7Xr39#scrollTo=GZpY0seut128&printMode=true 5/10
10/9/23, 17:18 desarrollo_actividad_1.ipynb - Colaboratory
| SEA| LAX|
| PDX| SEA|
| SEA| BLI|
| PDX| IAH|
| PDX| PHX|
| SEA| SLC|
| SEA| SBA|
| SEA| BWI|
| PDX| IAD|
| PDX| SFO|
| SEA| KOA|
| SEA| JAC|
| PDX| MCI|
| SEA| SJC|
| SEA| ABQ|
| SEA| SAT|
| PDX| ONT|
+------+----+
only showing top 20 rows

n_rutas: 115

Ejercicio 2 - Calculo
La función retrasoMedio se encarga de calcular el retraso medio a la llegada de los vuelos que tienen un retraso positivo, para cada aeropuerto
de destino. Los resultados se almacenan en un DataFrame de PySpark y se ordenan de mayor a menor retraso medio.

Argumentos de entrada: df (DataFrame de PySpark): El DataFrame de entrada que contiene los datos de los vuelos.

Filtrar los vuelos con retraso positivo: Se utiliza la función filter para seleccionar únicamente las filas que tienen un valor de retraso a la
llegada (arr_delay) mayor que cero. Estos son los vuelos que llegaron con retraso.

Calcular el retraso medio por destino: Se agrupan los datos filtrados por el aeropuerto de destino (dest) utilizando la función groupBy.
Luego, se calcula el retraso medio de llegada (retraso_medio) redondeado a 2 decimales utilizando la función round y la función de
agregación avg.

Ordenar el DataFrame resultante: El DataFrame resultante se ordena de mayor a menor retraso medio utilizando la función orderBy. Esto
permite tener una vista ordenada de los aeropuertos de destino con el mayor retraso medio primero.

Retorno del resultado: La función devuelve el DataFrame resultante con la columna "dest" (aeropuerto de destino) y "retraso_medio"
(retraso medio a la llegada)

Filtar los 3 aeropuertos con mayor retraso medio: Se realiza filtro con la función .limit(3) para conocer los 3 primeros aeropuetos con
mayor retraso medio

def retrasoMedio(df):

vuelos_con_retraso = df.filter(col("arr_delay") > 0)


retraso_medio_df = vuelos_con_retraso.groupBy("dest")\
.agg(round(avg("arr_delay"), 2)\
.alias("retraso_medio"))
retraso_medio_df = retraso_medio_df.orderBy(col("retraso_medio").desc())

return retraso_medio_df

retrasoMedioDF = retrasoMedio(flightsConvertido)
https://colab.research.google.com/drive/1VVkPUNnwYYHQ4Z7OSht_TRBWgxj7Xr39#scrollTo=GZpY0seut128&printMode=true 6/10
10/9/23, 17:18 desarrollo_actividad_1.ipynb - Colaboratory
( g )
retrasoMedioDF.show()
retrasoMedioDF.printSchema()

assert (lista[0].dest == "BOI" and abs(lista[0].retraso_medio - 64.75) < 0.01)


assert (lista[1].dest == "HDN" and abs(lista[1].retraso_medio - 46.8) < 0.01)
assert (lista[2].dest == "SFO" and abs(lista[2].retraso_medio - 41.19) < 0.01)

#Top 3 de los mayores aeropuestos con retraso medio


top3Retraso = retrasoMedioDF.limit(3)
top3Retraso.show()

+----+-------------+
|dest|retraso_medio|
+----+-------------+
| BOI| 64.75|
| HDN| 46.8|
| SFO| 41.19|
| CLE| 35.74|
| SBA| 35.39|
| COS| 35.06|
| BWI| 34.59|
| EWR| 33.53|
| DFW| 33.28|
| MIA| 32.66|
| ORD| 32.48|
| BNA| 31.95|
| JFK| 31.26|
| JAC| 30.25|
| PHL| 29.25|
| OGG| 27.51|
| IAD| 27.43|
| HOU| 27.33|
| LGB| 27.08|
| FAT| 26.85|
+----+-------------+
only showing top 20 rows

root
|-- dest: string (nullable = true)
|-- retraso_medio: double (nullable = true)

+----+-------------+
|dest|retraso_medio|
+----+-------------+
| BOI| 64.75|
| HDN| 46.8|
| SFO| 41.19|
+----+-------------+

Ejercicio 3 - Preparación y Entrenamiento de modelo


Este código muestra cómo preparar y entrenar un modelo de clasificación utilizando Apache Spark (PySpark). El objetivo es predecir si un vuelo
llegará con retraso o no, utilizando diversas características de los vuelos como lo son "monthIndexed", "day", "dep_time", "arr_time",
"carrierIndexed", "distance", "air_time"

https://colab.research.google.com/drive/1VVkPUNnwYYHQ4Z7OSht_TRBWgxj7Xr39#scrollTo=GZpY0seut128&printMode=true 7/10
10/9/23, 17:18 desarrollo_actividad_1.ipynb - Colaboratory

Indexación de Variables Categóricas: En esta etapa, se utilizan los StringIndexer de PySpark para convertir las variables categóricas
'month' y 'carrier' en representaciones numéricas. Los resultados se almacenan en las columnas 'monthIndexed' y 'carrierIndexed',
respectivamente.

Ensamblado de Características: En esta parte, se crea una lista de las columnas que se utilizarán como variables predictoras y se
almacenan en la variable columnas_ensamblar. Luego, se utiliza VectorAssembler para ensamblar estas características en una única
columna de tipo vector llamada 'features'.

Binarización de la Columna Objetivo: En esta etapa, se utiliza Binarizer para convertir la columna objetivo 'arr_delay' en una variable
binaria. Los vuelos con un retraso superior a 15 minutos se consideran "retrasados" y se almacenan en la columna 'arr_delay_binary'.

Creación del Modelo de Clasificación (Árbol de Decisión): Se crea un modelo de clasificación utilizando un árbol de decisión. Las
características ensambladas se utilizan como columnas de entrada (featuresCol) y la columna binaria 'arr_delay_binary' se utiliza como
columna objetivo (labelCol).

Creación de un Pipeline: Se crea un objeto Pipeline que encapsula todas las etapas anteriores en el orden adecuado. Esto permite
ejecutar todas las transformaciones y entrenar el modelo de manera organizada.

Entrenamiento del Modelo: El pipeline se entrena utilizando el método fit con los datos de vuelo proporcionados en flightsConvertido. El
modelo entrenado se almacena en pipelineModel.

Predicciones: Se utiliza el pipeline entrenado para realizar predicciones sobre el mismo DataFrame flightsConvertido. Las predicciones se
almacenan en flightsPredictions.

*Resumen de Predicciones: * Se muestra un resumen de las predicciones, incluyendo la columna binaria 'arr_delay_binary' y la columna de
predicción 'prediction', junto con el recuento de casos.

# Crear un StringIndexer para la variable categórica 'month'


indexerMonth = StringIndexer(inputCol="month", outputCol="monthIndexed")

# Crear un StringIndexer para la variable categórica 'carrier' (tipo de avión)


indexerCarrier = StringIndexer(inputCol="carrier", outputCol="carrierIndexed")

print(indexerMonth)
print(indexerCarrier)

assert(isinstance(indexerMonth, StringIndexer))
assert(isinstance(indexerCarrier, StringIndexer))
assert(indexerMonth.getInputCol() == "month")
assert(indexerMonth.getOutputCol() == "monthIndexed")
assert(indexerCarrier.getInputCol() == "carrier")
assert(indexerCarrier.getOutputCol() == "carrierIndexed")

# Definir la lista de columnas de entrada (variables predictoras)


columnas_ensamblar = ["monthIndexed", "day", "dep_time", "arr_time", "carrierIndexed", "distance", "air_time"]

# Crear el VectorAssembler para ensamblar las características en una columna de tipo vector
vectorAssembler = VectorAssembler(inputCols=columnas_ensamblar, outputCol="features")

print(vectorAssembler)

assert(isinstance(vectorAssembler, VectorAssembler))
assert(vectorAssembler.getOutputCol() == "features")
input_cols = vectorAssembler.getInputCols()
assert(len(input_cols) == 7)

https://colab.research.google.com/drive/1VVkPUNnwYYHQ4Z7OSht_TRBWgxj7Xr39#scrollTo=GZpY0seut128&printMode=true 8/10
10/9/23, 17:18 desarrollo_actividad_1.ipynb - Colaboratory
assert("arr_delay" not in input_cols)

# Crear el Binarizer con un umbral de 15 minutos para considerar vuelos retrasados


delayBinarizer = Binarizer(inputCol="arr_delay", outputCol="arr_delay_binary", threshold=15.0)

print(delayBinarizer)

assert(isinstance(delayBinarizer, Binarizer))
assert(delayBinarizer.getThreshold() == 15)
assert(delayBinarizer.getInputCol() == "arr_delay")
assert(delayBinarizer.getOutputCol() == "arr_delay_binary")

decisionTree = DecisionTreeClassifier(featuresCol="features", labelCol="arr_delay_binary")


print(decisionTree)

assert(isinstance(decisionTree, DecisionTreeClassifier))
assert(decisionTree.getFeaturesCol() == "features")
assert(decisionTree.getLabelCol() == "arr_delay_binary")

# Crear un objeto Pipeline con las etapas adecuadas


pipeline = Pipeline(stages=[indexerMonth, indexerCarrier, vectorAssembler, delayBinarizer, decisionTree])

# Entrenar el pipeline y obtener el modelo


pipelineModel = pipeline.fit(flightsConvertido)

# Aplicar el pipeline entrenado para predecir sobre el DataFrame flightsConvertido


flightsPredictions = pipelineModel.transform(flightsConvertido)

flightsPredictions.groupBy("arr_delay_binary", "prediction").count().show()

StringIndexer_6475c1839e85
StringIndexer_93f178a8a066
VectorAssembler_6c1a59d30fff
Binarizer_dbadf623ee8c
DecisionTreeClassifier_fa034ad0e89b
+----------------+----------+------+
|arr_delay_binary|prediction| count|
+----------------+----------+------+
| 1.0| 1.0| 796|
| 0.0| 1.0| 252|
| 1.0| 0.0| 23453|
| 0.0| 0.0|136247|
+----------------+----------+------+

Conclusiones:
El resumen de predicciones muestra la relación entre las predicciones realizadas por el modelo y la realidad en términos de la columna binaria
'arr_delay_binary'. Aquí hay una interpretación de los resultados:

Predicciones Positivas (1.0):

El modelo predijo correctamente 796 vuelos como retrasados (valor '1.0') y efectivamente eran retrasados en la realidad.

El modelo predijo erróneamente 252 vuelos como retrasados (valor '1.0'), pero en realidad no estaban retrasados.

Predicciones Negativas (0.0):

https://colab.research.google.com/drive/1VVkPUNnwYYHQ4Z7OSht_TRBWgxj7Xr39#scrollTo=GZpY0seut128&printMode=true 9/10
10/9/23, 17:18 desarrollo_actividad_1.ipynb - Colaboratory

El modelo predijo erróneamente 23.453 vuelos como no retrasados (valor '0.0'), pero en realidad estaban retrasados (valor '1.0').

El modelo predijo correctamente 136.247 vuelos como no retrasados (valor '0.0') y efectivamente no estaban retrasados en la realidad.

En resumen, el modelo de clasificación ha tenido un rendimiento mixto en sus predicciones. Logró identificar correctamente una cantidad
significativa de vuelos retrasados, pero también cometió un número considerable de falsos positivos y falsos negativos

check 0 s completado a las 16:31


No se ha podido establecer conexión con el servicio reCAPTCHA. Comprueba tu conexión a Internet y vuelve a cargar la página para ver otro reCAPTCHA.

https://colab.research.google.com/drive/1VVkPUNnwYYHQ4Z7OSht_TRBWgxj7Xr39#scrollTo=GZpY0seut128&printMode=true 10/10

También podría gustarte