实用脚本统计交叉跑位造成威胁几次?

wen 实用脚本 1

统计交叉跑位造成威胁次数

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

实用脚本统计交叉跑位造成威胁几次?

先定义"交叉跑位造成威胁"

需要量化标准,建议如下(可按项目调整):

条件 说明
交叉跑位 两名同队球员在时间窗 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 米 依追踪精度调整
威胁类型 按项目自定义 加"吸引防守""致对方犯规"等

进阶方向

  1. 用速度方向夹角判定"真正换位",避免只是靠近:
    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 表示相向交叉
  2. 预期威胁值 (xT) 替代硬性"射门/传球",用概率图加权。
  3. 可视化:用 mplsoccer 画出交叉点和威胁点轨迹热图。
  4. 对接真实数据:StatsBomb、Metrica、SkillCorner 的格式可直接适配。

需要我帮你适配具体数据格式(如 StatsBomb 事件或 SkillCorner 追踪),或者加上可视化热图吗?告诉我你的数据来源即可。

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