ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Wordle游戏预测:基于时间序列与NLP的数学建模实战

Wordle游戏预测:基于时间序列与NLP的数学建模实战 1. 项目概述当数学建模遇上每日热词去年年初我带着几个学生组队参加了美赛MCM/ICM。选题时C题“Predicting Wordle Results”一下子抓住了我们的眼球。Wordle这个当时火遍全球的每日猜词小游戏居然成了数学建模的考题这本身就很有意思。它不像传统的物理、工程问题有明确的公式也不像社会经济问题有海量的统计数据。它的核心是一串串由玩家每日生成的、看似随机的推特Twitter文本数据。题目要求我们基于历史数据构建模型来预测未来每一天Wordle游戏的结果分布包括解题尝试次数1-6次、失败X的百分比以及每天在推特上讨论该游戏的推文数量。这本质上是一个融合了时间序列预测、自然语言处理NLP情感/主题分析以及回归/分类模型的复杂问题。数据是现成的来自Kaggle包含了2022年1月至2023年1月每天的Wordle谜底、解题分布和相关的推文。难点在于如何从这些数据里挖出有效的特征来刻画“单词难度”和“社交热度”这两个核心预测目标。最终我们采用了一套结合了传统统计特征与机器学习模型的方案效果不错。今天我就把这套从数据清洗、特征工程到模型构建与评估的完整思路和Python代码实现毫无保留地分享出来。无论你是正在备战美赛、国赛的同学还是对数据科学、预测模型感兴趣的朋友这篇文章都能给你提供一个从零到一的实战参考。2. 核心问题拆解与整体建模思路面对这样一个预测问题第一步不是急着写代码而是把题目要求翻译成清晰的、可量化的数据科学任务。这能避免后期走弯路。2.1 预测目标究竟是什么题目提供了两个主要的预测目标目标一每日解题尝试次数的分布。即预测未来某一天所有玩家中在1次、2次、3次、4次、5次、6次尝试内猜中以及失败X的玩家各占百分之多少。这是一个多输出回归问题预测7个百分比其和为100%也可以看作是对7个类别概率的估计。目标二每日相关推文的数量。即预测未来某一天推特上发布与当天Wordle游戏相关推文的总数。这是一个典型的单变量时间序列回归问题。这两个目标相互关联。一个很难的单词比如“ZESTY”可能导致更多人需要更多尝试次数同时也可能激发更多的吐槽或讨论推文数激增。反之一个简单的单词比如“APPLE”可能大家很快猜出讨论热度反而一般。我们的模型需要捕捉这种关联。2.2 数据驱动的特征工程思路我们拥有的核心数据是每天的“单词”Word of the day。所有预测特征都必须从这个单词本身以及它与历史数据的交互中衍生出来。我们的特征工程围绕两个维度展开维度一单词语言学与游戏性特征这是预测“解题分布”的核心。一个单词的难度直观上与它的字母构成、常见度、与历史单词的相似度等有关。字母频率特征单词中元音字母A, E, I, O, U的数量、比例。元音通常提供更多关键信息。字母重复特征单词中是否有重复字母重复字母会减少信息量可能增加难度。词频特征该单词在大型语料库如英文维基百科、新闻语料中的词频或逆文档频率IDF。生僻词显然更难。词性特征该单词是名词、动词还是形容词这可以通过NLP工具如NLTK, spaCy获取。Wordle历史特征该单词的字母与过去N天比如30天的答案单词的字母重叠度。如果很多字母最近出现过玩家可能更有“感觉”。首字母特征单词的首字母。某些首字母开头的单词可能更常见或更罕见。维度二时间序列与社交热度特征这是预测“推文数”并辅助预测“解题分布”的关键。它关注趋势、周期性和社交反馈。滞后特征Lag Features这是时间序列预测的基石。包括前1天、前7天周周期、前30天月趋势的“解题分布”和“推文数”。例如tweets_lag1,tweets_lag7,hard_mode_percentage_lag1等。移动统计特征过去N天的移动平均值、移动标准差。例如过去7天平均推文数可以反映近期的热度基线。星期几特征将星期几进行独热编码One-hot Encoding。周末的玩家行为和社交活跃度可能与工作日不同。节假日特征标记当天是否为节假日如圣诞节、新年。节假日可能显著影响玩家参与度和社交讨论。历史表现特征如果这个单词在过去作为非答案出现时被玩家在猜测中使用的频率这需要更复杂的数据但可以从推文文本中近似挖掘例如分析历史推文中猜测该词的频率。实操心得特征并非越多越好。初期我们构建了超过50个特征但很多是高度共线性的比如各种移动平均之间。后来我们使用了相关性矩阵和基于树模型的特征重要性排序如XGBoost的feature_importances_进行筛选最终保留了约20个核心特征模型效果和训练速度都得到了提升。2.3 模型选型与架构设计我们采用了集成模型Ensemble Model的思路针对两个预测目标分别构建管道Pipeline。对于目标一解题分布预测我们将其视为一个多输出回归问题。7个输出1-6次和X的百分比是相关的总和为100%因此适合使用支持多输出的模型。基线模型多元线性回归。虽然简单但可以作为性能基准。核心模型多输出随机森林回归器。sklearn的MultiOutputRegressor包装器可以让我们使用任何回归器来处理多输出问题。随机森林能很好地处理特征间的非线性关系且对特征缩放不敏感。进阶尝试XGBoost回归器。我们同样使用MultiOutputRegressor包装XGBoost。XGBoost在精度和速度上通常有优势但需要仔细调参。后处理模型预测出的7个百分比之和可能不为100%。我们需要进行归一化处理预测值_i / sum(所有预测值) * 100。对于目标二推文数预测这是一个单变量时间序列问题但我们可以利用丰富的特征。基线模型自回归集成移动平均模型。这是经典方法但难以直接融入我们构造的“单词特征”。核心模型梯度提升回归树。我们使用XGBRegressor或LGBMRegressor。这类模型能高效地融合时间序列特征滞后值、移动平均和横截面特征单词特征、星期几效果通常比ARIMA更好。考虑序列依赖性推文数可能存在自相关和残差异方差。我们尝试在特征中加入预测残差的滞后项作为新特征或者使用专门处理时序的树模型但不强制。整体架构数据预处理与特征工程模块。将数据按时间顺序划分为训练集例如2022年1月-2022年11月和测试集2022年12月-2023年1月。绝对禁止使用未来数据训练两个独立的模型管道Model_Distribution和Model_Tweets。在测试集上进行预测并使用适当的指标评估。3. 数据预处理与特征工程实战理论说再多不如一行代码。我们直接进入实战环节。假设你已经从Kaggle下载了数据集通常包含wordle_results.csv和tweets.csv。3.1 数据加载与初步清洗import pandas as pd import numpy as np from datetime import datetime, timedelta import warnings warnings.filterwarnings(ignore) # 1. 加载数据 # 假设结果数据包含列date, word, 1_try, 2_tries, 3_tries, 4_tries, 5_tries, 6_tries, X_tries df_results pd.read_csv(wordle_results.csv) df_results[date] pd.to_datetime(df_results[date]) df_results.set_index(date, inplaceTrue) # 假设推文数据包含列date, tweet_count df_tweets pd.read_csv(tweets.csv) df_tweets[date] pd.to_datetime(df_tweets[date]) df_tweets.set_index(date, inplaceTrue) # 2. 数据合并与对齐 # 确保我们有一个完整的日期索引 all_dates pd.date_range(startdf_results.index.min(), enddf_results.index.max(), freqD) df pd.DataFrame(indexall_dates) df df.join(df_results[[word, 1_try, 2_tries, 3_tries, 4_tries, 5_tries, 6_tries, X_tries]], howleft) df df.join(df_tweets[[tweet_count]], howleft) # 检查缺失值 print(f缺失值情况:\n{df.isnull().sum()}) # 对于word缺失理论上不应该可以向前填充或视为异常日期。对于百分比和推文数可以用前后均值填充。 df[word].fillna(methodffill, inplaceTrue) # 单词向前填充 # 对数值列用简单插值 numeric_cols [1_try, 2_tries, 3_tries, 4_tries, 5_tries, 6_tries, X_tries, tweet_count] df[numeric_cols] df[numeric_cols].interpolate(methodlinear)3.2 核心特征构造这里是特征工程的核心代码块。我们将逐步添加特征。# 3. 构造单词语言学特征 def extract_word_features(word): 从单词字符串中提取基础特征 word word.upper().strip() features {} features[word_length] len(word) features[vowel_count] sum(1 for c in word if c in AEIOU) features[vowel_ratio] features[vowel_count] / features[word_length] features[unique_letters] len(set(word)) features[has_repeated] 1 if features[unique_letters] features[word_length] else 0 features[first_letter] word[0] # 可以添加更多如辅音数量、特定难字母J, Q, X, Z的数量等 return features # 应用函数 word_feats df[word].apply(lambda x: pd.Series(extract_word_features(x))) df pd.concat([df, word_feats], axis1) # 4. 构造词频特征 (这里需要外部词频数据例如从nltk.corpus导入) # 假设我们有一个预加载的字典 word_freq_dict键为大写单词值为频率 # 例如word_freq_dict {APPLE: 0.0012, ZESTY: 0.000023, ...} df[word_freq] df[word].map(lambda x: word_freq_dict.get(x.upper(), 1e-6)) # 给未登录词一个极小值 df[log_word_freq] np.log(df[word_freq] 1e-10) # 取对数使分布更平缓 # 5. 构造时间序列滞后特征 lags [1, 2, 3, 7, 14, 30] # 定义滞后天数 for col in [tweet_count] numeric_cols: # 为推文数和各尝试次数百分比创建滞后 for lag in lags: df[f{col}_lag_{lag}] df[col].shift(lag) # 6. 构造移动统计特征 window_sizes [7, 30] for col in [tweet_count, 3_tries]: # 以推文数和3次尝试百分比为例可扩展 for window in window_sizes: df[f{col}_rolling_mean_{window}] df[col].rolling(windowwindow, min_periods1).mean() df[f{col}_rolling_std_{window}] df[col].rolling(windowwindow, min_periods1).std() # 7. 构造时间特征 df[day_of_week] df.index.dayofweek # 周一0, 周日6 df[month] df.index.month df[is_weekend] (df[day_of_week] 5).astype(int) # 8. 构造单词历史相似度特征 (简化版计算与过去30天答案的字母集合Jaccard相似度) def jaccard_similarity(word1, word2): set1, set2 set(word1), set(word2) return len(set1 set2) / len(set1 | set2) if len(set1 | set2) 0 else 0 # 计算每个单词与过去30天不包括当天所有单词的平均相似度 similarity_list [] for i in range(len(df)): current_word df.iloc[i][word] past_start max(0, i-30) past_words df.iloc[past_start:i][word].tolist() if past_words: avg_sim np.mean([jaccard_similarity(current_word, w) for w in past_words]) else: avg_sim 0 similarity_list.append(avg_sim) df[avg_similarity_past_30d] similarity_list # 9. 对分类特征进行编码 df pd.get_dummies(df, columns[first_letter, day_of_week], drop_firstTrue) # 独热编码避免虚拟变量陷阱 # 10. 处理无穷值和缺失值由移动窗口和滞后产生 df.replace([np.inf, -np.inf], np.nan, inplaceTrue) df.fillna(methodbfill, inplaceTrue) # 用后向填充处理最开始的NaN df.fillna(0, inplaceTrue) # 如果还有NaN填0 print(f特征工程后数据形状: {df.shape}) print(f特征列示例: {df.columns.tolist()[:20]}...) # 查看部分特征注意事项特征工程是迭代过程。上述代码生成了大量特征。在实际操作中你需要根据模型训练后的特征重要性进行筛选移除不重要或高度相关的特征以防止过拟合和提升计算效率。可以使用sklearn的SelectFromModel或计算特征间的相关系数矩阵。4. 模型构建、训练与评估数据准备好了我们开始搭建模型。我们将分别构建解题分布预测模型和推文数预测模型。4.1 解题分布预测模型from sklearn.model_selection import TimeSeriesSplit from sklearn.multioutput import MultiOutputRegressor from sklearn.ensemble import RandomForestRegressor from xgboost import XGBRegressor from sklearn.metrics import mean_absolute_error, mean_squared_error import matplotlib.pyplot as plt # 1. 定义预测目标 target_columns [1_try, 2_tries, 3_tries, 4_tries, 5_tries, 6_tries, X_tries] y_dist df[target_columns] # 2. 定义特征排除目标列和原始单词列 feature_columns [col for col in df.columns if col not in target_columns [word, tweet_count]] X df[feature_columns] # 3. 按时间划分训练集和测试集 (例如前80%训练后20%测试) split_idx int(len(df) * 0.8) X_train, X_test X.iloc[:split_idx], X.iloc[split_idx:] y_train_dist, y_test_dist y_dist.iloc[:split_idx], y_dist.iloc[split_idx:] print(f训练集大小: {X_train.shape}, 测试集大小: {X_test.shape}) # 4. 使用多输出随机森林 print(\n--- 训练多输出随机森林模型 ---) rf_model MultiOutputRegressor(RandomForestRegressor(n_estimators100, random_state42, n_jobs-1)) rf_model.fit(X_train, y_train_dist) y_pred_rf rf_model.predict(X_test) # 5. 使用多输出XGBoost (需要为每个输出单独实例化或使用MultiOutputRegressor) print(\n--- 训练多输出XGBoost模型 ---) xgb_model MultiOutputRegressor(XGBRegressor(n_estimators100, learning_rate0.1, random_state42, n_jobs-1)) xgb_model.fit(X_train, y_train_dist) y_pred_xgb xgb_model.predict(X_test) # 6. 评估指标 def evaluate_predictions(y_true, y_pred, model_name): 评估多输出预测结果 mae_list [] rmse_list [] for i, col in enumerate(target_columns): mae mean_absolute_error(y_true.iloc[:, i], y_pred[:, i]) rmse np.sqrt(mean_squared_error(y_true.iloc[:, i], y_pred[:, i])) mae_list.append(mae) rmse_list.append(rmse) print(f{model_name} - {col}: MAE {mae:.4f}, RMSE {rmse:.4f}) print(f{model_name} - 平均 MAE: {np.mean(mae_list):.4f}, 平均 RMSE: {np.mean(rmse_list):.4f}) return np.mean(mae_list), np.mean(rmse_list) print(\n评估结果) mae_rf, rmse_rf evaluate_predictions(y_test_dist, y_pred_rf, 随机森林) mae_xgb, rmse_xgb evaluate_predictions(y_test_dist, y_pred_xgb, XGBoost) # 7. 后处理归一化预测结果使和为100% y_pred_rf_norm y_pred_rf / y_pred_rf.sum(axis1, keepdimsTrue) * 100 y_pred_xgb_norm y_pred_xgb / y_pred_xgb.sum(axis1, keepdimsTrue) * 100 # 评估归一化后的结果可选 # evaluate_predictions(y_test_dist, y_pred_rf_norm, 随机森林(归一化后)) # 8. 可视化对比以3_tries为例 plt.figure(figsize(12, 6)) plt.plot(y_test_dist.index, y_test_dist[3_tries], label真实值, markero, alpha0.7) plt.plot(y_test_dist.index, y_pred_rf_norm[:, 2], label随机森林预测, linestyle--) plt.plot(y_test_dist.index, y_pred_xgb_norm[:, 2], labelXGBoost预测, linestyle-.) plt.xlabel(日期) plt.ylabel(3次尝试百分比) plt.title(解题分布预测对比 (以3_tries为例)) plt.legend() plt.grid(True, alpha0.3) plt.tight_layout() plt.show()4.2 推文数预测模型# 1. 定义预测目标 y_tweets df[tweet_count] # 2. 划分数据集 (与分布预测使用相同的索引划分) y_train_tweets, y_test_tweets y_tweets.iloc[:split_idx], y_tweets.iloc[split_idx:] # 3. 训练XGBoost回归模型 (单目标) print(\n--- 训练推文数预测XGBoost模型 ---) from xgboost import XGBRegressor xgb_tweet_model XGBRegressor(n_estimators150, learning_rate0.05, max_depth5, random_state42) xgb_tweet_model.fit(X_train, y_train_tweets) y_pred_tweets xgb_tweet_model.predict(X_test) # 4. 评估 tweet_mae mean_absolute_error(y_test_tweets, y_pred_tweets) tweet_rmse np.sqrt(mean_squared_error(y_test_tweets, y_pred_tweets)) print(f推文数预测 - MAE: {tweet_mae:.2f}, RMSE: {tweet_rmse:.2f}) # 5. 特征重要性分析 importances xgb_tweet_model.feature_importances_ feat_imp_df pd.DataFrame({feature: X_train.columns, importance: importances}) feat_imp_df feat_imp_df.sort_values(importance, ascendingFalse).head(15) plt.figure(figsize(10, 6)) plt.barh(feat_imp_df[feature], feat_imp_df[importance]) plt.xlabel(特征重要性 (XGBoost)) plt.title(推文数预测模型 - 前15重要特征) plt.gca().invert_yaxis() plt.tight_layout() plt.show() # 6. 可视化预测结果 plt.figure(figsize(14, 7)) plt.plot(y_test_tweets.index, y_test_tweets.values, label真实推文数, linewidth2) plt.plot(y_test_tweets.index, y_pred_tweets, labelXGBoost预测, linestyle--, linewidth2) plt.fill_between(y_test_tweets.index, y_pred_tweets - tweet_rmse, y_pred_tweets tweet_rmse, alpha0.2, label±RMSE区间) plt.xlabel(日期) plt.ylabel(推文数量) plt.title(每日Wordle相关推文数预测) plt.legend() plt.grid(True, alpha0.3) plt.tight_layout() plt.show()4.3 模型集成与最终预测在实际提交的论文中我们通常不会只用一个模型。为了稳健性我们可以对多个模型的预测结果进行集成。# 简单加权平均集成 (以分布预测为例) # 假设我们有三个模型的预测结果: y_pred_rf, y_pred_xgb, 和一个线性回归模型 y_pred_lr # from sklearn.linear_model import LinearRegression # lr_model MultiOutputRegressor(LinearRegression()) # lr_model.fit(X_train, y_train_dist) # y_pred_lr lr_model.predict(X_test) # 赋予不同权重例如基于验证集表现 weights {rf: 0.4, xgb: 0.6} # 假设XGBoost表现稍好 y_pred_ensemble weights[rf] * y_pred_rf_norm weights[xgb] * y_pred_xgb_norm # 再次归一化 y_pred_ensemble y_pred_ensemble / y_pred_ensemble.sum(axis1, keepdimsTrue) * 100 print(集成模型评估:) evaluate_predictions(y_test_dist, y_pred_ensemble, 加权平均集成) # 对于推文数预测也可以做类似集成实操心得时间序列交叉验证。在调参和选择模型时使用标准的sklearn.model_selection.KFold会破坏时间顺序导致数据泄露用未来信息预测过去。务必使用TimeSeriesSplit。这能更真实地反映模型在时序数据上的泛化能力。from sklearn.model_selection import TimeSeriesSplit, GridSearchCV tscv TimeSeriesSplit(n_splits5) param_grid { n_estimators: [50, 100, 200], max_depth: [3, 5, 7], learning_rate: [0.01, 0.05, 0.1] } # 以推文数预测模型为例 xgb XGBRegressor(random_state42) grid_search GridSearchCV(estimatorxgb, param_gridparam_grid, cvtscv, scoringneg_mean_squared_error, n_jobs-1, verbose1) grid_search.fit(X_train, y_train_tweets) print(f最佳参数: {grid_search.best_params_}) print(f最佳CV分数: {-grid_search.best_score_:.2f})5. 常见问题、挑战与优化策略实录在实际操作中我们遇到了不少坑。这里把关键问题和解决方案整理出来希望能帮你省点时间。5.1 数据质量问题与处理问题1推文数据存在异常峰值或缺失。某些日期的推文数异常高可能因为名人效应或社会事件或为0数据抓取问题。解决对于异常高值可以使用移动中位数配合阈值如3倍标准差进行盖帽Capping处理。对于缺失值时间序列插值interpolate比简单用均值填充更合理。问题2单词特征中的“词频”数据难以获取或质量不高。解决我们使用了NLTK库中的wordnet和brown语料库作为替代虽然覆盖面有限但足以区分常见词和生僻词。更专业的做法是使用大型预训练语言模型如BERT获取单词的上下文嵌入向量作为特征但这需要更多计算资源。问题3特征间存在多重共线性。例如各种移动平均特征之间高度相关。解决计算特征间的相关系数矩阵手动移除相关系数大于0.9的其中一个特征。或者使用Variance Inflation Factor (VIF)进行检验。树模型对共线性不敏感但线性模型会受影响。5.2 模型预测的独特挑战挑战1解题分布预测的“总和约束”。模型独立预测7个百分比其和可能远非100%。解决如前所述后处理归一化是必须的。更高级的方法是使用狄利克雷回归Dirichlet Regression它直接对构成概率向量的多元响应进行建模天然保证分量和为1。可以使用scikit-learn扩展库或statsmodels尝试。挑战2推文数的“尖峰”难以预测。模型容易预测出平滑的趋势但难以捕捉突然的爆发。解决除了加入“节假日”特征可以尝试构造“社交动量”特征例如前一日推文数的变化率一阶差分或者识别出历史中的“爆点”日期作为二值特征加入。也可以考虑使用更擅长捕捉突变的模型如Prophet由Facebook开发或LSTM长短期记忆网络。挑战3单词的“语义难度”难以量化。“ZESTY”比“APPLE”难不仅因为字母频率还因为语义常见度。解决我们尝试使用单词嵌入向量如GloVe或Word2Vec的余弦相似度计算该单词与一个“简单单词集合”如最常用的1000个单词的平均距离作为“语义生僻度”特征。这有一定效果。5.3 性能优化与结果提升技巧技巧1特征交叉。不要只使用原始特征尝试创造一些交互特征。例如“单词词频”与“是否周末”的乘积可能捕捉到周末玩家对生僻词的容忍度变化。技巧2目标变量转换。对于推文数这种右偏分布可能存在少数极大值的数据尝试对其取对数np.log1p再进行预测预测后再指数转换回来。这能使模型更关注相对误差而非绝对误差。技巧3残差学习。先用一个简单模型如线性回归预测然后训练第二个模型如XGBoost来预测第一个模型的残差。两个模型的预测相加作为最终结果。这有时能提升精度。技巧4利用外部数据。题目没有限制我们可以引入外部数据。例如当天的天气数据是否下雨/下雪影响室内活动、重大新闻事件是否分散了社交媒体注意力甚至从推文文本中提取的情感得分正面/负面作为额外特征。这在美赛中是重要的加分项体现了数据思维的开放性。5.4 论文写作与可视化呈现模型做得好还要讲得好。在论文中清晰展示特征重要性图如上文代码所示这能直观证明你的特征工程是有效的。绘制预测值与真实值的对比时序图并标注出预测误差较大的点分析可能的原因例如当天单词异常难或发生了外部事件。对解题分布使用堆叠面积图来展示7个类别预测值随时间的变化非常直观。进行敏感性分析。例如展示如果移除“词频特征”或“滞后特征”模型性能下降多少从而论证这些特征的必要性。讨论模型的局限性。诚实地指出模型在预测极端值、处理全新单词与历史毫无相似性时的不足并提出未来改进方向如引入更深的语义理解模型。最后我想说数学建模比赛的魅力就在于从一团乱麻中理出线索用数据和模型讲一个逻辑自洽的故事。Wordle这道题尤其如此它没有标准答案考验的是你定义问题、创造特征和合理解释结果的能力。我们这套方案只是一个起点里面还有很多可以深挖和优化的地方比如引入深度学习模型、进行更复杂的时间序列分解等。希望这份详细的思路和代码能为你打开一扇门更重要的是能让你体验到从数据到洞察的完整过程。编程和调参只是工具真正的核心是你对问题的思考。祝你在接下来的比赛中或者在自己的数据科学项目里玩得开心有所收获。
返回列表