异常、重复、不一致:三种判据打出三个答案
这一章处理 2.4 清单上剩下的四类问题。它们比缺失值更危险,原因是缺失值会明晃晃地报错,而一个大小写不一致的类别列、一条重复行、一个多打了三个零的数字,全都能顺利通过所有代码,最后只表现为「模型效果差了一点」——而你永远不会知道那一点是从哪来的。
三种判据,三个答案
下面这台判据台在示例数据的 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·MAD | 29 个(6.0%) | 4 063 |
看 z-score 那一行的上界:122 万美元。也就是说,按 3σ 法则,一个人均 GDP 一百万美元的地区是「正常」的。
原因很直白:那四个错值把标准差撑到了 398 901(正常值的标准差只有 1 509)。而 z-score 的分母正是标准差——异常值把用来发现异常值的那把尺子弄坏了。这个现象叫掩蔽效应(masking),它是「不要用 z-score 做首轮异常检测」的全部理由。
IQR 和 MAD 不吃这一套,因为它们的分母是分位数和中位数——把最大的那几个值改成一万亿,Q3 和中位数一动不动。这叫稳健统计量。
- 先用「合法范围」这把尺子。它不是统计判据,是你在 2.2 数据字典里写下的领域知识:疫苗覆盖率必须在 0–100,识字率必须在 0–100,人均 GDP 在这个数据集的语境下不可能超过 20 000。这一轮抓到的东西是「错误」,不是「异常」。
- 再用 IQR 或 MAD 做统计筛查。它们对已有的极端值不敏感,适合首轮。
- 清洗之后,可以用 z-score 做复查。此时标准差已经不被污染(清洗后 z-score 在同一列上标出 7 个,1.5%,是个合理的数)。
- 最后逐个人工判断:修、留、还是删。这一步没法自动化,也正是 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_date 有 94 处用了 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 输出」并排两张图,比一千字描述有力。
| ISAS | OSAS | BDAS | |
|---|---|---|---|
| 去重 | Distinct 节点(可指定键) | drop_duplicates(subset=...) | dropDuplicates(['region','year']) |
| 类别规范化 | Filler / Derive 节点 + lowercase()、trim() | .str 系列 | trim(lower(col('region'))) + initcap |
| 条件替换 | Filler 节点 | np.where / mask | when(...).otherwise(...) |
| 异常筛查 | Data Audit 的 Outliers & Extremes 页签(自带 IQR 与 z 两套) | scipy.stats.zscore、手写 IQR | approxQuantile('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——为什么还值得做?