▶️ 查看代码
company revenue cost rating
1 锦城科技 1200 800 A
2 蜀汉制造 NA 650 B
3 天府物流 1560 NA A
4 青羊金融 760 500 C
5 锦江生物 NA 900 A
6 浣花实业 892 620 B
数据分析与R语言
2026年10月08日
is.na() 一族na.rm
airquality 走全流程重要
今天是项目三"数据清洗"的开篇课。项目二教会我们"准备"数据,但现实数据往往还不"干净":有缺失、有异常、有重复。项目三四课系统解决这些问题,今天从最常见的缺失值开始。
项目二够用了吗?先花三分钟,四道小题测一测
1.(第 8 讲)df %>% filter(score >= 60, grade == "A") 里,两个条件要同时满足还是满足一个就行?想"满足一个就行"该怎么写?
2.(第 9 讲)mutate() 和 summarise(),哪一个会把表"变小"(行数变少)?哪一个保持原来的行数?
3.(第 10 讲)left_join(A, B) 合并两表时,A 里那些在 B 中找不到匹配的行——会保留吗?B 带过来的列里会填进什么?
4.(第 10 讲)把"宽表"变成"长表"用哪个函数?反过来呢?
展示一份 company_data:蜀汉制造和锦江生物两家公司的 revenue 是空的(NA)——为什么会这样?(可能是财务人员漏填、系统对接失败、新公司还没有历史数据等)
提问:如果直接对 revenue 列求 mean(),R 会给出什么结果?
处理之前,先看清楚哪里缺、缺多少
company revenue cost rating
1 锦城科技 1200 800 A
2 蜀汉制造 NA 650 B
3 天府物流 1560 NA A
4 青羊金融 760 500 C
5 锦江生物 NA 900 A
6 浣花实业 892 620 B
NA 的含义是"不知道"——任何数和"不知道"比较,结果仍是"不知道"(NA)。所以判断缺失只有一个正确写法:is.na()。
NA 会"传染":对 mean() 这类默认不忽略缺失值的函数,只要输入中有 NA,结果通常也会是 NA。许多常用统计函数提供 na.rm = TRUE,允许计算时移除缺失项;但要查具体函数文档,且这不会替你处理原数据。
注记
colSums(is.na()) 一次给出各列的"体检报告"——revenue 缺 2 个(33.3%)、cost 缺 1 个(16.7%)。
6 格里两个 TRUE(2.2 图的第二行):均值 = 缺失个数 2 ÷ 总个数 6 = 0.333——所以 mean(is.na(x)) 与 sum(is.na(x)) / length(x) 完全等价。
表格单元格的常见缺失是 NA;NaN 是无效数值运算结果,也会被 is.na() 识别;NULL 则是长度为 0 的"什么都没有",不能把三者当成同一种空值。Excel 导入时还要留心空字符串、"N/A"、"-" 等字符型假缺失(见下一页)。
从 Excel 导出的报表里,"空单元格"常以空字符串、"N/A"、"-" 的面目进来——它们是字符串,is.na() 根本查不出来:
R 内置的 airquality——1973 年纽约的空气质量日观测,153 行、6 列。它是本讲练习的"主数据",后面的作业也用它:
colSums(is.na()) 统计每一列各有多少缺失;mean(is.na()) 算出 Ozone 列的缺失比例。提示
识别出 NA 只是"体检",还要追问它为什么缺。缺失机制通常分三类(用财务场景举例):
重要
缺失比例告诉你“有多少”,不告诉你“为什么”。 <5% 不是自动删除线;>20% 也不是自动均值填补线。本讲的方法是入门工具,正式推断还要考虑机制、分析目标、分组结构和不确定性。
机制不是空谈——本讲主数据 airquality 里,Ozone 的缺失并不是均匀散布的:
5、7、8 月各缺约 16%、9 月只缺 3%,6 月却缺了 70%——缺失明显和"月份"这个已观测变量挂钩,更像 MAR(当然也不能完全排除 MNAR,机制往往要靠业务背景判断)。这就提醒我们:删或填之前,先看缺失"扎堆"在哪里(6 月这个坑,第四章"分组填补"会正式回收)。
最简单直接,但不一定总合适
company revenue cost
1 锦城科技 1200 800
2 蜀汉制造 NA 650
3 天府物流 1560 NA
4 青羊金融 760 500
5 锦江生物 NA 900
6 浣花实业 892 NA
na.omit() 是"一行有一个 NA 就整行删"——两列各有 2 个 NA、分散在 4 行,一刀切删完只剩 2 行;它的短板是没法"只按某一列删",要按列删就得换下面的 drop_na()/filter()。
drop_na() 能指定只看某几列(如只对关键字段删),比 na.omit() 灵活得多。返回类型通常跟随输入:data.frame 输入仍是 data.frame,tibble 输入仍是 tibble;不必为了换类型而使用它。
同一份 dirty_data(revenue 缺在蜀汉制造、锦江生物,cost 缺在天府物流、浣花实业,两列各有 2 个 NA),两种删法结果完全不同:
重要
关键差别就在"天府物流""浣花实业"这两行:它们的 cost 是 NA,na.omit() 一刀切全删掉;而 drop_na(revenue) 只盯 revenue 列,它们 revenue 有值(1560、892)就留下,cost 的 NA 一动不动。删哪几列、删哪几行,drop_na() 由你指定,na.omit() 自己做不到按列删(要按列删就换 drop_na()/filter(),或 base R 的 complete.cases() 子集)。
company revenue cost
1 锦城科技 1200 800
2 天府物流 1560 NA
3 青羊金融 760 500
4 浣花实业 892 NA
company revenue cost
1 锦城科技 1200 800
2 青羊金融 760 500
!is.na(x) 读作"x 不缺失",就是第 8 讲 filter() 里最普通的一个条件,好处是能和别的条件写在同一句里——下一页对比。
| 我想…… | 用这个 |
|---|---|
| 一口气清掉整表里所有含缺失的行 | na.omit(df) |
| 在管道里、只按某几列删 | df %>% drop_na(col1, col2) |
| 不装 tidyr,用 base R 按列删 | df[complete.cases(df$col1), ] |
| 把"删缺失"和别的条件写在一起 | df %>% filter(!is.na(col), revenue > 1000) |
| 不想动数据,只想先算个数字看看 | mean(x, na.rm = TRUE) |
注:当条件里已经有
revenue > 1000时,再写!is.na(revenue)其实可省略(NA > 1000本就被filter()丢掉);把它显式写出来,是为了表明"我知道这列有缺失、我就是要按它删"这个意图。
对着一张 6 行的 dirty_data:图中这三种删除(na.omit/drop_na/filter,未含 base R 的 complete.cases)"剩几行"越删越少,mean(x, na.rm = TRUE) 数据一动不动。
重要
表里前四种(na.omit/drop_na/complete.cases/filter)都会真的把行删掉,na.rm = TRUE 一行都不删——一句话:前四种改数据,na.rm 不改数据。
警告
不要只凭缺失比例决定删还是补。 先问:缺失可能怎样产生?它是否和变量本身、分组或结果有关?若缺失近似完全随机(MCAR)、删后样本仍足够且目标分析允许,可考虑完整案例删除;若缺失与已观测信息相关(MAR)或与缺失值本身相关(MNAR),直接删可能造成系统偏差。缺失多也不代表应机械地用均值填补——先说明假设、比较方案,并保留缺失标记与处理记录。
airquality 共 153 行(1973 年 5–9 月日观测),其中 Ozone 缺 37 个、Solar.R 缺 7 个。
na.omit(airquality) 会删掉多少行?还剩多少?Ozone 缺失的行、保留 Solar.R 的缺失,写出两种等价写法;Ozone 不缺失、且 Temp 低于 70 的行——drop_na() 能单独做到吗?该用什么?删掉最省事,但很多时候你删不起。这一章把"填补"的招数过一遍
0 不处理——只算数、不改数据(na.rm = TRUE)
1 直接删除——缺失机制与分析目标允许、删后样本足(na.omit()/drop_na()/filter(!is.na()))
2 填固定值——业务上"缺就是 0"(ifelse()/if_else()/case_when())
3 均值 / 中位数——数值列的简易填补示范;表内有分组/季节结构时改用分组填补(组内各算各的,4.10–4.11);须说明代表值假设及其统计代价
4 前值 / 后值——相邻记录应共享值时(如合并单元格导出)的结构性缺失(fill())
5 前后平均(线性插值)——时间序列、顺着趋势补空位(lag()/lead())
6 多源拼合——同一指标有多个来源时,按优先级取第一个非缺失值(coalesce())
还有更专门的:模型填补(mice 多重插补)、真正的移动平均(zoo::rollmean(),选做加练见)——上面"删除 + 五种填补"这六条路做熟,这些专门方法知道什么时候该找它们就够了。
纸面报表里,"销售部"写过一次后下面几行常常省略不写;可 R 读不懂"省略",它看到的只有 NA。这种缺失是报表格式造出来的,叫"结构性缺失"——注意 dept 是文本列,待会儿你就知道这意味着什么。
警告
"填 0"是最危险的一招:营收缺填 0 → 这家公司被当成"没有收入";成本缺填 0 → 凭空造出一个"零成本"的异常值。只有业务上能确定"缺失就是零"时才用它;分类列填"其他"只是兜底,不等于真相。
ifelse(条件, 成立值, 不成立值) 是向量化的——一次处理整列,配上 mutate() 就是第 9 讲"按条件改列"的完整写法;后续几页的条件替换填补会沿用这组句式(4.3–4.7 讲透)。结构性延续、多来源取值和时间插值还有各自专用函数。
missing = 指定的值;不写则仍为 NAif_else(条件, TRUE 值, FALSE 值, missing = NA 值)——ifelse() 只管前两种,if_else() 把第三种也摆上台面,逼你表态。
!is.na(revenue) ~ revenueregion == "华东" ~ 1200TRUE ~ 1000顺序就是优先级:「原值不动」必须写在最前面;最后的 TRUE ~ ... 是兜底的那张网——不写它,没命中的格子直接漏成 NA。
把同一套"分档规则"落到本课的 company_data 上——按 rating 给缺失的 revenue 填不同的数:
company revenue cost rating revenue_filled
1 锦城科技 1200 800 A 1200
2 蜀汉制造 NA 650 B 800
3 天府物流 1560 NA A 1560
4 青羊金融 760 500 C 760
5 锦江生物 NA 900 A 1000
6 浣花实业 892 620 B 892
蜀汉制造(B 级)的 NA 填 800、锦江生物(A 级)的 NA 填 1000——同一次 case_when() 调用里,不同行走不同分支,这就是它比 if_else() 强的地方:if_else() 只有"是/否"两条路,case_when() 可以写任意多条。
case_when() 从上往下逐个判断、取第一个命中的结果,因此条件顺序就是优先级。上面这段先写 !is.na(revenue) ~ revenue 保留已有值,再按 rating 给缺失值套业务规则;要是把 !is.na(revenue) 那条挪到最后,本来有值的行会先被 rating 规则覆盖,原值就丢了。最后的 TRUE ~ ... 相当于 else;若没有兜底,未命中的位置会得到 NA。
警告
这页用其余 4 家的均值(1200、1560、760、892 的均值正好是 1103)演示简单填补,不代表两家缺失营收真的等于平均值。均值填补会压缩方差、改变变量之间的关系,也低估填补的不确定性;不宜把它当成严谨统计推断的通用方案。
company revenue cost rating revenue_was_missing revenue_filled
1 锦城科技 1200 800 A FALSE 1200
2 蜀汉制造 NA 650 B TRUE 1103
3 天府物流 1560 NA A FALSE 1560
4 青羊金融 760 500 C FALSE 760
5 锦江生物 NA 900 A TRUE 1103
6 浣花实业 892 620 B FALSE 892
提示
做真实项目时尽量保留原始列,新增 revenue_filled 和缺失标记,并记录填补规则;这样可以追溯哪些值是观测值、哪些是后来填入的。
提示
中位数比均值更不容易受极端值影响——如果 revenue 里有一两家公司营收特别高或特别低,中位数填补通常比均值更稳健(这 4 家的中位数是 1046、均值是 1103,两者已经不同)。从 4.8 起,均值、中位数、分组、前后平均这几种填补大多沿用 if_else(is.na(x), 填补值, x) 这一个句式,变的只是"填补值"是什么数;fill()、coalesce() 则各有专用函数。
原组均值 组内均值填 全局均值填
1 29.4 29.4 38.3
重要
5–9 月的 Ozone 差一倍(23.6 → 59.1 → 60.0),6 月还缺得最多(21 个)。用全局均值 42.1 填,6 月被抬到 38.3(真实 29.4),7、8 月反被压低——季节规律被抹平。
# A tibble: 5 × 2
Month 填补后月均
<int> <dbl>
1 5 23.6
2 6 29.4
3 7 59.1
4 8 60
5 9 31.4
group_by() 让 mutate() 里的 mean() 在每个组内分别计算:6 月的空位填的是 6 月自己的均值 29.4,而不是全局的 42.1——对照 4.10 里"6 月被抬到 38.3",这就是分组填补的全部秘密。
警告
但别忘了 2.11 的发现:6 月 30 天里 Ozone 缺了 21 天(70%)——分组填补之后,这一整月的均值 29.4 其实是由9 个真实观测算出、再回填到 21 个空位上的。一个月七成数据是"补"出来的,这种填补只是让表看起来完整,绝不能当成 6 月的真实测量。缺失越扎堆,填补越要谨慎,必要时宁可保留 NA 并在结论里说明。
重要
dept 是分类列——不存在"销售部与行政部的均值",这里的 NA 是报表省略造成的结构性缺失,应继承同组标签,所以用 fill(.direction = "down")。fill() 并不限于文本列,也能对数值列做前值/后值延续;但它只适用于“邻近记录确实应共享同一值”的情境。财务报表中,部门、科目只在每组首行有值时,才适合向下填;若 NA 代表真实未知,就不能机械抄邻居。
提示
✓ "down" 把组名正确抄下来;✗ "up" 会把销售部错标成行政部,末行还填不上——省略式报表的组名写在组首,所以用 down。
lag(y) 把整个序列下移一行:每格取上一行的值,第 1 格没有上一行 → NA;lead(y) 把序列上移一行:每格取下一行的值,末格 → NA。
注记
蓝色格 = 第 3 行的左右邻居(lag 取上一行、lead 取下一行);红色格 = 缺失。首尾行各缺一侧邻居、连续两空时邻居自己也是 NA——都填不了。
空位取左右邻居的中点:(108 + 124) ÷ 2 = 116——只填空位、不动原值。它可看作等间隔、单个内部缺口的线性插值简例;必须先按时间排序,且连续缺失、序列首尾都不能靠这一行 lag() / lead() 代码填。真实时间序列若间隔不等或有连续缺口,应使用带时间坐标的插值方法,并避免跨组/跨月份借用邻居。移动平均是另一种任务:它会重算窗口(选做第 3 题对比)。
第 2 行 A 是 NA → 落回 B 取 900;第 1、3 行 A 有值就直接取 A(第 3 行 B 虽然是 NA,但根本轮不到它)。注意第 1 行 A、B 都有值却不一样(1200 vs 1150),coalesce() 只管"A 有没有",一律取 A,不会告诉你两边对不上。
提示
多源补缺:同一指标有两个来源(系统自动记录 + 人工补录)、各自都缺一些时,coalesce() 按优先级"拼"出一份尽量完整的数据——只挑值、不做算术,每个数都整个来自某个来源,事后可追溯它来自 A 还是 B。
警告
但它不是"对账":第 1 行 A=1200、B=1150 其实对不上,coalesce() 会默默取 A、把 B 的差异藏起来。真要核对两个来源是否一致,得自己先比对(如 which(!is.na(revenue_A) & !is.na(revenue_B) & revenue_A != revenue_B),返回 1,正指向第 1 行),别指望 coalesce() 替你发现冲突。
airquality(153 行;Ozone 缺 24.2%、Solar.R 缺 4.6%)
Solar.R 只缺 7 个——用中位数填补它(提示:if_else());Ozone 缺 24%——分别用"整列均值"和"按月分组均值"填补,比较 6 月的结果差异;lag()/lead() 取前后相邻观测的平均,只填 Ozone 中"左右紧邻值都存在"的孤立缺口;先按 Month、Day 排序并按月分组,避免跨月借值。数一数还有多少 NA,并说明连续缺失和月首/月末缺口为什么填不了。starwars(87 行;mass 缺 28 个、height 缺 6 个)
mass 的缺失按 gender 分组、用组内中位数填补;hair_color 的缺失统一填成 "unknown";case_when() 把 height 分成 "≥190 高 / 170–189 中 / <170 矮 / 缺失" 四档。把本讲的工具串成一条完整工作流
任务:以 airquality(153 行;Ozone 缺 37 个/24.2%、Solar.R 缺 7 个/4.6%)为对象,做一次完整的缺失值处理:
colSums(is.na()) 看清每列的缺失,算出两列的缺失比例;Solar.R 与 Ozone 各选一种处理方法,并写清理由;drop_na() / if_else() / group_by() + mutate() / fill() / lag() 都行);colSums(is.na()),确认缺失已经清干净;.qmd)小报告,配上简短的文字说明。邀请 1–2 名同学展示自己的处理流程,并说明"为什么这样选"——重点不是代码写得多快,而是选择理由是否站得住脚。
一条流水线,贯穿本课始终
识别(is.na() / colSums(is.na()))→ 决策(删还是补?看缺失比例、看业务)→ 处理(删:na.omit()/drop_na()/filter(!is.na());补:固定值/均值·中位数(含分组)/前值后值/前后平均/多源拼合)→ 验证(再看一遍 colSums(is.na()))
重要
最容易混的一点:na.rm = TRUE 不是"处理缺失",它只是算的时候闭一只眼,数据本身一动不动。 真要处理,就用上面那六条路里的任意一条,而且处理完必须再验一遍——这一步最容易被略过,也最容易在期末项目里翻车。
is.na()/colSums() 找出并统计缺失值,不被 == NA 和字符串"假缺失"骗到na.omit/drop_na/complete.cases/filter)与 na.rm 的区别group_by() + if_else())、前值后值(fill())、前后平均(线性插值)、多源拼合(coalesce())——加上"删除"就是 4.1 的"六条路","不处理"(na.rm)另算ifelse()/if_else()/case_when() 条件替换着填请用 3 分钟,在纸条或在线问卷上简要回答:
filter(!is.na(x)) 和 mean(x, na.rm = TRUE) 都能"对付"缺失值,本质区别在哪?if_else() 比 ifelse() 好在哪两点?(一点关于类型,一点关于 NA)本次不再自造数据,全部使用 R 内置数据集(msleep 在 ggplot2 里,先 library(ggplot2))。按《实验报告5》模板完成:在"实现代码"处贴代码、"效果截图"处贴运行结果截图,并在"结果与分析"里写出结论。
colSums(is.na()) 与 mean(is.na()) 给出 msleep 每列的缺失个数与占比,指出缺得最多和最少的列;vore(食性)分组中位数"填补 sleep_rem 的缺失,比较食草动物(herbi)那一组的结果差异,解释为什么会有这个差别;presidents 是 1945–1974 年季度支持率时间序列(含 NA)。先用 data.frame(q = time(presidents), appr = as.numeric(presidents)) 转成数据框,再用 lag()/lead() 的前后邻居平均,填 appr 中左右紧邻值均非缺失的孤立内部缺口;统计剩余 NA,并解释序列开头和连续缺失为什么不能这样填。mass 的缺失按 gender 分组用组内中位数填补;把 hair_color 的缺失统一填成 "unknown";case_when() 把 height 分成 "≥190 高 / 170–189 中 / <170 矮 / 缺失" 四档;colSums(is.na()) 验证缺失是否已清干净,并在结论里写明"删除"与"填补"各适用什么条件。提交要求
在《实验报告5》模板里,把每题"代码 + 运行结果"的截图粘贴到相应位置,并在"结果与分析"里写出结论;文件命名为"学号_姓名_实验报告5",提交该 Word 文档。本次作业计入平时成绩的"实验报告"部分考核,请确保代码可运行、结果完整。
提示
选做加练(3 题)——课上讲到、但作业里没覆盖的三个延伸点:
fill() 的方向:4.13 已经把 "down" 和 "up" 的结果摆在一起了——解释为什么 "up" 会把销售部那两行填成"行政部",再想一想现实报表里什么样的表才适合"往上填";coalesce() 做多源补缺:造两份能互相补位的营收数据(系统导出 + 人工补录),用 coalesce() 拼成完整数据,再和"直接取平均"的结果比一比,说明补缺时为什么优先用 coalesce()(每个数都来自真实来源、可追溯);再想一想:如果两个来源在同一行都有值却对不上,coalesce() 会怎么处理、这样安全吗?library(zoo) 后用 zoo::rollmean(x, k = 3, fill = NA, align = "center"),对比它与 4.15 手写线性插值的结果差异,并说明各自适用场合。异常值检测与处理、数据标准化与归一化
提示
课前准备:保留好今天处理缺失值后的数据,下节课会在这份"干净"数据的基础上继续排查异常值。
第11讲:缺失值处理
"In God we trust; all others must bring data — and that data had better not be missing." 财务数据中的缺失不能"拍脑袋"填写,每一个"补数"决定都应当有据可查、有理可依,这与审计工作中对异常科目要"查明原因、审慎处理"的要求是相通的。
朱 奇 | 锦城大学 · 财会学院