3. 数据预处理基础:加载时间序列数据、处理缺失值、数据归一化与标准化

各位同学,欢迎来到第三章。

说实话,很多搞深度学习的同学,一上来就急着调模型、调参数。但我得泼盆冷水——数据预处理没做好,后面全是白搭。我在工业项目里见过太多次了,模型结构一模一样,就因为数据预处理差了那么一点点,效果天差地别。

这一章,我们就来啃下时间序列预测的“地基工程”。

核心观点: 时间序列数据预处理,不是简单的“填个空、归一化”就完事了。它涉及到时序依赖性的保持、未来信息的泄露防范,以及数值稳定性。每一步,都有坑。

时间序列数据预处理核心流程 原始时间序列 加载与解析 pandas.read_csv() 缺失值处理 插值 / 填充 归一化/标准化 MinMax / Z-score ↓ 关键注意事项 ↓ ⚠️ 防止未来信息泄露 归一化参数只能用 训练集计算 ⚠️ 保持时序顺序 不能随机打乱数据 必须按时间窗口切分 ⚠️ 缺失值处理策略 连续缺失 vs 离散缺失 处理方式不同

3.1 加载时间序列数据——别小看这一步

加载数据,听起来简单吧?pd.read_csv() 一行代码搞定。但我在实际项目中踩过坑,分享给你。

首先,时间序列数据通常长这样:

import pandas as pd
import numpy as np

# 加载数据
df = pd.read_csv('stock_prices.csv')

# 看一眼
print(df.head())

输出大概是这样:

dateopenhighlowclosevolume
2023-01-01100.5102.099.8101.21500000
2023-01-02101.0103.5100.2102.81800000
2023-01-03102.5104.0101.0103.52100000

我的习惯: 加载后第一件事,把日期列转成 datetime 类型,并设为索引。这样后续的切片、重采样会方便很多。

# 日期处理
df['date'] = pd.to_datetime(df['date'])
df.set_index('date', inplace=True)

# 检查时间间隔是否均匀
print(df.index.diff().value_counts())

为什么要检查时间间隔?因为很多模型假设数据是等间隔采样的。如果发现间隔不均匀,你可能需要重采样。

3.2 处理缺失值——最容易被忽视的坑

时间序列里的缺失值,跟普通表格数据不一样。你不能随便删,也不能随便填。

为什么? 因为时序数据有前后依赖关系。你删掉一行,时间轴就断了。你填错了,模型学到的是假规律。

3.2.1 先看看缺失值长什么样

# 检查缺失值
print(df.isnull().sum())

# 可视化缺失位置
import matplotlib.pyplot as plt
plt.figure(figsize=(12, 4))
plt.plot(df.index, df['close'].isnull().astype(int))
plt.title('缺失值分布')
plt.show()

3.2.2 处理策略——我常用的三种方法

缺失类型推荐方法适用场景
单个离散缺失线性插值数据变化平缓
连续小段缺失前向填充数据有惯性
连续大段缺失删除该段缺失超过总长度10%
# 方法1:线性插值(我最常用的)
df['close'] = df['close'].interpolate(method='linear')

# 方法2:前向填充(适合有惯性的数据)
df['close'] = df['close'].fillna(method='ffill')

# 方法3:删除大段缺失
df = df.dropna(subset=['close'])

我曾经踩过的坑: 有一次处理电力负荷数据,连续缺失了3天。我用插值填上了,结果模型预测那几天的负荷时完全不准。后来发现,那3天是节假日,负荷模式跟平时完全不同。所以,缺失值处理一定要结合业务背景,不能无脑插值。

3.3 数据归一化与标准化——让模型学得更快

为什么要做归一化?你想想看,LSTM 里用的激活函数(比如 tanh、sigmoid),输入范围太大或太小,梯度就容易消失或爆炸。

归一化,说白了就是把数据压到一个固定的范围里。标准化,则是让数据符合标准正态分布。

3.3.1 两种主流方法

方法公式输出范围适用场景
Min-Max 归一化(x - min) / (max - min)[0, 1]数据有明确边界
Z-score 标准化(x - mean) / std无固定范围数据有异常值
from sklearn.preprocessing import MinMaxScaler, StandardScaler

# Min-Max 归一化
scaler_minmax = MinMaxScaler(feature_range=(0, 1))
df_scaled = scaler_minmax.fit_transform(df[['close']])

# Z-score 标准化
scaler_std = StandardScaler()
df_standardized = scaler_std.fit_transform(df[['close']])

3.3.2 最重要的一个原则——防止未来信息泄露

这一点,我反复跟团队强调。归一化的参数(min、max、mean、std)只能用训练集计算,然后应用到验证集和测试集上。

为什么?因为测试集代表未来的数据。如果你用全局的 min 和 max 来归一化,相当于模型提前“看到”了未来的最大值,这会导致预测结果虚高。

# 正确的做法
train_size = int(len(df) * 0.8)
train_data = df.iloc[:train_size]
test_data = df.iloc[train_size:]

# 只用训练集拟合 scaler
scaler = MinMaxScaler()
scaler.fit(train_data[['close']])

# 分别转换
train_scaled = scaler.transform(train_data[['close']])
test_scaled = scaler.transform(test_data[['close']])

核心要点: 先切分,再归一化。顺序不能错。

3.4 完整的数据预处理流水线

好了,我们把上面讲的串起来,写一个完整的预处理函数。这是我个人习惯的写法,你可以直接拿去用。

def preprocess_time_series(df, target_col='close', test_ratio=0.2):
    """
    时间序列数据预处理流水线
    """
    # 1. 确保日期索引
    if 'date' in df.columns:
        df['date'] = pd.to_datetime(df['date'])
        df.set_index('date', inplace=True)
    
    # 2. 处理缺失值(线性插值)
    df[target_col] = df[target_col].interpolate(method='linear')
    
    # 3. 切分训练集和测试集
    split_idx = int(len(df) * (1 - test_ratio))
    train_data = df.iloc[:split_idx]
    test_data = df.iloc[split_idx:]
    
    # 4. 归一化(只拟合训练集)
    scaler = MinMaxScaler()
    scaler.fit(train_data[[target_col]])
    
    train_scaled = scaler.transform(train_data[[target_col]])
    test_scaled = scaler.transform(test_data[[target_col]])
    
    return train_scaled, test_scaled, scaler

# 使用示例
train, test, scaler = preprocess_time_series(df)
print(f'训练集形状: {train.shape}')
print(f'测试集形状: {test.shape}')

3.5 本章小结——记住这三件事

  • 加载数据时:检查时间索引、检查间隔是否均匀
  • 处理缺失值时:结合业务背景,不要无脑插值
  • 归一化时:先切分,再拟合,防止未来信息泄露

嗯,数据预处理这块,看起来都是些基础操作,但恰恰是这些基础操作决定了模型的上限。我见过太多人在这上面翻车了。

下一章,我们会把这些预处理好的数据,转换成 LSTM 能吃的序列样本。到时候你就知道,前面这些功夫没白费。


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