3. 数据清洗与预处理:缺失值处理、异常值检测、数据标准化与归一化、特征工程入门
大家好,我是老张。这一章咱们聊聊数据清洗和预处理。说实话,很多测试同学拿到数据就急着建模分析,结果模型跑出来一塌糊涂。为什么?因为数据本身就有问题。我见过太多项目,80%的时间都花在数据清洗上,真正建模反而很快。所以,别小看这一步。
3.1 缺失值处理:别让“空”坑了你
测试数据里,缺失值太常见了。比如某个接口响应时间字段,偶尔因为网络抖动没采集到;或者某个日志字段,因为版本升级被废弃了。这些“空”值,如果不处理,模型会直接报错,或者给出错误结论。
我个人习惯,拿到数据第一件事就是看缺失率。用 df.isnull().sum() 扫一眼,心里就有数了。
核心原则:缺失率低于5%,直接删除行;缺失率在5%-20%,用均值/中位数填充;缺失率超过20%,这个特征基本废了,考虑删除列。
举个例子,我在项目中遇到过压测数据,某个服务器的CPU使用率字段缺失了30%。我一开始想用均值填充,结果发现那台服务器是备用机,平时负载很低。用均值填充反而把异常值抹平了。后来我改用中位数填充,效果好了很多。
小技巧:对于时间序列数据,比如响应时间,用前向填充(ffill)或后向填充(bfill)往往比均值更合理。因为相邻时间点的数据相关性更强。
# 缺失值处理示例
import pandas as pd
import numpy as np
# 模拟测试数据
data = {'响应时间_ms': [120, 150, np.nan, 130, 200, np.nan, 110],
'CPU使用率_%': [45, 50, 55, np.nan, 60, 65, 70]}
df = pd.DataFrame(data)
# 查看缺失情况
print(df.isnull().sum())
# 方法1:删除缺失行(缺失率低时)
df_drop = df.dropna()
# 方法2:均值填充
df_fill_mean = df.fillna(df.mean())
# 方法3:前向填充(时间序列推荐)
df_fill_ffill = df.fillna(method='ffill')
print("原始数据:\n", df)
print("均值填充后:\n", df_fill_mean)
3.2 异常值检测:揪出“捣乱分子”
异常值,说白了就是那些明显偏离正常范围的数据点。比如99%的接口响应都在200ms以内,突然冒出一个5000ms的。这可能是网络超时重试导致的,也可能是被测系统真的出了故障。
你想想看,如果不处理这个异常值,计算平均响应时间时,一个点就能把均值拉高好几倍。这会让你的性能报告完全失真。
我常用的方法有两种:
- 3σ原则:数据服从正态分布时,超过均值±3倍标准差的值视为异常。适合CPU使用率、内存占用这类指标。
- 箱线图法(IQR):不依赖分布假设,用四分位数来界定。适合响应时间这种偏态分布的数据。
注意:异常值不一定是“坏”数据。我曾经在分析线上故障时,发现一个异常点恰好对应了一次服务重启。这个“异常”反而是关键线索。所以,先标记,再判断,别一删了之。
# 异常值检测示例
from scipy import stats
# 模拟响应时间数据(含异常值)
response_times = [120, 130, 125, 140, 135, 5000, 128, 132, 138, 6000]
# 方法1:3σ原则
z_scores = np.abs(stats.zscore(response_times))
threshold = 3
outliers_3sigma = np.where(z_scores > threshold)
print(f"3σ原则检测到的异常值索引:{outliers_3sigma}")
# 方法2:箱线图法(IQR)
Q1 = np.percentile(response_times, 25)
Q3 = np.percentile(response_times, 75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers_iqr = [i for i, v in enumerate(response_times) if v < lower_bound or v > upper_bound]
print(f"IQR法检测到的异常值索引:{outliers_iqr}")
3.3 数据标准化与归一化:让不同量纲的数据“对话”
测试数据里,特征的单位往往不同。比如响应时间是毫秒级(100-500),而CPU使用率是百分比(0-100)。如果直接拿去做聚类或回归,响应时间这个特征会被“淹没”,因为它的数值范围太小。
标准化(Standardization):把数据变成均值为0、标准差为1的分布。公式是 (x - μ) / σ。适合数据本身服从正态分布的场景,比如很多统计模型。
归一化(Normalization):把数据缩放到[0,1]区间。公式是 (x - min) / (max - min)。适合数据分布没有明显边界的情况,比如图像处理、神经网络。
我的经验:在测试数据分析中,我更喜欢用标准化。因为归一化对异常值非常敏感——一个极端值就能把其他所有数据压缩到很小的区间。而标准化相对鲁棒一些。
# 标准化与归一化示例
from sklearn.preprocessing import StandardScaler, MinMaxScaler
# 模拟测试数据:响应时间(ms) 和 CPU使用率(%)
data = np.array([[120, 45],
[150, 50],
[130, 55],
[200, 60],
[180, 65]])
# 标准化
scaler_std = StandardScaler()
data_std = scaler_std.fit_transform(data)
print("标准化后的数据:\n", data_std)
# 归一化
scaler_norm = MinMaxScaler()
data_norm = scaler_norm.fit_transform(data)
print("归一化后的数据:\n", data_norm)
3.4 特征工程入门:从原始数据中“挖”出价值
特征工程,说白了就是把原始数据变成模型能更好理解的形式。我见过很多测试同学,直接把几十个原始字段扔进模型,结果效果很差。其实,很多有价值的信息藏在字段之间的关系里。
常用的特征工程方法:
- 特征提取:从时间戳里提取“小时”、“星期几”、“是否工作日”。比如晚上8点的接口响应,和下午3点的,往往有显著差异。
- 特征组合:把“请求大小”和“响应时间”组合成“吞吐量”(请求大小/响应时间)。这个新特征往往比原始特征更有解释力。
- 特征编码:把“错误类型”这种文本标签,转成数值。比如用One-Hot编码,或者用Label Encoding。
避坑指南:我曾经在做一个故障预测模型时,把“错误码”直接当成数值特征扔进模型。结果模型学到的规律是“错误码越大,故障概率越高”——这完全是个假规律,因为错误码只是编号,没有大小意义。后来改成One-Hot编码,模型才正常了。
# 特征工程示例
import pandas as pd
# 原始数据:包含时间戳和错误码
data = {'timestamp': ['2024-01-15 08:30:00', '2024-01-15 14:45:00', '2024-01-16 20:15:00'],
'error_code': ['E001', 'E002', 'E001'],
'response_time_ms': [120, 150, 130]}
df = pd.DataFrame(data)
# 特征提取:从时间戳提取小时和星期几
df['timestamp'] = pd.to_datetime(df['timestamp'])
df['hour'] = df['timestamp'].dt.hour
df['day_of_week'] = df['timestamp'].dt.dayofweek # 0=周一, 6=周日
# 特征编码:One-Hot编码错误码
df_encoded = pd.get_dummies(df, columns=['error_code'], prefix='err')
print("特征工程后的数据:\n", df_encoded)
好了,这一章的内容就到这里。数据清洗和预处理,说白了就是给模型“喂”干净、有营养的数据。下一章我们会聊特征选择,看看怎么从一堆特征里挑出最有用的那几个。到时候见。