//青木繁伸 うまいプログラミングと確実なプログラミングは相反する部分があるそのあたりを書いてみるか。~ 実際に,色々なライブラリに書かれているプログラムから例題をとって,ちょっとずつ書いてみる。~ ちなみに,私自身は,COLOR(RED){トリッキーなプログラムや,ちょっとでも簡潔なプログラムというのは,大好き}なんである。 ---- #contents ---- *if - else if - else など concord ライブラリの coincidence.matrix 関数より if (any(is.na(x))) mc <- apply(x, 2, vn) - 1 else mc <- rep(1, dimx[2]) 何のコメントも必要ないものであるが, if (any(is.na(x))) { mc <- apply(x, 2, vn) - 1 } else { mc <- rep(1, dimx[2]) } と書くことを勧める(このプログラムをコンソールから入力しているなら問題が生じるが,そもそも,そんなプログラムをコンソールから直に入力すること自体考えられない)。~ if や else に対応する処理が一つの文で済んでいるうちはいいが,そのうち機能を追加するときにうっかり if (any(is.na(x))) mc <- apply(x, 2, vn) - 1 追加処理 else mc <- rep(1, dimx[2]) などと付け加えてしまうと,変なことになってしまう。~ 前もって { } を補っておけば,いくら付け加えても OK。転ばぬ先の杖。 ~ なお,R の特性を生かして書くなら, mc <- if (any(is.na(x))) apply(x, 2, vn) - 1 else rep(1, dimx[2]) とも書ける。しかし,こんな書き方をしても,何のメリットがあろうか。 stats の PP.test には, if (lshort) l <- trunc(4 * (n/100)^0.25) else l <- trunc(12 * (n/100)^0.25) という部分があるが, l <- 4 * (n/100)^0.25) if (lshort == FALSE) { l <- 3 * l } l <- trunc(l) の方が,なんぼかわかりやすい。 l <- trunc(ifelse(lshort, 4, 12) * (n/100)^0.25) とするかどうかは,議論の分かれるところかもしれない。 *R 特有のリサイクルという機能 同じく concord ライブラリの coincidence.matrix 関数より cm <- matrix(rep(0, nval * nval), nrow = nval) # (1) これが, cm <- matrix(0, nrow = nval, ncol = nval) # (2) や cm <- matrix(rep(0, nval * nval), nrow = nval, ncol = nval) # (3) と同じであることは,R の入門書の最初の方に書いてある。~ R 的には (2) なのだろうが(cm <- matrix(0, nval, nval) なら,さらに短いが),他の言語的には (3) がわかりやすいのかもしれない。~ しかし,こんなことを何百万回もやるのでなければ,どれでも同じである。 一方,リサイクルはうまく使った方がわかりやすいこともある。~ stats の cancor には,平均偏差データを作る部分に以下のような記述がある。 xcenter <- colMeans(x) # 列方向の平均値 x <- x - rep(xcenter, rep.int(nr, ncx)) # nr, ncx は x の行数と列数 二行目は, x <- scale(x, scale=FALSE) と書くことができるが,平均値を求め直すのを嫌ったのかもしれない(xcenter を後で使わないなら,scale 関数を使うべし)。 ~ リサイクル機能を使うなら, x <- t(t(x)-xcenter) でよいが,やはり,これは技巧を凝らしすぎていることになるかもしれない。~ しかし, x <- x - rep(xcenter, rep.int(nr, ncx)) のように rep 関数を二回も使う必要はなくて, x <- x - rep(xcenter, each=nr) とすることで,同じ結果になることも覚えておいて損はない(要は,関数をうまく使うことだ)。 *条件のチェック R では stopifnot という関数が用意されている。条件を満たさなければストップというのである。~ base の prop.test でも, if (any(n <= 0)) stop("Elements of n must be positive") というのが最初の方にあるが,stopifnot を使うとすれば stopifnot (all(n > 0)) ということで,もし条件が満たされないと, Error: all(n > 0) is not TRUE というエラーメッセージが返される。プログラムとしてはもう少し詳しいエラー状況の説明と,対処法も含めて報告する方が良いので,この stopifnot といのは使いでがない。~ また,stopif ではなくて stopifnot と,わざざざ not を付けたのはどういう意図によるのだろうか。~ stopif (any(n) <= 0) の方がわかりやすくはないのだろうか。~ 「条件を満たさなければ停止」というのと,「この条件ならばーー>停止」というのは,論理的には同じだが(あたりまえ),人間的には(日本語的には??)後者の方がわかりやすい? *論理式の真偽 その stopifnot の中に, for (i in 1:n) if (!(is.logical(r <- eval(ll[[i]])) && all(r))) stop(paste(deparse(mc[[i + 1]]), "is not TRUE"), call. = FALSE) というのがある。~ R に限らず,多くのプログラミング言語では,0のみが偽,0以外は真であり,偽は FALSE,真は TRUE と言うのも普通ではある。 if (!条件) の ! は,論理値の否定であるので,if (条件 != TRUE) および if (条件 == FALSE) と同義である。C 言語などでは ! 条件 が使われることも多い(if (条件) というのと同じく)が,if (条件 != TRUE) および if (条件 == FALSE)の方が良いように思う。後者のうちのいずれを使うかは,文脈でより適切な方があるかもしれない。~ ちなみに上のコードは,そのようにしただけではまだまだわかりにくい。~ わかりやすく書くと,効率が悪くなるのだろうか。そんなことはない。 *本に書かれている式をそのまま書く必要はない stats に含まれる Box.test に STATISTIC <- n * (n + 2) * sum(1/seq(n - 1, n - lag) * obs^2) # (1) と言う部分があるが, STATISTIC <- n * (n + 2) * sum(obs^2 / seq(n - 1, n - lag)) の方が好ましい。(1) を sum(1/(seq(n - 1, n - lag) * obs^2)) と誤解する人はいないであろうが,紛らわしい書き方はしない方がよい。~ ちょっとしたことが積み重なればプログラムはだんだんわかりにくくなる。 *関数のパラメータを活用する おなじく stats の Box.test に, PVAL <- 1 - pchisq(STATISTIC, lag) # (1) という部分があるが, PVAL <- pchisq(STATISTIC, lag, lower=FALSE) # (2) の方がよい。もし,(1)式の右辺に何か操作を加えるとき,かっこを付けないといけないし,意味的にも (2) の方がはっきりしている。将来のバグの温床は事前につぶしておくのが得策である。 *中間変数を使わない 式の計算の途中で,代入しない。 stats の PP.test に table <- cbind(c(4.38, 4.15, 4.04, 3.99, 3.98, 3.96), c(3.95, 3.8, 3.73, 3.69, 3.68, 3.66), c(3.6, 3.5, 3.45, 3.43, 3.42, 3.41), c(3.24, 3.18, 3.15, 3.13, 3.13, 3.12), c(1.14, 1.19, 1.22, 1.23, 1.24, 1.25), c(0.8, 0.87, 0.9, 0.92, 0.93, 0.94), c(0.5, 0.58, 0.62, 0.64, 0.65, 0.66), c(0.15, 0.24, 0.28, 0.31, 0.32, 0.33)) table <- -table がある。 table <- -cbind(c(4.38, 4.15, 中略, 0.33)) でよい。 *わかりやすい関数を使う stats の PP.test に tablen <- dim(table)[2] というのがある。この table(前項参照) は二次元配列なので, tablen <- ncol(table) の方が,わかりやすい。 ちなみに,NCOL という関数もあるが, > NCOL function (x) if (is.array(x) && length(dim(x)) > 1 || is.data.frame(x)) ncol(x) else as.integer(1) <environment: namespace:base> > ncol function (x) dim(x)[2] <environment: namespace:base> であり,ncol は,単に dim(x)[2] に過ぎないこともわかる。 > x <- 1:3 > NCOL(x) [1] 1 > ncol(x) NULL のいずれが好ましいか。私は ncol を使いたいと思う。 stats の cmdscale 関数に x[row(x) > col(x)] <- d^2 # x は正方行列 という部分がある。パズルのようだが,何のことはなくて, x[lower.tri(x)] <- d^2 のことである。どっちも,関数を知らないことにはわかりにくさは同じというものの,後者が好ましいとは思うのだ。確かに,lower.tri の関数定義は, > lower.tri function (x, diag = FALSE) { x <- as.matrix(x) if (diag) row(x) >= col(x) else row(x) > col(x) } ではあるのだが。 *for の効用と乱用と使用自粛 ライブラリに納められている関数を見ると,for は実に良く使われている。 stats の PP.test の中(前項に引き続く部分)に,次のような所がある。for が支配する文も { }でくくる方がよいが,問題は単に「R で for を使うのは避けよう」などということではなく,最初の行にある tableipl というベクトルのメモリを前もって確保している部分が不要であるということ(プログラムは,簡潔であるほどわかりやすい。わかりにくくなる方向で簡潔を目指してはいけないが)。 tableipl <- numeric(tablen) for (i in (1:tablen)) tableipl[i] <- approx(tableT, table[, i], n, rule = 2)$y sapply を使って tableipl <- sapply(1:tablen, function(i) approx(tableT, table[, i], n, rule = 2)$y) ということになる。 なぜ,for がよく使われるかというと,for が支配する文(処理)の集合が簡単なものではないということではないか。たとえば,以下のようなでっち上げの処理 for (i in 1:n) { n <- i*5 a <- sqrt(n)+beta/3 x <- atan(a) y <- cos(a+gamma) z <- pchisq(x+y, n, lower=FALSE) } を,sapply(1:n, function(i) 何とかかんとか) と書いても,簡単にも何にもならない。 なぜ,for がよく使われるかというと,for が支配する文(処理)の集合が簡単なものではないということではないか。たとえば,stats にある read.ftable の中の len <- length(tmp) for (k in seq(from = 1, to = n.row.vars)) { i <- seq(from = 1, to = len, by = len/n[k]) row.vars[[k]] <- unique(tmp[i]) tmp <- tmp[seq(from = 2, to = len/n[k])] len <- length(tmp) } というような部分を,sapply() を使って書いても,簡単にも何にもならない。 for を消去できるのは,for が支配できる文が簡単なものであって,R が用意している簡単な関数があるときだけではないか。 つまり, result <- 0 for (i in 1: n) { result <- result+x[i] } みたいなものなら, result <- sum(x) でよい,とかいうレベルに過ぎないのである。 sum などという便利な関数がないなら(計算過程が複雑ならば),for ループでしこしこやるしかない。