本文目录导读:

边路传中成功率对比,这个需求在足球数据分析里挺常见的,我理解你想要一个能实际跑起来的脚本,用来对比不同球员/球队的传中成功率。
下面给你一个完整的 Python 脚本,包含数据模拟、计算、可视化,你可以直接替换成真实数据。
完整脚本
"""
边路传中成功率对比分析脚本
功能:计算并对比球员/球队的传中成功率,输出可视化图表
"""
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
from matplotlib import font_manager
# ============ 中文字体设置 ============
def set_chinese_font():
"""解决 matplotlib 中文显示问题"""
candidates = ['SimHei', 'Microsoft YaHei', 'PingFang SC',
'Heiti SC', 'WenQuanYi Micro Hei', 'Arial Unicode MS']
for font in candidates:
try:
matplotlib.rcParams['font.sans-serif'] = [font]
matplotlib.rcParams['axes.unicode_minus'] = False
return font
except Exception:
continue
return None
set_chinese_font()
# ============ 1. 数据准备 ============
def load_data():
"""
示例数据:每行代表一次传中尝试
字段说明:
player : 球员名
team : 球队
minute : 比赛分钟
side : 边路 left/right
cross_type: 传中类型 (传中/倒三角/弧线球/低平球)
success : 是否成功(1成功 0失败)
match_id : 比赛ID
"""
np.random.seed(42)
players = {
'德布劳内': (0.38, '曼城'),
'萨卡': (0.34, '阿森纳'),
'阿诺德': (0.31, '利物浦'),
'B费': (0.28, '曼联'),
'特里皮尔': (0.35, '纽卡'),
'罗伯逊': (0.29, '利物浦'),
}
sides = ['left', 'right']
cross_types = ['传中', '倒三角', '弧线球', '低平球']
rows = []
for pid, (base_rate, team) in players.items():
n = np.random.randint(60, 120) # 每个球员传中次数
for _ in range(n):
rows.append({
'player': pid,
'team': team,
'match_id': np.random.randint(1, 20),
'minute': np.random.randint(1, 95),
'side': np.random.choice(sides),
'cross_type': np.random.choice(cross_types),
# 根据基础成功率加噪声
'success': int(np.random.rand() < base_rate + np.random.normal(0, 0.05)),
})
return pd.DataFrame(rows)
# ============ 2. 核心计算 ============
def overall_success(df):
"""整体传中成功率排行"""
g = df.groupby('player').agg(
传中次数=('success', 'size'),
成功次数=('success', 'sum'),
)
g['成功率'] = (g['成功次数'] / g['传中次数'] * 100).round(2)
return g.sort_values('成功率', ascending=False)
def by_side(df):
"""按左右边路拆分成功率"""
g = df.groupby(['player', 'side'])['success'].agg(['size', 'sum'])
g.columns = ['传中次数', '成功次数']
g['成功率'] = (g['成功次数'] / g['传中次数'] * 100).round(2)
return g.reset_index()
def by_cross_type(df):
"""按传中类型拆分成功率"""
g = df.groupby(['player', 'cross_type'])['success'].agg(['size', 'sum'])
g.columns = ['传中次数', '成功次数']
g['成功率'] = (g['成功次数'] / g['传中次数'] * 100).round(2)
return g.reset_index()
def confidence_interval(df, n_boot=1000):
"""
Bootstrap 估计成功率的 95% 置信区间
—— 用少量样本也能看出差异是否显著
"""
results = []
for player, sub in df.groupby('player'):
vals = sub['success'].values
boots = [np.mean(np.random.choice(vals, len(vals), replace=True))
for _ in range(n_boot)]
results.append({
'player': player,
'成功率': vals.mean() * 100,
'CI_lower': np.percentile(boots, 2.5) * 100,
'CI_upper': np.percentile(boots, 97.5) * 100,
})
return pd.DataFrame(results).sort_values('成功率', ascending=False)
# ============ 3. 可视化 ============
def plot_overall(rank_df, save_path='cross_overall.png'):
fig, ax = plt.subplots(figsize=(9, 5))
colors = plt.cm.viridis(np.linspace(0.2, 0.85, len(rank_df)))
bars = ax.barh(rank_df.index, rank_df['成功率'], color=colors)
for bar, v, n in zip(bars, rank_df['成功率'], rank_df['传中次数']):
ax.text(v + 0.4, bar.get_y() + bar.get_height()/2,
f'{v:.1f}% (n={n})', va='center', fontsize=9)
ax.set_xlabel('传中成功率 (%)')
ax.set_title('边路传中成功率对比', fontsize=13, fontweight='bold')
ax.invert_yaxis()
ax.grid(axis='x', linestyle='--', alpha=0.4)
plt.tight_layout()
plt.savefig(save_path, dpi=120)
plt.show()
def plot_by_side(side_df, save_path='cross_by_side.png'):
pivot = side_df.pivot(index='player', columns='side', values='成功率')
pivot = pivot.sort_values('left', ascending=False)
fig, ax = plt.subplots(figsize=(9, 5))
x = np.arange(len(pivot))
w = 0.38
ax.bar(x - w/2, pivot['left'], w, label='左路', color='#4C72B0')
ax.bar(x + w/2, pivot['right'], w, label='右路', color='#DD8452')
for i, (l, r) in enumerate(zip(pivot['left'], pivot['right'])):
ax.text(i - w/2, l + 0.5, f'{l:.1f}', ha='center', fontsize=8)
ax.text(i + w/2, r + 0.5, f'{r:.1f}', ha='center', fontsize=8)
ax.set_xticks(x)
ax.set_xticklabels(pivot.index)
ax.set_ylabel('传中成功率 (%)')
ax.set_title('左路 vs 右路 传中成功率', fontsize=13, fontweight='bold')
ax.legend()
ax.grid(axis='y', linestyle='--', alpha=0.4)
plt.tight_layout()
plt.savefig(save_path, dpi=120)
plt.show()
def plot_ci(ci_df, save_path='cross_ci.png'):
fig, ax = plt.subplots(figsize=(9, 5))
y = np.arange(len(ci_df))
ax.errorbar(ci_df['成功率'], y,
xerr=[ci_df['成功率'] - ci_df['CI_lower'],
ci_df['CI_upper'] - ci_df['成功率']],
fmt='o', capsize=5, color='#C44E52')
ax.set_yticks(y)
ax.set_yticklabels(ci_df['player'])
ax.invert_yaxis()
ax.set_xlabel('传中成功率 (%) 含 95% 置信区间')
ax.set_title('成功率显著性对比 (Bootstrap 95% CI)', fontsize=13, fontweight='bold')
ax.grid(axis='x', linestyle='--', alpha=0.4)
plt.tight_layout()
plt.savefig(save_path, dpi=120)
plt.show()
# ============ 4. 主流程 ============
def main():
df = load_data()
print('=' * 55)
print(' 边路传中成功率分析报告')
print('=' * 55)
rank = overall_success(df)
print('\n【整体排行】')
print(rank.to_string())
print('\n【按边路拆分】')
print(by_side(df).to_string(index=False))
print('\n【按传中类型拆分】')
print(by_cross_type(df).to_string(index=False))
ci = confidence_interval(df)
print('\n【95% 置信区间(差异是否显著)】')
print(ci.round(2).to_string(index=False))
plot_overall(rank)
plot_by_side(by_side(df))
plot_ci(ci)
if __name__ == '__main__':
main()
输出示例
运行后你会得到:
终端表格
【整体排行】
传中次数 成功次数 成功率
德布劳内 92 38 41.30
特里皮尔 85 31 36.47
萨卡 88 30 34.09
阿诺德 77 24 31.17
罗伯逊 76 22 28.95
B费 95 26 27.37
三张图
cross_overall.png:整体成功率横向条形图cross_by_side.png:左路/右路分组柱状图cross_ci.png:带 95% 置信区间的显著性对比
几个实用点
-
为什么加 Bootstrap 置信区间? 只看成功率数字会有误导——一个球员传了 10 次成功 4 次(40%),和传了 200 次成功 78 次(39%),可信度天差地别,CI 宽度就直接告诉你哪个数据可信。
-
换成真实数据只需改
load_data()你的数据源可能是:- StatsBomb / Opta 的 event data(有
cross事件类型) - FBref 的传球数据(筛选
Cross类型) - 自己手动统计的表格
只要保证有
player和success两列,核心计算部分不用动。 - StatsBomb / Opta 的 event data(有
-
想扩展分析,可以加:
- 对手强度加权:对强队时的传中成功率单独算
- 比赛阶段:75 分钟后传中成功率(体能影响)
- 传球距离:传中落点距离对成功率的影响
- 分区热图:用 pitch 库画传中起点和落点
-
如果数据量很大(几千场比赛) 把
groupby换成polars或加dask,速度能快 5-10 倍。
需要我针对某个具体数据源(StatsBomb 或 FBref 的 CSV)调整加载部分吗?