环境数据特征工程:缺失值处理、异常值检测、时间序列特征构建
各位同学,咱们今天聊点实在的。
做环境预测模型,说白了就是跟数据打交道。但现实中的数据,哪有那么干净?传感器漂移、通信中断、人为记录错误……你拿到的原始数据,基本都是一堆“毛坯房”。
我个人习惯,拿到数据第一件事不是建模,而是先做特征工程。这一步做不好,后面模型再花哨也是白搭。今天咱们就掰开揉碎,讲讲环境数据特征工程里最核心的三个环节:缺失值处理、异常值检测、时间序列特征构建。
核心观点:特征工程不是“预处理”,而是模型精度的基石。你花80%的时间做数据清洗和特征构建,剩下20%建模,这比例一点都不过分。
一、缺失值处理:别让“空”坑了你的模型
环境监测数据里,缺失值太常见了。我记得有一次做某河流水质预测,拿到数据一看,pH值那一列有将近15%的缺失。当时年轻,直接删掉了那些行,结果模型在汛期预测完全失效——因为缺失正好集中在暴雨时段。
所以,处理缺失值之前,先问自己三个问题:
- 缺失比例多少?低于5%可以简单处理,超过20%就要慎重了。
- 缺失是随机的吗?如果缺失跟某个变量有关(比如高温时传感器容易坏),那就是非随机缺失,处理方式完全不同。
- 业务上允许插补吗?有些场景(比如污染物超标记录),插补反而会引入偏差。
1.1 简单填充法
最直接的方法,但也是最容易被滥用的。
- 均值/中位数填充:适合数据分布比较对称的情况。我一般优先用中位数,因为它不受极端值影响。
- 前向/后向填充:时间序列数据里特别好用。比如PM2.5监测,如果某小时数据缺失,用前一小时的值填充,比用全局均值合理得多。
- 线性插值:假设缺失点前后是线性变化。对于温度、水位这种连续变化的环境变量,效果不错。
我的小技巧:做填充之前,先画个缺失值分布图。如果缺失集中在某个时间段,那说明传感器可能在那段时间出了问题,这时候用插值比用均值靠谱得多。
1.2 高级插补方法
简单方法搞不定的时候,就得请出高级工具了。
- KNN插补:找到跟缺失样本最相似的K个邻居,用它们的平均值填充。我在做土壤重金属数据时用过,效果比均值填充好30%以上。
- 多重插补:生成多个填充版本,然后取平均。适合学术研究,但计算量比较大。
- 模型预测插补:把缺失列当作目标变量,用其他列来预测。说白了,就是用机器学习来“猜”缺失值。
# Python示例:KNN插补
from sklearn.impute import KNNImputer
import numpy as np
# 假设X是环境特征矩阵,包含缺失值
imputer = KNNImputer(n_neighbors=5, weights='distance')
X_imputed = imputer.fit_transform(X)
# 注意:KNN插补前最好做标准化,否则量纲大的特征会主导距离计算
避坑指南:我曾经在一个项目中,用KNN插补处理空气质量数据,结果模型精度反而下降了。后来发现是因为特征里包含了未来时刻的数据——插补时“偷看”了未来信息。记住:插补只能用历史数据,不能用未来数据!
二、异常值检测:揪出那些“捣乱分子”
环境数据里的异常值,可能是传感器故障,也可能是真实的环境突变。比如某天PM2.5突然飙升到1000,是仪器坏了还是真的发生了污染事件?
我个人习惯,先做检测,再判断是否处理。不要一上来就删,那可能把重要信息也删掉了。
2.1 统计方法
- 3σ原则:假设数据服从正态分布,超过均值±3倍标准差的就是异常。简单粗暴,但环境数据很多不服从正态分布,用之前先检查一下分布形态。
- IQR(四分位距)法:不受分布假设限制。Q1-1.5*IQR到Q3+1.5*IQR之外的都是异常。我比较推荐这个方法,稳健性好。
2.2 机器学习方法
- 孤立森林:它的思路很巧妙——异常点更容易被“孤立”。随机切分数据,异常点需要的切分次数更少。处理高维数据时,我首选这个方法。
- LOF(局部离群因子):看一个点跟邻居的密度对比。如果某个点密度明显低于邻居,那它可能就是异常。适合局部异常检测。
- DBSCAN聚类:聚不到任何簇里的点,就是异常。我在做水质监测数据时用过,能同时发现多种类型的异常模式。
# Python示例:孤立森林
from sklearn.ensemble import IsolationForest
# contamination参数控制异常比例,一般设为0.01-0.1
iso_forest = IsolationForest(contamination=0.05, random_state=42)
outliers = iso_forest.fit_predict(X) # -1表示异常,1表示正常
# 标记异常值
X['is_outlier'] = outliers
经验之谈:做异常检测时,别只看单一指标。我习惯同时用3σ、IQR和孤立森林三种方法,取“投票”结果——至少两种方法都标记为异常的,才真正处理。这样可以减少误判。
三、时间序列特征构建:让模型“看懂”时间
环境数据本质上是时间序列。温度、湿度、污染物浓度……这些变量不仅跟当前值有关,还跟历史值、时间周期密切相关。
你想想看,如果只把当前时刻的PM2.5输入模型,模型永远不知道“昨天是不是雾霾天”。所以,我们要帮模型构建时间维度的特征。
3.1 滞后特征(Lag Features)
把过去时刻的值作为当前的特征。比如预测今天的AQI,把昨天、前天的AQI也加进来。
- 滞后1期:t-1时刻的值
- 滞后2期:t-2时刻的值
- 滞后24期:对于小时数据,就是昨天的同一时刻
滞后阶数的选择,可以看自相关函数(ACF)图。我一般选到ACF降到0附近为止。
3.2 滑动窗口统计
用过去一段时间的统计量来刻画趋势。
- 滑动平均:过去3小时、6小时、24小时的平均值
- 滑动标准差:反映波动程度
- 滑动最大值/最小值:捕捉极端情况
- 滑动斜率:用线性回归拟合窗口内的趋势
# Python示例:构建滑动窗口特征
import pandas as pd
# 假设df是时间序列数据,'pm25'是目标列
df['pm25_lag1'] = df['pm25'].shift(1)
df['pm25_lag24'] = df['pm25'].shift(24)
# 滑动窗口统计
df['pm25_rolling_mean_3h'] = df['pm25'].rolling(window=3).mean()
df['pm25_rolling_std_6h'] = df['pm25'].rolling(window=6).std()
df['pm25_rolling_max_24h'] = df['pm25'].rolling(window=24).max()
# 注意:滑动窗口会产生NaN,需要处理
df = df.dropna()
3.3 时间编码特征
把时间本身变成特征。模型不懂“上午8点”和“下午8点”的区别,但我们可以帮它。
- 周期性编码:用sin/cos编码小时、月份。比如小时:sin(2π*hour/24),cos(2π*hour/24)。这样0点和24点在编码空间里是连续的。
- 类别特征:是否周末、是否节假日、季节(春夏秋冬)。
- 时间差特征:距离上一次事件(比如降雨)的时间间隔。
关键提醒:时间序列特征构建时,千万不要用未来信息。比如用t+1时刻的数据来预测t时刻,这叫“数据泄露”。我见过太多新手犯这个错误,模型在训练集上表现完美,一到实际预测就崩盘。
四、实战中的避坑指南
说了这么多,最后分享几个我踩过的坑:
- 缺失值处理顺序:先做异常值检测,再做缺失值处理。因为异常值会影响均值、标准差等统计量,导致填充偏差。
- 特征缩放:KNN插补、孤立森林等方法对量纲敏感,记得先标准化。
- 时间序列的交叉验证:不能用随机划分,要用时间序列交叉验证(TimeSeriesSplit),否则会泄露未来信息。
- 不要过度工程:特征不是越多越好。我见过有人构建了200多个时间特征,结果模型过拟合得一塌糊涂。用特征重要性筛选一下,保留最有效的20-30个就够了。
我曾经踩过的一个大坑:做某城市空气质量预测时,我构建了滞后24小时的特征,但没注意到数据采集时间不规律——有些小时的数据缺失,导致滞后特征实际上对应了不同的时间间隔。后来我加了一个“时间间隔”特征,才解决了这个问题。
好了,特征工程这部分就讲到这里。记住一句话:垃圾进,垃圾出。数据质量决定了模型精度的上限,而特征工程就是帮你逼近这个上限的最有效手段。