简介:这份资源是2023年美国大学生数学建模竞赛C类获奖论文《通过数据分析揭示Wordle的秘密》的完整PDF,面向备战美赛的本科生、研究生及建模指导教师,尤其适合希望学习数据挖掘与预测建模思路的参赛者。论文围绕Wordle游戏展开,核心内容包括基于GRU算法预测2023年3月1日结果报告数量,相对误差率仅2.1569%;分析词频、字母频率之和、字母重复模式与词性对得分的影响,并给出相关系数;构建网格搜索随机森林GSRF模型预测单词EERIE的得分分布,MSE为20.70641、MAE为3.24388;还利用K-Means++完成难度分级,分类匹配率达93.33%。资源包共1个PDF文件,大小约5.74MB,内容完整、排版清晰,便于打印研读与批注。目前已有195人学习,适合作为美赛C题的数据分析范本,帮助读者掌握从建模、求解到论文写作的全流程方法。
1. 从一份美赛 C 类获奖论文里,能拆出哪些可复用的建模套路
2023 年美赛 C 类题目给的是 Wordle 这类猜词游戏的数据,要求预测结果分布、给出最优猜测策略,还要做难度分类。这份编号 2300348 的获奖论文之所以值得反复看,不是因为它拿了奖,而是它把「数据清洗 → 分布预测 → 策略优化 → 分类建模」这条链路走得很完整,每一步都能单独拎出来复用到别的赛题或业务里。如果你正在准备数学建模竞赛,或者手头有一个「给历史数据、预测未来分布、再给出决策建议」的实际问题,这篇论文的结构就是一份现成的骨架。它适合两类人:一类是想知道获奖论文到底强在哪、怎么模仿的新手;另一类是已经能跑模型、但总在「模型选得对不对、参数怎么调、结果怎么验证」上翻车的熟手。下面我按自己复现这类论文的习惯,把每个环节拆开讲。
2. 数据预处理与特征工程:把 Wordle 的原始结果变成能建模的表
2.1 先搞清楚原始数据长什么样
Wordle 的数据通常以「日期、答案词、猜测次数分布、失败人数」这类字段出现,不同来源格式差异很大。我一般先做三件事:确认字段含义、统计缺失和异常、把分布转成比例。很多队伍一上来就套 LSTM,结果因为没把「每天参与人数不同」这件事处理掉,预测出来的绝对人数毫无意义。正确做法是把每个猜测次数(1 到 6 次以及失败)除以当天总人数,得到概率分布,这样不同日期的数据才可比。
import pandas as pd import numpy as np # 读取原始数据,假设列名为 date, answer, n1..n6, nfail df = pd.read_csv("wordle_raw.csv", parse_dates=["date"]) # 计算每天总参与人数 count_cols = ["n1", "n2", "n3", "n4", "n5", "n6", "nfail"] df["total"] = df[count_cols].sum(axis=1) # 过滤掉总人数过少的异常日期 df = df[df["total"] > 1000].copy() # 转成概率分布 for c in count_cols: df[c + "_p"] = df[c] / df["total"] # 检查每行概率和是否为 1 prob_cols = [c + "_p" for c in count_cols] assert np.allclose(df[prob_cols].sum(axis=1), 1.0, atol=1e-6) print(df[["date", "answer"] + prob_cols].head())这段代码的关键在total > 1000这个阈值和概率归一化。阈值不是固定的,要看数据量级,目的是剔除爬虫抓取不全或统计口径变化的日期。概率归一化之后,后续无论用回归还是分类,目标变量都在 0 到 1 之间,模型不会因为某天人数暴涨而跑偏。
2.2 特征怎么构造才有区分度
获奖论文里通常会构造几类特征:单词本身的语言学特征(字母频率、重复字母、元音数量)、历史表现特征(前几天的平均猜测次数、难度趋势)、以及时间特征(星期几、是否节假日)。我自己的经验是,字母频率和重复字母这两个特征对预测分布贡献最大,因为 Wordle 的答案词难度直接取决于字母是否常见、是否有重复。构造时注意不要用未来数据,比如预测第 t 天时只能用 t-1 及之前的统计量,否则就是数据泄露,论文里如果没写清楚这一点,复现时很容易踩坑。
from collections import Counter # 英语字母频率表,可用通用频率,也可用训练集统计 letter_freq = Counter("etaoinshrdlucmfwypvbgkjqxz") def word_features(word): word = word.lower() counts = Counter(word) return { "len": len(word), "unique_letters": len(counts), "has_repeat": int(len(counts) < len(word)), "vowel_count": sum(1 for ch in word if ch in "aeiou"), "avg_letter_freq": np.mean([letter_freq[ch] for ch in word]), "rare_letter_count": sum(1 for ch in word if letter_freq[ch] < 5), } feat_df = df["answer"].apply(lambda w: pd.Series(word_features(w))) df = pd.concat([df, feat_df], axis=1) print(df[["answer", "avg_letter_freq", "rare_letter_count", "has_repeat"]].head())avg_letter_freq越低说明单词越冷门,rare_letter_count越高说明越难猜,这两个特征在后续分类难度时非常有用。注意letter_freq最好用训练集统计而不是通用英语频率,因为 Wordle 的答案词库本身有筛选,通用频率会有偏差。
3. 分布预测模型:从线性回归到梯度提升,怎么选、怎么调
3.1 为什么不能直接预测「平均猜测次数」
很多新手会直接把每天的加权平均猜测次数作为目标,用回归去拟合。这样做的问题是丢失了分布信息,而题目往往要求你给出完整的 1 到 6 次及失败的概率。正确做法是对每个概率分量分别建模,或者用多输出回归。我一般先用线性回归做基线,再用梯度提升树(如 LightGBM)做主力,因为树模型对特征交互和非线性关系捕捉更好,而且不需要太多特征缩放。
from sklearn.linear_model import LinearRegression from sklearn.multioutput import MultiOutputRegressor from sklearn.model_selection import TimeSeriesSplit from sklearn.metrics import mean_absolute_error import lightgbm as lgb X = df[["avg_letter_freq", "rare_letter_count", "has_repeat", "vowel_count", "unique_letters"]] y = df[prob_cols] # 时间序列切分,不能用随机切分 tscv = TimeSeriesSplit(n_splits=5) baseline_mae, lgb_mae = [], [] for train_idx, test_idx in tscv.split(X): X_tr, X_te = X.iloc[train_idx], X.iloc[test_idx] y_tr, y_te = y.iloc[train_idx], y.iloc[test_idx] lr = MultiOutputRegressor(LinearRegression()) lr.fit(X_tr, y_tr) baseline_mae.append(mean_absolute_error(y_te, lr.predict(X_te))) model = lgb.LGBMRegressor(n_estimators=300, learning_rate=0.05, num_leaves=31) multi = MultiOutputRegressor(model) multi.fit(X_tr, y_tr) lgb_mae.append(mean_absolute_error(y_te, multi.predict(X_te))) print("Linear baseline MAE:", np.mean(baseline_mae)) print("LightGBM MAE:", np.mean(lgb_mae))这里必须用TimeSeriesSplit,因为数据有时间顺序,随机切分会让模型看到未来信息,评估结果虚高。n_estimators=300、learning_rate=0.05、num_leaves=31是我常用的起点,如果 MAE 下降不明显就调低学习率、增加树的数量,但要盯着验证集防止过拟合。多输出回归会对每个概率分量独立建模,预测出来的概率和可能不等于 1,需要做一次归一化。
3.2 概率归一化和后处理
多输出回归的输出不能保证非负、也不能保证和为 1。我一般先做 clip 到 [0,1],再除以行和。如果某些分量预测为负,说明模型在该分量上不稳定,可以考虑对每个分量单独用分位数回归或者直接换用 softmax 输出结构。获奖论文里常见做法是加一层归一化,但不会写得太细,复现时这一步不做,后续策略优化就会因为概率不合法而出错。
pred = multi.predict(X_te) pred = np.clip(pred, 0, 1) pred = pred / pred.sum(axis=1, keepdims=True) print("归一化后行和:", pred.sum(axis=1)[:5])3.3 模型评估不能只看 MAE
MAE 只能告诉你平均偏差,但分布预测更关心形状是否对。我一般会额外看两个指标:一是预测分布和真实分布的 KL 散度,二是把预测分布还原成「最可能猜测次数」后和真实众数的命中率。KL 散度对零概率敏感,所以要先给所有分量加一个很小的 epsilon。这两个指标在论文里不一定都写,但复现时加上能帮你判断模型是不是真的学到了分布形状,而不是只拟合了均值。
4. 策略优化与难度分类:从预测结果到可执行建议
4.1 最优猜测策略怎么建模
Wordle 的策略优化本质是在每一步选择信息增益最大的词。获奖论文里通常用信息熵来衡量:对候选词集合,计算每个猜测词能带来的期望信息量,选熵最大的。复现时不需要真的去模拟所有单词,可以用一个简化版:只考虑首词选择,用答案词库的字母频率和位置频率来打分。我一般会先算每个字母在五个位置上的出现频率,再给每个候选词打分,选分数最高的作为推荐首词。
from collections import defaultdict # 假设 answers 是答案词列表 pos_freq = [defaultdict(int) for _ in range(5)] for w in answers: for i, ch in enumerate(w): pos_freq[i][ch] += 1 def score_word(word): score = 0 for i, ch in enumerate(word): score += pos_freq[i].get(ch, 0) # 惩罚重复字母,因为重复字母信息增益低 if len(set(word)) < len(word): score *= 0.8 return score best = sorted(answers, key=score_word, reverse=True)[:10] print("推荐首词:", best)这个打分函数是简化版,但已经能给出比随机猜好得多的首词。0.8这个惩罚系数是我试出来的,重复字母确实会降低信息增益,但具体数值可以根据模拟结果调整。如果要更严谨,可以用信息熵公式替换打分函数,但计算量会大很多。
4.2 难度分类的特征和模型选择
难度分类通常是把每天的答案词分成「简单、中等、困难」三档,依据可以是平均猜测次数或失败率。我一般用分位数切分,比如按平均猜测次数的 33% 和 66% 分位切。特征就用第 2 章构造的那些,模型用随机森林或 LightGBM 分类。注意类别不平衡问题,困难档通常样本少,可以用 class_weight 或者过采样。
from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report # 按平均猜测次数分三档 df["avg_guesses"] = sum(df[f"n{i}_p"] * i for i in range(1, 7)) + df["nfail_p"] * 7 df["difficulty"] = pd.qcut(df["avg_guesses"], q=3, labels=["easy", "medium", "hard"]) Xc = df[["avg_letter_freq", "rare_letter_count", "has_repeat", "vowel_count", "unique_letters"]] yc = df["difficulty"] clf = RandomForestClassifier(n_estimators=200, class_weight="balanced", random_state=42) clf.fit(Xc, yc) print(classification_report(yc, clf.predict(Xc)))class_weight="balanced"是为了缓解类别不平衡,n_estimators=200是常用起点。分类报告里重点看 hard 档的召回率,如果太低说明特征对困难词的区分度不够,需要补充更多语言学特征,比如字母组合的常见度。
5. 避坑与排查:复现这类论文时最容易翻车的 5 个地方
5.1 数据泄露:用了未来信息做特征
现象是验证集指标好得离谱,但换一段数据就崩。原因通常是构造特征时用了全局统计量,比如用整个数据集算字母频率,然后去预测早期日期。解决方法是所有统计量都只在训练集上计算,再应用到验证集和测试集。时间序列问题尤其要注意,滚动窗口统计也要确保窗口只包含过去数据。
5.2 概率预测不归一化导致策略优化失效
现象是信息熵计算出负值或者无穷大。原因是多输出回归的输出没有做 clip 和归一化,出现了负数或零。解决方法是在预测后强制 clip 到 [0,1] 再除以行和,如果还有零概率,加一个很小的 epsilon 比如 1e-6。
5.3 用随机切分代替时间序列切分
现象是交叉验证分数很高,但实际预测未来日期时误差很大。原因是随机切分让模型看到了未来数据,评估结果虚高。解决方法是始终用 TimeSeriesSplit 或按时间留出最后一段做测试,不要用 train_test_split 的默认随机。
5.4 难度分类的阈值拍脑袋定
现象是分类结果和直觉不符,简单词被分到困难档。原因是分位数切分受异常值影响,或者用了绝对阈值而不是相对分位。解决方法是先画平均猜测次数的分布图,确认分位数合理,必要时用聚类代替固定分位。另外类别不平衡时要看召回率而不是准确率。
5.5 策略优化只考虑首词,忽略后续步骤
现象是首词推荐看起来合理,但整体猜测次数没有下降。原因是 Wordle 是动态决策过程,首词之后要根据反馈调整候选集。解决方法是至少做一个两阶段模拟:首词后根据反馈筛选候选词,再算第二步的信息增益。如果计算资源有限,可以只对困难词做多步模拟,简单词用首词策略即可。
6. 把论文里的模型变成可复用的验证脚本
最后一章我想讲一个具体技巧:怎么用一份脚本快速验证你复现的模型是否真的学到了东西。我自己的习惯是写一个validate.py,输入是原始数据路径,输出是三个指标:分布预测的 KL 散度、难度分类的 macro F1、以及首词推荐在模拟中的平均猜测次数。这个脚本不依赖任何论文里的具体数值,只依赖数据本身,所以换一份 Wordle 数据也能跑。
import argparse import pandas as pd import numpy as np from scipy.stats import entropy from sklearn.metrics import f1_score from sklearn.model_selection import TimeSeriesSplit def validate(data_path): df = pd.read_csv(data_path, parse_dates=["date"]) # 这里省略特征构造和模型训练,假设已有 pred_prob 和 true_prob # 实际使用时把第 2、3 章的代码封装成函数调用 kl = np.mean([entropy(t, p + 1e-6) for t, p in zip(true_prob, pred_prob)]) f1 = f1_score(true_label, pred_label, average="macro") print(f"KL divergence: {kl:.4f}") print(f"Macro F1: {f1:.4f}") # 首词模拟:用推荐首词跑 1000 次随机答案,统计平均猜测次数 avg_guesses = simulate_first_word(best_first_word, answers, n=1000) print(f"Avg guesses with best first word: {avg_guesses:.2f}") if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--data", required=True) args = parser.parse_args() validate(args.data)这个脚本的价值在于把「模型好不好」变成一个可重复执行的命令,而不是靠肉眼看图表。KL 散度低于 0.1、macro F1 高于 0.7、平均猜测次数低于 4.5,这三个阈值是我在多个类似数据集上总结的经验值,可以作为你判断复现是否成功的参考。如果某个指标明显偏离,就回到对应章节检查特征、切分和归一化。我踩过最深的坑是只跑了一次随机切分就下结论,后来改成时间序列切分加多次滚动验证,才发现之前的分数全是假的。希望帮到你。
本文还有配套的精品资源,点击获取