python案例如何分析球员跑动热点图?

wen python案例 5

用Python分析球员跑动热点图案例

热点图(Heatmap)是体育数据分析中最直观的可视化方式之一,下面我给出一个完整的实战案例,从数据获取、坐标处理到可视化,覆盖核心流程。

python案例如何分析球员跑动热点图?


分析思路总览

原始追踪数据 → 坐标标准化 → 网格化统计 → 核密度估计(KDE) → 可视化 → 战术解读

关键点:

  1. 坐标系统一(不同厂商数据坐标不同)
  2. 区分"出现在该位置"和"跑动密度"(两种热点图含义不同)
  3. 按时间/比赛阶段切分(上半场 vs 下半场热量差异)
  4. 叠加球场底图才能读图

完整代码案例

准备数据

真实数据通常来自 StatsBomb / Opta / Catapult / 自建GPS,这里用模拟数据演示(结构一致):

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, Rectangle, Arc
from scipy.stats import gaussian_kde
# 模拟:某球员90分钟内每0.1秒的坐标(真实数据来自追踪系统)
np.random.seed(42)
def simulate_player_track(n=54000, style="winger"):
    """模拟边锋跑动轨迹:偏左路,前场多"""
    t = np.linspace(0, 90, n)
    # 基础位置:左边路
    base_x = 30 + 20 * np.sin(t / 15)          # 纵向摆动
    base_y = 15 + 5 * np.sin(t / 8)            # 靠左路(y小=左)
    # 加入冲刺突刺(模拟前插)
    sprints = np.random.randn(n) * 3
    x = np.clip(base_x + sprints, 0, 120)
    y = np.clip(base_y + np.random.randn(n) * 2, 0, 80)
    return pd.DataFrame({"x": x, "y": y, "minute": t})
df = simulate_player_track()
print(df.head())

真实场景替换:

  • StatsBomb:events.json 中的 location 字段 → 只需 [x, y]
  • Catapult GPS:CSV 中 Longitude/Latitude 或 x/y 字段
  • Opta:x, y 范围为 0-100,需缩放

绘制标准球场底图

def draw_pitch(ax, pitch_color="#2d5a3d", line_color="white"):
    """绘制标准 105x68 米球场 (FIFA标准)"""
    ax.set_facecolor(pitch_color)
    # 边线
    ax.add_patch(Rectangle((0, 0), 105, 68, fill=False,
                           edgecolor=line_color, linewidth=2))
    # 中线
    ax.plot([52.5, 52.5], [0, 68], color=line_color, lw=2)
    # 中圈
    ax.add_patch(Circle((52.5, 34), 9.15, fill=False,
                        edgecolor=line_color, lw=2))
    # 禁区 (左)
    ax.add_patch(Rectangle((0, 13.85), 16.5, 40.3, fill=False,
                           edgecolor=line_color, lw=2))
    ax.add_patch(Rectangle((0, 24.85), 5.5, 18.3, fill=False,
                           edgecolor=line_color, lw=2))
    # 禁区 (右)
    ax.add_patch(Rectangle((105-16.5, 13.85), 16.5, 40.3, fill=False,
                           edgecolor=line_color, lw=2))
    ax.add_patch(Rectangle((105-5.5, 24.85), 5.5, 18.3, fill=False,
                           edgecolor=line_color, lw=2))
    # 角球弧
    for (x, y, a) in [(0,0,0),(105,0,0),(0,68,0),(105,68,0)]:
        ax.add_patch(Arc((x, y), 2, 2, theta1=0, theta2=90,
                         edgecolor=line_color, lw=2))
    ax.set_xlim(0, 105); ax.set_ylim(0, 68)
    ax.set_aspect('equal'); ax.axis('off')

方法A:网格频率热点图(最简单,易解读)

def grid_heatmap(df, ax, bin_size=5):
    """按5m×5m网格统计出现次数"""
    H, xedges, yedges = np.histogram2d(
        df.x, df.y,
        bins=[np.arange(0, 105+bin_size, bin_size),
              np.arange(0, 68+bin_size, bin_size)]
    )
    im = ax.imshow(H.T, extent=[0, 105, 0, 68],
                   origin='lower', cmap='hot', alpha=0.75,
                   interpolation='bicubic')
    plt.colorbar(im, ax=ax, label='出现次数')

优点:客观量化,适合"停留时长统计"。
缺点:不连续,分辨率依赖网格大小。


方法B:核密度估计 KDE 热点图(最常用,平滑美观)

def kde_heatmap(df, ax, bw=1.5):
    """高斯核密度估计:反映跑动密度"""
    k = gaussian_kde(np.vstack([df.x, df.y]))
    k.set_bandwidth(bw_method=bw / np.std(df[['x','y']].values, axis=0))
    xi, yi = np.mgrid[0:105:200j, 0:68:140j]
    zi = k(np.vstack([xi.flatten(), yi.flatten()]))
    im = ax.contourf(xi, yi, zi.reshape(xi.shape),
                     levels=15, cmap='hot', alpha=0.75)
    ax.contour(xi, yi, zi.reshape(xi.shape),
               levels=15, colors='white', linewidths=0.3, alpha=0.4)
    plt.colorbar(im, ax=ax, label='密度')

参数 bw(bandwidth) 是关键:

  • 太小 → 过于碎片化,看不清整体
  • 太大 → 都糊成一坨,失去细节
  • 一般取 0~2.0 米 起步

组合成完整分析图

def plot_player_heatmap(df, player_name="球员 #10"):
    fig, axes = plt.subplots(1, 2, figsize=(16, 7))
    fig.suptitle(f"{player_name} 跑动热点分析", fontsize=15, weight='bold')
    # 左图:KDE密度
    draw_pitch(axes[0])
    kde_heatmap(df, axes[0])
    axes[0].set_title("核密度图(跑动密度)")
    # 右图:网格频率
    draw_pitch(axes[1])
    grid_heatmap(df, axes[1], bin_size=5)
    axes[1].set_title("网格分布图(停留频次)")
    plt.tight_layout()
    plt.savefig("heatmap.png", dpi=150, bbox_inches='tight')
    plt.show()
plot_player_heatmap(df, "案例球员-左边锋")

进阶分析:让热点图"能讲故事"

单纯密度图只是热身,实战中,热点图通常叠加多层信息:

按比赛阶段切分

# 上下半场对比
first_half = df[df.minute <= 45]
second_half = df[df.minute > 45]
fig, axes = plt.subplots(1, 2, figsize=(16, 7))
for ax, sub, title in zip(axes, [first_half, second_half],
                          ["上半场", "下半场"]):
    draw_pitch(ax)
    kde_heatmap(sub, ax)
    ax.set_title(title)

怎么看?

  • 半场后热点收缩到中场 → 体能下降 / 战术回收
  • 禁区热点增加 → 下半场加强进攻

叠加事件点(传球、射门、抢断)

# 假设已有事件点列表
shots = pd.DataFrame({'x': [95, 89, 100], 'y': [34, 28, 45]})
draw_pitch(ax)
kde_heatmap(df, ax)
ax.scatter(shots.x, shots.y, c='cyan', marker='*',
           s=200, edgecolor='black', label='射门')
ax.legend()

这样一眼能看出:他的热点是否在创造射门机会的区域。

区分"停留"和"跑动"

# 计算速度 → 只保留高速奔跑的点(>5.5 m/s)
df['vx'] = df.x.diff() / (df.minute.diff() * 60 / len(df))  # 简化
df['speed'] = np.sqrt(df.x.diff()**2 + df.y.diff()**2) * 10  # 近似 m/s
sprint_df = df[df.speed > 5.5]

停留热图 vs 冲刺热图 是两个完全不同的故事:

  • 停留热图:反映战术站位
  • 冲刺热图:反映爆发区域(更接近"威胁"信号)

数据源推荐(实战)

数据源 类型 获取方式 特点
StatsBomb Open 事件数据 statsbombpy 库 免费,含坐标
Opta/Stats Perform 事件+追踪 商业 精度高
Catapult/SSTATSports GPS追踪 俱乐部采购 每秒多帧
FIFA训练数据 追踪 Kaggle有售 学习友好
Wyscout/Football API 事件 订阅 覆盖广

快速拉 StatsBomb 数据示例:

from statsbombpy import sb
events = sb.events(match_id=3788741)  # 欧冠某场
passes = events[events.type == 'Pass']
x = passes.location.apply(lambda p: p[0])  # 事件坐标
y = passes.location.apply(lambda p: p[1])

常见坑 & 调优建议

问题 解决方案
坐标系统不统一 统一缩放到 FIFA 105×68 米;注意 y 轴方向(有的数据以球场上方为 y=0)
采样频率差异大 GPS 10Hz vs 事件触发式;KDE 前应重采样为等时间间隔
替补球员数据 按上场时间归一化,避免"少上场=热点稀"
颜色误导 用 hot 或 YlOrRd,不用 jet(感知不均)
门将专属 单独缩放 y 轴范围内做 KDE,否则热点被压扁
多球员对比 用相同色阶归一化(vmin/vmax 固定),否则不可比

战术解读速查表

热点形态 解读
集中在边路+后场 边后卫/防守型边翼
中圈为中心大团 后腰/组织核心
两个禁区都有 全能中场(Box-to-Box)
禁区前沿热点 影锋/前腰
热图很"瘦"纵向延伸 边锋/边卫
热点集中在禁区中央 中锋/抢点型前锋

一句话总结

Python 分析热点图 = pandas 处理坐标 + 绘图底图 + scipy.stats.gaussian_kde 或 np.histogram2d;真正的价值不在于图好不好看,而在于用切分维度(时间/速度/事件)让热图回答具体战术问题。

需要我把上面代码整合成一个可直接运行的完整脚本,或者针对某个数据源(StatsBomb / Opta / GPS CSV)写出具体读取方案吗?

上一篇根据python案例,强队翻车规律可循吗?

下一篇当前分类已是最新一篇

抱歉,评论功能暂时关闭!