3. 数据清洗与预处理:用pandas搞定脏数据
数据拿到手,别急着分析。我见过太多人,上来就画图,结果图是出来了,但全是错的。为啥?因为原始数据里藏着各种坑——缺失值、异常值、时间格式乱码。说白了,数据清洗这一步跳不过去。
我个人习惯,拿到数据第一件事就是跑 df.info() 和 df.describe()。先摸清家底,再动手干活。这节课咱们就聊聊,怎么用pandas把空气质量数据收拾得服服帖帖。
3.1 缺失值处理:别让空值毁了你的模型
空气质量监测设备偶尔会掉线,或者传感器故障,导致某些时间点没有数据。这些缺失值如果不处理,后续计算平均值、画趋势图都会出问题。
先看看缺失值长什么样:
import pandas as pd
import numpy as np
# 模拟一份空气质量数据
df = pd.DataFrame({
'时间': ['2024-01-01 08:00', '2024-01-01 09:00', '2024-01-01 10:00', '2024-01-01 11:00'],
'PM2.5': [35, np.nan, 42, 38],
'PM10': [60, 55, np.nan, 58],
'温度': [12.5, 13.0, 12.8, np.nan]
})
print(df.isnull().sum()) # 统计每列缺失数量
核心思路:缺失值处理有三种常见策略——删除、填充、插值。选哪种,取决于数据量和业务场景。
3.1.1 直接删除——简单粗暴
如果缺失值占比很小(比如不到5%),直接删掉最省事。
# 删除包含缺失值的行
df_clean = df.dropna()
# 只删除某列缺失的行
df_clean = df.dropna(subset=['PM2.5'])
我在项目中遇到过,有一次删了太多行,导致时间序列不连续,后面画图时出现了断层。所以删除前,一定先看看缺失比例。
3.1.2 填充——用合理值补上
空气质量数据是连续变化的,用前后值填充比较合理。
# 用前一个值填充(向前填充)
df['PM2.5'] = df['PM2.5'].ffill()
# 用后一个值填充(向后填充)
df['PM10'] = df['PM10'].bfill()
# 用平均值填充
df['温度'] = df['温度'].fillna(df['温度'].mean())
我的经验:对于空气质量这种时序数据,ffill() 和 bfill() 比平均值填充更靠谱。因为空气污染是连续变化的,用相邻时刻的值更符合实际情况。
3.1.3 插值——更平滑的处理
如果缺失了一段连续数据,插值法能给出更平滑的过渡。
# 线性插值
df['PM2.5'] = df['PM2.5'].interpolate(method='linear')
# 时间插值(考虑时间间隔)
df['PM2.5'] = df['PM2.5'].interpolate(method='time')
嗯,这里要注意:插值不是万能的。如果缺失数据超过连续3个点,插值结果可能偏差很大。我曾经在项目中遇到连续6小时数据缺失,插值出来的曲线完全失真,最后还是找运维要了原始日志。
3.2 异常值检测:揪出那些离谱的数据
传感器偶尔会抽风,比如PM2.5突然飙到9999,或者温度变成-273。这些异常值必须处理掉。
怎么判断异常?常用方法有两种:
| 方法 | 适用场景 | 原理 |
|---|---|---|
| 3σ原则 | 数据近似正态分布 | 超出均值±3倍标准差的值视为异常 |
| IQR四分位法 | 数据分布偏态严重 | 超出Q1-1.5IQR或Q3+1.5IQR的值视为异常 |
# 3σ原则检测异常值
mean = df['PM2.5'].mean()
std = df['PM2.5'].std()
df['异常标记'] = (df['PM2.5'] > mean + 3*std) | (df['PM2.5'] < mean - 3*std)
# IQR方法
Q1 = df['PM2.5'].quantile(0.25)
Q3 = df['PM2.5'].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
df['异常标记'] = (df['PM2.5'] < lower) | (df['PM2.5'] > upper)
我曾经踩过的坑:有一次用3σ原则检测PM10的异常值,结果发现所有数据都被标记为异常。后来一查,原来PM10的分布是严重右偏的,根本不符合正态分布。所以,用之前先画个直方图看看分布形态。
处理异常值,我建议先标记出来,不要直接删除。看看这些异常值是不是有规律——比如是不是每天固定时间出现?如果是,可能是设备校准导致的,需要单独处理。
3.3 时间戳格式化:统一标准,避免混乱
不同设备上报的时间格式五花八门:
2024-01-01 08:00:002024/01/01 08:0001-01-2024 08:00 AM1704067200(时间戳)
不统一格式,后面做时间序列分析根本没法搞。
3.3.1 用 pd.to_datetime() 统一转换
# 假设时间列是字符串
df['时间'] = pd.to_datetime(df['时间'], format='%Y-%m-%d %H:%M:%S')
# 如果格式不统一,让pandas自动推断
df['时间'] = pd.to_datetime(df['时间'], infer_datetime_format=True)
# 处理Unix时间戳(秒级)
df['时间'] = pd.to_datetime(df['时间'], unit='s')
我个人习惯:尽量指定 format 参数,这样解析速度更快,而且能避免自动推断出错。如果数据量很大(比如上百万条),指定格式能快好几倍。
3.3.2 设置时间索引
把时间列设为索引,后续按时间切片、重采样都方便。
df.set_index('时间', inplace=True)
# 按小时重采样
hourly = df.resample('H').mean()
# 按天聚合
daily = df.resample('D').agg({
'PM2.5': 'mean',
'PM10': 'mean',
'温度': ['min', 'max', 'mean']
})
3.3.3 处理时区问题
如果数据来自不同时区的设备,记得统一时区。
# 假设原始时间是UTC
df['时间'] = df['时间'].dt.tz_localize('UTC')
# 转换为北京时间
df['时间'] = df['时间'].dt.tz_convert('Asia/Shanghai')
# 去掉时区信息(转为无时区的时间)
df['时间'] = df['时间'].dt.tz_localize(None)
你想想看,如果北京和乌鲁木齐的设备数据混在一起,不处理时区,画出来的趋势图肯定对不上。
3.4 完整的数据清洗流程
把上面这些串起来,就是一个标准的数据清洗pipeline:
def clean_air_quality(df):
"""
空气质量数据清洗函数
"""
# 1. 统一时间格式
df['时间'] = pd.to_datetime(df['时间'])
df.set_index('时间', inplace=True)
# 2. 删除全空的行
df = df.dropna(how='all')
# 3. 处理缺失值(用前向填充)
df = df.ffill()
# 4. 检测并处理异常值(IQR方法)
for col in ['PM2.5', 'PM10', 'NO2', 'SO2']:
if col in df.columns:
Q1 = df[col].quantile(0.25)
Q3 = df[col].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
# 将异常值替换为边界值
df[col] = df[col].clip(lower, upper)
# 5. 确保数据按时间排序
df = df.sort_index()
return df
# 使用
df_clean = clean_air_quality(df_raw)
总结一下:数据清洗不是一次性工作。每次拿到新数据,都要跑一遍清洗流程。我建议把清洗逻辑封装成函数,方便复用。另外,清洗前后最好对比一下数据统计量,确保没有误伤正常数据。
好了,数据洗干净了,下一节咱们就可以开始做可视化了。记住一句话:垃圾进,垃圾出。数据清洗花的时间,后面分析时都会加倍还给你。