4、策略二:数据清洗与预处理:缺失值处理、异常值检测、重复数据去重、格式统一化、数据脱敏
数据清洗,说白了就是给数据「洗澡」。脏数据进,干净数据出,这个道理谁都懂。但真正动手做的时候,你会发现坑比想象的多得多。
我个人习惯把清洗分成五个步骤:缺失值、异常值、重复值、格式、脱敏。顺序很重要,别乱跳。下面我一个一个说。
4.1 缺失值处理:别急着删
缺失值是最常见的问题。你想想看,一个用户注册表单,手机号没填、邮箱没填、地址没填——这种数据能直接用吗?肯定不行。
处理缺失值,我一般分三步走:
- 先诊断缺失率——缺失超过 70% 的字段,直接考虑删除整列。
- 再判断缺失机制——是完全随机缺失,还是跟其他字段有关?
- 最后选方法——删除、填充、还是用模型预测。
我在项目中遇到过一件事:某电商订单表,收货地址缺失率只有 5%。团队直接删了那 5% 的记录。结果呢?那些订单全是高客单价的大客户,删除后销售额统计直接偏了。所以啊,别上来就删,先看看缺失的数据长什么样。
常用的填充方法,我整理了一张表:
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 删除记录 | 缺失率低(<5%)且随机 | 简单直接 | 可能丢失信息 |
| 均值/中位数填充 | 数值型、低缺失率 | 计算快 | 会降低方差 |
| 众数填充 | 类别型字段 | 保持分布 | 可能引入偏差 |
| 前向/后向填充 | 时间序列数据 | 利用时序信息 | 不适用于无序数据 |
| 模型预测填充 | 缺失率较高、字段间关联强 | 精度高 | 计算成本高 |
# Python 示例:中位数填充
import pandas as pd
df['age'].fillna(df['age'].median(), inplace=True)
# 类别型字段用众数
df['gender'].fillna(df['gender'].mode()[0], inplace=True)
4.2 异常值检测:别把「特殊」当「错误」
异常值检测,是我踩坑最多的环节。为什么?因为「异常」和「特殊」有时候很难分清楚。
我记得有一次做金融风控数据清洗,一个用户的月收入填了 9999999。团队直接判定为异常值删掉了。后来一查,那是个真实的高净值客户,年收入确实过亿。嗯,从那以后我学乖了——异常值要先分两类:
- 自然异常:数据本身没错,只是偏离常规(比如亿万富翁的收入)
- 错误异常:数据录入或采集出错(比如年龄填了 200 岁)
检测方法我常用三种:
- Z-Score 法:适合正态分布的数据,Z 值超过 3 的算异常。
- IQR 四分位距法:适合偏态分布,Q1 - 1.5*IQR 以下或 Q3 + 1.5*IQR 以上算异常。
- 业务规则法:比如年龄不能超过 120,温度不能低于 -273.15。
# IQR 法检测异常值
Q1 = df['amount'].quantile(0.25)
Q3 = df['amount'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = df[(df['amount'] < lower_bound) | (df['amount'] > upper_bound)]
4.3 重复数据去重:别只看「一模一样」
重复数据,听起来简单——两行完全一样,删掉一行就行。但现实往往更复杂。
我曾经处理过一个客户数据表,两条记录看起来一模一样,但仔细看,一条的邮箱是 zhang@example.com,另一条是 zhang@examp1e.com(数字 1 代替了字母 l)。这种叫「近似重复」,肉眼很难发现。
去重策略我分三个层次:
- 完全去重:所有字段完全一致,直接删除。
- 关键字段去重:比如身份证号、手机号相同,即使其他字段不同也视为重复。
- 模糊去重:用编辑距离(Levenshtein Distance)或 Jaccard 相似度,找出近似重复。
# 完全去重
df.drop_duplicates(inplace=True)
# 按关键字段去重(保留第一条)
df.drop_duplicates(subset=['id_card'], keep='first', inplace=True)
# 模糊去重(使用 recordlinkage 库)
import recordlinkage
indexer = recordlinkage.Index()
indexer.block('city')
pairs = indexer.index(df, df)
compare = recordlinkage.Compare()
compare.string('name', 'name', method='jarowinkler', threshold=0.85)
matches = compare.compute(pairs)
4.4 格式统一化:细节决定成败
格式问题,看着小,但影响巨大。你想想看,一个日期字段,有人写 2024-01-15,有人写 01/15/2024,还有人写 2024年1月15日。这种数据怎么聚合?怎么排序?
我个人习惯做以下几件事:
- 日期格式统一:全部转成 ISO 8601 标准(YYYY-MM-DD)
- 字符串标准化:去除首尾空格、统一大小写、全角半角转换
- 编码统一:全部转成 UTF-8,避免乱码
- 单位统一:比如重量全部转成 kg,金额全部转成元
我在项目中遇到过最头疼的事:一个跨国电商的数据,价格字段有的用美元,有的用欧元,有的用日元。而且没有标注货币单位。最后只能靠 IP 地址反推用户所在地,再匹配汇率换算。那叫一个折腾。
# 日期格式统一
df['date'] = pd.to_datetime(df['date'], format='mixed').dt.strftime('%Y-%m-%d')
# 字符串清洗
df['name'] = df['name'].str.strip().str.lower()
# 全角转半角
import unicodedata
df['text'] = df['text'].apply(lambda x: unicodedata.normalize('NFKC', x))
4.5 数据脱敏:安全第一
数据脱敏,说白了就是「把敏感信息藏起来,但保留分析价值」。这是数据清洗的最后一道工序,也是合规的底线。
常用的脱敏方法:
| 方法 | 说明 | 示例 |
|---|---|---|
| 掩码 | 保留部分字符,其余用 * 代替 | 138****1234 |
| 泛化 | 降低精度,保留统计特征 | 具体年龄 → 年龄段(20-30) |
| 替换 | 用随机值替换真实值 | 真实姓名 → 随机姓名 |
| 加密 | 可逆,但需要密钥 | AES 加密身份证号 |
| 差分隐私 | 添加噪声,保护个体 | 统计查询时加随机扰动 |
# 手机号掩码
def mask_phone(phone):
return phone[:3] + '****' + phone[7:]
df['phone'] = df['phone'].apply(mask_phone)
# 年龄泛化
def generalize_age(age):
if age < 18: return '0-17'
elif age < 30: return '18-29'
elif age < 45: return '30-44'
elif age < 60: return '45-59'
else: return '60+'
df['age_group'] = df['age'].apply(generalize_age)
好了,数据清洗的五个策略就讲到这里。记住一个原则:清洗不是目的,让数据可用才是。每一步操作,都要问自己——这个操作会不会引入新的偏差?会不会丢失重要信息?想清楚了再动手。