web-dev-qa-db-ja.com

ifelse()がDateオブジェクトを数値オブジェクトに変換しないようにする方法

関数ifelse()を使用して、日付ベクトルを操作しています。結果はクラスDateであると予想しましたが、代わりにnumericベクトルを取得することに驚きました。以下に例を示します。

dates <- as.Date(c('2011-01-01', '2011-01-02', '2011-01-03', '2011-01-04', '2011-01-05'))
dates <- ifelse(dates == '2011-01-01', dates - 1, dates)
str(dates)

ベクター全体で操作を実行するとDateオブジェクトが返されるため、これは特に驚くべきことです。

dates <- as.Date(c('2011-01-01', '2011-01-02', '2011-01-03', '2011-01-04','2011-01-05'))
dates <- dates - 1
str(dates)

Dateベクトルを操作するために他の関数を使用する必要がありますか?もしそうなら、どのような機能ですか?そうでない場合、どのようにしてifelseが入力と同じ型のベクトルを返すように強制するのですか?

ifelseのヘルプページは、これがバグではなく機能であることを示していますが、驚くべき動作であることがわかったものの説明を見つけるのに苦労しています。

131
Zach

_data.table::fifelse_(_data.table >= 1.12.3_)または_dplyr::if_else_を使用できます。


_data.table::fifelse_

ifelseとは異なり、fifelseは入力のタイプとクラスを保持します。

_library(data.table)
dates <- fifelse(dates == '2011-01-01', dates - 1, dates)
str(dates)
# Date[1:5], format: "2010-12-31" "2011-01-02" "2011-01-03" "2011-01-04" "2011-01-05"
_

ベンチマークを含むfifelseの詳細については、 開発バージョン1.12.3のニュース項目#21 を参照してください。開発バージョンのインストールについては、 here を参照してください。


_dplyr::if_else_

_dplyr 0.5.0_リリースノート : "[_if_else_]は、ifelse()trueおよびfalse引数は同じ型でなければなりません。これはそれほど驚くべき戻り値型を与えず、dates "のようなS3ベクトルを保持します。

_library(dplyr)
dates <- if_else(dates == '2011-01-01', dates - 1, dates)
str(dates)
# Date[1:5], format: "2010-12-31" "2011-01-02" "2011-01-03" "2011-01-04" "2011-01-05" 
_
99
Henrik

文書化されたValue of ifelseに関連します:

classおよびtestまたはyesの値からのデータ値と同じ長さと属性(次元および「no」を含む)のベクトル。最初にyesから取得した値、次にnoから取得した値に対応するために、回答のモードは論理から強制されます。

その意味に要約すると、ifelseは要因がレベルを失い、日付がクラスを失い、モード(「数値」)のみが復元されます。代わりにこれを試してください:

dates[dates == '2011-01-01'] <- dates[dates == '2011-01-01'] - 1
str(dates)
# Date[1:5], format: "2010-12-31" "2011-01-02" "2011-01-03" "2011-01-04" "2011-01-05"

safe.ifelseを作成できます:

safe.ifelse <- function(cond, yes, no){ class.y <- class(yes)
                                  X <- ifelse(cond, yes, no)
                                  class(X) <- class.y; return(X)}

safe.ifelse(dates == '2011-01-01', dates - 1, dates)
# [1] "2010-12-31" "2011-01-02" "2011-01-03" "2011-01-04" "2011-01-05"

後のメモ:Hadleyがif_elseをデータシェーピングパッケージのmagrittr/dplyr/tidyr複合体に組み込んだことがわかります。

62
42-

DWinの説明はすぐにわかります。 ifelseステートメントの後に単にクラスを強制することができることに気付く前に、私はしばらくこれをいじり、これと戦いました。

dates <- as.Date(c('2011-01-01','2011-01-02','2011-01-03','2011-01-04','2011-01-05'))
dates <- ifelse(dates=='2011-01-01',dates-1,dates)
str(dates)
class(dates)<- "Date"
str(dates)

最初は、これは私に少し「ハック」を感じました。しかし、今では、ifelse()から得られるパフォーマンスリターンを支払うための小さな価格だと考えています。さらに、ループよりもはるかに簡潔です。

15
JD Long

推奨される方法は、因子列では機能しません。この改善を提案したいと思います:

safe.ifelse <- function(cond, yes, no) {
  class.y <- class(yes)
  if (class.y == "factor") {
    levels.y = levels(yes)
  }
  X <- ifelse(cond,yes,no)
  if (class.y == "factor") {
    X = as.factor(X)
    levels(X) = levels.y
  } else {
    class(X) <- class.y
  }
  return(X)
}

ちなみに、ifelseには問題があります...大きな力には大きな責任が伴います。つまり、1x1行列や数値の型変換(たとえば、追加する必要がある場合)は問題ありませんが、ifelseでのこの型変換は明らかに不要です。私は今まで何度もifelseの同じ「バグ」にぶつかりましたが、時間を盗み続けています:-(

FW

6
Fabian Werner

これが機能しない理由は、ifelse()関数が値を係数に変換するためです。良い回避策は、評価する前に文字に変換することです。

dates <- as.Date(c('2011-01-01','2011-01-02','2011-01-03','2011-01-04','2011-01-05'))
dates_new <- dates - 1
dates <- as.Date(ifelse(dates =='2011-01-01',as.character(dates_new),as.character(dates)))

これには、ベースR以外のライブラリは必要ありません。

@ fabian-wernerによって提供される答えは素晴らしいですが、オブジェクトは複数のクラスを持つことができ、「factor」は必ずしもclass(yes)によって返される最初のものではない可能性があるため、すべてのクラス属性をチェックするためにこの小さな変更をお勧めします:

safe.ifelse <- function(cond, yes, no) {
      class.y <- class(yes)
      if ("factor" %in% class.y) {  # Note the small condition change here
        levels.y = levels(yes)
      }
      X <- ifelse(cond,yes,no)
      if ("factor" %in% class.y) {  # Note the small condition change here
        X = as.factor(X)
        levels(X) = levels.y
      } else {
        class(X) <- class.y
      }
      return(X)
    }

また、R開発チームに、保存する属性のユーザー選択に基づいて属性を保存するbase :: ifelse()を持つ文書化されたオプションを追加するリクエストを送信しました。リクエストはここにあります: https://bugs.r-project.org/bugzilla/show_bug.cgi?id=16609 -常に持っているという理由で既に「WONTFIX」としてフラグが付けられています今はそうでしたが、単純な追加でRユーザーの多くの頭痛の種を救うことができる理由についての補足説明を提供しました。おそらく、そのバグスレッドの「+1」は、R Coreチームにもう一度見てもらうことを奨励するでしょう。

編集:これは、「cond」(デフォルトのifelse()の振る舞い)、「yes」、上記のコードによる動作、または「no」のいずれかを保持する属性をユーザーが指定できるようにするより良いバージョンです。 「no」値の属性の方が優れています:

safe_ifelse <- function(cond, yes, no, preserved_attributes = "yes") {
    # Capture the user's choice for which attributes to preserve in return value
    preserved           <- switch(EXPR = preserved_attributes, "cond" = cond,
                                                               "yes"  = yes,
                                                               "no"   = no);
    # Preserve the desired values and check if object is a factor
    preserved_class     <- class(preserved);
    preserved_levels    <- levels(preserved);
    preserved_is_factor <- "factor" %in% preserved_class;

    # We have to use base::ifelse() for its vectorized properties
    # If we do our own if() {} else {}, then it will only work on first variable in a list
    return_obj <- ifelse(cond, yes, no);

    # If the object whose attributes we want to retain is a factor
    # Typecast the return object as.factor()
    # Set its levels()
    # Then check to see if it's also one or more classes in addition to "factor"
    # If so, set the classes, which will preserve "factor" too
    if (preserved_is_factor) {
        return_obj          <- as.factor(return_obj);
        levels(return_obj)  <- preserved_levels;
        if (length(preserved_class) > 1) {
          class(return_obj) <- preserved_class;
        }
    }
    # In all cases we want to preserve the class of the chosen object, so set it here
    else {
        class(return_obj)   <- preserved_class;
    }
    return(return_obj);

} # End safe_ifelse function
5
Mekki MacAulay