数学建模国赛倒计时 2 天 ——《异常值的处理(R语言)》

文章目录

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 里跑一遍,观察不同方法检测出的异常值数量差异。记住 :异常值处理没有标准答案,关键是在论文里写清楚你为什么这么处理。比赛加油!💪

相关推荐
乱七八糟的屋子1 小时前
NLopt 超详细实战教程(C++版)算法选型|多目标优化|批量寻优|动态约束|闭环仿真控制
数学建模·非线性优化·nlopt·多目标优化·机器人算法·工业仿真·c++优化
z落落4 小时前
C# WinForm Socket 转串口设备服务器+Modbus CRC16校验+Socket 客户端
服务器·开发语言·c#
小灰灰搞电子9 小时前
Rust+Slint 实现动态消息提示框源码分享
开发语言·后端·rust
传奇开心果编程11 小时前
【Rust入门知识点学与练】第21课:Trait 进阶 Advanced Traits
开发语言·学习·rust
新时代牛马11 小时前
字符设备驱动完整篇:从 cdev_add、file_operations 到chrdev_open 与排障
开发语言·python
swordbob11 小时前
ReentrantLock 与 AQS 完整学习手册
java·开发语言
白山编程大哥12 小时前
Java OutputStreamWriter 详解:从字符到字节的桥梁
java·开发语言·python
shmily麻瓜小菜鸡13 小时前
JavaScript / TypeScript 易踩坑知识点 —— 异步编程类
开发语言·javascript·typescript
七夜zippoe13 小时前
为什么 2026 年每个 Java 团队都该懂 AI Agent
java·开发语言·人工智能