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'。把时间列设为索引,后续所有时间操作都会方便很多。我在项目中遇到过有人不设索引,结果重采样时各种报错,折腾了半天。

小技巧: 如果你的时间列格式比较奇葩(比如 "2024-01-15 08:30:45.123" 带毫秒),可以用 pd.to_datetime() 手动转换,指定 format 参数能提速不少。

3.2 处理缺失值——别急着删

真实数据几乎没有完美的。传感器掉线、网络波动、存储异常……缺失值无处不在。你想想看,要是直接删掉,时间序列就断了,后续的滑动窗口、差分计算全乱套。

我一般分三步走:

  1. 先看看缺失情况——用 isnull().sum() 统计
  2. 判断缺失模式——是随机缺失,还是连续一大段?
  3. 选择合适的填充方法
# 第一步:统计缺失
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)
注意: 千万别对连续缺失超过5个点的数据用前向填充。我曾经在温度传感器数据上这么干过,结果模型学出来一条直线——因为传感器坏了三天,我硬生生把同一个值填了三天。

我个人最常用的是 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() 检查,然后决定取均值还是保留最后一条。
我的习惯: 每次加载数据后,先跑一个简单的检查脚本——统计缺失率、检查时间范围、看是否有重复索引。整个过程不超过30秒,但能避免后面几小时的debug。

好了,数据加载和预处理就讲到这里。下一章咱们开始做特征工程,把原始时间序列变成模型能理解的特征。记住一句话:数据预处理花的时间,会在模型训练和调优阶段加倍还给你。