Python 分析球员跑动热点图完整案例
下面我用一个完整的案例,带你从数据准备 → 数据清洗 → 坐标转换 → 热点图生成 → 深度分析走一遍流程。

核心思路
足球/篮球球员跑动热点图本质是 二维空间上的点密度分布,分析流程:
原始追踪数据 → 坐标归一化 → 密度估计(KDE/直方图) → 可视化 → 战术解读
关键库:
pandas/numpy:数据处理matplotlib/seaborn:可视化scipy.stats.gaussian_kde或sklearn.KernelDensity:核密度估计mplsoccer:足球专用绘图(强烈推荐)
数据准备
假设你有一份球员追踪数据(通常来自 StatsBomb、Metrica、Sportec 或自研 GPS):
player_id,timestamp,x,y 1,0.0,45.2,30.1 1,0.04,45.8,30.5 1,0.08,46.1,31.0 ...
坐标一般有两种:
- 米制:0~105 × 0~68(全场)
- 归一化:0~1
完整代码案例
数据加载与清洗
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import gaussian_kde
import matplotlib.patches as patches
# 读取数据
df = pd.read_csv("player_tracking.csv")
# 筛选目标球员
player_df = df[df["player_id"] == 1].copy()
# 剔除异常值(超出场地的坐标)
pitch_length, pitch_width = 105, 68
player_df = player_df[
(player_df["x"] >= 0) & (player_df["x"] <= pitch_length) &
(player_df["y"] >= 0) & (player_df["y"] <= pitch_width)
]
print(f"有效点数: {len(player_df)}")
绘制球场底图(自定义函数)
def draw_pitch(ax, length=105, width=68):
"""画一个标准足球场底图"""
ax.set_xlim(0, length)
ax.set_ylim(0, width)
ax.set_facecolor("#3a8c3a") # 草地绿
ax.add_patch(patches.Rectangle((0, 0), length, width,
edgecolor="white", facecolor="none"))
# 中线
ax.plot([length/2, length/2], [0, width], color="white")
# 中圈
ax.add_patch(patches.Circle((length/2, width/2), 9.15,
edgecolor="white", facecolor="none"))
# 禁区(左右)
ax.add_patch(patches.Rectangle((0, width/2-20.15), 16.5, 40.3,
edgecolor="white", facecolor="none"))
ax.add_patch(patches.Rectangle((length-16.5, width/2-20.15), 16.5, 40.3,
edgecolor="white", facecolor="none"))
ax.set_xticks([]); ax.set_yticks([])
方法一:直方图热力图(简单直观)
fig, ax = plt.subplots(figsize=(12, 8))
draw_pitch(ax)
# 2D 直方图
h = ax.hist2d(player_df["x"], player_df["y"],
bins=(30, 20), cmap="hot_r", alpha=0.7)
plt.colorbar(h[3], ax=ax, label="停留频次")
ax.set_title("球员跑动热点图(直方图)")
plt.show()
方法二:高斯核密度(KDE)— 更平滑、更专业
from scipy.stats import gaussian_kde
x = player_df["x"].values
y = player_df["y"].values
xy = np.vstack([x, y])
# 计算带宽(Scott's rule 自动)
kde = gaussian_kde(xy, bw_method=0.3)
# 生成网格
xi, yi = np.mgrid[0:105:200j, 0:68:150j]
zi = kde(np.vstack([xi.flatten(), yi.flatten()])).reshape(xi.shape)
fig, ax = plt.subplots(figsize=(12, 8))
draw_pitch(ax)
# 热力图 + 等高线
contour = ax.contourf(xi, yi, zi, levels=20, cmap="hot_r", alpha=0.75)
ax.contour(xi, yi, zi, levels=8, colors="white", linewidths=0.5, alpha=0.6)
plt.colorbar(contour, ax=ax, label="密度")
ax.set_title("球员跑动热点图(KDE)")
plt.show()
方法三:用 mplsoccer 一行生成(推荐)
pip install mplsoccer
from mplsoccer import Pitch
import matplotlib.pyplot as plt
pitch = Pitch(pitch_type='statsbomb', line_zorder=2,
pitch_color='#22312b', line_color='white')
fig, ax = pitch.draw(figsize=(12, 8))
kde_plot = pitch.kdeplot(player_df["x"], player_df["y"],
ax=ax, cmap='hot_r', levels=100, fill=True)"球员跑动热点图(mplsoccer)", color="white")
plt.show()
进阶分析方向
区分持球 / 无球跑动
# 假设数据里有 has_ball 字段
ball_on = player_df[player_df["has_ball"] == 1]
ball_off = player_df[player_df["has_ball"] == 0]
fig, axes = plt.subplots(1, 2, figsize=(18, 6))
for ax, data, title in zip(axes, [ball_on, ball_off], ["持球", "无球"]):
draw_pitch(ax)
ax.hist2d(data["x"], data["y"], bins=(30, 20), cmap="hot_r")
ax.set_title(f"{title}跑动分布")
与队友对比
fig, ax = plt.subplots(figsize=(12, 8))
draw_pitch(ax)
for pid, color in [(1, "Reds"), (2, "Blues")]:
sub = df[df["player_id"] == pid]
ax.hist2d(sub["x"], sub["y"], bins=(30, 20),
cmap=color, alpha=0.5, label=f"球员{pid}")
ax.legend()
plt.show()
按时间分段(前/中/后 15 分钟)
df["minute"] = df["timestamp"] / 60
periods = [(0, 15), (15, 30), (30, 45)]
fig, axes = plt.subplots(1, 3, figsize=(20, 6))
for ax, (s, e) in zip(axes, periods):
sub = player_df[(player_df["minute"] >= s) & (player_df["minute"] < e)]
draw_pitch(ax)
ax.hist2d(sub["x"], sub["y"], bins=(25, 17), cmap="hot_r")
ax.set_title(f"{s}-{e} 分钟")
覆盖率 / 活动面积指标
from scipy.spatial import ConvexHull
points = player_df[["x", "y"]].values
hull = ConvexHull(points)
convex_area = hull.volume # 2D 中 volume 就是面积
print(f"活动覆盖面积: {convex_area:.1f} 平方米")
# 简易"活动范围"半径
center = points.mean(axis=0)
distances = np.linalg.norm(points - center, axis=1)
print(f"平均活动半径: {distances.mean():.1f} 米")
战术解读常用指标
| 指标 | 含义 | 计算方法 |
|---|---|---|
| 平均位置 | 球员习惯站位 | (x.mean(), y.mean()) |
| 活动热区 | 停留最久区域 | KDE 峰值坐标 |
| 覆盖面积 | 跑动范围 | Convex Hull |
| 左右倾向 | 偏向哪一路 | y.mean() vs 球场中线 |
| 前后倾向 | 进攻/防守型 | x.mean() vs 中线 |
| 冲刺区域 | 高速移动集中区 | 过滤速度 > 7 m/s 的点 |
输出解读示例
生成热点图后,可以总结出类似结论:
- 该球员 70% 的时间集中在中前场右路(x: 60-90, y: 0-30)
- 持球热点位于右边路,无球时回撤至中线附近
- 平均站位 x=62, y=18,属于进攻型右边卫
- 覆盖面积 1,850 m²,活动范围偏大,符合边翼卫特征
常见坑与建议
- 带宽选择:
bw_method太小→噪声大;太大→过平滑,足球推荐 0.2~0.4。 - 坐标统一:不同数据源坐标定义不同(如 StatsBomb 是 120×80),绘图前务必归一化或转换。
- 采样频率:25 Hz 和 10 Hz 密度不同,直方图 bin 数要适配。
- 时间加权:短时高频采样会夸大静止区域,可对速度<0.5 m/s 的点降权。
- 可视化配色:
hot_r、Reds直观;viridis在深色底图上更专业。
如果你能告诉我数据来源格式(StatsBomb / Metrica / 自采 GPS)和目标是足球还是篮球,我可以给你一段即拿即用的完整脚本。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播