按行操作

向量化 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 

但我們能做得更好嗎?讓我們看看這裡發生了什麼:

  1. 首先,我們將 data.frame 轉換為 matrix。 (請注意,他發生在 apply 函式中。)這既低效又危險。matrix 一次不能容納多種列型別。因此,這種轉換可能會導致資訊丟失,有時會導致誤導結果(將 apply(iris, 2, class)str(iris)sapply(iris, class) 進行比較)。
  2. 其次,我們重複執行一次操作,每行一次。意思是,我們不得不評估一些 R 函式 nrow(mtcars) 次。在這個特定的情況下,mean 不是一個計算上昂貴的函式,因此即使對於大資料集,R 也可能很容易處理它,但是如果我們需要按行計算標準偏差會發生什麼(這涉及昂貴的平方根操作) ?這將我們帶到下一點:
  3. 我們多次評估 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 本質上是 listlistvector)。

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