系列前言:我的公众号从来没写过编程类的文章,虽然这可能是我除统计外最擅长的另一板斧。不写的原因很简单,好的系统性的教程/教材有很多(详见《推荐一些看过的R语言书(附点评,学习建议和下载链接)》)。靠看我的公众号文章来学习基础编程的人,其信息搜集能力和学习能力大概率是学不好编程的……所以我的文章不会有太大的意义。
但是这两天突然闪现了一丝灵感,打算就某个小的点做一个完全实践导向的,针对提升数据预处理技能的R语言教程。这个教程的关注点十分狭窄,旨在帮助学习者能够将科研中遇到的数据集,进行快速批量预处理,但也十分的实用,同时又是我自己比较有发言权的(我写过很多社科复杂数据集的预处理代码)。这个系列打算写个三到五期,从一些比较实用的基础技巧到实战的项目数据集预处理。每一期会写一篇教程,再附一篇作业习题(习题会设定少额付费)。
1 向量的索引
前两期我们主要讲一些实用的基础小技巧,它们有可能会在教程的后续用到,也有可能用不到,但是它能够大大增加我们的工作效率以及对很多R语言代码的底层理解。
我们先讲R语言中向量“索引”(Index)的底层逻辑。
假如我们有以下这个3元素的向量Vec1:
> Vec1 <- c(4,5,6)
我们知道,我们可以通过下述形式调取向量中的单个元素:
> Vec1[1] #调取第一个元素
[1] 4
或者可以通过下述形式来调取两个元素:
> Vec1[c(1,3)] #调取第一、三个元素
[1] 4 6
你可能会疑惑,我们为什么不能写成Vec1[1,3]呢?原因是:当我们对向量中的元素进行索引时,索引使用的目标必须也是一个向量!上述的c(1,3)自身就构成了一个向量。
这时你可能更疑惑了,不对呀!Vec[1]这个索引命令中,1就是一个单个元素/标量(Scalar)啊,不是一个向量啊!这里需要提醒大家的是,R语言把所有单个元素/标量都看作向量,也就是一个元素的向量。这样是不是就完全说得通了?
比如我现在想要提取前两个元素,我可以用下述两种方式进行提取:
> Vec1[c(1,2)]
> Vec1[1:2]
c(1,2)和1:2都帮助我们生成了一个(1,2)的向量进行索引。下面我们来讲讲“布尔型”(Boolean)索引,我们知道,我们不仅可以创建数值型向量,也可以创建布尔型向量,比如:
> Vec2 <- c(TRUE, FALSE, FALSE)
我们可以用这个布尔型向量作为索引向量去索引Vec1中的元素:
> Vec1[Vec2]
[1] 4
这个代码会做什么呢?它会依据Vec2中的第i个值依次判断是否需要输出Vec1中的第i个值。在目前的情况下,Vec2中仅有第一个元素是TRUE,所以这一代码只会输出Vec1的第一个元素,它和Vec1[1]是等效的。
你可能会疑惑我们为什么舍近求远,费那么大力提取第一个元素。先别急!看完下一个知识点你就知道了!
2 回收与向量化运算
我们接下来讲R语言中的回收(Recycling)和向量化运算(Vectorized computing)。回收这个名字比较不直观,大家可以忽略这个名字,直接看下述实例。回顾我们刚刚创建的Vec1:
> Vec1 <- c(4,5,6)
如果我们写下如下的代码:
> Vec1 + 3
首先,我们需要强调的是,连高中生也知道,这个代码在数学上是错误的!因为我们没有办法将一个向量与一个常数进行相加。但是R语言是编程语言,不是数学语言,它不会通过数学的角度去解读这一个算式,R语言会将上述代码的3扩充成一个充满3的,和Vec1长度相同的向量,也就是如下:
> Vec1+c(3,3,3)
[1] 7 8 9
现在Vec1只有三个元素,所以我们扩充写也没有麻烦很多,但是如果Vec1长度不确定,或者其长度为1000呢?事情就没那么简单了!所以R语言的开发者们开发了这个“回收”的特性:当我们进行运算的两个向量长度不同时,R语言会自动将较短的向量重复填充补齐到较长向量的长度。这里再回顾一下,上述的3虽然只是一个数字,但是我们提过R语言也将其看作向量,所以上述的操作也符合我们所说的回收的特性。我们再来看一个例子:
Vec3 <- c(1,2,3,4,5,6)
Vec4 <- c(1,0)
Vec3 + Vec4
R语言会如何执行这个加法运算呢?根据我们上述描述的回收特性,它会将较短的Vec4重复填充到与Vec3相同的长度,也就是说上述的运算等同于:
> c(1,2,3,4,5,6)+c(1,0,1,0,1,0)
[1] 2 2 4 4 6 6
看完加法运算,我们来看看乘法,假如我们有如下运算:
> Vec3 * 3
我们学了回收,知道R语言会对3进行填充补齐(再次强调,3也是一个向量):
> Vec3 * c(3,3,3,3,3,3)
可是这个运算又是什么意思呢?大家肯定知道数学上定义的点积(inner product)运算,但是数学上向量乘法是没有被定义的,所以这又是一个数学上没有意义的表达。R语言作为程序语言,定义了“向量化运算”,它会将上述乘法识别为:将两个(长度相等的)向量中的元素对应相乘,获得一个新的,相同长度的向量。上述的代码的向量化运算过程可被总结为:
c(1*3, 2*3, 3*3, 4*3, 5*3, 6*3)
[1] 3 6 9 12 15 18
向量化运算实际上就是一个“按元素进行”(Element-wise)的运算。
3 索引与回收的结合
我们最后来聊聊如何将索引与回收结合起来,帮助我们更高效地写代码。
假如我们现在有一个向量T,其中储存了一组被试做某一任务的反应时数据:
> T <- c(0.58, 0.96, 0.87, 0.99, 1.12,
0.96, 0.73, 0.62, 0.72, 1.11)
如果我们现在想要输出T中所有大于0.9的反应时,我们该如何做呢?我们可以新建一个如下的向量A:
> A <- T > 0.9
T>0.9是什么意思呢?这里我们要补充说明一下,R语言也会对“逻辑运算”进行回收,所以它会将>0.9补齐到“T长度”次的>0.9逻辑运算,或者说它会判断T中的每个元素是否大于0.9,并且输出一个布尔型向量。我们可以把上述过程具像化为:
> c(0.58 > 0.9, 0.96 > 0.9, 0.87 > 0.9, 0.99 > 0.9,
1.12 > 0.9, 0.96 > 0.9, 0.73 > 0.9, 0.62 > 0.9,
0.72 > 0.9, 1.11 > 0.9)
[1] FALSE TRUE FALSE TRUE
TRUE TRUE FALSE FALSE
FALSE TRUE
它最后会输出一个布尔型向量,这个布尔型向量中TRUE的位置对应了T中大于0.9的元素的位置。
结合前面索引的知识,我们可以机智地运用A对T进行索引,从而实现“输出T中所有大于0.9的反应时”的目的,或者我们可以直接写为:
T[T>0.9]
是不是很简单?如果你平时代码经验,你会发现上述规则和实例已经可以在你平时的数据处理中有很多应用了。
你可能会说你虽然不知道回收和索引规则,但是这个索引方式你本来就已经会了,学这个是不是浪费时间?当然不是,清楚代码背后的底层规则,能够帮助你更加灵活地利用这些规则,在不同的情境下高效地解决多种问题。
你可能疑惑,那么向量化运算最后在哪里用到了?其实,布尔型向量索引本身也是一个就是一种向量化运算(按元素进行),但是我们不在这里展开聊了。
如上,就是第一期“用R语言实现高效数据预处理”的全部内容,上述知识点还会有很多的变式情境与应用(比如应用于矩阵),我会在第一期的作业中来帮助大家巩固和掌握这些点。作业会设置3-5元的小额收费,大家届时可以根据上述内容来判断自己是否有需要购买。