Python案例:统计足球头球争顶成功率
下面我用一个完整的案例,演示如何用 Python 统计球员/球队的头球争顶成功率。

业务背景
头球争顶成功率 (Aerial Duel Success Rate) 是足球数据分析中常见指标:
成功率 = 争顶成功次数 / 争顶总次数 × 100%
常见分析维度:
- 单个球员
- 球队整体
- 位置分组(后卫/中场/前锋)
- 比赛时间段
数据结构设计
假设原始数据来自事件采集(类似 Opta、StatsBomb 的事件流),每条事件记录:
| 字段 | 说明 |
|---|---|
| match_id | 比赛ID |
| player | 球员名 |
| team | 球队 |
| minute | 分钟 |
| event_type | 事件类型(如 aerial_duel) |
| outcome | 结果(won / lost) |
完整代码
import pandas as pd
import numpy as np
# 1. 模拟一份事件数据(实际可替换为 StatsBomb/OPTA 导出的 csv)
data = [
# (match_id, minute, team, player, event_type, outcome)
(1, 5, '阿森纳', '萨利巴', 'aerial_duel', 'won'),
(1, 12, '阿森纳', '萨利巴', 'aerial_duel', 'won'),
(1, 20, '阿森纳', '萨利巴', 'aerial_duel', 'lost'),
(1, 33, '阿森纳', '加布里埃尔','aerial_duel', 'won'),
(1, 40, '阿森纳', '加布里埃尔','aerial_duel', 'lost'),
(1, 55, '阿森纳', '哈弗茨', 'aerial_duel', 'won'),
(1, 60, '阿森纳', '哈弗茨', 'aerial_duel', 'lost'),
(1, 72, '阿斯顿维拉', '沃特金斯', 'aerial_duel', 'won'),
(1, 78, '阿斯顿维拉', '沃特金斯', 'aerial_duel', 'lost'),
(1, 85, '阿斯顿维拉', '孔萨', 'aerial_duel', 'won'),
(2, 10, '阿森纳', '萨利巴', 'aerial_duel', 'won'),
(2, 25, '阿森纳', '萨利巴', 'aerial_duel', 'lost'),
(2, 45, '阿森纳', '加布里埃尔','aerial_duel', 'won'),
(2, 50, '切尔西', '杰克逊', 'aerial_duel', 'won'),
(2, 66, '切尔西', '杰克逊', 'aerial_duel', 'lost'),
(2, 80, '切尔西', '帕尔默', 'aerial_duel', 'lost'),
]
df = pd.DataFrame(data, columns=[
'match_id', 'minute', 'team', 'player', 'event_type', 'outcome'
])
# 2. 只保留头球争顶事件
aerials = df[df['event_type'] == 'aerial_duel'].copy()
# 3. 标记是否成功
aerials['is_won'] = (aerials['outcome'] == 'won').astype(int)
三种常用统计方式
1️⃣ 按球员统计
def player_duel_stats(df, min_duels=1):
"""按球员统计头球争顶成功率
min_duels: 过滤样本过少的球员,避免统计噪音
"""
g = df.groupby('player').agg(
team=('team', 'first'),
duels=('is_won', 'size'),
won=('is_won', 'sum'),
)
g['lost'] = g['duels'] - g['won']
g['success_rate'] = (g['won'] / g['duels'] * 100).round(1)
g = g[g['duels'] >= min_duels]
return g.sort_values(['success_rate', 'duels'], ascending=False)
player_stats = player_duel_stats(aerials, min_duels=2)
print("=== 球员头球争顶成功率 ===")
print(player_stats)
输出示例:
team duels won lost success_rate
player
萨利巴 阿森纳 4 3 1 75.0
沃特金斯 阿斯顿维拉 2 1 1 50.0
加布里埃尔 阿森纳 2 1 1 50.0
哈弗茨 阿森纳 2 1 1 50.0
杰克逊 切尔西 2 1 1 50.0
2️⃣ 按球队统计
def team_duel_stats(df):
g = df.groupby('team').agg(
duels=('is_won', 'size'),
won=('is_won', 'sum'),
)
g['success_rate'] = (g['won'] / g['duels'] * 100).round(1)
return g.sort_values('success_rate', ascending=False)
print("\n=== 球队头球争顶成功率 ===")
print(team_duel_stats(aerials))
输出:
duels won success_rate
team
阿森纳 8 5 62.5
阿斯顿维拉 3 2 66.7
切尔西 3 1 33.3
3️⃣ 按"位置"分组统计(进阶)
若数据里还有位置信息,可以做位置维度:
# 假设给球员手动映射位置(实战中可从阵容接口获取)
pos_map = {
'萨利巴': 'DF', '加布里埃尔': 'DF', '孔萨': 'DF',
'哈弗茨': 'MF', '帕尔默': 'MF',
'沃特金斯': 'FW', '杰克逊': 'FW',
}
aerials['position'] = aerials['player'].map(pos_map)
print("\n=== 按位置头球争顶成功率 ===")
print(aerials.groupby('position').apply(
lambda x: pd.Series({
'duels': len(x),
'won': x['is_won'].sum(),
'success_rate': round(x['is_won'].mean() * 100, 1)
})
))
进阶分析
✅ 按时间段统计(定位"下半场争顶下滑"问题)
bins = [0, 30, 60, 90]
labels = ['0-30min', '30-60min', '60-90min']
aerials['period'] = pd.cut(aerials['minute'], bins=bins, labels=labels)
print("\n=== 分时间段争顶成功率 ===")
print(aerials.groupby('period', observed=True)['is_won']
.agg(duels='size', rate=lambda x: round(x.mean()*100, 1)))
✅ 迷你回合/场景对比:高球 vs 半高球
若事件表里有 ball_height(地面/半高/高球),可加条件:
# 仅统计真正意义上的"高球"
high_balls = aerials[aerials.get('ball_height', 'high') == 'high']
✅ 置信区间(避免小样本误导)
样本数小时(如只争顶 2 次),75% 成功率并不可靠,可以加 Wilson 区间:
from math import sqrt
def wilson_ci(won, n, z=1.96):
if n == 0:
return (0, 0)
p = won / n
denom = 1 + z**2 / n
centre = (p + z**2 / (2*n)) / denom
margin = z * sqrt(p*(1-p)/n + z**2/(4*n**2)) / denom
return round((centre - margin) * 100, 1), round((centre + margin) * 100, 1)
player_stats[['ci_low', 'ci_high']] = player_stats.apply(
lambda r: pd.Series(wilson_ci(r['won'], r['duels'])), axis=1
)
print("\n=== 带 95% 置信区间的球员成功率 ===")
print(player_stats[['duels', 'won', 'success_rate', 'ci_low', 'ci_high']])
结果可视化(可选)
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei'] # 显示中文
plt.rcParams['axes.unicode_minus'] = False
fig, ax = plt.subplots(figsize=(8, 5))
player_stats['success_rate'].plot(kind='barh', ax=ax, color='#2E86AB')
ax.set_xlabel('头球争顶成功率 (%)')
ax.set_title('球员头球争顶成功率排行')
for i, v in enumerate(player_stats['success_rate']):
ax.text(v + 1, i, f"{v}%", va='center')
plt.tight_layout()
plt.show()
实战建议
| 场景 | 关键点 |
|---|---|
| 小样本过滤 | 争顶次数 < 3 的球员不要单独排名 |
| 置信区间 | 比赛多的球员成功率"更可信" |
| 位置归一化 | 中后卫天然争顶多,应与同位置对比 |
| 对手强度 | 强强对话数据更能反映真实水平 |
| 时间维度 | 体能下降常导致后段成功率下滑 |
| 数据源 | StatsBomb / Opta / Wyscout 的事件流字段名不同,需按需映射 |
如果你有 真实的事件数据(csv/json)结构,可以发我字段,我帮你写成直接可跑的完整脚本 👍