数据清洗实战:异常值检测与缺失值处理

各位同行,今天我们来聊聊数据清洗。说实话,我在储能行业摸爬滚打这些年,见过太多因为数据没洗干净就急着建模的案例——结果嘛,要么模型跑飞,要么结论完全不可信。数据清洗不是花架子,它是后续所有分析的基石。

这一节,我重点讲三个实战技能:异常值检测(3σ原则和IQR方法)、缺失值处理(插值法和前向填充),以及时间戳对齐与重采样。这些都是我每天在项目里反复用的东西。

数据清洗实战 异常值检测 3σ原则(正态分布) IQR方法(箱线图) 缺失值处理 插值法(线性/样条) 前向填充(ffill) 时间戳对齐与重采样 对齐(resample + 填充) 重采样(降采样/升采样) 目标:干净、连续、对齐的时间序列

一、异常值检测:别让一个坏点毁了整个模型

异常值在储能数据里太常见了。比如BMS上报的电压突然跳变到1000V,或者SOC从80%瞬间掉到5%——这明显是传感器故障或者通信丢包。如果不处理,后续的SOH估算、寿命预测全都会偏。

1. 3σ原则:简单粗暴,但好用

3σ原则基于正态分布假设。数据落在均值±3倍标准差之外的概率只有0.3%,所以这些点可以视为异常。我个人习惯先画个直方图看看数据分布,如果大致呈钟形,3σ就很靠谱。

适用场景:数据近似正态分布,且样本量较大(>50个点)。
import numpy as np
import pandas as pd

def detect_outliers_3sigma(series):
    mean = series.mean()
    std = series.std()
    lower = mean - 3 * std
    upper = mean + 3 * std
    outliers = series[(series < lower) | (series > upper)]
    return outliers, lower, upper

# 示例:某储能电站的日充放电量
data = pd.Series([120, 125, 118, 130, 122, 500, 119, 121, 123, 128])
outliers, low, up = detect_outliers_3sigma(data)
print(f"异常值: {outliers.tolist()}")
print(f"正常范围: [{low:.2f}, {up:.2f}]")
注意:3σ对极端值敏感。如果数据本身包含多个异常,均值和标准差会被拉偏,导致漏检。我建议先做一次粗筛,再跑3σ。

2. IQR方法:更稳健的选择

IQR(四分位距)不依赖正态分布,它用中位数和四分位数来界定范围。公式很简单:IQR = Q3 - Q1,异常值定义为小于Q1 - 1.5*IQR或大于Q3 + 1.5*IQR的点。

我在项目中遇到过BMS温度传感器漂移的问题。用3σ检测,因为数据本身有季节性波动,标准差很大,漏掉了不少坏点。换成IQR方法后,那些明显偏离正常区间的温度点全被抓出来了。

def detect_outliers_iqr(series):
    Q1 = series.quantile(0.25)
    Q3 = series.quantile(0.75)
    IQR = Q3 - Q1
    lower = Q1 - 1.5 * IQR
    upper = Q3 + 1.5 * IQR
    outliers = series[(series < lower) | (series > upper)]
    return outliers, lower, upper

# 同样的数据,用IQR试试
outliers_iqr, low_iqr, up_iqr = detect_outliers_iqr(data)
print(f"IQR异常值: {outliers_iqr.tolist()}")
print(f"IQR正常范围: [{low_iqr:.2f}, {up_iqr:.2f}]")
我的经验:对于储能电池的电压、电流数据,我优先用IQR。因为电池数据往往有偏态分布(比如充电段和放电段不对称),IQR不受极端值影响,更稳定。

二、缺失值处理:别让数据出现「断档」

储能系统的数据采集经常丢包——通信中断、传感器掉线、存储满……原因太多了。缺失值不处理,时间序列分析就没法做。我常用的两种方法:插值法和前向填充。

1. 插值法:让曲线更平滑

插值法适合数据变化比较平缓的场景。比如电池的SOC,在正常充放电过程中是连续变化的,中间丢几个点,用线性插值就能补得八九不离十。

# 模拟带缺失的SOC数据
soc = pd.Series([50, 52, np.nan, 56, 58, np.nan, np.nan, 64, 66],
                index=pd.date_range('2024-01-01', periods=9, freq='h'))
print("原始数据:")
print(soc)

# 线性插值
soc_linear = soc.interpolate(method='linear')
print("\n线性插值后:")
print(soc_linear)

# 样条插值(适合更平滑的曲线)
soc_spline = soc.interpolate(method='spline', order=2)
print("\n样条插值后:")
print(soc_spline)
选哪种插值?
  • 线性插值:简单快速,适合短时间缺失(1-2个点)
  • 样条插值:曲线更平滑,适合长时间缺失(3-5个点),但计算量稍大
  • 时间插值:如果时间间隔不均匀,用method='time'按时间权重插值

2. 前向填充:适合「保持状态」的场景

前向填充(ffill)就是用上一个有效值填充后面的缺失值。这在储能系统里很实用——比如断路器状态、运行模式这类离散变量,一旦状态改变,会一直保持到下一次变化。用插值法反而会算出中间状态,不合理。

我曾经处理过一个储能电站的告警日志,通信中断了2小时,恢复后数据才继续上报。这2小时内的运行状态用ffill填充,因为设备实际上一直在并网运行,状态没变过。

# 模拟运行状态数据(0=待机,1=充电,2=放电)
status = pd.Series([1, 1, np.nan, np.nan, 2, 2, np.nan, 0, 0],
                   index=pd.date_range('2024-01-01', periods=9, freq='h'))
print("原始状态:")
print(status)

# 前向填充
status_ffill = status.ffill()
print("\n前向填充后:")
print(status_ffill)

# 后向填充(bfill)也可以,但一般用于已知未来值的情况
status_bfill = status.bfill()
print("\n后向填充后:")
print(status_bfill)
避坑指南:我曾经在SOC数据上用了ffill,结果连续缺失5小时后,SOC一直显示50%,导致后续的充放电策略完全错乱。记住:连续缺失超过3个点,ffill就不太靠谱了,建议用插值或者直接标记为无效。

三、时间戳对齐与重采样:让不同设备「说同一种语言」

储能系统里,BMS、PCS、电表、温控……每个设备都有自己的采样频率。BMS可能每100ms上报一次电压,PCS每1秒上报一次功率,电表每15分钟上报一次电量。如果不做时间对齐,这些数据根本没法放在一起分析。

1. 时间戳对齐:统一时间基准

对齐的核心是选择一个基准时间频率,然后把所有数据都映射到这个频率上。我一般用pandas.resample配合ffillinterpolate来实现。

# 模拟两个不同频率的数据
bms_data = pd.DataFrame({
    'voltage': [3.5, 3.52, 3.51, 3.53],
    'current': [10, 12, 11, 13]
}, index=pd.to_datetime(['2024-01-01 00:00:00', '2024-01-01 00:00:01',
                         '2024-01-01 00:00:02', '2024-01-01 00:00:03']))

pcs_data = pd.DataFrame({
    'power': [3500, 3600, 3550],
    'temp': [25.1, 25.3, 25.2]
}, index=pd.to_datetime(['2024-01-01 00:00:00', '2024-01-01 00:00:02',
                         '2024-01-01 00:00:04']))

# 统一重采样到1秒间隔
bms_aligned = bms_data.resample('1S').ffill()
pcs_aligned = pcs_data.resample('1S').ffill()

# 合并
merged = pd.concat([bms_aligned, pcs_aligned], axis=1)
print("对齐后的数据:")
print(merged)

2. 重采样:改变数据粒度

重采样有两种方向:降采样(从高频到低频)和升采样(从低频到高频)。

  • 降采样:比如把1秒的数据聚合成1分钟的平均值。适合做长期趋势分析,减少数据量。
  • 升采样:比如把15分钟的电表数据插值到1分钟。适合与高频数据对齐,但要注意引入的误差。
# 降采样:1秒 -> 1分钟平均
high_freq = pd.Series(np.random.randn(600), 
                      index=pd.date_range('2024-01-01', periods=600, freq='1S'))
minute_avg = high_freq.resample('1T').mean()
print("降采样到1分钟:")
print(minute_avg.head())

# 升采样:15分钟 -> 1分钟线性插值
low_freq = pd.Series([100, 120, 110, 130],
                     index=pd.date_range('2024-01-01', periods=4, freq='15T'))
high_freq_interp = low_freq.resample('1T').interpolate(method='linear')
print("\n升采样到1分钟(线性插值):")
print(high_freq_interp.head(10))
我的习惯:做多设备关联分析时,我通常把所有数据统一重采样到1分钟频率。这个粒度既能保留充放电过程的细节,又不会因为数据量太大导致内存爆炸。当然,如果你做的是电池内阻分析这种高频场景,可能需要1秒甚至100ms。

实战小结

数据清洗没有银弹。我见过有人对所有数据都用3σ,结果把正常的充放电峰值当异常删掉了;也有人对离散状态用插值,补出了不存在的中间状态。记住三点:

  1. 异常值检测:先看分布,再选方法。正态用3σ,偏态用IQR。
  2. 缺失值处理:连续变量用插值,离散变量用ffill。连续缺失太多就标记为无效。
  3. 时间对齐:统一频率,降采样用聚合,升采样用插值或ffill。

嗯,这些方法说起来简单,但真正用好需要大量实践。我建议你拿自己项目里的数据跑一遍,看看不同方法的效果差异。数据干净了,后面的分析才能站得住脚。

蓝海数据掘金营,专注资料整理