import pandas as pd
import psycopg2 as psy
from psycopg2 import Error
import numpy as np
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
import plotly.graph_objects as go
import plotly.express as px
import matplotlib.pyplot as plt
import seaborn as sns
'white') sns.set_style(
Campeonato Mundial de la Formula 1 (1950 - 2023)
Visualización Científica
La Fórmula 1, también conocida como F1, representa la cúspide de las carreras internacionales de monoplazas de ruedas abiertas, bajo la supervisión de la Federación Internacional del Automóvil (FIA). Desde su primera temporada en 1950, el Campeonato Mundial de Pilotos, rebautizado como el Campeonato Mundial de Fórmula 1 de la FIA en 1981, ha destacado como una de las principales competiciones a nivel global. La palabra “fórmula” en su nombre alude al conjunto de reglas que guían a todos los participantes en cuanto a la construcción y funcionamiento de los vehículos.
¿Qué haremos?
En esta sección, llevaremos a cabo un análisis exploratorio centrado en la tabla constructors
, que abarca los datos de los equipos que han competido en las carreras disputadas desde 1950 hasta 2023. Sin embargo, para llevar a cabo este análisis de manera integral, necesitamos consolidar información proveniente de diversas fuentes. Utilizaremos consultas para unificar los datos de la tabla constructors
con aquellos de las tablas circuits
(que contiene información sobre los circuitos donde se celebran las carreras de Fórmula 1), results
(que proporciona los resultados de las carreras), pit_stops
(paradas realizadas en boxes).
Es fundamental destacar que, para la tabla results
nos enfocaremos exclusivamente en los equipos que obtuvieron puntos en cada carrera. Asimismo, de la tabla pit_stops
, extraeremos únicamente la información correspondiente a la parada en boxes más rápida realizada.
Librerías
Para este proyecto trabajaremos con las siguientes librerías:
- Pandas
- Psycopg2
- Plotly
- Matplotlib
- Seaborn
- Scikit-learn
Pueden instalarse utilizando el siguiente comando desde la terminal: pip install pandas psycopg2 plotly...
, o bien, mediante el archivo requirements.txt utilizando pip install -r requirements.txt
en la terminal.
Una vez instaladas, podemos importarlas en nuestro entorno de trabajo de la siguiente manera:
Además, haremos uso de la siguiente función para evitar la repetición de código y facilitar la conexión a la base de datos:
def connection_db() -> psy.extensions.connection:
try:
= psy.connect(DATABASE_URL)
conn return conn
except (Exception, Error) as e:
print('Error while connecting to PostgreSQL', e)
Es importante destacar que en esta función, obtenemos una variable de entorno que almacena los datos de conexión a la base de datos. En este caso, estamos utilizando Neon que nos permite crear un servidor de bases de datos con PostgreSQL
.
Obtención de los datos
Veamos inicialmente las columnas que tenemos para cada una de las tablas mencionadas.
Tabla constructors
try:
= connection_db()
connection = connection.cursor()
cursor
cursor.execute("""
SELECT *
FROM constructors
LIMIT 5;
"""
)
= cursor.fetchall()
records = pd.DataFrame(records)
records_data
= []
columns for column in cursor.description:
0])
columns.append(column[
= columns
records_data.columns
display(records_data)except (Exception, Error) as e:
print('Error while executing the query', e)
finally:
if(connection):
cursor.close() connection.close()
constructorid | constructorref | name | nationality | url | |
---|---|---|---|---|---|
0 | 1 | mclaren | McLaren | British | http://en.wikipedia.org/wiki/McLaren |
1 | 2 | bmw_sauber | BMW Sauber | German | http://en.wikipedia.org/wiki/BMW_Sauber |
2 | 3 | williams | Williams | British | http://en.wikipedia.org/wiki/Williams_Grand_Pr... |
3 | 4 | renault | Renault | French | http://en.wikipedia.org/wiki/Renault_in_Formul... |
4 | 5 | toro_rosso | Toro Rosso | Italian | http://en.wikipedia.org/wiki/Scuderia_Toro_Rosso |
Tabla circuits
try:
= connection_db()
connection = connection.cursor()
cursor
cursor.execute("""
SELECT *
FROM circuits
LIMIT 5;
"""
)
= cursor.fetchall()
records = pd.DataFrame(records)
records_data
= []
columns for column in cursor.description:
0])
columns.append(column[
= columns
records_data.columns
display(records_data)except (Exception, Error) as e:
print('Error while executing the query', e)
finally:
if(connection):
cursor.close() connection.close()
circuitid | circuitref | name | location | country | lat | lng | alt | url | |
---|---|---|---|---|---|---|---|---|---|
0 | 1 | albert_park | Albert Park Grand Prix Circuit | Melbourne | Australia | -37.8497 | 144.968 | 10 | http://en.wikipedia.org/wiki/Melbourne_Grand_P... |
1 | 2 | sepang | Sepang International Circuit | Kuala Lumpur | Malaysia | 2.76083 | 101.738 | 18 | http://en.wikipedia.org/wiki/Sepang_Internatio... |
2 | 3 | bahrain | Bahrain International Circuit | Sakhir | Bahrain | 26.0325 | 50.5106 | 7 | http://en.wikipedia.org/wiki/Bahrain_Internati... |
3 | 4 | catalunya | Circuit de Barcelona-Catalunya | Montmeló | Spain | 41.57 | 2.26111 | 109 | http://en.wikipedia.org/wiki/Circuit_de_Barcel... |
4 | 5 | istanbul | Istanbul Park | Istanbul | Turkey | 40.9517 | 29.405 | 130 | http://en.wikipedia.org/wiki/Istanbul_Park |
Tabla results
try:
= connection_db()
connection = connection.cursor()
cursor
cursor.execute("""
SELECT *
FROM results
LIMIT 5;
"""
)
= cursor.fetchall()
records = pd.DataFrame(records)
records_data
= []
columns for column in cursor.description:
0])
columns.append(column[
= columns
records_data.columns
display(records_data)except (Exception, Error) as e:
print('Error while executing the query', e)
finally:
if(connection):
cursor.close() connection.close()
resultid | raceid | driverid | constructorid | number | grid | position | positiontext | positionorder | points | laps | time | milliseconds | fastestlap | rank | fastestlaptime | fastestlapspeed | statusid | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 18 | 1 | 1 | 22 | 1 | 1 | 1 | 1 | 10 | 58 | 1:34:50.616 | 5690616 | 39 | 2 | 1:27.452 | 218.300 | 1 |
1 | 2 | 18 | 2 | 2 | 3 | 5 | 2 | 2 | 2 | 8 | 58 | +5.478 | 5696094 | 41 | 3 | 1:27.739 | 217.586 | 1 |
2 | 3 | 18 | 3 | 3 | 7 | 7 | 3 | 3 | 3 | 6 | 58 | +8.163 | 5698779 | 41 | 5 | 1:28.090 | 216.719 | 1 |
3 | 4 | 18 | 4 | 4 | 5 | 11 | 4 | 4 | 4 | 5 | 58 | +17.181 | 5707797 | 58 | 7 | 1:28.603 | 215.464 | 1 |
4 | 5 | 18 | 5 | 1 | 23 | 3 | 5 | 5 | 5 | 4 | 58 | +18.014 | 5708630 | 43 | 1 | 1:27.418 | 218.385 | 1 |
Tabla pit stops
try:
= connection_db()
connection = connection.cursor()
cursor
cursor.execute("""
SELECT *
FROM pit_stops
LIMIT 5;
"""
)
= cursor.fetchall()
records = pd.DataFrame(records)
records_data
= []
columns for column in cursor.description:
0])
columns.append(column[
= columns
records_data.columns
display(records_data)except (Exception, Error) as e:
print('Error while executing the query', e)
finally:
if(connection):
cursor.close() connection.close()
raceid | driverid | stop | lap | time | duration | milliseconds | |
---|---|---|---|---|---|---|---|
0 | 841 | 153 | 1 | 1 | 17:05:23 | 26.898 | 26898 |
1 | 841 | 30 | 1 | 1 | 17:05:52 | 25.021 | 25021 |
2 | 841 | 17 | 1 | 11 | 17:20:48 | 23.426 | 23426 |
3 | 841 | 4 | 1 | 12 | 17:22:34 | 23.251 | 23251 |
4 | 841 | 13 | 1 | 13 | 17:24:10 | 23.842 | 23842 |
Tabla final
Con base en las columnas proporcionadas de cada tabla, podemos listar las que se utilizarán en el análisis de la siguiente manera:
- constructors: constructorId, constructorRef, name, nationality
- Circuits: name, location, country.
- Results: raceId, driverId, points, grid, laps, milliseconds, fastestlap, rank, fastestlapspeed, number, status.
- Pit Stops: stop, miliseconds.
Realicemos entonces la consulta a la base de datos para obtener esta tabla.
try:
= connection_db()
connection = connection.cursor()
cursor
cursor.execute("""
SELECT
c.constructorid, c.constructorref, c.name, c.nationality,
circuits.name AS circuit_name, circuits.location AS circuit_location, circuits.country AS circuit_country,
races.year,
r.raceid, r.driverid, r.number, r.grid, r.positionorder, r.points, r.laps, r.milliseconds, r.fastestlap, r.rank, r.fastestlapspeed, r.statusid,
fastest_pit_stop.stop AS fastest_pit_stop, fastest_pit_stop.milliseconds AS fastest_pit_stop_time
FROM constructors c
JOIN constructor_results ON c.constructorId = constructor_results.constructorId
JOIN races ON constructor_results.raceId = races.raceId
JOIN circuits ON races.circuitId = circuits.circuitId
JOIN (
SELECT raceId, constructorId, MAX(points) AS points
FROM constructor_standings
GROUP BY raceId, constructorId
) AS cs ON constructor_results.raceId = cs.raceId AND constructor_results.constructorId = cs.constructorId
JOIN results as r ON constructor_results.raceId = r.raceId AND constructor_results.constructorId = r.constructorId AND r.points > 0
LEFT JOIN (
SELECT ps.raceId, ps.driverId, ps.stop, ps.milliseconds
FROM pit_stops ps
JOIN drivers d ON ps.driverId = d.driverId
WHERE (ps.raceId, ps.driverId, ps.milliseconds) IN (
SELECT raceId, driverId, MIN(milliseconds)
FROM pit_stops
GROUP BY raceId, driverId
)
) AS fastest_pit_stop ON constructor_results.raceId = fastest_pit_stop.raceId AND r.driverId = fastest_pit_stop.driverId;
"""
)
= cursor.fetchall()
records = pd.DataFrame(records)
records_data
= []
columns for column in cursor.description:
0])
columns.append(column[
= columns
records_data.columns
display(records_data.head())except (Exception, Error) as e:
print('Error while executing the query', e)
finally:
if(connection):
cursor.close() connection.close()
constructorid | constructorref | name | nationality | circuit_name | circuit_location | circuit_country | year | raceid | driverid | ... | positionorder | points | laps | milliseconds | fastestlap | rank | fastestlapspeed | statusid | fastest_pit_stop | fastest_pit_stop_time | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | mclaren | McLaren | British | Albert Park Grand Prix Circuit | Melbourne | Australia | 2008 | 18 | 1 | ... | 1 | 10 | 58 | 5690616.0 | 39.0 | 2.0 | 218.300 | 1 | NaN | NaN |
1 | 2 | bmw_sauber | BMW Sauber | German | Albert Park Grand Prix Circuit | Melbourne | Australia | 2008 | 18 | 2 | ... | 2 | 8 | 58 | 5696094.0 | 41.0 | 3.0 | 217.586 | 1 | NaN | NaN |
2 | 3 | williams | Williams | British | Albert Park Grand Prix Circuit | Melbourne | Australia | 2008 | 18 | 3 | ... | 3 | 6 | 58 | 5698779.0 | 41.0 | 5.0 | 216.719 | 1 | NaN | NaN |
3 | 4 | renault | Renault | French | Albert Park Grand Prix Circuit | Melbourne | Australia | 2008 | 18 | 4 | ... | 4 | 5 | 58 | 5707797.0 | 58.0 | 7.0 | 215.464 | 1 | NaN | NaN |
4 | 1 | mclaren | McLaren | British | Albert Park Grand Prix Circuit | Melbourne | Australia | 2008 | 18 | 5 | ... | 5 | 4 | 58 | 5708630.0 | 43.0 | 1.0 | 218.385 | 1 | NaN | NaN |
5 rows × 22 columns
Análisis exploratorio de datos
En esta sección nos sumergiremos en la comprensión de los datos disponibles, exploraremos los tipos de variables presentes, calcularemos medidas de tendencia central, llevaremos a cabo la depuración de los datos y procederemos a su visualización.
Para llevar a cabo este análisis, nos apoyaremos en el marco de trabajo establecido, APP Framework.
- Atención: Entender el conjunto de datos.
- Propósito: Establecer objetivos claros.
- Proceso: Realizar el Análisis Exploratorio de Datos (EDA) propiamente dicho.
- Beneficio: Extraer y aplicar los resultados obtenidos.
Conociendo los datos
Conocer los datos es un paso fundamental en cualquier análisis. Proporciona una comprensión inicial del problema, permite validar la calidad de los datos, seleccionar características relevantes, preparar los datos adecuadamente y generar ideas y hipótesis. En resumen, la exploración inicial de los datos sienta las bases para un análisis más profundo y asegura que los resultados sean significativos y confiables.
Tipos de datos
Para realizar un análisis exploratorio, primero debes conocer el tipo de variables con las que estamos tratando. Conocer si tenemos variables numéricas o categóricas podrían determinar el rumbo del análisis que realizaremos.
records_data.dtypes
constructorid int64
constructorref object
name object
nationality object
circuit_name object
circuit_location object
circuit_country object
year int64
raceid int64
driverid int64
number int64
grid int64
positionorder int64
points int64
laps int64
milliseconds float64
fastestlap float64
rank float64
fastestlapspeed object
statusid int64
fastest_pit_stop float64
fastest_pit_stop_time float64
dtype: object
Observemos que todas las variables tienen el tipo de dato correcto, excepto la columna fastestlapspeed
, que toma valores numéricos pero está siendo interpretada como un dato tipo object
. Por lo tanto, es necesario convertir esta columna en tipo numérico. Además, vamos a cambiar los tipos de datos de las variables raceid
, statusid
,circuitid
, driverid
y constructorid
a tipo object
.
'fastestlapspeed'] = pd.to_numeric(records_data['fastestlapspeed'])
records_data['raceid', 'driverid', 'constructorid', 'statusid']] = records_data[['raceid', 'driverid', 'constructorid', 'statusid']].astype('object')
records_data[[
records_data.dtypes
constructorid object
constructorref object
name object
nationality object
circuit_name object
circuit_location object
circuit_country object
year int64
raceid object
driverid object
number int64
grid int64
positionorder int64
points int64
laps int64
milliseconds float64
fastestlap float64
rank float64
fastestlapspeed float64
statusid object
fastest_pit_stop float64
fastest_pit_stop_time float64
dtype: object
Dimensiones de los registros
Determinar el tamaño de nuestros registros es fundamental, ya que nos permite comprender la magnitud de la información que estamos manejando. Esto a su vez nos ayuda a establecer posibles caminos a seguir en caso de realizar transformaciones y análisis adicionales.
records_data.shape
(7441, 22)
len(records_data['constructorref'].unique())
88
Esto indica que desde el año 1950 hasta el 2023, en las más de 1000 carreras que se han llevado a cabo, han competido 88 equipos en esta competencia. Además, teniendo más de 7000 registros significa que estamos trabajando con una cantidad considerable de datos sobre equipos constructores.
Datos faltantes
Determinar la presencia de datos faltantes es crucial, ya que puede indicar si podemos confiar en una columna para el análisis o si necesitamos tomar medidas para imputar esos valores ausentes.
sum() records_data.isnull().
constructorid 0
constructorref 0
name 0
nationality 0
circuit_name 0
circuit_location 0
circuit_country 0
year 0
raceid 0
driverid 0
number 0
grid 0
positionorder 0
points 0
laps 0
milliseconds 1546
fastestlap 3888
rank 3877
fastestlapspeed 3888
statusid 0
fastest_pit_stop 4932
fastest_pit_stop_time 4932
dtype: int64
Con los resultados obtenidos, observamos que tenemos una cantidad significativa de datos faltantes. Esta situación puede afectar los análisis futuros, dependiendo del tipo de variable que estemos considerando. Es importante determinar un método adecuado para la imputación de datos en caso de que sea necesario. Veamos el porcentaje que representa esta cantidad de datos faltantes en el total de nuestros datos.
= records_data.isnull().sum()
missing_values = round((missing_values / len(records_data)) * 100, 4)
missing_percentage missing_percentage
constructorid 0.0000
constructorref 0.0000
name 0.0000
nationality 0.0000
circuit_name 0.0000
circuit_location 0.0000
circuit_country 0.0000
year 0.0000
raceid 0.0000
driverid 0.0000
number 0.0000
grid 0.0000
positionorder 0.0000
points 0.0000
laps 0.0000
milliseconds 20.7768
fastestlap 52.2510
rank 52.1032
fastestlapspeed 52.2510
statusid 0.0000
fastest_pit_stop 66.2814
fastest_pit_stop_time 66.2814
dtype: float64
Tenemos un gran porcentaje de datos faltantes en nuestras variables. Sin embargo, estos datos faltantes parecen estar concentrados en las variables relacionadas con medidas de tiempos y velocidades. Esto sugiere que estos datos podrían faltar debido a limitaciones técnicas o falta de registro en las fechas más antiguas, donde la toma de estas medidas podría no haber sido sistemática.
Para comprender mejor la distribución de estos datos faltantes, examinemos en qué fechas están ocurriendo y verifiquemos la fecha máxima y mínima en la que faltan estas observaciones.
any(axis = 1)] records_data[records_data.isnull().
constructorid | constructorref | name | nationality | circuit_name | circuit_location | circuit_country | year | raceid | driverid | ... | positionorder | points | laps | milliseconds | fastestlap | rank | fastestlapspeed | statusid | fastest_pit_stop | fastest_pit_stop_time | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | mclaren | McLaren | British | Albert Park Grand Prix Circuit | Melbourne | Australia | 2008 | 18 | 1 | ... | 1 | 10 | 58 | 5690616.0 | 39.0 | 2.0 | 218.300 | 1 | NaN | NaN |
1 | 2 | bmw_sauber | BMW Sauber | German | Albert Park Grand Prix Circuit | Melbourne | Australia | 2008 | 18 | 2 | ... | 2 | 8 | 58 | 5696094.0 | 41.0 | 3.0 | 217.586 | 1 | NaN | NaN |
2 | 3 | williams | Williams | British | Albert Park Grand Prix Circuit | Melbourne | Australia | 2008 | 18 | 3 | ... | 3 | 6 | 58 | 5698779.0 | 41.0 | 5.0 | 216.719 | 1 | NaN | NaN |
3 | 4 | renault | Renault | French | Albert Park Grand Prix Circuit | Melbourne | Australia | 2008 | 18 | 4 | ... | 4 | 5 | 58 | 5707797.0 | 58.0 | 7.0 | 215.464 | 1 | NaN | NaN |
4 | 1 | mclaren | McLaren | British | Albert Park Grand Prix Circuit | Melbourne | Australia | 2008 | 18 | 5 | ... | 5 | 4 | 58 | 5708630.0 | 43.0 | 1.0 | 218.385 | 1 | NaN | NaN |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
7299 | 1 | mclaren | McLaren | British | Autódromo Hermanos Rodríguez | Mexico City | Mexico | 2022 | 1094 | 846 | ... | 9 | 2 | 70 | NaN | 48.0 | 17.0 | 185.779 | 11 | 1.0 | 22585.0 |
7300 | 51 | alfa | Alfa Romeo | Swiss | Autódromo Hermanos Rodríguez | Mexico City | Mexico | 2022 | 1094 | 822 | ... | 10 | 1 | 70 | NaN | 43.0 | 16.0 | 185.866 | 11 | 1.0 | 23863.0 |
7379 | 1 | mclaren | McLaren | British | Circuit de Monaco | Monte-Carlo | Monaco | 2023 | 1104 | 846 | ... | 9 | 2 | 77 | NaN | 46.0 | 19.0 | 154.324 | 11 | 1.0 | 24663.0 |
7380 | 1 | mclaren | McLaren | British | Circuit de Monaco | Monte-Carlo | Monaco | 2023 | 1104 | 857 | ... | 10 | 1 | 77 | NaN | 47.0 | 14.0 | 154.983 | 11 | 1.0 | 25851.0 |
7430 | 117 | aston_martin | Aston Martin | British | Hungaroring | Budapest | Hungary | 2023 | 1109 | 840 | ... | 10 | 1 | 69 | NaN | 54.0 | 11.0 | 189.051 | 11 | 1.0 | 21910.0 |
5280 rows × 22 columns
Fecha mínima: 1958
Fecha promedio: 1990
Fecha máxima: 2023
Observando estos resultados, podemos confirmar nuestra teoría. Estos registros faltantes pueden ser debidos a limitaciones técnicas en aquellos tiempos. Sin embargo, también tenemos datos faltantes recientes.
Exploración de los datos
En esta sección realizaremos el verdadero análisis exploratorio de nuestros datos. Abordaremos los siguientes aspectos:
Medidas de tendencia central: Calcularemos medidas como la media, la mediana y la moda para entender mejor la distribución de nuestros datos.
Limpieza de los datos: Abordaremos la limpieza de nuestros datos, incluyendo la búsqueda de datos atípicos.
Transformación: Determinaremos si es necesario aplicar alguna transformación a nuestros datos para facilitar los análisis subsiguientes.
Visualización: Utilizaremos herramientas gráficas para explorar el comportamiento de nuestros datos y extraer patrones o tendencias.
Esta fase nos permitirá comprender mejor la naturaleza de nuestros datos y prepararlos adecuadamente para análisis más avanzados.
Medidas de tendencia central
records_data.describe()
year | number | grid | positionorder | points | laps | milliseconds | fastestlap | rank | fastestlapspeed | fastest_pit_stop | fastest_pit_stop_time | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
count | 7441.000000 | 7441.000000 | 7441.000000 | 7441.000000 | 7441.000000 | 7441.000000 | 5.895000e+03 | 3553.000000 | 3564.000000 | 3553.000000 | 2509.000000 | 2.509000e+03 |
mean | 1997.963177 | 14.495498 | 7.137078 | 4.354522 | 6.467679 | 62.388792 | 5.989774e+06 | 46.603152 | 6.680415 | 206.006290 | 1.536070 | 3.173199e+04 |
std | 18.309481 | 15.192749 | 4.989723 | 2.463845 | 5.667732 | 13.737097 | 1.089727e+06 | 14.355980 | 4.232408 | 20.582848 | 0.761929 | 9.771809e+04 |
min | 1958.000000 | 0.000000 | 0.000000 | 1.000000 | 1.000000 | 1.000000 | 2.070710e+05 | 2.000000 | 0.000000 | 147.980000 | 1.000000 | 1.317300e+04 |
25% | 1983.000000 | 5.000000 | 3.000000 | 2.000000 | 2.000000 | 54.000000 | 5.388156e+06 | 39.000000 | 3.000000 | 195.557000 | 1.000000 | 2.147400e+04 |
50% | 2002.000000 | 10.000000 | 6.000000 | 4.000000 | 4.000000 | 61.000000 | 5.771321e+06 | 48.000000 | 6.000000 | 206.603000 | 1.000000 | 2.287400e+04 |
75% | 2014.000000 | 19.000000 | 10.000000 | 6.000000 | 9.000000 | 71.000000 | 6.295221e+06 | 56.000000 | 10.000000 | 219.005000 | 2.000000 | 2.463900e+04 |
max | 2023.000000 | 99.000000 | 26.000000 | 21.000000 | 50.000000 | 110.000000 | 1.472659e+07 | 85.000000 | 20.000000 | 257.320000 | 6.000000 | 1.517308e+06 |
Estos resultados nos pueden permitir concluir lo siguiente:
Puntos, posición en la parrilla y número de vueltas: Los datos muestran que el promedio de puntos obtenidos por carrera es de aproximadamente 6.47. La posición promedio en la parrilla de salida es alrededor de 7.14. En cuanto al número de vueltas, el promedio es de aproximadamente 62.39. Estas variabilidades indican que hay una amplia gama de resultados en términos de puntos, posición en la parrilla y número de vueltas, lo que podría atribuirse a diferencias en la dificultad de los circuitos, la calidad de los vehículos y las estrategias de los equipos en cada carrera.
Tiempo de carrera y velocidad: El tiempo medio de carrera es de aproximadamente 5.99 millones de milisegundos (alrededor de 99.83 minutos) millones de milisegundos. Esta variabilidad en los tiempos de carrera puede deberse a la longitud del circuito, las condiciones climáticas y la cantidad de incidentes en la pista. La velocidad media de la vuelta más rápida realizada por el ganador es de alrededor de 206.01 km/h. Estas diferencias en la velocidad pueden ser atribuibles a las características específicas del circuito y la competencia entre los conductores.
Paradas en boxes: El tiempo medio para la parada en boxes más rápida es de aproximadamente 31.73 segundos. Las paradas en boxes indican que hay una variabilidad en los tiempos de pit stop entre las carreras, que puede estar influenciada por factores como la estrategia del equipo y la eficiencia en el box.
Limpieza de los datos
La limpieza de datos es una etapa crucial en cualquier análisis, por lo que en este apartado trataremos los datos faltantes y observaremos si existen datos atípicos en nuestras variables.
Datos faltantes
Existen diversas estrategias para abordar este problema. Usualmente, en este tipo de análisis se recurre a la imputación
de valores faltantes utilizando la media
, moda
o mediana
, o llenando los datos con los valores anteriores o siguientes
. Sin embargo, estas técnicas pueden no ser óptimas para conjuntos de datos extensos o con características específicas.
En nuestro caso, una estrategia efectiva sería utilizar la imputación
de datos faltantes basada en puntos similares en los datos mediante el algoritmo KNN (K-Nearest Neighbors)
y Random Forest Classification
. Este método considera las características de observaciones similares para estimar los valores faltantes de manera más precisa y realista, lo que resulta especialmente útil en conjuntos de datos complejos como el nuestro.
Inicialmente, creemos un DataFrame
temporal donde estarán los mismos datos de records_data
pero sin las columnas correspondientes a tipo object
.
= records_data.select_dtypes(exclude=['object'])
temp_df
= IterativeImputer(min_value=0, max_iter=30, imputation_order='roman', random_state=1)
imputer = imputer.fit_transform(temp_df)
imputed_data
= pd.DataFrame(imputed_data, columns=temp_df.columns)
temp_df_imputed sum() temp_df_imputed.isnull().
year 0
number 0
grid 0
positionorder 0
points 0
laps 0
milliseconds 0
fastestlap 0
rank 0
fastestlapspeed 0
fastest_pit_stop 0
fastest_pit_stop_time 0
dtype: int64
Bien, ya no tenemos datos faltantes. Ahora, verifiquemos si los resultados obtenidos en las medidas de tendencia central del DataFrame original cambiaron significativamente.
temp_df_imputed.describe()
year | number | grid | positionorder | points | laps | milliseconds | fastestlap | rank | fastestlapspeed | fastest_pit_stop | fastest_pit_stop_time | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
count | 7441.000000 | 7441.000000 | 7441.000000 | 7441.000000 | 7441.000000 | 7441.000000 | 7.441000e+03 | 7441.000000 | 7441.000000 | 7441.000000 | 7441.000000 | 7.441000e+03 |
mean | 1997.963177 | 14.495498 | 7.137078 | 4.354522 | 6.467679 | 62.388792 | 6.081225e+06 | 37.759931 | 5.846977 | 196.131523 | 2.240600 | 3.454560e+04 |
std | 18.309481 | 15.192749 | 4.989723 | 2.463845 | 5.667732 | 13.737097 | 1.042933e+06 | 15.428452 | 3.313179 | 24.840240 | 0.824293 | 5.900247e+04 |
min | 1958.000000 | 0.000000 | 0.000000 | 1.000000 | 1.000000 | 1.000000 | 2.070710e+05 | 0.000000 | 0.000000 | 102.884413 | 0.646780 | 0.000000e+00 |
25% | 1983.000000 | 5.000000 | 3.000000 | 2.000000 | 2.000000 | 54.000000 | 5.471968e+06 | 28.193697 | 3.334282 | 181.618486 | 1.843325 | 2.172100e+04 |
50% | 2002.000000 | 10.000000 | 6.000000 | 4.000000 | 4.000000 | 61.000000 | 5.899051e+06 | 37.728190 | 5.300952 | 198.225000 | 2.185505 | 2.571253e+04 |
75% | 2014.000000 | 19.000000 | 10.000000 | 6.000000 | 9.000000 | 71.000000 | 6.496058e+06 | 48.000000 | 7.430359 | 212.063000 | 2.871513 | 3.848875e+04 |
max | 2023.000000 | 99.000000 | 26.000000 | 21.000000 | 50.000000 | 110.000000 | 1.472659e+07 | 85.000000 | 20.818095 | 327.756546 | 6.000000 | 1.517308e+06 |
Comparando los resultados de las medidas de tendencia central antes y después de la imputación de datos con el algoritmo KNN
, observamos algunas diferencias significativas en ciertas variables:
Puntos, posición en la parrilla y número de vueltas: No se observan cambios significativos en las medidas de tendencia central y dispersión de los puntos obtenidos, la posición en la parrilla de salida y el número de vueltas antes y después de la imputación de datos.
Tiempo de carrera y velocidad: Después de la imputación de datos, se observa un ligero aumento en la media del tiempo de carrera, mientras que la desviación estándar disminuye. Esto sugiere una mayor consistencia en los tiempos de carrera entre las carreras. Por otro lado, no hay cambios significativos en las estadísticas de velocidad de la vuelta más rápida antes y después de la imputación.
Paradas en boxes: Después de la imputación de datos, se observa un aumento en la media del tiempo de la parada más rápida, lo que indica que las paradas en boxes pueden haber sido ligeramente más lentas en promedio después de la imputación. Sin embargo, la desviación estándar disminuye, lo que sugiere una mayor consistencia en los tiempos de parada más rápida entre las carreras.
Ahora que hemos realizado la imputación de datos, pasemos estos datos a nuestro dataframe original.
= temp_df_imputed
records_data[temp_df.columns] sum() records_data.isnull().
constructorid 0
constructorref 0
name 0
nationality 0
circuit_name 0
circuit_location 0
circuit_country 0
year 0
raceid 0
driverid 0
number 0
grid 0
positionorder 0
points 0
laps 0
milliseconds 0
fastestlap 0
rank 0
fastestlapspeed 0
statusid 0
fastest_pit_stop 0
fastest_pit_stop_time 0
dtype: int64
Datos atípicos
Veamos ahora si existen datos atípicos
en nuestro registro. En este caso, utilizaremos el rango intercuartílico (IQR)
para identificar los valores atípicos. Si un valor cae por debajo de Q1 - 1.5 * IQR
o por encima de Q3 + 1.5 * IQR
, se considera un valor atípico.
= temp_df.columns
numeric_columns
for col in numeric_columns:
= records_data[col].quantile(0.25)
q1 = records_data[col].quantile(0.75)
q3
= q3 - q1
iqr = q1 - 1.5 * iqr
lower_bound = q3 + 1.5 * iqr
upper_bound = records_data[(records_data[col] < lower_bound) | (records_data[col] > upper_bound)]
outliers = len(outliers)
n_outliers print(f'#Outliers in {col} are {n_outliers} and represent a {round(n_outliers/len(records_data) * 100, 4)}% of total records')
#Outliers in year are 0 and represent a 0.0% of total records
#Outliers in number are 503 and represent a 6.7598% of total records
#Outliers in grid are 94 and represent a 1.2633% of total records
#Outliers in positionorder are 1 and represent a 0.0134% of total records
#Outliers in points are 274 and represent a 3.6823% of total records
#Outliers in laps are 263 and represent a 3.5345% of total records
#Outliers in milliseconds are 309 and represent a 4.1527% of total records
#Outliers in fastestlap are 10 and represent a 0.1344% of total records
#Outliers in rank are 272 and represent a 3.6554% of total records
#Outliers in fastestlapspeed are 155 and represent a 2.0831% of total records
#Outliers in fastest_pit_stop are 12 and represent a 0.1613% of total records
#Outliers in fastest_pit_stop_time are 451 and represent a 6.061% of total records
Como podemos observar, respecto a los más de 7000 registros que tiene la tabla, hay una pequeña cantidad significativa de datos atípicos en nuestras variables. Veamos gráficamente qué es lo que está ocurriendo con ellos.
Realizaremos un gráfico de caja y bigotes
e histogramas
para ver el comportamiento y la distribución de nuestros datos.
=(9,12))
plt.figure(figsize
= 1
i for col in numeric_columns:
5,3,i)
plt.subplot(=1.5)
plt.boxplot(records_data[col],whis
plt.title(col)
+= 1
i plt.show()
=(8,12))
plt.figure(figsize
= 1
i for col in numeric_columns:
5, 3, i)
plt.subplot(=True)
sns.histplot(records_data[col], kde
plt.title(col)+= 1
i
plt.tight_layout() plt.show()
Dada la naturaleza de las variables, en algunos casos como rank
, lap
, grid
, points
, entre otros que son datos numéricos discretos
y representan una categoría
específica, es normal que existan datos atípicos. Por otro lado, para las otras variables en nuestra base de datos, estos datos atípicos no están afectando mucho la distribución de cada una de ellas, por lo tanto, no realizaremos cambios en ellas.
Visualización
La visualización es uno de los puntos más importantes a la hora de realizar una exploración de los datos. Con ella, no solo podemos encontrar las relaciones que existen entre nuestras variables, sino que también podemos representar gráficamente las informaciones más relevantes de los datos.
Comencemos visualizando los gráficos de correlación
y dispersión
entre las variables para comprender mejor sus relaciones y encontrar posibles patrones.
Gráfico de correlación
= records_data[numeric_columns].corr()
corr = np.triu(np.ones_like(corr, dtype=bool))
mask
=True, cmap='PRGn', square=True, center=0, mask=mask) sns.heatmap(corr, annot
Como podemos observar, las variables con una alta correlación (>0.5
o <-0.5
) son aquellas que están relacionadas entre sí, como laps
, fastestlapspeed
, fastest_lap
, positionorder
, year
, fastest_pip_stop
entre otras. Por lo tanto, esto no debería ser un problema y podemos proseguir con la exploración de los datos.
Gráfico de dispersión
=(8, 12))
plt.figure(figsize
sns.pairplot(records_data[numeric_columns]) plt.show()
<Figure size 768x1152 with 0 Axes>
De este gráfico podemos notar que a medida que pasan los años, los equipos han ha evolucionado progresivamente en términos de velocidades y tiempos de carreras obtenidos. La velocidad y duración en boxes han aumentado, lo que indica una mejora en los automóviles y las técnicas de revisión de ellos. Sin embargo, estos cambios en las velocidades y tiempos también se han vuelto más dispersos a lo largo de los años, lo que implica que ha habido una gran diferencia entre los equipos constructores. Además, también podemos observar algunas relaciones proporcionales en nuestras variables, como aquellas relacionadas nuevamente con las velocidades y tiempos.
Distribución de carreras
= records_data.groupby('name').size().reset_index(name='total_races')
constructor_stats = constructor_stats.sort_values(by='total_races', ascending=False)
constructor_stats = constructor_stats.rename(columns={'name': 'constructor_name'})
constructor_stats
= constructor_stats.head(5)
top_constructors
=(8, 7))
plt.figure(figsize'total_races'], kde=False, bins=30, color='skyblue', edgecolor='black')
sns.histplot(constructor_stats['Distribución del número total de carreras por constructor')
plt.title('Número Total de Carreras')
plt.xlabel('Frecuencia')
plt.ylabel(
= '\n'.join([f"{i+1}. {row['constructor_name']}: {row['total_races']} carreras" for i, (index, row) in enumerate(top_constructors.iterrows(), start=0)])
text max(top_constructors['total_races']) * 1.02, plt.ylim()[1] * 0.9, text, ha='right', va='top', fontsize=10)
plt.text(
plt.tight_layout() plt.show()
Se observa que la mayoría de los constructores han participado en una cantidad menor de carreras, con una concentración significativa cerca del cero. Esto indica que hay muchos equipos que han tenido una presencia breve en el deporte (pocas temporadas). Sin embargo, hay un pequeño grupo de equipos, como Ferrari y McLaren, que resaltan con una participación en un número mucho mayor de carreras, superando los 800 Grandes Premios, lo que subraya su posición como pilares históricos de la Fórmula 1 con una larga tradición de competencia continua.
Distribución de costructores por nacionalidad
= records_data.groupby('nationality').size().reset_index(name='num_constructors')
constructor_stats = constructor_stats.sort_values(by='num_constructors', ascending=False)
constructor_stats = constructor_stats.rename(columns={'nationality': 'constructor_nationality'})
constructor_stats
= constructor_stats.head(5)
top_constructors
=(8, 7))
plt.figure(figsize'constructor_nationality')['num_constructors'].plot(kind='bar')
constructor_stats.set_index('Número de constructores por nacionalidad')
plt.title('Nacionalidad')
plt.xlabel('Número de Constructores')
plt.ylabel(=45)
plt.xticks(rotation
plt.tight_layout() plt.show()
El gráfico muestra la distribución de constructores de Fórmula 1 por nacionalidad. La nacionalidad británica domina claramente con la mayor cantidad de constructores, lo cual resalta la influencia y la historia del Reino Unido en el automovilismo de F1. Le siguen con menor frecuencia los constructores americanos e italianos, reflejando también su papel significativo en la F1.
La presencia de una variedad de otras nacionalidades indica la diversidad internacional de los equipos, aunque con una representación mucho menor comparada con las tres principales. Esta distribución no solo refleja la historia y geografía del deporte sino también las industrias automotrices nacionales y su apoyo al automovilismo.
Top 5 equipos más ganadores
= records_data[records_data['positionorder'] == 1]
constructor_stats = records_data.groupby('name').size().reset_index(name='total_wins')
constructor_stats = constructor_stats.sort_values(by='total_wins', ascending=False)
constructor_stats = constructor_stats.rename(columns={'name': 'constructor_name'})
constructor_stats
= constructor_stats.head(5)
top_constructors
=(8, 7))
plt.figure(figsize'constructor_name'], top_constructors['total_wins'], color='skyblue')
plt.bar(top_constructors['Top 5 Equipos más ganadores en F1')
plt.title('Equipo')
plt.xlabel('Número Total de Victorias')
plt.ylabel(
plt.xticks() plt.show()
El gráfico muestra el top 10 de equipos más ganadores en la historia de la Fórmula 1. Ferrari lidera con una diferencia significativa, destacando su legado como la escudería más exitosa. McLaren le sigue, con Mercedes, Williams y Red Bull completando los cinco primeros lugares. Estos equipos han sido fundamentales en el deporte, no solo por su número de victorias sino también por su influencia en la evolución de la competición.
Evolución de la velocidad
=(8, 8))
plt.figure(figsize='year', y='fastestlapspeed', data=records_data, label='Velocidad de Vuelta Más Rápida')
sns.lineplot(x'Evolución de la velocidad a lo largo de los años')
plt.title('Año')
plt.xlabel('Velocidad (km/h)')
plt.ylabel(
plt.legend() plt.show()
El gráfico presenta la evolución de la velocidad de la vuelta más rápida en la Fórmula 1 a lo largo de los años. Se observan fluctuaciones en la velocidad a lo largo del tiempo, con una tendencia general al aumento. La sombra alrededor de la línea indica la variabilidad en la velocidad de la vuelta más rápida cada año, sugiriendo que, aunque hay años con velocidades pico, también existen otros factores que pueden afectar la velocidad, como las regulaciones técnicas, las condiciones meteorológicas o el diseño de los circuitos.
El incremento significativo de la velocidad podría atribuirse a avances tecnológicos en los motores y la aerodinámica, así como a cambios en las regulaciones de la F1 que permiten vehículos más rápidos. Sin embargo, el pico en los años recientes también puede reflejar el desarrollo y perfeccionamiento constante en las estrategias de carrera y la optimización del rendimiento del vehículo.
Evolución de la velocidad máxima alcanzada por equipo
= plt.subplots(3, 2, figsize=(12, 8))
fig, axes
for ax, team in zip(axes.flatten(), top_constructors['constructor_name']):
= records_data[records_data['name'] == team]
team_data ='year', y='fastestlapspeed', data=team_data, ax=ax)
sns.lineplot(xf'Evolución de {team} - Velocidad máxima alcanzada')
ax.set_title('Año')
ax.set_xlabel('Velocidad máxima alcanzada (km/h)')
ax.set_ylabel(
plt.tight_layout() plt.show()
La evolución de la velocidad máxima en la Fórmula 1 a lo largo de los años es un resultado complejo de factores como la tecnología, las regulaciones, el diseño de los circuitos y la competencia entre equipos. Cada equipo tiene su propia trayectoria, y las mejoras constantes o altibajos pueden atribuirse a diversas razones.
Evolución de las paradas en boxes
'fastest_pit_stop_time_in_seconds'] = records_data['fastest_pit_stop_time'] / 1000
records_data[
=(8, 8))
plt.figure(figsize='year', y='fastestlapspeed', data=records_data, label='Velocidad de Vuelta Más Rápida')
sns.lineplot(x'Evolución de la velocidad a lo largo de los años')
plt.title('Año')
plt.xlabel('Velocidad (km/h)')
plt.ylabel(
plt.legend() plt.show()
Evolución de las paradas en boxes por equipo
= plt.subplots(3, 2, figsize=(12, 12))
fig, axes
for ax, team in zip(axes.flatten(), top_constructors['constructor_name']):
= records_data[records_data['name'] == team]
team_data ='year', y='fastest_pit_stop_time_in_seconds', data=team_data, ax=ax)
sns.lineplot(xf'Evolución de {team} - Tiempo Promedio de Parada en Boxes')
ax.set_title('Año')
ax.set_xlabel('Tiempo Promedio de Parada en Boxes (segundos)')
ax.set_ylabel(
plt.tight_layout() plt.show()
La gráfica muestra cómo ha evolucionado el tiempo promedio de parada en boxes para los mejores equipos de Fórmula 1 a lo largo de los años. Las mejoras tecnológicas, el entrenamiento del personal, las estrategias de carrera y los cambios en el diseño de los autos han influido en estos tiempos. Los equipos más eficientes han logrado reducir sus tiempos de parada, demostrando habilidad y dedicación en las carreras.