Applicare una funzione a ogni riga di una tabella utilizzando dplyr?


121

Quando lavoro con plyrho trovato spesso utile utilizzare adplyper le funzioni scalari che devo applicare a ogni riga.

per esempio

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

Ora ne sto usando di dplyrpiù, mi chiedo se esiste un modo ordinato / naturale per farlo? Poiché questo NON è quello che voglio:

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

Recentemente ho chiesto se ci fosse un equivalente di mdplyin dplyr e Hadley ha suggerito che potrebbero preparare qualcosa basato su do. Immagino che funzionerebbe anche qui.
baptiste

4
Alla fine dplyr avrà qualcosa di simile rowwise()che raggrupperebbe per ogni singola riga
hadley il

@hadley grazie, non dovrebbe comportarsi come adplyquando non usi un raggruppamento? poiché la sua funzione strettamente integrata si chiama group_byNOTsplit_by
Stephen Henderson

@StephenHenderson no, perché hai anche bisogno di un modo per operare sul tavolo nel suo insieme.
Hadley

1
@ HowYaDoing Sì ma questo metodo non generalizza. Ad esempio, non esistono psum, pmean o pmedian.
Stephen Henderson

Risposte:


202

A partire da dplyr 0.2 (penso) rowwise()è implementato, quindi la risposta a questo problema diventa:

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

Non rowwisealternativa

Cinque anni (!) Dopo questa risposta riceve ancora molto traffico. Da quando è stato dato, rowwiseè sempre più sconsigliato, anche se molte persone sembrano trovarlo intuitivo. Fatti un favore e segui i flussi di lavoro orientati alla riga di Jenny Bryan in R con il materiale ordinato per ottenere una buona gestione su questo argomento.

Il modo più semplice che ho trovato è basato su uno degli esempi di Hadley che utilizza pmap:

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

Usando questo approccio, puoi dare un numero arbitrario di argomenti alla funzione ( .f) all'interno pmap.

pmap è un buon approccio concettuale perché riflette il fatto che quando esegui operazioni per riga, stai effettivamente lavorando con tuple da un elenco di vettori (le colonne in un dataframe).


Ho cambiato questo (da quanto sopra) con la risposta ideale poiché penso che questo sia l'uso previsto.
Stephen Henderson,

1
è possibile aggiungere i valori di un datatframe formato dinamicamente? Quindi in questo frame di dati i nomi delle colonne non sono noti. Sono in grado di aggiungere se i nomi delle colonne sono noti.
Arun Raja

stackoverflow.com/questions/28807266/… ha appena trovato la risposta. In questo stanno usando la correlazione invece della somma. Ma stesso concetto.
Arun Raja

13
Se non funziona, assicurati di utilizzare effettivamente dplyr :: mutate non plyr :: mutate - mi ha fatto impazzire
jan-glx

Grazie YAK, anche questo mi ha morso. Se includi entrambi i pacchetti plyre dplyr, quasi certamente stai usando quello sbagliato a mutatemeno che non fornisci esplicitamente l'ambito dplyr::mutate.
Chris Warth

22

L'approccio idiomatico sarà quello di creare una funzione adeguatamente vettorializzata.

Rfornire pmaxche è adatto qui, tuttavia fornisce anche Vectorizeun wrapper per mapplyconsentire di creare una versione arbitraria vettorizzata di una funzione 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))

Nota che l'implementazione della vettorizzazione in C / C ++ sarà più veloce, ma non esiste un magicPonypacchetto che scriverà la funzione per te.


grazie, questa è un'ottima risposta, è un eccellente stile generale R -idiomatico come dici tu, ma non penso che stia davvero affrontando la mia domanda se esiste un dplyrmodo ... poiché sarebbe più semplice senza dplyr ad esempio with(df, Coalesce(a,b))Forse, è un tipo di risposta però - non usare dplyrper quello?
Stephen Henderson

4
Devo ammettere che ho ricontrollato che non ci sia un magicPonypacco. Peccato
rsoren

21

Devi raggruppare per riga:

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

Questo è ciò che hanno 1fatto adply.


Sembra che dovrebbe esserci una sintassi più semplice o "più gradevole".
Stephen Henderson

@StephenHenderson, potrebbe esserci, non sono un dplyresperto. Se tutto va bene, qualcun altro verrà con qualcosa di meglio. Nota che l'ho ripulito un po 'con 1:n().
BrodieG

Sospetto che tu abbia ragione, ma penso che il comportamento predefinito senza raggruppamento dovrebbe essere simile al group_by(1:n())comportamento. Se nessuno ha altre idee domattina, spunta le tue;)
Stephen Henderson

Inoltre, si noti che ciò è in qualche modo in contrasto con la documentazione per n: "Questa funzione è implementata in modo speciale per ciascuna origine dati e può essere utilizzata solo dall'interno di riepilogo.", Sebbene sembri funzionare.
BrodieG

Puoi fare riferimento a Sepal.Length e Petal.Length in qualche modo con il loro numero di indice? Se hai molte variabili sarebbe utile. Come ... Max.len = max ([c (1,3)])?
Rasmus Larsen

19

Aggiornamento 2017-08-03

Dopo aver scritto questo, Hadley ha cambiato di nuovo alcune cose. Le funzioni che erano in purrr ora sono in un nuovo pacchetto misto chiamato purrrlyr , descritto come:

purrrlyr contiene alcune funzioni che si trovano all'intersezione di purrr e dplyr. Sono state tolte dal purrr per alleggerire il pacco e perché sono state sostituite da altre soluzioni nel tidyverse.

Quindi, dovrai installare + caricare quel pacchetto per far funzionare il codice seguente.

Post originale

Hadley cambia spesso idea su cosa dovremmo usare, ma penso che dovremmo passare alle funzioni in purrr per ottenere la funzionalità per riga. Almeno, offrono la stessa funzionalità e hanno quasi la stessa interfaccia adplydi plyr .

Ci sono due funzioni correlate, by_rowe invoke_rows. La mia comprensione è che si utilizza by_rowquando si desidera eseguire il ciclo su righe e aggiungere i risultati a data.frame. invoke_rowsviene utilizzato quando si esegue il ciclo su righe di un data.frame e si passa ogni colonna come argomento a una funzione. Useremo solo il primo.

Esempi

library(tidyverse)

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

Questo ci consente di vedere gli interni (così possiamo vedere cosa stiamo facendo), che è lo stesso che farlo con 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

Per impostazione predefinita, by_rowaggiunge una colonna di elenco basata sull'output:

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

dà:

# 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

se invece restituiamo a data.frame, otteniamo 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
    )
  })

dà:

# 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

Il modo in cui aggiungiamo l'output della funzione è controllato dal .collateparametro. Sono disponibili tre opzioni: elenco, righe, colonne. Quando il nostro output ha lunghezza 1, non importa se usiamo righe o colonne.

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

entrambi producono:

# 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

Se produciamo un data.frame con 1 riga, importa solo leggermente quale usiamo:

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

entrambi danno:

# 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

tranne che il secondo ha la colonna chiamata .rowe il primo no.

Infine, se il nostro output è più lungo della lunghezza 1 sia come a vectorche come data.framecon righe, è importante che usiamo righe o colonne per .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, rispettivamente:

# 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

Quindi, linea di fondo. Se vuoi la adply(.margins = 1, ...)funzionalità, puoi usare by_row.


2
by_rowè deprecato, chiamandolo dice di "utilizzare una combinazione di: tidyr :: nest (); dplyr :: mutate (); purrr :: map ()" github.com/hadley/purrrlyr/blob/…
momeara

Sono un sacco di r.
qwr

14

Estendendo la risposta di BrodieG,

Se la funzione restituisce più di una riga mutate(), è do()necessario utilizzare invece di. Quindi per combinarlo di nuovo insieme, utilizzare rbind_all()dal dplyrpacchetto.

Nella dplyrversione dplyr_0.1.2, l'utilizzo 1:n()nella group_by()clausola non funziona per me. Si spera che Hadley lo implementeràrowwise() presto.

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

Testare le prestazioni,

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)

ha i seguenti risultati:

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

Questo dimostra che la nuova purrrversione è la più veloce


1

Qualcosa come questo?

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

1
Sì grazie, questa è una risposta molto specifica. Ma il mio esempio e la mia domanda stanno cercando di capire se esiste una dplyrsoluzione generale per qualsiasi funzione scalare.
Stephen Henderson

In generale, le funzioni dovrebbero essere vettorializzate: se è una funzione stravagante, potresti scrivere wacky.function <- function(col.1, col.2){...}e poi iris.wacky <- wacky.function(iris$Sepal.Length, iris$Petal.Length).
colcarroll

Spesso dovrebbero immaginarlo, ma penso che quando usi qualcosa come dplyro plyro dici data.tableche dovresti provare a usare i loro idiomi in modo che il tuo codice non diventi un mix di stili difficile da condividere. Da qui la domanda.
Stephen Henderson

La prima riga della plyrdocumentazione è "plyr è un insieme di strumenti che risolve una serie comune di problemi: è necessario suddividere un grosso problema in parti gestibili, operare su ogni pezzo e poi rimettere insieme tutti i pezzi". Questo sembra un problema molto diverso per il quale le operazioni di colonna elementari sono lo strumento migliore. Questo potrebbe anche spiegare perché non esiste un comando plyr/ "naturale" dplyrper farlo.
colcarroll

5
Per massacrare una famosa citazione: " Se tutto quello che hai è un pennarello finirai per usarlo anche come martello e come cacciavite "
thelatemail
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.