3. 时间序列数据清洗:时间戳对齐、时区处理、采样频率统一、时间窗口滑动处理

时间序列数据,说白了就是带时间标签的数据流。智慧农业里,传感器每隔几分钟上报一次温度、湿度、光照,这些数据天然就是时间序列。但问题来了——不同设备的时间戳格式不一样,有的用北京时间,有的用UTC,有的采样间隔是5分钟,有的是10分钟。你想想看,这些数据不处理直接丢进模型,结果能靠谱吗?

我个人习惯把时间序列清洗分成四个核心步骤:时间戳对齐、时区处理、采样频率统一、时间窗口滑动处理。下面我一个一个讲。

3.1 时间戳对齐

时间戳对齐,就是把所有数据的时间基准拉到同一个起点。我在项目中遇到过最典型的情况:大棚里的温湿度传感器用的是设备本地时间,而气象站的数据用的是网络时间协议(NTP)同步的UTC时间。两个时间戳差了8小时,直接合并分析,结果完全错位。

对齐的核心操作有两个:

  • 格式统一:把字符串时间(如"2024-06-15 14:30:00")转成统一的datetime对象或时间戳整数。
  • 基准对齐:选定一个参考时间点(比如数据采集开始时间),把所有时间戳减去这个基准值,得到相对偏移。

关键点:时间戳对齐不是简单的格式转换,而是要确保不同来源的数据在同一个时间轴上可以一一对应。如果某个传感器的时间戳偏差超过采样间隔的一半,我建议直接丢弃该条数据,或者用前后值插值补全。

import pandas as pd

# 示例:将不同格式的时间戳统一为datetime
df1 = pd.DataFrame({
    'time': ['2024-06-15 14:30:00', '2024-06-15 14:35:00'],
    'temp': [25.3, 25.6]
})
df1['time'] = pd.to_datetime(df1['time'])

# 另一个设备的时间戳是Unix时间戳(秒)
df2 = pd.DataFrame({
    'time': [1718440200, 1718440500],
    'humidity': [68.2, 68.5]
})
df2['time'] = pd.to_datetime(df2['time'], unit='s')

# 对齐后合并
df_aligned = pd.merge_asof(df1.sort_values('time'), 
                           df2.sort_values('time'), 
                           on='time', tolerance=pd.Timedelta('2min'))

3.2 时区处理

时区处理是时间序列清洗里最容易踩坑的地方。我曾经因为时区没处理好,导致一个灌溉模型的预测结果偏差了整整一个白天——模型以为凌晨3点是正午,直接触发了灌溉指令。

智慧农业项目里,常见的时区场景有:

  • 传感器设备使用UTC时间,但业务分析需要本地时间(如北京时间UTC+8)。
  • 跨区域农场的数据,不同地块使用不同时区。
  • 云端服务器存储的时间是UTC,但前端展示需要用户本地时间。

避坑指南:我曾经在项目中直接把带时区信息的时间字符串用pd.to_datetime()转换,结果pandas自动把时间转成了UTC,而我以为它保留了原时区。后来我养成了一个习惯:所有时间数据在入库前统一转为UTC存储,展示时再转回本地时间。这样能避免90%的时区问题。

import pandas as pd
import pytz

# 将北京时间转为UTC存储
beijing_tz = pytz.timezone('Asia/Shanghai')
utc_tz = pytz.timezone('UTC')

# 假设原始数据是北京时间
df['time_beijing'] = pd.to_datetime(df['time_beijing'])
df['time_beijing'] = df['time_beijing'].dt.tz_localize(beijing_tz)
df['time_utc'] = df['time_beijing'].dt.tz_convert(utc_tz)

# 分析时统一使用UTC
df.set_index('time_utc', inplace=True)

3.3 采样频率统一

不同传感器上报数据的频率不一样,这是常态。比如土壤湿度传感器每10分钟上报一次,而气象站每5分钟上报一次。要一起分析,必须把采样频率统一。

常用的方法有三种:

方法 适用场景 优缺点
降采样 高频数据向低频对齐 减少数据量,但可能丢失细节
升采样 低频数据向高频对齐 保留更多时间点,但需要插值
重采样 统一到固定频率 灵活,但需选择聚合函数

我个人习惯用重采样,因为它最灵活。比如把10分钟的数据重采样到5分钟,可以用线性插值;把5分钟的数据重采样到10分钟,可以用均值聚合。

# 重采样示例:将10分钟数据升采样到5分钟,线性插值
df_10min = pd.DataFrame({
    'time': pd.date_range('2024-06-15 00:00:00', periods=6, freq='10min'),
    'temp': [25.0, 25.5, 26.0, 26.3, 26.1, 25.8]
})
df_10min.set_index('time', inplace=True)

# 重采样到5分钟
df_5min = df_10min.resample('5min').interpolate(method='linear')
print(df_5min)

小技巧:升采样时,如果数据缺失较多,线性插值可能不准确。我一般会先用limit参数限制连续插值的最大数量,比如interpolate(limit=2),超过这个数量就保留NaN,后续再处理。

3.4 时间窗口滑动处理

时间窗口滑动处理,是时间序列特征工程的核心操作。说白了,就是用过去一段时间的数据来生成新的特征。比如用过去1小时的温度均值来预测未来10分钟的湿度变化。

常见的窗口操作有:

  • 滚动窗口:固定大小的窗口,每次滑动一个时间步。
  • 扩展窗口:窗口从起点开始,逐渐扩大。
  • 加权窗口:窗口内的数据赋予不同权重,比如指数加权。

我在智慧农业项目里用得最多的是滚动窗口,因为它能捕捉到近期的变化趋势。比如计算过去30分钟的土壤湿度变化率,来判断是否需要灌溉。

# 滚动窗口示例:计算过去30分钟的温度均值(假设数据是5分钟间隔)
df['temp_rolling_mean'] = df['temp'].rolling(window=6).mean()

# 加权窗口:指数加权移动平均,更关注近期数据
df['temp_ewm'] = df['temp'].ewm(span=6, adjust=False).mean()

# 窗口统计:同时计算多个统计量
df['temp_rolling_std'] = df['temp'].rolling(window=6).std()
df['temp_rolling_max'] = df['temp'].rolling(window=6).max()

注意:滚动窗口会导致前几个时间点出现NaN值(因为窗口还没填满)。我一般会用min_periods参数来控制最少需要多少个有效值才计算结果。比如rolling(window=6, min_periods=3),这样窗口内只要有3个有效值就计算,避免前期数据全部丢失。

3.5 整体流程与SVG框架图

下面这张图展示了时间序列数据清洗的完整流程,从原始数据到最终可用于建模的干净时间序列。

时间序列数据清洗流程框架 原始时间序列数据 步骤1:时间戳对齐 格式统一(字符串→datetime) 基准对齐(相对偏移) 异常时间戳检测与修正 步骤2:时区处理 统一转为UTC存储 时区感知时间戳处理 展示时转回本地时间 步骤3:采样频率统一 最终输出:干净、对齐、统一频率的时间序列数据,可用于时间窗口滑动处理与建模

嗯,到这里时间序列数据清洗的核心内容就讲完了。你可能会问,这些步骤在实际项目中是不是必须全部执行?我的建议是:根据数据质量灵活选择。如果所有传感器都使用NTP同步,时区处理可以简化;如果采样频率本来就一致,那步3可以跳过。但时间戳对齐这一步,我建议永远不要省——它是所有后续分析的基础。