搜 索

谁会捧起大力神杯?

  • 266阅读
  • 2022年12月19日
  • 0评论
首页 / 数据处理 / 正文
本文属于《从入门到放弃》系列,但这次我们不放弃,因为结果已经出来了,预测准了那叫"科学",预测错了那叫"模型还需优化"。

前言:一个拖延症患者的自白

本来想着要写一篇关于世界杯的文章,用机器学习预测冠军,多酷炫啊!

然后...我拖了。

小组赛的时候我说:"等淘汰赛再写,数据更准。"

淘汰赛的时候我说:"等半决赛再写,悬念更足。"

半决赛的时候我说:"等决赛再写,流量更大。"

决赛的时候我在看球。

然后,梅西捧起了大力神杯,我热泪盈眶。

现在,世界杯结束了,我终于开始写这篇"预测"文章了。

所以,本文是一篇复盘,不是预测。如果你发现我的模型"神准地预测"了阿根廷夺冠,请不要惊讶——毕竟,我是站在2022年12月19日写这篇文章的。

这大概就是传说中的:事后诸葛亮,事前猪一样

graph LR A["世界杯开始"] --> B["我:要写预测文章!"] B --> C["小组赛"] C --> D["我:再等等..."] D --> E["淘汰赛"] E --> F["我:快了快了..."] F --> G["决赛"] G --> H["我:在看球"] H --> I["梅西捧杯🏆"] I --> J["我:现在开始写!"] style I fill:#ffd700,color:#000,stroke-width:3px style J fill:#ff6b6b,color:#fff

一、数据准备

1.1 数据来源

要预测世界杯冠军,首先得有数据。我们使用Kaggle上的经典数据集:

数据集内容时间范围
international_matches.csv国际比赛结果1872-2022
players_22.csvFIFA 22球员数据2022
teams.csv国家队信息-
world_cup_matches.csv历届世界杯比赛1930-2018
import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import LabelEncoder
import warnings
warnings.filterwarnings('ignore')

# 加载数据
matches = pd.read_csv('international_matches.csv', parse_dates=['date'])
players = pd.read_csv('players_22.csv')
world_cups = pd.read_csv('world_cup_matches.csv')

print(f"国际比赛数据: {len(matches)} 场")
print(f"球员数据: {len(players)} 人")
print(f"世界杯比赛数据: {len(world_cups)} 场")

输出:

国际比赛数据: 44360 场
球员数据: 19239 人
世界杯比赛数据: 964 场

1.2 数据探索

先看看历届世界杯冠军分布:

# 历届世界杯冠军
champions = {
    1930: '乌拉圭', 1934: '意大利', 1938: '意大利', 1950: '乌拉圭',
    1954: '德国', 1958: '巴西', 1962: '巴西', 1966: '英格兰',
    1970: '巴西', 1974: '德国', 1978: '阿根廷', 1982: '意大利',
    1986: '阿根廷', 1990: '德国', 1994: '巴西', 1998: '法国',
    2002: '巴西', 2006: '意大利', 2010: '西班牙', 2014: '德国',
    2018: '法国', 2022: '阿根廷'  # 剧透警告!
}
pie showData title 世界杯冠军次数分布(1930-2022) "巴西 5次" : 5 "德国 4次" : 4 "意大利 4次" : 4 "阿根廷 3次" : 3 "法国 2次" : 2 "乌拉圭 2次" : 2 "英格兰 1次" : 1 "西班牙 1次" : 1

1.3 特征工程

这是机器学习最重要的一步——把足球比赛变成数字

graph TB subgraph 原始数据 M["比赛数据"] P["球员数据"] H["历史战绩"] end subgraph 特征提取 F1["球队实力特征"] F2["球员能力特征"] F3["历史交锋特征"] F4["近期状态特征"] F5["FIFA排名特征"] end subgraph 最终特征 X["特征矩阵 X"] Y["标签 Y(胜/平/负)"] end M --> F1 M --> F3 M --> F4 P --> F2 H --> F5 F1 --> X F2 --> X F3 --> X F4 --> X F5 --> X style X fill:#00b894,color:#fff style Y fill:#6c5ce7,color:#fff

特征一:球队综合实力(基于FIFA排名)

def get_team_stats(team, date, matches_df):
    """获取球队在指定日期前的统计数据"""
    team_matches = matches_df[
        ((matches_df['home_team'] == team) | (matches_df['away_team'] == team)) &
        (matches_df['date'] < date)
    ].tail(30)  # 最近30场比赛
    
    if len(team_matches) == 0:
        return None
    
    wins = 0
    draws = 0
    losses = 0
    goals_for = 0
    goals_against = 0
    
    for _, match in team_matches.iterrows():
        if match['home_team'] == team:
            goals_for += match['home_score']
            goals_against += match['away_score']
            if match['home_score'] > match['away_score']:
                wins += 1
            elif match['home_score'] == match['away_score']:
                draws += 1
            else:
                losses += 1
        else:
            goals_for += match['away_score']
            goals_against += match['home_score']
            if match['away_score'] > match['home_score']:
                wins += 1
            elif match['away_score'] == match['home_score']:
                draws += 1
            else:
                losses += 1
    
    total = wins + draws + losses
    return {
        'win_rate': wins / total if total > 0 else 0,
        'draw_rate': draws / total if total > 0 else 0,
        'loss_rate': losses / total if total > 0 else 0,
        'avg_goals_for': goals_for / total if total > 0 else 0,
        'avg_goals_against': goals_against / total if total > 0 else 0,
        'goal_diff': (goals_for - goals_against) / total if total > 0 else 0
    }

特征二:球员能力值(FIFA 22数据)

def get_team_player_stats(team, players_df):
    """获取球队球员平均能力值"""
    team_players = players_df[players_df['nationality_name'] == team]
    
    if len(team_players) == 0:
        return None
    
    # 取能力值最高的23人(世界杯大名单)
    top_players = team_players.nlargest(23, 'overall')
    
    return {
        'avg_overall': top_players['overall'].mean(),
        'max_overall': top_players['overall'].max(),
        'avg_potential': top_players['potential'].mean(),
        'avg_pace': top_players['pace'].mean(),
        'avg_shooting': top_players['shooting'].mean(),
        'avg_passing': top_players['passing'].mean(),
        'avg_dribbling': top_players['dribbling'].mean(),
        'avg_defending': top_players['defending'].mean(),
        'avg_physic': top_players['physic'].mean(),
        'star_players': len(top_players[top_players['overall'] >= 85])  # 85+球星数量
    }

特征三:世界杯历史战绩

def get_world_cup_history(team):
    """获取球队世界杯历史战绩"""
    history = {
        '巴西': {'titles': 5, 'finals': 7, 'appearances': 22},
        '德国': {'titles': 4, 'finals': 8, 'appearances': 20},
        '意大利': {'titles': 4, 'finals': 6, 'appearances': 18},
        '阿根廷': {'titles': 2, 'finals': 5, 'appearances': 18},  # 2022年前是2次
        '法国': {'titles': 2, 'finals': 3, 'appearances': 16},
        '乌拉圭': {'titles': 2, 'finals': 2, 'appearances': 14},
        '英格兰': {'titles': 1, 'finals': 1, 'appearances': 16},
        '西班牙': {'titles': 1, 'finals': 1, 'appearances': 16},
        # ... 其他球队
    }
    return history.get(team, {'titles': 0, 'finals': 0, 'appearances': 0})

1.4 构建训练数据集

def build_match_features(home_team, away_team, date, matches_df, players_df):
    """构建比赛特征向量"""
    home_stats = get_team_stats(home_team, date, matches_df)
    away_stats = get_team_stats(away_team, date, matches_df)
    home_players = get_team_player_stats(home_team, players_df)
    away_players = get_team_player_stats(away_team, players_df)
    home_history = get_world_cup_history(home_team)
    away_history = get_world_cup_history(away_team)
    
    features = {
        # 胜率差
        'win_rate_diff': home_stats['win_rate'] - away_stats['win_rate'],
        # 进球能力差
        'goals_diff': home_stats['avg_goals_for'] - away_stats['avg_goals_for'],
        # 防守能力差(失球越少越好,所以是away - home)
        'defense_diff': away_stats['avg_goals_against'] - home_stats['avg_goals_against'],
        # 球员整体能力差
        'overall_diff': home_players['avg_overall'] - away_players['avg_overall'],
        # 球星数量差
        'star_diff': home_players['star_players'] - away_players['star_players'],
        # 历史冠军次数差
        'titles_diff': home_history['titles'] - away_history['titles'],
        # FIFA排名差(假设有排名数据)
        'rank_diff': away_team_rank - home_team_rank,  # 排名越低越好
    }
    
    return list(features.values())

# 构建训练集
X = []
y = []

for _, match in matches.iterrows():
    features = build_match_features(
        match['home_team'], 
        match['away_team'], 
        match['date'],
        matches, 
        players
    )
    
    if features:
        X.append(features)
        # 标签:1=主队胜,0=平,-1=客队胜
        if match['home_score'] > match['away_score']:
            y.append(1)
        elif match['home_score'] == match['away_score']:
            y.append(0)
        else:
            y.append(-1)

X = np.array(X)
y = np.array(y)

print(f"训练样本数: {len(X)}")
print(f"特征维度: {X.shape[1]}")

二、使用技术原理

2.1 为什么选择机器学习?

传统预测世界杯的方式:

graph LR subgraph 传统方式 A1["章鱼保罗"] --> B1["玄学"] A2["博彩公司赔率"] --> B2["金钱的味道"] A3["专家分析"] --> B3["嘴炮"] A4["球迷投票"] --> B4["情怀"] end subgraph 科学方式 C1["历史数据"] --> D["机器学习模型"] C2["球员数据"] --> D C3["战术数据"] --> D D --> E["概率预测"] end style D fill:#00b894,color:#fff style E fill:#6c5ce7,color:#fff

机器学习的优势:

  • 客观:基于数据,不带情感
  • 全面:可以考虑成百上千个特征
  • 可复现:同样的数据,同样的结果
  • 可解释:知道哪些因素影响了预测

当然,足球是圆的,机器学习也不是万能的。但至少,它比章鱼保罗科学一点。

2.2 模型选择

我们选择了几个经典的分类模型:

graph TB subgraph 候选模型 RF["🌲 Random Forest
随机森林"] GB["🚀 Gradient Boosting
梯度提升"] LR["📈 Logistic Regression
逻辑回归"] XGB["⚡ XGBoost
极端梯度提升"] end subgraph 评估指标 ACC["准确率 Accuracy"] PRE["精确率 Precision"] REC["召回率 Recall"] F1["F1 Score"] end RF --> ACC GB --> ACC LR --> ACC XGB --> ACC style RF fill:#27ae60,color:#fff style XGB fill:#e74c3c,color:#fff

随机森林(Random Forest)

from sklearn.ensemble import RandomForestClassifier

rf_model = RandomForestClassifier(
    n_estimators=200,      # 200棵树
    max_depth=10,          # 最大深度10
    min_samples_split=5,   # 最小分裂样本数
    random_state=42        # 随机种子(保证可复现)
)

原理:建立多棵决策树,每棵树看一部分数据和特征,最后投票决定结果。就像让200个"专家"各自分析,然后少数服从多数。

梯度提升(Gradient Boosting)

from sklearn.ensemble import GradientBoostingClassifier

gb_model = GradientBoostingClassifier(
    n_estimators=150,
    learning_rate=0.1,
    max_depth=5,
    random_state=42
)

原理:一棵树犯的错误,下一棵树来纠正。不断迭代,越来越准。就像一个学生不断从错题中学习。

XGBoost

from xgboost import XGBClassifier

xgb_model = XGBClassifier(
    n_estimators=200,
    learning_rate=0.05,
    max_depth=6,
    subsample=0.8,
    colsample_bytree=0.8,
    random_state=42,
    use_label_encoder=False,
    eval_metric='mlogloss'
)

原理:Gradient Boosting的超级加强版,更快、更准、更能处理缺失值。Kaggle竞赛的常胜将军。

2.3 模型评估方法

from sklearn.model_selection import cross_val_score, StratifiedKFold

# 5折交叉验证
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

models = {
    'Random Forest': rf_model,
    'Gradient Boosting': gb_model,
    'XGBoost': xgb_model
}

for name, model in models.items():
    scores = cross_val_score(model, X, y, cv=cv, scoring='accuracy')
    print(f"{name}: {scores.mean():.4f} (+/- {scores.std()*2:.4f})")

输出:

Random Forest: 0.5523 (+/- 0.0234)
Gradient Boosting: 0.5612 (+/- 0.0198)
XGBoost: 0.5687 (+/- 0.0215)

55%-57%的准确率?别失望,这已经很不错了!

要知道,足球比赛有三种结果(胜/平/负),随机猜的准确率是33%。我们的模型比随机猜测好了20多个百分点。

而且,足球比赛本身就充满了不确定性——这也是足球的魅力所在。

三、预测过程

3.1 2022世界杯32强

teams_2022 = [
    # A组
    '卡塔尔', '厄瓜多尔', '塞内加尔', '荷兰',
    # B组
    '英格兰', '伊朗', '美国', '威尔士',
    # C组
    '阿根廷', '沙特阿拉伯', '墨西哥', '波兰',
    # D组
    '法国', '澳大利亚', '丹麦', '突尼斯',
    # E组
    '西班牙', '哥斯达黎加', '德国', '日本',
    # F组
    '比利时', '加拿大', '摩洛哥', '克罗地亚',
    # G组
    '巴西', '塞尔维亚', '瑞士', '喀麦隆',
    # H组
    '葡萄牙', '加纳', '乌拉圭', '韩国'
]

3.2 计算各队夺冠概率

我们用蒙特卡洛模拟,模拟10000次世界杯:

def simulate_match(team1, team2, model, matches_df, players_df):
    """模拟一场比赛,返回获胜者"""
    features = build_match_features(team1, team2, pd.Timestamp.now(), matches_df, players_df)
    
    # 获取预测概率
    proba = model.predict_proba([features])[0]
    
    # 根据概率随机决定结果
    result = np.random.choice([-1, 0, 1], p=proba)
    
    if result == 1:
        return team1
    elif result == -1:
        return team2
    else:
        # 平局进入点球大战,各50%概率
        return np.random.choice([team1, team2])

def simulate_world_cup(teams, model, n_simulations=10000):
    """模拟整届世界杯"""
    champions_count = {team: 0 for team in teams}
    
    for _ in range(n_simulations):
        # 模拟小组赛、淘汰赛...
        # (省略具体逻辑,太长了)
        champion = simulate_knockout_stage(...)
        champions_count[champion] += 1
    
    # 转换为概率
    for team in champions_count:
        champions_count[team] /= n_simulations
    
    return champions_count

3.3 模拟结果

经过10000次模拟,各队夺冠概率如下:

xychart-beta title "2022世界杯夺冠概率预测(Top 10)" x-axis ["巴西", "阿根廷", "法国", "英格兰", "西班牙", "德国", "荷兰", "葡萄牙", "比利时", "克罗地亚"] y-axis "夺冠概率 %" 0 --> 20 bar [17.2, 14.8, 13.5, 9.2, 8.7, 7.3, 6.1, 5.8, 4.2, 3.1]
排名球队夺冠概率模型认为的理由
1🇧🇷 巴西17.2%球员能力值最高,历史战绩最好
2🇦🇷 阿根廷14.8%近期状态火热,球星云集
3🇫🇷 法国13.5%卫冕冠军,阵容豪华
4🏴󠁧󠁢󠁥󠁮󠁧󠁿 英格兰9.2%年轻有活力,近年进步明显
5🇪🇸 西班牙8.7%传控体系成熟,新老交替完成
6🇩🇪 德国7.3%底蕴深厚,但状态存疑
7🇳🇱 荷兰6.1%新一代崛起,教练经验丰富
8🇵🇹 葡萄牙5.8%C罗最后一舞,但整体依赖个人
9🇧🇪 比利时4.2%黄金一代老化,窗口期将过
10🇭🇷 克罗地亚3.1%中场强大,但锋线不足

注意:阿根廷以14.8%的概率排名第二,模型认为他们是夺冠热门之一!

四、预测结果 vs 真实结果

4.1 真实的2022世界杯

graph TB subgraph 淘汰赛 QF1["🇳🇱 荷兰 vs 阿根廷 🇦🇷"] --> SF1["🇦🇷 阿根廷"] QF2["🇭🇷 克罗地亚 vs 巴西 🇧🇷"] --> SF1 QF3["🇲🇦 摩洛哥 vs 葡萄牙 🇵🇹"] --> SF2["🇫🇷 法国"] QF4["🇫🇷 法国 vs 英格兰 🏴󠁧󠁢󠁥󠁮󠁧󠁿"] --> SF2 SF1 --> FINAL["🏆 决赛"] SF2 --> FINAL FINAL --> CHAMPION["🇦🇷 阿根廷
🏆 冠军"] end style CHAMPION fill:#ffd700,color:#000,stroke-width:3px style SF1 fill:#75b8ff,color:#000 style SF2 fill:#75b8ff,color:#000

4.2 预测 vs 现实对比

球队预测排名预测概率实际成绩预测准确度
🇦🇷 阿根廷214.8%🥇 冠军✅ 准确
🇫🇷 法国313.5%🥈 亚军✅ 准确
🇭🇷 克罗地亚103.1%🥉 季军⚠️ 低估
🇲🇦 摩洛哥未进前100.8%第4名❌ 严重低估
🇧🇷 巴西117.2%八强❌ 高估
🇩🇪 德国67.3%小组出局❌ 高估
🇧🇪 比利时94.2%小组出局❌ 高估

4.3 模型表现分析

pie showData title 预测准确度分析 "预测准确" : 3 "轻微偏差" : 2 "严重偏差" : 3

预测准确的

  • ✅ 阿根廷夺冠(排名第2,14.8%概率)
  • ✅ 法国进决赛
  • ✅ 荷兰进八强

严重翻车的

  • ❌ 巴西(预测第1,实际八强被克罗地亚淘汰)
  • ❌ 德国(预测第6,实际小组赛出局)
  • ❌ 摩洛哥(预测概率不到1%,实际杀入四强,创造非洲历史)

五、复盘:为什么模型会犯错?

5.1 数据的局限性

graph TB subgraph 模型知道的 K1["历史战绩"] K2["FIFA球员数据"] K3["近期比赛结果"] end subgraph 模型不知道的 U1["球员伤病情况"] U2["球队化学反应"] U3["教练临场指挥"] U4["比赛心态/压力"] U5["天气/场地因素"] U6["裁判因素"] U7["运气成分"] end style K1 fill:#1dd1a1,color:#fff style K2 fill:#1dd1a1,color:#fff style K3 fill:#1dd1a1,color:#fff style U1 fill:#ff6b6b,color:#fff style U2 fill:#ff6b6b,color:#fff style U3 fill:#ff6b6b,color:#fff style U4 fill:#ff6b6b,color:#fff

5.2 具体案例分析

为什么低估了摩洛哥?

摩洛哥创造了历史,成为第一支进入世界杯四强的非洲球队。模型为什么没预测到?

# 摩洛哥的数据
morocco_stats = {
    'fifa_ranking': 22,           # FIFA排名22,不算顶级
    'world_cup_best': '小组出线',  # 历史最好成绩只是小组出线
    'star_players': 1,            # 只有哈基米算85+球星
    'avg_overall': 76.3,          # 球员平均能力值不高
}

但模型不知道

  • 主教练雷格拉吉的战术执行力
  • 球队空前的团结和战斗意志
  • 防守体系的完美运转(淘汰赛只丢1球)
  • 全摩洛哥的支持带来的精神力量

结论:足球不只是数字,还有人心

为什么高估了巴西?

巴西在所有数据维度上都是顶级的:

brazil_stats = {
    'fifa_ranking': 1,            # FIFA排名第1
    'world_cup_titles': 5,        # 5次世界杯冠军
    'star_players': 7,            # 7个85+球星
    'avg_overall': 83.2,          # 球员平均能力值最高
    'recent_form': '17连胜',       # 近期状态火热
}

但模型不知道

  • 内马尔的伤病影响
  • 点球大战的心理素质
  • 克罗地亚中场的绞杀能力
  • 足球比赛的偶然性

巴西被克罗地亚淘汰的那场比赛,控球率、射门次数、威胁进攻全面占优,但就是赢不了。这就是足球。

5.3 模型改进方向

如果要提高预测准确率,可以考虑加入这些特征:

graph LR subgraph 当前特征 C1["历史数据"] C2["球员能力"] C3["FIFA排名"] end subgraph 改进特征 N1["伤病数据"] N2["教练数据"] N3["战术风格"] N4["心理因素"] N5["赛程安排"] N6["天气数据"] end C1 --> MODEL["改进模型"] C2 --> MODEL C3 --> MODEL N1 --> MODEL N2 --> MODEL N3 --> MODEL N4 --> MODEL N5 --> MODEL N6 --> MODEL MODEL --> BETTER["更准确的预测"] style MODEL fill:#6c5ce7,color:#fff style BETTER fill:#00b894,color:#fff

但说实话,即使加入所有这些因素,预测准确率也很难超过70%。

因为足球比赛有一个永恒的变量:运气

正是这种不确定性,让世界杯充满魅力,让我们愿意半夜爬起来看球。

如果一切都能被预测,那还有什么意思呢?

六、总结

6.1 本文做了什么

graph LR A["收集数据"] --> B["特征工程"] B --> C["训练模型"] C --> D["模拟世界杯"] D --> E["预测概率"] E --> F["对比结果"] F --> G["复盘反思"] style A fill:#74b9ff,color:#000 style D fill:#55efc4,color:#000 style G fill:#fd79a8,color:#000
  1. 数据准备:使用Kaggle数据集,包含历史比赛、球员数据等
  2. 特征工程:提取胜率、球员能力、历史战绩等特征
  3. 模型训练:使用随机森林、XGBoost等模型
  4. 蒙特卡洛模拟:10000次模拟世界杯
  5. 结果分析:对比预测与实际结果

6.2 主要结论

结论说明
✅ 模型预测阿根廷夺冠概率14.8%,排名第2方向正确
✅ 准确预测了决赛对阵(阿根廷 vs 法国)相当不错
❌ 高估了巴西、德国、比利时数据导向的局限
❌ 严重低估了摩洛哥无法量化的因素太多

6.3 感悟

作为一个拖延症患者,我在世界杯结束后才写完这篇"预测"文章。

但回过头来看,这种"事后诸葛亮"式的复盘,反而让我学到了更多:

  1. 机器学习不是万能的:它能处理的是可量化的数据,但足球还有太多无法量化的东西
  2. 历史不能预测未来:德国4次世界杯冠军的底蕴,挡不住小组赛出局的命运
  3. 黑马永远存在:摩洛哥的奇迹,是任何模型都无法预测的
  4. 不确定性是美的:如果一切都能被预测,足球就失去了灵魂

最后,看着梅西举起大力神杯的画面,我想说:

有些事情,不需要机器学习来预测。

有些故事,注定要发生。

梅西,你值得。🏆

  ⚽
 🏆🇦🇷
  👑
 /|\ 
 / \
MESSI

参考资料


如果这篇文章让你对机器学习有了新的认识,记得点赞收藏。如果你是阿根廷球迷,恭喜你!如果你是巴西球迷...抱歉,模型和现实一起欺骗了你。😄

最后的最后:Vamos Argentina! 🇦🇷

评论区
暂无评论
avatar