Aplicando uma função a todas as linhas de uma tabela usando o dplyr?

121

Ao trabalhar com plyr, muitas vezes achei útil usar adplypara funções escalares que tenho que aplicar a cada linha.

por exemplo

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

Agora estou usando dplyrmais, estou me perguntando se existe uma maneira organizada / natural de fazer isso? Como não é isso que eu quero:

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
fonte
Recentemente, perguntei se havia um equivalente mdplyno dplyr e hadley sugeriu que eles estivessem fabricando algo com base do. Eu acho que também funcionaria aqui.
baptiste
4
Eventualmente dplyr terá algo parecido com rowwise()o que grupo por cada linha individual
Hadley
@hadley thx, não deveria se comportar como adplyquando você não usa um agrupamento? como sua função intimamente integrada é chamada group_byNOTsplit_by
Stephen Henderson
@StephenHenderson não, porque você também precisa de alguma maneira de operar a mesa como um todo.
Hadley
1
@HowYaDoing Sim, mas esse método não generaliza. Não há psum, pmean ou pmedian, por exemplo.
Stephen Henderson

Respostas:

202

A partir do dplyr 0.2 (eu acho) rowwise()implementado, a resposta para esse problema se torna:

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

Não rowwisealternativa

Cinco anos (!) Depois, essa resposta ainda recebe muito tráfego. Desde que foi dado, rowwiseé cada vez mais recomendado, embora muitas pessoas pareçam achar isso intuitivo. Faça um favor a si mesmo e repasse os fluxos de trabalho orientados a Row de Jenny Bryan em R com o material arrumado para entender melhor esse tópico.

A maneira mais direta que encontrei é baseada em um dos exemplos de Hadley usando pmap:

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

Usando essa abordagem, você pode fornecer um número arbitrário de argumentos para a função ( .f) dentro pmap.

pmap é uma boa abordagem conceitual porque reflete o fato de que, quando você está executando operações de linha, na verdade está trabalhando com tuplas de uma lista de vetores (as colunas em um quadro de dados).

alexwhan
fonte
Eu mudei isso (acima) para a resposta ideal, pois acho que esse é o uso pretendido.
Stephen Henderson
1
é possível adicionar os valores de um quadro de dados formado dinamicamente? Portanto, nesse quadro de dados, os nomes das colunas não são conhecidos. Eu posso adicionar se os nomes das colunas forem conhecidos.
Arun Raja
stackoverflow.com/questions/28807266/… acabou de encontrar a resposta. Nisso, eles estão usando correlação em vez de soma. Mas mesmo conceito.
Arun Raja
13
Se ele não funciona, verifique se você está realmente usando dplyr :: mutação não plyr :: mutação - me deixou louco
jan-GLX
Obrigado YAK, isso também me mordeu. Se você incluir os pacotes plyre os dois dplyr, certamente estará usando errado, a mutatemenos que forneça explicitamente o escopo dplyr::mutate.
Chris Warth
22

A abordagem idiomática será criar uma função adequadamente vetorizada.

Rforneça o pmaxque é adequado aqui, no entanto, ele também fornece Vectorizecomo wrapper para mapplypermitir que você crie uma versão arbitrária vetorizada de uma função arbitrária.

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

Observe que a implementação da vetorização em C / C ++ será mais rápida, mas não há um magicPonypacote que escreva a função para você.

mnel
fonte
thx, esta é uma ótima resposta, é excelente em geral, estilo R - idiomático como você diz, mas não acho que esteja realmente respondendo à minha pergunta se existe uma dplyrmaneira ... como seria mais simples sem o dplyr, por exemplo. with(df, Coalesce(a,b))Talvez, esse seja um tipo de resposta - não use dplyrpara isso?
Stephen Henderson
4
Tenho que admitir que verifiquei que não há um magicPonypacote. Muito ruim
rsoren 30/03
21

Você precisa agrupar por linha:

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

Isto é o que o 1fez adply.

BrodieG
fonte
Parece que deve haver uma sintaxe mais simples ou "mais agradável".
Stephen Henderson
@ StephenHenderson, pode haver, eu não sou dplyrespecialista. Espero que outra pessoa venha com algo melhor. Note que eu limpei um pouco com 1:n().
BrodieG
Eu suspeito que você esteja certo, mas acho que o comportamento padrão sem agrupamento deve ser o mesmo group_by(1:n()). Se ninguém tem outras ideias na parte da manhã eu vou assinalar o seu;)
Stephen Henderson
Além disso, observe que isso contraria a documentação de n: "Esta função é implementada especial para cada fonte de dados e só pode ser usada a partir de um resumo.", Embora pareça funcionar.
BrodieG
Você pode consultar Sepal.Length e Petal.Length pelo número de índice de alguma forma? Se você tiver muitas variáveis, seria útil. Como ... Max.len = max ([c (1,3)])?
Rasmus Larsen
19

Atualização 2017-08-03

Depois de escrever isso, Hadley mudou algumas coisas novamente. As funções que costumavam estar no purrr agora estão em um novo pacote misto chamado purrrlyr , descrito como:

O purrrlyr contém algumas funções que se encontram na interseção de purrr e dplyr. Eles foram removidos do ronronar para tornar a embalagem mais leve e porque foram substituídos por outras soluções na ordem inversa.

Portanto, você precisará instalar + carregar esse pacote para fazer o código abaixo funcionar.

Postagem original

Hadley frequentemente muda de idéia sobre o que devemos usar, mas acho que devemos mudar para as funções no ronronar para obter a funcionalidade por linha. Pelo menos, eles oferecem a mesma funcionalidade e têm quase a mesma interface queadply do plyr .

Existem duas funções relacionadas by_roweinvoke_rows . Meu entendimento é que você usa by_rowquando deseja fazer um loop sobre linhas e adicionar os resultados ao data.frame. invoke_rowsé usado quando você faz um loop sobre as linhas de um data.frame e passa cada coluna como argumento para uma função. Vamos usar apenas o primeiro.

Exemplos

library(tidyverse)

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

Isso nos permite ver os internos (para que possamos ver o que estamos fazendo), que é o mesmo que fazê-lo com 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

Por padrão, by_row adiciona uma coluna da lista com base na saída:

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 retornarmos a data.frame, obteremos uma lista comdata.frame s:

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

Como adicionamos a saída da função é controlado pelo .collateparâmetro Existem três opções: lista, linhas, colunas. Quando nossa saída possui comprimento 1, não importa se usamos linhas ou colunas.

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

ambos produzem:

# 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 produzirmos um data.frame com 1 linha, importa apenas um pouco o que usamos:

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

ambos dão:

# 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

exceto que o segundo tem a coluna chamada .rowe o primeiro não.

Por fim, se nossa saída for maior que o comprimento 1 como a vectorou data.framecom linhas, será importante usarmos linhas ou colunas para .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")

produz, respectivamente:

# 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

Então, linha de fundo. Se você deseja a adply(.margins = 1, ...)funcionalidade, pode usar by_row.

CoderGuy123
fonte
2
by_rowestá obsoleto, chamando-o de "use uma combinação de: tidyr :: nest (); dplyr :: mutate (); purrr :: map ()" github.com/hadley/purrrlyr/blob/…
momeara
São muitos r's.
QWR
14

Estendendo a resposta de BrodieG,

Se a função retornar mais de uma linha, em vez de mutate(), do()deverá ser usada. Em seguida, para combiná-lo novamente, use rbind_all()o dplyrpacote.

Na dplyrversão dplyr_0.1.2, usar 1:n()a group_by()cláusula não funciona para mim. Espero que o Hadley seja implementado emrowwise() breve.

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

Testando o desempenho,

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)

tem os seguintes resultados:

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

Isso mostra que a nova purrrversão é a mais rápida

momeara
fonte
1

Algo assim?

iris$Max.Len <- pmax(iris$Sepal.Length, iris$Petal.Length)
colcarroll
fonte
1
Sim, essa é uma resposta muito específica. Mas meu exemplo e pergunta estão tentando esclarecer se existe uma dplyrsolução geral para qualquer função escalar.
Stephen Henderson
Em geral, as funções devem ser vetorizadas - se for uma função maluca, você pode escrever wacky.function <- function(col.1, col.2){...}e depois iris.wacky <- wacky.function(iris$Sepal.Length, iris$Petal.Length).
colcarroll
Freqüentemente eles devem adivinhar, mas acho que quando você usa algo como dplyrou plyrou diz data.tableque deve tentar usar os idiomas deles para que seu código não se torne difícil de compartilhar uma mistura de estilos. Daí a questão.
Stephen Henderson
A primeira linha da plyrdocumentação é "plyr é um conjunto de ferramentas que resolve um conjunto comum de problemas: você precisa dividir um grande problema em pedaços gerenciáveis, operar em cada um deles e depois juntar todos os pedaços". Parece um problema muito diferente para o qual as operações elementares da coluna são a melhor ferramenta. Isso também pode explicar por que não há nenhuma "natural" plyr/ dplyrcomando para fazer isso.
colcarroll
5
Para massacrar uma citação famosa: " Se tudo o que você tem é um plyr, você também o usará como martelo e chave de fenda "
thelatemail