DEVFUT

Cómo Crear un Shot Map de Fútbol con Python: Salah en Premier 24/25

Mapa de calor que visualiza la distribución de los tiros a portería del jugador Mohamed Salah durante la temporada 2024/25 de la Premier League. El gráfico muestra un campo de fútbol con hexágonos de diferentes tonalidades de verde, indicando la frecuencia de los disparos desde distintas zonas del campo. Las áreas más oscuras representan una mayor concentración de tiros. Se observa una tendencia a disparar desde la banda derecha entrando al área y desde la frontal del área.

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:

Mapa de calor que detalla la ubicación de los 121 tiros a portería realizados por Mohamed Salah en la Premier League durante la temporada 2024/25. Los hexágonos sombreados en diferentes tonos de verde representan la frecuencia de sus disparos desde diversas zonas del campo. Las áreas más oscuras indican una mayor concentración de tiros, notablemente dentro del área por el lado derecho y en la frontal. Se destaca que Salah anotó 28 goles con un valor de Expected Goals (xG) de 24.14, promediando 0.20 xG por tiro. La distancia media de sus tiros fue de 12.0 metros. La imagen incluye el nombre del jugador, su fotografía, el logo del Liverpool FC y el título "Goleador de la Premier League 24/25". El mapa de tiros proporciona una visión estratégica de las tendencias de disparo del máximo goleador de la liga.

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!

Publicar un comentario

0 Comentarios