python案例统计头球争顶成功率如何?

wen python案例 2

Python案例:统计足球头球争顶成功率

下面我用一个完整的案例,演示如何用 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)结构,可以发我字段,我帮你写成直接可跑的完整脚本 👍

上一篇python案例认为大数据模型比传统预测更准吗?

下一篇当前分类已是最新一篇

抱歉,评论功能暂时关闭!