5  数据整洁化

5.1 引言

数据可视化和建模都要求数据处于“整洁”格式(tidy format)。虽然无法控制获取数据的原始结构,但可通过数据整理将其转换为整洁结构。

本章展示:

  • 整洁数据的定义与原则;
  • 如何使用 tidyr 包中的函数进行数据整洁化;
  • 如何识别和处理缺失值。

tidyrtidyverse 的核心之一,故我们须先载入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用于命名新产生的列,此处将该列命名为week

  • values_to用于命名存储在原各个列下的变量分离后产生的新列,此处将该列命名为 rank

需注意,weekrank都需要加上引号,因为这是正在创建的新变量。

初步处理完毕,不知道你有没有发现一个问题:如果原表的某个歌曲还未到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

下列组图形象地展现了这一转置过程:

  1. 已经是变量的列需要重复,每个转置列重复一次。

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

单元格值也会成为新变量中的值,其名称由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 rows

5.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 没有第三次血压测量,因此该单元格仍然缺失。