Appliquer une fonction à chaque ligne d'une table à l'aide de dplyr?

121

Lorsque plyrje travaille avec, j'ai souvent trouvé utile d'utiliser adplypour les fonctions scalaires que je dois appliquer à chaque ligne.

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 dplyrplus, je me demande s'il existe un moyen ordonné / naturel de le faire? Comme ce n'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
Stephen Henderson
la source
J'ai récemment demandé s'il y avait un équivalent de mdplyin dplyr, et hadley a suggéré qu'ils pourraient préparer quelque chose basé sur do. Je suppose que cela fonctionnerait également ici.
baptiste le
4
Finalement, dplyr aura quelque chose comme rowwise()qui regrouperait par chaque ligne individuelle
hadley
@hadley thx, cela ne devrait-il pas se comporter comme adplylorsque vous n'utilisez pas de regroupement? car sa fonction étroitement intégrée s'appelle group_byPASsplit_by
Stephen Henderson
@StephenHenderson non, car vous avez également besoin d'un moyen d'opérer sur la table dans son ensemble.
hadley
1
@HowYaDoing Oui, mais cette méthode ne se généralise pas. Il n'y a pas de psum, pmean ou pmedian par exemple.
Stephen Henderson

Réponses:

202

À 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 rowwisealternative

Cinq ans (!) Plus tard, cette réponse reçoit encore beaucoup de trafic. Depuis qu'il a été donné, il rowwiseest de moins en moins recommandé, bien que beaucoup de gens semblent le trouver intuitif. Faites-vous une faveur et parcourez les flux de travail orientés Row de Jenny Bryan en R avec le matériel tidyverse pour bien comprendre 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) à l'intérieur pmap.

pmap est une bonne approche conceptuelle car elle reflète le fait que lorsque vous effectuez des opérations par ligne, vous travaillez en fait avec des tuples à partir d'une liste de vecteurs (les colonnes d'un dataframe).

Alexwhan
la source
J'ai changé cela (de ce qui précède) à la réponse idéale car je pense que c'est l'utilisation prévue.
Stephen Henderson
1
est-il possible d'ajouter les valeurs d'un datatframe formé dynamiquement? Ainsi, dans ce bloc de données, les noms des colonnes ne sont pas connus. Je suis en mesure d'ajouter si les noms de colonnes sont connus.
Arun Raja
stackoverflow.com/questions/28807266/… vient de trouver la réponse. En cela, ils utilisent la corrélation au lieu de la somme. Mais même concept.
Arun Raja
13
Si cela ne fonctionne pas, assurez-vous que vous utilisez réellement dplyr :: mutate not plyr :: mutate - drove me fous
jan-glx
Merci YAK, ça m'a mordu aussi. Si vous incluez à la fois plyret des dplyrpackages, vous utilisez presque certainement le mauvais, mutatesauf si vous fournissez explicitement la portée dplyr::mutate.
Chris Warth
22

L'approche idiomatique sera de créer une fonction vectorisée de manière appropriée.

Rfournit pmaxce qui convient ici, mais il fournit également Vectorizeun wrapper pour mapplyvous 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 magicPonypackage qui écrira la fonction pour vous.

mnel
la source
thx, c'est une excellente réponse, c'est un excellent style général R -idiomatique comme tu le dis, mais je ne pense pas que ça répond vraiment à ma question de savoir s'il y a un dplyrmoyen ... car ce serait plus simple sans déplyr par exemple with(df, Coalesce(a,b))peut-être, c'est un genre de réponse cependant - ne pas utiliser dplyrpour ça?
Stephen Henderson
4
Je dois admettre que j'ai vérifié qu'il n'y avait pas de magicPonycolis. Dommage
rsoren
21

Vous devez regrouper par ligne:

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

C'est ce que le 1fait adply.

BrodieG
la source
Il semble qu'il devrait y avoir une syntaxe plus simple ou «plus agréable».
Stephen Henderson
@StephenHenderson, il y en a peut-être, je ne suis pas un dplyrexpert. J'espère que quelqu'un d'autre viendra avec quelque chose de mieux. Notez que je l'ai nettoyé un peu avec 1:n().
BrodieG
Je suppose que vous avez raison, mais j'ai en quelque sorte le sentiment que le comportement par défaut sans regroupement devrait être comme le group_by(1:n())comportement. Si personne n'a d'autres idées le matin, je coche les vôtres;)
Stephen Henderson
Notez également que cela est en quelque sorte en contradiction avec la documentation pour n: "Cette fonction est implémentée spécialement pour chaque source de données et ne peut être utilisée qu'à partir de la synthèse.", Bien que cela semble fonctionner.
BrodieG
Pouvez-vous faire référence à Sepal.Length et Petal.Length par leur numéro d'index d'une manière ou d'une autre? Si vous avez beaucoup de variables, ce serait pratique. Comme ... Max.len = max ([c (1,3)])?
Rasmus Larsen
19

Mise à jour 03/08/2017

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

purrrlyr contient des fonctions qui se trouvent à l'intersection de purrr et dplyr. Ils ont été retirés de purrr afin d'alléger l'emballage et parce qu'ils ont été remplacés par d'autres solutions dans le tidyverse.

Vous devrez donc installer + charger ce package pour que le code ci-dessous fonctionne.

Message original

Hadley change fréquemment d'avis sur ce que nous devrions utiliser, mais je pense que nous sommes censé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 adplyde plyr .

Il existe deux fonctions liées, by_rowet invoke_rows. Je crois comprendre que vous utilisez by_rowlorsque vous souhaitez boucler sur des lignes et ajouter les résultats au data.frame. invoke_rowsest utilisé lorsque vous bouclez sur les lignes d'un data.frame et passez chaque col comme 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 (afin 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_rowajoute 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 à la place on retourne a data.frame, on obtient 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 façon dont nous ajoutons la sortie de la fonction est contrôlée par le .collateparamètre. Il y a trois options: liste, lignes, cols. Lorsque notre sortie a la longueur 1, peu importe que nous utilisions des lignes ou des 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
  })

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 sortons 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 .rowet le premier pas.

Enfin, si notre sortie est plus longue que la longueur 1 sous forme de a vectorou de data.frameavec des lignes, alors il importe que nous utilisions des lignes ou des cols 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 adply(.margins = 1, ...)fonctionnalité, vous pouvez utiliser by_row.

CoderGuy123
la source
2
by_rowest obsolète, l'appelant dit "utiliser une combinaison de: tidyr :: nest (); dplyr :: mutate (); purrr :: map ()" github.com/hadley/purrrlyr/blob/…
momeara
C'est beaucoup de r.
qwr
14

Prolongeant la réponse de BrodieG,

Si la fonction renvoie plus d'une ligne, alors au lieu de mutate(), do()doit être utilisée. Ensuite, pour le combiner à nouveau, utilisez-le rbind_all()dans l' dplyremballage.

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

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

Tester les performances,

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 purrrversion est la plus rapide

momeara
la source
1

Quelque chose comme ça?

iris$Max.Len <- pmax(iris$Sepal.Length, iris$Petal.Length)
colcarroll
la source
1
Oui merci, c'est une réponse très précise. Mais mon exemple et ma question essaient de déterminer s'il existe une dplyrsolution générale pour une fonction scalaire.
Stephen Henderson
En général, les fonctions doivent être vectorisées - si c'est une fonction farfelue, vous pouvez écrire wacky.function <- function(col.1, col.2){...}, puis iris.wacky <- wacky.function(iris$Sepal.Length, iris$Petal.Length).
colcarroll
Souvent, je devrais deviner, mais je pense que lorsque vous utilisez quelque chose comme dplyrou plyrou dites que data.tablevous devriez essayer d'utiliser leurs expressions idiomatiques afin que votre code ne devienne pas un mélange de styles difficile à partager. D'où la question.
Stephen Henderson
La première ligne de la plyrdocumentation est "plyr est un ensemble d'outils qui résout un ensemble commun de problèmes: vous devez décomposer un gros problème en morceaux gérables, opérer sur chaque pièce et ensuite remonter toutes les pièces". Cela semble être un problème très différent pour lequel les opérations élémentaires sur les colonnes sont le meilleur outil. Cela pourrait aussi expliquer pourquoi il n'y a pas « naturel » plyr/ dplyrcommande pour ce faire.
colcarroll
5
Pour massacrer une citation célèbre: " Si tout ce que vous avez est un plyr, vous finirez par l'utiliser pour un marteau et un tournevis aussi "
thelatemail