R语言教程-R编程8

函数进阶

函数式编程

在map中使用无名函数以及简写方法

map调用的函数可以是在map中直接定义的函数

上面4个学生,每名3门成绩的问题。

library(purrr)
map_dbl(strsplit(s, split = ",", fixed = T),
function(x) sum(as.numeric(x)))

使用无名函数格式比较复杂, purrr包为在map()等泛函中使用无名函数提供了简化的写法, 将无名函数写成“~ 表达式”格式, 表达式就是无名函数定义, 用.表示只有一个自变量时的自变量名, 用.x.y表示只有两个自变量时的自变量名, 用..1..2..3这样的名字表示有多个自变量时的自变量名。 如:

map_dbl(strsplit(s, split = ",", fixed = T),
# 等同于function(x) sum(as.numeric(x)的写法
~ sum(as.numeric(.)))

无名函数中的其它变量在每次被map()应用到输入列表的元素时都会重新计算求值。 建议这样的情况改用有名函数, 这样其中访问其它变量时作用域规则比较容易掌控, 也不会重复求值。

map中提取列表元素成员的简写

较为复杂的数据, 有时表现为列表的列表, 每个列表元素都是列表或者向量。 JSON、YAML等格式转换为R对象就经常具有这种嵌套结构。

例如, 有如下的嵌套格式数据, 这样的数据不利于用数据框格式保存:

od <- list(
list(
101, name="李明", age=15,
hobbies=c("绘画","音乐")
),
list(
102, name="张聪", age=17,
hobbies=c("篮球"),
birth="1996-12-01"
)
)

为了取出每一项来,本来写成:

map_dbl(od, ~ .[[1]])
# 等于map_dbl(od, function(x) x[[1]])

purrr包提供了进一步的简化写法, 在需要一个函数或者一个“~ 表达式”的地方, 可以用整数下标值表示对每个列表元素提取其中的指定成分,如:

map_dbl(od, 1)
map_chr(od, 2)

类似地,可以在需要函数的地方写一个成员名, 提取每个列表元素中该成员,如:

map_chr(od, "name")

在应该用函数的地方还可以提供一个列表, 列表元素为成员序号或者成员名, 进行逐层挖掘,如:

map_chr(od, list("hobbies", 1))

表示取出每个列表元素的hobbies成员的第一个元素(每人的第一个业余爱好)。

取出不存在的成员会出错, 但可以用一个.default选项指定查找不到成员时的选项, 如:

map_chr(od, "birth", .default=NA)

问题:
如何取出两个List中的不一样长的hobbies项?

数据框分组处理示例

d.class

对d.class数据框, 希望分成男女生两个组, 每组内建立用身高预测体重的一元线性回归模型, 提取各模型的斜率项。 基本R的split函数可以按数据框的某列将数据框分成若干个子数据框, 结果为子数据框的列表。 借助于purrr包的map类函数和管道运算符, 可以将分组建模过程写成:

d.class %>%
# 结果为F和M两个tibble
split(d.class[["sex"]]) %>%
map(~ lm(weight ~ height, data=.)) %>%
# 这里的.是map需要输入的无名函数的自变量
map(coef) %>%
# 此处的.,可以省略
map_dbl(.,2)

这些步骤用基本R的lapply或者for循环也能够完成, 但会更难读懂, 或者需要生成许多中间临时变量, 不如上面例子这样步骤清晰而且不需要产生中间结果。

dplyr包和plyr包与这个例子的思想类似, 只不过更有针对性。

LM()即为Linear Models的缩写。lm是用来适应线性模型的。它可用于进行回归、单层分析方差分析和协方差分析。
Arguments | 参数

  • **formula:**指要拟合的模型形式,
  • **data:**是一个数据框,包含了用于拟合模型的数据。
    lm()函数举例:
a <- c(2, 3, 4)
b <- c(4, 6, 8)
# 线性回归模型,a是应变量,b是自变量
lm(a~b)

purrr包中map函数的变种

purrr包的map函数输入一个数据自变量和一个函数, 输出为列表; map_dbl()等将输出转化为基础类型的向量。
purrr包还提供了与map目的类似, 但输入输出类型有变化的一些函数, 包括:

  • modify(),输入一个数据自变量和一个函数, 输出与输入数据同类型的结果;
  • map2()可以输入两个数据自变量和一个函数, 将两个自变量相同下标的元素用函数进行变换, 输出列表;
  • imap()根据一个下标遍历;
  • walk()输入一个数据自变量和一个函数, 不返回任何结果,仅利用输入的函数的副作用;
  • 输入若干个数据自变量和一个函数, 对数据自变量相同下标的元素用函数进行变换;

输入输出类型相同的modify函数

对d.class中的三个数值型列, 都减去列中位数,其它列保持不变:

d1 <- modify(d.class, ~ if(is.numeric(.)) . - median(.) else .)

purrr包还提供了一个modify_if()函数, 可以对满足条件的列进行修改,

d2 <- modify_if(d.class, is.numeric, ~ . -median(.))
d2

对两个自变量的相同下标元素调用函数

map()函数仅支持一个输入数据的列表或向量。 map2()函数支持两个输入数据的列表或向量, map2(x, y, f, ...)对每个下标i调用f(x[[i]], y[[i]], ...), 结果返回一个列表。 如果知道函数f()会返回类型确定的标量值, 可以用map2_dbl()等变种。

例如, d1是某市2001年四个季度的若干项经济指标, d2是2002年的对应指标, 计算每项指标年度总和的同比增幅:

library(tibble)
d1 <- tibble(
x1 = c(106, 108, 103, 110),
x2 = c(101, 112, 107, 105) )
d2 <- tibble(
x1 = c(104, 111, 112, 109),
x2 = c(102, 114, 105, 107) )
# 其中d2,d1中的x1,x2分别相减
beis <- map2_dbl(d1,d2, ~ (sum(.y) - sum(.x)) / sum(.x))
# 显示百分比
baifs <- paste(beis*100, "%",sep='')
baifs

如果计算结果与两个输入数据类型相同, 可以用modify2()。 比如, 上面的例子数据计算每个指标的同比增幅:

modify2(d1,d2, ~(..2 -..1) / ..1)

map2()允许输入的x和y两个列表其中一个长度为1, 这时长度为1的列表的一个元素被重复利用。如:

d1b <- d1[,1, drop=F]
d1b
map2_dbl(d1b, d2, ~ (sum(.y) - sum(.x)) / sum(.x))

不产生输出的walk类函数

有时仅需要遍历一个数据结构调用函数进行一些显示、绘图, 这称为函数的副作用, 不需要返回结果。 purrr的walk函数针对这种情形。
显示数据框中每个变量的类别:

walk(d.class, ~ cat(typeof(.), "\n"))

上面这个例子缺点是没有显示对应的变量名。

walk2()函数可以接受两个数据自变量, 类似于map2()。 例如, 需要对一组数据分别保存到文件中, 就可以将数据列表与保存文件名的字符型向量作为walk2()的两个数据自变量。 下面的程序将d.class分成男女生两个子集, 保存到两个csv文件中:

# 按性别拆分为两个tibble
d1 <- split(d.class, d.class[["sex"]])
# paste0默认的连接符为空
walk2(d1, paste0("calss-", names(d1), ".csv"), ~ write.csv(.x, file = .y))

paste和paste0函数

paste函数可用于字符串连接

paste("a","b",sep = "=")#注意到用等号分隔了
## [1] "a=b"
#连接多个元素
paste("a",1:5,sep = "")#会自动每个元素与a相连
## [1] "a1" "a2" "a3" "a4" "a5"

paste("a",1:5,".pdf", sep = "")#比如想批量输出文件名
## [1] "a1.pdf" "a2.pdf" "a3.pdf" "a4.pdf" "a5.pdf"

sep是在每个元素的分隔符,collapse是元素之间的连接符

paste("a",1:5,sep = "")##先在元素间连接
## [1] "a1" "a2" "a3" "a4" "a5"
#然后折叠
paste("a",1:5,sep = "",collapse = "+")
## [1] "a1+a2+a3+a4+a5"

与paste0函数的区别:

paste(c("a", "b","c"),1:3)##默认空格符
## [1] "a 1" "b 2" "c 3"
paste0(c("a", "b","c"),1:3)#默认元素连接为sep=""
## [1] "a1" "b2" "c3"

改用管道运算符:

d.class %>%
split(d.class[["sex"]]) %>%
walk2(paste0("calss-", names(.), ".csv"), ~ write.csv(.x, file = .y))

walk、walk2并不是没有输出, 它们返回不显示的第一个自变量, 所以也适合用在管道运算的中间使得管道不至于中断。

可同时访问下标或元素名与元素值的imap类函数

walk函数显示数据框各列类型的例子中, 没有能够同时显示变量名。
如果x有元素名, imap(x, f)相当于imap2(x, names(x), f); 如果x没有元素名, imap(x, f)相当于imap2(x, seq_along(x), f)。 iwalk()与imap()类似但不返回信息。 调用的函数的第二个自变量, 或者无名函数的.y自变量是元素名或者元素下标。 imap_chr()等是固定返回类型的变种。

显示数据框各列的变量名:

iwalk(d.class, ~ cat(.y, ":", typeof(.x), sep = '', "\n"))

等同于:

iwalk(d.class, function(x,y) cat(y,":", typeof(x), sep = '', "\n"))

其他写法探索:

ft <- function(x,y){
for( i in seq_along(x)){
cat(y[i],":", x[i], sep = '', "\n")

}
}

ft(c('chr','chr','dbl','dbl','dbl'),names(d.class))

返回字符型向量的写法:

# unname() Remove the names or dimnames attribute of an R object.
imap_chr(d.class, ~ paste0(.y, " ==> ", typeof(.x))) %>% unname()

输入数据没有元素名的演示:

dn <- list(1:5, 101:103)
dn
iwalk(dn, ~ cat("NO. ", .y, ": ", .x[[1]], "\n"))