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.


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

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)

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

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
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.