本文目录导读:

分析球员跑动热点图(Positional Heatmap)是体育数据分析中的经典任务,下面从开源项目角度,介绍一套完整的技术方案,涵盖数据来源、处理流程、可视化到进阶分析。
整体技术栈概览
原始数据 → 坐标转换 → 密度估计 → 可视化 → 战术洞察
(追踪/事件) (标准化) (KDE/网格) (热力图) (聚类/指标)
常用开源工具:
| 环节 | 工具/库 |
|---|---|
| 数据处理 | pandas, numpy, polars |
| 空间计算 | scipy, shapely, geopandas |
| 密度估计 | scipy.stats.gaussian_kde, sklearn |
| 可视化 | matplotlib, seaborn, plotly, mplsoccer |
| 足球专用 | mplsoccer, soccerdata, kloppy |
| 追踪数据 | StatsBomb Open Data, Metrica Sports |
数据来源
事件数据(Event Data)
- StatsBomb Open Data:免费,含球员每次触球坐标(x, y)
- FBref / Understat:聚合统计数据
追踪数据(Tracking Data)
- Metrica Sports:开源样本数据,含每帧 25fps 的 22 名球员坐标
- SkillCorner:部分公开
数据结构示例(Metrica)
frame,player_id,x,y 1,5,0.52,0.31 1,6,0.48,0.29 ...
坐标通常归一化为 [0,1],需转换为标准球场尺寸(105×68 米)。
核心分析流程
步骤 1:坐标标准化
import numpy as np
def normalize_to_pitch(x, y, pitch_length=105, pitch_width=68):
"""将归一化坐标转为米制"""
return x * pitch_length, y * pitch_width
def mirror_if_needed(x, y, team_direction):
"""统一进攻方向(都向右攻)"""
if team_direction == 'left':
x = 1 - x
y = 1 - y
return x, y
步骤 2:密度估计(热力图核心)
方法 A:高斯核密度估计(KDE) — 最常用
from scipy.stats import gaussian_kde
import numpy as np
def compute_kde(x_coords, y_coords, grid_size=100):
positions = np.vstack([x_coords, y_coords])
kde = gaussian_kde(positions, bw_method=0.15)
# 生成球场网格
xgrid = np.linspace(0, 105, grid_size)
ygrid = np.linspace(0, 68, grid_size)
X, Y = np.meshgrid(xgrid, ygrid)
Z = kde(np.vstack([X.ravel(), Y.ravel()])).reshape(X.shape)
return X, Y, Z
方法 B:二维直方图(网格法) — 计算快
def grid_density(x, y, bins=(21, 14)):
H, _, _ = np.histogram2d(x, y, bins=bins, range=[[0,105],[0,68]])
return H
方法 C:Binned Statistic — 适合按时间/事件加权
from scipy.stats import binned_statistic_2d
stat, _, _, _ = binned_statistic_2d(x, y, values=speed,
statistic='mean',
bins=[21, 14],
range=[[0,105],[0,68]])
步骤 3:可视化(mplsoccer 推荐)
from mplsoccer import Pitch
import matplotlib.pyplot as plt
pitch = Pitch(pitch_type='statsbomb', line_zorder=2)
fig, ax = pitch.draw(figsize=(12, 8))
# 绘制热力图
pitch.kdeplot(x_coords, y_coords, ax=ax,
cmap='hot', fill=True, levels=100, alpha=0.7)
plt.savefig('heatmap.png', dpi=150)
进阶:网格热图
pitch = Pitch(pitch_type='statsbomb') fig, ax = pitch.draw() bs = pitch.bin_statistic(x, y, statistic='count', bins=(12, 8)) pitch.heatmap(bs, ax=ax, cmap='Reds', edgecolors='#22312b')
步骤 4:进阶分析
(1) 按比赛阶段分时段热图
# 上半场 vs 下半场 mask_1h = time <= 45 ax1 = pitch.kdeplot(x[mask_1h], y[mask_1h]) ax2 = pitch.kdeplot(x[~mask_1h], y[~mask_1h])
(2) 位置聚类(球员角色识别)
from sklearn.cluster import KMeans # 将每次触球位置聚类,识别球员活动区域 kmeans = KMeans(n_clusters=4).fit(np.column_stack([x, y])) # 质心可解释为:防守区、中场、进攻三区、边路等
(3) 与球队平均热图对比
diff = kde_player - kde_team_avg # 正值 = 球员比队友更常出现的区域 → 揭示个人职责
(4) 输出量化指标
- 活动范围(Convex Hull 面积)
- 平均位置(质心)
- 垂直/水平分散度(标准差)
- 进攻三区触球占比
- 左右路偏好
from scipy.spatial import ConvexHull hull = ConvexHull(np.column_stack([x, y])) area = hull.volume # 2D 中 volume 即面积
推荐开源项目参考
| 项目 | 用途 |
|---|---|
| mplsoccer | 足球专用绘图(热图、传球图、雷达图) |
| soccerdata | 抓取 FBref/Understat 数据 |
| kloppy | 标准化不同供应商的追踪/事件数据 |
| football-data | 赛程与结果数据 |
| socceraction | 事件数据 → 动作价值(VAEP) |
| StatsBomb Open Data | 免费高质量事件数据 |
完整最小示例(StatsBomb 数据)
from statsbombpy import sb from mplsoccer import Pitch import matplotlib.pyplot as plt # 1. 拉取梅西在某场比赛的事件 events = sb.events(match_id=3788741) messi = events[events.player == 'Lionel Messi'] # 2. 提取坐标 x, y = messi.location.apply(lambda p: p[0]), messi.location.apply(lambda p: p[1]) # 3. 绘制 pitch = Pitch(pitch_type='statsbomb') fig, ax = pitch.draw() pitch.kdeplot(x, y, ax=ax, cmap='hot', fill=True, levels=50) plt.show()
注意事项与避坑
- 样本量:KDE 在触球点 < 30 时不稳定,可改用网格或增大 bandwidth
- 坐标方向:不同数据源 y 轴方向不同(有的从上到下),需统一
- 进攻方向:主客队需镜像到同方向才能对比
- 时间维度:整场热图会掩盖换位、替补影响,建议按 15 分钟分段
- 带宽选择:
bw_method过小→噪点,过大→过度平滑,建议 0.1~0.3 网格搜索 - 归一化:展示时通常按球员自己总时间归一化,便于横向比较
如果你有具体的数据源(StatsBomb / Metrica / 自采 GPS)或想实现某个特定分析(对比两名边锋的跑位差异”),可以告诉我,我可以给出针对性的代码。