2.9 向量化的ifelse()函数
除了多数语言中常见的if-then-else结构,R还有一个向量化的版本:ifelse()函数。它的形式如下:
其中b是一个布尔值向量,而u和v是向量。
该函数返回的值也是向量,如果b[i]为真,则返回值的第i个元素为u[i],如果b[i]为假,则返回值的第i个元素为v[i]。这一概念相当抽象,因此我们看一个例子:
在这里,我们希望产生一个向量,这个向量在x中对应元素为偶数的位置取值是5,且在x中对应元素为奇数的位置取值12。因此,对应到形式参数b的实际参数是(F,T,F,T,F,T,F,T,F,T)。对应到u的第二个实际参数5,通过循环补齐成为(5,5,...)(十个5)。第三个参数12,同样循环补齐为(12,12,...)。
这里有另一个例子:
我们返回的向量由x的元素乘以2或3构成,到底是乘以2还是乘以3,取决于该元素是否大于6。
再次申明,弄明白这里真正发生了什么很重要。表达式x>6是一个布尔值向量。如果第i个元素为真,则返回值的第i个元素将被设定为2x的第i个元素,否则,它将被设定为3x[i],以此类推。
ifelse()相对于标准的if-then-else结构的优点是,它是向量化语句,因此有可能快很多。
2.9.1 扩展案例:度量相关性
评估两个变量的统计关系时,除了标准相关性度量方法(Pearson级差相关系数)之外,还有几种备选方法。一些读者可能听过的Spearman秩相关。这些度量方法有不同的目标,比如针对异常值的稳健性,异常值指的是那些取值很极端的数据,即很有可能出错的数据。
在这里,我们提出一种新的度量方法,这不是统计学上的新发现(实际上它与广泛使用的Kendall’s τ方法有关),只是为了阐述本章中的一些R编程技术,尤其是ifelse()。
考虑向量x和y,它们是时间序列,比如它们是每小时收集的气温和气压测量值。我们定义两者的相关性为x和y同时上升或下降次数占总观测数的比例,即y[i+1]-y[i]与x[i+1]-x[i]符号相同时的次数占总数i的比例。以下是代码:
在本例中,x和y在10次中同时上升3次(第一次同时上升是12到13,2到3),并同时下降1次。得到相关性的估计为4/10=0.4。
让我们看看它是如何工作的。首先需要把x和y的值编码为1和-1,1代表当前观测值较上期增加。这是在第5行和第6行进行。
例如,当我们对16个元素的v,调用findud()函数,想想看在第5行发生了什么。v[-1]会是一个15元素的向量,它从v的第2个元素开始。同样,v[-length(v)]也将是一个15元素的向量,它从v的第1个元素开始。结果是我们用右移一个时间段的序列值减去原始序列值。这个差值序列给我们提供了每个时间段序列增长/减少的状态—这正是我们想要的。
然后,我们需要依据差值的正负来把差值变换成1和-1。调用ifelse()可以简单而简洁地做到,并且它比循环版本的代码耗费更短的执行时间。
这里本应该写两个调用findud()的语句,一次对x,另一次对y。但实际上,我们把x和y放入列表中,然后使用lapply()函数,这样可以避免重复的代码。如果我们对很多向量采用相同操作,而不是只用两个,尤其是对于向量个数可变的情况,像这样使用lapply()可以使代码更加简洁明了,并且它可能稍快一些。
然后计算匹配的比例,如下所示:
需要注意的是lapply()返回一个列表。其组件是以1或-1编码的向量。语句ud[[1]] == ud[[2]]返回一个向量,其值由TRUE和FALSE构成,它们分别被mean()视作1和0。取均值就求出我们要的比例。
更高级的版本将使用R的diff()函数,该函数对向量做“滞后”运算。举例来说,我们可能要比较每个元素与它后面第三个元素(用术语说就是“滞后三期”)。默认的滞后期是一期,也就是我们这里所需的:
这无疑比最初的版本简短得多。但哪一个更好?对于大多数人,可能会用更长的时间才能想到这么写。并且尽管代码变短,但其实变得更难理解了。
所有的R程序员需要在简洁和清晰之间找到“恰到好处”的平衡点。
2.9.2 扩展案例:对鲍鱼数据集重新编码
由于其参数向量化的特性,ifelse()函数可以嵌套使用。下面的例子是鲍鱼的数据集,性别被编码为M、F或I(是Infant的缩写,这里意为幼虫)。我们希望将这些字符重新编码为1、2或3。实际的数据集包含超过4000条的观测值,但对于我们的例子,我们假设只有很少的数据存储在g中:
嵌套的ifelse()实际上做了什么?让我们仔细看一下。首先,为了表述更具体一些,我们找出函数ifelse()中形式参数的名字:
记住,对test中每一个取值为真的元素,函数使用yes中对应的元素作为结果。同样,如果test[i]值为假,则函数计算结果为no[i]。这样生成的所有值组成一个向量作为返回值。
在我们的例子中,R首先执行外层的ifelse(),其中的test是g==”M”,yes是1(会被循环补齐);no将是(之后)执行ifelse(g=="F",2,3)得到的结果。现在由于test[1]取真,则生成yes[1],也就是1。因此,外部函数调用所得的返回值第一个元素是1。
下一步,R将计算test[2],该值为假,因此R需要计算no[2]。R现在需要执行内部的ifelse()调用。之前并没有这样做,因为直到现在才需要它。R使用“惰性求值”(lazy evaluation)的原则,这意味着只有当需要时表达式才被计算,否则不计算。
R现在将计算ifelse(g=="F",2,3),得到(3,2,2,3,3,3,2),这是外部ifelse()的参数no,因此后者返回的第二个元素将是(3,2,2,3,3,3,2)中的第二个元素,即2。
当外层ifelse()函数调用执行到test[4]时,其取值为假,因此将返回no[4]。由于R已经计算过no,它有所需的值,即3。
需要注意,涉及的向量可能是矩阵的列,这是一个非常常见的情况。假设鲍鱼数据存储在矩阵ab中,性别是它的第一列。如果我们想像前例一样对其重新编码的话,可以这样做:
假设我们希望按照性别形成子集。可以使用which()来寻找M、F和I对应元素的编号。
更进一步,我们可以把这些子集保存在一个列表中,像如下这样:
需要注意的是,R的for()循环可以对字符串向量进行循环,我们正是利用了这一事实。(在4.4节中,你会看到一种更有效的方法。)
我们可以使用编码后的数据来绘制一些图形,探索鲍鱼数据集中的各种变量。通过给文件添加以下表头来概括变量的性质:
首先,我们读取数据集,将其赋值给变量aba为了提示我们这是鲍鱼数据)。read.csv()类似于在第1章使用过的read.table(),我们将在第6章和第10章讨论。然后构造abam和abaf,分别是aba下对应雄性和雌性的两个子矩阵。
接下来,我们来作图。第一条作图命令绘制了雄性鲍鱼直径对长度的散点图。第二条命令绘制的是雌性的图。因为希望此图与雄性的叠加在同一张图形上,我们设置参数new=FALSE,告诉R不要创建一个新的图形。参数pch=”x”意思是我们希望绘制在雌性图形上的字符用x,而不是默认的o字符。
图2-1显示了(整个数据集的)图形。顺便说一句,它并不十分让人满意。显然,直径和长度具有很强的相关性,以至于这些点密集地填充了部分图形,雄性和雌性的图形几乎完全一致(尽管雄性具有更多的变化)。这在统计图形中是一个常见的问题。更精细的图形分析会更有启发性,但至少我们看到了强相关的证据,而相关性在性别上的差距并不是很大。
在前面的例子中,我们可以用ifelse来压缩绘图代码。利用这样一个事实:pch参数可以是向量而不仅仅是单个字符。换句话说,我们可对每个点绘制不同的字符。
(在这里,我们省略了重新编码为1、2和3的过程,但出于某些原因,你可能希望保留它。)
请发表评论