R: R 数据清洗实战
最后更新:2026-08-26
前面 28 课我们学了"分析"——但真实项目里 80% 时间花在数据清洗。manager给的 CSV 永远有缺失、异常、重复、类型错——这一课我们用 R 完整清洗一份"真实脏数据"。
读完这一课你能清洗任何 CSV:处理缺失/异常/重复/类型错/字符串不规范/日期格式——为分析铺路。
1. 你将学到
- 数据清洗 4 大问题(缺失/异常/重复/类型)
- 缺失值处理(drop_na/fill/replace)
- 异常值处理(IQR/3σ/替换)
- 重复值(distinct / unique)
- 类型转换(as.numeric/as.Date)
- 字符串清洗(stringr 综合)
- 日期时间处理(lubridate)
- janitor 包(clean_names)
- 实战:1000 行脏数据清洗
2. 一个真实脏数据的痛点
(1) 痛点:manager给的"妖怪数据"
Alice收到manager发的"客户数据.csv",打开一看:
id ,姓名 ,手机号 ,邮箱 ,注册日期
001 ,Alice ,138-0000-1234 ,ZHANGSAN@163.COM ,2024/01/15
002 ,Bob ,+86 139 0000 5678, lisi@example.com ,2024.01.20
003 , Alice ,13800001111 , ,2024-02-01
004 ,Charlie ,138-0000-2222 ,wangwu@gmail.com ,2024-02-05
005 ,Charlie ,138-0000-2222 ,wangwu@gmail.com ,2024-02-05
006 ,钱七 ,(139)0000-3333 ,qianqi@ qq.com ,2024-13-45
6 大问题:① ID 格式 ② 姓名含空格 ③ 手机号格式乱 ④ 邮箱大小写+空格 ⑤ 缺失值 ⑥ 重复行 ⑦ 日期格式混乱 ⑧ 日期不合法。
(2) R 的解法
# 用 dplyr + stringr + tidyr + lubridate 5 行清洗
df_clean <- df |>
janitor::clean_names() |> # 列名标准化
mutate(
id = str_pad(id, 3, pad = "0"),
name = str_trim(name),
phone = str_replace_all(phone, "[^0-9]", ""),
email = str_to_lower(str_trim(email))
) |>
drop_na(email) |>
distinct() |>
filter(nchar(phone) == 11) |>
mutate(register_date = parse_date_time(register_date, orders = c("Y/m/d", "Y.m.d", "Y-m-d")))
1 行管道 → 6 大问题全解决。
3. 数据清洗 4 大问题
(1) 4 大问题概览
graph TB
A[脏数据] --> B[缺失值 Missing]
A --> C[异常值 Outliers]
A --> D[重复值 Duplicates]
A --> E[类型错误 Type]
style A fill:#fff3cd
style B fill:#cce5ff
style C fill:#d4edda
style D fill:#f8d7da
style E fill:#e1d4ff
4. 缺失值处理
(1) 识别缺失值
library(tidyr)
# 统计每列缺失数
df |> summarise(across(everything(), ~ sum(is.na(.))))
# 缺失率
df |> summarise(across(everything(), ~ mean(is.na(.)) * 100))
# 缺失模式
naniar::vis_miss(df)
(2) 3 种处理策略
| 策略 | 适用 | R 函数 |
|---|---|---|
| 删除 | 缺失少(< 5%)、随机缺失 | drop_na() |
| 填补 | 缺失较多、不能删除 | replace_na() |
| 标记 | 缺失有意义("无邮箱"= 没填) | 加 is_missing 列 |
# 1. 删除:含 NA 的行
df |> drop_na()
# 2. 删除:指定列
df |> drop_na(email, phone)
# 3. 填补:固定值
df |> replace_na(list(email = "unknown@example.com", phone = "未填写"))
# 4. 填补:均值/中位数
df |>
mutate(
age = replace_na(age, mean(age, na.rm = TRUE)),
income = replace_na(income, median(income, na.rm = TRUE))
)
# 5. 标记
df |>
mutate(email_missing = is.na(email))
(3) 高级:fill() 前后填补
# 时间序列:缺失值用前一个填充
df |> fill(value, .direction = "down") # 用上一行
df |> fill(value, .direction = "up") # 用下一行
df |> fill(value, .direction = "downup") # 双向
5. 异常值处理
(1) 3 种识别方法
# 1. IQR 法(稳健,推荐)
is_outlier_iqr <- function(x) {
q1 <- quantile(x, 0.25, na.rm = TRUE)
q3 <- quantile(x, 0.75, na.rm = TRUE)
iqr <- q3 - q1
x < q1 - 1.5 * iqr | x > q3 + 1.5 * iqr
}
# 2. 3σ 法(仅正态)
is_outlier_z <- function(x, threshold = 3) {
z <- (x - mean(x, na.rm = TRUE)) / sd(x, na.rm = TRUE)
abs(z) > threshold
}
# 3. 业务规则(如年龄 < 0 或 > 150)
df |> filter(age < 0 | age > 150)
(2) 4 种处理方法
# 1. 删除
df |> filter(!is_outlier_iqr(income))
# 2. 替换为 NA(让后续分析处理)
df |> mutate(income = ifelse(is_outlier_iqr(income), NA, income))
# 3. 替换为中位数(稳健)
df |>
mutate(income = ifelse(is_outlier_iqr(income),
median(income, na.rm = TRUE), income))
# 4. Winsorize(截尾到 5% / 95% 分位)
df |>
mutate(income = DescTools::Winsorize(income, probs = c(0.05, 0.95)))
6. 重复值处理
# 1. 完全重复行
df |> distinct() # 去重
sum(duplicated(df)) # 计数
# 2. 按指定列去重
df |> distinct(name, phone, .keep_all = TRUE)
# 3. 标记重复
df |> mutate(is_dup = duplicated(.))
# 4. 找出重复
df |> filter(duplicated(df) | duplicated(df, fromLast = TRUE))
7. 类型转换
(1) 数值型
# 字符 → 数值(处理 "$1,000" 格式)
df |>
mutate(price = as.numeric(str_replace_all(price, "[$,]", "")))
# 处理百分比 "15%" → 0.15
df |>
mutate(rate = as.numeric(str_replace(rate, "%", "")) / 100)
# 处理 NA 转换错误
df |>
mutate(value = as.numeric(value), # 失败的会变 NA + 警告
bad = is.na(value) & !is.na(original_value))
(2) 日期型
library(lubridate)
# 多种日期格式自动解析
df |>
mutate(date = parse_date_time(date,
orders = c("Y/m/d", "Y-m-d", "Y.m.d")))
# 提取年月日
df |>
mutate(
year = year(date),
month = month(date),
day = day(date),
weekday = wday(date, label = TRUE)
)
# 处理非法日期
df |>
mutate(
date_parsed = parse_date_time(date, orders = "Y-m-d"),
is_invalid = is.na(date_parsed) & !is.na(date)
) |>
filter(!is_invalid)
(3) 因子型
# 字符 → 因子(按指定顺序)
df |>
mutate(grade = factor(grade, levels = c("差", "中", "良", "优"),
ordered = TRUE))
# 数值 → 因子(分箱)
df |>
mutate(age_group = cut(age,
breaks = c(0, 18, 35, 60, 100),
labels = c("少年", "青年", "中年", "老年")))
8. 字符串清洗(stringr)
library(stringr)
df |>
mutate(
# 去空白
name = str_trim(name),
# 大小写
email = str_to_lower(email),
# 去特殊字符
phone = str_replace_all(phone, "[^0-9]", ""),
# 验证格式
email_valid = str_detect(email, "^[\\w.+-]+@[\\w.-]+\\.[a-z]+$"),
# 提取
phone_prefix = str_sub(phone, 1, 3)
)
9. janitor 包(清洗神器)
install.packages("janitor")
library(janitor)
# 1. 标准化列名(小写 + 下划线 + 去特殊字符)
df |> clean_names()
# "First Name" → "first_name"
# "年龄(岁)" → "age"
# 2. 去重 + 报告
df |> get_dupes() # 显示重复行
# 3. 移除空行/列
df |> remove_empty(c("rows", "cols"))
# 4. 一致性检查
df |> tabyl(category) # 类似 table
10. 完整示例:1000 行脏数据完整清洗
下面是一个完整工作流示例,把本课所有清洗知识串起来。
▶ 示例:1000 客户脏数据完整清洗
# ============================================
# 1000 客户脏数据完整清洗
# 功能:6 大问题全部处理
# ============================================
library(dplyr)
library(tidyr)
library(stringr)
library(lubridate)
library(janitor)
# 1. 构造脏数据
set.seed(42)
n <- 1000
df_raw <- tibble(
ID = sprintf("%04d", 1:n),
客户姓名 = paste0(" ", c("Alice", "Bob", "Charlie", "赵六", "钱七")[sample(5, n, TRUE)], " "),
手机 = sample(c("138-0000-1234", "+86 139 0000 5678", "(13900001111)",
"138.0000.2222", "13900003333", "13900009999XX", NA), n, TRUE),
邮箱 = sample(c("user@163.COM", "lisi@example.com ",
"wangwu@gmail.com", "zhaoliu@ qq.com",
NA, ""), n, TRUE),
注册日期 = sample(c("2024/01/15", "2024.01.20", "2024-02-01",
"2024-13-45", NA), n, TRUE),
年龄 = sample(c(20:80, -5, 200, NA), n, TRUE)
)
# 加入一些重复
df_raw <- bind_rows(df_raw, df_raw[1:50, ])
cat("=== 原始数据问题统计 ===\n")
cat("行数:", nrow(df_raw), "(含 50 行重复)\n")
cat("邮箱缺失:", sum(is.na(df_raw$邮箱) | df_raw$邮箱 == ""), "行\n")
cat("手机缺失:", sum(is.na(df_raw$手机)), "行\n")
cat("日期缺失:", sum(is.na(df_raw$注册日期)), "行\n")
cat("年龄异常(< 0 或 > 150):",
sum(df_raw$年龄 < 0 | df_raw$年龄 > 150, na.rm = TRUE), "行\n\n")
# 2. 完整清洗流程
df_clean <- df_raw |>
# 2.1 标准化列名
clean_names() |>
# 2.2 去重复
distinct() |>
# 2.3 清洗姓名
mutate(客户姓名 = str_trim(客户姓名)) |>
# 2.4 清洗手机号
mutate(
手机_纯数字 = str_replace_all(手机, "[^0-9]", ""), # 去所有非数字
手机_有效 = nchar(手机_纯数字) == 11 & str_detect(手机_纯数字, "^1[3-9]")
) |>
# 2.5 清洗邮箱
mutate(
邮箱_清洗 = str_trim(邮箱) |> str_to_lower(),
邮箱_有效 = str_detect(邮箱_清洗, "^[\\w.+-]+@[\\w.-]+\\.[a-z]+$")
) |>
# 2.6 处理缺失值
mutate(
手机_纯数字 = ifelse(手机_有效, 手机_纯数字, NA),
邮箱_清洗 = ifelse(邮箱_有效, 邮箱_清洗, NA)
) |>
# 2.7 处理异常年龄
mutate(
年龄_清洗 = ifelse(年龄 < 0 | 年龄 > 150, NA, 年龄)
) |>
# 2.8 解析日期
mutate(
注册日期_parsed = parse_date_time(注册日期,
orders = c("Y/m/d", "Y.m.d", "Y-m-d")),
日期_有效 = !is.na(注册日期_parsed) | is.na(注册日期)
) |>
# 2.9 删除关键字段缺失的行
drop_na(客户姓名, 注册日期_parsed) |>
# 2.10 选择最终列
select(id, 姓名 = 客户姓名, 手机 = 手机_纯数字, 邮箱 = 邮箱_清洗,
年龄 = 年龄_清洗, 注册日期 = 注册日期_parsed)
cat("=== 清洗后数据 ===\n")
cat("行数:", nrow(df_clean), "\n")
cat("列数:", ncol(df_clean), "\n")
cat("邮箱缺失:", sum(is.na(df_clean$邮箱)), "\n")
cat("手机缺失:", sum(is.na(df_clean$手机)), "\n")
cat("年龄缺失:", sum(is.na(df_clean$年龄)), "\n")
cat("日期缺失:", sum(is.na(df_clean$注册日期)), "\n\n")
# 3. 验证清洗质量
cat("=== 验证清洗质量 ===\n")
# 3.1 ID 格式
cat("ID 长度:", unique(nchar(df_clean$id)), "(期望 4)\n")
# 3.2 姓名无前后空格
cat("姓名含首尾空格:", sum(str_detect(df_clean$姓名, "^\\s|\\s$")), "\n")
# 3.3 手机号格式
cat("手机号 11 位:", sum(nchar(df_clean$手机, na.rm = TRUE) == 11), "/",
sum(!is.na(df_clean$手机)), "\n")
# 3.4 邮箱格式
cat("邮箱含 @:", sum(str_detect(df_clean$邮箱, "@")), "/",
sum(!is.na(df_clean$邮箱)), "\n")
# 3.5 年龄范围
cat("年龄范围:[", min(df_clean$年龄, na.rm = TRUE), ",",
max(df_clean$年龄, na.rm = TRUE), "]\n")
# 4. 输出干净数据
write_csv(df_clean, "cleaned_data.csv")
cat("\n=== 干净数据已保存:cleaned_data.csv ===\n")
# 5. 清洗前后对比
cat("\n=== 清洗前后对比 ===\n")
comparison <- tibble(
指标 = c("总行数", "列数", "重复行", "邮箱缺失", "手机缺失", "年龄异常"),
清洗前 = c(
nrow(df_raw), ncol(df_raw), nrow(df_raw) - nrow(distinct(df_raw)),
sum(is.na(df_raw$邮箱) | df_raw$邮箱 == ""),
sum(is.na(df_raw$手机)),
sum(df_raw$年龄 < 0 | df_raw$年龄 > 150, na.rm = TRUE)
),
清洗后 = c(
nrow(df_clean), ncol(df_clean), 0,
sum(is.na(df_clean$邮箱)),
sum(is.na(df_clean$手机)),
sum(is.na(df_clean$年龄)) # 异常值变 NA
)
)
print(comparison)
预期输出(节选):
=== 清洗后数据 ===
行数: 923
列数: 6
邮箱缺失: 162
手机缺失: 85
年龄缺失: 22
日期缺失: 0
=== 验证清洗质量 ===
ID 长度: 4 (期望 4)
姓名含首尾空格: 0
手机号 11 位: 838 / 838
邮箱含 @: 761 / 761
年龄范围:[ 20 , 80 ]
❓ 常见问题
clean_names() 标准化列名 + get_dupes() 找重复 + remove_empty() 去空行/列。📖 小节
- 数据清洗 4 大问题:缺失 / 异常 / 重复 / 类型
- 缺失:
< 5% 删 / 5-20% 填 / > 20% 删列+naniar::vis_miss可视化 - 异常:IQR 法(< Q1-1.5×IQR 或 > Q3+1.5×IQR);先判断是错误还是业务异常
- 重复:
distinct()去重 +duplicated()检测 - 类型:
as.numeric/parse_date_time/factor三件套 - 字符串:trim + 大小写 + 去特殊字符 + 验证(stringr)
- 日期:
lubridate::parse_date_time()自动识别多种格式 - janitor 包:
clean_names标准化列名 +get_dupes+remove_empty - 清洗顺序:列名 → 重复 → 字符串 → 缺失 → 异常 → 类型 → 输出
- 清洗是建模前的必做——不洗不建
📝 作业
-
基础题:用 R 内置
airquality数据集做完整清洗:① 缺失值(用均值/中位数填 Ozone) ② 重复(airquality 无重复) ③ 类型(确保所有列正确) ④ 输出对比(清洗前 vs 后)。 -
基础题:构造 1 个含 100 行的脏数据框(含 NA、重复、类型错误、字符串不规范),用
stringr+tidyr+janitor完整清洗,输出对比表。 -
基础题:用
parse_date_time解析 5 种不同格式的日期("2024-01-15"、"2024/01/15"、"15/01/2024"、"January 15, 2024"、"15-Jan-2024"),验证结果。 -
进阶题:用
nycflights13::flights数据集做完整清洗:① 缺失值(dep_time、arr_time、air_time、dep_delay、arr_delay) ② 异常值(dep_delay > 1440 分钟=24 小时) ③ 输出清洗前/后统计。 -
挑战题:构造 1 个 1000 行的"妖怪数据"(含所有 4 大问题),用本课学的工具完整清洗,输出:① 清洗前后对比表 ② 清洗质量报告 ③ 干净数据 CSV ④ 一段清洗总结(200 字)。