[英]dplyr mutate/replace several columns on a subset of rows
我正在嘗試基於 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,因為我不想多次輸入條件 - 這是一個簡化的示例,但有時會有很多基於單個條件的分配。
在此先感謝您的幫助!
這些解決方案 (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我們可以通過管道中的 sqldf 包對數據幀使用 SQL update
(但不能使用數據表,除非我們轉換它們——這可能代表 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
定義返回一個tibble:如何與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是對此處答案的回顧。
您可以使用magrittr
的雙向管道%<>%
:
library(dplyr)
library(magrittr)
dt[dt$measure=="exit",] %<>% mutate(qty.exit = qty,
cf = 0,
delta.watts = 13)
這減少了輸入量,但仍然比data.table
慢得多。
這是我喜歡的解決方案:
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)
)
這是非常易讀的——盡管它可能沒有它應有的性能。
正如上面的 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
我只是偶然發現了這個,真的很喜歡@G 的mutate_cond()
。 Grothendieck,但認為處理新變量可能會派上用場。 所以,下面有兩個補充:
無關:倒數第二行通過使用filter()
使dplyr
多dplyr
開頭的三個新行獲取用於mutate()
變量名稱,並在mutate()
發生之前初始化數據框中的任何新變量。 使用new_init
為data.frame
的其余部分初始化新變量,默認設置為缺失 ( 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
設置為list
以初始化具有不同值的多個新變量。 在這里,創建了兩個新變量,其中排除的行使用不同的值進行初始化( x
初始化為FALSE
, y
為NA
)
iris %>% mutate_cond(Species == "setosa" & Sepal.Length < 5,
x = TRUE, y = Sepal.Length ^ 2,
new_init = list(FALSE, NA))
一種簡潔的解決方案是對過濾后的子集進行變異,然后添加回表的非退出行:
library(dplyr)
dt %>%
filter(measure == 'exit') %>%
mutate(qty.exit = qty, cf = 0, delta.watts = 13) %>%
rbind(dt %>% filter(measure != 'exit'))
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
}
我實際上沒有看到dplyr
任何更改會使這變得更容易。 case_when
非常適合當一列有多個不同的條件和結果時,但對於您想根據一個條件更改多個列的情況沒有幫助。 同樣,如果您要替換一列中的多個不同值, recode
可以節省輸入,但一次在多列中這樣做無濟於事。 最后, mutate_at
等只將條件應用於列名而不是數據mutate_at
的行。 您可能會為 mutate_at 編寫一個函數來執行此操作,但我無法弄清楚您將如何使其對不同列的行為有所不同。
這就是我將如何使用nest
形式tidyr
和來自purrr
map
來處理它的方法。
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()
與創建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)
您可以拆分數據集並對TRUE
部分進行常規 mutate 調用。
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
使用tibble::rowid_to_column
,然后在rowid
上使用dplyr::arrange
並最后將其選中。
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)
我認為這個答案以前沒有提到過。 它的運行速度幾乎與“默認” data.table
-solution 一樣快。
使用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
在與通常的dplyr語法突破的費用,你可以使用within
從基地:
dt %>% within(qty.exit[measure == 'exit'] <- qty[measure == 'exit'],
delta.watts[measure == 'exit'] <- 13)
它似乎與管道整合得很好,你可以在里面做任何你想做的事情。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.