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

核心逻辑定义
判断"界外球进攻威胁"通常需要满足:
- 起始事件:界外球(Throw-in)
- 同一控球回合(Possession)内后续事件
- 威胁条件(满足任一即可):
- 射门(Shot)
- 传入禁区(Pass into box / 关键传球)
- 预期进球值 xG ≥ 阈值(如 0.05)
- 到达进攻三区(Final Third)
- 时间窗口:通常在界外球后 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 结果 |
注意事项
- 同一控球回合判断建议优先使用数据源自带的
possession字段,比时间窗口更准。 - 时间窗口不能太长:>20 秒后基本已脱离界外球直接战术范畴。
- xG 阈值需按联赛校准:一般 0.03~0.05 较为合理。
- 界外球不等于传中:如果界外球掷进禁区直接形成头球,也要算作威胁(用
end_x判断)。
如果你能告诉我具体数据格式(StatsBomb / Opta / 自采 CSV 字段),我可以帮你把脚本改成直接可跑的版本。