小联赛数据缺失问题的应对方案(Python案例)
小联赛(如低级别联赛、冷门联赛、青年队比赛)普遍存在数据稀疏、缺失严重的问题,以下是系统性的应对思路和 Python 实现。

问题分类与应对策略
| 缺失类型 | 典型场景 | 应对策略 |
|---|---|---|
| 完全无数据 | 新升班马、青年队 | 相似球队迁移、先验估计 |
| 部分比赛缺失 | 数据源不全 | 多重插补、时间衰减 |
| 特征缺失 | 无角球/控球统计 | 降维建模、只用核心特征 |
| 样本量小 | 整个联赛场次少 | 贝叶斯收缩、分层模型 |
核心代码实现
数据诊断:先量化缺失情况
import pandas as pd
import numpy as np
import missingno as msno
import matplotlib.pyplot as plt
def diagnose_missing(df: pd.DataFrame):
"""诊断缺失比例,按球队/特征两个维度"""
# 按特征缺失率
feat_missing = df.isnull().mean().sort_values(ascending=False)
print("=== 特征缺失率 ===")
print(feat_missing[feat_missing > 0])
# 按球队缺失率(小球队缺失更严重)
team_missing = df.groupby('home_team').apply(
lambda x: x.isnull().mean().mean()
).sort_values(ascending=False)
print("\n=== 缺失最严重的球队 ===")
print(team_missing.head(10))
msno.matrix(df.sample(min(500, len(df))))
plt.show()
return feat_missing
基础填充:区分"结构性缺失"和"随机缺失"
def smart_impute(df: pd.DataFrame):
"""
小联赛关键:不要盲目用均值填充,要按联赛/主客场分组
"""
df = df.copy()
# 1) 结构性缺失 → 填0(如该联赛根本没有角球统计)
structural_cols = ['corners', 'yellow_cards'] # 举例
for col in structural_cols:
if col in df.columns:
df[col] = df[col].fillna(0)
# 2) 数值型 → 分联赛+主客场的中位数(比全局均值更合理)
num_cols = df.select_dtypes(include=[np.number]).columns
for col in num_cols:
if df[col].isnull().any():
df[col] = df.groupby(['league', 'is_home'])[col].transform(
lambda s: s.fillna(s.median())
)
# 仍缺失则用联赛整体中位数兜底
df[col] = df.groupby('league')[col].transform(
lambda s: s.fillna(s.median())
)
df[col] = df[col].fillna(df[col].median())
return df
进阶:IterativeImputer(利用相关性插补)
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
from sklearn.ensemble import RandomForestRegressor
def iterative_impute(df, feature_cols):
"""
利用特征间相关性(如射门数↔进球数)进行迭代插补
适合部分字段缺失的小联赛
"""
imputer = IterativeImputer(
estimator=RandomForestRegressor(n_estimators=50, random_state=42),
max_iter=10,
random_state=42,
sample_posterior=True, # 小样本下推荐开启
)
df_imputed = df.copy()
df_imputed[feature_cols] = imputer.fit_transform(df[feature_cols])
return df_imputed
关键方法:贝叶斯收缩(解决小样本球队偏差)
这是应对小联赛最有效的方法,小球队样本少→估计不稳定→向全局均值"收缩"。
def bayesian_shrinkage(df, team_col, metric_col, prior_weight=5):
"""
对每支球队的指标做经验贝叶斯收缩
- prior_weight 越大,越向全局均值靠拢(小样本球队更保守)
- 公式: shrunk = (n * team_mean + k * global_mean) / (n + k)
- n=出场数, k=先验强度
"""
global_mean = df[metric_col].mean()
stats = df.groupby(team_col)[metric_col].agg(['mean', 'count'])
stats['shrunk'] = (
(stats['count'] * stats['mean'] + prior_weight * global_mean)
/ (stats['count'] + prior_weight)
)
return stats['shrunk'].to_dict()
# 用法
# shots_shrunk = bayesian_shrinkage(df, 'home_team', 'home_shots', prior_weight=5)
完全无数据球队:相似球队映射
新升班马没有任何顶级联赛数据时,用"相似球队"迁移先验。
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import NearestNeighbors
def find_similar_teams(df_known, new_team_profile, n=3):
"""
根据球队画像(联赛水平、进攻/防守风格等)找相似球队
new_team_profile: dict, 如 {'league_tier': 2, 'avg_goals': 1.2, ...}
"""
features = ['league_tier', 'avg_goals', 'avg_conceded', 'win_rate']
X = df_known[features].values
scaler = StandardScaler().fit(X)
X_scaled = scaler.transform(X)
new_vec = scaler.transform([[
new_team_profile[f] for f in features
]])
nn = NearestNeighbors(n_neighbors=n).fit(X_scaled)
_, idx = nn.kneighbors(new_vec)
return df_known.iloc[idx[0]][['team'] + features]
建模层面:分层贝叶斯(最强方案)
用 PyMC 建模,把小联赛球队参数视为从联赛分布中采样,天然处理小样本。
import pymc as pm
import arviz as az
def hierarchical_poisson_model(df):
"""
分层泊松模型预测进球数
- 球队攻防参数从联赛整体分布采样(自动收缩)
- 小样本球队被拉回联赛均值
"""
teams = pd.Categorical(df['home_team'].append(df['away_team']))
team_idx_home = teams.codes[:len(df)]
team_idx_away = teams.codes[len(df):]
with pm.Model() as model:
# 超先验
mu_att = pm.Normal('mu_att', 0, 1)
sigma_att = pm.HalfNormal('sigma_att', 1)
mu_def = pm.Normal('mu_def', 0, 1)
sigma_def = pm.HalfNormal('sigma_def', 1)
# 球队参数(分层结构实现收缩)
att = pm.Normal('att', mu_att, sigma_att, shape=len(teams))
deff = pm.Normal('def', mu_def, sigma_def, shape=len(teams))
home_adv = pm.Normal('home_adv', 0.2, 0.5)
# 期望进球
lam_home = pm.math.exp(att[team_idx_home] + deff[team_idx_away] + home_adv)
lam_away = pm.math.exp(att[team_idx_away] + deff[team_idx_home])
# 似然(用观测数据,缺失的主客场分别建模)
pm.Poisson('home_goals', lam_home, observed=df['home_goals'].values)
pm.Poisson('away_goals', lam_away, observed=df['away_goals'].values)
trace = pm.sample(1000, tune=1000, return_inferencedata=True)
return model, trace
特征工程兜底:只用"鲁棒特征"
# 小联赛推荐的核心特征(不依赖详细技术统计)
ROBUST_FEATURES = [
'home_goals_avg_last5', # 近5场进球(时间窗最短)
'away_goals_avg_last5',
'home_conceded_avg_last5',
'away_conceded_avg_last5',
'home_win_rate_season', # 赛季胜率
'rest_days_diff', # 休息天数差
'league_tier', # 联赛层级
]
def build_robust_features(df, window=5):
"""滚动窗口 + 赛季累计,避免依赖易缺失的细粒度统计"""
df = df.sort_values('date')
for team_col, prefix in [('home_team', 'home'), ('away_team', 'away')]:
df[f'{prefix}_goals_avg_last{window}'] = (
df.groupby(team_col)['goals_for']
.transform(lambda s: s.shift().rolling(window, min_periods=1).mean())
)
df[f'{prefix}_conceded_avg_last{window}'] = (
df.groupby(team_col)['goals_against']
.transform(lambda s: s.shift().rolling(window, min_periods=1).mean())
)
return df
完整流程建议
诊断缺失 → msno矩阵看清缺失模式
2. 区分缺失类型:
├─ 结构性缺失(该联赛不统计)→ 填0或删除该特征
├─ 随机缺失 → IterativeImputer / 分组中位数
└─ 整支球队无数据 → 相似球队迁移 / 联赛先验
3. 小样本估计 → 贝叶斯收缩(先验权重=5~10)
4. 建模 → 分层贝叶斯 > 简单GradientBoosting
5. 特征 → 只用滚动窗口+赛季累计的鲁棒特征
6. 评估 → 用log loss/Brier,且对小样本球队单独看误差
实战建议(避坑)
- 不要用全局均值填充——小联赛各层级水平差异极大,会污染数据。
- 先验权重
k要调:k 太大所有球队被抹平,太小失去收缩作用,建议 3~10,用交叉验证选。 - 监控数据质量:给每支球队打"数据可信度分"(样本量×字段完整度),低于阈值的不下注/不预测。
- 区分联赛层级:低级别联赛进球分布 ≠ 顶级联赛,先验均值要按层级分别算。
- 时间衰减:小联赛球队变动大,近半年数据权重应远高于两年前(可用指数衰减加权)。
需要我针对某个具体环节(比如分层贝叶斯完整可运行代码,或相似球队特征的自动构建)展开吗?