4. 数据预处理:缺失值处理与异常值修正
大家好,欢迎来到第四章。
说实话,在时间序列分析里,数据预处理这块儿,我吃过不少亏。刚入行那会儿,拿到数据就急着建模,结果模型跑出来一塌糊涂。后来才明白,数据预处理占整个项目时间的 70% 以上,一点都不夸张。
这一章,咱们就聊聊两个最头疼的问题:缺失值和异常值。你想想看,现实世界的数据哪有那么完美?传感器坏了、网络断了、人为录入错误……这些情况太常见了。怎么处理,直接决定了模型的上限。
4.1 缺失值处理:别让空值毁了你的模型
缺失值,说白了就是数据里出现了「窟窿」。在时间序列里,这个窟窿尤其要命,因为时间是有顺序的,你不能随便填。
4.1.1 前向填充(Forward Fill)
我个人习惯,在遇到短期、非周期性的缺失时,首选前向填充。什么意思?就是用上一个有效值,填充当前缺失值。
举个例子,你监控服务器 CPU 使用率,每 5 秒采集一次。第 10 秒的数据丢了,那第 10 秒的值就用第 5 秒的值代替。为什么?因为 CPU 使用率在短时间内变化不大,用上一个值是最合理的猜测。
- 高频采样数据(秒级、分钟级)
- 缺失时间较短(连续缺失不超过 3 个点)
- 数据变化平缓,没有突变
import pandas as pd
import numpy as np
# 模拟数据
dates = pd.date_range('2024-01-01', periods=10, freq='H')
values = [10, 12, np.nan, 15, np.nan, np.nan, 18, 20, 19, 21]
ts = pd.Series(values, index=dates)
# 前向填充
ts_ffill = ts.ffill()
print(ts_ffill)
嗯,这里要注意:前向填充有个坑。如果缺失值出现在序列开头,那就没法填充了。我曾经在一个项目里,前 3 个数据全是空值,直接 ffill 结果全是 NaN,模型直接报错。所以,开头缺失要先处理,要么删除,要么用全局均值填充。
4.1.2 插值法(Interpolation)
如果缺失值比较多,或者数据有明显的趋势,前向填充就不太够用了。这时候,我推荐用插值法。
插值法,说白了就是「猜」得更聪明一点。它利用缺失点前后的数据,拟合一条曲线,然后算出缺失点的值。
常用的插值方法有:
| 方法 | 原理 | 适用场景 |
|---|---|---|
| 线性插值 | 用前后两个点的直线连接 | 数据变化均匀,无明显波动 |
| 多项式插值 | 用多项式拟合多个点 | 数据有弯曲趋势,但不过度复杂 |
| 样条插值 | 分段低次多项式,平滑连接 | 数据波动较大,需要平滑曲线 |
| 时间插值 | 考虑时间间隔,按时间权重填充 | 时间间隔不均匀的数据 |
# 线性插值
ts_linear = ts.interpolate(method='linear')
print(ts_linear)
# 样条插值(需要安装 scipy)
ts_spline = ts.interpolate(method='spline', order=2)
print(ts_spline)
4.2 异常值检测与修正:揪出那些「捣乱分子」
异常值,就是那些明显偏离正常范围的数据点。比如,温度传感器突然跳到了 1000 度,或者股票价格瞬间跌到 0。这些点如果不处理,模型会被它们带偏。
4.2.1 基于统计的方法
最简单粗暴的方法,就是看数据分布。我常用的是 3σ 原则:
- 计算数据的均值 μ 和标准差 σ
- 任何超出 [μ - 3σ, μ + 3σ] 的点,都视为异常
这个方法对正态分布的数据很有效。但现实数据往往不是正态的,所以我会先用 Z-score 标准化一下。
from scipy import stats
# 计算 Z-score
z_scores = np.abs(stats.zscore(ts.dropna()))
threshold = 3
outliers = np.where(z_scores > threshold)[0]
print(f"异常值索引: {outliers}")
嗯,这里要注意:Z-score 对极端值很敏感。如果数据本身就有多个异常,均值和标准差会被拉偏,导致漏检。所以,我一般会先用 IQR(四分位距) 方法做第一轮筛选。
4.2.2 基于移动窗口的方法
时间序列有个特点:相邻时间点的值通常很接近。所以,我们可以用滑动窗口来检测异常。
具体做法是:
- 设定一个窗口大小,比如 5 个点
- 计算窗口内的均值 μ_w 和标准差 σ_w
- 如果当前点超出 [μ_w - k*σ_w, μ_w + k*σ_w],标记为异常
k 值一般取 2 或 3。我习惯取 2.5,比较折中。
def detect_outliers_moving_window(series, window=5, k=2.5):
outliers = []
for i in range(len(series)):
start = max(0, i - window)
end = min(len(series), i + window + 1)
window_data = series.iloc[start:end].dropna()
if len(window_data) < 3:
continue
mean = window_data.mean()
std = window_data.std()
if abs(series.iloc[i] - mean) > k * std:
outliers.append(i)
return outliers
outlier_idx = detect_outliers_moving_window(ts)
print(f"移动窗口检测到的异常索引: {outlier_idx}")
4.2.3 异常值修正:不是删掉就完事了
检测到异常值后,怎么处理?很多人直接删掉。但时间序列里,删掉一个点会破坏时间连续性。我建议用以下方法修正:
- 用前后均值替换:取异常点前后各 2 个点的均值
- 用插值法替换:把异常点当作缺失值,用线性插值填充
- 用中位数替换:对极端值不敏感,适合有多个异常的情况
# 用前后均值修正异常值
def fix_outliers(series, outlier_idx):
fixed = series.copy()
for idx in outlier_idx:
if idx == 0 or idx == len(series) - 1:
continue
prev_val = series.iloc[idx - 1]
next_val = series.iloc[idx + 1]
fixed.iloc[idx] = (prev_val + next_val) / 2
return fixed
ts_fixed = fix_outliers(ts, outlier_idx)
print(ts_fixed)
4.3 本章知识体系
为了让你更直观地理解,我画了一张流程图,展示数据预处理的完整逻辑:
这张图很清楚地展示了流程:原始数据 → 缺失值处理 → 异常值检测 → 异常值修正。每一步都环环相扣,缺一不可。
好了,这一章的内容就到这里。数据预处理虽然枯燥,但它是建模的基石。你想想看,地基没打好,房子能稳吗?下一章,咱们会进入更核心的内容——时间序列分解。到时候见。
公众号:蓝海资料掘金营,微信deep3321