简介这份资源面向备战2025年美国大学生数学建模竞赛MCM/ICMC题的参赛者与数据分析学习者围绕奥运会奖牌预测这一赛题提供一套完整的机器学习建模方案与部分思路分析。项目以历史奖牌、主办国、运动员及项目数据为基础构建综合预测模型估算未来奥运会各国奖牌数量并剖析影响奖牌分布的关键因素适合希望掌握赛题建模流程与特征分析方法的同学参考。压缩包共33个文件约12.16MB包含11个csv数据文件、8个Python脚本、2个Jupyter Notebook以及xml、md、pdf、docx等配置与说明文档覆盖数据处理、模型训练、结果可视化等环节。资源中提供多版本预测系统代码、Ridge回归对比模型、噪声分析与主办国关联作图脚本并附2028年奖牌预测表及中美德等国分析结果便于读者对照复现建模思路、理解特征工程与模型评估方法。目前已有114人学习下载。1. 奥运奖牌预测这道题为什么每年都有人栽在“数据泄漏”上2025年美国大学生数学建模竞赛C题把“奥运奖牌预测”推到了台前热搜里数学建模、机器学习、预测模型、奥运会奖牌这几个词几乎绑在一起出现。但真正做过这套题的人都知道奖牌预测的难点从来不是模型选型而是历史数据本身极不规整早期奥运会项目设置频繁变动、部分国家经历解体与合并、东道主效应在个别年份极端放大。我见过太多队伍一上来就上XGBoost回归预测模型特征工程却把“当年奖牌数”直接喂进去结果交叉验证R²高得离谱一换测试集就翻车。这篇笔记面向准备复现或改进这套C题方案的从业者把特征构建、模型训练、时序验证和避坑点拆开讲清楚目标是让你拿到一份能跑通、能解释、能写进论文的完整流程。2. 奖牌预测的特征工程从国家-年份面板到可训练矩阵2.1 数据源选择与面板结构设计奥运奖牌数据最可靠的公开来源是Kaggle上的“120 years of Olympic history”数据集包含athlete_events.csv和noc_regions.csv两个文件。前者是运动员级别的明细后者提供国家代码到地区名的映射。做奖牌预测时第一步是把运动员级别数据聚合成“国家-年份-奖牌类型”的三维面板。常见做法是构建一张宽表每行是一个国家在某一届奥运会上的表现列包括金牌数、银牌数、铜牌数、总奖牌数以及从athlete_events里派生出的参赛人数、参赛项目数、运动员平均年龄、女性运动员占比等。这些派生特征往往比原始奖牌数更有预测价值因为它们反映了一个国家在当届的投入规模。import pandas as pd athletes pd.read_csv(athlete_events.csv) regions pd.read_csv(noc_regions.csv) # 合并国家地区信息 df athletes.merge(regions, onNOC, howleft) # 只保留夏季奥运会冬季奥运会项目结构差异太大 df df[df[Season] Summer] # 聚合到国家-年份层面 panel df.groupby([NOC, Year]).agg( athletes_count(ID, nunique), events_count(Event, nunique), sports_count(Sport, nunique), female_ratio(Sex, lambda x: (x F).mean()), avg_age(Age, mean), gold(Medal, lambda x: (x Gold).sum()), silver(Medal, lambda x: (x Silver).sum()), bronze(Medal, lambda x: (x Bronze).sum()), ).reset_index() panel[total_medal] panel[gold] panel[silver] panel[bronze]这段代码的关键在于聚合粒度。按NOC和Year分组后athletes_count用nunique去重避免同一运动员参加多个项目被重复计数。female_ratio和avg_age这类特征在后续分析“影响奖牌分布的关键因素”时可以直接做相关性检验。注意Medal列在未获奖时是NaN用lambda做条件计数时NaN不会被计入任何一类这是正确的。2.2 滞后特征与滚动窗口让模型看到“历史惯性”奖牌数有极强的国家惯性——美国、俄罗斯、中国、德国这些国家几乎每届都在前列。如果只用当届特征预测当届奖牌模型学到的只是“参赛人数多则奖牌多”这种同期相关没有预测意义。正确的做法是构造滞后特征用上一届、上上届的奖牌数和参赛规模来预测当届。panel panel.sort_values([NOC, Year]) # 构造上一届奖牌数滞后1期 panel[prev_total] panel.groupby(NOC)[total_medal].shift(1) panel[prev_gold] panel.groupby(NOC)[gold].shift(1) panel[prev_athletes] panel.groupby(NOC)[athletes_count].shift(1) # 滚动三届均值捕捉中期趋势 panel[roll3_total] ( panel.groupby(NOC)[total_medal] .transform(lambda x: x.shift(1).rolling(3, min_periods1).mean()) ) # 东道主标记 hosts {1896: GRE, 1900: FRA, 1904: USA, 1908: GBR, 1912: SWE, 1920: BEL, 1924: FRA, 1928: NED, 1932: USA, 1936: GER, 1948: GBR, 1952: FIN, 1956: AUS, 1960: ITA, 1964: JPN, 1968: MEX, 1972: GER, 1976: CAN, 1980: URS, 1984: USA, 1988: KOR, 1992: ESP, 1996: USA, 2000: AUS, 2004: GRE, 2008: CHN, 2012: GBR, 2016: BRA, 2020: JPN} panel[is_host] panel.apply( lambda r: 1 if hosts.get(r[Year]) r[NOC] else 0, axis1 )shift(1)保证不会把当届信息泄漏进特征。rolling(3)配合shift(1)是先移一位再滚动这样第t届的滚动均值只用到t-1、t-2、t-3届的数据。东道主效应是奥运预测里少数几个稳定显著的外生变量历届东道主奖牌数普遍比前一届提升20%到40%这个特征必须显式加入否则模型会把东道主爆发误判为随机波动。2.3 处理国家解体与代码变更苏联1992年解体、南斯拉夫分裂、捷克斯洛伐克分家这些历史事件会让NOC代码在时间序列上断裂。如果直接按NOC分组做滞后俄罗斯1996年的prev_total会是NaN因为苏联的URS代码在1992年就消失了。常见做法有两种一是手动维护一张继承映射表把URS的奖牌按比例分配给俄罗斯等继承国二是对断裂国家单独建模或直接剔除早期数据。我一般会选第一种因为奥运奖牌预测的样本量本来就不大剔除太多会削弱模型泛化能力。# 简化版继承映射将苏联主要继承给俄罗斯 inherit_map {URS: RUS, YUG: SRB, TCH: CZE} def remap_noc(row): return inherit_map.get(row[NOC], row[NOC]) panel[NOC_adj] panel.apply(remap_noc, axis1) # 重新聚合把继承国的历史奖牌合并 panel_adj panel.groupby([NOC_adj, Year]).agg( total_medal(total_medal, sum), gold(gold, sum), athletes_count(athletes_count, sum), ).reset_index()这张映射表不需要精确到每一块奖牌的归属只要保证时间序列连续即可。论文里可以说明这是简化处理并做敏感性分析验证结论稳健性。3. 用XGBoost回归预测模型跑通奖牌预测基线3.1 特征矩阵组装与训练集划分特征工程做完后把panel整理成模型可用的X矩阵和y向量。y选total_medalX包括prev_total、prev_gold、prev_athletes、roll3_total、athletes_count、events_count、sports_count、female_ratio、avg_age、is_host。注意athletes_count和events_count是当届已知信息在真实预测场景中赛前就能拿到参赛名单所以不算泄漏。import numpy as np from sklearn.model_selection import TimeSeriesSplit feature_cols [ prev_total, prev_gold, prev_athletes, roll3_total, athletes_count, events_count, sports_count, female_ratio, avg_age, is_host ] model_df panel_adj.dropna(subset[prev_total, roll3_total]).copy() model_df model_df.sort_values(Year) X model_df[feature_cols].values y model_df[total_medal].values # 时序交叉验证不能随机打乱 tscv TimeSeriesSplit(n_splits5)这里必须用TimeSeriesSplit而不是KFold。奥运数据是按年份排列的时序数据随机打乱会让模型在训练集里看到未来信息交叉验证分数虚高。TimeSeriesSplit保证每一折的训练集年份都早于验证集年份模拟真实预测场景。3.2 XGBoost参数配置与训练XGBoost回归预测模型在中小规模表格数据上表现稳定奥运奖牌预测的样本量通常在1000到2000行之间正好落在XGBoost的舒适区。参数方面max_depth控制在4到6之间太深容易过拟合小样本learning_rate设0.05到0.1配合early_stopping找最优迭代轮数。import xgboost as xgb from sklearn.metrics import mean_absolute_error, r2_score params { objective: reg:squarederror, max_depth: 5, learning_rate: 0.08, subsample: 0.8, colsample_bytree: 0.8, min_child_weight: 3, reg_alpha: 0.1, reg_lambda: 1.0, random_state: 42, } mae_scores, r2_scores [], [] for train_idx, val_idx in tscv.split(X): X_train, X_val X[train_idx], X[val_idx] y_train, y_val y[train_idx], y[val_idx] dtrain xgb.DMatrix(X_train, labely_train) dval xgb.DMatrix(X_val, labely_val) model xgb.train( params, dtrain, num_boost_round500, evals[(dval, val)], early_stopping_rounds30, verbose_evalFalse, ) preds model.predict(dval, iteration_range(0, model.best_iteration 1)) mae_scores.append(mean_absolute_error(y_val, preds)) r2_scores.append(r2_score(y_val, preds)) print(fMAE: {np.mean(mae_scores):.2f} ± {np.std(mae_scores):.2f}) print(fR2: {np.mean(r2_scores):.3f} ± {np.std(r2_scores):.3f})min_child_weight设3是为了防止叶子节点样本过少导致过拟合奥运数据里很多小国只有一两行记录。early_stopping_rounds30表示验证集损失连续30轮不下降就停止避免无效迭代。iteration_range用best_iteration截断保证预测时只用最优轮数的模型。3.3 特征重要性与影响因素分析C题除了预测还要求分析影响奖牌分布的关键因素。XGBoost自带特征重要性接口可以输出gain、weight、cover三种指标。gain表示特征在分裂时带来的平均增益最能反映特征对预测的贡献。importance model.get_score(importance_typegain) importance_df pd.DataFrame({ feature: list(importance.keys()), gain: list(importance.values()) }).sort_values(gain, ascendingFalse) print(importance_df.to_string(indexFalse))从经验看prev_total和roll3_total通常占据前两位说明历史奖牌惯性是最强预测因子。athletes_count和events_count紧随其后反映参赛规模的基础作用。is_host的gain值不会最高但在东道主年份的局部预测中影响显著。female_ratio和avg_age的gain值一般较低但在论文里可以作为“结构性因素”讨论说明性别平等程度和运动员年龄结构对奖牌产出有微弱正向影响。4. 避坑与排查奖牌预测里最容易翻车的五个地方4.1 用当届奖牌数做特征导致数据泄漏现象交叉验证R²超过0.95但用2016年数据预测2020年时误差巨大。原因特征里混入了当届的金牌数、银牌数或总奖牌数模型直接抄答案。解决检查所有特征列确保没有任何一列是当届奖牌的直接函数。滞后特征必须用shift(1)或更早的窗口滚动统计要先shift再rolling。4.2 按NOC分组后小国样本被shift成NaN现象模型训练时发现大量国家只有一两届记录prev_total全是NaNdropna后样本量骤减。原因小国参赛不连续按NOC分组做shift时没有前序记录。解决对样本量少于3届的国家用该国历史均值填充prev_total或直接标记为“新参赛国”并用0填充。不要简单dropna否则会丢失大量有价值的小国样本。4.3 东道主效应被模型学成随机噪声现象模型对东道主年份的预测误差明显高于非东道主年份。原因is_host特征没有加入或加入后样本量太少导致树模型没有充分分裂。解决显式构造is_host列并在论文里单独分析东道主效应。如果样本量允许可以对东道主年份做加权训练提高模型对这类样本的关注度。4.4 用随机KFold导致交叉验证分数虚高现象KFold交叉验证R²0.9TimeSeriesSplit只有0.7。原因随机KFold让训练集包含未来年份数据模型学到了时间趋势。解决统一用TimeSeriesSplit并在论文里说明时序验证的必要性。如果审稿人质疑样本量小可以补充留一届法leave-one-edition-out作为稳健性检验。4.5 忽略项目设置变化导致特征漂移现象早期奥运会1896-1920的events_count和sports_count与当代差异巨大模型在早期数据上训练后对当代预测偏差大。原因奥运会项目数量从几十个增长到三百多个参赛人数从几百人增长到上万人特征分布随时间漂移。解决对events_count和athletes_count做对数变换或按届标准化减小量纲差异。也可以在论文里限定研究范围为1980年之后的奥运会避开早期数据的不稳定性。5. 把预测结果落到论文里可视化与敏感性分析技巧预测跑通只是第一步C题论文还需要把结果讲清楚。我一般会做三张图第一张是各国奖牌数预测值与真实值的散点图按年份分面直观展示模型在不同届次的误差分布第二张是特征重要性条形图用gain值排序配合文字解释每个特征的经济学含义第三张是东道主效应的箱线图对比东道主年份和非东道主年份的残差分布。敏感性分析方面重点做两件事一是把is_host从特征里去掉看模型MAE上升多少量化东道主效应的贡献二是把训练集截止年份从2016年提前到2012年用2016年做测试验证模型在完全未见数据上的表现。这两项分析写进论文比单纯堆模型参数更有说服力。import matplotlib.pyplot as plt # 东道主效应残差分析 model_df[pred] model.predict(xgb.DMatrix(X)) model_df[residual] model_df[total_medal] - model_df[pred] host_res model_df[model_df[is_host] 1][residual] non_host_res model_df[model_df[is_host] 0][residual] fig, ax plt.subplots(figsize(6, 4)) ax.boxplot([non_host_res, host_res], labels[Non-host, Host]) ax.set_ylabel(Residual (Actual - Predicted)) ax.set_title(Host Effect on Medal Prediction Residual) plt.tight_layout() plt.savefig(host_effect.png, dpi150)这张图如果显示东道主残差中位数明显高于非东道主就说明模型系统性低估了东道主表现论文里可以据此建议在东道主年份对预测值做向上修正。修正幅度可以用残差均值估计一般在5到15块奖牌之间。最后说一个我自己的习惯每次跑完模型先把特征列表打印出来逐列检查一遍确认没有当届奖牌泄漏再看一眼时间戳排序是否正确。这个动作花不了两分钟但能省掉后面反复调参却找不到问题根源的几小时。奖牌预测这道题模型本身不难难的是对数据生成过程的理解和对时序边界的敬畏。希望帮到你。本文还有配套的精品资源点击获取