1.5 向量化与隐式循环
数据分析语言的有趣特征之一是函数可以应用许多不同的数据对象,如向量、矩阵、数组与数据集等,而非仅仅标量而已,此即称为向量化(vectorization),请看下面范例(Kabacoff,2015)。
上例中a为一常数(标量),而函数sqrt()如同计算机(calculator)一般执行于单值标量上。如果将函数round()与log()分别应用到一维向量或二维的矩阵,其结果如下:
从上面的结果读者不难发现,sqrt()、round()与log()等函数是施加在数据中的每一个元素上,但是有些函数就并非如此执行了!例如下面常用的平均值计算函数mean():
mean()函数计算矩阵m的12个元素的算术平均数,因此读者须经常注意输入的数据对象(此处m为3×4矩阵),经函数处理后产生的输出对象(上例传回单值),其维度是否改变?数据结构是否改变?类型是否改变?这是掌握数据驱动程序设计的重要概念(参见1.9节程序调试与效率监测)。
上例中如果要计算矩阵m三行的平均值或四列的平均值,可以运用apply()函数:
其语法为:
apply (m, MARGIN, FUN,...)
apply()函数是将FUN运算施加于矩阵或数组对象的某一维度上,其中m是数组(包括二维矩阵),MARGIN是给定运作维度的数值向量或字符串向量,FUN是欲应用的函数,而...是额外要传入FUN的参数值。m为二维的矩阵或数据集时,MARGIN=1表示逐行套用函数,MARGIN=2表示逐列套用函数。在数据驱动的程序语言中,R或Python的numpy与pandas等模块,建议避免写显式循环(explicit looping,即for循环),而以隐式循环(implicit looping)的apply()系列函数取代之,不过上例中apply()还是比rowMeans()或colMeans()等更直接的向量化函数慢。Python语言apply()方法的编程应用,请参见1.6节编程范式与面向对象概念、2.2.3节Python语言群组与摘要,以及4.2.2节在线音乐城关联规则分析等各节范例。
若为一维的向量或列表对象,可以使用lapply()或sapply()函数,两者执行方式相同,其中"s"意指简化(simplify),此函数在必要时将简化lapply()函数返回的数据对象。以下用简例说明两者的用法:
R语言apply系列函数众多,mapply()可施加一个函数于多个列表或向量的对应元素上,下例中firstList与secondList均是长度为3的列表对象,mapply()将identical()函数施加在上述两列表的对应元素上,判断其是否完全相同。
mapply()函数语法中的FUN也可以是如下自定义的匿名函数(anonymous function),计算firstList与secondList对应元素的列数和,其他apply系列函数也可以调用自定义的匿名函数。
活用mapply()函数有时可以快速完成某些分组处理或可视化的工作,下例在mapply()函数中定义绘制各组回归直线的匿名函数后,将其添加到iris数据集的Sepal.Width对Petal.Length的散点图上,然后在适当位置标出图例(图1.9)(Verzani,2014)。
图1.9 鸢尾花花萼宽度与花瓣长度分布情形及分组回归直线图
其实数据驱动程序设计中输入输出的变量符号(symbol)大多是数据对象,它们可能是一维、二维或更高维的结构。因此,数据分析语言多采用向量化数据处理与计算的方式,以避免额外循环执行,提升工作效率。许多运算符(如乘方运算符^、比较运算符>、加号+)及函数(如apply(),lapply(),sapply(),scale(),rowMeans(),colMeans())都是向量化函数,也就是说函数中隐藏着循环(implicit looping)的处理方式。所以再次提醒读者注意思考下面问题:输入的数据对象经函数处理后产生的输出对象,其维度是否改变?数据结构是否改变?类型是否改变?(参见1.9节程序调试与效率监测)反复思索上述问题可以掌握数据驱动程序设计背后的运行逻辑。