INGENIERÍA DE DATOS — Trabajo Grupal

Temas cubiertos: 4 (Procesado y transformación de datos), 5 (Exploración de datos)

Descripción: Este documento carga los datos limpios del Script 1, crea campos calculados, une datasets con joins, realiza agregaciones y ejecuta un análisis exploratorio (EDA) univariante y bivariante. El dataset final enriquecido se guarda en datos/gold/.

IMPORTANTE: Ejecutar primero 01_ingesta_y_limpieza.Rmd.


0. Carga de librerías

library(dplyr)      # Manipulación de datos
library(ggplot2)    # Visualizaciones
library(corrplot)   # Matrices de correlación

1. Carga de datos limpios

# [PROPORCIONADO]
df_resultados <- readRDS("datos/clean/resultados_clean.rds")
df_api_pilotos <- readRDS("datos/clean/api_pilotos_clean.rds")
df_api_constructores <- readRDS("datos/clean/api_constructores_clean.rds")
df_equipos <- readRDS("datos/clean/equipos_clean.rds")
df_presupuestos <- readRDS("datos/clean/presupuestos_clean.rds")
df_pilotos_bio <- readRDS("datos/clean/pilotos_bio_clean.rds")

cat("Datos limpios cargados correctamente.\n")
## Datos limpios cargados correctamente.
cat("df_resultados:", nrow(df_resultados), "filas\n")
## df_resultados: 3760 filas

2. Campos calculados con mutate()

Referencia: Tema 4 — Procesado y transformación de datos

>>> BLOQUE 1 — A COMPLETAR POR EL GRUPO <<<

Crear al menos 3 variables nuevas en df_resultados usando mutate():

  1. posiciones_ganadas: cuántas posiciones ganó el piloto. Se calcula como grid - position (positivo = ganó posiciones).
  2. es_podio: indicador binario (1/0) de si terminó en el podio (posición 1, 2 o 3). PISTA: ifelse(position <= 3, 1, 0).
  3. puntos_por_vuelta: eficiencia = points / laps. Cuidado con división por 0.
df_resultados <- df_resultados %>%
  # Creamos variables derivadas para capturar rendimiento y eficiencia
  # posiciones_ganadas mide mejora respecto a la salida
  # es_podio identifica resultados destacados
  # puntos_por_vuelta normaliza el rendimiento por duración de carrera
  mutate(
    posiciones_ganadas = grid - position,
    es_podio = ifelse(position <= 3, 1, 0),
    puntos_por_vuelta = ifelse(laps > 0, points / laps, NA_real_)
  )

summary(df_resultados %>% select(posiciones_ganadas, es_podio, puntos_por_vuelta))
##  posiciones_ganadas    es_podio      puntos_por_vuelta
##  Min.   :-19.0000   Min.   :0.0000   Min.   :0.00000  
##  1st Qu.: -6.0000   1st Qu.:0.0000   1st Qu.:0.00000  
##  Median :  0.0000   Median :0.0000   Median :0.00000  
##  Mean   : -0.0174   Mean   :0.1499   Mean   :0.05358  
##  3rd Qu.:  6.0000   3rd Qu.:0.0000   3rd Qu.:0.05882  
##  Max.   : 19.0000   Max.   :1.0000   Max.   :0.51020  
##  NA's   :1465       NA's   :1465

Las variables derivadas permiten medir mejor el rendimiento. Por ejemplo, la tasa media de podio en el dataset es del 14,99% y la ganancia media de posiciones es de -1,74%, lo que ayuda a separar resultados finales de la posicion de salida.

Además, la media cercana a cero en posiciones_ganadas indica que, en promedio, los pilotos tienden a mantener su posición inicial, lo que sugiere un equilibrio entre adelantamientos y pérdidas de posición.


3. Join (merge) de datasets

Referencia: Tema 4 — Procesado y transformación de datos (joins)

>>> BLOQUE 2 — A COMPLETAR POR EL GRUPO <<<

Unir df_resultados con al menos uno de los otros datasets para enriquecer la información.

  • Opción A: Unir con df_equipos por constructor ↔︎ nombre_equipo
  • Opción B: Unir con df_pilotos_bio por driver_id

PISTA con merge():

df_enriquecido <- merge(df_resultados, df_equipos,
                         by.x = "constructor", by.y = "nombre_equipo",
                         all.x = TRUE)

O con dplyr: left_join(df_resultados, df_equipos, by = c("constructor" = "nombre_equipo"))

Guardar en df_enriquecido. Mostrar dim() para verificar que no se han perdido filas.

# Preparamos claves de unión homogéneas (lowercase + trim)
# para evitar errores de matching entre datasets
df_resultados_join <- df_resultados %>%
  mutate(
    constructor_key = tolower(trimws(as.character(constructor))),
    driver_id_key = tolower(trimws(driver_id))
  )

df_equipos_join <- df_equipos %>%
  mutate(constructor_key = tolower(trimws(nombre_equipo)))

df_presupuestos_join <- df_presupuestos %>%
  mutate(constructor_key = tolower(trimws(nombre_equipo)))

df_pilotos_bio_join <- df_pilotos_bio %>%
  mutate(driver_id_key = tolower(trimws(driver_id)))

# Integramos múltiples fuentes para enriquecer el dataset
# (equipos, presupuestos y datos biográficos de pilotos)
df_enriquecido <- df_resultados_join %>%
  left_join(df_equipos_join, by = "constructor_key") %>%
  left_join(df_presupuestos_join, by = c("constructor_key", "season" = "temporada"), suffix = c("", "_presupuesto")) %>%
  left_join(df_pilotos_bio_join, by = "driver_id_key", suffix = c("", "_bio"))

dim(df_enriquecido)
## [1] 3760   43

El join mantiene 3760 filas, por lo que no se pierden observaciones del historico. A cambio, el dataset gana variables de contexto sobre equipos, presupuestos y pilotos, lo que lo hace mas rico para EDA y modelado.

Este enriquecimiento permite incorporar variables externas que pueden explicar mejor el rendimiento de los pilotos.


4. Agregaciones con group_by() y summarise()

Referencia: Tema 4 — Procesado y transformación de datos

>>> BLOQUE 3 — A COMPLETAR POR EL GRUPO <<<

Crear dos resúmenes agregados:

Resumen 1 — Por piloto (df_resumen_pilotos): agrupar por driver_id y driver_name, calcular total_carreras (n()), puntos_totales (sum(points)), posicion_media (mean(position)), tasa_podio (mean(es_podio) * 100). Ordenar por puntos descendente.

Resumen 2 — Por constructor y temporada (df_resumen_constructores): agrupar por constructor y season, calcular puntos_totales, victorias (nº de veces que position == 1), carreras (n()).

# Agregamos información a nivel de piloto para analizar rendimiento global
df_resumen_pilotos <- df_enriquecido %>%
  group_by(driver_id, driver_name) %>%
  summarise(
    total_carreras = n(),
    puntos_totales = sum(points, na.rm = TRUE),
    posicion_media = mean(position, na.rm = TRUE),
    # tasa_podio como indicador de consistencia en resultados top
    tasa_podio = mean(es_podio, na.rm = TRUE) * 100,
    .groups = "drop"
  ) %>%
  arrange(desc(puntos_totales))

# Agregamos por constructor y temporada para analizar rendimiento estructural
df_resumen_constructores <- df_enriquecido %>%
  group_by(constructor, season) %>%
  summarise(
    puntos_totales = sum(points, na.rm = TRUE),
    victorias = sum(position == 1, na.rm = TRUE),
    carreras = n(),
    .groups = "drop"
  ) %>%
  arrange(season, desc(puntos_totales))

head(df_resumen_pilotos, 10)
## # A tibble: 10 × 6
##    driver_id driver_name total_carreras puntos_totales posicion_media tasa_podio
##    <chr>     <chr>                <int>          <dbl>          <dbl>      <dbl>
##  1 sargeant  Logan Sarg…            183           616.          10.3        17.0
##  2 lawson    Liam Lawson            165           604            9.91       16.2
##  3 hulkenbe… Nico Hülke…            161           601.           9.82       21.9
##  4 zhou      Guanyu Zhou            184           600.          10.5        18.2
##  5 magnussen Kevin Magn…            166           562            9.78       17  
##  6 gasly     Pierre Gas…            155           514.          10.8        17.3
##  7 grosjean  Romain Gro…            178           506.          10.8        11.6
##  8 de_vries  Nyck de Vr…            173           505.          10.3        15.0
##  9 schumach… Mick Schum…            168           499           10.4        11.7
## 10 tsunoda   Yuki Tsuno…            155           488.          10.5        16.1
head(df_resumen_constructores, 10)
## # A tibble: 10 × 5
##    constructor season puntos_totales victorias carreras
##    <fct>        <dbl>          <dbl>     <int>    <int>
##  1 toro rosso    2014           227          4       36
##  2 manor         2014           168          3       36
##  3 williams      2014           160          1       36
##  4 Mercedes      2014           120          0       36
##  5 lotus         2014           112          1       36
##  6 sauber        2014           107.         0       36
##  7 red bull      2014           104          0       36
##  8 force india   2014           101          1       36
##  9 mclaren       2014           100          1       36
## 10 ferrari       2014            95          1       36

En el resumen por piloto, el lider por puntos acumulados es Logan Sargeant con 183 puntos. El resumen por constructor y temporada permite comparar no solo puntuacion total, sino tambien regularidad y victorias por curso.

Dado que el dataset combina múltiples temporadas y carreras, los valores acumulados deben interpretarse con cautela, ya que no reflejan necesariamente el rendimiento real histórico de los pilotos, sino el comportamiento dentro de este conjunto de datos.


5. Pipeline encadenado con dplyr

Referencia: Tema 4 — Procesado y transformación de datos

>>> BLOQUE 4 — A COMPLETAR POR EL GRUPO <<<

Construir un pipeline encadenado con %>% que combine al menos 3 operaciones de dplyr (filter, group_by, summarise, arrange…).

Ejemplo sugerido: “Top 5 pilotos con más puntos en las temporadas 2020-2023”.

# Pipeline encadenado: filtramos, agregamos y ordenamos sin crear objetos intermedios
df_top5_reciente <- df_enriquecido %>%
  filter(season >= 2020, season <= 2023) %>%
  group_by(driver_id, driver_name) %>%
  summarise(
    puntos_totales = sum(points, na.rm = TRUE),
    carreras = n(),
    .groups = "drop"
  ) %>%
  arrange(desc(puntos_totales)) %>%
  slice_head(n = 5)

df_top5_reciente
## # A tibble: 5 × 4
##   driver_id driver_name    puntos_totales carreras
##   <chr>     <chr>                   <dbl>    <int>
## 1 sargeant  Logan Sargeant           388.       86
## 2 lawson    Liam Lawson              280        55
## 3 de_vries  Nyck de Vries            228        75
## 4 tsunoda   Yuki Tsunoda             220.       67
## 5 gasly     Pierre Gasly             216.       61

Entre 2020 y 2023, el piloto con mas puntos acumulados en este historico es Logan Sargeant. Este tipo de pipeline condensa filtros, agregaciones y ordenacion en un flujo legible y facil de mantener.

Este resultado está condicionado por el subconjunto de datos disponible, por lo que debe interpretarse dentro del contexto del dataset y no como un ranking absoluto de pilotos.


6. EDA univariante

Referencia: Tema 5 — Exploración de datos

# [PROPORCIONADO] — Estadísticas descriptivas
cat("--- Estadísticas descriptivas de las variables numéricas ---\n")
## --- Estadísticas descriptivas de las variables numéricas ---
summary(df_enriquecido %>% select_if(is.numeric))
##      season         round             grid          position   
##  Min.   :2014   Min.   : 1.000   Min.   : 1.00   Min.   : 1.0  
##  1st Qu.:2016   1st Qu.: 5.000   1st Qu.: 5.00   1st Qu.: 5.0  
##  Median :2018   Median :10.000   Median :10.00   Median :10.0  
##  Mean   :2019   Mean   : 9.915   Mean   :10.43   Mean   :10.4  
##  3rd Qu.:2021   3rd Qu.:15.000   3rd Qu.:15.00   3rd Qu.:15.0  
##  Max.   :2023   Max.   :20.000   Max.   :20.00   Max.   :20.0  
##                                                  NA's   :1465  
##      points            laps       fastest_lap_speed posiciones_ganadas
##  Min.   : 0.000   Min.   : 5.00   Min.   :185.0     Min.   :-19.0000  
##  1st Qu.: 0.000   1st Qu.:37.00   1st Qu.:197.0     1st Qu.: -6.0000  
##  Median : 0.000   Median :54.00   Median :209.6     Median :  0.0000  
##  Mean   : 3.144   Mean   :48.28   Mean   :209.7     Mean   : -0.0174  
##  3rd Qu.: 4.000   3rd Qu.:62.00   3rd Qu.:222.2     3rd Qu.:  6.0000  
##  Max.   :25.000   Max.   :70.00   Max.   :235.0     Max.   : 19.0000  
##                                   NA's   :500       NA's   :1465      
##     es_podio      puntos_por_vuelta anio_fundacion num_empleados 
##  Min.   :0.0000   Min.   :0.00000   Min.   :1929   Min.   : 300  
##  1st Qu.:0.0000   1st Qu.:0.00000   1st Qu.:1954   1st Qu.: 650  
##  Median :0.0000   Median :0.00000   Median :1977   Median : 850  
##  Mean   :0.1499   Mean   :0.05358   Mean   :1983   Mean   : 795  
##  3rd Qu.:0.0000   3rd Qu.:0.05882   3rd Qu.:2016   3rd Qu.:1100  
##  Max.   :1.0000   Max.   :0.51020   Max.   :2021   Max.   :1200  
##  NA's   :1465                       NA's   :974    NA's   :974   
##  presupuesto_millones pct_desarrollo   pct_personal   estatura_cm  
##  Min.   : 77.8        Min.   :35.40   Min.   :25.3   Min.   :159   
##  1st Qu.: 97.5        1st Qu.:39.30   1st Qu.:28.6   1st Qu.:174   
##  Median :127.7        Median :46.10   Median :31.8   Median :177   
##  Mean   :121.0        Mean   :45.21   Mean   :32.5   Mean   :177   
##  3rd Qu.:138.2        3rd Qu.:49.80   3rd Qu.:36.5   3rd Qu.:182   
##  Max.   :159.0        Max.   :54.40   Max.   :40.0   Max.   :186   
##  NA's   :2044         NA's   :2044    NA's   :2044   NA's   :1329  
##     peso_kg      salario_anual_millones titulos_mundiales
##  Min.   :54.00   Min.   : 0.00          Min.   :0.0000   
##  1st Qu.:66.00   1st Qu.: 3.00          1st Qu.:0.0000   
##  Median :69.00   Median : 6.00          Median :0.0000   
##  Mean   :67.96   Mean   :11.15          Mean   :0.6339   
##  3rd Qu.:70.00   3rd Qu.:10.00          3rd Qu.:0.0000   
##  Max.   :74.00   Max.   :55.00          Max.   :7.0000   
##  NA's   :1329    NA's   :1329           NA's   :1329

>>> BLOQUE 5 — A COMPLETAR POR EL GRUPO <<<

Crear 3 gráficos con ggplot2:

Gráfico 1: Histograma de puntos por carrera (points).

# Visualizamos la distribución de puntos para identificar concentración y outliers
ggplot(df_enriquecido, aes(x = points)) +
  geom_histogram(fill = "steelblue", color = "white", bins = 25) +
  labs(
    title = "Distribución de puntos por carrera",
    x = "Puntos",
    y = "Frecuencia"
  ) +
  theme_minimal()

El histograma suele concentrar muchas observaciones en valores bajos o nulos de puntos, lo que refleja que en una carrera la mayoria de pilotos no puntua demasiado y solo unos pocos se sitúan en la parte alta de la distribucion.

Esto refleja la estructura del sistema de puntuación de la Fórmula 1, donde solo los primeros clasificados obtienen puntos significativos.

Gráfico 2: Boxplot de posición final por constructor (top 5 equipos).

# Comparamos la distribución de posiciones finales entre los constructores más representados
top5_constructores <- df_enriquecido %>%
  count(constructor, sort = TRUE) %>%
  slice_head(n = 5) %>%
  pull(constructor)

df_boxplot <- df_enriquecido %>%
  filter(constructor %in% top5_constructores)

ggplot(df_boxplot, aes(x = constructor, y = position, fill = constructor)) +
  geom_boxplot(show.legend = FALSE) +
  labs(
    title = "Posición final por constructor (top 5 por número de registros)",
    x = "Constructor",
    y = "Posición final"
  ) +
  theme_minimal()
## Warning: Removed 735 rows containing non-finite outside the scale range
## (`stat_boxplot()`).

El boxplot permite comparar de un vistazo la mediana y la dispersion de cada equipo. Los constructores con cajas mas bajas y compactas tienden a ser mas competitivos y regulares en sus resultados finales.

Se han eliminado valores nulos en la variable de posición para evitar distorsiones en la visualización.

Gráfico 3: Barplot de frecuencia de nacionalidades de pilotos (top 10).

# Analizamos la frecuencia de nacionalidades para entender la composición del dataset
df_nacionalidades <- df_enriquecido %>%
  count(driver_nationality, sort = TRUE) %>%
  slice_head(n = 10)

ggplot(df_nacionalidades, aes(x = reorder(as.character(driver_nationality), n), y = n)) +
  geom_col(fill = "darkorange") +
  coord_flip() +
  labs(
    title = "Top 10 nacionalidades de pilotos",
    x = "Nacionalidad",
    y = "Frecuencia"
  ) +
  theme_minimal()

El barplot muestra que algunas nacionalidades concentran mas presencia en el historico, lo que ayuda a contextualizar el peso de ciertos paises dentro de la muestra analizada.

El análisis exploratorio permite identificar patrones clave en los datos, proporcionando una base sólida para la construcción del modelo predictivo en el siguiente script.


7. EDA bivariante

Referencia: Tema 5 — Exploración de datos (correlación)

>>> BLOQUE 6 — A COMPLETAR POR EL GRUPO <<<

Gráfico 4: Scatter plot de posición de salida (grid) vs. posición final (position) con línea de tendencia.

# Exploramos la relación entre posición de salida y resultado final
# para detectar patrones de dependencia entre ambas variables
ggplot(df_enriquecido, aes(x = grid, y = position)) +
  geom_point(alpha = 0.25, color = "navy") +
  geom_smooth(method = "lm", color = "firebrick", se = TRUE) +
  labs(
    title = "Relación entre posición de salida y posición final",
    x = "Posición de salida (grid)",
    y = "Posición final"
  ) +
  theme_minimal()
## `geom_smooth()` using formula = 'y ~ x'
## Warning: Removed 1465 rows containing non-finite outside the scale range
## (`stat_smooth()`).
## Warning: Removed 1465 rows containing missing values or values outside the scale range
## (`geom_point()`).

La nube de puntos y la recta de tendencia sugieren una relacion positiva: salir mas atras suele asociarse con terminar tambien en posiciones peores, aunque hay bastante dispersion por el efecto de adelantamientos, abandonos o estrategia.

[PROPORCIONADO] — Matriz de correlación

# [PROPORCIONADO]
df_num <- df_enriquecido %>%
  select(grid, position, points, laps, posiciones_ganadas, puntos_por_vuelta) %>%
  select_if(is.numeric)

cor_matrix <- cor(df_num, use = "complete.obs")

corrplot(cor_matrix,
         method = "color",
         addCoef.col = "black",
         tl.cex = 0.8,
         number.cex = 0.7,
         main = "Matriz de Correlación - Variables Numéricas F1")

Esta relación confirma que la posición de salida es un factor relevante en el rendimiento final, aunque no es determinante debido a la influencia de otros factores como estrategia o abandonos.


8. Guardado del dataset final

# [PROPORCIONADO]
saveRDS(df_enriquecido, "datos/gold/f1_datos_gold.rds")
saveRDS(df_resumen_pilotos, "datos/gold/resumen_pilotos.rds")
saveRDS(df_resumen_constructores, "datos/gold/resumen_constructores.rds")

cat("\n========================================\n")
## 
## ========================================
cat("Dataset Gold y resúmenes guardados en datos/gold/\n")
## Dataset Gold y resúmenes guardados en datos/gold/
cat("Script 2 completado con éxito.\n")
## Script 2 completado con éxito.
cat("========================================\n")
## ========================================

Pregunta teórica 3

¿Qué ventaja tiene utilizar pipelines de dplyr (operador %>%) frente a crear variables intermedias? Ilustra tu respuesta con un ejemplo de tu propio código.

Tu respuesta aquí (5-10 líneas):

Los pipelines de dplyr mejoran la legibilidad porque permiten expresar una secuencia de transformaciones como un flujo continuo, sin crear demasiadas variables intermedias. Eso hace mas facil seguir la logica del analisis y reduce el riesgo de reutilizar por error objetos antiguos o inconsistentes. En este trabajo, por ejemplo, df_top5_reciente encadena filter(), group_by(), summarise() y arrange() hasta llegar al resultado final. Si cada paso se guardara en un objeto distinto, el codigo seria mas largo y mas dificil de mantener.


Pregunta teórica 4

¿Qué información aporta un boxplot que no aporta un histograma? ¿Y una matriz de correlación, para qué sirve en la fase de EDA?

Tu respuesta aquí (5-10 líneas):

Un histograma muestra la distribucion general de una variable numerica, pero no resume tan bien los valores extremos ni facilita tanto la comparacion entre grupos como un boxplot. El boxplot aporta mediana, rango intercuartil, dispersion y posibles outliers, por eso resulta muy util al comparar constructores. La matriz de correlacion, en cambio, sirve para detectar relaciones lineales entre variables numericas y ver si algunas se mueven juntas. En EDA ayuda a decidir que variables merece la pena estudiar con mas detalle o usar despues en un modelo.