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 处理方法:从简单到复杂

我一般按这个优先级来选:

  1. 直接删除:缺失比例小于5%,且是随机缺失。用 dropna() 搞定。
  2. 向前/向后填充:适合缓慢变化的指标,比如气温、水位。用 ffill()bfill()
  3. 线性插值:适合趋势明显的数据。比如股票价格,用 interpolate(method='linear')
  4. 时间序列插值:考虑时间间隔的插值。比如 interpolate(method='time')
  5. 模型预测填充:用 ARIMA 或 Prophet 预测缺失值。这是最后的手段,计算量大,但精度高。

举个例子,我处理过一组传感器数据,缺失了大约20%的值。直接删掉太浪费,向前填充又太粗糙。最后用了时间插值:

# 时间序列插值示例
df['value'] = df['value'].interpolate(method='time')
# 如果数据有周期性,可以用 spline 插值
df['value'] = df['value'].interpolate(method='spline', order=3)
注意: 千万别在缺失值超过50%的列上做插值!我曾经犯过这个错,结果插出来的数据比原始数据还「漂亮」,但模型一跑就崩。说白了,数据质量太差时,宁可删列也别硬填。

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)
小经验: 如果数据跨时区,记得统一转成 UTC。我有个项目因为没处理时区,结果模型在夏令时切换那天直接崩了。用 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)

知识体系总览

下面这张图,是我自己总结的数据预处理全流程。你看一眼,心里就有谱了:

时间序列数据预处理流程 原始时间序列 第一步:缺失值处理 删除 / 填充 / 插值 模型预测填充 第二步:异常值检测与处理 IQR / Z-Score / 移动平均 孤立森林 / 业务规则 第三步:时间戳规范化 + 重采样

你看,整个流程就像一条流水线。原始数据进来,先处理缺失值,再揪出异常值,最后把时间戳对齐到统一频率。每一步都环环相扣,漏掉一环,后面模型就白搭。

好了,数据预处理这块就聊到这儿。记住一句话:预处理花的时间,会在模型训练阶段加倍还给你。别嫌麻烦,慢慢来,比较快。

专注资料整理