数据挖掘与R语言

第6讲:tidyverse 数据操作 ~ Part 2

2026年04月03日

上讲回顾

  • tidyverse 核心理念:tidy data——每列一个变量,每行一个观测,每格一个值
  • 管道操作符|> 让代码从左到右、从上到下,与逻辑顺序一致
  • count():快速频率统计;add_count() 保留原始行数
  • 行操作filter() 按条件筛选;arrange() 排序;slice_max() 等取极值行
  • 列操作select() 选列;mutate() 创建/修改列;if_else()case_when() 条件判断
  • 汇总与分组summarise() 压缩为汇总行;.by= 在函数内部直接分组

本讲内容

  • Part 1:替换字段中的数据 ——修改已有列的值(约15分钟)
  • Part 2:批量处理缺失值 ——多种策略替换 NA(约20分钟)
  • Part 3:增加行与行合并 ——add_row()bind_rows()(约15分钟)
  • Part 4:列合并与表连接 ——bind_cols()*_join()(约25分钟)
  • Part 5:table() 与交叉表 ——快速列联表(约10分钟)
  • Part 6:实用技巧拾遗 ——值得掌握的其他命令(约15分钟)

提示

本讲是第5讲的延伸,聚焦数据清洗中最常见的"脏活":修数据、补缺失、拼表格。学完这两讲,你就能应对日常分析中 80% 的数据准备工作。

Part 1 替换字段中的数据

修改已有列的值

1.1 用 mutate() 直接覆盖已有列

最基本的替换方式:mutate() 对同名列赋新值,原列被覆盖。

▶️ 查看代码
# 将 species 列统一改为大写
penguins |>
  mutate(species = str_to_upper(species)) |>
  count(species)
▶️ 查看代码
# 将体重单位从克改为千克(覆盖原列)
penguins |>
  mutate(body_mass = body_mass / 1000) |>
  select(species, body_mass) |>
  head(4)

注记

mutate(列名 = 新值) 当列名已存在时,会覆盖原列;当列名不存在时,会新增一列。这是同一个函数的两种行为,区别只在于列名是否已经存在。

1.2 条件替换:if_else()case_when()

实际数据中,往往只想替换满足某条件的值,其余保持不变。

▶️ 查看代码
# if_else():满足条件时替换,否则保持原值
penguins |>
  mutate(
    island = if_else(island == "Torgersen", "托格森岛", island)
  ) |>
  count(island)
▶️ 查看代码
# case_when():多条件分支替换
penguins |>
  mutate(
    species = case_when(
      species == "Adelie"    ~ "阿德利企鹅",
      species == "Chinstrap" ~ "帽带企鹅",
      species == "Gentoo"    ~ "巴布亚企鹅"
    )
  ) |>
  count(species)

提示

case_when() 中如果没有匹配任何条件,结果为 NA。用 .default = 原值 可以保留未匹配的值,例如 .default = species

1.3 字符串替换:str_replace()str_remove()

当替换逻辑基于字符串模式时,stringr 包的函数更方便:

▶️ 查看代码
# str_replace():替换第一个匹配的模式
# str_replace_all():替换所有匹配的模式
df <- tibble(
  产品名称 = c("iPhone_15_Pro", "iPad_Air_5", "MacBook_Pro_14")
)

df |>
  mutate(产品名称 = str_replace_all(产品名称, "_", " "))
▶️ 查看代码
# str_remove():删除匹配的模式(相当于替换为空字符串)
df |>
  mutate(产品名称 = str_remove(产品名称, "_.*"))  # 删除第一个 _ 及其后所有内容
▶️ 查看代码
# 正则表达式:清洗含有杂质的数字列
tibble(价格 = c("¥1,200", "¥980", "1500元")) |>
  mutate(
    价格数值 = str_remove_all(价格, "[¥¥,元]") |> as.numeric()
  )

1.4 类型转换:as.*() 函数族

字段替换中另一类常见操作是改变列的数据类型

▶️ 查看代码
# 模拟一份从 CSV 读入的"脏"数据(数字被读成字符串)
df_dirty <- tibble(
  id      = c("1", "2", "3"),
  score   = c("85.5", "90", "78.3"),
  date    = c("2024-01-01", "2024-01-02", "2024-01-03"),
  is_pass = c("TRUE", "FALSE", "TRUE")
)

df_dirty |>
  mutate(
    id      = as.integer(id),
    score   = as.numeric(score),
    date    = ymd(date),
    is_pass = as.logical(is_pass)
  )

注记

目标类型 转换函数
整数 as.integer()
浮点数 as.numeric() / as.double()
字符串 as.character()
逻辑值 as.logical()
日期 as.Date()
因子 as.factor() / factor()

1.5 批量类型转换:across() + as.*()

当多列需要转换为同一类型时,across() 大幅简化代码:

▶️ 查看代码
df_dirty <- tibble(
  id      = "1",
  score_1 = "85",
  score_2 = "90",
  score_3 = "78"
)

# 将所有以 score_ 开头的列批量转为数值型
df_dirty |>
  mutate(
    across(starts_with("score_"), as.numeric)
  )
▶️ 查看代码
# across() 同样适用于 summarise()
# 对所有数值列同时计算均值
penguins |>
  summarise(
    across(where(is.numeric), ~ mean(.x, na.rm = TRUE))
  )

penguins |>
  select(where(is.numeric)) |>
  drop_na() |> 
  summarise(
    bill_len_mean = mean(bill_len),
    bill_dep_mean = mean(bill_dep),
    flipper_len_mean = mean(bill_dep),
    body_mass_mean = mean(bill_dep),
    year_mean = mean(year)
            )
▶️ 查看代码
penguins |>
  select(where(is.numeric)) |>
  colMeans(na.rm = TRUE) |> 
  enframe()
# A tibble: 5 × 2
  name         value
  <chr>        <dbl>
1 bill_len      43.9
2 bill_dep      17.2
3 flipper_len  201. 
4 body_mass   4202. 
5 year        2008. 

提示

across(列选择器, 函数) 是 dplyr 1.0 引入的核心机制,where(is.numeric) 选择所有数值列,starts_with("score_") 按前缀选列,c(col1, col2) 指定具体列——与 select() 的辅助函数完全通用。

Part 2 批量处理缺失值

多种策略替换 NA

2.1 缺失值诊断:先看清楚再动手

在替换之前,先了解缺失值的分布是好习惯:

▶️ 查看代码
summary(penguins)
      species          island       bill_len       bill_dep     flipper_len 
 Adelie   :152   Biscoe   :168   Min.   :32.1   Min.   :13.1   Min.   :172  
 Chinstrap: 68   Dream    :124   1st Qu.:39.2   1st Qu.:15.6   1st Qu.:190  
 Gentoo   :124   Torgersen: 52   Median :44.5   Median :17.3   Median :197  
                                 Mean   :43.9   Mean   :17.2   Mean   :201  
                                 3rd Qu.:48.5   3rd Qu.:18.7   3rd Qu.:213  
                                 Max.   :59.6   Max.   :21.5   Max.   :231  
                                 NA's   :2      NA's   :2      NA's   :2    
   body_mass        sex           year     
 Min.   :2700   female:165   Min.   :2007  
 1st Qu.:3550   male  :168   1st Qu.:2007  
 Median :4050   NA's  : 11   Median :2008  
 Mean   :4202                Mean   :2008  
 3rd Qu.:4750                3rd Qu.:2009  
 Max.   :6300                Max.   :2009  
 NA's   :2                                 
▶️ 查看代码
library(modelsummary)
penguins |> 
  datasummary_skim()
Unique Missing Pct. Mean SD Min Median Max Histogram
bill_len 165 1 43.9 5.5 32.1 44.5 59.6
bill_dep 81 1 17.2 2.0 13.1 17.3 21.5
flipper_len 56 1 200.9 14.1 172.0 197.0 231.0
body_mass 95 1 4201.8 802.0 2700.0 4050.0 6300.0
year 3 0 2008.0 0.8 2007.0 2008.0 2009.0
N %
species Adelie 152 44.2
Chinstrap 68 19.8
Gentoo 124 36.0
island Biscoe 168 48.8
Dream 124 36.0
Torgersen 52 15.1
sex female 165 48.0
male 168 48.8

2.2 drop_na():删除含缺失值的行

最激进的处理方式——直接删行:

▶️ 查看代码
# 删除任意列含 NA 的行
penguins |> 
  drop_na()

# 只删除指定列含 NA 的行(更常用)
penguins |> 
  drop_na(body_mass)

警告

drop_na() 看似简单,但代价是丢失数据。应先确认缺失机制(完全随机缺失、随机缺失、非随机缺失),再决定是否删除。对于较小的数据集,轻率删行可能严重影响分析结果。

▶️ 查看代码
starwars |> 
  filter(if_any(everything(), is.na)) |> 
  #view()
  drop_na()
# A tibble: 0 × 14
# ℹ 14 variables: name <chr>, height <int>, mass <dbl>, hair_color <chr>,
#   skin_color <chr>, eye_color <chr>, birth_year <dbl>, sex <chr>,
#   gender <chr>, homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

2.3 replace_na():用固定值替换 NA

最直接的填补方式:

▶️ 查看代码
# 将 sex 列的 NA 替换为 "未知"
penguins |>
  mutate(gender = as.character(sex)) |> 
  mutate(gender = replace_na(gender, "未知")) |>
  count(gender)

penguins |>
  mutate(gender = as.character(sex)) |> 
  mutate(gender = replace_na(gender, "male")) |>
  count(gender)
▶️ 查看代码
# 同时替换多列(在 mutate + across 中使用)
penguins |>
  mutate(
    across(where(is.numeric), ~ replace_na(.x, 0))
  ) |>
  summarise(across(where(is.numeric), ~ sum(is.na(.x))))

2.4 fill():用前值或后值填充 NA

当数据是有序的(如时间序列、问卷中合并单元格导出的数据),用相邻值填充很自然:

▶️ 查看代码
# 模拟"合并单元格"导出的数据
df_merged <- tibble(
  部门   = c("销售部", NA, NA, "技术部", NA),
  姓名   = c("张三", "李四", "王五", "赵六", "钱七"),
  绩效   = c(85, 90, 78, 92, 88)
)

df_merged
▶️ 查看代码
# fill():用前值(.direction = "down",默认)向下填充
df_merged |>
  fill(部门)
▶️ 查看代码
# .direction 的四个选项
df_merged |> fill(部门, .direction = "down")   # 用前值填充(向下)
df_merged |> fill(部门, .direction = "up")     # 用后值填充(向上)
df_merged |> fill(部门, .direction = "downup") # 先向下再向上
df_merged |> fill(部门, .direction = "updown") # 先向上再向下

注记

fill() 来自 tidyr 包,专门处理"合并单元格"导出或纵向记录类数据。这是实际工作中非常高频的操作。

2.5 用均值或中位数替换 NA

数值型列的常用策略:用集中趋势替换:

▶️ 查看代码
# 用全局均值替换(简单但粗糙)
penguins |>
  mutate(body_mass_new = as.double(body_mass), .after = body_mass) |> 
  mutate(
    body_mass_new = replace_na(body_mass_new, mean(body_mass, na.rm = TRUE))
  ) |> 
  select(species, starts_with("body_mass")) |> 
  filter(is.na(body_mass))
▶️ 查看代码
# 更合理:用分组均值替换(同物种同性别的均值)
penguins |>
  mutate(
    body_mass_new = if_else(
      is.na(body_mass),
      mean(body_mass, na.rm = TRUE),
      body_mass
    ),
    .by = c(species, sex),
    .after = body_mass
  )

提示

分组均值填充比全局均值更合理,因为它保留了组间差异。实际操作中,if_else(is.na(x), mean(x, na.rm=TRUE), x) 配合 .by= 是一个高频模式,值得记住。

2.6 用移动平均替换 NA

时间序列数据中,移动平均(滑动窗口均值)是常见的平滑与填补策略:

▶️ 查看代码
# 安装并加载 zoo 包(提供滑动窗口函数)
# install.packages("zoo")
library(zoo)

# 模拟含 NA 的时间序列
ts_df <- tibble(
  日期 = seq(as.Date("2024-01-01"), by = "day", length.out = 10),
  销售额 = c(100, NA, 120, NA, 130, 125, NA, 140, 145, NA)
)

ts_df
▶️ 查看代码
# 用移动平均填充 NA(窗口宽度 = 3,至少有 1 个非 NA 才计算)
ts_df |>
  mutate(
    销售额_填充 = if_else(
      is.na(销售额),
      rollmean(销售额, k = 3, fill = NA, align = "right", na.rm = TRUE),
      销售额
    )
  )

注记

zoo::rollmean() 常用参数:k 为窗口宽度;fill = NA 表示窗口不足时填 NAalign = "right" 用当前时间点之前的值(因果方向,避免未来信息泄露);na.rm = TRUE 忽略窗口内的 NA

2.7 缺失值处理策略小结

策略 函数 适用场景
删除缺失行 drop_na() 缺失比例极低,且为完全随机缺失
固定值填充 replace_na() 类别型(如"未知"),或数值型(如 0)
前/后值填充 fill() 有序数据,合并单元格导出,时间序列
全局均值/中位数 mutate() + replace_na() 数值型,无明显分组结构
分组均值/中位数 mutate(.by=) + if_else() 数值型,有明显的分组结构
移动平均 zoo::rollmean() 时间序列,需要平滑处理

警告

没有万能的缺失值处理方法。 选择策略之前,必须先理解数据的业务含义和缺失的原因——是数据录入遗漏、传感器故障、还是本就"不适用"?原因不同,处理方式也应不同。

Part 3 增加行与行合并

add_row()bind_rows()

3.1 add_row():手动添加单行或多行

当需要向数据框中手动插入少量新观测值时:

▶️ 查看代码
# 创建一个小数据框
学生成绩 <- tibble(
  姓名   = c("张三", "李四", "王五"),
  科目   = c("数学", "数学", "数学"),
  成绩   = c(85, 90, 78)
)

学生成绩
▶️ 查看代码
# 在末尾添加一行
学生成绩 |>
  add_row(姓名 = "赵六", 科目 = "数学", 成绩 = 92)
▶️ 查看代码
# 在指定位置插入(.before 或 .after 参数)
学生成绩 |>
  add_row(姓名 = "新生", 科目 = "数学", .before = 2)

注记

add_row()未指定的列自动填充 NA。适合添加少量行;添加大量行时应改用 bind_rows()

3.2 bind_rows():纵向合并多个数据框

实际工作中最常见的场景:多个来源的数据结构相同,需要堆叠(如多月份报表、多地区数据):

▶️ 查看代码
# 模拟两个月份的销售数据
一月 <- tibble(月份 = "一月", 产品 = c("A", "B"), 销售额 = c(100, 200))
二月 <- tibble(月份 = "二月", 产品 = c("A", "B"), 销售额 = c(110, 190))
一月
二月
# 纵向合并
bind_rows(一月, 二月)
▶️ 查看代码
# 合并一个列表中的所有数据框(常见于批量读文件后)
月度数据 <- list(一月, 二月)
bind_rows(月度数据) 
▶️ 查看代码
# 列名不完全相同时:缺失列自动填 NA
三月 <- tibble(月份 = "三月", 产品 = "A", 销售额 = 120, 促销 = TRUE)
bind_rows(一月, 三月)

3.3 批量读取文件后合并:实战场景

▶️ 查看代码
# 场景:文件夹下有多个 CSV,每个是一个月的数据
# 批量读取 + 自动合并为一个数据框

文件列表 <- list.files("data/monthly/", pattern = "\\.csv$", full.names = TRUE)

所有数据 <- 文件列表 |>
  map(read_csv) |>           # 用 purrr::map() 批量读取,返回列表
  bind_rows()                # 纵向合并成一个大数据框

# 或者更简洁(purrr 1.0+):
所有数据 <- 文件列表 |>
  map(read_csv) |>
  list_rbind()

提示

list_rbind() 是 purrr 1.0 新增的函数,等价于 bind_rows() 作用于列表——是批量读文件后合并数据的标准范式,非常值得掌握。

参数 .names_to = "文件名" 可以保留每行来源的文件名,便于溯源。

Part 4 列合并与表连接

bind_cols()*_join()

4.1 bind_cols():横向拼接列

当两个数据框行数相同且顺序一致,只需将列并排:

▶️ 查看代码
基本信息 <- tibble(
  姓名 = c("张三", "李四", "王五"),
  年龄 = c(25, 32, 28)
)

基本信息

测试成绩 <- tibble(
  语文 = c(85, 78, 92),
  数学 = c(90, 88, 85)
)

测试成绩

bind_cols(基本信息, 测试成绩)

警告

bind_cols() 不做任何匹配——它假设两个数据框的行严格对应。如果行的顺序不一致,结果就是错误的。有共同标识符(ID 列)时,请用 *_join() 而非 bind_cols()

4.2 *_join():基于共同键的表连接

join 函数族是 dplyr 中最强大的数据整合工具,用于按共同的标识符列(键)将两个表匹配合并

▶️ 查看代码
# 准备两个有共同键的表
(学生 <- tibble(
  学号 = c("S001", "S002", "S003", "S004"),
  姓名 = c("张三", "李四", "王五", "赵六")
))

(成绩 <- tibble(
  学号 = c("S001", "S002", "S003", "S005"),
  分数 = c(85, 90, 78, 92)
))

4.3 四种主要 join 类型

▶️ 查看代码
# full_join():保留两表所有行
学生 |> full_join(成绩, by = "学号")
# 结果:S001、S002、S003、S004(分数 NA)、S005(姓名 NA)
▶️ 查看代码
# inner_join():只保留两表都有的行
学生 |> inner_join(成绩, by = "学号")
# 结果:只有 S001、S002、S003(S004 和 S005 都被丢弃)
▶️ 查看代码
# left_join():保留左表所有行(最常用)
学生 |> left_join(成绩, by = "学号")
# 张三、李四、王五有成绩;赵六(S004)在成绩表中没有 → 分数为 NA
# S005 在学生表中没有 → 被丢弃
▶️ 查看代码
# right_join():保留右表所有行
学生 |> right_join(成绩, by = "学号")
# 结果:S001、S002、S003、S005(S004 被丢弃,S005 的姓名为 NA)

注记

函数 保留行 典型用途
left_join() 左表全部 以主表为基础,附加信息
inner_join() 两表交集 只分析有完整信息的观测
right_join() 右表全部 较少用,可用 left_join 替代
full_join() 两表并集 保留所有数据,缺失填 NA

4.4 anti_join()semi_join():过滤型 join

▶️ 查看代码
# anti_join():保留左表中"在右表中找不到匹配"的行(常用于找差集)
学生 |> anti_join(成绩, by = "学号")
# 结果:只有赵六(S004)——在成绩表中没有记录的学生
▶️ 查看代码
# semi_join():保留左表中"在右表中能找到匹配"的行(但不附加右表的列)
学生 |> semi_join(成绩, by = "学号")
# 结果:张三、李四、王五——与 inner_join 相比,不附加分数列

提示

anti_join() 是实际工作中非常有用的"找差异"工具:比如找出"已下单但未付款的订单"、"在黑名单中的用户"、"数据更新后新增的记录"。

4.5 连接键的进阶用法

▶️ 查看代码
# 当两表中键列名不同时
(订单 <- tibble(order_id = 1:3, 客户编号 = c("C01", "C02", "C01")))
(客户 <- tibble(编号 = c("C01", "C02"), 客户名 = c("A公司", "B公司")))

订单 |> left_join(客户, by = join_by(客户编号 == 编号))
▶️ 查看代码
成绩表1 <- tibble(
  学号 = c("S001", "S001", "S002", "S002", "S003"),
  科目 = c("数学", "语文", "数学", "语文", "数学"),
  分数 = c(85, 90, 78, 88, 92)
)

成绩表2 <- tibble(
  学号 = c("S001", "S001", "S002", "S002"),
  科目 = c("数学", "语文", "数学", "语文"),
  满分 = c(100, 100, 150, 100)
)

成绩表1 |> left_join(成绩表2, by = join_by(学号, 科目))

结果:

  学号  科目   分数  满分
  S001  数学   85    100
  S001  语文   90    100
  S002  数学   78    150
  S002  语文   88    100
  S003  数学   92    NA     ← S003 在成绩表2中没有,满分为 NA

Part 5 table() 与交叉表

快速列联表与频率分析

5.1 table():base R 的快速频率统计

table() 是 base R 的内置函数,无需加载任何包,适合快速探索

▶️ 查看代码
# 单变量频率表
table(penguins$species)
▶️ 查看代码
# 双变量交叉表(列联表)
table(penguins$species, penguins$island)
▶️ 查看代码
# 三变量交叉表
table(penguins$species, penguins$island, penguins$sex)

注记

table() 的输出是一个特殊的 table 对象,而不是 tibble。它打印友好,但不能直接用管道操作。如需进一步处理,可以用 as.data.frame()as_tibble() 转换。

5.2 table() 的实用功能

▶️ 查看代码
# useNA = "ifany":显示 NA 的计数
table(penguins$sex, useNA = "ifany")
▶️ 查看代码
# prop.table():转换为比例
prop.table(table(penguins$species))
▶️ 查看代码
# prop.table() 的边际比例:
# margin = 1 → 行比例(每行加总为 1)
# margin = 2 → 列比例(每列加总为 1)
sp_island <- table(penguins$species, penguins$island)
prop.table(sp_island, margin = 1) |> round(2)
▶️ 查看代码
# addmargins():添加边际合计(行/列求和)
addmargins(sp_island)

5.3 table() 与 tidyverse 的对比

▶️ 查看代码
# table() 写法(base R)
table(penguins$species, penguins$island)

Part 6 实用技巧拾遗

值得掌握的其他命令与技巧

6.1 distinct():去重

▶️ 查看代码
# 删除完全重复的行
penguins |> distinct()

# 只保留指定列的唯一组合(类似 SQL 的 SELECT DISTINCT)
penguins |>
  distinct(species, island)
▶️ 查看代码
# .keep_all = TRUE:保留所有列,但只保留每组第一行
penguins |>
  distinct(species, island, .keep_all = TRUE)

注记

distinct() 在去除重复数据时非常有用,尤其是在多表 join 之后可能产生重复行的场景。配合 .keep_all = TRUE 可以在去重的同时保留其余列的信息。

6.2 relocate():调整列顺序

▶️ 查看代码
# 将 sex 和 year 移到最前面
penguins |>
  relocate(sex, year) |>
  head(3)
▶️ 查看代码
# .before / .after:精确控制位置
penguins |>
  relocate(body_mass, .before = bill_len) |>
  head(3)
▶️ 查看代码
# 将所有字符型列移到最前面
penguins |>
  relocate(where(is.character)) |>
  head(3)

6.3 pull():提取单列为向量

▶️ 查看代码
# 从 tibble 中提取一列,返回向量(而非单列 tibble)
penguins |>
  filter(species == "Adelie") |>
  pull(body_mass)
▶️ 查看代码
# 实际用途:计算后直接拿到结果向量,而非 tibble
penguins |>
  drop_na(body_mass) |>
  summarise(均值 = mean(body_mass)) |>
  pull(均值)     # 返回一个数值,而非含一行一列的 tibble

注记

pull() 等价于 $ 操作符,但可以放在管道末尾——在需要"最终拿到一个向量"的场景下,比 $ 更优雅。

6.4 n_distinct()tabyl()

▶️ 查看代码
# n_distinct():统计唯一值数量(可在 summarise 中使用)
penguins |>
  summarise(
    物种数   = n_distinct(species),
    岛屿数   = n_distinct(island),
    年份数   = n_distinct(year)
  )
▶️ 查看代码
# janitor::tabyl():更强大的频率表(包含比例、百分比)
# install.packages("janitor")
library(janitor)

penguins |>
  tabyl(species)

# 双变量交叉表,自动显示比例
penguins |>
  tabyl(species, island) |>
  adorn_percentages("row") |>   # 行比例
  adorn_pct_formatting(digits = 1) |>  # 格式化为百分比
  adorn_ns()                    # 同时显示原始计数

提示

janitor 包是数据清洗的利器,tabyl()table()count() 都更强大,直接输出包含频率和百分比的整洁表格,是数据探索阶段的好帮手。

6.5 coalesce():取第一个非 NA 值

▶️ 查看代码
# coalesce():从多个向量中依次取第一个非 NA 的值
# 常用于"用备用数据填补主数据的缺失"
df <- tibble(
  主数据源 = c(100, NA, 300, NA, 500),
  备用数据 = c(NA,  200, NA,  400, 999)
)

df |>
  mutate(
    最终值 = coalesce(主数据源, 备用数据)
  )
# 结果:100, 200, 300, 400, 500
▶️ 查看代码
# 实际场景:整合来自不同渠道的数据
客户信息 <- tibble(
  客户ID  = 1:3,
  CRM电话 = c("138-0000", NA, "139-1111"),
  官网电话 = c(NA, "137-2222", "139-3333")
)

客户信息 |>
  mutate(
    联系电话 = coalesce(CRM电话, 官网电话)
  )

6.6 janitor::clean_names():自动清洗列名

▶️ 查看代码
# 真实数据的列名往往"乱七八糟"
df_messy <- tibble(
  `姓  名`    = c("张三", "李四"),
  `Age (岁)`  = c(25, 32),
  `2024销售额` = c(100, 200),
  `Is.Active?` = c(TRUE, FALSE)
)

df_messy
#> [1] "姓  名"     "Age (岁)"  "2024销售额" "Is.Active?"
▶️ 查看代码
library(janitor)

# clean_names() 自动将列名转换为规范的 snake_case
df_messy |>
  clean_names()

提示

clean_names() 是读入外部数据后第一步就该运行的函数——它会去除空格、特殊字符,统一转换为小写 snake_case,让后续的列引用不再需要反引号。

6.7 glimpse()skim():快速了解数据结构

▶️ 查看代码
# glimpse():转置打印,每列一行,显示类型和前几个值
glimpse(penguins)
▶️ 查看代码
# skimr::skim():更全面的数据摘要(强烈推荐!)
# install.packages("skimr")
library(skimr)

skim(penguins)
# 输出包含:
# - 数据框基本信息(行数、列数)
# - 每列:类型、缺失数/率、唯一值数
# - 数值列:均值、标准差、分位数、迷你直方图
# - 字符/因子列:空字符串数、唯一值数

注记

在开始任何分析前,glimpse() + skim() 的组合是了解一份陌生数据集的最快方式。skim() 的迷你直方图(在终端中显示为 Unicode 字符)能让你在不作图的情况下,直觉地感受数据分布。

6.8 across() 的命名控制:.names 参数

▶️ 查看代码
# 默认情况下,across() 会覆盖原列
penguins |>
  mutate(across(where(is.numeric), ~ .x / 1000)) |>
  head(3)
▶️ 查看代码
# .names 参数:控制新列名,{.col} 是原列名占位符
penguins |>
  mutate(
    across(
      where(is.numeric),
      ~ .x / 1000,
      .names = "{.col}_千"   # 新列名 = 原列名 + "_千"
    )
  ) |>
  select(species, ends_with("_千")) |>
  head(3)
# 这样原列不被覆盖,而是新增 body_mass_千、bill_len_千 等列

提示

.names 参数的 {.col}{.fn} 是两个占位符:{.col} 代表当前列名,{.fn} 代表当前函数名(当传入多个函数时使用)。掌握这个参数,across() 就真正"满血"了。

综合实战

从"乱"数据到分析结果的完整流程

综合实战:数据清洗全流程

▶️ 查看代码
# 模拟一份"来自真实世界"的脏数据
raw_data <- tibble(
  `学生 ID`  = c("S001", "S002", "S003", "S001", "S004"),
  `姓名`     = c("张三", "李四", "王五", "张三", NA),
  `成绩(%)`  = c("85%", "90%", NA, "85%", "78%"),
  `等级`     = c("良好", NA, "及格", "良好", "良好"),
  `班级`     = c("A班", "A班", NA, "A班", "B班")
)

raw_data
▶️ 查看代码
# 完整清洗流程
clean_data <- raw_data |>
  # 1. 清洗列名
  janitor::clean_names() |>

  # 2. 去重
  distinct() |>

  # 3. 修复数据类型
  mutate(
    cheng_ji = str_remove(cheng_ji_percent, "%") |> as.numeric()
  ) |>

  # 4. 填充缺失值
  fill(ban_ji, .direction = "down") |>
  mutate(
    deng_ji = replace_na(deng_ji, "待评定"),
    xing_ming = replace_na(xing_ming, "未知")
  ) |>

  # 5. 用分班均值填补成绩缺失
  mutate(
    cheng_ji = if_else(
      is.na(cheng_ji),
      mean(cheng_ji, na.rm = TRUE),
      cheng_ji
    ),
    .by = ban_ji
  ) |>

  # 6. 整理列顺序
  select(xue_sheng_id, xing_ming, ban_ji, cheng_ji, deng_ji)

clean_data

本讲小结

  • 替换字段数据mutate() 覆盖同名列;if_else() / case_when() 条件替换;str_replace() 字符串替换;as.*() 类型转换;across() 批量处理多列

  • 处理缺失值drop_na() 删行;replace_na() 固定值填充;fill() 前/后值填充(处理合并单元格必备);分组均值填充;zoo::rollmean() 移动平均——无万能策略,理解业务含义是前提

  • 增加行与行合并add_row() 手动插入少量行;bind_rows() 纵向合并结构相同的多个表;map() |> list_rbind() 批量读文件后合并

  • 列合并与表连接bind_cols() 仅用于行严格对应时;left_join() / inner_join() / full_join() 基于键列匹配合并;anti_join() 找差集;semi_join() 过滤但不附加列

  • table():base R 快速列联表;prop.table() 转换比例;addmargins() 添加合计;探索阶段首选,需要继续管道时改用 count()

  • 实用技巧distinct() 去重;relocate() 调列序;pull() 提取为向量;coalesce() 取第一个非 NA;janitor::clean_names() 清洗列名;skimr::skim() 全面数据摘要

课后练习

基础练习(必做)

  1. 使用 penguins 数据,将 species 列中的英文名替换为中文(阿德利/帽带/巴布亚),然后用 table()count() 分别制作物种 × 岛屿的交叉表,比较两者的输出格式
  2. 模拟一份含有缺失值的数据框(至少含数值列和字符列),分别尝试:固定值填充、前值填充、分组均值填充三种方式,观察结果的差异
  3. 准备两个有公共键列的 tibble,分别执行 left_join()inner_join()full_join()anti_join(),观察每种方式保留的行数差异,并用文字解释原因

进阶挑战(选做)

  1. bind_rows() 模拟批量合并场景:手动创建 3 个结构略有差异的 tibble(列名不完全相同),合并后观察 NA 的产生规律,再用 replace_na() 补全
  2. 使用 across(.names = ...) 在不覆盖原列的情况下,对 penguins 所有数值列同时计算标准化值((x - mean(x)) / sd(x)),新列命名为 原列名_z

下讲预告

第7讲:tidyr 数据整形

  • pivot_longer():宽表转长表(最常用的整形操作)
  • pivot_wider():长表转宽表
  • separate()unite():拆分与合并列
  • nest()unnest():嵌套数据框入门
  • 综合实战:从原始宽格式数据到可视化就绪的长格式

提示

数据整形是 tidyverse 中最"烧脑"但也最有价值的技能之一。学完第7讲,你就能把任何格式的数据转换成 ggplot2 所需要的形式——这是从"数据清洗"迈向"数据可视化"的关键一步。

谢谢!

第6讲:tidyverse 数据操作(下)


「数据清洗占据了数据分析 80% 的时间——但正是这 80%,决定了那 20% 分析结论的可靠程度。」