3  整洁数据

在数据分析过程中,数据清理和整理是非常重要的步骤。整洁数据(Tidy Data)是一种数据组织方式,使得数据更易于分析和可视化。在本章中,我们将介绍如何使用tidyr包来整理数据,使其符合整洁数据的原则。

3.1 整洁数据的原则

  • 变量:是可以测量的数量、性质或属性。
  • :是一个变量在被测量时的状态。多次测量变量时其值可能会发生变化。
  • 观测:也称为数据点,在相似条件下进行的一组测量。一个观测包括了多个值,每个值与不同的变量关联。
图 3.1: 整洁数据的基本原则

图 3.1 , 整洁数据的基本原则包括:

  1. 每个变量形成一列,每一列代表一个变量。例如,温度、湿度等不同的测量指标应分别存储在不同的列中。
  2. 每个观测形成一行,每一行代表一个观测。例如,每个时间点或每个实验条件下的测量结果应存储在不同的行中。
  3. 每个值占一个单元格,每个单元格代表一个值。
  4. 每个类型的观测单位形成一个表格。

我们看3个例子来理解这些原则。

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符合整洁数据的原则。

  • table2table3则不符合。table2中的casespopulation变量被存储在同一列type中,而table3中将casespopulation变量的计算结果储存在rate列中。

3.2 Lengthening data- 长格式化数据

遗憾的是,为了阅读方便,大多数时候我们并不能直接获得整洁的数据。我们需要对数据进行整理,使其符合整洁数据的原则。

tidyr 提供了两个用于数据转置的函数: pivot_longer()pivot_wider() 。要进行这两个函数的操作,我们首选需要弄清楚哪些是变量,哪些是观测,这有时看起来会很复杂;然后将数据变换为整洁模式,变量位于列中,观测位于行中。

我们先从pivot_longer()函数开始,它用于将数据从宽格式转换为长格式。它将多个列合并为两列:一列用于存储变量名称,另一列用于存储变量值。

3.2.1 列名中包含单个变量

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>, …

billboard数据集包含了不同歌曲在不同时间点的排名信息。

  • 前三列artist, trackdate.entered是描述歌曲信息的变量。
  • wk1wk76列表示歌曲在不同周的排名。这些列的列名实际应该是一个变量week的不同取值,而列中的值则是另一个变量rank的取值。

理解了这些变量后,我们可以使用pivot_longer()函数将数据转换为tidy格式。

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” 和 “rank”,都用引号括起来了,这是因为我们在调用pivot_longer()函数时,“week” 和 “rank”在数据集中还不存在,是要新建的变量。

How does pivoting work 中有关于pivot_longer()函数工作原理的更详细解释。

3.2.2 列名中包含多个变量

who2
# A tibble: 7,240 × 58
   country      year sp_m_014 sp_m_1524 sp_m_2534 sp_m_3544 sp_m_4554 sp_m_5564
   <chr>       <dbl>    <dbl>     <dbl>     <dbl>     <dbl>     <dbl>     <dbl>
 1 Afghanistan  1980       NA        NA        NA        NA        NA        NA
 2 Afghanistan  1981       NA        NA        NA        NA        NA        NA
 3 Afghanistan  1982       NA        NA        NA        NA        NA        NA
 4 Afghanistan  1983       NA        NA        NA        NA        NA        NA
 5 Afghanistan  1984       NA        NA        NA        NA        NA        NA
 6 Afghanistan  1985       NA        NA        NA        NA        NA        NA
 7 Afghanistan  1986       NA        NA        NA        NA        NA        NA
 8 Afghanistan  1987       NA        NA        NA        NA        NA        NA
 9 Afghanistan  1988       NA        NA        NA        NA        NA        NA
10 Afghanistan  1989       NA        NA        NA        NA        NA        NA
# ℹ 7,230 more rows
# ℹ 50 more variables: sp_m_65 <dbl>, sp_f_014 <dbl>, sp_f_1524 <dbl>,
#   sp_f_2534 <dbl>, sp_f_3544 <dbl>, sp_f_4554 <dbl>, sp_f_5564 <dbl>,
#   sp_f_65 <dbl>, sn_m_014 <dbl>, sn_m_1524 <dbl>, sn_m_2534 <dbl>,
#   sn_m_3544 <dbl>, sn_m_4554 <dbl>, sn_m_5564 <dbl>, sn_m_65 <dbl>,
#   sn_f_014 <dbl>, sn_f_1524 <dbl>, sn_f_2534 <dbl>, sn_f_3544 <dbl>,
#   sn_f_4554 <dbl>, sn_f_5564 <dbl>, sn_f_65 <dbl>, ep_m_014 <dbl>, …

有时,列名中可能包含多个变量的信息。例如,who2数据集中:

  • 前2列country, year已经是描述观察的变量。
  • 其余的56列,例如sp_m_014ep_m_4554rel_m_3544 ,它们均由3个部分组成,分别表示诊断疾病所用的方式(sp, ep, rel),性别(m, f)和年龄组(014, 4554, 3544),这些列名实际上包含了3个变量的信息,并用_连接符分割

理解了这些变量后,我们发现who2数据集其实应该包含6个变量,分别为:

  • 已经存在的countryyear
  • 需要从列名中提取的diagnosissexage,以及
  • 这些列别中患者的数量count
who2 |>
  pivot_longer(
    cols = !(c(country, year)), # 选择要转换的列
    names_to = c("diagnosis", "sex", "age"), # 新列用于存储变量名称
    names_sep = "_", # 列名中的变量是用"_"分割的
    values_to = "count", # 新列用于存储变量值
    # values_drop_na = TRUE # 同样可以删除缺失值
  )
# A tibble: 405,440 × 6
   country      year diagnosis sex   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

3.2.3 列名中包含变量和数据

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     

有时,列名中可能同时包含变量和数据的信息。例如household数据集中:

  • family列已经是描述观察的变量。

  • 后四列中,列名包含了两个变量(dobname),以及另一个变量的值(child,值为1或2)。

  • 理解了这些变量后,我们发现household数据集其实应该包含4个变量,分别为:

    • 已经存在的family
    • 需要从列名中提取的dobname,以及
    • 这些列别中孩子的编号child
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 
7      4 child2 2009-08-27 Khai  
8      5 child1 2000-12-05 Parker
9      5 child2 2005-02-28 Gracie

以上代码在names_to参数中使用.value来指示哪些新列是变量名称,哪些是变量值。.value是一个特殊的占位符,它告诉pivot_longer()函数将.value所在位置的组件作为输出中变量名,而对应的值则作为输出中的值。简单的说,.value表示输入的列名中既包含了变量名称,也包含了变量值。

这块比较难以理解,我们深入看看:

图 3.2: pivot_long()中的.value

图 3.2 所示:使用 names_to = c(".value", "num") 拆分列名成两部分:第一部分确定输出列名( xy ),第二部分确定 num 列的值。在以上代码中,dobname 就对应 xy, child_1child_2就对应12

3.3 Widening data-宽格式化数据

在开始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数据集包含了医疗保险和医疗补助服务中心在不同年份的患者满意度评分信息。

数据集中,每个组织(org)的数据分散在多行中,每一行对应某一机构的其中一项测量结果,但存在几个问题:

  1. measure_cd列包含了不同的测量指标代码,这些代码实际上应该是变量measure的不同取值。
  2. measure_title列包含了测量指标的描述信息,是一个包含了空格的长句子,这样的列名不利于数据分析。

实际展示时,我们希望每个测量指标都有自己的列,这样每个组织的数据就可以集中在一行中,即measure_cd列需要根据其不同的值进行扩展,即宽格式化。

更通俗的讲,我们希望在不改变以org开头的列的同时,从measure_cd列获取扩展后的列名,从prf_rate列中获取对应的值
cms_patient_experience |>
  pivot_wider(
    id_cols = starts_with("org"), # 指定不进行转换的列
    names_from = measure_cd, # 新列的列名来自measure_cd列
    values_from = prf_rate # 新列的值来自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>

pivot_wider()函数可能会产生原先数据集中不存在的缺失值,我们将在 (sce-missing-values?) 章节中介绍如何处理缺失值。

3.4 总结

在本章中,我们介绍了整洁数据的基本原则以及如何使用tidyr包中的pivot_longer()pivot_wider()函数来整理数据。通过这些工具,我们可以将数据转换为更适合分析和可视化的格式。理解和应用这些原则和工具,将有助于我们更高效地进行数据分析工作。

值得注意的是,对于某些特定的数据集,很难明确的界定”长格式”或”宽格式”哪种才是真正的”整洁数据”。这里Hadley的建议是只要有利于简化分析过程,任何数据组织形式都可以被视为变量。因此当你在计算过程中遇到瓶颈时,不妨尝试调整数据组织结构——大胆地进行非整洁化处理、转换格式,并根据需要重新整理数据!