5 数据整洁化
5.1 引言
数据可视化和建模都要求数据处于“整洁”格式(tidy format)。虽然无法控制获取数据的原始结构,但可通过数据整理将其转换为整洁结构。
本章展示:
- 整洁数据的定义与原则;
- 如何使用
tidyr包中的函数进行数据整洁化; - 如何识别和处理缺失值。
tidyr 是 tidyverse 的核心之一,故我们须先载入tidyverse。
library(tidyverse)5.2 整洁数据
整洁数据具有以下三个核心特征:
- 每个变量成一列;
- 每个观测值成一行;
- 每个观测单位成一个表。
table1
#> # A tibble: 6 × 4
#> country year cases population
#> <chr> <dbl> <dbl> <dbl>
#> 1 Afghanistan 1999 745 19987071
#> 2 Afghanistan 2000 2666 20595360
#> 3 Brazil 1999 37737 172006362
#> 4 Brazil 2000 80488 174504898
#> 5 China 1999 212258 1272915272
#> 6 China 2000 213766 1280428583
在 table1 中,每行表示一个国家在某一年的观测,有 country, year, cases, population 四列,结构清晰,便于进一步处理和可视化。
可以基于已有变量创建派生变量。例如:
# 计算每万人中的发病率
table1 |>
mutate(rate = cases / population * 10000)
#> # A tibble: 6 × 5
#> country year cases population rate
#> <chr> <dbl> <dbl> <dbl> <dbl>
#> 1 Afghanistan 1999 745 19987071 0.373
#> 2 Afghanistan 2000 2666 20595360 1.29
#> 3 Brazil 1999 37737 172006362 2.19
#> 4 Brazil 2000 80488 174504898 4.61
#> 5 China 1999 212258 1272915272 1.67
#> 6 China 2000 213766 1280428583 1.67
# 计算每年病例数
table1 |>
group_by(year) |>
summarize(total_cases = sum(cases))
#> # A tibble: 2 × 2
#> year total_cases
#> <dbl> <dbl>
#> 1 1999 250740
#> 2 2000 296920可视化如下:
# 可视化
ggplot(table1, aes(x = year, y = cases)) +
geom_line(aes(group = country), color = "grey50") +
geom_point(aes(color = country, shape = country)) +
scale_x_continuous(breaks = c(1999, 2000)) # x-axis breaks at 1999 and 2000
5.3 pivot_long()
虽然整洁数据貌似很好实现,但现实中的数据往往不符合整洁格式。两个常见原因:
- 数据为录入/展示方便而设计(如宽格式)
- 多数数据制作者未系统学习整洁数据的规范
下面逐一介绍相关问题及解决方法。
5.3.1 错误其一 | 变量作为列名
下面这个名为 billboard 的数据集记录了 2000 年歌曲的公告牌排名:
billboard
#> # A tibble: 317 × 79
#> artist track date.entered wk1 wk2 wk3 wk4 wk5
#> <chr> <chr> <date> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 2 Pac Baby Don't Cry (Ke… 2000-02-26 87 82 72 77 87
#> 2 2Ge+her The Hardest Part O… 2000-09-02 91 87 92 NA NA
#> 3 3 Doors Down Kryptonite 2000-04-08 81 70 68 67 66
#> 4 3 Doors Down Loser 2000-10-21 76 76 72 69 67
#> 5 504 Boyz Wobble Wobble 2000-04-15 57 34 25 17 17
#> 6 98^0 Give Me Just One N… 2000-08-19 51 39 34 26 26
#> # ℹ 311 more rows
#> # ℹ 71 more variables: wk6 <dbl>, wk7 <dbl>, wk8 <dbl>, wk9 <dbl>, …表格宽度相当浮夸,因为制表者将不同星期这一变量作为列名,整整列出了76列星期(此处引用的表格仅展示wk1~wk5)。显然,星期数是一个变量,将其作为列名很不合理、很不整洁。
使用 pivot_longer() 可进行处理,将星期这一变量进行转置:
billboard |>
pivot_longer(
cols = starts_with("wk"),
names_to = "week",
values_to = "rank"
)
#> # A tibble: 24,092 × 5
#> artist track date.entered week rank
#> <chr> <chr> <date> <chr> <dbl>
#> 1 2 Pac Baby Don't Cry (Keep... 2000-02-26 wk1 87
#> 2 2 Pac Baby Don't Cry (Keep... 2000-02-26 wk2 82
#> 3 2 Pac Baby Don't Cry (Keep... 2000-02-26 wk3 72
#> 4 2 Pac Baby Don't Cry (Keep... 2000-02-26 wk4 77
#> 5 2 Pac Baby Don't Cry (Keep... 2000-02-26 wk5 87
#> 6 2 Pac Baby Don't Cry (Keep... 2000-02-26 wk6 94
#> 7 2 Pac Baby Don't Cry (Keep... 2000-02-26 wk7 99
#> 8 2 Pac Baby Don't Cry (Keep... 2000-02-26 wk8 NA
#> 9 2 Pac Baby Don't Cry (Keep... 2000-02-26 wk9 NA
#> 10 2 Pac Baby Don't Cry (Keep... 2000-02-26 wk10 NA
#> # ℹ 24,082 more rows参数解释:
cols用于指定需要转置的列。除了starts_with("wk"),也可写成!c(artist, track, date.entered)names_to用于命名新产生的列,此处将该列命名为weekvalues_to用于命名存储在原各个列下的变量分离后产生的新列,此处将该列命名为rank
需注意,week和rank都需要加上引号,因为这是正在创建的新变量。
初步处理完毕,不知道你有没有发现一个问题:如果原表的某个歌曲还未到wk76就已经跌出榜单而观测值为空白,那么转置后岂不是会产生若干只含有NA的行?这显然仍需改进。
方法很简单,只需在pivot_longer()中加上一个参数:values_drop_na = TRUE,即可删去只含NA的行。
将转置后表格命名为 billboard_longer 并进行可视化如下图所示:

5.3.2 pivot_long() 的运作原理
通过上一节的例子能看到pivot函数的奇妙之处,让人不禁好奇其原理如何。接下来以另一个数据框为例进行展示。
假设有 3 名患者,id 分别是 A 、 B 和 C,现在对每位患者进行两次血压测量,以此创建 tibble 如下。
df <- tribble(
~id, ~bp1, ~bp2,
"A", 100, 120,
"B", 140, 115,
"C", 120, 125
)
tribble()函数用于快捷创建tibble。
接下来我们需要将其修整为新的数据框,含有三个变量:id、measurement、value,如下实现:
df |>
pivot_longer(
cols = bp1:bp2,
names_to = "measurement",
values_to = "value"
)
#> # A tibble: 6 × 3
#> id measurement value
#> <chr> <chr> <dbl>
#> 1 A bp1 100
#> 2 A bp2 120
#> 3 B bp1 140
#> 4 B bp2 115
#> 5 C bp1 120
#> 6 C bp2 125下列组图形象地展现了这一转置过程:
- 已经是变量的列需要重复,每个转置列重复一次。

- 转置列的列名将成为新列中的值。对于原始数据集的每一行,这些值需要重复一次。

单元格值也会成为新变量中的值,其名称由values_to定义,且逐行展开。

5.3.3 错误其二 | 列名包含多个变量
有时制表者为了图方便,列名可能一次性塞入了若干变量,导致要素过多而很不整洁。下面以一名为 who2 的整理了结核病的数据框为例。
who2
#> # A tibble: 7,240 × 58
#> country year sp_m_014 sp_m_1524 sp_m_2534 sp_m_3544 sp_m_4554
#> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 Afghanistan 1980 NA NA NA NA NA
#> 2 Afghanistan 1981 NA NA NA NA NA
#> 3 Afghanistan 1982 NA NA NA NA NA
#> 4 Afghanistan 1983 NA NA NA NA NA
#> 5 Afghanistan 1984 NA NA NA NA NA
#> 6 Afghanistan 1985 NA NA NA NA NA
#> # ℹ 7,234 more rows
#> # ℹ 51 more variables: sp_m_5564 <dbl>, sp_m_65 <dbl>, sp_f_014 <dbl>, …这里解释一下制表者对第三列及后续列的命名思路:以下划线分为三个部分,第一部分表示诊断方法,第二部分表示性别,第三部分表示年龄范围。比如 sp_m_014 表示诊断方式sp,男性,0~14岁。
可见若拆分后该表将包含六个变量:国家、年份、诊断方法、性别、年龄范围、患者数。
下面依然使用pivot_long()函数进行修整:
who2 |>
pivot_longer(
cols = !(country:year),
names_to = c("diagnosis", "gender", "age"),
names_sep = "_",
values_to = "count"
)
#> # A tibble: 405,440 × 6
#> country year diagnosis gender age count
#> <chr> <dbl> <chr> <chr> <chr> <dbl>
#> 1 Afghanistan 1980 sp m 014 NA
#> 2 Afghanistan 1980 sp m 1524 NA
#> 3 Afghanistan 1980 sp m 2534 NA
#> 4 Afghanistan 1980 sp m 3544 NA
#> 5 Afghanistan 1980 sp m 4554 NA
#> 6 Afghanistan 1980 sp m 5564 NA
#> # ℹ 405,434 more rows这里有新增参数names_sep,可将对应列以指定方式进行拆分。
这个例子同时体现了同时命名多个新列的方式,即使用集合c()。
5.3.4 错误其三 | 列名同时包含变量的名称与值
还有更复杂的错误,即变量的名与值相混合。下面以一个名为 household 的记录家庭孩子数与姓名的数据框为例。
household
#> # A tibble: 5 × 5
#> family dob_child1 dob_child2 name_child1 name_child2
#> <int> <date> <date> <chr> <chr>
#> 1 1 1998-11-26 2000-01-29 Susan Jose
#> 2 2 1996-06-22 NA Mark <NA>
#> 3 3 2002-07-11 2004-04-05 Sam Seth
#> 4 4 2004-10-10 2009-08-27 Craig Khai
#> 5 5 2000-12-05 2005-02-28 Parker Gracie可以看出新的挑战是:列名包含两个变量名(dob、name)和另一变量的值(child,值为 1 或 2)。
为解决这一问题,需要在给参数names_to提供的向量的首位加一个.value,这并不是某一个变量名称,而是表示使用转置列名的第一个组分作为输出中的变量名(既然要有组分之分,故还得加一个names_sep参数)。同时别忘了values_drop_na = TRUE参数,因为一个孩子的家庭会出现 NA行。
household |>
pivot_longer(
cols = !family,
names_to = c(".value", "child"),
names_sep = "_",
values_drop_na = TRUE
)
#> # A tibble: 9 × 4
#> family child dob name
#> <int> <chr> <date> <chr>
#> 1 1 child1 1998-11-26 Susan
#> 2 1 child2 2000-01-29 Jose
#> 3 2 child1 1996-06-22 Mark
#> 4 3 child1 2002-07-11 Sam
#> 5 3 child2 2004-04-05 Seth
#> 6 4 child1 2004-10-10 Craig
#> # ℹ 3 more rows5.4 pivot_wider()
整个 5.3 节均使用pivot_long()函数对表格进行修整,整体变得更长。下面讨论将数据框变宽的修整情形,以包含医疗保险患者体检数据的数据框为例:
cms_patient_experience
#> # A tibble: 500 × 5
#> org_pac_id org_nm measure_cd measure_title prf_rate
#> <chr> <chr> <chr> <chr> <dbl>
#> 1 0446157747 USC CARE MEDICAL GROUP INC CAHPS_GRP_1 CAHPS for MIPS… 63
#> 2 0446157747 USC CARE MEDICAL GROUP INC CAHPS_GRP_2 CAHPS for MIPS… 87
#> 3 0446157747 USC CARE MEDICAL GROUP INC CAHPS_GRP_3 CAHPS for MIPS… 86
#> 4 0446157747 USC CARE MEDICAL GROUP INC CAHPS_GRP_5 CAHPS for MIPS… 57
#> 5 0446157747 USC CARE MEDICAL GROUP INC CAHPS_GRP_8 CAHPS for MIPS… 85
#> 6 0446157747 USC CARE MEDICAL GROUP INC CAHPS_GRP_12 CAHPS for MIPS… 24
#> # ℹ 494 more rows现在研究的重点是组织名称(org_nm),要确保每个组织名称仅有一行。
此时使用pivot_wider()进行修正如下:
cms_patient_experience |>
pivot_wider(
id_cols = starts_with("org"),
names_from = measure_cd,
values_from = prf_rate
)
#> # A tibble: 95 × 8
#> org_pac_id org_nm CAHPS_GRP_1 CAHPS_GRP_2 CAHPS_GRP_3 CAHPS_GRP_5
#> <chr> <chr> <dbl> <dbl> <dbl> <dbl>
#> 1 0446157747 USC CARE MEDICA… 63 87 86 57
#> 2 0446162697 ASSOCIATION OF … 59 85 83 63
#> 3 0547164295 BEAVER MEDICAL … 49 NA 75 44
#> 4 0749333730 CAPE PHYSICIANS… 67 84 85 65
#> 5 0840104360 ALLIANCE PHYSIC… 66 87 87 64
#> 6 0840109864 REX HOSPITAL INC 73 87 84 67
#> # ℹ 89 more rows
#> # ℹ 2 more variables: CAHPS_GRP_8 <dbl>, CAHPS_GRP_12 <dbl>参数说明:
id_cols表示保留的列的特征,此处仅保留以 org 开头的列,而排除了 measure_title 列names_from表示新列名在原表中所在列的列名values_from表示新列中包含变量值在原表中所在列的列名
5.4.1 pivot_wider() 的运作原理
为了了解工作原理,再次从一个最简单的数据集开始。假设有两个 id 为 A 和 B 的两名患者,对患者 A 进行了 3 次血压测量,对患者 B 进行了 2 次:
df <- tribble(
~id, ~measurement, ~value,
"A", "bp1", 100,
"B", "bp1", 140,
"B", "bp2", 115,
"A", "bp2", 120,
"A", "bp3", 105
)下面从 value 列取值,从 measurement 列取列名:
df |>
pivot_wider(
names_from = measurement,
values_from = value
)
#> # A tibble: 2 × 4
#> id bp1 bp2 bp3
#> <chr> <dbl> <dbl> <dbl>
#> 1 A 100 120 105
#> 2 B 140 115 NA首先,此过程需要弄清楚新的行和列中的内容。新列的名称将是 measurement 中的唯一值,可通过distinct()函数确认:
df |>
distinct(measurement) |>
pull()
#> [1] "bp1" "bp2" "bp3"默认情况下,输出的新行由所有未进入新列名或值的变量决定,这些行称为id_cols。如下:
df |>
select(-measurement, -value) |>
distinct()
#> # A tibble: 2 × 1
#> id
#> <chr>
#> 1 A
#> 2 B然后,pivot_wider()将这些结果组合起来,生成一个空数据框:
df |>
select(-measurement, -value) |>
distinct() |>
mutate(x = NA, y = NA, z = NA)
#> # A tibble: 2 × 4
#> id x y z
#> <chr> <lgl> <lgl> <lgl>
#> 1 A NA NA NA
#> 2 B NA NA NA最后它会使用输入中的数据填充所有NA值。在这种情况下,并非输出中的每个单元格在输入中都有相应的值,因为患者 B 没有第三次血压测量,因此该单元格仍然缺失。