统计界外球进攻威胁次数
下面用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 |
本次界外球回合内的威胁事件数量 |
进阶扩展建议
- 精确回合切分:使用
possession/sequence字段对齐 StatsBomb 的 poss自然回合,避免用固定时间窗。 - 位置加权:前场界外球(x>60)应给予更高权重,区分“常规界外球”和“前场界外球”。
- xT/VAEP 模型:用 Expected Threat 或 VAEP 值替代简单二值判定,量化每次界外球的期望威胁。
- 对手强度校正:按对手实力归一化,避免样本偏差。
- 长掷界外球识别:单独标记掷入禁区的界外球(可视为定位球),其威胁率通常显著高于普通界外球。
真实数据来源
- 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 真实数据改写完整读取+分析脚本,告诉我即可。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播