La agrupación, o análisis de conglomerados, es una técnica de aprendizaje automático no supervisado que tiene como objetivo organizar un conjunto de datos en grupos (clústeres) basados en sus características similares. El algoritmo K-Means es uno de los métodos más populares y ampliamente utilizados para esta tarea. Funciona dividiendo los datos en k clústeres, donde k es un número predefinido por el usuario.
En el fútbol, el algoritmo K-Means puede utilizarse para agrupar jugadores basándose en sus estadísticas, como número de pases, goles, recuperaciones, regates, etc. Esto permite identificar patrones y categorizar a los jugadores en grupos como «delanteros eficientes», «mediocampistas creativos» o «defensores sólidos». A continuación, realizaremos la agrupación de los defensores del Campeonato Brasileño de 2024 basándonos en los datos.
El código utilizado en este proyecto es una adaptación del código originalmente publicado por Tommy-las en su repositorio de GitHub. Se realizaron modificaciones y ajustes para adaptar la implementación a las necesidades específicas del análisis en cuestión, manteniendo la estructura base y la lógica principal propuesta por el autor. Agradecemos a Tommy-las por disponibilizar el código de manera abierta, lo que permitió su uso y adaptación para este contexto.
1. Importación de las bibliotecas y clases necesarias
import pandas as pd
import numpy as np
import seaborn as sns
from matplotlib import pyplot as plt
from sklearn.preprocessing import MinMaxScaler
from sklearn.decomposition import PCA
from sklearn.cluster import KMeans
- Pandas: Para manipulación y análisis de datos.
- NumPy: Para operaciones matemáticas y manipulación de arrays.
- Seaborn y Matplotlib: Para visualización de datos.
- MinMaxScaler: Para normalización de los datos.
- PCA: Para reducción de dimensionalidad.
- KMeans: Para realizar el agrupamiento de los datos.
2. Lectura del dataset
players_df = pd.read_csv('br2024FBref.csv', sep=";")
- El dataset se carga desde un archivo CSV (br2024FBref.csv), que contiene estadísticas de jugadores de la liga brasileña 2024.
3. Preprocesamiento y limpieza de los datos
3.1. Filtrado por posición
players_df = players_df[
(players_df["pos"] == "DF") |
(players_df["pos"] == "DF,FW") |
(players_df["pos"] == "FW,DF") |
(players_df["pos"] == "DF,MF") |
(players_df["pos"] == "MF,DF")]
- Filtra los jugadores cuya posición es defensor (DF).
3.2. Selección de métricas relevantes
metrics = ['player', 'Min', 'xG','Interceptions', 'Tackles', 'Total Blocks', 'Aerials Won', 'Take-ons Attempted','Progressive Passes', 'Progressive Carries', 'Crosses into Penalty Area', 'Crosses', 'Long Passes Attempted', 'Key Passes', 'Medium Passes Attempted', 'Passes into Final Third', 'Passes into Penalty Area', 'Progressive Passing Distance', 'xA', 'Through Balls', 'Clearances']
players_df = players_df[metrics]
players_df.columns
- Selecciona solo las columnas (métricas) relevantes para el análisis, como intercepciones, pases progresivos, regates, etc.
3.3. Conversión de columnas a valores numéricos
players_df.iloc[:, 1:] = players_df.iloc[:, 1:].apply(pd.to_numeric, errors='coerce')
- Convierte todas las columnas, excepto la columna player, a valores numéricos. Los valores inválidos se convierten en NaN.
3.4. Filtrado por minutos jugados
players_df = players_df[players_df['Min'] > 1000]
players_df.reset_index(inplace=True, drop=True)
- Elimina jugadores que jugaron menos de 1000 minutos, para garantizar que los datos sean representativos.
3.5. Eliminación de filas con muchos valores ausentes
threshold = int(0.7 * len(players_df.columns))
# Drop rows that do not meet the threshold
players_df = players_df.dropna(thresh=threshold)
players_df.reset_index(inplace=True, drop=True)
- Elimina filas donde más del 30% de los valores están ausentes (NaN).
4. Ingeniería de características
4.1. Normalización de los datos a escala por 90 minutos
players_90 = players_df.copy(deep=True)
# Create a new column for the matches played, which is the player's total minutes divided by 90
players_90["Min_per_90"] = players_90["Min"] / 90
players_90[["player", "Min", "Min_per_90"]].head()
# Perform the division for columns starting from the third column
players_90.iloc[:, 2:] = players_90.iloc[:,2:].div(players_90["Min_per_90"], axis=0)
- Crea una nueva columna Min_per_90 para calcular el número de partidos jugados (minutos totales divididos por 90).
- Normaliza las métricas a una escala de 90 minutos.
4.2. Cálculo de percentiles
def calculate_percentiles(df, ignore_columns=[]):
percentiles_df = df.rank(pct=True)
for col in ignore_columns:
percentiles_df[col] = df[col]
return percentiles_df
ignore_columns = ['player']
percentiles_df = calculate_percentiles(players_90, ignore_columns)
percentiles_df
- Calcula los percentiles para cada métrica, excepto para la columna player.
4.3. Eliminación de columnas innecesarias
players_90.drop(columns=["Min_per_90", "Min"], inplace=True)
- Elimina las columnas Min_per_90 y Min, que ya no son necesarias.
5. Reducción de dimensionalidad con PCA
minmax_scaler = MinMaxScaler()
# Exclude the player column
norm_X = players_df.iloc[:, 1:]
# Perform the normalization
norm_X = minmax_scaler.fit_transform(norm_X.to_numpy())
norm_X = np.nan_to_num(norm_X)
# For KMeans algorithm, we want to reduce the numbers of components for an easier cluster using Principal Component Analysis (PCA)
pca = PCA(n_components=2)
reduced = pca.fit_transform(norm_X)
reduced = pd.DataFrame(reduced)
- Normaliza los datos utilizando MinMaxScaler.
- Reduce la dimensionalidad de los datos a 2 componentes principales utilizando PCA
6. Determinación del número ideal de clusters (Método del Codo)
wcss = []
for i in range(1,11):
kmeans = KMeans(n_clusters = i, init = 'k-means++', random_state=42)
kmeans.fit(reduced)
wcss.append(kmeans.inertia_)
plt.plot(range(1,11), wcss)
plt.xlabel("# Clusters")
plt.ylabel("WCSS")
plt.show()
● Calcula la suma de cuadrados intra-cluster (WCSS) para diferentes números de clusters.
● Grafica el método del codo para determinar el número ideal de clusters.
7. Entrenamiento del modelo KMeans
kmeans = KMeans(n_clusters=4)
kmeans = kmeans.fit(reduced)
labels = kmeans.predict(reduced)
clusters = kmeans.labels_.tolist()
- Entrena el modelo KMeans con 4 clusters.
- Asigna cada jugador a un cluster.
8. Visualización de los clusters
reduced['cluster'] = clusters
reduced['player'] = players
reduced.columns = ['x', 'y', 'cluster', 'player']
ax = sns.lmplot(x="x", y="y", hue='cluster', data=reduced, legend=True,
fit_reg=False, height=10, scatter_kws={"s": 40})
texts = []
- Combina los datos reducidos con los nombres de los jugadores y los clusters.
- Grafica los clusters en un gráfico 2D, donde cada punto representa un jugador.
9. Interpretación de los clusters
- Clúster 0: Defensores buenos en el juego aéreo que conducen poco el balón, como Maicon y Rodrigo Battaglia, destacan en métricas de bloqueos, duelos aéreos y pases largos.
- Clúster 1: Defensores y laterales con mayor conducción de balón, como Léo Ortiz y Santi Arias, destacan en métricas de conducciones progresivas, pases progresivos y pases al tercio final.
- Clúster 2: Defensores y laterales con mayor combatividad defensiva, como Guillermo Varela y Vitor Reis, destacan en métricas de recuperaciones, intercepciones y despejes.
- Clúster 3: Laterales muy ofensivos, como Gustavo Scarpa y Wesley França, destacan en métricas de centros, asistencias esperadas (xA), pases clave y pases al área pequeña.
Los archivos utilizados en este artículo pueden descargarse en el siguiente enlace: