Saltar a contenido

Práctico 03: EDA Multi-fuentes y Joins

🔧 Paso 1: Setup Inicial

# Importar librerías que vamos a usar
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import sqlite3
from pathlib import Path

# Configurar visualizaciones
plt.style.use('default')
sns.set_palette('husl')
plt.rcParams['figure.figsize'] = (10, 6)

print("✅ Setup completo para análisis multi-fuentes!")
✅ Setup completo para análisis multi-fuentes!

🚕 Paso 2: Carga de Datos desde Múltiples Fuentes

# === CARGAR DATOS DE MÚLTIPLES FUENTES ===

# 1. Cargar datos de viajes desde Parquet (Dataset oficial completo NYC)
print("Cargando datos oficiales de NYC Taxi (dataset completo)...")
trips_url = "https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2023-01.parquet"

# Cargar dataset oficial (~3M registros de enero 2023)
trips = pd.read_parquet(trips_url, engine='fastparquet') # dejar que pandas elija el engine por defecto para evitar conflictos de extensión de tipos

print(f"   Viajes cargados: {trips.shape[0]:,} filas, {trips.shape[1]} columnas")
print(f"   Columnas: {list(trips.columns)}")
print(f"   Período: {trips['tpep_pickup_datetime'].min()} a {trips['tpep_pickup_datetime'].max()}")
print(f"   Tamaño en memoria: {trips.memory_usage(deep=True).sum() / 1024**2:.1f} MB")

# 2. Cargar datos de zonas desde CSV (Dataset oficial completo)
print("\nCargando datos oficiales de zonas NYC...")
zones_url = "https://d37ci6vzurychx.cloudfront.net/misc/taxi+_zone_lookup.csv"
zones = pd.read_csv(zones_url)  # función estándar para archivos CSV --read_csv

print(f"   Zonas cargadas: {zones.shape[0]} filas, {zones.shape[1]} columnas")
print(f"   Columnas: {list(zones.columns)}")
print(f"   Boroughs únicos: {zones['Borough'].unique()}")

# 3. Cargar calendario de eventos desde JSON
print("\nCargando datos de calendario de eventos...")
calendar_url = "https://juanfkurucz.com/ucu-id/ut1/data/calendar.json"
calendar = pd.read_json(calendar_url)  # función para archivos JSON --read_json
calendar['date'] = pd.to_datetime(calendar['date']).dt.date  # convertir strings a fechas, luego extraer solo la fecha

print(f"   Eventos calendario: {calendar.shape[0]} filas")
print(f"   Columnas: {list(calendar.columns)}")

# 4. Mostrar primeras filas de cada dataset
print("\nVISTA PREVIA DE DATOS:")
print("\n--- TRIPS ---")
print(trips.head())  # método para mostrar primeras filas de un DataFrame --HEAD
print("\n--- ZONES ---")
print(zones.describe())  # mismo método para ver estructura de datos -- DESCRIBE
print("\n--- CALENDAR ---")
print(calendar.info())  # revisar formato de los eventos -- INFO
Cargando datos oficiales de NYC Taxi (dataset completo)...
   Viajes cargados: 3,066,766 filas, 19 columnas
   Columnas: ['VendorID', 'tpep_pickup_datetime', 'tpep_dropoff_datetime', 'passenger_count', 'trip_distance', 'RatecodeID', 'store_and_fwd_flag', 'PULocationID', 'DOLocationID', 'payment_type', 'fare_amount', 'extra', 'mta_tax', 'tip_amount', 'tolls_amount', 'improvement_surcharge', 'total_amount', 'congestion_surcharge', 'airport_fee']
   Período: 2008-12-31 23:01:42 a 2023-02-01 00:56:53
   Tamaño en memoria: 565.6 MB

Cargando datos oficiales de zonas NYC...
   Zonas cargadas: 265 filas, 4 columnas
   Columnas: ['LocationID', 'Borough', 'Zone', 'service_zone']
   Boroughs únicos: ['EWR' 'Queens' 'Bronx' 'Manhattan' 'Staten Island' 'Brooklyn' 'Unknown'
 nan]

Cargando datos de calendario de eventos...
   Eventos calendario: 3 filas
   Columnas: ['date', 'name', 'special']

VISTA PREVIA DE DATOS:

--- TRIPS ---
   VendorID tpep_pickup_datetime tpep_dropoff_datetime  passenger_count  \
0         2  2023-01-01 00:32:10   2023-01-01 00:40:36              1.0   
1         2  2023-01-01 00:55:08   2023-01-01 01:01:27              1.0   
2         2  2023-01-01 00:25:04   2023-01-01 00:37:49              1.0   
3         1  2023-01-01 00:03:48   2023-01-01 00:13:25              0.0   
4         2  2023-01-01 00:10:29   2023-01-01 00:21:19              1.0   

   trip_distance  RatecodeID store_and_fwd_flag  PULocationID  DOLocationID  \
0           0.97         1.0                  N           161           141   
1           1.10         1.0                  N            43           237   
2           2.51         1.0                  N            48           238   
3           1.90         1.0                  N           138             7   
4           1.43         1.0                  N           107            79   

   payment_type  fare_amount  extra  mta_tax  tip_amount  tolls_amount  \
0             2          9.3   1.00      0.5        0.00           0.0   
1             1          7.9   1.00      0.5        4.00           0.0   
2             1         14.9   1.00      0.5       15.00           0.0   
3             1         12.1   7.25      0.5        0.00           0.0   
4             1         11.4   1.00      0.5        3.28           0.0   

   improvement_surcharge  total_amount  congestion_surcharge  airport_fee  
0                    1.0         14.30                   2.5         0.00  
1                    1.0         16.90                   2.5         0.00  
2                    1.0         34.90                   2.5         0.00  
3                    1.0         20.85                   0.0         1.25  
4                    1.0         19.68                   2.5         0.00  

--- ZONES ---
       LocationID
count  265.000000
mean   133.000000
std     76.643112
min      1.000000
25%     67.000000
50%    133.000000
75%    199.000000
max    265.000000

--- CALENDAR ---
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3 entries, 0 to 2
Data columns (total 3 columns):
 #   Column   Non-Null Count  Dtype 
---  ------   --------------  ----- 
 0   date     3 non-null      object
 1   name     3 non-null      object
 2   special  3 non-null      bool  
dtypes: bool(1), object(2)
memory usage: 183.0+ bytes
None

🧹 Paso 3: Normalización de Datos

# === NORMALIZAR Y PREPARAR DATOS PARA JOINS ===

# 1. Estandarizar nombres de columnas
print("Normalizando nombres de columnas...")
trips.columns = trips.columns.str.lower()  # convertir todas las columnas a minúsculas
zones.columns = zones.columns.str.lower()  # misma transformación para consistencia

print(f"   Trips columnas: {list(trips.columns)}")
print(f"   Zones columnas: {list(zones.columns)}")

# 2. Crear columna de fecha para el join con calendario
trips['pickup_date'] = trips['tpep_pickup_datetime'].dt.date  # extraer solo la fecha (sin hora) de la columna datetime

print(f"   Columna pickup_date creada")
print(f"   Rango de fechas: {trips['pickup_date'].min()} a {trips['pickup_date'].max()}")

# 3. Verificar tipos de datos para joins
print("\nVERIFICACIÓN DE TIPOS PARA JOINS:")
print(f"   trips['pulocationid'] tipo: {trips['pulocationid'].dtype}")
print(f"   zones['locationid'] tipo: {zones['locationid'].dtype}")
print(f"   trips['pickup_date'] tipo: {type(trips['pickup_date'].iloc[0])}")
print(f"   calendar['date'] tipo: {type(calendar['date'].iloc[0])}")

# 4. Optimización para datasets grandes (~3M registros)
print("\nOPTIMIZACIÓN PARA DATASETS GRANDES:")
initial_memory = trips.memory_usage(deep=True).sum() / 1024**2
print(f"   Memoria inicial: {initial_memory:.1f} MB")

# Optimizar tipos de datos para 3+ millones de registros
print("   Optimizando tipos de datos para 3M+ registros...")

# Limpiar valores nulos antes de convertir tipos
print("   Limpiando valores nulos antes de optimización...")
trips['passenger_count'] = trips['passenger_count'].fillna(0)  # método para rellenar valores nulos con un valor específico
trips = trips.dropna(subset=['pulocationid', 'dolocationid'])  # eliminar filas críticas sin ubicación (necesarias para joins)

# Convertir tipos después de limpiar
trips['pulocationid'] = trips['pulocationid'].astype('int16')
trips['dolocationid'] = trips['dolocationid'].astype('int16')
trips['passenger_count'] = trips['passenger_count'].astype('int8')
zones['locationid'] = zones['locationid'].astype('int16')

print(f"   Registros después de limpieza: {len(trips):,}")

optimized_memory = trips.memory_usage(deep=True).sum() / 1024**2
savings = ((initial_memory - optimized_memory) / initial_memory * 100)

print(f"   Memoria optimizada: {optimized_memory:.1f} MB")
print(f"   Ahorro de memoria: {savings:.1f}%")

# 5. Revisar datos faltantes antes de joins
print("\nDATOS FALTANTES ANTES DE JOINS:")
print("Trips (top 5 columnas con más nulos):")
trips_nulls = trips.isna().sum().sort_values(ascending=False).head()  # método para detectar valores nulos, sumar y ordenar
print(trips_nulls)

print("\nZones:")
zones_nulls = zones.isna().sum()  # revisar si hay valores faltantes en lookup table
print(zones_nulls)

print("\nCalendar:")
calendar_nulls = calendar.isna().sum()  # verificar integridad del calendario de eventos
print(calendar_nulls)

# Análisis de calidad de datos
print("\nANÁLISIS DE CALIDAD:")
total_trips = len(trips)
print(f"   Total de viajes: {total_trips:,}")
print(f"   Viajes sin pickup location: {trips['pulocationid'].isna().sum():,}")
print(f"   Viajes sin dropoff location: {trips['dolocationid'].isna().sum():,}")
print(f"   Viajes sin passenger_count: {trips['passenger_count'].isna().sum():,}")

# Estrategias de limpieza recomendadas
print("\nESTRATEGIAS DE LIMPIEZA:")
print("   Ubicaciones nulas: Eliminar (crítico para joins)")
print("   Passenger_count nulos: Rellenar con valor típico (1)")
print("   Tarifas nulas: Revisar caso por caso")
Normalizando nombres de columnas...
   Trips columnas: ['vendorid', 'tpep_pickup_datetime', 'tpep_dropoff_datetime', 'passenger_count', 'trip_distance', 'ratecodeid', 'store_and_fwd_flag', 'pulocationid', 'dolocationid', 'payment_type', 'fare_amount', 'extra', 'mta_tax', 'tip_amount', 'tolls_amount', 'improvement_surcharge', 'total_amount', 'congestion_surcharge', 'airport_fee']
   Zones columnas: ['locationid', 'borough', 'zone', 'service_zone']
   Columna pickup_date creada
   Rango de fechas: 2008-12-31 a 2023-02-01

VERIFICACIÓN DE TIPOS PARA JOINS:
   trips['pulocationid'] tipo: int64
   zones['locationid'] tipo: int64
   trips['pickup_date'] tipo: <class 'datetime.date'>
   calendar['date'] tipo: <class 'datetime.date'>

OPTIMIZACIÓN PARA DATASETS GRANDES:
   Memoria inicial: 682.6 MB
   Optimizando tipos de datos para 3M+ registros...
   Limpiando valores nulos antes de optimización...
   Registros después de limpieza: 3,066,766
   Memoria optimizada: 627.0 MB
   Ahorro de memoria: 8.1%

DATOS FALTANTES ANTES DE JOINS:
Trips (top 5 columnas con más nulos):
airport_fee             71743
congestion_surcharge    71743
store_and_fwd_flag      71743
ratecodeid              71743
passenger_count             0
dtype: int64

Zones:
locationid      0
borough         1
zone            1
service_zone    2
dtype: int64

Calendar:
date       0
name       0
special    0
dtype: int64

ANÁLISIS DE CALIDAD:
   Total de viajes: 3,066,766
   Viajes sin pickup location: 0
   Viajes sin dropoff location: 0
   Viajes sin passenger_count: 0

ESTRATEGIAS DE LIMPIEZA:
   Ubicaciones nulas: Eliminar (crítico para joins)
   Passenger_count nulos: Rellenar con valor típico (1)
   Tarifas nulas: Revisar caso por caso

🔗 Paso 4: Join Principal - Trips con Zones

## === PRIMER JOIN: TRIPS + ZONES ===

# 1. Hacer join de trips con zones para obtener información geográfica
print("Realizando join: trips + zones...")
trips_with_zones = trips.merge(zones,   # método principal para unir DataFrames
                                left_on='pulocationid',   # columna de trips que contiene ID de zona de pickup
                                right_on='locationid',  # columna de zones que contiene ID correspondiente
                                how='left')       # tipo de join que mantiene todos los trips

print(f"   Registros antes del join: {len(trips)}")
print(f"   Registros después del join: {len(trips_with_zones)}")
print(f"   Nuevas columnas añadidas: {[col for col in trips_with_zones.columns if col not in trips.columns]}")

# 2. Verificar el resultado del join
print("\nVERIFICACIÓN DEL JOIN:")
print("Conteo por Borough:")
print(trips_with_zones['borough'].value_counts())

# 3. Verificar si hay valores nulos después del join
null_after_join = trips_with_zones['borough'].isnull().sum()  # contar nulos en columna borough
print(f"\nViajes sin borough asignado: {null_after_join}")

if null_after_join > 0:
    print("   Algunos viajes no encontraron su zona correspondiente")
    print("   LocationIDs problemáticos:")
    problematic_ids = trips_with_zones[trips_with_zones['borough'].isnull()]['pulocationid'].unique()  # filtrar filas con nulos
    print(f"   {problematic_ids}")

# 4. Mostrar muestra del resultado
print("\nMUESTRA DEL DATASET INTEGRADO:")
print(trips_with_zones[['pulocationid', 'borough', 'zone', 'trip_distance', 'total_amount']].head())
Realizando join: trips + zones...
   Registros antes del join: 3066766
   Registros después del join: 3066766
   Nuevas columnas añadidas: ['locationid', 'borough', 'zone', 'service_zone']

VERIFICACIÓN DEL JOIN:
Conteo por Borough:
borough
Manhattan        2715369
Queens            286645
Unknown            40116
Brooklyn           18076
Bronx               4162
EWR                  410
Staten Island        341
Name: count, dtype: int64

Viajes sin borough asignado: 1647
   Algunos viajes no encontraron su zona correspondiente
   LocationIDs problemáticos:
   [265]

MUESTRA DEL DATASET INTEGRADO:
   pulocationid    borough               zone  trip_distance  total_amount
0           161  Manhattan     Midtown Center           0.97         14.30
1            43  Manhattan       Central Park           1.10         16.90
2            48  Manhattan       Clinton East           2.51         34.90
3           138     Queens  LaGuardia Airport           1.90         20.85
4           107  Manhattan           Gramercy           1.43         19.68

📅 Paso 5: Segundo Join - Agregar Datos de Calendario

# === SEGUNDO JOIN: TRIPS_ZONES + CALENDAR ===

# 1. Hacer join con datos de calendario
print("Realizando join: trips_zones + calendar...")
trips_complete = trips_with_zones.merge(calendar,   # mismo método de join que antes
                                         left_on='pickup_date',   # columna de fecha que creamos en trips
                                         right_on='date',  # columna de fecha en calendar
                                         how='left')       # tipo que mantiene todos los trips aunque no haya evento especial

print(f"   Registros antes del join: {len(trips_with_zones)}")
print(f"   Registros después del join: {len(trips_complete)}")

# 2. Crear flag de evento especial
trips_complete['is_special_day'] = trips_complete['special'].fillna('False')  # método para rellenar nulos con valor por defecto

print("\nDISTRIBUCIÓN DE DÍAS ESPECIALES:")
print(trips_complete['is_special_day'].value_counts())
print("\nEjemplos de eventos especiales:")
special_days = trips_complete[trips_complete['is_special_day'] == True]
if len(special_days) > 0:
    print(special_days[['pickup_date', 'special', 'borough']].drop_duplicates())
else:
    print("   No hay eventos especiales en este período")

# 3. Mostrar dataset final integrado
print("\nDATASET FINAL INTEGRADO:")
print(f"   Total registros: {len(trips_complete)}")
print(f"   Total columnas: {len(trips_complete.columns)}")
print(f"   Columnas principales: {['borough', 'zone', 'is_special_day', 'trip_distance', 'total_amount']}")

# 4. Verificar integridad de los datos finales
print("\nVERIFICACIÓN FINAL:")
print("Datos faltantes por columna clave:")
key_columns = ['borough', 'zone', 'trip_distance', 'total_amount', 'is_special_day']
for col in key_columns:
    missing = trips_complete[col].isna().sum()  # verificar nulos en cada columna clave final
    print(f"   {col}: {missing} nulos")
Realizando join: trips_zones + calendar...
   Registros antes del join: 3066766
   Registros después del join: 3066766

DISTRIBUCIÓN DE DÍAS ESPECIALES:
is_special_day
False    3066766
Name: count, dtype: int64

Ejemplos de eventos especiales:
   No hay eventos especiales en este período

DATASET FINAL INTEGRADO:
   Total registros: 3066766
   Total columnas: 28
   Columnas principales: ['borough', 'zone', 'is_special_day', 'trip_distance', 'total_amount']

VERIFICACIÓN FINAL:
Datos faltantes por columna clave:
   borough: 1647 nulos
   zone: 40116 nulos
   trip_distance: 0 nulos
   total_amount: 0 nulos
   is_special_day: 0 nulos

📈 Paso 6: Análisis por Borough

# === ANÁLISIS AGREGADO POR BOROUGH ===

# 1. Análisis básico por borough (con dataset grande)
print("Análisis por Borough (procesando datos grandes)...")
borough_analysis = trips_complete.groupby(by='borough').agg({   # método para agrupar datos, por qué columna geográfica?
    'pulocationid': 'count',  # función para contar número de registros/viajes
    'trip_distance': ['mean', 'std', 'median'],  # función para promedio + desviación + mediana
    'total_amount': ['mean', 'std', 'median'],   # mismas estadísticas para tarifas
    'fare_amount': 'mean',     # solo promedio de tarifa base
    'tip_amount': ['mean', 'median'],  # estadísticas de propinas
    'passenger_count': 'mean'  # función para promedio de pasajeros
}).round(2)

# Aplanar columnas multi-nivel
borough_analysis.columns = ['num_trips', 'avg_distance', 'std_distance', 'median_distance',
                           'avg_total', 'std_total', 'median_total', 'avg_fare',
                           'avg_tip', 'median_tip', 'avg_passengers']

# Ordenar por número de viajes
borough_analysis = borough_analysis.sort_values(by='num_trips', ascending=False)  # método para ordenar DataFrame por una columna específica

print("\nANÁLISIS COMPLETO POR BOROUGH:")
print(borough_analysis)

# 2. Calcular métricas adicionales empresariales
borough_analysis['revenue_per_km'] = (borough_analysis['avg_total'] /
                                     borough_analysis['avg_distance']).round(2)
borough_analysis['tip_rate'] = (borough_analysis['avg_tip'] /
                               borough_analysis['avg_fare'] * 100).round(1)
borough_analysis['market_share'] = (borough_analysis['num_trips'] /
                                  borough_analysis['num_trips'].sum() * 100).round(1)

print("\nANÁLISIS CON MÉTRICAS EMPRESARIALES:")
print(borough_analysis[['num_trips', 'market_share', 'revenue_per_km', 'tip_rate']])

# 3. Encontrar insights
print("\nINSIGHTS PRINCIPALES:")
print(f"   Borough con más viajes: {borough_analysis.index[0]}")
print(f"   Borough con viajes más largos: {borough_analysis['avg_distance'].idxmax()}")
print(f"   Borough con tarifas más altas: {borough_analysis['avg_total'].idxmax()}")
print(f"   Mejor revenue por km: {borough_analysis['revenue_per_km'].idxmax()}")
Análisis por Borough (procesando datos grandes)...

ANÁLISIS COMPLETO POR BOROUGH:
               num_trips  avg_distance  std_distance  median_distance  \
borough                                                                 
Manhattan        2715369          2.88        264.53             1.63   
Queens            286645         12.32         14.42            11.24   
Unknown            40116          7.57        144.96             2.64   
Brooklyn           18076          5.68         70.86             3.45   
Bronx               4162          5.30          6.34             3.10   
EWR                  410          1.59          5.68             0.00   
Staten Island        341         11.36         10.21            14.80   

               avg_total  std_total  median_total  avg_fare  avg_tip  \
borough                                                                
Manhattan          22.49      14.54         19.25     14.78     2.88   
Queens             67.27      33.64         70.35     49.98     7.85   
Unknown            38.08      30.41         25.38     26.44     4.82   
Brooklyn           33.02      22.56         28.64     26.81     2.94   
Bronx              34.54      33.26         29.70     30.24     0.78   
EWR               104.38      62.75        118.55     87.99    12.44   
Staten Island      62.53      44.92         67.80     48.74     1.32   

               median_tip  avg_passengers  
borough                                    
Manhattan            2.66            1.33  
Queens               8.18            1.38  
Unknown              3.14            1.34  
Brooklyn             0.60            1.08  
Bronx                0.00            1.03  
EWR                 10.00            1.58  
Staten Island        0.00            1.12  

ANÁLISIS CON MÉTRICAS EMPRESARIALES:
               num_trips  market_share  revenue_per_km  tip_rate
borough                                                         
Manhattan        2715369          88.6            7.81      19.5
Queens            286645           9.4            5.46      15.7
Unknown            40116           1.3            5.03      18.2
Brooklyn           18076           0.6            5.81      11.0
Bronx               4162           0.1            6.52       2.6
EWR                  410           0.0           65.65      14.1
Staten Island        341           0.0            5.50       2.7

INSIGHTS PRINCIPALES:
   Borough con más viajes: Manhattan
   Borough con viajes más largos: Queens
   Borough con tarifas más altas: EWR
   Mejor revenue por km: EWR

📅 Paso 7: Análisis por Borough y Día Especial

# === ANÁLISIS COMPARATIVO: DÍAS NORMALES VS ESPECIALES ===

# 1. Análisis por borough y tipo de día
print("📅 Análisis: Borough + Día Especial...")
borough_day_analysis = trips_complete.groupby(by=['borough', 'is_special_day']).agg({  # agrupar por DOS columnas: geografía y tipo de día
    'pulocationid': 'count',  # función para contar viajes
    'trip_distance': 'mean',  # función para promedio de distancia
    'total_amount': 'mean'    # función para promedio de tarifa
}).round(2)

borough_day_analysis.columns = ['num_trips', 'avg_distance', 'avg_total']

print("\n📊 ANÁLISIS BOROUGH + DÍA ESPECIAL:")
print(borough_day_analysis)

# 2. Comparar días normales vs especiales
print("\n🔍 COMPARACIÓN DÍAS NORMALES VS ESPECIALES:")

# Pivotear para comparar fácilmente
comparison = trips_complete.groupby(by='is_special_day').agg({  # agrupar solo por tipo de día para comparación general
    'trip_distance': 'mean',    # promedio de distancia por tipo de día
    'total_amount': 'mean',     # promedio de tarifa por tipo de día
    'pulocationid': 'count'     # conteo de viajes por tipo de día
}).round(2)

# Renombrar índices según los valores únicos encontrados
unique_day_types = comparison.index.tolist()
if len(unique_day_types) == 2:
    comparison.index = ['Día Normal', 'Día Especial']
elif len(unique_day_types) == 1:
    if unique_day_types[0] in ['False', False]:
        comparison.index = ['Día Normal']
    else:
        comparison.index = ['Día Especial']

comparison.columns = ['Avg Distance', 'Avg Amount', 'Num Trips']

print(comparison)

# 3. Calcular diferencias porcentuales
if len(comparison) > 1:
    # Hay tanto días normales como especiales
    if 'Día Normal' in comparison.index and 'Día Especial' in comparison.index:
        normal_day = comparison.loc['Día Normal']
        special_day = comparison.loc['Día Especial']

        print("\nIMPACTO DE DÍAS ESPECIALES:")
        distance_change = ((special_day['Avg Distance'] - normal_day['Avg Distance']) / normal_day['Avg Distance'] * 100)
        amount_change = ((special_day['Avg Amount'] - normal_day['Avg Amount']) / normal_day['Avg Amount'] * 100)

        print(f"   Cambio en distancia promedio: {distance_change:+.1f}%")
        print(f"   Cambio en tarifa promedio: {amount_change:+.1f}%")
    else:
        print("\nINFORMACIÓN DE DÍAS:")
        for idx, row in comparison.iterrows():
            print(f"   {idx}: {row['Num Trips']:,} viajes, ${row['Avg Amount']:.2f} promedio")
else:
    print(f"\nSOLO HAY {comparison.index[0]}:")
    print(f"   Viajes: {comparison.iloc[0]['Num Trips']:,}")
    print(f"   Distancia promedio: {comparison.iloc[0]['Avg Distance']:.2f} millas")
    print(f"   Tarifa promedio: ${comparison.iloc[0]['Avg Amount']:.2f}")
    print("   No hay datos de días especiales para comparar en este período")
📅 Análisis: Borough + Día Especial...

📊 ANÁLISIS BOROUGH + DÍA ESPECIAL:
                              num_trips  avg_distance  avg_total
borough       is_special_day                                    
Bronx         False                4162          5.30      34.54
Brooklyn      False               18076          5.68      33.02
EWR           False                 410          1.59     104.38
Manhattan     False             2715369          2.88      22.49
Queens        False              286645         12.32      67.27
Staten Island False                 341         11.36      62.53
Unknown       False               40116          7.57      38.08

🔍 COMPARACIÓN DÍAS NORMALES VS ESPECIALES:
            Avg Distance  Avg Amount  Num Trips
Día Normal          3.85       27.02    3066766

SOLO HAY Día Normal:
   Viajes: 3,066,766.0
   Distancia promedio: 3.85 millas
   Tarifa promedio: $27.02
   No hay datos de días especiales para comparar en este período

⚡ Paso 8: Técnicas para Datasets Grandes

# === TÉCNICAS PARA TRABAJAR CON DATASETS GRANDES ===

# 1. Sampling estratégico para visualizaciones
print("⚡ Aplicando técnicas para datasets grandes...")

# Si el dataset es muy grande, usar muestra para visualizaciones
if len(trips_complete) > 50000:
    print(f"   📊 Dataset grande detectado: {len(trips_complete):,} registros")
    print("   🎯 Creando muestra estratificada para visualizaciones...")

    # Muestra proporcional por borough (simple aleatoria aquí)
    sample_size = min(10000, len(trips_complete) // 10)
    trips_sample = trips_complete.sample(n=sample_size, random_state=42)  # método para tomar muestra aleatoria de n registros

    print(f"   ✅ Muestra creada: {len(trips_sample):,} registros ({len(trips_sample)/len(trips_complete)*100:.1f}%)")
else:
    trips_sample = trips_complete
    print("   ℹ️ Dataset pequeño, usando datos completos para visualización")

# 2. Análisis de performance de joins
print("\n📈 ANÁLISIS DE PERFORMANCE:")
join_stats = {
    'total_trips': len(trips),
    'matched_zones': (trips_complete['borough'].notna()).sum(),
    'match_rate': (trips_complete['borough'].notna().sum() / len(trips) * 100),
    'unique_zones_used': trips_complete['zone'].nunique(),
    'total_zones_available': len(zones),
    'zone_coverage': (trips_complete['zone'].nunique() / len(zones) * 100)
}

for key, value in join_stats.items():
    if 'rate' in key or 'coverage' in key:
        print(f"   {key}: {value:.1f}%")
    else:
        print(f"   {key}: {value:,}")

# 3. Análisis temporal avanzado (solo si hay suficientes datos)
if len(trips_complete) > 1000:
    print("\n📅 ANÁLISIS TEMPORAL AVANZADO:")

    # Análisis por hora del día
    trips_complete['pickup_hour'] = trips_complete['tpep_pickup_datetime'].dt.hour  # extraer hora de la fecha/hora
    hourly_analysis = trips_complete.groupby(by='pickup_hour').agg({  # agrupar por hora del día
        'pulocationid': 'count',     # contar viajes por hora (en minúsculas tras .str.lower())
        'total_amount': 'mean',      # tarifa promedio por hora
        'trip_distance': 'mean'      # distancia promedio por hora
    }).round(2)

    hourly_analysis.columns = ['trips_count', 'avg_amount', 'avg_distance']

    print("   ⏰ Horas pico por número de viajes:")
    peak_hours = hourly_analysis.sort_values(by='trips_count', ascending=False).head(3)  # ordenar por más viajes, tomar top 3
    for hour, stats in peak_hours.iterrows():
        print(f"      {hour:02d}:00 - {stats['trips_count']:,} viajes")
⚡ Aplicando técnicas para datasets grandes...
   📊 Dataset grande detectado: 3,066,766 registros
   🎯 Creando muestra estratificada para visualizaciones...
   ✅ Muestra creada: 10,000 registros (0.3%)

📈 ANÁLISIS DE PERFORMANCE:
   total_trips: 3,066,766
   matched_zones: 3,065,119
   match_rate: 99.9%
   unique_zones_used: 255
   total_zones_available: 265
   zone_coverage: 96.2%

📅 ANÁLISIS TEMPORAL AVANZADO:
   ⏰ Horas pico por número de viajes:
      18:00 - 215,889.0 viajes
      17:00 - 209,493.0 viajes
      15:00 - 196,424.0 viajes

📊 Paso 9: Análisis de Correlaciones

# === ANÁLISIS DE CORRELACIONES NUMÉRICAS ===

# Calcular correlaciones entre variables numéricas
print("Calculando correlaciones entre variables numéricas...")
numeric_cols = ['trip_distance', 'total_amount', 'fare_amount', 'tip_amount']
corr_matrix = trips_complete[numeric_cols].corr()  # método para calcular matriz de correlación

print("\nMatriz de Correlación:")
print(corr_matrix.round(3))

print("\nCorrelaciones más fuertes:")
corr_pairs = []
for i in range(len(corr_matrix.columns)):
    for j in range(i+1, len(corr_matrix.columns)):
        corr_pairs.append((corr_matrix.columns[i], corr_matrix.columns[j], corr_matrix.iloc[i, j]))

corr_pairs.sort(key=lambda x: abs(x[2]), reverse=True)
for var1, var2, corr in corr_pairs[:3]:
    print(f"   {var1} vs {var2}: {corr:.3f}")

print("\nINTERPRETACIÓN DE CORRELACIONES:")
print("   > 0.7: Correlación fuerte positiva")
print("   0.3-0.7: Correlación moderada positiva")
print("   -0.3-0.3: Correlación débil")
print("   < -0.7: Correlación fuerte negativa")
Calculando correlaciones entre variables numéricas...

Matriz de Correlación:
               trip_distance  total_amount  fare_amount  tip_amount
trip_distance          1.000         0.016        0.016       0.011
total_amount           0.016         1.000        0.980       0.710
fare_amount            0.016         0.980        1.000       0.590
tip_amount             0.011         0.710        0.590       1.000

Correlaciones más fuertes:
   total_amount vs fare_amount: 0.980
   total_amount vs tip_amount: 0.710
   fare_amount vs tip_amount: 0.590

INTERPRETACIÓN DE CORRELACIONES:
   > 0.7: Correlación fuerte positiva
   0.3-0.7: Correlación moderada positiva
   -0.3-0.3: Correlación débil
   < -0.7: Correlación fuerte negativa

❓ Paso 10: Preguntas de Reflexión sobre Joins

1. ¿Qué diferencia hay entre un LEFT JOIN y un INNER JOIN?

Un left join

El left join prioriza la persistencia de los datos que tiene la tabla de la izquierda - Recaba los datos relacionados con la tabla de la izquierda

El right join prioriza la persistencia de los datos que tiene la tabla de la derecha - Recaba los datos relacionados con la tabla de la derecha

El inner join realzia una cconjunción de ambas tablas, por lo que no hay una que esté más ponderada que la otra - Recaba los datos relacionados con ambas tablas

2. ¿Por qué usamos LEFT JOIN en lugar de INNER JOIN para trips+zones?

Esto se debe a que priorizamos los registros de viajes, antes que los de zonas, hay algunos registros cuyo PULocationID no están relacionados a una zona. Si ponderamos la zona perderíamos viajes asociados a ellas.

3. ¿Qué problemas pueden surgir al hacer joins con datos de fechas?

En caso de que el formato de las fechas sea distinto esto generaría conflicto También pueden darse problemas debido a un desfase horario u errores de especificación de las fechas A su vez las fechas que están en nul generan pérdidades totales de registros en la consulta realizada

En sí todo recae en que el dato fecha es un dato que tiene demasiados formatos, y varía mucho de dataset en dataset, primero se debe normalizar y estandarizar los datos, generando un formato único para realziar el análisis.

4. ¿Cuál es la ventaja de integrar múltiples fuentes de datos?

La complejidad de los mismos, nos permite realizar un análisis más amplio, manejando un repertorio de datos más grande.

5. ¿Qué insights de negocio obtuviste del análisis integrado?

Las zonas nos permiten visualziar dónde hay más demanda y hacia dónde, permitiendo generar nuevas rutas más efectivas. El calendario nos permite comprender cómo se dan los patrones de movilidad cuando hay eventos importantes.