Python量化主队球迷人数对比赛结果的影响
下面我用一个完整案例,从数据构造→特征工程→建模→因果推断四个层面演示如何量化"主队球迷人数"对比赛结果的影响。

问题定义
目标:量化主队球迷人数(或上座率)对主队胜率/净胜球的影响。
难点:
- 球迷人数不是随机分配的(强队球迷多),直接回归会有内生性偏误
- 需要控制球队实力、对手强度等混淆变量
- 最好用因果推断方法而非单纯相关分析
数据构造(示例)
import numpy as np
import pandas as pd
import statsmodels.api as sm
import statsmodels.formula.api as smf
np.random.seed(42)
n = 1000 # 1000场比赛
# 模拟:球队实力(1-10)
team_strength = np.random.uniform(1, 10, n)
opponent_strength = np.random.uniform(1, 10, n)
# 球场容量(固定)
stadium_capacity = np.random.choice([20000, 40000, 60000], n)
# 球迷人数:受球队实力影响(强队吸引人多)+ 随机波动
attendance = (
stadium_capacity * 0.4
+ team_strength * 1500
+ np.random.normal(0, 3000, n)
)
attendance = np.clip(attendance, 0, stadium_capacity)
attendance_rate = attendance / stadium_capacity
# 比赛结果:受实力差 + 球迷人数影响(真实因果效应)
strength_diff = team_strength - opponent_strength
goal_diff = (
1.2 * strength_diff
+ 0.8 * attendance_rate * 3 # 真实因果效应:上座率每+1,净胜球+0.8*3
+ np.random.normal(0, 1.5, n)
)
df = pd.DataFrame({
'team_strength': team_strength,
'opponent_strength': opponent_strength,
'strength_diff': strength_diff,
'attendance': attendance,
'attendance_rate': attendance_rate,
'stadium_capacity': stadium_capacity,
'goal_diff': goal_diff,
'win': (goal_diff > 0).astype(int),
})
方法一:朴素OLS(有偏)
model_naive = smf.ols('goal_diff ~ attendance_rate', data=df).fit()
print(model_naive.params)
# attendance_rate 系数会被高估,因为它吸收了球队实力的影响
问题:球迷多 → 球队强 → 更容易赢,OLS把"球队实力"的功劳算到了球迷头上。
方法二:控制混淆变量
model_ctrl = smf.ols(
'goal_diff ~ attendance_rate + strength_diff + opponent_strength',
data=df
).fit()
print(model_ctrl.summary())
加入实力差后,attendance_rate 的系数会更接近真实值,但仍可能有剩余混淆(如士气、天气等未观测因素)。
方法三:工具变量法(IV/2SLS)—— 推荐
思路:找一个只通过"球迷人数"影响比赛结果、不直接影响结果的变量。
经典IV:天气(下雨人少,但天气本身不影响球员实力)。
# 加入工具变量:天气(0晴 1雨)
df['rainy'] = np.random.binomial(1, 0.3, n)
# 雨天减少球迷
df['attendance_rate'] = np.clip(
df['attendance_rate'] - 0.15 * df['rainy'], 0, 1
)
# 重新计算goal_diff(因果效应不变,但attendance被外生变化)
df['goal_diff'] = (
1.2 * df['strength_diff']
+ 2.4 * df['attendance_rate']
+ np.random.normal(0, 1.5, n)
)
from linearmodels.iv import IV2SLS
iv_model = IV2SLS.from_formula(
'goal_diff ~ 1 + strength_diff + [attendance_rate ~ rainy]',
data=df
).fit()
print(iv_model.summary)
IV估计出的 attendance_rate 系数就是因果效应的无偏估计。
方法四:倾向得分匹配(PSM)
把"高上座率"与"低上座率"比赛在实力、对手等维度配对:
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import NearestNeighbors
# 定义处理组:上座率是否高于中位数
median = df['attendance_rate'].median()
df['high_attendance'] = (df['attendance_rate'] > median).astype(int)
# 估计倾向得分
ps_model = LogisticRegression()
ps_model.fit(df[['strength_diff', 'opponent_strength']], df['high_attendance'])
df['ps'] = ps_model.predict_proba(df[['strength_diff', 'opponent_strength']])[:, 1]
# 最近邻匹配
treated = df[df['high_attendance'] == 1]
control = df[df['high_attendance'] == 0]
nn = NearestNeighbors(n_neighbors=1).fit(control[['ps']])
_, idx = nn.kneighbors(treated[['ps']])
matched_control = control.iloc[idx.flatten()]
# 计算ATT(处理组的平均处理效应)
att = treated['goal_diff'].mean() - matched_control['goal_diff'].mean()
print(f'ATT(高上座率的因果效应): {att:.3f}')
方法五:分位数回归看非线性
球迷人数可能存在边际递减(坐满后效果边际下降):
import statsmodels.quantreg as qr
for q in [0.25, 0.5, 0.75]:
res = qr.quantreg('goal_diff ~ attendance_rate + strength_diff', df).fit(q=q)
print(f'分位数 {q}: 系数 = {res.params["attendance_rate"]:.3f}')
实战注意事项
| 问题 | 解决思路 |
|---|---|
| 内生性(强队人多) | IV、PSM、固定效应 |
| 非线性 | 加平方项、分位数回归、GAM |
| 球队个体效应 | 面板固定效应 PanelOLS |
| 数据来源 | Transfermarkt、FBref、football-data.co.uk |
| 真正随机 | 疫情期间空场/限流=天然实验(DiD) |
DiD 思路(疫情空场):
# 处理组:疫情期比赛;对照组:疫情前 # goal_diff ~ treated + post + treated:post + controls # treated:post 系数 = 球迷的真实因果效应
结论展示模板
控制球队实力、对手强度后,上座率每提高10个百分点,主队净胜球平均增加约 24 球(95% CI: [0.18, 0.30]),胜率提高约 6%,IV估计结果与PSM匹配结果一致,说明该效应稳健。
如果你需要,我可以针对真实数据集(如英超)写一个可直接运行的完整脚本,或者在 面板固定效应 或 DiD(疫情空场) 上进一步展开,需要哪一部分?