R语言是一门学习路线陡峭的编程语言,其中大大小小的沟壑无数。作为函数式编程语言,首先需要对R的函数进行深入的学习。
1. 为什么要进行函数式编程?R函数编程初步
简单来说,在R语言中采用函数式编程就是为了提高效率同时减少错误。下面采用例子的方式进行说明:
首先我们建立一个随机的数据框df
set.seed(1014)
df <- data.frame(replicate(6, sample(c(1:10, -99), 6, replace = TRUE)))
names(df) <- letters[1:6]
df
该数据如下图
为对该数据框中的缺失值(-99)进行替换(替换为R标准的缺失值NA),我们可以采用如下代码:
df$a[df$a == -99] <- NA
显然,该方法针对每列需重复6次,且每次复制粘贴语句需修改代码中的两个字母(复制粘贴并修改字母是很多初学者都会做的,但根据经验犯错概率也是相当高的)。
针对该问题,我们采用编写函数的方式对缺失值进行处理。
fix_missing <- function(x) {
x[x == -99] <- NA
x
}
fix_missing(df$a)
运行该段代码,则返回缺失值处理后的df$a,如下图
> fix_missing(df$a)
[1] 1 10 7 2 1 6
此时,该函数减少了出错误的可能性,即规范了“将某列中的-99赋值为NA”的过程,但如果此时仍采用复制粘贴6遍的方式处理每一列,仍可能出错,如下:
df$a <- fix_missing(df$a)
df$b <- fix_missing(df$b)
df$c <- fix_missing(df$c)
df$d <- fix_missing(df$d)
df$e <- fix_missing(df$e)
df$f <- fix_missing(df$g)
最后一行在手动修改过程中依然出错。此时的解决方法就是使用lapply函数(R中最重要的函数之一),对数据框df中的每列数据(本质为list,“lapply”也即“list apply”,是对每列数据应用相同的函数进行计算),具体实现如下:
df <- lapply(df, fix_missing)
此时,fix_missing函数被应用于df数据框中的每一列中,但输出结果和我们想象中存在差别,数据框df被赋值成为list的数组
可采用如下两种方式进行修改:
# method 1
df <- as.data.frame(lapply(df, fix_missing))
# method 2
df[] <- lapply(df, fix_missing)
方法1:将缺失值处理后的list数组重新整合为data.frame
方法2:利用原有的df,获得处理缺失值后的dataframe。
个人而言,从语句简洁和内存占用两方面考虑,我会选择方法2.(当然,方法2更能体现R程序员对R运行机制的理解和自身水平)
当仅需对部分列进行处理时,采用lapply也很方便,如下
df[1:4] <- lapply(df[1:4], fix_missing)
总而言之,学会使用函数和lapply()的组合,可以极大提高编程及后续的运行效率。
注:
#1. lapply() 使用C语言实现,因此效率较高。采用R语言的循环语句也可以实现类似的功能,代码如下:
out <- vector("list", length(df))
for(i in seq_along(df)) {
out[[i]] <- fix_missing(df[[i]])
}
out <- as.data.frame(out)
names(out) <- letters[1:6]
#2. 最后我们讲解下lapply的另一方便应用。在数据分析中,如果想获取同一数据集的多个指标(如中位数、均值和标准差),一种方法是依次调用多个函数,如下:
median(df$a)
mean(df$a)
sd(df$a)
但如果要继续计算df$b列的结果,那就又再次进入了复制粘贴重复代码的漩涡了。此时需要用到的技术就是R语言中强大的列表存储函数功能,即可将函数名存储在列表中,而后采用lapply()对列表中的每个函数都进行一次运算,代码如下:
summary_rewrite <- function(x) {
funs <- c(median, mean, sd)
lapply(funs, function(f) f(x, na.rm = TRUE))
}
summary_rewrite(df$a)
2. 匿名函数和闭包
如果对python编程有初步经验,那么一定对python中lambda函数印象深刻,R中也有类似的匿名函数可以使用。使用匿名函数进行编程可省去大量重复命名函数的痛苦(很多时候编程中最痛苦的事情就是命名了...)。
上一节中讲过lapply()的用法,即
lapply(数据框/列表数组,函数名)
但很多时候往往只想对列表数组中每个列表进行很简单的操作,那根本没有必要再写一个新函数并且命名(有时也实在不好命名),那就可以采用匿名函数。比如我要实现一个功能是计算每列的均值,并且给均值平方,那么实现的代码如下:
lapply(df, function(x) mean(x, na.rm = TRUE)^2)
该语句中就采用了匿名函数,当函数体较为简单时,使用匿名函数可以提高编程效率。
当在lapply外使用匿名函数时需要注意的是需要采用括号,将函数体和输入区分开,如下:
(function(x) 3) (3)
(function(x) x + 100) (100)
以上两行代码完全是为了理解原理,实际使用中很少构造这样恶心的东西。实际上,匿名函数最重要的应用之一就是构造闭包,此处需要加重,闭包!!!
如果你有JAVA或者JS的编程经验,那么你一定对闭包有着深刻的理解,或者深恶痛绝,然而在函数编程驱动的R语言中,闭包发挥的作用还是挺大的,深刻地理解R语言的闭包并学会使用,是由R语言调包小白升级成为“会编一点”的R语言大白的重要一步。
一个伟人说过:闭包就是分家单过的儿子(子函数),但是他把爸爸(父函数)的家(父函数的数据和功能-工作环境)席卷了一空,连带把爸爸也接走了。——(By SteveHuxtable in 大二)
比方说我有一项工作是要计算某个数字减n的平方,如果按照朴素的想法,我们会构造一个函数如下
minN_then_square <- function(x, n) {
(x - n)^2
}
minN_then_square(10, 9)
但这个函数客观来说不够优雅,如果你的需求是求分别减去1,2,3,...,10后的平方结果,那么代码又会变为冗杂的
minN_then_square(10, 1)
minN_then_square(10, 2)
minN_then_square(10, 3)
minN_then_square(10, 4)
minN_then_square(10, 5)
minN_then_square(10, 6)
minN_then_square(10, 7)
minN_then_square(10, 8)
minN_then_square(10, 9)
这还算好,但如果需求是分别求10到100分别减去1,2,3,...,10后的平方结果呢,灾难出现了,解决方法1则是开始运用循环语句,不过不够优雅,解决方法二就是采用闭包及工厂模式(优雅并高效)。
minN_then_square <- function(N) {
function(x) {
(x - N) ^ 2
}
}
min1_then_square <- minN_then_square(1)
min2_then_square <- minN_then_square(2)
min3_then_square <- minN_then_square(3)
此时的min1_then_square就是一个闭包。它带有两方面内容:1)父环境定义的运算; 2)N=1这一数据
此时的min1_then_square()就可以直接实现减1后平方的功能,如下:
闭包就是将父函数的数据和运算封装到一起的新函数。
下面进入关键的函数工厂的讲解。
为啥要使用函数工厂的设计模式?(当然如果你懂啥是设计模式,那就不需要再学了)
如上文所说,有些问题需要遍历数组,寻找一个方程某个系数的合适的值(如机器学习中的梯度下降算法),在这种情况下,我们需要一方面保持函数的结构的稳定,另一方面又要将大量的数代入函数进行计算。此时,如果没有稳定的新函数生成模式,那么代码会冗杂,同时易出错。举例来说,就好像可口可乐公司要尝试新logo印出来打印在饮料瓶上应该多大最合适,这个工作一定是已经制作好了打印机,而后用打印机重复性地将不同大小的logo打印在饮料瓶上,而不是每次都要重新安装一次打印机。
其实上文中,已经涉及了函数工厂的思想,后续文章会对函数工厂和闭包应用进行更深的讲解。
附:
#1. 由于还没有讲解变量和函数的运行环境,那么我们就简单讲下相关内容。
在R语言中,一个变量的存在范围是有限的,比如在某个函数中创建的变量i,在这个函数以外就无法访问,如下
# global env
i = 10
output_i <- function(x) {
# local i in func
i <- x
i
}
output_i(100) #输出100
i #输出10
可见,无论在output_i函数为i赋值为多少,当脱离函数的局部环境返回全局环境是,i依然是10.
也可在局部环境中对全局环境中的变量进行修改,其方法就是将“<-”改为"<<-",但这种修改是极其危险的。
# global env
i = 10
output_i <- function(x) {
# local i in func
i <<- x
i
}
output_i(100) #输出100
i #输出100
讨论至此,那么闭包究竟是什么呢?回归基本概念,闭包就是在创建时将父函数空间中的变量和运算打包封装的函数。每当创建一个闭包(子函数)时父函数的变量和运算都会被复制一份给闭包,且这些变量和运算是不会被垃圾回收的。
我们写一个简单的闭包,对闭包的概念进行解释
# a simple one
multi10 <- function() {
i = 10
function() {
i <<- i * 10
i
}
}
multi10_1 <- multi10()
multi10_2 <- multi10()
上段代码展示了闭包的写法,闭包multi10_1和multi10_2这两个闭包实现了对数据i和函数的封装。两个闭包有着不同的环境,两个闭包中的变量i不会共享。为了实验,我们执行闭包函数以查看结果。
multi10_1() # [1] 100
multi10_1() # [1] 1000
multi10_2() # [1] 100
可见,当闭包multi10_1()中的变量i已经为1000后,另一个闭包中的i仍为10(运行函数后变为100)。
至此,我们已经对匿名函数、函数作用域,闭包等概念有了了解。在后续的R函数相关内容讲解中,匿名函数和闭包会有大量应用。
该内容会在每周一和周四夜间更新。
参考书目:
1. 《高级R语言编程指南》, Hadley Wickham, 2016.