Wordle玩家表现预测:时间序列分析与概率分布建模实战

📅 2026/8/27 7:22:06
Wordle玩家表现预测:时间序列分析与概率分布建模实战
1. 项目概述从一道赛题到一套预测方法论去年美赛C题一出来我身边不少搞建模的朋友都乐了题目叫“Predicting Results for Wordle”。Wordle这玩意儿说白了就是个每天一次的猜词小游戏规则简单到不行六次机会猜一个五个字母的单词每次猜测后系统会用绿、黄、灰三种颜色告诉你字母的位置正确与否。但美赛组委会愣是把它包装成了一个充满不确定性的时间序列预测问题。题目给了一组从2023年1月到2023年3月的真实玩家数据包括每天猜出答案所需的尝试次数分布1次到7次以及失败要求我们建立一个模型来预测未来某一天比如2023年3月31日玩家们完成Wordle的尝试次数分布。这题妙就妙在它看起来是个游戏问题内核却是一个典型的时间序列分析与概率预测问题。你不能把它当成简单的游戏攻略来写核心在于如何从历史数据中捕捉规律并量化未来结果的不确定性。我当时的思路很明确放弃对“明天具体是哪个单词”这种不可预测因素的纠结转而聚焦于“玩家群体的整体表现”这一宏观统计量。这就像天气预报我们无法精准预测每一片云朵的轨迹但可以相当可靠地预测明天全城的平均降雨概率。本文将详细拆解我应对这道赛题的完整建模思路、核心算法实现以及那些在论文里不会写的实操陷阱和调参心得。无论你是正在备战数模的新手还是对时间序列预测感兴趣的数据爱好者相信这套从数据清洗、特征工程、模型构建到结果评估的完整流程都能给你带来直接的启发。2. 核心思路拆解为什么是“分布预测”而非“单词预测”面对这道题第一个需要破除的思维定势就是我们不是在预测明天Wordle的答案是什么。很多队伍一开始就钻进了牛角尖试图去分析单词的字母频率、词库结构甚至想用NLP模型来猜词这完全是跑偏了。题目给出的数据是玩家尝试次数的分布即每天有多少比例的人用了1次猜中、2次猜中……直到失败。我们的目标是预测未来某一天的这个分布向量。2.1 问题本质多变量时间序列的概率预测让我们把数据格式理清楚。假设我们有过去N天的数据那么对于每一天t我们观察到一个7维的向量对应尝试1-6次和失败共7类P_t [p1_t, p2_t, p3_t, p4_t, p5_t, p6_t, p_fail_t]其中p1_t代表在第t天一次猜中的玩家比例且所有维度之和为1。因此我们的数据集实际上是一个N x 7的矩阵。预测未来一天tN1的P_{N1}就是一个多变量时间序列预测问题。每一个维度如“一次猜中的比例”自身是一个单变量时间序列同时这7个维度之间存在着强烈的约束关系和为1和竞争关系比如一个很难的单词可能导致p1下降p6和p_fail上升。2.2 核心挑战与应对策略这个预测任务有几个鲜明的挑战短期波动与长期趋势玩家整体水平在缓慢提升学习效应但每日单词的难度波动巨大是主要噪声来源。成分数据约束预测出的7个概率值必须在0到1之间且和为1。普通回归模型预测出的值很容易违反这个约束。不确定性量化题目要求预测的是一个分布我们模型给出的也应该是一个分布预测最好能给出置信区间而不仅仅是一个点估计。我的核心策略是分层处理第一步趋势分解。先将每个比例序列拆解出长期的趋势成分反映玩家技能增长和季节/周期成分如果有的话比如周末效应。第二步残差建模。剔除趋势和周期后剩下的残差序列主要反映了“每日单词难度”的随机波动。对这一部分进行概率建模。第三步约束处理与集成。将趋势、周期预测与残差的概率分布结合通过归一化等后处理手段满足和为1的约束最终输出一个完整的概率分布预测。基于这个策略我选择了两种经典但有效的模型进行融合**灰色预测模型GM(1,1)**用于捕捉和预测隐含的指数增长趋势**二次指数平滑法Holt‘s Linear Trend Method**用于拟合具有线性趋势的时间序列并对未来进行点预测及区间估计。两者结合既能利用灰色系统对小样本、趋势性数据拟合好的优点又能借助指数平滑得到直观的预测区间。3. 数据预处理与特征工程清洗出模型的“好粮食”题目提供的数据通常是粗糙的。直接对原始比例序列建模效果会很差必须进行精细的预处理。3.1 数据清洗与格式转换假设我们拿到了一个包含日期和尝试次数计数的数据集。第一步是计算每日的比例。import pandas as pd import numpy as np # 假设原始数据格式Date, Try1_Count, Try2_Count, ..., Try6_Count, Fail_Count df pd.read_csv(wordle_data.csv) df[Date] pd.to_datetime(df[Date]) # 计算总玩家数每天 attempt_columns [Try1_Count, Try2_Count, Try3_Count, Try4_Count, Try5_Count, Try6_Count, Fail_Count] df[Total_Players] df[attempt_columns].sum(axis1) # 计算比例 for col in attempt_columns: df[col.replace(_Count, _Prop)] df[col] / df[Total_Players] # 得到比例数据框 prop_df df[[Date] [col.replace(_Count, _Prop) for col in attempt_columns]] prop_df.set_index(Date, inplaceTrue)3.2 探索性分析与关键发现对prop_df进行初步的可视化和分析有几个关键发现直接影响模型设计“3次猜中”是众数几乎每一天Try3_Prop和Try4_Prop都是最高的这符合人类猜词的认知规律。这意味着我们的模型需要特别关注中间类别3、4次的预测准确性。“1次猜中”极不稳定Try1_Prop序列波动剧烈某些天异常高可能是简单单词或运气某些天接近0。这提示我们对于极端类别可能需要更稳健的建模方法或考虑对其进行变换如logit变换。“失败率”与单词难度强相关Fail_Prop在个别日期显著飙升这些日期对应的往往是公认的“难词日”。这是一个重要的信号可以尝试构建一个“每日难度”的代理特征。周末效应Weak Seasonality通过绘制序列图发现周六周日的玩家分布可能与工作日有细微差别例如尝试次数的分布更分散。这暗示可能存在以7天为周期的季节性。3.3 特征构建从比例到可建模的序列直接对比例p建模会遇到[0,1]区间的限制。一个标准的做法是使用logit变换将其映射到整个实数域。y log(p / (1-p))但这里p是多元的且和为1直接对每个p做logit变换会破坏约束。更实用的方法是对于中间比例如p3, p4由于其值相对稳定且远离0和1可以暂时直接使用或做轻微平滑。对于极端比例如p1, p_fail考虑使用移动平均或指数加权平均进行平滑以过滤掉极端噪声得到一个更能反映潜在趋势的序列。构建一个“综合难度指数”。例如可以用(2*p_fail 1*p6 0.5*p5) / (p1 0.5*p2)来构造一个指标值越高表示单词越难。将这个指数作为一个额外的特征可以帮助模型理解不同比例序列间的协同变化。# 示例平滑极端序列与构建难度指数 prop_df[Try1_Prop_Smooth] prop_df[Try1_Prop].rolling(window5, centerTrue, min_periods1).mean() prop_df[Fail_Prop_Smooth] prop_df[Fail_Prop].rolling(window5, centerTrue, min_periods1).mean() # 构建一个简单的难度指数可调整权重 prop_df[Difficulty_Index] (2*prop_df[Fail_Prop] 1*prop_df[Try6_Prop] 0.5*prop_df[Try5_Prop]) / (prop_df[Try1_Prop] 0.5*prop_df[Try2_Prop] 0.01) # 加0.01防止除零 prop_df[Difficulty_Index] prop_df[Difficulty_Index].replace([np.inf, -np.inf], np.nan).fillna(methodffill)注意平滑窗口的选择需要谨慎。窗口太小去噪效果不佳窗口太大会过度平滑抹杀真实波动。通常可以尝试3、5、7等奇数窗口并通过观察平滑后序列与原始序列的对比图来决定。对于比赛可以保留多个不同平滑程度的序列作为特征让模型选择。4. 模型一灰色预测模型GM(1,1)的实战应用灰色预测特别适用于数据量少、趋势明显且具有指数规律的情况。Wordle玩家整体水平的提升近似一个缓慢的指数学习曲线适合用GM(1,1)来捕捉其趋势成分。4.1 模型原理与实现步骤GM(1,1)的核心思想是通过累加生成AGO将原始随机性较强的序列转化为规律性更强的序列然后用一阶微分方程灰色微分方程进行拟合最后再通过累减生成IAGO还原得到预测值。步骤1数据准备。选取一个比例序列例如Try3_Prop_Smooth平滑后的3次猜中比例。设原始序列为X^(0) (x^(0)(1), x^(0)(2), ..., x^(0)(n))步骤2一次累加生成1-AGO。生成新序列X^(1)其中x^(1)(k) sum_{i1}^{k} x^(0)(i)。步骤3建立GM(1,1)模型。灰色微分方程的基本形式为dx^(1)/dt a * x^(1) b。其中a为发展系数b为灰色作用量。通过最小二乘法可以估计参数a和b。步骤4求解时间响应式。上述微分方程的解即累加序列的预测函数为\hat{x}^(1)(k1) (x^(0)(1) - b/a) * e^{-a*k} b/a步骤5累减还原。将累加预测值还原为原始序列的预测值\hat{x}^(0)(k1) \hat{x}^(1)(k1) - \hat{x}^(1)(k)下面是Python实现的核心代码import numpy as np class GM11: def __init__(self): self.a None self.b None self.x0 None def fit(self, data): 拟合GM(1,1)模型 data: 一维数组原始时间序列 self.x0 np.array(data) n len(self.x0) # 1-AGO x1 np.cumsum(self.x0) # 构造数据矩阵B和Y B np.zeros((n-1, 2)) Y np.zeros((n-1, 1)) for i in range(n-1): B[i, 0] -0.5 * (x1[i] x1[i1]) B[i, 1] 1 Y[i, 0] self.x0[i1] # 最小二乘法估计参数 BTB_inv np.linalg.inv(np.dot(B.T, B)) self.a, self.b np.dot(np.dot(BTB_inv, B.T), Y).flatten() def predict(self, steps): 预测未来steps步 steps: 预测步数 n len(self.x0) x1_hat np.zeros(n steps) x0_hat np.zeros(n steps) # 累加序列的拟合值 for k in range(n steps): x1_hat[k] (self.x0[0] - self.b/self.a) * np.exp(-self.a * k) self.b/self.a # 累减还原得到原始序列预测值 x0_hat[0] self.x0[0] for k in range(1, n steps): x0_hat[k] x1_hat[k] - x1_hat[k-1] # 返回所有值包括历史拟合和未来预测 return x0_hat # 使用示例预测Try3比例的趋势 model_gm GM11() # 使用平滑后的序列进行拟合以获取趋势 try3_trend_data prop_df[Try3_Prop_Smooth].dropna().values[-30:] # 取最近30天数据 model_gm.fit(try3_trend_data) future_trend_gm model_gm.predict(steps7) # 预测未来7天包括历史拟合 predicted_try3_trend future_trend_gm[-7:] # 取出未来7天的趋势预测值4.2 模型评估与适用性分析GM(1,1)的预测结果是一条光滑的指数曲线。将其与原始序列对比我们可以得到序列的“趋势成分”。残差 原始值 - 趋势值。这个残差序列就包含了每日单词难度波动、随机噪声等信息。注意事项数据量要求GM(1,1)对数据量要求不高但通常需要至少4个以上数据点。对于Wordle这种每日数据取最近30-60天数据建模是合理的。指数假设它隐含了序列服从近似指数增长的假设。如果序列是线性或更复杂的趋势GM(1,1)的长期预测偏差会较大。因此它更适合用于短期趋势外推。发展系数a的意义a的大小和符号反映了趋势。a0表示序列递减a0表示序列递增。在Wordle场景中对于Try1_Prop玩家一次猜中率由于玩家水平提升可能呈现微弱的递增趋势a为负且绝对值很小对于Fail_Prop可能呈现递减趋势a为正且很小。边界问题预测出的比例值可能超出[0,1]范围这需要在后续的集成与后处理阶段进行修正如截断。5. 模型二二次指数平滑法Holt‘s Linear Trend的深度解析二次指数平滑Holt‘s方法是处理具有线性趋势时间序列的经典方法。它比简单指数平滑多了一个趋势分量并且可以方便地计算预测区间非常适合用来对Wordle数据中那些具有明显线性变化倾向的比例序列如玩家失败率的缓慢下降进行建模和不确定性量化。5.1 模型原理与参数解读Holt‘s方法包含三个核心方程水平方程l_t α * y_t (1 - α) * (l_{t-1} b_{t-1})l_t时刻t的估计水平。α水平平滑参数0α1控制当前观测值y_t的权重。趋势方程b_t β * (l_t - l_{t-1}) (1 - β) * b_{t-1}b_t时刻t的估计趋势斜率。β趋势平滑参数0β1控制趋势更新的权重。预测方程ŷ_{th|t} l_t h * b_tŷ_{th|t}基于时刻t的信息对th时刻的预测值。h预测步长。此外为了计算预测区间我们还需要估计预测误差的标准差。一个简单的方法是使用历史预测误差的均方根RMSE。5.2 Python实现与预测区间计算我们可以使用statsmodels库来实现但为了更透彻地理解参数影响这里也给出一个简化版的自定义实现。from statsmodels.tsa.holtwinters import ExponentialSmoothing import numpy as np def holt_predict_with_interval(data, steps, alpha0.3, beta0.1, confidence0.95): 使用Holt‘s线性趋势模型进行预测并计算预测区间。 data: 一维时间序列数据 steps: 预测步数 alpha, beta: 平滑参数 confidence: 置信水平 model ExponentialSmoothing(data, trendadd, seasonalNone, initialization_methodestimated) fit_model model.fit(smoothing_levelalpha, smoothing_trendbeta, optimizedFalse) forecast fit_model.forecast(stepssteps) # 计算历史拟合值的残差用于估计预测误差 fitted_values fit_model.fittedvalues residuals data - fitted_values # 假设残差独立同分布用其标准差作为预测误差的估计 resid_std np.std(residuals, ddof1) # 样本标准差 # 计算预测区间 # Holt‘s模型在h步预测的预测误差方差近似为Var ≈ h * σ^2这里做简化处理 # 更精确的方法需要考虑模型参数的不确定性但比赛环境下简化处理是可接受的。 from scipy import stats z_score stats.norm.ppf((1 confidence) / 2) # 对于正态假设 forecast_se resid_std * np.sqrt(np.arange(1, steps1)) # 标准误随步长增大 lower forecast - z_score * forecast_se upper forecast z_score * forecast_se return forecast, lower, upper, fit_model # 示例对失败率序列进行预测 fail_data prop_df[Fail_Prop_Smooth].dropna().values forecast_fail, lower_fail, upper_fail, model_fail holt_predict_with_interval(fail_data, steps7, alpha0.2, beta0.05) print(f未来7天失败率点预测: {forecast_fail}) print(f95%置信区间下限: {lower_fail}) print(f95%置信区间上限: {upper_fail})5.3 参数调优与实战心得alpha和beta的选择至关重要它们控制了模型对近期变化和趋势变化的反应速度。alpha水平平滑值越大模型对序列的最新水平越敏感。对于波动较大的序列如Try1_Propalpha应设得小一些如0.1-0.3以平滑噪声对于变化平缓的序列如长期趋势可以设得大一些。beta趋势平滑值越大模型对趋势方向的变化越敏感。通常beta设置得比alpha更小如0.05-0.2因为趋势的变化通常比水平值的变化更缓慢、更平滑。实操技巧网格搜索可以定义一个参数网格以历史数据的均方误差MSE或平均绝对百分比误差MAPE为指标寻找最优的(alpha, beta)组合。可视化诊断将拟合值与原始序列绘制在一起。如果拟合线严重滞后于实际转折点可能需要增大alpha或beta如果拟合线过于“抖动”跟随噪声波动则需要减小alpha或beta。初始值设定statsmodels的initialization_methodestimated通常效果不错。如果自己实现简单的设定是l_1 y_1,b_1 y_2 - y_1。预测区间的理解上述方法计算的区间是基于残差独立同分布且服从正态分布的假设。对于Wordle数据这个假设可能不完美残差可能存在异方差或自相关但作为比赛模型提供一个量化的不确定性评估已经足够好且易于解释。更复杂的方法可以考虑使用模拟如Bootstrap。6. 模型融合与后处理从独立预测到合规分布现在我们有了两个模型对各个比例序列的趋势预测GM(1,1)和点估计及区间预测Holt‘s。直接使用任何一个模型的原始输出作为最终预测都可能有问题GM(1,1)的预测可能超出范围Holt‘s的点预测可能不满足和为1。我们需要一个融合与后处理流程。6.1 趋势提取与残差分析首先利用GM(1,1)获取每个比例序列的“基准趋势”。以Try3_Prop为例# 假设已用GM(1,1)拟合得到历史趋势序列 trend_gm_history (长度与原始数据相同) trend_gm_history model_gm.predict(steps0)[:len(try3_data)] # 获取历史拟合部分 residuals try3_data - trend_gm_history # 原始数据 - GM趋势 残差包含难度波动和噪声这个residuals序列是我们需要重点研究的对象。我们可以分析它的统计特性均值、方差、自相关性甚至可以用一个简单的AR模型或移动平均模型来预测未来一天的残差。但在比赛时间有限的情况下一个有效的简化策略是假设未来一天的残差服从一个以0为中心的正态分布其标准差由历史残差序列估计。即residual_future ~ N(0, sigma)。6.2 加权融合预测对于第i个比例序列如Try3我们有两种预测pred_gm_i: GM(1,1)预测的未来值趋势外推。pred_holt_i, lower_i, upper_i: Holt‘s方法预测的未来值及区间。一个简单的融合方式是加权平均pred_fused_i w * pred_gm_i (1-w) * pred_holt_i权重w可以根据两个模型在历史数据上的表现如MSE来分配。例如如果GM(1,1)对趋势捕捉更好Holt‘s对近期波动反应更灵敏可以给GM(1,1)较高的权重如0.7来把握主趋势给Holt‘s较低的权重0.3来吸收近期变化。6.3 满足约束的后处理归一化与截断融合后的7个预测值[pred_fused_1, pred_fused_2, ..., pred_fused_fail]很可能和不等于1也可能有负值或大于1的值。必须进行后处理。步骤1截断。将所有预测值截断到合理区间例如[0.0001, 0.9999]。使用一个很小的正数代替0避免后续计算问题。pred_clipped_i np.clip(pred_fused_i, a_min1e-4, a_max1-1e-4)步骤2归一化。将截断后的预测值归一化使其和为1。pred_final_i pred_clipped_i / np.sum(pred_clipped_i)经过这两步我们就得到了一个合法的概率分布预测。6.4 生成分布预测与置信区间为了回答题目中“预测分布”的要求我们不仅要给出点估计pred_final_i最好还能给出一个可能的范围。这里可以利用Holt‘s方法已经计算出的预测区间。生成场景对于每个比例序列i我们从其预测分布中抽样。例如假设pred_holt_i是均值(lower_i, upper_i)是95%区间我们可以近似认为该序列的未来值服从N(pred_holt_i, sigma_i)其中sigma_i (upper_i - lower_i) / (2 * 1.96)。联合抽样同时从7个序列的预测正态分布中各抽取一个值得到一个7维样本向量。后处理对这个样本向量进行上述的截断和归一化操作。重复重复步骤2-3很多次如10000次我们就得到了10000个可能的未来分布向量。汇总对于每个尝试次数类别我们可以计算这10000个样本的中位数作为最终的点预测计算5%和95%分位数作为80%或90%的预测区间。这种方法生成的预测结果既包含了中心趋势点预测也包含了不确定性预测区间在数模论文中会显得非常丰满和专业。def generate_distribution_forecast(holt_forecasts, holt_lowers, holt_uppers, gm_forecasts, weight_gm0.7, n_samples10000): 生成未来一天的尝试次数分布预测点估计和区间估计。 holt_forecasts: 列表Holt‘s模型对7个类别的点预测 holt_lowers: 列表Holt‘s模型对7个类别的区间下限 holt_uppers: 列表Holt‘s模型对7个类别的区间上限 gm_forecasts: 列表GM(1,1)模型对7个类别的点预测 weight_gm: GM模型在融合中的权重 n_samples: 蒙特卡洛模拟次数 num_categories 7 samples [] # 计算每个类别的预测分布参数简化假设为正态 means [] stds [] for i in range(num_categories): # 融合点预测 fused_mean weight_gm * gm_forecasts[i] (1-weight_gm) * holt_forecasts[i] # 用Holt‘s的区间宽度近似标准差假设正态 sigma_i (holt_uppers[i] - holt_lowers[i]) / (2 * 1.96) if (holt_uppers[i] - holt_lowers[i]) 0 else 0.01 # 也可以考虑融合GM的预测不确定性这里简化处理 means.append(fused_mean) stds.append(sigma_i) # 蒙特卡洛模拟 for _ in range(n_samples): sample [] for i in range(num_categories): # 从预测分布中抽样 raw_sample np.random.normal(locmeans[i], scalestds[i]) sample.append(raw_sample) sample np.array(sample) # 截断和归一化 sample np.clip(sample, 1e-4, 1-1e-4) sample sample / sample.sum() samples.append(sample) samples np.array(samples) # shape: (n_samples, 7) # 计算点预测中位数和预测区间5% 95%分位数 point_forecast np.median(samples, axis0) lower_bound np.percentile(samples, 5, axis0) upper_bound np.percentile(samples, 95, axis0) return point_forecast, lower_bound, upper_bound, samples7. 模型评估、对比与赛题策略延伸在真正的比赛中我们不仅要把模型建出来还要证明它好并且知道怎么用它来回答问题。7.1 回溯测试与评估指标使用历史数据的一部分作为训练集另一部分作为测试集进行回溯测试。假设我们用1月1日到2月28日的数据训练预测3月1日到3月7日的分布并与实际值比较。评估指标平均绝对误差MAE对于每个类别计算预测比例与实际比例之差的绝对值然后对所有类别和所有预测日取平均。MAE mean(|pred_i - actual_i|)。这个指标直观易懂。KL散度Kullback-Leibler Divergence衡量预测分布P_pred与实际分布P_actual之间的差异。KL(P_actual || P_pred) sum(P_actual * log(P_actual / P_pred))。KL散度越小说明预测分布越接近真实分布。这是一个非常适用于概率分布预测的指标。Ranked Probability ScoreRPS对于有序分类变量尝试次数1到7及失败是有序的RPS是更专业的评分规则。它惩罚“预测概率分布偏离真实结果”的程度考虑了整个分布的形状。在论文中应展示多个指标的结果并解释为什么选择这些指标。例如“我们采用MAE和KL散度作为主要评估指标。MAE从绝对误差角度衡量预测精度而KL散度从信息论角度衡量整个预测分布与真实分布的相似性。”7.2 模型对比与敏感性分析对比基准模型建立一个简单的基准模型例如“历史平均模型”用过去7天的平均值预测明天或“朴素模型”用昨天的值预测明天。我们的融合模型应该在指标上显著优于这些基准模型。敏感性分析展示关键参数如GM(1,1)的建模数据长度、Holt‘s的alpha和beta、融合权重w变化时模型性能如KL散度如何变化。这能体现模型的稳健性也是论文的加分项。可以用一张热力图来展示不同参数组合下的误差。7.3 针对赛题的策略延伸与论文写作要点结果可视化一定要用图表说话。至少应包括历史序列与模型拟合图展示GM和Holt‘s的拟合效果。未来预测的点估计与区间估计图用带状图表示置信区间。预测分布与实际分布的对比条形图对于预测日。敏感性分析热力图。讨论模型局限性主动指出模型的不足会显得思考全面。例如模型假设残差独立同分布未考虑单词难度可能存在的自相关比如连续出现难词。模型未引入外部特征如单词的字母频率、元音辅音比例等这些可能提升预测精度。对于“1次猜中”这种极端事件模型预测能力有限。提出改进方向在结论部分可以简要提出如果时间允许可以尝试的进阶方法例如使用Prophet等更高级的时间序列模型自动处理季节性和节假日效应。将7个比例序列视为成分数据使用Dirichlet回归或对数比变换进行建模。引入单词特征如词频、字母熵作为协变量构建回归-时间序列混合模型。代码与可复现性在附录中提供清晰、注释完整的核心代码如本文展示的代码片段。确保评委能理解你的实现逻辑。最后记住数学建模竞赛的核心是解决一个实际问题并用清晰的逻辑和证据展示你的解决方案。本文详述的“灰色预测指数平滑融合后处理”框架提供了一个坚实、可解释、且易于实现的基线方案。你可以在此基础上根据自己对问题的更深理解进行迭代和创新。祝你在接下来的比赛中能从这道有趣的Wordle预测题中挖掘出更多有价值的洞见取得好成绩。