4、时间序列处理:时间戳标准化、重采样与时间特征提取
各位同学好,我是老张。在风场干了这么多年数据,我最大的感触就是——时间戳处理是SCADA数据清洗的「第一道鬼门关」。你想想看,风机每时每刻都在产生数据,但各个机组、各个采集系统的时间格式五花八门。有的用Unix时间戳,有的用字符串,有的甚至带时区偏移。嗯,今天我们就来把这团乱麻理清楚。
4.1 时间戳标准化——先把「钟」调准
我在项目中遇到过最离谱的一次:同一个风场,A机组的时间是UTC+8,B机组的时间是UTC+0,两台风机明明同时发电,时间戳却差了8个小时。你说这数据怎么分析?
所以第一步,必须把所有时间戳统一成无时区的标准时间。我个人习惯统一转成datetime64[ns]格式,时区全部对齐到北京时间(UTC+8)。
核心原则:入库前先标准化,入库后不折腾。
来看一段我常用的清洗代码:
import pandas as pd
import numpy as np
# 假设原始数据有各种奇葩时间格式
df = pd.DataFrame({
'timestamp_raw': [
'2024-01-15 10:30:00+08:00', # 带时区
'1705300200', # Unix秒
'2024-01-15T02:30:00Z', # UTC时间
'20240115_103000' # 自定义格式
]
})
# 第一步:统一转成datetime
def parse_timestamp(ts):
if isinstance(ts, str):
# 尝试多种格式解析
try:
return pd.to_datetime(ts, utc=True)
except:
# 处理自定义格式
return pd.to_datetime(ts, format='%Y%m%d_%H%M%S', utc=True)
elif isinstance(ts, (int, float)):
# Unix时间戳
return pd.to_datetime(ts, unit='s', utc=True)
return pd.NaT
df['timestamp_std'] = df['timestamp_raw'].apply(parse_timestamp)
# 第二步:转成北京时间(UTC+8)
df['timestamp_beijing'] = df['timestamp_std'].dt.tz_convert('Asia/Shanghai')
# 第三步:去掉时区信息,方便后续计算
df['timestamp'] = df['timestamp_beijing'].dt.tz_localize(None)
避坑指南:我曾经因为没去掉时区信息,直接用带时区的列做groupby,结果pandas报了一堆时区不一致的错误。所以标准化后,记得用tz_localize(None)把时区信息剥离掉。
4.2 重采样——把数据「梳」整齐
SCADA数据采集频率其实很不稳定。有的风机10秒一条,有的1分钟一条,遇到通讯拥堵时可能5分钟才来一条。但做分析时,我们需要统一的时间间隔。说白了,就是把不规则的数据「梳」成规整的梳子齿。
4.2.1 10分钟平均——行业标准
风功率曲线测试、发电量评估,这些场景都要求10分钟平均数据。这是行业硬标准,IEC 61400-12也这么规定。
# 先确保时间戳是索引,且排序
df = df.set_index('timestamp').sort_index()
# 10分钟重采样,取平均值
df_10min = df.resample('10T').mean()
# 注意:如果某10分钟内数据点太少,平均值不可靠
# 我一般要求至少50%的数据点存在
min_points = 30 # 假设原始是10秒一条,10分钟应有60个点
df_10min_valid = df_10min[df_10min['count'] >= min_points]
我的经验:10分钟平均时,别光用mean()。我习惯同时算std()、min()、max(),这样能看出这10分钟内的波动情况。比如风速标准差大,说明湍流强,这对功率预测很重要。
4.2.2 1分钟平均——精细化分析
做故障诊断或控制策略优化时,10分钟太粗了。这时候需要1分钟平均数据。但要注意,1分钟平均的数据噪声更大,需要配合滤波使用。
# 1分钟重采样
df_1min = df.resample('1T').agg({
'wind_speed': ['mean', 'std', 'max'],
'power': ['mean', 'std', 'max'],
'rotor_speed': 'mean'
})
# 展平多级列索引
df_1min.columns = ['_'.join(col).strip() for col in df_1min.columns.values]
注意:1分钟重采样时,如果原始数据是10秒一条,那1分钟只有6个点。遇到通讯中断,可能只有2-3个点。这种短间隔的平均值方差很大,我一般会标记为「低置信度」数据,后续分析时酌情剔除。
4.3 时间特征提取——让模型「看懂」时间
风机发电量跟时间关系大吗?太大了!白天风大还是晚上风大?夏天风多还是冬天风多?这些规律模型自己学不会,得我们帮它提取出来。
4.3.1 小时特征
一天24小时,不同时段的风速、电价、负荷都不一样。我习惯把小时拆成两个维度:
df['hour'] = df['timestamp'].dt.hour
# 用正弦余弦编码,避免0点和23点的「跳跃」
df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / 24)
df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / 24)
为什么要用正弦余弦?你想想看,23点和0点只差1小时,但如果直接用数值编码,23和0差了23。模型会以为这两个时间点差异很大,其实它们挨着呢。正弦余弦编码就能解决这个问题。
4.3.2 季节特征
季节对风资源的影响是决定性的。我见过一个风场,冬季平均风速8m/s,夏季只有4m/s,发电量差了一倍。
# 提取季节
df['month'] = df['timestamp'].dt.month
# 北半球:春3-5,夏6-8,秋9-11,冬12-2
def get_season(month):
if month in [3,4,5]: return 0 # 春
elif month in [6,7,8]: return 1 # 夏
elif month in [9,10,11]: return 2 # 秋
else: return 3 # 冬
df['season'] = df['month'].apply(get_season)
# 同样用正弦余弦编码
df['season_sin'] = np.sin(2 * np.pi * df['season'] / 4)
df['season_cos'] = np.cos(2 * np.pi * df['season'] / 4)
4.3.3 月份特征
有时候季节太粗了,比如10月和11月虽然都是秋天,但风速差异可能很大。这时候需要月份特征。
# 月份直接作为类别特征
df['month'] = df['timestamp'].dt.month.astype('category')
# 或者用数值编码
df['month_num'] = df['timestamp'].dt.month
我的建议:月份特征最好用one-hot编码或者正弦余弦编码。直接用1-12的数值,模型会认为12月比1月大,这不对。月份是循环的,12月之后是1月。
4.4 本章知识体系
下面这张图是我自己整理的,把时间序列处理的三个核心步骤串起来了:
4.5 实战小贴士
最后分享几个我踩过的坑:
- 时间戳排序:重采样前一定要先排序。我有一回忘了排序,结果重采样出来的数据乱成一锅粥。
- 缺失值处理:重采样后会有NaN,别急着填充。先看看缺失比例,超过20%的时段直接标记为无效。
- 夏令时问题:如果你处理的是国外风场,注意夏令时切换那天会多一小时或少一小时。我一般直接统一用UTC,省心。
- 性能优化:几百万行的数据,别用
apply逐行处理时间戳。用pd.to_datetime的向量化操作,速度能快100倍。
嗯,最后说一句:时间序列处理没有银弹。每个风场的数据特点都不一样,多花点时间做探索性分析,比直接套模板强得多。