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()
实战避坑指南
- 坐标系必须统一:StatsBomb 是 (120, 80) 归一化坐标,要按真实场地尺寸(105×68)换算。
- 过滤静止点:球员站桩时不增加覆盖面积,可先剔除速度 < 0.5 m/s 的点。
- 区分区域:
# 只算前场覆盖 fwd = points[points[:, 0] > 52.5]
- 多方法交叉验证:凸包 ≥ Alpha Shape ≥ 栅格法,差异过大说明轨迹异常。
- 对比队友/对手:用同一 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 公开数据的加载示例
- 绘制球员对比雷达图(覆盖面积 + 冲刺 + 距离)