7  R 数据处理进阶

8 R 数据处理进阶

上一章我们学习了 R 的基础操作。本章将介绍 tidyverse——R 中最流行的数据处理生态系统,它能让你的数据处理代码更简洁、更易读。

8.1 tidyverse 简介

tidyverse 是一组协同工作的 R 包集合,由 Hadley Wickham 等人开发:

library(tidyverse)

加载 tidyverse 会同时加载以下核心包:

包名 功能
dplyr 数据操作(筛选、排序、汇总)
tidyr 数据重塑(长宽格式转换)
ggplot2 数据可视化
readr 数据读取
stringr 字符串处理
forcats 因子处理
tibble 增强版数据框
purrr 函数式编程

8.2 管道操作符

管道操作符 |> 是 tidyverse 编程风格的核心。它的作用是:把左边的结果传给右边函数的第一个参数。这意味着右边的函数必须在第一个参数位置接收数据,tidyverse 的函数都遵循这个设计。

# 传统写法(从内向外读,难以理解)
round(mean(c(1.5, 2.3, 3.7, 4.1)), 1)

# 管道写法(从左到右读,清晰直观)
c(1.5, 2.3, 3.7, 4.1) |>
  mean() |>
  round(1)

上面的管道写法等价于:

  1. c(1.5, 2.3, 3.7, 4.1) 的结果传给 mean() → 得到 2.9
  2. 2.9 传给 round(1) → 即 round(2.9, 1) → 得到 2.9

你可以把 |> 读作”然后”:取这些数字,然后求平均,然后四舍五入。

Tip管道的快捷键

在 RStudio 中,按 Ctrl + Shift + M 可以快速输入管道操作符 %>%(如果你使用的是 magrittr/tidyverse 包)或 Ctrl + Shift + M 输入原生管道 |>(R 4.1+)。此外,还有一些提高效率的快捷键值得掌握。

常用 RStudio 快捷键

Ctrl + Shift + M — 输入管道操作符 |>(Windows/Linux)或 %>%(macOS)。这是使用管道时最常用的快捷键,省去每次手动输入符号的麻烦。

Ctrl + Shift + K — 渲染当前 Quarto/R Markdown 文档。一键生成 PDF、HTML 或 Word 格式的输出报告。

Ctrl + Shift + Enter — 运行当前代码块(而非只运行当前行)。在调试脚本时非常有用。

Ctrl + Shift + C — 批量注释/取消注释选中的多行代码。

Ctrl + Alt + I — 在 R Markdown/Quarto 中插入新的代码块。这是写报告时的常用操作。

Ctrl + Shift + A — 重新格式化(reformat)选中的 R 代码,自动对齐缩进和赋值运算符。

管道与快捷键的配合使用

管道操作通常与快捷键配合使用可以大幅提升效率。以下是一个典型的工作流:

library(tidyverse)

# 演示数据:土壤调查数据
soil <- tibble(
  site_id = rep(c("S1", "S2", "S3", "S4"), each = 3),
  depth = rep(c("0-10cm", "10-20cm", "20-40cm"), 4),
  ph = round(rnorm(12, mean = 5.5, sd = 0.5), 2),
  organic_c = round(rnorm(12, mean = 25, sd = 8), 2)
)

# 不用管道(逐行赋值)
result_1 <- filter(soil, ph > 5.0)
result_2 <- mutate(result_1, log_oc = log(organic_c))
result_3 <- arrange(result_2, desc(log_oc))

# 用管道(优雅简洁)
result <- soil |>
  filter(ph > 5.0) |>
  mutate(log_oc = log(organic_c)) |>
  arrange(desc(log_oc))

# 快捷键 Ctrl+Shift+M 输入 |> 后,再用 Ctrl+Shift+Enter 运行整块

# 进阶:多步汇总
summary_table <- soil |>
  group_by(site_id) |>
  summarise(
    mean_ph = mean(ph),
    mean_oc = mean(organic_c),
    n_samples = n()
  ) |>
  mutate(status = ifelse(mean_ph > 5.5, "偏酸", "中性"))

print(summary_table)

生态学案例

在分析马尾松林土壤数据时,一位学生写了20行嵌套函数:arrange(mutate(filter(group_by(survey, site), ...)))。这种写法不仅难以阅读,而且调试困难。改用管道后,同样的操作变成了4行线性代码,每一步的功能一目了然。更重要的是,当他需要在中途增加一个步骤(如计算多样性指数)时,只需要在管道中插入一行即可,无需重写整个嵌套结构。管道不仅是语法糖,更是提升代码可维护性的关键工具

扩展记录: 2026-04-10 | 扩展者:Clawd | 目标字数:800+

Note|>%>% 的区别

你可能在网上看到另一种管道 %>%(来自 magrittr 包)。主要区别:

  • |> 是 R 4.1+ 内置的原生管道,不需要加载任何包
  • %>% 是 magrittr 包提供的管道,需要加载 tidyverse 或 magrittr
  • %>% 支持用 . 作为占位符(如 x %>% f(2, .)),|> 不支持这种写法

本课程统一使用 |>。在大多数场景下两者可以互换,但如果你在旧教程中看到 %>% 配合 . 占位符的用法,需要改写为中间变量或匿名函数的形式。

8.2.1 示例数据

本章使用一个模拟的植物样方调查数据集:

# 创建示例数据
set.seed(2027)
survey <- tibble(
  plot_id = rep(paste0("P", 1:10), each = 5),
  species = sample(c("马尾松", "杉木", "桉树", "樟树", "楠木"), 50, replace = TRUE),
  height = round(rnorm(50, mean = 12, sd = 4), 1),
  dbh = round(rnorm(50, mean = 20, sd = 8), 1),
  habitat = rep(sample(c("山脊", "山坡", "山谷"), 10, replace = TRUE), each = 5),
  soil_ph = round(rnorm(50, mean = 5.5, sd = 0.8), 2)
)

# 人为加入一些缺失值
survey$height[c(3, 17, 28)] <- NA
survey$dbh[c(8, 42)] <- NA

head(survey, 10)

8.3 dplyr:数据操作五大动词

8.3.1 filter() — 筛选行

# 筛选马尾松
survey |>
  filter(species == "马尾松")

# 多条件筛选:株高 > 15 且 土壤 pH < 6
survey |>
  filter(height > 15, soil_ph < 6)

# 或条件:马尾松或杉木
survey |>
  filter(species %in% c("马尾松", "杉木"))

8.3.2 select() — 选择列

# 选择特定列
survey |>
  select(plot_id, species, height)

# 排除某列
survey |>
  select(-soil_ph)

# 选择数值列
survey |>
  select(where(is.numeric))

8.3.3 mutate() — 新增/修改列

# 新增列:计算树木断面积(basal area)
survey |>
  mutate(
    basal_area = pi * (dbh / 200)^2,    # 单位:m²
    height_class = case_when(
      is.na(height) ~ NA_character_,   # 先处理缺失值,避免被误判
      height < 8    ~ "矮",
      height < 15   ~ "中",
      TRUE          ~ "高"
    )
  ) |>
  head()

8.3.4 arrange() — 排序

# 按株高降序排列
survey |>
  arrange(desc(height)) |>
  head()

# 先按样方排序,再按株高排序
survey |>
  arrange(plot_id, desc(height)) |>
  head()

8.3.5 summarise() + group_by() — 分组汇总

# 按物种分组统计
survey |>
  group_by(species) |>
  summarise(
    n = n(),
    mean_height = mean(height, na.rm = TRUE),
    sd_height = sd(height, na.rm = TRUE),
    mean_dbh = mean(dbh, na.rm = TRUE)
  ) |>
  arrange(desc(mean_height))
# 按样方和生境分组
survey |>
  group_by(habitat) |>
  summarise(
    n_trees = n(),
    n_species = n_distinct(species),
    mean_ph = mean(soil_ph, na.rm = TRUE)
  )

8.4 数据查看与质量检查

在实际数据分析前,首先要对数据进行全面”体检”,了解数据的基本情况,及时发现缺失值、异常值、类型错误等问题。

8.4.1 基本查看函数

# 查看数据基本信息(显示每列的类型和前几个值)
glimpse(survey)

# 查看数据前几行
head(survey)

# 查看数据后几行
tail(survey)

# 数据框的行列数
dim(survey)
nrow(survey)  # 行数
ncol(survey)  # 列数

# 查看完整列名
names(survey)

# 查看数值列的统计摘要
summary(survey)

8.4.2 缺失值诊断

# 统计每列缺失值数量
survey |>
  summarise(across(everything(), \(x) sum(is.na(x)))) |>
  pivot_longer(everything(), names_to = "列名", values_to = "缺失数")

# 查看含缺失值的行
survey |>
  filter(if_any(everything(), is.na)) |>
  head()

# 缺失值热图(可视化缺失模式)
# install.packages("naniar")
library(naniar)
gg_miss_var(survey)

8.4.3 异常值检测

# 使用箱线图识别异常值
survey |>
  filter(!is.na(height)) |>
  ggplot(aes(y = height)) +
  geom_boxplot()

# 基于 1.5 倍 IQR 法则标记异常值
survey |>
  mutate(
    height_q1 = quantile(height, 0.25),
    height_q3 = quantile(height, 0.75),
    height_iqr = height_q3 - height_q1,
    is_outlier = height < (height_q1 - 1.5 * height_iqr) |
                 height > (height_q3 + 1.5 * height_iqr)
  ) |>
  filter(is_outlier) |>
  select(plot_id, species, height)
Tip数据检查的标准流程
  1. 运行 glimpse() —— 快速了解列名和类型
  2. 运行 summary() —— 查看数值列的分布(最小、均值、最大)
  3. 检查 is.na() —— 确认缺失值的位置和数量
  4. 可视化 —— 用 ggplot2 绑制直方图/箱线图发现异常值

8.4.4 across():对多列同时操作

across() 是 dplyr 1.0+ 引入的强大函数,可以在 mutate()summarise() 中对多列同时应用相同的变换:

# 对所有数值列计算均值
survey |>
  summarise(across(where(is.numeric), mean, na.rm = TRUE))

# 对多个特定列分别计算
survey |>
  summarise(across(c(height, dbh, soil_ph), list(mean = mean, sd = sd), na.rm = TRUE))

# 在 mutate 中使用 across 批量标准化
survey |>
  mutate(across(where(is.numeric), \(x) scale(x)[,1], .names = "z_{.col}")) |>
  select(plot_id, z_height, z_dbh, z_soil_ph) |>
  head()

# 按组计算并保留分组列
survey |>
  group_by(habitat) |>
  summarise(across(where(is.numeric), \(x) round(mean(x, na.rm = TRUE), 2)), .groups = "drop")

生态学应用——多样本多指标汇总

# 快速生成所有数值指标的汇总表
survey |>
  group_by(species) |>
  summarise(
    across(where(is.numeric), \(x) list(
      mean = round(mean(x, na.rm = TRUE), 2),
      sd = round(sd(x, na.rm = TRUE), 2),
      min = round(min(x, na.rm = TRUE), 2),
      max = round(max(x, na.rm = TRUE), 2)
    ))
  )

8.4.5 if_else() 与 case_when() 的区别

函数 适用场景 返回类型
if_else(condition, true, false) 单一条件判断 必须类型一致
case_when(...) 多条件多返回值 灵活,不要求类型一致
dplyr::if_else(...) 同上,但参数更严格 严格类型检查
# if_else:简单二选一
survey |>
  mutate(height_level = if_else(height > 12, "高", "矮"))

# case_when:多条件(类似其他语言的 switch)
survey |>
  mutate(dbh_class = case_when(
    dbh < 10           ~ "小径材",
    dbh >= 10 & dbh < 25 ~ "中径材",
    dbh >= 25          ~ "大径材",
    TRUE               ~ NA_character_
  ))

8.4.6 实战:条件筛选与分组的典型模式

# 模式 1:找出每个样方中最高的树
tallest_per_plot <- survey |>
  filter(!is.na(height)) |>
  slice_max(height, n = 1, by = plot_id)

# 模式 2:计算每个物种的超优势度(占样方的比例)
dominance <- survey |>
  filter(!is.na(height)) |>
  group_by(plot_id, species) |>
  summarise(n_trees = n(), .groups = "drop_last") |>
  mutate(prop = n_trees / sum(n_trees)) |>
  slice_max(prop, n = 1, by = plot_id)

# 模式 3:按条件筛选后分组统计
survey |>
  filter(
    !is.na(height),
    !is.na(dbh),
    habitat %in% c("山脊", "山坡")
  ) |>
  group_by(habitat, species) |>
  summarise(
    n = n(),
    mean_h = round(mean(height), 1),
    mean_dbh = round(mean(dbh), 1),
    .groups = "drop"
  ) |>
  filter(n >= 3) |>
  arrange(habitat, desc(mean_h))

8.5 组合使用:数据处理流水线

dplyr 的强大之处在于可以用管道将多个操作串联起来:

# 完整的数据处理流水线
result <- survey |>
  filter(!is.na(height), !is.na(dbh)) |>       # 1. 去除缺失值
  mutate(basal_area = pi * (dbh / 200)^2) |>    # 2. 计算断面积
  group_by(habitat, species) |>                  # 3. 按生境和物种分组
  summarise(
    n = n(),
    mean_height = round(mean(height), 1),
    total_ba = round(sum(basal_area), 4),
    .groups = "drop"
  ) |>
  arrange(habitat, desc(total_ba))               # 4. 排序

result

8.6 tidyr:数据重塑

生态学数据经常需要在”长格式”和”宽格式”之间转换。

8.6.1 宽格式 → 长格式:pivot_longer()

# 宽格式数据:每个物种一列
wide_data <- tibble(
  plot = paste0("P", 1:5),
  马尾松 = c(12, 8, 0, 15, 6),
  杉木 = c(5, 0, 10, 3, 8),
  桉树 = c(0, 7, 4, 0, 2)
)
wide_data

# 转为长格式
long_data <- wide_data |>
  pivot_longer(
    cols = -plot,           # 除了 plot 列,其他都转
    names_to = "species",   # 列名变成 species 列
    values_to = "count"     # 值变成 count 列
  )
long_data

8.6.2 长格式 → 宽格式:pivot_wider()

pivot_wider()pivot_longer() 的逆操作,用于将长格式数据转换为宽格式。在生态学数据分析中,这种转换常用于以下场景:

典型应用场景

  1. 物种多度矩阵构建:将样方-物种-多度的长格式数据转换为”样方×物种”矩阵,用于排序分析(PCA、NMDS)或多样性指数计算。
  2. 时间序列数据展示:将监测站点-日期-指标的长格式数据转换为”日期×站点”表格,便于横向对比不同站点的变化趋势。
  3. 实验数据汇总:将处理-重复-测量值的长格式数据转换为”处理×重复”表格,用于方差分析或制作汇总表。

基本语法

# 转回宽格式
long_data |>
  pivot_wider(
    names_from = species,    # 哪一列的值变成新列名
    values_from = count      # 哪一列的值填充到新列中
  )

参数说明

  • names_from:指定哪一列的唯一值将成为新列的列名(如物种名)
  • values_from:指定哪一列的值将填充到新列中(如多度值)
  • values_fill:当某些组合缺失时,用什么值填充(默认 NA,常用 0
  • names_prefix:为新列名添加前缀(如 "sp_"

生态学实战案例——物种多度矩阵构建

# 模拟样方调查长格式数据
set.seed(2027)
survey_long <- tibble(
  plot_id = rep(paste0("P", 1:5), each = 3),
  species = rep(c("马尾松", "杉木", "桉树"), 5),
  abundance = sample(0:20, 15, replace = TRUE)
) |>
  filter(abundance > 0)  # 移除未出现的物种

# 转换为物种×样方矩阵(用于 vegan 包的排序分析)
species_matrix <- survey_long |>
  pivot_wider(
    names_from = species,
    values_from = abundance,
    values_fill = 0  # 未记录的物种填充为 0
  )

species_matrix

处理重复值问题

names_fromid_cols(标识列)的组合不唯一时,pivot_wider() 会报错或产生列表列。此时需要先聚合数据:

# 错误示例:同一样方同一物种有多条记录
duplicate_data <- tibble(
  plot = c("P1", "P1", "P2"),
  species = c("马尾松", "马尾松", "杉木"),
  count = c(5, 8, 10)
)

# 解决方案:先聚合再转换
duplicate_data |>
  group_by(plot, species) |>
  summarise(total_count = sum(count), .groups = "drop") |>
  pivot_wider(names_from = species, values_from = total_count, values_fill = 0)

高级用法——多值列转换

# 同时转换多个测量值(如均值和标准差)
survey_stats <- tibble(
  plot = rep(paste0("P", 1:3), each = 2),
  species = rep(c("马尾松", "杉木"), 3),
  mean_height = round(rnorm(6, 12, 2), 1),
  sd_height = round(rnorm(6, 1.5, 0.3), 2)
)

survey_stats |>
  pivot_wider(
    names_from = species,
    values_from = c(mean_height, sd_height),
    names_glue = "{species}_{.value}"  # 自定义列名格式
  )

注意事项

  • 转换前确保 names_from 列的值适合作为列名(避免特殊字符或过长)
  • 如果原数据中某些组合缺失,values_fill = 0 可避免 NA 干扰后续计算
  • 宽格式数据占用内存较大,仅在必要时转换(如输出报告或特定分析需求)

扩展记录: 2026-04-11 | 扩展者:Clawd | 目标字数:800+

Note什么时候用长格式?什么时候用宽格式?

数据格式的选择直接影响分析效率和代码复杂度。理解长格式和宽格式的适用场景,可以避免在数据处理中走弯路。

长格式(Long Format)的特点

  • 每一行代表一个观测值(observation)
  • 变量名存储在一列中(如”物种”列),对应的值存储在另一列中(如”多度”列)
  • 数据行数多,列数少
  • 符合”整洁数据”(Tidy Data)原则:每个变量一列,每个观测一行

宽格式(Wide Format)的特点

  • 每一行代表一个观测单元(如一个样方)
  • 不同变量分散在多列中(如每个物种一列)
  • 数据行数少,列数多
  • 更接近人类阅读习惯(如 Excel 表格)

格式选择的决策树

需要用 ggplot2 绘图?
  └─ 是 → 长格式(facet_wrap/facet_grid 需要长格式)
  └─ 否 → 继续判断

需要按组统计(group_by + summarise)?
  └─ 是 → 长格式(分组变量在一列中更方便)
  └─ 否 → 继续判断

需要进行矩阵运算(如 PCA、相关矩阵)?
  └─ 是 → 宽格式(样本×变量矩阵)
  └─ 否 → 继续判断

需要展示给他人查看或导出到 Excel?
  └─ 是 → 宽格式(更易读)
  └─ 否 → 默认使用长格式(便于后续处理)

生态学典型场景对比

分析任务 推荐格式 原因
物种多样性指数计算(Shannon、Simpson) 长格式 按样方分组计算,每个物种一行更方便
ggplot2 绘图(如物种多度柱状图) 长格式 facet_wrap(~species) 需要物种在一列中
排序分析(NMDS、PCA) 宽格式 vegan 包要求样方×物种矩阵
相关性分析(cor()) 宽格式 需要变量在不同列中
数据展示与报告 宽格式 表格更易读,符合人类阅读习惯

实战案例——物种多度数据的格式转换

# 宽格式(物种×样方表,适合展示)
sp_wide <- tibble(
  plot = paste0("P", 1:6),
  马尾松 = c(12, 8, 15, 0, 10, 6),
  杉木 = c(5, 0, 8, 12, 3, 9),
  阔叶树 = c(8, 15, 6, 5, 12, 10)
)

# 宽 → 长:用于 ggplot2 绘图和多样性计算
sp_long <- sp_wide |>
  pivot_longer(-plot, names_to = "species", values_to = "abundance") |>
  filter(abundance > 0)  # 移除未出现的物种

# 长 → 宽:用于 vegan 包的排序分析
sp_wide2 <- sp_long |>
  pivot_wider(names_from = species, values_from = abundance, values_fill = 0)

一般原则

  1. 存储和分析用长格式:便于筛选、分组、可视化
  2. 展示和报告用宽格式:更符合人类阅读习惯
  3. 矩阵运算用宽格式:PCA、相关分析等需要样本×变量矩阵
  4. 不确定时优先长格式:长格式更灵活,可以随时转换为宽格式

常见误区

  • ❌ 认为宽格式”更整洁”(实际上长格式才是 Tidy Data)
  • ❌ 在 Excel 中习惯宽格式,就在 R 中也一直用宽格式(会增加代码复杂度)
  • ❌ 不知道可以随时转换,导致为了适应数据格式而修改分析流程

扩展记录: 2026-04-11 | 扩展者:Clawd | 目标字数:800+

Note生态学中的宽长格式转换

植物群落调查数据通常记录为”物种×样方”矩阵(宽格式),但进行以下分析时需要转换为长格式:

分析目的 推荐格式 原因
物种多样性指数计算(Shannon、Simpson) 长格式 按样方分组计算,每个物种一行更方便
ggplot2 绑图 长格式 facet_wrap 按物种分面需要长格式
DCA/CCA 排序分析(vegan 包) 长格式 多数排序函数要求长格式
PCA/NMDS 分析 宽格式 物种×样方矩阵直接输入
数据展示与报告 宽格式 表格更易读

示例:物种多度数据的宽转长与长转宽

# 宽格式(物种×样方表)
sp_wide <- tibble(
  plot = paste0("P", 1:6),
  马尾松 = c(12, 8, 15, 0, 10, 6),
  杉木 = c(5, 0, 8, 12, 3, 9),
  阔叶树 = c(8, 15, 6, 5, 12, 10)
)

# 宽 → 长:用于多样性计算
sp_long <- sp_wide |>
  pivot_longer(-plot, names_to = "species", values_to = "abundance") |>
  filter(abundance > 0)

# 长 → 宽:用于排序分析
sp_wide2 <- sp_long |>
  pivot_wider(names_from = species, values_from = abundance, values_fill = 0)

8.6.3 缺失值处理:drop_na() + fill() + replace_na()

野外观测数据中难免存在缺失值,tidyr 提供了直观的缺失值处理函数:

# 创建含缺失值的时间序列数据
set.seed(42)
df_missing <- tibble(
  plot_id = rep(paste0("P", 1:5), each = 3),
  month = rep(c("2024-01", "2024-02", "2024-03"), each = 5),
  temperature = c(18.2, NA, 19.1, 17.5, 20.3,
                  NA, 19.8, 20.1, 18.9, 21.2,
                  19.5, 20.0, NA, 19.2, 22.0)
)
df_missing

# 方法 1:直接去除含缺失值的行
df_missing |> drop_na()

# 方法 2:用前一值填充(时间序列前向填充)
df_missing |> fill(temperature, .direction = "down")

# 方法 3:用后一值填充(后向填充)
df_missing |> fill(temperature, .direction = "up")

# 方法 4:用均值填充
df_missing |> replace_na(list(temperature = mean(df_missing$temperature, na.rm = TRUE)))

# 方法 5:按组填充(不同样方用各自的均值)
df_missing |>
  group_by(plot_id) |>
  fill(temperature, .direction = "down") |>
  ungroup()

生态学应用——处理样方调查中的缺失读数

# 某样方的土壤含水量因仪器故障缺失
# 策略:用相邻两次调查的均值替代
set.seed(42)
soil_data <- tibble(
  plot_id = rep("P1", 6),
  date = ymd(c("2024-03-01", "2024-03-08", "2024-03-15", "2024-03-22", "2024-03-29", "2024-04-05")),
  soil_moisture = c(28.5, 26.2, NA, 24.8, 23.1, 21.5)
)

# 前向填充(用上次数据填充当前缺失)
soil_data |>
  fill(soil_moisture, .direction = "down")

# 插值填充(更精确的时间序列插补)
# install.packages("zoo")
library(zoo)
soil_data |>
  mutate(soil_moisture = na.approx(soil_moisture))

8.6.4 separate() 与 unite():列的拆分与合并

separate() 将一列拆分为多列;unite() 将多列合并为一列:

# 拆分:将日期列拆为年、月、日
survey_with_date <- tibble(
  plot_id = paste0("P", 1:5),
  survey_date = ymd(c("2024-05-10", "2024-05-12", "2024-05-15", "2024-05-18", "2024-05-20"))
)

survey_with_date |>
  separate(survey_date, into = c("year", "month", "day"), sep = "-")

# 合并:将年、月、日合并为日期
tibble(
  year = 2024, month = c("03", "04", "05"), day = c("15", "20", "25")
) |>
  unite("date", year, month, day, sep = "-") |>
  mutate(date = ymd(date))

8.7 数据合并

研究中经常需要合并来自不同来源的数据:

# 样方环境数据
plot_env <- tibble(
  plot_id = paste0("P", 1:10),
  elevation = round(runif(10, 200, 800)),
  slope = round(runif(10, 5, 35)),
  aspect = sample(c("N", "S", "E", "W"), 10, replace = TRUE)
)

# 将环境数据合并到调查数据
survey_full <- survey |>
  left_join(plot_env, by = "plot_id")

head(survey_full)

常用的合并方式:

函数 说明
left_join(x, y) 保留 x 的所有行,匹配 y 的列
right_join(x, y) 保留 y 的所有行
inner_join(x, y) 只保留两边都有的行
full_join(x, y) 保留所有行

生态学应用——合并林分调查与环境数据

# 林分蓄积量数据(注意:缺少 P9, P10)
plot_volume <- tibble(
  plot_id = paste0("P", c(1:8, 11:12)),
  volume_m3 = round(runif(10, 50, 200), 1)
)

# 合并林分数据(使用 left_join,保留所有调查样方)
survey_with_volume <- survey_full |>
  left_join(plot_volume, by = "plot_id")

# 检查哪些样方没有林分数据(NA)
survey_with_volume |>
  filter(is.na(volume_m3)) |>
  select(plot_id, species) |>
  distinct(plot_id)

# 使用 inner_join 只保留有完整数据的样方
survey_complete <- survey_full |>
  inner_join(plot_volume, by = "plot_id")

cat("原始样方数:", nrow(survey_full), "\n")
cat("有林分数据的样方数:", nrow(survey_complete), "\n")

多键值合并

当两个数据框需要用多个键合并时,使用 by = c("键1", "键2")

# 假设环境数据还有生境字段,需要与调查数据匹配
plot_env2 <- tibble(
  plot_id = paste0("P", 1:10),
  habitat = rep(c("山脊", "山坡", "山谷"), length.out = 10),
  elevation = round(runif(10, 200, 800))
)

# 按样方ID和生境两个键合并
survey_checked <- survey |>
  left_join(plot_env2, by = c("plot_id", "habitat"))
head(survey_checked)

四种合并方式的区别

函数 说明 适用场景
left_join(x, y) 保留 x 的所有行,y 中没有匹配的填 NA 最常用,用环境属性丰富调查数据
right_join(x, y) 保留 y 的所有行 较少用,结果等于 left_join 的反向
inner_join(x, y) 只保留两边都有的行 只分析有完整数据的样方
full_join(x, y) 保留所有行,缺失处填 NA 合并多个数据源时保留全部信息
Note合并时的注意事项
  • 键的类型:合并键的数据类型必须一致(如都为字符型),否则 R 会自动转换或报错
  • 键的名称:默认按同名列合并;不同名列用 by = c("左表列" = "右表列") 指定
  • 重复键:右表中有重复键时,左表每行会匹配多条;需注意避免意外膨胀
  • NA 处理:NA 不会匹配任何值,包括另一个 NA

8.8 stringr:字符串处理

生态学数据中常有物种学名、样方编号等字符串变量,需要清洗和提取。

8.8.1 常用字符串函数

检测与匹配

# str_detect():检测是否包含某模式
c("马尾松", "杉木", "Pinus massoniana", "阔叶树") |>
  str_detect("松")

# 筛选含"松"的物种名(物种名录清洗)
species_list <- c("马尾松", "杉木", "Pinus massoniana", "Pinus taeda",
                  "桉树", "Eucalyptus globulus", "阔叶树")
species_list |>
  str_subset("松")  # 匹配"松"

# str_count():统计模式出现次数
str_count(c("马尾松-杉木-桉树", "马尾松-马尾松", "桉树"), "马尾松")

提取与替换

# str_extract():提取首次匹配
str_extract("P1-马尾松-2024", "[A-Z]\\d")    # 提取样方编号

# str_extract_all():提取所有匹配
str_extract_all("P1马尾松P2杉木P3桉树", "[A-Z]\\d") |>
  unlist()

# str_replace():替换首次匹配
str_replace("马尾松林", "马尾松", "湿地松")

# str_replace_all():替换所有匹配
str_replace_all("马尾松-杉木-马尾松-桉树", "马尾松", "湿地松")

拆分与合并

# str_split():按分隔符拆分
str_split("P1-马尾松-2024-广西", "-", simplify = TRUE)

# str_c():拼接字符串
str_c("样方", 1:5, sep = "-")
str_c(c("A", "B"), c("1", "2"), sep = "-", collapse = ";")

修整与格式化

# 去除首尾空格
str_trim("   马尾松   ")

# 去除所有多余空格(包含中间空格)
str_squish("   马尾   松   ")

# 统一大小写
str_to_upper("massoniana")          # 转为大写
str_to_title("pinus massoniana")    # 首字母大写

8.8.2 生态学应用:物种学名清洗

# 模拟含有录入错误的物种名录
raw_species <- c(
  "Pinus massoniana", "Pinus massoniana", "pinus massoniana",
  " Cunninghamia lanceolata", "Cunninghamia lanceolata",
  "Eucalyptus globulus ", "Eucalyptus globulus",
  "  Schima superba", "Schima superba", "Shima superba"  # 有录入错误
)

# 清洗步骤:去空格 → 统一大小写 → 去重
clean_species <- raw_species |>
  str_trim() |>                      # 去除首尾空格
  str_to_lower() |>                  # 统一转为小写
  unique()                           # 去重

clean_species

使用正则表达式处理样方编码

# 提取样方编号
plot_codes <- c("P01-山脊-A", "P02-山谷-B", "P10-山坡-C", "P03-山脊-D")

# 提取数字编号
str_extract(plot_codes, "P(\\d+)")
str_extract(plot_codes, "(\\d+)-") |> str_remove("-")

# 提取生境类型
str_extract(plot_codes, "(山脊|山坡|山谷)")

8.9 forcats:因子变量处理

因子(factor)是 R 中用于表示分类变量的数据类型,在生态学中常用于物种分类、生境类型、调查月份等。

8.9.1 创建与转换因子

# 使用 factor() 创建因子
habitat_factor <- factor(c("山脊", "山谷", "山坡", "山谷", "山脊"))
habitat_factor

# 指定因子水平顺序(影响排列和绘图顺序)
habitat_ordered <- factor(
  c("山谷", "山坡", "山脊"),
  levels = c("山谷", "山坡", "山脊")  # 从低到高排列
)
habitat_ordered

8.9.2 常用 forcats 函数

改变水平顺序

# fct_relevel():手动调整水平顺序(将参照水平放第一位)
survey |>
  mutate(habitat = fct_relevel(habitat, "山谷")) |>
  pull(habitat) |>
  levels()

# fct_reorder():按另一变量统计量排序
survey |>
  group_by(habitat) |>
  summarise(mean_h = mean(height, na.rm = TRUE)) |>
  mutate(habitat = fct_reorder(habitat, mean_h)) |>
  pull(habitat) |>
  levels()

合并与重编码水平

# fct_collapse():将多个水平合并为一个
survey |>
  mutate(
    habitat_big = fct_collapse(
      habitat,
      "低海拔" = c("山谷"),
      "中海拔" = c("山坡"),
      "高海拔" = c("山脊")
    )
  ) |>
  count(habitat_big)

处理稀有水平

# fct_lump_min():将出现次数少于阈值的水平合并为"其他"
survey |>
  mutate(species_lumped = fct_lump_min(species, min = 5)) |>
  count(species_lumped)

# fct_lump_n():保留出现最多的 n 个,其余归为"其他"
survey |>
  mutate(species_top3 = fct_lump_n(species, n = 3)) |>
  count(species_top3)

8.9.3 生态学应用:生境与物种分析

# 按生境统计并确保生境水平按样地海拔排序
habitat_summary <- survey |>
  filter(!is.na(height)) |>
  mutate(
    # 调整生境水平顺序(山谷→山坡→山脊,体现海拔梯度)
    habitat = fct_relevel(habitat, "山谷", "山坡", "山脊")
  ) |>
  group_by(habitat) |>
  summarise(
    n = n(),
    mean_height = round(mean(height), 2),
    mean_dbh = round(mean(dbh, na.rm = TRUE), 2)
  )

habitat_summary

8.10 lubridate:日期时间数据处理

野外观测数据常包含采样日期和时间,lubridate 让日期时间处理变得简单。

8.10.1 解析日期时间

# 常见日期格式的解析
ymd("2024-03-15")           # 年-月-日
mdy("03/15/2024")           # 月/日/年
dmy("15-03-2024")           # 日-月-年

# 解析带时间的格式
ymd_hms("2024-03-15 14:30:00")
ymd_hm("2024-03-15 1430")

# 从字符串向量批量解析
dates <- c("2024-01-05", "2024-02-20", "2024-03-12")
ymd(dates)

8.10.2 提取时间成分

survey_date <- ymd("2024-05-20")

# 提取年、月、日、星期
year(survey_date)
month(survey_date)
day(survey_date)
wday(survey_date, label = TRUE)   # 星期几(英文标签)

# 提取季度
quarter(survey_date)

# 提取一年中的第几周
epiweek(survey_date)

8.10.3 日期时间计算

date1 <- ymd("2024-01-01")
date2 <- ymd("2024-06-15")

# 计算日期间隔(单位:天)
interval(date1, date2) / days(1)

# 计算月份差
interval(date1, date2) / months(1)

# 加减日期
ymd("2024-01-01") + months(3)
ymd("2024-01-01") + years(1)

# 今天是星期几
today()
now()

8.10.4 生态学应用:采样日期与季节分析

# 模拟带有采样日期的森林动态监测数据
set.seed(2027)
monitoring <- tibble(
  plot_id = paste0("P", 1:20),
  survey_date = seq(ymd("2023-01-01"), ymd("2023-12-31"), length.out = 20) |>
    sort() |>
    ymd(),
  new_trees = sample(0:10, 20, replace = TRUE),
  mortality = sample(0:3, 20, replace = TRUE)
)

# 添加季节和月份列
monitoring <- monitoring |>
  mutate(
    month = month(survey_date, label = TRUE),
    season = case_when(
      month %in% c("12", "1", "2") ~ "冬季",
      month %in% c("3", "4", "5")  ~ "春季",
      month %in% c("6", "7", "8")  ~ "夏季",
      month %in% c("9", "10", "11") ~ "秋季"
    ),
    year_month = floor_date(survey_date, "month")  # 截断到月
  )

monitoring

# 按季节统计新增与死亡树木
monitoring |>
  group_by(season) |>
  summarise(
    total_new = sum(new_trees),
    total_dead = sum(mortality),
    net_change = total_new - total_dead
  )

8.11 实战:马尾松林调查数据清洗

本节综合运用本章所有技能,对模拟的马尾松林调查数据进行完整的数据清洗流程。

8.11.1 数据背景

某研究团队在广西大学实验林场开展了马尾松混交林样地调查,获得以下数据:

  • 乔木层数据(50 株):样方号、树种、树高、胸径、生境类型、土壤 pH
  • 环境数据(10 个样方):海拔、坡度、坡向
  • 物种名录(含录入错误):中文名和学名混杂

8.11.2 Step 1:加载数据与环境设置

# 加载必要的包
library(tidyverse)
library(lubridate)

# 重新生成原始数据(保证可重复性)
set.seed(2027)

# 乔木层调查数据
survey_raw <- tibble(
  plot_id = rep(paste0("P", 1:10), each = 5),
  species_cn = sample(
    c("马尾松", "杉木", "桉树", "樟树", "楠木",
      "马尾松 ", "马尾松林", "杉 木", "桉树 ", " 樟树"),
    50, replace = TRUE
  ),
  height = round(rnorm(50, mean = 12, sd = 4), 1),
  dbh = round(rnorm(50, mean = 20, sd = 8), 1),
  habitat = rep(sample(c("山脊", "山坡", "山谷"), 10, replace = TRUE), each = 5),
  soil_ph = round(rnorm(50, mean = 5.5, sd = 0.8), 2)
)

# 人为注入问题
survey_raw$height[c(3, 17, 28)] <- NA
survey_raw$dbh[c(8, 42)] <- NA
survey_raw$soil_ph[15] <- NA

# 物种学名对照表
species_dict <- tribble(
  ~species_cn_clean, ~species_en, ~family,
  "马尾松", "Pinus massoniana", "Pinaceae",
  "杉木", "Cunninghamia lanceolata", "Pinaceae",
  "桉树", "Eucalyptus globulus", "Myrtaceae",
  "樟树", "Cinnamomum camphora", "Lauraceae",
  "楠木", "Phoebe zhennan", "Lauraceae"
)

# 环境数据
plot_env <- tibble(
  plot_id = paste0("P", 1:10),
  elevation = round(runif(10, 200, 800)),
  slope = round(runif(10, 5, 35)),
  aspect = sample(c("N", "S", "E", "W"), 10, replace = TRUE)
)

cat("=== Step 1: 原始数据 ===\n")
glimpse(survey_raw)

8.11.3 Step 2:物种名称清洗

# 物种名录中存在以下问题:
# - 首尾空格("马尾松 ")
# - 多余字符("马尾松林")
# - 空格混乱("杉 木")

survey_clean1 <- survey_raw |>
  mutate(
    species_cn_clean = species_cn |>
      str_trim() |>
      str_remove("林$") |>
      str_replace_all("\\s+", "")
  )

# 清洗前后对比
cat("=== Step 2: 物种名称清洗 ===\n")
survey_clean1 |>
  select(species_cn, species_cn_clean) |>
  distinct() |>
  print(n = Inf)

# 合并学名信息
survey_clean2 <- survey_clean1 |>
  left_join(species_dict, by = "species_cn_clean")

head(survey_clean2)

8.11.4 Step 3:处理缺失值

# 查看缺失值情况
cat("=== Step 3: 缺失值处理 ===\n")
survey_clean2 |>
  summarise(
    across(everything(), \\x) sum(is.na(x)))
  ) |>
  pivot_longer(everything(), names_to = "变量", values_to = "NA数量")

# 策略:删除树高和胸径均有缺失的记录;其他用均值填充
survey_clean3 <- survey_clean2 |>
  filter(!(is.na(height) & is.na(dbh))) |>
  group_by(species_cn_clean) |>
  mutate(height = if_else(is.na(height), mean(height, na.rm = TRUE), height)) |>
  ungroup() |>
  mutate(dbh = coalesce(dbh, median(dbh, na.rm = TRUE))) |>
  mutate(soil_ph = coalesce(soil_ph, mean(soil_ph, na.rm = TRUE)))

cat("处理后缺失值:", sum(is.na(survey_clean3$height)), sum(is.na(survey_clean3$dbh)), "\n")

8.11.5 Step 4:合并环境数据

在生态学研究中,物种数据和环境数据通常分别采集和存储。物种数据记录每个样方或样地内的物种组成、个体数量、生物量等信息,而环境数据记录样地的海拔、坡度、土壤性质、气候因子等背景信息。将这两类数据合并是进行环境-物种关系分析的前提。数据合并的核心是找到两个数据表之间的”连接键”(join key),通常是样地编号(plot_id)或样方编号(quadrat_id)。

数据合并的类型

  1. 左连接(left_join):保留左表的所有行,右表中没有匹配的行用 NA 填充。这是最常用的合并方式,适合”以物种数据为主,补充环境数据”的场景。

  2. 右连接(right_join):保留右表的所有行,左表中没有匹配的行用 NA 填充。

  3. 内连接(inner_join):只保留两表都有匹配的行,适合”只分析同时有物种和环境数据的样地”的场景。

  4. 全连接(full_join):保留两表的所有行,没有匹配的用 NA 填充。

生态学应用场景

假设我们有 50 个样地的树木调查数据(每个样地有多行记录,每行代表一棵树),以及 50 个样地的环境数据(每个样地一行)。我们需要将环境数据合并到树木数据中,以便分析”不同海拔、不同生境类型下的树木生长差异”。

cat("=== Step 4: 合并环境数据 ===\n")
survey_final <- survey_clean3 |>
  left_join(plot_env, by = "plot_id") |>
  mutate(
    basal_area = pi * (dbh / 200)^2,
    volume = 0.5 * basal_area * height
  )

glimpse(survey_final)

代码解析

  1. left_join(plot_env, by = "plot_id"):将 plot_env 表(环境数据)按 plot_id 列合并到 survey_clean3 表(树木数据)中。由于使用左连接,即使某个样地在 plot_env 中没有记录,该样地的树木数据仍会保留,只是环境变量为 NA

  2. mutate() 计算派生变量

    • basal_area:胸高断面积(m²),公式为 π × (胸径/200)²。胸径单位为 cm,除以 200 转换为半径(m)。
    • volume:树木材积(m³),简化公式为 0.5 × 胸高断面积 × 树高。实际研究中应使用树种特定的材积公式。

合并后的数据检查

合并数据后,务必检查以下几点:

  1. 行数是否正确:左连接后,行数应等于左表的行数。如果行数增加,说明右表中有重复的连接键(一个 plot_id 对应多行),需要检查数据是否有误。

  2. 是否有未匹配的行:检查合并后环境变量是否有 NA。如果有,说明某些样地在环境数据表中缺失,需要补充数据或在分析中排除这些样地。

  3. 连接键是否唯一:环境数据表中,每个 plot_id 应该只有一行。如果有重复,合并后会产生笛卡尔积(每棵树的记录会重复多次),导致数据错误。

常见错误与解决方案

  1. 连接键名称不一致:如果左表的连接键是 plot_id,右表的连接键是 PlotID,需要在合并前统一列名,或使用 by = c("plot_id" = "PlotID") 指定不同的列名。

  2. 连接键数据类型不一致:如果左表的 plot_id 是字符型(“P01”),右表的 plot_id 是数值型(1),合并会失败。需要先统一数据类型:mutate(plot_id = as.character(plot_id))

  3. 连接键有前导/尾随空格:Excel 数据中常见的问题。使用 mutate(plot_id = str_trim(plot_id)) 去除空格。

实战示例

假设我们有马尾松林 30 个样地的树木调查数据和环境数据,需要分析”海拔和生境类型对树木生长的影响”:

# 树木数据(每个样地多行)
tree_data <- tibble(
  plot_id = rep(paste0("P", 1:30), each = 10),  # 每个样地 10 棵树
  species = sample(c("马尾松", "杉木", "樟树"), 300, replace = TRUE),
  dbh = rnorm(300, mean = 15, sd = 5),
  height = rnorm(300, mean = 12, sd = 3)
)

# 环境数据(每个样地一行)
env_data <- tibble(
  plot_id = paste0("P", 1:30),
  elevation = runif(30, 500, 1500),
  habitat = sample(c("阳坡", "阴坡", "山脊"), 30, replace = TRUE),
  soil_ph = rnorm(30, mean = 5.5, sd = 0.8)
)

# 合并数据
combined_data <- tree_data |>
  left_join(env_data, by = "plot_id") |>
  mutate(
    basal_area = pi * (dbh / 200)^2,
    volume = 0.5 * basal_area * height
  )

# 检查合并结果
cat("合并前树木数据行数:", nrow(tree_data), "\n")
cat("合并后数据行数:", nrow(combined_data), "\n")
cat("环境变量缺失数:", sum(is.na(combined_data$elevation)), "\n")

# 按海拔和生境分组统计
summary_stats <- combined_data |>
  mutate(elevation_class = cut(elevation, breaks = c(0, 800, 1200, 2000),
                                labels = c("低海拔", "中海拔", "高海拔"))) |>
  group_by(elevation_class, habitat) |>
  summarise(
    mean_dbh = mean(dbh, na.rm = TRUE),
    mean_height = mean(height, na.rm = TRUE),
    total_volume = sum(volume, na.rm = TRUE),
    .groups = "drop"
  )

print(summary_stats)

这个流程展示了从数据合并到分组统计的完整过程。掌握数据合并技巧,是进行多源数据整合分析的关键能力。

8.11.6 Step 5:生成汇总报告

数据清洗和合并完成后,生成汇总报告是数据分析流程的重要环节。汇总报告不仅用于检查数据质量,还能快速呈现研究的核心发现,为后续的统计建模和可视化提供基础信息。在生态学研究中,汇总报告通常包括样本量统计、物种多样性指数、环境因子的描述性统计、以及按分组变量(如生境类型、海拔梯度)的对比分析。

生态学汇总报告的核心内容

  1. 样本量统计:每个样地的个体数量、物种数量、样地数量等基础信息。
  2. 多样性指数:Shannon 指数、Simpson 指数、物种丰富度、均匀度等。
  3. 生物量/材积统计:总生物量、平均生物量、生物量分布等。
  4. 环境因子统计:海拔范围、土壤 pH 范围、坡度分布等。
  5. 分组对比:不同生境类型、不同海拔梯度、不同处理组之间的差异。
cat("=== Step 5: 多样性汇总报告 ===\n")
diversity_report <- survey_final |>
  group_by(plot_id, habitat, elevation) |>
  summarise(
    n_individuals = n(),
    n_species = n_distinct(species_cn_clean),
    shannon_H = -sum((table(species_cn_clean) / n_individuals) *
                       log(table(species_cn_clean) / n_individuals)),
    total_volume = round(sum(volume, na.rm = TRUE), 2),

代码解析

  1. group_by(plot_id, habitat, elevation):按样地编号、生境类型和海拔分组。这意味着后续的统计函数会在每个样地内分别计算。

  2. n_individuals = n():统计每个样地的个体数量(行数)。n() 是 dplyr 包提供的计数函数,返回当前分组的行数。

  3. n_species = n_distinct(species_cn_clean):统计每个样地的物种数量(物种丰富度)。n_distinct() 返回不重复的值的数量。

  4. shannon_H:计算 Shannon 多样性指数。公式为 H’ = -Σ(p_i × ln(p_i)),其中 p_i 是第 i 个物种的相对多度。这里用 table(species_cn_clean) / n_individuals 计算每个物种的相对多度,然后乘以其对数并求和。

  5. total_volume:计算每个样地的总材积,na.rm = TRUE 排除缺失值。

Shannon 指数的生态学意义

Shannon 指数(H’)综合考虑了物种丰富度和均匀度,是生态学中最常用的多样性指数之一。H’ 值越大,表示群落的多样性越高。典型取值范围:

  • H’ < 1:多样性很低,群落由少数优势种主导
  • 1 ≤ H’ < 3:多样性中等
  • H’ ≥ 3:多样性较高,物种分布较均匀

扩展汇总指标

除了上述基础指标,还可以计算以下生态学指标:

diversity_report_extended <- survey_final |>
  group_by(plot_id, habitat, elevation) |>
  summarise(
    # 基础统计
    n_individuals = n(),
    n_species = n_distinct(species_cn_clean),

    # 多样性指数
    shannon_H = -sum((table(species_cn_clean) / n_individuals) *
                       log(table(species_cn_clean) / n_individuals)),
    simpson_D = sum((table(species_cn_clean) / n_individuals)^2),
    evenness = shannon_H / log(n_species),  # Pielou 均匀度

    # 生物量统计
    total_volume = sum(volume, na.rm = TRUE),
    mean_volume = mean(volume, na.rm = TRUE),
    sd_volume = sd(volume, na.rm = TRUE),

    # 树木大小统计
    mean_dbh = mean(dbh, na.rm = TRUE),
    max_dbh = max(dbh, na.rm = TRUE),
    mean_height = mean(height, na.rm = TRUE),

    # 优势种识别
    dominant_species = names(sort(table(species_cn_clean), decreasing = TRUE))[1],
    dominant_abundance = max(table(species_cn_clean)) / n_individuals,

    .groups = "drop"
  )

汇总报告的输出与检查

生成汇总报告后,应进行以下检查:

  1. 数值合理性:Shannon 指数是否在合理范围内?材积是否为正值?物种数量是否小于等于个体数量?

  2. 缺失值检查:是否有样地的统计指标为 NA?如果有,需要追溯原因(可能是该样地的原始数据有问题)。

  3. 异常值检查:是否有样地的统计指标明显偏离其他样地?例如,某个样地的 Shannon 指数远高于其他样地,可能是数据录入错误或该样地确实具有特殊性。

汇总报告的可视化

生成汇总报告后,通常需要可视化展示:

library(ggplot2)

# 物种丰富度与海拔的关系
ggplot(diversity_report, aes(x = elevation, y = n_species, color = habitat)) +
  geom_point(size = 3, alpha = 0.7) +
  geom_smooth(method = "lm", se = FALSE) +
  labs(x = "海拔 (m)", y = "物种丰富度", color = "生境类型",
       title = "物种丰富度沿海拔梯度的变化") +
  theme_minimal()

# Shannon 指数的箱线图
ggplot(diversity_report, aes(x = habitat, y = shannon_H, fill = habitat)) +
  geom_boxplot(alpha = 0.7) +
  geom_jitter(width = 0.2, alpha = 0.3) +
  labs(x = "生境类型", y = "Shannon 指数", fill = "生境类型",
       title = "不同生境类型的 Shannon 多样性指数") +
  theme_minimal() +
  theme(legend.position = "none")

实战示例

假设我们完成了马尾松林 30 个样地的数据清洗和合并,现在生成汇总报告并导出为 Excel 文件,供团队成员查看:

library(openxlsx)

# 生成汇总报告
diversity_report <- survey_final |>
  group_by(plot_id, habitat, elevation) |>
  summarise(
    n_individuals = n(),
    n_species = n_distinct(species_cn_clean),
    shannon_H = round(-sum((table(species_cn_clean) / n_individuals) *
                             log(table(species_cn_clean) / n_individuals)), 3),
    total_volume = round(sum(volume, na.rm = TRUE), 2),
    mean_dbh = round(mean(dbh, na.rm = TRUE), 2),
    .groups = "drop"
  )

# 导出为 Excel
write.xlsx(diversity_report, "output/diversity_summary_report.xlsx")

# 打印前 10 行
print(head(diversity_report, 10))

# 生成总体统计
overall_summary <- diversity_report |>
  summarise(
    total_plots = n(),
    total_species = sum(n_species),
    mean_shannon = mean(shannon_H, na.rm = TRUE),
    total_volume = sum(total_volume, na.rm = TRUE)
  )

cat("\n=== 总体统计 ===\n")
print(overall_summary)

这个流程展示了从数据清洗到汇总报告生成的完整过程。掌握汇总报告的生成技巧,是数据分析能力的重要体现,也是向合作者和审稿人展示研究质量的关键环节。 .groups = “drop” ) |> arrange(desc(shannon_H))

diversity_report


## 课后练习

1. **dplyr 基础练习**:使用 `iris` 数据集,用 dplyr 计算每个物种的花瓣长度和宽度的平均值。

2. **宽长格式转换**:将 `iris` 数据集转换为长格式(4 个测量值变成一列 `measurement`,值变成 `value`)。

3. **条件筛选**:筛选出花萼长度大于 6 的记录,按物种分组统计数量。

4. **数据处理流水线**:创建一个完整流水线——筛选 → 新增列(花瓣面积 = 长 × 宽)→ 分组汇总(按物种求均值)→ 排序。

5. **实战练习**:参考本章「实战:马尾松林调查数据清洗」一节,将 `mtcars` 数据集按汽缸数(`cyl`)分组,计算每加仑里程(`mpg`)的平均值和标准差,并将结果保存为 CSV 文件。

::: {.callout-tip}
### 提交要求

将以上练习代码保存为 `.qmd` 文件(至少包含一个代码块和简要说明),渲染为 HTML 后提交到课程 GitHub 仓库。完整的提交流程包括代码编写、本地测试、版本控制和远程同步四个关键步骤。

#### 1. 文件组织与命名规范

作业文件应遵循统一的命名规范,便于教师批改和同学间交流。推荐格式为 `homework-0106-姓名拼音.qmd`,例如 `homework-0106-zhangsan.qmd`。文件应保存在课程仓库的 `homework/` 目录下,并按章节建立子目录(如 `homework/chapter01/`)。

```r
# 在 R 中检查当前工作目录
getwd()

# 如果不在课程仓库根目录,使用 setwd() 切换
setwd("E:/Github/Data-collection-and-preprocessing-2027-Spring")

# 创建作业目录(如果不存在)
if (!dir.exists("homework/chapter01")) {
  dir.create("homework/chapter01", recursive = TRUE)
}

2. Quarto 文档渲染与检查

在提交前,务必在本地完成渲染测试,确保代码无误、图表正常显示。使用 RStudio 的 “Render” 按钮或命令行工具:

# 在终端中渲染 Quarto 文档
quarto render homework/chapter01/homework-0106-zhangsan.qmd

# 检查生成的 HTML 文件
ls homework/chapter01/*.html

渲染成功后,应检查以下内容:

  • 所有代码块是否正常执行(无报错信息)
  • 数据框输出是否完整(未被截断)
  • 图表是否清晰可见(分辨率适中)
  • 中文字符是否正常显示(避免乱码)

3. Git 版本控制流程

课程采用 Git 进行版本管理,标准提交流程如下:

# 1. 查看当前分支和文件状态
git status

# 2. 将作业文件添加到暂存区
git add homework/chapter01/homework-0106-zhangsan.qmd
git add homework/chapter01/homework-0106-zhangsan.html

# 3. 提交到本地仓库(commit message 要清晰)
git commit -m "feat(homework): 完成第1章第6节 dplyr 数据处理练习"

# 4. 推送到远程仓库
git push origin main

Commit Message 规范

  • feat: 新增功能或作业
  • fix: 修复错误
  • docs: 文档更新
  • style: 代码格式调整(不影响功能)

4. GitHub Pull Request 创建(协作模式)

如果课程采用 Fork + Pull Request 的协作模式,需要额外步骤:

  1. Fork 课程仓库到个人 GitHub 账号
  2. Clone 个人仓库到本地:git clone https://github.com/你的用户名/仓库名.git
  3. 创建功能分支git checkout -b homework-0106
  4. 完成作业并提交到个人仓库
  5. 在 GitHub 网页端创建 Pull Request,目标分支为课程仓库的 main

PR 标题示例[作业提交] 张三 - 第1章第6节 dplyr 练习

PR 描述模板

## 作业信息
- 章节:第1章第6节 R 数据处理
- 完成日期:2026-04-11
- 学号:202401001

## 完成情况
- [x] 练习1:select 和 filter 基础操作
- [x] 练习2:mutate 创建新列
- [x] 练习3:条件筛选与分组统计
- [x] 练习4:完整数据处理流水线
- [x] 练习5:mtcars 实战练习

## 遇到的问题
在练习4中,最初使用 `summarise()` 时忘记 `group_by()`,导致结果不符合预期。通过查阅文档和调试,最终解决。

5. 代码审查注意事项

提交后,教师或助教会进行代码审查(Code Review)。常见反馈包括:

  • 代码风格:变量命名是否规范(使用 snake_case
  • 注释质量:关键步骤是否有中文注释
  • 可重现性:是否使用相对路径(避免硬编码绝对路径)
  • 效率问题:是否存在冗余操作(如重复读取数据)

生态学案例:某研究团队在分析全球森林碳汇数据时,因未统一文件路径规范,导致协作者无法重现分析结果。后采用 RStudio Project + 相对路径 + Git 版本控制的标准流程,大幅提升了团队协作效率。该案例强调了规范化提交流程在科研协作中的重要性。

6. 常见问题排查

问题 原因 解决方案
渲染失败 代码块存在语法错误 逐块运行代码,定位错误位置
推送被拒绝 远程仓库有新提交 git pullgit push
中文乱码 文件编码非 UTF-8 RStudio 中设置 File > Reopen with Encoding > UTF-8
图片不显示 相对路径错误 使用 here::here() 管理路径

扩展记录: 2026-04-11 | 扩展者:Clawd | 目标字数:800+