美赛C题建模全链路:数据预处理、ARIMA预测与PLS-Kmeans分档

简介:围绕2023年美赛C题Wordle数据建模的M论文,以单份PDF形式呈现,适合备战美赛、需要完整赛题解题框架的参赛者,以及做数据分析方向毕业设计的读者参考。压缩包仅含1个PDF文件,大小约2.18MB,篇幅集中,便于通读与对照赛题复盘。内容从数据清洗、缺失值与异常文本处理、尝试次数标准化入手,提取9个单词属性并做Spearman相关性分析;随后用时间序列模型完成结果数量的区间预测,以偏最小二乘回归预测不同尝试次数得分百分比,并结合K-means、肘部法则和轮廓系数完成词性与难度分组。文中还给出MAE约3~4、MSE约22~24的误差表现,以及混淆矩阵热图、准确率0.941等评估结果,并讨论数据质量、模型假设等不确定性。已有170人学习,适合用来梳理建模流程、复现关键方法并积累论文写作素材。

1. 开篇:2023 美赛 C 题给的数据干净得反常,掉档的人恰恰栽在这上面

2023 美赛 C 题给了一份五字母猜词游戏的日度数据,359 行、12 列,没有缺失值,看上去干净得不像建模题。真正拉开差距的地方第一天就出现了:一多半队伍抓起数据直接上 LSTM、XGBoost,论文里堆了五六个模型,最后只拿到 S;拿 M 奖的那批论文,模型未必多高级,但四个问题各自能闭环,预处理交代得清清楚楚。这道题反直觉的地方在于,它考的不是调参水平,而是你能不能把一份「脏得刚刚好」的数据讲成一条从清洗、预测到分类的完整证据链。正在准备美赛的人、打算把美赛赛题改造成毕业设计选题的人,都该按这条链来练。下面按考点分布、高频考点、真题演练、易错点的顺序,一层层拆开。

2. 数据预处理:评委前五分钟会盯的三个动作

数据预处理在美赛评分表里没有单独一栏,但它决定了评委对你整篇论文的第一印象。359 行数据、12 列字段,量小到评委可以直接抽查任意一行,任何一处没交代的处理都会被追问。C 题的实际数据结构是:日期、每日参与人数、困难模式人数,以及猜中轮次 1、2、3、4、5、6 次和失败(X)的七个百分比列。这七个百分比加起来本应等于 100%,实际上有 98%、99%、100%、101%、102% 五种情况,光这一条就够写一页预处理。

预处理要交付三样东西:异常值怎么发现的、怎么替换的、替换后对结论有没有影响。三样缺一样,后面的 ARIMA 预测和 PLS 回归都站不住脚。很多队伍把清洗写成一句「我们对数据进行了预处理」,这是典型的失分写法——美赛评委看不到你的中间过程,只能从你写出来的每一步判断你是否真的动过手。

2.1 异常值:两个离群点怎么定位、怎么替换、论文里怎么写

把「每日报告人数」按时间画成折线,会看到一处断崖式下跌:某一天的数值是 2569,而它前后两天分别是 23739 和 22628。困难模式人数也有同样的问题,某天是 3249,两侧是 9310 和 10343。这两处不是真实的游戏热度变化,而是数据记录问题,如果不处理,ARIMA 的差分项会被这两个点带偏,预测区间会宽得没法用。

处理方式用的是邻域均值替换:取异常点前后各两个点求平均,四舍五入后回填。2569 那一处替换成 23184,3249 那一处替换成 9827。这个方法比直接删除整行更稳妥,因为删掉一天会让时间序列出现断点,asfreq("D")之后还要补值,反而多一层麻烦。

import pandas as pd def fix_spike(series, idx, window=2): """用 idx 前后各 window 个点的均值替换异常值""" left = series.iloc[max(0, idx - window):idx] right = series.iloc[idx + 1: idx + 1 + window] return round(pd.concat([left, right]).mean()) df = pd.read_csv("daily_results.csv", parse_dates=["date"]).sort_values("date") df = df.reset_index(drop=True) for col in ["reported", "hard_mode"]: # 以 5 日中心滚动中位数为基准,低于基准 20% 视为断崖异常 base = df[col].rolling(5, center=True).median() bad_idx = df.index[df[col] < base * 0.2] for i in bad_idx: df.loc[i, col] = fix_spike(df[col], i, window=2)

window=2取前后各两天共四个点求均值,是因为这两处异常都只影响单日,邻域越大越容易把真实波动一起抹平。阈值取 0.2 而不是 0.5,是因为正常日间的波动幅度在 ±30% 以内,只有这种数量级下跌才属于记录错误。换完值以后一定要再画一次折线图放进论文,让评委看到替换前后的对比——这一步是「可验证」的来源,也是 M 奖论文和 S 奖论文最直观的区别。

2.2 百分比归一化:98% 到 102% 的五种和,处理方式要写清楚

把七个百分比列按行相加,会得到 98%、99%、100%、101%、102% 五种结果,这说明原始数据在四舍五入或统计口径上有偏差。如果直接拿去做 PLS 回归,因变量本身就不闭合,模型学到的会是一部分噪声。

正确做法是按行归一化:每行的七个百分比同除以该行总和,再乘 100。归一化只对总和落在合理区间的行生效,总和明显离谱的行(比如整行为空或只有一列有值)应当单独标记出来,而不是一起拉成 100%,否则会把缺失数据伪装成真实分布。

pct_cols = ["p1", "p2", "p3", "p4", "p5", "p6", "px"] total = df[pct_cols].sum(axis=1) # 只处理总和在 95~105 之间的行,超出的行记录下来单独说明 mask = total.between(95, 105) df.loc[mask, pct_cols] = df.loc[mask, pct_cols].div(total[mask], axis=0) * 100 print("未处理行数:", (~mask).sum())

代码里between(95, 105)的上下界不是随手定的:七个百分比各自由大样本四舍五入而来,单项误差最大 0.5 个百分点,七项累计不超过 3.5,再留一点余量就是 5。归一化以后要在论文里补一句「归一化前后的分布形态一致,仅总量修正」,这句话能挡掉评委关于「你是不是篡改了数据」的质疑。

提示:归一化后重新核对每一行的和是否为 100,并把核对结果作为一张小表放进附录,比空口说「已归一化」有说服力得多。

2.3 九个单词属性的提取:这一步决定后两问能不能做

问题 1 要回答「单词属性是否影响困难模式占比」,问题 2、3 要预测分布和分难度,都需要把每个单词转成可计算的数值。原文提取了 9 个维度,这是整道题里最容易被低估的一步——属性选得不对,后面 Spearman 相关系数全部不显著,你会被迫得出「属性无影响」的结论,而这是错的。

这 9 个维度通常这么定:字母个数(是否含重复字母)、重复字母的个数、元音字母占比、首字母在全库中的出现频率、末字母频率、字母在常见位置上的平均频率、单词的语料词频(越生僻越难)、字母组合的常见度(是否含有 rare 组合如双写)、以及是否含不发音字母。前四项是结构属性,后五项偏语言属性,两类混着提,才能同时解释「结构难」和「认知难」。

数据里还有两处需要修正:有两个单词只有四个字母,查证后应为 clean 和 trash 一类的五字母词。这类修正必须在论文里列出原始值和修正值,因为评委手里的原始数据是四字母,你不写清楚,他会以为你编数据。

注意:属性提取的依据一定要写「为什么选这 9 个」,比如「词频高说明玩家更熟悉,猜中轮次应当更少」。只列属性不给假设,评委会认为你是凑数。

3. 问题 1 拆解:ARIMA 定阶、预测区间、相关性三件事

问题 1 有两问:一是解释每日报告人数的波动并给出 2023 年 3 月 1 日的预测区间,二是判断单词属性是否影响困难模式占比。第一问考时间序列,第二问考相关性分析,两问共用同一份清洗后的数据,但方法论完全不同,论文里必须分成两个小节写,不能混在一段里。

这道题最容易出现的结构性错误是:把两问写成「先预测、再顺便做个相关」,导致第二问只有相关系数表没有结论。正确的展开顺序是——先描述波动形态(先升后降的长期趋势 + 周末效应),再建模,再给区间,最后单独用一节处理属性相关性。

3.1 ARIMA(p,d,q) 三个参数在论文里怎么讲到点上

ARIMA 的三个参数里,d 最好讲:原始序列有明显趋势,一阶差分后围绕均值波动,所以 d=1。p 和 q 是自回归项数和滑动平均项数,靠 ACF(自相关图)和 PACF(偏自相关图)的拖尾、截尾特征初定,再用 AIC 网格搜索在小范围里选最优。

论文里不要只写「我们用 ARIMA 模型预测」,要写三句话:序列经过一阶差分后 ADF 检验 p 值小于 0.05,序列平稳;ACF 在 2 阶后截尾、PACF 拖尾,初定 p=1~3、q=0~2;在候选集里按 AIC 最小选出最终阶数。这三句把「你为什么选这个模型」讲透了,比贴一张模型结构图管用。

from statsmodels.tsa.arima.model import ARIMA import warnings warnings.filterwarnings("ignore") y = df.set_index("date")["reported"].asfreq("D") best = None for p in range(0, 4): for q in range(0, 3): try: m = ARIMA(y, order=(p, 1, q)).fit() if best is None or m.aic < best[0]: best = (m.aic, p, q, m) except Exception: continue aic, p, q, model = best print(f"最优阶数 p={p}, q={q}, AIC={aic:.2f}")

这段网格搜索的循环范围不用开太大,p 取 0 到 3、q 取 0 到 2 已经覆盖绝大多数日度数据。d固定为 1 是因为差分次数由平稳性检验决定,不该参与搜索,否则会选出过差分的结果。try/except是为了跳过不收敛的参数组合,这在日度数据上很常见。

3.2 置信区间和预测区间不是一回事,别写反

论文给出的 2023 年 3 月 1 日预测区间是 [17363, 18994]。这个区间的宽度来自两部分误差:参数估计的不确定性和未来观测值本身的随机性。两者的区别必须讲清楚——置信区间描述的是「未来那天人数的期望值落在哪」,预测区间描述的是「未来那天实际观测到的人数落在哪」,后者永远比前者宽。

很多论文在这一步翻车,把软件默认输出的区间直接叫成置信区间,评委会立刻判断你没搞懂自己在算什么。正确的写法是:先给点预测,再说明「95% 置信水平下,均值的置信区间为 A,观测值的预测区间为 B」,两个数都列出来,并解释为什么 B 更宽。

提示:如果时间紧张只能给一个区间,给预测区间,并在文中说明「预测区间包含观测噪声,对实际决策更有意义」。这句话本身就是加分点。

3.3 属性与困难模式占比:Spearman 相关的答题模板

第二问的因变量是「困难模式人数占当日总人数的百分比」,自变量是 9 个单词属性。这里用 Spearman 而不是 Pearson,原因是占比数据不服从正态分布,且属性和占比之间更可能是单调关系而非线性关系。

答题模板分四步写:第一步说明为什么选 Spearman(秩相关、对异常值稳健、不要求线性);第二步给相关系数矩阵热图,标出显著项;第三步逐条解释显著项的方向和量级,比如「重复字母数每增加一个,困难模式占比上升若干个百分点,因为重复字母会误导玩家对字母位置的判断」;第四步给结论——属性有影响,但解释力有限,大部分变异仍由玩家群体构成解释。

第四步是拉开档次的地方。只写「有影响」是及格线,写出「影响存在但边际」才是 M 奖水平,因为这说明你真的看过残差,而不是硬凑显著。

4. 问题 2 和问题 3:PLS 回归与 K-means 分档的完整写法

问题 2 要预测未来某天某单词在 1 到 6 次及失败上的七个百分比,因变量有七个,且彼此不独立(加起来等于 100);自变量是 9 个单词属性。多因变量 + 多自变量 + 中等样本量,这三个条件同时成立时,普通多元线性回归会因为共线性直接崩掉,PLS 是这道题的标准解。

问题 3 要按难度给单词分类,但数据里没有难度标签,只能做无监督学习。先 K-means 分档,再用 PLS 预测的分布算加权难度分,最后用混淆矩阵验证。这两问在逻辑上是串起来的:问题 2 的输出是问题 3 的输入。

4.1 为什么是 PLS 而不是普通回归或主成分回归

PLS 的实质是把自变量和因变量的协方差最大化,同时做降维和回归。它和主成分回归的区别在于:主成分回归只对自变量做 PCA,找到的方差最大方向可能和因变量毫无关系;PLS 在降维时把因变量也纳进来,提取的成分对预测更有用。

选它的三个理由要在论文里写全:样本量三百多,属于中等偏小,不适合深度模型;9 个属性之间存在明显共线性(词频和字母常见度高度相关);7 个因变量共享同一组自变量,分开建七个模型会丢掉因变量之间的相关性。

from sklearn.cross_decomposition import PLSRegression from sklearn.model_selection import cross_val_predict from sklearn.metrics import mean_absolute_error, mean_squared_error from sklearn.preprocessing import StandardScaler X_attr = StandardScaler().fit_transform(word_features) # 9 个属性标准化 Y_pct = df[pct_cols].values # 7 列百分比 pls = PLSRegression(n_components=3, scale=True) pred = cross_val_predict(pls, X_attr, Y_pct, cv=5) print("MAE:", mean_absolute_error(Y_pct, pred, multioutput="raw_values")) print("MSE:", mean_squared_error(Y_pct, pred, multioutput="raw_values"))

n_components=3是主成分个数,通常用交叉验证的预测残差平方和曲线来定,曲线拐点处取值。scale=True让 PLS 内部再做一次标准化,防止词频这类量纲大的属性主导成分方向。multioutput="raw_values"会输出七个因变量各自的误差,论文里应当列成表——只报一个平均 MAE 会掩盖某一轮次预测特别差的情况。

原文报告的 MAE 落在 3 到 4 之间,MSE 落在 22 到 24 之间。这个量级在百分比尺度上是合理的:单轮次百分比的平均绝对误差三四个百分点,说明模型能抓住大致分布形态,但不足以精确到小数点。论文里必须补一句「误差主要来自 X(失败)列,因为失败率本身波动最大」,否则评委看到 MAE 3.5 会追问误差结构。

4.2 肘部法则与轮廓系数:K 怎么定、难度区间怎么切

K-means 的第一个问题是 K 取几。这道题的答案是五档,但结论必须由两个指标交叉验证得出,不能直接说「我们分成五类」。

肘部法则看的是簇内平方和(inertia)随 K 变化的曲线,拐点处就是合理 K 值;轮廓系数看的是每个点到本簇和其他簇的平均距离之比,取值 -1 到 1,越接近 1 说明分得越开。两个指标都指向 5 时,K=5 才站得住。

from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score inertia, sil = [], {} for k in range(2, 9): km = KMeans(n_clusters=k, n_init=10, random_state=42).fit(X_attr) inertia.append(km.inertia_) sil[k] = silhouette_score(X_attr, km.labels_) print("inertia:", [round(v, 1) for v in inertia]) print("silhouette:", {k: round(v, 3) for k, v in sil.items()})

n_init=10表示用 10 组不同初始质心各跑一次取最优,避免陷入局部最优,这个参数在样本量小的时候尤其重要。random_state=42是为了结果可复现,论文里应当写明随机种子。定完 K 之后,把五档的难度区间写出来(比如最容易档的平均猜中轮次 3.2,最难档 4.8),区间怎么切、依据是什么,都要交代。

4.3 EERIE 的难度分 4.635 是怎么算出来的

难度分不是聚类结果直接给的,而是用 PLS 预测出的七轮百分比加权算出来的。加权公式的思路是:越靠后的轮次权重越高,失败(X)权重最高。设七轮权重为 w1 到 wX,难度分 = Σ(wi × pi),其中 pi 是该单词在该轮次被猜中的百分比。

EERIE 的难度分是 4.635,落在第五档(最难档),说明这个词整体偏难。这个结果符合直觉:它含两个重复的 E、字母组合不常见、首字母在词首频率低。论文里要把「为什么它难」和「属性分析」对上——如果属性分析说重复字母提高难度,而 EERIE 恰好卡在高难度档,这条证据链就闭合了。

答题时容易漏掉的一步是归一化权重。七轮百分比加起来是 100,权重和也应当归一,否则难度分的量纲会随权重设定漂移,无法跨单词比较。

4.4 混淆矩阵热图和 0.941 准确率该怎么表述

分类模型的验证用混淆矩阵:行是真实档位,列是预测档位,对角线越深越好。原文报告的准确率是 0.941,这个数字在五分类任务里相当高,但高准确率本身需要解释——如果某一档样本极少,模型全预测成多数档也能拿到高准确率。

所以论文里不能只写 0.941,要写三件事:各类别的样本数是否均衡、每一档的召回率分别是多少、错分主要发生在哪两档之间。通常错分集中在相邻两档,因为难度本身是连续量,硬切成五段必然有边界样本。把「错分集中在相邻档,跨档错分极少」这句话写出来,评委会认可你理解了模型的性质,而不是只会报一个数。

注意:混淆矩阵的颜色深浅要在图注里说明含义(深色 = 样本多),否则评委得自己猜。

5. 易错点:这五处一写就掉档

以下五条来自这道题最常见的失分方式,每条按「错在哪、为什么错、怎么答对」拆开,都是可以在交稿前逐条自查的。

第一条:把异常值直接删掉,或者根本不提异常值。错在哪:论文里数据描述只有一句「数据完整无缺失」,而原始曲线里明明有两处断崖。为什么错:评委手里有同一份数据,画一遍图就能发现,你不提等于承认没做探索。怎么答对:把异常点定位过程、替换方法、替换前后对比图三样放进去,并说明替换对预测区间的影响幅度。

第二条:七列百分比不归一化直接进模型。错在哪:直接把 98% 到 102% 的数据当因变量喂给 PLS。为什么错:因变量不闭合,模型拟合的是一部分舍入噪声,MAE 会虚高,且误差结构无法解释。怎么答对:按行归一化并说明只对 95% 到 105% 的行生效,附录里附归一化前后的和校验表。

第三条:把置信区间当成预测区间写。错在哪:文中写「95% 置信区间为 [17363, 18994]」,但用的其实是软件输出的预测区间。为什么错:两个概念混淆说明建模细节没吃透,评委一旦追问就露馅。怎么答对:两个区间都给,明确写出「均值区间更窄、观测区间更宽」,并解释宽度差异来自观测噪声。

第四条:K-means 的 K 直接拍板定成 5,不给依据。错在哪:论文写「我们将单词分为五类」,前后没有任何指标支撑。为什么错:无监督学习不给定 K 依据,等于把结论建立在主观判断上,这是方法论的硬伤。怎么答对:肘部法则曲线 + 轮廓系数表两个证据并列,两个指标同时指向 5 才下结论,冲突时说明取舍理由。

第五条:准确率 0.941 单独出现,不做误差结构分析。错在哪:只报总体准确率,不报各类别召回率和错分分布。为什么错:五分类任务里,类别不均衡时总体准确率会严重美化模型,评委看的是你能不能识别这一点。怎么答对:混淆矩阵热图 + 每档召回率 + 一句「错分集中在相邻档,符合难度连续变化的预期」。

提示:这五条按顺序自查一遍,基本能挡掉评委最常问的追问。特别是第一条和第三条,几乎是每篇 S 奖论文的通病。

6. 押题与答题技巧:把四问写成一条能自证的链

美赛 C 题这类题目,阅卷时间有限,评委翻论文的顺序通常是:摘要、图表、结论、模型假设。想让四问在十分钟内被看懂,最有效的做法是让每一问的输出成为下一问的输入,形成一条能自证的链:清洗后的数据 → ARIMA 给出人数区间 → 属性相关性筛选出有效特征 → PLS 用这些特征预测七轮分布 → 分布加权成难度分 → K-means 分档 → 混淆矩阵回头验证分档。链条一旦成立,任何一问被质疑,你都能指回上一环。

具体操作上,我建议交稿前跑一遍下面这张自检表,每一项都对应一个可能被追问的点:

自检项合格标准常见不合格写法
异常值处理有定位方法、替换值、前后对比图「数据已清洗」一句话带过
百分比归一说明适用范围和校验方式直接归一,不提前提
ARIMA 定阶ADF 检验 + ACF/PACF + AIC 三证据只写「用 ARIMA 预测」
区间表述置信区间与预测区间分别列出两个概念混用
属性选择每个属性配一条影响假设只列 9 个名词
PLS 误差七列分别报 MAE/MSE只报一个平均值
聚类定 K肘部 + 轮廓双证据直接写 K=5
分类评估混淆矩阵 + 分档召回率只报总体准确率

另外有两个提分动作值得单独说。一是把问题 4 的「有趣特征」写成四条有数据支撑的观察,而不是罗列图表:参与人数随时间先升后降、首字母分布高度集中、猜中轮次近似正态、部分词缀组合异常偏高。每条观察后面跟一句「这说明什么」,比堆四张图有用。二是给编辑的那封信不要写成客套话,写成三条可执行建议,比如「对高难度词提前在提示位放一个元音」,这种具体建议在评分表上属于创新性加分。

我自己带队那年就吃过亏:预处理写得太省,模型部分倒是花了三天调参,结果评委反馈里第一条就是「数据清洗过程不透明」。从那以后我每次交稿前都强制走一遍上面那张表,从异常值一路核到混淆矩阵,哪怕时间只剩两小时也先核这张表再改文字。模型可以普通,链条不能断——这是这道题最实在的一条经验。希望帮到你。

本文还有配套的精品资源,点击获取