高球传中争顶成功率统计案例
需求分析
高球传中争顶成功率 = 成功争顶次数 / 总争顶次数 × 100%

关键概念:
- 高球传中:进攻方从边路/中路起高球传入禁区
- 争顶:进攻球员与防守球员在空中争抢落点
- 成功争顶:本方球员抢到球(形成攻门、做球或控球)
完整案例代码
import pandas as pd
import numpy as np
# ============ 1. 模拟原始数据 ============
# 假设这是从比赛事件数据中提取的争顶记录
data = {
'match_id': [1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2],
'player_id': [101, 102, 103, 104, 101, 102, 105, 106, 201, 202, 203, 204, 201, 205, 206, 207],
'player_name': ['张伟','李强','王磊','赵勇','张伟','李强','孙鹏','周凯',
'陈明','刘洋','杨帆','黄鑫','陈明','吴昊','徐亮','朱峰'],
'team': ['A队','A队','A队','A队','A队','A队','A队','A队',
'B队','B队','B队','B队','B队','B队','B队','B队'],
'cross_type': ['高球传中']*8 + ['高球传中']*8, # 传中类型
'aerial_duel': [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], # 是否争顶
'duel_won': [1,1,0,1,0,1,1,0,1,0,1,0,1,1,0,1], # 是否争顶成功(1成功/0失败)
'height_cm': [185,188,178,182,185,188,190,175,
183,180,186,177,183,189,181,179],
}
df = pd.DataFrame(data)
print("=== 原始数据前8行 ===")
print(df.head(8))
核心统计逻辑
# ============ 2. 只保留高球传中 → 争顶事件 ============
high_cross = df[(df['cross_type'] == '高球传中') & (df['aerial_duel'] == 1)].copy()
# ============ 3. 按球员统计争顶成功率 ============
def calc_success_rate(group):
total = len(group)
won = group['duel_won'].sum()
rate = won / total * 100 if total > 0 else 0
return pd.Series({
'争顶总次数': total,
'成功次数': won,
'成功率(%)': round(rate, 1)
})
player_stats = high_cross.groupby(['player_name','team']).apply(
calc_success_rate, include_groups=False
).reset_index()
# 按成功率排序(至少争顶1次)
player_stats = player_stats.sort_values('成功率(%)', ascending=False)
print("\n=== 球员高球传中争顶成功率排名 ===")
print(player_stats.to_string(index=False))
输出示例:
=== 球员高球传中争顶成功率排名 ===
player_name team 争顶总次数 成功次数 成功率(%)
孙鹏 A队 1 1 100.0
徐亮 B队 1 1 100.0
王磊 A队 1 1 100.0
徐亮 B队 1 1 100.0
张伟 A队 2 1 50.0
...
按球队汇总
# ============ 4. 按球队统计 ============
team_stats = high_cross.groupby('team').agg(
争顶总次数=('duel_won', 'count'),
成功次数=('duel_won', 'sum')
)
team_stats['成功率(%)'] = (team_stats['成功次数'] / team_stats['争顶总次数'] * 100).round(1)
print("\n=== 球队高球传中争顶成功率 ===")
print(team_stats)
按身高分组分析(进阶)
# ============ 5. 按身高分组看成功率 ============
high_cross['身高档位'] = pd.cut(
high_cross['height_cm'],
bins=[0, 180, 185, 190, 999],
labels=['<180', '180-185', '185-190', '>190']
)
height_stats = high_cross.groupby('身高档位', observed=True).agg(
人次=('duel_won', 'count'),
成功=('duel_won', 'sum')
)
height_stats['成功率(%)'] = (height_stats['成功'] / height_stats['人次'] * 100).round(1)
print("\n=== 身高与争顶成功率关系 ===")
print(height_stats)
数据可视化
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 图1:球员成功率柱状图
axes[0].bar(player_stats['player_name'], player_stats['成功率(%)'], color='steelblue')
axes[0].set_title('球员高球传中争顶成功率')
axes[0].set_ylabel('成功率 (%)')
axes[0].tick_params(axis='x', rotation=45)
# 图2:球队成功率对比
axes[1].bar(team_stats.index, team_stats['成功率(%)'], color=['tomato','seagreen'])
axes[1].set_title('球队高球传中争顶成功率')
axes[1].set_ylabel('成功率 (%)')
for i, v in enumerate(team_stats['成功率(%)']):
axes[1].text(i, v+0.5, f'{v}%', ha='center')
plt.tight_layout()
plt.savefig('aerial_duel_success.png', dpi=120)
plt.show()
关键代码要点总结
| 步骤 | 关键操作 | 代码 |
|---|---|---|
| 筛选条件 | 传中类型+争顶事件 | df[(df['cross_type']=='高球传中') & (df['aerial_duel']==1)] |
| 分组统计 | 按球员分组 | groupby(['player_name','team']) |
| 成功率计算 | 求和 / 次数 | won / total * 100 |
| 排序 | 降序排名 | sort_values('成功率(%)', ascending=False) |
| 分档分析 | 身高分箱 | pd.cut(...) |
真实数据接入建议
如果你有真实比赛数据(如 StatsBomb、Opta、WhoScored 导出),建议:
- 字段映射:把
cross_type对应到pass_type中的Cross,aerial_duel对应Aerial事件,duel_won对应outcome - 样本量过滤:建议对"争顶次数 < 3"的球员不参与排名,避免偶然性
- 位置拆分:中锋、中卫、边锋的争顶成功率应分开评估
- 情境细分:区分进攻三区、防守三区、定位球传中
需要我针对某个具体数据源(如 StatsBomb open data)写一个真实接入版本吗?