文章目录
-
- 0、引言
- 一、先搞清楚:什么是异常值?
- [二、用 airquality 数据演示](#二、用 airquality 数据演示)
- 三、异常值检测的四种方法
-
- 方法一:箱线图法(IQR,最常用)
- [方法二:Z-score 法(标准化)](#方法二:Z-score 法(标准化))
- 方法三:可视化检测(散点图矩阵)
- 方法四:时间序列图(看突变)
- 四、异常值怎么处理?
- 五、四种检测方法总结对比
- 六、国赛现场实操建议
- 七、论文模板(直接套用)
- 八、完整代码(复制即用)
0、引言
距离比赛还有 2 天,异常值处理比缺失值更考验功力。处理不当会扭曲回归系数、拉偏预测结果。本文用
airquality数据集,给你一套可直接运行的异常值检测与处理流程。
一、先搞清楚:什么是异常值?
异常值分三种情况:
| 类型 | 说明 | 举例 |
|---|---|---|
| 数据录入错误 | 人为输错、传感器故障 | 温度写成 999°C |
| 真实极端值 | 数据本身就这么极端 | 房价 1 个亿 |
| 有意义的离群点 | 代表特殊现象,不能删 | 金融危机期间的股市数据 |
国赛黄金法则:
先判断是"错误"还是"特殊"。错误就修/删,特殊就保留或单独建模。
二、用 airquality 数据演示
r
# 加载数据
data("airquality")
df <- airquality
# 看一眼 Ozone 的分布
summary(df$Ozone)
运行结果:
Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
1.00 18.00 31.50 42.13 63.50 168.00 37
- 最小值 1,最大值 168
- 均值 42.13,中位数 31.5(说明有右偏)
问题:168 算异常值吗?还是只是臭氧浓度偶尔爆表?需要用方法来判断。
三、异常值检测的四种方法
方法一:箱线图法(IQR,最常用)
原理:IQR = Q3 - Q1。超出 Q1 - 1.5×IQR 或 Q3 + 1.5×IQR 的点视为异常。
r
# 计算 Ozone 的 IQR(忽略缺失值)
Q1 <- quantile(df$Ozone, 0.25, na.rm = TRUE)
Q3 <- quantile(df$Ozone, 0.75, na.rm = TRUE)
IQR_value <- Q3 - Q1
# 计算上下界
lower_bound <- Q1 - 1.5 * IQR_value
upper_bound <- Q3 + 1.5 * IQR_value
# 找出异常值
outliers <- df$Ozone[!is.na(df$Ozone) &
(df$Ozone < lower_bound | df$Ozone > upper_bound)]
cat("下界:", lower_bound, "\n")
cat("上界:", upper_bound, "\n")
cat("异常值个数:", length(outliers), "\n")
print(outliers)
运行结果:
下界: -50.25
上界: 131.75
异常值个数: 4
[1] 135 168 122 150
解读:Ozone 有 4 个点被判定为异常(122、135、150、168)。
可视化:
r
# 画箱线图
boxplot(df$Ozone,
main = "Ozone 箱线图(红点=异常值)",
ylab = "Ozone 浓度",
col = "lightblue",
outline = TRUE)

箱线图里,超出上下须的点会自动标成圆点,一目了然。
方法二:Z-score 法(标准化)
原理:计算每个值偏离均值多少个标准差。通常 |Z| > 3 视为异常。
r
# 计算均值和标准差
mean_oz <- mean(df$Ozone, na.rm = TRUE)
sd_oz <- sd(df$Ozone, na.rm = TRUE)
# 计算 Z-score
df$Z_score <- (df$Ozone - mean_oz) / sd_oz
# 标记异常
df$Outlier_Z <- ifelse(!is.na(df$Z_score) & abs(df$Z_score) > 3, "异常", "正常")
# 看看哪些是异常
df[df$Outlier_Z == "异常", c("Ozone", "Z_score")]
运行结果:
Ozone Z_score
168 3.815664
解读:只有 168 被 Z-score 判为异常。Z-score 法比 IQR 更宽松,因为均值被其他大值拉高了。
什么时候用 Z-score? 数据近似正态分布时用;数据有偏斜时,Z-score 可能漏掉异常值。
方法三:可视化检测(散点图矩阵)
优点:同时看多个变量,发现多维异常值(单变量正常,组合起来异常)。
r
# 选几个关键变量
df_sub <- df[, c("Ozone", "Solar.R", "Wind", "Temp")]
# 画散点图矩阵
pairs(df_sub,
main = "变量散点图矩阵(寻找异常点)",
col = "steelblue",
pch = 19,
cex = 0.8)

运行后会弹出 4×4 的散点图矩阵。
怎么看:
- 主对角线:每个变量的直方图
- 非对角线:两个变量的散点图
- 远离点群的点就是潜在的异常值
具体找异常点:
r
# 比如找出 Wind 特别大、Ozone 特别低的点
df[df$Wind > 15 & df$Ozone < 10, c("Ozone", "Solar.R", "Wind", "Temp")]
Ozone Solar.R Wind Temp
9 8 19 20.1 61
18 6 78 18.4 57
NA NA NA NA NA
NA.1 NA NA NA NA
方法四:时间序列图(看突变)
适用:数据有先后顺序时,看是否有突然的跳变。
r
# 创建日期
df$Date <- as.Date(paste("1973", df$Month, df$Day, sep = "-"))
# 画时间序列
plot(df$Date, df$Ozone,
type = "b", # 点和线
col = "steelblue",
pch = 19,
main = "Ozone 随时间变化(红点=潜在异常)",
xlab = "日期",
ylab = "Ozone 浓度")
# 标出 IQR 法找到的异常值
outliers_iqr <- df[!is.na(df$Ozone) &
(df$Ozone < lower_bound | df$Ozone > upper_bound), ]
points(outliers_iqr$Date, outliers_iqr$Ozone,
col = "red", pch = 19, cex = 1.5)

运行后会看到 4 个红点分散在时间轴上,如果它们集中在某几天,可能和天气事件有关(不能删);如果孤立出现,可能是测量错误。
四、异常值怎么处理?
处理方案对照表
| 异常值情况 | 处理方法 | 代码 |
|---|---|---|
| 明显录入错误(如负数、999) | 直接删除或改为 NA | df$col[df$col < 0] <- NA |
| 单个极端值,不影响大局 | 用上下界截断(缩尾) | 见下文 |
| 多个异常值,怀疑是错误 | 用中位数替换 | df$col[outlier_idx] <- median(df$col, na.rm = TRUE) |
| 是真实极端值(如房价) | 保留不动 | 不处理 |
| 异常值过多(> 5%) | 考虑用稳健回归或分位数回归 | 换模型 |
方案一:缩尾处理(Winsorize)
把异常值压缩到上下界,而不是删除。
r
# 定义缩尾函数
winsorize <- function(x, lower_percentile = 0.01, upper_percentile = 0.99) {
lower <- quantile(x, lower_percentile, na.rm = TRUE)
upper <- quantile(x, upper_percentile, na.rm = TRUE)
x[x < lower] <- lower
x[x > upper] <- upper
return(x)
}
# 对 Ozone 做缩尾
df$Ozone_winsor <- winsorize(df$Ozone, 0.01, 0.99)
# 对比处理前后
summary(df$Ozone)
summary(df$Ozone_winsor)
运行结果对比:
# 处理前
Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
1.00 18.00 31.50 42.13 63.50 168.00 37
# 处理后(1%和99%分位数缩尾)
Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
1.000 18.00 31.50 41.82 63.50 135.00 37
最大值从 168 降到了 135,但保留了样本不删除。
方案二:中位数替换
r
# 用 IQR 法找异常值
Q1 <- quantile(df$Ozone, 0.25, na.rm = TRUE)
Q3 <- quantile(df$Ozone, 0.75, na.rm = TRUE)
IQR_value <- Q3 - Q1
lower_bound <- Q1 - 1.5 * IQR_value
upper_bound <- Q3 + 1.5 * IQR_value
# 把异常值替换成中位数
median_oz <- median(df$Ozone, na.rm = TRUE)
df$Ozone_fixed <- df$Ozone
df$Ozone_fixed[!is.na(df$Ozone_fixed) &
(df$Ozone_fixed < lower_bound | df$Ozone_fixed > upper_bound)] <- median_oz
# 对比
summary(df$Ozone)
summary(df$Ozone_fixed)
> summary(df$Ozone)
Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
1.00 18.00 31.50 42.13 63.25 168.00 37
> summary(df$Ozone_fixed)
Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
1.00 18.00 31.25 40.06 59.50 122.00 37
方案三:直接删除异常行
r
# 删除 Ozone 异常的行(用 IQR 法)
df_clean <- df[!(!is.na(df$Ozone) &
(df$Ozone < lower_bound | df$Ozone > upper_bound)), ]
cat("原始行数:", nrow(df), "\n")
cat("删除后行数:", nrow(df_clean), "\n")
五、四种检测方法总结对比
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 箱线图(IQR) | 任意分布 | 稳健、不受极端值影响 | 对偏态数据可能过于严格 |
| Z-score | 正态分布 | 简单、统计意义明确 | 受极端值影响大 |
| 散点图矩阵 | 多变量探索 | 发现组合异常 | 主观、不能自动化 |
| 时间序列图 | 时序数据 | 发现突变点 | 需要人工判断 |
六、国赛现场实操建议
标准操作流程:
#mermaid-svg-Y3pX9htKLlA4Es0T{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Y3pX9htKLlA4Es0T .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Y3pX9htKLlA4Es0T .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Y3pX9htKLlA4Es0T .error-icon{fill:#552222;}#mermaid-svg-Y3pX9htKLlA4Es0T .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Y3pX9htKLlA4Es0T .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Y3pX9htKLlA4Es0T .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Y3pX9htKLlA4Es0T .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Y3pX9htKLlA4Es0T .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Y3pX9htKLlA4Es0T .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Y3pX9htKLlA4Es0T .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Y3pX9htKLlA4Es0T .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Y3pX9htKLlA4Es0T .marker.cross{stroke:#333333;}#mermaid-svg-Y3pX9htKLlA4Es0T svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Y3pX9htKLlA4Es0T p{margin:0;}#mermaid-svg-Y3pX9htKLlA4Es0T .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Y3pX9htKLlA4Es0T .cluster-label text{fill:#333;}#mermaid-svg-Y3pX9htKLlA4Es0T .cluster-label span{color:#333;}#mermaid-svg-Y3pX9htKLlA4Es0T .cluster-label span p{background-color:transparent;}#mermaid-svg-Y3pX9htKLlA4Es0T .label text,#mermaid-svg-Y3pX9htKLlA4Es0T span{fill:#333;color:#333;}#mermaid-svg-Y3pX9htKLlA4Es0T .node rect,#mermaid-svg-Y3pX9htKLlA4Es0T .node circle,#mermaid-svg-Y3pX9htKLlA4Es0T .node ellipse,#mermaid-svg-Y3pX9htKLlA4Es0T .node polygon,#mermaid-svg-Y3pX9htKLlA4Es0T .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Y3pX9htKLlA4Es0T .rough-node .label text,#mermaid-svg-Y3pX9htKLlA4Es0T .node .label text,#mermaid-svg-Y3pX9htKLlA4Es0T .image-shape .label,#mermaid-svg-Y3pX9htKLlA4Es0T .icon-shape .label{text-anchor:middle;}#mermaid-svg-Y3pX9htKLlA4Es0T .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Y3pX9htKLlA4Es0T .rough-node .label,#mermaid-svg-Y3pX9htKLlA4Es0T .node .label,#mermaid-svg-Y3pX9htKLlA4Es0T .image-shape .label,#mermaid-svg-Y3pX9htKLlA4Es0T .icon-shape .label{text-align:center;}#mermaid-svg-Y3pX9htKLlA4Es0T .node.clickable{cursor:pointer;}#mermaid-svg-Y3pX9htKLlA4Es0T .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Y3pX9htKLlA4Es0T .arrowheadPath{fill:#333333;}#mermaid-svg-Y3pX9htKLlA4Es0T .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Y3pX9htKLlA4Es0T .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Y3pX9htKLlA4Es0T .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Y3pX9htKLlA4Es0T .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Y3pX9htKLlA4Es0T .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Y3pX9htKLlA4Es0T .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Y3pX9htKLlA4Es0T .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Y3pX9htKLlA4Es0T .cluster text{fill:#333;}#mermaid-svg-Y3pX9htKLlA4Es0T .cluster span{color:#333;}#mermaid-svg-Y3pX9htKLlA4Es0T div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Y3pX9htKLlA4Es0T .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Y3pX9htKLlA4Es0T rect.text{fill:none;stroke-width:0;}#mermaid-svg-Y3pX9htKLlA4Es0T .icon-shape,#mermaid-svg-Y3pX9htKLlA4Es0T .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Y3pX9htKLlA4Es0T .icon-shape p,#mermaid-svg-Y3pX9htKLlA4Es0T .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Y3pX9htKLlA4Es0T .icon-shape .label rect,#mermaid-svg-Y3pX9htKLlA4Es0T .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Y3pX9htKLlA4Es0T .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Y3pX9htKLlA4Es0T .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Y3pX9htKLlA4Es0T :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 没有
有
是
否
<3%
>3%
画箱线图+散点图
有异常值吗?
直接建模
是录入错误?
改为NA或删除
异常值占比?
缩尾或中位数替换
保留+使用稳健模型
使用稳健回归/分位数回归
时间分配建议:
- 检测:5-8 分钟(画图 + 看 summary)
- 判断和处理:10 分钟
- 总计不超过 20 分钟
七、论文模板(直接套用)
正文段落:
本文采用箱线图法对各变量进行异常值检测。以 Ozone 变量为例,其下四分位数 Q1 = 18,上四分位数 Q3 = 63.5,IQR = 45.5,根据 1.5×IQR 准则,共检测出 4 个异常值(122、135、150、168)。进一步检查原始记录发现,这些高值均出现在夏季高温时段,属于真实气象现象,因此予以保留。
或者:
由于 Wind 变量存在 2 个明显偏离正常范围的数值(经核查为录入错误),本文将其替换为变量中位数,以避免对回归结果产生不利影响。
图注模板:
图 X Ozone 变量的箱线图(红圈标注为异常值)
八、完整代码(复制即用)
r
# ===== 加载数据 =====
data("airquality")
df <- airquality
# ===== 1. 箱线图法检测 =====
Q1 <- quantile(df$Ozone, 0.25, na.rm = TRUE)
Q3 <- quantile(df$Ozone, 0.75, na.rm = TRUE)
IQR_val <- Q3 - Q1
lower <- Q1 - 1.5 * IQR_val
upper <- Q3 + 1.5 * IQR_val
outliers_iqr <- df$Ozone[!is.na(df$Ozone) &
(df$Ozone < lower | df$Ozone > upper)]
cat("箱线图法检测到异常值:", length(outliers_iqr), "个\n")
print(outliers_iqr)
# ===== 2. Z-score法检测 =====
mean_oz <- mean(df$Ozone, na.rm = TRUE)
sd_oz <- sd(df$Ozone, na.rm = TRUE)
z_scores <- (df$Ozone - mean_oz) / sd_oz
outliers_z <- df$Ozone[!is.na(z_scores) & abs(z_scores) > 3]
cat("Z-score法检测到异常值:", length(outliers_z), "个\n")
print(outliers_z)
# ===== 3. 可视化 =====
par(mfrow = c(1, 2))
# 箱线图
boxplot(df$Ozone,
main = "箱线图检测异常值",
ylab = "Ozone",
col = "lightblue")
# 直方图+密度曲线
hist(df$Ozone,
prob = TRUE,
main = "Ozone分布",
xlab = "Ozone",
col = "lightgreen")
lines(density(df$Ozone, na.rm = TRUE), col = "red", lwd = 2)
par(mfrow = c(1, 1))
# ===== 4. 缩尾处理 =====
winsorize <- function(x, lower_p = 0.01, upper_p = 0.99) {
lower <- quantile(x, lower_p, na.rm = TRUE)
upper <- quantile(x, upper_p, na.rm = TRUE)
x[x < lower] <- lower
x[x > upper] <- upper
return(x)
}
df$Ozone_winsor <- winsorize(df$Ozone, 0.01, 0.99)
# 对比
cat("\n处理前后对比:\n")
cat("处理前 Max:", max(df$Ozone, na.rm = TRUE), "\n")
cat("处理后 Max:", max(df$Ozone_winsor, na.rm = TRUE), "\n")
把代码复制到 RStudio 里跑一遍,观察不同方法检测出的异常值数量差异。记住 :异常值处理没有标准答案,关键是在论文里写清楚你为什么这么处理。比赛加油!💪