简介:面向气候预测与时间序列分析学习者,这套Python实战资源包以趋势与季节性为主线,聚焦含明显周期波动和长期走向数据的建模预测问题。压缩包共9个文件,核心为3个Jupyter Notebook,配套1个CSV日最低气温数据集,并含若干XML配置与工程文件,总大小仅2.95MB,无需额外大体积环境即可直接运行。内容从Pandas滚动平均提取趋势开始,逐步过渡到STL季节性分解、ARIMA/SARIMA模型搭建、网格搜索自动调参,以及基于真实温度数据的训练集切分、MSE/RMSE/MAE误差评估和预测结果可视化,形成完整闭环;同时提供合成数据集版本,便于对照理解模型行为。已有528人学习下载,非常适合希望从零快速掌握气候预测中时间序列建模流程,并能动手验证各步骤原理的数据分析师与气象爱好者。
1. 基于趋势和季节性的时间序列预测:先拆解再建模才是真正的实战
时间序列预测最常翻车的点,不是模型不够强,而是很多人直接把原始序列丢给 LSTM 或 ARIMA,忽略了一个关键前提:序列里的趋势和季节性,决定了你要用哪种建模方式、要设什么参数、预测结果能达到什么精度。趋势是长期缓慢的上升或下降,季节性则是固定周期内反复出现的波动。把这两者先拆开、单独建模、再合回预测值,通常比一股脑端给黑箱模型更稳,也更容易解释。
这套思路适合电商销量、服务器流量、工厂能耗这类既有日/周/月周期,又有长期增长或衰减的业务数据。你不需要跑一次实验就换一种模型,而是先做好分解这一步,后续选器、调参、上线监控才有章可循。实战包里那些“跑不出来”“预测永远是一条直线”的问题,多半就出在把趋势和季节性搅在一起处理。
2. 趋势与季节性拆解的三层做法:从统计分解到业务特征
2.1 STL 分解:先看数据里到底有什么成分
拿到序列后,我的习惯是先用 STL 做一次分解,把原始序列拆成趋势项、季节项和残差项。这一步不是为了直接拿残差去做预测,而是为了摸清数据的脾气,看趋势是单调递增还是有拐点,季节性是否稳定,残差里是否还藏着周内效应或节假日脉冲。
常见做法是用 statsmodels 里的 STL 方法,核心参数有两个:period 和 seasonal。seasonal 决定季节项的光滑程度,设置太大会把趋势吃掉;period 必须符合业务周期,不能拍脑袋写 7。比如日粒度电商数据,周期是 7 天;小时粒度服务器流量,周期是 24。
import pandas as pd import matplotlib.pyplot as plt from statsmodels.tsa.seasonal import STL df = pd.read_csv("orders.csv", parse_dates=["date"], index_col="date") df["orders"] = df["orders"].astype(float) stl = STL(df["orders"], period=7, seasonal=13, robust=True) result = stl.fit() fig = result.plot() fig.set_size_inches(12, 8) plt.show() trend = result.trend seasonal = result.seasonal resid = result.resid逻辑说明:STL 通过局部加权回归反复迭代,把趋势和季节性交替估计出来。robust=True 会在迭代中给极端值降权,避免双十一这种异常销量把季节项拉变形。代码里 period=7 对齐业务周周期,seasonal=13 表示季节项平滑窗口宽度为 13 个观测点,窗口越宽季节曲线越平缓。
参数说明:如果你的数据是月度粒度,seasonal 一般取 7 到 15 之间;如果是日粒度周周期,取 11 到 15 比较安全。seasonal 太小会出现季节项里混入噪声,太大又会把趋势的拐弯处吸走。判断标准很简单:画完图后看 trend 是否还能保留转折,seasonal 是否周期性清晰可见。
2.2 可解释性更强的做法:分离趋势作为特征而不是当噪声
STL 分解适合分析和可视化,但直接拿 trend 和 seasonal 当训练特征时,会遇到一个时序预测里的通病:分解组件在预测区间上不存在,必须外推。很多实战包里会直接 fit 一个 STL 到全量数据,把 fitted 的 trend 当特征喂给模型,到了预测阶段才发现 trend 没有未来值,只能用线性外推硬补,结果一塌糊涂。
我一般会做两步转换。第一步是显式构造趋势特征,用时间戳自带的连续变量,比如从起点到当前日期的天数,或者序号。第二步是把季节性编码成周期性特征,用正弦和余弦函数对周期索引做傅里叶展开,这样模型能学到“第几小时”或“第几周”的循环平滑关系。
import numpy as np df["t"] = np.arange(len(df)) df["week_sin"] = np.sin(2 * np.pi * df.index.dayofweek / 7) df["week_cos"] = np.cos(2 * np.pi * df.index.dayofweek / 7) df["month_sin"] = np.sin(2 * np.pi * df.index.month / 12) df["month_cos"] = np.cos(2 * np.pi * df.index.month / 12)逻辑说明:序号变量 t 让模型学到单调变化趋势;傅里叶特征让模型学到周期性相似度。判断一个周期特征编码是否合理,看预测曲线上周期衔接处是否平滑即可。sin 和 cos 的组合能把 dayofweek=0 和 dayofweek=6 之间的距离拉近,避免把周一和周日当成完全不相关的两个类别。
参数说明:7 对应星期周期,12 对应月份周期,这是最常见的两组。如果你做小时级流量预测,周期是 24,那就把 24 换成 7 和 12 对应位置的除数即可。如果序列同时存在日周期和年周期,也可以叠加多组傅里叶特征,但要注意列数不要超过训练样本数的十分之一,否则模型容易过拟合。
2.3 趋势和季节性的常见误用:直接把差分当处理完
不少人在实战里会用差分来“去掉趋势和季节性”,一阶差分去趋势,七阶差分去周季节性。这个思路在处理非平稳序列做平稳性检验时没问题,但放在预测建模里有个副作用:差分后的序列会放大噪声,预测误差会在逆差分时被逐级累积放大。
我倾向的做法是保留原始数值,把趋势和季节性作为显式特征建模,让模型自己学习如何组合它们。这样预测结果天然保持在原始量纲上,评估指标和业务口径都是一一对应的。只有在用 ARIMA 这类需要平稳性的模型时,差分才是必要的。
from statsmodels.tsa.stattools import adfuller result = adfuller(df["orders"].dropna()) print(f"ADF p-value: {result[1]:.4f}") result_diff = adfuller(df["orders"].diff().dropna()) print(f"ADF p-value (diff): {result_diff[1]:.4f}")逻辑说明:ADF 检验的零假设是序列存在单位根,p 值小于 0.05 就认为序列平稳。上面第一句是在检验原始序列,第二句是在检验一阶差分后的序列。如果你需要给 ARIMA 定阶,这个结果能确认差分数目;如果你走的是特征工程路线,这个结果只是告诉你是否需要额外处理趋势。
参数说明:diff() 不传参数就是一阶差分。如果你发现一阶差分后序列仍有年度趋势,再考虑 add 一个周期差分层,或者把时间序号特征的高阶项(平方、三次)加进去。切忌在一阶差分不平稳时盲目叠加差分阶数,差分的阶数每加一,预测误差的方差会显著膨胀。
3. 构建预测模型的三个落地方案:从基线到深度学习
3.1 基线与快速验证:用回归模型搭一套可解释的强基准
做趋势和季节性的预测,我建议不要一上来就调 LSTM。先用线性回归或者 LightGBM 这类特征模型跑一版,既能快速验证趋势特征和周期特征是否抓准,也能当后续复杂模型的对比基线。很多时候这个基线的效果就已经能支撑业务使用了,没必要求大求全。
这里提一个实战里的铁律:时间序列预测不能使用普通 K 折交叉验证,否则未来信息会泄漏到训练集里,验证指标虚高,上线后直接翻车。需要用时间顺序切分,训练集永远在验证集之前。
from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error df["lag_1"] = df["orders"].shift(1) df["lag_7"] = df["orders"].shift(7) df["rolling_mean_7"] = df["orders"].shift(7).rolling(7).mean() train = df.iloc[:-14] test = df.iloc[-14:] features = ["t", "week_sin", "week_cos", "month_sin", "month_cos", "lag_1", "lag_7", "rolling_mean_7"] model = LinearRegression() model.fit(train[features], train["orders"]) pred = model.predict(test[features]) mae = mean_absolute_error(test["orders"], pred) print(f"MAE on test: {mae:.2f}")逻辑说明:lag_1 和 lag_7 捕捉短期自相关和上周同一天的水平,rolling_mean_7 做了平滑处理,降低单日波动对预测的干扰。线性回归虽然简单,但这里的关键是特征构造是否贴合趋势和季节性的业务含义,模型本身只是做一个线性组合。预测结果出来后,如果 MAE 比均值预测还差,那问题一定出在趋势和季节特征构造或者滞后项对齐上。
参数说明:train 用 iloc[:-14] 把最后 14 天留给测试,是因为后续模型要预测两周的数据。lag_1 和 lag_7 同时放入时要注意共线性,如果效果不升反降,可以先去掉 lag_1 只保留 lag_7。rolling_mean_7 的窗口要和周期一致,不然只是多引入了一个噪声特征。
3.2 用 Prophet 做趋势和季节性的联合建模:业务参数怎么设
如果你不想手搓傅里叶特征,又希望模型自动处理趋势突变和节假日效应,Prophet 是当前落地效率比较高的选择。它本身就把趋势项和季节项分开建模,趋势用分段线性或逻辑斯蒂增长拟合,季节项用傅里叶级数逼近,这正是标题所指向的“基于趋势和季节性”的核心思路。
Prophet 的参数里,最值得花时间调的是 changepoint_prior_scale 和 seasonality_prior_scale。前者控制趋势拐点的灵活度,数值越大,模型越容易拟合突发增长或断崖下降;后者控制季节项的平滑程度,数值过大会把季节项拉平,过小则会把噪声当作周期。
from prophet import Prophet df_prophet = df.reset_index() df_prophet = df_prophet.rename(columns={"date": "ds", "orders": "y"}) model = Prophet( yearly_seasonality=False, weekly_seasonality=True, daily_seasonality=False, changepoint_prior_scale=0.05, seasonality_prior_scale=10, ) model.add_seasonality(name="monthly", period=30.4, fourier_order=5) model.fit(df_prophet) future = model.make_future_dataframe(periods=14, freq="D") forecast = model.predict(future) model.plot(forecast)逻辑说明:weekly_seasonality=True 会默认用傅里叶级数拟合 7 天周期;add_seasonality 再补充月度周期,用于捕捉类似月末冲量或账单周期类规律。changepoint_prior_scale=0.05 的意思是允许趋势在历史数据中出现拐点,默认值 0.05 适用于大多数业务场景;seasonality_prior_scale=10 表示对季节项的约束比较宽松,允许季节形状变化。
参数说明:period=30.4 是平均月长天数,fourier_order=5 控制月度季节项的复杂度,值越大拟合越精细,但泛化能力会下降。如果你的数据里存在大促这类强事件,可以将 changepoint_prior_scale 调到 0.1 到 0.3 之间试探效果。注意 Prophe t输出的 yhat 是原始量纲的预测值,yhat_lower 和 yhat_upper 是不确定性区间,业务汇报时一般建议直接用 yhat,不要过度解读区间范围。
3.3 用 LSTM 做序列预测:数据处理和训练要点
LSTM 在这类任务里的真实定位是:当你发现线性模型和 Prophet 的误差停留在某个平台,且残差里仍存在明显的非线性模式时,才值得引入。LSTM 的优势是能直接消费原始序列做端到端预测,但这恰恰也是最容易出错的地方,很多人把序列长度设置成 7 或者 30,但这往往只是照抄经验值,不是从数据里探出来的。
我一般会在进入 LSTM 之前,先对序列做窗口扫描,分别测试 input_window 在 7、14、28 下的验证集误差,选定窗口后再构建样本。另外,必须做归一化,而且归一化的 fit 只能用在训练集上,防止验证集信息提前进入 scaler。
import numpy as np from sklearn.preprocessing import MinMaxScaler from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense values = df["orders"].values.reshape(-1, 1) scaler = MinMaxScaler(feature_range=(0, 1)) scaled = scaler.fit_transform(values[: -14]) window = 14 X, y = [], [] for i in range(window, len(scaled)): X.append(scaled[i - window: i]) y.append(scaled[i]) X = np.array(X) y = np.array(y) split = int(len(X) * 0.8) X_train, X_val = X[:split], X[split:] y_train, y_val = y[:split], y[split:] model = Sequential() model.add(LSTM(32, activation="tanh", input_shape=(window, 1))) model.add(Dense(1)) model.compile(optimizer="adam", loss="mse") model.fit(X_train, y_train, validation_data=(X_val, y_val), epochs=30, batch_size=64, verbose=1)逻辑说明:每个训练样本是一个 window 长度的时间窗口,形状是 (window, 1),1 表示单变量特征。LSTM 层用 32 个隐单元,输入维度与窗口一致;输出层直接输出归一化后的下一个值。训练时把最后 14 天的原始数据排除在 scaling 之外,防止 MinMaxScaler 因为见到未来最大值而把历史数据压扁。
参数说明:window=14 表示用过去两周预测下一天,适合周周期明显的日粒度数据。batch_size=64 控制每次参数更新的样本量,数据量小时可以降到 16。epochs=30 是一个保守起点,观察 val_loss 连续 5 轮不下降就回调提前终止。激活函数默认 tanh 用于 LSTM 内部状态更新,Dense 层不配激活函数是因为这是回归任务。
预测时,把最后一个窗口的真实值作为输入,每次预测出的新值追加到窗口末尾,滚动预测 14 天。这里有个血泪经验:不要用预测出来的值反复作为下一轮的输入超过 7 步,误差会按指数叠加,超过 7 步后预测曲线通常会退化成一条平线。如果你确实需要长周期预测,建议用迭代预测的中间结果做一次残差校正,或者直接切换到 Prophet 这类全局模型。
4. 参数取舍与调优路径:从数据尺度到训练策略的选择
4.1 趋势跨度与季节性周期的参数联动
在趋势和季节性拆解类实战中,参数不是孤立的。趋势跨度影响你要用多少历史数据来拟合,季节性周期则决定傅里叶特征或滞后项的间隔。最常见的翻车就是把两者各调各的,趋势选 90 天,季节周期却写 7,最后模型对近期拐点反应迟钝。
我的原则是:模型训练窗口至少包含 3 个完整季节性周期。日粒度周周期,训练集至少 21 天;月粒度年周期,至少 3 年。短了,季节性模式学不完整;长了,趋势项会把遥远的旧模式强行带入,拖慢对最近业务变化的响应。
df["week_of_year"] = df.index.isocalendar().week print(df.groupby("week_of_year")["orders"].std().sort_values(ascending=False).head(10))逻辑说明:这行代码用来量化季节性强度。按周序号分组计算标准差,如果不同周之间的标准差差异很大,说明季节性在大幅波动;如果差异很小,说明季节性趋于稳定,建模时不需要给季节项太高自由度。实战中建议保守一点,先看数据的内在周期个数,再决定训练长度和季节特征阶数,而不是直接套一个全网通用的 window。
参数说明:isocalendar().week 是 pandas 1.1 以后的标准用法,返回 ISO 周序号,跨年时不会出现 0 和 53 的混淆。这个检查适合周季节性显式的数据,月粒度数据就换成 df.index.month。
4.2 调节平滑参数和鲁棒性参数的方式
无论是 STL 系数的平滑窗口,还是 LSTM 训练周期,背后都存在一个权衡:参数调得越大,曲线越平滑,对近期变化的敏感度越低;调得越小,拟合越激进,验证集误差可能先降后升。具体调参时不要追求验证集 MAE 无限低,要同时观察训练集和验证集的误差gap。
实战中我会跑一个简单网格,对比几个候选参数下的验证集误差,然后选择误差相对稳定、预测曲线不抖动的那个。不推荐单独依赖自动化调参工具一次跑完,因为时间序列中连续参数之间存在非线性耦合。
import itertools seasonal_candidates = [7, 11, 13, 15] best_params = None best_mae = float("inf") for seasonal in seasonal_candidates: stl = STL(df["orders"], period=7, seasonal=seasonal, robust=True) result = stl.fit() maes = [] for i in range(7, 14): pred = result.trend[-1] + result.seasonal[-i] actual = df["orders"].iloc[-i] maes.append(abs(pred - actual)) avg_mae = np.mean(maes) if avg_mae < best_mae: best_mae = avg_mae best_params = seasonal print(f"Best seasonal: {best_params}, repeated MAE: {best_mae:.2f}")逻辑说明:这里用一个朴素策略评估不同 seasonal 参数下分解结果对最近一周回看的预测能力。预测值用最后一个趋势值加对应时序的季节项,虽然粗糙,但足以反映分解参数是否让季节项稳定到可以用来预测的程度。真正的建模阶段会用完整训练集重拟合,但参数选型阶段这个做法足够快速有效。
参数说明:maes 循环从 7 到 13 共 7 个点,对应一周回看长度,实际上是用历史最后 7 天检验分解质量。这种方式不适合在线更新参数,只是给你一个参数选择的数理依据,避免纯凭感觉拍参。
4.3 模型训练中三个值得优先调试的数据处理参数
时间序列实战里,模型结构往往不是瓶颈,数据处理参数才是。第一个是滞后阶数,偏自相关函数会在哪个 lag 上截尾;第二个是归一化范围,MinMax 的上下界是否被离群点拉爆;第三个是训练验证切分比例,验证集是否包含完整季节性周期。
from statsmodels.tsa.stattools import pacf pacf_values = pacf(df["orders"].dropna(), nlags=20) for lag, value in enumerate(pacf_values[1:], start=1): if abs(value) > 0.2: print(f"Lag {lag}: {value:.3f}")逻辑说明:PACF 用于检测序列在滞后阶数上的直接相关性,超过阈值的 lag 就是值得放入特征的滞后边。比如 lag_7 的系数显著,说明上周同一天对今天有独立预测力,应当保留。lag 1 显著则是典型的短期惯性。超过 0.2 阈值的 lag 越多,说明序列依赖更多历史信息,LSTM 这类模型的价值就越大。
参数说明:nlags=20 表示计算 20 阶偏自相关,日粒度周周期数据一般 20 阶足够;月粒度数据建议设置 nlags=36。阈值 0.2 不是硬标准,如果数据量少,可以放宽到 0.15,但不要低于 0.1,否则会把纯噪声当趋势带进模型。
5. 趋势和季节性预测的避坑清单:五个现象、原因与解法
5.1 预测期末端出现“断崖或尖刺”
现象:预测曲线在最后几天突然掉头向下或直线冲高。原因通常是趋势项和季节项在预测区间边界上相位错位,或者 LSTM 滚动预测时误差累积触发了极端输出。解决:检查训练集末尾是否包含异常值,比如大促或补货造成的单日脉冲;把序列末尾三天的数据用中位数平滑再进模型,预测出结果后也不要直接交付,先做平滑后处理。
df["orders_smoothed"] = df["orders"].rolling(3, center=True, min_periods=1).median()逻辑说明:rolling median 比均值更抗离群点,center=True 保证平滑值时序不偏移。预测前用平滑序列替代原始序列建模,能显著降低点尾效应。注意在最终评估时还是要用原始值计算误差,平滑只是为了稳定模型训练。
5.2 验证集误差低、上线误差高
现象:离线测试 MAE 很好看,一上线就偏差很大。原因多数是验证集切分不合理,测试区间正好落在季节性高点或低点,而模型在训练阶段没见过类似位置。解决:用滚动时间序列验证,多次切分取平均,不要只凭一次留出法下结论。验证集应至少包含两个完整周期,或者采用扩展窗口训练、滚动预测的方式模拟线上行为。
5.3 差分后预测无法还原
现象:用了差分平稳化,逆差分后误差被放大到不可接受。原因在于误差在差分的累积反转过程中会多次放大,尤其在多步预测时更严重。如果你不是在做 ARIMA 定阶研究,建议优先把趋势和季节项作为特征建模,而不是对原始序列做差分。这属于模型选型问题,不是调参能解决的。
5.4 季节性周期设定错误
现象:分解结果看不出固定周期,或季节项和趋势项纠缠不清。原因通常是 period 设定与实际业务周期不一致。比如日粒度数据有月度冲量,你就不能只设定 period=7,要增加 period=30 的月度季节项;小时级数据同时存在 24 小时和 168 小时周期,只保留其中一个,残差里就会出现明显的条纹状噪声。
解决方式是先做频谱分析确认主周期,再回头修正 period。周期识别错误会让后面所有特征工程失去方向,这也是先把数据拆开看的原因。
5.5 LSTM 预测逐渐退化成均值线
现象:迭代预测超过一定步数后,输出趋向于训练集的均值,周期细节完全消失。原因在于多步递归时误差不断叠加,模型最终只能输出一个最保守的中间值。解决方法是改用直接多步预测,即输出层用多个神经元,一次预测未来 N 天;或者在 LSTM 后面接一个线性输出层,分步预测再拼接。如果业务只关心未来一天,则保持单步预测即可,不要盲目追求预测长度。
6. 验证预测质量的三个手段:残差分析、反向测试与对比基线
预测模型上线前,只看 MAE 是不够的。我一般会额外跑三个验证手段。第一,残差分析:把预测残差对时间序号作图,如果残差里还有明显的周期性条纹,说明季节项没提取干净,回去调整傅里叶阶数或 STL 周期。第二,反向测试:用训练集末端的某个历史节点做起点,模拟当时的模型输入并预测“未来”,然后和真实历史对比,这样能验证模型对拐点和周期变化的真实响应能力。第三,基线对比:始终在报告里留一行“用上一周期实际值直接当预测”的误差,如果复杂模型连这个都赢不了,就不该上线。
import matplotlib.pyplot as plt resid = test["orders"] - pred plt.figure(figsize=(10, 4)) plt.plot(resid) plt.axhline(y=0, color="r", linestyle="--") plt.title("Residual over time") plt.show()逻辑说明:残差图中如果出现与周期等距的锯齿状波动,说明模型没有完整捕捉到季节性。比如 7 天周期的数据,残差每隔 7 点出现一个峰,意味着周季节模式的幅度或形状在训练集和测试集之间发生了偏移。此时优先检查傅里叶阶数是否过低,或者 STL 的 seasonal 参数是否过小。
第二个反向测试的具体做法是:从训练集倒数第 14 天处截断,只用更早的数据重新训练,然后用同样的时间顺序预测后面 14 天,与真实值对比。这个流程模拟的是“站在过去预测现在”的场景,比单纯对保留集预测更能暴露数据泄漏和滞后特征带来的乐观偏差。注意反向测试和正式验证使用同一套参数,不要调两次参。
参数层面,还有两个实际经验值得记录:预测输出的整数化处理不要把小数直接四舍五入,对于销量类数据,业务上通常期望预测值带一位小数,方便看趋势;但对于容量规划类场景,则要向上取整保证资源充足。另一个是数据异常检测不能只看预测偏差,还要关注真实值的采集稳定性,尤其当上游数据链路出现延迟补报时,预测模型会把缺失当成真实下跌来学习。
最后,每轮模型迭代后,我会把自己调过的参数和验证结论记在一张表里,包括训练窗口、周期设定、seasonal 参数、滞后阶数和验证误差。这张表比代码本身更宝贵。没有它,三个月后再看同一个实战包,你只记得当初调参调得很痛苦,却不知道哪个参数救了你。希望帮到你。
本文还有配套的精品资源,点击获取