数据预处理实战:缺失值处理、异常值处理、数据标准化与归一化、特征工程入门

各位同学,欢迎来到第四讲。说实话,数据预处理这活儿,看着不起眼,但在我做过的储能项目里,80% 的坑都埋在这一步。你想想看,原始数据就像刚从工地拉回来的砖,有缺角的、有裂痕的、尺寸还不一样。直接拿它盖楼?那楼肯定歪。

今天我们就来聊聊,怎么把这些「脏砖」变成能用的「标准砖」。我会结合我在储能BMS(电池管理系统)项目中的实际经验,带大家走一遍完整的预处理流程。

核心观点: 数据预处理不是「洗数据」,而是「数据炼金术」。你投入的每一分精力,都会在模型精度上得到回报。

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

储能系统的数据采集,经常出幺蛾子。传感器掉线、通信中断、存储故障……结果就是数据表里出现一堆 NaNnull。我见过最夸张的一次,某风电场的数据缺失率高达 40%,那真是「千疮百孔」。

处理缺失值,常用的就三招:

  1. 直接删除:简单粗暴,但只适合缺失比例极低(<5%)的情况。删多了,样本量不够,模型就「营养不良」。
  2. 填充法:用均值、中位数、众数去填。我个人习惯,对正态分布的数据用均值,对有异常值的数据用中位数。比如电池电压数据,偶尔有跳变,用中位数更稳。
  3. 插值法:线性插值、多项式插值,适合时间序列数据。比如温度曲线,前后两个点之间缺了一个,用线性插值基本能还原。

举个例子,假设我们有一组电池SOC(荷电状态)数据,中间缺了几个点:

import pandas as pd
import numpy as np

# 模拟数据
data = {'time': ['00:00', '00:05', '00:10', '00:15', '00:20'],
        'soc': [80, np.nan, np.nan, 75, 73]}
df = pd.DataFrame(data)

# 线性插值
df['soc_interp'] = df['soc'].interpolate(method='linear')
print(df)
我的经验: 千万别一上来就用均值填充。我曾经在某个储能项目中,用均值填充了电压缺失值,结果导致后续的异常检测模型把正常数据全判成了异常。后来才发现,电压数据有明显的昼夜规律,应该用「同一时刻的历史均值」来填充。

4.2 异常值处理:揪出「捣乱分子」

异常值,说白了就是数据里的「刺头」。传感器故障、通信误码、人为操作失误,都会产生异常值。比如电池温度突然飙到 200°C,这明显是传感器坏了,不是真的热失控。

怎么揪出它们?我常用两种方法:

  • 3σ 原则:数据服从正态分布时,超过均值±3倍标准差的值,视为异常。简单,但要求数据分布比较「规矩」。
  • 箱线图法(IQR):不依赖分布假设。用四分位距(Q3 - Q1)来界定。低于 Q1 - 1.5×IQR 或高于 Q3 + 1.5×IQR 的,就是异常。我在项目中用得最多,因为储能数据往往不是正态分布。

处理方式也分几种:

  1. 直接删除:如果异常值明显是错误数据,删了最干净。
  2. 修正:用前后时刻的均值或中位数替换。比如某个时间点的电流值突然跳变,用前后5个点的均值去修正。
  3. 标记:保留异常值,但加一个「异常标记」列。有时候异常值本身也包含信息,比如它可能预示着设备故障的前兆。
注意: 我曾经犯过一个错——把所有的异常值都删了。结果模型在测试集上表现很好,一上线就崩。为什么?因为真实场景中就是有噪声的,模型没见过噪声,反而「水土不服」。所以,不要过度清洗,保留一部分「合理范围内的异常」反而更鲁棒。

4.3 数据标准化与归一化:把「尺子」统一

储能数据里,电压是伏特(V),电流是安培(A),温度是摄氏度(°C),SOC 是百分比(%)。这些特征的量纲完全不同。如果你直接拿原始数据去跑模型,量级大的特征会「吃掉」量级小的特征,模型就学偏了。

举个例子,电压 400V 和 SOC 80%,数值上差了好几个数量级。如果不做处理,模型会认为电压更重要,但实际上 SOC 对电池状态的影响同样关键。

常用的两种方法:

方法 公式 适用场景
Min-Max 归一化 X' = (X - X_min) / (X_max - X_min) 数据有明确边界,如 SOC(0~100%)
Z-score 标准化 X' = (X - μ) / σ 数据分布未知或有异常值,如电压、电流

我个人习惯:对聚类算法(如 K-Means),优先用 Z-score 标准化。因为 Min-Max 对异常值太敏感,一个离群点就能把整个缩放范围拉偏。而 Z-score 用均值和标准差,相对稳健。

from sklearn.preprocessing import StandardScaler, MinMaxScaler
import numpy as np

# 模拟数据:电压、电流、温度
data = np.array([[400, 50, 25],
                 [410, 55, 26],
                 [390, 45, 24],
                 [405, 60, 27]])

# Z-score 标准化
scaler = StandardScaler()
data_std = scaler.fit_transform(data)
print("标准化后:\n", data_std)

# Min-Max 归一化
scaler_mm = MinMaxScaler()
data_mm = scaler_mm.fit_transform(data)
print("归一化后:\n", data_mm)
避坑指南: 标准化和归一化的参数(均值、标准差、最大最小值),一定要用训练集的数据计算,然后应用到测试集和验证集。千万别把测试集的数据混进来一起算,否则会造成「数据泄露」,模型评估结果会虚高。我见过不止一个新手在这里翻车。

4.4 特征工程入门:从「数据」到「信息」

特征工程,说白了就是「造新特征」。原始数据里的电压、电流、温度,是「原材料」。但真正对模型有用的,往往是这些原始特征的组合或变换。

比如,在储能系统中,我们关心电池的「内阻」。但内阻不能直接测量,只能通过电压和电流算出来:R = ΔV / ΔI。这就是一个典型的特征工程操作。

常用的特征工程方法:

  • 统计特征:均值、方差、最大值、最小值、峰度、偏度。比如过去1小时内的电压波动幅度,能反映电池的稳定性。
  • 时序特征:差分(当前值 - 上一时刻值)、滑动窗口均值、指数加权移动平均。比如电流的差分,能捕捉到充放电切换的瞬间。
  • 频域特征:通过 FFT(快速傅里叶变换)提取频谱特征。我在做储能系统谐波分析时用过,能发现一些时域里看不到的规律。
  • 组合特征:两个特征的比值、乘积、差值。比如「电压 × 电流 = 功率」,这个新特征对电池状态评估非常关键。

举个例子,从原始数据中提取「电压波动特征」:

import pandas as pd
import numpy as np

# 模拟电压时间序列
np.random.seed(42)
voltage = 400 + np.random.normal(0, 2, 100)  # 均值400,标准差2

df = pd.DataFrame({'voltage': voltage})

# 提取特征
df['voltage_diff'] = df['voltage'].diff()  # 一阶差分
df['voltage_rolling_mean'] = df['voltage'].rolling(window=10).mean()  # 10点滑动均值
df['voltage_rolling_std'] = df['voltage'].rolling(window=10).std()    # 10点滑动标准差

print(df.head(15))
核心思想: 特征工程不是「越多越好」。我见过有人一口气造了200个特征,结果模型过拟合得一塌糊涂。好的特征工程,是基于对业务的理解,造出那些真正有物理意义、能解释问题的特征。少而精,才是高手。

4.5 本章知识体系总览

下面这张图,是我自己梳理的「数据预处理四步走」流程。你可以把它当作一个检查清单,每次做预处理时对照着来:

数据预处理四步走流程 缺失值处理 直接删除 填充法 插值法 异常值处理 3σ原则 箱线图法 删除/修正/标记 标准化/归一化 Min-Max Z-score 特征工程入门 统计特征(均值/方差) 时序特征(差分/滑动窗) 频域特征(FFT) 组合特征(比值/乘积) 注意:每一步之间是串行关系,但实际项目中可能需要反复迭代调整

嗯,到这里,数据预处理的核心内容就讲完了。你可能会觉得,这些操作看起来都不难啊?确实,每个步骤单独看都不复杂。但难的是把它们组合成一个完整的流水线,并且每一步都做对、做扎实。

我在实际项目中,经常要花 60%~70% 的时间在数据预处理上。剩下的 30% 才是建模和调参。很多初学者急着跑模型,结果数据没处理好,模型效果一塌糊涂,然后反过来怀疑算法不行。其实,很多时候不是算法的问题,是数据的问题

记住一句话:垃圾进,垃圾出(Garbage In, Garbage Out)。把数据预处理做好了,你的模型就已经成功了一半。


专注资料整理