4-2 异常值的影像判定:离群点怎么看 本节摘要:describe与分位数负责先看片,IQR与Z分数是两套判读标准,判出可疑之后的处置是修、留、标三条路。本节的核心观点:技术只能圈出可疑值,定性必须回到业务——异常值不等于错误值。 类型转好了(4-1),影像科拿到一张金额分布的片子。有一个12万的数字混在几百几千的单子里,是拍片时的伪影,还是真的有个大客户? 先看片:describe读片法 看片要点:mean与50%(中位数)严重背离,说明分布被极端值拉歪。均值会说谎,中位数不会——这是读片的第一句口诀。 判读标准一:IQR围栏 IQR不假设分布,靠四分位数撑起围栏,对 skewed 数据稳。1.5倍是惯例,可按业务收紧或放宽。
本节摘要:describe与分位数负责先看片,IQR与Z分数是两套判读标准,判出可疑之后的处置是修、留、标三条路。本节的核心观点:技术只能圈出可疑值,定性必须回到业务——异常值不等于错误值。
类型转好了(4-1),影像科拿到一张金额分布的片子。有一个12万的数字混在几百几千的单子里,是拍片时的伪影,还是真的有个大客户?
import pandas as pd amounts = pd.Series([880, 960, 1280, 1350, 1732, 2100, 11800, 120000]) print(amounts.describe()) # count 8.000000 # mean 15150.250000 ← 均值被12万拖着走 # std 40742.982006 # min 880.000000 # 25% 1235.000000 # 50% 1541.000000 ← 中位数还在正常区间 # 75% 4375.000000 # max 120000.000000 print(amounts.quantile([0.01, 0.05, 0.95, 0.99])) # 0.01 915.2 # 0.05 1028.0 # 0.95 82330.0 # 0.99 109646.0
看片要点:mean与50%(中位数)严重背离,说明分布被极端值拉歪。均值会说谎,中位数不会——这是读片的第一句口诀。
q1, q3 = amounts.quantile([0.25, 0.75]) iqr = q3 - q1 lower, upper = q1 - 1.5 * iqr, q3 + 1.5 * iqr print(f'正常区间:{lower:.0f} 到 {upper:.0f}') # 正常区间:-2123 到 7733 suspects = amounts[(amounts < lower) | (amounts > upper)] print(suspects.tolist()) # [11800, 120000]
IQR不假设分布,靠四分位数撑起围栏,对 skewed 数据稳。1.5倍是惯例,可按业务收紧或放宽。
z = (amounts - amounts.mean()) / amounts.std() print(amounts[z.abs() > 2].tolist()) # [120000] ← 只有12万过线,1.18万没过
Z分数问"离均值几个标准差",适合接近正态的数据。它的软肋在这张片子上暴露无遗:极端值自己把标准差撑大了,反而掩护了次极端的11800。销售金额这类右偏数据,我更信任IQR。
df = pd.DataFrame({'单号': ['T1', 'T2', 'T3', 'T4'], '金额': [960, 1732, 11800, 120000]}) # 出路一:修——有证据的录入错误(多打一个零) df.loc[df['单号'] == 'T4', '金额'] = 12000 # 出路二:留——真实大单,分析口径需要它(算总销售额就离不开) total = df['金额'].sum() print(total) # 26492 # 出路三:标——不动原值,加标记列让下游自选口径 q1, q3 = df['金额'].quantile([0.25, 0.75]) fence = q3 + 1.5 * (q3 - q1) df['离群标记'] = df['金额'] > fence print(df) # 单号 金额 离群标记 # 0 T1 960 False # 1 T2 1732 False # 2 T3 11800 True # 3 T4 12000 True
⚠️ 常见坑:见异常就删是数据分析里最粗暴的事故源。删掉真实大单,门店销售额排名直接改写。默认出路是"标"——保留原值加标记,让每个分析口径自己决定包不包含。
💡 关键直觉:中位数与IQR统计量本身就是"稳健派",聚合想免疫离群值,直接用median代替mean,比逐个清洗更省事。

除了修留标三条路,统计建模圈还有两种温和处置在业界很常用。修剪(trim)直接掐头去尾:把低于1%分位和高于99%分位的值切掉,适合"极端值对模型干扰大、且业务上可以接受舍弃尾部"的场景;盖帽(winsorize)不删行,而是把超出分位围栏的值压到围栏上:12万被压成99%分位数的水平,信息保留、量级受控。两者的共同点是阈值必须先于处理确定并留档,否则就成了"看结果调阈值"的自欺。
# 盖帽:右尾压到95%分位 cap = amounts.quantile(0.95) winsorized = amounts.clip(upper=cap) print(amounts.max(), '→', winsorized.max()) # 120000 → 82330 # 修剪:两端各切5% lo, hi = amounts.quantile([0.05, 0.95]) trimmed = amounts[(amounts >= lo) & (amounts <= hi)] print(len(amounts), '→', len(trimmed)) # 8 → 8(本例样本小,两端值恰好都在界内)
一次完整的异常判定该留下什么?三样东西:围栏参数(方法与倍数)、可疑清单(哪些单号、金额多少、距围栏多远)、处置决定(每条的修留标及理由)。下面这段代码就是最小可用的报告生成器:
def outlier_report(s, k=1.5): q1, q3 = s.quantile([0.25, 0.75]) fence = q3 + k * (q3 - q1) suspects = s[s > fence] return {'方法': f'IQR {k}倍', '上围栏': round(fence, 1), '可疑数': len(suspects), '明细': suspects.to_dict()} print(outlier_report(pd.Series([880, 960, 1280, 1350, 1732, 2100, 11800, 120000]))) # {'方法': 'IQR 1.5倍', '上围栏': 7732.5, '可疑数': 2, # '明细': {6: 11800, 7: 120000}}
参数、名单、决定三样齐了,这份片子才算读完了。
下一节进整形科:文本列的空格、大小写与格式统一。