26 数据整理
26.1 tidyverse系统
假设数据以tibble格式保存。 数据集如果用于统计与绘图, 需要满足一定的格式要求, (Wickham 2014)称之为整洁数据(tidy data), 基本要求是每行一个观测, 每列一个变量, 每个单元格恰好有一个数据值。 这些变量应该是真正的属性, 而不是同一属性在不同年、月等时间的值分别放到单独的列。
数据集经常需要选行子集、选列子集、排序、定义新变量、横向合并、长宽转换等操作,
而且经常会用若干个连续的操作分步处理,
magrittr包的管道运算符%>%
特别适用于这种分步处理。
dplyr包和tidyr包定义了一系列“动词”,
可以用比较自然的方式进行数据整理。
较复杂的分组操作还可以利用purrr包的map
类函数。
为了使用这些功能,可以载入tidyverse
包,
则magrittr包,readr包,dplyr包和tidyr包都会被自动载入:
下面的例子中用如下的一个班的学生数据作为例子,
保存在如下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(
"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数据框:
## [1] 10000 76
## [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.2 用filter()
选择行子集
数据框的任何行子集仍为数据框,即使只有一行而且都是数值也是如此。
行子集可以用行下标选取,
如d.class[8:12,]
。
函数head()
取出数据框的前面若干行,
tail()
取出数据框的最后若干行。
dplyr包的filter()
函数可以按条件选出符合条件的行组成的子集。
下例从d.class
中选出年龄在13岁和13岁以下的女生:
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
行,如:
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, ...)
可以用来选择指定序号的行子集,
正的序号表示保留,负的序号表示排除。如:
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.4 用sample_n()
对观测随机抽样
dplyr包的sample_n(tbl, size)
函数可以从数据集tbl
中随机无放回抽取size
行,如:
name | sex | age | height | weight |
---|---|---|---|---|
Alfred | M | 14 | 69.0 | 112.5 |
Gail | F | 14 | 64.3 | 90.0 |
Karen | F | 12 | 56.3 | 77.0 |
sample_n()
中加选项replace=TRUE
可以变成有放回抽样。
可以用weight
选项指定数据框中的一列作为抽样权重,
进行不等概抽样。
26.5 用distinct()
去除重复行
有时我们希望得到一个或若干个变量组合的所有不同值。
dplyr包的distinct()
函数可以对数据框指定若干变量,
然后筛选出所有不同值,
每组不同值仅保留一行。
指定变量名时不是写成字符串形式而是直接写变量名,
这是dplyr和tidyr包的特点。
例如,筛选出性别与年龄的所有不同组合:
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的组合的不同值的个数:
## [1] 6779
这个结果提示有些人在某一调查年中有多个观测。
26.6 用drop_na()
去除指定的变量有缺失值的行
在进行统计建模时,
通常需要用到的因变量和自变量都不包含缺失值。
tidyr包的drop_na()
函数可以对数据框指定一到多个变量,
删去指定的变量有缺失值的行。
不指定变量时有任何变量缺失的行都会被删去。
例如,将NHANES中所有存在缺失值的行删去后数出保留的行数, 原来有10000行:
## [1] 0
可见所有行都有缺失值。下面仅剔除AlcoholDay缺失的观测并计数:
## [1] 4914
基本stats包的complete.cases
函数返回是否无缺失值的逻辑向量,
na.omit
函数则返回无缺失值的观测的子集。
26.7 用select()
选择列子集
dplyr包的select()
选择列子集,并返回列子集结果。
可以指定变量名,如
name | age |
---|---|
Alice | 13 |
Becka | 13 |
Gail | 14 |
可以用冒号表示列范围,如
age | height | weight |
---|---|---|
13 | 56.5 | 84 |
13 | 65.3 | 98 |
14 | 64.3 | 90 |
可以用数字序号表示列范围,如
age | height | weight |
---|---|---|
13 | 56.5 | 84 |
13 | 65.3 | 98 |
14 | 64.3 | 90 |
参数中前面写负号表示扣除,如
sex | height | weight |
---|---|---|
F | 56.5 | 84 |
F | 65.3 | 98 |
F | 64.3 | 90 |
如果要选择的变量名已经保存为一个字符型向量,
可以用one_of()
函数引入,如
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()
函数,如:
## [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.9 用arrange()
排序
dplyr包的arrange()
按照数据框的某一列或某几列排序,
返回排序后的结果,如
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()
包裹想要降序排列的变量,如
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.10 用rename()
修改变量名
在dplyr包的rename()
中用“新名字=旧名字”格式修改变量名,
如
注意这样改名字不是对原始数据框修改而是返回改了名字后的新数据框。
rename()
这个函数可能出现在其它包中,
保险起见写成dplyr::rename()
。
26.11 用mutate()
计算新变量
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软件的巧妙设计保证了这样虽然是生成了新数据框, 但是与原来数据框重复的列并不会重复保存。
计算公式中可以包含对数据框中变量的统计函数结果,如
新变量可以与老变量名相同, 这样就在输出中修改了老变量。
26.12 用tranmute()
生成新变量的数据框
函数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 |
可见结果中仅保留了新定义的变量。
定义新变量也可以直接为数据框的新变量赋值:
这样的做法与mutate()
的区别是这样不会生成新数据框,
新变量是在原数据框中增加的。
给数据框中某个变量赋值为NULL
可以修改数据框,
从数据框中删去该变量。
26.13 用管道连接多次操作
管道运算符特别适用于对同一数据集进行多次操作。
例如,对d.class
数据,先选出所有女生,
再去掉性别和age变量:
name | height | weight | rwh |
---|---|---|---|
Alice | 56.5 | 84.0 | 1.4867257 |
Becka | 65.3 | 98.0 | 1.5007657 |
Gail | 64.3 | 90.0 | 1.3996890 |
Karen | 56.3 | 77.0 | 1.3676732 |
Kathy | 59.8 | 84.5 | 1.4130435 |
Mary | 66.5 | 112.0 | 1.6842105 |
Sandy | 51.3 | 50.5 | 0.9844055 |
Sharon | 62.5 | 112.5 | 1.8000000 |
Tammy | 62.8 | 102.5 | 1.6321656 |
管道操作的结果可以保存为新的tibble,如:
也可以将赋值用->
写在最后,如:
如果管道传递的变量在下一层调用中不是第一自变量,
可以用.
代表,如:
## (Intercept) height
## -143.02692 3.89903
为了明确表示不使用管道输入作为第一自变量, 可以将管道操作的那一层加上大括号,如:
## (Intercept) height
## -143.02692 3.89903
26.14 宽表转换为长表
26.14.1 pivot_longer
函数
tidyr的pivot_longer()
函数可以将横向的多次观测堆叠在一列中。
例如,
下面的数据:
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()
函数实现:
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
是保存了1到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.14.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")
。
26.14.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
代表男性中反对人数。
为了利用这样的数据, 需要将不同性别和两种意见的人数都合并到一列中, 增加性别和意见列。 对于这种用一定规则将多个变量值编码进入列名的情形, 需要使用正则表达式的方式将有变量值的部分用正则表达式的捕获子集标记出来, 关于正则表达式详见36.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.14.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.15 长表转换为宽表
26.15.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
指定保存实际变量值的列:
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。 直接转换为宽表:
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.15.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:
id | x1 | x2 |
---|---|---|
1 | 11 | 10 |
2 | 15 | 13 |
3 | 18 | 16 |
26.15.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 |
下面的程序将每年的数据合并到一行中:
year | F_Benign | F_Malignant | M_Benign | M_Malignant |
---|---|---|---|---|
2018 | 4 | 9 | 18 | 3 |
2019 | 6 | 10 | 20 | 5 |
26.15.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的两种统计量都放到同一行中:
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.15.5 长宽转换混合使用
有时数据需要使用两个方向的转换才能达到可用的程度, 比如下面的数据:
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.16 拆分数据列
有时应该放在不同列的数据用分隔符分隔后放在同一列中了。 比如,下面数据集中“succ/total”列存放了用“/”分隔开的成功数与试验数:
testid | succ/total |
---|---|
1 | 1/10 |
2 | 3/5 |
3 | 2/8 |
用tidyr::separate()
可以将这样的列拆分为各自的变量列,如
testid | succ | total |
---|---|---|
1 | 1 | 10 |
2 | 3 | 5 |
3 | 2 | 8 |
其中into
指定拆分后新变量名,
sep
指定分隔符,
convert=TRUE
要求自动将分割后的值转换为适当的类型。
sep
还可以指定取子串的字符位置,
按位置拆分各个子串。
选项extra
指出拆分时有多余内容的处理方法,
选项fill
指出有不足内容的处理方法。
拆分的也可以是变量名和因子, 比如, 变量包括血压的高压和低压, 分男女计算了平均值, 结果表格可能为如下格式:
var | avg |
---|---|
male:systolicbp | 118 |
male:diastolicbp | 85 |
female:systolicbp | 115 |
female:diastolicbp | 83 |
用separate()
函数将变量名和性别值分开:
sex | var | avg |
---|---|---|
male | systolicbp | 118 |
male | diastolicbp | 85 |
female | systolicbp | 115 |
female | diastolicbp | 83 |
实际上,这个数据集可能还需要将高压和低压变成两列,
用pivot_wider()
函数:
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 |
fac1 | fac2 | response |
---|---|---|
A | A | 120 |
A | B | 110 |
B | A | 105 |
B | B | 95 |
26.17 合并数据列
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.18 数据框纵向合并
矩阵或数据框要纵向合并,使用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")
合并行如下:
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 |
将下面的数据框的变量列次序打乱, 合并不受影响:
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.19 横向合并
为了将两个行数相同的数据框按行号对齐合并,
可以用基本R的cbind()
函数或者dplyr包的bind_cols()
函数。
实际数据往往没有存放在单一的表中, 需要从多个表查找数据。 多个表之间的连接, 一般靠关键列(key)对准来连接。 连接可以是一对一的, 一对多的。 多对多连接应用较少, 因为多对多连接是所有两两组合。
在规范的数据库中,每个表都应该有主键,
这可以是一列,也可以是多列的组合。
为了确定某列是主键,
可以用count()
和filter()
,如
## # A tibble: 0 x 2
## # ... with 2 variables: name <chr>, n <int>
没有发现重复出现的name
,
说明d.class
中name
可以作为主键。
为了演示一对一的横向连接,
我们将d.class拆分为两个数据集d1.class和d2.class,
两个数据集都有主键name
,
d1.class包含变量name
, sex
,
d2.class包含变量name
, age
, height
, weight
,
并删去某些观测:
d1.class <- d.class %>%
select(name, sex) %>%
filter(!(name %in% "Becka"))
d2.class <- d.class %>%
select(name, age, height, weight)
用dplyr包的inner_join()
函数将两个数据框按键值横向合并,
仅保留能匹配的观测。因为d1.class中丢失了Becka的观测,
所以合并后的数据框中也没有Becka的观测:
## Joining, by = "name"
name | sex | age | height | weight |
---|---|---|---|---|
Alice | F | 13 | 56.5 | 84.0 |
Gail | F | 14 | 64.3 | 90.0 |
Karen | F | 12 | 56.3 | 77.0 |
Kathy | F | 12 | 59.8 | 84.5 |
Mary | F | 15 | 66.5 | 112.0 |
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.0 | 112.5 |
Duke | M | 14 | 63.5 | 102.5 |
Guido | M | 15 | 67.0 | 133.0 |
James | M | 12 | 57.3 | 83.0 |
Jeffrey | M | 13 | 62.5 | 84.0 |
John | M | 12 | 59.0 | 99.5 |
Philip | M | 16 | 72.0 | 150.0 |
Robert | M | 12 | 64.8 | 128.0 |
Thomas | M | 11 | 57.5 | 85.0 |
William | M | 15 | 66.5 | 112.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 |
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.20 利用第二个数据集筛选
left_join()
将右表中与左表匹配的观测的额外的列添加到左表中。
如果希望按照右表筛选左表的观测,
可以用semi_join()
,
函数anti_join()
则是要求保留与右表不匹配的观测。
26.21 数据集的集合操作
R的intersect()
,union()
, setdiff()
本来是以向量作为集合进行集合操作。
dplyr包也提供了这些函数,
但是将两个tibble的各行作为元素进行集合操作。
26.22 标准化
设x是各列都为数值的列表(包括数据框和tibble)或数值型矩阵,
用scale(x)
可以把每一列都标准化,
即每一列都减去该列的平均值,然后除以该列的样本标准差。
用scale(x, center=TRUE, scale=FALSE)
仅中心化而不标准化。
如
## 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/.