基于tidyr的长/宽数据转换
Tidyr是tidyverse的核心包之一,其目标是帮助创建整洁的数据。整洁的数据是具有以下特征的数据:
每个变量都是一列
每个观测值是一行
每个值是一个单元格
1 加载包
这里的案例数据显示了以三种不同方式组织的相同数据。每个数据集都有相同的四个变量:country(国家)、year(年份)、population(总人口)和cases(结核病病例数),但每个数据集以不同的方式组织这些值。
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
table2
# A tibble: 12 × 4
country year type count
<chr> <dbl> <chr> <dbl>
1 Afghanistan 1999 cases 745
2 Afghanistan 1999 population 19987071
3 Afghanistan 2000 cases 2666
4 Afghanistan 2000 population 20595360
5 Brazil 1999 cases 37737
6 Brazil 1999 population 172006362
7 Brazil 2000 cases 80488
8 Brazil 2000 population 174504898
9 China 1999 cases 212258
10 China 1999 population 1272915272
11 China 2000 cases 213766
12 China 2000 population 1280428583
table3
# A tibble: 6 × 3
country year rate
<chr> <dbl> <chr>
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中的数据符合 Figure 1 中的整洁数据规范。
2 pivot_longer()
:转换成长数据
这里我们以billboard数据集为例。该数据记录了 2000 年歌曲在广告牌上的排名情况:
billboard
# A tibble: 317 × 79
artist track date.entered wk1 wk2 wk3 wk4 wk5 wk6 wk7 wk8
<chr> <chr> <date> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 2 Pac Baby… 2000-02-26 87 82 72 77 87 94 99 NA
2 2Ge+her The … 2000-09-02 91 87 92 NA NA NA NA NA
3 3 Doors D… Kryp… 2000-04-08 81 70 68 67 66 57 54 53
4 3 Doors D… Loser 2000-10-21 76 76 72 69 67 65 55 59
5 504 Boyz Wobb… 2000-04-15 57 34 25 17 17 31 36 49
6 98^0 Give… 2000-08-19 51 39 34 26 26 19 2 2
7 A*Teens Danc… 2000-07-08 97 97 96 95 100 NA NA NA
8 Aaliyah I Do… 2000-01-29 84 62 51 41 38 35 35 38
9 Aaliyah Try … 2000-03-18 59 53 38 28 21 18 16 14
10 Adams, Yo… Open… 2000-08-26 76 76 74 69 68 67 61 58
# ℹ 307 more rows
# ℹ 68 more variables: wk9 <dbl>, wk10 <dbl>, wk11 <dbl>, wk12 <dbl>,
# wk13 <dbl>, wk14 <dbl>, wk15 <dbl>, wk16 <dbl>, wk17 <dbl>, wk18 <dbl>,
# wk19 <dbl>, wk20 <dbl>, wk21 <dbl>, wk22 <dbl>, wk23 <dbl>, wk24 <dbl>,
# wk25 <dbl>, wk26 <dbl>, wk27 <dbl>, wk28 <dbl>, wk29 <dbl>, wk30 <dbl>,
# wk31 <dbl>, wk32 <dbl>, wk33 <dbl>, wk34 <dbl>, wk35 <dbl>, wk36 <dbl>,
# wk37 <dbl>, wk38 <dbl>, wk39 <dbl>, wk40 <dbl>, wk41 <dbl>, wk42 <dbl>, …
在这个数据集中,每个观测(行)都是一首歌曲。前三列(“artist”、“track”和“date.entered”)是描述歌曲的变量。在此之后的76 列(“wk1”-“wk76”)描述了该歌曲在发行后每周的排名信息(仅在进入前100名时记录名次信息)。因此,对于“wk1”-“wk76”中的数据,所有的列名是一个变量(周数),每个单元格值则是另一个变量(每周的排名)。因此,这个数据集是不符合整洁数据的要求的。
下面,我们通过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
:指定需要转换哪些列,即哪些列不是变量。此参数使用与select()
相同的语法(见此前章节),因此在这里我们可以使用!c(artist, track, date.entered)
或starts_with("wk")
来选择“wk1”到“wk76”列。names_to
:转换后的列的列名,即新变量名。这里我们将该变量命名为 “week” 。values_to
:指定单元格值所代表的变量的变量名。我们将该变量命名为 “rank”。⚠️注意”week”和”rank”加了引号。
现在,让我们聚焦生成的长数据。如果一首歌进入前 100 名的时间少于 76 周,会发生什么情况?以 2 Pac 的《Baby Don’t Cry》为例。上面的输出结果表明,这首歌在前 100 名中只停留了 7 周,其余的周数都是缺失值。这些缺失值其实并不代表未知观测值,而是转换后的长数据集的结构迫使它们存在,因此我们可以通过设置 values_drop_na = TRUE
来要求 pivot_longer()
去掉它们:
billboard |>
pivot_longer(
cols = starts_with("wk"),
names_to = "week",
values_to = "rank",
values_drop_na = TRUE
)
# A tibble: 5,307 × 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 2Ge+her The Hardest Part Of ... 2000-09-02 wk1 91
9 2Ge+her The Hardest Part Of ... 2000-09-02 wk2 87
10 2Ge+her The Hardest Part Of ... 2000-09-02 wk3 92
# ℹ 5,297 more rows
注意到“week”目前为字符型变量,可以通过readr包中的parse_number()
将其转换成数值型变量(详见此前章节),便于后续分析:
billboard_longer <- billboard |>
pivot_longer(
cols = starts_with("wk"),
names_to = "week",
values_to = "rank",
values_drop_na = TRUE
) |>
mutate(
week = parse_number(week)
)
billboard_longer
# A tibble: 5,307 × 5
artist track date.entered week rank
<chr> <chr> <date> <dbl> <dbl>
1 2 Pac Baby Don't Cry (Keep... 2000-02-26 1 87
2 2 Pac Baby Don't Cry (Keep... 2000-02-26 2 82
3 2 Pac Baby Don't Cry (Keep... 2000-02-26 3 72
4 2 Pac Baby Don't Cry (Keep... 2000-02-26 4 77
5 2 Pac Baby Don't Cry (Keep... 2000-02-26 5 87
6 2 Pac Baby Don't Cry (Keep... 2000-02-26 6 94
7 2 Pac Baby Don't Cry (Keep... 2000-02-26 7 99
8 2Ge+her The Hardest Part Of ... 2000-09-02 1 91
9 2Ge+her The Hardest Part Of ... 2000-09-02 2 87
10 2Ge+her The Hardest Part Of ... 2000-09-02 3 92
# ℹ 5,297 more rows
现在,我们在一个变量中拥有了所有的周数,在另一个变量中拥有了所有的排名值,这样我们就可以很好地直观显示歌曲排名随时间的变化情况。代码如下,结果如 Figure 2 所示。我们可以看到,很少有歌曲能在前 100 名中停留 20 周以上。
billboard_longer |>
ggplot(aes(x = week,
y = rank,
group = track)) +
geom_line(alpha = 0.25) +
scale_y_reverse() +
theme_bw()
2.1 pivot_longer()
的转换原理
假设我们有三个病人,id 分别为 A、B 和 C,我们为每个病人测量了两次血压。这里我们用在此前章节中已经介绍过的 tribble()
来创建这个数据:
df <- tribble(
~id, ~bp1, ~bp2,
"A", 100, 120,
"B", 140, 115,
"C", 120, 125
)
该数据集的“bp1”和“bp2”列不符合整洁数据的要求。因此,我们通过pivot_longer()
将该数据转换成包含三个变量: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
转换是如何进行的呢?如果我们逐列考虑,就会比较容易理解。如下图所示,在原始数据集中已经是变量的一列(id)的值需要重复,重复的次数等于需要转换的列数。这里我们需要转换的列是“bp1”和“bp2”,因此每个“id”需要重复两次。
旧列名将成为新变量的值,该变量的名称由 names_to
参数定义。
单元格值也会变成一个新变量的值,其名称由 values_to
定义。这些值将逐行填充。
2.2 包含多个变量信息的列名
更具挑战性的情况是,列名中包含了多个变量信息,而我们希望将这些信息分别存储在不同的新变量中。以 who2 数据集为例,该数据集由世界卫生组织收集,记录了肺结核诊断的相关信息:
# 查看3:56的列名规则
colnames(who2)[3:56] %>%
str_split(
pattern = "_",
simplify = T
) %>%
apply(MARGIN = 2, unique)
[[1]]
[1] "sp" "sn" "ep" "rel"
[[2]]
[1] "m" "f"
[[3]]
[1] "014" "1524" "2534" "3544" "4554" "5564" "65"
可以看到,该数据的前两列“country”和“year”是正常的变量。剩余的56列,每一列的名称都由三个部分组成,中间用 “_” 分隔:
第一部分为“sp/rel/ep”,代表采用的诊断方法;
第二部分为“m/f” ,代表性别;
第三部分为“014/1524/2534/3535/44/4554/5564/65”, 代表年龄范围(例如,014 代表 0-14岁)。
因此,在这种情况下,who2 数据中记录了六条变量信息:国家和年份(已列出);诊断方法、性别和年龄范围(包含在3-56列的列名中),以及该类别中的患者人数(单元格值)。为了将这六条变量信息转换到六个独立的列中,我们可以通过指定 pivot_longer()
中的 names_sep
参数,按照“_”分隔符将原始变量名分割成若干个字符串:
who2 |>
pivot_longer(
cols = !(country:year),
names_sep = "_", # 指定原始列名的分隔符
names_to = c("diagnosis", "gender", "age"),
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
7 Afghanistan 1980 sp m 65 NA
8 Afghanistan 1980 sp f 014 NA
9 Afghanistan 1980 sp f 1524 NA
10 Afghanistan 1980 sp f 2534 NA
# ℹ 405,430 more rows
除了用 names_sep
,也可以使用 names_pattern
参数,通过正则表达式来实现上述效果。在这个案例中的数据转换过程参考如下示意图:
2.3 混合了变量名和变量取值的列名
该数据集包含五个家庭的数据,每个家庭最多记录两个孩子的姓名(name_child1、name_child2)和出生日期(dob_child1、dob_child2)。仔细观察 Table 1 和 Table 2 可以发现 :Table 1 中需要转换的列名均由不同变量的具体取值构成(如“sp_m_014”,我们就可以知道这一列对应的诊断方法为“sp”,性别为“m”,年龄段为0-14岁);而 Table 2 的列名既有变量的具体取值,如“child1”告诉我们这是编号为1的child,也有变量的名称,如“dob”(出生日期)和“name”(姓名)。
为了解决这个问题,我们同样需要根据“_”分隔符分割原始列名,然后向 names_to
提供新变量名。但由于列名混合了变量名和变量取值,我们难以一一对应新旧列名。因此,这时候我们使用特殊的”.value “字符,让分割后的旧列名的第一个元素直接作为新变量的名称;分割后的旧列名的第二个元素(child1或child2)作为新的“child”变量的取值:
household |>
pivot_longer(
cols = !family,
names_sep = "_",
names_to = c(".value", "child"),
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
7 4 child2 2009-08-27 Khai
8 5 child1 2000-12-05 Parker
9 5 child2 2005-02-28 Gracie
使用 names_to = c(".value", "num")
进行数据转换会将列名分成两部分:第一部分决定输出的列名(x 或 y),第二部分决定 num 列的值。见下面的示意图:
3 pivot_wider()
:转换成宽数据
pivot_wider()
可以通过增加列数和减少行数使数据集变宽。例如在重复测量数据中,一般而言我们会将每次观测作为一个个案,这时候一个对象多个时间点的观测数据分布在多行。这样的数据即为长数据,便于进行各种统计分析。但是如果你想了解每个对象的观测情况,就可以通过pivot_wider()
来将一个对象的多次观测结果合并到一行,即转换为宽数据。这种情况的实际应用场景较少,因此下面我们只简单演示了一个转换宽数据的基本语法,相关详细解释参考该链接。
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… CAHPS for MI… 63
2 0446157747 USC CARE MEDICAL GROUP INC CAHPS_GRP… CAHPS for MI… 87
3 0446157747 USC CARE MEDICAL GROUP INC CAHPS_GRP… CAHPS for MI… 86
4 0446157747 USC CARE MEDICAL GROUP INC CAHPS_GRP… CAHPS for MI… 57
5 0446157747 USC CARE MEDICAL GROUP INC CAHPS_GRP… CAHPS for MI… 85
6 0446157747 USC CARE MEDICAL GROUP INC CAHPS_GRP… CAHPS for MI… 24
7 0446162697 ASSOCIATION OF UNIVERSITY PHYSI… CAHPS_GRP… CAHPS for MI… 59
8 0446162697 ASSOCIATION OF UNIVERSITY PHYSI… CAHPS_GRP… CAHPS for MI… 85
9 0446162697 ASSOCIATION OF UNIVERSITY PHYSI… CAHPS_GRP… CAHPS for MI… 83
10 0446162697 ASSOCIATION OF UNIVERSITY PHYSI… CAHPS_GRP… CAHPS for MI… 63
# ℹ 490 more rows
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 CAHPS_GRP_8
<chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
1 0446157747 USC C… 63 87 86 57 85
2 0446162697 ASSOC… 59 85 83 63 88
3 0547164295 BEAVE… 49 NA 75 44 73
4 0749333730 CAPE … 67 84 85 65 82
5 0840104360 ALLIA… 66 87 87 64 87
6 0840109864 REX H… 73 87 84 67 91
7 0840513552 SCL H… 58 83 76 58 78
8 0941545784 GRITM… 46 86 81 54 NA
9 1052612785 COMMU… 65 84 80 58 87
10 1254237779 OUR L… 61 NA NA 65 NA
# ℹ 85 more rows
# ℹ 1 more variable: CAHPS_GRP_12 <dbl>
4 拆分/合并列
4.1 unite()
:合并多列
unite(data, col, ..., sep, remove, na.rm)
可以将多列中的字符粘贴起来构成新的一列。其中:
data
:目标数据集。col
:新列的名称。...
:需要合并的列名(若用“:”连接则表示合并两列及之间的所有列)。sep
:指定连接符。remove
:是否移除原始列。na.rm
:如果为“True”,则在合并每个值之前将删除缺失值。
# A tibble: 336,776 × 17
date dep_time sched_dep_time dep_delay arr_time sched_arr_time arr_delay
<chr> <int> <int> <dbl> <int> <int> <dbl>
1 2013/1/1 517 515 2 830 819 11
2 2013/1/1 533 529 4 850 830 20
3 2013/1/1 542 540 2 923 850 33
4 2013/1/1 544 545 -1 1004 1022 -18
5 2013/1/1 554 600 -6 812 837 -25
6 2013/1/1 554 558 -4 740 728 12
7 2013/1/1 555 600 -5 913 854 19
8 2013/1/1 557 600 -3 709 723 -14
9 2013/1/1 557 600 -3 838 846 -8
10 2013/1/1 558 600 -2 753 745 8
# ℹ 336,766 more rows
# ℹ 10 more variables: carrier <chr>, flight <int>, tailnum <chr>,
# origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>, hour <dbl>,
# minute <dbl>, time_hour <dttm>
也可以用 str_c()
或 str_glue()
(见基于tidyr的字符串处理-拼接字符串)实现同样的效果:
# A tibble: 336,776 × 20
date year month day dep_time sched_dep_time dep_delay arr_time
<chr> <int> <int> <int> <int> <int> <dbl> <int>
1 2013/1/1 2013 1 1 517 515 2 830
2 2013/1/1 2013 1 1 533 529 4 850
3 2013/1/1 2013 1 1 542 540 2 923
4 2013/1/1 2013 1 1 544 545 -1 1004
5 2013/1/1 2013 1 1 554 600 -6 812
6 2013/1/1 2013 1 1 554 558 -4 740
7 2013/1/1 2013 1 1 555 600 -5 913
8 2013/1/1 2013 1 1 557 600 -3 709
9 2013/1/1 2013 1 1 557 600 -3 838
10 2013/1/1 2013 1 1 558 600 -2 753
# ℹ 336,766 more rows
# ℹ 12 more variables: sched_arr_time <int>, arr_delay <dbl>, carrier <chr>,
# flight <int>, tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>,
# distance <dbl>, hour <dbl>, minute <dbl>, time_hour <dttm>
4.2 拆分列
在 tidyr
中,separate_wider_position()
、 separate_wider_delim()
以及 separate_wider_regex()
可以将某一列根据分割符(separate_wider_delim()
)、固定宽度(separate_wider_position()
)或正则表达式(separate_wider_regex()
)拆分为多列。把这些函数中的“wider”替换成“longer”就构成了另外三个类似函数,用于将某一列根据分割符、固定宽度或正则表达式拆分为多行(不常用)。因为涉及到字串的处理以及正则表达式,我们在基于tidyr的字符串处理-拼接字符串中对这些函数进行了详细介绍。