¿Aplicando una función a cada fila de una tabla usando dplyr?

121

Cuando trabajaba, a plyrmenudo me resultaba útil usar adplypara funciones escalares que tenía que aplicar a todas y cada una de las filas.

p.ej

data(iris)
library(plyr)
head(
     adply(iris, 1, transform , Max.Len= max(Sepal.Length,Petal.Length))
    )
  Sepal.Length Sepal.Width Petal.Length Petal.Width Species Max.Len
1          5.1         3.5          1.4         0.2  setosa     5.1
2          4.9         3.0          1.4         0.2  setosa     4.9
3          4.7         3.2          1.3         0.2  setosa     4.7
4          4.6         3.1          1.5         0.2  setosa     4.6
5          5.0         3.6          1.4         0.2  setosa     5.0
6          5.4         3.9          1.7         0.4  setosa     5.4

Ahora estoy usando dplyrmás, me pregunto si hay una forma ordenada / natural de hacer esto. Como esto NO es lo que quiero:

library(dplyr)
head(
     mutate(iris, Max.Len= max(Sepal.Length,Petal.Length))
    )
  Sepal.Length Sepal.Width Petal.Length Petal.Width Species Max.Len
1          5.1         3.5          1.4         0.2  setosa     7.9
2          4.9         3.0          1.4         0.2  setosa     7.9
3          4.7         3.2          1.3         0.2  setosa     7.9
4          4.6         3.1          1.5         0.2  setosa     7.9
5          5.0         3.6          1.4         0.2  setosa     7.9
6          5.4         3.9          1.7         0.4  setosa     7.9
Stephen Henderson
fuente
mdplyHace poco pregunté si había un equivalente de in dplyr, y Hadley sugirió que podrían estar preparando algo basado en do. Supongo que también funcionaría aquí.
Baptiste
44
Eventualmente, dplyr tendrá algo como lo rowwise()que se agruparía por cada fila individual
hadley
@hadley thx, ¿no debería comportarse simplemente como adplycuando no usas una agrupación? como su función estrechamente integrada se llama group_byNOsplit_by
Stephen Henderson
@StephenHenderson no, porque también necesita alguna forma de operar sobre la mesa como un todo.
hadley
1
@HowYaDoing Sí, pero ese método no se generaliza. No hay psum, pmean o pmedian por ejemplo.
Stephen Henderson el

Respuestas:

202

A partir de dplyr 0.2 (creo) rowwise()se implementa, por lo que la respuesta a este problema se convierte en:

iris %>% 
  rowwise() %>% 
  mutate(Max.Len= max(Sepal.Length,Petal.Length))

No rowwisealternativa

Cinco años (!) Más tarde, esta respuesta todavía recibe mucho tráfico. Desde que se le dio, rowwisecada vez más no se recomienda, aunque muchas personas parecen encontrarlo intuitivo. Hágase un favor y revise los flujos de trabajo orientados a filas de Jenny Bryan en R con el material tidyverse para obtener un buen manejo de este tema.

La forma más directa que he encontrado se basa en uno de los ejemplos de Hadley que usa pmap:

iris %>% 
  mutate(Max.Len= purrr::pmap_dbl(list(Sepal.Length, Petal.Length), max))

Con este enfoque, puede dar un número arbitrario de argumentos a la función ( .f) dentro pmap.

pmap es un buen enfoque conceptual porque refleja el hecho de que cuando estás haciendo operaciones en filas, en realidad estás trabajando con tuplas de una lista de vectores (las columnas en un marco de datos).

alexwhan
fuente
He cambiado esto (de lo anterior) a la respuesta ideal, ya que creo que este es el uso previsto.
Stephen Henderson el
1
¿Es posible agregar los valores de un marco de datos formado dinámicamente? Entonces, en este marco de datos, no se conocen los nombres de las columnas. Puedo agregar si se conocen los nombres de columna.
Arun Raja
stackoverflow.com/questions/28807266/... acabo de encontrar la respuesta. En esto están usando correlación en lugar de suma. Pero el mismo concepto.
Arun Raja
13
Si no funciona, asegúrese de estar usando dplyr :: mutate not plyr :: mutate - me volvió loco
jan-glx
Gracias YAK, esto también me mordió. Si incluye ambos plyry dplyrpaquetes, es casi seguro que está utilizando el incorrecto a mutatemenos que explícitamente proporcione alcance dplyr::mutate.
Chris Warth
22

El enfoque idiomático será crear una función adecuadamente vectorizada.

Rproporcionar lo pmaxque es adecuado aquí, sin embargo, también proporciona Vectorizeun contenedor para mapplypermitirle crear una versión arbitraria vectorizada de una función arbitraria.

library(dplyr)
# use base R pmax (vectorized in C)
iris %>% mutate(max.len = pmax(Sepal.Length, Petal.Length))
# use vectorize to create your own function
# for example, a horribly inefficient get first non-Na value function
# a version that is not vectorized
coalesce <- function(a,b) {r <- c(a[1],b[1]); r[!is.na(r)][1]}
# a vectorized version
Coalesce <- Vectorize(coalesce, vectorize.args = c('a','b'))
# some example data
df <- data.frame(a = c(1:5,NA,7:10), b = c(1:3,NA,NA,6,NA,10:8))
df %>% mutate(ab =Coalesce(a,b))

Tenga en cuenta que implementar la vectorización en C / C ++ será más rápido, pero no hay un magicPonypaquete que escriba la función por usted.

mnel
fuente
Gracias, esta es una gran respuesta, es excelente estilo R general -diomático como usted dice, pero no creo que realmente esté respondiendo a mi pregunta si hay una dplyrforma ... ya que sería más simple sin dplyr, por ejemplo with(df, Coalesce(a,b)), tal vez, eso es un tipo de respuesta sin embargo, ¿no lo usas dplyrpara eso?
Stephen Henderson
44
Tengo que admitir que verifiqué dos veces que no hay un magicPonypaquete. Lástima
rsoren
21

Necesita agrupar por fila:

iris %>% group_by(1:n()) %>% mutate(Max.Len= max(Sepal.Length,Petal.Length))

Esto es lo que 1hicieron adply.

BrodieG
fuente
Parece que debería haber una sintaxis más simple o "más agradable".
Stephen Henderson
@StephenHenderson, puede haber, no soy un dplyrexperto. Esperemos que alguien más venga con algo mejor. Tenga en cuenta que lo limpié un poco con 1:n().
BrodieG
Sospecho que tienes razón, pero siento que el comportamiento predeterminado sin agrupación debería ser como el group_by(1:n())comportamiento. Si nadie tiene otras ideas en la mañana, marcaré las tuyas;)
Stephen Henderson
Además, tenga en cuenta que esto es algo en contravención de la documentación para n: "Esta función se implementa de manera especial para cada fuente de datos y solo se puede usar desde el resumen", aunque parece funcionar.
BrodieG
¿Puede referirse a Sepal.Length y Petal.Length por su número de índice de alguna manera? Si tiene muchas variables, sería útil. ¿Como ... Max.len = max ([c (1,3)])?
Rasmus Larsen
19

Actualizar 2017-08-03

Después de escribir esto, Hadley cambió algunas cosas nuevamente. Las funciones que solían estar en purrr ahora están en un nuevo paquete mixto llamado purrrlyr , descrito como:

purrrlyr contiene algunas funciones que se encuentran en la intersección de purrr y dplyr. Se han eliminado del ronroneo para hacer el paquete más ligero y porque han sido reemplazados por otras soluciones en el tidyverse.

Por lo tanto, deberá instalar + cargar ese paquete para que el siguiente código funcione.

Publicación original

Hadley frecuentemente cambia de opinión acerca de lo que deberíamos usar, pero creo que se supone que debemos cambiar a las funciones en ronroneo para obtener la funcionalidad por fila. Por lo menos, ofrecen la misma funcionalidad y tienen casi la misma interfaz que adplydesde plyr .

Hay dos funciones relacionadas, by_rowy invoke_rows. by_rowSegún tengo entendido, lo usa cuando desea recorrer las filas y agregar los resultados al data.frame. invoke_rowsse usa cuando recorre las filas de un data.frame y pasa cada col como argumento a una función. Solo usaremos el primero.

Ejemplos

library(tidyverse)

iris %>% 
  by_row(..f = function(this_row) {
    browser()
  })

Esto nos permite ver los elementos internos (para que podamos ver lo que estamos haciendo), que es lo mismo que hacerlo adply.

Called from: ..f(.d[[i]], ...)
Browse[1]> this_row
# A tibble: 1 × 5
  Sepal.Length Sepal.Width Petal.Length Petal.Width Species
         <dbl>       <dbl>        <dbl>       <dbl>  <fctr>
1          5.1         3.5          1.4         0.2  setosa
Browse[1]> Q

Por defecto, by_rowagrega una columna de lista basada en la salida:

iris %>% 
  by_row(..f = function(this_row) {
      this_row[1:4] %>% unlist %>% mean
  })

da:

# A tibble: 150 × 6
   Sepal.Length Sepal.Width Petal.Length Petal.Width Species      .out
          <dbl>       <dbl>        <dbl>       <dbl>  <fctr>    <list>
1           5.1         3.5          1.4         0.2  setosa <dbl [1]>
2           4.9         3.0          1.4         0.2  setosa <dbl [1]>
3           4.7         3.2          1.3         0.2  setosa <dbl [1]>
4           4.6         3.1          1.5         0.2  setosa <dbl [1]>
5           5.0         3.6          1.4         0.2  setosa <dbl [1]>
6           5.4         3.9          1.7         0.4  setosa <dbl [1]>
7           4.6         3.4          1.4         0.3  setosa <dbl [1]>
8           5.0         3.4          1.5         0.2  setosa <dbl [1]>
9           4.4         2.9          1.4         0.2  setosa <dbl [1]>
10          4.9         3.1          1.5         0.1  setosa <dbl [1]>
# ... with 140 more rows

si en cambio devolvemos a data.frame, obtenemos una lista con data.frames:

iris %>% 
  by_row( ..f = function(this_row) {
    data.frame(
      new_col_mean = this_row[1:4] %>% unlist %>% mean,
      new_col_median = this_row[1:4] %>% unlist %>% median
    )
  })

da:

# A tibble: 150 × 6
   Sepal.Length Sepal.Width Petal.Length Petal.Width Species                 .out
          <dbl>       <dbl>        <dbl>       <dbl>  <fctr>               <list>
1           5.1         3.5          1.4         0.2  setosa <data.frame [1 × 2]>
2           4.9         3.0          1.4         0.2  setosa <data.frame [1 × 2]>
3           4.7         3.2          1.3         0.2  setosa <data.frame [1 × 2]>
4           4.6         3.1          1.5         0.2  setosa <data.frame [1 × 2]>
5           5.0         3.6          1.4         0.2  setosa <data.frame [1 × 2]>
6           5.4         3.9          1.7         0.4  setosa <data.frame [1 × 2]>
7           4.6         3.4          1.4         0.3  setosa <data.frame [1 × 2]>
8           5.0         3.4          1.5         0.2  setosa <data.frame [1 × 2]>
9           4.4         2.9          1.4         0.2  setosa <data.frame [1 × 2]>
10          4.9         3.1          1.5         0.1  setosa <data.frame [1 × 2]>
# ... with 140 more rows

La forma en que agregamos la salida de la función está controlada por el .collateparámetro. Hay tres opciones: lista, filas, columnas. Cuando nuestra salida tiene longitud 1, no importa si usamos filas o cols.

iris %>% 
  by_row(.collate = "cols", ..f = function(this_row) {
    this_row[1:4] %>% unlist %>% mean
  })

iris %>% 
  by_row(.collate = "rows", ..f = function(this_row) {
    this_row[1:4] %>% unlist %>% mean
  })

ambos producen:

# A tibble: 150 × 6
   Sepal.Length Sepal.Width Petal.Length Petal.Width Species  .out
          <dbl>       <dbl>        <dbl>       <dbl>  <fctr> <dbl>
1           5.1         3.5          1.4         0.2  setosa 2.550
2           4.9         3.0          1.4         0.2  setosa 2.375
3           4.7         3.2          1.3         0.2  setosa 2.350
4           4.6         3.1          1.5         0.2  setosa 2.350
5           5.0         3.6          1.4         0.2  setosa 2.550
6           5.4         3.9          1.7         0.4  setosa 2.850
7           4.6         3.4          1.4         0.3  setosa 2.425
8           5.0         3.4          1.5         0.2  setosa 2.525
9           4.4         2.9          1.4         0.2  setosa 2.225
10          4.9         3.1          1.5         0.1  setosa 2.400
# ... with 140 more rows

Si sacamos un data.frame con 1 fila, solo importa un poco lo que usamos:

iris %>% 
  by_row(.collate = "cols", ..f = function(this_row) {
    data.frame(
      new_col_mean = this_row[1:4] %>% unlist %>% mean,
      new_col_median = this_row[1:4] %>% unlist %>% median
      )
  })

iris %>% 
  by_row(.collate = "rows", ..f = function(this_row) {
    data.frame(
      new_col_mean = this_row[1:4] %>% unlist %>% mean,
      new_col_median = this_row[1:4] %>% unlist %>% median
    )
  })

ambos dan:

# A tibble: 150 × 8
   Sepal.Length Sepal.Width Petal.Length Petal.Width Species  .row new_col_mean new_col_median
          <dbl>       <dbl>        <dbl>       <dbl>  <fctr> <int>        <dbl>          <dbl>
1           5.1         3.5          1.4         0.2  setosa     1        2.550           2.45
2           4.9         3.0          1.4         0.2  setosa     2        2.375           2.20
3           4.7         3.2          1.3         0.2  setosa     3        2.350           2.25
4           4.6         3.1          1.5         0.2  setosa     4        2.350           2.30
5           5.0         3.6          1.4         0.2  setosa     5        2.550           2.50
6           5.4         3.9          1.7         0.4  setosa     6        2.850           2.80
7           4.6         3.4          1.4         0.3  setosa     7        2.425           2.40
8           5.0         3.4          1.5         0.2  setosa     8        2.525           2.45
9           4.4         2.9          1.4         0.2  setosa     9        2.225           2.15
10          4.9         3.1          1.5         0.1  setosa    10        2.400           2.30
# ... with 140 more rows

excepto que el segundo tiene la columna llamada .rowy el primero no.

Finalmente, si nuestra salida es más larga que la longitud 1, ya sea como vectoro como a data.framecon filas, entonces importa si usamos filas o columnas para .collate:

mtcars[1:2] %>% by_row(function(x) 1:5)
mtcars[1:2] %>% by_row(function(x) 1:5, .collate = "rows")
mtcars[1:2] %>% by_row(function(x) 1:5, .collate = "cols")

produce, respectivamente:

# A tibble: 32 × 3
     mpg   cyl      .out
   <dbl> <dbl>    <list>
1   21.0     6 <int [5]>
2   21.0     6 <int [5]>
3   22.8     4 <int [5]>
4   21.4     6 <int [5]>
5   18.7     8 <int [5]>
6   18.1     6 <int [5]>
7   14.3     8 <int [5]>
8   24.4     4 <int [5]>
9   22.8     4 <int [5]>
10  19.2     6 <int [5]>
# ... with 22 more rows

# A tibble: 160 × 4
     mpg   cyl  .row  .out
   <dbl> <dbl> <int> <int>
1     21     6     1     1
2     21     6     1     2
3     21     6     1     3
4     21     6     1     4
5     21     6     1     5
6     21     6     2     1
7     21     6     2     2
8     21     6     2     3
9     21     6     2     4
10    21     6     2     5
# ... with 150 more rows

# A tibble: 32 × 7
     mpg   cyl .out1 .out2 .out3 .out4 .out5
   <dbl> <dbl> <int> <int> <int> <int> <int>
1   21.0     6     1     2     3     4     5
2   21.0     6     1     2     3     4     5
3   22.8     4     1     2     3     4     5
4   21.4     6     1     2     3     4     5
5   18.7     8     1     2     3     4     5
6   18.1     6     1     2     3     4     5
7   14.3     8     1     2     3     4     5
8   24.4     4     1     2     3     4     5
9   22.8     4     1     2     3     4     5
10  19.2     6     1     2     3     4     5
# ... with 22 more rows

Entonces, el resultado final. Si desea la adply(.margins = 1, ...)funcionalidad, puede usar by_row.

CoderGuy123
fuente
2
by_rowestá en desuso, al llamarlo dice "usar una combinación de: tidyr :: nest (); dplyr :: mutate (); purrr :: map ()" github.com/hadley/purrrlyr/blob/…
momeara
Eso es un montón de r.
qwr
14

Extendiendo la respuesta de BrodieG,

Si la función devuelve más de una fila, entonces, en lugar de mutate(), do()debe usarse. Luego, para combinarlo nuevamente, úselo rbind_all()del dplyrpaquete.

En la dplyrversión dplyr_0.1.2, usar 1:n()en la group_by()cláusula no funciona para mí. Esperemos que Hadley lo implementerowwise() pronto.

iris %>%
    group_by(1:nrow(iris)) %>%
    do(do_fn) %>%
    rbind_all()

Probar el rendimiento,

library(plyr)    # plyr_1.8.4.9000
library(dplyr)   # dplyr_0.8.0.9000
library(purrr)   # purrr_0.2.99.9000
library(microbenchmark)

d1_count <- 1000
d2_count <- 10

d1 <- data.frame(a=runif(d1_count))

do_fn <- function(row){data.frame(a=row$a, b=runif(d2_count))}
do_fn2 <- function(a){data.frame(a=a, b=runif(d2_count))}

op <- microbenchmark(
        plyr_version = plyr::adply(d1, 1, do_fn),
        dplyr_version = d1 %>%
            dplyr::group_by(1:nrow(d1)) %>%
            dplyr::do(do_fn(.)) %>%
            dplyr::bind_rows(),
        purrr_version = d1 %>% purrr::pmap_dfr(do_fn2),
        times=50)

tiene los siguientes resultados:

Unit: milliseconds
          expr       min        lq      mean    median        uq       max neval
  plyr_version 1227.2589 1275.1363 1317.3431 1293.5759 1314.4266 1616.5449    50
 dplyr_version  977.3025 1012.6340 1035.9436 1025.6267 1040.5882 1449.0978    50
 purrr_version  609.5790  629.7565  643.8498  644.2505  656.1959  686.8128    50

Esto muestra que la nueva purrrversión es la más rápida.

momeara
fuente
1

¿Algo como esto?

iris$Max.Len <- pmax(iris$Sepal.Length, iris$Petal.Length)
colcarroll
fuente
1
Sí, gracias, esa es una respuesta muy específica. Pero mi ejemplo y mi pregunta están tratando de descifrar si hay una dplyrsolución general para cualquier función escalar.
Stephen Henderson
En general, las funciones deben estar vectorizadas; si es una función extraña, puede escribir wacky.function <- function(col.1, col.2){...}, y luego iris.wacky <- wacky.function(iris$Sepal.Length, iris$Petal.Length).
colcarroll
A menudo deberían adivinar, pero creo que cuando estás usando algo como dplyro plyrdecir data.tableque debes tratar de usar sus expresiones idiomáticas para que tu código no se convierta en una mezcla de estilos difícil de compartir. De ahí la pregunta.
Stephen Henderson
La primera línea de la plyrdocumentación es "plyr es un conjunto de herramientas que resuelve un conjunto común de problemas: necesita dividir un gran problema en partes manejables, operar en cada pieza y luego volver a unir todas las piezas". Este parece ser un problema muy diferente para el cual las operaciones de columnas elementales son la mejor herramienta. Esto también podría explicar por qué no hay "natural" plyr/ dplyrcomando para hacer esto.
colcarroll
55
Para descifrar una cita famosa: " Si todo lo que tienes es una plyr, terminarás usándola también para un martillo y un destornillador "
thelatemail