python案例统计界外球进攻威胁次数?

wen python案例 3

统计界外球进攻威胁次数

下面用Python实现一个界外球进攻威胁的统计分析案例,核心思路:筛选所有界外球事件,判断该界外球结束后一段时间内(或本次进攻回合内)是否产生了威胁(射门/进球/危险进攻/进入进攻三区等)。

python案例统计界外球进攻威胁次数?

数据说明

假设有事件数据(类似StatsBomb/Opta格式),字段包括:

字段 说明
match_id 比赛ID
event_id 事件ID
timestamp 时间
team 球队
event_type 事件类型(Throw-in / Pass / Shot / Goal...)
x, y 坐标(0-100归一化,x为纵深)
outcome 结果(如射正、进球)

完整代码

import pandas as pd
import numpy as np
# ---------- 1. 模拟数据(实际使用时替换为读取CSV) ----------
np.random.seed(42)
events = []
teams = ['A队', 'B队']
event_types = ['Throw-in', 'Pass', 'Shot', 'Goal', 'Carry', 'Tackle', 'Interception']
for mid in [1, 2]:                      # 2场比赛
    for team in teams:
        for i in range(200):
            et = np.random.choice(event_types, p=[0.08, 0.45, 0.08, 0.02, 0.20, 0.09, 0.08])
            events.append({
                'match_id': mid,
                'event_id': len(events),
                'timestamp': i * 2,               # 每2秒一个事件
                'team': team,
                'event_type': et,
                'x': np.random.randint(0, 101),
                'y': np.random.randint(0, 101),
                'outcome': np.random.choice(['成功', '失败', '射正', '进球'],
                                            p=[0.6, 0.3, 0.08, 0.02])
            })
df = pd.DataFrame(events).sort_values(['match_id', 'event_id']).reset_index(drop=True)
# ---------- 2. 定义"威胁"判定规则 ----------
# 威胁事件:射门(Shot) 或 进球(Goal) 或 进入进攻三区(x>=67)的危险传球
def is_threat(row):
    if row['event_type'] in ('Shot', 'Goal'):
        return True
    if row['event_type'] == 'Pass' and row['x'] >= 67 and row['outcome'] == '成功':
        return True
    return False
df['is_threat'] = df.apply(is_threat, axis=1)
# ---------- 3. 为每个界外球关联后续进攻回合 ----------
# 规则:界外球后,同一球队连续控球(未被对手打断)且在 X 秒内的威胁事件归属于该界外球
WINDOW_SECONDS = 20          # 界外球后20秒内的进攻视为本次界外球发起
attack_three_zone_x = 67     # 进攻三区阈值
results = []
for (mid), grp in df.groupby('match_id'):
    grp = grp.sort_values('event_id').reset_index(drop=True)
    throwins = grp[grp['event_type'] == 'Throw-in']
    for _, ti in throwins.iterrows():
        t0 = ti['timestamp']
        team = ti['team']
        # 界外球之后、20秒内的事件
        window = grp[(grp['timestamp'] > t0) &
                     (grp['timestamp'] <= t0 + WINDOW_SECONDS)]
        # 判断控球是否被对手打断(简化:出现对手事件即认为回合结束)
        opponents = window[window['team'] != team]
        if not opponents.empty:
            first_opp_time = opponents['timestamp'].min()
            # 只保留对手事件之前的本方进攻
            window = window[window['timestamp'] < first_opp_time]
        own_actions = window[window['team'] == team]
        if own_actions.empty:
            threat_count = 0
            entered_final_third = False
        else:
            threat_count = own_actions['is_threat'].sum()
            entered_final_third = (own_actions['x'] >= attack_three_zone_x).any()
        results.append({
            'match_id': mid,
            'event_id': ti['event_id'],
            'team': team,
            'timestamp': t0,
            'threat_count': threat_count,
            'entered_final_third': entered_final_third,
            'has_threat': threat_count > 0,
            'is_dangerous_throwin': entered_final_third
        })
res = pd.DataFrame(results)
# ---------- 4. 汇总统计 ----------
total_throwins = len(res)
threat_throwins = res['has_threat'].sum()
danger_throwins = res['is_dangerous_throwin'].sum()
print("========== 界外球进攻威胁统计 ==========")
print(f"总界外球次数:{total_throwins}")
print(f"产生威胁(有射门/进球/三区传球)的界外球次数:{threat_throwins}")
print(f"进入进攻三区的界外球次数:{danger_throwins}")
print(f"威胁转化率:{threat_throwins / total_throwins:.2%}")
print(f"进入三区比例:{danger_throwins / total_throwins:.2%}\n")
# 按球队统计
team_stat = res.groupby('team').agg(
    throwins=('event_id', 'count'),
    threat_count=('has_threat', 'sum'),
    dangerous_count=('is_dangerous_throwin', 'sum'),
    total_threats=('threat_count', 'sum')
).reset_index()
team_stat['threat_rate'] = (team_stat['threat_count'] / team_stat['throwins']).map('{:.2%}'.format)
print("按球队统计:")
print(team_stat.to_string(index=False))
# ---------- 5. 可视化 ----------
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
team_stat.plot(kind='bar', x='team', y=['throwins', 'threat_count', 'dangerous_count'],
               ax=axes[0], color=['#4C72B0', '#DD8452', '#55A868'])
axes[0].set_title('各队界外球 vs 威胁界外球')
axes[0].set_ylabel('次数')
res['threat_count'].value_counts().sort_index().plot(
    kind='bar', ax=axes[1], color='#C44E52')
axes[1].set_title('每次界外球产生的威胁次数分布')
axes[1].set_xlabel('威胁次数')
axes[1].set_ylabel('界外球个数')
plt.tight_layout()
plt.savefig('throwin_threat.png', dpi=120)
plt.show()

关键逻辑解析

威胁定义(可自定义)

def is_threat(row):
    if row['event_type'] in ('Shot', 'Goal'):   # 直接射门/进球
        return True
    if row['event_type'] == 'Pass' and row['x'] >= 67:  # 进入进攻三区
        return True
    return False

界外球 → 进攻回合 的归属

  • 时间窗口法:界外球后 20 秒内的事件视为“由该界外球发起的进攻”。
  • 控球连续性:若窗口内出现对手事件(抢断、拦截、对方传球),则回合终止,只统计之前本方的动作。

威胁判定维度

维度 含义
has_threat 是否产生过射门/进球/威胁传球
is_dangerous_throwin 是否成功推进到进攻三区(x≥67)
threat_count 本次界外球回合内的威胁事件数量

进阶扩展建议

  1. 精确回合切分:使用 possession / sequence 字段对齐 StatsBomb 的 poss自然回合,避免用固定时间窗。
  2. 位置加权:前场界外球(x>60)应给予更高权重,区分“常规界外球”和“前场界外球”。
  3. xT/VAEP 模型:用 Expected Threat 或 VAEP 值替代简单二值判定,量化每次界外球的期望威胁。
  4. 对手强度校正:按对手实力归一化,避免样本偏差。
  5. 长掷界外球识别:单独标记掷入禁区的界外球(可视为定位球),其威胁率通常显著高于普通界外球。

真实数据来源

  • StatsBomb Open Data:https://github.com/statsbomb/open-data(含 Throw-in 事件与坐标)
  • FBref / Wyscout:需自建解析
  • StatsBomb 中界外球事件类型为 "Pass" 且 pass.type == "Throw-in",需要相应调整筛选条件:
# StatsBomb 格式下的筛选
throwins = df[(df['type'] == 'Pass') & (df['pass_type'] == 'Throw-in')]

如需我针对 StatsBomb 真实数据改写完整读取+分析脚本,告诉我即可。

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

友情链接攻略窝 · 礼包窝 · 配置窝 · 换算窝 · 算法窝 · 查文窝 · 手册窝 · 问答窝 · 全文搜 · 手册库
相关站点礼包窝 · 查文窝