4、策略二:数据清洗与预处理:缺失值处理、异常值检测、重复数据去重、格式统一化、数据脱敏

数据清洗,说白了就是给数据「洗澡」。脏数据进,干净数据出,这个道理谁都懂。但真正动手做的时候,你会发现坑比想象的多得多。

我个人习惯把清洗分成五个步骤:缺失值、异常值、重复值、格式、脱敏。顺序很重要,别乱跳。下面我一个一个说。

核心原则:清洗不是「删掉完事」,而是「理解数据后做合理决策」。每删一条记录,都要问自己:我为什么删它?
数据清洗与预处理五步流程 原始数据 ① 缺失值处理 ② 异常值检测 ③ 重复数据去重 ④ 格式统一化 ⑤ 数据脱敏 避坑提醒 顺序不要乱调 先修缺失再查异常 脱敏放在最后一步

4.1 缺失值处理:别急着删

缺失值是最常见的问题。你想想看,一个用户注册表单,手机号没填、邮箱没填、地址没填——这种数据能直接用吗?肯定不行。

处理缺失值,我一般分三步走:

  1. 先诊断缺失率——缺失超过 70% 的字段,直接考虑删除整列。
  2. 再判断缺失机制——是完全随机缺失,还是跟其他字段有关?
  3. 最后选方法——删除、填充、还是用模型预测。

我在项目中遇到过一件事:某电商订单表,收货地址缺失率只有 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 岁)

检测方法我常用三种:

  1. Z-Score 法:适合正态分布的数据,Z 值超过 3 的算异常。
  2. IQR 四分位距法:适合偏态分布,Q1 - 1.5*IQR 以下或 Q3 + 1.5*IQR 以上算异常。
  3. 业务规则法:比如年龄不能超过 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 加密身份证号
差分隐私 添加噪声,保护个体 统计查询时加随机扰动
合规红线:身份证号、手机号、银行卡号、家庭住址、生物识别信息——这些字段必须脱敏。GDPR 和《个人信息保护法》可不是闹着玩的。我见过有公司因为脱敏不到位,被罚了全年营收的 4%。
# 手机号掩码
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)

好了,数据清洗的五个策略就讲到这里。记住一个原则:清洗不是目的,让数据可用才是。每一步操作,都要问自己——这个操作会不会引入新的偏差?会不会丢失重要信息?想清楚了再动手。