頑健なRプログラムを書くために工夫できること

>100 Views

January 27, 26

スライド概要

profile-image

SAS言語を中心として,解析業務担当者・プログラマなのコミュニティを活性化したいです

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

ダウンロード

関連スライド

各ページのテキスト
1.

2026年1月28日 第12回 大阪SAS勉強会 頑健なRプログラムを書くために工夫できること 大山暁史

2.

はじめに Rプログラムを動かす機会が増えたが、意図通りに処理されているか不安を感じた → 頑健なプログラムを書きたい <頑健なRプログラムの例> ・ 想定外のデータが入ってきた場合にも意図通りの処理または手当てができる ・ 実行環境を変えても意図通りの処理になる ・ バージョンが変わっても意図通りの処理になる ・ 処理が意図通りでない場合には検知できる ・ 可読である(他プログラマーも問題なく流用・メンテナンスできる) Copyright©EPS All rights reserved. 2

3.

本発表について Rの標準パッケージ(version 4.5.2)のコーディングについて、 想定外の挙動を起こさないように理解しておくべきこと、工夫できることを調査・検討した 以下の内容は本発表のスコープ外とする ・ 追加パッケージのコーディング・処理 ・ パッケージ/バージョン管理 ・ バリデーション ・ ログチェック ・ IDE(Rstudio)の設定 Copyright©EPS All rights reserved. 3

4.

プログラム実行前に作業メモリ上のデータを消去する 以下コードを実行することで、作業メモリ上のオブジェクトが消去される 不要なオブジェクトの影響を受けないよう、Rスクリプトの先頭に記載すると良い rm(list=ls()) ※SASの以下の処理のようなもの proc datasets kill nolist; quit; Copyright©EPS All rights reserved. 4

5.

式の途中の改行に注意 改行位置が演算子前後で処理が変わる a1 <- 1 + 2 + 3 +4+5 a2 <- 1 + 2 + 3 + 4+5 1+2+3 と4+5でそれぞれ2つの計算式としてみなされるので、 実行するとa1には6が格納され、コンソールに9が出力される a2に15という結果が格納される 行が演算子で終わっていると式は完結していないとみなされる →基本的に不要な改行はすべきではないが、 可読性の観点などから式の途中で改行する場合は注意 (SASではセミコロンで式が終わったか判断するのでこのようなことは起こらない) Copyright©EPS All rights reserved. 5

6.

演算子の前後には半角スペースを入れる (例)x が -10以下の値である場合には ”xは-10より小さい” と出力したい x <- 15 x if (x < -10){ print("xは-10より小さい") } x if (x <-10){ print("xは-10より小さい") } x ← xに15を代入する ← xには15が代入されており、 xが-10より小さいと判定されない ← 演算子(<と-)の間にスペースがないことで、 代入演算子(<-)と判定され、 x<-10により、xに10が代入されてしまう また、条件は常に真と判定される Rでは演算子の前後には半角スペースを入れないと意図しない処理になる危険がある Copyright©EPS All rights reserved. 6

7.

NaNもNAと判定される NA(欠損値)があると結果をNAで戻すなど、うまく動かない関数がある 事前にNAの有無を確認しておく必要がある NAの判定についてはis.na()関数を用いる x <- c(1, 2, 3, NA) x == NA is.na(x) ただし、is.na()はNaN(非数)もTRUEで判定してしまうことに留意 NaNを判定するにはis.nan()を用いる x2 <- c(1, 2, 3, NA, NaN) is.na(x2) is.nan(x2) なお、sum関数など、実行時にNA(およびNaN)を除外して処理できるna.rmオプションがついている関数もある Copyright©EPS All rights reserved. 7

8.
[beta]
データハンドリング時にNULLを発生させないように留意
NULLは何もないことを示すものであり、解析時に意図しない挙動を起こしうる
以下のように条件分岐で該当がない場合、NULLが戻るので、
該当しない場合の情報もオブジェクトに含む必要がある場合には、
該当しない場合はNA(欠損値)を格納するようにコードに明示する必要がある
df <- data.frame(id = 1:3, aval = c(1, -1, 2))
make_flag <- function(x) {
if (x > 0) {
return("Y") # ifに該当しない場合、手当をしないと NULL が戻る
} # NAを格納するにはelse if (x <= 0){return(NA)} を追記
}
df$flag <- sapply(df$aval, make_flag)

Copyright©EPS All rights reserved.

8

9.

算術処理前に適切な値か確認する log()関数やsqrt()関数に負の数値を指定するとwarningが出る しかし、割合の分母に0を指定した場合にはwarningなしでInfやNaNが格納されるため、 分母が0でないか前処理で確認する必要がある(なお、SASでは0で除算する旨のNoteが出る) event <- 10 subject <- 0 proportion <- event / subject #Infが格納される event <- 0 subject <- 0 proportion <- event / subject #NaNが格納される Copyright©EPS All rights reserved. 9

10.
[beta]
算術処理前に適切な値か確認する
また、平均はNA, Inf, NaNでない数値が1以上、SDは2以上ないとうまく算出できないため、
is.finite関数やlength関数で確認してから算出すると良い
x <- c(10, 12, NA, Inf, NaN)
x2 <- x[is.finite(x)] #有限な数値だけを取り出す
#lengthはオブジェクトの要素数を戻す関数
if (length(x2) >= 2) {
sd(x2)
} else {
NA
}

Copyright©EPS All rights reserved.

10

11.

switch関数の条件分岐でも想定外のデータを検知できるように SASでもif文、else if文を記載して、 それらに当てはまらない想定外のデータをelse文で捕捉することがあるが、 Rのswitch関数で条件分岐を実施する場合には、 閉じ括弧の上の行に該当する条件がない旨を検知できるような処理などを書く switch(a, "1" = x <- 1, "2" = x <- 2, print("該当する条件がありません") ) Copyright©EPS All rights reserved. 11

12.

代入演算子関連 ・ 代入演算子として = は使わない 可読性、検索性の観点からオブジェクトに値を代入する際は <- を使うことが良いと考える (Tidyverse style guideでも代入に <- を用いるよう記載されている) y = mean (x=1:5) #1つ目の=と2つ目の=は意味が違う y <- mean (x=1:5) #好みの問題もあるが、mean関数の結果をオブジェクトyに格納することが分かりやすい ・ 永続代入演算子 <<- は使わない 関数定義内からグローバル (関数定義外)のオブジェクトを上書きするもの ミスの原因になることから、基本的に使用しない方が良い Copyright©EPS All rights reserved. 12

13.
[beta]
小数点以下の表示桁数の設定
SASと同様、算出結果の小数点以下の最後の数字が 0 だった場合には、
0 より手前までの値しか表示されない
帳票に載せる値を算出する際はformat関数やsprintf関数などを用いて
事前に小数点以下表示桁数を指定しておくと良い
y <- round(0.12300, digits = 4)
y1 <- format(y, nsmall = 4) #nsmallに小数点以下の桁数を指定する
y2 <- sprintf("%.4f", y) #%.とfの間に小数点以下の桁数を指定する

なお、例示で用いているround関数はIEEE式であり、
そのままだとSASと異なる結果を戻す場合があることに留意

(SASと同様の処理をしたい場合は右記のような自作関数を用意する)

Copyright©EPS All rights reserved.

sas_round <- function(x, digits = 0) {
p <- 10^digits
(x * p * 2 + 1) %/% 2 / p
}
13

14.

factor型のデータの数値型への変換はひとまず文字型に変換してから factor型のデータを数値型に変えると、 factorの中身ではなくレベル(水準)番号が格納される そもそも数字として処理し得るものはfactor型に設定しないほうが良いが、 factor型に設定されてしまっているデータを数値として扱うには、一度文字型に変換することに留意 score <- factor(c( "10", "20", "30", "20")) as.numeric(score) as.numeric(as.character(score)) mean(as.numeric(score)) mean(as.numeric(as.character(score))) Copyright©EPS All rights reserved. 14

15.

t関数で転置するなら転置後データフレームに戻す データフレームをt関数で転置するとデータフレームはmatrix(行列)になる そのため、転置後はデータフレーム型に戻すためにas.data.frame関数を使う なお、 matrixは単一の型しか保持できないため、以下の優先順で1つの型に統一されることに注意 character(文字型) > complex(複素数型) > double(小数型) > integer(整数型) > logical(論理型) df <- data.frame( USUBJID = c("01", "02"), AVAL = c(10, 20), ADT = as.Date(c("2024-01-01" , "2024-01-02")), FLAG = c(TRUE, FALSE) ) df_t <- t(df) #そのまま転置するとmatrixになる #matrix変換時に全ての値が文字型になっている df2 <- as.data.frame(t(df)) #データフレームに変換する Copyright©EPS All rights reserved. 15

16.

文字値から日付値への変換はフォーマットに注意 文字値をas.Date関数を用いて日付値に変換する際、 文字値がyyyy-mm-dd形式かyyyy/mm/dd形式であればフォーマット指定なしで変換可能 x1 <- as.Date(c("2026-01-01")) x2 <- as.Date(c("2026/01/01")) しかし、Rではデータに複数形式が存在する場合、 指定フォーマットと対応していない値が警告なしにNAを戻すので注意 →変換後にNAが発生していないか確認すると安全 x3 <- as.Date(c("2026-01-01", "2026/01/01")) #フォーマットを指定しなければyyyy-mm-dd形式が変換される x4 <- as.Date(c("2026-01-01", "2026/01/01"), "%Y-%m-%d") x5 <- as.Date(c("2026-01-01", "2026/01/01"), "%Y/%m/%d") if (any(is.na(x3))) stop("変換が適切であるか要確認") Copyright©EPS All rights reserved. 16

17.

文字値から日付値への変換はフォーマットに注意 日本語環境で%Y(4桁の西暦) ,%m(2桁の月), %d(2桁の日)以外のフォーマットを使う場合、 日本語文字に変換されてしまい、戻り値がNAとなってしまう →Sys.setlocale関数でロケール(国・地域の内部設定)をC言語の標準(北米)に設定する as.Date("02JAN2026", "%d%b%Y") Sys.setlocale("LC_TIME" , "C") as.Date("02JAN2026" , "%d%b%Y") Copyright©EPS All rights reserved. 17

18.

予約語に注意 以下などはRの処理系によって先に予約されているため、 オブジェクト名として使用することができない break else FALSE for function if in Inf NA NA_integer_ NA_real_ NA_complex_ NA_character_ NaN next NULL repeat TRUE while Copyright©EPS All rights reserved. 18

19.

予約語に注意 また、pi,T,F はそれぞれ円周率、TRUE、FALSE を 意味する記号として初期設定されているため、 オブジェクト名として使わないように(上書きしないように)留意が必要 TやFはオプションの引数に使われることもあるため、 上書きされてしまうと以下のように意図通りの処理にならない x <- c(1, 2, 3, NA) mean(x, na.rm = T) #na.rmがTRUEならNAを除外して処理する T <- FALSE mean(x, na.rm = T) →TRUEやFALSEについてはTやFのように省略せずに記載したほうが安全 Copyright©EPS All rights reserved. 19

20.

シード値設定漏れに注意 set.seed関数で乱数のシード値を設定できる コード内で同じ結果を再現したい場合には毎回シード値を設定する必要がある set.seed(123) rnorm(5) rnorm(5) #2行目の結果が再現されない set.seed(123) rnorm(5) set.seed(123) rnorm(5) #2行目の結果が再現される Copyright©EPS All rights reserved. 20

21.

source関数によるプログラム呼び出し 各データ・帳票作成プログラム中で共通して用いる自作関数等は、 別途プログラムを用意しておき、source関数で呼び出すと良い(個々で関数を作ると効率悪い&ミスにつながるため) working directory (getwd()を実行すると出力される作業パス) にあるプログラムであれば、パスは記載不要 source("パス/Myfunction.R") echoオプションをTRUEにすると、呼び出したファイルのコードがコンソールに表示される source("パス/Myfunction.R", echo = TRUE) Copyright©EPS All rights reserved. 21

22.

source関数によるプログラム呼び出し 呼び出すプログラム内のオブジェクトとグローバルのオブジェクトが競合する場合には、 以下のように上書きされてしまう x <- 5 source("test.R", echo = TRUE) # test.Rの処理(x <- 3) でxが上書きされる x 新たな環境を作成し、その環境下に呼び出すと、 環境名$オブジェクト名に値が格納されるため、グローバルのオブジェクトは上書きされない x <- 5 temp_env <- new.env(parent = baseenv()) #base環境の生成 source("test.R", local = temp_env, echo = TRUE) x temp_env$x Copyright©EPS All rights reserved. 22

23.

相対パスを活用する 他試験にプログラムを流用したり、CROで作成したプログラムをメーカー環境で実行するなど、 別環境で実行することが良くあるが、絶対パスを記載しているとそのまま実行することができない 別環境での実行を前提とするのであれば、 相対パス (working directoryを./で、その1つ上の階層を../で表す)を用いて記載する方が良い source("../source/Myfunction.R") Copyright©EPS All rights reserved. 23

24.

警告レベルを制御する options(warn=)で警告レベルを指定できる 設定値 動作 負の値 警告を表示しない 0 (デフォルト) 警告を出力するが、 コードは続行 1 警告を即時表示 (遅延表示しない) 2 警告をエラー扱いにする warn=2を設定することにより、warningもerrorとして扱われ、 処理を止めることができる(エラートラップ) →不備に気づきやすくなる Copyright©EPS All rights reserved. 24

25.
[beta]
警告レベルを制御する
x_list <- list(
c("1", "2", "3"),
c("4", "a", "6"), # ← "a" が混入(数値変換できない)
c("7", "8", "9")
)
options(warn = 0)
for (i in 1:3) {
as.numeric(x_list[[i]]) # 警告は最後に表示される
message("done i = ", i)
}
options(warn = 1)
for (i in 1:3) {
as.numeric(x_list[[i]]) # 警告がその場で表示される
message("done i = ", i)
}
options(warn = 2)
for (i in 1:3) {
as.numeric(x_list[[i]]) # エラーになる
message("done i = ", i)
}

Copyright©EPS All rights reserved.

25

26.

オプション設定をデフォルトに戻す Rセッションを切るまではオプション設定が維持されてしまうので、 オプション変更前には情報をオブジェクトに退避し、 必要に応じてデフォルトに戻せるようにしておく initial_option <- options() #デフォルトのオプションを任意のオブジェクトに格納 getOption("warn") options(warn=2) #オプションを変更する getOption("warn") options(initial_option) #デフォルトのオプションに戻す getOption("warn") Copyright©EPS All rights reserved. 26

27.
[beta]
ユーザー定義のWarning/Errorを活用する
warning関数を用いてWarningを、stop関数を用いてエラーを出すことができる

(使用例:データに未来日付が入っている場合、UATを用いた開発時にはWarningを、本番実行時にはエラーが出るように組む)

また、以下のように該当データを出力することも可能

dates <- as.Date(c("2025-08-25", "2025-12-10", "2026-11-22", "2027-03-05"))
# 今日の日付
today <- Sys.Date()
# 未来日付があれば警告
if (any(dates > today)) {
warning("未来日付が含まれています: ", paste(dates[dates > today], collapse = ", "))
}
# 未来日付があればエラー
if (any(dates > today)) {
stop("未来日付が含まれています: ", paste(dates[dates > today], collapse = ", "))
}
Copyright©EPS All rights reserved.

27

28.
[beta]
ユーザー定義のWarning/Errorを活用する
例えば”warning”という単語でログを検索しているなどの都合上、
該当データがない場合にはwarningという文言をコンソールに出力したくない場合は、
message関数を用いて以下のように記載すれば良い
# 未来日付があれば警告(該当データがなければwarningという文言はコンソールに表示されない)
if (any(dates > today)) {
message("w", "arning 未来日付が含まれています:", paste(dates[dates > today], collapse = ", "))
}

また、該当データの出力は難しいが、意図しない処理の検知にはstopifnot関数も有用
x <- 0
stopifnot("x > 3でない場合はエラー" = x > 3)
x <- 5
stopifnot("x > 3でない場合はエラー" = x > 3)
Copyright©EPS All rights reserved.

28

29.
[beta]
tryCatch関数によるエラーハンドリング
Errorが生じた場合、特に手当を行わないと処理が止まってしまうが、
tryCatch関数を用いれば、エラーや警告が出ても処理を止めずに手当(例外処理)を
行うことができる
# 例として、xが0未満ならばエラーとなり、 xが0ならば警告となる関数を用意する
f <- function(x) {
if (x < 0) {
stop("Error! xが0未満")
} else if (x == 0) {
warning("Warning! xが0")
}
}

Copyright©EPS All rights reserved.

29

30.
[beta]
tryCatch関数によるエラーハンドリング
# tryCatchを用いることで、エラーや警告が出ても処理を止めずに手当(例外処理)を行うことができる
g <- function(x) {
tryCatch({
f(x)
return(x)
},
error = function(e) {
cat( e$message, "であるためNAを返します", "¥n") # エラー時にはこちらの処理が動く。e$messageにはエラーメッセージが格納されている
return(NA)
},
warning = function(w) {
cat( w$message, "であるためNAを返します", "¥n") #警告時にはこちらの処理が動く。w$messageには警告メッセージが格納されている
return(NA)
},
finally = {
cat("処理完了", "¥n") #必要に応じて、最後に必ず実行されるfinallyオプションに処理を記載する
}
)
}
g(2)
g(1)
g(0)
g(-1)
Copyright©EPS All rights reserved.

30

31.

制限時間の設定 setTimeLimit関数を用いて、 実行時間が想定以上に長い処理は強制的にエラーにさせることができる setTimeLimit(elapsed = 5) # 制限時間を5秒に設定 Sys.sleep(10) # 確認したい処理を記載する。(便宜上、例示では10秒のスリープ処理を入れている) setTimeLimit() # 制限時間の設定を解除 Copyright©EPS All rights reserved. 31

32.
[beta]
制限時間の設定
実行時間が長い場合を検知したいがエラーとして処理を止めたくない場合には、
tryCatch関数とconditionMessage関数を用れば良い
なお、実行時間が長い場合以外のエラーも検知できるようにすることに留意
setTimeLimit(elapsed = 5)
res <- tryCatch(
Sys.sleep(10),
error = function(e) {
if (grepl( "time limit" , conditionMessage(e))) {
message("処理時間が想定以上に長いので確認してください")
return(NULL)
}
stop(e) # 処理時間以外に関するエラーがある場合にはここで出力する
}
)
setTimeLimit()
Copyright©EPS All rights reserved.

32

33.

途中記載に警告を出力するオプション ①関数オプション名の途中記載 R は関数のオプション名を途中まで書いても一致させてしまう仕様であるが、 オプション名を正確に記載していないと、 バージョンが上がってオプション名が増えた場合などに挙動が変わる恐れがある →options(warnPartialMatchArgs = TRUE) を設定することで、警告を出し得る round(0.12300, digits = 4) round(0.12300, digit = 4) #digitをdigitsとみなして処理される options(warnPartialMatchArgs = TRUE) round(0.12300, digits = 4) round(0.12300, digit = 4) #digitは正確なオプション名ではないため警告 Copyright©EPS All rights reserved. 33

34.

途中記載に警告を出力するオプション ②属性の途中記載 属性名を途中までの一致で取得してしまう →options(warnPartialMatchAttr = TRUE) を設定することで、警告を出し得る x <- 1:3 attr(x, "label") <- "Score" attr(x, "lab") #labをlabelとみなして属性を拾ってしまう options(warnPartialMatchAttr = TRUE) attr(x, "lab") #設定されていない属性でないため警告 Copyright©EPS All rights reserved. 34

35.

途中記載に警告を出力するオプション ③$で指定した列名の途中記載 データフレーム中の$で指定した列名を途中までの一致で取得してしまう →options(warnPartialMatchDollar = TRUE) を設定することで、警告を出し得る df <- data.frame( testvar = 1:3, othervar = 4:6 ) df$testv df$oth options(warnPartialMatchDollar = TRUE) df$testv df$oth Copyright©EPS All rights reserved. 35

36.

途中記載に警告を出力するオプション ただし、以下のように途中までの記載から、列名を一意に推測・特定できない場合、 options(warnPartialMatchDollar = TRUE) の設定に関わらずNULLを戻す adsl <- data.frame( TRT01PN = c(1,2,1), TRT01P = c("A","B","A") ) adsl$TRT options(warnPartialMatchDollar = TRUE) adsl$TRT そのため、データフレームから列を取得する場合には$を使わず、 不正確な列名の場合エラーを戻すことができる、データフレーム名[ ,列名]の書き方をすると良い Copyright©EPS All rights reserved. 36

37.

attach関数は使用しない データフレームの変数を操作する際に、 毎度データフレーム名を指定する必要がないようにattach関数が用意されているが、 以下のように意図通りの処理にならない恐れがあるため使用しない方が良い AVAL <- 10 df <- data.frame(AVAL = 1:3, TRT=1:3) attach(df) TRT #df内のTRTをdf名の指定なしで展開できる(変数名が競合しないので適切に動く) AVAL #df内のAVALではなく、もともと定義していたAVALが優先される df <- data.frame(AVAL = 1:3) attach(df) AVAL <- AVAL + 1 #新しいオブジェクトに上書きされる AVAL df #dfのAVALには反映されていない Copyright©EPS All rights reserved. 37

38.

関数名の重複に注意 追加パッケージに含まれる関数名が、既にロードしていたパッケージの関数名と重複する場合、 追加パッケージのロード時に以下のようなNoteが出て、関数が上書きされる 関数はstats::lagのようにパッケージ名::関数名で呼び出すことが可能なので、 複数パッケージに存在する関数を用いる場合には、 パッケージ名::関数名という形で記載したほうが良い Copyright©EPS All rights reserved. 38

39.

自作関数は既存関数名と別名で定義する 自作関数名がRで既に用意されている関数名と同名であっても、 警告が表示されないことが多いので注意 log(1) log <- function(x){ return(x) } log(1) #関数が上書きされた rm(log) #関数を上書きしてしまった場合はオブジェクトを消去 log(1) Copyright©EPS All rights reserved. 39

40.

関数名の重複確認・所在確認 ロード済みの関数のうち、重複した関数はconflicts()で調べることができる (インストールしているがロードしていないパッケージは判定されない) また、各関数の所在はfind(“関数名”)で調べることができる Copyright©EPS All rights reserved. 40

41.

まとめ 標準パッケージを頑健なコーディングについて、検討した内容を紹介した 今後はpharmaverseなどの追加パッケージについても検討したい 皆様が工夫されていることがございましたら、是非コメントいただけますと幸いです Copyright©EPS All rights reserved. 41

42.

参考文献 [1] The R Project https://www.r-project.org [2] Rdocumentation https://www.rdocumentation.org [3] 舟尾 暢男 (2016) The R Tips 第3版 ーデータ解析環境Rの基本技・グラフィックス活用集ー オーム社 [4] Name all but the most important arguments Tidy design principles https://design.tidyverse.org/call-data-details.html [5] Rのスクリプト作成時の注意点とエラーへの対処 名古屋大学 大学院教育発達科学研究科・教育学部 https://www.educa.nagoya-u.ac.jp/~ishii-h/materials/R_errors.pdf [6] Tidyverse style guide https://style.tidyverse.org/ Copyright©EPS All rights reserved. 42