Table of Contents
引言
在本章中,你将学习一种在 R 里以一致方式组织数据的方法,也就是所谓的“整洁数据(tidy data)”体系。要把数据整理成这种格式,前期需要付出一些工作,但从长远来看这些付出会带来回报。等你拥有了整洁数据,你将花更少的时间把数据从一种表示形式“搓”到另一种表示形式,从而让你能够把更多时间投入到你真正关心的数据问题上。
在本章中,你将首先学习整洁数据的定义,并用一个简单的玩具数据集来加以说明。然后,我们会深入讲解你在整理数据时将使用的主要工具:pivot(透视/旋转)。pivot 允许你在不改变任何数值的情况下,改变数据的形态。
整洁数据
你可以用多种方式来表示同一份底层数据。下面的例子展示了同一份数据以三种不同的方式组织起来。每个数据集都包含四个变量的相同取值:country(国家)、year(年份)、population(人口)以及 TB(结核病)的已记录病例数(number of documented cases of TB),但每个数据集对这些取值的组织方式都不同。
library(tidyr)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 1280428583table2#> # 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#> # ℹ 6 more rowstable3#> # 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)会更容易进行处理,因为它是“整洁的(tidy)”。
有三条相互关联的规则决定了数据集是否整洁:
- 每个变量都是一列;每一列都是一个变量。
- 每个观测值都是一行;每一行都是一个观测值。
- 每个值都是一个单元格;每个单元格都是一个单一的值。

为什么要确保你的数据是整洁的(tidy)?主要有两个好处:
- 选择一种一致的数据存储方式带来的总体优势。如果你的数据结构是统一的,那么学习那些与之配套的工具会更容易,因为它们背后有一种共同的、相对统一的规律。
- 把变量放在列中带来的特定优势:这能让 R 的向量化特性充分发挥。大多数内置的 R 函数都能作用在一组值的向量上。因此,对整洁数据进行转换会感觉特别自然。
dplyr、ggplot2包,都被设计成可以与整洁数据(tidy data)协同工作。下面是几个小例子,展示你可能会如何使用 table1。
# Compute rate per 10,000table1 |> 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# Compute total cases per yeartable1 |> group_by(year) |> summarize(total_cases = sum(cases))#> # A tibble: 2 × 2#> year total_cases#> <dbl> <dbl>#> 1 1999 250740#> 2 2000 296920# Visualize changes over timeggplot(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
拉长数据
整洁数据(tidy data)的原则看起来似乎如此显而易见,以至于你可能会想:你是不是再也不会遇到不整洁的数据集了。然而,不幸的是,大多数真实数据都是不整洁的。有两个主要原因:
数据往往是为实现某种目标(而不是为了分析)而进行组织的。例如,数据被设计成便于数据录入而不是便于分析,这是很常见的。
大多数人并不熟悉整洁数据的原则,而且除非你花很多时间亲自处理数据,否则很难自己推导出这些原则。
这意味着,大多数真实分析都至少需要做一点整理(tidying)。你将从弄清楚底层的变量和观测值是什么开始。有时这很容易;但有时你需要去查阅最初生成这些数据的人。接下来,你会把数据转换成整洁的形式:把变量放在列中,把观测值放在行中。
tidyr 提供了两个用于数据透视(pivot)的函数:pivot_longer() 和 pivot_wider()。我们会先从最常见的情况 pivot_longer() 开始。下面我们进入一些例子。
列名中的数据
广告牌数据集记录了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>, …在该数据集中,每个观测值都是一首歌曲。前三列(artist,track 和 date.entered)是描述这首歌曲的变量。接着我们有 76 列(wk1-wk76),用于描述该歌曲在每周的排名。这里,列名是一个变量(周次),单元格的值是另一个变量(排名)。
为了整理这份数据,我们将使用 pivot_longer():
billboard |> pivot_longer( cols = starts_with("wk"), # 选择所有列名以“wk”开头的列(例如 wk1, wk2, ... wk76) names_to = "week", # 把这些列的列名(wk1, wk2, ...)整理到新变量 week 中 values_to = "rank" # 把这些列对应的单元格值(排名数字)整理到新变量 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 指定需要进行 pivot 的列,也就是哪些列不是变量。该参数使用与
select()相同的语法,因此这里我们可以使用!c(artist, track, date.entered)或starts_with("wk")。 - names_to 指定将存储在列名中的变量,我们把该变量命名为 week。
- values_to 指定将存储在单元格值中的变量,我们把该变量命名为 rank。
注意:在代码中之所以给 "week" 和 "rank" 加上引号,是因为它们是我们要新创建的变量;在运行 pivot_longer() 调用时,这些变量在原始数据中还不存在。
现在让我们把注意力转向整理后的、更长的数据框。如果一首歌进入了前 100 名的时间少于 76 周,会发生什么?例如,2 Pac 的 “Baby Don’t Cry”。上面的输出表明它只在前 100 名中出现了 7 周,而其余周都会填入缺失值。这些 NA 并不真正表示未知的观测值;它们是由数据集的结构“被迫”存在的,所以我们可以通过设置 values_drop_na = TRUE 让 pivot_longer() 把它们去掉:
billboard |> pivot_longer( cols = starts_with("wk"), # 选择所有列名以“wk”开头的列(即 wk1-wk76),把它们从宽表转换为长表 names_to = "week", # 将这些列名(wk1、wk2、...)存到新变量 week 中 values_to = "rank", # 将每个单元格的值(排名数字)存到新变量 rank 中 values_drop_na = TRUE # 删除生成结果中的缺失值(NA);避免为不足 76 周的歌曲补出的无效观测 )#> # 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#> # ℹ 5,301 more rows行数现在大幅减少,说明丢掉了很多包含 NA 的行。
你可能还会好奇:如果一首歌在前 100 名中超过了 76 周会怎样?仅凭这份数据我们无法得知,但你可能会猜想,数据集中会新增更多列,如 wk77、wk78、……。
这份数据现在已经整理得很整齐了,不过我们可以通过使用 mutate() 和 readr::parse_number(),把将 week 的值从字符字符串转换为数字,从而让后续计算更容易一些。parse_number() 是一个很方便的函数:它会从字符串中提取第一个数字,并忽略字符串中的其他所有文本。
library(readr)billboard_longer <- billboard |> pivot_longer( cols = starts_with("wk"), # 选择所有以“wk”开头的列(wk1-wk76)进行透视展开 names_to = "week", # 把原列名(wk1、wk2、...)存入新变量 week values_to = "rank", # 把原单元格值(排名)存入新变量 rank values_drop_na = TRUE # 删除由“未覆盖到所有周”产生的 NA,避免无效观测 ) |> mutate( week = parse_number(week) # 将 week 由字符串(例如 "wk1")转换为数字(例如 1) )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#> # ℹ 5,301 more rows现在我们已经把所有的周次编号放在同一个变量里、把所有的排名数值放在另一个变量里了,因此我们很适合用可视化来展示歌曲排名如何随时间变化。下面给出了代码,运行结果如图 5.2 所示。我们可以看到,很少有歌曲会在前 100 名中停留超过 20 周。
billboard_longer |> ggplot(aes(x = week, y = rank, group = track)) + # 指定绘图数据与美学映射:横轴 week,纵轴 rank;按 track 分组以便连线 geom_line(alpha = 0.25) + # 用折线图连接同一首歌(同一 track)在不同周的排名;alpha=0.25 设置透明度,便于观察重叠 scale_y_reverse() # 反转 y 轴方向:排名 1(最好)显示在最上方
透视(pivot)是如何工作的?
现在你已经看到了我们如何使用透视(pivoting)来重塑数据,让我们花一点时间来建立直观理解:pivoting 到底会如何改变数据。我们先从一个非常简单的数据集开始,这样更容易看清发生了什么。假设我们有三位患者,id 分别是 A、B 和 C,并且每位患者都有两次血压测量。我们将使用 tribble() 来创建这些数据——tribble() 是一个很方便的函数,可以手动构造小的 tibble:
df <- tribble( ~id, ~bp1, ~bp2, "A", 100, 120, "B", 140, 115, "C", 120, 125)我们希望新的数据集包含三个变量:id(已经存在)、measurement(列名)以及 value(单元格里的值)。为实现这一点,我们需要把 df 透视为更长的格式(pivot df longer):
df |> pivot_longer( cols = bp1:bp2, # 选择要透视展开的列范围(bp1 到 bp2),这些列名会被转成新的变量 measurement names_to = "measurement",# 将被透视的列名(如 bp1、bp2)放入新列 measurement values_to = "value" # 将对应单元格的数值放入新列 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重塑(reshaping)是如何工作的?如果我们按列来想,会更容易理解。正如图 5.3 所示:在原始数据集中已经是变量的某一列(id)里的值,需要为每一个被透视展开(pivoted)的列各重复一次。

列名会变成一个新变量中的取值,而该新变量的名称由 names_to 定义,如图 5.4 所示。它们需要针对原始数据集中的每一行重复一次。

单元格里的值也会变成新变量中的值,其名称由 values_to 指定。它们会按行逐行“解开”(unwound)。图 5.5 展示了这个过程。

列名中有很多变量
当列名中塞进了多条信息,而且你希望把这些信息分别存到若干个新的变量中时,就会遇到更具挑战性的情况。比如,来看一下 who2 数据集——也就是你在上面看到的 table1 及其相关内容的来源:
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>, …该数据集由世界卫生组织收集,记录了关于结核病(tuberculosis)诊断的信息。已有两列变量,且很容易理解:country 和 year。它们之后还有 56 列,例如 sp_m_014、ep_m_4554 和 rel_m_3544。只要盯着这些列名看够久,你就会发现其中存在规律。每个列名由三部分组成,它们之间用 _ 分隔:第一部分 sp/rel/ep 描述用于诊断的方法;第二部分 m/f 是性别(在该数据集中以二元变量编码);第三部分 014/1524/2534/3544/4554/5564/65 表示年龄范围(例如 014 代表 0-14)。
所以在这个例子中,who2 里记录了六条信息:国家和年份(已经是列);诊断方法、性别类别和年龄段类别(包含在其他列名中);以及该类别下患者的数量(单元格的值)。为了把这六条信息组织到六个独立的列中,我们使用 pivot_longer():对 names_to 使用一个列名向量,并对 instructors 使用用于拆分原始变量名为若干片段的设置,同时还需要一个用于指定 values_to 的列名:
who2 |> pivot_longer( cols = !(country:year), # 选择除 country 和 year 之外的所有列作为要被“转成长表”的列 names_to = c("diagnosis", "gender", "age"), # 将列名按分隔符拆成三部分,分别存入这三个新变量 names_sep = "_", # 三部分之间用 "_" 分隔(对应列名格式:diagnosis_gender_age) values_to = "count" # 被透视后的数值放到新列 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从概念上来说,这只是对你之前已经见过的更简单情况的一种小幅变化。图 5.6 展示了这个基本思路:现在,不再是把列名透视(pivot)到一个单独的列中,而是透视到多个列中。你可以把这个过程想象为两步完成(先透视,再分离),但在底层它会在一步内完成,因为这样更快。

数据与变量名出现在列标题中
复杂度更进一步时,列名中会同时包含变量值和变量名。例如,来看家庭数据集:
library(tidyr)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” 哨兵;这并不是某个变量的名称,而是一个独特的值,用来告诉 pivot_longer() 采取不同的处理方式。它会覆盖通常的 values_to 参数,把透视后列名的第一个组成部分作为输出中的变量名。
household |> # 以 household 数据框为起点,接着把结果传给 pivot_longer() pivot_longer( # 将数据从宽格式转换为长格式 cols = !family, # 除了 family 列以外,其余列都作为要整理的列 names_to = c(".value", "child"), # 将列名拆成两部分:前半部分作为输出列名,后半部分存入 child names_sep = "_", # 按下划线 "_" 拆分列名 values_drop_na = TRUE # 删除转换后产生的缺失值行 ) # 结束 pivot_longer() 调用#> # 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 rows我们再次使用 values_drop_na = TRUE,因为输入数据的形状会强制创建显式的缺失变量(例如,对于只有一个孩子的家庭)。
图 5.7 用一个更简单的例子说明了这一基本思路。当你在 names_to 中使用 “.value” 时,输入中的列名会同时参与输出中的值和变量名。

数据变宽
到目前为止,我们一直使用 pivot_longer() 来解决一种常见问题:值被放到了列名中。接下来我们转向 pivot_wider(),它通过增加列数、减少行数来让数据集变宽,并且在一条观察被分散在多行中时很有帮助。这种情况在实际中似乎没那么常见,但在处理政府数据时似乎经常出现。
我们先来看 cms_patient_experience,这是来自医疗保险和医疗补助服务中心(Centers of Medicare and Medicaid Services)的一个数据集,用于收集患者体验方面的数据:
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研究的核心单位是一个组织,但每个组织分布在六行中,调查中对组织所做的每一次测量各占一行。我们可以通过使用 distinct() 来查看 measure_cd 和 measure_title 的完整取值集合:
cms_patient_experience |> # 以 cms_patient_experience 数据框为起点 distinct(measure_cd, measure_title) # 提取 measure_cd 和 measure_title 的唯一组合#> # A tibble: 6 × 2#> measure_cd measure_title #> <chr> <chr> #> 1 CAHPS_GRP_1 CAHPS for MIPS SSM: Getting Timely Care, Appointments, and In…#> 2 CAHPS_GRP_2 CAHPS for MIPS SSM: How Well Providers Communicate #> 3 CAHPS_GRP_3 CAHPS for MIPS SSM: Patient's Rating of Provider #> 4 CAHPS_GRP_5 CAHPS for MIPS SSM: Health Promotion and Education #> 5 CAHPS_GRP_8 CAHPS for MIPS SSM: Courteous and Helpful Office Staff #> 6 CAHPS_GRP_12 CAHPS for MIPS SSM: Stewardship of Patient Resources这两个列名都不太适合作为变量名:measure_cd 不能说明变量含义,而 measure_title 是一个包含空格的长句子。现在我们先用 measure_cd 作为新列名的来源,但在真正的分析中,你可能会想自己创建既简短又有意义的变量名。
pivot_wider() 的接口与 pivot_longer() 正好相反:我们不是选择新的列名,而是需要提供用于定义值的现有列(values_from)以及用于定义列名的列(names_from):
cms_patient_experience |> # 以 cms_patient_experience 数据框为起点 pivot_wider( # 将数据从长格式转换为宽格式 names_from = measure_cd, # 使用 measure_cd 的值作为新列名 values_from = prf_rate # 使用 prf_rate 的值填充新生成的列 ) # 结束 pivot_wider() 调用#> # A tibble: 500 × 9#> org_pac_id org_nm measure_title CAHPS_GRP_1 CAHPS_GRP_2#> <chr> <chr> <chr> <dbl> <dbl>#> 1 0446157747 USC CARE MEDICAL GROUP … CAHPS for MIPS… 63 NA#> 2 0446157747 USC CARE MEDICAL GROUP … CAHPS for MIPS… NA 87#> 3 0446157747 USC CARE MEDICAL GROUP … CAHPS for MIPS… NA NA#> 4 0446157747 USC CARE MEDICAL GROUP … CAHPS for MIPS… NA NA#> 5 0446157747 USC CARE MEDICAL GROUP … CAHPS for MIPS… NA NA#> 6 0446157747 USC CARE MEDICAL GROUP … CAHPS for MIPS… NA NA#> # ℹ 494 more rows#> # ℹ 4 more variables: CAHPS_GRP_3 <dbl>, CAHPS_GRP_5 <dbl>, …输出结果看起来不太对;我们似乎仍然为每个组织保留了多行。这是因为,我们还需要告诉 pivot_wider() 哪一列或哪些列的值能够唯一标识每一行;在这个例子中,这些列是以 “org” 开头的变量:
cms_patient_experience |> # 以 cms_patient_experience 数据框为起点 pivot_wider( # 将数据从长格式转换为宽格式 id_cols = starts_with("org"), # 使用以 "org" 开头的列作为标识列 names_from = measure_cd, # 使用 measure_cd 的值作为新列名 values_from = prf_rate # 使用 prf_rate 的值填充新生成的列 ) # 结束 pivot_wider() 调用#> # 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>这就得到了我们想要的输出。
pivot_wider() 是如何工作的?
为了理解 pivot_wider() 的工作方式,我们再次从一个非常简单的数据集开始。这次我们有两位患者,ID 分别为 A 和 B,患者 A 有三次血压测量,患者 B 有两次:
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要开始这个过程,pivot_wider() 需要先确定哪些内容放在行里、哪些内容放在列里。新的列名将取自 measurement 的唯一值。
df |> distinct(measurement) |> pull()#> [1] "bp1" "bp2" "bp3"默认情况下,输出中的行由所有不用于生成新名称或新值的变量决定。这些变量称为 id_cols。这里只有一列,但一般来说可以有任意数量。
df |> select(!measurement & !value) |> distinct()#> # A tibble: 2 × 1#> id #> <chr>#> 1 A #> 2 Bpivot_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然后,它会使用输入中的数据填充所有缺失的值。在这种情况下,输出中的每个单元格都不一定在输入中有对应值,因为患者 B 没有第三次血压测量,所以那个单元格仍然是缺失的。
你可能还会想,如果输入中有多行对应输出中的一个单元格,会发生什么。下面的例子中有两行对应于 id “A” 和 measurement “bp1”:
df <- tribble( ~id, ~measurement, ~value, "A", "bp1", 100, "A", "bp1", 102, # 这行重复了 "A", "bp2", 120, "B", "bp1", 140, "B", "bp2", 115)如果我们尝试对此进行透视,就会得到一个包含列表列的输出
df2 <- df |> pivot_wider( names_from = measurement, values_from = value )
由于你还不知道如何处理这类数据,你需要按照警告中的提示来找出问题所在:
# 按 id 和 measurement 分组,统计每组的行数df |> group_by(id, measurement) |> summarize(n = n(), .groups = "drop") |> # 只保留重复出现的组合(n > 1) filter(n > 1)#> # A tibble: 1 × 3#> id measurement n#> <chr> <chr> <int>#> 1 A bp1 2接下来就要由你来弄清楚数据出了什么问题,并修复底层损坏,或者运用分组和汇总技巧,确保每一种行值和列值的组合都只有一行。
总结
在这一章中,你学习了整洁数据:即变量放在列中、观测放在行中的数据。整洁数据能让你更轻松地工作,因为它是一种大多数函数都能理解的一致结构;而主要的挑战,是把你接收到的任何结构的数据转换成整洁格式。为此,你学习了 pivot_longer() 和 pivot_wider(),它们可以帮助你整理许多不整洁的数据集。我们这里展示的示例,是 vignette("pivot", package = "tidyr") 中示例的一部分,所以如果你遇到本章无法帮助你解决的问题,那篇 vignette 是下一个很好的尝试地点。
另一个挑战是,对于某个数据集来说,长格式或宽格式版本有时都无法被明确称为“整洁”的那个。这在一定程度上反映了我们对整洁数据的定义:我们说整洁数据是每一列对应一个变量,但我们实际上并没有定义什么是变量(而且这件事出乎意料地难以界定)。实用一点地说,把变量定义为“最有利于你的分析的东西”完全没问题。所以如果你在弄清楚如何进行某个计算时遇到困难,不妨尝试重新组织数据;不要害怕在需要时先弄得不整洁、再转换、然后再整理回整洁!
既然你已经开始编写大量的 R 代码,现在是时候进一步学习如何把代码组织到文件和目录中了。
发表评论