Llame a la función de aplicar en cada fila de trama de datos con múltiples argumentos de cada fila

168

Tengo un marco de datos con varias columnas. Para cada fila en el marco de datos, quiero llamar a una función en la fila, y la entrada de la función está usando múltiples columnas de esa fila. Por ejemplo, digamos que tengo estos datos y este testFunc que acepta dos argumentos:

> df <- data.frame(x=c(1,2), y=c(3,4), z=c(5,6))
> df
  x y z
1 1 3 5
2 2 4 6
> testFunc <- function(a, b) a + b

Digamos que quiero aplicar este testFunc a las columnas xyz. Entonces, para la fila 1 quiero 1 + 5, y para la fila 2 quiero 2 + 6. ¿Hay alguna manera de hacer esto sin escribir un bucle for, tal vez con la familia de funciones apply?

Intenté esto:

> df[,c('x','z')]
  x z
1 1 5
2 2 6
> lapply(df[,c('x','z')], testFunc)
Error in a + b : 'b' is missing

Pero tiene error, alguna idea?

EDITAR: la función real que quiero llamar no es una suma simple, sino que es power.t.test. Usé a + b solo por ejemplo. El objetivo final es poder hacer algo como esto (escrito en pseudocódigo):

df = data.frame(
    delta=c(delta_values), 
    power=c(power_values), 
    sig.level=c(sig.level_values)
)

lapply(df, power.t.test(delta_from_each_row_of_df, 
                        power_from_each_row_of_df, 
                        sig.level_from_each_row_of_df
))

donde el resultado es un vector de salidas para power.t.test para cada fila de df.

vasek1
fuente
Consulte también stackoverflow.com/a/24728107/946850 para conocer el dplyrcamino.
krlmlr

Respuestas:

137

Puede aplicar applya un subconjunto de los datos originales.

 dat <- data.frame(x=c(1,2), y=c(3,4), z=c(5,6))
 apply(dat[,c('x','z')], 1, function(x) sum(x) )

o si su función es solo suma use la versión vectorizada:

rowSums(dat[,c('x','z')])
[1] 6 8

Si quieres usar testFunc

 testFunc <- function(a, b) a + b
 apply(dat[,c('x','z')], 1, function(x) testFunc(x[1],x[2]))

EDITAR Para acceder a las columnas por nombre y no por índice, puede hacer algo como esto:

 testFunc <- function(a, b) a + b
 apply(dat[,c('x','z')], 1, function(y) testFunc(y['z'],y['x']))
estudio
fuente
gracias @agstudy, eso funcionó! ¿Sabes si hay alguna forma de especificar los argumentos por nombre en lugar de por índice? entonces, para testFunc, algo así como apply (dat [, c ('x', 'z')], 1, [pseudocode] testFunc (a = x, b = y))? la razón es que estoy llamando a power.t.test de esta manera, y me encantaría poder hacer referencia a los parámetros delta, power, sig.level por nombre en lugar de pegarlos en una matriz con posiciones predeterminadas y luego haciendo referencia a esa posición, por ser más robusta. en cualquier caso muchas gracias!
vasek1
perdón por el comentario anterior, presione enter antes de terminar de escribir :) lo eliminó y publicó la versión completa.
vasek1
21
No lo use applyen big data.frames, copiará todo el objeto (para convertirlo en una matriz). Esto también causará problemas si tiene diferentes objetos de clase dentro del data.frame.
mnel
105

A data.framees un list, entonces ...

Para las funciones vectorizadas do.call suele ser una buena apuesta. Pero los nombres de los argumentos entran en juego. Aquí testFuncse llama a tu con args x e y en lugar de a y b. El ...permite args irrelevantes que se pasarán sin causar un error:

do.call( function(x,z,...) testFunc(x,z), df )

Para las funciones no vectorizadas , mapplyfuncionará, pero debe coincidir con el orden de los argumentos o nombrarlos explícitamente:

mapply(testFunc, df$x, df$z)

A veces applyfuncionará, como cuando todos los argumentos son del mismo tipo, por lo que coaccionar data.framea una matriz no causa problemas al cambiar los tipos de datos. Su ejemplo fue de este tipo.

Si su función se llamará dentro de otra función en la que se pasan todos los argumentos, hay un método mucho más ingenioso que estos. Estudie las primeras líneas del cuerpo de lm()si desea seguir esa ruta.

usuario2087984
fuente
8
+10 si pudiera. Bienvenido a SO. gran respuesta - podría valer la pena mencionarlo Vectorizecomo un contenedor mapplypara vectorizar funciones
mnel
wow, eso es astuto. La función original que utilicé no estaba vectorizada (una extensión personalizada sobre power.t.test), pero creo que la vectorizaré y usaré do.call (...). ¡Gracias!
vasek1
3
Solo reiterando la nota de que esta respuesta ya dice que apply (df, 1, function (row) ...) puede ser malo porque apply convierte el df en una matriz !!!! Esto puede ser malo y provocar una gran cantidad de tirones de cabello. ¡Las alternativas para aplicar son muy necesarias!
Colin D
Muchas gracias por diferenciar entre Vectorizado / no vectorizado, esta es absolutamente la respuesta que estaba buscando
Usuario632716
31

Utilizar mapply

> df <- data.frame(x=c(1,2), y=c(3,4), z=c(5,6))
> df
  x y z
1 1 3 5
2 2 4 6
> mapply(function(x,y) x+y, df$x, df$z)
[1] 6 8

> cbind(df,f = mapply(function(x,y) x+y, df$x, df$z) )
  x y z f
1 1 3 5 6
2 2 4 6 8
Chinmay Patil
fuente
20

Nueva respuesta con dplyr paquete

Si la función que desea aplicar está vectorizada, puede usar la mutatefunción del dplyrpaquete:

> library(dplyr)
> myf <- function(tens, ones) { 10 * tens + ones }
> x <- data.frame(hundreds = 7:9, tens = 1:3, ones = 4:6)
> mutate(x, value = myf(tens, ones))
  hundreds tens ones value
1        7    1    4    14
2        8    2    5    25
3        9    3    6    36

Antigua respuesta con plyr paquete

En mi humilde opinión, la herramienta más adecuada para la tarea es mdply proviene del plyrpaquete.

Ejemplo:

> library(plyr)
> x <- data.frame(tens = 1:3, ones = 4:6)
> mdply(x, function(tens, ones) { 10 * tens + ones })
  tens ones V1
1    1    4 14
2    2    5 25
3    3    6 36

Desafortunadamente, como señaló Bertjan Broeksema , este enfoque falla si no utiliza todas las columnas del marco de datos en la mdplyllamada. Por ejemplo,

> library(plyr)
> x <- data.frame(hundreds = 7:9, tens = 1:3, ones = 4:6)
> mdply(x, function(tens, ones) { 10 * tens + ones })
Error in (function (tens, ones)  : unused argument (hundreds = 7)
Me gusta codificar
fuente
1
Es agradable cuando solo tienes un pequeño número de columnas. Traté de hacer algo como: mdply (df, function (col1, col3) {}) y mdply se rescata, quejándose de que col2 no se utiliza. Ahora, si tiene decenas o incluso cientos de columnas, este enfoque no es muy atractivo.
Bertjan Broeksema
1
@BertjanBroeksema para modificar muchas columnas, puede usar dplyr::mutate_each. Por ejemplo: iris %>% mutate_each(funs(half = . / 2),-Species).
Paul Rougieux
¿No podría simplemente pasar elipses o los cientos a la función y simplemente no usarla? ¿Eso debería arreglar ese error?
Shawn
11

Otros han señalado correctamente que mapplyestá hecho para este propósito, pero (en aras de la exhaustividad) un método conceptualmente más simple es usar un forbucle.

for (row in 1:nrow(df)) { 
    df$newvar[row] <- testFunc(df$x[row], df$z[row]) 
}
rsoren
fuente
1
Tienes razón. Para usar mapply de manera efectiva, creo que debe comprender que es solo un bucle "for" detrás de escena, especialmente si proviene de un entorno de programación de procedimientos como C ++ o C #.
Contango
10

Muchas funciones ya son vectorización, por lo que no hay necesidad de iteraciones (ni forbucles ni *pplyfunciones). Tu testFunces uno de esos ejemplos. Simplemente puedes llamar:

  testFunc(df[, "x"], df[, "z"])

En general, recomendaría probar primero dichos enfoques de vectorización y ver si le dan los resultados deseados.


Alternativamente, si necesita pasar varios argumentos a una función que no está vectorizada, mapplypodría ser lo que está buscando:

  mapply(power.t.test, df[, "x"], df[, "z"])
Ricardo Saporta
fuente
Oh dulce. ¿Sabes si hay una manera de especificar argumentos por nombre en mapply? es decir, algo así como [pseudocódigo] mapply (power.t.test, delta = df [, 'delta'], power = df [, 'power'], ...)?
vasek1
1
Sí, ¡es exactamente como lo tienes! ;)
Ricardo Saporta
4

Aquí hay un enfoque alternativo. Es mas intuitivo.

Un aspecto clave que creo que algunas de las respuestas no tuvieron en cuenta, que señalo para la posteridad, es apply () le permite hacer cálculos de fila fácilmente, pero solo para datos de matriz (todos numéricos)

las operaciones en columnas aún son posibles para marcos de datos:

as.data.frame(lapply(df, myFunctionForColumn()))

Para operar en filas, hacemos primero la transposición.

tdf<-as.data.frame(t(df))
as.data.frame(lapply(tdf, myFunctionForRow()))

La desventaja es que creo que R hará una copia de su tabla de datos. Lo que podría ser un problema de memoria. (Esto es realmente triste, porque es programáticamente simple que tdf sea solo un iterador del df original, ahorrando así memoria, pero R no permite referencias de puntero o iterador).

Además, una pregunta relacionada es cómo operar en cada celda individual en un marco de datos.

newdf <- as.data.frame(lapply(df, function(x) {sapply(x, myFunctionForEachCell()}))
Tocino BAMF4
fuente
4

Vine aquí buscando el nombre de la función tidyverse , que sabía que existía. Agregando esto para (mi) referencia futura y para tidyverseentusiastas: purrrlyr:invoke_rows( purrr:invoke_rowsen versiones anteriores).

Con la conexión a los métodos de estadísticas estándar como en la pregunta original, el paquete de escoba probablemente ayudaría.

liborm
fuente
3

La respuesta de @ user20877984 es excelente. Como lo resumieron mucho mejor que mi respuesta anterior, aquí está mi intento (posiblemente aún de mala calidad) de aplicar el concepto:

Utilizando do.callde manera básica:

powvalues <- list(power=0.9,delta=2)
do.call(power.t.test,powvalues)

Trabajando en un conjunto de datos completo:

# get the example data
df <- data.frame(delta=c(1,1,2,2), power=c(.90,.85,.75,.45))

#> df
#  delta power
#1     1  0.90
#2     1  0.85
#3     2  0.75
#4     2  0.45

lapplyla power.t.testfunción para cada una de las filas de valores especificados:

result <- lapply(
  split(df,1:nrow(df)),
  function(x) do.call(power.t.test,x)
)

> str(result)
List of 4
 $ 1:List of 8
  ..$ n          : num 22
  ..$ delta      : num 1
  ..$ sd         : num 1
  ..$ sig.level  : num 0.05
  ..$ power      : num 0.9
  ..$ alternative: chr "two.sided"
  ..$ note       : chr "n is number in *each* group"
  ..$ method     : chr "Two-sample t test power calculation"
  ..- attr(*, "class")= chr "power.htest"
 $ 2:List of 8
  ..$ n          : num 19
  ..$ delta      : num 1
  ..$ sd         : num 1
  ..$ sig.level  : num 0.05
  ..$ power      : num 0.85
... ...
thelatemail
fuente
Jaja enredado quizás? ;) ¿por qué estás usando t () y aplicando sobre 2, por qué no solo aplicar sobre 1?
Ricardo Saporta
3

data.table tiene una forma realmente intuitiva de hacer esto también:

library(data.table)

sample_fxn = function(x,y,z){
    return((x+y)*z)
}

df = data.table(A = 1:5,B=seq(2,10,2),C = 6:10)
> df
   A  B  C
1: 1  2  6
2: 2  4  7
3: 3  6  8
4: 4  8  9
5: 5 10 10

Se :=puede llamar al operador entre paréntesis para agregar una nueva columna usando una función

df[,new_column := sample_fxn(A,B,C)]
> df
   A  B  C new_column
1: 1  2  6         18
2: 2  4  7         42
3: 3  6  8         72
4: 4  8  9        108
5: 5 10 10        150

También es fácil aceptar constantes como argumentos usando este método:

df[,new_column2 := sample_fxn(A,B,2)]

> df
   A  B  C new_column new_column2
1: 1  2  6         18           6
2: 2  4  7         42          12
3: 3  6  8         72          18
4: 4  8  9        108          24
5: 5 10 10        150          30
Pete M
fuente
1

Si las columnas data.frame son de diferentes tipos, apply()tiene un problema. Una sutileza acerca de la iteración de fila es cómo la apply(a.data.frame, 1, ...)conversión de tipo implícito a tipos de caracteres cuando las columnas son tipos diferentes; p.ej. un factor y una columna numérica. Aquí hay un ejemplo, usando un factor en una columna para modificar una columna numérica:

mean.height = list(BOY=69.5, GIRL=64.0)

subjects = data.frame(gender = factor(c("BOY", "GIRL", "GIRL", "BOY"))
         , height = c(71.0, 59.3, 62.1, 62.1))

apply(height, 1, function(x) x[2] - mean.height[[x[1]]])

La resta falla porque las columnas se convierten en tipos de caracteres.

Una solución es volver a convertir la segunda columna a un número:

apply(subjects, 1, function(x) as.numeric(x[2]) - mean.height[[x[1]]])

Pero las conversiones se pueden evitar manteniendo las columnas separadas y utilizando mapply():

mapply(function(x,y) y - mean.height[[x]], subjects$gender, subjects$height)

mapply()es necesario porque [[ ]]no acepta un argumento vectorial. Entonces, la iteración de la columna podría hacerse antes de la resta pasando un vector a [], por un código un poco más feo:

subjects$height - unlist(mean.height[subjects$gender])
John Mark
fuente
1

Una función muy bonito de esto es adplya partir plyr, sobre todo si desea añadir el resultado a la trama de datos originales. ¡Esta función y su primo ddplyme han ahorrado muchos dolores de cabeza y líneas de código!

df_appended <- adply(df, 1, mutate, sum=x+z)

Alternativamente, puede llamar a la función que desee.

df_appended <- adply(df, 1, mutate, sum=testFunc(x,z))
Zach S.
fuente
puede adply () ocuparse de funciones que devuelven listas o marcos de datos? por ejemplo, ¿qué pasa si testFunc () devuelve una lista? ¿se usaría unnest () para mutarlo en columnas adicionales de su df_appened?
val