Python案例:统计高球传中争顶成功率
下面用一个完整的案例,演示如何从足球比赛事件数据中统计高球传中(High Cross) 的争顶成功率。

业务背景
在足球数据分析中,传中争顶成功率是衡量球队进攻效率的重要指标:
- 传中(Cross):从边路或肋部将球传向禁区
- 高球传中:传球类型为高球/挑传(
pass_type = 'high_cross') - 争顶成功:接球方球员在对抗中赢得头球(
aerial_won = True)
争顶成功率 = 争顶成功次数 / 传中总次数 × 100%
模拟数据结构
import pandas as pd
import numpy as np
# 模拟一场比赛的事件数据
data = {
'event_id': range(1, 21),
'minute': [3, 8, 12, 15, 22, 28, 33, 40, 45, 52,
58, 63, 68, 72, 78, 82, 85, 88, 90, 93],
'team': ['A', 'A', 'B', 'A', 'B', 'A', 'A', 'B', 'A', 'B',
'A', 'A', 'B', 'A', 'B', 'A', 'A', 'B', 'A', 'B'],
'player': ['P1', 'P2', 'P3', 'P4', 'P5', 'P6', 'P7', 'P8', 'P9', 'P10',
'P11', 'P1', 'P3', 'P4', 'P5', 'P6', 'P7', 'P8', 'P9', 'P10'],
'pass_type': ['high_cross', 'short_pass', 'high_cross', 'high_cross',
'high_cross', 'through_ball', 'high_cross', 'high_cross',
'short_pass', 'high_cross', 'high_cross', 'high_cross',
'high_cross', 'short_pass', 'high_cross', 'high_cross',
'high_cross', 'high_cross', 'high_cross', 'high_cross'],
'aerial_won': [True, np.nan, False, True, False, np.nan, True, False,
np.nan, True, False, True, False, np.nan, True, False,
True, True, False, True]
}
df = pd.DataFrame(data)
print(df.head(10))
核心统计逻辑
基础统计(按球队)
# 1. 筛选出高球传中事件
high_cross = df[df['pass_type'] == 'high_cross'].copy()
# 2. 按球队分组统计
result = high_cross.groupby('team').agg(
total_cross=('event_id', 'count'), # 传中总次数
won_cross=('aerial_won', 'sum'), # 争顶成功次数
).reset_index()
# 3. 计算成功率
result['success_rate'] = (result['won_cross'] / result['total_cross'] * 100).round(2)
print(result)
输出示例:
| team | total_cross | won_cross | success_rate |
|---|---|---|---|
| A | 10 | 6 | 00 |
| B | 6 | 2 | 33 |
按球员统计(谁传中质量最好)
# 按球员统计传中争顶成功率
player_stats = high_cross.groupby(['team', 'player']).agg(
total_cross=('event_id', 'count'),
won_cross=('aerial_won', 'sum')
).reset_index()
player_stats['success_rate'] = (
player_stats['won_cross'] / player_stats['total_cross'] * 100
).round(2)
# 按成功率排序(传中次数≥2的球员)
player_stats = player_stats[player_stats['total_cross'] >= 2]
print(player_stats.sort_values('success_rate', ascending=False))
区分主客场 / 时间段
# 假设有主客场字段
df['venue'] = ['home' if m <= 45 else 'away' for m in df['minute']]
df['half'] = df['minute'].apply(lambda x: '1H' if x <= 45 else '2H')
high_cross = df[df['pass_type'] == 'high_cross']
# 按半场统计
half_stats = high_cross.groupby(['team', 'half']).agg(
total=('event_id', 'count'),
won=('aerial_won', 'sum')
).reset_index()
half_stats['success_rate'] = (half_stats['won'] / half_stats['total'] * 100).round(2)
print(half_stats)
进阶:结合可视化
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
fig, ax = plt.subplots(figsize=(8, 5))
colors = ['#2E86AB', '#E63946']
bars = ax.bar(result['team'], result['success_rate'],
color=colors, width=0.5, edgecolor='black')
# 添加数值标签
for bar, rate, won, total in zip(bars, result['success_rate'],
result['won_cross'], result['total_cross']):
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1,
f'{rate}%\n({won}/{total})', ha='center', fontsize=11)
ax.set_title('高球传中争顶成功率对比', fontsize=14, fontweight='bold')
ax.set_ylabel('成功率 (%)')
ax.set_ylim(0, 100)
ax.grid(axis='y', linestyle='--', alpha=0.6)
plt.tight_layout()
plt.savefig('cross_success_rate.png', dpi=150)
plt.show()
关键要点与常见坑
| 问题 | 解决方案 |
|---|---|
aerial_won 有 NaN(非争顶事件) |
用 dropna() 或 fillna(False) 后再统计 |
| 分母要选"传中次数"还是"争顶次数"? | 成功率 = 争顶成功 / 传中总次数;口径二:胜率 = 争顶成功 / 实际争顶次数,业务上通常用口径一衡量传中质量 |
| 门将出击拦截 | 应算作"传中失败",需保留在分母中 |
| 被防守球员解围 | 若未发生争顶,归为"未成功",计入分母 |
| 比分/时间段影响 | 落后方传中更多,可单独分析比赛状态 |
完整可复用函数
def cross_aerial_success(df, group_col='team',
cross_type='high_cross',
cross_field='pass_type',
won_field='aerial_won'):
"""
统计高球传中争顶成功率
:param df: 事件DataFrame
:param group_col: 分组维度(team / player 等)
:param cross_type: 传中类型标识
:param cross_field: 传中类型列名
:param won_field: 争顶结果列名
:return: 含 total, won, success_rate 的 DataFrame
"""
sub = df[df[cross_field] == cross_type].copy()
sub[won_field] = sub[won_field].fillna(False)
res = sub.groupby(group_col).agg(
total=(won_field, 'count'),
won=(won_field, 'sum')
).reset_index()
res['success_rate'] = (res['won'] / res['total'] * 100).round(2)
return res.sort_values('success_rate', ascending=False)
# 使用
print(cross_aerial_success(df, group_col='team'))
print(cross_aerial_success(df, group_col='player'))
总结一句话: 用 pandas 先按 pass_type == 'high_cross' 过滤传中事件,再按球队/球员分组对 aerial_won 求和除以总数,即可得到争顶成功率;再结合可视化就能生成完整的分析报告,需要我帮你适配真实数据源(如 StatsBomb、Opta、Wyscout) 的字段吗?