数据清洗基础方法:缺失值处理、异常值检测与去重策略
各位同行,今天我们来聊聊数据清洗。说实话,我在储能行业摸爬滚打这些年,最头疼的往往不是算法选型,而是数据本身的质量问题。你想想看,一堆脏数据喂进去,再牛的模型也是白搭。今天我就把数据清洗里最核心的三个环节——缺失值、异常值、重复数据——掰开揉碎了讲清楚。
一、缺失值处理:别急着删,先看看情况
缺失值在储能数据里太常见了。传感器掉线、通信中断、存储异常,都会导致数据出现空洞。我个人习惯是,拿到数据先做一次缺失率统计。如果某个字段缺失超过80%,我基本会直接放弃这个字段——留着也是添乱。
1. 删除法:简单粗暴,但有代价
删除缺失值是最直接的办法。但要注意,不是所有情况都适合删。
- 行删除:如果缺失比例很小(比如<5%),直接删掉这几行数据,不影响大局。
- 列删除:某个特征缺失太多,或者这个特征本身不重要,直接删列。
2. 填充法:用合理值补上空洞
填充比删除更温和。常用的填充方式有几种:
| 填充方式 | 适用场景 | 我常用的场景 |
|---|---|---|
| 均值/中位数填充 | 数值型、分布较对称 | 电压、电流的短时缺失 |
| 众数填充 | 类别型数据 | 电池型号、运行模式 |
| 前向/后向填充 | 时间序列数据 | 连续采样的温度数据 |
| 固定值填充 | 有业务含义的默认值 | 状态标志位缺失填0 |
举个例子,储能电站的电压数据如果缺失了1-2个采样点,用前后值的均值填充,基本不影响后续分析。但如果是长时间缺失,填充就不靠谱了。
3. 插值法:更精细的补全手段
插值比简单填充更聪明。它会利用数据的变化趋势来估算缺失值。
# 线性插值示例
import pandas as pd
import numpy as np
# 模拟储能电压数据,中间有缺失
data = pd.Series([3.2, 3.25, np.nan, 3.35, 3.38, np.nan, 3.45])
# 线性插值
filled_data = data.interpolate(method='linear')
print(filled_data)
# 输出: 3.20, 3.25, 3.30, 3.35, 3.38, 3.415, 3.45
我个人在电池容量衰减分析中,经常用样条插值。因为电池的衰减曲线不是线性的,样条插值能更好地拟合真实趋势。不过要注意,插值不是万能的——如果缺失段太长,插出来的值基本就是瞎猜了。
二、异常值检测:揪出那些「不听话」的数据点
异常值在储能数据里特别常见。电池突然跳变、传感器瞬间漂移、通信误码,都会产生异常值。如果不处理,这些异常点会严重干扰模型训练。
1. 3σ原则:最经典的方法
3σ原则基于正态分布假设。数据落在均值±3倍标准差之外的概率不到0.3%,这些点就可以视为异常。
# 3σ异常检测
def detect_outliers_3sigma(data):
mean = np.mean(data)
std = np.std(data)
lower_bound = mean - 3 * std
upper_bound = mean + 3 * std
outliers = data[(data < lower_bound) | (data > upper_bound)]
return outliers
# 以储能电池电压为例
voltages = [3.2, 3.21, 3.19, 3.22, 3.18, 4.5, 3.2, 3.21] # 4.5是异常
outliers = detect_outliers_3sigma(np.array(voltages))
print(f"检测到异常值: {outliers.values}")
但说实话,3σ方法有个硬伤——它对异常值本身很敏感。如果数据里已经有几个很大的异常值,均值和标准差都会被拉偏,导致检测不准。我在项目中遇到过这种情况,后来改用中位数和MAD(中位数绝对偏差)来替代,鲁棒性好很多。
2. IQR方法:更稳健的选择
IQR(四分位距)方法不受极端值影响,我个人更常用。
它的逻辑很简单:计算数据的Q1(25%分位数)和Q3(75%分位数),IQR = Q3 - Q1。然后定义正常范围:
- 下界:Q1 - 1.5 * IQR
- 上界:Q3 + 1.5 * IQR
超出这个范围的就是异常值。
# IQR异常检测
def detect_outliers_iqr(data):
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
outliers = data[(data < lower_bound) | (data > upper_bound)]
return outliers
# 同样的电压数据
outliers_iqr = detect_outliers_iqr(np.array(voltages))
print(f"IQR检测到异常值: {outliers_iqr.values}")
为什么IQR更稳健?因为它用的是分位数,不受极端值影响。你想想看,就算数据里有个1000的异常值,Q1和Q3基本不变,检测结果依然可靠。
3. 孤立森林:高维数据的利器
当数据维度高了(比如同时看电压、电流、温度、SOC),3σ和IQR就不好使了。这时候我推荐孤立森林。
孤立森林的原理很有意思:异常点通常「孤立」得很快——你随机切几刀,异常点很快就被单独分出来了。正常点则需要很多次切割才能分离。
from sklearn.ensemble import IsolationForest
# 假设我们有电压、电流、温度三个维度的数据
X = np.array([
[3.2, 10, 25],
[3.21, 10.1, 25.2],
[3.19, 9.8, 24.8],
[4.5, 10, 25], # 电压异常
[3.2, 10, 80], # 温度异常
[3.21, 10.1, 25.1]
])
model = IsolationForest(contamination=0.1, random_state=42)
predictions = model.fit_predict(X)
# -1表示异常,1表示正常
print(f"异常点索引: {np.where(predictions == -1)[0]}")
三、重复数据去重:别让同一份数据算两遍
重复数据在储能系统里也很常见。比如数据采集程序重启后重复上报、通信重传机制导致的数据冗余。重复数据会让统计结果偏大,模型训练时也会给某些样本过高的权重。
1. 完全重复去重
最简单的情况:两行数据一模一样。直接用pandas的drop_duplicates()就行。
import pandas as pd
df = pd.DataFrame({
'timestamp': ['2024-01-01 00:00', '2024-01-01 00:00', '2024-01-01 00:01'],
'voltage': [3.2, 3.2, 3.21],
'current': [10, 10, 10.1]
})
# 去除完全重复的行
df_clean = df.drop_duplicates()
print(df_clean)
2. 部分重复去重
更常见的情况是:时间戳相同,但其他字段有微小差异。比如同一时刻上报了两条电压数据,一个3.2V,一个3.21V。这时候需要业务规则来判断保留哪条。
我个人常用的策略:
- 保留第一条:适用于数据本身可靠,只是通信冗余的场景
- 保留平均值:适用于传感器噪声较大的场景
- 保留最新一条:适用于数据有版本更新的场景
# 按时间戳去重,保留平均值
df_dedup = df.groupby('timestamp').agg({
'voltage': 'mean',
'current': 'mean'
}).reset_index()
3. 模糊去重
还有一种情况:数据不完全相同,但非常相似。比如两条记录的时间戳差了1毫秒,其他字段几乎一样。这种「准重复」数据也需要处理。
我的做法是:先对时间戳做四舍五入(比如精确到秒),然后再去重。或者用相似度计算,设定一个阈值来判断是否重复。
知识体系总览
下面这张图是我自己总结的数据清洗核心逻辑,你可以对照着看:
这张图把今天讲的内容串起来了。你仔细看就会发现,三个分支的底层逻辑其实是一样的:先诊断问题类型,再选择对应方法,最后验证处理效果。我在实际项目中,从来不会一上来就用孤立森林或者样条插值——先看看数据长什么样,再决定用什么工具。
好了,数据清洗的基础方法就讲到这里。记住一句话:清洗数据不是目的,让数据能真实反映物理世界才是。下一节我们会聊数据标准化和归一化,到时候再细说。