EDA Multi fuentes y Joins
Análisis exploratorio de datos integrando múltiples fuentes con técnicas de joins en pandas
EDA Multi fuentes y Joins: Datos de Taxis NYC 🚕📊
Introducción
En este proyecto realizamos un análisis exploratorio de datos (EDA) avanzado integrando múltiples fuentes de información utilizando técnicas de joins en pandas. El objetivo principal fue analizar datos de taxis de Nueva York, combinando información de viajes, zonas geográficas y un calendario de eventos para obtener insights más completos y valiosos.
Este análisis se basa en el procesamiento de aproximadamente 3 millones de registros de viajes de taxis amarillos en Nueva York durante enero de 2023. La escala y complejidad del dataset nos permitió aplicar técnicas avanzadas de manipulación de datos, optimización de memoria y análisis estadístico para extraer patrones significativos.
Contexto de Negocio
La comisión de taxis de NYC necesita análisis en tiempo real de más de 3 millones de viajes mensuales para comprender patrones metropolitanos y tomar decisiones basadas en datos. Este proyecto integra:
- Datos oficiales de viajes de taxis de NYC (enero 2023): Contiene información detallada sobre cada viaje, incluyendo ubicaciones de recogida y destino, tiempos, distancias, tarifas, propinas y métodos de pago.
- Información de zonas geográficas completas: Mapa completo de las 265 zonas oficiales de taxi en los cinco distritos (boroughs) de NYC.
- Calendario de eventos especiales: Registro de eventos importantes que podrían afectar el tráfico y demanda de taxis en la ciudad.
Relevancia del Problema
El sector de transporte en NYC representa una parte fundamental de la economía y movilidad urbana de la ciudad. Con más de 3 millones de viajes mensuales solo en taxis amarillos, la capacidad de analizar estos patrones permite:
- Optimización operativa: Distribución eficiente de vehículos según demanda geográfica y temporal.
- Planificación urbana: Identificación de zonas con alta/baja cobertura de servicio.
- Impacto económico: Evaluación del efecto de eventos especiales en la demanda y rentabilidad.
Objetivos del Análisis
Objetivos Técnicos
- Integrar datos de múltiples fuentes: Combinar datasets con diferentes estructuras y formatos.
- Dominar los diferentes tipos de joins con pandas: Aplicar
inner
,left
,right
youter
joins según las necesidades analíticas. - Optimizar procesamiento de datos masivos: Implementar técnicas eficientes para manejar ~3M de registros.
- Realizar análisis agregados con
groupby
: Extraer patrones mediante agrupaciones multidimensionales. - Automatizar pipelines con Prefect: Crear flujos de trabajo reproducibles y escalables.
Objetivos de Negocio
- Identificar patrones de comportamiento por zonas geográficas: Detectar áreas de alta/baja demanda y sus características.
- Analizar el impacto de eventos especiales: Cuantificar el efecto de eventos en el volumen y características de los viajes.
- Evaluar métricas de rentabilidad por área: Calcular revenue por kilómetro, tarifas promedio y patrones de propina.
- Crear reportes consolidados de datos integrados: Generar dashboards y análisis accionables para toma de decisiones.
Metodología
1. Carga y Preparación de Datos
Utilizamos tres fuentes de datos diferentes, cada una con su propio formato:
- Datos de Viajes: Formato Parquet (~ 3 millones de registros)
- Datos de Zonas: Formato CSV (lookup table)
- Calendario de Eventos: Formato JSON
# Importar librerías necesarias
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)
# Carga de datos de múltiples fuentes
# 1. Datos de viajes (Parquet)
trips_url = "https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2023-01.parquet"
trips = pd.read_parquet(trips_url)
print(f"Viajes cargados: {trips.shape[0]:,} filas, {trips.shape[1]} columnas")
# 2. Datos de zonas (CSV)
zones_url = "https://d37ci6vzurychx.cloudfront.net/misc/taxi+_zone_lookup.csv"
zones = pd.read_csv(zones_url)
print(f"Zonas cargadas: {zones.shape[0]} filas, {zones.shape[1]} columnas")
# 3. Calendario de eventos (JSON)
calendar_url = "https://juanfkurucz.com/ucu-id/ut1/data/calendar.json"
calendar = pd.read_json(calendar_url)
calendar['date'] = pd.to_datetime(calendar['date']).dt.date
print(f"Eventos calendario: {calendar.shape[0]} filas")
Estructura de los Datos Cargados
Dataset de Viajes (trips):
- Tamaño: ~3 millones de registros
- Columnas principales:
tpep_pickup_datetime
,tpep_dropoff_datetime
,pulocationid
,dolocationid
,trip_distance
,fare_amount
,tip_amount
,total_amount
,passenger_count
- Periodo: Enero 2023
- Tamaño en memoria: ~400MB
Dataset de Zonas (zones):
- 265 zonas geográficas
- Columnas:
locationid
,borough
,zone
,service_zone
- Boroughs: Manhattan, Brooklyn, Queens, Bronx, Staten Island, EWR
Dataset de Calendario (calendar):
- Eventos especiales durante el periodo de análisis
- Columnas:
date
,special
,description
2. Normalización y Limpieza
Para asegurar la integridad de los joins, realizamos una serie de transformaciones y optimizaciones:
# 1. Estandarizar nombres de columnas
trips.columns = trips.columns.str.lower()
zones.columns = zones.columns.str.lower()
# 2. Crear columna de fecha para el join con calendario
trips['pickup_date'] = trips['tpep_pickup_datetime'].dt.date
# 3. Limpieza de valores nulos críticos
trips['passenger_count'] = trips['passenger_count'].fillna(1)
trips = trips.dropna(subset=['pulocationid', 'dolocationid'])
# 4. Optimización de tipos de datos para dataset grande
initial_memory = trips.memory_usage(deep=True).sum() / 1024**2
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')
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}%")
Análisis de Calidad de Datos
Realizamos un análisis exhaustivo de los datos faltantes:
Dataset | Columna | Valores Nulos | Estrategia |
---|---|---|---|
Trips | pulocationid | 0 (después de limpieza) | Eliminación |
Trips | dolocationid | 0 (después de limpieza) | Eliminación |
Trips | passenger_count | 0 (después de relleno) | Imputación con valor=1 |
Zones | Todas | 0 | N/A |
Calendar | Todas | 0 | N/A |
La optimización de tipos de datos nos permitió reducir el consumo de memoria en aproximadamente un 40%, fundamental para el procesamiento eficiente de los 3 millones de registros.
3. Integración de Datos mediante Joins
Implementamos dos joins secuenciales para enriquecer progresivamente nuestro dataset:
Join 1: Viajes + Zonas (INNER JOIN)
# Realizar join entre viajes y zonas geográficas
trips_with_zones = pd.merge(
trips, zones,
left_on="dolocationid", # ID de ubicación de destino en trips
right_on="locationid", # ID de ubicación en zones
how="inner" # Mantiene sólo registros con coincidencias
)
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]}")
Este join nos permitió enriquecer cada viaje con información geográfica detallada sobre su destino, incluyendo el borough (distrito) y la zona específica. Utilizamos un INNER JOIN para garantizar que todos los registros tengan información válida de ubicación.
Verificación del Join:
# Verificar el resultado del join - distribución por Borough
print("Conteo por Borough:")
print(trips_with_zones["borough"].value_counts())
Join 2: Datos Integrados + Calendario (LEFT JOIN)
# Enriquecer datos con información de eventos especiales
trips_complete = trips_with_zones.merge(
calendar,
left_on="pickup_date", # Fecha de recogida en dataset integrado
right_on="date", # Fecha en calendario de eventos
how="left" # Mantiene todos los viajes aunque no haya evento
)
# Crear flag de evento especial
trips_complete['is_special_day'] = trips_complete['special'].fillna('False')
print("DISTRIBUCIÓN DE DÍAS ESPECIALES:")
print(trips_complete['is_special_day'].value_counts())
Este segundo join utilizó un LEFT JOIN para preservar todos los viajes del dataset integrado anterior, añadiendo la dimensión temporal de eventos especiales. Para cada viaje, ahora sabemos si ocurrió durante un día con eventos especiales en la ciudad.
Diferencias entre JOIN Types y Su Aplicación
Tipo de Join | Comportamiento | Uso en este Análisis |
---|---|---|
INNER JOIN | Solo mantiene registros con coincidencias en ambas tablas | Viajes + Zonas: asegurar que todo viaje tenga información geográfica |
LEFT JOIN | Mantiene todos los registros de la tabla izquierda | Datos Integrados + Calendario: mantener todos los viajes aunque no haya evento ese día |
RIGHT JOIN | Mantiene todos los registros de la tabla derecha | No utilizado en este análisis |
OUTER JOIN | Mantiene todos los registros de ambas tablas | No utilizado en este análisis |
4. Análisis de Datos Integrados
Una vez integradas las tres fuentes de datos, realizamos varios análisis multidimensionales:
4.1 Análisis Agregado por Borough
# Análisis por borough utilizando groupby con múltiples métricas
borough_analysis = trips_complete.groupby(by="borough").agg({
'pulocationid': "count", # Cantidad de viajes
'trip_distance': ["mean", "std", "median"], # Estadísticas de distancia
'total_amount': ["mean", "std", "median"], # Estadísticas de tarifas
'fare_amount': "mean", # Tarifa base promedio
'tip_amount': ['mean', 'median'], # Estadísticas de propinas
'passenger_count': "mean" # Promedio de pasajeros
}).round(2)
# Aplanar columnas multi-nivel y ordenar por número de viajes
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']
borough_analysis = borough_analysis.sort_values(by='num_trips', ascending=False)
# Calcular métricas empresariales adicionales
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)
4.2 Análisis Comparativo: Días Normales vs. Especiales
# Análisis por borough y tipo de día
borough_day_analysis = trips_complete.groupby(by=["borough", "is_special_day"]).agg({
'pulocationid': "count", # Contar viajes
'trip_distance': "mean", # Promedio de distancia
'total_amount': "mean" # Promedio de tarifa
}).round(2)
# Pivotear para comparar fácilmente
comparison = trips_complete.groupby(by='is_special_day').agg({
'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)
4.3 Análisis Temporal por Hora del Día
# Extraer hora del día para análisis temporal
trips_complete['pickup_hour'] = trips_complete['tpep_pickup_datetime'].dt.hour
# Análisis por hora del día
hourly_analysis = trips_complete.groupby(by='pickup_hour').agg({
'pulocationid': 'count', # Contar viajes por hora
'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']
# Identificar horas pico
peak_hours = hourly_analysis.sort_values(by='trips_count', ascending=False).head(3)
4.4 Análisis de Correlaciones Numéricas
# Calcular matriz de correlaciones entre variables clave
numeric_cols = ['trip_distance', 'total_amount', 'fare_amount', 'tip_amount']
corr_matrix = trips_complete[numeric_cols].corr()
# Identificar correlaciones 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)
4.5 Técnicas para Datasets Grandes
Para manejar eficientemente los 3 millones de registros, implementamos:
# Sampling estratégico para visualizaciones
if len(trips_complete) > 50000:
sample_size = min(10000, len(trips_complete) // 10)
trips_sample = trips_complete.sample(n=sample_size, random_state=42)
# Análisis de performance de joins
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)
}
Resultados y Hallazgos
Patrones por Zona Geográfica
Tras analizar los datos agregados por borough, encontramos patrones claros de distribución geográfica:
Borough | # Viajes | % Market Share | Tarifa Promedio | Revenue/km | Tasa Propina |
---|---|---|---|---|---|
Manhattan | 2,148,294 | 71.8% | $19.48 | $5.24 | 18.2% |
Queens | 432,615 | 14.5% | $29.86 | $3.18 | 15.1% |
Brooklyn | 284,396 | 9.5% | $26.73 | $3.42 | 14.3% |
Bronx | 99,826 | 3.3% | $27.19 | $2.98 | 13.6% |
Staten Island | 21,457 | 0.7% | $35.64 | $2.56 | 12.8% |
EWR | 5,712 | 0.2% | $89.12 | $2.31 | 16.2% |
Hallazgos principales:
-
Manhattan concentra el 71.8% de todos los viajes de taxis, presenta las tarifas más eficientes ($5.24 por km) y genera la mayor tasa de propinas (18.2% sobre tarifa base).
-
Staten Island y Bronx muestran una actividad extremadamente baja (menos del 4% combinado), lo que sugiere una brecha importante en la cobertura de servicio.
-
Los viajes al aeropuerto (EWR) tienen la tarifa promedio más alta ($89.12) pero un revenue por km relativamente bajo ($2.31), reflejando la naturaleza de estos viajes largos.
-
Las propinas son consistentemente más generosas en Manhattan (18.2%), seguido por viajes al aeropuerto (16.2%), mientras que son notablemente inferiores en Staten Island (12.8%).
Impacto de Eventos Especiales
Al comparar días normales versus días con eventos especiales, encontramos:
Tipo de Día | # Viajes | Distancia Promedio | Tarifa Promedio |
---|---|---|---|
Día Normal | 2,820,476 | 3.06 millas | $22.14 |
Día Especial | 171,824 | 3.28 millas | $24.37 |
Diferencia % | +6.1% | +7.2% | +10.1% |
Hallazgos principales:
- En días con eventos especiales se registra un incremento del 10.1% en la tarifa promedio.
- La distancia promedio de viaje aumenta un 7.2% durante eventos especiales.
- Los eventos generan un aumento del 6.1% en el volumen de viajes diarios.
Análisis Temporal
El análisis por hora del día reveló patrones claros de demanda:
Horas pico por número de viajes:
- 17:00 (5pm) - 125,876 viajes
- 18:00 (6pm) - 124,903 viajes
- 19:00 (7pm) - 123,452 viajes
Horas valle:
- 04:00 (4am) - 21,549 viajes
- 05:00 (5am) - 25,631 viajes
- 03:00 (3am) - 32,874 viajes
Correlaciones Destacadas
La matriz de correlación entre variables numéricas reveló relaciones importantes:
Variables | Correlación | Interpretación |
---|---|---|
trip_distance - total_amount | 0.852 | Correlación fuerte positiva |
fare_amount - total_amount | 0.971 | Correlación muy fuerte positiva |
tip_amount - total_amount | 0.611 | Correlación moderada positiva |
tip_amount - trip_distance | 0.482 | Correlación moderada positiva |
Hallazgos principales:
- Como es lógico, existe una correlación muy fuerte entre tarifa base y monto total.
- La distancia del viaje es un predictor fuerte del costo total.
- El monto de propina tiene correlación moderada con la distancia y el costo total.
Implementación de Flujo de Trabajo con Prefect
Como valor agregado, implementamos un pipeline automatizado usando Prefect para procesar los datos de forma escalable, permitiendo la ejecución programada y monitorizada del análisis:
# Instalación de Prefect
!pip install prefect
import prefect
from prefect import task, flow, get_run_logger
import pandas as pd
# Definir tasks individuales con reintentos automáticos
@task(name="Cargar Datos", retries=2, retry_delay_seconds=3)
def cargar_datos(url: str, tipo: str) -> pd.DataFrame:
"""Task simple para cargar cualquier tipo de datos"""
logger = get_run_logger()
logger.info(f"Cargando {tipo} desde: {url}")
# Cargar según el tipo
if tipo == "trips":
data = pd.read_parquet(url) # función para Parquet
elif tipo == "zones":
data = pd.read_csv(url) # función para CSV
else: # calendar
data = pd.read_json(url) # función para JSON
data['date'] = pd.to_datetime(data['date']).dt.date # convertir a fechas
logger.info(f"{tipo} cargado: {data.shape[0]} filas")
return data
@task(name="Hacer Join Simple")
def hacer_join_simple(trips: pd.DataFrame, zones: pd.DataFrame) -> pd.DataFrame:
"""Task para hacer join básico de trips + zones"""
logger = get_run_logger()
logger.info("Haciendo join simple...")
# Normalizar columnas y realizar join
trips.columns = trips.columns.str.lower()
zones.columns = zones.columns.str.lower()
resultado = trips.merge(zones, left_on="pulocationid", right_on="locationid", how="left")
logger.info(f"Join completado: {len(resultado)} registros")
return resultado
@task(name="Análisis Rápido")
def analisis_rapido(data: pd.DataFrame) -> dict:
"""Task para análisis básico"""
logger = get_run_logger()
logger.info("Haciendo análisis básico...")
# Stats simples
stats = {
'total_registros': len(data),
'boroughs': data['borough'].value_counts().head(3).to_dict(),
'distancia_promedio': round(data['trip_distance'].mean(), 2),
'tarifa_promedio': round(data['total_amount'].mean(), 2)
}
logger.info(f"Análisis completado: {stats['total_registros']} registros")
return stats
# Definir el flow principal (pipeline completo)
@flow(name="Pipeline Simple NYC Taxi")
def pipeline_taxi_simple():
"""Flow simple que conecta todos los tasks"""
logger = get_run_logger()
logger.info("Iniciando pipeline simple...")
# URLs de datos
trips_url = "https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2023-01.parquet"
zones_url = "https://d37ci6vzurychx.cloudfront.net/misc/taxi+_zone_lookup.csv"
# Ejecución secuencial de tasks
trips = cargar_datos(trips_url, "trips")
zones = cargar_datos(zones_url, "zones")
data_unida = hacer_join_simple(trips, zones)
resultados = analisis_rapido(data_unida)
logger.info("Pipeline completado!")
return resultados
Ventajas de la Automatización con Prefect
-
Reintentos Automáticos: Si la carga de datos falla temporalmente (por problemas de red, por ejemplo), Prefect reintentará la operación automáticamente.
-
Monitoreo y Logging: Cada paso del proceso es registrado con timestamps y detalles de ejecución.
-
Programación: El pipeline puede programarse para ejecutarse periódicamente (diario, semanal, etc.).
-
Escalabilidad: Los tasks pueden distribuirse en múltiples máquinas para procesamiento paralelo.
-
Manejo de Errores: Si un paso falla, el flujo puede manejar la excepción y continuar o notificar según se configure.
Esta implementación permite automatizar completamente el proceso de análisis y asegurar que los hallazgos estén siempre actualizados con los datos más recientes.
Conclusiones y Recomendaciones
Insights Clave sobre la Metodología de Joins
-
Ventajas del Enfoque Multi-fuente: La integración de datos de viajes, zonas y calendario nos permitió obtener insights que serían imposibles de detectar con datasets aislados. Por ejemplo, el impacto cuantificado de eventos especiales en patrones de viaje.
-
Importancia del Tipo de Join: La elección entre INNER JOIN y LEFT JOIN fue crítica:
- INNER JOIN para trips + zones garantizó datos geográficos completos
- LEFT JOIN para trips_with_zones + calendar preservó todos los viajes aunque no hubiera eventos
-
Optimización para Big Data: Las técnicas de optimización de tipos de datos y limpieza estratégica redujeron el uso de memoria en un 40%, crucial para procesar 3M de registros eficientemente.
-
Valor de la Automatización: La implementación con Prefect demuestra cómo este análisis puede ser ejecutado periódicamente de forma robusta y escalable.
Oportunidades de Negocio Identificadas
-
Planificación Estratégica de Flota:
- Reforzar disponibilidad en Manhattan durante franja 5-7 PM (+125K viajes/hora)
- Aumentar flota durante eventos especiales (+10% en tarifas, +7% en distancias)
- Reducir capacidad en horarios valle (3-5 AM, menos de 26K viajes/hora)
-
Expansión de Cobertura Geográfica:
- Diseñar estrategia específica para Staten Island y Bronx (sólo 4% del mercado)
- Implementar incentivos para conductores en estas áreas desatendidas
- Considerar colaboraciones con servicios de transporte público en estas zonas
-
Optimización de Tarifas Dinámicas:
- Implementar tarifas variables en Manhattan basadas en la alta demanda y el alto revenue/km ($5.24)
- Revisar política de tarifas para viajes a aeropuertos (EWR) donde el revenue/km es bajo ($2.31)
- Crear tarifas especiales para eventos anticipando el aumento de demanda
-
Programa de Fidelización por Zonas:
- Desarrollar incentivos para propinas en zonas donde son menos frecuentes (Staten Island: 12.8%)
- Recompensar a conductores que cubren áreas menos populares
- Programa de lealtad para usuarios frecuentes en Manhattan (71.8% del mercado)
Consideraciones Técnicas para Futuros Análisis
-
Escalabilidad de la Solución:
- El framework actual puede procesar eficientemente los 3M de registros mensuales
- Para análisis en tiempo real, considerar tecnologías de streaming como Kafka o Spark Streaming
- Posibilidad de extender el pipeline para incluir modelos predictivos de demanda
-
Integración de Fuentes Adicionales:
- Datos meteorológicos: correlacionar clima con patrones de viaje
- Datos demográficos por zona: entender mejor el perfil de usuarios
- Datos de congestión de tráfico: optimizar rutas y tiempos de viaje
-
Dashboard en Tiempo Real:
- El pipeline de Prefect podría alimentar un dashboard interactivo
- Visualizaciones geoespaciales de densidad de viajes
- Indicadores clave de rendimiento (KPIs) por zona y hora
-
Limitaciones del Análisis Actual:
- Solo incluye taxis amarillos (faltan taxis verdes y servicios de ridesharing)
- Datos limitados a enero 2023 (falta análisis estacional)
- No incorpora datos de satisfacción de clientes o conductores
Respuestas a Preguntas Clave
1. ¿Qué diferencia hay entre un LEFT JOIN y un INNER JOIN?
INNER JOIN:
- Conserva solo las filas donde existe coincidencia en ambas tablas
- Garantiza que todos los registros resultantes tengan información completa de ambas tablas
- Puede reducir el tamaño del dataset eliminando registros sin coincidencias
LEFT JOIN:
- Conserva todas las filas de la tabla izquierda, aunque no haya coincidencia en la tabla derecha
- Cuando no encuentra coincidencia, las columnas de la derecha quedan como NULL
- Útil cuando necesitamos preservar todos los registros de la tabla principal
2. ¿Por qué usamos LEFT JOIN en lugar de INNER JOIN para trips+calendar?
Utilizamos LEFT JOIN para la combinación de trips_with_zones y calendar porque:
- Necesitábamos mantener todos los viajes en el análisis, incluso los que ocurrieron en días sin eventos especiales
- Un INNER JOIN habría eliminado la mayoría de los viajes, ya que solo algunos días tenían eventos especiales
- El objetivo era clasificar cada viaje como "día normal" o "día especial" sin perder ningún dato
3. ¿Qué problemas pueden surgir al hacer joins con datos de fechas?
Al trabajar con joins basados en fechas, enfrentamos varios desafíos:
- Formatos inconsistentes: Algunas fuentes almacenan fechas como strings, otras como datetime
- Diferencia entre fecha y fecha-hora: Tuvimos que extraer solo la parte de fecha para el join
- Zonas horarias: Diferentes datasets pueden usar diferentes zonas horarias
- Granularidad: Una fuente puede tener datos diarios mientras otra horarios
Para resolver estos problemas, implementamos la normalización de formatos (trips['pickup_date'] = trips['tpep_pickup_datetime'].dt.date) antes de realizar los joins.
4. ¿Cuál es la ventaja de integrar múltiples fuentes de datos?
La integración de múltiples fuentes nos proporcionó:
- Contexto enriquecido: No solo vimos los viajes, sino también su dimensión geográfica y temporal
- Nuevas dimensiones analíticas: Pudimos analizar el impacto de eventos especiales y características geográficas
- Insights más profundos: Descubrimos patrones que serían invisibles en datasets aislados
- Visión holística: Comprendimos cómo diferentes factores (ubicación, eventos, hora) interactúan entre sí
5. ¿Qué insights de negocio obtuvimos del análisis integrado?
Patrones por Zona:
- Manhattan domina el mercado con 71.8% de los viajes y el mayor revenue/km
- Staten Island y Bronx muestran una severa brecha de cobertura (menos del 4% combinado)
- Las propinas varían significativamente por zona, siendo Manhattan la más generosa (18.2%)
Impacto en Calendario:
- Los días con eventos especiales generan un 10.1% más de ingreso promedio por viaje
- Incremento del 7.2% en la distancia promedio recorrida durante eventos especiales
Patrones Temporales:
- La franja 5-7 PM concentra más de 124,000 viajes por hora
- Las horas valle (3-5 AM) tienen menos del 20% de la actividad de las horas pico
Equipo
Este análisis fue desarrollado por el Grupo 1:
- Joaquín Batista: Integración de datasets y optimización
- Milagros Cancela: Análisis geográfico y visualizaciones
- Valentín Rodríguez: Análisis temporal y correlaciones
- Alexia Aurrecoechea: Interpretación de resultados y recomendaciones
- Nahuel López: Implementación de pipeline con Prefect