统计界外球进攻威胁次数
下面是一个完整的 Python 案例,演示如何统计界外球(Throw-in)后的进攻威胁次数,核心思路是:找到界外球事件 → 追踪随后 N 秒/若干次传球内是否产生射门/射正/进球/进入禁区等威胁事件。

数据准备
假设你有事件数据(类似 StatsBomb / Opta 格式),字段如下:
| 字段 | 说明 |
|---|---|
event_id |
事件ID |
match_id |
比赛ID |
period |
半场 |
timestamp |
秒级时间戳 |
team |
球队 |
type |
事件类型(Throw-in / Pass / Shot / Goal 等) |
x, y |
坐标(0-100 归一化) |
end_x, end_y |
传球/射门终点 |
威胁定义(可自定义)
一次界外球进攻被计为有威胁,需在界外球后 10 秒 + 最多 3 次传球内出现以下任一:
- 射门
Shot(含射正Shot on Target) - 进球
Goal - 传球进入对方禁区(x ≥ 83,y 在 18–82,按 0-100 坐标)
- 传中进入小禁区(x ≥ 94)
完整代码
import pandas as pd
THREAT_WINDOW_SEC = 10 # 界外球后观察窗口(秒)
MAX_PASS_CHAIN = 3 # 最多追踪传球次数
BOX_X_START = 83 # 对方禁区起点 x
BOX_Y_MIN, BOX_Y_MAX = 18, 82
SIX_X_START = 94 # 小禁区 x
THREAT_TYPES = {"Shot", "Shot on Target", "Goal"}
def is_into_box(row):
"""传球终点是否进入对方禁区"""
if pd.isna(row.get("end_x")):
return False
return row["end_x"] >= BOX_X_START and BOX_Y_MIN <= row["end_y"] <= BOX_Y_MAX
def is_into_six_yard(row):
return (not pd.isna(row.get("end_x"))) and row["end_x"] >= SIX_X_START
def count_threats_from_throwins(df: pd.DataFrame) -> pd.DataFrame:
"""
返回每场比赛的界外球总数与威胁界外球数
"""
df = df.sort_values(["match_id", "period", "timestamp"]).reset_index(drop=True)
results = []
for match_id, g in df.groupby("match_id"):
g = g.reset_index(drop=True)
throw_idx = g.index[g["type"] == "Throw-in"].tolist()
total, threat = 0, 0
for i in throw_idx:
total += 1
throw_row = g.loc[i]
team = throw_row["team"]
t0 = throw_row["timestamp"]
period = throw_row["period"]
# 进入后续事件窗口
j = i + 1
pass_count = 0
threatened = False
while j < len(g):
row = g.loc[j]
# 换半场/换队(非本队事件)即中断链条
if row["period"] != period:
break
if row["timestamp"] - t0 > THREAT_WINDOW_SEC:
break
if row["team"] != team: # 球权丢失,链条终止
break
etype = row["type"]
# 威胁判定
if etype in THREAT_TYPES:
threatened = True
break
if etype == "Pass":
if is_into_box(row) or is_into_six_yard(row):
threatened = True
break
pass_count += 1
if pass_count > MAX_PASS_CHAIN:
break
j += 1
if threatened:
threat += 1
results.append({
"match_id": match_id,
"total_throwins": total,
"threat_throwins": threat,
"threat_rate": round(threat / total, 3) if total else 0
})
return pd.DataFrame(results)
# ---------- 构造示例数据 ----------
data = [
# 比赛1:一次界外球后 5 秒射门(威胁)
{"match_id": 1, "period": 1, "timestamp": 100, "team": "A", "type": "Throw-in", "x": 40, "y": 50, "end_x": None, "end_y": None},
{"match_id": 1, "period": 1, "timestamp": 103, "team": "A", "type": "Pass", "x": 45, "y": 55, "end_x": 90, "end_y": 40},
{"match_id": 1, "period": 1, "timestamp": 105, "team": "A", "type": "Shot", "x": 90, "y": 40, "end_x": None, "end_y": None},
# 比赛1:一次界外球后无威胁(3次传球内没进禁区)
{"match_id": 1, "period": 1, "timestamp": 200, "team": "A", "type": "Throw-in", "x": 30, "y": 50, "end_x": None, "end_y": None},
{"match_id": 1, "period": 1, "timestamp": 203, "team": "A", "type": "Pass", "x": 35, "y": 55, "end_x": 50, "end_y": 60},
{"match_id": 1, "period": 1, "timestamp": 206, "team": "A", "type": "Pass", "x": 55, "y": 60, "end_x": 70, "end_y": 65},
# 比赛2:界外球后传球直达禁区(威胁)
{"match_id": 2, "period": 1, "timestamp": 50, "team": "B", "type": "Throw-in", "x": 60, "y": 20, "end_x": None, "end_y": None},
{"match_id": 2, "period": 1, "timestamp": 53, "team": "B", "type": "Pass", "x": 65, "y": 25, "end_x": 85, "end_y": 50},
]
df = pd.DataFrame(data)
result = count_threats_from_throwins(df)
print(result)
输出:
match_id total_throwins threat_throwins threat_rate
0 1 2 1 0.5
1 2 1 1 1.0
关键设计说明
| 设计点 | 说明 |
|---|---|
| 时间窗口 | THREAT_WINDOW_SEC=10,可按联赛/战术微调(英超常 8–15 秒) |
| 传球链限制 | 避免一次界外球后很长时间都算威胁,限制 MAX_PASS_CHAIN |
| 球权中断 | 对方触球即结束链条,确保归因于界外球队 |
| 威胁判定 | 射门/进球/进禁区/进小禁区四类,可扩展为 xG 累计 |
| 退化版本 | 若数据没有 end_x/y,退化为仅统计"射门/进球"即可 |
进阶扩展
- 使用 xG 而非二分:把窗口内所有射门的 xG 累加,作为"威胁强度"更细腻。
xg_threat = g.loc[i+1:j, "xg"].sum()
- 区分界外球位置:
- 前场界外球(x ≥ 60)vs 后场界外球
- 左侧/右侧
- Defensive Third / Middle / Final Third 分区统计
threat_rate。 - 可视化:用 matplotlib 画界外球起点散点图,颜色表示是否威胁。
如果你能提供实际数据结构(列名/示例行),我可以把上面的代码直接改写成适配你数据的版本(StatsBomb 的 event.json 或 Wyscout 格式)。
版权声明:本站文章为原创整理,转载请注明出处。