備忘ログ

チラシの裏的備忘録&メモ

Rでapply()系などの中で無名関数を使ったときにちょっと不思議な挙動をしたのでメモ

Rでapply()で無名関数を使ったときにちょっと不思議な挙動をしたのでメモしておく。

多分、apply()というより関数内に無名関数を仕込むとこういうことになるんだと思う。関数内に無名関数を仕込むのはapply()系で多いと思うから表出したのだと思う。

解決策自体は簡単でコードは注意して正しく書きましょうということ。

例題:絶対値を求める自作関数

例として、絶対値を求める自作関数を次のように作ったとする(他に条件分岐する簡単な例が思いつかなかっただけで、絶対値を求める関数は{base}abs()があるのでそれを使うのが良い)。

zettaichi <- function(x){
  if(x < 0) ans <- x * -1
  else ans <- x
  
  ans
}
zettaichi(-1)
## [1] 1
zettaichi(3)
## [1] 3

もし与えられた値が負の値なら-1をかけて答えとして、そうでないならそのままの値を答えとする関数を作ってみる。

なんとうまい関数ができたと満足しそうだが、複数の値を入れたときにうまく挙動しなくなる。

zettaichi(c(-3, -2, -1, 0, 1, 2, 3))
## Warning in if (x < 0) ans <- x * -1 else ans <- x: 条件が長さが 2 以上なので、最
## 初の 1 つだけが使われます

## [1]  3  2  1  0 -1 -2 -3

これは条件分岐するときに条件としてxに与えられた最初の値、今回の場合は-3だけで条件分岐が行われ、c(-3, -2, -1, 0, 1, 2, 3)というベクトルに対して-1をかけるという処理が行われているせいである。その旨の警告文もちゃんと出ている。

ちなみに今後めんどくさいのでc(-3, -2, -1, 0, 1, 2, 3)は-3:3を書く(同じ意味)。条件分岐で最初の値が使われちゃうというのがこうやって書いたほうがわかりやすと思ったから初回だけこうやって書いた。

今回の本旨とは関係ないが、絶対値を求めたいだけなら値を2乗して平方根を求めれば条件分岐しなくても良くなるし、ベクトルのままいけるので今回の指摘はあたらない。ただし、今回は条件分岐しないとapply()系を使う動機がまったくなくなるのでこれは採用しないこととする(そもそも前述したように既存の関数を使うべきである)。

zettaichi <- function(x){
  sqrt(x ^ 2)
}
zettaichi(-3:3)
## [1] 3 2 1 0 1 2 3

apply()系を使う

条件分岐の問題を解決するには、与えた値一つ一つ(または指定した形で複数の値を引数として)で関数を実行してもらえれば良い。こういうときにRの{base}系の書き方ではapply()系の関数をつかって書くことが多い(という個人的な理解)。

# 今回はベクトルの値を受け取ってベクトルで答えを返したいので`sapply()`を使う。
# 先に作った自作関数`zettaichi()`を実行させる関数(FUNの引数)として使う。
zettaichi <- function(x){
  if(x < 0) ans <- x * -1
  else ans <- x
  
  ans
}
zettaichi_sapply01 <- function(x){
  sapply(x, zettaichi)
}
zettaichi_sapply01(-3:3)
## [1] 3 2 1 0 1 2 3

これで問題なくベクトルでも実行できる関数ができた。

このとき、先に作ったzettaichi()を使っているのだが、apply()系では無名関数(名前をつけていない関数をその場で記載)でも実行できるようになっている。具体的にはつぎのようになる。

zettaichi_sapply02 <- function(x){
  sapply(x, function(x){
    if(x < 0) ans <- x * -1
    else ans <- x
    
    ans
  })
}
zettaichi_sapply02(-3:3)
## [1] 3 2 1 0 1 2 3

先にzettaichi()としていた関数をsapply()のFUNの引数のところで無名関数として記載している。

無名関数で実施できると良いことのひとつとして、わざわざ名前をつけなくても良いところである。たかが命名かもしれないが、色々名前をつけていると意図せず同じ名前を使ってしまって上書きしてみたり、そもそも適切な名前がつけられなかったり、適当につけた名前のせいで中身がなんだかわからないということがよく起こる。

ここまでが正しい(?)書き方になるが、apply()で無名関数を扱うときにうっかり書き間違いをするとうまく意図したとおりに挙動しなくなる。

例えば、完全にだめな例でだめなのも理解できる書き方として。

zettaichi_sapply03 <- function(x){
  sapply(n, function(x){
    if(x < 0) ans <- x * -1
    else ans <- x
    
    ans
  })
}
zettaichi_sapply03(-3:3)
## Error in lapply(X = X, FUN = FUN, ...):  オブジェクト 'n' がありません

これは、関数の最初にベクトルをxのオブジェクトとして受けているところにsapply()でオブジェクトnとして関数に与えようとしてnなんてオブジェクトはないよと怒られている。ないものを指定しようとしているので怒られるのはごもとってもである。ところで、今回は小さい関数なので特に問題ないが、大きい関数になってきて間違ったオブジェクトをsapply()で指定したときに、たまたまそのオブジェクトが関数内で指定されていたりすると、関数自体は動いてしまう。

別のパターンとして、

zettaichi_sapply04 <- function(x){
  sapply(n, function(n){
    if(x < 0) ans <- x * -1
    else ans <- x
    
    ans
  })
}
zettaichi_sapply04(-3:3)
## Error in lapply(X = X, FUN = FUN, ...):  オブジェクト 'n' がありません

も同様の結果になる。これもオブジェクトnなんて無いよと怒られる。この例はぼさっとコピペ作業をしているとやりかねないので注意が必要となる。

apply()系の謎挙動

さて、問題となるのはつぎからで、

zettaichi_sapply05 <- function(x){
  sapply(x, function(n){
    if(x < 0) ans <- x * -1
    else ans <- x

    ans
  })
}
zettaichi_sapply05(-3:3)
## Warning in if (x < 0) ans <- x * -1 else ans <- x: 条件が長さが 2 以上なので、最
## 初の 1 つだけが使われます

## Warning in if (x < 0) ans <- x * -1 else ans <- x: 条件が長さが 2 以上なので、最
## 初の 1 つだけが使われます

## Warning in if (x < 0) ans <- x * -1 else ans <- x: 条件が長さが 2 以上なので、最
## 初の 1 つだけが使われます

## Warning in if (x < 0) ans <- x * -1 else ans <- x: 条件が長さが 2 以上なので、最
## 初の 1 つだけが使われます

## Warning in if (x < 0) ans <- x * -1 else ans <- x: 条件が長さが 2 以上なので、最
## 初の 1 つだけが使われます

## Warning in if (x < 0) ans <- x * -1 else ans <- x: 条件が長さが 2 以上なので、最
## 初の 1 つだけが使われます

## Warning in if (x < 0) ans <- x * -1 else ans <- x: 条件が長さが 2 以上なので、最
## 初の 1 つだけが使われます

##      [,1] [,2] [,3] [,4] [,5] [,6] [,7]
## [1,]    3    3    3    3    3    3    3
## [2,]    2    2    2    2    2    2    2
## [3,]    1    1    1    1    1    1    1
## [4,]    0    0    0    0    0    0    0
## [5,]   -1   -1   -1   -1   -1   -1   -1
## [6,]   -2   -2   -2   -2   -2   -2   -2
## [7,]   -3   -3   -3   -3   -3   -3   -3

無名関数の引数のオブジェクトをn(使っていないオブジェクト)を指定した場合、上記のような不思議な挙動をする(そもそもおかしな書き方しているのは承知している)。

つぎのように途中でそれぞれのオブジェクトがどうなっているのかprint()を使って見てみると

zettaichi_sapply05 <- function(x){
  print(paste("sapplyに入る前のx", x))
  sapply(x, function(n){
    print(paste("sapplyの中のn", n))
    print(paste("sapplyの中のx", x))
    # 関数の処理自体はコメントアウトしておく
    # if(x < 0) ans <- x * -1
    # else ans <- x

    # ans
  })
  print(paste("sapplyのあとのx", x))
  print(paste("sapplyのあとのn", n))
}
# -3:3だと多いので-1:1で実行する
zettaichi_sapply05(-1:1)
## [1] "sapplyに入る前のx -1" "sapplyに入る前のx 0"  "sapplyに入る前のx 1" 
## [1] "sapplyの中のn -1"
## [1] "sapplyの中のx -1" "sapplyの中のx 0"  "sapplyの中のx 1" 
## [1] "sapplyの中のn 0"
## [1] "sapplyの中のx -1" "sapplyの中のx 0"  "sapplyの中のx 1" 
## [1] "sapplyの中のn 1"
## [1] "sapplyの中のx -1" "sapplyの中のx 0"  "sapplyの中のx 1" 
## [1] "sapplyのあとのx -1" "sapplyのあとのx 0"  "sapplyのあとのx 1"

## Error in paste("sapplyのあとのn", n):  オブジェクト 'n' がありません

となる。

このことから、xのオブジェクトはsapply()で一つずつnに値として与えられているが、無名関数の中での処理がxになっているので関数の最初でオブジェクトxに投入されたベクトルがそのままsapply()内の無名関数で扱われ、というのがnに与えられる数だけ計算されることになる様子。

オブジェクトnについては無名関数内でのみのローカル変数で使い捨ての様子で、無名関数を抜けると消える。xは関数全体でのローカル関数なのでsapply()内の無名関数(関数内関数)でも生きている……ということなんだと思う。

purrr::mapでも同様の謎挙動

apply()系の{tidyvers}版のpurrr::map()でも似たようなことが起こる。

正しく挙動する記法は省いて、早速謎挙動をする例から。

# 前述の`zettaichi_sapply05`と同様の記法
zettaichi_purrr05 <- function(x){
  purrr::map(x, function(n){
    if(x < 0) ans <- x * -1
    else ans <- x
    
    ans
  })
}
zettaichi_purrr05(-3:3)
## Warning in if (x < 0) ans <- x * -1 else ans <- x: 条件が長さが 2 以上なので、最
## 初の 1 つだけが使われます

## Warning in if (x < 0) ans <- x * -1 else ans <- x: 条件が長さが 2 以上なので、最
## 初の 1 つだけが使われます

## Warning in if (x < 0) ans <- x * -1 else ans <- x: 条件が長さが 2 以上なので、最
## 初の 1 つだけが使われます

## Warning in if (x < 0) ans <- x * -1 else ans <- x: 条件が長さが 2 以上なので、最
## 初の 1 つだけが使われます

## Warning in if (x < 0) ans <- x * -1 else ans <- x: 条件が長さが 2 以上なので、最
## 初の 1 つだけが使われます

## Warning in if (x < 0) ans <- x * -1 else ans <- x: 条件が長さが 2 以上なので、最
## 初の 1 つだけが使われます

## Warning in if (x < 0) ans <- x * -1 else ans <- x: 条件が長さが 2 以上なので、最
## 初の 1 つだけが使われます

## [[1]]
## [1]  3  2  1  0 -1 -2 -3
## 
## [[2]]
## [1]  3  2  1  0 -1 -2 -3
## 
## [[3]]
## [1]  3  2  1  0 -1 -2 -3
## 
## [[4]]
## [1]  3  2  1  0 -1 -2 -3
## 
## [[5]]
## [1]  3  2  1  0 -1 -2 -3
## 
## [[6]]
## [1]  3  2  1  0 -1 -2 -3
## 
## [[7]]
## [1]  3  2  1  0 -1 -2 -3

無名関数の引数だけをオブジェクとして使っていないnとして場合、同じようにapply()系で書いたときと似た結果が返ってくる。

多分apply()系と同じ理由だと思う。つまり最初に書いたようにこれらはapply()だからというわけじゃなく関数内に無名関数を仕込んだときに、注意が不足していると起こり得ることだと思う。

OMAKE

apply()系に与える値をすでに既定値があるものだったり関数内で別に規定されているオブジェクトと同じ藻にしてしまった場合がつぎの通りとなる。

# `pi`はπのことで3.14……を意味する既定値
zettaichi_sapply_omake01 <- function(x){
  sapply(pi, function(x){
    if(x < 0) ans <- x * -1
    else ans <- x
    
    ans
  })
}
zettaichi_sapply_omake01(-3:3)
## [1] 3.141593

これはわかりやすくてxの値がどうであれ、piの値がsapply()内の無名関数に投入されて、無名関数内ではx = piで計算され、結果としてpiの絶対値が計算されたということになると思う。

つぎはfunctionのオブジェクトだけpiにしてみる。

zettaichi_sapply_omake02 <- function(x){
  sapply(x, function(pi){
    if(x < 0) ans <- x * -1
    else ans <- x
    
    ans
  })
}
zettaichi_sapply_omake02(-3:3)
## Warning in if (x < 0) ans <- x * -1 else ans <- x: 条件が長さが 2 以上なので、最
## 初の 1 つだけが使われます

## Warning in if (x < 0) ans <- x * -1 else ans <- x: 条件が長さが 2 以上なので、最
## 初の 1 つだけが使われます

## Warning in if (x < 0) ans <- x * -1 else ans <- x: 条件が長さが 2 以上なので、最
## 初の 1 つだけが使われます

## Warning in if (x < 0) ans <- x * -1 else ans <- x: 条件が長さが 2 以上なので、最
## 初の 1 つだけが使われます

## Warning in if (x < 0) ans <- x * -1 else ans <- x: 条件が長さが 2 以上なので、最
## 初の 1 つだけが使われます

## Warning in if (x < 0) ans <- x * -1 else ans <- x: 条件が長さが 2 以上なので、最
## 初の 1 つだけが使われます

## Warning in if (x < 0) ans <- x * -1 else ans <- x: 条件が長さが 2 以上なので、最
## 初の 1 つだけが使われます

##      [,1] [,2] [,3] [,4] [,5] [,6] [,7]
## [1,]    3    3    3    3    3    3    3
## [2,]    2    2    2    2    2    2    2
## [3,]    1    1    1    1    1    1    1
## [4,]    0    0    0    0    0    0    0
## [5,]   -1   -1   -1   -1   -1   -1   -1
## [6,]   -2   -2   -2   -2   -2   -2   -2
## [7,]   -3   -3   -3   -3   -3   -3   -3

functionのオブジェクトだけをnしたときと同じ挙動をする。ここではpiがπとしてではなくpiというオブジェクトとしていろいろな値が代入されるオブジェクトとなっている状況で、いままでの同様の結果が返ってきたものと同じだと思う。

つぎが謎挙動。

zettaichi_sapply_omake03 <- function(x){
  sapply(pi, function(pi){
    if(x < 0) ans <- x * -1
    else ans <- x
    
    ans
  })
}
zettaichi_sapply_omake03(-3:3)
## Warning in if (x < 0) ans <- x * -1 else ans <- x: 条件が長さが 2 以上なので、最
## 初の 1 つだけが使われます

##      [,1]
## [1,]    3
## [2,]    2
## [3,]    1
## [4,]    0
## [5,]   -1
## [6,]   -2
## [7,]   -3

sapply()に与えた最初のpiが無名関数のpiに渡されてpi = piとされ1回だけ処理される……が中身はxを処理するものなので、xのオブジェクトがxベクトルの最初の-3の値で条件分岐して、ベクトルに-1がかけられる……ように見えるが、なぜ縦長(行列)になった? 一回だけのベクトルに対する計算なんだからベクトルのままでもいいんじゃないか? そういえばいままでも何故か行列になっているがなんで行列なんだ?と思った。

よくわからないけど、もしかしたら以外に簡単な理由なのかもしれない。