← 📊 Fundamentos
basico🧪 Lab práctico

1.4 · Lab 1: Exploración de datos reales con pandas

⏱ 50 minMódulo 1: Fundamentos de Big Data

🎯 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-01 por 2024-06 o el mes que quieras. También puedes recortar el DataFrame con df = 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"]
Pipeline del lab: el ciclo de vida del dato en miniatura.

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.

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?

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?

📚 Lecturas y fuentes

RecursoTipoPor qué leerlo
NYC TLC Trip Record DataDatasetLa 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 pandasDocumentaciónEl 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 DocumentationDocumentaciónLee “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).