4、数据清洗与质量控制:缺失值处理、异常值检测、空间一致性校验
大家好,我是老张。做环境数据分析这些年,我踩过最大的坑,就是数据质量不过关就急着建模。你想想看,垃圾数据进去,再漂亮的算法也白搭。今天咱们聊聊数据清洗这个“脏活累活”,我尽量讲得实在点。
核心观点:数据清洗不是体力活,是技术活。好的清洗策略,能让你后续分析省掉一半的麻烦。
4.1 缺失值处理——别急着删数据
环境监测数据里,缺失值太常见了。传感器故障、通信中断、人为漏采,原因五花八门。我个人习惯,拿到数据先看缺失率。低于5%的,直接删掉整行问题不大;超过20%的,就得认真想想怎么补了。
4.1.1 插值法——最朴素但最实用
插值法说白了,就是用已知点去“猜”未知点。环境数据通常有空间连续性,比如PM2.5浓度,相邻站点不会差太多。我常用的插值方法有几种:
- 线性插值:适合时间序列数据,比如某小时缺失,用前后两小时的平均值补上。简单粗暴,但效果还行。
- 样条插值:适合变化平滑的数据,比如温度、气压。我在项目中遇到过,用样条插值补气温数据,误差能控制在0.5℃以内。
- 克里金插值:这个就高级点了,考虑了空间相关性。做空气质量监测时,我经常用这个补站点间的空白区域。
# 线性插值示例(Python)
import pandas as pd
import numpy as np
# 模拟缺失数据
data = pd.Series([22.1, np.nan, 22.3, 22.5, np.nan, 22.8])
# 线性插值
filled_data = data.interpolate(method='linear')
print(filled_data)
# 输出:22.1, 22.2, 22.3, 22.5, 22.65, 22.8
我的小技巧:插值前先看看数据分布。如果数据波动很大,比如瞬时风速,插值反而会引入误差。这时候我宁愿用KNN。
4.1.2 KNN插值——找邻居帮忙
KNN(K近邻)插值,思路很简单:找最相似的K个样本,用它们的平均值来填补缺失值。环境数据里,相似性可以基于空间距离、时间特征、甚至气象条件。
我记得有一次做水质监测数据清洗,某个监测站的溶解氧数据缺失了整整一周。用线性插值补出来的结果,跟实际情况差很多。后来改用KNN,找了上下游两个站点的数据做参考,效果就好多了。
# KNN插值示例(使用sklearn)
from sklearn.impute import KNNImputer
import numpy as np
# 假设有3个站点,第2个站点第3个时间点缺失
X = np.array([[7.2, 6.8, 7.0],
[7.1, np.nan, 6.9],
[7.3, 7.0, 7.1]])
imputer = KNNImputer(n_neighbors=2)
X_filled = imputer.fit_transform(X)
print(X_filled)
# 缺失值会被补成附近两个站点的平均值
注意:KNN的K值不是越大越好。我试过K=10,结果把不同区域的站点混在一起,补出来的数据完全失真。一般K取3-5比较稳妥。
4.2 异常值检测——揪出“捣乱分子”
异常值,说白了就是那些明显不对劲的数据。比如PM2.5突然飙到1000,或者温度在冬天跳到40℃。这些数据不处理,模型会被带偏。
4.2.1 3σ法则——简单但有效
3σ法则基于正态分布假设。数据落在均值±3个标准差之外的概率只有0.3%,基本可以认为是异常。我在项目中用过很多次,尤其是处理风速、气压这类相对稳定的数据。
# 3σ异常值检测
import numpy as np
data = np.array([22.1, 22.3, 22.5, 22.8, 45.2, 22.2, 22.4])
mean = np.mean(data)
std = np.std(data)
# 标记异常值
anomalies = data[(data < mean - 3*std) | (data > mean + 3*std)]
print(f"异常值:{anomalies}")
# 输出:45.2
经验之谈:3σ法则对极端值敏感。如果数据本身就有多个异常,均值和标准差会被拉偏,导致漏检。这时候我建议先用IQR方法。
4.2.2 IQR方法——更稳健的选择
IQR(四分位距)方法不依赖正态分布,对异常值更鲁棒。它的逻辑是:计算第一四分位数(Q1)和第三四分位数(Q3),然后定义正常范围在Q1-1.5IQR到Q3+1.5IQR之间。
我曾经处理过一组臭氧监测数据,里面有明显的季节性波动。用3σ法则,把夏季的正常高值也当成了异常。换成IQR方法后,问题就解决了。
# IQR异常值检测
Q1 = np.percentile(data, 25)
Q3 = np.percentile(data, 75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
anomalies_iqr = data[(data < lower_bound) | (data > upper_bound)]
print(f"IQR异常值:{anomalies_iqr}")
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 3σ法则 | 正态分布数据 | 计算简单 | 对极端值敏感 |
| IQR方法 | 偏态分布数据 | 稳健性强 | 可能漏掉边缘异常 |
4.3 空间一致性校验——让数据“对得上”
环境监测数据有个特点:空间相关性。相邻站点的数据不会差太多。空间一致性校验,就是利用这个特点,找出那些“不合群”的数据点。
嗯,这里要注意。空间一致性不是简单的距离远近,还要考虑地形、风向、污染源等因素。我做过一个项目,两个站点直线距离只有2公里,但中间隔了一座山,数据差异就很大。这时候用距离做校验,反而会误判。
4.3.1 空间插值验证法
这个方法我经常用。具体做法是:用周围站点的数据做空间插值,得到目标站点的预测值,然后跟实际值对比。如果偏差超过阈值,就标记为可疑数据。
# 空间一致性校验伪代码
def spatial_consistency_check(target_station, neighbor_stations):
# 用邻居站点做反距离加权插值
predicted_value = idw_interpolation(target_station, neighbor_stations)
actual_value = target_station['value']
# 计算相对误差
relative_error = abs(predicted_value - actual_value) / actual_value
if relative_error > 0.3: # 阈值设为30%
return "可疑数据"
else:
return "正常数据"
避坑指南:我曾经把阈值设得太低(10%),结果把很多正常数据都标记成了异常。后来根据数据波动情况,把阈值调整到30%,效果就好多了。阈值没有标准答案,得根据实际数据来调。
4.3.2 空间自相关检验
空间自相关检验,说白了就是看数据在空间上是不是“抱团”的。如果某个站点的数据跟周围站点完全没关系,那就有问题了。常用的指标是莫兰指数(Moran's I)。
我记得有一次做全国空气质量数据分析,发现某个站点的SO2浓度异常高,但周围站点都正常。用莫兰指数一算,这个站点的局部空间自相关是负的,说明它是个“异类”。后来查实,是那个站点的传感器出了问题。
# 莫兰指数计算(简化版)
def moran_i(data, weights):
n = len(data)
mean = np.mean(data)
numerator = 0
denominator = 0
for i in range(n):
for j in range(n):
if i != j:
numerator += weights[i][j] * (data[i] - mean) * (data[j] - mean)
denominator += (data[i] - mean) ** 2
return n / sum(sum(weights)) * numerator / denominator
总结一下:数据清洗不是一次性工作,而是迭代过程。我通常的做法是:先处理缺失值,再检测异常值,最后做空间一致性校验。每做完一步,都要回头看看效果,别指望一步到位。
好了,这一章的内容就这些。数据清洗看着琐碎,但做扎实了,后面的分析会顺畅很多。下一章咱们聊聊特征工程,到时候见。