library(tidyverse)17 数据质量控制
18 数据质量控制
数据清洗和特征工程解决了”数据怎么处理”的问题,数据质量控制解决的是”怎么确保数据可靠、可追溯、可复用”。这是可重复性研究的核心环节。
18.1 数据验证
18.1.1 范围检查
确保数值在合理范围内:
validate_soil <- function(data) {
issues <- list()
# pH 范围检查(0-14)
bad_ph <- data |> filter(ph < 0 | ph > 14)
if (nrow(bad_ph) > 0) issues$ph <- paste(nrow(bad_ph), "条 pH 超出范围")
# 有机碳不能为负
bad_oc <- data |> filter(organic_carbon < 0)
if (nrow(bad_oc) > 0) issues$oc <- paste(nrow(bad_oc), "条有机碳为负值")
# 百分比之和检查
if ("clay_pct" %in% names(data) && "sand_pct" %in% names(data)) {
bad_pct <- data |> filter(clay_pct + sand_pct > 100)
if (nrow(bad_pct) > 0) issues$pct <- paste(nrow(bad_pct), "条粒径百分比之和超过100%")
}
if (length(issues) == 0) {
cat("✅ 所有验证通过\n")
} else {
cat("⚠️ 发现以下问题:\n")
for (name in names(issues)) {
cat(" -", issues[[name]], "\n")
}
}
invisible(issues)
}
# 示例
set.seed(2027)
soil <- tibble(
site_id = paste0("S", 1:10),
ph = c(6.2, 5.8, 7.1, -0.5, 6.5, 15.2, 6.8, 5.9, 6.3, 7.0),
organic_carbon = c(25, 30, 22, 28, -5, 35, 20, 27, 31, 24),
clay_pct = c(30, 45, 20, 55, 40, 35, 25, 50, 38, 42),
sand_pct = c(40, 30, 50, 60, 35, 45, 55, 30, 40, 35)
)
validate_soil(soil)18.1.2 一致性检查
# 检查同一样点不同深度的数据是否一致
# 例如:表层有机碳应该高于深层
check_depth_consistency <- function(data) {
if (!("depth" %in% names(data))) return(invisible(NULL))
inconsistent <- data |>
group_by(site_id) |>
arrange(depth) |>
mutate(oc_decreasing = organic_carbon < lag(organic_carbon)) |>
filter(!is.na(oc_decreasing) & !oc_decreasing) |>
ungroup()
if (nrow(inconsistent) > 0) {
cat("⚠️", nrow(inconsistent), "条记录的有机碳未随深度递减(可能需要核实)\n")
} else {
cat("✅ 有机碳随深度递减,符合预期\n")
}
invisible(inconsistent)
}
# 示例:不同深度的土壤数据
soil_depth <- tibble(
site_id = rep(c("S1", "S2", "S3"), each = 3),
depth = rep(c(10, 20, 30), 3),
organic_carbon = c(
35, 22, 15, # S1: 正常递减
28, 32, 18, # S2: 20cm 处异常升高
40, 30, 25 # S3: 正常递减
)
)
check_depth_consistency(soil_depth)18.2 元数据(Metadata)
元数据是”描述数据的数据”。没有元数据的数据集,就像没有说明书的仪器——别人(包括未来的你)无法正确使用。
18.2.1 元数据应包含的内容
| 项目 | 说明 | 示例 |
|---|---|---|
| 数据集名称 | 简明描述 | 广西大学校园土壤调查数据 |
| 采集时间 | 数据采集的时间范围 | 2027年3月15日-17日 |
| 采集地点 | 地理位置和坐标 | 广西南宁市广西大学校园(108.29°E, 22.83°N) |
| 采集方法 | 采样设计和方法 | 随机布设20个样点,每点采集0-30cm三层土壤 |
| 变量说明 | 每个变量的含义和单位 | ph: 土壤pH值(水土比1:2.5) |
| 数据处理 | 清洗和转换的方法 | 缺失值用中位数填充,pH异常值(<0或>14)标记为NA |
| 联系人 | 数据负责人 | 刘华清 huaqingliu@gxu.edu.cn |
| 许可证 | 数据使用许可 | CC BY 4.0 |
18.2.2 编写元数据文件
在项目中创建 data/README.md:
# 数据说明
## 数据集:广西大学校园土壤调查
- **采集时间**:2027年3月15日-17日
- **采集地点**:广西南宁市广西大学校园
- **采集人**:XXX, XXX
- **样本量**:20个样点 × 3个深度 = 60条记录
### 变量说明
| 变量名 | 类型 | 单位 | 说明 |
|:---|:---|:---|:---|
| site_id | 字符 | - | 样点编号(S01-S20) |
| depth | 因子 | cm | 采样深度(0-10, 10-20, 20-30) |
| ph | 数值 | - | 土壤pH(水土比1:2.5) |
| organic_carbon | 数值 | g/kg | 土壤有机碳含量 |
| nitrogen | 数值 | g/kg | 全氮含量 |
| clay_pct | 数值 | % | 黏粒含量 |
| sand_pct | 数值 | % | 砂粒含量 |
### 数据处理记录
1. 去除重复记录(3条)
2. pH异常值(<0或>14)标记为NA
3. 有机碳异常值(<0或>100)标记为NA
4. 缺失值用各变量中位数填充
5. 字符串统一为小写并去除空格18.3 数据存储格式
18.3.1 常见格式对比
| 格式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| CSV | 通用、人类可读 | 不保留数据类型 | 数据共享、发布 |
| RDS | 保留 R 对象完整信息 | 仅 R 可读 | R 项目中间结果 |
| Excel | 直观、易编辑 | 格式易损坏 | 数据录入(不推荐分析用) |
| Parquet | 高效压缩、列式存储 | 需要额外包 | 大数据集 |
18.3.2 推荐做法
# 原始数据保存为 CSV(通用性最好)
write_csv(soil_raw, "data/raw/soil_survey_raw.csv")
# 处理后的数据保存为 RDS(保留因子等类型信息)
saveRDS(soil_clean, "data/processed/soil_survey_clean.rds")
# 读取
soil_clean <- readRDS("data/processed/soil_survey_clean.rds")
Tip数据存储的黄金法则
- 原始数据只读:永远不修改
data/raw/中的文件 - 处理过程可追溯:所有清洗步骤都写在脚本里,不要手动修改数据
- 中间结果可重建:
data/processed/中的文件都能通过运行脚本重新生成 - 共享用 CSV:给别人的数据用 CSV 格式,附带元数据说明
18.4 Codebook(数据字典)
Codebook 是比 README 更详细的变量说明文档,可以用 R 自动生成:
# 自动生成 codebook
generate_codebook <- function(data) {
tibble(
variable = names(data),
type = map_chr(data, ~class(.x)[1]),
n_missing = map_int(data, ~sum(is.na(.x))),
n_unique = map_int(data, ~n_distinct(.x)),
example = map_chr(data, ~paste(head(unique(.x), 3), collapse = ", "))
)
}
generate_codebook(soil)18.5 数据质量检查清单
在提交数据或开始分析之前,逐项检查:
18.6 实战案例:从”脏数据”到”干净数据”
下面通过一个完整的案例,演示数据质量控制的全流程。假设我们收到了一份来自野外调查的土壤数据,需要在分析前进行质量控制。
18.6.1 原始数据
# 模拟一份典型的"脏数据"
set.seed(2027)
n <- 30
raw_soil <- tibble(
sample_id = paste0("GX", sprintf("%03d", 1:n)),
site = rep(c("林地", "林地 ", "Linland", "草地", "草地", "农田"), each = 5),
date = c(rep("2027-03-15", 10), rep("2027/03/16", 10), rep("15/3/2027", 10)),
ph = c(6.2, 5.8, 7.1, 6.5, 6.8, 5.9, 6.3, 7.0, 5.5, 6.1,
-0.5, 6.4, 15.2, 5.7, 6.6, 6.0, 6.9, 5.3, 6.7, 6.2,
NA, 6.5, 5.8, 7.2, 6.1, 6.8, 5.6, 6.3, 6.0, 6.4),
organic_carbon = c(25, 30, 22, 28, 35, 20, 27, 31, 24, 29,
33, -5, 26, 999, 21, 28, 32, 19, 27, 30,
24, 26, 31, 23, 28, 25, 29, 22, 27, 26),
nitrogen = c(2.1, 2.5, 1.8, 2.3, 2.9, 1.7, 2.2, 2.6, 2.0, 2.4,
2.8, 2.1, 2.2, 2.5, 1.9, 2.3, 2.7, 1.6, 2.2, 2.5,
2.0, 2.1, 2.6, 1.9, 2.3, 2.1, 2.4, 1.8, 2.2, 2.1)
)
cat("原始数据概览:\n")
glimpse(raw_soil)这份数据至少有以下问题:
site列有拼写不一致(“林地” vs “林地” vs “Linland”)date列有三种不同的日期格式ph列有超出范围的值(-0.5, 15.2)和缺失值organic_carbon列有负值(-5)和明显的录入错误(999)
18.6.2 逐步清洗
clean_soil <- raw_soil |>
# 1. 统一站点名称
mutate(
site = str_trim(site),
site = case_match(site,
"Linland" ~ "林地",
.default = site
)
) |>
# 2. 统一日期格式
mutate(
date = case_when(
str_detect(date, "^\\d{4}-") ~ ymd(date),
str_detect(date, "^\\d{4}/") ~ ymd(date),
str_detect(date, "^\\d{2}/") ~ dmy(date),
TRUE ~ NA_Date_
)
) |>
# 3. 标记并处理异常值
mutate(
ph_flag = case_when(
is.na(ph) ~ "缺失",
ph < 0 | ph > 14 ~ "超出范围",
TRUE ~ "正常"
),
oc_flag = case_when(
organic_carbon < 0 ~ "负值",
organic_carbon > 200 ~ "录入错误",
TRUE ~ "正常"
)
) |>
# 4. 将异常值替换为 NA
mutate(
ph = if_else(ph < 0 | ph > 14, NA_real_, ph),
organic_carbon = if_else(organic_carbon < 0 | organic_carbon > 200, NA_real_, organic_carbon)
)
# 查看清洗结果
cat("--- 站点名称统一 ---\n")
count(clean_soil, site)
cat("\n--- 异常值标记 ---\n")
count(clean_soil, ph_flag)
count(clean_soil, oc_flag)18.6.3 生成质量报告
# 汇总数据质量
quality_report <- clean_soil |>
summarise(
total_records = n(),
ph_missing = sum(is.na(ph)),
ph_flagged = sum(ph_flag != "正常"),
oc_missing = sum(is.na(organic_carbon)),
oc_flagged = sum(oc_flag != "正常"),
date_formats_fixed = sum(!is.na(date)),
site_categories = n_distinct(site)
)
quality_report |>
pivot_longer(everything(), names_to = "检查项", values_to = "结果") |>
mutate(检查项 = case_match(检查项,
"total_records" ~ "总记录数",
"ph_missing" ~ "pH 缺失数",
"ph_flagged" ~ "pH 异常标记数",
"oc_missing" ~ "有机碳缺失数",
"oc_flagged" ~ "有机碳异常标记数",
"date_formats_fixed" ~ "日期成功解析数",
"site_categories" ~ "站点类别数"
))
Important关键原则:标记优先于删除
在上面的案例中,我们先用 _flag 列标记异常值,再决定如何处理。这样做的好处是:
- 保留了原始信息,方便回溯
- 可以统计异常值的数量和分布
- 不同的分析可能对异常值有不同的处理策略
18.7 课后练习
- 为你的课程项目数据编写一份完整的元数据文件(README.md)
- 编写一个数据验证函数,检查你的数据是否满足预期的范围和一致性
- 用
generate_codebook()函数为你的数据生成 codebook - 将原始数据保存为 CSV,处理后的数据保存为 RDS
- 确保你的数据处理流程完全可重复:删除
data/processed/中的文件,重新运行脚本能得到相同结果