Rのこと。

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

データフレームの名前

はじめに

データフレームの名前属性(names)についてまとめておく。data.frame()tibble()では名前に対する扱いが異なる、ということ。詳細はRepair the names of a vector こちら。

data.frame()tibble()

data.frame()の場合、同じ名前がついていると、自動で識別して、重複しないように直してくれる。

df <- data.frame(
  x = NA,
  x = "hoge",
  x = NA,
  x = "fuga",
  x = NA,
  x = "piyo")

   x  x.1 x.2  x.3 x.4  x.5
1 NA hoge  NA fuga  NA piyo

tibble()ではエラーが返される。つまり、Tidyverseの世界観では、よく言われる話ですが、意図しないことが起きないように厳密なわけです。型とかもそうですし。

df <- tibble(
  x = NA,
  x = "hoge",
  x = NA,
  x = "fuga",
  x = NA,
  x = "piyo")


 エラー: Column names `x`, `x`, `x`, `x`, `x` must not be duplicated.
Use .name_repair to specify repair.
Run `rlang::last_error()` to see where the error occurred.

tibble()で同じようなことをするためには、.name_repair = "minimal"が必要。universalというオプションもあるが、列選択で微妙に違いが出るみたい。ドキュメントみてください。

df <- tibble(x = NA,
             x = "hoge",
             x = NA,
             x = "fuga",
             x = NA,
             x = "piyo",
             .name_repair = "minimal")

df
# A tibble: 1 x 6
  x     x     x     x     x     x    
  <lgl> <chr> <lgl> <chr> <lgl> <chr>
1 NA    hoge  NA    fuga  NA    piyo 

.name_repair = "unique"をつけておくと、重複しないように名前をつけてくれる。

df <- tibble(x = NA,
             x = "hoge",
             x = NA,
             x = "fuga",
             x = NA,
             x = "piyo",
             .name_repair = "unique")

df
# A tibble: 1 x 6
  x...1 x...2 x...3 x...4 x...5 x...6
  <lgl> <chr> <lgl> <chr> <lgl> <chr>
1 NA    hoge  NA    fuga  NA    piyo 

.name_repair = "unique"がついていても、上書きはできるので、そのtibbleの名前属性を制御するというものではないと思われる(実装まで確認できていない…)。

df <- tibble(x1 = NA,
             x2 = "hoge",
             x3 = NA,
             x4 = "fuga",
             x5 = NA,
             x6 = "piyo",
             .name_repair = "unique")

row_value_to_colname <- function(data, slice_no) {
  tmp <- data %>%
    dplyr::slice(slice_no) %>%
    dplyr::mutate_all(as.character) %>%
    unlist(., use.names = FALSE)
  
  colnames(data) <- tmp
  return(data)
}

df %>% 
  row_value_to_colname(., slice_no = 1)

# A tibble: 1 x 6
  NA    hoge  NA    fuga  NA    piyo 
  <lgl> <chr> <lgl> <chr> <lgl> <chr>
1 NA    hoge  NA    fuga  NA    piyo 

その場合、as_tibble()でオプションをつければ問題ない。

df %>% 
  row_value_to_colname(., slice_no = 1) %>% 
  as_tibble(., .name_repair = "unique")

New names:
* `` -> ...1
* `` -> ...3
* `` -> ...5
# A tibble: 1 x 6
  ...1  hoge  ...3  fuga  ...5  piyo 
  <lgl> <chr> <lgl> <chr> <lgl> <chr>
1 NA    hoge  NA    fuga  NA    piyo 

この手のデータフレームに対するnames()names2()の扱いも異なる。NAと空白がまじる場合。

df <- tibble(x1 = NA,
             x2 = "hoge",
             x3 = NA,
             x4 = "",
             x5 = NA,
             x6 = "",
             .name_repair = "minimal")


df %>% 
  row_value_to_colname(., slice_no = 1)

# A tibble: 1 x 6
  NA    hoge  NA    ``    NA    ``   
  <lgl> <chr> <lgl> <chr> <lgl> <chr>
1 NA    hoge  NA    ""    NA    ""   

names()は、NANAとして扱う一方で、names2()は空白としてNAを扱う。

df %>% 
  row_value_to_colname(., slice_no = 1) %>% 
  base::names()
[1] NA     "hoge" NA     ""     NA     ""    

df %>% 
  row_value_to_colname(., slice_no = 1) %>% 
  rlang::names2()
[1] ""     "hoge" ""     ""     ""     ""    

まぁこんなことすることないとおもうけど…Excelのデータをいじっていて、Omz....となった次第ですが。

R関数の引数チェック

はじめに

R関数の引数チェックについて、社内勉強会用にまとめておく。本当は{rlang}abort()warn()inform()あたりをまとめたかったのだが、時間がなかったので、基本的な部分だけまとめている。今後追記していく予定。

引数チェックとは

基本的に関数を書くときは、引数をチェックするためのスクリプトを記述する。しかし、意図と違うものが入力されると、ストップなりワーニングを返すようにしているが、全てをカバーしきれない。実力不足と言われれば…そうである…。なので、作った関数を誰かに渡すと、予期していないような使い方をするので、エラーが出る。エラーが出ればいいが、計算が進んで、変な結論を生むことは避けたい。

例えば、三角形の面積を求める関数を書いたとする。この段階では何ら問題はない。

trianle_area1 <- function(base, height){
  ans <- (base * height) * 0.5
  return(ans)
  }

trianle_area1(base = 2, height = 10)
[1] 10

しかし、誰かに渡すとこのように使われたりする。つまり、負の値が入力される…面積なので、負の値はありえない。

 trianle_area1(base = 2, height = -10)
[1] -10

なので、こうならないようにstop()をおいておく。

trianle_area2 <- function(base, height){
  
  if (0 > height) stop("`height` must be >= 0")
  if (0 >   base) stop("`base` must be >= 0")
  
  ans <- (base * height) * 0.5
  return(ans)
}

trianle_area2(base = 2, height = -10)
trianle_area2(base = 2, height = -10) でエラー: `height` must be >= 0

すると、次はこうなったりする。負の値を含むベクトル(Rでは、長さ1の場合でもベクトルと呼ぶ…確か、スカラとは呼ばない買ったような…まぁどっちゃでもいいか。ここでは本質ではないので。)である。この場合、Rはワーニングは出すが、負の値ではない1番目の値だけで非負かどうかの判定がなされ、ベクトルの長さにあうように値がリサイクルされる。このようなあからさまにわかりやすい計算であれば、結果がおかしいと気づくが、難しい計算であれば、気づかないし、人によってはワーニングを無視する。

trianle_area2(base = c(1, -2), height = 5)
[1]  2.5 -5.0
 警告メッセージ: 
 if (0 > base) stop("`base` must be >= 0") で: 
   条件が長さが 2 以上なので、最初の 1 つだけが使われます 

なので、これでは困るために、インプットのベクトルの長さを1に制限する。

trianle_area3 <- function(base, height){
  
  if (!length(base) == 1) stop("`base` length must be = 1")
  if (0 > base) stop("`base` must be >= 0")
   
  if (!length(height) == 1) stop("`height` length must be = 1")
  if (0 > height) stop("`height` must be >= 0")
  
  ans <- (base * height) * 0.5
  return(ans)
}

trianle_area3(base = c(1, -2), height = 5)
trianle_area3(base = c(1, -2), height = 5) でエラー: 
  `base` length must be = 1

かといって、これではせっかくベクトル化されているのに、それを使わないようにするのはもったいない。なので、if(any(hoge))と対処することもできる。負の値が含まれている場合は計算されない。

trianle_area4 <- function(base, height){
  
  if (any(0 > base)) stop("`base` must be >= 0")
  if (any(0 > height)) stop("`height` must be >= 0")
  
  ans <- (base * height) * 0.5
  return(ans)
}

trianle_area(base = c(1, 2), height = c(1,-2))
 trianle_area(base = c(1, 2), height = c(1, -2)) でエラー: 
  `height` must be >= 0

trianle_area(base = c(1, 2), height = c(1,2))
[1] 0.5 2.0

ベクトル化とは

Rの関数のほとんどは、引数としてベクトルをとって、ベクトルを返すようになっている。こうした関数の内側では、内部的にC言語のサブルーチンで処理することで、高速に計算されるように実装されています。非常にありがたい。なので、ベクトルを入力するとベクトルをfor()などを使わずに返すようにしたかったりするが、if()などはベクトルに対応していない。数年前に、なんとかプロジェクトでfor()などが大幅に改善されたとかあったような気がする。うる覚えで申し訳ないが…。

そして、for()を避ければよいという話ではない。モノによってはこっちのほうが速い。

例えば、負、ゼロ、正で値をわけてフラグを立てる関数my_sign()

my_sign <- function(x){
  
  if(!is.numeric(x)){
    stop("argument is not numeric : returning NA")
  }
  
  output <- NA_character_
  
  if(is.na(x)){
    output <- NA
  } else if(0 > x) {
    output <- "Negative"
  } else if(x == 0){
    output <- "Zero"
  } else {
    output <- "Positive"
  }
  
  return(output)
}

my_sign(1)
[1] "Positive"

my_sign(0)
[1] "Zero"

my_sign(-1)
[1] "Negative"

my_sign("abc")
 my_sign("abc") でエラー: argument is not numeric : returning NA

my_sign(NA)
 my_sign(NA) でエラー: argument is not numeric : returning NA

ベクトルを入力すると、フラグを立てるところにif()があるので、ベクトルが返ってこない。

vals <- c(-5,0,5,NA_integer_)
vals
[1] -5  0  5 NA

my_sign(vals)
[1] "Negative"
 警告メッセージ: 
1:  if (is.na(x)) { で: 
   条件が長さが 2 以上なので、最初の 1 つだけが使われます 
2:  if (0 > x) { で: 
   条件が長さが 2 以上なので、最初の 1 つだけが使われます 

なので、これを対応できるようにfor()を使うとする。気づいた人はいるかも知れないが、ベクトル化してくれる関数を使えばよいのでは?と思うかもしれない…。こんなやつである。

my_sign_vec <- base::Vectorize(FUN = my_sign)
my_sign_vec(vals)
[1] "Negative" "Zero"     "Positive" NA        

sapply(X = vals, FUN = my_sign, simplify = TRUE)
[1] "Negative" "Zero"     "Positive" NA        

vals %>% map_chr(.x = ., .f = function(x){ my_sign(x) })
[1] "Negative" "Zero"     "Positive" NA        

さっきのことを忘れて、for()で対応する。

my_sign2 <- function(x){

  if(!is.numeric(x)){
    stop("argument is not numeric : returning NA")
  }
  
  n_len <- length(x)
  output <- vector(mode = "character", length = n_len)
  
  for (i in seq_along(x)) {
    
    if(is.na(x[[i]])){
      output[[i]] <- NA_character_
    } else if(0 > x[[i]]) {
      output[[i]] <- "Negative"
    } else if(x[[i]] == 0){
      output[[i]] <- "Zero"
    } else {
      output[[i]] <- "Positive"
    }
  }
  return(output)
}

my_sign2(vals)
[1] "Negative" "Zero"     "Positive" NA        

なんだけれど、このような場合はif()の代わりにcase_when()で代替できるので、こっちを使ったほうがよい。5000万件くらい処理すると、10秒くらい変わってくる。

my_sign3 <- function(x){
  
  if(!is.numeric(x)){
    stop("argument is not numeric : returning NA")
  }
  
  output <- case_when(is.na(x) ~ NA_character_,
          0 > x ~ "Negative",
          x == 0 ~ "Zero",
          TRUE ~ "Postive")
  return(output)
}

my_sign3(vals)


system.time(
  my_sign2(rnorm(5e7))
)

ユーザ   システム       経過  
24.712      0.391     25.337 

system.time(
  my_sign3(rnorm(5e7))
)

ユーザ   システム       経過  
12.103      3.958     16.845

1億件くらい処理すると、15秒くらい変わってくる。

system.time(
  my_sign2(rnorm(1e8))
)

ユーザ   システム       経過  
53.781      1.076     60.426 

system.time(
  my_sign3(rnorm(1e8))
)

ユーザ   システム       経過  
24.987     12.033     44.488

再帰関数

再帰関数、再帰呼び出しについて。これは関数内で自分自身を呼び出す関数のこと。

rsum <- function(n){
  if (n == 1){
    return(1)
  }
  
  return(n + rsum(n - 1))
}

rsum(5)
[1] 15

やっていることは下記と同じではある。

sum(1:5)
[1] 15

res = 0
for (i in 1:5) {
  res = res + i
}
res
[1] 15

上記のnまでの数を合計する関数の挙動について考える。下記の画像のように自分自身であるrsum()を呼び出している。

f:id:AZUMINO:20201226194307j:plain

ここでは、終了条件として、 if (n == 1){return(1)}があり、ここまで潜ると、returnが実行され、随時、結果が返され、画像のように動く。つまり、returnは下記のようになる。

f:id:AZUMINO:20201226195753p:plain