综合赛后 Python 案例:两队实力真实差距分析
这是一个经典的体育数据分析问题,单场比赛的结果往往包含大量随机性(运气、裁判、状态波动),要评估两队真实实力差距,需要综合多场比赛数据。

核心思路
真实实力差距 ≠ 单场比分差
需要从多个维度综合估计:
| 维度 | 说明 |
|---|---|
| 期望进球 (xG) | 剔除运气成分的进攻质量 |
| 历史交锋 | 多场平均而非单场 |
| 控球/射门/射正 | 场面压制程度 |
| 近期状态 | 加权近期比赛 |
| 主客场因素 | 分离主客场效应 |
Python 完整案例
数据准备
import numpy as np
import pandas as pd
from scipy import stats
# 模拟:A队 vs B队 10 次交锋数据
data = {
'match': range(1, 11),
'A_goals': [2, 1, 3, 1, 2, 0, 2, 1, 1, 2],
'B_goals': [1, 1, 1, 2, 0, 1, 1, 1, 0, 1],
'A_xG': [1.8, 1.2, 2.5, 1.1, 1.9, 0.8, 1.7, 1.3, 1.0, 1.6],
'B_xG': [1.0, 0.9, 1.2, 1.5, 0.7, 1.3, 1.1, 1.0, 0.8, 0.9],
'A_shots': [15, 12, 18, 10, 14, 8, 16, 11, 9, 13],
'B_shots': [10, 9, 11, 13, 7, 12, 10, 9, 8, 10],
}
df = pd.DataFrame(data)
print(df)
基础统计对比
def summarize(df):
result = {}
for team in ['A', 'B']:
result[team] = {
'场均进球': df[f'{team}_goals'].mean(),
'场均xG': df[f'{team}_xG'].mean(),
'场均射门': df[f'{team}_shots'].mean(),
'进球标准差': df[f'{team}_goals'].std(),
}
return pd.DataFrame(result)
summary = summarize(df)
summary['差值(A-B)'] = summary['A'] - summary['B']
print(summary.round(2))
输出示例:
A B 差值(A-B)
场均进球 1.50 0.90 0.60
场均xG 1.59 1.04 0.55
场均射门 12.60 9.90 2.70
进球标准差 0.85 0.57 0.28
统计显著性检验(真实差距是否显著)
# 独立样本 t 检验:进球差异是否显著
t_stat, p_value = stats.ttest_ind(df['A_goals'], df['B_goals'])
print(f"t = {t_stat:.3f}, p = {p_value:.4f}")
# 用 xG 做检验(更稳定)
t_xg, p_xg = stats.ttest_ind(df['A_xG'], df['B_xG'])
print(f"xG: t = {t_xg:.3f}, p = {p_xg:.4f}")
# 效应量 Cohen's d
def cohens_d(x, y):
pooled_std = np.sqrt((x.std()**2 + y.std()**2) / 2)
return (x.mean() - y.mean()) / pooled_std
d = cohens_d(df['A_xG'], df['B_xG'])
print(f"Cohen's d = {d:.2f}")
解读标准:
- p < 0.05 → 差距统计显著
- Cohen's d: 0.2 小 / 0.5 中 / 0.8 大
贝叶斯估计(推荐方法)
单场比分波动大,用贝叶斯能给出真实差距的概率分布:
import pymc as pm
import arviz as az
with pm.Model() as model:
# 先验:两队真实进攻强度
mu_A = pm.Normal('mu_A', mu=1.5, sigma=1)
mu_B = pm.Normal('mu_B', mu=1.5, sigma=1)
# 观测似然(泊松分布适合进球数据)
goals_A = pm.Poisson('goals_A', mu=mu_A, observed=df['A_goals'])
goals_B = pm.Poisson('goals_B', mu=mu_B, observed=df['B_goals'])
# 关键:计算实力差距
diff = pm.Deterministic('diff', mu_A - mu_B)
trace = pm.sample(2000, return_inferencedata=True)
az.summary(trace, var_names=['mu_A', 'mu_B', 'diff'])
输出解读:
mean sd hdi_3% hdi_97%
mu_A 1.52 0.38 0.82 2.24
mu_B 0.93 0.30 0.38 1.50
diff 0.59 0.48 -0.34 1.51
diff的均值 ≈ 59 球- HDI 区间跨 0 → 差距存在但不绝对确定
Elo 评分法(长期真实实力)
def update_elo(rating_a, rating_b, score_a, k=20):
"""score_a: 1=胜, 0.5=平, 0=负"""
expected_a = 1 / (1 + 10**((rating_b - rating_a) / 400))
new_a = rating_a + k * (score_a - expected_a)
new_b = rating_b + k * ((1 - score_a) - (1 - expected_a))
return new_a, new_b
# 初始 1500
ra, rb = 1500, 1500
for _, row in df.iterrows():
if row['A_goals'] > row['B_goals']:
s = 1
elif row['A_goals'] < row['B_goals']:
s = 0
else:
s = 0.5
ra, rb = update_elo(ra, rb, s)
print(f"A队 Elo: {ra:.1f}")
print(f"B队 Elo: {rb:.1f}")
print(f"实力差距: {ra - rb:.1f} 分")
Elo 差距 → 胜率换算:
def win_probability(rating_diff):
return 1 / (1 + 10**(-rating_diff / 400))
print(f"A队胜率: {win_probability(ra - rb):.1%}")
如何判断"真实差距"
def real_gap(df):
"""综合评估两队真实实力差距"""
report = {}
# 1. 进球差
report['进球差(场均)'] = df['A_goals'].mean() - df['B_goals'].mean()
# 2. xG 差(更可信)
report['xG差(场均)'] = df['A_xG'].mean() - df['B_xG'].mean()
# 3. 显著性
_, p = stats.ttest_ind(df['A_xG'], df['B_xG'])
report['p值'] = p
report['显著'] = '✅ 显著' if p < 0.05 else '❌ 不显著'
# 4. 效应量
report["Cohen's d"] = cohens_d(df['A_xG'], df['B_xG'])
return report
import pprint
pprint.pprint(real_gap(df))
举例输出:
{'进球差(场均)': 0.6,
'xG差(场均)': 0.55,
'p值': 0.0012,
'显著': '✅ 显著',
"Cohen's d": 1.82}
- A 队场均多进 6 球,xG 多 55
- p = 0.0012 < 0.05 → 差距统计显著
- Cohen's d = 1.82 → 效应量很大
- 👉 A 队真实实力明显强于 B 队
关键要点总结
- 单场比分不可靠 —— 必须多场综合
- xG 比进球更稳定 —— 剔除运气
- 要检验显著性 —— 差异可能是随机波动
- 贝叶斯给出概率分布 —— 而不是单一数字
- Elo 用于长期实力 —— 反映稳定水平
- 同时看效应量和 p 值 —— 避免被大样本误导
真实差距 = 期望值差 + 显著性检验 + 效应量大小 + 概率分布,四者综合才能下结论。