Anwenden einer Funktion auf jede Zeile einer Tabelle mit dplyr?

121

Bei der Arbeit mit fand plyrich es oft nützlich, adplyfür Skalarfunktionen zu verwenden, die ich auf jede einzelne Zeile anwenden muss.

z.B

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

Jetzt benutze ich dplyrmehr und frage mich, ob es einen ordentlichen / natürlichen Weg gibt, dies zu tun? Da dies NICHT das ist, was ich will:

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
quelle
Ich habe kürzlich gefragt, ob es ein Äquivalent von mdplyin dplyr gibt, und Hadley hat vorgeschlagen, dass sie etwas basierend auf brauen könnten do. Ich denke, es würde auch hier funktionieren.
Taufe
4
Irgendwann wird dplyr so etwas haben, rowwise()das sich nach jeder einzelnen Reihe
gruppieren
@ Hadley Danke, sollte es sich nicht einfach so verhalten, adplywenn Sie keine Gruppierung verwenden? als seine eng integrierte Funktion heißt group_byNOTsplit_by
Stephen Henderson
@StephenHenderson nein, weil Sie auch eine Möglichkeit benötigen, um den gesamten Tisch zu bearbeiten.
Hadley
1
@HowYaDoing Ja, aber diese Methode verallgemeinert nicht. Es gibt zum Beispiel kein Psum, PMean oder Pmedian.
Stephen Henderson

Antworten:

202

Ab dplyr 0.2 (glaube ich) rowwise()ist implementiert, so lautet die Antwort auf dieses Problem:

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

Keine rowwiseAlternative

Fünf Jahre (!) Später bekommt diese Antwort immer noch viel Verkehr. Da es gegeben wurde, rowwisewird es zunehmend nicht empfohlen, obwohl viele Leute es intuitiv zu finden scheinen. Tun Sie sich selbst einen Gefallen und gehen Sie Jenny Bryans zeilenorientierte Workflows in R mit dem aufgeräumten Material durch, um dieses Thema gut in den Griff zu bekommen.

Der einfachste Weg, den ich gefunden habe, basiert auf einem von Hadleys Beispielen mit pmap:

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

Mit diesem Ansatz können Sie der Funktion ( .f) im Inneren eine beliebige Anzahl von Argumenten geben pmap.

pmap ist ein guter konzeptioneller Ansatz, da er die Tatsache widerspiegelt, dass Sie bei zeilenweisen Operationen tatsächlich mit Tupeln aus einer Liste von Vektoren (den Spalten in einem Datenrahmen) arbeiten.

alexwhan
quelle
Ich habe dies (von oben) in die ideale Antwort geändert, da ich denke, dass dies die beabsichtigte Verwendung ist.
Stephen Henderson
1
Ist es möglich, die Werte eines dynamisch geformten Datenrahmens hinzuzufügen? In diesem Datenrahmen sind die Spaltennamen also nicht bekannt. Ich kann hinzufügen, ob Spaltennamen bekannt sind.
Arun Raja
stackoverflow.com/questions/28807266/… hat gerade die Antwort gefunden. Dabei verwenden sie Korrelation anstelle von Summe. Aber das gleiche Konzept.
Arun Raja
13
Wenn es nicht funktioniert, stellen Sie sicher, dass Sie tatsächlich dplyr :: mutate verwenden, nicht plyr :: mutate - hat mich verrückt gemacht
jan-glx
Danke YAK, das hat mich auch gebissen. Wenn Sie sowohl plyrals auch dplyrPakete einschließen , verwenden Sie mit ziemlicher Sicherheit das Falsche, es mutatesei denn, Sie geben ausdrücklich den Umfang an dplyr::mutate.
Chris Warth
22

Der idiomatische Ansatz besteht darin, eine entsprechend vektorisierte Funktion zu erstellen.

Rbieten , pmaxdie hier geeignet ist, aber es bietet auch Vectorizeals Wrapper für mapplyermöglichen, eine vektorisiert beliebige Version einer beliebigen Funktion zu erstellen.

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

Beachten Sie, dass die Implementierung der Vektorisierung in C / C ++ schneller ist, es jedoch kein magicPonyPaket gibt, das die Funktion für Sie schreibt.

mnel
quelle
thx, das ist eine großartige Antwort, ist ein ausgezeichneter allgemeiner R-Stil - wie Sie sagen, aber ich denke nicht, dass er meine Frage wirklich beantwortet, ob es einen dplyrWeg gibt ... wie es ohne dplyr einfacher wäre, zB with(df, Coalesce(a,b))Vielleicht ist das ein Art der Antwort - nicht dafür verwenden dplyr?
Stephen Henderson
4
Ich muss zugeben, dass ich doppelt überprüft habe, dass es kein magicPonyPaket gibt. Schade
rsoren
21

Sie müssen nach Zeilen gruppieren:

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

Dies ist , was das 1tat in adply.

BrodieG
quelle
Es scheint, dass es eine einfachere oder "schönere" Syntax geben sollte.
Stephen Henderson
@ StephenHenderson, vielleicht bin ich kein dplyrExperte. Hoffentlich kommt jemand anderes mit etwas Besserem. Beachten Sie, dass ich es ein bisschen aufgeräumt habe 1:n().
BrodieG
Ich vermute, Sie haben Recht, aber ich habe das Gefühl, dass das Standardverhalten ohne Gruppierung dem group_by(1:n())Verhalten entsprechen sollte. Wenn am Morgen niemand andere Ideen hat, kreuze ich deine an;)
Stephen Henderson
Beachten Sie auch, dass dies etwas im Widerspruch zur Dokumentation steht für n: "Diese Funktion ist speziell für jede Datenquelle implementiert und kann nur innerhalb von summate verwendet werden.", Obwohl sie zu funktionieren scheint.
BrodieG
Können Sie Sepal.Length und Petal.Length in irgendeiner Weise anhand ihrer Indexnummer referenzieren? Wenn Sie viele Variablen haben, wäre das praktisch. Wie ... Max.len = max ([c (1,3)])?
Rasmus Larsen
19

Update 2017-08-03

Nachdem Hadley dies geschrieben hatte, änderte er wieder einige Dinge. Die Funktionen, die früher in purrr enthalten waren, befinden sich jetzt in einem neuen gemischten Paket namens purrrlyr , das wie folgt beschrieben wird:

purrrlyr enthält einige Funktionen, die am Schnittpunkt von purrr und dplyr liegen. Sie wurden aus purrr entfernt, um die Verpackung leichter zu machen, und weil sie durch andere Lösungen in der Tidyverse ersetzt wurden.

Sie müssen dieses Paket also installieren und laden, damit der folgende Code funktioniert.

Ursprünglicher Beitrag

Hadley ändert häufig seine Meinung darüber, was wir verwenden sollen, aber ich denke, wir sollten zu den Funktionen in purrr wechseln, um die Funktionalität nach Zeilen zu erhalten. Zumindest bieten sie die gleiche Funktionalität und haben fast die gleiche Oberfläche wie adplyvon plyr .

Es gibt zwei verwandte Funktionen by_rowund invoke_rows. Mein Verständnis ist, dass Sie verwenden, by_rowwenn Sie Zeilen durchlaufen und die Ergebnisse zum data.frame hinzufügen möchten. invoke_rowswird verwendet, wenn Sie Zeilen eines data.frame durchlaufen und jede Spalte als Argument an eine Funktion übergeben. Wir werden nur die erste verwenden.

Beispiele

library(tidyverse)

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

Auf diese Weise können wir die Interna sehen (damit wir sehen können, was wir tun), was dem entspricht, mit dem wir es tun 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

Fügt standardmäßig by_roweine Listenspalte hinzu, die auf der Ausgabe basiert:

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

gibt:

# 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

Wenn wir stattdessen a zurückgeben data.frame, erhalten wir eine Liste mit 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
    )
  })

gibt:

# 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

Wie wir die Ausgabe der Funktion hinzufügen, wird durch den .collateParameter gesteuert . Es gibt drei Optionen: Liste, Zeilen, Spalten. Wenn unsere Ausgabe die Länge 1 hat, spielt es keine Rolle, ob wir Zeilen oder Spalten verwenden.

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

beide produzieren:

# 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

Wenn wir einen data.frame mit 1 Zeile ausgeben, ist es nur unwichtig, welche wir verwenden:

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

beide geben:

# 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

außer dass die zweite die aufgerufene Spalte hat .rowund die erste nicht.

Wenn unsere Ausgabe länger als Länge 1 ist, entweder als vectoroder als data.framemit Zeilen, ist es wichtig, ob wir Zeilen oder Spalten verwenden für .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")

produziert jeweils:

# 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

Unterm Strich also. Wenn Sie die adply(.margins = 1, ...)Funktionalität wünschen , können Sie verwenden by_row.

CoderGuy123
quelle
2
by_rowist veraltet und heißt "eine Kombination aus: tidyr :: nest (); dplyr :: mutate (); purrr :: map ()" github.com/hadley/purrrlyr/blob/…
momeara
Das sind viele Rs.
qwr
14

Erweiterung der Antwort von BrodieG,

Wenn die Funktion mehr als eine Zeile zurückgibt mutate(), do()muss anstelle von verwendet werden. Um es dann wieder zusammen zu kombinieren, verwenden Sie es rbind_all()aus der dplyrPackung.

In der dplyrVersion funktioniert die dplyr_0.1.2Verwendung 1:n()in der group_by()Klausel bei mir nicht. Hoffentlich wird Hadleyrowwise() bald implementieren .

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

Testen der Leistung,

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)

es hat die folgenden Ergebnisse:

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

Dies zeigt, dass die neue purrrVersion die schnellste ist

Momeara
quelle
1

Etwas wie das?

iris$Max.Len <- pmax(iris$Sepal.Length, iris$Petal.Length)
Colcarroll
quelle
1
Ja danke, das ist eine sehr spezifische Antwort. Aber mein Beispiel und meine Frage versuchen herauszufinden, ob es eine allgemeine dplyrLösung für eine Skalarfunktion gibt.
Stephen Henderson
Im Allgemeinen sollten Funktionen vektorisiert werden - wenn es sich um eine verrückte Funktion handelt, können Sie schreiben wacky.function <- function(col.1, col.2){...}und dann iris.wacky <- wacky.function(iris$Sepal.Length, iris$Petal.Length).
Colcarroll
Oft sollten sie ich denke, aber ich denke, wenn Sie etwas wie dplyroder plyroder verwenden, data.tablesollten Sie versuchen, ihre Redewendungen zu verwenden, damit Ihr Code nicht zu einem schwierig zu teilenden Stilmix wird. Daher die Frage.
Stephen Henderson
Die erste Zeile der plyrDokumentation lautet: "Plyr ist eine Reihe von Werkzeugen, die häufig auftretende Probleme lösen: Sie müssen ein großes Problem in überschaubare Teile zerlegen, jedes Teil bearbeiten und dann alle Teile wieder zusammensetzen." Dies scheint ein ganz anderes Problem zu sein, für das elementare Spaltenoperationen das beste Werkzeug sind. Dies könnte auch erklären, warum es dafür keinen "natürlichen" plyr/ dplyrBefehl gibt.
Colcarroll
5
Um ein berühmtes Zitat zu schlachten : " Wenn Sie nur einen Plyr haben, werden Sie ihn am Ende auch für einen Hammer und einen Schraubenzieher verwenden "
E-Mail vom