import pandas as pd
import psycopg2 as psy
from psycopg2 import Error
import plotly.graph_objects as go
import plotly.express as px
import numpy as np
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.
Definición
El Análisis de Componentes Principales (PCA) es una técnica que permite resumir y simplificar conjuntos de datos complejos y multidimensionales. Consolida variables que están correlacionadas entre sí en nuevas variables, combinando linealmente las originales de manera que se conserve la mayor cantidad posible de información observada.
Mediante PCA, es posible extraer la esencia de la información contenida en los datos al agrupar múltiples variables que describen a los individuos. Además de su capacidad para resumir datos, PCA también se utiliza como herramienta visual para comprender las estructuras de los datos. Logra esto al reducir la dimensionalidad de los datos, proyectándolos en un espacio de menor dimensión, como una línea, un plano o un espacio tridimensional. Esta reducción de dimensión facilita la interpretación y el análisis de conjuntos de datos complejos (Rubio 2022).
En el Análisis de Componentes Principales (PCA), seguimos una secuencia de ejes de proyección con un propósito específico. Primero, identificamos el eje de proyección que maximiza la varianza global. Este eje se conoce como el primer componente principal. Su objetivo es capturar la mayor cantidad posible de variabilidad presente en los datos.
Luego, buscamos el segundo eje de proyección que maximiza la varianza, pero bajo la restricción de ser ortogonal al primer componente principal. Este segundo componente principal ayuda a capturar la variabilidad restante que no fue explicada por el primer componente. La ortogonalidad entre las componentes principales garantiza que cada una capture diferentes aspectos de la variabilidad de los datos. Esta representación óptima facilita la interpretación y el análisis de los datos al proporcionar una visión clara de las relaciones entre las variables originales.
Planteamiento del problema
El análisis de componentes principales (PCA) y la técnica de clustering se utilizarán para clasificar y agrupar los distintos equipos (constructores) que han participado en la Fórmula 1 desde su inicio en 1950 hasta el año 2023. Este análisis se basará en una variedad de características relacionadas con el desempeño de los equipos en las carreras de F1. Las características incluirán la cantidad de puntos promedio obtenidos, la posición inicial en parrilla promedio, la posición final promedio, el número de vueltas promedio, la velocidad más alta promedio conseguida en la vuelta más rápida, el promedio de victorias, las paradas en pits promedio y el promedio de abandonos (no finalizaron la carrera).
Variables utilizadas * Cantidad de Puntos Promedio (avg_points): Representa la cantidad media de puntos obtenidos por el equipo en una temporada. * Posición Inicial en Parrilla Promedio (avg_grid): La posición media en la que el equipo comenzó las carreras en una temporada. * Posición Final Promedio (avg_positionOrder): La posición media en la que el equipo terminó las carreras en una temporada. * Número de Vueltas Promedio (avg_laps): La cantidad media de vueltas completadas por el equipo en una carrera. * Velocidad Más Alta Promedio en la Vuelta Más Rápida (avg_fastestlapspeed): La velocidad media más alta alcanzada por el equipo en la vuelta más rápida durante una carrera. * Promedio de Victorias (avg_wins): El número medio de victorias obtenidas por el equipo en una temporada. * Paradas en Pits Promedio (avg_stop): La cantidad media de paradas en pits realizadas por el equipo en una carrera. * Promedio de Abandonos (avg_retirements): La cantidad media de abandonos experimentados por el equipo en una temporada.
El objetivo principal será proporcionar una comprensión más profunda de la evolución y diversidad en el desempeño de los equipos de F1 a lo largo de la historia. Además, busca facilitar análisis comparativos y estratégicos para equipos, aficionados y analistas de la Fórmula 1.
Obtención de los datos
Siguiendo el mismo enfoque utilizado en la sección anterior para llevar a cabo los análisis exploratorios, emplearemos una función para establecer la conexión con la base de datos.
Primero, importamos las bibliotecas necesarias:
A continuación, creamos la función que facilita las conexiones:
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 esta función utiliza una variable de entorno para almacenar 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
.
Como mencionamos previamente, las tablas y sus respectivas columnas que utilizaremos para el desarrollo de este modelo son las siguientes:
- Results: points (determinará si finalizó la carrera o no), grid (posición inicial), milliseconds (duración de la carrera en milisegundos), fastestlapspeed (velocidad más alta alcanzada en la vuelta más rápida) y constructorid (identificador del equipo).
- Races: date (fecha en que se celebró la carrera).
- Drivers: dob (fecha de nacimiento del piloto).
- Pit_stops: stop (número de paradas en boxes).
Luego, ejecutamos la consulta SQL para obtener los datos relevantes:
try:
= connection_db()
connection = connection.cursor()
cursor
cursor.execute("""
SELECT
c.constructorId, c.name,
ROUND(AVG(res.points), 4) AS avg_points, ROUND(AVG(res.grid), 4) AS avg_grid,
ROUND(AVG(res.positionOrder), 4) AS avg_positionOrder, ROUND(AVG(res.laps), 4) AS avg_laps,
ROUND(AVG(res.fastestLapSpeed), 4) AS avg_fastestLapSpeed,
ROUND(AVG(CASE WHEN res.positionOrder = 1 THEN 1 ELSE 0 END), 4) AS avg_wins,
ROUND(AVG(p.stop), 4) AS avg_stops,
ROUND(AVG(CASE WHEN sta.status != 'Finished' THEN 1 ELSE 0 END), 4) AS avg_abandonos
FROM Constructors c
JOIN Results res ON c.constructorId = res.constructorId
LEFT JOIN Pit_Stops p ON res.raceId = p.raceId AND res.driverId = p.driverId
LEFT JOIN Status sta ON res.statusId = sta.statusId
GROUP BY c.constructorId, c.name;
"""
)
= cursor.fetchall()
records = pd.DataFrame(records)
constructor_data
= []
columns for column in cursor.description:
0])
columns.append(column[
= columns
constructor_data.columns
display(constructor_data)except (Exception, Error) as e:
print('Error while executing the query', e)
finally:
if(connection):
cursor.close() connection.close()
constructorid | name | avg_points | avg_grid | avg_positionorder | avg_laps | avg_fastestlapspeed | avg_wins | avg_stops | avg_abandonos | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 157 | Langley | 0.0000 | 31.0000 | 16.0000 | 128.0000 | 140.3620 | 0.0000 | 1.0000 | 1.0000 |
1 | 53 | Toleman | 0.1985 | 9.9771 | 19.7099 | 22.0763 | 217.7816 | 0.0000 | 1.0000 | 0.9237 |
2 | 32 | Team Lotus | 1.1424 | 11.0436 | 13.0563 | 42.3651 | 210.1351 | 0.0517 | 1.0034 | 0.7956 |
3 | 7 | Toyota | 0.9964 | 10.5357 | 11.4500 | 51.2964 | 207.5047 | 0.0000 | 1.0071 | 0.6500 |
4 | 100 | ENB | 0.0000 | 25.0000 | 16.0000 | 14.0000 | 230.0360 | 0.0000 | 1.0000 | 1.0000 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
205 | 142 | Turner | 0.0000 | 23.0000 | 21.0000 | 146.0000 | 123.2220 | 0.0000 | 1.0000 | 1.0000 |
206 | 152 | Aston Butterworth | 0.0000 | 9.0000 | 25.2500 | 4.7500 | 223.6188 | 0.0000 | 1.0000 | 1.0000 |
207 | 41 | Leyton House | 0.1250 | 13.7188 | 17.4531 | 33.3438 | 212.6000 | 0.0000 | 1.0000 | 0.9531 |
208 | 206 | Marussia | 0.0105 | 19.8653 | 17.6882 | 53.5263 | 194.6222 | 0.0000 | 1.7885 | 0.8950 |
209 | 46 | Onyx | 0.1154 | 9.9038 | 23.2115 | 20.7308 | 213.7659 | 0.0000 | 1.0000 | 0.9808 |
210 rows × 10 columns
Con este procedimiento, hemos obtenido los datos necesarios para nuestro análisis.
Componentes principales (PCA)
Convertiremos los datos a numeric, particularmente para manejar datos de tipo int64
, además rellenaremos los datos NA
y trataremos con los valores inf
= constructor_data.apply(pd.to_numeric, errors='ignore')
constructor_data
= constructor_data.fillna(0)
constructor_data = constructor_data.apply(lambda x: x.replace([float('inf'), float('-inf')], 0) if x.dtype.kind in 'biufc' else x) constructor_data
Ahora, normalizaremos las características para asegurar que todas tengan la misma escala y contribución al análisis.
from sklearn.preprocessing import StandardScaler
= StandardScaler()
scaler 2:10] = scaler.fit_transform(constructor_data.iloc[:, 2:10]) constructor_data.iloc[:,
Realicemos ahora el análisis de componentes principales
from sklearn.decomposition import PCA
= PCA(n_components=4)
pca = pca.fit_transform(constructor_data.iloc[:, 2:10]) pca_results
Eigenvalues
Dim.1 Dim.2 Dim.3 Dim.4
Variance 3.266 2.177 0.922 0.778
% of var. 40.635 27.081 11.466 9.679
Cumulative % of var. 40.635 67.716 79.181 88.860
Individuals (the 10 first)
Dist | Dim.1 | Dim.2 | Dim.3 | Dim.4 | ctr | cos2 | ctr | cos2 | ctr | cos2 |
1 |3.708 | 0.259 | 3.606 | -0.346 | -0.746 | 0.070 | 0.973 | -0.093 | -0.201 |
2 |1.147 | -0.856 | -0.707 | 0.243 | 0.150 | -0.747 | -0.617 | 0.212 | 0.131 |
3 |1.576 | 0.940 | -0.740 | 0.728 | -0.723 | 0.596 | -0.469 | 0.462 | -0.459 |
4 |1.069 | 0.909 | -0.318 | 0.122 | -0.448 | 0.850 | -0.297 | 0.114 | -0.419 |
5 |2.045 | -1.110 | 0.021 | -0.992 | -1.402 | -0.543 | 0.010 | -0.485 | -0.686 |
6 |1.058 | 0.648 | -0.477 | 0.282 | -0.626 | 0.613 | -0.451 | 0.266 | -0.592 |
7 |2.517 | -0.445 | 1.736 | 1.094 | 1.388 | -0.177 | 0.690 | 0.435 | 0.551 |
8 |3.126 | -1.879 | -1.479 | 1.026 | 1.732 | -0.601 | -0.473 | 0.328 | 0.554 |
9 |1.726 | 0.341 | 1.416 | 0.797 | 0.473 | 0.198 | 0.820 | 0.461 | 0.274 |
10 |2.035 | -1.052 | -0.468 | -0.857 | -1.442 | -0.517 | -0.230 | -0.421 | -0.709 |
Variables
avg_points | 0.842 | -0.370 | 0.004 | 0.054 |
avg_grid | -0.066 | 0.695 | -0.437 | -0.387 |
avg_positionorder | -0.677 | 0.052 | 0.180 | 0.597 |
avg_laps | 0.515 | 0.831 | 0.137 | 0.090 |
avg_fastestlapspeed | -0.396 | -0.855 | -0.224 | -0.242 |
avg_wins | 0.706 | -0.310 | 0.421 | -0.177 |
avg_stops | 0.562 | -0.181 | -0.672 | 0.402 |
avg_abandonos | -0.926 | 0.059 | -0.010 | -0.097 |
= {
labels str(i): f"PC {i+1} ({var:.1f}%)"
for i, var in enumerate(pca.explained_variance_ratio_ * 100)
}
'color'] = 'Equipos'
labels[
= px.scatter(
fig
pca_results, =0,
x=1,
y=constructor_data["name"],
color=constructor_data["name"],
hover_name={
hover_data'Avg Wins: ': round(constructor_data['avg_wins'], 2),
'Avg Points: ': round(constructor_data['avg_points'], 2),
'Avg Abandonos: ': round(constructor_data['avg_abandonos'], 2),
}
)
fig.update_layout(={'b': 0, 'r': 30, 'l': 30, 't': 30},
margin={'title': 'Primera Dimensión', 'gridcolor': 'rgba(0, 0, 0, 0.0)', 'tickfont': {'color': 'white'}},
xaxis={'title': 'Segunda Dimensión', 'gridcolor': 'rgba(0, 0, 0, 0.0)', 'tickfont': {'color': 'white'}},
yaxis='rgba(0, 0, 0, 0.0)',
plot_bgcolor='rgba(0, 0, 0, 0.0)',
paper_bgcolor="white",
font_color=dict(
hoverlabel="#111"
bgcolor
),= False
showlegend
) fig.show()
Clustericemos ahora estos equipos mediante el método de KMeans
.
from sklearn.cluster import KMeans
= KMeans(n_clusters=4, random_state=123)
kmeans = pd.DataFrame(pca_results, columns=["Dim.1", "Dim.2", "Dim.3", "Dim.4"])
pca_results_df
= kmeans.fit_predict(pca_results_df.iloc[:, :2])
kmeans_result
= pd.DataFrame(pca_results_df.iloc[:, :2], columns=["Dim.1", "Dim.2"])
pca_ind 'constructorName'] = constructor_data['name']
pca_ind['avgWins'] = constructor_data['avg_wins']
pca_ind['avgPoints'] = constructor_data['avg_points']
pca_ind['avgAbandonos'] = constructor_data['avg_abandonos']
pca_ind['cluster'] = kmeans_result
pca_ind[
= px.scatter(
fig ='Dim.1', y='Dim.2', color='cluster', hover_name='constructorName',
pca_ind, x={'avgWins': True, 'avgPoints': True, 'avgAbandonos': True, 'cluster': True},
hover_data={'Dim.1': 'Primera Dimensión', 'Dim.2': 'Segunda Dimensión'},
labels=f"PCA de Constructores de F1",
title='Viridis', size_max=10
color_continuous_scale
)=False)
fig.update(layout_coloraxis_showscale
fig.update_layout(={'b': 0, 'r': 30, 'l': 30, 't': 30},
margin={'title': 'Primera Dimensión', 'gridcolor': 'rgba(0, 0, 0, 0.0)', 'tickfont': {'color': 'white'}},
xaxis={'title': 'Segunda Dimensión', 'gridcolor': 'rgba(0, 0, 0, 0.0)', 'tickfont': {'color': 'white'}},
yaxis='rgba(0, 0, 0, 0.0)',
plot_bgcolor='rgba(0, 0, 0, 0.0)',
paper_bgcolor="white",
font_color=dict(
hoverlabel="#111"
bgcolor
),= False
showlegend
)
for i, feature in enumerate(constructor_data.iloc[:, 2:10]):
fig.add_annotation(=0, ay=0,
ax="x", ayref="y",
axref=variables_contributions[i, 0],
x=variables_contributions[i, 1],
y=True,
showarrow=2,
arrowsize=2,
arrowhead="right",
xanchor="top",
yanchor=dict(color="white", size=10)
font
)
fig.add_annotation(=variables_contributions[i, 0],
x=variables_contributions[i, 1],
y=0, ay=0,
ax="center",
xanchor="bottom",
yanchor=feature,
text=5,
yshift
)
fig.show()
= kmeans.fit_predict(pca_results_df.iloc[:, :3])
kmeans_result
= pd.DataFrame(pca_results_df.iloc[:, :3], columns=["Dim.1", "Dim.2", "Dim.3"])
pca_ind 'constructorName'] = constructor_data['name']
pca_ind['avgWins'] = constructor_data['avg_wins']
pca_ind['avgPoints'] = constructor_data['avg_points']
pca_ind['avgAbandonos'] = constructor_data['avg_abandonos']
pca_ind['cluster'] = kmeans_result
pca_ind[
= px.scatter_3d(
fig ='Dim.1', y='Dim.2', z='Dim.3', color='cluster',
pca_ind, x='constructorName',
hover_name={'avgWins': True, 'avgPoints': True, 'cluster': False},
hover_data={'Dim.1': 'Primera Dimensión', 'Dim.2': 'Segunda Dimensión', 'Dim.3': 'Tercera Dimensión'},
labels=f"3D PCA de Constructores de F1",
title='Viridis', size_max=5
color_continuous_scale
)
=False)
fig.update(layout_coloraxis_showscale
fig.update_layout(={'b': 0, 'r': 30, 'l': 30, 't': 30},
margin={'title': 'Primera Dimensión', 'gridcolor': 'rgba(0, 0, 0, 0.0)', 'tickfont': {'color': 'white'}},
xaxis={'title': 'Segunda Dimensión', 'gridcolor': 'rgba(0, 0, 0, 0.0)', 'tickfont': {'color': 'white'}},
yaxis='rgba(0, 0, 0, 0.0)',
plot_bgcolor='rgba(0, 0, 0, 0.0)',
paper_bgcolor="white",
font_color=dict(
hoverlabel="#111"
bgcolor
),=dict(
scene=dict(backgroundcolor='rgba(0, 0, 0, 0)', gridcolor= '#333'),
xaxis=dict(backgroundcolor='rgba(0, 0, 0, 0)', gridcolor= '#333'),
yaxis=dict(backgroundcolor='rgba(0, 0, 0, 0)', gridcolor= '#333'),
zaxis='rgba(0, 0, 0, 0)',
bgcolor
),= False
showlegend
)
fig.show()