统计界外球进攻威胁次数 — 实用脚本方案
下面给你一套可直接落地的思路和脚本,覆盖数据源选择 → 事件定义 → 统计逻辑 → 代码实现 → 输出报表。

先明确定义(关键!)
“界外球进攻威胁”必须量化,否则统计无意义,建议采用如下分层定义:
| 层级 | 定义 | 判定条件 |
|---|---|---|
| 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)
- 明细表
throw_in_threats_detail.csv:每次界外球一行,含时间、球队、是否形成进攻、是否威胁、射门/进球数。 - 汇总表
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 加权威胁值 的计算;
- ③ 生成 可视化图表(热点图 + 威胁链桑基图)。
告诉我你的数据源格式即可。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播