Rのこと。

記事は引っ越し作業中。2023年中までに引っ越しを完了させてブログは削除予定

Advanced R: Conditions

はじめに

Advanced R 2nd EditionのPaperBook版が届いたので、Rについてのおさらい備忘録。また、書籍を読んでいるにも関わらず誤った解釈や用語の誤用もあるかもしれませんので、参考にする際は自己責任でお願いします。

この記事のライセンスはAdvanced R 2nd Editionと同様で下記の通りです。

Conditions

Conditionsでは、状態を処理するための関数、条件オブジェクト、条件ハンドラについて説明されている。

シグナリング条件

コードで通知できる条件は3つ。エラー、警告、およびメッセージ。

  • エラー:最も深刻。実行を停止する必要があることを示す。
  • 警告:通常は問題が発生したものの、関数が少なくとも部分的に回復したことを示す。
  • メッセージ:何らかのアクションが実行されたことをユーザーに知らせる。
stop("This is what an error looks like")
Error in eval(expr, envir, enclos): This is what an error looks like

warning("This is what a warning looks like")
Warning: This is what a warning looks like

message("This is what a message looks like")
This is what a message looks like

エラー

stop()によってエラーは通知される。呼び出しの関数を非表示にする場合call. = FALSEオプションをつける。

f <- function() g()
g <- function() h()
h <- function() stop("This is an error!")

f()
Error in h(): This is an error!

rlang::abort()stop()は同等。

警告

warning()によって通知される。何かがうまくいっていないことを知らせるが、コードは実行される。エラーとは異なり、単一の関数呼び出しから複数の警告を受け取ることが可能。関数を非推奨にするときなどに使うことが推奨される。

fw <- function() {
  cat("1\n")
  warning("W1")
  cat("2\n")
  warning("W2")
  cat("3\n")
  warning("W3")
}

fw()
1
2
3
 警告メッセージ: 
1:  fw() で:  W1
2:  fw() で:  W2
3:  fw() で:  W3

メッセージ

message()によって通知される。何らかのアクションをユーザーに伝える。

fm <- function() {
  cat("1\n")
  message("M1")
  cat("2\n")
  message("M2")
  cat("3\n")
  message("M3")
}

fm()
1
M1
2
M2
3
M3

状態を無視する方法

Rで状態を扱う最も簡単な方法は、それらを無視すること。

  • try():エラーを無視して実行する。
  • suppressWarnings():警告を無視する。
  • suppressMessages():メッセージを無視する。
f2 <- function(x) {
    try(log(x)) # try
    10
}

f2("a")
Error in log(x) :  数学関数に数値でない引数が渡されました 
[1] 10

suppressWarnings()suppressMessages()はすべての警告とメッセージを無視して実行する。エラーとは異なり、メッセージと警告は実行を終了させないため、1つのブロックに複数の警告とメッセージが表示される。

suppressWarnings({
  warning("Uhoh!")
  warning("Another warning")
  1
})
[1] 1

suppressMessages({
  message("Hello there")
  2
})
[1] 2

suppressWarnings({
  message("You can still see me")
  3
})
You can still see me
[1] 3

try()のサンプルコードを載せておく。

f <- function(x){
  try({
    if(missing(x)){
      print("'x' is missing")
    }
      res = x * 10
      return(res)
      })
}

f()
[1] "'x' is missing"

f(1)
[1] 10

条件ハンドラ

すべての条件にはデフォルトの動作がある。エラーは実行を停止する、警告は表示されても処理が実行され、メッセージはすぐに表示される。条件ハンドラを使用すると、デフォルトの動作を一時的に上書きまたは補足することが可能。

tryCatch()withCallingHandlers()は受け取った条件を単一の引数として取る関数を条件ハンドラに登録することを可能にする。

tryCatch(
  error = function(cnd) {
    # code to run when error is thrown
  },
  code_to_run_while_handlers_are_active
)

withCallingHandlers(
  warning = function(cnd) {
    # code to run when warning is signalled
  },
  message = function(cnd) {
    # code to run when message is signalled
  },
  code_to_run_while_handlers_are_active
)

tryCatch()は、終了ハンドラを定義する。条件が処理された後、tryCatch()は呼び出されたコンテキストに戻る。tryCatch()は終了しなければならないので、エラーと割り込みを扱うのに適している。

withCallingHandlers()は、呼び出しハンドラを定義する。条件が取り込まれた後、条件が通知されたコンテキストに戻る。エラーのない状況での作業に適したものになる。

tryCatch()に関するサンプルのコードを載せておく。つまり、tryCatch()は、エラー処理の部分と必ず実行する部分の処理を分けて書くことができる。

f <- function(x){
  tryCatch(
  if(missing(x)){
    print("'x' is missing")
    x = 0
    },
  finally = {
    res = x * 10
    return(res)
    }
  )
}

f()
[1] "'x' is missing"
[1] 0

f(1)
[1] 10

状態オブジェクト

rlang::catch_cnd()で状態オブジェクトがどのようなものか見れる。

cnd <- catch_cnd(stop("An error"))
str(cnd)
List of 2
 $ message: chr "An error"
 $ call   : language force(expr)
 - attr(*, "class")= chr [1:3] "simpleError" "error" "condition"

終了ハンドラ

tryCatch()でハンドラを終了することを登録し、通常はエラー状態を処理するために使用する。これにより、デフォルトのエラー動作を上書きすることを可能になる。tryCatch()によって登録されたハンドラは、終了ハンドラと呼ばれる。

f3 <- function(x) {
  tryCatch(
    error = function(cnd) NA,
    log(x)
  )
}

f3("x")
[1] NA

条件がない場合、コードは通常通り実行される。

ryCatch(
  error = function(cnd) 10,
  1 + 1
)
[1] 2

tryCatch(
  error = function(cnd) 10,
  {
    message("Hi!")
    1 + 1
  }
)
Hi!
[1] 2

呼び出しハンドラ

withCallingHandlers()によって登録されたハンドラは、呼び出しハンドラと呼ばれる。最初のケースでは、終了するハンドラが完了するとコードが終了するため、メッセージは出力されない。下のケースでは、呼び出しハンドラが終了しないので、メッセージが出力されます。

tryCatch(
  message = function(cnd) cat("Caught a message!\n"), 
  {
    message("Someone there?")
    message("Why, yes!")
  }
)
Caught a message!

withCallingHandlers(
  message = function(cnd) cat("Caught a message!\n"), 
  {
    message("Someone there?")
    message("Why, yes!")
  }
)
Caught a message!
Someone there?
Caught a message!
Why, yes!

状態ハンドリングのモチベーション

base::log()を使って、状態ハンドリングを行うべきモチベーションを理解する。下記は、通常のbase::log()のエラーである。

log(letters)
Error in log(letters): non-numeric argument to mathematical function

log(1:10, base = letters)
Error in log(1:10, base = letters): non-numeric argument to mathematical
function

これをより明確にすることで、ユーザーが素早く修正できる。エラーメッセージが正しい修正をユーザー案内するので、インタラクティブな改善といえる。

my_log <- function(x, base = exp(1)) {
  if (!is.numeric(x)) {
    abort(paste0(
      "`x` must be a numeric vector; not ", typeof(x), "."
    ))
  }
  if (!is.numeric(base)) {
    abort(paste0(
      "`base` must be a numeric vector; not ", typeof(base), "."
    ))
  }

  base::log(x, base = base)
}

my_log(letters)
Error: `x` must be a numeric vector; not character.

my_log(1:10, base = letters)
Error: `base` must be a numeric vector; not character.

glue::glue()を使うと、更に改善が可能。

abort_bad_argument <- function(arg, must, not = NULL) {
  msg <- glue::glue("`{arg}` must {must}")
  if (!is.null(not)) {
    not <- typeof(not)
    msg <- glue::glue("{msg}; not {not}.")
  }
  
  abort("error_bad_argument", 
    message = msg, 
    arg = arg, 
    must = must, 
    not = not
  )
}

my_log <- function(x, base = exp(1)) {
  if (!is.numeric(x)) {
    abort_bad_argument("x", must = "be numeric", not = x)
  }
  if (!is.numeric(base)) {
    abort_bad_argument("base", must = "be numeric", not = base)
  }

  base::log(x, base = base)
}

my_log()自体はそれほど短くはないが、引数が間違っていてもエラーメッセージが関数間で一貫していることを保証できる。

my_log(letters)
Error: `x` must be numeric; not character.

my_log(1:10, base = letters)
Error: `base` must be numeric; not character.

ifについて

ここでは紹介されていなかったが、ifも状態のハンドリングとして使える。下記はサンプルコード。

x <- rpois(10, 10)

GetCI <- function(x, level = 0.95){
  if(missing(x)){
    stop("'x' is missing")
  }
  
  if(level <= 0 || level >= 1){
    stop("The 'level' argument must be >0 and <1" )
  }
    
  if(level < 0.5){
    warning("Confidence levels are often close to 1, e.g. 0.95")
  }
  m <- mean(x)
  n <- length(x)
  SE <- sd(x)/sqrt(n)
  upper <- 1 - (1-level)/2
  ci <- m + c(-1, 1)*qt(upper, n-1)*SE
  return(list(mean = m, 
              se = SE, 
              ci = ci))
}

GetCI()
GetCI() でエラー: 'x' is missing

GetCI(x, level = 0.05)
$mean
[1] 9.9

$se
[1] 1.277585

$ci
[1] 9.817625 9.982375

 警告メッセージ: 
 GetCI(x, level = 0.05) で: 
  Confidence levels are often close to 1, e.g. 0.95