6.2 Pivoting

细看两表,不难发现它们实质上相同的数据(相对于第二张表,第一张是以 id 为行字段,key 为列字段,val 为值的数据透视表)。第一种称为宽数据 (wide data,Cartesian data,笛卡尔型数据),需要看行和列的交叉点来找到对应的值。而第二种形式称为长数据(long data,indexed data,指标型数据),在长数据(指标型)数据汇总,你需要看指标来找到需要变量的数值(变量x,y,z的值)。。很难简单地说哪一种格式更优,因为两种形式都有可能是整洁的,这取决于值“A”、“B”、“C”、“D”的含义。

数据整理常需要化宽为长,但偶尔也需要化长为宽, tidyr 分别提供了cpivot_longer()pivot_wider() 来实现以上两种形式的转换操作(统称为 pivoting)。在 tidyr 1.0.0 及更早的版本中,gather()spread() 分别承担相同的工作,实际效果与 pivot_ 函数一样,但后者有更易理解的命名和 api。

names_tovalues_to 参数相当于原来 gather() 中的 keyvalue,其中 “键” 列的默认名称变为 “name”
同理, names_fromvalues_from 相当于原来 spread() 中的 keyvalue

6.2.1 pivot_longer()

tidyr::relig_income 是一个典型的宽数据,除第一列以外的所有列表示收入的不同水平,值表示对应的人数:

另一个例子:美国劳工市场的月度数据,首先创建一个 messy data:

化宽为长:

以上就是 pviot_longer 的基本用法,下面来处理一些更复杂的情况。

6.2.1.1 Numeric data in column names

pivot_longer() 提供了 names_ptypevalues_ptypes 调整数据集变长后键列和值列的数据类型。看一下 billboard 数据集:

显然,我们希望将所有以 "wk"开头的列聚合以得到整洁数据,键列和值列分别命名为 “week” 和 “rank”。另外要考虑的一点是,我们很可能之后想计算歌曲保持在榜单上的周数,故需要将 “week” 列转换为数值类型:

names_prefix 去除前缀 “wk”(否则无法从字符串转换为数值),names_ptype 以列表的形式转换键列的数据类型。同理, values_ptype 可以转换值列的数据类型。

6.2.1.2 Many variables in column names

在 tidyr 1.0.0 之前,当进行一定处理后发现多个变量被糅合到一列中时,可能会考虑使用 separate() 或者 extract():

who
#> # A tibble: 7,240 x 60
#>   country iso2  iso3   year new_sp_m014 new_sp_m1524 new_sp_m2534 new_sp_m3544
#>   <chr>   <chr> <chr> <int>       <int>        <int>        <int>        <int>
#> 1 Afghan~ AF    AFG    1980          NA           NA           NA           NA
#> 2 Afghan~ AF    AFG    1981          NA           NA           NA           NA
#> 3 Afghan~ AF    AFG    1982          NA           NA           NA           NA
#> 4 Afghan~ AF    AFG    1983          NA           NA           NA           NA
#> 5 Afghan~ AF    AFG    1984          NA           NA           NA           NA
#> 6 Afghan~ AF    AFG    1985          NA           NA           NA           NA
#> # ... with 7,234 more rows, and 52 more variables: new_sp_m4554 <int>,
#> #   new_sp_m5564 <int>, new_sp_m65 <int>, new_sp_f014 <int>,
#> #   new_sp_f1524 <int>, new_sp_f2534 <int>, new_sp_f3544 <int>,
#> #   new_sp_f4554 <int>, new_sp_f5564 <int>, new_sp_f65 <int>,
#> #   new_sn_m014 <int>, new_sn_m1524 <int>, new_sn_m2534 <int>,
#> #   new_sn_m3544 <int>, new_sn_m4554 <int>, new_sn_m5564 <int>,
#> #   new_sn_m65 <int>, new_sn_f014 <int>, new_sn_f1524 <int>,
#> #   new_sn_f2534 <int>, new_sn_f3544 <int>, new_sn_f4554 <int>,
#> #   new_sn_f5564 <int>, new_sn_f65 <int>, new_ep_m014 <int>,
#> #   new_ep_m1524 <int>, new_ep_m2534 <int>, new_ep_m3544 <int>,
#> #   new_ep_m4554 <int>, new_ep_m5564 <int>, new_ep_m65 <int>,
#> #   new_ep_f014 <int>, new_ep_f1524 <int>, new_ep_f2534 <int>,
#> #   new_ep_f3544 <int>, new_ep_f4554 <int>, new_ep_f5564 <int>,
#> #   new_ep_f65 <int>, newrel_m014 <int>, newrel_m1524 <int>,
#> #   newrel_m2534 <int>, newrel_m3544 <int>, newrel_m4554 <int>,
#> #   newrel_m5564 <int>, newrel_m65 <int>, newrel_f014 <int>,
#> #   newrel_f1524 <int>, newrel_f2534 <int>, newrel_f3544 <int>,
#> #   newrel_f4554 <int>, newrel_f5564 <int>, newrel_f65 <int>
## 原书 tidyr 一章中使用的方法
who %>% 
  gather(starts_with("new"), 
         key = key, 
         value = value,
         na.rm = T) %>% 
  extract(key,
          into = c("diagnosis", "gender", "age"),
          regex = "new_?(.*)_(.)(.*)")
#> gather: reorganized (new_sp_m014, new_sp_m1524, new_sp_m2534, new_sp_m3544, new_sp_m4554, …) into (key, value) [was 7240x60, now 76046x6]
#> # A tibble: 76,046 x 8
#>   country     iso2  iso3   year diagnosis gender age   value
#>   <chr>       <chr> <chr> <int> <chr>     <chr>  <chr> <int>
#> 1 Afghanistan AF    AFG    1997 sp        m      014       0
#> 2 Afghanistan AF    AFG    1998 sp        m      014      30
#> 3 Afghanistan AF    AFG    1999 sp        m      014       8
#> 4 Afghanistan AF    AFG    2000 sp        m      014      52
#> 5 Afghanistan AF    AFG    2001 sp        m      014     129
#> 6 Afghanistan AF    AFG    2002 sp        m      014      90
#> # ... with 76,040 more rows

现在,pivot_longer() 现在可以在化宽为长的下一步直接完成拆分任务,可以直接在 names_to 中传入一个向量表示分裂后的各个键列,并在 names_sep(分隔符) 或者 names_pattern 中(正则表达式)指定分裂的模式:

更进一步,顺便设定好整理后 genderage 的类型:

6.2.1.3 Multiple observations per row

(多个值列)

So far, we have been working with data frames that have one observation per row, but many important pivotting problems involve multiple observations per row. You can usually recognise this case because name of the column that you want to appear in the output is part of the column name in the input. In this section, you’ll learn how to pivot this sort of data.

理想中的数据格式(两个值列)

family child dob gender
1 1 1998-11-26 1
1 2 2000-01-29 2
2 1 1996-06-22 2
3 1 2002-07-11 2
3 2 2004-04-05 2
4 1 2004-10-10 1
4 2 2009-08-27 1
5 1 2000-12-05 2
5 2 2005-02-28 1

Note that we have two pieces of information (or values) for each child: their gender and their dob (date of birth). These need to go into separate columns in the result. Again we supply multiple variables to names_to, using names_sep to split up each variable name. Note the special name .value: this tells pivot_longer() that that part of the column name specifies the “value” being measured (which will become a variable in the output)

.value 在这里指代 dobgender 两个值列

在这里,dob_child1dob_child2gender_child1gender_child2四个列名的后半部分被当做键列的值。例如,可以认为对于 family == 1的观测,首先生成了如下的结构:

family child dob dob gender gender
1 child1 1998-11-16 2000-01-29 1 2
2 child2

而后名称相同的值列合并:

family child dob gender
1 child1 1998-11-26 1
1 child2 2000-01-29 2

另一个例子:

叕一个例子:

6.2.2 pivot_wider()

pivot_wider()pivot_longer() 的逆操作,虽然在获得 tidy data 上,前者没有后者常用,但它经常被用来创建一些 summary table。

fish_encounters 数据记录了一些沿河观测站对一批鱼群的观测情况(seen 为发现次数):

很多后续分析工具需要每个观测站的观测情况单独成一列,使用 pivot_wider():

关于 pivot_wider(),很重要的一点是它会暴露出数据中的隐式缺失值(implicit missing value)。这些没有出现在原数据中的 NA 值不是源自于记录错误或者遗失,只是没有对应的观测而已(观测站只能记录发生了的观测)。参数 values_fill 可以以一个列表填充 pivot_wider() 结果中的 NA, 当然如何处理这些隐式缺失值要按具体情境而定,在鱼群的例子里,用 0 填充是合适的:

fish_encoutners 的贡献者 Myfanwy Johnston 在个人网站上有一篇相关的文章

6.2.2.1 Aggregation

pivot_wider() 可以用来执行一些简单的聚合操作。warpbreaks 是一个关于经纱强度的控制试验,每个处理 (wool, tension) 上进行了 9 次试验:

现在想知道每个处理下的平均断头次数,只需展开 wooltension 中的任意一个:

由于 (wool, tension) 不能唯一确认一个观测,多个观测被压缩至一个列表中,values_fn(breaks = mean) 求得平均值:

For more complex summary operations, I recommend summarising before reshaping, but for simple cases it’s often convenient to summarise within pivot_wider()

6.2.2.4 When there is no identifying variable

A final challenge is inspired by Jiena Gu. Imagine you have a contact list that you’ve copied and pasted from a website:

This is challenging because there’s no variable that identifies which observations belong together.

直接化宽时,出现列表列(没有第三个标识变量)

We can fix this by noting that every contact starts with a name, so we can create a unique id by counting every time we see “name” as the field:

6.2.3 Combining pivot_longer() and pivot_wider()

Some problems can’t be solved by pivotting in a single direction. The examples in this section show how you might combine pivot_longer() and pivot_wider() to solve more complex problems.

6.2.3.1 world bank data

world_bank_pop contains data from the World Bank about population per country from 2000 to 2018.

It’s not obvious exactly what steps are needed yet, but I’ll start with the most obvious problem: year is spread across multiple columns.

Next we need to consider the indicator variable:

Here SP.POP.GROW is population growth, SP.POP.TOTL is total population, and SP.URB.* are the same but only for urban areas. Let’s split this up into two variables: area (total or urban) and the actual variable (population or growth):

Now we can complete the tidying by pivoting variable and value to make TOTL and GROW columns:

6.2.3.2 mutli choice data

Based on a suggestion by Maxime Wack, https://github.com/tidyverse/tidyr/issues/384), the final example shows how to deal with a common way of recording multiple choice data. Often you will get such data as follows:

But the actual order isn’t important, and you’d prefer to have the individual questions in the columns. You can achieve the desired transformation in two steps. First, you make the data longer, eliminating the explcit NAs, and adding a column to indicate that this choice was chosen:

Then you make the data wider, filling in the missing observations with FALSE; note the use of id_cols = id here, this eliminated the name column and combines mutilples rows per person into one row, since we don’t need name in identifying an observation:

6.2.4 Exercises

Exercise 6.2 在下面的例子中,研究为什么 pivot_longer()pivot_wider() 不是完美对称的

先后使用 pivot_wider()pivot_longer()无法得到一个相同的数据集(除了列的顺序)是因为,数据整理有时会丢失列的类型信息。当 pivot_wider() 将变量 year 的值 20152016 用作列的名字时,它们自然被转化为了字符串"2015""2016";随后 pivot_longer() 把列名用作键列year的值,从而year自然变成了一个字符向量,可以用 names_ptypes 避免这一点

Exercise 6.3 为什么下面的数据框不能应用 pivot_wider()?可以添加一列解决这个问题吗?

这个例子和 6.2.2.4 中的 contact 很类似,虽然这里有第三列 name,但仍不足以唯一标识任意观测

因为这个数据集里有两个对于 “Phillip Woods” 在 age 上年龄的观测,pivot_wider() 就要把由(Phillips Woods, age)确定的单元格里“塞进两个值”。本质上因为 namekey 这两个变量上的值不能唯一确定一行,所以我们只要添加一列,让namekey和新列可以唯一确定一行即可: