卷 III · 备料CH 10工位 10/24

异常、重复、不一致:三种判据打出三个答案

工位 STEP 3.2 清洗数据(续) 15% 的核心 评分员在找:「我们删掉了异常值」是扣分句。要写成「用 X 判据标出 N 个,逐个判断后 M 个是录入错误已修正、K 个是真实极值予以保留」。

这一章处理 2.4 清单上剩下的四类问题。它们比缺失值更危险,原因是缺失值会明晃晃地报错,而一个大小写不一致的类别列、一条重复行、一个多打了三个零的数字,全都能顺利通过所有代码,最后只表现为「模型效果差了一点」——而你永远不会知道那一点是从哪来的。

IQR / z-score / MAD掩蔽效应重复行类别规范化

三种判据,三个答案

下面这台判据台在示例数据的 gdp_pc 列上跑了三种常见的异常判据。默认显示的是原始数据(含那 4 个多打了三个零的值):

结果是这样的:

判据公式在原始 gdp_pc 上标出正常区间上界
IQR 法[Q1 − 1.5·IQR, Q3 + 1.5·IQR]21 个(4.3%)4 518
z-score|x − 均值| > 3·标准差2 个(0.4%)1 221 255
稳健 z(MAD)|x − 中位数| > 3·1.4826·MAD29 个(6.0%)4 063

看 z-score 那一行的上界:122 万美元。也就是说,按 3σ 法则,一个人均 GDP 一百万美元的地区是「正常」的。

原因很直白:那四个错值把标准差撑到了 398 901(正常值的标准差只有 1 509)。而 z-score 的分母正是标准差——异常值把用来发现异常值的那把尺子弄坏了。这个现象叫掩蔽效应(masking),它是「不要用 z-score 做首轮异常检测」的全部理由。

IQR 和 MAD 不吃这一套,因为它们的分母是分位数和中位数——把最大的那几个值改成一万亿,Q3 和中位数一动不动。这叫稳健统计量。

◆ 一个可以写进报告的判据选择顺序
  1. 先用「合法范围」这把尺子。它不是统计判据,是你在 2.2 数据字典里写下的领域知识:疫苗覆盖率必须在 0–100,识字率必须在 0–100,人均 GDP 在这个数据集的语境下不可能超过 20 000。这一轮抓到的东西是「错误」,不是「异常」。
  2. 再用 IQR 或 MAD 做统计筛查。它们对已有的极端值不敏感,适合首轮。
  3. 清洗之后,可以用 z-score 做复查。此时标准差已经不被污染(清洗后 z-score 在同一列上标出 7 个,1.5%,是个合理的数)。
  4. 最后逐个人工判断:修、留、还是删。这一步没法自动化,也正是 Step 3 的 15% 想买的东西。

修、留、删:三种处置,三种写法

处置什么情况示例报告里怎么写
能确定是录入错误,且能推断正确值gdp_pc 4 行超中位数 100 倍 → 判为量纲错,÷1000「4 行人均 GDP 超出中位数 100 倍以上,且除以 1000 后落入该地区历史区间,判为量纲录入错误并据此修正(受影响行 id 见附录)。」
是真实极值,业务上完全可能清洗后仍有 18 个 IQR 异常(3.8%)——沿海富裕地区「其余 18 个统计异常经核对为真实极值(沿海地区人均 GDP 达 16 869 美元),予以保留;因该列右偏(偏度 3.39),在 Step 4 采用对数变换以降低其杠杆作用。」
既不能修也不能信,且数量极少(本示例无此类)「删除 N 行(占 X%),原因:目标变量缺失且无法补全。已确认删除后各特征分布无显著变化(对照表见附录)。」

「删」是最后手段,而且必须证明删完之后分布没变。删行的危险和上一章讲的一样:如果被删的行有共同特征,你就在无意中改变了样本的构成。

⚠ 那句会被扣分的话

「我们使用 IQR 法检测并删除了异常值。」

三个问题:① 没说删了多少;② 没说判断过它们是错误还是真实极值;③ 把统计判据当成了决定权——IQR 只是提示,决定权在领域判断。

改法就是上面表格第四列那种句子。模板:判据 + 数量 + 逐类处置 + 理由 + 影响评估。

重复行:三种「重复」不是一回事

类型怎么查处置
完全重复(所有列一致)df.duplicated().sum()直接去重,保留第一条
业务键重复(除 id 外一致,或主键重复)df.duplicated(subset=['region','year']).sum()这才是真正要查的。示例数据里 7 行属于此类(导出时被写了两遍)
近似重复(同一实体的两条略有差异的记录)按业务键分组后看组内差异最麻烦。要决定用哪条(最新?最完整?)并写明规则

为什么业务键重复更重要:如果你的数据里「北区 2019 年」出现了两次,它会同时进入训练集和测试集——测试集里躺着一条训练集见过的记录,测试分数因此虚高。这是「泄漏」的一种,第 13 章会正式点名。

去重要写清三件事:按什么键判重(不是「所有列」而是业务键)、去掉了几条、保留规则是什么。示例数据:按 (region, year, 各指标值) 判重,去掉 7 条,保留第一条,487 → 480 行。

不一致:那些让唯一值从 6 变成 18 的空格

示例数据里 region 只该有 6 个取值,实际有 18 个——因为 128 处大小写/尾随空格不一致。record_date94 处用了 DD/MM/YYYY 而不是 ISO 格式。

这类问题的处理很机械,但顺序有讲究:

# 类别列规范化:一行代码,四个动作
df['region'] = (df['region']
                .str.strip()          # 去首尾空格
                .str.lower()          # 统一小写
                .str.replace(r'\s+', ' ', regex=True)   # 内部多空格压成一个
                .str.title())         # 再统一成 Title Case

# 验证:唯一值必须回到预期数量
assert df['region'].nunique() == 6, df['region'].value_counts()

# 日期列:先统一格式,再转类型
df['record_date'] = pd.to_datetime(df['record_date'],
                                   format='mixed', dayfirst=True, errors='coerce')
assert df['record_date'].isna().sum() == 0   # 转失败的会变 NaT,必须为 0

那两个 assert 是这段代码里最重要的部分。规范化本身不会失败,失败的是你以为规范化完了——多一个错别字(「Nrth」)、多一种日期格式(2019年3月),代码照样跑完,问题留到 Step 6。写下预期数量,让代码替你检查。

✎ 一个五行的清洗自检函数,四条产线都能用

把预期写成断言,每次清洗后跑一遍。它会在 Step 3 就抓住那些本来要到 Step 8 才暴露的问题:

def audit(df, expect_rows=None):
    print('行数         ', len(df))
    print('重复业务键   ', df.duplicated(subset=['region', 'year']).sum())
    print('缺失格子     ', int(df.isna().sum().sum()))
    print('region 唯一值', sorted(df['region'].unique()))
    for c, (lo, hi) in {'vaccine': (0, 100), 'literacy': (0, 100),
                        'water': (0, 100), 'gdp_pc': (0, 20000)}.items():
        bad = ((df[c] < lo) | (df[c] > hi)).sum()
        print(f'{c:10s} 越界 {bad}')
    if expect_rows: assert len(df) == expect_rows

audit(df_clean, expect_rows=480)

它的输出可以直接截图当 3.2 的证据。「清洗前 audit 输出」和「清洗后 audit 输出」并排两张图,比一千字描述有力。

⇄ 三条产线上的清洗节点
ISASOSASBDAS
去重Distinct 节点(可指定键)drop_duplicates(subset=...)dropDuplicates(['region','year'])
类别规范化Filler / Derive 节点 + lowercase()trim().str 系列trim(lower(col('region'))) + initcap
条件替换Filler 节点np.where / maskwhen(...).otherwise(...)
异常筛查Data Audit 的 Outliers & Extremes 页签(自带 IQR 与 z 两套scipy.stats.zscore、手写 IQRapproxQuantile('gdp_pc',[0.25,0.75],0.01)
Kettle(OSAS 里)Kettle/Spoon 适合做的正是这一章:「Unique rows」、「Replace in string」、「Data validator」三个 step 就能搭出一条可视化的清洗管道。OSAS 那次用它做清洗、用 Python 做建模,是最省事的分工,也让你有话写「为什么用了 Kettle」。
▣ 本章交付物 —— 报告里放什么

1. 异常检测小节:三判据对照表(IQR / z / MAD 各标出多少)+ 掩蔽效应那一句解释 + 逐类处置(修 M 个、留 K 个、删 N 个)+ 每类的理由。

2. 去重小节:判重键、去掉条数、保留规则、去重前后行数。

3. 不一致小节:类别列唯一值「清洗前 18 → 清洗后 6」、日期格式统一处数、以及 audit 函数的前后两张输出截图

4. 一句给 Step 7 的伏笔:「业务键重复已在 3.2 去除,以避免同一记录同时出现在训练集与测试集(见 7.1)。」

这一章的一句话

同一列数据,IQR 标出 21 个、z-score 只标出 2 个、MAD 标出 29 个——因为异常值会撑大标准差,把用来发现异常值的尺子弄坏;正确的顺序是先用领域知识的「合法范围」抓错误,再用稳健判据筛查,然后逐个决定修、留、还是删,并把这个判断过程写出来。

下一章是 Step 3 最有创造性的部分:构造新列。取一次对数就能把相关系数从 −0.674 提到 −0.864,但它对模型精度的提升只有 0.005——为什么还值得做?