简介2025年美赛C题建模与数据分析代码合集面向参赛学生、数据分析爱好者与教练团队围绕奥运奖牌预测与相关因素分析提供完整可运行的Python解决方案。资源覆盖数据清洗、特征工程、奖牌预测、进步退步分析、零奖牌统计、奖牌突破概率、东道主效应及项目设置影响等模块采用线性回归、随机森林等模型并配有matplotlib/seaborn可视化输出可支撑从数据预处理到结果解读的完整流程。整套资源共91个文件以28个csv数据、21个py脚本、12个docx文档、10个ipynb分析笔记本为主另有pdf赛题原文、markdown说明与汇报PPT等压缩包约25MB目录按原始数据、清洗数据、预测脚本、论文报告等划分便于快速定位。已有167人学习下载。随包提供中英文赛题、可复现代码、建模思路文档与配套报告并附有数据字典、分组中间结果等辅助产物适合需要系统备赛或复现美赛C题完整方案的同学。1. 2025年美赛C题不是一道数据挖掘题而是一道“时序结构”题2025年美赛C题的核心任务是基于一百二十多年的奥运会历史数据预测下一届各代表团NOC的奖牌数并对奖牌变化背后的相关因素做分析。很多队伍拿到题的第一反应是把它当成回归竞赛划分训练测试集、调XGBoost、用RMSE排名次。这个思路在第一轮就会翻车因为这批数据的训练集和测试集分布严重不一致——每届奥运会的参赛代表团范围不同、比赛小项数量不同、主办国效应年年有但强弱差很多模型很容易学到“年份和总奖牌数的伪关系”而不是国家实力。你调参调得再漂亮预测下一届前几名照样全错。这道题真正考的是你能不能把历史数据整理成一个能支撑时序推理的面板结构再在这个结构上谈预测。本篇文章讲的就是这样一套完整做法从数据清洗到特征工程从奖牌预测到进步退步分析再到可视化呈现每一节都有能直接落地的代码和参数说明。适合准备参赛的建模队也适合想系统做一次奥运数据项目的从业者。2. 数据清洗与特征工程把120年奖牌表整理成能建模的“国家-届次”面板2.1 数据清洗先确定观测单元再谈特征美赛C题原始数据通常是“行级明细”每一行代表一枚奖牌包含年份、代表团代码、运动项目、小项名称、奖牌类型。建模的第一步不是跑模型而是把行级明细转换成面板数据。我给这个步骤定的原则非常简单一行一个国家、一届奥运会、一个统计口径。这里最容易犯的错误是低估了“唯一键”的重要性——如果单纯按年份加总你会得到每届的总奖牌数但那只是一个数字无法建模国家粒度如果按“年份-代表团”聚合又会遇到代表团改名、国家合并、解体继承等历史问题。我的做法是先建立一个代表团映射表把历史上出现过的所有NOC代码统一到一个“延续代码”上再去做GroupBy。import pandas as pd # medals.csv 至少包含 year, noc, sport, event, medal 五列 raw pd.read_csv(medals.csv) # 映射表把历史NOC代码统一到延续代码 mapping { URS: RUS, # 举例历史代码 - 延续代码 TCH: CZE, # 实际比赛中根据官方NOC变更说明维护这张表 } raw[noc_clean] raw[noc].replace(mapping) # 按 年份-延续代码 聚合出金/银/铜/总数 agg ( raw.groupby([year, noc_clean]) .agg( gold(medal, lambda s: (s Gold).sum()), silver(medal, lambda s: (s Silver).sum()), bronze(medal, lambda s: (s Bronze).sum()), ) .reset_index() ) agg[total] agg[gold] agg[silver] agg[bronze]这段代码的逻辑是先对NOC做一次“口径统一”再用groupby同时统计金、银、铜数量。拆开统计比直接计数更有用因为后面可以做加权奖牌数也可以分别建模“金牌预测”和“总奖牌预测”。参数说明里有两个细节一是lambda计数只统计medal字段等于指定值的行聚合前最好过滤掉medal为空的脏数据二是在真实比赛中你要确认奖牌明细里“没有获得奖牌但参赛”的代表团是否出现在别的表里——那一部分才是真正的0奖牌样本不能让非参赛国混进面板。2.2 特征工程的三个必做项东道主效应、项目数变化、历史实力面板生成后下一步是特征。我做过几轮奥运会数据项目最重要的经验是不要一上来想着造几百个高阶交叉特征先把三个结构性特征做扎实模型效果立马不一样。第一个是东道主效应。每届主办国在当届的奖牌数普遍有明显提升但提升幅度不固定而且下一届往往会回落到正常水平附近。所以要做的不是“主办国1”这种单变量而是三列host_now当届主场、host_prev上届主场、host_next下届主场。第二个是项目数变化。奥运会每届的小项总数不一样新增项目往往对东道主和传统优势国更有利直接用“当届总项目数”作为整体暴露量即可如果数据更细可以做成“该国优势小项的数量”。第三个是历史实力。代表队实力有惯性但距离现在越久参考价值越低用指数衰减加权来处理比简单用上一届奖牌数更平滑。# 在 panel 基础上继续做特征 panel agg.sort_values([noc_clean, year]).reset_index(dropTrue) # 上届奖牌数 / 上上届奖牌数 panel[last_total] panel.groupby(noc_clean)[total].shift(1) panel[last2_total] panel.groupby(noc_clean)[total].shift(2) # 东道主三列需要提前准备好 host_map键是年份值是主办NOC panel[host_now] panel[year].map(host_map).eq(panel[noc_clean]).astype(int) panel[host_prev] panel.groupby(noc_clean)[host_now].shift(1).fillna(0) panel[host_next] panel.groupby(noc_clean)[host_now].shift(-1).fillna(0) # 指数衰减加权历史实力最近三年权重最高 import numpy as np def weighted_hist(x, decay0.7): if len(x) 0: return np.nan w decay ** np.arange(len(x) - 1, -1, -1) return float(np.dot(x, w) / w.sum()) panel[hist_str] ( panel.groupby(noc_clean)[total] .transform(lambda s: s.rolling(3, min_periods1).apply(weighted_hist, rawTrue)) )这段代码里的shift是按国家分组后的滞后操作不会串到别的国家。rolling(3)表示最多看前三届decay0.7表示越早的一届权重越低三届的权重比大约是1:0.7:0.49近期成绩主导但历史稳定性也在。decay的调参方向是如果模型预测波动过大说明对近期过度敏感可以提高到0.8甚至0.9如果模型反应迟钝调低到0.5。host_next这一列用于训练是没问题的因为训练时“下一届”是已知的历史届次但最终预测2028年时这一列必须置零否则会用到不存在的未来信息这一点务必在预测阶段单独处理。2.3 探索性可视化的第一张图先看东道主效应再决定建模策略特征做完之后不要急着建模先用可视化确认假设。我的固定动作是画两张图第一张是东道主奖牌数与其前后两届的对比折线用来确认东道主效应是否显著第二张是全体代表团的奖牌数分布直方图确认数据是零膨胀计数分布还是近似正态。第二张图往往会让队伍意识到奖牌数分布右侧长尾非常严重“奖牌数为0”的参赛代表团占了一半以上这种情况下直接用线性回归是危险的。import matplotlib.pyplot as plt # 第一张图东道主轨迹抽样 host_nocs panel[panel[host_now] 1][noc_clean].unique()[:5] fig, ax plt.subplots(figsize(8, 4)) for noc in host_nocs: sub panel[panel[noc_clean] noc].set_index(year)[total] sub.plot(axax, markero, labelstr(noc)) ax.set_title(Host Effect: medal trend around hosting year) ax.legend() plt.tight_layout() plt.show() # 第二张图奖牌数分布 panel[total].hist(bins50) plt.title(Distribution of total medals per NOC per Games) plt.show()这两张图的价值不是“好看”而是给模型选型提供依据。如果发现东道主效应在不同国家间差异很大比如有些国家主办年翻倍、有些国家只是小幅上升那么建模时就要考虑“主办国 × 历史实力”的交互项而不是只放一个哑变量。如果第二张图确认了零膨胀和长尾那么第3章的负二项回归就是比线性回归更合理的选择。可视化的输出不需要精致到论文级别此时它更像是你手里的证据帮你决定下一步往哪个方向使劲。3. 奖牌预测从负二项回归基线到机器学习校准3.1 第一版模型必须是可解释基线不能是调包奖牌数预测这件事评委和客户关心的往往不只是“数字是多少”更关心“为什么是这个数”。所以第一版模型我会选择可解释的计数模型而不是直接把数据丢给随机森林。奖牌数是一个典型计数变量取值是非负整数方差往往大于均值存在大量小国低奖牌样本。泊松回归要求均值等于方差在奥运数据上基本不满足负二项回归多了一个离差参数alpha允许方差大于均值更适合这种过离散数据。建模时用“国家固定效应 年份连续趋势 东道主效应 上届奖牌 历史实力”这一套组合的意义在于每一项都有明确业务含义输出系数可以直接写进论文的“因素分析”章节。import statsmodels.formula.api as smf # 训练集用截至上届的数据测试集留最后一届 train panel[panel[year] 2024].dropna(subset[total]) test panel[panel[year] 2024].copy() formula total ~ C(noc_clean) year host_now host_prev last_total hist_str model smf.negativebinomial(formula, datatrain).fit() print(model.summary()) # 预测并保留区间 test[pred_total] model.predict(test)负二项回归的输出里先看alpha系数的置信区间。如果区间包含0说明数据不过离散退回泊松更简约如果alpha显著大于0负二项就是必要的。C(noc_clean)会为每个代表团生成一个哑变量对于参赛历史长的大国来说没问题但小国样本少、系数不稳定我一般会在拟合前过滤掉“参赛次数低于5届”的代表团避免固定效应把小国的历史噪声当成实力。year系数代表全球奖牌总数随时间缓慢上升的趋势如果这一项不显著说明总奖牌量主要由项目数变化决定此时应该把year换成“当届项目总数”作为显式暴露量负二项回归完整写法是model.fit(exposurenp.log(events))但我个人更建议直接用“当届项目数”做成特征这样解释起来更直观。3.2 模型诊断与参数调整用什么指标判断预测好坏对于计数预测RMSE可以被用到但它对长尾大国的误差过于敏感。我习惯同时看三个指标R方回归视角、平均绝对误差MAE、以及一个更贴近题目诉求的指标——Top10命中率即预测奖牌数前10名中有几个真的出现在实际前10名里。这三个指标视角不同R方看整体拟合MAE看平均偏差Top10命中率看排序能力。比赛论文里最容易被挑战的恰恰是最后一项评委不会关心你预测的第45名误差多小他们更关心前几名是否靠谱。from sklearn.metrics import mean_absolute_error mae mean_absolute_error(test[total], test[pred_total]) print(MAE:, mae) # Top10命中率预测前10 vs 实际前10 pred_top10 set(test.nlargest(10, pred_total)[noc_clean]) true_top10 set(test.nlargest(10, total)[noc_clean]) hit_rate len(pred_top10 true_top10) / 10 print(Top10 Hit Rate:, hit_rate)这段代码是评估口径不在建模复杂度上做文章。MAE如果超过10枚说明模型对大国单届几十枚奖牌的预测系统偏差较大要去查是不是某个主办国的大年没被模型捕捉Top10命中率如果低于50%优先怀疑特征缺口而不是模型复杂度。负二项回归本身没有超参数可调真正可调的是特征所以“参数怎么改”要落回特征构造host_prev的方向和显著性能提示你东道主效应是“持续两届”还是“一届游”last_total的系数接近1还是大于1决定你是否需要对动量强的队伍做额外校准。3.3 用LightGBM做残差校准让非线性信号补位负二项回归的可解释性很好但表达不了非线性交互。比如“某国既是东道主又在近两届新增了好几个优势小项”这种组合效应线性模型很难拟合。我的做法是分两步第一步用负二项回归得到基线预测计算训练集残差第二步用LightGBM在残差上做回归学习线性模型剩下的非线性信号。最终预测等于基线预测加残差预测。为什么不在第一步直接上GBDT因为那样会丢失“主办国系数是多少、历史实力弹性是多少”这些论文里最值钱的解释材料。残差校准是折中方案主体可解释局部补非线性。import lightgbm as lgb train[base_pred] model.predict(train) train[resid] train[total] - train[base_pred] feat_cols [year, host_now, host_prev, last_total, hist_str, gold_last] # 还可以加收入、人口等外部特征但需要确保2028年也能取到 dtrain lgb.Dataset(train[feat_cols], labeltrain[resid]) dval lgb.Dataset(test[feat_cols], labeltest[total] - model.predict(test)) params { objective: regression, learning_rate: 0.02, num_leaves: 15, min_data_in_leaf: 20, verbosity: -1, } model_gbdt lgb.train( params, dtrain, num_boost_round500, valid_sets[dval], callbacks[lgb.early_stopping(50), lgb.log_evaluation(100)], ) test[pred_final] test[pred_total] model_gbdt.predict(test[feat_cols])这里有几个关键参数值得解释。learning_rate设置为0.02是为了让模型学得慢一点配合early_stopping防止在残差上学过头。num_leaves15限制了树的复杂度因为残差里能学到的真实信号本来就有限如果你的训练集有几十个代表团和十几届数据树太深一定会过拟合。min_data_in_leaf20保证每个叶子至少有20个样本进一步压过拟合。最容易被忽略的是GBDT训练时用的特征是“基线模型也用过的”这意味着它没有办法创造出全新信息只是把已有特征的非线性组合找出来。如果你想让校准带来明显提升应该在这个阶段加入基线没用过的外部特征比如该代表团在上届各项目中的“银牌转金牌率”这类特征代表的是实力上升的微观信号。用LightGBM校准后的最终预测我建议不要再把区间做复杂推导直接用训练集残差的标准差乘以0.7作为预测区间的半宽简洁且诚实。4. 进步退步分析把“某国这届涨了很多”拆成真实崛起与周期波动4.1 量化定义不要用“较上届增长百分比”评价小国进步退步分析最直观的想法是“这届奖牌数减上届奖牌数”但直接算绝对值或百分比都有缺陷。奖牌基数为1的国家下届变3枚就是增长200%从统计学上根本无法区分这是真实进步还是随机波动。所以我在做这道题时会先把绝对奖牌数转换成两个更稳的口径一是奖牌占比即该国奖牌数除以当届总奖牌数二是奖牌数在所有参赛代表团中的百分位排名。用这两个口径计算进步退步才不会让小数点级别的变化被无限放大。尤其是当题目要求“分析进步与退步的背后因素”时评委关心的是哪些代表团属于结构性上升哪些只是东道主一年游。# 奖牌占比 panel[share] panel[total] / panel.groupby(year)[total].transform(sum) # 连续两届占比变化 panel panel.sort_values([noc_clean, year]).reset_index(dropTrue) panel[share_chg] panel.groupby(noc_clean)[share].diff() # 三届趋势斜率用最简单的线性回归斜率做动量方向 def trend_slope(y): if len(y) 3: return 0.0 x np.arange(len(y), dtypefloat) return float(np.polyfit(x, y, 1)[0]) panel[trend] ( panel.groupby(noc_clean)[share] .transform(lambda s: s.rolling(3, min_periods3).apply(trend_slope, rawTrue)) )share_chg是短期变化trend是中期趋势。两者方向一致时进步/退步的判断可靠性最高方向矛盾时比如短期上涨但三届斜率向下说明只是波动而不是趋势。使用这两个特征时要注意rolling(3)会把早期的值变成NaN后续处理时要么填充0要么直接丢弃早期样本我倾向于丢弃因为“进步退步”本来就需要至少三届数据才有意义。4.2 判定算法阈值划分与形状聚类有了share_chg和trend之后下一步是划分类别。最简单的做法是设双阈值趋势大于0.01且短期变化大于0.005标为“强势进步”趋势小于负0.01标为“明显退步”中间地带叫“波动调整”。阈值怎么定我的经验是不要拍脑袋看数据的分布把trend和share_chg做散点图找出自然的分类边界。更稳健的替代方案是用KMeans对“该国连续几届的奖牌占比时间序列”做形状聚类把时间序列归一化后聚类成三类持续上升型、波动型、持续退步型。这种方法的优点是省去手工设阈值缺点是需要你手动解释每个簇的业务含义工作量反而更大。我用KMeans做出来的类别常常与直觉不符因为这届大题的数据覆盖了一百多年不同时期奥运会规模差异巨大归一化虽然保留了形状却丢掉了量级所以最后我还是选择“阈值划分人工校准”的方法写进论文。# 阈值划分示例阈值为示例值需结合数据分布调整 def classify_progress(row): if pd.isna(row[trend]): return 数据不足 if row[trend] 0.01 and row[share_chg] 0.005: return 持续进步 if row[trend] -0.01: return 明显退步 if row[share_chg] 0.005: return 短期上升 return 波动调整 panel[progress_type] panel.apply(classify_progress, axis1)这段代码的价值在于把主观判断变成一个可复现的规则方便在论文里写清楚“进步退步的定义”。注意阈值不是通用的2012年之后的数据分布和前一百年完全不同所以我实际应用时会按“近期四届”重新计算阈值。这里有一个关键约束进步退步分析只能对“连续参赛的代表团”做中间缺了一届的代表团无论是战争停办还是主动缺席会导致diff和trend断崖处理方法是把缺届视为NaN而不是0否则会把一届普通成绩错判成退步。4.3 进步退步分析如何反哺预测模型进步退步分析不只是为了凑一个分析章节它应该直接服务预测。我在做最终预测时会构造一个“动量虚拟变量”如果某代表团被分到“持续进步”在GBDT残差校准阶段额外加权如果被分到“明显退步”则在最后一位预测时把基线预测下调一定幅度幅度与退步斜率成正比。这个思路的本质是奖牌惯性不是均匀的线性模型给每个国家同样的惯性系数但进步退步分析告诉你有些国家的惯性是负的。除此之外进步退步分类还有一些“非预测用途”它可以帮助你在因素分析中回答“哪些因素和进步/退步相关”比如计算进步类代表团的平均新增项目数、是否东道主、上届参与小项数等统计量。这比在论文里堆一堆相关系数更有说服力。5. 避坑与常见问题奖牌预测翻车的5个高发位置5.1 时间序列数据用随机K折交叉验证导致预测结果虚高现象模型在验证集上MAE很小Top10命中率高得离谱但一旦预测下一届真实未来数据排名完全错乱。原因随机K折把不同届次的样本打散进训练集和测试集模型在训练时已经见过“未来届次”的数据分布尤其是东道主效应和历史实力特征会泄露信息。解决只使用前向验证。具体做法是以时间排序前几届训练、后一届验证依次滚动。例如训练到2020届预测2024届再用到2024届预测2028届。在负二项回归和LightGBM校准中都严格按这个时间窗划分数据代码里的train/test就是按“year 2024”和“year 2024”切出来的这就属于前向验证。提示前向验证的测试集只有一届样本量少、评估波动大这是奥运数据题目绕不开的现实历史届次总共二十多次不可能变出更多的“未来”。接受这一点不要通过随机K折制造虚假的信心。5.2 东道主效应被无限外推预测下届主办国奖牌爆炸现象模型预测下一届主办国奖牌数比其历史峰值还高出一倍看起来就不可信。原因负二项回归的host_now系数很大预测时将host_now1直接套用模型认为该系数对所有国家一致没有考虑“东道主红利”会随该国原有实力、上届是否已经透支而衰减。解决给东道主效应乘以一个收缩系数通常取0.5~0.8。更精细的做法是构造“东道主 × 上届奖牌占比”的交互项让模型自己去学原来实力弱但占便宜大的国家收缩更大还是原来实力强的国家加成才多。我在实际项目中一般分两步走先不加交互项看主效果再加交互项看系数变化如果交互项显著说明收缩逻辑成立最终预测时用交互后的预测值。5.3 聚合后的奖牌总数和原始明细不一致被评委抓到口径错误现象论文里写的总奖牌数和官方历史数据集对不上或者某国某届的金牌数比实际少了三枚。原因聚合时丢失了NOC改名、奖牌类型大小写不一致、以及部分行在读取时被当作空值过滤掉。这些错误在行级明细几十万行的数据里很容易被忽略。解决每次聚合后做全链路校验。第一层校验聚合后的goldsilverbronze必须等于原始明细中medal非空的行数注意一枚奖牌对应一行所以是计数不是数值求和。第二层校验任意挑三个大国、两届三届的数据和公开的官方奖牌表手动比对确保NOC映射表没搞错。assert agg[total].sum() raw[medal].notna().sum(), 奖牌总数不一致 assert (agg[[gold, silver, bronze]].sum(axis1) agg[total]).all(), 分项与总数不一致这两行断言写在清洗脚本的最后一旦不通过立即中断不要带着脏数据往下走。我踩过最大的坑是映射表把某个历史代码映射到了两个不同的延续代码导致该国的数据被切成了两半断言一查就暴露了。5.4 进步退步分析把小样本波动当成了趋势现象某个只拿过一两枚奖牌的小代表团被标成“显著进步500%”在论文里显得很滑稽。原因基数太小百分比变化被放大到失真。这是比例类指标在小样本下的通病。解决给进步退步分析加“最小奖牌量门槛”比如近三届平均奖牌数大于等于5才参与分类低于门槛的全部并入“数据不足”。如果不想丢弃小国就用奖牌占比的变化来判断占比每届变化0.005在排名上是可比的但在一个奖牌总数3000枚的奥运会上0.005占比只对应15枚奖牌仍然会高估小国。所以我看重的是“门槛 占比双条件”两者都过才算有效信号。5.5 可视化把金银铜用颜色区分导致数量维度被误读现象论文里的堆叠柱状图每个国家的金牌区、银牌区、铜牌区用不同颜色区分评委看不出总数差异只看到颜色块的高度实际上最长的颜色块对应的可能是铜牌最多而不是总奖牌第一。原因金银铜是分类信息不适合直接对颜色做读者解释正确的映射是“数量”用高度或长度“类别”用颜色或图例。解决画“总奖牌数排序柱状图 金/银/铜占比堆叠条”或者把金银铜映射到同一个柱状图的内部堆叠区并在图例上明确说明数字为主、颜色为辅。如果目标是突出金牌直接画“加权奖牌数”权重按金3银2铜1颜色只标显著进步/退步类别。可视化的原则是“数量维永远优先于类别维”这句话写进论文不会错。6. 进阶用法稳健性检验与情景分析让预测结论经得起追问6.1 稳健性检验换口径、删届次、调参数评委最常问的一句话是“你的结论对数据选择敏感吗”。为了回答这个问题我会跑三组稳健性检验一是换奖牌口径把“总奖牌”换成“加权奖牌数”重跑整套流程如果Top10预测结果变化不大说明结论不依赖口径选择二是剔除特殊届次一战二战期间的奥运会、以及早期参赛规模极小的几届从训练集中拿掉后重新拟合看东道主系数和趋势系数是否仍然成立三是调整历史实力特征的衰减系数从0.7改成0.5和0.9看排名稳定性。这三组结果整理成一张参数对照表放进论文附录比任何文字解释都有力。6.2 情景分析把点预测升级成决策区间点预测只是一个数字没有决策参考价值。我给最终预测输出三个情景基准情景即模型原始预测强主办国情景将东道主效应收缩系数调低相当于更信任主办国红利同时假设该国上届已存在的优势项目全部保留保守情景把全体预测值向历史均值收缩10%。三个情景共享同一个特征工程和建模流程只是参数不同因此可以写成两个函数复跑。这样做的好处是论文里可以写“我们的模型预测东道主代表团将获得35~45枚奖牌中位数38枚”而不是写一个孤零零的38。评审对区间的接受度远高于点估计因为区间本质上是诚实地承认了模型不确定性。6.3 一套代码组织方式让整套方案可复现这道题的工作流可以稳定分成四个脚本clean.py负责数据清洗与口径校验features.py负责面板构建与特征生成model.py负责负二项回归和LightGBM校准analyze.py负责进步退步分析、可视化和稳健性检验。四个脚本按顺序执行中间产物分别是panel.csv、features.csv、prediction.csv、analysis_results.csv。我在做这类题时一直保留这套结构换一个数据集、换一届年份只需要改clean.py里的映射表和features.py里的host_map后面的脚本一行都不用动。“开箱即用”说的就是这种结构数据进去论文图表和预测表格出来。多年做这类题的习惯让我养成了一个偏好把所有“口径定义”写成一个单独的字典放在代码最前面比如金牌权重、东道主收缩系数、最小奖牌量门槛这样每次跑完看到结果时第一个动作不是怀疑模型而是检查口径字典是否还符合这次比赛的要求。希望帮到你。本文还有配套的精品资源点击获取