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

wen 实用脚本 1

统计界外球进攻威胁次数 — 实用脚本方案

下面给你一套可直接落地的思路和脚本,覆盖数据源选择 → 事件定义 → 统计逻辑 → 代码实现 → 输出报表。

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


先明确定义(关键!)

“界外球进攻威胁”必须量化,否则统计无意义,建议采用如下分层定义:

层级 定义 判定条件
L1 界外球发起 由界外球重新开球 event_type = throw_in
L2 形成进攻 界外球后 N 秒内本方控球推进 后续 N 秒内传球≥2次且进入前场
L3 形成威胁 界外球后 N 秒内产生射门/传中/禁区触球 后续事件含 shot/cross/box_touch
L4 高威胁 界外球后直接导致射门或进球 链式关联到 shot/goal

推荐 N = 10~15 秒(界外球战术典型持续时间)。


数据源

  • StatsBomb Open Data(免费,含坐标、事件链)— 首选
  • Wyscout(付费,事件细)
  • Opta / F24(商业)
  • 自采视频 + 人工标注(小样本)

下面脚本以 StatsBomb JSON 为例(结构最规范)。


核心脚本(Python)

"""
统计界外球进攻威胁次数
输入: StatsBomb events JSON
输出: CSV + 控制台摘要
"""
import json
import pandas as pd
from collections import defaultdict
# ---------- 配置 ----------
THROW_WINDOW_SEC = 12          # 界外球后观察窗口(秒)
MIN_PASSES_FOR_ATTACK = 2      # 形成进攻最少传球数
THREAT_EVENTS = {"Shot", "Pass"}  # Pass 需进一步判断 cross / 进禁区
BOX_X, BOX_Y = 102, 40         # StatsBomb 场地 120x80, 前场禁区近似
# ---------- 工具 ----------
def load_events(path):
    with open(path) as f:
        return json.load(f)
def ts(e):
    return e["timestamp"]  # "00:01:23.456"
def to_sec(t):
    h, m, s = t.split(":")
    return int(h)*3600 + int(m)*60 + float(s)
def in_final_third(e):
    loc = e.get("location")
    return loc and loc[0] >= 80  # 前场1/3
def is_cross(e):
    return (e["type"]["name"] == "Pass"
            and e.get("pass", {}).get("cross"))
def is_box_touch(e):
    loc = e.get("location")
    if not loc: return False
    x, y = loc
    return x >= BOX_X and 18 <= y <= 62
def is_threat(e):
    t = e["type"]["name"]
    if t == "Shot":
        return True
    if is_cross(e):
        return True
    if is_box_touch(e):
        return True
    return False
# ---------- 主逻辑 ----------
def analyze_throw_ins(events, team_name=None):
    """
    返回 list[dict]: 每次界外球的威胁记录
    """
    records = []
    events = sorted(events, key=lambda e: to_sec(e["timestamp"]))
    for i, ev in enumerate(events):
        if ev["type"]["name"] != "Throw-In":
            continue
        team = ev["team"]["name"]
        if team_name and team != team_name:
            continue
        t0 = to_sec(ev["timestamp"])
        period = ev["period"]
        window = []
        for nxt in events[i+1:]:
            if nxt["period"] != period:
                break
            dt = to_sec(nxt["timestamp"]) - t0
            if dt > THROW_WINDOW_SEC:
                break
            window.append(nxt)
        # 只统计本方后续事件
        own = [e for e in window if e.get("team", {}).get("name") == team]
        passes = [e for e in own if e["type"]["name"] == "Pass"]
        threats = [e for e in own if is_threat(e)]
        shots   = [e for e in own if e["type"]["name"] == "Shot"]
        goals   = [e for e in shots if e.get("shot", {}).get("outcome", {}).get("name") == "Goal"]
        formed_attack = len(passes) >= MIN_PASSES_FOR_ATTACK and \
                        any(in_final_third(e) for e in own)
        is_threatened = len(threats) > 0
        records.append({
            "minute": ev["minute"],
            "second": ev["second"],
            "team": team,
            "period": period,
            "formed_attack": formed_attack,
            "threat": is_threatened,
            "shots": len(shots),
            "goals": len(goals),
            "n_passes": len(passes),
            "n_threat_events": len(threats),
        })
    return records
def summarize(records):
    df = pd.DataFrame(records)
    if df.empty:
        print("无界外球记录"); return df
    summary = df.groupby("team").agg(
        throw_ins=("team", "count"),
        formed_attacks=("formed_attack", "sum"),
        threats=("threat", "sum"),
        shots=("shots", "sum"),
        goals=("goals", "sum"),
    )
    summary["threat_rate_%"] = (summary["threats"] / summary["throw_ins"] * 100).round(1)
    summary["attack_rate_%"] = (summary["formed_attacks"] / summary["throw_ins"] * 100).round(1)
    return summary.sort_values("threats", ascending=False)
# ---------- 运行 ----------
if __name__ == "__main__":
    EVENTS_FILE = "events.json"  # 替换为你的 StatsBomb 文件
    events = load_events(EVENTS_FILE)
    recs = analyze_throw_ins(events)
    detail = pd.DataFrame(recs)
    detail.to_csv("throw_in_threats_detail.csv", index=False, encoding="utf-8-sig")
    summary = summarize(recs)
    summary.to_csv("throw_in_threats_summary.csv", encoding="utf-8-sig")
    print(summary)

  1. 明细表 throw_in_threats_detail.csv:每次界外球一行,含时间、球队、是否形成进攻、是否威胁、射门/进球数。
  2. 汇总表 throw_in_threats_summary.csv:
    • 界外球总数
    • 形成进攻次数 / 比例
    • 威胁次数 / 威胁率
    • 射门数、进球数

进阶优化建议

优化点 做法
对手干扰 窗口内若对方先触球,则本链终止(防止“捡漏”误计)
界外球区域 区分前场/中场/后场界外球,前场威胁率更有参考价值
xT / xG 加权 用 threat_events 的 xT 增量或后续 xG 加权为“威胁值”,而非二元计数
防守方视角 反转 team 字段,统计被界外球打成的威胁(防守弱点)
可视化 用 mplsoccer 画界外球热点图和威胁链箭头图
多场聚合 遍历目录下所有 matches,按球队/联赛合并

示例:加“对手先触球即终止”逻辑只需在窗口遍历时判断:

opp = None
for nxt in events[i+1:]:
    ...
    if nxt.get("team", {}).get("name") != team:
        break        # 对方接球即链结束

如果只有视频/无结构化数据

可用 人工标注模板 + 脚本聚合:

match_id,team,minute,throw_in_zone,formed_attack,threat_type,shot,goal
M001,TeamA,23,前场,1,传中,1,0

再把 CSV 喂给上面的 summarize() 即可(把字段映射一下)。


需要的话我可以继续帮你:

  • ① 直接适配你手上的具体数据格式(Opta/Wyscout/自建 CSV);
  • ② 加上 xT 加权威胁值 的计算;
  • ③ 生成 可视化图表(热点图 + 威胁链桑基图)。

告诉我你的数据源格式即可。

上一篇综合赛后实用脚本,新赛季格局有何变化?

下一篇当前分类已是最新一篇

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

友情链接攻略窝 · 礼包窝 · 配置窝 · 换算窝 · 算法窝 · 查文窝 · 手册窝 · 问答窝 · 全文搜 · 手册库
相关站点礼包窝 · 查文窝