dplyr мутує / замінює кілька стовпців у підмножині рядків


85

Я в процесі випробування робочого процесу на основі dplyr (а не використовую в основному data.table, до якого я звик), і я зіткнувся з проблемою, через яку не можу знайти рівнозначного рішення dplyr . Я зазвичай стикаюся зі сценарієм, коли мені потрібно умовно оновити / замінити кілька стовпців на основі однієї умови. Ось приклад коду з моїм рішенням data.table:

library(data.table)

# Create some sample data
set.seed(1)
dt <- data.table(site = sample(1:6, 50, replace=T),
                 space = sample(1:4, 50, replace=T),
                 measure = sample(c('cfl', 'led', 'linear', 'exit'), 50, 
                               replace=T),
                 qty = round(runif(50) * 30),
                 qty.exit = 0,
                 delta.watts = sample(10.5:100.5, 50, replace=T),
                 cf = runif(50))

# Replace the values of several columns for rows where measure is "exit"
dt <- dt[measure == 'exit', 
         `:=`(qty.exit = qty,
              cf = 0,
              delta.watts = 13)]

Чи існує просте рішення dplyr для цієї самої проблеми? Я хотів би уникати використання ifelse, оскільки я не хочу, щоб набирати умову кілька разів - це спрощений приклад, але іноді існує багато призначень на основі однієї умови.

Заздалегідь дякуємо за допомогу!

Відповіді:


81

Ці рішення (1) підтримують конвеєр, (2) не перезаписують вхідні дані (3) вимагають, щоб умова була вказана один раз:

1a) mutate_cond Створіть просту функцію для фреймів даних або таблиць даних, які можна включити в конвеєри. Ця функція подібна, mutateале діє лише на рядки, що задовольняють умові:

mutate_cond <- function(.data, condition, ..., envir = parent.frame()) {
  condition <- eval(substitute(condition), .data, envir)
  .data[condition, ] <- .data[condition, ] %>% mutate(...)
  .data
}

DF %>% mutate_cond(measure == 'exit', qty.exit = qty, cf = 0, delta.watts = 13)

1b) mutate_last Це альтернативна функція для фреймів даних або таблиць даних, яка знову схожа, mutateале використовується лише всередині group_by(як у прикладі нижче) і працює лише з останньою групою, а не з кожною групою. Зверніть увагу, що TRUE> FALSE, тому, якщо group_byвказує умову, тоді mutate_lastбуде діяти лише в рядках, що задовольняють цій умові.

mutate_last <- function(.data, ...) {
  n <- n_groups(.data)
  indices <- attr(.data, "indices")[[n]] + 1
  .data[indices, ] <- .data[indices, ] %>% mutate(...)
  .data
}


DF %>% 
   group_by(is.exit = measure == 'exit') %>%
   mutate_last(qty.exit = qty, cf = 0, delta.watts = 13) %>%
   ungroup() %>%
   select(-is.exit)

2) вилучити умову Виділити умову, зробивши з неї додатковий стовпець, який згодом буде видалено. Потім з допомогою ifelse, replaceабо арифметичний з логічними виразами, як показано. Це також працює для таблиць даних.

library(dplyr)

DF %>% mutate(is.exit = measure == 'exit',
              qty.exit = ifelse(is.exit, qty, qty.exit),
              cf = (!is.exit) * cf,
              delta.watts = replace(delta.watts, is.exit, 13)) %>%
       select(-is.exit)

3) sqldf Ми могли б використовувати SQL updateчерез пакет sqldf в конвеєрі для фреймів даних (але не таблиць даних, якщо ми їх не перетворимо - це може представляти помилку в dplyr. Див. Випуск dplyr 1579 ). Може здатися, що ми небажано модифікуємо введення в цьому коді через існування, updateале насправді updateвін діє на копію вхідних даних у тимчасово сформованій базі даних, а не на фактичний вхід.

library(sqldf)

DF %>% 
   do(sqldf(c("update '.' 
                 set 'qty.exit' = qty, cf = 0, 'delta.watts' = 13 
                 where measure = 'exit'", 
              "select * from '.'")))

4) row_case_when Також ознайомтесь із row_case_whenвизначеним у Поверненні таблиці: як векторизувати case_when? . Він використовує синтаксис, подібний до, case_whenале застосовується до рядків.

library(dplyr)

DF %>%
  row_case_when(
    measure == "exit" ~ data.frame(qty.exit = qty, cf = 0, delta.watts = 13),
    TRUE ~ data.frame(qty.exit, cf, delta.watts)
  )

Примітка 1. Ми використовували це якDF

set.seed(1)
DF <- data.frame(site = sample(1:6, 50, replace=T),
                 space = sample(1:4, 50, replace=T),
                 measure = sample(c('cfl', 'led', 'linear', 'exit'), 50, 
                               replace=T),
                 qty = round(runif(50) * 30),
                 qty.exit = 0,
                 delta.watts = sample(10.5:100.5, 50, replace=T),
                 cf = runif(50))

Примітка 2: Проблема того, як легко визначити оновлення підмножини рядків, також обговорюється у виданнях dplyr 134 , 631 , 1518 та 1573, причому 631 є основним потоком, а 1573 - оглядом відповідей тут.


1
Відмінна відповідь, дякую! І ваш mutate_cond, і @Kevin Ushey's mutate_when є хорошими рішеннями цієї проблеми. Я думаю, що я трохи віддаю перевагу читабельності / гнучкості mutate_when, але я дам цій відповіді "перевірку" на ретельність.
Кріс Ньютон,

Мені дуже подобається підхід mutate_cond. Мені теж здається, що ця функція або щось дуже близьке до неї заслуговує на включення до dplyr і було б кращим рішенням, ніж VectorizedSwitch (що обговорюється в github.com/hadley/dplyr/issues/1573 ) для випадку використання, про який думають люди приблизно тут ...
Магнус

Я люблю mutate_cond. Різні варіанти мали бути окремими відповідями.
Холгер Брандл

Минуло пару років, і проблеми з github здаються закритими і заблокованими. Чи існує офіційне рішення цієї проблеми?
static_rtti

27

Ви можете зробити це за допомогою magrittrдвосторонньої труби %<>%:

library(dplyr)
library(magrittr)

dt[dt$measure=="exit",] %<>% mutate(qty.exit = qty,
                                    cf = 0,  
                                    delta.watts = 13)

Це зменшує кількість набору тексту, але все ще набагато повільніше, ніж data.table.


Насправді, тепер, коли я мав можливість це перевірити, я віддав би перевагу рішенню, яке уникає необхідності підмножини, використовуючи нотацію dt [dt $ measure == 'exit',], оскільки це може стати громіздкішим із довшим dt імена.
Кріс Ньютон,

Просто FYI, але це рішення буде працювати, лише якщо data.frame/ tibbleвже містить стовпець, визначений mutate. Це не спрацює, якщо ви намагаєтеся додати новий стовпець, наприклад, перший раз пробігшись через цикл та змінивши a data.frame.
Ursus Frost

@UrsusFrost додавання нового стовпця, який є лише підмножиною набору даних, мені здається дивним. Ви додаєте NA до рядків, які не є підгрупованими?
Баралюх,

@Baraliuh Так, я можу це оцінити. Це частина циклу, в якому я збільшую та додаю дані за списком дат. Перші кілька дат повинні трактуватися інакше, ніж наступні дати, оскільки вони відтворюють реальні бізнес-процеси. У подальших ітераціях, залежно від умов дат, дані обчислюються по-різному. Через обумовленість, я не хочу ненавмисно змінювати попередні дати в data.frame. FWIW, я просто повернувся до використання, data.tableа не dplyrтому, що його iвираз це легко справляється - плюс загальний цикл працює набагато швидше.
Урсус Мороз

18

Ось рішення, яке мені подобається:

mutate_when <- function(data, ...) {
  dots <- eval(substitute(alist(...)))
  for (i in seq(1, length(dots), by = 2)) {
    condition <- eval(dots[[i]], envir = data)
    mutations <- eval(dots[[i + 1]], envir = data[condition, , drop = FALSE])
    data[condition, names(mutations)] <- mutations
  }
  data
}

Це дозволяє писати такі речі, як напр

mtcars %>% mutate_when(
  mpg > 22,    list(cyl = 100),
  disp == 160, list(cyl = 200)
)

що є цілком читабельним - хоча воно може бути не таким продуктивним, як могло б бути.


14

Як показано вище eipi10, існує непростий спосіб замінити підмножину в dplyr, оскільки DT використовує семантику передачі-посилання проти dplyr, використовуючи передане значення. dplyr вимагає використання ifelse()на всьому векторі, тоді як DT буде виконувати підмножину та оновлювати за посиланням (повертаючи весь DT). Отже, для цієї вправи ДТ буде значно швидшим.

Ви можете спочатку підмножину, потім оновити і, нарешті, рекомбінувати:

dt.sub <- dt[dt$measure == "exit",] %>%
  mutate(qty.exit= qty, cf= 0, delta.watts= 13)

dt.new <- rbind(dt.sub, dt[dt$measure != "exit",])

Але DT буде значно швидшим: (відредаговано для використання нової відповіді eipi10)

library(data.table)
library(dplyr)
library(microbenchmark)
microbenchmark(dt= {dt <- dt[measure == 'exit', 
                            `:=`(qty.exit = qty,
                                 cf = 0,
                                 delta.watts = 13)]},
               eipi10= {dt[dt$measure=="exit",] %<>% mutate(qty.exit = qty,
                                cf = 0,  
                                delta.watts = 13)},
               alex= {dt.sub <- dt[dt$measure == "exit",] %>%
                 mutate(qty.exit= qty, cf= 0, delta.watts= 13)

               dt.new <- rbind(dt.sub, dt[dt$measure != "exit",])})


Unit: microseconds
expr      min        lq      mean   median       uq      max neval cld
     dt  591.480  672.2565  747.0771  743.341  780.973 1837.539   100  a 
 eipi10 3481.212 3677.1685 4008.0314 3796.909 3936.796 6857.509   100   b
   alex 3412.029 3637.6350 3867.0649 3726.204 3936.985 5424.427   100   b

10

Я просто натрапив на це і дуже подобається mutate_cond()@G. Гротендіком, але вважав, що це може стати в нагоді також для обробки нових змінних. Отже, нижче є два доповнення:

Без зв’язку: Другий останній рядок зробив трохи більше dplyrза допомогоюfilter()

Три нові рядки на початку отримують імена змінних для використання в mutate()і ініціалізують будь-які нові змінні у фреймі даних до того, як це mutate()відбудеться. Нові змінні ініціалізуються до кінця data.frameвикористання new_init, для якого NAза замовчуванням встановлено відсутній ( ).

mutate_cond <- function(.data, condition, ..., new_init = NA, envir = parent.frame()) {
  # Initialize any new variables as new_init
  new_vars <- substitute(list(...))[-1]
  new_vars %<>% sapply(deparse) %>% names %>% setdiff(names(.data))
  .data[, new_vars] <- new_init

  condition <- eval(substitute(condition), .data, envir)
  .data[condition, ] <- .data %>% filter(condition) %>% mutate(...)
  .data
}

Ось кілька прикладів використання даних райдужки:

Змінити Petal.Lengthна 88 де Species == "setosa". Це буде працювати як в оригінальній функції, так і в цій новій версії.

iris %>% mutate_cond(Species == "setosa", Petal.Length = 88)

Те саме, що вище, але також створіть нову змінну x( NAу рядках, не включених до умови). Раніше не можна.

iris %>% mutate_cond(Species == "setosa", Petal.Length = 88, x = TRUE)

Так само, як і вище, але рядки, не включені в умову для, xмають значення FALSE.

iris %>% mutate_cond(Species == "setosa", Petal.Length = 88, x = TRUE, new_init = FALSE)

Цей приклад показує, як new_initможна встановити значення a listдля ініціалізації кількох нових змінних з різними значеннями. Тут створюються дві нові змінні з виключеними рядками, які ініціалізуються за допомогою різних значень ( xініціалізуються як FALSE, yяк NA)

iris %>% mutate_cond(Species == "setosa" & Sepal.Length < 5,
                  x = TRUE, y = Sepal.Length ^ 2,
                  new_init = list(FALSE, NA))

Ваша mutate_condфункція виявляє помилку в моєму наборі даних, а функція Гротендікса - ні. Error: incorrect length (4700), expecting: 168Здається, це пов’язано з функцією фільтра.
RHA

Ви помістили це в бібліотеку чи формалізували як функцію? Здається, нічого не вимагає, особливо з усіма вдосконаленнями.
Кропива,

1
Ні. Я думаю, що найкращим підходом до dplyr на даний момент є поєднання мутації з if_elseабо case_when.
Саймон Джексон,

Чи можете ви навести приклад (або посилання) на цей підхід?
Кропива

6

mutate_cond - чудова функція, але вона видає помилку, якщо в стовпці (колонках), що використовується для створення умови, є NA. Я вважаю, що умовна мутація повинна просто залишити такі ряди в спокої. Це відповідає поведінці filter (), який повертає рядки, коли умова TRUE, але опускає обидва рядки з FALSE і NA.

За допомогою цієї невеликої зміни функція працює як шарм:

mutate_cond <- function(.data, condition, ..., envir = parent.frame()) {
    condition <- eval(substitute(condition), .data, envir)
    condition[is.na(condition)] = FALSE
    .data[condition, ] <- .data[condition, ] %>% mutate(...)
    .data
}

Дякую Магнусу! Я використовую це для оновлення таблиці, що містить дії та таймінги для всіх об’єктів, що складають анімацію. Я зіткнувся з проблемою NA, оскільки дані настільки різноманітні, що деякі дії не мають сенсу для деяких об’єктів, тому в цих клітинках є NA. Інший mutate_cond вище зазнав аварії, але ваше рішення спрацювало як шарм.
Філ ван Клер

Якщо це вам корисно, ця функція доступна в невеликому пакеті, який я написав, "zulutils". Він не на CRAN, але ви можете встановити його за допомогою пультів дистанційного керування :: install_github ("torfason / zulutils")
Magnus

Чудово! Дуже дякую. Я все ще користуюся ним.
Філ ван Клер,

4

Насправді я не бачу жодних змін, dplyrякі б значно полегшили це. case_whenчудово підходить для випадків, коли для одного стовпця існує кілька різних умов і результатів, але це не допомагає в цьому випадку, коли ви хочете змінити кілька стовпців на основі однієї умови. Подібним чином, recodeзберігає введення тексту, якщо ви замінюєте кілька різних значень в одному стовпці, але не допомагає робити це в декількох стовпцях одночасно. Нарешті, mutate_atтощо застосовують умови лише до імен стовпців, а не до рядків у фреймі даних. Ви потенційно можете написати функцію для mutate_at, яка це зробить, але я не можу зрозуміти, як ви змусили б її поводитися по-різному для різних стовпців.

Тут сказано, як би я підійшов до цього, використовуючи nestформу tidyrта mapз purrr.

library(data.table)
library(dplyr)
library(tidyr)
library(purrr)

# Create some sample data
set.seed(1)
dt <- data.table(site = sample(1:6, 50, replace=T),
                 space = sample(1:4, 50, replace=T),
                 measure = sample(c('cfl', 'led', 'linear', 'exit'), 50, 
                                  replace=T),
                 qty = round(runif(50) * 30),
                 qty.exit = 0,
                 delta.watts = sample(10.5:100.5, 50, replace=T),
                 cf = runif(50))

dt2 <- dt %>% 
  nest(-measure) %>% 
  mutate(data = if_else(
    measure == "exit", 
    map(data, function(x) mutate(x, qty.exit = qty, cf = 0, delta.watts = 13)),
    data
  )) %>%
  unnest()

1
Єдине, що я б запропонував, це використовувати, nest(-measure)щоб уникнутиgroup_by
Dave Gruenewald

Відредаговано для відображення пропозиції
@DaveGruenewald

4

Одним лаконічним рішенням було б зробити мутацію у відфільтрованому підмножині, а потім додати назад невихідні рядки таблиці:

library(dplyr)

dt %>% 
    filter(measure == 'exit') %>%
    mutate(qty.exit = qty, cf = 0, delta.watts = 13) %>%
    rbind(dt %>% filter(measure != 'exit'))

3

Зі створенням rlangможлива дещо змінена версія прикладу Гротендика 1a, усуваючи потребу в envirаргументі, оскільки enquo()фіксує середовище, яке .pстворюється автоматично.

mutate_rows <- function(.data, .p, ...) {
  .p <- rlang::enquo(.p)
  .p_lgl <- rlang::eval_tidy(.p, .data)
  .data[.p_lgl, ] <- .data[.p_lgl, ] %>% mutate(...)
  .data
}

dt %>% mutate_rows(measure == "exit", qty.exit = qty, cf = 0, delta.watts = 13)

2

Ви можете розділити набір даних і здійснити звичайний виклик для мутації TRUEчастини.

dplyr 0.8 має функцію, group_splitяка розбивається на групи (а групи можна визначити безпосередньо у виклику), тому ми будемо використовувати її тут, але вона також base::splitпрацює.

library(tidyverse)
df1 %>%
  group_split(measure == "exit", keep=FALSE) %>% # or `split(.$measure == "exit")`
  modify_at(2,~mutate(.,qty.exit = qty, cf = 0, delta.watts = 13)) %>%
  bind_rows()

#    site space measure qty qty.exit delta.watts          cf
# 1     1     4     led   1        0        73.5 0.246240409
# 2     2     3     cfl  25        0        56.5 0.360315879
# 3     5     4     cfl   3        0        38.5 0.279966850
# 4     5     3  linear  19        0        40.5 0.281439486
# 5     2     3  linear  18        0        82.5 0.007898384
# 6     5     1  linear  29        0        33.5 0.392412729
# 7     5     3  linear   6        0        46.5 0.970848817
# 8     4     1     led  10        0        89.5 0.404447182
# 9     4     1     led  18        0        96.5 0.115594622
# 10    6     3  linear  18        0        15.5 0.017919745
# 11    4     3     led  22        0        54.5 0.901829577
# 12    3     3     led  17        0        79.5 0.063949974
# 13    1     3     led  16        0        86.5 0.551321441
# 14    6     4     cfl   5        0        65.5 0.256845013
# 15    4     2     led  12        0        29.5 0.340603733
# 16    5     3  linear  27        0        63.5 0.895166931
# 17    1     4     led   0        0        47.5 0.173088800
# 18    5     3  linear  20        0        89.5 0.438504370
# 19    2     4     cfl  18        0        45.5 0.031725246
# 20    2     3     led  24        0        94.5 0.456653397
# 21    3     3     cfl  24        0        73.5 0.161274319
# 22    5     3     led   9        0        62.5 0.252212124
# 23    5     1     led  15        0        40.5 0.115608182
# 24    3     3     cfl   3        0        89.5 0.066147321
# 25    6     4     cfl   2        0        35.5 0.007888337
# 26    5     1  linear   7        0        51.5 0.835458916
# 27    2     3  linear  28        0        36.5 0.691483644
# 28    5     4     led   6        0        43.5 0.604847889
# 29    6     1  linear  12        0        59.5 0.918838163
# 30    3     3  linear   7        0        73.5 0.471644760
# 31    4     2     led   5        0        34.5 0.972078100
# 32    1     3     cfl  17        0        80.5 0.457241602
# 33    5     4  linear   3        0        16.5 0.492500255
# 34    3     2     cfl  12        0        44.5 0.804236607
# 35    2     2     cfl  21        0        50.5 0.845094268
# 36    3     2  linear  10        0        23.5 0.637194873
# 37    4     3     led   6        0        69.5 0.161431896
# 38    3     2    exit  19       19        13.0 0.000000000
# 39    6     3    exit   7        7        13.0 0.000000000
# 40    6     2    exit  20       20        13.0 0.000000000
# 41    3     2    exit   1        1        13.0 0.000000000
# 42    2     4    exit  19       19        13.0 0.000000000
# 43    3     1    exit  24       24        13.0 0.000000000
# 44    3     3    exit  16       16        13.0 0.000000000
# 45    5     3    exit   9        9        13.0 0.000000000
# 46    2     3    exit   6        6        13.0 0.000000000
# 47    4     1    exit   1        1        13.0 0.000000000
# 48    1     1    exit  14       14        13.0 0.000000000
# 49    6     3    exit   7        7        13.0 0.000000000
# 50    2     4    exit   3        3        13.0 0.000000000

Якщо порядок рядків має значення, tibble::rowid_to_columnспочатку використовуйте , потім dplyr::arrangeувімкніть rowidі виділіть його в кінці.

даних

df1 <- data.frame(site = sample(1:6, 50, replace=T),
                 space = sample(1:4, 50, replace=T),
                 measure = sample(c('cfl', 'led', 'linear', 'exit'), 50, 
                                  replace=T),
                 qty = round(runif(50) * 30),
                 qty.exit = 0,
                 delta.watts = sample(10.5:100.5, 50, replace=T),
                 cf = runif(50),
                 stringsAsFactors = F)

2

Думаю, про цю відповідь раніше не згадувалося. Він працює майже так само швидко, як рішення за замовчуванням data.table.

Використовуйте base::replace()

df %>% mutate( qty.exit = replace( qty.exit, measure == 'exit', qty[ measure == 'exit'] ),
                          cf = replace( cf, measure == 'exit', 0 ),
                          delta.watts = replace( delta.watts, measure == 'exit', 13 ) )

replace переробляє значення заміни, тому, коли ви хочете, щоб значення стовпців, qtyвведені в colums qty.exit, вам також потрібно було б підмножити qty ... отже, qty[ measure == 'exit']при першій заміні ..

тепер, ви, мабуть, не хочете measure == 'exit'весь час повторно вводити текст ... так що ви можете створити вектор-індекс, що містить цей вибір, і використовувати його у наведених вище функціях.

#build an index-vector matching the condition
index.v <- which( df$measure == 'exit' )

df %>% mutate( qty.exit = replace( qty.exit, index.v, qty[ index.v] ),
               cf = replace( cf, index.v, 0 ),
               delta.watts = replace( delta.watts, index.v, 13 ) )

орієнтири

# Unit: milliseconds
#         expr      min       lq     mean   median       uq      max neval
# data.table   1.005018 1.053370 1.137456 1.112871 1.186228 1.690996   100
# wimpel       1.061052 1.079128 1.218183 1.105037 1.137272 7.390613   100
# wimpel.index 1.043881 1.064818 1.131675 1.085304 1.108502 4.192995   100

1

На шкоду пошкодженню звичного синтаксису dplyr, ви можете використовувати withinз основи:

dt %>% within(qty.exit[measure == 'exit'] <- qty[measure == 'exit'],
              delta.watts[measure == 'exit'] <- 13)

Здається, він добре інтегрується з трубою, і ви можете робити в ній майже все, що завгодно.


Це не працює як написано, оскільки друге завдання насправді не відбувається. Але якщо ви це зробите, dt %>% within({ delta.watts[measure == 'exit'] <- 13 ; qty.exit[measure == 'exit'] <- qty[measure == 'exit'] ; cf[measure == 'exit'] <- 0 })це справді спрацює
див. 24
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.