数据清洗:处理缺失值、异常值检测、时间序列重采样

各位同学,咱们今天聊聊数据清洗。说实话,这步看着不起眼,但我在实际项目中吃过不少亏。有一次我拿到的能源价格数据,表面上看整整齐齐,结果一跑模型,预测结果完全偏离现实。后来才发现,数据里有好几个缺失值没处理,模型直接跑偏了。

数据清洗说白了就是给数据「洗澡」。脏数据不洗干净,后面分析得再漂亮也是白搭。咱们这章就讲三个核心环节:缺失值处理、异常值检测、时间序列重采样。

缺失值处理:别让「空」坑了你

缺失值在真实数据里太常见了。传感器故障、人工录入遗漏、数据传输丢包……原因五花八门。我个人习惯,拿到数据第一件事就是检查缺失情况。

核心思路:先定位缺失,再决定怎么补。

定位缺失很简单,用 isnull()isna() 就行。我一般会算一下缺失比例,如果超过 30%,这列数据基本就废了,直接删掉更省事。

import pandas as pd
import numpy as np

# 加载数据
df = pd.read_csv('energy_price.csv')

# 检查缺失情况
missing_ratio = df.isnull().mean()
print(missing_ratio[missing_ratio > 0])

补缺失值的方法,我常用的有三种:

  • 均值/中位数填充:适合数值型数据,分布比较对称用均值,有偏态用中位数。我在处理工资数据时,经常用中位数,因为工资分布往往右偏,均值会被高薪拉高。
  • 前向/后向填充:时间序列数据的好朋友。比如能源价格,今天缺失了,用昨天的价格补上,逻辑上说得通。
  • 插值法:线性插值、多项式插值,适合变化平滑的数据。我处理过一组原油价格数据,用线性插值效果就不错。
# 均值填充
df['price'].fillna(df['price'].mean(), inplace=True)

# 前向填充
df['price'].fillna(method='ffill', inplace=True)

# 线性插值
df['price'].interpolate(method='linear', inplace=True)

我的小经验:别盲目用均值填充。如果缺失值集中在某个时间段,比如节假日,那用均值填充会抹掉季节性特征。这时候前向填充更靠谱。

异常值检测:揪出「捣乱分子」

异常值是什么?就是那些明显偏离正常范围的数据点。比如原油价格突然跳到 500 美元一桶,这明显是录入错误。异常值对模型影响很大,尤其是线性模型,一个异常点就能把回归线拉偏。

我常用的检测方法有两种:

3σ 原则

假设数据服从正态分布,超出均值 ±3 倍标准差的值就算异常。这个方法简单粗暴,但数据偏态严重时不太准。

mean = df['price'].mean()
std = df['price'].std()
outliers = df[(df['price'] < mean - 3*std) | (df['price'] > mean + 3*std)]
print(f"检测到 {len(outliers)} 个异常值")

箱线图法(IQR)

这个方法更稳健,不依赖正态分布假设。用四分位距(IQR)来定义异常边界:下界是 Q1 - 1.5*IQR,上界是 Q3 + 1.5*IQR。我在项目中更常用这个,因为能源价格数据经常有厚尾分布。

Q1 = df['price'].quantile(0.25)
Q3 = df['price'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = df[(df['price'] < lower_bound) | (df['price'] > upper_bound)]

注意:异常值不一定是错误。比如金融危机期间,能源价格暴跌,那可能是真实的市场反应。我曾经犯过这个错,把金融危机期间的异常值全删了,结果模型完全无法预测危机场景。所以,先判断异常值是「错误」还是「极端事件」,再决定处理方式。

处理异常值,我一般三种策略:

  • 删除:明显录入错误,直接删。
  • 修正:比如用上下界值替换。
  • 保留:如果是极端事件,保留并做标记,单独分析。

时间序列重采样:对齐数据的时间步

能源价格数据可能是日频的,工资数据可能是月频的。你想分析两者的联动关系,时间频率必须对齐。重采样就是干这个的。

重采样说白了就是改变数据的时间频率。从高频到低频叫降采样,从低频到高频叫升采样。我处理通胀数据时,经常需要把日频的能源价格降采样到月频,跟工资数据对齐。

# 假设 df 的索引是日期
df.index = pd.to_datetime(df.index)

# 降采样:日频 -> 月频,取每月均值
monthly_price = df['price'].resample('M').mean()

# 升采样:月频 -> 日频,用前向填充
daily_price = monthly_price.resample('D').ffill()

降采样时,聚合函数怎么选?我一般看业务场景:

  • 均值:适合平稳变化的数据,比如平均气温。
  • 总和:适合累积量,比如月总交易量。
  • 最后值:适合快照数据,比如月末库存。

升采样时,填充方法也很关键:

  • 前向填充:假设数据在区间内保持不变,适合低频更新的数据。
  • 线性插值:假设数据线性变化,适合趋势明显的数据。

避坑指南:我曾经在处理工资数据时,直接用均值降采样,结果忽略了工资的季节性调整。后来发现,很多工资数据是按季度调整的,直接用均值会丢失调整信息。所以,重采样前一定要搞清楚数据的原始频率和业务含义。

知识体系总览

下面这张图,把咱们这章的核心逻辑串起来了。数据清洗不是孤立的一步,它跟后续分析环环相扣。

数据清洗核心流程 原始数据 缺失值处理 异常值检测 时间序列重采样 清洗后数据 联动分析 / 模型构建 均值/中位数填充 前向/后向填充 插值法 3σ原则 箱线图法(IQR) 降采样/升采样 聚合函数选择 填充方法

数据清洗这步,说白了就是给后续分析打地基。地基不稳,楼盖得再高也得塌。我个人习惯,每次清洗完数据都会做一次可视化检查,看看分布、趋势有没有异常。嗯,这一步虽然费时间,但绝对值得。

专注资料整理