2.2 异常、重复与实体一致性:识别可疑记录而不是自动删除
阿花在战报中发现温度 999°C 和两笔相似订单,但管理员不允许把“看着可疑”直接变成删除动作。
温度 999°C 可能超过设备量程,也可能是缺失占位符;支付金额突然增长十倍可能是攻击,也可能是促销日。异常检测首先产生调查候选,不直接产生删除命令。
本课目标
- 区分无效值、统计离群点和真正异常事件;
- 正确使用 IQR、稳健分数和分组基线;
- 区分重复投递、业务重复和实体重复;
- 用确定性 survivorship 规则合并记录并保留审计轨迹。
1. 三类“异常”不要混用
| 类别 | 判断依据 | 示例 | 常见处理 |
|---|---|---|---|
| 无效值 | 违反契约或物理边界 | 设备量程上限 150,却记录 999 | 隔离、置缺失、修源头 |
| 统计离群点 | 相对分布罕见 | 收入位于顶部 0.1% | 标记、稳健建模、核查 |
| 业务异常 | 与风险或事件定义相关 | 同账户一分钟百次转账 | 调查、告警、保留证据 |
离群不等于错误,正常范围内也可能有欺诈。删除罕见值会让系统失去最值得研究的事件。
2. 领域约束优先
invalid_temperature = ~frame["temperature_c"].between(-50, 150)
invalid_duration = frame["ended_at"] < frame["started_at"]
invalid_status = ~frame["status"].isin({"ok", "warning", "failed"})这些规则需要版本和适用条件:不同传感器型号量程不同,某些时钟校正可能产生暂时负 duration。把条件写进规则配置并记录命中原因。
跨字段规则往往比单列范围更有价值:结束时间不得早于开始时间,completed 状态必须有完成时间,货币与金额精度要匹配。
3. IQR 规则只标记候选
$$ IQR=Q_3-Q_1, $$
常见围栏:
$$ [Q_1-1.5IQR,\ Q_3+1.5IQR]. $$
series = frame["measurement"].dropna()
q1, q3 = series.quantile([0.25, 0.75])
iqr = q3 - q1
lower = q1 - 1.5 * iqr
upper = q3 + 1.5 * iqr
frame["measurement_iqr_flag"] = ~frame["measurement"].between(lower, upper)1.5 是惯例,不是异常真值。强偏态、多峰和小样本会产生大量误标。若 $IQR=0$,规则尤其需要单独处理。
全局基线还可能掩盖群体差异。数据来自多个设备时,应先按型号、地点或运行模式分组,在样本量足够且不会碎片化过度的前提下建立基线。
4. 时间序列异常要看上下文
时序数据需要考虑:
- 趋势与季节性;
- 采样间隔变化;
- 设备重启和校准;
- 突变、持续偏移和孤立尖峰;
- 迟到或乱序记录。
滚动中位数和 MAD 可提供稳健局部基线,但窗口不能包含未来数据用于在线告警。历史回看与实时检测应分别实现,避免前视偏差。
5. 截尾与变换会改变问题
lower, upper = training["amount"].quantile([0.01, 0.99])
transformed = frame.assign(
amount_clipped=frame["amount"].clip(lower=lower, upper=upper),
amount_was_clipped=~frame["amount"].between(lower, upper),
)Winsorization 会把不同极端值压成边界值。它可能稳定某些模型,却不适合审计、风险检测或尾部估计。阈值必须只从训练数据拟合,并保留原值与标记。
对数变换可压缩右偏分布,但零、负值和解释尺度需要处理。变换不是“清洗掉异常”,而是改变模型表示。
6. 完全重复只是最简单情况
exact_duplicates = frame.duplicated(keep=False)完全相同行可能来自文件拼接、重复导出,也可能是合法的两次同值测量。没有 event ID 或业务键,无法只凭整行相同判断。
重复类型:
- delivery duplicate:同一 event ID 被重投;
- version duplicate:同一实体多个更新时间版本;
- business duplicate:用户重复提交同一业务动作;
- entity duplicate:不同 ID 实际指向同一人或对象。
每一类需要不同规则和证据。
7. 确定性去重需要总排序
ordered = frame.sort_values(
["mission_id", "sensor_id", "event_time", "ingested_at", "event_id"]
)
latest = ordered.drop_duplicates(
subset=["mission_id", "sensor_id"],
keep="last",
)只按 timestamp 排序可能在时间相同时得到不稳定结果。加入唯一 event ID 形成确定性 tie-breaker。选择“最新”也必须符合语义:事件事实可能应全部保留,当前状态表才选最新版本。
去重输出应带:
- canonical record ID;
- 被合并记录列表;
- 选择规则版本;
- 合并时间与 run ID;
- 字段级来源。
这样规则变更后才能解释为何某条记录消失。
8. 实体解析不是 drop_duplicates
姓名、地址相似并不证明是同一实体。实体解析可能使用规范化、阻塞、相似度和人工复核,并评估 false merge 与 false split。
错误合并两个真实用户通常比漏合并风险更高,尤其涉及财务、医疗或访问权限。阈值应按业务成本决定,不能只最大化总体准确率。
规范化也要谨慎:统一大小写、空白和 Unicode 形式可能有助匹配,却不能无条件改写需要原样展示或具有区域语义的字段。保留原值,另建 normalized key。
常见误区
- IQR 外的值就是错误:它只是相对分布的候选标记。
- 截尾不会影响结论:尾部风险、均值和回归关系都会变化。
- 整行相同一定是重复:没有事件身份就无法区分重复投递与两次相同观测。
- 保留最新记录总是正确:事件表与当前态表的语义不同。
练习
- 为温度数据分别写无效值、离群值和业务告警规则。
- 构造一个全局 IQR 误伤某设备型号正常数据的例子。
- 为时间相同的两条版本记录设计确定性 tie-breaker。
- 给实体解析定义 false merge 与 false split 的业务成本。
小结
异常规则回答“值得调查什么”,契约规则回答“什么不可能合法”,去重规则回答“哪些记录代表同一事实”。三者不能用一个 drop 操作替代。所有变换都应保留原值、原因和规则版本。
下一课把这些单点规则组装成可重跑的清洗管道,并用不变量、隔离区和差异报告验证清洗没有悄悄改坏数据。