niszetの日記

アナログCMOS系雑用エンジニアが頑張る備忘録系日記

(R) 規模が大きくなってきたRmdは階層化して管理する (1回目)

複雑すぎるものは管理できない。管理できるレベルまでシンプルになるように分解する。

業務に関する内容でRによる可視化、ドキュメント化を進めていって、かなり短時間で綺麗に所望の出力が得られるようになったため、今まで見れなかった部分が見れるようになった等、Rの力が存分に発揮されてきました。

そして、これに関連してRmdの構造・構成、パッケージ化など色々と検討してきました。
drakeパッケージによる依存関係の管理についても書いたのですが、一旦原点に返ってknitr側だけでどこまで出来るのか?を考えます。そして、そちらで最適かなという構成が見つかったところでdrakeパッケージによる管理を加えることで、Rを使用したある程度の大規模なドキュメントの生成・管理の環境について考えてみようと思います。

タイトルにもあるように、長大で複雑になってしまったドキュメントの整備は非常に大変です。出来るは出来るんですが、苦痛。
ドキュメントの作成も当然、業務のうちではありますが、ドキュメントを作ることだけが業務ではないし、そもそも構造が複雑で全部読み解かないといけない…的な時間が多いというのはあまりうれしいものではないですよね(はやく帰って🍺を飲もう)

このあたりが動機になります。確実に、しかし手間を減らしわかりやすく、短時間で出来るようにしよう、ということです。

RMarkdown の chunk に 別のRMarkdown を child として使う

さて、ある程度RMarkdownの分量が増えてきた場合の対応策として、chunk部分を別のRMarkdownとして切り出して、子RMarkdownとして使う方法があります。

調べると日本語の記事もヒットします。🐘さんだー!

https://qiita.com/kazutan/items/72276e204944b191c5d5qiita.com

大仏様だー!

www.slideshare.net

そして、先日発売されたこちらですね。おススメ。

www.kyoritsu-pub.co.jp

あ、これ話それる奴だ。これはまたどこかで書きます。子RMarkdownの話を続けます。

子RMarkdownを使うメリットは、分割して小さな単位で管理できることですね。以前、外部の.Rファイルからチャンクを抜き出して使用する、などをしていましたが、どのチャンクがどのファイルにあったのかがわからなくなり、破たんしました。その時は処理部分を全てパッケージ化し、RMarkdownのchunkでそれを呼び出す形を取りました。

この辺りの顛末は末尾のリンクにまとめてあります。

これで、複雑な構造をそれぞれのRmdに分解したことで管理が容易になってめでたしめでたし…なのですが

変数のグローバル汚染問題

この子Rmdを使用する場合でも、ちょっとした問題があります。それは、親のRMarkdownと子のRMarkdownは同じ環境にいます。
どういうことかというと、親RMarkdownに

\```{r}
print("親で代入")
a <- 1
\```

\```{r child="child.Rmd"}
\```

\```{r}
print("親で表示")
a
\```

というチャンクたちを作り、

子RMarkdown(child.Rmdというファイル名で作成)に

\```{r}
print("childで代入")
a <- 10
\```

というチャンクを作り、親側でknitを実行すると、

f:id:niszet:20180514231335p:plain

のように表示されます(チャンクオプションを何も指定していないデフォルト動作の場合)

ということで、親RMarkdownと子RMarkdownの環境は地続きになっていて、子の方で修正や削除をすると、親側の動作に影響があることになります。これは子RMarkdownを挿入する位置によって(つまり、親RMarkdownのchunkの場所によって)動作が異なる原因となり、避けたいところです。

しかし、チャンクオプションとしてchildを渡した際にはこれをどうにかする方法はなさそうです1。 しかし、このchildチャンクオプションに対応した関数が存在し、そちらでは環境を渡すことによって親RMarkdownと子RMarkdownの環境を分離することが出来ます。

実際にやってみます。親側だけ下記のように修正します。

\```{r}
# 親で代入
a <- 1
envir <- new.env()
envir$a <- a
\```

\```{r}
# 子に環境を渡す
knitr::asis_output(knitr::knit_child("child.Rmd", envir=envir, quiet=TRUE))
\```

\```{r}
# 親で表示
a
\```

で、実行結果はこんな感じ。

f:id:niszet:20180514235359p:plain

これで、子環境で変数をうっかり修正してしまっても、親側では影響をうけなくなりました。めでたしめでたし。

追記

これについて、なんと@kohskeさんからご指摘をいただきました。

と、上記のコードでは明示的に変数を子Rmdの環境に与えていますが、子Rmdの環境の親環境が呼び出し元の親Rmdの環境なので、Rは指定された変数が自分の環境に存在しない場合は親の環境を探しに行くので、この場合は親Rmdの環境を探しに行って、そこで見つけることになります。よって、明示的に環境中に変数を与えなくても子Rmdから変数にアクセス可能、ということになります。

これを回避するには、ということで

のように、呼び出し元の親Rmdの環境を飛び越えるようにするために、new.env()parent引数(親となる環境を与えるための引数)にparent.frame()と、呼び出し側である「親Rmdの親」の環境を与えることで、変数を探索する際に親Rmdを飛び越えて探すようになります。階層が深くなったり、ライブラリの読み込み順に起因する環境ツリーの階層依存などは回避できませんが…

後者の、ライブラリ読み込み順などはAtsushiさんのコメント、

のように、共通のsetupチャンクを使用する、ということで回避できます。この辺りを階層Rmdで本気でやるには既に読まれていたら読まないようにする、などの処理が入って煩雑にはなっていきますが…。実際に、自分は既にsetupは別のsetup.Rmdの形にして分離してあります。最終的には一番上のRmdは構造だけを持っていて、データは(文章であっても)持っていてほしくないんですが、そこまでたどりつけていない…。

さらにさらに、🐘さんからも、

と、bookdownのことを教えていただきました。章ごとに管理する、などの際にbookdownでは環境をうまく使い分けているようなのですが、まだ実際に使っていないので感覚がわからず。薄い本作りたいし、bookdownもやっていこう💪

次回以降

さて、これだけだとあまり恩恵を感じないかもしれません。というか、コードの説明もあまりしていませんしね。
現在進行形で階層RMarkdownの効率的な作成方法については色々実験中なので、「これはもう硬いかな…」という部分は不定期に放流していこうかなと思います。気が向いたときにね…。

過去記事

過去、処理部分をパッケージにして分離するとか、chunkを他から読んでくるとかしていました。
このあたり、どれがベストかは規模とか、Rmdで進めてきたかRのコードで進めてきたのか、色々状況によると思います。
ただ、規模が大きくなってきたら単一のRmdで管理するのは結構きついです。きついですが、RStudio IDEの支援機能の、「未定義の変数を使用した」などのチェックはひっかけやすくなるので、下手な分離方法をとるくらいならば1つにまとめた方が良いケースもあるかと。これも人依存かもしれません(私は上にも書いたように、適当にチャンクを分離したりしたのでファイルが見つからなくなったりしましたねぇ…)

niszet.hatenablog.com

niszet.hatenablog.com

niszet.hatenablog.com

niszet.hatenablog.com

niszet.hatenablog.com

頑張っていきましょう。

Enjoy!!


  1. あったら教えてください。