dlopen(/Library/Frameworks/R.framework/Versions/4.0/Resources/library/Rcpp/libs/Rcpp.so, 6): Symbol not found: _EXTPTR_PTR

とあるパッケージをgithubからインストールしようとしたら失敗した上に、以降tidyverseパッケージをロードしようとすると以下のようなメッセージが出てエラーになる。

 

dlopen(/Library/Frameworks/R.framework/Versions/4.0/Resources/library/Rcpp/libs/Rcpp.so, 6): Symbol not found: _EXTPTR_PTR

 

何もわからない。

tidyverseやRcppをリムーブして再インストールしても直らない。

とりあえずRそのものを再インストールして無事ロードできるようになった。

何もわからない。

最近officerパッケージでハマったこと3つ

Rひとりアドベントカレンダー26日目です。

Rのofficerパッケージは便利です。

パワーポイントでレポートが簡単に作れる。

こんな感じで会社用のテンプレートを読み込んで出力なんてことも簡単にできる。

library(officer)
read_pptx("会社のテンプレート.pptx") %>%
    add_slide(layout = "表紙", master = "hoxom") %>%
    ph_with(value = "サンプルレポート", location = ph_location_type(type = "ctrTitle")) %>% 
    ph_with(value = "株式会社ホクソエム", location = ph_location_type(type = "subTitle")) %>% 
    print(my_pres, target = "結果報告.pptx")

そしてShinyと組み合わせれば、データをアップロードして、分析を自動実行し、pptx形式でダウンロードみたいな仕組みも簡単に作れる。

が、いくつか混乱するポイントもあるので対応策を列挙したい。

officerパッケージで検索して出てくる事例は大抵古い

以前はPowerPoint上のスライドにテキストを挿入したい場合はph_with_text()、ggplot2で作ったプロットを挿入したい場合はph_with_gg()など関数を使い分けていたようである。

が、最近仕様が変わり、ph_with()がS3クラスの関数として実装されたので、渡されたオブジェクトによって、よしなに関数を使い分けてくれるようになった(S3ディスパッチ)。

そしてph_with_text()等の旧関数群は次のバージョンで廃止されるらしい。

(ph_with_textのヘルプより)
Description
add text into a new shape in a slide. This function will be deprecated in favor of  ph_with in the next release.

ということで、検索して出てくる事例は旧関数群を使っていることが多いので、コピペで済ませたい人は公式サイトを参照した方が良い。

https://davidgohel.github.io/officer/articles/offcran/powerpoint.html

明らかにggplot2で作ったプロットをggplot2で作ったプロットとして認識してくれない

生存時間分析のプロットをggplot2で実施してくれるsurvminerパッケージというものがある。

例えばこのパッケージのggsurvplot()にsurvfitクラスのオブジェクトを渡すとカプランマイヤー曲線をよしなに描画してくれる。

> library(survival)
> library(survminer)
> fit <- survfit(Surv(time, status) ~ sex, data = kidney)
> ggsurvplot(fit)

f:id:dichika:20191126204603p:plain

そして、通常であればggplot2で作ったプロットをph_with()に渡すとスライド上にそのまま描画してくれるので、このggsurvplot()で作ったオブジェクトを渡してみるとエラーになる。

> res_gg <- ggsurvplot(fit)
> my_pres <- my_pres %>% 
+     add_slide(layout = "タイトルとテキストとコンテンツ", master = "hoxom") %>% 
+     ph_with(value = res_gg, location = ph_location_type(type = "body", id = 2))
UseMethod("ph_with", value) でエラー: 
   'ph_with' をクラス "c('ggsurvplot', 'ggsurv', 'list')" のオブジェクトに適用できるようなメソッドがありません

実はggplot2に対応するph_with()のS3メソッドがph_with.gg()なのに対して、ggsurvplot()が返すオブジェクトはggクラスもggplotクラスも持たない。

とりあえずの応急処置としてはggsurvplot()で作ったオブジェクトにggクラスを付与すれば良い(長期的にはofficerかsurvminerにプルリクを出すべきだとは思うが面倒なのでやってない)。

> res_gg <- ggsurvplot(fit)
> class(res_gg)
[1] "ggsurvplot" "ggsurv"     "list" 
# とりあえずggを付与する
> class(res_gg) <- c(class(res_gg), "gg")

ggplot2に限らずph_with()にオブジェクトを渡して、期待した動作をしない場合はS3ディスパッチ周りを疑ってみてほしい。

ph_location_type()で指定するidはlayout_properties()で示されるidではない

スライドにコンテンツを挿入可能なオブジェクトが複数あり、そこにテキストや図表を挿入したい場合、これらを区別して指定する必要がある。

区別にはまずtypeで指定するという方法がある。

layout_properties()でマスタを指定すると、そのマスタに含まれるオブジェクトの一覧が表示される。

例えば「表紙」という名称のオブジェクトは以下のように示される(一部のみ示す)。

# layout_properties()で取得できる情報(一部)
> layout_properties(my_pres, "表紙")
   master_name name     type id 
1          hoxom 表紙     body  5      
2          hoxom 表紙     body  6      
3          hoxom 表紙     body 22 
19         hoxom 表紙 ctrTitle  2 
20         hoxom 表紙 subTitle  3 

ここでctrTitleとsubTitleのようにオブジェクトのtypeが異なる場合、それぞれのtypeを指定すれば良い。

しかしtypeが異なる場合、idで指定する必要がある。

ここでポイントなのが、ここで指定するidはlayout_properties()で表示されるidではなく、オブジェクトの作成順に対応した1から始まる連番である。

例えば、先のマスタの場合、typeが同じオブジェクトが3つあるのでidは1〜3のどれかとなり、ここで表示されている5,6,22ではない。

# これで表示されるidを指定するとエラーになる
> layout_properties(my_pres, "表紙")
   master_name name     type id 
1          hoxom 表紙     body  5      
2          hoxom 表紙     body  6      
3          hoxom 表紙     body 22 
19         hoxom 表紙 ctrTitle  2 
20         hoxom 表紙 subTitle  3 

オブジェクトの作成順を覚えていれば問題ないが、覚えていない場合、ここは試行錯誤することになる。

私は試行錯誤した。みんなも試行錯誤してほしい。

というわけで最近のofficerでハマったことを列挙した。

とはいえかつてPowerPoint VBAをいじっていた身としてはofficerは非常に使いやすく嬉しいパッケージである。

未体験の方はぜひ使ってみてほしい。

どうかな

RでUMINの臨床試験データを取得する

Rひとりアドベントカレンダー22日目です。終盤に差し掛かってまいりました。

さて、国内の臨床試験はUMIN-CTRに登録するようになっている(特定臨床研究はjRCTだったりするが)。

そしてUMIN-CTRはスナップショットとして登録情報をCSVで公開している。

https://www.umin.ac.jp/ctr/csvdata.html

列名が2段になっているのでとりあえず1行目はスキップして2行目を列名として読み込むようにする。

library(readr)
ctr <- read_csv("https://upload.umin.ac.jp/ctr_csv/ctr_data_j.csv.gz", skip = 1)

ただし、1行目とひもづけることでユニークになる列名も多数あるようで列名が重複してるぜというメッセージが出るので注意。

ちゃんと分析に使いたい場合は自分で列名を綺麗にした方が良い。

ちなみにアメリカの場合、clinicaltrials.govという臨床試験登録サイトがある。

こちらはWeb APIを提供している。

https://clinicaltrials.gov/api/gui

APIがあれば誰かがパッケージを作ってくれているのが世の常。嬉しい。

https://github.com/sachsmc/rclinicaltrials

どうかな。

老化によるset.seed忘れを防ぎたい

Rひとりアドベントカレンダー1日目。

set.seed()は乱数の種を設定する関数ということはよく知られている。 乱数の種を設定することで、乱数に再現性がとれるようになる。 例えば以下の2行のコードはまとめて実行することで毎回同じ結果を返す。

> set.seed(1)
> rnorm(5)
[1] -0.6264538  0.1836433 -0.8356286  1.5952808  0.3295078

だがここでもう一回rnorm()を実行したらどうなるだろう。 なんとなく同じrnorm(5)の結果が得られるように思えないだろうか。 答えはブーである。

> rnorm(5)
[1] -0.8204684  0.4874291  0.7383247  0.5757814 -0.3053884

同じrnorm(5)の結果を得るにはset.seed(1)を再び実行する必要がある。

> set.seed(1)
> rnorm(5)
[1] -0.6264538  0.1836433 -0.8356286  1.5952808  0.3295078

ハハッ、こんなん当たり前っしょとか思ってる人、dplyr::sample_n()を利用する際もちゃんとset.seed()を併せて実行しているだろうか。

set.seed(1)
result <- iris %>% sample_n()

こんなの絶対にset.seed()の実行忘れる自信がある。10回中1回は絶対忘れる。俺は日々老化している。

ということで、dplyrのイシューにもsample_n()のオプションにseed入れたほうがよくね?という提案がある。却下されているが。

github.com

却下されている理由としては、汎用的なwithr::with_seed()があるからというものである。

使い方はこんな感じ。

library(withr)
> with_seed(1, iris %>% sample_n(5))
  Sepal.Length Sepal.Width Petal.Length Petal.Width    Species
1          5.1         3.4          1.5         0.2     setosa
2          5.7         2.8          4.5         1.3 versicolor
3          5.4         3.0          4.5         1.5 versicolor
4          6.3         2.8          5.1         1.5  virginica
5          4.7         3.2          1.6         0.2     setosa
> with_seed(1, iris %>% sample_n(5))
  Sepal.Length Sepal.Width Petal.Length Petal.Width    Species
1          5.1         3.4          1.5         0.2     setosa
2          5.7         2.8          4.5         1.3 versicolor
3          5.4         3.0          4.5         1.5 versicolor
4          6.3         2.8          5.1         1.5  virginica
5          4.7         3.2          1.6         0.2     setosa

確かにこうしておけば、seedとの一体感が生まれるし何より実行忘れがない。 そしてwith_preserve_seed()を使えば、いちいち指定しなくても最初にset.seed()したシードで統一できる。

library(withr)
> set.seed(1)
> with_preserve_seed(iris %>% sample_n(5))
  Sepal.Length Sepal.Width Petal.Length Petal.Width    Species
1          5.1         3.4          1.5         0.2     setosa
2          5.7         2.8          4.5         1.3 versicolor
3          5.4         3.0          4.5         1.5 versicolor
4          6.3         2.8          5.1         1.5  virginica
5          4.7         3.2          1.6         0.2     setosa
> with_preserve_seed(iris %>% sample_n(5))
  Sepal.Length Sepal.Width Petal.Length Petal.Width    Species
1          5.1         3.4          1.5         0.2     setosa
2          5.7         2.8          4.5         1.3 versicolor
3          5.4         3.0          4.5         1.5 versicolor
4          6.3         2.8          5.1         1.5  virginica
5          4.7         3.2          1.6         0.2     setosa

今後はwith_seed()そしてwith_preserve_seed()を使っていくことにする。

どうかな。

不正な入力がないだろうと思われるデータなのに不正な入力がありましたと言われた時は文字コードを疑う

read.csv()で不正な入力がありましたと言われる。 警告だけだからまあ無視するかと思いきやデータを読み込めていない。

> data <- read.csv('data/hoge.csv', as.is = TRUE, fileEncoding = "CP932")
 警告メッセージ: 
1:  read.table(file = file, header = header, sep = sep, quote = quote,: 
   入力コネクション ''data/hoge.csv' に不正な入力がありました 
2:  read.table(file = file, header = header, sep = sep, quote = quote,  で: 
  incomplete final line found by readTableHeader on 'data/hoge.csv'
> nrow(data)
[1] 0

もしやと思い文字コードUTF-8に指定してみるとうまくいく。

> data <- read.csv('data/hoge.csv', as.is = TRUE, fileEncoding = "UTF-8")
> nrow(data)
[1] 10

普段エクセルでしかデータを扱っていない方なので大体もらうデータはCP932なのだが システム部から出力してもらったデータらしくUTF-8が紛れ込んでいたというオチだった。

Rでunix時間からJSTに変更する際はas_datetime()

よくあるタイムスタンプであるUNIX時間をJST日本標準時)に変更したい。 もちろんbaseのas.POSIXctを使うという手もあるが、私はoriginを指定するのに疲れたみたい。baseを嫌いになったわけじゃない。 ということでlubridateパッケージのas_datetimeでタイムゾーンをtzに指定していくやり方に統一することにする。

library(lubridate)
as_datetime(1500000000, tz="Asia/Tokyo")

参考にしたのは以下のSOの回答。tidyverseにこだわらないならanytimeパッケージもありだ。 stackoverflow.com

タイムスタンプがUNIXマイクロ秒のこともある。 それをミリ秒表記に直したいときは例えば以下のようにやる。

as_datetime(1559000000000000/(1000^2)) %>% format("%Y-%m-%d %H:%M:%OS3")

BigqueryのJSONデータをRで読み込む際はndjsonのstream_in関数を利用する

Bigqueryで取得したデータを整形したい。

Bigqueryで全て完結するならそうしたいがとりあえず時間がないのである程度BQでやってあとはRでやることにする。

しかしネストされているのでJSONでしかダウンロードできないよと言われてしまった。

んじゃJSONでダウンロードしてjsonliteパッケージので読もうとしたら以下のようなエラーが出た。

parse_con(txt, bigint_as_char) でエラー: parse error: trailing garbage

検索すると以下のSOにたどり着く。

https://stackoverflow.com/questions/26519455/error-parsing-json-file-with-the-jsonlite-package/26522000

どうやら各行はJSONのフォーマットになっているものの各オブジェクトがJSONフォーマットに則っていないらしい。

It turns out this is a "pseudo-JSON" file. I come across these in many naive API systems I work in. Each line is valid JSON, but the individual objects aren't in a JSON array. 

ということで行単位の処理が必要となる。 jsonliteパッケージの場合、stream_in関数を用いる。fileでコネクションを作るのがポイント。

data <- jsonlite::stream_in(file(path_data))

この場合、ネストされたオブジェクトはネストされたまま読み込まれる。

ネストされたオブジェクトをよしなに展開してほしい(flatten処理)場合はndjsonパッケージのstream_in関数が良い。この場合、明示的にコネクションを作る必要はない。

data <- ndjson::stream_in(path_data)

いかがでしたか?