Stosowanie funkcji do każdego wiersza tabeli przy użyciu narzędzia dplyr?


121

Podczas pracy plyrczęsto uważałem, że przydatne jest użycie adplyfunkcji skalarnych, które muszę zastosować do każdego wiersza.

na przykład

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

Teraz używam dplyrwięcej, zastanawiam się, czy istnieje uporządkowany / naturalny sposób na zrobienie tego? Ponieważ NIE tego chcę:

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

Niedawno zapytałem, czy istnieje odpowiednik mdplyin dplyr, a hadley zasugerował, że mogą parzyć coś na podstawie do. Myślę, że to też by działało tutaj.
baptiste

4
Ostatecznie dplyr będzie miał coś takiego, rowwise()co będzie grupowane według każdego wiersza
Hadley

@hadley thx, czy nie powinno jednak zachowywać się tak, jak adplywtedy, gdy nie używasz grupowania? ponieważ jego ściśle zintegrowana funkcja nazywa się group_byNOTsplit_by
Stephen Henderson

@StephenHenderson nie, ponieważ potrzebujesz także sposobu na operowanie całym stołem.
hadley,

1
@HowYaDoing Tak, ale ta metoda nie uogólnia. Na przykład nie ma psum, pmean ani pmedian.
Stephen Henderson

Odpowiedzi:


202

Od dplyr 0.2 (myślę) rowwise()jest zaimplementowany, więc odpowiedź na ten problem brzmi:

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

Brak rowwisealternatywy

Pięć lat (!) Później ta odpowiedź jest nadal bardzo ruchliwa. Odkąd została podana, rowwisecoraz częściej nie jest zalecana, chociaż wiele osób wydaje się uważać ją za intuicyjną. Zrób sobie przysługę i przejdź przez przepływy pracy Jenny Bryan zorientowane na Row w R z uporządkowanym materiałem, aby dobrze zrozumieć ten temat.

Najprostszy sposób, jaki znalazłem, oparty jest na jednym z przykładów Hadleya z użyciem pmap:

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

Korzystając z tego podejścia, możesz podać dowolną liczbę argumentów funkcji ( .f) wewnątrz pmap.

pmap jest dobrym podejściem koncepcyjnym, ponieważ odzwierciedla fakt, że wykonując operacje na wierszach, w rzeczywistości pracujesz z krotkami z listy wektorów (kolumnami w ramce danych).


Zmieniłem to (z powyższego) na idealną odpowiedź, ponieważ myślę, że jest to zamierzone użycie.
Stephen Henderson

1
czy można dodać wartości dynamicznie utworzonej ramki danych? Więc w tej ramce danych nazwy kolumn nie są znane. Jestem w stanie dodać, jeśli nazwy kolumn są znane.
Arun Raja

stackoverflow.com/questions/28807266/ ... właśnie znalazłem odpowiedź. W tym przypadku używają korelacji zamiast sumy. Ale ta sama koncepcja.
Arun Raja

13
Jeśli to nie działa, upewnij się, że rzeczywiście przy dplyr :: mutować nie plyr :: mutacji - zawiózł mnie orzechy
jan-GLX

Dzięki YAK, to też mnie ugryzło. Jeśli dołączysz zarówno pakiety, jak plyri dplyr, prawie na pewno używasz niewłaściwego, mutatechyba że jawnie podasz zakres dplyr::mutate.
Chris Warth,

22

Idiomatycznym podejściem będzie stworzenie odpowiednio wektoryzowanej funkcji.

Rzapewnia, pmaxktóry jest tutaj odpowiedni, ale zapewnia również Vectorizejako opakowanie mapplyumożliwiające utworzenie wektoryzowanej dowolnej wersji dowolnej funkcji.

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

Zauważ, że implementacja wektoryzacji w C / C ++ będzie szybsza, ale nie ma magicPonypakietu, który zapisze funkcję za Ciebie.


thx, jest to świetna odpowiedź, jest doskonała ogólny styl R -idiomatic jak mówisz, ale nie sądzę, jest naprawdę adresowania moje pytanie, czy istnieje dplyrsposób ... jak byłoby prostsze bez dplyr np with(df, Coalesce(a,b))Być może, że jest to rodzaj odpowiedzi - nie używać dplyrdo tego?
Stephen Henderson

4
Muszę przyznać, że dwukrotnie sprawdziłem, czy nie ma magicPonypaczki. Szkoda
rsoren

21

Musisz pogrupować według wiersza:

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

Oto, co 1zrobili adply.


Wygląda na to, że powinna istnieć prostsza lub „ładniejsza” składnia.
Stephen Henderson

@StephenHenderson, może być, nie jestem dplyrekspertem. Miejmy nadzieję, że ktoś inny przyjdzie z czymś lepszym. Uwaga: trochę go wyczyściłem 1:n().
BrodieG

Podejrzewam, że masz rację, ale wydaje mi się, że domyślne zachowanie bez grupowania powinno być takie jak group_by(1:n())zachowanie. Jeśli nikt nie ma innych pomysłów rano, zaznaczę twój;)
Stephen Henderson

Należy również zauważyć, że jest to nieco sprzeczne z dokumentacją dotyczącą n: „Ta funkcja jest zaimplementowana specjalnie dla każdego źródła danych i może być używana tylko z poziomu podsumowania.”, Chociaż wydaje się, że działa.
BrodieG

Czy możesz w jakiś sposób odnieść się do Sepal.Length i Petal.Length za pomocą ich numeru indeksu? Jeśli masz wiele zmiennych, byłoby to przydatne. Na przykład ... Max.len = max ([c (1,3)])?
Rasmus Larsen

19

Aktualizacja 2017-08-03

Po napisaniu tego Hadley ponownie zmienił kilka rzeczy. Funkcje, które kiedyś znajdowały się w mruczeniu, są teraz w nowym mieszanym pakiecie o nazwie purrrlyr , opisanym jako:

purrrlyr zawiera pewne funkcje, które znajdują się na przecięciu purrr i dplyr. Zostały usunięte z mruczenia, aby opakowanie było lżejsze i ponieważ zostały zastąpione innymi rozwiązaniami w porządku.

Będziesz więc musiał zainstalować + załadować ten pakiet, aby poniższy kod działał.

Oryginalny post

Hadley często zmienia zdanie na temat tego, czego powinniśmy użyć, ale myślę, że powinniśmy przełączyć się na funkcje w mruczeniu, aby uzyskać funkcjonalność według wiersza. Przynajmniej, oferują taką samą funkcjonalność i mają prawie taki sam interfejs jak adplyz plyr .

Istnieją dwie powiązane funkcje by_rowi invoke_rows. Rozumiem, że używasz go, by_rowgdy chcesz zapętlić wiersze i dodać wyniki do data.frame. invoke_rowsjest używany, gdy zapętlasz wiersze data.frame i przekazujesz każdy col jako argument funkcji. Użyjemy tylko pierwszego.

Przykłady

library(tidyverse)

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

To pozwala nam zobaczyć elementy wewnętrzne (abyśmy mogli zobaczyć, co robimy), co jest tym samym, co robienie tego z 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

Domyślnie by_rowdodaje kolumnę listy na podstawie wyniku:

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

daje:

# 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

jeśli zamiast tego zwrócimy a data.frame, otrzymamy listę z 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
    )
  })

daje:

# 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

Sposób dodawania danych wyjściowych funkcji jest kontrolowany przez .collateparametr. Dostępne są trzy opcje: lista, wiersze, kolumny. Kiedy nasze dane wyjściowe mają długość 1, nie ma znaczenia, czy używamy wierszy, czy kolumn.

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

oba produkują:

# 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

Jeśli wyprowadzimy ramkę data.frame z 1 wierszem, liczy się tylko nieznacznie, którego użyjemy:

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

obie dają:

# 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

poza tym, że druga ma wywołaną kolumnę, .rowa pierwsza nie.

Wreszcie, jeśli nasze dane wyjściowe są dłuższe niż długość 1 jako a vectorlub jako a data.framez wierszami, wtedy ma znaczenie, czy użyjemy wierszy, czy kolumn do .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")

produkuje odpowiednio:

# 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

Tak więc, podsumowanie. Jeśli chcesz mieć adply(.margins = 1, ...)funkcjonalność, możesz użyć by_row.


2
by_rowjest przestarzały, wywołując go w celu „użyj kombinacji: tidyr :: nest (); dplyr :: mutate (); purrr :: map ()” github.com/hadley/purrrlyr/blob/…
momeara

To dużo r.
qwr

14

Rozszerzając odpowiedź BrodieG,

Jeśli funkcja zwraca więcej niż jeden wiersz mutate(), do()należy użyć zamiast . Następnie, aby ponownie połączyć, użyj rbind_all()z dplyropakowania.

W dplyrwersji dplyr_0.1.2używanie 1:n()w group_by()klauzuli nie działa dla mnie. Miejmy nadzieję, że Hadleyrowwise() wkrótce się wdroży .

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

Testowanie wydajności,

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)

ma następujące wyniki:

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

To pokazuje, że nowa purrrwersja jest najszybsza


1

Coś takiego?

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

1
Tak, dzięki, to bardzo konkretna odpowiedź. Ale mój przykład i pytanie próbują wydusić, czy istnieje ogólne dplyrrozwiązanie dla dowolnej funkcji skalarnej.
Stephen Henderson

Ogólnie funkcje powinny być wektoryzowane - jeśli jest to funkcja zwariowana, możesz napisać wacky.function <- function(col.1, col.2){...}, a potem iris.wacky <- wacky.function(iris$Sepal.Length, iris$Petal.Length).
colcarroll

Często powinienem, ale myślę, że kiedy używasz czegoś takiego jak dplyrlub plyrlub mówisz data.table, powinieneś spróbować użyć ich idiomów, aby twój kod nie stał się trudnym do podzielenia się mieszanką stylów. Stąd pytanie.
Stephen Henderson

Pierwsza linia plyrdokumentacji brzmi: „plyr to zestaw narzędzi, które rozwiązują typowy zestaw problemów: musisz rozbić duży problem na łatwe do zarządzania części, operować na każdym kawałku, a następnie złożyć wszystkie elementy razem”. Wydaje się, że to zupełnie inny problem, dla którego podstawowym narzędziem są operacje na kolumnach elementarnych. Może to również wyjaśniać, dlaczego nie ma „naturalnego” polecenia plyr/ dplyrdo tego.
colcarroll

5
Aby rzucić słynny cytat: „ Jeśli wszystko, co masz, to plyr, w końcu użyjesz go również do młotka i śrubokręta
thelatemail
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.