第四章:数据清洗实战——缺失值处理、异常值检测、重复数据去重、数据标准化
数据清洗,说白了就是给数据「洗澡」。
我做了这么多年芯片行业的数据挖掘,见过太多人一上来就建模,结果模型跑出来一塌糊涂。为什么?因为数据本身就有问题。你想想看,海关数据这种来源复杂的东西,脏数据是家常便饭。今天这一章,我就带你把这四个最关键的清洗步骤走一遍。
核心观点:数据清洗占整个数据挖掘工作量的60%-80%。别嫌烦,这一步做不好,后面全是白搭。
4.1 缺失值处理——别让「空」坑了你
海关数据里,缺失值太常见了。比如某个芯片型号的「封装形式」字段是空的,或者「供应商国家」没填。我见过最夸张的一次,一个客户拿到的数据里,价格字段有40%都是空的。你说这能直接用吗?
处理缺失值,我个人习惯分三步走:
- 先诊断——看看缺失率有多高
- 再判断——这个字段重要吗?
- 最后动手——选合适的方法处理
来看看代码怎么写:
import pandas as pd
import numpy as np
# 加载海关数据
df = pd.read_csv('chip_import_data.csv')
# 第一步:查看缺失情况
print(df.isnull().sum())
print(df.isnull().sum() / len(df) * 100) # 缺失百分比
# 第二步:根据情况处理
# 方案A:缺失率 < 5%,直接删除
df_clean = df.dropna(subset=['price', 'quantity'])
# 方案B:缺失率 5%-30%,用均值或中位数填充
df['price'].fillna(df['price'].median(), inplace=True)
# 方案C:时间序列数据,用前向填充
df['ship_date'].fillna(method='ffill', inplace=True)
# 方案D:分类数据,用众数填充
df['package_type'].fillna(df['package_type'].mode()[0], inplace=True)
我的经验:对于芯片价格这种字段,我一般用中位数而不是均值。为什么?因为价格分布往往有长尾,几个高端芯片能把均值拉得很高,中位数更稳健。我在一个项目中试过,用均值填充后模型偏差大了15%。
注意:千万别一上来就全删。如果某个字段缺失率超过50%,我建议你直接放弃这个字段,而不是硬填。填出来的数据比空着还危险。
4.2 异常值检测——揪出那些「不对劲」的数据
异常值,说白了就是数据里的「刺头」。比如一条记录显示某批芯片单价是0.01美元,或者数量是-1000个。你想想看,这合理吗?
我常用的方法有三种:
| 方法 | 适用场景 | 阈值 | 优缺点 |
|---|---|---|---|
| IQR(四分位距) | 偏态分布数据 | Q1 - 1.5*IQR 到 Q3 + 1.5*IQR | 不受极端值影响,稳健 |
| Z-Score | 近似正态分布 | |Z| > 3 | 简单直观,但对小样本不友好 |
| 业务规则 | 有明确业务约束 | 自定义阈值 | 最可靠,但需要领域知识 |
实战代码来了:
# IQR方法检测异常值
Q1 = df['unit_price'].quantile(0.25)
Q3 = df['unit_price'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = df[(df['unit_price'] < lower_bound) | (df['unit_price'] > upper_bound)]
print(f"检测到 {len(outliers)} 个异常值")
# Z-Score方法
from scipy import stats
z_scores = np.abs(stats.zscore(df['unit_price']))
outliers_z = df[z_scores > 3]
# 业务规则:芯片单价不可能低于0.1美元或高于10000美元
df_valid = df[(df['unit_price'] >= 0.1) & (df['unit_price'] <= 10000)]
关键提醒:异常值不一定是错误值。我曾经处理过一个案例,某批军用级芯片单价是普通芯片的20倍,统计上算异常,但业务上完全合理。所以,先问业务,再动手删。
4.3 重复数据去重——别让同一批货算两次
海关数据里重复记录太常见了。同一个报关单号,可能因为系统重传出现两次;或者同一批货,不同关区都录入了。我见过最离谱的,一条记录重复了7次。
去重策略我一般分两种:
- 完全重复:所有字段都一样,直接删
- 部分重复:关键字段相同,但其他字段有差异,需要合并或保留最新
# 完全重复去重
df_dedup = df.drop_duplicates()
print(f"去重前:{len(df)} 条,去重后:{len(df_dedup)} 条")
# 基于关键字段去重(比如报关单号+芯片型号)
df_dedup_key = df.drop_duplicates(subset=['declaration_no', 'chip_model'], keep='last')
# 保留最新记录(按时间戳)
df_sorted = df.sort_values('record_date')
df_dedup_latest = df_sorted.drop_duplicates(subset=['declaration_no'], keep='last')
我的习惯:去重之前,先看看重复数据的比例。如果超过10%,我建议你回头检查一下数据源是不是有问题。我曾经遇到一个项目,重复率高达30%,后来发现是ETL脚本写错了,每天全量导入时没做增量判断。
4.4 数据标准化——让不同尺度的数据「对齐」
芯片海关数据里,字段的量纲千差万别。价格是美元,数量是个数,重量是千克。你想想看,如果不做标准化,模型里价格这个特征会主导一切,其他特征基本没贡献。
我常用的两种标准化方法:
| 方法 | 公式 | 输出范围 | 适用场景 |
|---|---|---|---|
| Min-Max归一化 | (x - min) / (max - min) | [0, 1] | 数据有明确边界,如价格 |
| Z-Score标准化 | (x - mean) / std | 均值为0,标准差为1 | 数据近似正态分布 |
from sklearn.preprocessing import MinMaxScaler, StandardScaler
# Min-Max归一化
scaler_minmax = MinMaxScaler()
df['price_normalized'] = scaler_minmax.fit_transform(df[['unit_price']])
# Z-Score标准化
scaler_std = StandardScaler()
df['price_standardized'] = scaler_std.fit_transform(df[['unit_price']])
# 查看效果
print("原始价格范围:", df['unit_price'].min(), "-", df['unit_price'].max())
print("归一化后范围:", df['price_normalized'].min(), "-", df['price_normalized'].max())
print("标准化后均值:", df['price_standardized'].mean(), "标准差:", df['price_standardized'].std())
避坑指南:我曾经犯过一个错误——先做了标准化,然后才拆分训练集和测试集。结果测试集的信息「泄露」到了训练集里,模型评估虚高。正确的做法是:先拆分,再对训练集fit,然后用训练集的参数transform测试集。
4.5 清洗流程串联——一条龙搞定
好了,四个步骤都讲完了。在实际项目中,我不会一个一个单独做,而是写一个清洗流水线:
def clean_chip_data(df):
"""芯片海关数据清洗流水线"""
# 1. 缺失值处理
df = df.dropna(subset=['declaration_no', 'chip_model']) # 关键字段不能空
df['unit_price'].fillna(df['unit_price'].median(), inplace=True)
df['quantity'].fillna(df['quantity'].median(), inplace=True)
# 2. 异常值检测(业务规则优先)
df = df[(df['unit_price'] >= 0.1) & (df['unit_price'] <= 10000)]
df = df[df['quantity'] > 0]
# 3. 重复数据去重
df = df.drop_duplicates(subset=['declaration_no', 'chip_model'], keep='last')
# 4. 数据标准化
scaler = MinMaxScaler()
df[['unit_price', 'quantity', 'weight']] = scaler.fit_transform(
df[['unit_price', 'quantity', 'weight']]
)
return df
# 执行清洗
df_clean = clean_chip_data(df)
print(f"清洗完成,剩余 {len(df_clean)} 条有效数据")
嗯,到这里,数据清洗这一关就算过了。你想想看,原始数据就像一块璞玉,清洗就是雕琢的过程。雕好了,后面建模、分析都顺风顺水;雕不好,再好的算法也白搭。
我个人觉得,数据清洗是最能体现一个数据工程师功力的地方。代码谁都会写,但能不能根据业务场景选对方法,这才是真本事。