本文目录导读:

这是一个很有意思的足球数据分析问题,下面用 Python 来做一个案例分析,看看边路传中到底是不是得分利器。
分析思路
我们需要验证的核心假设是:
边路传中能显著提高进球概率吗?
分析框架:
| 维度 | 说明 |
|---|---|
| 数据来源 | StatsBomb 开放数据(免费) |
| 传中定义 | 从边路区域(wing)传入禁区的传球 |
| 对比对象 | 传中后的射门转化率 vs 非传中的射门转化率 |
| 关键指标 | xG(预期进球)、进球率、射门次数 |
环境准备
pip install statsbombpy pandas numpy matplotlib seaborn
完整代码案例
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from statsbombpy import sb
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# ========== 1. 加载数据 ==========
# 以 2018 世界杯决赛为例(法国 vs 克罗地亚)
matches = sb.matches(competition_id=43, season_id=3) # 2018世界杯
match_id = matches.iloc[0]['match_id']
events = sb.events(match_id=match_id)
print(f"比赛事件总数: {len(events)}")
print(events['type'].value_counts().head(10))
识别边路传中
# ========== 2. 提取传中事件 ==========
def classify_cross(row):
"""判断是否为边路传中"""
if row['type'] != 'Pass':
return False
loc = row['location']
if not isinstance(loc, list):
return False
x, y = loc[0], loc[1]
# 边路区域定义:y < 20 或 y > 60(场地宽80)
is_wing = (y < 20) or (y > 60)
# 前场区域:x > 60
is_advanced = x > 60
# 传球类型为传中
pass_type = row.get('pass_type', '')
is_cross = (pass_type == 'Cross') if pd.notna(pass_type) else False
return is_wing and is_advanced and is_cross
events['is_cross'] = events.apply(classify_cross, axis=1)
crosses = events[events['is_cross']]
print(f"边路传中次数: {len(crosses)}")
关联传中与射门
# ========== 3. 找到传中后的射门 ==========
def find_shot_after_cross(events, crosses):
"""追踪传中后是否形成射门"""
results = []
for _, cross in crosses.iterrows():
cross_id = cross['id']
cross_poss = cross['possession']
# 同一控球回合内的射门
same_poss = events[
(events['possession'] == cross_poss) &
(events['type'] == 'Shot')
]
if len(same_poss) > 0:
shot = same_poss.iloc[0]
results.append({
'cross_id': cross_id,
'shot_xg': shot.get('shot_statsbomb_xg', 0),
'is_goal': shot['shot_outcome'] == 'Goal'
})
return pd.DataFrame(results)
cross_shots = find_shot_after_cross(events, crosses)
print(f"传中形成射门数: {len(cross_shots)}")
if len(cross_shots) > 0:
print(f"传中转化进球数: {cross_shots['is_goal'].sum()}")
对比分析
# ========== 4. 对比所有射门来源 ==========
all_shots = events[events['type'] == 'Shot'].copy()
all_shots['xg'] = all_shots['shot_statsbomb_xg'].fillna(0)
all_shots['is_goal'] = all_shots['shot_outcome'] == 'Goal'
# 标记是否来自传中
cross_shot_ids = set(cross_shots['cross_id']) if len(cross_shots) > 0 else set()
def shot_from_cross(shot):
poss = shot['possession']
related = events[
(events['possession'] == poss) &
(events['is_cross'] == True)
]
return len(related) > 0
all_shots['from_cross'] = all_shots.apply(shot_from_cross, axis=1)
# 分组统计
summary = all_shots.groupby('from_cross').agg(
射门数=('id', 'count'),
进球数=('is_goal', 'sum'),
平均xG=('xg', 'mean'),
总xG=('xg', 'sum')
).reset_index()
summary['转化率'] = summary['进球数'] / summary['射门数']
print("\n===== 传中 vs 非传中 射门效率对比 =====")
print(summary.to_string(index=False))
可视化
# ========== 5. 可视化 ==========
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
# 图1:射门数对比
axes[0].bar(summary['from_cross'].astype(str), summary['射门数'],
color=['#3498db', '#e74c3c'])
axes[0].set_title('射门次数对比')
axes[0].set_xlabel('是否来自传中')
axes[0].set_ylabel('射门数')
# 图2:平均xG对比
axes[1].bar(summary['from_cross'].astype(str), summary['平均xG'],
color=['#3498db', '#e74c3c'])
axes[1].set_title('平均 xG 对比')
axes[1].set_xlabel('是否来自传中')
axes[1].set_ylabel('平均 xG')
# 图3:转化率对比
axes[2].bar(summary['from_cross'].astype(str), summary['转化率'],
color=['#3498db', '#e74c3c'])
axes[2].set_title('射门转化率对比')
axes[2].set_xlabel('是否来自传中')
axes[2].set_ylabel('转化率')
plt.tight_layout()
plt.savefig('cross_analysis.png', dpi=150)
plt.show()
用大数据集验证
单场比赛样本太小,建议用整个赛季数据:
# ========== 6. 多场比赛批量分析 ==========
def analyze_season(competition_id, season_id):
matches = sb.matches(competition_id=competition_id, season_id=season_id)
all_results = []
for match_id in matches['match_id'].head(50): # 取前50场
try:
ev = sb.events(match_id=match_id)
ev['is_cross'] = ev.apply(classify_cross, axis=1)
shots = ev[ev['type'] == 'Shot'].copy()
shots['xg'] = shots['shot_statsbomb_xg'].fillna(0)
shots['is_goal'] = shots['shot_outcome'] == 'Goal'
# 每场统计
for _, shot in shots.iterrows():
poss = shot['possession']
has_cross = len(ev[(ev['possession']==poss) & (ev['is_cross'])]) > 0
all_results.append({
'match_id': match_id,
'from_cross': has_cross,
'xg': shot['xg'],
'is_goal': shot['is_goal']
})
except Exception as e:
continue
return pd.DataFrame(all_results)
# 分析 2018 世界杯
df = analyze_season(43, 3)
result = df.groupby('from_cross').agg(
射门数=('xg', 'count'),
进球数=('is_goal', 'sum'),
平均xG=('xg', 'mean'),
总xG=('xg', 'sum')
)
result['转化率'] = result['进球数'] / result['射门数']
print("\n===== 2018世界杯 传中效果分析 =====")
print(result)
典型分析结论
根据 StatsBomb 等公开数据的研究,通常会发现:
| 指标 | 来自传中 | 非传中(阵地/反击) |
|---|---|---|
| 平均 xG | 06 ~ 0.09 | 10 ~ 0.15 |
| 转化率 | ~8-10% | ~11-15% |
| 射门占比 | 25-35% | 65-75% |
核心结论:
- 边路传中数量多,但效率偏低 — 单次传中的期望进球值通常只有 0.05-0.10
- 传中依赖度高 ≠ 得分利器 — 很多球队传中占比 30%+,但只贡献 20% 左右的进球
- 高质量传中才有价值 — 倒三角、低平球、后点传中效率明显高于盲目高吊
- 顶级球队趋势 — 曼城、利物浦等更倾向于肋部渗透而非传统边路传中
一句话总结:
边路传中是战术手段而非得分利器,它是保底进攻方式,但单次效率远低于直塞、反击和定位球,真正的得分利器是高质量的机会创造,传中只是其中一种低效选项。
进阶方向
- 加入 防守压力 因子(防守球员距离)
- 区分 传中类型(高球、低平、倒三角)
- 用 逻辑回归/贝叶斯 建模预测进球概率
- 结合 球员能力(传中精度)
需要我针对某个具体联赛或球队做完整分析吗?