Применять функцию к каждой строке таблицы с помощью dplyr?

121

При работе с ним plyrя часто находил полезным использовать adplyскалярные функции, которые мне приходилось применять к каждой строке.

например

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

Теперь я использую dplyrбольше, мне интересно, есть ли аккуратный / естественный способ сделать это? Поскольку это НЕ то, что я хочу:

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
Стивен Хендерсон
источник
Недавно я спросил, есть ли эквивалент mdplyin dplyr, и Хэдли предположил, что они могут варить что-то на основе do. Думаю, здесь тоже сработает.
baptiste
4
В конце концов, у dplyr будет что-то вроде того, rowwise()что будет группироваться по каждой отдельной строке
Хэдли
@hadley thx, разве он не должен вести себя так же, как adplyесли бы вы не использовали группировку? поскольку его тесно интегрированная функция называется group_byНЕsplit_by
Стивен Хендерсон
@StephenHenderson нет, потому что вам также нужен способ работы с таблицей в целом.
Хэдли
1
@HowYaDoing Да, но этот метод не обобщает. Например, нет psum, pmean или pmedian.
Стивен Хендерсон

Ответы:

202

Поскольку dplyr 0.2 (я думаю) rowwise()реализован, поэтому ответ на эту проблему таков:

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

Non rowwiseальтернатива

Спустя пять лет (!) Этот ответ все еще пользуется большой популярностью. Поскольку это было дано, rowwiseвсе чаще не рекомендуется, хотя многие люди, кажется, находят его интуитивно понятным. Сделайте себе одолжение и ознакомьтесь с рабочими процессами Дженни Брайан, ориентированными на строки в R, с материалом tidyverse, чтобы лучше разобраться в этой теме.

Самый простой способ, который я нашел, основан на одном из примеров Хэдли pmap:

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

Используя этот подход, вы можете передать произвольное количество аргументов функции ( .f) внутри pmap.

pmap - хороший концептуальный подход, поскольку он отражает тот факт, что при выполнении строковых операций вы фактически работаете с кортежами из списка векторов (столбцы в фрейме данных).

alexwhan
источник
Я изменил это (из приведенного выше) на идеальный ответ, так как я думаю, что это предполагаемое использование.
Стивен Хендерсон
1
можно ли добавить значения динамически сформированного фрейма данных? Итак, в этом фрейме данных имена столбцов неизвестны. Я могу добавить, если известны имена столбцов.
Арун Раджа
stackoverflow.com/questions/28807266/… только что нашел ответ. При этом они используют корреляцию вместо суммы. Но та же концепция.
Арун Раджа
13
Если это не сработает, убедитесь, что вы действительно используете dplyr :: mutate, а не plyr :: mutate - меня бесило
jan-glx
Спасибо ЯК, меня это тоже укусило. Если включить как plyrи dplyrпакеты, вы почти наверняка с помощью неправильно , mutateесли вы явно не предоставите сферы dplyr::mutate.
Крис Варт,
22

Идиоматический подход заключается в создании соответствующим образом векторизованной функции.

Rprovide, pmaxкоторый подходит здесь, однако он также предоставляет Vectorizeв качестве оболочки для mapplyсоздания векторизованной произвольной версии произвольной функции.

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

Обратите внимание, что реализация векторизации на C / C ++ будет быстрее, но нет magicPonyпакета, который напишет функцию за вас.

mnel
источник
спасибо, это отличный ответ, отличный общий стиль R - идиоматический, как вы говорите, но я не думаю, что он действительно решает мой вопрос, есть ли dplyrспособ ... так как было бы проще без dplyr, например, with(df, Coalesce(a,b))возможно, это ответ вроде - не использовать dplyrдля этого?
Стивен Хендерсон
4
Должен признать, я дважды проверил, что magicPonyпакета нет. Слишком плохо
rsoren
21

Вам нужно сгруппировать по строкам:

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

Это то, что 1сделали в adply.

BrodieG
источник
Похоже, должен быть более простой или «приятный» синтаксис.
Стивен Хендерсон
@StephenHenderson, может быть, я не dplyrэксперт. Надеюсь, кто-то другой предложит что-нибудь получше. Обратите внимание, я немного очистил его с помощью 1:n().
BrodieG
Я подозреваю, что вы правы, но мне кажется, что поведение по умолчанию без группировки должно быть таким же, как и group_by(1:n())поведение. Если утром ни у кого не будет других идей, я помечу вашу;)
Стивен Хендерсон
Также обратите внимание, что это несколько противоречит документации n: «Эта функция реализована специально для каждого источника данных и может использоваться только из summarize.», Хотя, похоже, это работает.
BrodieG
Можете ли вы каким-то образом ссылаться на Sepal.Length и Petal.Length по их порядковому номеру? Если у вас много переменных, это было бы удобно. Как ... Max.len = max ([c (1,3)])?
Расмус Ларсен
19

Обновление 2017-08-03

Написав это, Хэдли снова кое-что изменил. Функции, которые раньше находились в purrr, теперь находятся в новом смешанном пакете под названием purrrlyr , описанном как:

purrrlyr содержит некоторые функции, которые лежат на пересечении purrr и dplyr. Они были удалены из purrr, чтобы облегчить упаковку, и потому, что они были заменены другими решениями в tidyverse.

Итак, вам нужно будет установить + загрузить этот пакет, чтобы приведенный ниже код работал.

Исходный пост

Хэдли часто меняет свое мнение о том, что нам следует использовать, но я думаю, что мы должны переключиться на функции в purrr, чтобы получить функциональность по строкам. По крайней мере, они предлагают тот же функционал и почти такой же интерфейс, что и adplyу plyr .

Есть две связанные функции by_rowи invoke_rows. Насколько я понимаю, вы используете, by_rowкогда хотите перебрать строки и добавить результаты в data.frame. invoke_rowsиспользуется, когда вы перебираете строки в data.frame и передаете каждый столбец в качестве аргумента функции. Мы будем использовать только первый.

Примеры

library(tidyverse)

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

Это позволяет нам видеть внутреннее устройство (чтобы мы могли видеть, что мы делаем), что аналогично тому, как мы делаем это с 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

По умолчанию by_rowдобавляет столбец списка на основе вывода:

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

дает:

# 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

если вместо этого мы вернем a data.frame, мы получим список с 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
    )
  })

дает:

# 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

То, как мы добавляем вывод функции, контролируется параметром .collateparam. Есть три варианта: список, строки, столбцы. Когда наш вывод имеет длину 1, не имеет значения, используем ли мы строки или столбцы.

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

оба производят:

# 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

Если мы выводим data.frame с 1 строкой, неважно, что мы используем:

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

оба дают:

# 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

за исключением того, что во втором столбце .rowвызывается, а в первом нет.

Наконец, если наш вывод длиннее 1 либо как, vectorлибо как data.frameсо строками, то имеет значение, используем ли мы строки или столбцы для .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")

производит соответственно:

# 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

Итак, итоги. Если вам нужна adply(.margins = 1, ...)функциональность, вы можете использовать by_row.

CoderGuy123
источник
2
by_rowустарело, вызывая его, чтобы "использовать комбинацию: tidyr :: nest (); dplyr :: mutate (); purrr :: map ()" github.com/hadley/purrrlyr/blob/…
momeara
Это много r.
qwr
14

Расширяя ответ BrodieG,

Если функция возвращает более одной строки, вместо mutate(), do()необходимо использовать. Затем, чтобы снова соединить, используйте rbind_all()из dplyrупаковки.

В dplyrверсии dplyr_0.1.2использование 1:n()в group_by()предложении у меня не работает. Надеюсь, Хэдли скоро реализуетrowwise() .

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

Тестирование производительности,

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)

он дает следующие результаты:

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

Это показывает, что новая purrrверсия самая быстрая

momeara
источник
1

Что-то вроде этого?

iris$Max.Len <- pmax(iris$Sepal.Length, iris$Petal.Length)
colcarroll
источник
1
Да, спасибо, это очень конкретный ответ. Но мой пример и вопрос пытаются выяснить, есть ли общее dplyrрешение для какой-либо скалярной функции.
Стивен Хендерсон
В общем, функции должны быть векторизованы - если это дурацкая функция, вы можете написать wacky.function <- function(col.1, col.2){...}, а затем iris.wacky <- wacky.function(iris$Sepal.Length, iris$Petal.Length).
colcarroll
Часто им следует угадывать, но я думаю, когда вы используете что-то вроде dplyrили plyrили говорите, data.tableвы должны попытаться использовать их идиомы, чтобы ваш код не стал трудным для совместного использования сочетания стилей. Отсюда вопрос.
Стивен Хендерсон
Первая строка plyrдокументации: «plyr - это набор инструментов, который решает общий набор проблем: вам нужно разбить большую проблему на управляемые части, обработать каждую часть, а затем собрать все части вместе». Это похоже на совсем другую проблему, для которой элементарные операции с столбцами являются лучшим инструментом. Это также может объяснить, почему для этого нет "естественной" команды plyr/ dplyr.
colcarroll
5
Чтобы вырезать известную цитату: « Если все, что у вас есть, это плир, вы в конечном итоге используете его вместо молотка и отвертки »
thelatemail