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)
避坑指南: 我曾经在一个项目里,连续缺失了 10 个点,用了高阶多项式插值(order=5),结果插出来的值直接飞到了 100 多,而正常值只有 20 左右。高阶多项式容易过拟合,产生「龙格现象」。记住:缺失越多,插值方法越要保守。连续缺失超过 5 个点,我建议直接删除这段数据。

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 基于移动窗口的方法

时间序列有个特点:相邻时间点的值通常很接近。所以,我们可以用滑动窗口来检测异常。

具体做法是:

  1. 设定一个窗口大小,比如 5 个点
  2. 计算窗口内的均值 μ_w 和标准差 σ_w
  3. 如果当前点超出 [μ_w - k*σ_w, μ_w + k*σ_w],标记为异常

k 值一般取 2 或 3。我习惯取 2.5,比较折中。

为什么用移动窗口? 因为数据可能有趋势或季节性。比如,夏天温度高,冬天温度低。全局的 3σ 会把夏天的正常高温误判为异常。但移动窗口只关注局部,能更好地适应数据变化。
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 个点的均值
  • 用插值法替换:把异常点当作缺失值,用线性插值填充
  • 用中位数替换:对极端值不敏感,适合有多个异常的情况
我的习惯: 如果异常值数量很少(少于 1%),我会用前后均值替换。如果异常值较多(5% 左右),我会先用中位数替换,然后再做一次检测,确保没有漏网之鱼。记住:修正比删除更安全,因为保留了时间序列的长度和顺序。
# 用前后均值修正异常值
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 本章知识体系

为了让你更直观地理解,我画了一张流程图,展示数据预处理的完整逻辑:

时间序列数据预处理流程 原始时间序列 缺失值处理 前向填充 | 线性插值 | 样条插值 异常值检测 3σ原则 | Z-score | 移动窗口 异常值修正 前后均值 | 插值替换 | 中位数替换

这张图很清楚地展示了流程:原始数据 → 缺失值处理 → 异常值检测 → 异常值修正。每一步都环环相扣,缺一不可。

好了,这一章的内容就到这里。数据预处理虽然枯燥,但它是建模的基石。你想想看,地基没打好,房子能稳吗?下一章,咱们会进入更核心的内容——时间序列分解。到时候见。


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