大家好,我是邓飞。
今天介绍一下数据分析前最重要的一份工作:数据清洗。
辛辛苦苦跑完一个混合模型,结果出来一堆显著,一顿汇报,领导一问: "这个品种的产量负值是怎么回事?"
然后你打开原始数据,发现……某行的 y3 写的是 -99,是当年数据录入时缺失值的占位符,根本不是真实值。
模型没有错,数据先错了。
今天这篇文章,就来聊聊做正式分析之前,那个最容易被忽视、却最容易坑人的环节——数据清洗。
有一句话在数据圈广为流传:
Garbage in, garbage out.(垃圾进,垃圾出。)
数据清洗,说白了就是在"正式开饭"之前,先把食材洗干净、切好、检查有没有坏的。
特别是农业多环境试验数据——多地点、多品种、多性状、多年份,数据量一大,手工录入误差、仪器故障、田间意外几乎无法避免。不清洗直接建模,等于用没洗的菜炒了一道大厨级别的菜,卖相再好,吃了也可能拉肚子。
今天以一份一年10地点、9个品种、4个区组、5个性状的农业试验数据为例,带你过一遍完整的清洗流程。
library(tidyverse)
library(data.table)
dat <- fread("one-year-multi-site.csv")
dim(dat) # 多少行多少列?
head(dat) # 长什么样?
str(dat) # 每列是什么类型?
fread() 是 data.table 包的函数,读大文件比 read.csv() 快好几倍,是我的日常主力。
看完之后,你要在心里有个基本印象:
我们这份数据长这样:

辛辛苦苦跑完一个混合模型,结果出来一堆显著,一顿汇报,领导一问: "这个品种的产量负值是怎么回事?"
然后你打开原始数据,发现……某行的 y3 写的是 -99,是当年数据录入时缺失值的占位符,根本不是真实值。
模型没有错,数据先错了。
今天这篇文章,就来聊聊做正式分析之前,那个最容易被忽视、却最容易坑人的环节——数据清洗。
五个性状,量纲差异极大——y1 在 1~3 之间,y5 能跑到 130+。这个信息先记下来,后面标准化用得上。
这一步很多人跳过,然后在后面某个步骤踩坑,找半天原因。
loc、cul、block 是分组变量,应该是因子,不是字符串,更不是数字:
dat <- dat %>%
mutate(
loc = factor(loc),
cul = factor(cul),
block = factor(block)
)
# 检查因子水平数
nlevels(dat$loc) # 应为10
nlevels(dat$cul) # 应为9
nlevels(dat$block) # 应为4
如果 cul(品种)是 1~9 的数字,R 默认读成 integer,一旦建模的时候忘了转因子,就会把品种当成连续变量处理,结果会让你怀疑人生。
# 各列缺失值数量
colSums(is.na(dat))
# 缺失比例
round(colSums(is.na(dat)) / nrow(dat) * 100, 2)
如果缺失比例小,可以考虑插补;如果某一列缺了 30% 以上,这列数据的可靠性就要打个大问号了。
推荐用 naniar 包可视化缺失模式,一张图胜过一百行代码:
library(naniar)
vis_miss(dat) # 缺哪里,一目了然
别忘了检查试验设计是否平衡:
dat %>%
count(loc, cul, block) %>%
filter(n > 1) # 有输出 = 有重复记录,需要排查
理论上 10 地点 × 9 品种 × 4 区组 = 360 行,实际行数对不上,必须找原因。
summary(dat) # 基础版
huizong(dat[, -c(1:3)]) # learnasreml包的增强版

除了看全局,还要分组看:
# 各地点性状均值
dat %>%
group_by(loc) %>%
summarise(across(y1:y5, ~round(mean(.x, na.rm = TRUE), 3)))

地点间差异大是正常的——这正是多环境试验的意义所在。但如果某个地点的均值是其他地点的 10 倍,就需要认真核实一下田间记录了。
这是整个清洗流程的重头戏。
方法一:箱线图,肉眼侦察
dat_long <- dat %>%
pivot_longer(cols = y1:y5, names_to = "trait", values_to = "value")
ggplot(dat_long, aes(x = loc, y = value, fill = loc)) +
geom_boxplot(outlier.colour = "red", outlier.shape = 16) +
facet_wrap(~trait, scales = "free_y") +
theme_bw() +
labs(title = "各性状箱线图(红点为潜在异常值)")

图出来之后,凡是飘在箱体之外的红点,都值得重点审查。
方法二:IQR法,定量判断
find_outliers_iqr <- function(x, k = 3) {
Q1 <- quantile(x, 0.25, na.rm = TRUE)
Q3 <- quantile(x, 0.75, na.rm = TRUE)
IQR <- Q3 - Q1
x < (Q1 - k * IQR) | x > (Q3 + k * IQR)
}
# 统计各性状异常值数量
dat %>%
summarise(across(y1:y5, ~sum(find_outliers_iqr(.x), na.rm = TRUE)))

这里用的是 k = 3(比标准的 1.5 更保守),农业数据本身变异就大,标准不要定太严,不然把真实的高产品种也删掉了,那就太冤了。
重要提醒: 异常值≠错误值。发现异常值之后,第一步是标记,而不是删除。留给专业人员判断:是录入错误?仪器故障?还是真实的极端表现?
dat_flagged <- dat %>%
mutate(
flag_y1 = find_outliers_iqr(y1),
# ... 其他性状
any_outlier = flag_y1 | flag_y2 | flag_y3 | flag_y4 | flag_y5
)
# 输出异常记录供核查
dat_flagged %>% filter(any_outlier)
很多统计方法(方差分析、混合模型)都有正态假设,做一下分布检验是好习惯:
# Shapiro-Wilk 正态性检验
dat %>%
summarise(across(y1:y5, ~shapiro.test(.x)$p.value))

p > 0.05:不拒绝正态假设,放心用。 p < 0.05:分布有偏,考虑对数变换或非参数方法。
配合直方图+密度曲线,结果更直观:

ggplot(dat_long, aes(x = value)) +
geom_histogram(aes(y = after_stat(density)), fill = "steelblue", alpha = 0.7) +
geom_density(color = "red", linewidth = 1) +
facet_wrap(~trait, scales = "free") +
theme_bw()
library(Hmisc)
library(corrplot)
trait_mat <- dat %>% select(y1:y5) %>% as.matrix()
corr_result <- rcorr(trait_mat)
# 相关热图
corrplot(
corr_result$r,
method = "color", type = "upper",
addCoef.col = "black",
p.mat = corr_result$P, sig.level = 0.05, insig = "blank"
)


热图里空白的格子表示相关性不显著(p > 0.05),颜色越深相关性越强。
如果 y3 和 y5 的相关系数高达 0.95,那这两个性状在模型里几乎是同一回事,做多性状分析时要小心共线性问题。
我们这份数据 y1 在 1~3 之间,y5 在 10~130 之间,量纲差距悬殊。如果要做主成分分析或者多性状综合指数,不标准化的话,y5 会直接"碾压"其他性状,让分析结果严重失真。
# Z-score标准化:均值0,标准差1
dat_scaled <- dat %>%
mutate(across(y1:y5, ~as.numeric(scale(.x))))
# 带标记版(供人工核查)
fwrite(dat_flagged, "data_with_outlier_flags.csv")
# 干净版(供建模使用)
dat_clean <- dat %>%
filter(!find_outliers_iqr(y1), !find_outliers_iqr(y2),
!find_outliers_iqr(y3), !find_outliers_iqr(y4),
!find_outliers_iqr(y5))
fwrite(dat_clean, "data_clean.csv")
步骤 | 内容 | 关键函数 |
|---|---|---|
1 | 读取数据,了解基本结构 | fread(), str(), head() |
2 | 纠正数据类型 | mutate(), factor() |
3 | 缺失值检查 + 设计完整性 | is.na(), vis_miss(), count() |
4 | 汇总统计 | summary(), group_by() |
5 | 异常值检测 + 标记 | boxplot, IQR法, Z-score法 |
6 | 分布检验 | shapiro.test(), 直方图 |
7 | 相关性分析 | rcorr(), corrplot() |
8 | 标准化(按需) | scale(), Min-Max |
9 | 导出清洗结果 | fwrite() |
数据清洗这件事,没有一劳永逸的方案,每份数据都有自己的"个性"。但有了这套流程,至少能确保你在开始建模之前,对数据的脾气摸了个七七八八。
模型跑出来的结论,才经得起追问。
使用包:tidyverse / data.table / learnasreml / naniar / Hmisc / PerformanceAnalytics / corrplot