第12讲:异常值检测与处理、数据标准化与归一化

数据挖掘与R语言

2026年11月24日

本讲导览

  • 一、前课测验——四道小题(8 min)
  • 二、问题导入——一个错录的 15000 绑架了均值(8 min)
  • 三、识别——箱线图 / IQR 与 3σ / Z-score(22 min)
  • 四、处理——修正、删除/替换NA、截断、保留(16 min)
  • 五、标准化与归一化——Min-Max 与 Z-score(16 min)
  • 六、综合实战——用 airquality 走全流程(13 min)
  • 七、课堂总结——成果自查与 Exit Ticket(7 min)

重要

上节课把缺失值处理干净了——但没有缺失,不代表数据合理。今天是项目三“数据清洗”的第二课,解决两个新问题:

任务 工具 什么时候用
① 识别:箱线图 / IQR boxplot()·quantile() 偏态数据优先(财务数据的常态)
② 识别:3σ / Z-score mean()·sd() 数据接近正态、样本量较大时参考
③ 处理:修正 / 删除 / 替换NA / 截断 / 保留 filter()·if_else() 先判断“是错还是真”,再选方法
④ 标准化:Min-Max / Z-score 手算公式·scale() 量纲不统一、要放在一起比较时

一、前课测验

缺失值处理的四步流水线

1.1 选择题(第 11 讲回顾)

1. 跑完 sum(x, na.rm = TRUE) 之后,x 里原来的那些 NA 会怎样?

A. 被删除了
B. 被填成了 0
C. 还在原地,一个没少
D. 被填成了均值

2. 财务报表里“销售部”只写在每组第一行、下面几行空着——这种缺失该用哪个函数补?

A. na.omit()
B. fill()
C. 分组均值填补
D. coalesce()

1.2 填空题(第 11 讲回顾)

3. 想看一列的缺失占比,写 mean(______(x));想把缺失的行挑出来看,写 filter(______(x))。

4. 按组均值填补缺失的完整句式是:group_by(分组列) %>% mutate(col = if_else(is.na(col), ______(col, na.rm = ______), col))。

二、问题导入——一个错录的 15000 绑架了均值

先不谈怎么识别,先看看“不识别”的后果

2.1 承接上节课:一份“干净”的财务表

上节课我们把 company_data 的缺失值全部补齐了——16 家公司,营收一栏再没有 NA:

▶️ 查看代码
company_data    # 缺失已补齐:蜀汉制造、锦江生物、锦西物流填了西南组均值 1463,青城建材填了华东组均值 807
    company region revenue debt_ratio rating
1  锦城科技   华东    1200       0.45      A
2  蜀汉制造   西南    1463       0.62      B
3  天府物流   西南    1560       0.38      A
4  青羊金融   华东     760       0.71      C
5  锦江生物   西南    1463       0.55      A
6  浣花实业   华东     892       0.48      B
7  锦西商贸   西南    1340       0.66      B
8  蜀都餐饮   西南     680       0.52      C
9  青城建材   华东     807       0.44      A
10 天府冷链   西南    2100       0.58      A
11 锦江农产   华东     450       0.75      C
12 浣花纺织   华东     980       0.41      B
13 锦西物流   西南    1463       0.63      C
14 蜀汉电子   西南    1720       0.49      A
15 青羊置业   华东     560       0.68      C
16 天府食品   西南    1380       0.57      B

2.2 一封财务科的更正邮件

数据交给财务科复核,对方回了封邮件:“锦江生物的营收录错了,多打了一个 0,实际应为 1500。”

我们拿到的这份表里,它现在长这样:

▶️ 查看代码
company_data$revenue[5] <- 15000    # 锦江生物被错录成 15000(实际应为 1500)
revenue <- company_data$revenue
revenue
 [1]  1200  1463  1560   760 15000   892  1340   680   807  2100   450   980
[13]  1463  1720   560  1380

2.3 均值被绑架,中位数不动

在改回来之前,先看看这个 15000 会造成什么后果——算一下平均营收:

▶️ 查看代码
mean(revenue)                # 含错录值的"平均营收"
[1] 2022
▶️ 查看代码
mean(revenue[-5])            # 剔除锦江生物后的真实水平
[1] 1157
▶️ 查看代码
median(revenue)              # 中位数几乎不受影响
[1] 1270

重要

数据没有缺失,不代表数据“干净”。 均值对极端值毫无抵抗力——这就是今天要先学“识别异常值”的原因。而中位数纹丝不动,这解释了为什么第三节的 IQR 法(基于四分位数)比 3σ 法(基于均值和标准差)更稳健。

2.4 【即时练习 1】

  1. 把 15000 修正为实际值 1500 后,新的平均营收约是多少?(不用跑代码,用 2.3 的结果估)
  2. 为什么中位数在两种情况下都是 1270?

三、异常值的识别——箱线图 / IQR 与 3σ / Z-score

两把尺子:一把基于四分位数,一把基于均值

3.1 先看图:boxplot() 一眼定位

▶️ 查看代码
boxplot(revenue, main = "营收分布箱线图")

提示

箱线图中,超出上下须(whisker)的点会被单独画成离群点——15000 明显“飘”在箱子外面,这是最直观的识别方式。问题是:这条“须”到底画在什么位置?下一节把它算出来。

3.2 IQR 的构造——箱子两端各伸出 1.5 倍“箱长”

3.3 用代码算出上下界,圈出异常值

▶️ 查看代码
q1  <- quantile(revenue, 0.25)    # 第一四分位数:25% 的值比它小
q3  <- quantile(revenue, 0.75)    # 第三四分位数:75% 的值比它小
iqr <- q3 - q1                    # 四分位距(也可直接写 IQR(revenue))

lower <- q1 - 1.5 * iqr           # 下界
upper <- q3 + 1.5 * iqr           # 上界
c(lower, upper)
 25%  75% 
-243 2525 
▶️ 查看代码
company_data %>% filter(revenue < lower | revenue > upper) %>% select(company, revenue)
   company revenue
1 锦江生物   15000

重要

IQR 反映的是数据“中间 50%”的离散程度,不受两端极端值本身的影响——哪怕 15000 错成 150000,Q1、Q3、上下界都纹丝不动。1.5 倍 IQR 是最常用的经验系数,能覆盖绝大多数正常波动。

3.4 3σ 原则与 Z-score——另一把尺子

▶️ 查看代码
m <- mean(revenue)
s <- sd(revenue)
z_scores <- (revenue - m) / s     # Z-score:每个值距离均值有几个标准差
round(z_scores, 2)
 [1] -0.24 -0.16 -0.13 -0.36  3.72 -0.32 -0.20 -0.38 -0.35  0.02 -0.45 -0.30
[13] -0.16 -0.09 -0.42 -0.18
▶️ 查看代码
revenue[abs(z_scores) > 3]        # 3σ 原则:|Z-score| > 3 即判为异常
[1] 15000

提示

Z-score(标准分数)衡量“某个值距离均值有几个标准差”:z = (x − 均值) / 标准差。3σ 原则就是“|Z-score| 超过 3 判为异常”——教材里的“Z-score 法”和 3σ 原则是同一件事的两种说法。这次它抓到了 15000(z ≈ 3.7),但下一节会看到,它并不总是靠得住。

3.5 两把尺子怎么选

IQR 法 3σ / Z-score 法
依据 中位数、四分位数 均值、标准差
分布假设 不需要 数据应接近正态
抗极端值干扰 强(尺子本身不动) 弱(尺子会被极端值拉偏)
财务数据中的角色 首选 仅作参考

警告

3σ 法有一个数学上的死穴:掩蔽效应。 可以证明,n 个数据里任何一个值的 |Z-score| 都不会超过 (n−1)/√n——16 个数据时上限是 3.75,10 个数据时上限只有 2.85,连 3 都够不到。样本越小、极端值越夸张,均值和标准差就越被它自己拉偏,z 反而超不过 3。极端值“掩蔽”了自己。即时练习 2 就专门演示这一点。

3.6 【即时练习 2】两把尺子 PK

给定 x <- c(10, 11, 12, 10, 12, 13, 11, 12, 13, 100):

  1. 用 IQR 法算上下界,100 会被判为异常吗?
  2. 用 Z-score 法,100 的 z 值是多少?会被判为异常吗?
  3. 两种方法结论一致吗?不一致的话,问题出在哪把尺子上?

四、异常值的处理——先判断“是错还是真”

识别出来只是第一步,怎么处理更需要判断力

4.0 删之前先问一句:这个异常是错还是真?

和缺失值一样,处理异常值没有“万能正确”的方法,先判断它属于哪种情形:

  • 录入错误——比如锦江生物多打了一个 0。有正确值就修正;没有正确值就删除或替换为 NA。
  • 真实但极端——比如头部企业营收天然是同行十倍。值本身没错,但会带偏后续分析,可以截断控制影响。
  • 真实且是业务信号——比如某公司当月暴雷、费用激增。保留,并在报告里说明——它可能正是最重要的发现。

重要

统计规则只负责“圈出来”,业务判断才决定“怎么办”。 仅凭超界就删数据,是数据分析里最危险的习惯之一。

4.1 删除 / 替换为 NA——确认是错误数据时

▶️ 查看代码
company_data %>% filter(revenue >= lower, revenue <= upper)    # 删除:16 行 → 15 行
    company region revenue debt_ratio rating
1  锦城科技   华东    1200       0.45      A
2  蜀汉制造   西南    1463       0.62      B
3  天府物流   西南    1560       0.38      A
4  青羊金融   华东     760       0.71      C
5  浣花实业   华东     892       0.48      B
6  锦西商贸   西南    1340       0.66      B
7  蜀都餐饮   西南     680       0.52      C
8  青城建材   华东     807       0.44      A
9  天府冷链   西南    2100       0.58      A
10 锦江农产   华东     450       0.75      C
11 浣花纺织   华东     980       0.41      B
12 锦西物流   西南    1463       0.63      C
13 蜀汉电子   西南    1720       0.49      A
14 青羊置业   华东     560       0.68      C
15 天府食品   西南    1380       0.57      B
▶️ 查看代码
company_data %>%
  mutate(revenue = if_else(revenue < lower | revenue > upper, NA, revenue))    # 替换为 NA:行数不变
    company region revenue debt_ratio rating
1  锦城科技   华东    1200       0.45      A
2  蜀汉制造   西南    1463       0.62      B
3  天府物流   西南    1560       0.38      A
4  青羊金融   华东     760       0.71      C
5  锦江生物   西南      NA       0.55      A
6  浣花实业   华东     892       0.48      B
7  锦西商贸   西南    1340       0.66      B
8  蜀都餐饮   西南     680       0.52      C
9  青城建材   华东     807       0.44      A
10 天府冷链   西南    2100       0.58      A
11 锦江农产   华东     450       0.75      C
12 浣花纺织   华东     980       0.41      B
13 锦西物流   西南    1463       0.63      C
14 蜀汉电子   西南    1720       0.49      A
15 青羊置业   华东     560       0.68      C
16 天府食品   西南    1380       0.57      B

if_else() 的句式第 9、11 讲已经用过两次——替换成 NA 之后,就可以直接复用上节课的全套填补方法(均值 / 中位数 / 分组均值)。

4.2 截断——Winsorize,把极端值拉回边界

▶️ 查看代码
revenue_capped <- revenue
revenue_capped[revenue_capped > upper] <- upper    # 超过上界的,压缩到上界
revenue_capped[revenue_capped < lower] <- lower    # 低于下界的,压缩到下界
revenue_capped
 [1] 1200 1463 1560  760 2525  892 1340  680  807 2100  450  980 1463 1720  560
[16] 1380

截断(Winsorize)不删观测、不造缺失,只是不让极端值撒野——保留了样本量,又降低了它对均值等后续分析的影响。

4.3 保留不处理——异常本身就是信号

如果复核发现 15000 是真实的(比如锦江生物当月签下大单),正确做法是原样保留,并在分析报告里写明:“本月均值被单一事件显著抬高,另附中位数作参考。”

4.4 本题的正解:修正

回到锦江生物——财务科已经告诉了我们正确值,那最好的方法既不是删也不是截,而是直接改回来:

▶️ 查看代码
company_data$revenue[5] <- 1500    # 按财务科更正邮件修正
revenue_fixed <- company_data$revenue
mean(revenue_fixed)                # 均值回到真实水平
[1] 1178

4.5 四种处理——对照着记

情形 方法 样本量 在 company_data 上
有正确值的错误 修正 不变 revenue[5] <- 1500(本题正解)
无正确值的错误 删除 / 替换为 NA 减少 / 不变 16 → 15 行,或产生 1 个 NA 待填补
真实但极端 截断 winsorize 不变 15000 压到上界 2525
真实且是信号 保留 不变 保留并在报告中说明

4.6 【即时练习 3】该用哪种处理?

给下面三个情境各选一个处理方法,并说一句理由:

  1. 录入员把“管理费用 860”敲成了“8600”,原始凭证已找不到;
  2. 双 11 当月,电商子公司营收是平时的 8 倍——财务确认属实;
  3. 集团并表后发现,头部子公司资产规模是其余 20 家之和,直接算均值会严重失真,但这数据没录错。

五、数据标准化与归一化——让不同量纲的数据可比

同一份数据,为什么要“再处理一遍”?

5.1 量纲不统一:营收和负债率没法直接比

company_data 里有两个数值列,量级差了三个数量级:

▶️ 查看代码
company_data %>% select(company, revenue, debt_ratio)    # revenue 几百到几千(万元),debt_ratio 只有 0 到 1
    company revenue debt_ratio
1  锦城科技    1200       0.45
2  蜀汉制造    1463       0.62
3  天府物流    1560       0.38
4  青羊金融     760       0.71
5  锦江生物    1500       0.55
6  浣花实业     892       0.48
7  锦西商贸    1340       0.66
8  蜀都餐饮     680       0.52
9  青城建材     807       0.44
10 天府冷链    2100       0.58
11 锦江农产     450       0.75
12 浣花纺织     980       0.41
13 锦西物流    1463       0.63
14 蜀汉电子    1720       0.49
15 青羊置业     560       0.68
16 天府食品    1380       0.57

如果直接把这两列放在一起做距离计算、聚类或综合评分,量级大的 revenue 会“喧宾夺主”——debt_ratio 差 0.1 是天壤之别,revenue 差 0.1 连零头都算不上,但算法不知道这些。

提示

标准化 / 归一化的目的:把不同量纲的变量压缩到同一个可比的尺度上,消除单位差异的影响。注意它和清洗的关系——先处理异常值,再做标准化,原因在 5.2 马上揭晓。

5.2 Min-Max 归一化——线性压缩到 [0, 1]

公式:x' = (x − 最小值) / (最大值 − 最小值)。用修正后的 revenue_fixed 演示:

▶️ 查看代码
revenue_minmax <- (revenue_fixed - min(revenue_fixed)) / (max(revenue_fixed) - min(revenue_fixed))
round(revenue_minmax, 2)    # 最小值变 0,最大值变 1,其余按比例落在中间
 [1] 0.45 0.61 0.67 0.19 0.64 0.27 0.54 0.14 0.22 1.00 0.00 0.32 0.61 0.77 0.07
[16] 0.56

警告

Min-Max 对异常值极其敏感:它完全由最大值、最小值这两个端点决定。左图就是教训——15000 没处理时,其余公司的归一化结果全挤在 0.1 以内,彼此差异被抹平,标准化失去了意义。

5.3 Z-score 标准化——转换成“距离均值几个标准差”

公式:x' = (x − 均值) / 标准差——和 3.4 识别异常值用的是同一个公式,只是目的从“找异常”变成了“统一量纲”:

▶️ 查看代码
revenue_zscore <- (revenue_fixed - mean(revenue_fixed)) / sd(revenue_fixed)
round(revenue_zscore, 2)    # 均值变成 0、标准差变成 1;正负号表示在均值之上还是之下
 [1]  0.05  0.61  0.82 -0.90  0.69 -0.62  0.35 -1.08 -0.80  1.99 -1.57 -0.43
[13]  0.61  1.17 -1.34  0.44

5.4 scale()——一次处理整张表

手算公式是理解原理,实务中用 R 内置的 scale(),一次标准化多列:

▶️ 查看代码
scaled <- scale(company_data[, c("revenue", "debt_ratio")])
round(scaled, 2)              # 两列都转成了 Z-score,量纲统一
      revenue debt_ratio
 [1,]    0.05      -0.96
 [2,]    0.61       0.56
 [3,]    0.82      -1.59
 [4,]   -0.90       1.37
 [5,]    0.69      -0.07
 [6,]   -0.62      -0.70
 [7,]    0.35       0.92
 [8,]   -1.08      -0.34
 [9,]   -0.80      -1.05
[10,]    1.99       0.20
[11,]   -1.57       1.73
[12,]   -0.43      -1.32
[13,]    0.61       0.65
[14,]    1.17      -0.61
[15,]   -1.34       1.10
[16,]    0.44       0.11
attr(,"scaled:center")
   revenue debt_ratio 
  1178.438      0.557 
attr(,"scaled:scale")
   revenue debt_ratio 
   463.085      0.111 
▶️ 查看代码
colMeans(scaled)              # 验证:均值 ≈ 0
   revenue debt_ratio 
 -1.34e-17   5.29e-17 
▶️ 查看代码
apply(scaled, 2, sd)          # 验证:标准差 = 1
   revenue debt_ratio 
         1          1 

重要

两种标准化怎么选:Min-Max 把数据压进 [0,1] 固定区间,适合需要明确边界的场景(距离计算、图像、神经网络输入),但对异常值敏感;Z-score 不固定区间,均值 0、标准差 1,保留了“离均值多远”的信息,是统计建模的默认选择,且相对耐极端值(但仍建议先处理异常)。

5.5 数据分箱与虚拟变量——两个“先了解”的概念

▶️ 查看代码
cut(revenue_fixed, breaks = 3)    # 把连续的营收切成 3 个区间,变成分类变量
 [1] (1e+03,1.55e+03]   (1e+03,1.55e+03]   (1.55e+03,2.1e+03] (448,1e+03]       
 [5] (1e+03,1.55e+03]   (448,1e+03]        (1e+03,1.55e+03]   (448,1e+03]       
 [9] (448,1e+03]        (1.55e+03,2.1e+03] (448,1e+03]        (448,1e+03]       
[13] (1e+03,1.55e+03]   (1.55e+03,2.1e+03] (448,1e+03]        (1e+03,1.55e+03]  
Levels: (448,1e+03] (1e+03,1.55e+03] (1.55e+03,2.1e+03]

提示

分箱(binning) 把连续数值切成区间、转成分类变量,本课程不深入展开。预告:像 rating 这样的分类变量,本身也要转成数值才能参与建模,这就是项目五会详细学的虚拟变量(dummy variable)——今天先记住名字。

5.6 【即时练习 4】手算两种标准化

给定 x <- c(100, 200, 300)(均值 200,标准差 100):

  1. 手算 Min-Max 归一化的结果;
  2. 手算 Z-score 标准化的结果;
  3. 用 scale() 跑一遍验证,再算标准化后结果的均值和标准差——应该是多少?

六、综合实战——识别 → 处理 → 标准化 → 验证

把两节课的方法串成一条完整工作流

6.1 动手实战:在 airquality 上走完整流程

任务:继续以 airquality 为对象(第 11 讲已处理过它的缺失),完成一次“异常值 + 标准化”的完整处理:

  1. 承接——按第 11 讲的方案补齐 Ozone、Solar.R 的缺失(复习一遍);
  2. 识别——对 Ozone 画箱线图,并用 IQR 法算出上下界、圈出异常值(提示:用补齐前的原始观测算上下界更稳妥);
  3. 处理——为圈出的异常值选一种处理方法,并写清理由;
  4. 标准化——对处理后的 Ozone、Temp、Solar.R 三列用 scale() 做 Z-score 标准化,验证均值为 0、标准差为 1。

6.2 课堂展示

邀请 1–2 名同学展示自己的处理流程,并说明“为什么这样选”——重点不是代码写得多快,而是每一步的选择理由是否站得住脚。

七、课堂总结、学习成果确认与 Exit Ticket

一条流水线,贯穿本课始终

7.1 知识回顾——五步流水线

识别(箱线图 / IQR / Z-score)→ 判断(是错还是真?)→ 处理(修正 / 删除 / 替换NA / 截断 / 保留)→ 标准化(Min-Max / Z-score·scale())→ 验证(均值≈0、标准差=1)

重要

最容易混的两点:IQR 与 3σ 不是“两种任选其一的算法”,适用条件不同——偏态数据、小样本优先 IQR;标准化必须排在异常值处理之后,尤其 Min-Max,一个未处理的异常值就能把它毁掉。下节课“重复值处理”会在这份数据的基础上继续。

7.2 学习成果自查

  • ✓ 能说清一个极端值为什么会“绑架”均值,而中位数不受影响
  • ✓ 能用箱线图直观定位异常值,并用 quantile() + 1.5×IQR 算出上下界、圈出异常行
  • ✓ 能写出 Z-score 公式,并说清 3σ 原则就是“|z| > 3 判异常”
  • ✓ 能说出 3σ 法的两个软肋:依赖正态假设、小样本有掩蔽效应
  • ✓ 能按“错还是真”选择处理方法:修正 / 删除 / 替换NA / 截断 / 保留
  • ✓ 能手算 Min-Max 和 Z-score 标准化,并用 scale() 一次处理多列
  • ✓ 能说出为什么“异常值处理”必须安排在“标准化”之前

7.3 Exit Ticket —— 写给自己的四个问题

请用 3 分钟,在纸条或在线问卷上简要回答:

  1. IQR 法和 3σ/Z-score 法各自的“尺子”是什么?偏态的财务数据优先用哪把?
  2. 为什么小样本里 3σ 原则可能永远抓不到那个最明显的异常值?
  3. 确认是录入错误的异常值,最优处理是什么?真实但极端的值呢?
  4. 为什么必须先处理异常值、再做 Min-Max 标准化,而不是反过来?

课后作业

异常值处理与数据标准化综合练习

建议使用 Quarto(.qmd)完成并 Render 生成 Word 文档,保留代码与结果:

  1. 用 R 代码生成一份至少 6 家公司的虚构财务数据,人为构造 1–2 个明显异常值
  2. 分别用 IQR 法和 3σ/Z-score 法识别异常值,比较两种方法的结果是否一致
  3. 针对识别出的异常值,选择合适的处理方法(修正 / 删除 / 替换NA / 截断 / 保留),并说明选择理由
  4. 对处理后的数据分别做 Min-Max 归一化和 Z-score 标准化,并用 scale() 验证标准化后均值是否接近 0、标准差是否接近 1

提交要求

提交 .qmd 源文件以及 Render 生成的 Word 文档,命名为“学号_姓名_第12课作业”。本次作业计入平时成绩中的“实验报告”部分考核,请确保代码可运行、结果完整。

下讲预告

重复值处理——duplicated() 与 distinct()

  • 重复值的识别——duplicated() 与 count()
  • 重复值的删除——distinct()
  • 部分字段重复——业务主键去重

提示

课前准备:保留好今天处理完异常值、完成标准化的数据,下节课会在这份数据的基础上继续排查重复记录。

谢谢!

第12讲:异常值检测与处理、数据标准化与归一化

"An outlier is either a mistake to be corrected or a story waiting to be told." (“异常值要么是一个待修正的错误,要么是一个待讲述的故事。”) 财务数据中的异常值往往不是"错误",可能是重大业务事件的信号;不同量纲的财务指标也不能简单比较,需要转换到同一尺度。这种严谨规范的处理方式,正是财务分析工作中"可比性"原则的体现。

朱 奇 | 锦城大学 · 财会学院