异常值识别:先看走势,再定去留
上周处理一份日活数据时,遇到一个刺眼的点:某天日活突然从12万跳到47万,次日又落回13万。团队第一反应是“删掉”,但删之前,先问了一句:这个点是错误,还是真实发生的事件?异常值识别,难的不是计算,而是判断。
先看数据长什么样。一组100个用户的月消费数据,均值320元,标准差80元。按Z-score大于3算异常,阈值是560元。可如果数据里真有两位年消费超600元的高价值用户,他们会被直接标红。问题在于,这100人里大多数月消费在200到400元之间,分布右偏。此时Z-score的假设——正态分布——已经不成立。用IQR更稳。把数据从小到大排,取25%分位(Q1)和75%分位(Q3),IQR等于Q3减Q1。低于Q1减1.5倍IQR,或高于Q3加1.5倍IQR,算异常。上面那组数据,Q1约260元,Q3约380元,IQR为120元,上限是560元,和Z-score结果接近,但IQR不依赖均值和标准差,对偏态更宽容。
不过IQR也有边界。样本量小于30时,分位数本身就不稳定。曾见过一份只有14天的日订单数据,Q1和Q3几乎贴着中位数,结果把正常的周末高峰全标成了异常。这种情况下,MAD(绝对中位差)更合适。它先算每个点与中位数的距离,再取这些距离的中位数。MAD乘1.4826可近似标准差,但受极端值影响小。一个具体例子:数据为[10, 12, 12, 13, 14, 15, 100],中位数13,绝对偏差为[3,1,1,0,1,2,87],MAD是1。按MAD的3倍阈值,100会被识别,而10到15都保留。
多维数据要换思路。用户行为数据往往有十几个字段:登录次数、停留时长、点击深度、分享次数。单看某一维正常,组合起来却异常。比如一个账号日登录200次,停留时长却只有3秒,点击深度为1。孤立森林(Isolation Forest)适合这种场景。它随机选特征和切分点,异常点更容易被“孤立”——路径更短。实践中,设contamination为0.05,即假设5%为异常。用Python的sklearn,几行代码就能跑出每个样本的异常分数。但要注意,孤立森林对高维稀疏数据敏感,特征先做归一化,否则量纲大的字段会主导切分。
识别出来之后,去留才是关键。我们内部有个三步判断:第一步,查采集链路。某次异常是埋点SDK版本升级导致重复上报,这种直接修。第二步,看走势是否可持续。如果连续三天以上偏离,且幅度逐步扩大,可能是真实趋势变化,比如渠道投放带来了新用户群。第三步,问业务方。2023年双十一,某电商的退款率数据出现一个尖峰,算法标为异常,但业务确认是促销后集中退款,属于正常波动,不应剔除。
时间窗口的选择也会改变结论。用30天滚动窗口算Z-score,和用7天窗口,结果可能完全不同。某API调用量数据,按7天窗口,周末的低谷会被标异常;按30天窗口,周末低谷被均摊,反而不显著。建议至少跑两个窗口:短窗口捕捉突变,长窗口观察趋势。两个窗口都标异常的点,才值得优先处理。
最后,别把异常值当敌人。一份电商销售数据里,某品类日销售额突然翻倍,排查后发现是竞品下架带来的短期转移。这个异常点,后来成了预测该品类走势的重要信号。异常值识别是工具,不是目的。记录每次判断的理由,比单纯删掉一个点更有价值。
总结几条实践清单:样本量小于30用MAD,单变量偏态用IQR,多维组合用孤立森林;先查采集,再看走势,最后问业务;至少跑两个时间窗口;所有剔除操作留日志。数据清洗没有万能公式,但有一套可重复的观察流程,比凭感觉删点可靠得多。