统计交叉跑位造成威胁次数
下面给你一个可落地的统计脚本方案,适用于足球/篮球等团队运动的比赛数据(事件日志/坐标追踪)。

先定义"交叉跑位造成威胁"
需要量化标准,建议如下(可按项目调整):
| 条件 | 说明 |
|---|---|
| 交叉跑位 | 两名同队球员在时间窗 T(如3秒)内跑动轨迹相交/换位 |
| 造成威胁 | 交叉后 W 秒(如5秒)内出现:射门、关键传球、进入禁区、对方失位、犯规等 |
| 计数 | 每次有效交叉+后续威胁 = 1次 |
数据输入格式(示例 CSV)
frame,time,player_id,team,x,y 1,0.0,P1,A,30.0,40.0 1,0.0,P2,A,32.0,42.0 2,0.2,P1,A,30.5,40.2 2,0.2,P2,A,31.8,41.7 ...
事件表:
time,type,team,player 12.4,shot,A,P3 12.6,key_pass,A,P1
核心脚本(Python)
import pandas as pd
import numpy as np
from itertools import combinations
# ============ 参数 ============
CROSS_WINDOW = 3.0 # 交叉判定时间窗(秒)
THREAT_WINDOW = 5.0 # 交叉后多久内算威胁
MIN_DIST = 1.5 # 两球员最近距离阈值(米) 视为"交叉"
SAME_TEAM_ONLY = True
# ============ 读取数据 ============
tracks = pd.read_csv("tracking.csv") # frame,time,player_id,team,x,y
events = pd.read_csv("events.csv") # time,type,team,player
THREAT_TYPES = {"shot", "key_pass", "penalty_area_entry", "foul_won", "goal"}
# ============ 1. 检测交叉跑位 ============
def detect_crossings(tracks):
crossings = []
for (team, pid1, pid2), grp in tracks.groupby(
["team", "player_id", "player_id"] # 占位,下面重算
):
pass
# 更清晰写法:按队伍分组,遍历同队球员对
for team, team_df in tracks.groupby("team"):
players = team_df["player_id"].unique()
for p1, p2 in combinations(players, 2):
d1 = team_df[team_df.player_id == p1].sort_values("time")
d2 = team_df[team_df.player_id == p2].sort_values("time")
merged = pd.merge_asof(
d1, d2, on="time", suffixes=("_1", "_2"),
tolerance=0.2, direction="nearest"
).dropna(subset=["x_2", "y_2"])
if merged.empty:
continue
merged["dist"] = np.hypot(
merged["x_1"] - merged["x_2"],
merged["y_1"] - merged["y_2"]
)
# 找距离小于阈值的时刻 = 交叉发生点
close = merged[merged["dist"] < MIN_DIST]
if close.empty:
continue
# 合并相近时刻为一组"交叉事件"
close = close.sort_values("time").copy()
close["gap"] = close["time"].diff().fillna(999)
close["group"] = (close["gap"] > CROSS_WINDOW).cumsum()
for _, g in close.groupby("group"):
crossings.append({
"team": team,
"p1": p1, "p2": p2,
"time": g["time"].iloc[0],
"x": (g["x_1"].mean() + g["x_2"].mean()) / 2,
"y": (g["y_1"].mean() + g["y_2"].mean()) / 2,
})
return pd.DataFrame(crossings)
# ============ 2. 判定交叉后是否造成威胁 ============
def tag_threat(crossings, events):
events = events[events["type"].isin(THREAT_TYPES)].copy()
results = []
for _, c in crossings.iterrows():
window = events[
(events["team"] == c["team"]) &
(events["time"] >= c["time"]) &
(events["time"] <= c["time"] + THREAT_WINDOW)
]
c2 = c.copy()
c2["threat"] = not window.empty
c2["threat_type"] = ",".join(window["type"].tolist())
c2["threat_time"] = window["time"].iloc[0] if not window.empty else None
results.append(c2)
return pd.DataFrame(results)
# ============ 3. 执行 ============
crossings = detect_crossings(tracks)
result = tag_threat(crossings, events)
threat_count = result["threat"].sum()
total_cross = len(result)
print(f"总交叉跑位次数: {total_cross}")
print(f"造成威胁次数: {threat_count}")
print(f"转化率: {threat_count / total_cross:.1%}" if total_cross else "无交叉")
result.to_csv("crossing_threats.csv", index=False)
# 按球员统计贡献
if not result.empty:
print("\n— 球员参与威胁交叉排行 —")
long_df = result[result.threat].melt(
id_vars=["time", "threat_type"],
value_vars=["p1", "p2"],
value_name="player"
)
print(long_df["player"].value_counts())
运行 & 输出
python crossing_threat.py
输出示例:
总交叉跑位次数: 27
造成威胁次数: 8
转化率: 29.6%
— 球员参与威胁交叉排行 —
P7 3
P11 3
P4 2
P9 2
生成 crossing_threats.csv,含每次威胁交叉的时间、位置、球员、威胁类型。
调优建议
| 参数 | 建议值 | 用途 |
|---|---|---|
CROSS_WINDOW |
2–4 秒 | 太短会拆分,太长会合并 |
THREAT_WINDOW |
3–8 秒 | 反映"交叉后立即威胁" |
MIN_DIST |
0–2.0 米 | 依追踪精度调整 |
| 威胁类型 | 按项目自定义 | 加"吸引防守""致对方犯规"等 |
进阶方向
- 用速度方向夹角判定"真正换位",避免只是靠近:
v1 = np.array([dx1, dy1]); v2 = np.array([dx2, dy2]) dot = (v1 @ v2) / (np.linalg.norm(v1)*np.linalg.norm(v2) + 1e-6) # dot < -0.5 表示相向交叉
- 预期威胁值 (xT) 替代硬性"射门/传球",用概率图加权。
- 可视化:用 mplsoccer 画出交叉点和威胁点轨迹热图。
- 对接真实数据:StatsBomb、Metrica、SkillCorner 的格式可直接适配。
需要我帮你适配具体数据格式(如 StatsBomb 事件或 SkillCorner 追踪),或者加上可视化热图吗?告诉我你的数据来源即可。