按行操作
向量化 R 代码的关键是减少或消除按行操作或 R 函数的方法调度。
这意味着当接近乍一看需要按行操作的问题时,例如计算每行的平均值时,需要问自己:
- 我正在处理的数据集的类是什么?
- 是否有现成的编译代码可以实现这一点而无需重复评估 R 函数?
- 如果没有,我可以按行而不是按行进行这些操作吗?
- 最后,是否值得花费大量时间来开发复杂的矢量化代码,而不是仅仅运行一个简单的
apply
循环?换句话说,数据是否足够大或复杂,以至于 R 无法使用简单的循环有效地处理它?
暂且不考虑内存预分配问题和循环中增长的对象,我们将在这个例子中关注如何避免 apply
循环,方法调度或重新评估循环中的 R 函数。
按行计算平均值的标准/简便方法是:
apply(mtcars, 1, mean)
Mazda RX4 Mazda RX4 Wag Datsun 710 Hornet 4 Drive Hornet Sportabout Valiant Duster 360
29.90727 29.98136 23.59818 38.73955 53.66455 35.04909 59.72000
Merc 240D Merc 230 Merc 280 Merc 280C Merc 450SE Merc 450SL Merc 450SLC
24.63455 27.23364 31.86000 31.78727 46.43091 46.50000 46.35000
Cadillac Fleetwood Lincoln Continental Chrysler Imperial Fiat 128 Honda Civic Toyota Corolla Toyota Corona
66.23273 66.05855 65.97227 19.44091 17.74227 18.81409 24.88864
Dodge Challenger AMC Javelin Camaro Z28 Pontiac Firebird Fiat X1-9 Porsche 914-2 Lotus Europa
47.24091 46.00773 58.75273 57.37955 18.92864 24.77909 24.88027
Ford Pantera L Ferrari Dino Maserati Bora Volvo 142E
60.97182 34.50818 63.15545 26.26273
但我们能做得更好吗?让我们看看这里发生了什么:
- 首先,我们将
data.frame
转换为matrix
。 (请注意,他发生在apply
函数中。)这既低效又危险。matrix
一次不能容纳多种列类型。因此,这种转换可能会导致信息丢失,有时会导致误导结果(将apply(iris, 2, class)
与str(iris)
或sapply(iris, class)
进行比较)。 - 其次,我们重复执行一次操作,每行一次。意思是,我们不得不评估一些 R 函数
nrow(mtcars)
次。在这个特定的情况下,mean
不是一个计算上昂贵的函数,因此即使对于大数据集,R 也可能很容易处理它,但是如果我们需要按行计算标准偏差会发生什么(这涉及昂贵的平方根操作) ?这将我们带到下一点: - 我们多次评估 R 函数,但是可能已经有了这个操作的编译版本?
确,我们可以做到:
rowMeans(mtcars)
Mazda RX4 Mazda RX4 Wag Datsun 710 Hornet 4 Drive Hornet Sportabout Valiant Duster 360
29.90727 29.98136 23.59818 38.73955 53.66455 35.04909 59.72000
Merc 240D Merc 230 Merc 280 Merc 280C Merc 450SE Merc 450SL Merc 450SLC
24.63455 27.23364 31.86000 31.78727 46.43091 46.50000 46.35000
Cadillac Fleetwood Lincoln Continental Chrysler Imperial Fiat 128 Honda Civic Toyota Corolla Toyota Corona
66.23273 66.05855 65.97227 19.44091 17.74227 18.81409 24.88864
Dodge Challenger AMC Javelin Camaro Z28 Pontiac Firebird Fiat X1-9 Porsche 914-2 Lotus Europa
47.24091 46.00773 58.75273 57.37955 18.92864 24.77909 24.88027
Ford Pantera L Ferrari Dino Maserati Bora Volvo 142E
60.97182 34.50818 63.15545 26.26273
这不涉及行操作,因此不重复评估 R 函数。但是,我们仍然将 data.frame
转换为 matrix
。尽管 rowMeans
具有错误处理机制,并且它不会在无法处理的数据集上运行,但它仍然具有效率成本。
rowMeans(iris)
Error in rowMeans(iris) : 'x' must be numeric
但是,我们能做得更好吗?我们可以尝试用错误处理代替矩阵转换,这种方法允许我们使用 mtcars
作为向量(因为 data.frame
本质上是 list
而 list
是 vector
)。
Reduce(`+`, mtcars)/ncol(mtcars)
[1] 29.90727 29.98136 23.59818 38.73955 53.66455 35.04909 59.72000 24.63455 27.23364 31.86000 31.78727 46.43091 46.50000 46.35000 66.23273 66.05855
[17] 65.97227 19.44091 17.74227 18.81409 24.88864 47.24091 46.00773 58.75273 57.37955 18.92864 24.77909 24.88027 60.97182 34.50818 63.15545 26.26273
现在为了获得可能的速度增益,我们丢失了列名和错误处理(包括 NA
处理)。
另一个例子是按组计算均值,使用我们可以尝试的基数 R.
aggregate(. ~ cyl, mtcars, mean)
cyl mpg disp hp drat wt qsec vs am gear carb
1 4 26.66364 105.1364 82.63636 4.070909 2.285727 19.13727 0.9090909 0.7272727 4.090909 1.545455
2 6 19.74286 183.3143 122.28571 3.585714 3.117143 17.97714 0.5714286 0.4285714 3.857143 3.428571
3 8 15.10000 353.1000 209.21429 3.229286 3.999214 16.77214 0.0000000 0.1428571 3.285714 3.500000
尽管如此,我们基本上是在循环中评估 R 函数,但循环现在隐藏在内部 C 函数中(无论是 C 还是 R 循环都无关紧要)。
我们能避免吗?那么在 R 中有一个叫做 rowsum
的编译函数,因此我们可以这样做:
rowsum(mtcars[-2], mtcars$cyl)/table(mtcars$cyl)
mpg disp hp drat wt qsec vs am gear carb
4 26.66364 105.1364 82.63636 4.070909 2.285727 19.13727 0.9090909 0.7272727 4.090909 1.545455
6 19.74286 183.3143 122.28571 3.585714 3.117143 17.97714 0.5714286 0.4285714 3.857143 3.428571
8 15.10000 353.1000 209.21429 3.229286 3.999214 16.77214 0.0000000 0.1428571 3.285714 3.500000
虽然我们也必须首先转换为矩阵。
在这一点上,我们可能会质疑我们当前的数据结构是否是最合适的。data.frame
是最好的做法吗?或者,为了提高效率,是否应该切换到 matrix
数据结构?
随着我们开始每次评估昂贵的功能,行操作将变得越来越昂贵(甚至在矩阵中)。让我们考虑行示例的方差计算。
让我们说我们有一个矩阵 m
:
set.seed(100)
m <- matrix(sample(1e2), 10)
m
[,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
[1,] 8 33 39 86 71 100 81 68 89 84
[2,] 12 16 57 80 32 82 69 11 41 92
[3,] 62 91 53 13 42 31 60 70 98 79
[4,] 66 94 29 67 45 59 20 96 64 1
[5,] 36 63 76 6 10 48 85 75 99 2
[6,] 18 4 27 19 44 56 37 95 26 40
[7,] 3 24 21 25 52 51 83 28 49 17
[8,] 46 5 22 43 47 74 35 97 77 65
[9,] 55 54 78 34 50 90 30 61 14 58
[10,] 88 73 38 15 9 72 7 93 23 87
人们可以这样做:
apply(m, 1, var)
[1] 871.6556 957.5111 699.2111 941.4333 1237.3333 641.8222 539.7889 759.4333 500.4889 1255.6111
另一方面,也可以通过遵循方差公式完全矢量化该操作
RowVar <- function(x) {
rowSums((x - rowMeans(x))^2)/(dim(x)[2] - 1)
}
RowVar(m)
[1] 871.6556 957.5111 699.2111 941.4333 1237.3333 641.8222 539.7889 759.4333 500.4889 1255.6111