第6讲:tidyverse 数据操作 ~ Part 2
2026年04月03日
|> 让代码从左到右、从上到下,与逻辑顺序一致count():快速频率统计;add_count() 保留原始行数filter() 按条件筛选;arrange() 排序;slice_max() 等取极值行select() 选列;mutate() 创建/修改列;if_else() 与 case_when() 条件判断summarise() 压缩为汇总行;.by= 在函数内部直接分组NA(约20分钟)add_row() 与 bind_rows()(约15分钟)bind_cols() 与 *_join()(约25分钟)table() 与交叉表 ——快速列联表(约10分钟)提示
本讲是第5讲的延伸,聚焦数据清洗中最常见的"脏活":修数据、补缺失、拼表格。学完这两讲,你就能应对日常分析中 80% 的数据准备工作。
修改已有列的值
mutate() 直接覆盖已有列最基本的替换方式:用 mutate() 对同名列赋新值,原列被覆盖。
注记
mutate(列名 = 新值) 当列名已存在时,会覆盖原列;当列名不存在时,会新增一列。这是同一个函数的两种行为,区别只在于列名是否已经存在。
if_else() 与 case_when()
实际数据中,往往只想替换满足某条件的值,其余保持不变。
提示
case_when() 中如果没有匹配任何条件,结果为 NA。用 .default = 原值 可以保留未匹配的值,例如 .default = species。
str_replace() 与 str_remove()
当替换逻辑基于字符串模式时,stringr 包的函数更方便:
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()
|
across() + as.*()
当多列需要转换为同一类型时,across() 大幅简化代码:
# 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)
)提示
across(列选择器, 函数) 是 dplyr 1.0 引入的核心机制,where(is.numeric) 选择所有数值列,starts_with("score_") 按前缀选列,c(col1, col2) 指定具体列——与 select() 的辅助函数完全通用。
多种策略替换 NA
在替换之前,先了解缺失值的分布是好习惯:
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
| 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 |
drop_na():删除含缺失值的行最激进的处理方式——直接删行:
警告
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>
replace_na():用固定值替换 NA最直接的填补方式:
fill():用前值或后值填充 NA当数据是有序的(如时间序列、问卷中合并单元格导出的数据),用相邻值填充很自然:
注记
fill() 来自 tidyr 包,专门处理"合并单元格"导出或纵向记录类数据。这是实际工作中非常高频的操作。
数值型列的常用策略:用集中趋势替换:
提示
分组均值填充比全局均值更合理,因为它保留了组间差异。实际操作中,if_else(is.na(x), mean(x, na.rm=TRUE), x) 配合 .by= 是一个高频模式,值得记住。
时间序列数据中,移动平均(滑动窗口均值)是常见的平滑与填补策略:
注记
zoo::rollmean() 常用参数:k 为窗口宽度;fill = NA 表示窗口不足时填 NA;align = "right" 用当前时间点之前的值(因果方向,避免未来信息泄露);na.rm = TRUE 忽略窗口内的 NA。
| 策略 | 函数 | 适用场景 |
|---|---|---|
| 删除缺失行 | drop_na() |
缺失比例极低,且为完全随机缺失 |
| 固定值填充 | replace_na() |
类别型(如"未知"),或数值型(如 0) |
| 前/后值填充 | fill() |
有序数据,合并单元格导出,时间序列 |
| 全局均值/中位数 |
mutate() + replace_na()
|
数值型,无明显分组结构 |
| 分组均值/中位数 |
mutate(.by=) + if_else()
|
数值型,有明显的分组结构 |
| 移动平均 | zoo::rollmean() |
时间序列,需要平滑处理 |
警告
没有万能的缺失值处理方法。 选择策略之前,必须先理解数据的业务含义和缺失的原因——是数据录入遗漏、传感器故障、还是本就"不适用"?原因不同,处理方式也应不同。
add_row() 与 bind_rows()
add_row():手动添加单行或多行当需要向数据框中手动插入少量新观测值时:
注记
add_row() 中未指定的列自动填充 NA。适合添加少量行;添加大量行时应改用 bind_rows()。
bind_rows():纵向合并多个数据框实际工作中最常见的场景:多个来源的数据结构相同,需要堆叠(如多月份报表、多地区数据):
提示
list_rbind() 是 purrr 1.0 新增的函数,等价于 bind_rows() 作用于列表——是批量读文件后合并数据的标准范式,非常值得掌握。
参数 .names_to = "文件名" 可以保留每行来源的文件名,便于溯源。
bind_cols() 与 *_join()
bind_cols():横向拼接列当两个数据框行数相同且顺序一致,只需将列并排:
警告
bind_cols() 不做任何匹配——它假设两个数据框的行严格对应。如果行的顺序不一致,结果就是错误的。有共同标识符(ID 列)时,请用 *_join() 而非 bind_cols()。
*_join():基于共同键的表连接join 函数族是 dplyr 中最强大的数据整合工具,用于按共同的标识符列(键)将两个表匹配合并:
注记
| 函数 | 保留行 | 典型用途 |
|---|---|---|
left_join() |
左表全部 | 以主表为基础,附加信息 |
inner_join() |
两表交集 | 只分析有完整信息的观测 |
right_join() |
右表全部 | 较少用,可用 left_join 替代 |
full_join() |
两表并集 | 保留所有数据,缺失填 NA |
anti_join() 与 semi_join():过滤型 join提示
anti_join() 是实际工作中非常有用的"找差异"工具:比如找出"已下单但未付款的订单"、"在黑名单中的用户"、"数据更新后新增的记录"。
结果:
学号 科目 分数 满分
S001 数学 85 100
S001 语文 90 100
S002 数学 78 150
S002 语文 88 100
S003 数学 92 NA ← S003 在成绩表2中没有,满分为 NA
table() 与交叉表快速列联表与频率分析
table():base R 的快速频率统计table() 是 base R 的内置函数,无需加载任何包,适合快速探索:
注记
table() 的输出是一个特殊的 table 对象,而不是 tibble。它打印友好,但不能直接用管道操作。如需进一步处理,可以用 as.data.frame() 或 as_tibble() 转换。
table() 的实用功能table() 与 tidyverse 的对比值得掌握的其他命令与技巧
distinct():去重注记
distinct() 在去除重复数据时非常有用,尤其是在多表 join 之后可能产生重复行的场景。配合 .keep_all = TRUE 可以在去重的同时保留其余列的信息。
relocate():调整列顺序pull():提取单列为向量注记
pull() 等价于 $ 操作符,但可以放在管道末尾——在需要"最终拿到一个向量"的场景下,比 $ 更优雅。
n_distinct() 与 tabyl()
提示
janitor 包是数据清洗的利器,tabyl() 比 table() 和 count() 都更强大,直接输出包含频率和百分比的整洁表格,是数据探索阶段的好帮手。
coalesce():取第一个非 NA 值janitor::clean_names():自动清洗列名提示
clean_names() 是读入外部数据后第一步就该运行的函数——它会去除空格、特殊字符,统一转换为小写 snake_case,让后续的列引用不再需要反引号。
glimpse() 与 skim():快速了解数据结构注记
在开始任何分析前,glimpse() + skim() 的组合是了解一份陌生数据集的最快方式。skim() 的迷你直方图(在终端中显示为 Unicode 字符)能让你在不作图的情况下,直觉地感受数据分布。
across() 的命名控制:.names 参数提示
.names 参数的 {.col} 和 {.fn} 是两个占位符:{.col} 代表当前列名,{.fn} 代表当前函数名(当传入多个函数时使用)。掌握这个参数,across() 就真正"满血"了。
从"乱"数据到分析结果的完整流程
# 完整清洗流程
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() 全面数据摘要
penguins 数据,将 species 列中的英文名替换为中文(阿德利/帽带/巴布亚),然后用 table() 和 count() 分别制作物种 × 岛屿的交叉表,比较两者的输出格式left_join()、inner_join()、full_join()、anti_join(),观察每种方式保留的行数差异,并用文字解释原因bind_rows() 模拟批量合并场景:手动创建 3 个结构略有差异的 tibble(列名不完全相同),合并后观察 NA 的产生规律,再用 replace_na() 补全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% 分析结论的可靠程度。」
数据挖掘与R语言 | 第6讲:tidyverse 数据操作 ~ 2