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

wen python案例 7

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

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

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


核心思路

足球/篮球球员跑动热点图本质是 二维空间上的点密度分布,分析流程:

原始追踪数据 → 坐标归一化 → 密度估计(KDE/直方图) → 可视化 → 战术解读

关键库:

  • pandas / numpy:数据处理
  • matplotlib / seaborn:可视化
  • scipy.stats.gaussian_kdesklearn.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²,活动范围偏大,符合边翼卫特征

常见坑与建议

  1. 带宽选择:bw_method 太小→噪声大;太大→过平滑,足球推荐 0.2~0.4。
  2. 坐标统一:不同数据源坐标定义不同(如 StatsBomb 是 120×80),绘图前务必归一化或转换。
  3. 采样频率:25 Hz 和 10 Hz 密度不同,直方图 bin 数要适配。
  4. 时间加权:短时高频采样会夸大静止区域,可对速度<0.5 m/s 的点降权。
  5. 可视化配色:hot_rReds 直观;viridis 在深色底图上更专业。

如果你能告诉我数据来源格式(StatsBomb / Metrica / 自采 GPS)和目标是足球还是篮球,我可以给你一段即拿即用的完整脚本。

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