3. 数据预处理基础:加载时间序列数据、处理缺失值、数据归一化与标准化
各位同学,欢迎来到第三章。
说实话,很多搞深度学习的同学,一上来就急着调模型、调参数。但我得泼盆冷水——数据预处理没做好,后面全是白搭。我在工业项目里见过太多次了,模型结构一模一样,就因为数据预处理差了那么一点点,效果天差地别。
这一章,我们就来啃下时间序列预测的“地基工程”。
核心观点: 时间序列数据预处理,不是简单的“填个空、归一化”就完事了。它涉及到时序依赖性的保持、未来信息的泄露防范,以及数值稳定性。每一步,都有坑。
3.1 加载时间序列数据——别小看这一步
加载数据,听起来简单吧?pd.read_csv() 一行代码搞定。但我在实际项目中踩过坑,分享给你。
首先,时间序列数据通常长这样:
import pandas as pd
import numpy as np
# 加载数据
df = pd.read_csv('stock_prices.csv')
# 看一眼
print(df.head())
输出大概是这样:
| date | open | high | low | close | volume |
|---|---|---|---|---|---|
| 2023-01-01 | 100.5 | 102.0 | 99.8 | 101.2 | 1500000 |
| 2023-01-02 | 101.0 | 103.5 | 100.2 | 102.8 | 1800000 |
| 2023-01-03 | 102.5 | 104.0 | 101.0 | 103.5 | 2100000 |
我的习惯: 加载后第一件事,把日期列转成 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