1. 时间序列分析入门从数据理解到模式拆解如果你手头有一份按时间顺序记录的数据比如过去几年的每日销售额、每小时的网站访问量或者每分钟的传感器读数那么恭喜你你正面对着一个充满宝藏也布满陷阱的领域——时间序列分析。我处理过太多类似的项目从电商销量预测到工业设备故障预警第一步永远不是急着上模型而是静下心来像侦探一样“读懂”数据本身。时间序列数据最大的特点就是它的“记忆性”今天的值深受昨天、甚至上周同期的影响。这和我们熟悉的、可以随意打乱顺序的表格数据截然不同。理解这种内在的依赖关系是后续一切建模、预测工作的基石。这篇文章我就以一个真实的美国小时级电力消耗数据集为例带你走一遍时间序列分析的完整流程把数据里藏着的趋势、季节性和随机波动都揪出来为后续的精准预测打下坚实基础。2. 项目环境与数据初探2.1 工具链搭建为什么选择这些库工欲善其事必先利其器。对于时间序列分析Python生态提供了极其强大的工具集。我们首先需要搭建环境。我强烈建议使用Anaconda创建一个独立的虚拟环境避免包冲突。核心库包括pandas numpy: 数据操作的基石无需多言。statsmodels: 这是时间序列分析的“瑞士军刀”提供了从经典分解seasonal_decompose到现代STL/MSTL分解再到自相关分析acf, pacf的全套工具。它的API设计非常统计学家友好能帮助我们深刻理解数据生成过程。pmdarima: 一个封装了自动ARIMA模型选择的库其中的ndiffs和nsdiffs函数能智能地建议使序列平稳化所需的最佳差分阶数省去了大量手动尝试。plotly: 我选择Plotly而非Matplotlib进行可视化是因为其交互性。在分析时间序列时能够缩放、平移图表查看特定时间点的精确值对于发现异常、理解局部模式至关重要。scikit-learn: 主要用于离群值检测Isolation Forest和评估指标计算MAE, MAPE。安装命令很简单pip install pandas numpy statsmodels pmdarima plotly scikit-learn这里有个小坑需要注意statsmodels的新版本有时会与Python版本有兼容性问题。如果遇到报错可以尝试指定一个稍旧的稳定版本例如pip install statsmodels0.13.2。2.2 数据加载与特征工程挖掘时间维度信息我们使用的数据集是AEP美国电力公司从2004年到2018年的每小时电力消耗数据。原始数据通常很“瘦”只有两列时间戳datetime和消耗量MW。但正是这个时间戳列蕴藏着金矿。import pandas as pd import numpy as np # 加载数据确保时间戳被正确解析 df pd.read_csv(AEP_hourly.csv) df.columns [datetime, elec_cons] # 规范列名 df[datetime] pd.to_datetime(df[datetime], errorscoerce) df df.sort_values(datetime).reset_index(dropTrue) # 按时间排序是必须的 # 预览数据 print(df.head()) print(f数据时间范围{df[datetime].min()} 到 {df[datetime].max()}) print(f总记录数{len(df)})接下来是最关键的一步时间特征提取。这不仅仅是简单的字段拆分而是将连续的时间流解构成可供模型理解的周期性信号。# 提取丰富的时序特征 df[year] df[datetime].dt.year df[month] df[datetime].dt.month df[day_of_month] df[datetime].dt.day df[hour] df[datetime].dt.hour df[day_of_week] df[datetime].dt.dayofweek # 周一0 周日6 df[week_of_year] df[datetime].dt.isocalendar().week df[is_weekend] df[day_of_week].isin([5, 6]).astype(int) # 创建更具语义的类别特征并排序 df[month_name] pd.Categorical(df[datetime].dt.month_name(), categories[January, February, March, April, May, June, July, August, September, October, November, December], orderedTrue) df[day_name] pd.Categorical(df[datetime].dt.day_name(), categories[Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday], orderedTrue)为什么这么做电力消耗显然受多种周期影响日内周期白天 vs 夜晚、周内周期工作日 vs 周末、年内周期夏季空调用电 vs 冬季取暖用电。将这些周期显式地作为特征提取出来不仅有助于后续的可视化分析在构建机器学习模型时这些特征也是极强的预测因子。例如一个简单的模型可能发现hour和is_weekend是预测下一个小时用电量的关键变量。重要提示在处理任何时间序列数据时绝对不要进行随机打乱shuffle。时序的先后顺序是信息的核心载体。打乱顺序等于摧毁了数据中最重要的因果关系得到的模型将毫无预测能力。这一点与处理图像或通用表格数据有本质区别。3. 可视化分析用眼睛“看见”模式与异常可视化不是点缀而是分析的核心。它帮助我们形成对数据的直觉指导后续的建模方向。3.1 整体趋势与滚动平均首先我们看看数据的“全貌”。直接绘制原始序列可能会因为噪声太大而看不清趋势。import plotly.express as px import plotly.graph_objects as go from plotly.subplots import make_subplots # 绘制2015年原始数据 df_2015 df[df[year] 2015].copy() fig_raw px.line(df_2015, xdatetime, yelec_cons, title2015年原始小时电力消耗数据 (噪声较大)) fig_raw.show()这张图波动剧烈很难看出规律。这时就需要**滚动平均Rolling Average**来平滑短期波动揭示长期趋势和周期。窗口大小的选择有讲究太小则平滑效果不足太大则会过度平滑丢失重要细节。对于小时数据30天720小时的窗口可以很好地平滑掉日度和周度波动展现月度以上的趋势。# 计算30天滚动平均 df_2015[rolling_30d] df_2015[elec_cons].rolling(window24*30, centerTrue, min_periods1).mean() fig make_subplots(rows2, cols1, shared_xaxesTrue, subplot_titles(原始序列, 30天滚动平均序列)) fig.add_trace(go.Scatter(xdf_2015[datetime], ydf_2015[elec_cons], modelines, name原始值, linedict(colorlightblue)), row1, col1) fig.add_trace(go.Scatter(xdf_2015[datetime], ydf_2015[rolling_30d], modelines, name30天滚动平均, linedict(colorred, width2)), row2, col1) fig.update_layout(height600, title_text滚动平均平滑效果对比) fig.show()从滚动平均曲线可以清晰看到电力消耗在夏季7-8月和冬季12-1月有两个高峰这对应着空调和取暖的需求。而春季和秋季则是用电低谷。这就是数据的长期趋势和年度季节性。3.2 多角度季节性分析年度季节性对比将不同年份同月份的数据放在一起对比可以看季节性模式是否稳定。# 选取多个年份计算月平均 years_to_plot [2005, 2008, 2011, 2014] seasonal_df df[df[year].isin(years_to_plot)].groupby([year, month_name])[elec_cons].mean().reset_index() fig px.line(seasonal_df, xmonth_name, yelec_cons, coloryear, title不同年份的月度平均电力消耗对比, labels{elec_cons: 平均消耗 (MW), month_name: 月份}) fig.show()如果各年份的曲线形状高度相似说明年度季节性非常稳定这对预测是利好。如果差异较大则可能暗示存在长期趋势或有外部事件影响。周内-小时热力图这是分析日内和周内模式的利器。# 创建“星期几-小时”平均消耗热力图 pivot_df df.pivot_table(indexday_name, columnshour, valueselec_cons, aggfuncmean) fig px.imshow(pivot_df, labelsdict(x小时, y星期几, color平均消耗 (MW)), x[f{h:02d}:00 for h in range(24)], title电力消耗模式星期几 vs 小时, aspectauto) fig.update_xaxes(sidetop) fig.show()从热力图中你可以一目了然地看到工作日的白天9点至17点是消耗高峰对应商业活动夜晚和周末清晨消耗最低。周五晚上可能有一个小高峰而周六的白天模式与工作日明显不同。这种可视化能瞬间抓住最核心的周期性规律。年度-月度热力图观察长期趋势与月度季节性的叠加。yearly_monthly_pivot df.pivot_table(indexyear, columnsmonth_name, valueselec_cons, aggfuncmean) fig px.imshow(yearly_monthly_pivot, title年度-月度平均电力消耗热力图, labelsdict(x月份, y年份, color平均消耗 (MW))) fig.show()这张图可以同时展示两个维度横向月份看年度季节性纵向年份看长期趋势。你可能发现不仅每年夏季的峰值在升高可能由于经济增长或气候变暖而且整体颜色逐年变深这直观地显示了长期向上的趋势。4. 数据预处理处理缺失值与异常点真实数据从不完美。缺失值和异常点是时间序列分析中的两大常见问题处理不当会严重误导模型。4.1 缺失值处理如何科学地“填空”时间序列的缺失值处理有其特殊性因为相邻点之间存在相关性。常用的方法有前向填充ffill/后向填充bfill用前一个或后一个已知值填充。适用于数据缺失时间短、序列相对平稳的情况。但缺点是会制造出“平台”扭曲真实波动。线性插值/时间插值在已知点之间画一条直线或曲线进行填充。methodtime参数会考虑时间索引的间隔比简单的线性插值更合理。这是处理中小规模缺失的常用方法。季节性线性插值对于具有强季节性的数据可以先估计出季节性成分再对去季节化的序列进行插值最后加回季节性。这更复杂但更精准。基于模型的预测填充用ARIMA等模型预测缺失段的值。这是最“高级”的方法但依赖于模型假设且容易在缺失段两端产生不连续。如何选择一个实用的方法是模拟评估。我们可以在完整的数据段上人工制造一小段缺失然后用不同方法填充并与真实值比较选择误差最小的方法。from sklearn.metrics import mean_absolute_error, mean_absolute_percentage_error # 选取一段已知的完整数据作为测试床 test_period df[(df[datetime] 2014-06-01) (df[datetime] 2014-07-01)].copy() test_period.set_index(datetime, inplaceTrue) # 人工制造3天的缺失 mask_start 2014-06-10 mask_end 2014-06-13 test_period_missing test_period.copy() test_period_missing.loc[mask_start:mask_end, elec_cons] np.nan # 应用不同填充方法 test_period_missing[ffill] test_period_missing[elec_cons].ffill() test_period_missing[bfill] test_period_missing[elec_cons].bfill() test_period_missing[linear] test_period_missing[elec_cons].interpolate(methodlinear) test_period_missing[time] test_period_missing[elec_cons].interpolate(methodtime) # 简单均值填充通常效果最差仅作对比 test_period_missing[mean] test_period_missing[elec_cons].fillna(test_period[elec_cons].mean()) # 评估 results {} true_values test_period.loc[mask_start:mask_end, elec_cons] for method in [ffill, bfill, linear, time, mean]: imputed_values test_period_missing.loc[mask_start:mask_end, method] mae mean_absolute_error(true_values, imputed_values) mape mean_absolute_percentage_error(true_values, imputed_values) results[method] {MAE: mae, MAPE: mape} results_df pd.DataFrame(results).T print(results_df)在我的多次实践中对于具有趋势和季节性的数据如电力消耗时间插值time或线性插值linear通常是稳健的首选它们的MAE和MAPE往往最低。前向/后向填充会引入阶梯状伪影而均值填充完全忽略了时序结构应尽量避免。4.2 异常值检测与处理别让“坏点”带偏模型异常值可能是数据错误如传感器故障也可能是真实但罕见的事件如重大节日、停电。Isolation Forest是一种无监督算法特别适合时间序列因为它不假设数据的分布且对高维数据有效。from sklearn.ensemble import IsolationForest from sklearn.preprocessing import StandardScaler # 准备数据 values df[elec_cons].values.reshape(-1, 1) # 标准化非常重要因为Isolation Forest对尺度敏感 scaler StandardScaler() values_scaled scaler.fit_transform(values) # 训练Isolation Forest模型 # contamination参数是异常值比例的估计可根据领域知识调整例如设为0.011% iso_forest IsolationForest(contamination0.01, random_state42, n_jobs-1) outlier_labels iso_forest.fit_predict(values_scaled) # 标记异常点IsolationForest返回1为正常-1为异常 df[is_outlier] outlier_labels -1 print(f检测到异常点数量{df[is_outlier].sum()}) # 可视化异常点 fig px.scatter(df, xdatetime, yelec_cons, coloris_outlier, title电力消耗数据异常点检测 (Isolation Forest), color_discrete_map{False: blue, True: red}, opacity0.6) fig.show()检测到异常点后如何处理删除如果确认是数据错误且缺失比例很小直接删除是最干净的。修正Imputation如果异常点较多或处于关键时间位置删除会导致信息损失。可以采用类似处理缺失值的方法进行修正例如用前后点的线性插值替代或者用该小时在历史同期例如同星期几、同小时的中位数替代。# 方法用前后各12小时共24小时的均值替代异常值简单示例 df[elec_cons_cleaned] df[elec_cons].copy() outlier_indices df[df[is_outlier]].index for idx in outlier_indices: start max(0, idx - 12) end min(len(df), idx 13) # 包含后12个点 # 取窗口内非异常点的均值进行替换 window_values df.loc[start:end, elec_cons][~df.loc[start:end, is_outlier]] if not window_values.empty: df.at[idx, elec_cons_cleaned] window_values.mean() else: # 如果窗口内全是异常则用更全局的方法如该小时的历史中位数 hour_of_day df.at[idx, hour] df.at[idx, elec_cons_cleaned] df[df[hour] hour_of_day][elec_cons].median()核心原则处理异常值的逻辑必须符合业务常识。例如圣诞节当天的低用电量可能不是异常而是真实的季节性事件这类“异常”不应该被平滑掉而应作为特殊事件特征引入模型。5. 深入核心时间序列分解与平稳性检验5.1 时间序列分解拆解趋势、季节与残差经典的时间序列分解认为一个序列可以拆解为三个部分趋势Trend、季节性Seasonality和残差Residual或称不规则波动。分解有两种模式加法模型Y T S R和乘法模型Y T * S * R。当季节波动的幅度不随趋势水平变化时用加法模型反之用乘法模型。电力数据通常趋势增长夏季峰值也逐年增高可能更适合乘法模型但我们可以先尝试加法模型观察。statsmodels库提供了简单的经典分解from statsmodels.tsa.seasonal import seasonal_decompose # 选取一段数据频率设置为每日‘D’但我们的数据是每小时需要聚合或使用更高频设置 # 为了演示我们先按日聚合 df_daily df.set_index(datetime)[elec_cons_cleaned].resample(D).mean() # 进行加法模型分解假设年度季节性period365 result_add seasonal_decompose(df_daily, modeladditive, period365, extrapolate_trendfreq) result_add.plot() plt.show()对于更复杂的数据特别是存在多个季节性周期如小时、周、年时STLSeasonal-Trend decomposition using LOESS或MSTLMultiple STL是更强大的工具。它们使用鲁棒的局部加权回归进行平滑对异常值不敏感并能处理复杂的季节性形态。from statsmodels.tsa.seasonal import STL, MSTL # 使用STL分解指定季节性周期为24小时和16824*7周 # 注意STL/MSTL需要较高频率的原始数据这里我们用小时数据 df_hourly df.set_index(datetime)[elec_cons_cleaned].asfreq(H) # 确保频率规则 df_hourly_filled df_hourly.interpolate(methodtime) # 如有缺失需处理 # 由于MSTL计算量大我们选取一个子集例如一年 subset df_hourly_filled[2014-01-01:2014-12-31] # MSTL分解同时考虑日周期(24)和周周期(168) mstl_result MSTL(subset, periods(24, 168), stl_kwargs{robust: True}).fit() fig mstl_result.plot() plt.show()通过分解图你可以清晰地看到趋势成分缓慢上升日度季节性成分周期24显示白天高、夜晚低的规律周度季节性成分周期168显示工作日与周末的差异残差成分则看起来像是随机噪声。一个健康的残差应该是没有明显模式、近似白噪声的。如果残差中还有规律说明模型未能完全捕捉数据中的信息。5.2 平稳性检验时间序列建模的入场券大多数经典时间序列模型如ARIMA都要求数据是平稳的。平稳性意味着序列的统计特性如均值、方差不随时间变化。显然有趋势和季节性的序列是非平稳的。如何检验平稳性最常用的是增强迪基-富勒检验ADF Test。其原假设是“序列具有单位根即非平稳”。如果p值小于显著性水平如0.05我们拒绝原假设认为序列是平稳的。from statsmodels.tsa.stattools import adfuller adf_result adfuller(df_daily.dropna()) # 使用清洗后的日度数据 print(fADF Statistic: {adf_result[0]}) print(fp-value: {adf_result[1]}) print(Critical Values:) for key, value in adf_result[4].items(): print(f\t{key}: {value})对于电力数据ADF检验几乎肯定会给出一个很大的p值0.05表明序列非平稳。如何使序列平稳——差分Differencing。一阶差分可以消除线性趋势季节性差分可以消除季节性。# 一阶差分消除趋势 df_daily_diff1 df_daily.diff().dropna() # 季节性差分周期为7消除周效应通常用于更细粒度数据日数据可尝试周期7 df_daily_seasonal_diff df_daily.diff(periods7).dropna() # 对差分后的序列再次进行ADF检验 adf_result_diff adfuller(df_daily_diff1.dropna()) print(f一阶差分后序列的p-value: {adf_result_diff[1]})通常经过一阶或季节性差分后序列会变得平稳。pmdarima库的ndiffs函数可以自动建议最优的差分阶数。from pmdarima.arima import ndiffs # 自动检测需要几阶差分才能使序列平稳基于ADF检验 n_adf ndiffs(df_daily, testadf) print(f建议的差分阶数 (基于ADF检验): {n_adf})5.3 自相关与偏自相关分析洞察数据的内在记忆自相关函数ACF描述了一个时间序列与其自身滞后版本的相关性。偏自相关函数PACF则在控制了中间滞后项的影响后描述了序列与某一滞后项的直接相关性。它们是确定ARIMA模型参数(p, d, q)的关键工具。ACF图如果ACF缓慢衰减拖尾表明序列非平稳或具有长期记忆。如果ACF在滞后s, 2s, 3s...处出现峰值表明存在周期为s的季节性。PACF图PACF在滞后p之后突然截断降至不显著则提示AR模型的阶数可能为p。from statsmodels.graphics.tsaplots import plot_acf, plot_pacf # 对平稳化后的序列例如一阶差分后绘制ACF和PACF fig, axes plt.subplots(1, 2, figsize(12, 4)) plot_acf(df_daily_diff1.dropna(), lags40, axaxes[0]) plot_pacf(df_daily_diff1.dropna(), lags40, axaxes[1], methodywm) # 使用Yule-Walker方法 plt.show()如何解读对于差分后的日度电力数据ACF图可能会显示在滞后7、14等处有显著相关周季节性而PACF图可能在滞后1、2处显著然后截断。这提示我们可能需要一个包含AR项和季节性成分的模型比如SARIMA。6. 实战经验与避坑指南经过上面这些步骤你已经对时间序列数据完成了从外到内的“体检”。下面分享几个我踩过坑才总结出的经验数据频率与业务逻辑对齐你的分析频率必须匹配业务决策频率。如果你需要预测未来一小时的用电量却用日均数据建模就会丢失关键的日内模式。反之如果你只需要月度预算却用分钟级数据则会引入不必要的噪声和计算负担。在开始分析前务必明确业务问题。处理“大日期”当你的数据跨越多年特别是包含闰年时以“一年365天”作为季节性周期可能会产生微小偏差。对于精细预测可以考虑使用“一年365.25天”或直接使用傅里叶项Fourier terms来模拟季节性这比固定周期更灵活。警惕“未来信息泄露”这是时间序列预处理中最常见的错误。例如在计算整个数据集的滚动平均值或标准化减去均值、除以标准差时如果使用了未来的数据就会导致模型在训练时“偷看”到答案造成过于乐观的评估结果。正确的做法是仅使用历史信息。例如滚动平均应该用“向前滚动”rolling(min_periods1).mean()标准化应该在每个滚动窗口内独立计算。分解与预测的顺序一种常见的策略是“先分解后预测”。即先将序列分解为趋势、季节性和残差然后对相对平稳的残差序列进行建模预测最后将预测结果加回趋势和季节性成分。这种方法如Facebook Prophet的核心思想对于具有强且稳定季节性的序列非常有效。另一种策略是使用直接能处理趋势和季节性的模型如SARIMA或带有季节性特征的梯度提升树LightGBM, XGBoost。没有绝对的好坏需要根据数据特性和预测目标进行选择。可视化是永不落伍的调试工具无论模型多复杂永远要相信你的眼睛。将预测结果与真实值画在一起将残差序列画出来检查是否随机。一个模式明显的残差图就是在告诉你模型还有改进空间。我习惯在建模的每个关键步骤后都进行可视化这能帮我快速定位问题。至此我们已经完成了时间序列分析的第一部分——理解与准备。我们像剥洋葱一样通过可视化看到了数据的宏观形态通过预处理清洗了数据瑕疵通过分解理解了其内在结构并通过平稳性检验和自相关分析为建模做好了准备。这些步骤虽然繁琐但至关重要。一个建立在错误理解数据基础上的复杂模型其预测能力往往还不如一个建立在深刻理解基础上的简单模型。在下一部分我们将基于这些分析着手构建预测模型将理解转化为预见未来的能力。
时间序列分析实战:从电力数据分解到平稳性检验
发布时间:2026/6/1 12:20:59
1. 时间序列分析入门从数据理解到模式拆解如果你手头有一份按时间顺序记录的数据比如过去几年的每日销售额、每小时的网站访问量或者每分钟的传感器读数那么恭喜你你正面对着一个充满宝藏也布满陷阱的领域——时间序列分析。我处理过太多类似的项目从电商销量预测到工业设备故障预警第一步永远不是急着上模型而是静下心来像侦探一样“读懂”数据本身。时间序列数据最大的特点就是它的“记忆性”今天的值深受昨天、甚至上周同期的影响。这和我们熟悉的、可以随意打乱顺序的表格数据截然不同。理解这种内在的依赖关系是后续一切建模、预测工作的基石。这篇文章我就以一个真实的美国小时级电力消耗数据集为例带你走一遍时间序列分析的完整流程把数据里藏着的趋势、季节性和随机波动都揪出来为后续的精准预测打下坚实基础。2. 项目环境与数据初探2.1 工具链搭建为什么选择这些库工欲善其事必先利其器。对于时间序列分析Python生态提供了极其强大的工具集。我们首先需要搭建环境。我强烈建议使用Anaconda创建一个独立的虚拟环境避免包冲突。核心库包括pandas numpy: 数据操作的基石无需多言。statsmodels: 这是时间序列分析的“瑞士军刀”提供了从经典分解seasonal_decompose到现代STL/MSTL分解再到自相关分析acf, pacf的全套工具。它的API设计非常统计学家友好能帮助我们深刻理解数据生成过程。pmdarima: 一个封装了自动ARIMA模型选择的库其中的ndiffs和nsdiffs函数能智能地建议使序列平稳化所需的最佳差分阶数省去了大量手动尝试。plotly: 我选择Plotly而非Matplotlib进行可视化是因为其交互性。在分析时间序列时能够缩放、平移图表查看特定时间点的精确值对于发现异常、理解局部模式至关重要。scikit-learn: 主要用于离群值检测Isolation Forest和评估指标计算MAE, MAPE。安装命令很简单pip install pandas numpy statsmodels pmdarima plotly scikit-learn这里有个小坑需要注意statsmodels的新版本有时会与Python版本有兼容性问题。如果遇到报错可以尝试指定一个稍旧的稳定版本例如pip install statsmodels0.13.2。2.2 数据加载与特征工程挖掘时间维度信息我们使用的数据集是AEP美国电力公司从2004年到2018年的每小时电力消耗数据。原始数据通常很“瘦”只有两列时间戳datetime和消耗量MW。但正是这个时间戳列蕴藏着金矿。import pandas as pd import numpy as np # 加载数据确保时间戳被正确解析 df pd.read_csv(AEP_hourly.csv) df.columns [datetime, elec_cons] # 规范列名 df[datetime] pd.to_datetime(df[datetime], errorscoerce) df df.sort_values(datetime).reset_index(dropTrue) # 按时间排序是必须的 # 预览数据 print(df.head()) print(f数据时间范围{df[datetime].min()} 到 {df[datetime].max()}) print(f总记录数{len(df)})接下来是最关键的一步时间特征提取。这不仅仅是简单的字段拆分而是将连续的时间流解构成可供模型理解的周期性信号。# 提取丰富的时序特征 df[year] df[datetime].dt.year df[month] df[datetime].dt.month df[day_of_month] df[datetime].dt.day df[hour] df[datetime].dt.hour df[day_of_week] df[datetime].dt.dayofweek # 周一0 周日6 df[week_of_year] df[datetime].dt.isocalendar().week df[is_weekend] df[day_of_week].isin([5, 6]).astype(int) # 创建更具语义的类别特征并排序 df[month_name] pd.Categorical(df[datetime].dt.month_name(), categories[January, February, March, April, May, June, July, August, September, October, November, December], orderedTrue) df[day_name] pd.Categorical(df[datetime].dt.day_name(), categories[Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday], orderedTrue)为什么这么做电力消耗显然受多种周期影响日内周期白天 vs 夜晚、周内周期工作日 vs 周末、年内周期夏季空调用电 vs 冬季取暖用电。将这些周期显式地作为特征提取出来不仅有助于后续的可视化分析在构建机器学习模型时这些特征也是极强的预测因子。例如一个简单的模型可能发现hour和is_weekend是预测下一个小时用电量的关键变量。重要提示在处理任何时间序列数据时绝对不要进行随机打乱shuffle。时序的先后顺序是信息的核心载体。打乱顺序等于摧毁了数据中最重要的因果关系得到的模型将毫无预测能力。这一点与处理图像或通用表格数据有本质区别。3. 可视化分析用眼睛“看见”模式与异常可视化不是点缀而是分析的核心。它帮助我们形成对数据的直觉指导后续的建模方向。3.1 整体趋势与滚动平均首先我们看看数据的“全貌”。直接绘制原始序列可能会因为噪声太大而看不清趋势。import plotly.express as px import plotly.graph_objects as go from plotly.subplots import make_subplots # 绘制2015年原始数据 df_2015 df[df[year] 2015].copy() fig_raw px.line(df_2015, xdatetime, yelec_cons, title2015年原始小时电力消耗数据 (噪声较大)) fig_raw.show()这张图波动剧烈很难看出规律。这时就需要**滚动平均Rolling Average**来平滑短期波动揭示长期趋势和周期。窗口大小的选择有讲究太小则平滑效果不足太大则会过度平滑丢失重要细节。对于小时数据30天720小时的窗口可以很好地平滑掉日度和周度波动展现月度以上的趋势。# 计算30天滚动平均 df_2015[rolling_30d] df_2015[elec_cons].rolling(window24*30, centerTrue, min_periods1).mean() fig make_subplots(rows2, cols1, shared_xaxesTrue, subplot_titles(原始序列, 30天滚动平均序列)) fig.add_trace(go.Scatter(xdf_2015[datetime], ydf_2015[elec_cons], modelines, name原始值, linedict(colorlightblue)), row1, col1) fig.add_trace(go.Scatter(xdf_2015[datetime], ydf_2015[rolling_30d], modelines, name30天滚动平均, linedict(colorred, width2)), row2, col1) fig.update_layout(height600, title_text滚动平均平滑效果对比) fig.show()从滚动平均曲线可以清晰看到电力消耗在夏季7-8月和冬季12-1月有两个高峰这对应着空调和取暖的需求。而春季和秋季则是用电低谷。这就是数据的长期趋势和年度季节性。3.2 多角度季节性分析年度季节性对比将不同年份同月份的数据放在一起对比可以看季节性模式是否稳定。# 选取多个年份计算月平均 years_to_plot [2005, 2008, 2011, 2014] seasonal_df df[df[year].isin(years_to_plot)].groupby([year, month_name])[elec_cons].mean().reset_index() fig px.line(seasonal_df, xmonth_name, yelec_cons, coloryear, title不同年份的月度平均电力消耗对比, labels{elec_cons: 平均消耗 (MW), month_name: 月份}) fig.show()如果各年份的曲线形状高度相似说明年度季节性非常稳定这对预测是利好。如果差异较大则可能暗示存在长期趋势或有外部事件影响。周内-小时热力图这是分析日内和周内模式的利器。# 创建“星期几-小时”平均消耗热力图 pivot_df df.pivot_table(indexday_name, columnshour, valueselec_cons, aggfuncmean) fig px.imshow(pivot_df, labelsdict(x小时, y星期几, color平均消耗 (MW)), x[f{h:02d}:00 for h in range(24)], title电力消耗模式星期几 vs 小时, aspectauto) fig.update_xaxes(sidetop) fig.show()从热力图中你可以一目了然地看到工作日的白天9点至17点是消耗高峰对应商业活动夜晚和周末清晨消耗最低。周五晚上可能有一个小高峰而周六的白天模式与工作日明显不同。这种可视化能瞬间抓住最核心的周期性规律。年度-月度热力图观察长期趋势与月度季节性的叠加。yearly_monthly_pivot df.pivot_table(indexyear, columnsmonth_name, valueselec_cons, aggfuncmean) fig px.imshow(yearly_monthly_pivot, title年度-月度平均电力消耗热力图, labelsdict(x月份, y年份, color平均消耗 (MW))) fig.show()这张图可以同时展示两个维度横向月份看年度季节性纵向年份看长期趋势。你可能发现不仅每年夏季的峰值在升高可能由于经济增长或气候变暖而且整体颜色逐年变深这直观地显示了长期向上的趋势。4. 数据预处理处理缺失值与异常点真实数据从不完美。缺失值和异常点是时间序列分析中的两大常见问题处理不当会严重误导模型。4.1 缺失值处理如何科学地“填空”时间序列的缺失值处理有其特殊性因为相邻点之间存在相关性。常用的方法有前向填充ffill/后向填充bfill用前一个或后一个已知值填充。适用于数据缺失时间短、序列相对平稳的情况。但缺点是会制造出“平台”扭曲真实波动。线性插值/时间插值在已知点之间画一条直线或曲线进行填充。methodtime参数会考虑时间索引的间隔比简单的线性插值更合理。这是处理中小规模缺失的常用方法。季节性线性插值对于具有强季节性的数据可以先估计出季节性成分再对去季节化的序列进行插值最后加回季节性。这更复杂但更精准。基于模型的预测填充用ARIMA等模型预测缺失段的值。这是最“高级”的方法但依赖于模型假设且容易在缺失段两端产生不连续。如何选择一个实用的方法是模拟评估。我们可以在完整的数据段上人工制造一小段缺失然后用不同方法填充并与真实值比较选择误差最小的方法。from sklearn.metrics import mean_absolute_error, mean_absolute_percentage_error # 选取一段已知的完整数据作为测试床 test_period df[(df[datetime] 2014-06-01) (df[datetime] 2014-07-01)].copy() test_period.set_index(datetime, inplaceTrue) # 人工制造3天的缺失 mask_start 2014-06-10 mask_end 2014-06-13 test_period_missing test_period.copy() test_period_missing.loc[mask_start:mask_end, elec_cons] np.nan # 应用不同填充方法 test_period_missing[ffill] test_period_missing[elec_cons].ffill() test_period_missing[bfill] test_period_missing[elec_cons].bfill() test_period_missing[linear] test_period_missing[elec_cons].interpolate(methodlinear) test_period_missing[time] test_period_missing[elec_cons].interpolate(methodtime) # 简单均值填充通常效果最差仅作对比 test_period_missing[mean] test_period_missing[elec_cons].fillna(test_period[elec_cons].mean()) # 评估 results {} true_values test_period.loc[mask_start:mask_end, elec_cons] for method in [ffill, bfill, linear, time, mean]: imputed_values test_period_missing.loc[mask_start:mask_end, method] mae mean_absolute_error(true_values, imputed_values) mape mean_absolute_percentage_error(true_values, imputed_values) results[method] {MAE: mae, MAPE: mape} results_df pd.DataFrame(results).T print(results_df)在我的多次实践中对于具有趋势和季节性的数据如电力消耗时间插值time或线性插值linear通常是稳健的首选它们的MAE和MAPE往往最低。前向/后向填充会引入阶梯状伪影而均值填充完全忽略了时序结构应尽量避免。4.2 异常值检测与处理别让“坏点”带偏模型异常值可能是数据错误如传感器故障也可能是真实但罕见的事件如重大节日、停电。Isolation Forest是一种无监督算法特别适合时间序列因为它不假设数据的分布且对高维数据有效。from sklearn.ensemble import IsolationForest from sklearn.preprocessing import StandardScaler # 准备数据 values df[elec_cons].values.reshape(-1, 1) # 标准化非常重要因为Isolation Forest对尺度敏感 scaler StandardScaler() values_scaled scaler.fit_transform(values) # 训练Isolation Forest模型 # contamination参数是异常值比例的估计可根据领域知识调整例如设为0.011% iso_forest IsolationForest(contamination0.01, random_state42, n_jobs-1) outlier_labels iso_forest.fit_predict(values_scaled) # 标记异常点IsolationForest返回1为正常-1为异常 df[is_outlier] outlier_labels -1 print(f检测到异常点数量{df[is_outlier].sum()}) # 可视化异常点 fig px.scatter(df, xdatetime, yelec_cons, coloris_outlier, title电力消耗数据异常点检测 (Isolation Forest), color_discrete_map{False: blue, True: red}, opacity0.6) fig.show()检测到异常点后如何处理删除如果确认是数据错误且缺失比例很小直接删除是最干净的。修正Imputation如果异常点较多或处于关键时间位置删除会导致信息损失。可以采用类似处理缺失值的方法进行修正例如用前后点的线性插值替代或者用该小时在历史同期例如同星期几、同小时的中位数替代。# 方法用前后各12小时共24小时的均值替代异常值简单示例 df[elec_cons_cleaned] df[elec_cons].copy() outlier_indices df[df[is_outlier]].index for idx in outlier_indices: start max(0, idx - 12) end min(len(df), idx 13) # 包含后12个点 # 取窗口内非异常点的均值进行替换 window_values df.loc[start:end, elec_cons][~df.loc[start:end, is_outlier]] if not window_values.empty: df.at[idx, elec_cons_cleaned] window_values.mean() else: # 如果窗口内全是异常则用更全局的方法如该小时的历史中位数 hour_of_day df.at[idx, hour] df.at[idx, elec_cons_cleaned] df[df[hour] hour_of_day][elec_cons].median()核心原则处理异常值的逻辑必须符合业务常识。例如圣诞节当天的低用电量可能不是异常而是真实的季节性事件这类“异常”不应该被平滑掉而应作为特殊事件特征引入模型。5. 深入核心时间序列分解与平稳性检验5.1 时间序列分解拆解趋势、季节与残差经典的时间序列分解认为一个序列可以拆解为三个部分趋势Trend、季节性Seasonality和残差Residual或称不规则波动。分解有两种模式加法模型Y T S R和乘法模型Y T * S * R。当季节波动的幅度不随趋势水平变化时用加法模型反之用乘法模型。电力数据通常趋势增长夏季峰值也逐年增高可能更适合乘法模型但我们可以先尝试加法模型观察。statsmodels库提供了简单的经典分解from statsmodels.tsa.seasonal import seasonal_decompose # 选取一段数据频率设置为每日‘D’但我们的数据是每小时需要聚合或使用更高频设置 # 为了演示我们先按日聚合 df_daily df.set_index(datetime)[elec_cons_cleaned].resample(D).mean() # 进行加法模型分解假设年度季节性period365 result_add seasonal_decompose(df_daily, modeladditive, period365, extrapolate_trendfreq) result_add.plot() plt.show()对于更复杂的数据特别是存在多个季节性周期如小时、周、年时STLSeasonal-Trend decomposition using LOESS或MSTLMultiple STL是更强大的工具。它们使用鲁棒的局部加权回归进行平滑对异常值不敏感并能处理复杂的季节性形态。from statsmodels.tsa.seasonal import STL, MSTL # 使用STL分解指定季节性周期为24小时和16824*7周 # 注意STL/MSTL需要较高频率的原始数据这里我们用小时数据 df_hourly df.set_index(datetime)[elec_cons_cleaned].asfreq(H) # 确保频率规则 df_hourly_filled df_hourly.interpolate(methodtime) # 如有缺失需处理 # 由于MSTL计算量大我们选取一个子集例如一年 subset df_hourly_filled[2014-01-01:2014-12-31] # MSTL分解同时考虑日周期(24)和周周期(168) mstl_result MSTL(subset, periods(24, 168), stl_kwargs{robust: True}).fit() fig mstl_result.plot() plt.show()通过分解图你可以清晰地看到趋势成分缓慢上升日度季节性成分周期24显示白天高、夜晚低的规律周度季节性成分周期168显示工作日与周末的差异残差成分则看起来像是随机噪声。一个健康的残差应该是没有明显模式、近似白噪声的。如果残差中还有规律说明模型未能完全捕捉数据中的信息。5.2 平稳性检验时间序列建模的入场券大多数经典时间序列模型如ARIMA都要求数据是平稳的。平稳性意味着序列的统计特性如均值、方差不随时间变化。显然有趋势和季节性的序列是非平稳的。如何检验平稳性最常用的是增强迪基-富勒检验ADF Test。其原假设是“序列具有单位根即非平稳”。如果p值小于显著性水平如0.05我们拒绝原假设认为序列是平稳的。from statsmodels.tsa.stattools import adfuller adf_result adfuller(df_daily.dropna()) # 使用清洗后的日度数据 print(fADF Statistic: {adf_result[0]}) print(fp-value: {adf_result[1]}) print(Critical Values:) for key, value in adf_result[4].items(): print(f\t{key}: {value})对于电力数据ADF检验几乎肯定会给出一个很大的p值0.05表明序列非平稳。如何使序列平稳——差分Differencing。一阶差分可以消除线性趋势季节性差分可以消除季节性。# 一阶差分消除趋势 df_daily_diff1 df_daily.diff().dropna() # 季节性差分周期为7消除周效应通常用于更细粒度数据日数据可尝试周期7 df_daily_seasonal_diff df_daily.diff(periods7).dropna() # 对差分后的序列再次进行ADF检验 adf_result_diff adfuller(df_daily_diff1.dropna()) print(f一阶差分后序列的p-value: {adf_result_diff[1]})通常经过一阶或季节性差分后序列会变得平稳。pmdarima库的ndiffs函数可以自动建议最优的差分阶数。from pmdarima.arima import ndiffs # 自动检测需要几阶差分才能使序列平稳基于ADF检验 n_adf ndiffs(df_daily, testadf) print(f建议的差分阶数 (基于ADF检验): {n_adf})5.3 自相关与偏自相关分析洞察数据的内在记忆自相关函数ACF描述了一个时间序列与其自身滞后版本的相关性。偏自相关函数PACF则在控制了中间滞后项的影响后描述了序列与某一滞后项的直接相关性。它们是确定ARIMA模型参数(p, d, q)的关键工具。ACF图如果ACF缓慢衰减拖尾表明序列非平稳或具有长期记忆。如果ACF在滞后s, 2s, 3s...处出现峰值表明存在周期为s的季节性。PACF图PACF在滞后p之后突然截断降至不显著则提示AR模型的阶数可能为p。from statsmodels.graphics.tsaplots import plot_acf, plot_pacf # 对平稳化后的序列例如一阶差分后绘制ACF和PACF fig, axes plt.subplots(1, 2, figsize(12, 4)) plot_acf(df_daily_diff1.dropna(), lags40, axaxes[0]) plot_pacf(df_daily_diff1.dropna(), lags40, axaxes[1], methodywm) # 使用Yule-Walker方法 plt.show()如何解读对于差分后的日度电力数据ACF图可能会显示在滞后7、14等处有显著相关周季节性而PACF图可能在滞后1、2处显著然后截断。这提示我们可能需要一个包含AR项和季节性成分的模型比如SARIMA。6. 实战经验与避坑指南经过上面这些步骤你已经对时间序列数据完成了从外到内的“体检”。下面分享几个我踩过坑才总结出的经验数据频率与业务逻辑对齐你的分析频率必须匹配业务决策频率。如果你需要预测未来一小时的用电量却用日均数据建模就会丢失关键的日内模式。反之如果你只需要月度预算却用分钟级数据则会引入不必要的噪声和计算负担。在开始分析前务必明确业务问题。处理“大日期”当你的数据跨越多年特别是包含闰年时以“一年365天”作为季节性周期可能会产生微小偏差。对于精细预测可以考虑使用“一年365.25天”或直接使用傅里叶项Fourier terms来模拟季节性这比固定周期更灵活。警惕“未来信息泄露”这是时间序列预处理中最常见的错误。例如在计算整个数据集的滚动平均值或标准化减去均值、除以标准差时如果使用了未来的数据就会导致模型在训练时“偷看”到答案造成过于乐观的评估结果。正确的做法是仅使用历史信息。例如滚动平均应该用“向前滚动”rolling(min_periods1).mean()标准化应该在每个滚动窗口内独立计算。分解与预测的顺序一种常见的策略是“先分解后预测”。即先将序列分解为趋势、季节性和残差然后对相对平稳的残差序列进行建模预测最后将预测结果加回趋势和季节性成分。这种方法如Facebook Prophet的核心思想对于具有强且稳定季节性的序列非常有效。另一种策略是使用直接能处理趋势和季节性的模型如SARIMA或带有季节性特征的梯度提升树LightGBM, XGBoost。没有绝对的好坏需要根据数据特性和预测目标进行选择。可视化是永不落伍的调试工具无论模型多复杂永远要相信你的眼睛。将预测结果与真实值画在一起将残差序列画出来检查是否随机。一个模式明显的残差图就是在告诉你模型还有改进空间。我习惯在建模的每个关键步骤后都进行可视化这能帮我快速定位问题。至此我们已经完成了时间序列分析的第一部分——理解与准备。我们像剥洋葱一样通过可视化看到了数据的宏观形态通过预处理清洗了数据瑕疵通过分解理解了其内在结构并通过平稳性检验和自相关分析为建模做好了准备。这些步骤虽然繁琐但至关重要。一个建立在错误理解数据基础上的复杂模型其预测能力往往还不如一个建立在深刻理解基础上的简单模型。在下一部分我们将基于这些分析着手构建预测模型将理解转化为预见未来的能力。