R Language
Mejores prácticas de vectorización de código R
Buscar..
Por operaciones de fila
La clave para vectorizar el código R, es reducir o eliminar "por operaciones de fila" o el método de envío de funciones R.
Eso significa que cuando se aborda un problema que a primera vista requiere "operaciones de fila", como calcular los medios de cada fila, uno debe preguntarse:
- ¿Cuáles son las clases de los conjuntos de datos con los que estoy tratando?
- ¿Existe un código compilado existente que pueda lograr esto sin la necesidad de una evaluación repetitiva de las funciones de R?
- Si no, ¿puedo hacer esta operación por columnas en lugar de por fila?
- Finalmente, ¿vale la pena dedicar mucho tiempo a desarrollar código vectorizado complicado en lugar de simplemente ejecutar un simple bucle de
apply
? En otras palabras, ¿los datos son lo suficientemente grandes / sofisticados como para que R no pueda manejarlos de manera eficiente utilizando un simple bucle?
Dejando a un lado el problema de la pre-asignación de memoria y el aumento del objeto en los bucles, en este ejemplo nos centraremos en cómo evitar los bucles de apply
, el envío de métodos o la reevaluación de las funciones R dentro de los bucles.
Una forma estándar / fácil de calcular la media por fila sería:
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
¿Pero podemos hacerlo mejor? Veamos lo que pasó aquí:
- Primero, convertimos un
data.frame
en unamatrix
. (Tenga en cuenta que esto ocurre dentro de la función deapply
). Esto es ineficiente y peligroso. unamatrix
no puede contener varios tipos de columnas a la vez. Por lo tanto, tal conversión probablemente llevará a la pérdida de información y algunas veces a resultados engañosos (compareapply(iris, 2, class)
constr(iris)
o consapply(iris, class)
). - En segundo lugar, realizamos una operación repetitivamente, una vez para cada fila. Es decir, tuvimos que evaluar algunas
nrow(mtcars)
función Rnrow(mtcars)
. En este caso específico, lamean
no es una función computacionalmente costosa, por lo tanto, R podría manejarlo fácilmente incluso para un conjunto de datos grande, pero qué pasaría si tuviéramos que calcular la desviación estándar por fila (lo que implica una operación costosa de raíz cuadrada) ? Lo que nos lleva al siguiente punto: - Evaluamos la función R muchas veces, pero ¿quizás ya hay una versión compilada de esta operación?
De hecho, podríamos simplemente hacer:
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
Esto implica no operaciones por filas y, por lo tanto, no hay una evaluación repetitiva de las funciones R. Sin embargo , aún convertimos un data.frame
a una matrix
. Aunque rowMeans
tiene un mecanismo de manejo de errores y no se ejecuta en un conjunto de datos que no puede manejar, todavía tiene un costo de eficiencia.
rowMeans(iris)
Error in rowMeans(iris) : 'x' must be numeric
Pero aún así, ¿podemos hacerlo mejor? Podríamos intentar en lugar de una conversión matricial con manejo de errores, un método diferente que nos permitirá usar mtcars
como un vector (porque un data.frame
es esencialmente una list
y una list
es un 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
Ahora, para una posible ganancia de velocidad, perdimos los nombres de las columnas y el manejo de errores (incluido el manejo de NA
).
Otro ejemplo sería calcular la media por grupo, usando la base R que podríamos probar
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
Aún así, básicamente estamos evaluando una función R en un bucle, pero ahora el bucle está oculto en una función interna C (importa poco si es un bucle C o R).
¿Podríamos evitarlo? Bueno, hay una función compilada en R llamada rowsum
, por lo que podríamos hacer:
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
Aunque también teníamos que convertirnos primero en una matriz.
En este punto podemos preguntarnos si nuestra estructura de datos actual es la más apropiada. ¿Un data.frame
es la mejor práctica? ¿O debería uno simplemente cambiar a una estructura de datos matrix
para ganar eficiencia?
Las operaciones por fila serán cada vez más caras (incluso en matrices) a medida que comencemos a evaluar funciones caras cada vez. Nos permite considerar un cálculo de varianza por ejemplo de fila.
Digamos que tenemos una matriz 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
Uno podría simplemente hacer:
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
Por otro lado, también se podría vectorizar completamente esta operación siguiendo la fórmula de varianza
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