python案例如何量化球员的跑动覆盖面积?

wen python案例 2

Python量化球员跑动覆盖面积

下面给你一套完整的实战方案,从数据获取 → 坐标处理 → 面积计算 → 可视化。

python案例如何量化球员的跑动覆盖面积?


核心思路

球员跑动覆盖面积 ≠ 跑动距离,而是空间覆盖范围,常用三种量化方法:

方法 原理 适用
凸包 (Convex Hull) 最小凸多边形包围所有轨迹点 整体活动范围
Alpha Shape 凹包,贴合真实跑动形状 更贴近真实覆盖
栅格法 (Grid) 把场地切成小格,统计访问过的格子 密集区域热图

准备数据

假设你有球员逐帧追踪数据(如 StatsBomb、SkillCorner、或自采 YOLO 追踪):

import pandas as pd
import numpy as np
# 示例:一帧一个坐标点(至少200帧以上)
df = pd.DataFrame({
    "frame": range(500),
    "x": np.random.uniform(0, 105, 500),   # 场地长 105m
    "y": np.random.uniform(0, 68, 500),    # 场地宽 68m
})

方法 1:凸包(最常用)

from scipy.spatial import ConvexHull
import matplotlib.pyplot as plt
points = df[["x", "y"]].values
hull = ConvexHull(points)
area_m2 = hull.volume          # 二维时 volume 就是面积
print(f"凸包覆盖面积: {area_m2:.2f} m²")
# 可视化
hull_pts = points[hull.vertices]
hull_pts = np.vstack([hull_pts, hull_pts[0]])
plt.figure(figsize=(10, 6))
plt.plot(hull_pts[:, 0], hull_pts[:, 1], "r-", lw=2, label=f"Hull = {area_m2:.0f} m²")
plt.scatter(df["x"], df["y"], s=3, alpha=0.3)
plt.xlim(0, 105); plt.ylim(0, 68)
plt.gca().set_aspect("equal")
plt.legend(); plt.title("球员跑动凸包")
plt.show()

⚠️ 问题:凸包会把球员"绕圈跑"时中间的空白也算进去,面积偏大。


方法 2:Alpha Shape(凹包,更准确)

pip install alphashape
import alphashape
from shapely.geometry import MultiPoint
# alpha 越小越贴合(一般 0.5~3,需调参)
alpha = 1.5
shape = alphashape.alphashape(points, alpha)
print(f"Alpha Shape 面积: {shape.area:.2f} m²")
print(f"凸包面积对比: {ConvexHull(points).volume:.2f} m²")
# 可视化
fig, ax = plt.subplots(figsize=(10, 6))
ax.scatter(df["x"], df["y"], s=3, alpha=0.3)
if shape.geom_type == "Polygon":
    x, y = shape.exterior.xy
    ax.fill(x, y, alpha=0.3, fc="orange", ec="red", lw=2)
plt.xlim(0, 105); plt.ylim(0, 68)
ax.set_aspect("equal")f"Alpha Shape = {shape.area:.0f} m²")
plt.show()

方法 3:栅格法(最稳健,可处理碎片区域)

# 把场地按 1m×1m 栅格化
GRID = 1.0
visited = set()
for x, y in points:
    visited.add((int(x // GRID), int(y // GRID)))
area_m2 = len(visited) * (GRID ** 2)
print(f"栅格覆盖面积: {area_m2:.2f} m² (访问 {len(visited)} 个格子)")
# 可视化热图
grid_map = np.zeros((int(68/GRID), int(105/GRID)))
for gx, gy in visited:
    grid_map[gy, gx] += 1
plt.figure(figsize=(12, 6))
plt.imshow(grid_map, origin="lower", extent=[0, 105, 0, 68], cmap="hot", aspect="auto")
plt.colorbar(label="访问频次")"跑动覆盖热图")
plt.show()

优点:不怕断裂的轨迹(比如球员多次冲刺到不同区域),都能算进去。


进阶:按时间窗口分析活动强度

# 每 5 分钟为一个窗口,比较覆盖面积变化
df["window"] = (df["frame"] // 150).astype(int)  # 假设30fps,150帧=5s
results = []
for w, g in df.groupby("window"):
    pts = g[["x", "y"]].values
    if len(pts) >= 3:
        results.append({
            "window": w,
            "hull_area": ConvexHull(pts).volume,
            "grid_area": len({(int(x), int(y)) for x, y in pts}),
        })
pd.DataFrame(results).plot(x="window", y=["hull_area", "grid_area"])"分时段覆盖面积变化")
plt.show()

实战避坑指南

  1. 坐标系必须统一:StatsBomb 是 (120, 80) 归一化坐标,要按真实场地尺寸(105×68)换算。
  2. 过滤静止点:球员站桩时不增加覆盖面积,可先剔除速度 < 0.5 m/s 的点。
  3. 区分区域:
    # 只算前场覆盖
    fwd = points[points[:, 0] > 52.5]
  4. 多方法交叉验证:凸包 ≥ Alpha Shape ≥ 栅格法,差异过大说明轨迹异常。
  5. 对比队友/对手:用同一 alpha 参数才能横向比较。

完整封装示例

def coverage_area(df, method="alphashape", alpha=1.5, grid=1.0):
    pts = df[["x", "y"]].values
    if len(pts) < 3:
        return 0.0
    if method == "hull":
        return ConvexHull(pts).volume
    elif method == "alphashape":
        return alphashape.alphashape(pts, alpha).area
    elif method == "grid":
        cells = {(int(x // grid), int(y // grid)) for x, y in pts}
        return len(cells) * grid ** 2
    raise ValueError("unknown method")
# 使用
print(coverage_area(df, method="hull"))
print(coverage_area(df, method="alphashape", alpha=1.2))
print(coverage_area(df, method="grid", grid=1.0))

需要的话我可以继续给你:

  • 从视频用 YOLO + ByteTrack 提取球员坐标的完整代码
  • 接入 StatsBomb / Metrica 公开数据的加载示例
  • 绘制球员对比雷达图(覆盖面积 + 冲刺 + 距离)

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