4. 数据清洗与预处理:让数据“干净”起来

说实话,做机器学习项目这么多年,我踩过最大的坑往往不是模型选得不对,而是数据本身有问题。你想想看,垃圾数据喂进去,再牛的模型也白搭。今天咱们就聊聊数据清洗与预处理,这是整个空气质量预测模型的基石。

我个人习惯,拿到数据第一件事不是急着建模,而是先做一次“体检”。看看有没有缺失值、异常值,数据类型对不对,有没有重复记录。这一步做好了,后面能省80%的麻烦。

核心要点:数据清洗不是可选项,是必选项。它直接决定模型的上限。

4.1 处理缺失值:删还是补?这是个问题

空气质量数据里,缺失值太常见了。传感器故障、网络中断、维护期间……各种原因都会导致数据缺失。我遇到过最夸张的一次,某个监测站连续三天没数据,后来发现是设备被鸟筑巢了。

处理缺失值,主要有两条路:删除填充

4.1.1 删除策略

什么时候该删?很简单:

  • 缺失比例太高(比如超过50%),这个特征基本废了
  • 缺失是随机发生的,且样本量足够大
  • 你实在找不到合理的填充方法

代码实现也很直接:

import pandas as pd

# 删除含有缺失值的行
df_clean = df.dropna()

# 删除缺失比例超过50%的列
threshold = len(df) * 0.5
df_clean = df.dropna(thresh=threshold, axis=1)
注意:删除数据要谨慎。我曾经因为删了太多行,导致时间序列出现断层,模型预测直接崩了。删之前,一定看看数据分布有没有变化。

4.1.2 填充策略

大多数情况下,我更倾向于填充。毕竟数据来之不易,能留就留。常用的填充方法有:

方法 适用场景 优缺点
均值/中位数填充 数值型特征,数据分布较对称 简单快速,但会降低方差
众数填充 类别型特征 适合离散数据,但可能引入偏差
前向/后向填充 时间序列数据 保持时序连续性,但假设变化缓慢
插值法 数据有趋势或周期性 更精确,但计算量大

举个例子,空气质量数据是典型的时间序列。我一般先用前向填充,因为污染物浓度不会突变:

# 前向填充
df['PM2.5'] = df['PM2.5'].fillna(method='ffill')

# 如果还有缺失,再用插值
df['PM2.5'] = df['PM2.5'].interpolate(method='linear')
我的经验:别只用一种方法。我会先试试几种填充策略,然后对比填充后的数据分布和原始分布。选那个最接近真实情况的。

4.2 处理异常值:别让“离群点”带偏了模型

异常值,说白了就是那些“不合群”的数据点。比如PM2.5突然飙到1000,或者温度显示-50度。这些数据要么是传感器故障,要么是极端天气事件。

我常用的两种方法:IQR法则3σ法则

4.2.1 IQR法则

这个方法对异常值不敏感,适合数据分布偏态的情况。原理很简单:

  • 计算第一四分位数(Q1)和第三四分位数(Q3)
  • IQR = Q3 - Q1
  • 正常范围:[Q1 - 1.5*IQR, Q3 + 1.5*IQR]
Q1 = df['PM2.5'].quantile(0.25)
Q3 = df['PM2.5'].quantile(0.75)
IQR = Q3 - Q1

lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

# 标记异常值
df['is_outlier'] = (df['PM2.5'] < lower_bound) | (df['PM2.5'] > upper_bound)

4.2.2 3σ法则

这个方法假设数据服从正态分布。超出均值±3倍标准差的数据,被视为异常。适合数据分布比较对称的情况。

mean = df['PM2.5'].mean()
std = df['PM2.5'].std()

lower_bound = mean - 3 * std
upper_bound = mean + 3 * std

df['is_outlier'] = (df['PM2.5'] < lower_bound) | (df['PM2.5'] > upper_bound)
避坑指南:我曾经在某个项目里直接用3σ法则处理所有特征,结果把正常的季节性波动也当成了异常。记住,异常值检测要结合业务背景。比如空气质量在春节期间PM2.5飙升,那是正常的。

4.3 数据类型转换与标准化

这一步很多人会忽略,但恰恰是模型能否收敛的关键。

4.3.1 数据类型转换

我见过最坑的情况:日期列是字符串类型,模型直接报错。所以第一步,先把数据类型搞对:

# 转换日期格式
df['date'] = pd.to_datetime(df['date'])

# 转换类别型数据
df['wind_direction'] = df['wind_direction'].astype('category')

# 数值型数据确保是float
df['PM2.5'] = df['PM2.5'].astype(float)

4.3.2 标准化

为什么要标准化?你想想看,PM2.5的数值范围是0-500,而温度是-10到40度。如果不做处理,模型会天然认为PM2.5更重要,这显然不对。

常用的两种方法:

方法 公式 适用场景
Z-score标准化 (x - μ) / σ 数据近似正态分布
Min-Max归一化 (x - min) / (max - min) 数据有明确边界
from sklearn.preprocessing import StandardScaler, MinMaxScaler

# Z-score标准化
scaler = StandardScaler()
df_scaled = scaler.fit_transform(df[['PM2.5', 'temperature', 'humidity']])

# Min-Max归一化
scaler = MinMaxScaler()
df_normalized = scaler.fit_transform(df[['PM2.5', 'temperature', 'humidity']])
我的建议:对于空气质量数据,我一般先用Z-score标准化。因为污染物浓度分布往往偏态,Z-score能更好地保留分布特征。

4.4 重复数据处理

重复数据看起来无害,但会让模型“过度学习”某些模式。尤其是时间序列数据,重复记录会破坏时序关系。

处理起来很简单:

# 检查重复行
duplicates = df.duplicated().sum()
print(f"发现 {duplicates} 条重复数据")

# 删除重复行
df_clean = df.drop_duplicates()

# 基于特定列去重(比如同一时间点只能有一条记录)
df_clean = df.drop_duplicates(subset=['date', 'station_id'])
注意:别盲目去重。我遇到过一种情况:两个监测站同一时间上报了相同数据,但其实是不同地点的真实测量值。去重前,先搞清楚重复的原因。

知识体系总览

下面这张图,把整个数据清洗与预处理的流程串起来了。你可以把它当作操作手册:

数据清洗与预处理流程 原始数据 缺失值处理 删除策略 | 填充策略(均值/前向/插值) 异常值处理 IQR法则 | 3σ法则 数据类型转换 & 标准化 日期/类别转换 | Z-score / Min-Max 重复数据处理 干净数据 ✅

整个流程就像一条流水线。数据从原始状态进来,经过缺失值处理、异常值检测、类型转换、标准化、去重,最后变成干净可用的数据。每一步都不能跳过,否则模型质量就会打折扣。

总结一下:数据清洗不是技术活,是良心活。你花多少心思在数据上,模型就回报你多少精度。别急着调参,先把数据搞干净。

专注资料整理