3、数据加载与预处理:使用Pandas读取CSV/Excel时间序列数据、处理缺失值、重采样与对齐时间索引
好,咱们进入实战环节。数据加载和预处理,说白了就是给模型「喂饭」。饭要是没洗干净、切得不规整,模型再厉害也学不出好东西。我这些年踩过的坑,十有八九都出在这一步。
3.1 用Pandas读取时间序列数据
Pandas读取CSV和Excel,基础操作大家都会。但时间序列数据有个特殊要求——你得告诉Pandas:「嘿,这一列是时间!」
我个人习惯用 parse_dates 参数,一步到位:
import pandas as pd
# 读取CSV,指定时间列
df = pd.read_csv('sensor_data.csv', parse_dates=['timestamp'], index_col='timestamp')
# 读取Excel,同样处理
df_excel = pd.read_excel('factory_data.xlsx', parse_dates=['datetime'], index_col='datetime')
# 看一眼数据长啥样
print(df.head())
print(df.info()) # 确认时间列是datetime类型
这里有个细节——index_col='timestamp'。把时间列设为索引,后续所有时间操作都会方便很多。我在项目中遇到过有人不设索引,结果重采样时各种报错,折腾了半天。
pd.to_datetime() 手动转换,指定 format 参数能提速不少。
3.2 处理缺失值——别急着删
真实数据几乎没有完美的。传感器掉线、网络波动、存储异常……缺失值无处不在。你想想看,要是直接删掉,时间序列就断了,后续的滑动窗口、差分计算全乱套。
我一般分三步走:
- 先看看缺失情况——用
isnull().sum()统计 - 判断缺失模式——是随机缺失,还是连续一大段?
- 选择合适的填充方法
# 第一步:统计缺失
print(df.isnull().sum())
# 第二步:看看缺失位置
missing_idx = df[df['value'].isnull()].index
print(f"缺失时间点:{missing_idx[:10]}") # 只看前10个
# 第三步:填充
# 方法1:前向填充(适合传感器短暂掉线)
df['value'].fillna(method='ffill', inplace=True)
# 方法2:线性插值(适合缓慢变化的指标)
df['value'].interpolate(method='linear', inplace=True)
# 方法3:时间加权插值(更精确)
df['value'].interpolate(method='time', inplace=True)
我个人最常用的是 interpolate(method='time')。它根据时间间隔做线性插值,比普通线性插值更合理。举个例子,如果8:00和8:10有数据,8:05缺失,它会按时间比例算中间值,而不是简单取平均。
3.3 重采样——统一时间粒度
不同传感器采样频率可能不一样。有的每秒采一次,有的每分钟采一次。你要做多变量分析,必须把它们对齐到同一个时间频率上。
重采样说白了就是「降频」或「升频」:
# 原始数据是秒级,想变成分钟级
df_minute = df.resample('1T').mean() # '1T' 表示1分钟
# 或者变成小时级
df_hour = df.resample('1H').mean()
# 升频:从分钟变成秒(需要插值)
df_second = df.resample('1S').interpolate(method='time')
重采样时选什么聚合函数,得看业务场景:
| 场景 | 推荐聚合方式 | 原因 |
|---|---|---|
| 温度、湿度 | 均值 | 变化平缓,均值能代表整体 |
| 流量、计数 | 求和 | 总量才有意义 |
| 峰值检测 | 最大值 | 关注极端值 |
| 异常检测 | 中位数 | 抗噪性强 |
method='time' 插值。
3.4 对齐时间索引——多数据源合并
这是最容易被忽视的一步。你有两个DataFrame,一个从8:00到18:00,另一个从9:00到17:00。直接合并,时间索引对不上,结果全是NaN。
我常用的对齐方式有三种:
# 准备两个时间序列
df1 = pd.DataFrame({'temp': [20, 21, 22]},
index=pd.date_range('2024-01-01', periods=3, freq='H'))
df2 = pd.DataFrame({'humidity': [60, 65, 70]},
index=pd.date_range('2024-01-01 00:30', periods=3, freq='H'))
# 方式1:外连接(保留所有时间点)
aligned_outer = pd.concat([df1, df2], axis=1).interpolate()
# 方式2:内连接(只保留共同时间范围)
aligned_inner = pd.concat([df1, df2], axis=1, join='inner')
# 方式3:重采样后再合并(推荐)
df1_hourly = df1.resample('1H').mean()
df2_hourly = df2.resample('1H').mean()
aligned_resampled = pd.concat([df1_hourly, df2_hourly], axis=1)
嗯,这里要注意——pd.concat 默认是外连接,时间索引取并集。如果你两个数据源时间范围差很多,结果会多出一堆NaN。我建议先重采样到统一频率,再合并,这样逻辑最清晰。
3.5 实战避坑指南
最后分享几个我踩过的坑:
- 时区问题: 不同数据源可能用不同时区。统一用UTC,最后展示时再转本地时间。我曾经因为没处理时区,模型在夏令时切换那天直接崩了。
- 时间戳精度: 有的数据精确到秒,有的精确到毫秒。合并前先统一精度,否则你以为对齐了,其实差了几毫秒。
- 节假日缺失: 工厂数据在节假日可能全为空。别用插值填充,直接标记为「非工作日」更合理。
- 重复索引: 同一个时间点出现两条数据?用
df.index.duplicated()检查,然后决定取均值还是保留最后一条。
好了,数据加载和预处理就讲到这里。下一章咱们开始做特征工程,把原始时间序列变成模型能理解的特征。记住一句话:数据预处理花的时间,会在模型训练和调优阶段加倍还给你。