R包-tidyverse

主要基于《数据科学中的R语言》的学习笔记。

dplyr

dplyr 定义了数据处理的规范语法,其中主要包含以下10个主要的函数。

  • mutate(), select(), rename() , filter()
  • summarise(), group_by(), arrange()
  • left_join(), right_join()full_join()

这里我们使用的数据如下,这是一个学生成绩表。

1
2
3
4
5
6
7
8
> df
name type score
1 Alice english 80
2 Alice math 60
3 Bob english 70
4 Bob math 69
5 Carol english 80
6 Carol math 90

管道 %>%

实际运用中,我们经常要使用函数,比如计算向量c(1:10)所有元素值的和

1
sum(c(1:10))
1
## [1] 55

现在有个与上面的等价的写法,就是使用管道操作符%>%

1
c(1:10) %>% sum()
1
## [1] 55

这条语句的意思是f(x) 写成 x %>% f(),这里向量 c(1:10) 通过管道操作符 %>% ,传递到函数sum()的第一个参数位置,即sum(c(1:10)), 这个 %>% 管道操作符还是很形象的。在Windows系统中可以通过Ctrl + Shift + M 快捷键产生 %>%,苹果系统对应的快捷键是Cmd + Shift + M

新增一列 mutate()

假设我们要新增一列奖励分 reward ,那么我们运行以下代码:

1
2
reward <- c(2, 5, 9, 8, 5, 6)
mutate(.data = df, extra = reward)

mutate() 不会修改原数据框 df,而是会直接打印出来。

其第一个参数是 .data,接受要处理的数据框,比如这里的df(由于几乎所有 dplyr 的函数第一个参数都是 这个,因此 .data 可以省略不写 )。

第二个参数是Name-value对, 比如extra = reward

1
2
3
4
5
6
7
8
> mutate(.data = df, extra = reward) 
name type score extra
1 Alice english 80 2
2 Alice math 60 5
3 Bob english 70 9
4 Bob math 69 8
5 Carol english 80 5
6 Carol math 90 6

使用管道的方式如下,这是更加通用的写法。

1
df %>% mutate(extra = reward) 

mutate() 函数中可以通过使用别的函数对指定列进行处理,再赋值给新列。

比如,我们想计算每位同学分数的平方,然后构建数据框新的一列,则可以计算如下

1
2
3
4
5
6
calc_square <- function(x) {
x^2
}

df %>%
mutate(new_col = calc_square(score))

更直接的写法如下:

1
df %>% mutate(new_col = score^2)

之前的操作并没有保存,只是打印出来了,我们可以通过赋值保存为一个新的数据框。

1
df_new <- df %>% mutate(new_col = score^2)

选取列 select()

选取一列

1
df_new %>% select(name)

选取多列,则直接输入多个列名

1
df_new %>% select(name, extra)

如果只是想要剔除某列,则用 -!

1
2
df_new %>% select(-type)
df_new %>% select(!type)

也可以通过位置索引来进行选取

1
2
3
df_new %>% select(1, 2, 3)
df_new %>% select(2:3)
df_new %>% select(-1)

基于列名的首字母、尾字母、是否包含某字母来进行选取

1
2
3
df_new %>% select(starts_with("s"))
df_new %>% select(ends_with("e"))
df_new %>% select(contains("score"))

基于每一列的类型来选取

1
2
df_new %>% select(where(is.character))
df_new %>% select(where(is.numeric))

同时使用多个条件,& 表示和条件,| 表示或条件,! 表示取否。

1
2
3
df_new %>% select(where(is.numeric) & starts_with("t"))
df_new %>% select(starts_with("s") | ends_with("e"))
df_new %>% select(!starts_with("s"))

修改列名 rename()

例如将 total 修改为 total_score

1
2
3
df_new %>% 
select(name, type, total) %>%
rename(total_score = total)

选取行 filter()

前面select()是列方向的选择,而用filter()函数,我们可以对数据框行方向进行筛选,选出符合特定条件的某些行。

比如这里把成绩等于90分的同学筛选出来 。

1
df_new %>% filter(score == 90)

R提供了其他比较关系的算符: <, >, <=, >=, == (equal), != (not equal), %in%, is.na()!is.na() .

也可以限定多个条件进行筛选, 比如,限定英语学科,同时要求成绩高于75分的所有条目筛选出来

1
df_new %>% filter(type == "english", score >= 75)

也就说,逗号分隔的两个条件都要满足

当然,我们也可以使用逻辑运算符号,举例如下

1
2
df_new %>% filter(type == "english" & score >= 75)
df_new %>% filter(score == 70 | score == 90)

这里的第二条语句也可以改写为

1
df_new %>% filter(score %in% c(70, 90))

当然还可以配合一些函数使用,比如把最高分的同学找出来

1
df_new %>% filter(score == max(score))

把成绩高于均值的找出来

1
df_new %>% filter(score > mean(score))

统计汇总 summarise()

summarise()函数非常强大,主要用于统计汇总,往往与其他函数配合使用,比如计算所有同学考试成绩的均值

1
df_new %>% summarise(mean_score = mean(score))

也可以同时完成多个统计。其中 n()函数统计行数。

1
2
3
4
5
6
df_new %>% summarise(
mean_score = mean(score),
median_score = median(score),
n = n(),
sum = sum(score)
)

不过其实直接用 R 自带的 summary() 也差不多。

1
2
3
> summary(df_new$score)
Min. 1st Qu. Median Mean 3rd Qu. Max.
60.00 69.25 75.00 74.83 80.00 90.00

排序 arrange()

比如我们按照考试总成绩从低到高排序,然后输出

1
df_new %>% arrange(total)

默认是从低到高,如果需要采用从高到低排序,有两种方法,第一种方法是在用于排序的变量前面加 - 号;第二种方法可读性更强些,需要使用desc()函数

1
2
df_new %>% arrange(-total)
df_new %>% arrange(desc(total))

也可对多个变量依次排序。比如,我们先按学科类型排序,然后按照成绩从高到底降序排列

1
2
df_new %>% 
arrange(type, desc(total))

左联结 left_join()

左联结是保存第一个表的所有内容,第二个表的内容允许缺失(NA)。

正常写法如下

1
left_join(df1, df2, by = "name")

管道写法如下

1
df1 %>% left_join(df2, by = "name")

右联结 right_join()

右联结同理

1
df1 %>% right_join(df2, by = "name")

满联结 full_join()

有时候,我们不想丢失项,可以使用full_join(),该函数确保条目是完整的,信息缺失的地方为NA

也就是两个表的键的并集

1
df1 %>% full_join(df2, by = "name")

内联结inner_join()

两个表的键的交集(两个表的共同的键)。

1
df1 %>% inner_join(df2, by = "name")

去重 distinct()

distinct()函数是去除重复的行(保留第一次出现的行)

1
2
df %>%
distinct()

如果需要查看某一列去重后的结果;多列则输入多列名称

1
2
df %>% distinct(name)
df %>% distinct(name, type)
1
2
3
4
   name
1 Alice
2 Bob
3 Carol
1
2
3
4
5
6
7
   name    type
1 Alice english
2 Alice math
3 Bob english
4 Bob math
5 Carol english
6 Carol math

如果是数据框基于某一列或某几列去重后的结果,即保留全部列,则设置 .keep_all=TRUE

1
df %>% distinct(name, .keep_all=TRUE)
1
2
3
4
   name    type score
1 Alice english 80
2 Bob english 70
3 Carol english 80

统计唯一值出现此时 count()

count() 是统计某一列各组(即唯一值)出现的次数,如果设置sort=TRUE 参数则进行排序。

1
2
df %>%  count(name)
df %>% count(name,sort=TRUE)
1
2
3
4
   name n
1 Alice 2
2 Bob 2
3 Carol 2

也可以统计多列的结果,即统计多列不同组合出现的次数

1
df %>%  count(name,type, sort=TRUE)
1
2
3
4
5
6
7
   name    type n
1 Alice english 1
2 Alice math 1
3 Bob english 1
4 Bob math 1
5 Carol english 1
6 Carol math 1

除了统计分类变量,count() 也可以统计连续变量基于某个条件的分组数目。

例如,假如我们要统计分数大于70和小于等于70的数目,另外也可以按照学科进行分类统计

1
2
df %>%  count(score > 70)
df %>% count(type, score > 70)
1
2
3
  score > 70 n
1 FALSE 3
2 TRUE 3
1
2
3
4
5
     type score > 70 n
1 english FALSE 1
2 english TRUE 2
3 math FALSE 2
4 math TRUE 1

数据规整

宽表格变成长表格

假定这里有 A, B, CD 四种植物每天生长的记录,

1
2
3
4
5
6
7
8
9
10
plant_height <- data.frame(
Day = 1:5,
A = c(0.7, 1.0, 1.5, 1.8, 2.2),
B = c(0.5, 0.7, 0.9, 1.3, 1.8),
C = c(0.3, 0.6, 1.0, 1.2, 2.2),
D = c(0.4, 0.7, 1.2, 1.5, 3.2)
)


plant_height

假如我们想用不同的颜色画出四种植物生长曲线,然而,发现遇到了问题?数据的格式与我们期望的不一样!

怎么解决呢?想用上面的语句,数据就得变形。那么怎么变形呢?

我们可以使用下面两个函数

  • tidyr::pivot_longer() 宽表格变成长表格
  • tidyr::pivot_wider() 长表格变成宽表格

所以现在我们使用 pivot_longer() 函数

1
2
3
4
5
6
7
long <- plant_height %>%
pivot_longer(
cols = A:D,
names_to = "plant",
values_to = "height"
)
long

这里pivot_longer()函数有三个主要的参数:

  • 参数cols,表示哪些列需要转换.
  • 参数names_to,表示cols选取的这些列的名字,构成了新的一列,这里需要取一个名字.
  • 参数values_to, 表示cols选取的这些列的,构成了新的一列,这里也需要取一个名字.
  • 数据框总的信息量不会丢失

这里 cols 参数的写法有多种形式,例如

  • cols = -Day
  • cols = c(A, B, C, D)
  • cols = c(“A”, “B”, “C”, “D”)

画图的问题也就解决了

1
2
3
long %>% 
ggplot(aes(x = Day, y = height, color = plant)) +
geom_line()

长表格变成宽表格

如果,长表格变回宽表格呢?需要用到pivot_wider()

1
2
3
4
5
6
wide <- long %>% 
pivot_wider(
names_from = "plant",
values_from = "height"
)
wide

列名转换成多列

假定 A, B, C 三种植物每天生长的记录,包括三个特征(height, width, depth)

1
2
3
4
5
6
7
8
9
10
11
12
13
plant_record <- data.frame(
day = c(1L, 2L, 3L, 4L, 5L),
A_height = c(1.1, 1.2, 1.3, 1.4, 1.5),
A_width = c(2.1, 2.2, 2.3, 2.4, 2.5),
A_depth = c(3.1, 3.2, 3.3, 3.4, 3.5),
B_height = c(4.1, 4.2, 4.3, 4.4, 4.5),
B_width = c(5.1, 5.2, 5.3, 5.4, 5.5),
B_depth = c(6.1, 6.2, 6.3, 6.4, 6.5),
C_height = c(7.1, 7.2, 7.3, 7.4, 7.5),
C_width = c(8.1, 8.2, 8.3, 8.4, 8.5),
C_depth = c(9.1, 9.2, 9.3, 9.4, 9.5)
)
plant_record

我们想原始数据框的列名,转换成多个变量,比如A,B,C成为物种(species)变量,(height, width, depth)成为parameter变量。

pivot_longer()函数,

1
2
3
4
5
6
7
plant_record %>% 
tidyr::pivot_longer(
cols = !day,
names_to = c("species", "parameter"),
names_pattern = "(.*)_(.*)",
values_to = "value"
)

我们希望原始数据框的列名中,一部分进入变量,一部分保持原来的列名:

其中 .value 说明这里不是单个列名,而是匹配得到的多个值做列名。

1
2
3
4
5
6
7
plant_record_longer <- plant_record %>% 
tidyr::pivot_longer(
cols = !day,
names_to = c("species", ".value"),
names_pattern = "(.*)_(.*)"
)
plant_record_longer

反过来,又该怎么弄呢?

1
2
3
4
5
6
plant_record_longer %>% 
tidyr::pivot_wider(
names_from = species,
values_from = c(height, width, depth),
names_glue = "{species}_{.value}"
)

separate()unite()

separate() 用于将一列基于某个分隔符拆分为多列。其常用参数为:

  • 参数col : 需要拆分的列
  • 参数into :拆分后新列名称
  • 参数 sep :分隔符
1
2
3
4
5
6
7
8
9
10
11
12
tb <- tibble::tribble(
~day, ~price,
1, "30-45",
2, "40-95",
3, "89-65",
4, "45-63",
5, "52-42"
)

tb1 <- tb %>%
separate(col = price, into = c("low", "high"), sep = "-")
tb1

unite()函数相反,用于将多列合并为一列。其常用参数为:

  • 参数col : 合并后新列名称
  • 所有需要合并的列(无参数名称)
  • 参数 sep :分隔符
  • 参数remove : 是否移除需要合并的列
1
2
tb1 %>%
unite(col = "price", c(low, high), sep = ":", remove = FALSE)

drop_na()replace_na()

使用数据如下

1
2
3
4
5
6
7
8
9
10
11
df <- tibble::tribble(
~name, ~type, ~score, ~extra,
"Alice", "english", 80, 10,
"Alice", "math", NA, 5,
"Bob", "english", NA, 9,
"Bob", "math", 69, NA,
"Carol", "english", 80, 10,
"Carol", "math", 90, 5
)

df

如果score列中有缺失值NA,就删除所在的行

1
2
df %>%
filter(!is.na(score))

现在有更简单的方法

1
2
df %>%
drop_na(score)

如果只是剔除存在NA的行

1
2
df %>%
drop_na()

如果需要替换 NA ,可以使用 replace_na() 。例如将 score 中的 NA 替换为 0 。

1
df %>% mutate(score = replace_na(score, 0))

如果将 score 中的 NA 替换为平均值

1
2
3
4
df %>%
mutate(
score = replace_na(score, mean(score, na.rm = TRUE))
)

字符串处理 (正则)

字符串长度

想获取字符串的长度,可以使用str_length()函数

1
str_length("R for data science")
1
## [1] 18

字符串向量,也适用

1
str_length(c("a", "R for data science", NA))
1
## [1]  1 18 NA

数据框里配合dplyr函数,同样很方便

1
2
3
4
data.frame(
x = c("a", "R for data science", NA)
) %>%
mutate(y = str_length(x))
1
2
3
4
##                    x  y
## 1 a 1
## 2 R for data science 18
## 3 <NA> NA

字符串拼接

把字符串拼接在一起,使用 str_c() 函数(有点类似 paste0()

1
str_c("x", "y")
1
[1] "xy"

可以设置分隔符

1
str_c("x", "y", sep = ",")
1
[1] "x,y"

也可以用于两个字符串向量

1
str_c(c("x", "y", "z"), c("x", "y", "z"), sep = ", ")
1
[1] "x, x" "y, y" "z, z"

用在数据框中

1
2
3
4
5
data.frame(
x = c("I", "love", "you"),
y = c("you", "like", "me")
) %>%
mutate(z = str_c(x, y, sep = "|"))

str_c() 函数还有一个可选的参数 collapse 。使用collapse选项,是先(按照sep)拼接,然后再转换成单个字符串,大家对比下

1
2
3
str_c(c("x", "y", "z"), c("a", "b", "c"), sep = "|")
str_c(c("x", "y", "z"), c("a", "b", "c"), collapse = "|")
str_c(c("x", "y", "z"), c("a", "b", "c"), sep = ",", collapse = "|")
1
2
3
[1] "x|a" "y|b" "z|c"
[1] "xa|yb|zc"
[1] "x,a|y,b|z,c"

提供单个字符串向量,再比对一下

1
2
str_c(c("x", "y", "z"), sep = ", ")
str_c(c("x", "y", "z"), collapse = ", ")
1
2
[1] "x" "y" "z"
[1] "x, y, z"

提取字符串子集

截取字符串的一部分,需要指定截取的开始位置和结束位置,使用 str_sub() 命令

1
2
x <- c("Apple", "Banana", "Pear")
str_sub(x, 1, 3)
1
[1] "App" "Ban" "Pea"

开始位置和结束位置如果是负整数,就表示位置是从后往前数,比如下面这段代码,截取倒数第3个至倒数第1个位置上的字符串

1
2
x <- c("Apple", "Banana", "Pear")
str_sub(x, -3, -1)
1
[1] "ple" "ana" "ear"

也可以进行字符串的替换

1
2
3
x <- c("Apple", "Banana", "Pear")
str_sub(x, 1, 1) <- "Q"
x
1
[1] "Qpple"  "Qanana" "Qear"  

如果需要更加灵活地提取字符串子集,可以使用 str_extract() 函数(提取匹配的第一项),其中可以使用正则表达式。

举个例子,如果需要提取字符串向量中的数字,则可以使用以下命令

1
2
y = c("wk 3", "week-1", "7", "w#9")
str_extract(y, "[0-9]")
1
[1] "3" "1" "7" "9"

如果需要提取所有匹配的项,则使用 str_extract_all() 函数,输出为一个 list 。

1
2
y = c("2016123456", "AB2016123456", "J2017000987")
str_extract_all(y, "[A-Z]")
1
2
3
4
5
6
7
8
[[1]]
character(0)

[[2]]
[1] "A" "B"

[[3]]
[1] "J"

替换匹配内容

只替换匹配的第一项,使用 str_replace() 函数

1
2
x <- c("apple", "pear", "banana")
str_replace(x, "[aeiou]", "-")
1
[1] "-pple"  "p-ar"   "b-nana"

替换全部匹配项,使用 str_replace_all() 函数

1
str_replace_all(x, "[aeiou]", "-")
1
[1] "-ppl-"  "p--r"   "b-n-n-"

拆分字符串

使用 str_split() 命令拆分字符串,输出为一个 list

1
2
lines <- "I love my country"
str_split(lines, " ")
1
2
[[1]]
[1] "I" "love" "my" "country"

如果输入的是一个单个字符串,也可以使用 str_split_1() 函数,输出为一个字符串向量。

1
str_split_1(lines, " ")
1
str_split_1(lines, " ")

因子型变量

什么是因子

因子是把数据进行分类并标记为不同层级(level,有时候也翻译成因子水平, 我个人觉得翻译为层级,更接近它的特性,因此,我都会用层级来描述)的数据对象,他们可以存储字符串和整数。因子类型有三个属性:

  • 存储类别的数据类型
  • 离散变量
  • 因子的层级是有限的,只能取因子层级中的值或缺失(NA)

我们创建一个因子

1
2
income <- c("low", "high", "medium", "medium", "low", "high",  "high")
factor(income)
1
2
[1] low    high   medium medium low    high   high  
Levels: high low medium

因子层级会自动按照字符串的字母顺序排序,比如high low medium。也可以指定顺序,

1
factor(income, levels = c("low", "high", "medium") )

不属于因子层级中的值, 会作为缺失值。比如这里因子层只有c("low", "high"),那么”medium”会被当作NA。

1
factor(income, levels = c("low", "high") )
1
2
[1] low  high <NA> <NA> low  high high
Levels: low high

在R 4.0之前,data.frame()stringsAsFactors选项,默认将字符串类型转换为因子类型,但这个默认也带来一些不方便,因此在R 4.0之后取消了这个默认。

在 tidyverse 集合里,有专门处理因子的宏包forcats,因此,本章将围绕forcats宏包讲解如何处理因子类型变量。

调整因子顺序

前面看到因子层级是按照字母顺序排序

1
2
x <- factor(income)
x
1
2
[1] low    high   medium medium low    high   high  
Levels: high low medium

可以指定顺序

1
x %>% fct_relevel( c("high", "medium", "low"))
1
2
[1] low    high   medium medium low    high   high  
Levels: high medium low

或者让”medium” 移动到最前面

1
x %>% fct_relevel( c("medium"))
1
2
[1] low    high   medium medium low    high   high  
Levels: medium high low

可以按照字符串第一次出现的次序

1
x %>% fct_inorder()
1
2
[1] low    high   medium medium low    high   high  
Levels: low high medium

按照其他变量的中位数的升序排序

1
x %>% fct_reorder(c(1:7), .fun = median)  
1
2
[1] low    high   medium medium low    high   high  
Levels: low medium high

按照层级的出现次数排序(从大到小)

1
x %>% fct_infreq()
1
2
[1] low    high   medium medium low    high   high  
Levels: high low medium

按照层级的出现次数排序(从小到大),可以仍使用 fct_reorder()

1
x %>% fct_reorder(x, .fun = length)
1
2
[1] low    high   medium medium low    high   high  
Levels: low medium high

应用

fct_reorder()

调整因子层级有什么用呢?

这个功能在 ggplot() 可视化中调整分类变量的顺序非常方便。这里我们使用以下数据框

1
2
3
4
5
6
d <- tibble(
x = c("a","a", "b", "b", "c", "c"),
y = c(2, 2, 1, 5, 0, 3)

)
d

先画个散点图

1
2
3
d %>% 
ggplot(aes(x = x, y = y)) +
geom_point()

我们看到,横坐标上是a-b-c的顺序。

我们通过fct_reorder()可以让x的顺序按照x中每个分类变量对应y值的中位数升序排序,具体为

  • a对应的y值c(2, 2) 中位数是median(c(2, 2)) = 2
  • b对应的y值c(1, 5) 中位数是median(c(1, 5)) = 3
  • c对应的y值c(0, 3) 中位数是median(c(0, 3)) = 1.5

因此,x的因子层级的顺序调整为c-a-b

1
2
3
d %>% 
ggplot(aes(x = fct_reorder(x, y, .fun = median), y = y)) +
geom_point()

当然,我们可以加一个参数.desc = TRUE让因子层级变为降序排列b-a-c

1
2
3
d %>% 
ggplot(aes(x = fct_reorder(x, y, .fun = median, .desc = TRUE), y = y)) +
geom_point()

但这样会造成x坐标标签一大串,因此建议可以写mutate()函数里

1
2
3
4
d %>% 
mutate(x = fct_reorder(x, y, .fun = median, .desc = TRUE)) %>%
ggplot(aes(x = x, y = y)) +
geom_point()

fct_rev()

按照因子层级的逆序排序,原来是 a-b-c,逆序排列就是 c-b-a 。

1
2
3
4
d %>% 
mutate(x = fct_rev(x)) %>%
ggplot(aes(x = x, y = y)) +
geom_point()

fct_relevel()

自定义因子层级的排序方式

1
2
3
4
5
6
7
d %>% 
mutate(
x = fct_relevel(x, c("c", "a", "b"))
) %>%

ggplot(aes(x = x, y = y)) +
geom_point()

简单数据框

个人感觉 tibble 用处不大

人性化的tibble

tibble继承了data.frame,是弱类型的。换句话说,tibble是data.frame的子类型。

tibble对data.frame做了重新的设定:

  • tibble,不关心输入类型,可存储任意类型,包括list类型
  • tibble,没有行名设置 row.names
  • tibble,支持任意的列名
  • tibble,会自动添加列名
  • tibble,类型只能回收长度为1的输入
  • tibble,会懒加载参数,并按顺序运行
  • tibble,是tbl_df类型

因此我们创建数据框的时候可以直接用 tibble() 命令替代 data.frame() 命令。

1
2
3
4
tibble(
a = 1:5,
b = letters[1:5]
)

我们有时候喜欢这样,构建两个有关联的变量, 比如(此时用传统的 data.frame() 命令会报错)

1
2
3
4
5
tb <- tibble(
x = 1:3,
y = x + 2
)
tb

tibble数据操作

上面已经介绍了创建 tibble 类型的数据框

转换成tibble类型

转换成tibble类型意思就是说,刚开始不是tibble, 现在转换成tibble, 包括

  • data.frame转换成tibble
  • vector转换成tibble
  • list转换成tibble
  • matrix转换成tibble

将 data.frame 转换为 tibble ,使用 as_tibble() 命令

1
as_tibble(df)

将 vector 转型到 tibble ,同上

1
x <- as_tibble(1:5)

把 list 转型为tibble,同上

1
2
df <- as_tibble(list(x = 1:6, y = runif(6), z = 6:1))
df

把tibble再转为list? as.list(df)

把 matrix 转型为tibble,同上

1
2
m <- matrix(rnorm(15), ncol = 5)
as_tibble(m)

tibble转回matrix? as.matrix(df)

tibble 简单操作

构建一个简单的数据框

1
2
3
4
5
6
df <- tibble(
x = 1:2,
y = 2:1
)

df

增加两列

1
add_column(df, z = 0:1, w = 0)

增加一行

1
add_row(df, x = 99, y = 9)

在第二行前面,增加一行

1
add_row(df, x = 99, y = 9, .before = 2)

有用的函数lst

lst,创建一个list,具有tibble特性的list。

1
lst(n = 5, x = runif(n), y = TRUE)
1
2
3
4
5
6
7
8
## $n
## [1] 5
##
## $x
## [1] 0.3069388 0.6828981 0.3568501 0.7939576 0.3833751
##
## $y
## [1] TRUE

有用的函数enframe

enframe()将矢量快速创建tibble,,创建的tibble只有2列: name和value

1
enframe(1:3)
1
2
3
4
5
6
## # A tibble: 3 × 2
## name value
## <int> <int>
## 1 1 1
## 2 2 2
## 3 3 3
1
enframe(c(a = 5, b = 7, c = 9))
1
2
3
4
5
6
## # A tibble: 3 × 2
## name value
## <chr> <dbl>
## 1 a 5
## 2 b 7
## 3 c 9

有用的函数deframe

deframe()可以看做是enframe() 的反操作,把tibble反向转成向量

1
2
df <- enframe(c(a = 5, b = 7))
df
1
2
3
4
5
6
7
## # A tibble: 2 × 2
## name value
## <chr> <dbl>
## 1 a 5
## 2 b 7
# change to vector
deframe(df)
1
2
## a b 
## 5 7

读取文件

read_csv()读取文件时,生成的直接就是tibble

1
read_csv("./demo_data/wages.csv")

关于行名

data.frame是支持行名的,但tibble不支持行名,这也是两者不同的地方

1
2
3
4
5
6
#  create data.frame
df <- data.frame(x = 1:3, y = 3:1)

# add row name
row.names(df) <- LETTERS[1:3]
df
1
2
3
4
##   x y
## A 1 3
## B 2 2
## C 3 1

判断是否有行名

1
has_rownames(df)
1
## [1] TRUE

但是对于tibble

1
2
3
tb <- tibble(x = 1:3, y = 3:1)

row.names(tb) <- LETTERS[1:3]
1
## Warning: Setting row names on a tibble is deprecated.

需要注意的:

  • 有时候遇到含有行名的data.frame,转换成tibble后,行名会被丢弃
  • 如果想保留行名,就需要把行名转换成单独的一列

举个例子

1
2
df <- mtcars[1:3, 1:3]
df
1
2
3
4
##                mpg cyl disp
## Mazda RX4 21.0 6 160
## Mazda RX4 Wag 21.0 6 160
## Datsun 710 22.8 4 108

把行名转换为单独的一列

1
rownames_to_column(df, var = "rowname")
1
2
3
4
##         rowname  mpg cyl disp
## 1 Mazda RX4 21.0 6 160
## 2 Mazda RX4 Wag 21.0 6 160
## 3 Datsun 710 22.8 4 108

把行索引转换为单独的一列

1
rowid_to_column(df, var = "rowid")
1
2
3
4
##   rowid  mpg cyl disp
## 1 1 21.0 6 160
## 2 2 21.0 6 160
## 3 3 22.8 4 108

函数式编程

向量化运算

1
a <- c(2, 4, 3, 1, 5, 7)

for()循环,让向量的每个元素乘以2

1
2
3
for (i in 1:length(a)) {
print(a[i] * 2)
}
1
2
3
4
5
6
## [1] 4
## [1] 8
## [1] 6
## [1] 2
## [1] 10
## [1] 14

事实上,R语言是支持向量化(将运算符或者函数作用在向量的每一个元素上),可以用向量化代替循环

1
a * 2
1
## [1]  4  8  6  2 10 14

达到同样的效果。

多说说列表

我们构造一个列表

1
2
3
4
5
6
a_list <- list(
num = c(8, 9),
log = TRUE,
cha = c("a", "b", "c")
)
a_list
1
2
3
4
5
6
7
8
## $num
## [1] 8 9
##
## $log
## [1] TRUE
##
## $cha
## [1] "a" "b" "c"

要想访问某个元素,可以这样

1
a_list["num"]
1
2
## $num
## [1] 8 9

注意返回结果,第一行是$num,说明返回的结果仍然是列表, 相比a_list来说,a_list["num"]是只包含一个元素的列表。

想将num元素里面的向量提取出来,就得用两个[[

1
a_list[["num"]]
1
## [1] 8 9

大家知道程序员都是偷懒的,为了节省体力,用一个美元符号$代替[[" "]]六个字符

1
a_list$num

在tidyverse里,还可以用

1
a_list %>% pluck(1)
1
## [1] 8 9

或者

1
a_list %>% pluck("num")
1
## [1] 8 9

列表 vs 向量

假定一向量

1
2
v <- c(-2, -1, 0, 1, 2)
v
1
## [1] -2 -1  0  1  2

我们对元素分别取绝对值

1
abs(v)
1
## [1] 2 1 0 1 2

如果是列表形式,abs函数应用到列表中就会报错

1
lst <- list(-2, -1, 0, 1, 2)
1
abs(lst)
1
## Error in abs(lst): non-numeric argument to mathematical function

报错了。用在向量的函数用在list上,往往行不通。

再来一个例子:我们模拟了5个学生的10次考试的成绩

1
2
3
4
5
6
7
8
exams <- list(
student1 = round(runif(10, 50, 100)),
student2 = round(runif(10, 50, 100)),
student3 = round(runif(10, 50, 100)),
student4 = round(runif(10, 50, 100)),
student5 = round(runif(10, 50, 100))
)
exams
1
2
3
4
5
6
7
8
9
10
11
12
13
14
## $student1
## [1] 90 64 92 51 93 67 73 89 71 88
##
## $student2
## [1] 52 84 92 74 75 81 98 99 71 88
##
## $student3
## [1] 65 66 59 79 73 62 63 74 51 98
##
## $student4
## [1] 60 80 83 77 63 90 73 93 76 94
##
## $student5
## [1] 70 63 55 74 79 80 77 59 94 81

很显然,exams是一个列表。那么,每个学生的平均成绩是多呢?

我们可能会想到用mean函数,但是

1
mean(exams)
1
2
3
## Warning in mean.default(exams): argument is not numeric or logical: returning
## NA
## [1] NA

发现报错了,可以看看帮助文档看看问题出在什么地方

1
?mean()

帮助文档告诉我们,mean()要求第一个参数是数值型或者逻辑型的向量。 而我们这里的exams是列表,因此无法运行。

那好,我们就用笨办法吧

1
2
3
4
5
6
7
list(
student1 = mean(exams$student1),
student2 = mean(exams$student2),
student3 = mean(exams$student3),
student4 = mean(exams$student4),
student5 = mean(exams$student5)
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
## $student1
## [1] 77.8
##
## $student2
## [1] 81.4
##
## $student3
## [1] 69
##
## $student4
## [1] 78.9
##
## $student5
## [1] 73.2

成功了。但发现我们写了好多代码,如果有100个学生,那就得写更多的代码,如果是这样,程序员就不高兴了,这太累了啊。于是purrr包的map函数来解救我们,下面主角出场了。

purrr

介绍之前,先试试

1
map(exams, mean)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
## $student1
## [1] 77.8
##
## $student2
## [1] 81.4
##
## $student3
## [1] 69
##
## $student4
## [1] 78.9
##
## $student5
## [1] 73.2

哇,短短一句话,得出了相同的结果。

map函数

map()函数的第一个参数是list或者vector (数据框是列表的一种特殊形式,因此数据框也是可以的), 第二个参数是函数。

函数 f 应用到list/vector 的每个元素,于是输入的 list/vector 中的每个元素,都对应一个输出。

最后,所有的输出元素,聚合成一个新的list。

整个过程,可以想象 list/vector 是生产线上的盒子,依次将里面的元素,送入加工机器。 函数决定了机器该如何处理每个元素,机器依次处理完毕后,结果打包成list,最后送出机器。

map函数家族

如果希望返回的是数值型的向量,可以这样写map_dbl()dbl 应该是 double 的缩写)

1
exams %>% map_dbl(mean)
1
2
## student1 student2 student3 student4 student5 
## 77.8 81.4 69.0 78.9 73.2

map_dbl()要求每个输出的元素必须是数值型

如果希望返回的结果是数据框,可以使用 map_df()

1
exams %>% map_df(mean)
1
2
3
4
## # A tibble: 1 × 5
## student1 student2 student3 student4 student5
## <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 77.8 81.4 69 78.9 73.2

更多函数见下图

额外参数

将每位同学的成绩排序,默认的是升序。

1
map(exams, sort)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
## $student1
## [1] 51 64 67 71 73 88 89 90 92 93
##
## $student2
## [1] 52 71 74 75 81 84 88 92 98 99
##
## $student3
## [1] 51 59 62 63 65 66 73 74 79 98
##
## $student4
## [1] 60 63 73 76 77 80 83 90 93 94
##
## $student5
## [1] 55 59 63 70 74 77 79 80 81 94

如果我们想降序排,需要在sort()函数里添加参数 decreasing = TRUE。比如

1
sort(exams$student1, decreasing = TRUE)
1
##  [1] 93 92 90 89 88 73 71 67 64 51

map很人性化,可以让函数的参数直接跟随在函数名之和

1
map(exams, sort, decreasing = TRUE)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
## $student1
## [1] 93 92 90 89 88 73 71 67 64 51
##
## $student2
## [1] 99 98 92 88 84 81 75 74 71 52
##
## $student3
## [1] 98 79 74 73 66 65 63 62 59 51
##
## $student4
## [1] 94 93 90 83 80 77 76 73 63 60
##
## $student5
## [1] 94 81 80 79 77 74 70 63 59 55

当然,也可以添加更多的参数,map()会自动的传递给函数。

匿名函数

我们也可以不用命名函数,而使用匿名函数。匿名函数顾名思义,就是没有名字的函数,

1
function(x) x - mean(x)

我们能将匿名函数直接放在map()函数中

1
2
exams %>% 
map(function(x) x - mean(x))

还可以更加偷懒,用~代替function(),但代价是参数必须是规定的写法,比如.x

1
exams %>% map(~ .x - mean(.x))

有时候,程序员觉得x还是有点多余,于是更够懒一点,只用., 也是可以的

1
exams %>% map(~ . - mean(.))

~ 告诉 map() 后面跟随的是一个匿名函数,. 对应函数的参数,可以认为是一个占位符,等待传送带的student1、student2到student5 依次传递到函数机器。

map2()

事实上,purrr()家族还有其它map()函数,可以在多个向量中迭代。也就说,同时接受多个向量的元素,并行计算。比如,map2()函数可以处理两个向量,而pmap()函数可以处理更多向量。

map2()函数和map()函数类似,不同在于map2()接受两个的向量,这两个向量必须是等长

map()函数使用匿名函数,可以用 . 代表输入向量的每个元素。在map2()函数, .不够用,所有需要需要用 .x 代表第一个向量的元素,.y代表第二个向量的元素

1
2
3
4
x <- c(1, 2, 3)
y <- c(4, 5, 6)

map2(x, y, ~ .x + .y)
1
2
3
4
5
6
7
8
## [[1]]
## [1] 5
##
## [[2]]
## [1] 7
##
## [[3]]
## [1] 9

pmap()

没有map3()或者map4()函数,只有 pmap() 函数可用(p 的意思是 parallel)

pmap()函数稍微有点不一样的地方:

  • map()map2()函数,指定传递给函数f的向量,向量各自放在各自的位置上
  • pmap()需要将传递给函数的向量名,先装入一个list()中, 再传递给函数f

事实上,map2()pmap()的一种特殊情况

1
map2_dbl(x, y, min)
1
## [1] 1 2 3
1
pmap_dbl(list(x, y), min)
1
## [1] 1 2 3

用在tibble

tibble本质上就是list,这种结构就是pmap()所需要的,因此,直接应用函数即可。

1
2
3
4
5
6
tibble(
a = c(50, 60, 70),
b = c(10, 90, 40),
c = c(1, 105, 200)
) %>%
pmap_dbl(min)
1
## [1]  1 60 40

匿名函数

pmap()可以接受多个向量,因此在pmap()种使用匿名函数,就需要一种新的方法来标识每个向量。

由于向量是多个,因此不再用.x.y,而是用..1, ..2, ..3 分别代表第一个向量、第二个向量和第三个向量。

1
2
3
pmap(
list(1:5, 5:1, 2), ~ ..1 + ..2 - ..3
)

命名函数

1
2
3
4
5
6
7
8
params <- tibble::tribble(
~ n, ~ min, ~ max,
1L, 0, 1,
2L, 10, 100,
3L, 100, 1000
)

pmap(params, ~runif(n = ..1, min = ..2, max = ..3))
1
2
3
4
5
6
7
8
## [[1]]
## [1] 0.05862021
##
## [[2]]
## [1] 26.21417 86.82195
##
## [[3]]
## [1] 386.8337 558.0976 604.6650

如果提供给pmap().f 是命名函数,比如runif(n, min = , max = ),它有三个参数 n, min, max, 而我们输入的列表刚好也有三个同名的元素,那么他们会自动匹配,代码因此变得更加简练

1
pmap(params, runif)
1
2
3
4
5
6
7
8
## [[1]]
## [1] 0.09750563
##
## [[2]]
## [1] 85.51192 39.48302
##
## [[3]]
## [1] 879.0590 638.0751 596.4989

当然,这里需要注意的是

  • 输入列表的元素,其个数要与函数的参数个数一致
  • 输入列表的元素,其变量名也要与函数的参数名一致
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2019-2025 Vincere Zhou
  • 访问人数: | 浏览次数:

请我喝杯茶吧~

支付宝
微信