实用脚本统计界外球进攻威胁次数?

wen 实用脚本 2

统计界外球进攻威胁次数 - 实用脚本

下面提供一个通用可复用的 Python 脚本框架,用于统计比赛中界外球(Throw-in)之后形成的进攻威胁次数,你可以根据数据源(StatsBomb、Opta、自采数据等)稍作适配。

实用脚本统计界外球进攻威胁次数?


核心逻辑定义

判断"界外球进攻威胁"通常需要满足:

  1. 起始事件:界外球(Throw-in)
  2. 同一控球回合(Possession)内后续事件
  3. 威胁条件(满足任一即可):
    • 射门(Shot)
    • 传入禁区(Pass into box / 关键传球)
    • 预期进球值 xG ≥ 阈值(如 0.05)
    • 到达进攻三区(Final Third)
  4. 时间窗口:通常在界外球后 15 秒 / 或同一 possession 内

完整 Python 脚本

"""
统计界外球进攻威胁次数
Author: analytic-script
Input : 事件级数据 DataFrame,字段如下:
    - match_id      比赛ID
    - period        半场 (1/2)
    - minute, second 时间
    - team          球队
    - player        球员
    - event_type    事件类型 (Throw-in, Pass, Shot, Carry...)
    - possession    控球回合编号
    - x, y          坐标 (0-100 或 0-120 归一化)
    - end_x, end_y  传球/带球终点
    - xg            预期进球 (射门事件)
Output: 每队每场的界外球威胁次数统计
"""
import pandas as pd
# ============ 1. 参数配置 ============
TIME_WINDOW = 15          # 界外球后多少秒内算作同一次进攻
XG_THRESHOLD = 0.03       # 射门威胁阈值
FINAL_THIRD_X = 66.7      # 进攻三区起点(按 0-100 归一化)
# ============ 2. 读取数据 ============
def load_events(path: str) -> pd.DataFrame:
    df = pd.read_csv(path)  # 或 read_parquet / read_json
    # 统一时间字段
    df["time_sec"] = df["minute"] * 60 + df["second"]
    return df
# ============ 3. 定位所有界外球 ============
def find_throw_ins(df: pd.DataFrame) -> pd.DataFrame:
    return df[df["event_type"].str.lower() == "throw-in"].copy()
# ============ 4. 判断界外球是否形成威胁 ============
def is_threatening(throw_in: pd.Series, df: pd.DataFrame) -> bool:
    """判断单次界外球是否有威胁"""
    t0 = throw_in["time_sec"]
    team = throw_in["team"]
    poss = throw_in["possession"]
    # 筛选同一回合、同一球队、时间窗口内的事件
    sub = df[
        (df["team"] == team) &
        (df["possession"] == poss) &
        (df["time_sec"] >= t0) &
        (df["time_sec"] <= t0 + TIME_WINDOW)
    ]
    if sub.empty:
        return False
    # 威胁条件1:有射门且 xG 达标
    shots = sub[sub["event_type"].str.lower() == "shot"]
    if not shots.empty and shots["xg"].max() >= XG_THRESHOLD:
        return True
    # 威胁条件2:传入禁区
    passes = sub[sub["event_type"].str.lower() == "pass"]
    if not passes.empty:
        into_box = passes[
            (passes["end_x"] >= 83) &           # 进入禁区 x 坐标
            (passes["end_y"].between(21, 79))   # 禁区内 y 范围
        ]
        if not into_box.empty:
            return True
    # 威胁条件3:到达进攻三区(可选,宽松标准)
    # if (sub["end_x"] >= FINAL_THIRD_X).any():
    #     return True
    return False
# ============ 5. 批量统计 ============
def count_threatening_throw_ins(df: pd.DataFrame) -> pd.DataFrame:
    throw_ins = find_throw_ins(df)
    records = []
    for _, ti in throw_ins.iterrows():
        records.append({
            "match_id": ti["match_id"],
            "team": ti["team"],
            "minute": ti["minute"],
            "player": ti["player"],
            "is_threat": int(is_threatening(ti, df)),
        })
    result = pd.DataFrame(records)
    # 汇总
    summary = (
        result.groupby(["match_id", "team"])
        .agg(
            界外球总数=("is_threat", "count"),
            威胁界外球数=("is_threat", "sum"),
        )
        .reset_index()
    )
    summary["威胁率%"] = (summary["威胁界外球数"] / summary["界外球总数"] * 100).round(2)
    return summary, result
# ============ 6. 主流程 ============
if __name__ == "__main__":
    df = load_events("events.csv")
    summary, detail = count_threatening_throw_ins(df)
    print(summary)
    summary.to_csv("throw_in_threat_summary.csv", index=False)
    detail.to_csv("throw_in_threat_detail.csv", index=False)

运行示例输出

match_id team 界外球总数 威胁界外球数 威胁率%
12345 Arsenal 18 5 78
12345 Chelsea 22 3 64

若使用 StatsBomb 数据(推荐)

StatsBomb 免费开放数据可直接对接:

from statsbombpy import sb
events = sb.events(match_id=3788741)   # 示例比赛
events = events.rename(columns={
    "type": "event_type",
    "possession_team": "team",
    "location": "loc"
})
# 拆分坐标
events["x"]     = events["loc"].apply(lambda v: v[0] if isinstance(v, list) else None)
events["y"]     = events["loc"].apply(lambda v: v[1] if isinstance(v, list) else None)
events["end_x"] = events["pass_end_location"].apply(lambda v: v[0] if isinstance(v, list) else None)
events["end_y"] = events["pass_end_location"].apply(lambda v: v[1] if isinstance(v, list) else None)
events["xg"]    = events.get("shot_statsbomb_xg", 0)
# 时间字段
events["minute"] = events["minute"].fillna(0).astype(int)
events["second"] = events["second"].fillna(0).astype(int)

StatsBomb 坐标范围为 0-120 x 0-80,需把 into_box 判断改为 end_x >= 102 & end_y between 18~62。


可扩展方向

功能 实现思路
可视化 用 mplsoccer 画界外球位置 + 威胁射门热图
界外球类型分档 按球员/前场/后场界外球分别统计
对手对比 groupby 增加 opponent 列
xT 模型 用足球场网格 xT 值替代简单坐标判断
多场汇总 循环 match_id,concat 结果

注意事项

  1. 同一控球回合判断建议优先使用数据源自带的 possession 字段,比时间窗口更准。
  2. 时间窗口不能太长:>20 秒后基本已脱离界外球直接战术范畴。
  3. xG 阈值需按联赛校准:一般 0.03~0.05 较为合理。
  4. 界外球不等于传中:如果界外球掷进禁区直接形成头球,也要算作威胁(用 end_x 判断)。

如果你能告诉我具体数据格式(StatsBomb / Opta / 自采 CSV 字段),我可以帮你把脚本改成直接可跑的版本。

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