0.3 R语言编程思想
0.3.1 面向对象
R语言是一种基于对象的编程语言,即在定义类的基础上,创建与操作对象,而数值向量、函数、图形等都是对象。Python的一切皆为对象也适用于R语言。
a = 1L
class(a)
## [1] “integer”
b = 1:10
class(b)
## [1] “integer”
f = function(x) x + 1
class(f)
## [1] “function”
早期的R语言和底层R语言中的面向对象编程是通过泛型函数来实现的,以S3类、S4类为代表。新出现的R6类更适合用来实现通常所说的面向对象编程,包括类、属性、方法、继承、多态等概念。
面向对象的内容是R语言编程的高级内容,本书不做具体展开,只在附录中提供一个用R6类面向对象编程的简单示例。
0.3.2 面向函数
笼统地来说,R语言的主要工作就是对数据应用操作。这个操作就是函数,包括R语言自带的函数,各种扩展包里的函数以及自定义的函数。
所以,使用R语言的大部分时间都是在与函数打交道,学会了使用函数,R语言也就学会了一半,很多人说R简单易学,也是因此。
代码中的函数是用来实现某个功能。很多时候,我们使用R语言自带的或来自其他包中的现成函数就够了。
那么,如何找到并使用现成函数解决自己想要解决的问题?比如想做线性回归,通过查资料
知道是用R语言自带的lm()
函数实现。那么先通过以下命令打开该函数的帮助,如图0.8所示:
?lm
图0.8 R函数的帮助页面
执行“?
函数名”(若函数来自扩展包需要事先加载包),在Rstudio
右下角窗口打开函数帮助界面,一般至少包括如下内容:
● 函数描述(Description);
● 函数语法格式(Usage);
● 函数参数说明(Arguments);
● 函数返回值(Value);
● 函数示例(Examples)。
先阅读函数描述、参数说明、返回值,再调试示例,我们就能快速掌握该函数的使用方法。
函数包含很多参数,常用参数往往只是前几个。比如lm()
的常用参数如下所示。
● formula
:设置线性回归公式形式“因变量~自变量+自变量”。
● data
:提供数据(框)。
接下来使用自带的mtcars
数据集演示,按照函数参数要求的对象类型提供实参:
head(mtcars)
## mpg cyl disp hp drat wt qsec vs am gear carb
## Mazda RX4 21.0 6 160 110 3.90 2.620 16.46 0 1 4 4
## Mazda RX4 Wag 21.0 6 160 110 3.90 2.875 17.02 0 1 4 4
## Datsun 710 22.8 4 108 93 3.85 2.320 18.61 1 1 4 1
## Hornet 4 Drive 21.4 6 258 110 3.08 3.215 19.44 1 0 3 1
## Hornet Sportabout 18.7 8 360 175 3.15 3.440 17.02 0 0 3 2
## Valiant 18.1 6 225 105 2.76 3.460 20.22 1 0 3 1
model = lm(mpg ~ disp, data = mtcars)
summary(model) # 查看回归汇总结果
##
## Call:
## lm(formula = mpg ~ disp, data = mtcars)
##
## Residuals:
## Min 1Q Median 3Q Max
## -4.8922 -2.2022 -0.9631 1.6272 7.2305
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 29.599855 1.229720 24.070 < 2e-16 ***
## disp -0.041215 0.004712 -8.747 9.38e-10 ***
## ---
## Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ‘ 1
##
## Residual standard error: 3.251 on 30 degrees of freedom
## Multiple R-squared: 0.7183, Adjusted R-squared: 0.709
## F-statistic: 76.51 on 1 and 30 DF, p-value: 9.38e-10
所有的R函数,即使是陌生的,也都可以这样来使用。
编程中一种重要的思维就是函数式思维,包括自定义函数(把解决某问题的过程封装成函数)和泛函式编程(把函数依次应用到一系列的对象上)。
如果找不到现成的函数解决自己的问题,那就需要自定义函数,R自定义函数的基本语法如下:
函数名 = function(输入1, ..., 输入n) {
...
return(输出) # 若有多个输出, 需要打包成一个list
}
比如,想要计算很多圆的面积,就有必要把如何计算一个圆的面积定义成函数,需要输入半径,才能计算圆的面积:
AreaCircle = function(r) {
S = pi * r * r
return(S)
}
有了函数之后,再计算圆的面积,你只需要把输入给函数,它就能在内部进行相应处理,把你想要的输出结果返回给你。如果想批量计算圆的面积,按泛函式编程思维,只需要将该函数依次应用到一系列的半径上即可。
比如计算半径为5
的圆的面积和批量计算半径为2
、4
和7
的圆的面积,代码如下所示:
AreaCircle(5)
## [1] 78.53982
rs = c(2,4,7)
map_dbl(rs, AreaCircle) # purrr包
## [1] 12.56637 50.26548 153.93804
定义函数就好比创造一个模具,调用函数就好比用模具批量生成产品。使用函数最大的好处就是将某个功能封装成模具,从而可以反复使用。这就避免了写大量重复的代码,程序的可读性也大大加强。
0.3.3 向量化编程
高级编程语言提倡向量化编程[2],说白了就是对一列数据、矩阵或多维数组中的数据同时做同样的操作,既提升程序效率又大大简化代码。
[2] 向量化编程中的向量,泛指向量、矩阵、多维数组。
向量化编程关键是要用整体考量的思维来思考和表示运算,这需要用到线性代数的知识,其实我觉得线性代数最有用的知识就是用向量、矩阵表示运算。
比如考虑n元一次线性方程组:
若从整体的角度来考量,可以引入矩阵和向量:
前面的n元一次线性方程组,可以向量化表示为:
可见,向量化表示大大简化了表达式。这放在编程中,就相当于本来用两层for
循环才能表示的代码,简化为短短一行代码。
向量化编程其实并不难,关键是要转变思维方式:很多人学完C语言的“后遗症”,就是首先想到的总是使用for
循环。想摆脱这种思维,可以调动头脑里的线性代数知识,尝试用向量、矩阵表示,长此以往,向量化编程思维就有了。
下面以计算决策树算法中的样本经验熵为例来演示向量化编程。
对于分类变量D,表示第类数据所占的比例,则D的样本经验熵为:
其中,表示集合包含的元素个数。
在实际需求中,我们经常遇到要把数学式子变成代码,与前文所谈到的一样,首先你要看懂式子,用简单实例逐代码片段调试就能解决。
以著名的“西瓜书”(《机器学习》)中的西瓜分类数据中的因变量“好瓜”为例,表示是否为好瓜,取值为“是”和“否”:
y = c(rep(“是”, 8), rep(“否”, 9))
y
## [1] “是” “是” “是” “是” “是” “是” “是” “是” “否” “否” “否” “否” “否”
## [14] “否” “否” “否” “否”
则D分为两类:为好瓜类,为坏瓜类。
从内到外先要计算,用向量化的思维同时计算,就是统计各分类的样本数,再除以总样本数:
table(y) # 计算各分类的频数, 得到向量
## y
## 否 是
## 9 8
p = table(y) / length(y) # 向量除以标量
p
## y
## 否 是
## 0.5294118 0.4705882
继续代入公式计算,谨记R自带的函数天然就接受向量做输入参数:
log(p) # 向量取对数
## y
## 否 是
## -0.6359888 -0.7537718
p * log(p) # 向量乘以向量, 对应元素做乘法
## y
## 否 是
## -0.3366999 -0.3547161
- sum(p * log(p)) # 向量求和
## [1] 0.6914161
看着挺复杂的公式用向量化编程之后,核心代码只有两行:计算p
和最后一行。这个实例虽然简单,但基本涉及所有常用的向量化操作:
● 向量与标量做运算;
● 向量与向量做四则运算;
● 把函数作用到向量。