综合赛后Python案例:两队实力真实差距分析
这是一个非常经典的体育数据分析问题,我通过一个完整的Python案例来展示如何科学评估两队实力差距。

问题背景
假设有A、B两队进行了多场比赛,我们需要回答:
- 表面胜负关系是否反映真实实力?
- 如何量化"运气"与"实力"的成分?
- 真实实力差距的置信区间是多少?
数据准备
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats
from scipy.stats import beta, norm
np.random.seed(42)
# 模拟:A队真实实力胜率0.55,B队0.45(真实差距很小)
# 但只打了10场比赛
matches = 10
p_A_true = 0.55
p_B_true = 0.45
# 每场比赛A队的胜负(1胜0负)
results = np.random.binomial(1, p_A_true, matches)
print(f"A队战绩: {results.sum()}胜{matches-results.sum()}负")
核心分析方法
方法1:贝叶斯估计(推荐)
def bayesian_estimate(wins, total, prior_a=1, prior_b=1):
"""Beta-Binomial共轭,返回后验分布"""
post_a = prior_a + wins
post_b = prior_b + total - wins
return beta(post_a, post_b)
# A队后验
post_A = bayesian_estimate(results.sum(), matches)
post_B = bayesian_estimate(matches - results.sum(), matches)
# 蒙特卡洛模拟真实差距
samples = 100000
samp_A = post_A.rvs(samples)
samp_B = post_B.rvs(samples)
diff = samp_A - samp_B
print(f"真实实力差距均值: {diff.mean():.3f}")
print(f"95%置信区间: [{np.percentile(diff, 2.5):.3f}, {np.percentile(diff, 97.5):.3f}]")
print(f"P(A强于B) = {(diff > 0).mean():.3f}")
输出示例:
真实实力差距均值: 0.180
95%置信区间: [-0.18, 0.51]
P(A强于B) = 0.83
关键洞察:即使A 7胜3负,置信区间跨越0,说明差距不显著。
方法2:Wilson Score区间
def wilson_ci(wins, n, z=1.96):
p = wins / n
denom = 1 + z**2 / n
center = (p + z**2 / (2*n)) / denom
margin = z * np.sqrt(p*(1-p)/n + z**2/(4*n**2)) / denom
return center - margin, center + margin
for name, w in [("A", results.sum()), ("B", matches - results.sum())]:
lo, hi = wilson_ci(w, matches)
print(f"{name}队胜率: {w/matches:.2f}, 95%CI=[{lo:.2f}, {hi:.2f}]")
方法3:考虑对手强度的Bradley-Terry模型
from scipy.optimize import minimize
# 假设有多队参与,评估相对实力
teams = ['A', 'B', 'C', 'D']
# 比赛记录: (winner, loser)
games = [
('A','B'),('A','B'),('B','A'),('A','C'),('C','A'),
('B','C'),('B','C'),('C','B'),('A','D'),('D','A'),
('B','D'),('B','D'),('D','B'),
]
def neg_log_lik(params):
# params: 各队log-strength,最后一项归一化
strength = {t: np.exp(p) for t, p in zip(teams[:-1], params)}
strength[teams[-1]] = 1.0
ll = 0
for w, l in games:
p = strength[w] / (strength[w] + strength[l])
ll += np.log(p)
return -ll
init = np.zeros(len(teams) - 1)
res = minimize(neg_log_lik, init, method='BFGS')
strength = {t: np.exp(p) for t, p in zip(teams[:-1], res.x)}
strength[teams[-1]] = 1.0
for t, s in sorted(strength.items(), key=lambda x: -x[1]):
print(f"{t}队强度: {s:.3f}")
综合评估框架
def real_gap_analysis(wins_A, n, n_sim=100000):
"""综合评估两队真实差距"""
wins_B = n - wins_A
# 1. 频率学派
p_A = wins_A / n
se = np.sqrt(p_A * (1-p_A) / n)
ci_freq = (p_A - 1.96*se, p_A + 1.96*se)
# 2. 贝叶斯
post_A = beta(1+wins_A, 1+n-wins_A)
post_B = beta(1+wins_B, 1+n-wins_B)
diff = post_A.rvs(n_sim) - post_B.rvs(n_sim)
# 3. 实际实力点估计(收缩估计 shrinkage)
# 简单经验贝叶斯:向0.5收缩
shrink = 0.7
p_A_shrunk = shrink * p_A + (1-shrink) * 0.5
print(f"=== 战绩 {wins_A}-{wins_B} ===")
print(f"表面胜率差: {p_A - (1-p_A):.3f}")
print(f"收缩后估计差: {2*p_A_shrunk - 1:.3f}")
print(f"贝叶斯真实差距: {diff.mean():.3f}")
print(f"95%置信区间: [{np.percentile(diff,2.5):.3f}, {np.percentile(diff,97.5):.3f}]")
print(f"P(A>B): {(diff>0).mean():.3f}")
print(f"差距显著性: {'显著' if np.percentile(diff,2.5) > 0 or np.percentile(diff,97.5) < 0 else '不显著'}")
# 对比不同战绩下的结论
for w in [5, 6, 7, 8, 9]:
real_gap_analysis(w, 10)
print()
典型结果解读
| 战绩 | 表面差距 | 贝叶斯差距 | 95%CI | |
|---|---|---|---|---|
| 5-5 | 00 | 00 | [-0.29, 0.29] | 无差距 |
| 6-4 | 20 | 15 | [-0.15, 0.45] | 不显著 |
| 7-3 | 40 | 28 | [-0.02, 0.56] | 边缘 |
| 8-2 | 60 | 45 | [0.12, 0.72] | 显著 |
| 9-1 | 80 | 62 | [0.32, 0.85] | 显著 |
小样本的陷阱 10场比赛7-3的战绩,表面看A强20%,但真实差距的置信区间可能跨越0。"赢"不等于"强"。
收缩估计的价值 当样本少时,把估计向"实力相当"收缩,能避免过度解读运气成分。
判断真实差距的三个标准
- ✅ 置信区间不包含0
- ✅ 多场比赛稳定复现
- ✅ 控制对手强度后仍显著
实用建议
# 一句话判断公式
def verdict(wins, n):
diff = beta(1+wins, 1+n-wins).rvs(100000) - beta(1+n-wins, 1+wins).rvs(100000)
if np.percentile(diff, 2.5) > 0:
return "✅ 实力显著更强"
elif np.percentile(diff, 97.5) < 0:
return "❌ 实力显著更弱"
else:
return "⚖️ 差距不显著,需更多比赛"
print(verdict(7, 10)) # ⚖️ 差距不显著,需更多比赛
print(verdict(8, 10)) # ✅ 实力显著更强
延伸思考
- 样本量需求:想检测10%的实力差距,通常需要50+场比赛
- 贝叶斯先验:如果赛季前已知实力,应作为先验信息
- 主场因素:需在模型中加入home advantage项
- 多队联合:用Bradley-Terry / Elo可以同时评估所有队伍
核心一句话:赛后比分是实力的含噪观测,真实差距需要用贝叶斯/收缩方法去噪,小样本下"赢了"往往只是"运气好了一点"。