ある日tidyと一緒に: tidyverseは厳しいがとても優しい
この記事はtidyポエムAdvent Calendarの4日目の記事となります。タイトルは釣りです。釣られた人は乙でした。
本当のタイトルは「tidyverseは厳しいがとても優しい」です。某ホクソエム氏のようです(個人の意見です)。
tidyverseな世界に慣れてきて、つまづきやすいかなという点をまとめました。小ネタ的なものですが、少しでも皆さんの参考になればと思います。言いたいことは、データ型に気をつけろ、ということです。
tibble
はじめに言っておくと、私はtibble大好きです。界隈ではtibbleのせいで、tibbleってなんだよ、と言った声も聞こえてくるわけですが、tibbleにはメリットしか感じません。tibble::as_tibble()
、みんなtibbleになるといいよ(というのは言い過ぎか)。
まず、tibbleをご存知でない方に説明しておくと、tibbleというのはRのデータフレームオブジェクトを拡張したオブジェクト形式の一種です。tibbleの特徴をまとめると次のようになります。
- 入力された値(文字列)をそのまま評価する(文字列型として扱う。因子型に変換しない。)
- row.namesを与えない
- 列名の空白や記号を入力の通りに評価する
- 変数の遅延評価を行える
- リストクラスのオブジェクトを内包することが可能
- 長さが1しかないベクトルの値が再利用される
- 引数に与えた列のデータを別の列に利用できる(変数の再利用)
- 出力の際にデータフレーム全体を表示するのではなく、データフレームのデータフレ0.ムのソースと大きさ、変数のデータ型を表示する
- 変数を参照する際、正確な名称を指定しない限りNULLを返す
個人的に大事だなと思うのが、5と8です。5については階層構造のあるデータフレーム、として大事な概念です。ここで述べると長くなるので以前書いた記事へのリンクを貼ります。このおかげでdplyr::do
やpurrr::dmap()
がいい感じで機能します。ここでは8の特徴について説明します。
Hadley Ecosystem 2016 by Uryu Shinya
通常のデータフレームは、オブジェクトを出力するとデータの全体が返ってきます。データ数が少なければ良いですが、これって実は見にくいですよね。行数が多いと、変数名とデータを照らし合わせるのにコンソールをスクロールをしなければならない。でもtibbleでは、出力は先頭の数行だけです(オプションで変更可能)。また、データがどのような型をしているのかも表示されるので、因子型と文字列型の違いによるトラブルを避けやすくなります。全体のサイズも表示してくれるのも良いですね。優しい!
iris %>% class() ## [1] "data.frame" iris %>% tibble::as_tibble() ## # A tibble: 150 x 5 ## Sepal.Length Sepal.Width Petal.Length Petal.Width Species ## <dbl> <dbl> <dbl> <dbl> <fctr> ## 1 5.1 3.5 1.4 0.2 setosa ## 2 4.9 3.0 1.4 0.2 setosa ## 3 4.7 3.2 1.3 0.2 setosa ## 4 4.6 3.1 1.5 0.2 setosa ## 5 5.0 3.6 1.4 0.2 setosa ## 6 5.4 3.9 1.7 0.4 setosa ## 7 4.6 3.4 1.4 0.3 setosa ## 8 5.0 3.4 1.5 0.2 setosa ## 9 4.4 2.9 1.4 0.2 setosa ## 10 4.9 3.1 1.5 0.1 setosa ## # ... with 140 more rows
データ型
tidyverseに慣れてくると、特に、先ほどのtibbleで出力されるデータ型を意識するようになります。一見するとハマりどころです。
if_elseの例
この話はすでにyutannihilationさんブログに書かれていることですが、アンサーソングへのアンサーソングという感じで。
Rのifelse()
はちょっとゆるいという話です。例えば次のように条件分岐で返り値が変化する処理を書いた場合、結果のデータ型が複数定義されていても問題とはなりません。
# 文字列と数値を含める ifelse(10 > 1, "a", FALSE) ## [1] "a" ifelse(10 > 1, TRUE, "b") ## [1] TRUE
しかし条件によってデータ型が変わってしまうのはトラブルの元になりかねません。ifelse()
をより厳密にしたdplyr::if_else()
では上の処理はエラーとなります。
dplyr::if_else(10 > 1, "a", FALSE) # Error: `false` must be type string, not double dplyr::if_else(10 > 1, TRUE, "b") # Error: `false` must be type logical, not character
dplyr::if_else()
の中での返り値は、複数の型を混ぜることはできません。上の処理を実行するには文字列で揃える必要があります。
# 文字列にしたいなら明示的に文字列としておく dplyr::if_else(10 > 1, as.character(TRUE), "b") ## [1] "TRUE"
特にハマるポイントなのが欠損値の処理です。次のように入力値が10未満である場合に入力値をそのまま返す、という処理を組んだ際、NAをNAとして返そうとするとエラーです。
hoge <- function(x) { dplyr::if_else(10 > x, NA, "10以上") } hoge(5) # Error: `false` must be type string, not logical
欠損値にもデータ型に応じて複数の種類があります。この場合はFALSEの値が文字列ですので、TRUEの際も文字列としての欠損値が返るようにしなくてはいけません。
# NAのデータ型を文字列由来にする fuga <- function(x) { dplyr::if_else(10 > x, NA_character_, "10以上") } # 返り値はNA fuga(5) ## [1] NA
join, bindの例
データの結合にjoin、bind群の関数を使いますが、ここでもデータ型に注意が必要です。次の例はエラーになります。なぜでしょうか。
dplyr::full_join(iris[, "Species"], iris[, "Species"] %>% tibble::as_tibble(), by = "Species") # Error in UseMethod("full_join") : # no applicable method for 'full_join' applied to an object of class "factor"
これは因子型であるiris$Species
と、tibble化され文字列となったiris$Species
の列がデータ型が異なるがためのエラーです。もう一つ例を見ましょう。次は正解例から示します。
d <- data.frame( var = c(1, 3, 5), var2 = letters[1:3] ) d2 <- data.frame( var = c(1, 2, 3) ) dplyr::bind_rows(d, d2) ## var var2 ## 1 1 a ## 2 3 b ## 3 5 c ## 4 1 <NA> ## 5 2 <NA> ## 6 3 <NA> dplyr::left_join(d, d2, by = "var") ## var var2 ## 1 1 a ## 2 3 b ## 3 5 c
こちらが失敗する例です。d3というデータフレームでは文字列var変数を持っています。
d3 <- data.frame( var = c("1", "2", "3") ) dplyr::bind_rows(d, d3) # Error in bind_rows_(x, .id) : # Column `var` can't be converted from numeric to factor dplyr::left_join(d, d3, by = "var") # Error in left_join_impl(x, y, by$x, by$y, suffix$x, suffix$y, check_na_matches(na_matches)) : # Can't join on 'var' x 'var' because of incompatible types (factor / numeric)
このように、変数名やデータ構造が一致していても、データ型が異なる場合には処理を停止してくれる(今後のトラブルを防ぐ)処理が施されています。優しい!
purrr::mapの例
purrrパッケージのmap()
の返り値はリストオブジェクトですが、map()
の返り値をオブジェクトの種類を関数名を変更することで指定できます。例えば文字列として返したいならばpurrr::map_chr()
を使います。
letters[1:4] %>% purrr::map(toupper) ## [[1]] ## [1] "A" ## ## [[2]] ## [1] "B" ## ## [[3]] ## [1] "C" ## ## [[4]] ## [1] "D" letters[1:4] %>% purrr::map_chr(toupper) ## [1] "A" "B" "C" "D"
一方で、指定した型へ変換できない際は怒られます。当然ですね。データ型の変換は、「論理型、整数型、倍精度小数点型、文字列」の順で行われます。なので、数値を文字列にすることはできてもその逆はできません。気をつけましょう。
letters[1:4] %>% purrr::map_dbl(toupper) # Error: Can't coerce element 1 from a character to a double
この他にもdplyrのselect_if
、mutate_if
などを利用する際にデータ型によって処理を適用する、ということがあるかと思います。データ型の確認は重要。繰り返しておきます。
tidyverseのコンフリクトやバージョン
さて、tidyverseはRでtidyデータを扱うための概念とも捉えられるわけですが、実態としてのtidyverseも存在します。ズバリtidyverseパッケージを読み込みます。
library(tidyverse)
するとこのような出力がでます。上から、tidyverseパッケージ自体のバージョンおよびアタッチされた(利用可能となった)パッケージとそのバージョン、名前空間の衝突が発生している関数名の一覧です。多数のパッケージを読み込み、標準実装されている関数名とも衝突するtidyverseパッケージ、その情報を出力してくれるなんて、優しさに溢れています!
これはRStudioでの実行結果ですが、コンソールでもR.appでもカラーリングは実行されます。これにはcrayonパッケージが一役買っています。色をつけることで、情報の次元が広がりますね。優しい!
いかがだったでしょう。tidyverseの優しさと厳しさ、Hadleyの愛、伝わりましたでしょうか。今日のポエムはここで締めます。また会いましょう。
Enjoy!