ggplot2でna.rmが効かなくなった話(2.2.0以降)

先日、ggplot2の2.2.0がリリースされました。この変更点については以下の記事にまとめました。

notchained.hatenablog.com

で、一点、

これはちょっとわかりづらいので、別途解説を書こうと思います。。

と言ってた宿題があるのでそれをこの記事では書きます。

発端

実は、r-wakalangでの議論から発展して、以下のIssueを報告したのが発端です(すみません...)。

そもそもggplot2のna.rmとはどういう意味だったのか

例えば、手元のRで?geom_pointと打ってみてください。ヘルプに以下のような記述があるはずです。

na.rm If FALSE, the default, missing values are removed with a warning. If TRUE, missing values are silently removed.

...おわかりいただけただろうか。

え、FALSEでもTRUEでも欠損値は消えるの??という疑問符が頭の中を駆け巡っていることでしょう。そう、ggplot2においてそもそもna.rmNAを取り除くかどうかをコントロールする引数ではなかったんです。いずれにしてもNAは取り除かれて、ただ警告が出るかどうかが変わるだけです。この記述は実に2011年から変わっていません。

さて、頭が混乱してきたことだと思いますが、いったん深呼吸しましょう。ここからもう一段階ややこしくなります。

NAを取り除くか取り除かないかを選びたい

上の記述を見ると常にNAが取り除かれるように読めますが、実はggplot2の挙動はそうはなっていません。NAを可能な限りプロットに含めようとします。「可能な限り」というのは、離散値であればNAを軸のラベルの一つとして扱うことができますが、連続値の場合にはできないからです。

しかし、NAを軸から取り除きたい場合というのはよくあります。この挙動をコントロールする方法がほしい、というのでできたのがna.translate引数です。これをTRUEにすると、NAを軸のラベルに含めます(これがデフォルト)。FALSEにするとNAは無視します。ちなみに、na.translateは、na.rmのようにgeom_*()とかstat_*()の引数ではなく、scale_*_discrete()の引数に指定します。「NAを軸から取り除くか」というのは軸に属する要素だからです。

実際に使ってみましょう。こういう欠損値を含むデータがあるとします。

library(ggplot2)

set.seed(1)
x_levels <- c("A", "B", NA)
df <- tibble::data_frame(
   x = sample(x_levels, size = 100, replace = TRUE, prob = c(0.7, 0.2, 0.1)),
   y = rnorm(100)
)

これをそのままプロットすると以下のようになります。NAも軸のひとつとして扱われています。

ggplot(df) + geom_boxplot(aes(x, y))

f:id:yutannihilation:20161123101835p:plain:w600

次に、X軸にna.translate = FALSEを指定してみましょう。今度は、警告とともにNAが取り除かれています。

ggplot(df) + geom_boxplot(aes(x, y)) + scale_x_discrete(na.translate = FALSE)
#> Warning message:
#> Removed 6 rows containing non-finite values (stat_boxplot).

f:id:yutannihilation:20161123101852p:plain:w600

そしてここでおもむろにna.rm = TRUEを指定してみましょう。なんと...警告が出なくなります。えっ、それだけ...?

ggplot(df) + geom_boxplot(aes(x, y), na.rm = TRUE) + scale_x_discrete(na.translate = FALSE)

はい、それだけです。かつてNAをすべて消し去るほどの力があると信じられていたna.rmも、今では警告を消す程度が関の山です。泣けますね。

na.rmの説明をもう一度読み返してみましょう。

na.rm If FALSE, the default, missing values are removed with a warning. If TRUE, missing values are silently removed.

英語力が足りなくて理解できてませんでしたが、これは「NAを警告あり/なしで取り除くよ」と言ってるのではなくて、「NAが取り除かれるときに警告が出る/出ないようになるよ」という意味みたいです。うーん、英語難しい...

個人的な感想

そんなわけで、ggplot2でNAを軸から取り除くためにはscale_x_discrete(na.translate = FALSE)と打つか、あるいは元データからあらかじめNAを取り除いておかなくてはいけません。はっきり言って面倒です。

ですが、NAが黙って消されるとするとそれはそれで危険です。データの探索に上の例のようなコードを打つことはよくあります。もしここで、NAが無視されてデータにNAが大量に含まれていることに気づかなければ、NAが含まれないきれいなデータだと思い込んで分析を進めてしまいかねません。

きれいなグラフを描くのは手間をかければできます。探索的にやるときのために、安全側に倒してなるべく元データの情報を失わないように、汚いデータは汚いグラフになるように、という方針になっているんだと思います。てことで、まあ仕方ないのかなあと個人的には思います。

そうは言ってもna.rmわかりにくすぎだろ...