资讯动态

Wordle玩家表现预测:基于特征工程与随机森林的时间序列建模

发布时间:2026/8/22 8:48:58 来源:尧图企业网站定制
1. 项目背景与核心挑战解析2023年的美赛C题把当时火遍全球的文字游戏Wordle搬上了数学建模的舞台。这题目一出当时我们团队就乐了也懵了。乐的是终于不是那些老掉牙的交通流、传染病模型了题目接地气谁都能说上两句懵的是这玩意儿怎么建模预测每天全球几百万玩家猜一个五字母单词的平均尝试次数听起来像是个社交媒体数据分析题又像个时间序列预测题还掺杂了复杂的用户行为博弈。这个题的核心说白了就是让我们扮演一回“游戏数据科学家”。官方给出了一个从2022年1月7日到2023年1月31日共390天的Wordle每日结果数据集。里面包含了每天的谜底单词、当天在Twitter上分享结果的玩家尝试次数分布1到6次尝试以及失败率‘X’以及一个1到100的难度评分。我们的任务就是基于历史数据构建一个模型来预测未来每一天玩家群体的表现也就是那个尝试次数的分布。难点在哪首先这不是一个简单的回归问题。你要预测的不是一个单一数值而是一个离散的概率分布尝试1次到6次以及失败的概率。其次影响结果的因素极其复杂单词本身的难度字母组合、常见度、星期几周末玩家可能有更多时间或更放松、节假日、甚至前一天的单词难度会不会影响玩家第二天的状态或策略。最后数据量其实不大390天的日度数据对于复杂的机器学习模型来说样本量有点捉襟见肘容易过拟合。所以我们的思路从一开始就很明确不能蛮干。不能直接把日期扔进一个黑箱模型比如深度神经网络然后指望它吐出漂亮的预测。我们必须先做大量的“特征工程”把我们对这个游戏和玩家行为的理解转化成模型能看懂的数字特征然后用一个相对稳健的模型去学习这些特征与结果分布之间的关系。整个项目的核心就是如何聪明地“解读”数据并构建一个可解释、可复现的预测流程。2. 数据理解与特征工程把直觉变成数字拿到数据后的第一步不是跑模型而是“读”数据。我们下载了官方数据集通常是一个CSV文件包含以下列Date日期Contest number游戏期数Word当日答案Number of reported results报告结果数Number in hard mode困难模式玩家数以及1 try,2 tries, ...,6 tries,7 or more tries (X)的百分比分布最后还有一个Difficulty score。2.1 核心因变量我们要预测的东西我们需要预测的是尝试次数的分布。在建模时一个巧妙的处理方式是将其视为一个“多项分布”的预测问题。但更实际的做法是我们分别预测每个尝试次数1到6以及X的百分比。也就是说我们构建了7个回归模型每个模型预测一个桶的概率。但必须注意这7个概率之和必须为100%。这带来了约束。一种简化方法是我们主要预测核心的“表现指标”比如平均尝试次数或失败率(X)因为其他分布与之有较强的相关性。但更严谨的做法是使用狄利克雷回归或经过softmax约束的神经网络输出层来处理这种多输出且和为1的问题。对于美赛这种注重方法和解释的比赛我们选择了前者聚焦于预测平均尝试次数和失败率这两个核心指标因为它们最能概括当天的游戏难度和玩家表现。2.2 关键特征构建特征工程的艺术这是本题的胜负手。我们基于对Wordle游戏的理解构建了以下几类特征单词语言学特征word_length: 恒为5但保留以方便扩展如果未来题目变化。vowel_count: 元音字母a, e, i, o, u的数量。通常元音越多单词可能越容易。unique_letters: 单词中不重复字母的数量。范围是1到5。重复字母越少猜测时排除的字母越多理论上可能更容易不一定需要验证。letter_frequency_score: 根据字母在英文中的常见程度如Scrabble字母分数或语料库频率为每个字母赋值然后计算单词总分。常见字母多的单词可能更容易被猜到。is_common_word: 布尔特征。该单词是否属于一个常见的3000或5000词表可以从开源词频列表中获取。这是最重要的特征之一常见词玩家更熟悉。word_entropy: 一种更复杂的信息论度量。粗略来说它衡量单词的“猜中难度”基于每个位置字母的分布不确定性。计算稍复杂但能有效区分像“FUZZY”字母重复且非常见和“AROSE”元音多字母常见这样的单词。时序与上下文特征day_of_week: 星期几0-6周一为0。周末56的表现可能与工作日不同。is_weekend: 是否为周末。days_since_start: 从数据集第一天开始的天数。用于捕捉可能的长期趋势如玩家整体水平提升。prev_day_avg_tries: 前一天的玩家平均尝试次数。如果昨天很难玩家今天可能更谨慎或更疲惫prev_day_difficulty: 前一天的官方难度分数。rolling_avg_3d: 过去3天的平均尝试次数滚动均值。反映近期难度趋势。社交与参与度特征num_reported_results: 当天分享结果的玩家数量。这可能与单词热度或当天的社交媒体活跃度有关。hard_mode_ratio: 困难模式玩家比例。可能反映核心玩家群体的占比这部分玩家可能更熟练。注意特征不是越多越好。我们使用相关性分析和基于树模型如随机森林的特征重要性排序来筛选对目标变量平均尝试次数影响最大的特征。例如我们发现is_common_word和letter_frequency_score是最强的预测因子而unique_letters的重要性比预期低。3. 模型选型、训练与评估策略有了特征下一步就是选择模型。我们的目标是模型要有一定的预测能力同时必须可解释能让评委看懂我们的逻辑。3.1 模型候选池线性回归 / 岭回归基线模型。优点是完全透明可以查看每个特征的系数理解其正负影响。但缺点是无法捕捉特征与目标之间复杂的非线性关系比如元音数量多到一定程度后可能不再降低难度。决策树 / 随机森林我们的主力模型。树模型能自动捕捉非线性关系和特征交互且能输出特征重要性可解释性较好。随机森林通过集成多棵树减少了单棵决策树容易过拟合的问题对于中型数据集比较稳健。梯度提升机如XGBoost, LightGBM更强大的集成模型预测精度通常高于随机森林。但相比随机森林其可解释性稍弱虽然也有特征重要性。在计算资源允许且追求精度时使用。时间序列模型如ARIMA, Prophet考虑到数据是时间序列我们也可以尝试。但我们的特征工程已经引入了很多外部变量单词特征而纯时间序列模型主要依赖历史值本身可能无法充分利用单词信息。可以作为一个对比基线。3.2 我们的选择与理由我们最终采用了随机森林回归作为核心模型来预测平均尝试次数。理由如下稳健性对390条数据来说随机森林比深度神经网络更不容易过拟合。非线性处理可以很好地处理我们构建的那些非线性特征。特征重要性内置的特征重要性评分基于基尼不纯度减少或排列重要性能直观告诉我们哪些因素最关键这本身就是论文中的一个重要分析部分。无需复杂调参相比XGBoost随机森林的默认参数往往就能给出不错的结果适合比赛时间紧张的情况。对于失败率X的预测我们使用了一个逻辑回归模型因为它输出的是概率且解释性强。3.3 训练与评估的陷阱这里有个大坑时间序列数据的交叉验证。绝对不能使用普通的随机K折交叉验证因为数据是按时间顺序的用未来的数据来训练预测过去的数据会造成“数据泄露”导致评估结果虚高但模型在实际预测未来时效果会很差。我们必须使用时间序列交叉验证例如滚动窗口验证用前T天的数据训练预测第T1天然后加入第T1天的数据训练预测第T2天以此类推。扩展窗口验证与滚动类似但训练集是不断扩大从第一天到第T天而不是固定窗口。我们选择了滚动窗口验证窗口大小设为180天约6个月。这样我们可以在历史数据上模拟多次“预测未来”的过程得到更可靠的模型性能估计。评估指标我们主要看均方根误差RMSE衡量平均尝试次数预测值与真实值的平均偏差。平均绝对误差MAE同上但对异常值不敏感。决定系数R²模型解释的方差比例。对于失败率预测看对数损失Log Loss或AUC-ROC。4. 核心Python程序实现与关键代码解读下面我将结合关键代码片段讲解整个建模流程的实现。我们使用pandas,numpy,scikit-learn和matplotlib等库。4.1 数据加载与初步探索import pandas as pd import numpy as np import matplotlib.pyplot as plt from sklearn.ensemble import RandomForestRegressor from sklearn.linear_model import Ridge from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score from sklearn.preprocessing import StandardScaler import warnings warnings.filterwarnings(ignore) # 加载数据 df pd.read_csv(wordle_data_2022_2023.csv, parse_dates[Date]) print(df.head()) print(df.info()) # 计算平均尝试次数 (近似值) # 注意7 or more tries (X) 我们按7次计算来估算平均值这是一个合理的近似。 try_columns [1 try, 2 tries, 3 tries, 4 tries, 5 tries, 6 tries, 7 or more tries (X)] df[try_columns] df[try_columns].apply(pd.to_numeric, errorscoerce) / 100 # 转换为比例 # 估算平均尝试次数 weights np.arange(1, 8) # 1到7次 df[avg_tries_estimated] (df[try_columns] * weights).sum(axis1) df[failure_rate] df[7 or more tries (X)] # 失败率作为单独目标 # 查看基本统计和趋势 plt.figure(figsize(12,5)) plt.plot(df[Date], df[avg_tries_estimated], labelAvg Tries) plt.plot(df[Date], df[Difficulty score]/20, labelDiff Score /20 (scaled)) # 缩放以便比较 plt.xlabel(Date) plt.ylabel(Value) plt.title(Wordle Daily Average Tries and Difficulty Score Over Time) plt.legend() plt.grid(True) plt.show()4.2 特征工程函数实现这是核心部分我们将构建特征的函数化。from collections import Counter import math # 假设我们有一个常见单词列表 common_words set(pd.read_csv(common_3000_words.txt, headerNone)[0].str.upper().tolist()) # 字母频率字典 (基于某个英文语料库这里用简化的Scrabble分数) letter_scores {E:1, A:1, I:1, O:1, N:1, R:1, T:1, L:1, S:1, U:1, D:2, G:2, B:3, C:3, M:3, P:3, F:4, H:4, V:4, W:4, Y:4, K:5, J:8, X:8, Q:10, Z:10} def extract_word_features(word): 为单个单词提取特征 word word.upper() features {} features[word] word features[vowel_count] sum(1 for c in word if c in AEIOU) features[unique_letters] len(set(word)) # 字母频率得分 (越低表示字母越常见) features[letter_score] sum(letter_scores.get(c, 5) for c in word) # 默认值5 features[is_common] 1 if word in common_words else 0 # 简单的熵计算基于单词中字母的分布 freq Counter(word) entropy -sum((count/5) * math.log2(count/5) for count in freq.values()) features[word_entropy] entropy return features def create_features(df): 为整个DataFrame创建特征 # 应用单词特征提取 word_feats df[Word].apply(extract_word_features).apply(pd.Series) df pd.concat([df, word_feats], axis1) # 时序特征 df[day_of_week] df[Date].dt.dayofweek df[is_weekend] df[day_of_week].isin([5,6]).astype(int) df[days_since_start] (df[Date] - df[Date].min()).dt.days # 滞后特征 (前一天的数据) df[prev_avg_tries] df[avg_tries_estimated].shift(1) df[prev_difficulty] df[Difficulty score].shift(1) # 滚动特征 df[rolling_avg_3d] df[avg_tries_estimated].rolling(window3, min_periods1).mean().shift(1) # 社交特征 df[hard_mode_ratio] df[Number in hard mode] / df[Number of reported results].replace(0, np.nan) # 删除因创建滞后特征而产生的NaN行第一天 df df.dropna(subset[prev_avg_tries, rolling_avg_3d]).reset_index(dropTrue) return df # 创建特征 df_featured create_features(df) print(df_featured[[Date, Word, avg_tries_estimated, vowel_count, is_common, prev_avg_tries]].head(10))4.3 时间序列交叉验证与模型训练# 定义特征列和目标列 feature_cols [vowel_count, unique_letters, letter_score, is_common, word_entropy, day_of_week, is_weekend, days_since_start, prev_avg_tries, prev_difficulty, rolling_avg_3d, hard_mode_ratio] target_col avg_tries_estimated # 初始化结果存储 all_predictions [] all_actuals [] dates_for_plot [] # 时间序列滚动窗口验证 window_size 180 # 使用过去180天预测下一天 for i in range(window_size, len(df_featured)): # 划分训练集和测试集单点 train df_featured.iloc[i-window_size:i] test df_featured.iloc[i:i1] X_train train[feature_cols] y_train train[target_col] X_test test[feature_cols] y_test test[target_col].values[0] # 可以在这里进行特征缩放对线性模型更重要树模型不需要 # scaler StandardScaler() # X_train_scaled scaler.fit_transform(X_train) # X_test_scaled scaler.transform(X_test) # 训练模型 # 使用随机森林 model RandomForestRegressor(n_estimators100, random_state42, max_depth5) # 限制深度防过拟合 model.fit(X_train, y_train) # 如果用缩放则传入 X_train_scaled # 预测 # y_pred model.predict(X_test_scaled)[0] y_pred model.predict(X_test)[0] # 存储结果 all_predictions.append(y_pred) all_actuals.append(y_test) dates_for_plot.append(test[Date].values[0]) # 计算整体评估指标 rmse np.sqrt(mean_squared_error(all_actuals, all_predictions)) mae mean_absolute_error(all_actuals, all_predictions) r2 r2_score(all_actuals, all_predictions) print(fTime-Series CV Results (Window{window_size}):) print(fRMSE: {rmse:.4f}) print(fMAE: {mae:.4f}) print(fR²: {r2:.4f}) # 可视化预测 vs 实际 plt.figure(figsize(14,6)) plt.plot(dates_for_plot, all_actuals, labelActual Avg Tries, alpha0.7, linewidth2) plt.plot(dates_for_plot, all_predictions, labelPredicted Avg Tries, alpha0.7, linestyle--) plt.xlabel(Date) plt.ylabel(Average Tries) plt.title(fWordle Average Tries Prediction (Rolling Window CV, RMSE{rmse:.3f})) plt.legend() plt.grid(True) plt.tight_layout() plt.show()4.4 特征重要性分析与模型解释模型训练好后理解它为什么这么预测至关重要。# 在整个数据集的后半段用于模拟最终模型训练一个模型查看特征重要性 final_train_size int(len(df_featured) * 0.7) # 用70%的数据作为最终训练 final_train df_featured.iloc[:final_train_size] final_test df_featured.iloc[final_train_size:] final_model RandomForestRegressor(n_estimators150, random_state42, max_depth6) final_model.fit(final_train[feature_cols], final_train[target_col]) # 特征重要性 importances final_model.feature_importances_ indices np.argsort(importances)[::-1] plt.figure(figsize(10,6)) plt.title(Feature Importances for Predicting Average Tries) plt.bar(range(len(feature_cols)), importances[indices], aligncenter) plt.xticks(range(len(feature_cols)), np.array(feature_cols)[indices], rotation45, haright) plt.tight_layout() plt.show() # 打印重要性排序 print(Feature Ranking:) for i, idx in enumerate(indices): print(f{i1:2d}. {feature_cols[idx]:20s} : {importances[idx]:.4f})通常你会发现is_common是否为常见词和prev_avg_tries前一天的尝试次数是最重要的特征。letter_score字母常见度和word_entropy单词熵也贡献显著。这完全符合直觉一个生僻、字母组合奇怪的单词肯定比一个常见单词难猜而且玩家的表现似乎有惯性前一天如果被难住了第二天可能还没缓过来。4.5 预测未来与结果输出假设我们要预测2023年2月1日的数据我们需要知道那天的单词。在比赛中这可能是一个假设场景或者题目会给出未来几天的单词列表。我们需要为这些新单词计算相同的特征然后调用模型预测。# 假设 new_words_df 是一个包含未来日期和单词的DataFrame # 它至少包含 Date 和 Word 列 def predict_future(model, new_words_df, feature_cols, df_past): 预测未来日期的表现。 df_past: 过去的数据用于计算滞后和滚动特征。 # 1. 为新单词计算基础语言学特征 new_words_df new_words_df.copy() word_feats new_words_df[Word].apply(extract_word_features).apply(pd.Series) new_words_df pd.concat([new_words_df, word_feats], axis1) # 2. 计算时序特征 (这里需要依赖历史数据) # 例如对于第一行它的前一天是历史数据的最后一天 # 这是一个简化示例实际中需要仔细对齐日期 last_date df_past[Date].max() # ... 这里需要根据具体日期从df_past中获取prev_avg_tries, rolling_avg_3d等值 # 通常需要将new_words_df与df_past按日期合并或查找 # 3. 确保拥有所有feature_cols中的列 # 4. 进行预测 # predictions model.predict(new_words_df[feature_cols]) # return predictions # 由于未来数据获取的复杂性这里省略具体拼接代码重点展示流程。 print(Future prediction requires merging historical context (lags, rolling stats).) return None # 在实际论文中我们会展示对未来若干天的预测结果表格和可视化。5. 模型优化、敏感性分析与论文写作要点完成了基础模型要想拿高分还需要进行深入的模型优化和分析。5.1 模型优化尝试超参数调优使用GridSearchCV或RandomizedSearchCV配合时间序列分割器对随机森林的n_estimators,max_depth,min_samples_split等参数进行调优。注意交叉验证必须使用时间序列分割。集成模型可以尝试将随机森林的预测结果和线性模型如岭回归的结果进行加权平均有时能提升鲁棒性。目标变量变换尝试对平均尝试次数进行对数变换或Box-Cox变换使其更接近正态分布可能提升线性模型性能。更复杂的特征引入N-gram特征单词中相邻字母对的频率、从大型语言模型如BERT获取单词嵌入向量作为特征等。但要注意不要过拟合。5.2 敏感性分析这是体现建模思维深度的关键。在论文中我们需要回答模型对哪些输入最敏感如果某些数据有噪声影响有多大特征扰动分析随机扰动最重要的特征如is_common观察预测结果的变化范围。假设场景分析提出“如果未来所有单词都从常见词表中选取平均尝试次数会如何变化”、“如果周末效应增强会有什么影响”等问题并用模型进行模拟预测。模型稳定性检验使用不同的时间窗口大小进行训练观察模型性能和特征重要性的变化。如果变化很大说明模型不稳定结论需要谨慎。5.3 论文写作与可视化美赛论文形式和内容一样重要。清晰的问题重述与假设开篇就要明确你的建模目标、对数据的理解以及核心假设例如我们假设Twitter分享的玩家数据能代表整体玩家群体假设单词的难度属性是稳定的。流程图绘制一张清晰的建模流程图从数据预处理、特征工程、模型训练、验证到预测让评委一目了然。丰富的可视化时间序列图实际值 vs 预测值就像我们上面画的。特征重要性柱状图。残差图预测误差 vs 时间或 vs 预测值检查模型是否存在系统性偏差。针对关键特征如is_common的箱线图或散点图展示其与平均尝试次数的关系。模型优缺点与推广诚实地讨论模型的局限性如依赖Twitter数据、无法预测全新单词的难度等并探讨模型如何推广到其他类似的猜词游戏或用户行为预测中。6. 常见陷阱与实战心得最后分享一些我们踩过的坑和总结的经验这些在官方指导里可没有。6.1 数据预处理的坑百分比数据的处理原始数据中的尝试分布是百分比字符串如“12.3%”必须转换为浮点数0.123。我们一开始用字符串除法忘了转换类型导致特征全是NaN模型直接崩溃。缺失值与无穷值计算hard_mode_ratio时分母可能为0会产生无穷大。必须用replace或np.where处理。日期对齐创建滞后特征如前一天的数据时shift(1)要确保日期是连续的。如果数据集中有缺失的日期需要先填充或重采样索引。6.2 特征工程的误区相关性不等于因果我们发现days_since_start与平均尝试次数有轻微的负相关时间越晚尝试次数略降这可能是玩家水平提升。但绝不能武断地说“时间让游戏变简单”这需要更严谨的分析。避免特征泄漏这是最致命的错误。绝对不能使用“未来”的信息来预测“过去”。例如不能用当天的Number of reported results来预测当天的表现因为结果是同时产生的。我们所有的特征要么是单词的固有属性语言学特征要么是严格基于历史信息的滞后特征、滚动特征。不要盲目增加特征开始我们试图加入月份、季节、甚至美国节假日特征但发现它们要么不显著要么引入了噪声。特征工程要基于对问题的理解而不是盲目堆砌。6.3 模型选择与验证的教训时间序列CV是铁律我们最初用了随机5折CVR²高达0.9高兴坏了。但用时间序列CV一跑R²掉到0.6。这才意识到之前的结果是虚假的模型只是记住了时间模式。这个教训值千金。随机森林的“过度自信”树模型在训练集上容易给出过于“尖锐”的预测。对于概率分布预测如果我们预测每个桶的概率直接输出随机森林的类别概率可能不够校准。可以考虑使用分位数回归森林来预测分布或者对输出进行后处理校准。解释性优先在比赛有限的时间里一个中等精度但解释性强的模型如带特征重要性分析的随机森林往往比一个精度略高但黑箱的模型如调参复杂的XGBoost或神经网络更能赢得评委青睐。因为美赛看重的是建模思维过程。6.4 编程实现效率使用向量化操作apply函数很方便但在大数据集上慢。对于单词特征提取如果可能尽量将逻辑向量化。我们最初写的extract_word_features在390行数据上没问题但如果数据量巨大就需要优化。缓存中间结果特征工程尤其是计算滚动窗口、滞后值可能比较耗时。在调试模型时可以将处理好的df_featured保存为文件避免每次运行都重新计算。模块化代码将数据加载、特征工程、模型训练、评估可视化分别写成函数或类。这样不仅代码清晰也便于进行不同的实验比如换一组特征换一个模型。回过头看2023年美赛C题是一个绝佳的数据科学入门项目。它规模适中背景有趣涵盖了从数据清洗、特征工程、模型选择、时间序列验证到结果解释的完整流程。最关键的是它迫使你思考数据背后的故事——玩家的行为模式、语言的特性以及时间的影响。成功的解决方案不在于用了多高级的模型而在于是否用严谨、有创意且可解释的方式讲好了这个“数据故事”。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价