R: R 数据清洗实战

最后更新:2026-08-26

前面 28 课我们学了"分析"——但真实项目里 80% 时间花在数据清洗。manager给的 CSV 永远有缺失、异常、重复、类型错——这一课我们用 R 完整清洗一份"真实脏数据"。

读完这一课你能清洗任何 CSV:处理缺失/异常/重复/类型错/字符串不规范/日期格式——为分析铺路。

1. 你将学到



2. 一个真实脏数据的痛点

(1) 痛点:manager给的"妖怪数据"

Alice收到manager发的"客户数据.csv",打开一看:

TEXT 📖 仅展示
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 的解法

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 大问题概览

100%
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) 识别缺失值

R
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
R
# 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() 前后填补

R
# 时间序列:缺失值用前一个填充
df |> fill(value, .direction = "down")   # 用上一行
df |> fill(value, .direction = "up")     # 用下一行
df |> fill(value, .direction = "downup") # 双向


5. 异常值处理

(1) 3 种识别方法

R
# 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 种处理方法

R
# 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. 重复值处理

R
# 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) 数值型

R
# 字符 → 数值(处理 "$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) 日期型

R
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) 因子型

R
# 字符 → 因子(按指定顺序)
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)

R
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 包(清洗神器)

R
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 客户脏数据完整清洗

R 📖 仅展示
# ============================================
# 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)
逻辑代码 89 行(超过 40 行限制,仅展示)

预期输出(节选):

TEXT 📖 仅展示
=== 清洗后数据 ===
行数: 923
列数: 6
邮箱缺失: 162
手机缺失: 85
年龄缺失: 22
日期缺失: 0

=== 验证清洗质量 ===
ID 长度: 4 (期望 4)
姓名含首尾空格: 0
手机号 11 位: 838 / 838
邮箱含 @: 761 / 761
年龄范围:[ 20 , 80 ]

❓ 常见问题

Q 字符串清洗的步骤?
A ① 去空白(trim) ② 大小写 ③ 去特殊字符 ④ 验证格式 ⑤ 提取关键信息。
Q 日期处理有什么坑?
A 3 大坑:① 多种格式(YYYY-MM-DD vs MM/DD/YYYY) ② 时区 ③ 非法日期(如 13 月)。
Q janitor 包最常用?
A clean_names() 标准化列名 + get_dupes() 找重复 + remove_empty() 去空行/列。

📖 小节


📝 作业

  1. 基础题:用 R 内置 airquality 数据集做完整清洗:① 缺失值(用均值/中位数填 Ozone) ② 重复(airquality 无重复) ③ 类型(确保所有列正确) ④ 输出对比(清洗前 vs 后)。

  2. 基础题:构造 1 个含 100 行的脏数据框(含 NA、重复、类型错误、字符串不规范),用 stringr + tidyr + janitor 完整清洗,输出对比表。

  3. 基础题:用 parse_date_time 解析 5 种不同格式的日期("2024-01-15"、"2024/01/15"、"15/01/2024"、"January 15, 2024"、"15-Jan-2024"),验证结果。

  4. 进阶题:用 nycflights13::flights 数据集做完整清洗:① 缺失值(dep_time、arr_time、air_time、dep_delay、arr_delay) ② 异常值(dep_delay > 1440 分钟=24 小时) ③ 输出清洗前/后统计。

  5. 挑战题:构造 1 个 1000 行的"妖怪数据"(含所有 4 大问题),用本课学的工具完整清洗,输出:① 清洗前后对比表 ② 清洗质量报告 ③ 干净数据 CSV ④ 一段清洗总结(200 字)。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏