Застосування функції до кожного рядка таблиці за допомогою 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

нещодавно я запитав, чи є еквівалент mdplyу dplyr, і Хадлі припустив, що вони можуть варити щось на основі do. Я здогадуюсь, це також працювало б тут.
баптист

4
Врешті-решт у dplyr з'явиться щось на кшталт того, rowwise()що б згрупувати кожен окремий ряд
hadley

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

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

Через п’ять років (!) Ця відповідь все ще отримує багато трафіку. Оскільки він був наданий, rowwiseвсе частіше не рекомендується, хоча багато людей, здається, вважають це інтуїтивно зрозумілим. Зробіть собі послугу і пройдіть робочий процес, орієнтований на Дженні Брайан на рядок у R, з охайною стороною матеріалом щоб отримати хорошу справу з цієї теми.

Найпростіший спосіб, який я знайшов, заснований на одному з прикладів Хедлі, використовуючи pmap:

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

Використовуючи такий підхід, ви можете навести довільну кількість аргументів функції ( .f) всередині pmap.

pmap це хороший концептуальний підхід, оскільки він відображає той факт, що, виконуючи операції з виконання рядкових операцій, ви насправді працюєте з кортежами зі списку векторів (стовпці в кадрі даних).


Я змінив це (з вищесказаного) на ідеальну відповідь, оскільки думаю, що це призначене використання.
Стівен Хендерсон

1
чи можна додати значення динамічно сформованого фрейму даних? Отже в цьому кадрі даних назви стовпців не відомі. Я можу додати, якщо відомі імена стовпців.
Арун Раджа

stackoverflow.com/questions/28807266/… щойно знайшов відповідь. У цьому вони використовують кореляцію замість суми. Але та сама концепція.
Арун Раджа

13
Якщо це не працює, переконайтеся, що ви фактично використовуєте dplyr :: mutate not plyr :: mutate - загнав мене горіхами
jan-glx

Дякую ЯК, і це мене теж покусало. Якщо ви включаєте і обидва, plyrі dplyrпакети, ви майже напевно використовуєте неправильне, mutateякщо явно не надаєте сферу застосування dplyr::mutate.
Кріс Уорт

22

Ідіоматичний підхід полягає у створенні відповідно векторизованої функції.

RПодайте, 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пакету, який би написав цю функцію для вас.


THX, це чудова відповідь, це відмінний загальний стиль 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.


Схоже, має бути простіший або «приємніший» синтаксис.
Стівен Хендерсон

@StephenHenderson, можливо, я не dplyrексперт. Сподіваємось, хтось інший прийде разом із чимось кращим. Зверніть увагу, я трохи почистив його 1:n().
BrodieG

Я підозрюю, що ти маєш рацію, але мені здається, що поведінка за замовчуванням без групування має бути схожою на group_by(1:n())поведінку. Якщо вранці ніхто не має інших ідей, я позначу ваші;)
Стівен Хендерсон

Також зауважте, що це дещо суперечить документації для n: "Ця функція реалізована спеціально для кожного джерела даних і може використовуватися лише з узагальненого підсумку.", Хоча це, здається, працює.
BrodieG

Чи можете ви якось посилатися на Sepal.Length та Petal.Length за їх індексним номером? Якщо у вас багато змінних, було б корисно. Як ... Макс.лен = макс ([c (1,3)])?
Расмус Ларсен

19

Оновити 2017-08-03

Написавши це, Хедлі знову змінив деякі речі. Функції, які раніше були у purrr, тепер у новому змішаному пакеті під назвою purrrlyr , описаному як:

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

Отже, вам потрібно буде встановити + завантажити цей пакет, щоб зробити код, який працює нижче.

Оригінальна публікація

Хедлі часто міняє свою думку про те, що нам слід використовувати, але я думаю, що ми повинні переключитися на функції в порядку, щоб отримати функціональність ряду. Принаймні, вони пропонують той же функціонал і мають майже той самий інтерфейс, що і 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

Як ми додаємо вихід функції, контролюється .collateпарам. Є три варіанти: список, рядки, команди. Коли наш вихід має довжину 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.


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версія є найшвидшою


1

Щось на зразок цього?

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

1
Так, це дуже конкретна відповідь. Але мій приклад і запитання намагаються дражнити, чи є загальне dplyrрішення для будь-якої скалярної функції.
Стівен Хендерсон

Загалом, функції повинні бути векторизованими - якщо це нерозважлива функція, ви можете написати wacky.function <- function(col.1, col.2){...}і потім iris.wacky <- wacky.function(iris$Sepal.Length, iris$Petal.Length).
коляска

Часто вони повинні здогадуватися, але я думаю, коли ви використовуєте щось подібне dplyrабо plyrскажете, data.tableвам слід спробувати використовувати їх ідіоми, щоб ваш код не став важким для спільного використання стилів. Звідси питання.
Стівен Хендерсон

Перший рядок plyrдокументації - "plyr" - це набір інструментів, що вирішує загальний набір проблем: вам потрібно розбити велику проблему на керовані частини, оперувати кожними фрагментами, а потім зібрати всі шматки назад. Це здається зовсім іншою проблемою, для якої елементарні операції стовпців є найкращим інструментом. Це також може пояснити, чому немає "природного" plyr/ dplyrкоманди для цього.
коляска

5
До м'яса відома цитата: " Якщо все, що у вас є, це плір, ви в кінцевому підсумку використовуєте його і для молотка, і для викрутки "
thelatemail
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.