dplyrは、行のサブセットのいくつかの列を変更/置換します


86

私は(私が慣れているほとんどのdata.tableを使用するのではなく)dplyrベースのワークフローを試している最中ですが、同等の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)条件を1回指定するだけで済みます。

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)条件を除外する後で削除される余分な列にすることで、条件を除外します。その後、使用ifelsereplace示されるように論理名または算術。これはデータテーブルでも機能します。

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)sqldfupdateデータフレームのパイプラインでsqldfパッケージを介してSQLを使用できます(ただし、変換しない限りデータテーブルは使用できません。これは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の問題が議論されている1346311518年1573年を631は、メインスレッドであることと、1573年はここに答えの見直しいます。


1
素晴らしい答え、ありがとう!mutate_condと@KevinUsheyのmutate_whenは、どちらもこの問題の優れた解決策です。mutate_whenの可読性/柔軟性には少し好みがあると思いますが、この回答を徹底的に「チェック」します。
クリスニュートン

私はmutate_condアプローチが本当に好きです。この関数またはそれに非常に近いものはdplyrに含める価値があり、人々が考えているユースケースにはVectorizedSwitch(github.com/hadley/dplyr/issues/1573で説明されています)よりも優れたソリューションであるとも思えますここについて
Magnus

mutate_condが大好きです。さまざまなオプションは別々の答えでなければなりませんでした。
Holger Brandl 2018年

数年経ちましたが、githubの問題は解決されてロックされているようです。この問題に対する公式の解決策はありますか?
static_rtti 2018

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
Ursus Frost

@UrsusFrostがデータセットのサブセットにすぎない新しい列を追加するのは、私には奇妙に思えます。サブセット化されていない行にNAを追加しますか?
Baraliuh

@Baraliuhはい、ありがたいです。これは、日付のリストにデータをインクリメントして追加するループの一部です。最初のいくつかの日付は、実際のビジネスプロセスを複製しているため、後続の日付とは異なる方法で処理する必要があります。それ以降の反復では、日付の条件に応じて、データの計算方法が異なります。条件付きのため、の前の日付を誤って変更したくありませんdata.frame。FWIW、式がこれを簡単に処理できるため、data.table代わりに使用に戻りました。さらに、ループ全体がはるかに高速に実行されます。dplyri
UrsusFrost18年

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が上に示したように、DTは参照渡しのセマンティクスを使用するのに対し、dplyrは値渡しを使用するため、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がです。Grothendieckですが、新しい変数も処理するのに役立つかもしれないと考えました。したがって、以下に2つの追加があります。

無関係:最後から2番目の行を使用してもう少しdplyr作成filter()

最初の3つの新しい行は、で使用する変数名を取得しmutate()mutate()発生する前にデータフレーム内の新しい変数を初期化します。新しい変数は、デフォルトでmissing()に設定されているdata.frameusingの残りの部分で初期化されます。new_initNA

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)

上記と同じですが、新しい変数も作成しますxNA条件に含まれていない行に)。以前は不可能でした。

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異なる値で複数の新しい変数を初期化する方法を示しています。ここでは、2つの新しい変数は、(異なる値を使用して初期化されている除外行を使用して作成されているxように初期化FALSEyなどNA

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

あなたのmutate_cond関数は私のデータセットでエラーをスローしますが、Grothendiecksの関数はエラーをスローしません。Error: incorrect length (4700), expecting: 168フィルタ機能に関連しているようです。
RHA 2016年

これをライブラリに入れたり、関数として形式化したりしましたか?特にすべての改善により、これは簡単なことのように思えます。
イラクサ2018年

1
いいえ。現時点でのdplyrの最善のアプローチは、mutateとif_elseまたはを組み合わせることだと思いますcase_when
サイモンジャクソン

このアプローチの例(またはリンク)を提供できますか?
イラクサ2018年

6

mutate_condは優れた関数ですが、条件の作成に使用された列にNAがある場合はエラーになります。条件付きミューテーションは、そのような行をそのままにしておくべきだと思います。これは、条件がTRUEの場合に行を返すfilter()の動作と一致しますが、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はクラッシュしましたが、ソリューションは魅力のように機能しました。
Phil vanKleur20年

これがあなたにとって有用であるならば、この関数は私が書いた小さなパッケージ「zulutils」で利用可能です。これは、CRANにはありませんが、あなたはリモコンを使用してインストールすることができます:: install_github(「torfason / zulutils」)
マグナス

4

dplyrこれがはるかに簡単になるような変更は実際には見られません。case_when1つの列に複数の異なる条件と結果がある場合に最適ですが、1つの条件に基づいて複数の列を変更する場合には役立ちません。同様に、recode1つの列で複数の異なる値を置き換える場合は入力を節約できますが、一度に複数の列で置き換えることはできません。最終的に、mutate_atなどは、データフレーム内の行ではなく、列名にのみ条件を適用します。mutate_atの関数を作成してそれを実行できる可能性がありますが、列ごとに異なる動作をする方法がわかりません。

つまり、nestフォームtidyrmapfromを使用してアプローチする方法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 2018

@DaveGruenewaldの提案を反映するように編集
2018

4

簡潔な解決策の1つは、フィルター処理されたサブセットでミューテーションを実行してから、テーブルの非終了行を追加し直すことです。

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.tabledefault'-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

1

通常のdplyr構文を破ることを犠牲にして、withinベースから使用できます。

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

パイプとうまく統合されているようで、パイプ内でやりたいことはほとんど何でもできます。


2番目の割り当ては実際には発生しないため、これは記述どおりに機能しません。あなたがしなければしかしdt %>% within({ delta.watts[measure == 'exit'] <- 13 ; qty.exit[measure == 'exit'] <- qty[measure == 'exit'] ; cf[measure == 'exit'] <- 0 })、それは動作しません
see24
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.