实用脚本统计头球争顶成功率如何?

wen 实用脚本 3

用脚本统计头球争顶成功率的方法

下面给你一套完整的思路和可运行脚本(Python),适用于足球比赛事件数据(如 StatsBomb、Opta、Wyscout 等格式)。

实用脚本统计头球争顶成功率如何?


核心逻辑

头球争顶成功率 = 争顶成功次数 / 总争顶次数

关键难点在于数据源如何标记头球争顶,常见字段:

数据源 争顶事件字段 成功标记
StatsBomb duel + duel.type = "Aerial Lost" 无"成功"版本,用对手的 Aerial Lost 反推
Opta Aerial / Challenge outcome = 1(won)
Wyscout Air duel successful = 1
FotMob/Sofascore duel_won / aerial_won 已聚合

⚠️ 注意:StatsBomb 只记录 Aerial Lost(争顶失败方),成功方通过"同一 duel 的另一名球员"推断,这是统计时最容易出错的点。


StatsBomb 版本脚本(最常用)

import pandas as pd
from statsbombpy import sb
def aerial_duel_stats(match_id: int) -> pd.DataFrame:
    """
    统计单场比赛每名球员的头球争顶数据
    返回: player, team, aerials_total, aerials_won, success_rate
    """
    events = sb.events(match_id=match_id)
    # 1. 筛选空中对抗事件
    duels = events[events["type"] == "Duel"].copy()
    duels = duels[duels["duel_type"] == "Aerial Lost"]  # 只记录失败方
    # 2. 每名球员的"争顶失败"次数
    lost = (
        duels.groupby(["player", "team"])
        .size()
        .rename("aerials_lost")
        .reset_index()
    )
    # 3. 通过 duel 配对推断"争顶成功"次数
    #    StatsBomb 中一次争顶 = 一条 Aerial Lost 事件
    #    该事件属于失败方;成功方为"同一时间段内的对手"
    #    简化做法:总争顶 = 双方各计一次,成功 = 总 - 失败
    #    更严谨:用 duel 的 related_events 配对(见下方进阶版)
    # 简化统计:以球队为单位反推
    # 每名球员参与的争顶总数 = 该球员所在 duel 数
    # 此处用近似:成功 = 该队总争顶数 - 该球员失败数(需精确配对)
    print("提示:精确统计请使用下方配对版本")
    return lost
def aerial_duel_stats_paired(match_id: int) -> pd.DataFrame:
    """
    精确版:通过 duel 配对统计成功/失败
    原理:每条 Aerial Lost 对应两名球员,失败方已标记,
         成功方 = 该事件中另一名参与球员
    """
    events = sb.events(match_id=match_id).reset_index(drop=True)
    # StatsBomb 的 Duel 事件本身不含对手ID,需借助 duel 的 related_events
    # 实际做法:一次争顶通常生成两条事件(双方各一条,互为 related)
    duels = events[events["type"] == "Duel"].copy()
    # 通过 related_events 找到配对事件
    # 这里用更稳的方法:按 (timestamp, 位置相近) 匹配
    # ——实际生产中推荐直接用 Wyscout/Opta 的 outcome 字段
    # 若数据源已带 outcome 字段(如 Opta 转换后):
    if "duel_outcome" in duels.columns:
        duels["won"] = duels["duel_outcome"] == "Won"
        stats = (
            duels.groupby(["player", "team"])
            .agg(aerials_total=("won", "size"), aerials_won=("won", "sum"))
            .reset_index()
        )
        stats["success_rate"] = (stats["aerials_won"] / stats["aerials_total"] * 100).round(1)
        return stats.sort_values("success_rate", ascending=False)
    raise ValueError("该数据源需先做 duel 配对,建议改用带 outcome 的字段")

通用版(适用于已带 outcome 的数据)

适合 Wyscout / Opta / 自己整理的事件表:

import pandas as pd
def aerial_success_rate(df: pd.DataFrame, min_duels: int = 5) -> pd.DataFrame:
    """
    df 需包含列: player, team, event_type, outcome
    仅统计 event_type 含 'aerial' 或 'air duel' 的行
    """
    aerials = df[df["event_type"].str.lower().str.contains("aerial|air duel", na=False)].copy()
    aerials["won"] = aerials["outcome"].astype(str).str.lower().isin(["won", "success", "1", "true"])
    stats = (
        aerials.groupby(["player", "team"])
        .agg(aerials_total=("won", "size"), aerials_won=("won", "sum"))
        .reset_index()
    )
    stats["success_rate_%"] = (stats["aerials_won"] / stats["aerials_total"] * 100).round(1)
    # 过滤样本过少的球员(避免 1/1 = 100% 的噪声)
    stats = stats[stats["aerials_total"] >= min_duels]
    return stats.sort_values("success_rate_%", ascending=False).reset_index(drop=True)
# 示例
# result = aerial_success_rate(events_df, min_duels=10)
# print(result.head(20))

进阶:按位置/区域拆分

def aerial_by_zone(df):
    """按球场区域统计争顶成功率(需 x,y 坐标字段)"""
    aerials = df[df["event_type"].str.contains("aerial", case=False, na=False)].copy()
    aerials["won"] = aerials["outcome"].isin(["won", 1, True])
    # 划分为后场/中场/前场(按 x 坐标 0-120 或 0-100)
    aerials["zone"] = pd.cut(
        aerials["x"], bins=[0, 40, 80, 120],
        labels=["后场", "中场", "前场"]
    )
    return (
        aerials.groupby(["player", "zone"], observed=True)
        .agg(total=("won", "size"), won=("won", "sum"))
        .assign(rate=lambda d: (d["won"] / d["total"] * 100).round(1))
        .reset_index()
    )

常见坑与建议

  1. StatsBomb 的 Aerial Lost 陷阱:它只记录失败方,直接 count(Aerial Lost) 会得到"失败数"而非"总数",必须配对或反推。
  2. 样本量过滤:min_duels 很关键,否则替补球员 1/1 = 100% 会污染排名。
  3. "争顶"≠"头球":Aerial duel 包含用胸、肩的争顶;若只统计头球,需再过滤 body_part = Head。
  4. 门将排除:门将的争顶多为出击,建议按位置过滤。
  5. 跨比赛聚合:先按 match 统计再汇总,避免单场噪声。

快速上手(StatsBomb 实战)

# 统计某场比赛头球争顶成功率
match_id = 3788741  # 举例
stats = aerial_duel_stats_paired(match_id)
print(stats.head(15))

如果你告诉我具体数据源格式(StatsBomb JSON / Opta CSV / Wyscout / 自建表),我可以把脚本改成直接能跑的版本,包括 duel 配对的精确实现,你需要哪一种?

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