2、时间序列数据预处理:缺失值处理、异常值检测与处理、时间戳规范化、重采样与对齐
说实话,做时间序列分析这么多年,我踩过最大的坑往往不是模型选错了,而是数据预处理没做好。你想想看,原始数据就像刚从菜市场买回来的鱼——鳞没刮、内脏没掏,直接下锅能好吃吗?
这一章,咱们就把「刮鳞掏内脏」的功夫练扎实。我会结合自己实际项目中的血泪教训,把缺失值、异常值、时间戳乱码、重采样这几个硬骨头啃下来。
2.1 缺失值处理:别让「空」毁了你的模型
我在做电力负荷预测时遇到过这么个事:某天的数据因为传感器故障,连续缺失了6个小时。如果直接删掉,模型就学不到那天的夜间模式;如果随便填个0,模型又会被带偏。嗯,这里要分情况讨论。
2.1.1 缺失值类型与检测
先搞清楚缺失值长什么样。常见的有三种:
- 完全随机缺失(MCAR):比如有人填问卷时手滑跳过了某题。这种最「善良」,直接删掉影响不大。
- 随机缺失(MAR):缺失跟其他变量有关。比如高收入人群更不愿意透露收入——这跟收入本身有关,但跟「是否缺失」这个行为有关。
- 非随机缺失(MNAR):缺失值本身就有信息量。比如温度计在超过50度时烧坏了——缺失本身就意味着「高温」。
检测方法很简单,用 isnull() 看一眼就行。但我个人习惯先画个热力图:
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
# 假设 df 是你的时间序列数据
plt.figure(figsize=(10, 6))
sns.heatmap(df.isnull(), cbar=False, cmap='viridis')
plt.title('缺失值分布热力图')
plt.show()
2.1.2 处理方法:从简单到复杂
我一般按这个优先级来选:
- 直接删除:缺失比例小于5%,且是随机缺失。用
dropna()搞定。 - 向前/向后填充:适合缓慢变化的指标,比如气温、水位。用
ffill()或bfill()。 - 线性插值:适合趋势明显的数据。比如股票价格,用
interpolate(method='linear')。 - 时间序列插值:考虑时间间隔的插值。比如
interpolate(method='time')。 - 模型预测填充:用 ARIMA 或 Prophet 预测缺失值。这是最后的手段,计算量大,但精度高。
举个例子,我处理过一组传感器数据,缺失了大约20%的值。直接删掉太浪费,向前填充又太粗糙。最后用了时间插值:
# 时间序列插值示例
df['value'] = df['value'].interpolate(method='time')
# 如果数据有周期性,可以用 spline 插值
df['value'] = df['value'].interpolate(method='spline', order=3)
2.2 异常值检测与处理:揪出那些「捣蛋鬼」
异常值这东西,有时候是宝贝(比如发现了一次网络攻击),有时候是垃圾(比如传感器误报)。怎么区分?我总结了三个步骤。
2.2.1 检测方法
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Z-Score | 正态分布数据 | 简单快速 | 对非正态分布不友好 |
| IQR(四分位距) | 任意分布 | 稳健,不受极端值影响 | 对尾部数据不敏感 |
| 移动平均法 | 时间序列 | 考虑时间上下文 | 需要调窗口大小 |
| 孤立森林 | 高维数据 | 无监督,效果好 | 计算量大 |
我个人最常用的是 IQR 方法,因为它不假设数据分布。你看:
def detect_outliers_iqr(data, column):
Q1 = data[column].quantile(0.25)
Q3 = data[column].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = data[(data[column] < lower_bound) | (data[column] > upper_bound)]
return outliers
# 使用示例
outliers = detect_outliers_iqr(df, 'value')
print(f'检测到 {len(outliers)} 个异常值')
2.2.2 处理方法
检测出来之后怎么处理?我一般按这个顺序判断:
- 确认是否为真实异常:比如突然的流量峰值,可能是促销活动导致的,那就保留。
- 如果是噪声:用中位数或前后均值替换。别用均值,均值会被异常值本身拉偏。
- 如果是传感器故障:直接删除,或者用缺失值处理方法填充。
避坑指南: 我曾经在处理金融数据时,把所有超过3倍标准差的值都删了。结果发现那些「异常值」其实是市场崩盘时的真实交易数据。删完之后,模型永远学不会极端行情。所以,先理解业务,再动手删。
2.3 时间戳规范化:让时间「说人话」
你遇到过这种情况吗?同一个数据集里,有的时间是 "2024-01-01",有的是 "01/01/2024",还有的是 "2024年1月1日"。嗯,这就是时间戳没规范化的后果。
我习惯一上来就把所有时间列转成 datetime 类型:
# 统一转换时间戳
df['timestamp'] = pd.to_datetime(df['timestamp'], format='mixed')
# 如果知道具体格式,可以指定 format 提高速度
df['timestamp'] = pd.to_datetime(df['timestamp'], format='%Y-%m-%d %H:%M:%S')
然后设置成索引,方便后续操作:
df.set_index('timestamp', inplace=True)
# 确保索引是单调递增的
df.sort_index(inplace=True)
tz_convert('UTC') 可以搞定。
2.4 重采样与对齐:把数据「对齐」到同一个节拍
重采样说白了就是改变数据的时间频率。比如把秒级数据聚合成分钟级,或者把日数据下采样到小时级。
2.4.1 降采样(高频→低频)
降采样时,你要决定怎么聚合。常用的有:
- 均值:适合平稳过程,比如温度。
- 求和:适合累积量,比如销售额。
- OHLC:金融数据专用,开盘、最高、最低、收盘。
# 将分钟数据重采样为小时数据,取均值
df_hourly = df.resample('H').mean()
# 如果是交易数据,用 OHLC
df_ohlc = df.resample('H').ohlc()
2.4.2 升采样(低频→高频)
升采样需要插值。我一般用向前填充,因为不会引入未来信息:
# 将日数据升采样为小时数据,向前填充
df_hourly = df.resample('H').ffill()
# 或者用线性插值
df_hourly = df.resample('H').interpolate(method='linear')
bfill(),那会用到未来的数据。在时间序列里,这叫「未来信息泄露」,模型会过拟合得一塌糊涂。
2.4.3 对齐:让多个时间序列「步调一致」
做多变量分析时,不同传感器可能采样频率不同。这时候要对齐到同一个时间轴上:
# 将两个时间序列对齐到分钟级
df1_aligned = df1.resample('T').mean()
df2_aligned = df2.resample('T').mean()
# 合并
df_combined = pd.concat([df1_aligned, df2_aligned], axis=1)
# 删除对齐后仍然缺失的行
df_combined.dropna(inplace=True)
知识体系总览
下面这张图,是我自己总结的数据预处理全流程。你看一眼,心里就有谱了:
你看,整个流程就像一条流水线。原始数据进来,先处理缺失值,再揪出异常值,最后把时间戳对齐到统一频率。每一步都环环相扣,漏掉一环,后面模型就白搭。
好了,数据预处理这块就聊到这儿。记住一句话:预处理花的时间,会在模型训练阶段加倍还给你。别嫌麻烦,慢慢来,比较快。