第二章:时间序列基础与可视化

各位好,我是老张。在电力系统干了十几年,每天打交道最多的就是负荷数据。说实话,刚入行那会儿,面对一堆时间戳和数字,我头都是大的。后来慢慢摸出门道——时间序列处理,说白了就是跟「时间」这个维度谈恋爱,你得懂它的脾气。

这一章,我们聊聊Pandas的时间序列索引、重采样、滚动统计,还有怎么用Matplotlib和Plotly把负荷曲线画得漂漂亮亮。嗯,这些都是基本功,但也是后面所有预测模型的基石。

核心知识点速览:

  • Pandas时间序列索引:让数据带上时间标签
  • Resample重采样:把数据从高频变低频,或反过来
  • 滚动统计:滑动窗口里的平均值、最大值
  • Matplotlib/Plotly可视化:静态图与交互图的取舍
  • 季节性分解:把负荷拆成趋势、季节、残差三部分
时间序列基础与可视化知识体系 时间序列处理 Pandas时间索引 Resample重采样 滚动统计 Matplotlib/Plotly DatetimeIndex 时间戳切片 降采样(聚合) 升采样(插值) rolling()窗口 均值/标准差 静态图 交互图 最终目标:季节性分解

2.1 Pandas时间序列索引:给数据贴上时间标签

我个人习惯,拿到负荷数据第一件事,就是把时间列转成Pandas的DatetimeIndex。为什么?因为只有索引是时间类型,你才能用切片、重采样这些神器。

import pandas as pd
import numpy as np

# 模拟负荷数据:2024年1月,每15分钟一个点
date_rng = pd.date_range(start='2024-01-01', end='2024-01-31 23:45:00', freq='15T')
load_data = np.random.normal(loc=500, scale=50, size=len(date_rng)) + 100*np.sin(np.arange(len(date_rng))*2*np.pi/(96*7))

df = pd.DataFrame(load_data, index=date_rng, columns=['load'])
print(df.head())

你看,索引变成了DatetimeIndex,这时候想取某一天的数据,直接写 df['2024-01-15'] 就行。我在项目中遇到过,有人用字符串匹配去筛选时间,效率低不说,还容易出错。用时间索引切片,又快又准。

小技巧: 如果你的原始数据时间列是字符串,用 pd.to_datetime() 转换,然后 set_index() 设置索引。我一般这么写:df.index = pd.to_datetime(df['time_column'])

2.2 Resample重采样:数据频率的魔术师

实际工作中,负荷数据来源五花八门。SCADA系统可能是15分钟一个点,电表可能是每小时一个点,气象数据可能是每天一个点。你想把它们放一起分析?必须统一频率。

重采样分两种:降采样和升采样。

操作 说明 常用方法 示例
降采样 高频→低频(如15分钟→1小时) mean(), sum(), max() df.resample('1H').mean()
升采样 低频→高频(如1小时→15分钟) ffill(), bfill(), interpolate() df.resample('15T').ffill()
# 降采样:15分钟数据聚合成小时级
hourly_load = df.resample('1H').mean()
print(hourly_load.head())

# 升采样:小时级数据插值到15分钟
# 先模拟小时数据
hourly_data = df.resample('1H').mean()
# 再升采样
ffill_data = hourly_data.resample('15T').ffill()  # 向前填充
interp_data = hourly_data.resample('15T').interpolate()  # 线性插值

嗯,这里要注意:降采样时,聚合函数的选择很关键。算平均负荷用mean,算峰值负荷用max,算总用电量用sum。我曾经犯过一个错,用mean去算日最大负荷,结果偏小很多,后来被领导骂了一顿。

避坑指南: 升采样时,ffill(向前填充)会引入滞后,interpolate(线性插值)更平滑但可能产生不合理值。我个人建议,如果数据变化平缓用interpolate,如果数据有突变(比如负荷跳变)用ffill更安全。

2.3 滚动统计:滑动窗口里的秘密

滚动统计,说白了就是在一个滑动窗口里算平均值、最大值、标准差。这玩意儿在负荷预测里太常用了——比如用过去7天的平均负荷来预测今天的负荷。

# 7天滚动平均(每天96个15分钟点,7天=672个点)
df['load_7d_avg'] = df['load'].rolling(window=672).mean()
df['load_7d_std'] = df['load'].rolling(window=672).std()

# 查看结果
print(df[['load', 'load_7d_avg', 'load_7d_std']].head(700))

你想想看,滚动窗口的大小怎么选?太小了噪声大,太大了反应迟钝。我一般先画个图看看,选一个能让曲线既平滑又不失主要特征的窗口。比如负荷数据,7天窗口能消除日周期性,30天窗口能消除周周期性。

经验之谈: 滚动统计的边界处理要注意。默认情况下,前N-1个数据点是NaN。可以用 min_periods=1 让前面也有值,但精度会差一些。我在做实时预测时,通常用 min_periods=int(window*0.7),既保证数据量,又不让起始段空太多。

2.4 Matplotlib/Plotly绘制负荷曲线

画图是检验数据质量的照妖镜。我拿到新数据,第一件事就是画个全貌图。Matplotlib适合做静态报告,Plotly适合做交互式探索。

2.4.1 Matplotlib:经典静态图

import matplotlib.pyplot as plt

plt.figure(figsize=(12, 6))
plt.plot(df.index[:1000], df['load'][:1000], label='原始负荷', color='#3498db', linewidth=0.8)
plt.plot(df.index[:1000], df['load_7d_avg'][:1000], label='7天滚动平均', color='#e74c3c', linewidth=1.5)
plt.title('负荷曲线与滚动平均(前1000个点)', fontsize=14)
plt.xlabel('时间')
plt.ylabel('负荷 (MW)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Matplotlib的好处是出图稳定,适合写报告。但缺点是不能交互,想看某个点的具体数值得自己猜。

2.4.2 Plotly:交互式探索

import plotly.graph_objects as go

fig = go.Figure()
fig.add_trace(go.Scatter(x=df.index[:1000], y=df['load'][:1000], 
                         mode='lines', name='原始负荷', line=dict(color='#3498db', width=1)))
fig.add_trace(go.Scatter(x=df.index[:1000], y=df['load_7d_avg'][:1000], 
                         mode='lines', name='7天滚动平均', line=dict(color='#e74c3c', width=2)))

fig.update_layout(title='负荷曲线交互图', xaxis_title='时间', yaxis_title='负荷 (MW)')
fig.show()

Plotly的交互功能太强了——鼠标悬停显示数值、缩放、平移、下载图片。我在做项目汇报时,经常用Plotly生成HTML文件,领导可以自己拖拽看细节。

我的习惯: 探索阶段用Plotly,出报告用Matplotlib。Plotly的图可以保存为HTML,直接发给客户,他们打开就能交互,不用装Python。

2.5 季节性分解:把负荷拆开看

负荷数据有三个成分:趋势(长期变化)、季节性(日/周/年周期)、残差(随机波动)。季节性分解就是把这仨拆开,方便我们分别建模。

from statsmodels.tsa.seasonal import seasonal_decompose

# 先降采样到小时级,减少计算量
hourly = df['load'].resample('1H').mean()
# 填充缺失值
hourly = hourly.fillna(method='ffill')

# 季节性分解:周期设为24小时(一天)
result = seasonal_decompose(hourly, model='additive', period=24)

# 提取成分
trend = result.trend
seasonal = result.seasonal
residual = result.resid

# 画图
plt.figure(figsize=(12, 10))
plt.subplot(4, 1, 1)
plt.plot(hourly.index, hourly, label='原始', color='#3498db')
plt.legend()
plt.subplot(4, 1, 2)
plt.plot(trend.index, trend, label='趋势', color='#e67e22')
plt.legend()
plt.subplot(4, 1, 3)
plt.plot(seasonal.index, seasonal, label='季节性', color='#2ecc71')
plt.legend()
plt.subplot(4, 1, 4)
plt.plot(residual.index, residual, label='残差', color='#e74c3c')
plt.legend()
plt.tight_layout()
plt.show()

为什么用加法模型?因为负荷的季节性幅度基本不随趋势变化。如果季节性幅度随趋势变大(比如夏天用电高峰时波动也大),那就用乘法模型。我一般先试加法,如果残差有明显模式再换乘法。

注意: 季节性分解要求数据没有缺失值。如果数据有空洞,先用ffill或interpolate补上。另外,周期参数要设对——日周期是24(小时级),周周期是168(24*7)。设错了,分解出来的季节性就是一团乱麻。

好了,这一章的内容就这些。时间序列索引是基础,重采样是工具,滚动统计是平滑利器,可视化是检验手段,季节性分解是深入理解数据的钥匙。把这些玩熟了,后面做预测模型就顺手多了。


公众号:蓝海资料掘金营,微信deep3321