cucumber flesh

Rを中心としたデータ分析・統計解析らへんの話題をしていくだけ

ある日tidyと一緒に: tidyverseは厳しいがとても優しい

この記事はtidyポエムAdvent Calendarの4日目の記事となります。タイトルは釣りです。釣られた人は乙でした。

本当のタイトルは「tidyverseは厳しいがとても優しい」です。某ホクソエム氏のようです(個人の意見です)。

uribo.hatenablog.com

tidyverseな世界に慣れてきて、つまづきやすいかなという点をまとめました。小ネタ的なものですが、少しでも皆さんの参考になればと思います。言いたいことは、データ型に気をつけろ、ということです。

tibble

はじめに言っておくと、私はtibble大好きです。界隈ではtibbleのせいで、tibbleってなんだよ、と言った声も聞こえてくるわけですが、tibbleにはメリットしか感じません。tibble::as_tibble()、みんなtibbleになるといいよ(というのは言い過ぎか)。

まず、tibbleをご存知でない方に説明しておくと、tibbleというのはRのデータフレームオブジェクトを拡張したオブジェクト形式の一種です。tibbleの特徴をまとめると次のようになります。

  1. 入力された値(文字列)をそのまま評価する(文字列型として扱う。因子型に変換しない。)
  2. row.namesを与えない
  3. 列名の空白や記号を入力の通りに評価する
  4. 変数の遅延評価を行える
  5. リストクラスのオブジェクトを内包することが可能
  6. 長さが1しかないベクトルの値が再利用される
  7. 引数に与えた列のデータを別の列に利用できる(変数の再利用)
  8. 出力の際にデータフレーム全体を表示するのではなく、データフレームのデータフレ0.ムのソースと大きさ、変数のデータ型を表示する
  9. 変数を参照する際、正確な名称を指定しない限りNULLを返す

個人的に大事だなと思うのが、5と8です。5については階層構造のあるデータフレーム、として大事な概念です。ここで述べると長くなるので以前書いた記事へのリンクを貼ります。このおかげでdplyr::dopurrr::dmap()がいい感じで機能します。ここでは8の特徴について説明します。

suryu.me

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

この他にもdplyrselect_ifmutate_ifなどを利用する際にデータ型によって処理を適用する、ということがあるかと思います。データ型の確認は重要。繰り返しておきます。

tidyverseのコンフリクトやバージョン

さて、tidyverseはRでtidyデータを扱うための概念とも捉えられるわけですが、実態としてのtidyverseも存在します。ズバリtidyverseパッケージを読み込みます。

library(tidyverse)

f:id:u_ribo:20171204102850p:plain

するとこのような出力がでます。上から、tidyverseパッケージ自体のバージョンおよびアタッチされた(利用可能となった)パッケージとそのバージョン、名前空間の衝突が発生している関数名の一覧です。多数のパッケージを読み込み、標準実装されている関数名とも衝突するtidyverseパッケージ、その情報を出力してくれるなんて、優しさに溢れています!

これはRStudioでの実行結果ですが、コンソールでもR.appでもカラーリングは実行されます。これにはcrayonパッケージが一役買っています。色をつけることで、情報の次元が広がりますね。優しい!

いかがだったでしょう。tidyverseの優しさと厳しさ、Hadleyの愛、伝わりましたでしょうか。今日のポエムはここで締めます。また会いましょう。

Enjoy!

私とホクソエム

この記事は「HOXO-M Advent Calendar 2017」の2日めです。昨日は id:yutannihilationさんの「出ない順ホクソエム語彙集(その1)」でした。ホクソエムってなんなんでしょうね笑

さて、2日目は私が担当します。それでは聞いてください「私とホクソエム」。


現在の日本のRコミュニティで、もっとも長い歴史とたくさんの発表者・参加者がいるTokyo.R。その中で私が最初にスゲーと思ったのがこのアドベントカレンダーの主役である「ホクソエム(hoxo_m)」氏である。某ECサイトのロゴを連想させるアイコンと可愛げのある「h(o x o )m」という表現での顔文字(?)もまた可愛いのがにくい。

そんな彼との最初の出会いは、もちろんネットである。私がどっぷりと統計言語Rにハマりだした2014年から2015年にかけて、ホクソエム氏もまたブログやQiitaにRの記事を書いていた。いや、ホクソエム氏の記事があったから私はここまでRが好きになれたのかもしれない。

本物の「ホクソエム」をみたのは2014年のBUGS/stan勉強会 #3である。相関係数という身近な物を題材にStanを使った解説をしてくれた。

d.hatena.ne.jp

で、それに触発されて私も参加記事を書いた。

qiita.com

だがしかし、その時はまだホクソエム氏に認識されていなかったようである。

Oh... まあ、仕方ないよね。

以降、ホクソエム氏に認識されてもらうべく、私のQiitaでの活動は熱を増していった(かどうかは記憶が定かではないが、記事をめっちゃ書いている)。

そんな活動が認められたのか、運命の時は訪れる。2015年2月のTokyo.R#46だ。自分はLTをした。発表後、ホクソエムさんがやってこられて名刺を頂いた。ホクソエム氏のブログの最初の記事には

2年ほど前にバイオインフォマティクス系の会社に入社

http://d.hatena.ne.jp/hoxo_m/20100727/1280236673

とあり、その会社にいる頃に名刺を頂いた(だと思う)。当時大学院生だった私は交換する名刺を持ち合わせていなかったが、憧れの「ホクソエム」名刺を手に入れて名刺文化も悪くないなと思った記憶がある。まだ匿名知的集団ホクソエムも株式会社ホクソエムも誕生していなかったが、当時からホクソエムというブランドがあったのは確かである。

これでホクソエム氏とお近づきになったぞ!と思っていたのもつかの間、今度は雷を落とされる。ホクソエム氏は2015年10月に転職をしているのだが、前職の退職記念に飲み会が開催されることになっていて、幸運なことにそれに参加させて頂いた。そこで事件はおこる。

ホクソエム「うりさんは研究者になりたいならRやっている場合じゃないんじゃないの?
Rで食っていくの?? そんなことできるの???」

私「え、その...(今それいう??)」

だいたいそんな感じの会話があった。Rで記事を書いて、Qiitaなんかでちょっともてはやされて浮かれていた私にはマイティー・ソーの放つ雷よりも強烈な言葉だった。

その言葉は私が大学院を退学するまで、いや今も心の中で響いている(ホクソエムさんの言葉が引き金になったとか最後の一撃になったとかではないです)。

ホクソエムさんはこのように、厳しい。Twitterでも自然な発言が発端となってどこかに流れ弾が飛んでいることもあるように思う。私も何度か食らった。だが私はその厳しさを見習いたいと思う。きっとホクソエム氏は自分の息子(界隈ではきーたと呼んでいる)にも厳格な父であるのだろう。そんなホクソエムを尊敬する。

といいつつも、褒めるときは褒めてくれるし、メンションで気楽に絡んでくれる時もあり、優しい。

と振り返ってみたが、期間は長いがそこまで深い付き合いとは言えないのが現実だ。ベロベロさんを交えて進捗カフェへ行くこともあったが、二人で話した機会はそんなにない気がする。 にも関わらず、ホクソエムが会社を立ち上げた際には、追加メンバー?として誘ってくれたし、ホクソエム経由で仕事も回してもらったこともある。大変感謝している。 最近では、私が転職で悩んでいる際も相談に乗ってくれた。ホクソエム氏は裏表のない言葉をかけてくれる(と思っている)ので、なんだかありがたい。

「うりさんの文章はどこかに欠陥があるんだよ」と言った数日後に手渡してくれたことも忘れない。上司にしたいホクソエムの鑑だ。

思えばホクソエムは、いつも私の先にいる。統計モデリング、DB, Shiny, ウェブスクレイピング, ラムダ式... これらはホクソエムに教えてもらったと言っても過言ではない。少なくとも一度は彼の記事を参考にしている。Qiitaも未だホクソエムを超えられないでいる(今年ほんの一瞬抜いたがすぐに再び追い越された)。日本を代表するRユーザである彼を超えるのが、私の目標だ。

f:id:u_ribo:20171201234532p:plain

最後に、ホクソエム氏の盟友 ナギ=テラモさんの言葉をお借りしたい。

偉そうであるが、これからも親分を応援している。良い父であり、良いデータサイエンティストであることを願う。寿司、行っていないんでいきましょうね笑(あ、北海道でいってたw) こっぱずかしくて直面してこんなポエムは語れないので、このアドベントカレンダーがあってよかったです。

明日は R_Linux さんが書きます。ホクソエムの誓いならぬアキバの誓い、なんなんでしょう。

macOS (High Sierra)で利用しているRからタイムゾーンの警告がでる件とその対策

タイトルの通り、macOSを使っている皆さん、こんな警告を見たことはないだろうか。

lubridate::today()
# [1] "2017-11-30"
Warning message:
In as.POSIXlt.POSIXct(x) :
  unknown timezone 'zone/tz/2017c.1.0/zoneinfo/Asia/Tokyo'

なにやらタイムゾーンで怒られている。これはセッション中に日付・時間のオブジェクトを扱う処理を実行すると出る警告だが、よくわからない理由で怒られるとビビる。

これはOSとR間でのバグらしく、すでに報告が上がっている。

17355 – unknown timezone when printing (on OSX High Sierra)

で、この問題は次期バージョンとなるR 3.4.3で修正されるらしい。ちなみにR 3.4.3は今日リリースされた(というのを先ほどメーリングリストで知った)ので、バイナリも近いうちにダウンロード可能になるだろう。安心安心。

A workaround has been added for the changes in location of time-zone files in macOS 10.13 'High Sierra' and again in 10.13.1, so the default time zone is deduced correctly from the system setting when R is configured with --with-internal-tzcode (the default on macOS).

しばらくバージョンをあげる予定がなかったり、いますぐにこの警告を消したい時は、ここにある通り「裏技」を使う。

Sys.setenv("TZ" = "Asia/Tokyo")

安心安心。