Te mostraré cómo visualicé los disparos de Mohamed Salah en la temporada 2024/2025 de la Premier League utilizando Python y Google Colab. A través de gráficos hexagonales y análisis de métricas clave como xG (expected goals) y número total de tiros, construiremos un shot map que nos permite entender mejor desde dónde Salah genera más peligro.
Este tipo de visualizaciones no solo son atractivas, sino que también son una herramienta poderosa para el análisis de rendimiento en fútbol. Verás cómo, con unas pocas librerías de Python como matplotlib, seaborn y pandas, puedes transformar datos crudos en una historia visual clara y útil tanto para fans como para analistas.
🧠 ¿Quieres explorar el código completo? Aquí tienes el Google Colab:
🔗 https://colab.research.google.com/drive/1c9FDYrVlwWrql0sHRfB3_RTEo0cifkQA?usp=sharing
Paso 1: Importación de librerías
# importaciones
import requests
import time
import pandas as pd
import matplotlib as mpl
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import matplotlib.patheffects as path_effects
import matplotlib.font_manager as fm
import matplotlib.colors as mcolors
import urllib
import numpy as np
from mplsoccer import VerticalPitch, FontManager
from highlight_text import fig_text, ax_text
from PIL import Image
from matplotlib import cm
from matplotlib.patches import RegularPolygon
font_title = FontManager('https://github.com/google/fonts/blob/main/ofl/bungeeinline/BungeeInline-Regular.ttf?raw=true')
Paso 2: Crear un colormap personalizado
En este paso definimos una paleta de colores personalizada para usar en nuestros gráficos de tiros. Usamos la función LinearSegmentedColormap
de matplotlib.colors
para construir un degradado que irá desde tonos grisáceos hasta verdes azulados.
# colormap personalizado en Matplotlib
colors = [
'#d0d6d4',
'#c5d0cd',
'#bbcac7',
'#b0c3c1',
'#a6bdbb',
'#9bb7b5',
'#91b1af',
'#86aaa8',
'#7ca4a2',
'#719e9c',
'#679896',
'#5c9190',
'#528b8a',
'#478583',
'#3d7f7d',
'#327877',
'#287271',
]
soc_cm = mcolors.LinearSegmentedColormap.from_list('SOC', colors, N=50)
mpl.colormaps.register(soc_cm)
Paso 3: Funciones auxiliares para obtener los datos y dibujar el gráfico
En esta sección definimos dos funciones importantes: función para obtener datos directamente desde la API de FotMob y una función que devuelve las coordenadas X e Y para dibujar un semicírculo inferior, útil para marcar zonas como el punto medio de tiros.
def fotmob_request(path):
# Headers para FotMob
headers = {
*ESTA PARTE LA ENCONTRARÁN EN EL ARCHIVO*
}
try:
*ESTA PARTE LA ENCONTRARÁN EN EL ARCHIVO*
except requests.exceptions.ConnectionError:
raise ConnectionError("Unable to connect to the session cookie server.")
token = r.json()
headers_with_token = headers | token
response = requests.get(f'https://www.fotmob.com/api/{path}', headers=headers_with_token)
time.sleep(3)
return response
def semicircle(r, h, k):
x0 = h - r
x1 = h + r
x = np.linspace(x0, x1, 10000)
y = k - np.sqrt(r**2 - (x - h)**2)
return x, y
Paso 4: Obtener el ID del jugador desde FotMob
Para consultar las estadísticas avanzadas de un jugador, necesitamos su ID, el cual se encuentra en la URL del perfil del jugador en FotMob. Por ejemplo, si visitamos: https://www.fotmob.com/es/players/292462/mohamed-salah
El número 292462 es el ID del jugador Mohamed Salah. A partir de este ID, podemos construir la ruta para hacer la solicitud a la API no oficial de FotMob. Aquí te muestro cómo hacerlo en Python:
id = int(input('Ingresa el ID del jugador: '))
path = f'playerData?id={id}'
response = fotmob_request(path)
shotmap = response.json()
shotmap = shotmap['firstSeasonStats']['shotmap']
data = pd.DataFrame(shotmap)
data_shotmap = data[['eventType', 'playerName', 'x', 'y', 'expectedGoals', 'teamId', 'teamColor', 'teamColorDark', 'teamId']]
# guardar el nombre del jugador
player_name = data_shotmap['playerName'][0]
Paso 5: Visualizar el mapa de tiros con Hexbins
Una vez que tenemos los datos de los tiros del jugador, el siguiente paso es representarlos visualmente. Para ello, vamos a crear un gráfico de tipo hexbin, el cual agrupa los tiros en celdas hexagonales y permite visualizar con mayor claridad las zonas desde donde más dispara el jugador.
def plot_hexbin_shot(ax, data):
# dibujo del campo
pitch = VerticalPitch(
pitch_type='custom',
half=True,
goal_type='box',
linewidth=1.25,
line_color='black',
pad_bottom=-8,
pad_top=10,
pitch_length=105,
pitch_width=68
)
pitch.draw(ax = ax)
bins = pitch.hexbin(x=data['x'], y=data['y'], ax=ax, cmap='SOC', gridsize=(14,14), zorder=-1, edgecolors='#efe9e6', alpha=0.9, lw=.25)
# llamamos función semicircle
x_circle, y_circle = semicircle(104.8 - data['x'].median(), 34, 104.8)
ax.plot(x_circle, y_circle, ls='--', color='red', lw=.75)
annot_x = [54 - x*14 for x in range(0,4)]
annot_texts = ['Goles', 'xG', 'Tiros', 'xG/tiro']
annot_stats = [data[data['eventType'] == 'Goal'].shape[0], round(data.expectedGoals.sum(), 2), data.shape[0], round(data['expectedGoals'].sum()/data.shape[0],2)]
# stats
for x,s,stat in zip(annot_x, annot_texts, annot_stats):
hex_annotation = RegularPolygon((x, 70), numVertices=6, radius=4.5, edgecolor='black', fc='None', hatch='.........', lw=1.25)
ax.add_patch(hex_annotation)
ax.annotate(
xy=(x,70),
text=s,
xytext=(0,-14),
textcoords='offset points',
size=7,
ha='center',
va='center',
fontproperties=font_title.prop
)
if isinstance(stat, int):
text_stat = f'{stat:.0f}'
else:
text_stat = f'{stat:.2f}'
text_ = ax.annotate(
xy=(x,70),
text=text_stat,
xytext=(0,0),
textcoords='offset points',
size=8,
ha='center',
va='center',
weight='bold',
fontproperties=font_title.prop
)
text_.set_path_effects(
[path_effects.Stroke(linewidth=1.5, foreground='#efe9e6'), path_effects.Normal()]
)
# Dibujamos las anotaciones
median_annotation = ax.annotate(
xy=(34,110),
xytext=(x_circle[-1], 110),
text=f"{((105 - data['x'].median())*18)/16.5:.1f} m.",
size=7,
color='red',
ha='right',
va='center',
arrowprops=dict(arrowstyle=*COMPLETO EN EL ARCHIVO*, head_width=0.35, head_length=0.65',
color='red',
fc='#efe9e6',
lw=0.75)
)
ax.annotate(
xy=(34,110),
xytext=(4,0),
text=f"Distancia mediana de tiros",
textcoords='offset points',
size=7,
color='red',
ha='left',
va='center',
alpha=0.5,
fontproperties=font_title.prop
)
ax.annotate(
xy=(34,115),
text=f"{data['playerName'].iloc[0].upper()}",
size=17.5,
color='black',
ha='center',
va='center',
weight='bold',
fontproperties=font_title.prop
)
ax_size = 0.1
fig, ax = plt.subplots(figsize=(6, 8), dpi=300)
fig.set_facecolor('white')
plt.rcParams['hatch.linewidth'] = .02
# Aquí colocamos el DataFrame de tiros
plot_hexbin_shot(ax, data_shotmap)
plt.tight_layout()
# Transformaciones para insertar imagen en coordenadas del pitch
DC_to_FC = ax.transData.transform
FC_to_NFC = fig.transFigure.inverted().transform
DC_to_NFC = lambda x: FC_to_NFC(DC_to_FC(x))
# --- Añadir logo del equipo
# reemplaza con el ID real del equipo
team_id = 8650
ax_coords = DC_to_NFC((8, 101))
ax_size = 0.07
image_ax = fig.add_axes([ax_coords[0], ax_coords[1], ax_size, ax_size], fc='None')
fotmob_url = f'https://images.fotmob.com/image_resources/logo/teamlogo/{team_id}.png'
club_icon = Image.open(urllib.request.urlopen(fotmob_url))
image_ax.imshow(club_icon)
image_ax.axis('off')
# --- Añadir imagen del jugador
# Reemplaza con el ID real del jugador
player_id = 292462
ax_coords = DC_to_NFC((14, 101))
ax_size = 0.08
image_ax = fig.add_axes([ax_coords[0], ax_coords[1], ax_size, ax_size], fc='None')
fotmob_url = f'https://images.fotmob.com/image_resources/playerimages/{player_id}.png'
player_icon = Image.open(urllib.request.urlopen(fotmob_url))
image_ax.imshow(player_icon)
image_ax.axis('off')
#Título
fig_text(
x = 0.52, y = .84,
s = "Goleador de la Premier League 24/25",
va = "bottom", ha = "center",
fontsize = 15, color = "black", weight = "bold", fontproperties=font_title.prop
)
# Subtítulo
fig_text(
x = 0.5, y = .81,
s = f"Tiros de {player_name} en la Premier League 24/25 \n Inspirado por @sonofacorner | Data: FotMob",
# highlight_textprops=[{"weight": "bold", "color": "black"}],
va = "bottom", ha = "center",
fontsize = 5, color = "#4E616C", fontproperties=font_title.prop
)
#Creador
fig_text(
x = 0.84, y = 0.22,
s = "@nicolee.palomino",
va = "bottom", ha = "center",
fontsize = 6, color = "#4E616C", fontproperties=font_title.prop
)
plt.show()
Paso 6: Resultado final — Visualización del mapa de tiros
Después de ejecutar la función plot_hexbin_shot con los datos del jugador, obtenemos un gráfico como el siguiente:
📸 Resultado:
Este gráfico representa el mapa de tiros del jugador en la mitad ofensiva del campo. Cada hexágono indica la densidad de disparos en esa zona (más oscuro, más disparos). También incluye:
- ⚽ Goles anotados
- 📊 xG acumulado
- 🎯 Total de tiros
- 💥 xG / tiro
- 🔴 Distancia mediana de disparo (línea punteada roja)
Es una excelente forma de analizar visualmente el comportamiento ofensivo de cualquier futbolista.
🧠 ¡Y listo! Ahora puedes usar este mismo flujo para analizar a cualquier jugador de FotMob. Solo necesitas cambiar el ID del jugador en la URL y seguir el mismo procedimiento.
🤝 ¿Te gustó este análisis? ¡Conectemos!
Me apasiona el análisis de datos aplicado al fútbol y otros proyectos con Python. Si te interesa este tipo de contenido, puedes seguirme en mis redes sociales para más proyectos, tutoriales y herramientas:
🎯 Facebook: https://www.facebook.com/developfutbol
🐦 Twitter: https://x.com/aless_palomino
📷 Instagram: https://www.instagram.com/nicolee.palomino/
💼 LinkedIn: https://www.linkedin.com/in/nicole-palomino-alvarado/
🎯 Github: https://github.com/Nicole-Palomino
📬 También puedes dejarme tus dudas o sugerencias en los comentarios. ¡Gracias por leer!
0 Comentarios