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.
library(dplyr) # Manipulación de datos
library(ggplot2) # Visualizaciones
library(corrplot) # Matrices de correlación
# [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
mutate()Referencia: Tema 4 — Procesado y transformación de datos
Crear al menos 3 variables nuevas en
df_resultados usando mutate():
posiciones_ganadas: cuántas posiciones ganó el piloto.
Se calcula como grid - position (positivo = ganó
posiciones).es_podio: indicador binario (1/0) de si terminó en el
podio (posición 1, 2 o 3). PISTA:
ifelse(position <= 3, 1, 0).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.
Referencia: Tema 4 — Procesado y transformación de datos (joins)
Unir df_resultados con al menos uno de
los otros datasets para enriquecer la información.
df_equipos por
constructor ↔︎ nombre_equipodf_pilotos_bio por
driver_idPISTA 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.
group_by() y
summarise()Referencia: Tema 4 — Procesado y transformación de datos
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.
Referencia: Tema 4 — Procesado y transformación de datos
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.
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
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.
Referencia: Tema 5 — Exploración de datos (correlación)
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]
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.
# [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")
## ========================================
¿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.
¿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.