dplyr изменить / заменить несколько столбцов в подмножестве строк

86

Я пытаюсь опробовать рабочий процесс на основе 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, потому что не хочу, чтобы условие вводилось несколько раз - это упрощенный пример, но иногда бывает много назначений, основанных на одном условии.

Заранее спасибо за помощь!

Крис Ньютон
источник

Ответы:

83

Эти решения (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, поэтому if 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 issue 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: проблема того, как легко указать обновление подмножества строк, также обсуждается в вопросах 134 , 631 , 1518 и 1573 dplyr , где 631 является основным потоком, а 1573 является обзором ответов здесь.

Г. Гротендик
источник
1
Отличный ответ, спасибо! Ваши mutate_cond и mutate_when @Kevin Ushey являются хорошими решениями этой проблемы. Я думаю, что я немного предпочитаю удобочитаемость / гибкость 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.

eipi10
источник
На самом деле, теперь, когда у меня была возможность проверить это, я бы предпочел решение, которое избегает необходимости подмножества с использованием нотации dt [dt $ measure == 'exit',], поскольку это может стать громоздким с более длинными dt имена.
Крис Ньютон
Просто к сведению, но это решение будет работать, только если data.frame/ tibbleуже содержит столбец, определенный с помощью mutate. Это не сработает, если вы пытаетесь добавить новый столбец, например, при первом запуске цикла и изменении файла data.frame.
Урсус Фрост
Мне кажется странным, что @UrsusFrost добавляет новый столбец, который является лишь подмножеством набора данных. Вы добавляете NA к строкам, которые не входят в подмножество?
Baraliuh
@Baraliuh Да, я могу это оценить. Это часть цикла, в котором я увеличиваю и добавляю данные в список дат. Первые несколько дат должны обрабатываться иначе, чем последующие даты, поскольку они повторяют реальные бизнес-процессы. В дальнейших итерациях, в зависимости от условий дат, данные рассчитываются по-разному. Из-за условности я не хочу непреднамеренно изменять предыдущие даты в data.frame. FWIW, я просто вернулся к использованию, data.tableа не dplyrпотому, что его iвыражение легко справляется с этим - плюс общий цикл выполняется намного быстрее.
Ursus Frost
19

Вот решение, которое мне нравится:

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 будет значительно быстрее.

В качестве альтернативы вы можете сначала подмножество, затем обновить и, наконец, рекомбинировать:

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умолчанию установлено значение missing ( ).

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
Вы поместили это в библиотеку или формализовали как функцию? Вроде бы и ежу понятно, особенно со всеми улучшениями.
Nettle
1
Нет. Я думаю, что в настоящее время лучший подход к dplyr - это комбинировать mutate с if_elseили case_when.
Саймон Джексон
Можете ли вы привести пример (или ссылку) на этот подход?
Nettle
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")
Магнус,
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()
см24
источник
1
Единственное , что я хотел бы предложить, чтобы использовать , nest(-measure)чтобы избежатьgroup_by
Dave Gruenewald
Отредактировано, чтобы отразить предложение @DaveGruenewald
24
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)
Moody_Mudskipper
источник
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введены в столбцы 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
Wimpel
источник
1

За счет отказа от обычного синтаксиса dplyr вы можете использовать withinfrom base:

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