2、数据探索与预处理:缺失值处理、数据标准化、时间序列对齐、类别特征编码

数据预处理这件事,说通俗点就是「把原材料洗好切好」。我在工业传感器项目中见过太多「生数据」——缺胳膊少腿、单位和量级五花八门、时间戳对不上号。相信我,数据不洗,后面算法跑得再欢也是白搭。

本章主要解决四个问题:缺失值怎么填、数据怎么统一度量衡、时间轴怎么对齐、类别特征怎么喂给机器。嗯,这块活儿理论上不难,但坑特别多。我带你逐个踩一遍。

核心原则:预处理不是为了好看,是为了让异常检测算法能真正理解数据的「正常模式」。预处理的质量,直接决定了模型的上限。你在后期花再多钱调参,都无法弥补预处理阶段埋下的雷。

2.1 缺失值处理:填还是不填,这是个问题

实际采集的数据,很少是完美的。传感器坏了、网络丢包、数据库漏存,都会造成缺失。我的第一个坑就踩在这儿——以为缺失值会自动被算法忽略,结果模型直接崩了。从那以后,我把缺失值处理当成了模块中的第一优先级。

处理策略取决于缺失的「机制」和「比例」。先看一张对照表格:

缺失机制 说明 常见处理方式 适用场景
完全随机缺失(MCAR) 缺失与任何变量无关 删除、均值/中位数填充 缺失比例<5%,数据量大
随机缺失(MAR) 缺失与其他观测变量有关 回归填充、KNN 填充 缺失比例5%-20%
非随机缺失(MNAR) 缺失本身与缺失值有关 模型法(如插补模型)、标记缺失 缺失比例高或领域知识明确

具体操作的时候,我习惯按以下步骤来:

  1. 先看比例:缺失超过80%的字段,直接删除或单独标记,不硬填。
  2. 再看分布:连续型用中位数(对异常值鲁棒),离散型用众数。
  3. 时间序列特殊处理:前向填充(ffill)或插值。我个人偏爱线性插值,够快也够稳。
  4. 高阶方案:缺失模式复杂的,用 KNN 或迭代插补(如 MICE)。
# 一个很实用的处理流程示例(伪代码风格)
import pandas as pd
import numpy as np

df = pd.read_csv('sensor_data.csv')
# 第一步:检查缺失率
missing_rate = df.isnull().mean()

# 第二步:高缺失率字段直接丢掉
drop_cols = missing_rate[missing_rate > 0.8].index
df.drop(columns=drop_cols, inplace=True)

# 第三步:时序数据前向填充 + 线性插值回填
df['temperature'].fillna(method='ffill', inplace=True)
df['pressure'].interpolate(method='linear', inplace=True)

# 第四步:离散类别用众数
df['machine_status'].fillna(df['machine_status'].mode()[0], inplace=True)

我曾经踩过的坑:一次性对整个DataFrame用均值填充,结果把异常值也给填「平」了。异常检测最怕的就是把异常拉回正常分布,一旦填错,后面的模型就是个瞎子。所以,我建议你分组填充——按设备ID、按时间段,会好很多。

2.2 数据标准化:量纲不同的数据不能直接比

为什么需要标准化?你可以想想看,温度的单位是摄氏度(0~100),压力的单位是千帕(100~1000),直接丢进距离计算模型里,压力的影响力会碾压温度。异常检测算法往往依赖距离或梯度,不平量纲,它根本就不公平。

其实主流的标准化方法就三种,记牢就好。

方法 公式 特点 适用算法
Z-score 标准化 (X - μ) / σ 均值为0,标准差为1;对异常值敏感 线性回归、PCA、KNN
Min-Max 归一化 (X - Xmin) / (Xmax - Xmin) 缩放到[0,1];受极端值影响大 神经网络、距离模型
Robust Scaling (X - 中位数) / IQR 使用中位数和四分位距;对异常值稳健 数据含大量离群点

我个人习惯先画出数据的分布图,看看有没有明显的异常值。有的话就用 Robust Scaling,没有就 Z-score。别一上来就 Min-Max,有时候极端值会把正常数据压成一个点——嗯,我吃过这个亏。

from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler

# Z-score 标准化
scaler_z = StandardScaler()
X_z = scaler_z.fit_transform(df[['temperature', 'pressure']])

# Min-Max 归一化
scaler_mm = MinMaxScaler()
X_mm = scaler_mm.fit_transform(df[['temperature', 'pressure']])

# Robust 标准化
scaler_robust = RobustScaler()
X_robust = scaler_robust.fit_transform(df[['temperature', 'pressure']])

我的一个小技巧:标准化参数(μ、σ、min、max)在训练集上拟合,然后用同样的参数去变换验证集和测试集。千万不要在全量数据上做标准化,否则就是数据泄露——你的模型已经提前看到了全局统计量。

2.3 时间序列对齐:让每个时间戳都有数据

异常检测经常处理多源时序数据。但不同数据源的时间戳粒度不一样,有的每秒一条,有的每分钟一条,有的甚至不固定。如果不做对齐,后面的分析寸步难行。

我记得有一次,一个振动传感器是10Hz采集,另一个温度传感器是1Hz采集,直接拼接后,时间轴对不上,异常事件在振动上已经出现了一大段,在温度上还纹丝不动。其实核心问题不在于数据本身,在于时间基准没统一。

对齐的常见策略:

  • 重采样:选择一个统一的频率(如1秒或1分钟),将全部数据向它对齐。
  • 插值对齐:高频率数据降采样,低频率数据升采样,使用线性或样条插值。
  • 时间窗口聚合:比如用滑动窗口计算均值、最大值、最小值,把不规则时序转换成固定间隔的特征。
import pandas as pd

# 假设两个数据源,时间列都是 'timestamp'
vibration = pd.read_csv('vib.csv', parse_dates=['timestamp'])
temperature = pd.read_csv('temp.csv', parse_dates=['timestamp'])

# 统一索引并重采样到1秒
vib_resampled = vibration.set_index('timestamp').resample('1S').mean()
temp_resampled = temperature.set_index('timestamp').resample('1S').interpolate()

# 合并对齐后的数据
aligned = pd.merge(vib_resampled, temp_resampled,
                   left_index=True, right_index=True, how='outer')

避坑指南:我遇到过一种隐形的对齐错误——时区不统一。一个传感器用的UTC,一个用的本地时间,差了8小时,对出来的数据全是错位的。在做对齐前,务必统一时区,最好全部转成UTC。

2.4 类别特征编码:把文字变成数字

很多异常检测场景中,特征不仅仅是连续数值,还有类别——设备型号、操作模式、故障类型。算法只认得数字,所以需要把类别转化成数值。这个事情看起来简单,但编码方式会直接影响模型对「相似性」的认知。

三种常用编码方式:

编码方式 做法 优点 缺点
Label Encoding(标签编码) 给每个类别一个整数 简单、省内存 暗示了顺序关系(A=1, B=2, C=3,模型会认为C比A大)
One-Hot Encoding(独热编码) 每个类别变成一列0/1 无顺序假设,公平表示 类别多时维度爆炸
Target Encoding(目标编码) 用类别对应的目标均值替代 保留了类别与目标的关系 容易过拟合,需要交叉验证

我个人的选择逻辑是这样的:类别较少(<10类)且无序——独热编码;类别较多(>10类)——标签编码配合树模型(树模型不太在乎数值大小);需要压缩维度且目标值稳定——目标编码(但要防止数据泄露)。

import pandas as pd
from sklearn.preprocessing import LabelEncoder, OneHotEncoder

# 标签编码
df['machine_type_encoded'] = LabelEncoder().fit_transform(df['machine_type'])

# 独热编码
one_hot = pd.get_dummies(df['machine_type'], prefix='type')
df = pd.concat([df, one_hot], axis=1)

还有一个隐藏的坑:测试集里出现了训练集没有的类别。这在工程里很常见,新设备型号上线,之前的编码器没见过。我一般在编码时加一个「unknown」桶,把低频或者新类别统一丢进去。