用Python分析边锋内切打法的威胁——案例思路与代码
下面给你一个可落地的分析框架,用公开的足球事件数据(StatsBomb Open Data 免费)来量化"边锋内切"的威胁,核心思路是:识别内切事件 → 度量推进价值 → 关联后续进攻结果。

分析框架设计
| 步骤 | 目标 | 关键指标 |
|---|---|---|
| 数据获取 | 拿到带坐标的事件数据 | StatsBomb / Wyscout |
| 定义内切 | 识别边锋从边路向中路带球 | 起终点 x 位移、y 向中路收敛 |
| 威胁度量 | 量化这次内切的价值 | 推进距离、进入危险区、xT/VAEP |
| 结果关联 | 内切后是否形成射门/助攻 | 后续N秒射门概率、xG |
| 可视化 | 热图、传球网络、内切走廊 | pitch plot |
核心代码实现
环境与数据
# pip install statsbombpy mplsoccer pandas numpy from statsbombpy import sb import pandas as pd import numpy as np from mplsoccer import Pitch import matplotlib.pyplot as plt # 免费公开数据:某场比赛 matches = sb.matches(competition_id=11, season_id=90) # 西甲某赛季 match_id = matches.iloc[0]['match_id'] events = sb.events(match_id=match_id)
定义"内切"事件
内切 = 边锋/边前卫的带球(carry),起点在边路(|y-40| > 20),终点更靠近中路,且向前推进。
def extract_carries(events):
df = events[events['type'] == 'Carry'].copy()
df = df.dropna(subset=['location', 'carry_end_location'])
df['x'] = df['location'].apply(lambda l: l[0])
df['y'] = df['location'].apply(lambda l: l[1])
df['end_x'] = df['carry_end_location'].apply(lambda l: l[0])
df['end_y'] = df['carry_end_location'].apply(lambda l: l[1])
return df
def is_inside_cut(carry):
"""判断是否为内切:从边路向中路带球"""
start_wide = abs(carry['y'] - 40) > 20 # 起点在边路
toward_center = abs(carry['end_y'] - 40) < abs(carry['y'] - 40) # 向中路收敛
forward = carry['end_x'] - carry['x'] > 0 # 向前
return start_wide and toward_center and forward
carries = extract_carries(events)
carries['is_cut'] = carries.apply(is_inside_cut, axis=1)
cuts = carries[carries['is_cut']]
print(f"内切次数: {len(cuts)}")
威胁度量:推进距离 + 危险区 + xT
def threat_score(carry):
dx = carry['end_x'] - carry['x']
dy = abs(carry['y'] - 40) - abs(carry['end_y'] - 40) # 向中路位移
# 进入进攻三区加权
zone_bonus = 1.5 if carry['end_x'] > 80 else 1.0
# 进入禁区加权
box_bonus = 2.0 if (carry['end_x'] > 102 and 18 < carry['end_y'] < 62) else 1.0
return (dx * 0.5 + dy * 0.3) * zone_bonus * box_bonus
cuts = cuts.copy()
cuts['threat'] = cuts.apply(threat_score, axis=1)
print(cuts[['player', 'x','y','end_x','end_y','threat']].sort_values('threat', ascending=False).head(10))
进阶:用 xT(Expected Threat)网格替代手写权重,把带球起点终点映射到 16×12 网格的价值表,
xT_end - xT_start就是这次内切创造的预期威胁增量,Karun Singh 的 xT 表可直接复用。
关联后续进攻结果(关键)
内切本身不是目的,要看它是否引出射门/助攻,用时间窗口匹配后续事件:
def link_outcome(cuts, events, window=8):
results = []
for _, c in cuts.iterrows():
t = c['timestamp']
later = events[(events['timestamp'] > t)]
# 取同一进攻内后续N个事件,看是否出现射门
nxt = later.head(window)
shot = nxt[nxt['type'] == 'Shot']
results.append({
'player': c['player'],
'threat': c['threat'],
'led_to_shot': len(shot) > 0,
'xG': shot['shot_statsbomb_xg'].sum() if len(shot) else 0
})
return pd.DataFrame(results)
outcome = link_outcome(cuts, events)
print(outcome.groupby('player').agg(
cuts=('threat','count'),
avg_threat=('threat','mean'),
shot_rate=('led_to_shot','mean'),
total_xG=('xG','sum')
).sort_values('total_xG', ascending=False))
可视化:内切走廊图
pitch = Pitch(pitch_type='statsbomb', line_zorder=2)
fig, ax = pitch.draw(figsize=(10, 7))
# 画所有内切轨迹
for _, c in cuts.iterrows():
pitch.arrows(c['x'], c['y'], c['end_x'], c['end_y'],
color='#c72c41', width=1.5, headwidth=4, alpha=0.6, ax=ax)
# 起点热图
pitch.kdeplot(cuts['x'], cuts['y'], ax=ax, cmap='Reds', alpha=0.4, levels=50)
ax.set_title('边锋内切轨迹与起点热区', fontsize=15)
plt.show()
分析结论怎么读
跑完后你会得到一张球员维度表,解读方式:
- 内切次数多 + shot_rate 高 → 内切真正转化为威胁(如罗本式"内切+远射")
- 内切次数多 + shot_rate 低 → 内切后陷入密集防守,需要下底或回传,威胁被高估
- avg_threat 高但 xG 低 → 推进到危险区但缺少终结,说明队友接应不足
- 对比左右路:把
y < 40和y > 40分开统计,验证"逆足边锋内切更威胁"的假设
进阶方向
- VAEP / xT 替代手写打分:更严谨地评估每次带球的价值增量。
- 防守反应分析:内切后防守方是否收缩中路(用防守事件坐标变化衡量)。
- 机器学习建模:以"内切后是否形成射门"为目标做逻辑回归,特征含起终点、防守人数、比赛时段。
- 对手针对性:按对手分组,看哪支球队最怕内切(内切后失球/被射最多)。
需要的话我可以把 xT 网格接入、VAEP 打分或单场比赛完整可运行脚本补全给你,你手头有具体数据源(StatsBomb / Wyscout / 自采)吗?