🎯 Objetivo
Al terminar sabrás cargar un dataset público de millones de filas con pandas, perfilarlo, limpiarlo y agregarlo, y podrás explicar con números medidos por ti mismo dónde está el límite de una sola máquina.
✅ Antes de empezar
Necesitas tres cosas. Nada más.
- Python 3.10 o superior. Compruébalo con
python --version. - Las librerías:
pip install pandas pyarrow matplotlib - Unos 500 MB de RAM libre. Cierra el navegador con 40 pestañas.
¿No quieres instalar nada? Abre Google Colab. Trae pandas, pyarrow y matplotlib preinstalados, y el lab funciona igual copiando cada bloque en una celda.
El dataset
Vas a usar los NYC Yellow Taxi Trip Records: cada viaje de taxi amarillo de Nueva York, publicado por la Taxi and Limousine Commission. Es un dataset abierto, real y sucio, justo lo que necesitas.
- Fichero:
https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2024-01.parquet - Tamaño: unos 48 MB en disco, cerca de 3 millones de filas.
- Página oficial y diccionario de datos: NYC TLC Trip Record Data
¿Conexión lenta? Cualquier otro mes del mismo bucket sirve: cambia
2024-01por2024-06o el mes que quieras. También puedes recortar el DataFrame condf = df.head(300_000)justo después de cargarlo; los checkpoints cambiarán de escala pero el lab se sigue entendiendo.
Paso 1: Descargar el dataset
Primero traemos el fichero a disco. Lo guardamos localmente en vez de leerlo desde la URL cada vez, porque vas a cargarlo varias veces y no tiene sentido pagar la descarga en cada intento.
import urllib.request
from pathlib import Path
URL = "https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2024-01.parquet"
DESTINO = Path("yellow_tripdata_2024-01.parquet")
if not DESTINO.exists():
print("Descargando... (puede tardar un minuto)")
urllib.request.urlretrieve(URL, DESTINO)
else:
print("Ya estaba descargado.")
tam_mb = DESTINO.stat().st_size / 1024**2
print(f"Archivo: {DESTINO.name}")
print(f"Tamaño en disco: {tam_mb:.1f} MB")
Paso 2: Cargar y medir el peso real
Ahora lo cargamos en memoria y medimos cuánto ocupa de verdad. Parquet es un formato columnar y comprimido: en disco es pequeño, pero pandas tiene que expandirlo a arrays de NumPy sin comprimir. Aquí es donde el Volumen deja de ser una palabra de diapositiva.
import pandas as pd
df = pd.read_parquet(DESTINO)
print(f"Filas: {len(df):,}")
print(f"Columnas: {df.shape[1]}")
print()
df.info(memory_usage='deep')
ram_mb = df.memory_usage(deep=True).sum() / 1024**2
print()
print(f"En disco: {tam_mb:.0f} MB")
print(f"En RAM: {ram_mb:.0f} MB")
print(f"Factor de expansión: x{ram_mb / tam_mb:.1f}")
Paso 3: Perfilar y limpiar
Los datos reales vienen rotos. Antes de calcular nada, hay que mirar qué falta y qué es imposible: tarifas negativas, viajes con cero pasajeros, distancias de cero kilómetros. Si agregas sin limpiar, tus medias mienten.
# Qué falta
print("--- Nulos por columna (top 8) ---")
print(df.isna().sum().sort_values(ascending=False).head(8))
# Qué es imposible
print()
print("--- Valores imposibles ---")
print(f"Tarifas negativas: {(df['fare_amount'] < 0).sum():,}")
print(f"Cero pasajeros: {(df['passenger_count'] == 0).sum():,}")
print(f"Distancia cero: {(df['trip_distance'] == 0).sum():,}")
# Limpiamos
antes = len(df)
limpio = df[
(df["fare_amount"] > 0)
& (df["trip_distance"] > 0)
& (df["passenger_count"] > 0)
].copy()
print()
print(f"Filas antes: {antes:,}")
print(f"Filas después: {len(limpio):,}")
print(f"Descartado: {100 * (antes - len(limpio)) / antes:.1f}%")
Dónde estás ahora
Lo que acabas de hacer no es un ejercicio suelto: es el ciclo de vida del dato de la lección m1-02 comprimido en tu portátil.
graph LR A["Descargar<br/>parquet"] --> B["Cargar en<br/>pandas"] B --> C["Perfilar<br/>y limpiar"] C --> D["Agregar<br/>por hora"] D --> E["Visualizar"]
Compáralo con el diagrama de m1-02: descargar es la ingesta, el parquet en disco es el almacenamiento, limpiar es el procesamiento, agregar es el análisis y el gráfico final es la visualización. Las mismas cinco etapas. La única diferencia con un sistema Big Data real es que aquí todo cabe en una máquina. Por poco.
Paso 4: Agregar por hora del día
Con los datos limpios ya podemos preguntar algo. La pregunta más simple y más útil: ¿a qué hora se mueve Nueva York? Un groupby sobre la hora de recogida responde en una línea.
limpio["hora"] = limpio["tpep_pickup_datetime"].dt.hour
por_hora = limpio.groupby("hora").agg(
viajes=("hora", "size"),
tarifa_media=("fare_amount", "mean"),
distancia_media=("trip_distance", "mean"),
).round(2)
print(por_hora)
print()
print(f"Hora punta: {por_hora['viajes'].idxmax()}h "
f"({por_hora['viajes'].max():,} viajes)")
print(f"Hora valle: {por_hora['viajes'].idxmin()}h "
f"({por_hora['viajes'].min():,} viajes)")
Paso 5: Cronometrar y extrapolar
Aquí llega la parte incómoda. Vas a medir cuánto tarda una agregación sobre un mes y a proyectar qué pasaría con diez años de datos. El TLC lleva publicando estos ficheros desde 2009, así que no es un ejemplo inventado.
import time
t0 = time.perf_counter()
_ = limpio.groupby("hora")["fare_amount"].mean()
t_agg = time.perf_counter() - t0
MESES = 120 # 10 años
print(f"Agregación sobre 1 mes: {t_agg:.3f} s")
print(f"Extrapolado a {MESES} meses: {t_agg * MESES:.1f} s")
print()
print(f"RAM para 1 mes: {ram_mb:.0f} MB")
print(f"RAM para {MESES} meses: {ram_mb * MESES / 1024:.0f} GB")
print(f"Filas para {MESES} meses: {len(limpio) * MESES / 1e6:.0f} millones")
Paso 6: Visualizar (opcional)
Un número en una tabla convence a poca gente; una curva convence sola. Cerramos el pipeline con la última etapa del ciclo de vida.
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(9, 4))
ax.bar(por_hora.index, por_hora["viajes"], color="#2563eb")
ax.set_xlabel("Hora del día")
ax.set_ylabel("Número de viajes")
ax.set_title("Demanda de taxis por hora — NYC, enero 2024")
ax.set_xticks(range(24))
ax.grid(axis="y", alpha=0.3)
fig.tight_layout()
fig.savefig("demanda_por_hora.png", dpi=120)
print("Guardado: demanda_por_hora.png")
🧪 Reto final
🧪 Ejercicio
¿Cuándo propinan más los neoyorquinos?
Usando el DataFrame limpio, calcula la propina media como porcentaje de la tarifa y averigua en qué combinación de día de la semana y hora se propina más. Filtra solo los pagos con tarjeta (payment_type == 1), porque las propinas en efectivo no quedan registradas y hundirían la media. Descarta también los porcentajes absurdos y las combinaciones con pocos viajes.
🔍 Crea una columna con el porcentaje, filtra el rango razonable con .between(), añade columnas de día con .dt.day_name() y de hora con .dt.hour, y agrupa por las dos a la vez. Usa .agg() para obtener media y recuento, y descarta los grupos con menos de 1000 viajes antes de ordenar.
# Solo pagos con tarjeta: las propinas en efectivo no se registran
tarjeta = limpio[limpio["payment_type"] == 1].copy()
# Propina como porcentaje de la tarifa
tarjeta["pct_propina"] = 100 * tarjeta["tip_amount"] / tarjeta["fare_amount"]
# Fuera los porcentajes imposibles (errores de captura)
tarjeta = tarjeta[tarjeta["pct_propina"].between(0, 100)]
# Dimensiones temporales
tarjeta["dia"] = tarjeta["tpep_pickup_datetime"].dt.day_name()
tarjeta["hora"] = tarjeta["tpep_pickup_datetime"].dt.hour
tabla = (
tarjeta.groupby(["dia", "hora"])["pct_propina"]
.agg(media="mean", viajes="size")
)
# Solo combinaciones con muestra suficiente
tabla = tabla[tabla["viajes"] > 1000].sort_values("media", ascending=False)
print("--- Top 10: donde mas se propina ---")
print(tabla.head(10).round(2))
print()
print("--- Top 10: donde menos se propina ---")
print(tabla.tail(10).round(2))
print()
print(f"Propina media global: {tarjeta['pct_propina'].mean():.2f}%")Comprueba lo aprendido
Comprueba que lo pillaste
En el paso 2 un archivo de 48 MB en disco ocupó cerca de 700 MB en RAM. ¿Cuál es la explicación correcta?
Parquet aplica compresión y codificación columnar, que en datos tabulares repetitivos rinde muchísimo. En memoria, en cambio, pandas necesita arrays planos: cada float64 son 8 bytes fijos y cada cadena es un objeto de Python con su propia sobrecarga. Por eso el tamaño en disco nunca sirve para estimar la RAM necesaria.
Comprueba que lo pillaste
Según la extrapolación del paso 5, ¿qué recurso es el que primero te impide analizar 10 años de viajes en tu portátil?
La agregación extrapolada tardaba apenas segundos y los parquet comprimidos ocupan unos pocos gigabytes en disco. El cuello de botella es la memoria: pandas necesita el dataset entero en RAM, y 120 meses suman entre 70 y 90 GB. Esa es precisamente la razón de ser del escalamiento horizontal y de herramientas como Spark.
📚 Lecturas y fuentes
| Recurso | Tipo | Por qué leerlo |
|---|---|---|
| NYC TLC Trip Record Data | Dataset | La fuente oficial. Descarga el “Data Dictionary - Yellow Taxi Trip Records” (PDF, 2 páginas) para saber qué significa cada una de las 19 columnas (en inglés). |
| 10 minutes to pandas | Documentación | El tour oficial de pandas. Si algo del lab te sonó a magia, aquí están las secciones de “Selection”, “Grouping” y “Plotting” (en inglés). |
| Apache Parquet Documentation | Documentación | Lee “File Format” y “Concepts” para entender por qué un formato columnar comprime tanto y por qué se ha convertido en el estándar del almacenamiento analítico (en inglés). |