实用脚本如何分析球员跑动热点图?

wen 实用脚本 1

本文目录导读:

实用脚本如何分析球员跑动热点图?

  1. 数据获取思路(前置条件)
  2. 核心分析脚本(Python + Matplotlib/Plotly)
  3. 深度解读:如何从热点图看战术?
  4. 进阶技巧(针对生产环境)

分析球员跑动热点图(Heatmap)是足球数据分析中非常实用的一项技能,热点图能够直观展示球员在球场上的活动范围和活跃程度,帮助教练评估战术执行、体能分配和跑动习惯。

下面我为你提供一套完整的实用分析脚本方案,包含数据获取思路核心Python脚本以及深度分析维度

数据获取思路(前置条件)

在写脚本之前,你需要有数据源,通常有三种方式:

  1. 官方API(如StatsBomb, Wyscout):最专业,包含坐标数据,但需要权限。
  2. 公开事件数据(如Kaggle数据集):包含x, y坐标(通常归一化到0-120 x 0-80)。
  3. 视频识别(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分段),如果下半场活跃范围明显缩小,存在体能下降或对手针对性限制的问题。
  • 反手侧热区:如果一名右路球员,热点图大量出现在左路,可能战术要求换位,或者是自由人踢法。

进阶技巧(针对生产环境)

  1. 时空切片:将比赛划分为每15分钟一段,查看热点变化,前30分钟高压逼抢区在对方半场,后30分钟缩回本方,说明体能下降。
  2. 对手差异化:对阵强队(高位逼抢)和弱队(阵地防守),热点图会完全不同,建议按对手实力分组绘制。
  3. 速度叠加:将热点图分为“慢走/步行”、“小跑”、“高速冲刺”四个图层。“冲刺热区” 往往比总热区更有战术价值(它代表最关键的压迫或反击时机)。

如果还没有现成的坐标数据,建议先从 StatsBomb公开数据集Kaggle 上下载免费数据练习,掌握了上述脚本逻辑后,你就可以针对任何球员生成专业的跑动分析报告了。

如果遇到特定数据格式解析问题(比如Wyscout的JSON结构),欢迎随时提供字段名,我可以帮你调整适配代码。

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