web-dev-qa-db-fra.com

Appliquer une fonction à chaque ligne d'un tableau en utilisant dplyr?

Lorsque je travaillais avec plyr, j’ai souvent trouvé utile d’utiliser adply pour les fonctions scalaires que je dois appliquer à chaque rangée.

par exemple.

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

Maintenant, j'utilise plus dplyr plus, je me demande s'il existe une façon naturelle de le faire? Comme c'est PAS ce que je veux:

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
111
Stephen Henderson

A partir de dplyr 0.2 (je pense) rowwise() est implémenté, la réponse à ce problème devient:

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

Non rowwise alternative

Cinq ans plus tard (!), Cette réponse génère encore beaucoup de trafic. Depuis qu'il a été donné, rowwise est de moins en moins recommandé, bien que beaucoup de gens semblent le trouver intuitif. Rendez-vous service et parcourez les informations de Jenny Bryan: flux de travaux orientés lignes en R avec le sens inverse pour bien maîtriser ce sujet.

Le moyen le plus simple que j'ai trouvé est basé sur l'un des exemples de Hadley utilisant pmap:

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

En utilisant cette approche, vous pouvez donner un nombre arbitraire d'arguments à la fonction (.f) dans pmap.

pmap est une bonne approche conceptuelle, car elle reflète le fait que lorsque vous effectuez des opérations par rangées, vous travaillez avec des n-uplets à partir d'une liste de vecteurs (les colonnes d'un cadre de données).

180
alexwhan

L’approche idiomatique consiste à créer une fonction vectorisée appropriée.

R fournit pmax ce qui convient ici, mais fournit également Vectorize comme wrapper pour mapply afin de vous permettre de créer une version arbitraire vectorisée d'une fonction arbitraire.

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))

Notez que l'implémentation de la vectorisation en C/C++ sera plus rapide, mais il n'y a pas de paquetage magicPony pour écrire la fonction pour vous.

20
mnel

Vous devez grouper par rangée:

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

C'est ce que le 1 a fait dans adply.

20
BrodieG

Mise à jour 2017-08-03

Après avoir écrit cela, Hadley a encore changé certaines choses. Les fonctions qui étaient auparavant dans purrr sont maintenant dans n nouveau paquet mixte appelé purrrlyr , décrit comme suit:

purrrlyr contient des fonctions situées à l'intersection de purrr et de dplyr. Ils ont été supprimés de Purrr afin d'alléger le paquet et parce qu'ils ont été remplacés par d'autres solutions de Tidyverse.

Donc, vous devrez installer + charger ce paquet pour que le code ci-dessous fonctionne.

Original post

Hadley change souvent d’avis sur ce que nous devrions utiliser, mais je pense que nous sommes supposés passer aux fonctions de purrr pour obtenir la fonctionnalité par ligne. Au moins, ils offrent les mêmes fonctionnalités et ont presque la même interface que adply de plyr .

Il existe deux fonctions associées, by_row et invoke_rows. Si je comprends bien, vous utilisez by_row lorsque vous souhaitez effectuer une boucle sur des lignes et ajouter les résultats à data.frame. invoke_rows est utilisé lorsque vous passez en boucle sur les lignes d'un data.frame et transmettez chaque colonne en tant qu'argument à une fonction. Nous n'utiliserons que le premier.

Exemples

library(tidyverse)

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

Cela nous permet de voir les éléments internes (pour que nous puissions voir ce que nous faisons), ce qui revient à le faire avec 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

Par défaut, by_row ajoute une colonne de liste basée sur la sortie:

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

donne:

# 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 au lieu de cela nous retournons un data.frame, nous obtenons une liste avec 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
    )
  })

donne:

# 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 manière dont nous ajoutons la sortie de la fonction est contrôlée par le paramètre .collate. Il y a trois options: liste, lignes, colonnes. Lorsque notre sortie a une longueur de 1, peu importe que nous utilisions des lignes ou des colonnes.

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
  })

les deux produisent:

# 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 nous produisons un data.frame avec 1 ligne, peu importe ce que nous utilisons:

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
    )
  })

les deux donnent:

# 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

sauf que le second a la colonne appelée .row et le premier pas.

Enfin, si notre sortie dépasse la longueur 1, soit en tant que vector, soit en tant que data.frame avec des lignes, il importe que nous utilisions des lignes ou des colonnes pour .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")

produit respectivement:

# 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

Donc, en bout de ligne. Si vous voulez la fonctionnalité adply(.margins = 1, ...), vous pouvez utiliser by_row.

17
Deleet

Prolonger la réponse de BrodieG,

Si la fonction renvoie plus d'une ligne, alors, au lieu de mutate(), do() doit être utilisé. Ensuite, pour le combiner, utilisez rbind_all() dans le package dplyr.

Dans dplyr version dplyr_0.1.2, utiliser 1:n() dans la clause group_by() ne fonctionne pas pour moi. Espérons que Hadley implémentera rowwise() bientôt.

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

Tester la performance,

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)

il a les résultats suivants:

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

Cela montre que la nouvelle version purrr est la plus rapide

14
momeara

Quelque chose comme ça?

iris$Max.Len <- pmax(iris$Sepal.Length, iris$Petal.Length)
1
colcarroll