本文目录导读:

分析球员跑动热点图(Heatmap)是足球数据分析中非常实用的一项技能,热点图能够直观展示球员在球场上的活动范围和活跃程度,帮助教练评估战术执行、体能分配和跑动习惯。
下面我为你提供一套完整的实用分析脚本方案,包含数据获取思路、核心Python脚本以及深度分析维度。
数据获取思路(前置条件)
在写脚本之前,你需要有数据源,通常有三种方式:
- 官方API(如StatsBomb, Wyscout):最专业,包含坐标数据,但需要权限。
- 公开事件数据(如Kaggle数据集):包含
x,y坐标(通常归一化到0-120 x 0-80)。 - 视频识别(OpenCV + YOLO):通过计算机视觉从比赛录像中提取球员位置。
假设:下面的脚本默认你有一份CSV或DataFrame数据,包含字段:player_id(球员)、x(0-120)、y(0-80)、match_id(场次)、timestamp(时间戳)。
核心分析脚本(Python + Matplotlib/Plotly)
这是最实用的部分,包含数据清洗、单场热点图和多场聚合图。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from scipy.ndimage import gaussian_filter
# ======================
# 第一步:数据清洗与标准化
# ======================
def load_and_prepare_data(file_path):
"""
加载数据并标准化坐标到 [0, 1] 区间。
标准数据通常是 0-120 (x) 和 0-80 (y)。
"""
df = pd.read_csv(file_path)
# 假设你的原始数据列名为 'x_loc', 'y_loc'
# 如果是其他数据源,请修改这里
if 'x_loc' in df.columns:
df.rename(columns={'x_loc':'x', 'y_loc':'y'}, inplace=True)
# 归一化到 0-1(方便画图)
df['x_norm'] = df['x'] / 120.0
df['y_norm'] = df['y'] / 80.0
# 去除无效坐标(有些数据点可能为0,0但没实际意义)
df = df[(df['x_norm'] > 0.01) & (df['y_norm'] > 0.01)]
return df
# ======================
# 第二步:绘制单场热点图(核心)
# ======================
def plot_heatmap_single_match(df, player_name, match_id, save_path=None):
"""
使用核密度估计(KDE)绘制球员跑动热点图。
"""
# 筛选特定球员和比赛
player_data = df[(df['player'] == player_name) & (df['match_id'] == match_id)]
if len(player_data) == 0:
print(f"没有找到 {player_name} 在 {match_id} 的数据")
return
# 创建热力图网格(120 x 80 的分辨率)
heatmap, xedges, yedges = np.histogram2d(
player_data['x'], player_data['y'],
bins=[120, 80],
range=[[0, 120], [0, 80]]
)
# 使用高斯平滑让热力图更柔和,去掉噪点
heatmap_smooth = gaussian_filter(heatmap, sigma=2)
# 开始绘图
fig, ax = plt.subplots(figsize=(10, 7))
# 反转Y轴(球场通常从上往下看)
im = ax.imshow(heatmap_smooth.T, extent=[0, 120, 0, 80],
origin='lower', cmap='Reds', aspect='auto',
interpolation='bilinear', alpha=0.8)
# 绘制球场轮廓(简化版)
draw_pitch(ax)
# 添加颜色条(表示活跃度)
cbar = plt.colorbar(im, ax=ax, shrink=0.8)
cbar.set_label('跑动频率', fontsize=12)
ax.set_title(f'{player_name} 跑动热点图 (比赛 {match_id})', fontsize=14, fontweight='bold')
ax.set_xlabel('球场长度 (0-120m)', fontsize=10)
ax.set_ylabel('球场宽度 (0-80m)', fontsize=10)
# 关闭坐标轴刻度,看起来更干净
ax.set_xticks([])
ax.set_yticks([])
plt.tight_layout()
if save_path:
plt.savefig(save_path, dpi=300, bbox_inches='tight')
print(f"图片已保存至: {save_path}")
plt.show()
# ======================
# 辅助函数:绘制球场
# ======================
def draw_pitch(ax):
"""绘制一个简化的足球场轮廓(用于背景)"""
# 外框
ax.plot([0, 0, 120, 120, 0], [0, 80, 80, 0, 0], color='black', linewidth=1.5)
# 中线
ax.plot([60, 60], [0, 80], color='black', linewidth=1)
ax.plot([0, 120], [40, 40], color='black', linewidth=0.5, linestyle='--')
# 中圈
circle = plt.Circle((60, 40), 10, color='black', fill=False, linewidth=1)
ax.add_patch(circle)
# 禁区
ax.add_patch(mpatches.Rectangle((0, 18), 16, 44, fill=False, edgecolor='black'))
ax.add_patch(mpatches.Rectangle((104, 18), 16, 44, fill=False, edgecolor='black'))
# 球门
ax.add_patch(mpatches.Rectangle((-2, 30), 2, 20, fill=False, edgecolor='black'))
ax.add_patch(mpatches.Rectangle((120, 30), 2, 20, fill=False, edgecolor='black'))
# ======================
# 第三步:多场次聚合分析(评估跑动稳定性)
# ======================
def plot_aggregated_heatmap(df, player_name, min_matches=3):
"""
聚合一个球员多场比赛的跑动数据,得出赛季热点倾向图。
这能更真实反映球员的战术位置感。
"""
player_data = df[df['player'] == player_name]
if player_data['match_id'].nunique() < min_matches:
print(f"数据不足,要求至少 {min_matches} 场比赛的数据")
return
# 同样用直方图聚合
heatmap, xedges, yedges = np.histogram2d(
player_data['x'], player_data['y'],
bins=[120, 80],
range=[[0, 120], [0, 80]]
)
heatmap_smooth = gaussian_filter(heatmap, sigma=2.5) # 稍微多平滑一点
fig, ax = plt.subplots(figsize=(10, 7))
im = ax.imshow(heatmap_smooth.T, extent=[0, 120, 0, 80],
origin='lower', cmap='plasma', aspect='auto',
alpha=0.8)
draw_pitch(ax)
# 添加等值线,更直观标出高密度区域(核心跑动区)
contours = ax.contour(heatmap_smooth.T, extent=[0, 120, 0, 80],
levels=np.percentile(heatmap_smooth, [50, 80, 95]),
colors='blue', alpha=0.5, linewidths=0.8)
ax.clabel(contours, inline=True, fontsize=8, fmt='%.0f')
cbar = plt.colorbar(im, ax=ax, shrink=0.8)
cbar.set_label('累计跑动次数')
match_count = player_data['match_id'].nunique()
ax.set_title(f'{player_name} 赛季聚合热点图 ({match_count}场比赛)', fontsize=14, fontweight='bold')
ax.set_xticks([])
ax.set_yticks([])
plt.tight_layout()
plt.show()
# ======================
# 第四步:量化指标计算(比图更重要的数据)
# ======================
def calculate_run_metrics(df, player_name, match_id=None):
"""
输出量化指标,辅助分析师做判断。
"""
if match_id:
data = df[(df['player'] == player_name) & (df['match_id'] == match_id)]
else:
data = df[df['player'] == player_name]
if len(data) == 0:
return None
# 计算“重心”(质心)——球员平均站位
centroid_x = np.mean(data['x'])
centroid_y = np.mean(data['y'])
# 计算“覆盖面”——90%的数据点所在区域
x_coverage = np.percentile(data['x'], 90) - np.percentile(data['x'], 10)
y_coverage = np.percentile(data['y'], 90) - np.percentile(data['y'], 10)
# 计算“冲刺距离”——通过时间戳计算(需要时间字段)
# 这里假设有 'timestamp' 字段
if 'timestamp' in data.columns:
data = data.sort_values('timestamp')
dx = np.diff(data['x'])
dy = np.diff(data['y'])
distances = np.sqrt(dx**2 + dy**2)
# 过滤掉过于微小的移动(静态站桩)
high_speed_runs = distances[distances > 2.0] # 假设每帧移动超过2米算冲刺
sprint_distance = np.sum(high_speed_runs)
else:
sprint_distance = 0
metrics = {
'球员': player_name,
'样本点数': len(data),
'核心站位 (X,Y)': (round(centroid_x, 1), round(centroid_y, 1)),
'进攻纵深覆盖 (米)': round(x_coverage, 1),
'横向覆盖 (米)': round(y_coverage, 1),
'估计冲刺距离 (米)': round(sprint_distance, 1)
}
return metrics
# ======================
# 主程序入口示例
# ======================
if __name__ == "__main__":
# 示例数据路径(你需要替换为你自己的数据)
df = load_and_prepare_data('your_tracking_data.csv')
# 1. 看某球员单场热点图
plot_heatmap_single_match(df, '梅西', match_id='M001', save_path='messi_heatmap.png')
# 2. 看赛季聚合图
plot_aggregated_heatmap(df, '梅西', min_matches=10)
# 3. 输出量化指标
metrics = calculate_run_metrics(df, '梅西', match_id='M001')
print("量化指标如下:")
for key, value in metrics.items():
print(f"{key}: {value}")
深度解读:如何从热点图看战术?
脚本只是工具,分析师的洞察才是关键,拿到图后,建议按以下三观去拆解:
微观视角(单场)
- 红黄区域:是高频活动区,检查是否覆盖了本方半场和对方半场。
- 空白区:如果是中场球员,热点图在中场中路密集(如布斯克茨),说明是“节拍器”;如果热点图几乎不进入禁区,说明是“纯组织者”,缺少前插。
宏观视角(多场)
- 流动性:聚合图如果是“一团火”集中在局部,说明球员定位固定;如果是“长条形”或“覆盖全场”,说明是B2B(从禁区到禁区)中场或边路球员。
- 纵向覆盖:对比
calculate_run_metrics输出的进攻纵深覆盖值,如果是边后卫,通常攻势覆盖在20m-70m之间,如果超过80m,说明战术非常激进。
异常侦测
- 下半场热点稀疏:对比上下半场球员数据(需要将timestamp分段),如果下半场活跃范围明显缩小,存在体能下降或对手针对性限制的问题。
- 反手侧热区:如果一名右路球员,热点图大量出现在左路,可能战术要求换位,或者是自由人踢法。
进阶技巧(针对生产环境)
- 时空切片:将比赛划分为每15分钟一段,查看热点变化,前30分钟高压逼抢区在对方半场,后30分钟缩回本方,说明体能下降。
- 对手差异化:对阵强队(高位逼抢)和弱队(阵地防守),热点图会完全不同,建议按对手实力分组绘制。
- 速度叠加:将热点图分为“慢走/步行”、“小跑”、“高速冲刺”四个图层。“冲刺热区” 往往比总热区更有战术价值(它代表最关键的压迫或反击时机)。
如果还没有现成的坐标数据,建议先从 StatsBomb公开数据集 或 Kaggle 上下载免费数据练习,掌握了上述脚本逻辑后,你就可以针对任何球员生成专业的跑动分析报告了。
如果遇到特定数据格式解析问题(比如Wyscout的JSON结构),欢迎随时提供字段名,我可以帮你调整适配代码。