按行操作
向量化 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