26 数据整理

26.1 tidyverse系统

假设数据以tibble格式保存。 数据集如果用于统计与绘图, 需要满足一定的格式要求, (H. Wickham 2014)称之为整洁数据(tidy data), 基本要求是每行一个观测, 每列一个变量, 每个单元格恰好有一个数据值。 这些变量应该是真正的属性, 而不是同一属性在不同年、月等时间的值分别放到单独的列。

数据集经常需要选行子集、选列子集、排序、定义新变量、横向合并、长宽转换等操作, 而且经常会用若干个连续的操作分步处理, R的管道运算符|>和magrittr包的|>特别适用于这种分步处理。

dplyr包和tidyr包定义了一系列“动词”, 可以用比较自然的方式进行数据整理。 较复杂的分组操作还可以利用purrr包的map类函数。

为了使用这些功能,可以载入tidyverse包, 则magrittr包,readr包,dplyr包和tidyr包都会被自动载入:

library(tidyverse)

下面的例子中用如下的一个班的学生数据作为例子, 保存在如下data/class.csv文件中:

name,sex,age,height,weight
Alice,F,13,56.5,84
Becka,F,13,65.3,98
Gail,F,14,64.3,90
Karen,F,12,56.3,77
Kathy,F,12,59.8,84.5
Mary,F,15,66.5,112
Sandy,F,11,51.3,50.5
Sharon,F,15,62.5,112.5
Tammy,F,14,62.8,102.5
Alfred,M,14,69,112.5
Duke,M,14,63.5,102.5
Guido,M,15,67,133
James,M,12,57.3,83
Jeffrey,M,13,62.5,84
John,M,12,59,99.5
Philip,M,16,72,150
Robert,M,12,64.8,128
Thomas,M,11,57.5,85
William,M,15,66.5,112

读入为tibble:

d.class <- read_csv(
  "data/class.csv", 
  col_types=cols(
  .default = col_double(),
  name=col_character(),
  sex=col_factor(levels=c("M", "F"))
))

这个数据框有19个观测, 有如下5个变量:

  • name
  • sex
  • age
  • height
  • weight

另一个例子数据集是R的NHANES扩展包提供的NHANES, 这是一个规模更大的示例数据框, 可以看作是美国扣除住院病人以外的人群的一个随机样本, 有10000个观测,有76个变量, 主题是个人的健康与营养方面的信息。 仅作为教学使用而不足以作为严谨的科研用数据。 原始数据的情况详见http://www.cdc.gov/nchs/nhanes.htm。 载入NHANES数据框:

library(NHANES)
data(NHANES)
print(dim(NHANES))
## [1] 10000    76
print(names(NHANES))
##  [1] "ID"               "SurveyYr"         "Gender"           "Age"             
##  [5] "AgeDecade"        "AgeMonths"        "Race1"            "Race3"           
##  [9] "Education"        "MaritalStatus"    "HHIncome"         "HHIncomeMid"     
## [13] "Poverty"          "HomeRooms"        "HomeOwn"          "Work"            
## [17] "Weight"           "Length"           "HeadCirc"         "Height"          
## [21] "BMI"              "BMICatUnder20yrs" "BMI_WHO"          "Pulse"           
## [25] "BPSysAve"         "BPDiaAve"         "BPSys1"           "BPDia1"          
## [29] "BPSys2"           "BPDia2"           "BPSys3"           "BPDia3"          
## [33] "Testosterone"     "DirectChol"       "TotChol"          "UrineVol1"       
## [37] "UrineFlow1"       "UrineVol2"        "UrineFlow2"       "Diabetes"        
## [41] "DiabetesAge"      "HealthGen"        "DaysPhysHlthBad"  "DaysMentHlthBad" 
## [45] "LittleInterest"   "Depressed"        "nPregnancies"     "nBabies"         
## [49] "Age1stBaby"       "SleepHrsNight"    "SleepTrouble"     "PhysActive"      
## [53] "PhysActiveDays"   "TVHrsDay"         "CompHrsDay"       "TVHrsDayChild"   
## [57] "CompHrsDayChild"  "Alcohol12PlusYr"  "AlcoholDay"       "AlcoholYear"     
## [61] "SmokeNow"         "Smoke100"         "Smoke100n"        "SmokeAge"        
## [65] "Marijuana"        "AgeFirstMarij"    "RegularMarij"     "AgeRegMarij"     
## [69] "HardDrugs"        "SexEver"          "SexAge"           "SexNumPartnLife" 
## [73] "SexNumPartYear"   "SameSex"          "SexOrientation"   "PregnantNow"

变量ID是受试者编号, SurveyYr是调查年份, 同一受试者可能在多个调查年份中有数据。 变量中包括性别、年龄、种族、收入等人口学数据, 包括体重、身高、脉搏、血压等基本体检数据, 以及是否糖尿病、是否抑郁、是否怀孕、已生产子女数等更详细的健康数据, 运动习惯、饮酒、性生活等行为方面的数据。 这个教学用数据集最初的使用者是Cashmere高中的Michelle Dalrymple 和新西兰奥克兰大学的Chris Wild。

26.2filter()选择行子集

数据框的任何行子集仍为数据框,即使只有一行而且都是数值也是如此。 行子集可以用行下标选取, 如d.class[8:12,]。 函数head()取出数据框的前面若干行, tail()取出数据框的最后若干行。

dplyr包的filter()函数可以按条件选出符合条件的行组成的子集。 下例从d.class中选出年龄在13岁和13岁以下的女生:

d.class |>
  filter(sex=="F", age<=13) |>
  knitr::kable()
name sex age height weight
Alice F 13 56.5 84.0
Becka F 13 65.3 98.0
Karen F 12 56.3 77.0
Kathy F 12 59.8 84.5
Sandy F 11 51.3 50.5

filter()函数第一个参数是要选择的数据框, 后续的参数是条件, 这些条件是需要同时满足的, 另外, 条件中取缺失值的观测自动放弃, 这一点与直接在数据框的行下标中用逻辑下标有所不同, 逻辑下标中有缺失值会在结果中产生缺失值。

filter()会自动舍弃行名, 如果需要行名只能将其转换成数据框的一列。

filter()的结果为行子集数据框。 用在管道操作当中的时候第一自变量省略(是管道传递下来的)。

26.3 按行序号选择行子集

基本R的utils包的函数head(x, n)可以用来选择数据框x前面n行, tail(x, n)可以用来选择数据框x后面n行,如:

d.class |>
  head(n=5) |>
  knitr::kable()
name sex age height weight
Alice F 13 56.5 84.0
Becka F 13 65.3 98.0
Gail F 14 64.3 90.0
Karen F 12 56.3 77.0
Kathy F 12 59.8 84.5

dplyr包的函数slice(.data, ...)可以用来选择指定序号的行子集, 正的序号表示保留,负的序号表示排除。如:

d.class |>
  slice(3:5) |>
  knitr::kable()
name sex age height weight
Gail F 14 64.3 90.0
Karen F 12 56.3 77.0
Kathy F 12 59.8 84.5

26.4sample_n()对观测随机抽样

dplyr包的sample_n(tbl, size)函数可以从数据集tbl中随机无放回抽取size行,如:

d.class |>
  sample_n(size = 3) |>
  knitr::kable()
name sex age height weight
Sandy F 11 51.3 50.5
Thomas M 11 57.5 85.0
Tammy F 14 62.8 102.5

sample_n()中加选项replace=TRUE可以变成有放回抽样。 可以用weight选项指定数据框中的一列作为抽样权重, 进行不等概抽样。

26.5distinct()去除重复行

有时我们希望得到一个或若干个变量组合的所有不同值。 dplyr包的distinct()函数可以对数据框指定若干变量, 然后筛选出所有不同值, 每组不同值仅保留一行。 指定变量名时不是写成字符串形式而是直接写变量名, 这是dplyr和tidyr包的特点。 例如,筛选出性别与年龄的所有不同组合:

d.class |>
  distinct(sex, age) |>
  knitr::kable()
sex age
F 13
F 14
F 12
F 15
F 11
M 14
M 15
M 12
M 13
M 16
M 11

如果希望保留数据框中其它变量, 可以加选项.keep_all=TRUE

下面的程序查看NHANES数据框中ID与SurveyYr的组合的不同值的个数:

NHANES |>
  distinct(ID, SurveyYr) |>
  nrow()
## [1] 6779

这个结果提示有些人在某一调查年中有多个观测。

26.6drop_na()去除指定的变量有缺失值的行

在进行统计建模时, 通常需要用到的因变量和自变量都不包含缺失值。 tidyr包的drop_na()函数可以对数据框指定一到多个变量, 删去指定的变量有缺失值的行。 不指定变量时有任何变量缺失的行都会被删去。

例如,将NHANES中所有存在缺失值的行删去后数出保留的行数, 原来有10000行:

NHANES |>
  drop_na() |>
  nrow()
## [1] 0

可见所有行都有缺失值。下面仅剔除AlcoholDay缺失的观测并计数:

NHANES |>
  drop_na(AlcoholDay) |>
  nrow()
## [1] 4914

基本stats包的complete.cases函数返回是否无缺失值的逻辑向量, na.omit函数则返回无缺失值的观测的子集。

26.7select()选择列子集

dplyr包的select()选择列子集,并返回列子集结果。

可以指定变量名,如

d.class |>
  select(name, age) |>
  head(n=3) |>
  knitr::kable()
name age
Alice 13
Becka 13
Gail 14

可以用冒号表示列范围,如

d.class |>
  select(age:weight) |>
  head(n=3) |>
  knitr::kable()
age height weight
13 56.5 84
13 65.3 98
14 64.3 90

可以用数字序号表示列范围,如

d.class |>
  select(3:5) |>
  head(n=3) |>
  knitr::kable()
age height weight
13 56.5 84
13 65.3 98
14 64.3 90

参数中前面写负号表示扣除,如

d.class |>
  select(-name, -age) |>
  head(n=3) |>
  knitr::kable()
sex height weight
F 56.5 84
F 65.3 98
F 64.3 90

如果要选择的变量名已经保存为一个字符型向量, 可以用one_of()函数引入,如

vars <- c("name", "sex")
d.class |>
  select(one_of(vars)) |>
  head(n=3) |>
  knitr::kable()
name sex
Alice F
Becka F
Gail F

R的字符串函数(如paste())和正则表达式函数可以用来生成变量名子集, 然后在select中配合one_of使用。

select()有若干个配套函数可以按名字的模式选择变量列, 如

  • starts_with("se"): 选择名字以“se”`开头的变量列;
  • ends_with("ght"): 选择名字以“ght”`结尾的变量列;
  • contains("no"): 选择名字中含有子串“no”`的变量列;
  • matches("^[[:alpha:]]+[[:digit:]]+$"), 选择列名匹配某个正则表达式模式的变量列, 这里匹配前一部分是字母,后一部分是数字的变量名,如abc12
  • num_range("x", 1:3),选择x1, x2, x3
  • everything(): 代指所有选中的变量, 这可以用来将指定的变量次序提前, 其它变量排在后面。

R函数subset也能对数据框选取列子集和行子集。

26.8 取出单个变量为向量

如果需要选择单个变量并使得结果为普通向量, 可以用dplyr包的pull()函数,如:

d.class |> 
  head(n=3) |>
  pull(name) |>
  paste(collapse=":")
## [1] "Alice:Becka:Gail"

pull()可以指定单个变量名, 也可以指定变量序号, 负的变量序号从最后一个变量数起。 缺省变量名和序号时取出最后一个变量。

如果要取出的变量名保存在一个字符型变量varname中, 可以用pull(.data, !!sym(varname))这种格式; 如果varname是函数的自变量, 可以用pull(.data, {{ varname }})这种格式。 或者, 先选择仅有一个变量的子数据框再用pull(),如:

varname <- "name"
d.class |> 
  head(n=3) |>
  select(one_of(varname)) |>
  pull() |>
  paste(collapse=":")
## [1] "Alice:Becka:Gail"

基于基本R, 也可以用d.class[["name"]]这种格式取出一列为普通变量, 如果varname保存了变量名, 可以用d.class[[varname]]这种格式。

不能用d.class[,"name"]这种方法, 对于tibble类型, 其结果仍是一个子数据框; 用d.class["name"]这种格式, 结果也是一个子数据框。

26.9arrange()排序

dplyr包的arrange()按照数据框的某一列或某几列排序, 返回排序后的结果,如

d.class |>
  arrange(sex, age) |>
  knitr::kable()
name sex age height weight
Thomas M 11 57.5 85.0
James M 12 57.3 83.0
John M 12 59.0 99.5
Robert M 12 64.8 128.0
Jeffrey M 13 62.5 84.0
Alfred M 14 69.0 112.5
Duke M 14 63.5 102.5
Guido M 15 67.0 133.0
William M 15 66.5 112.0
Philip M 16 72.0 150.0
Sandy F 11 51.3 50.5
Karen F 12 56.3 77.0
Kathy F 12 59.8 84.5
Alice F 13 56.5 84.0
Becka F 13 65.3 98.0
Gail F 14 64.3 90.0
Tammy F 14 62.8 102.5
Mary F 15 66.5 112.0
Sharon F 15 62.5 112.5

desc()包裹想要降序排列的变量,如

d.class |>
  arrange(sex, desc(age)) |>
  knitr::kable()
name sex age height weight
Philip M 16 72.0 150.0
Guido M 15 67.0 133.0
William M 15 66.5 112.0
Alfred M 14 69.0 112.5
Duke M 14 63.5 102.5
Jeffrey M 13 62.5 84.0
James M 12 57.3 83.0
John M 12 59.0 99.5
Robert M 12 64.8 128.0
Thomas M 11 57.5 85.0
Mary F 15 66.5 112.0
Sharon F 15 62.5 112.5
Gail F 14 64.3 90.0
Tammy F 14 62.8 102.5
Alice F 13 56.5 84.0
Becka F 13 65.3 98.0
Karen F 12 56.3 77.0
Kathy F 12 59.8 84.5
Sandy F 11 51.3 50.5

排序时不论升序还是降序, 所有的缺失值都自动排到末尾。

R函数order()可以用来给出数据框的排序次序, 然后以其输出为数据框行下标, 可以将数据框排序。

26.10rename()修改变量名

在dplyr包的rename()中用“新名字=旧名字”格式修改变量名, 如

d2.class <- d.class |>
  dplyr::rename(h=height, w=weight)

注意这样改名字不是对原始数据框修改而是返回改了名字后的新数据框。 也可以利用赋值运算符->写成:

d.class |>
  dplyr::rename(h=height, w=weight) ->
  d2.class

rename()这个函数可能出现在其它包中, 保险起见写成dplyr::rename()

26.11mutate()计算新变量

dplyr包的mutate()可以为数据框计算新变量, 返回含有新变量以及原变量的新数据框。 如

d.class |>
  mutate(
    rwh=weight/height, 
    sexc=ifelse(sex=="F", "女", "男")) |>
  head(n=3) |>
  knitr::kable()
name sex age height weight rwh sexc
Alice F 13 56.5 84 1.486726
Becka F 13 65.3 98 1.500766
Gail F 14 64.3 90 1.399689

mutate()计算新变量时如果计算比较复杂, 也可以用多个语句组成复合语句,如:

d.class |>
  mutate(
    sexc = {
      x <- rep("男", length(sex))
      x[sex == "F"] <- "女"
      x
    }  ) |>
  head(n=3) |>
  knitr::kable()
name sex age height weight sexc
Alice F 13 56.5 84
Becka F 13 65.3 98
Gail F 14 64.3 90

注意这样生成新变量不是在原来的数据框中添加, 原来的数据框没有被修改, 而是返回添加了新变量的新数据框。 R软件的巧妙设计保证了这样虽然是生成了新数据框, 但是与原来数据框重复的列并不会重复保存。

计算公式中可以包含对数据框中变量的统计函数结果,如

d.class |>
  mutate(
    cheight = height - mean(height)) |>
  knitr::kable()

新变量可以与老变量名相同, 这样就在输出中修改了老变量。

26.12tranmute()生成新变量的数据框

函数transmute()用法与mutate()类似, 但是仅保留新定义的变量, 不保留原来的所有变量。 如:

d.class |>
  transmute(
    height_cm = round(height*2.54),
    weight_kg = round(weight*0.4535924),
    bmi =  weight_kg / (height_cm / 100)^2) |>
  head(n=3) |>
  knitr::kable()
height_cm weight_kg bmi
144 38 18.32562
166 44 15.96748
163 41 15.43152

可见结果中仅保留了新定义的变量。

定义新变量也可以直接为数据框的新变量赋值:

d.class[["rwh"]] <- d.class[["weight"]] / d.class[["height"]]

这样的做法与mutate()的区别是这样不会生成新数据框, 新变量是在原数据框中增加的。

给数据框中某个变量赋值为NULL可以修改数据框, 从数据框中删去该变量。

26.13 用管道连接多次操作

管道运算符特别适用于对同一数据集进行多次操作。 例如,对d.class数据,先选出所有女生, 再去掉性别和age变量:

d.class |>
  filter(sex=="F") |>
  select(-sex, -age) |>
  knitr::kable()
name height weight
Alice 56.5 84.0
Becka 65.3 98.0
Gail 64.3 90.0
Karen 56.3 77.0
Kathy 59.8 84.5
Mary 66.5 112.0
Sandy 51.3 50.5
Sharon 62.5 112.5
Tammy 62.8 102.5

管道操作的结果可以保存为新的tibble,如:

class_F <- d.class |>
  filter(sex=="F") |>
  select(-sex, -age)

也可以将赋值用->写在最后,如:

d.class |>
  filter(sex=="F") |>
  select(-sex, -age) -> class_F

如果管道传递的变量在下一层调用中不是第一自变量, 可以用.代表, 这种用法需要使用magrittr的%>%管道而不是标准的|>管道, 如:

d.class %>%
  lm(weight ~ height, data=.) %>%
  coef()
## (Intercept)      height 
##  -143.02692     3.89903

为了明确表示不使用管道输入作为第一自变量, 可以将管道操作的那一层加上大括号,如:

d.class %>% {
  lm(weight ~ height, data=.) } %>%
  coef()
## (Intercept)      height 
##  -143.02692     3.89903

26.14 expand_grid()函数

在进行有多个因素的试验设计时, 往往需要生成多个因素完全搭配并重复的表格。 tidyr包的函数expand_grid()可以生成这样的重复模式。 基本R的expand.grid()功能类似。 基本R的gl函数提供了更简单的功能。

比如,下面的例子:

tidyr::expand_grid(
  group=1:3,
  subgroup=1:2,
  obs=1:2) |>
  knitr::kable()
group subgroup obs
1 1 1
1 1 2
1 2 1
1 2 2
2 1 1
2 1 2
2 2 1
2 2 2
3 1 1
3 1 2
3 2 1
3 2 2

结果的数据框d有三个变量: group是大组,共分3个大组,每组4个观测; subgroup是子组,在每个大组内分为2个子组,每个子组2个观测。 共有\(3 \times 2 \times 2 = 12\)个观测(行)。

26.15 宽表转换为长表

实际数据中经常需要将数据框进行长宽格式的转换。 以典型的纵向数据为例, 每个受试者有次随访的记录值, 如果每个受试者的所有随访记录值存放在一个观测中, 就称为宽表, 如果每个受试者的随访记录值存放在多个观测的一列中, 另外增加一列表示记录时间, 就称为长表。 许多统计建模函数需要使用长表格式。

tidyr的pivot_longer()可以将宽表转换成长表, pivot_wider()可以将长表转换成宽表。 基本R的reshape()函数也可以进行长宽格式的转换。 reshape2包也提供了较丰富的长宽表转换功能。 建议优先使用tidyr包的功能。

26.15.1 pivot_longer函数

tidyr的pivot_longer()函数可以将横向的多次观测堆叠在一列中。 例如, 下面的数据:

knitr::kable(dwide1)
subject 1 2 3 4
1 1 NA NA NA
2 NA 7 NA 4
3 5 10 NA NA
4 NA NA 9 NA

subject是受试者编号, 每个受试者有4次随访, NA表示缺失。 数据分析和绘图用的函数一般不能直接使用这样的数据, 一般需要将4次测量合并在一列中作为分析变量, 将随访序号单独放在另外一列中。 用pivot_longer()函数实现:

dwide1 |>
  pivot_longer(`1`:`4`, 
     names_to = "time", 
     values_to = "response") |>
  knitr::kable()
subject time response
1 1 1
1 2 NA
1 3 NA
1 4 NA
2 1 NA
2 2 7
2 3 NA
2 4 4
3 1 5
3 2 10
3 3 NA
3 4 NA
4 1 NA
4 2 NA
4 3 9
4 4 NA

选项names_to指定一个新变量名, 将原来的列标题转换为该变量的值; 选项values_to指定一个新变量名, 将原来的各个列对应的测量值保存在该变量名的列中。

注意原来的变量名不是合法R变量名, 所以在pivot_longer()中用反单撇号保护, 并用了冒号来表示变量范围, 也可以仿照select函数中指定变量名的方法将程序中的`1`:`4`替换为:

  • c("1", "2", "3", "4")
  • -subject
  • cols = one_of(vars), 其中vars被赋值为c("1", "2", "3", "4")

如果转换结果中不希望保留那些NA, 可以加values_drop_na=TRUE:

dwide1 |>
  pivot_longer(`1`:`4`, 
     names_to = "time", 
     values_to = "response",
     values_drop_na = TRUE) |>
  knitr::kable()
subject time response
1 1 1
2 2 7
2 4 4
3 1 5
3 2 10
4 3 9

26.15.2 从列名中提取数值

有时要合并的列名中带有数值, 需要将这些数值部分提取出来, 这时可以用names_prefix指定要去掉的非数值前缀, 用names_transform指定将列名转换为值时结果的类型, names_transform是一个列表, 实现列表元素名到转换函数的映射。 例如,上述的dwide1数据框变成这样:

subject FU1 FU2 FU3 FU4
1 1 NA NA NA
2 NA 7 NA 4
3 5 10 NA NA
4 NA NA 9 NA

可以用如下程序将随访编号变成整数值存入一列:

dwide2 |>
  pivot_longer(cols = paste0("FU", 1:4), 
     names_to = "time", 
     values_to = "response",
     names_prefix = "FU",
     names_transform = list(time = as.integer),
     values_drop_na = TRUE) |>
  knitr::kable()
subject time response
1 1 1
2 2 7
2 4 4
3 1 5
3 2 10
4 3 9

其中的cols = paste0("FU", 1:4)也可以写成cols = starts_with("FU")

考虑nlmeU扩展包的armd.wide数据框。 这个数据框中有240个受试者的信息, 每行为一个受试者, 包括5个时间点:visual0, visual4, visual12, visual24, visual52。 数据如:

library(nlmeU)
## 
## 载入程辑包:'nlmeU'
## The following object is masked from 'package:stats':
## 
##     sigma
data(armd.wide)
knitr::kable(head(armd.wide, 3))
subject lesion line0 visual0 visual4 visual12 visual24 visual52 treat.f miss.pat
1 3 12 59 55 45 NA NA Active –XX
2 1 13 65 70 65 65 55 Active —-
3 4 8 40 40 37 17 NA Placebo —X

将其转换为长表格式并提取时间信息的程序如下:

armd.wide |>
  pivot_longer(
    cols = starts_with("visual"),
    names_to = "time",
    values_to = "visual",
    names_prefix = "visual",
    names_transform = list(time = as.integer),
    values_drop_na = TRUE) |>
  head(10) |> 
  knitr::kable()
subject lesion line0 treat.f miss.pat time visual
1 3 12 Active –XX 0 59
1 3 12 Active –XX 4 55
1 3 12 Active –XX 12 45
2 1 13 Active —- 0 65
2 1 13 Active —- 4 70
2 1 13 Active —- 12 65
2 1 13 Active —- 24 65
2 1 13 Active —- 52 55
3 4 8 Placebo —X 0 40
3 4 8 Placebo —X 4 40

在转换的长表中, 希望增加基线测量值到每个观测中, 并希望将时间0, 4, 12, 24, 52增加一个时间序号0, 1, 2, 3, 4。 程序修改为:

armd.wide |>
  mutate(base = visual0) |>
  pivot_longer(
    cols = starts_with("visual"),
    names_to = "time",
    values_to = "visual",
    names_prefix = "visual",
    names_transform = list(time = as.integer),
    values_drop_na = TRUE) |>
  mutate(timep = as.integer(factor(time, levels=c(0, 4, 12, 24, 52))) - 1) |>
  head(10) |> 
  knitr::kable()
subject lesion line0 treat.f miss.pat base time visual timep
1 3 12 Active –XX 59 0 59 0
1 3 12 Active –XX 59 4 55 1
1 3 12 Active –XX 59 12 45 2
2 1 13 Active —- 65 0 65 0
2 1 13 Active —- 65 4 70 1
2 1 13 Active —- 65 12 65 2
2 1 13 Active —- 65 24 65 3
2 1 13 Active —- 65 52 55 4
3 4 8 Placebo —X 40 0 40 0
3 4 8 Placebo —X 40 4 40 1

26.15.3 从列名中提取多个分类变量值

上面的dwide2数据集的FU1到FU4变量中包含了随访次数这一个变量的值。 有些数据集在列名中用编码形式保存了不止一个变量的信息, 假设那些列保存的数值仍属于同一属性。 例如,下面的数据:

unit F_1 F_2 M_1 M_2
1 55 52 64 60
2 98 93 120 116
3 40 38 44 40

假设这是对某个问题的赞成或反对意见的某个抽样调查的频数表, unit是不同的抽样子集, 其它四列都是频数, F_1代表女性中赞成人数, F_2代表女性中反对人数, M_1代表男性中赞成人数, M_2代表男性中反对人数。

为了利用这样的数据, 需要将不同性别和两种意见的人数都合并到一列中, 增加性别和意见列。 对于这种用一定规则将多个变量值编码进入列名的情形, 需要使用正则表达式的方式将有变量值的部分用正则表达式的捕获子集标记出来, 关于正则表达式详见41.3。 程序为:

dwide3 |>
  pivot_longer(
    cols = F_1:M_2,
    names_to = c("gender", "response"),
    values_to = "freq",
    names_pattern = "(F|M)_(1|2)",
    names_ptypes = list(
      gender = factor(
        levels = c("F", "M")),
      response = factor(
        levels = c("1", "2"))
    )
  ) |>
  knitr::kable()
unit gender response freq
1 F 1 55
1 F 2 52
1 M 1 64
1 M 2 60
2 F 1 98
2 F 2 93
2 M 1 120
2 M 2 116
3 F 1 40
3 F 2 38
3 M 1 44
3 M 2 40

在列名的各个部分之间有分隔符如_时, 可以用names_sep选项代替names_pattern选项,如:

dwide3 |>
  pivot_longer(
    cols = F_1:M_2,
    names_to = c("gender", "response"),
    values_to = "freq",
    names_sep = "_",
    names_ptypes = list(
      gender = factor(
        levels = c("F", "M")),
      response = factor(
        levels = c("1", "2"))
    )
  ) |>
  knitr::kable()
unit gender response freq
1 F 1 55
1 F 2 52
1 M 1 64
1 M 2 60
2 F 1 98
2 F 2 93
2 M 1 120
2 M 2 116
3 F 1 40
3 F 2 38
3 M 1 44
3 M 2 40

26.15.4 一行中有多个属性的多次观测的情形

设有多个属性的多次测量用编号的列名保存在了同一观测中, 例如, 基本R软件中的anscombe数据集的一部分行:

dwide4 <- anscombe[1:3,]
dwide4[["id"]] <- seq(3)
dwide4 <- dwide4 |>
  select(id, everything())
knitr::kable(dwide4)
id x1 x2 x3 x4 y1 y2 y3 y4
1 10 10 10 8 8.04 9.14 7.46 6.58
2 8 8 8 8 6.95 8.14 6.77 5.76
3 13 13 13 8 7.58 8.74 12.74 7.71

这可以看成是每个受试者的x, y两个变量的4次随访的值保存在了一个观测中。 用names_pattern指定切分变量名和随访号的模式, 在对应的names_to中用特殊的".value"名字表示切分出来的那一部分实际是变量名, 这时不需要values_to选项。 程序如下:

dwide4 |>
  pivot_longer(
    -id,
    names_pattern = "(x|y)([[:digit:]])",
    names_to = c(".value", "time")
  ) |>
  knitr::kable()
id time x y
1 1 10 8.04
1 2 10 9.14
1 3 10 7.46
1 4 8 6.58
2 1 8 6.95
2 2 8 8.14
2 3 8 6.77
2 4 8 5.76
3 1 13 7.58
3 2 13 8.74
3 3 13 12.74
3 4 8 7.71

26.16 长表转换为宽表

26.16.1 将多个混在一起的变量拆开

tidyr包的pivot_wider函数可以将长表变成宽表。 这适用于将多个变量保存到了一列的情况。 例如,下面的长表将变量x和y放在了同一列中:

id variable value
1 x 11
1 y 23
2 x 10
2 y 20
3 x 15
3 y 28

这样的数据也不利于进行统计分析, 我们用pivot_wider函数将两个变量放到各自的列中, 用names_from选项指定区分不同变量的列, 用values_from指定保存实际变量值的列:

dlong1 |>
  pivot_wider(
    names_from = "variable",
    values_from = "value"  ) |>
  knitr::kable()
id x y
1 11 23
2 10 20
3 15 28

其中的变量名也可以不用双撇号保护。

在这样拆分列时, 有可能某些变量值不存在,例如:

id variable value
1 x 11
1 y 23
2 x 10
3 y 28

这里2号id缺少y,3号id缺少x。 直接转换为宽表:

dlong2 |>
  pivot_wider(
    names_from = variable,
    values_from = value  ) |>
  knitr::kable()
id x y
1 11 23
2 10 NA
3 NA 28

产生了缺失值。 如果知道缺失值实际等于0, 可以用选项values_fill=选项指定,如:

dlong2 |>
  pivot_wider(
    names_from = variable,
    values_from = value,
    values_fill = list(
      value = 0)  ) |>
  knitr::kable()
id x y
1 11 23
2 10 0
3 0 28

26.16.2 将多个类别合并到一个观测

设3个受试者的2次测量值放在变量x中, 用time区分2次测量值:

id time x
1 1 11
1 2 10
2 1 15
2 2 13
3 1 18
3 2 16

下面的程序将x的两次测量变成变量x1和x2:

dlong3 |>
  pivot_wider(
    names_from = time,
    values_from = x,
    names_prefix = "x") |>
  knitr::kable()
id x1 x2
1 11 10
2 15 13
3 18 16

26.16.3 将交叉类别合并到一个观测

考虑如下的频数表数据:

year sex type count
2018 F Benign 4
2018 F Malignant 9
2018 M Benign 18
2018 M Malignant 3
2019 F Benign 6
2019 F Malignant 10
2019 M Benign 20
2019 M Malignant 5

下面的程序将每年的数据合并到一行中:

dlong4 |>
  pivot_wider(
    names_from = c("sex", "type"),
    values_from = "count"
  ) |>
  knitr::kable()
year F_Benign F_Malignant M_Benign M_Malignant
2018 4 9 18 3
2019 6 10 20 5

26.16.4 多个变量的多种值

设有如下的x变量和y变量的分组汇总统计数据:

group variable avg sd
1 x 1.2 0.5
1 y -5.1 0.4
2 x 1.4 0.5
2 y -4.9 0.8
3 x 1.3 0.7
3 y -4.3 0.9

下面将x和y的两种统计量都放到同一行中:

dlong5 |>
  pivot_wider(
    names_from = "variable",
    values_from = c("avg", "sd")
  ) |> 
  knitr::kable()
group avg_x avg_y sd_x sd_y
1 1.2 -5.1 0.5 0.4
2 1.4 -4.9 0.5 0.8
3 1.3 -4.3 0.7 0.9

26.16.5 长宽转换混合使用

有时数据需要使用两个方向的转换才能达到可用的程度, 比如下面的数据:

knitr::kable(dlong6)
id variable 2018 2019
1 x 1.2 1.3
1 y -5.1 -5.4
2 x 1.4 1.6
2 y -4.9 -4.2
3 x 1.3 1.5
3 y -4.3 -4.1

这个数据的问题是x, y应该放在两列中却合并成一个了, 2018和2019应该放在一列中却分成了两列。 先合并2018和2019这两列, 然后再拆分x和y:

dlong6 |>
  pivot_longer(
    `2018`:`2019`,
    names_to = "year",
    values_to = "value" 
    ) |>
  pivot_wider(
    names_from = "variable",
    values_from = "value"
    ) |>
  knitr::kable()
id year x y
1 2018 1.2 -5.1
1 2019 1.3 -5.4
2 2018 1.4 -4.9
2 2019 1.6 -4.2
3 2018 1.3 -4.3
3 2019 1.5 -4.1

26.17 拆分数据列

有时应该放在不同列的数据用分隔符分隔后放在同一列中了。 比如,下面数据集中“succ/total”列存放了用“/”分隔开的成功数与试验数:

d.sep <- read_csv(
"testid, succ/total
1, 1/10
2, 3/5
3, 2/8
")
## Rows: 3 Columns: 2
## -- Column specification --------------------------------------------------------
## Delimiter: ","
## chr (1): succ/total
## dbl (1): testid
## 
## i Use `spec()` to retrieve the full column specification for this data.
## i Specify the column types or set `show_col_types = FALSE` to quiet this message.
knitr::kable(d.sep)
testid succ/total
1 1/10
2 3/5
3 2/8

tidyr::separate()可以将这样的列拆分为各自的变量列,如

d.sep |>
  separate(`succ/total`, into=c("succ", "total"), 
           sep="/", convert=TRUE) |> knitr::kable()
testid succ total
1 1 10
2 3 5
3 2 8

其中into指定拆分后新变量名, sep指定分隔符, convert=TRUE要求自动将分割后的值转换为适当的类型。 sep还可以指定取子串的字符位置, 按位置拆分各个子串。

选项extra指出拆分时有多余内容的处理方法, 选项fill指出有不足内容的处理方法。

拆分的也可以是变量名和因子, 比如, 变量包括血压的高压和低压, 分男女计算了平均值, 结果表格可能为如下格式:

knitr::kable(dbpa)
var avg
male:systolicbp 118
male:diastolicbp 85
female:systolicbp 115
female:diastolicbp 83

separate()函数将变量名和性别值分开:

dbpa2 <- dbpa |>
  separate(var, into = c("sex", "var"), sep=":")
knitr::kable(dbpa2)
sex var avg
male systolicbp 118
male diastolicbp 85
female systolicbp 115
female diastolicbp 83

实际上,这个数据集可能还需要将高压和低压变成两列, 用pivot_wider()函数:

dbpa3 <- dbpa2 |>
  pivot_wider(
    names_from = "var", values_from = "avg")
knitr::kable(dbpa3)
sex systolicbp diastolicbp
male 118 85
female 115 83

函数extract()可以按照某种正则表达式表示的模式从指定列拆分出对应于正则表达式中捕获组的一列或多列内容。 例如,下面的数据中factors水平AA, AB, BA, BB实际是两个因子的组合, 将其拆分出来:

dexp <- tibble(
  design = c("AA", "AB", "BA", "BB"),
  response = c(120, 110, 105, 95))
knitr::kable(dexp)
design response
AA 120
AB 110
BA 105
BB 95
dexp |>
  extract(
    design,
    into = c("fac1", "fac2"),
    regex = "(.)(.)"
  ) |>
  knitr::kable()
fac1 fac2 response
A A 120
A B 110
B A 105
B B 95

26.18 合并数据列

tidyr::unite()函数可以将同一行的两列或多列的内容合并成一列。 这是separate()的反向操作, 如:

d.sep |>
  separate(`succ/total`, into=c("succ", "total"), 
           sep="/", convert=TRUE) |>
  unite(ratio, succ, total, sep=":") |>
  knitr::kable()
testid ratio
1 1:10
2 3:5
3 2:8

unite()的第一个参数是要修改的数据框, 这里用管道|>传递进来, 第二个参数是合并后的变量名(ratio变量), 其它参数是要合并的变量名,sep指定分隔符。 实际上用mutate()paste()或者sprintf()也能完成合并。

26.19 数据框纵向合并

矩阵或数据框要纵向合并,使用rbind函数即可。 dplyr包的bind_rows()函数也可以对两个或多个数据框纵向合并。 要求变量集合是相同的,变量次序可以不同。

比如,有如下两个分开男生、女生的数据框:

d3.class <- d.class |>
  select(name, sex, age) |>
  filter(sex=="M")
d4.class <- d.class |>
  select(name, sex, age) |>
  filter(sex=="F")

合并行如下:

d3.class |>
  bind_rows(d4.class) |>
  knitr::kable()
name sex age
Alfred M 14
Duke M 14
Guido M 15
James M 12
Jeffrey M 13
John M 12
Philip M 16
Robert M 12
Thomas M 11
William M 15
Alice F 13
Becka F 13
Gail F 14
Karen F 12
Kathy F 12
Mary F 15
Sandy F 11
Sharon F 15
Tammy F 14

将下面的数据框的变量列次序打乱, 合并不受影响:

d3.class |>
  select(age, name, sex) |>
  bind_rows(d4.class) |>
  knitr::kable()
age name sex
14 Alfred M
14 Duke M
15 Guido M
12 James M
13 Jeffrey M
12 John M
16 Philip M
12 Robert M
11 Thomas M
15 William M
13 Alice F
13 Becka F
14 Gail F
12 Karen F
12 Kathy F
15 Mary F
11 Sandy F
15 Sharon F
14 Tammy F

26.20 横向合并

为了将两个行数相同的数据框按行号对齐合并, 可以用基本R的cbind()函数或者dplyr包的bind_cols()函数。

实际数据往往没有存放在单一的表中, 需要从多个表查找数据。 多个表之间的连接, 一般靠关键列(key)对准来连接。 连接可以是一对一的, 一对多的。 多对多连接应用较少, 因为多对多连接是所有两两组合。

在规范的数据库中,每个表都应该有主键, 这可以是一列,也可以是多列的组合。 为了确定某列是主键, 可以用count()filter(),如

d.class |>
  count(name) |>
  filter(n>1) |>
  nrow()
## [1] 0

没有发现重复出现的name, 说明d.classname可以作为主键。

为了演示一对一的横向连接, 我们将d.class取11和12岁的子集, 然后拆分为两个数据集d1.class和d2.class, 两个数据集都有主键name, d1.class包含变量name, sex, d2.class包含变量name, age, height, weight, 并删去某些观测:

d1.class <- d.class |>
  filter(age <= 12) |>
  select(name, sex) |>
  filter(!(name %in%  "Sandy"))
d2.class <- d.class |>
  filter(age <= 12) |>
  select(name, age, height, weight)

用dplyr包的inner_join()函数将两个数据框按键值横向合并, 仅保留能匹配的观测。因为d1.class中丢失了Sandy的观测, 所以合并后的数据框中也没有Sandy的观测:

d1.class |>
  inner_join(d2.class) |>
  knitr::kable()
## Joining, by = "name"
name sex age height weight
Karen F 12 56.3 77.0
Kathy F 12 59.8 84.5
James M 12 57.3 83.0
John M 12 59.0 99.5
Robert M 12 64.8 128.0
Thomas M 11 57.5 85.0

横向连接自动找到了共同的变量name作为连接的键值, 可以在inner_join()中用by=指定键值变量名, 如果有不同的变量名, 可以用by = c("a"="b")的格式指定左数据框的键值a与右数据框的键值b匹配进行连接。

两个表的横向连接, 经常是多对一连接。 例如, d.stu中有学生学号、班级号、姓名、性别, d.cl中有班级号、班主任名、年级, 可以通过班级号将两个表连接起来:

d.stu <- tibble(
  sid=c(1,2,3,4,5,6),
  cid=c(1,2,1,2,1,2),
  sname=c("John", "Mary", "James", "Kitty", "Jasmine", "Kim"),
  sex=c("M", "F", "M", "F", "F", "M"))
knitr::kable(d.stu)
sid cid sname sex
1 1 John M
2 2 Mary F
3 1 James M
4 2 Kitty F
5 1 Jasmine F
6 2 Kim M
d.cl <- tibble(
  cid=c(1,2),
  tname=c("Philip", "Joane"),
  grade=c("2017", "2016")
)
knitr::kable(d.cl)
cid tname grade
1 Philip 2017
2 Joane 2016
d.stu |>
  left_join(d.cl, by="cid") |>
  knitr::kable()
sid cid sname sex tname grade
1 1 John M Philip 2017
2 2 Mary F Joane 2016
3 1 James M Philip 2017
4 2 Kitty F Joane 2016
5 1 Jasmine F Philip 2017
6 2 Kim M Joane 2016

left_join()按照by变量指定的关键列匹配观测, 左数据集所有观测不论匹配与否全部保留, 右数据集仅使用与左数据集能匹配的观测。 不指定by变量时, 使用左、右数据集的共同列作为关键列。 如果左右数据集关键列变量名不同, 可以用by=c("左名"="右名")的格式。

类似地, right_join()保留右数据集的所有观测, 而仅保留左数据集中能匹配的观测。 full_join()保留所有观测。 inner_join()仅保留能匹配的观测。

26.21 利用第二个数据集筛选

left_join()将右表中与左表匹配的观测的额外的列添加到左表中。 如果希望按照右表筛选左表的观测, 可以用semi_join(), 函数anti_join()则是要求保留与右表不匹配的观测。

26.22 数据集的集合操作

R的intersect()union(), setdiff()本来是以向量作为集合进行集合操作。 dplyr包也提供了这些函数, 但是将两个tibble的各行作为元素进行集合操作。

26.23 标准化

设x是各列都为数值的列表(包括数据框和tibble)或数值型矩阵, 用scale(x)可以把每一列都标准化, 即每一列都减去该列的平均值,然后除以该列的样本标准差。 用scale(x, center=TRUE, scale=FALSE)仅中心化而不标准化。 如

d.class |> 
  select(height, weight) |>
  scale()
##            height      weight
##  [1,] -1.13843504 -0.70371312
##  [2,]  0.57794313 -0.08897522
##  [3,]  0.38290015 -0.44025402
##  [4,] -1.17744363 -1.01108207
##  [5,] -0.49479323 -0.68175819
##  [6,]  0.81199469  0.52576268
##  [7,] -2.15265850 -2.17469309
##  [8,]  0.03182280  0.54771760
##  [9,]  0.09033569  0.10861910
## [10,]  1.29960213  0.54771760
## [11,]  0.22686577  0.10861910
## [12,]  0.90951618  1.44786952
## [13,] -0.98240066 -0.74762297
## [14,]  0.03182280 -0.70371312
## [15,] -0.65082761 -0.02311045
## [16,]  1.88473105  2.19433697
## [17,]  0.48042164  1.22832027
## [18,] -0.94339207 -0.65980327
## [19,]  0.81199469  0.52576268
## attr(,"scaled:center")
##    height    weight 
##  62.33684 100.02632 
## attr(,"scaled:scale")
##    height    weight 
##  5.127075 22.773933

为了把x的每列变到\([0,1]\)内,可以用如下的方法:

d.class %>%
  select(height, weight) %>%
  scale(center=apply(., 2, min),
      scale=apply(., 2, max) - apply(., 2, min))

其中的.%>%管道操作中表示被传递处理的变量(一般是数据框)。 也可以写一个自定义的进行零一标准化的函数:

scale01 <- function(x){
  mind <- apply(x, 2, min)
  maxd <- apply(x, 2, max)
  scale(x, center=mind, scale=maxd-mind)
}
d.class |> 
  select(height, weight) |>
  scale01()
##          height    weight
##  [1,] 0.2512077 0.3366834
##  [2,] 0.6763285 0.4773869
##  [3,] 0.6280193 0.3969849
##  [4,] 0.2415459 0.2663317
##  [5,] 0.4106280 0.3417085
##  [6,] 0.7342995 0.6180905
##  [7,] 0.0000000 0.0000000
##  [8,] 0.5410628 0.6231156
##  [9,] 0.5555556 0.5226131
## [10,] 0.8550725 0.6231156
## [11,] 0.5893720 0.5226131
## [12,] 0.7584541 0.8291457
## [13,] 0.2898551 0.3266332
## [14,] 0.5410628 0.3366834
## [15,] 0.3719807 0.4924623
## [16,] 1.0000000 1.0000000
## [17,] 0.6521739 0.7788945
## [18,] 0.2995169 0.3467337
## [19,] 0.7342995 0.6180905
## attr(,"scaled:center")
## height weight 
##   51.3   50.5 
## attr(,"scaled:scale")
## height weight 
##   20.7   99.5

函数sweep()可以执行对每列更一般的变换。

References

Wickham, H. 2014. “Tidy Data.” J Stat Software 59. http://www.jstatsoft.org/v59/i10/.