備忘ログ

チラシの裏的備忘録&メモ

CRANにsubmitしたあと公開(またはreject)されるまでのパッケージはどこにあるか

CRANにパッケージをsubmit(投稿)したあと、基本的な順序としては、自動チェック後、CRAN Maintainerにより手動チェックされ、問題なければCRAN上で公開、CRAN Policyに合致しないなど問題があればrejectされた理由がメールされる。

パッケージのsubmit後は自動チェック後、公式には10日(営業日)以内にCRAN登録可否等ののメールがくることとなっている(自動チェック後の自動送信メールに載ってる)。

公式では10日以内とされているが、Hadley氏いわくだいたい5日以内にメールがあるとされているが、個人的な体感としては(そんなに多くのパッケージを投稿した経験はないが)概ね3日以内にはメールが来ることが多い気がしている。

ところで、CRANにパッケージを投稿後、今どのような状態(ステータス)にあるのか気になることときがある。

R パッケージを CRAN で公開する - StatsFragmentsに日本語情報としてあるが、

公開待ちのパッケージは 以下の FTP サーバにて確認できる。

ftp://cran.r-project.org/incoming/

パッケージによっては 拡張子の後に以下のようなステータス情報が付けられる場合がある。

と現在のステータス情報を確認することができる。また、httpsでも状態を見ることができるCRANページもある。

cran.r-project.org

httpsだとウェブブラウザから参照できるので見やすい。

さらに、r-hubがGitHub Actionを使って1時間に1回ftpサーバーをクロールして現在CRANに投稿中にパッケージのステータスをダッシュボードで公開してくれているページもあり、パッケージとステータス状態はこちらの方が見やすい。

r-hub.github.io

CRANに投稿中のパッケージのステータスの詳細については上記のページにあり、

  • inspect: your package is awaiting manual inspection by the CRAN team, probably because automated tests found a problem that is likely to be a false positive
  • newbies: a specific queue for the manual inspection of first-time CRAN submissions.
  • pending: a CRAN team member has to do a closer inspection and needs more time.
  • human: your package has been assigned to a specific CRAN member (with initials as indicated by subfolder) for further inspection.
  • waiting: the CRAN team is waiting for an answer from you, e.g. because issues are present that CRAN cannot automatically check for, such as maintainer changes (check your e-mail …)
  • pretest: the CRAN maintainers restarted automated tests on your package to see whether an issue has been fixed by your action or is still present.
  • archive: your package has been rejected from CRAN because it did not pass checks cleanly and the problems were not likely to be false positives.
  • recheck: your package has passed basic checks. Because other CRAN packages depend on yours (“reverse dependencies”), a reverse-dependency-checking step is underway to see if your update has broken anything downstream.
  • publish: you’re all set! Your package has passed the review process and will soon be available on CRAN.

となっているよう(抄訳しようかと思ったけど機械翻訳と対して変わらないからやめた)。

要するにnewbiesであるうちは手動チェックが未の状態で、それ以外のステータスになったらCRAN Maintainerのチェックが始まった(または終わった)と思って良さそう。

RStudioのCheck for Package Updatesはupdate.packages()と微妙に挙動が異なる ほか2本

R関係で最近知った些末なことをメモしておく。一つずつ書くほどでもないので三本建て。

RStudioのTools > Check for Package Updatesはupdate.packages()と微妙に挙動が異なる

RStudioを使っていてRのパッケージに更新があるか確認しようと思ったときにRStudioの提供する機能を使って、上のメニューのTools > Check for Package Updatesでチェックするか、PackagesのペインにあるUpdateのボタンをクリックしてチェックすることができる。

RStudioのCheck for Package Updateはold.pakages()で表示できる今インストールされている更新があるパッケージとインストール中のバージョン番号と新バージョンのバージョン番号を比較表示してくれるだけでなく、CRANのwebページ上でパッケージのNEWSが見られる場合はNEWSをクリックするとそのページに飛ぶことができ更新情報をすぐに確認できるというのがとてもいいと個人的には感じている。

これは勝手に、update.packages()GUI版(ラッパー的なもの)なのかな、などと思っていたが微妙にそうではなく、RStudioのTools > Check for Package Updatesで更新の有無の確認対象となるパッケージは一般ユーザー(管理者権限じゃないという意味)でRStudioを起動するとUser Libraryにインストールしているパッケージだけで、System Libraryは範囲外となる様子。管理者としてRStudioを実行(スーパユーザーとして実行)するとSystem Libraryも確認対象となる模様で、その時のファイルパーミッションで更新できるパッケージのみが更新の有無の確認対象となる様子。

一方でupdate.packages()は特に指定しないと、一般ユーザーで実行しても.libPaths()で検索できるパッケージがインストールされているディレクトリすべて、つまりUser Libraryに加えて、System Libraryもアップデートの対象になる。ただし、一般ユーザーではファイルパーミッションの関係でSystem Libraryのパッケージは更新できずエラーになるので、結局System Libraryのパッケージを更新する場合には管理者として実行(スーパユーザーとして実行)することが必要になる。

結局System Libraryのパッケージを更新するすためには管理者権限が必要になるのは双方変わりないが、RStudioのTools > Check for Package UpdatesではそもそもSystem Libraryにインストールされているパッケージにアップデートがあるかどうかすら確認されないので、普段一般ユーザーとしてRStudioを使っていてTools > Check for Package Updatesだけでパッケージのアップデートを行っている場合はSystem Libraryにインストールされているパッケージにアップデートがあるのかどうかもわからない。

この差はR本体をリリースごとにアップデートして使っている範囲ではほとんど気にする機会はなくR本体のアップデートのタイミングでSystem Libraryのパッケージもアップデートされるので多くの場合はそのタイミングのアップデートで問題ないが、極稀にR本体のリリース間でSystem Libraryにインストールされているパッケージがアップデートされていないためにトラブルが起こるということを経験するとちょっと嵌まる。

たとえばこの間経験した、{lme4}など。

indenkun.hatenablog.com

これは{Matrix}が1.6-2以前と以降でバイナリ互換性がないためのよう。

stackoverflow.com

現状としては、普段一般ユーザー(管理者権限じゃないという意味)としてRStudioを使用している場合はこの問題に嵌りたくないのであれば、たまにold.packages()で、System Libraryを含めたパッケージの更新の有無を確認するしてSystem Libraryのパッケージのアップデートがあれば管理者権限でRやRStudioを起動してパッケージをアップデートするしかないかもしれない。

または管理者権限でRStudioを起動するスタイルにするというのも手だが、個人的には管理者権限でなくても基本的には問題なく動くソフトウェアを常に管理者権限で動かすのは運用上自分の好みではないので微妙である。

この件については実環境的には、User Libraryを指定しているディレクトリのパーミッションなど個別の環境依存の問題があり、それぞれで多少事情は異なるとは思う。

::で関数を呼び出してもパッケージはロードされている

今まで勝手に、library()を使わずに::をつかってパッケージの関数を呼び出したときには、パッケージの::で呼び出した関数をそのタイミングだけ読んで、あとはさっと捨てているのかと思いこんでいた(呼び出したタイミングだけふわっとでてきて、終わったらふわっと消えていくイメージ)。

しかし、挙動をよく見ると::でパッケージの一部関数を呼び出すとパッケージがロードされている模様。

ドキュメントにも次のようにちゃんと書いている。

For a package pkg, pkg::name returns the value of the exported variable name in namespace pkg, whereas pkg:::name returns the value of the internal variable name. The package namespace will be loaded if it was not loaded before the call, but the package will not be attached to the search path.

つまり検索パスにattachされないだけで、ちゃんとロードされているとのこと。

Hadley WickhamのR Packagesにも

Loading will load code, data, and any DLLs; register S3 and S4 methods; and run the .onLoad() function. After loading, the package is available in memory, but because it’s not in the search path, you won’t be able to access its components without using ::. Confusingly, :: will also load a package automatically if it isn’t already loaded.

ってちゃんと書いてる。

r-pkgs.org

ロードされているから::で読み込んだパッケージのS3、S4クラスのジェネリック関数のメソッドも登録される。

たとえば、{ggplot2}::で読み込む前は、

methods(`+`)
## [1] +.Date   +.POSIXt
## see '?methods' for accessing help and source code

となるが、適当に1回でも{ggplot2}::で読み込んだだけで+.ggなどの{ggplot2}などで定義されるジェネリック関数のメソッドが登録されている。

ggplot2::qplot(Sepal.Length, Sepal.Width, data = iris)
## Warning: `qplot()` was deprecated in ggplot2 3.4.0.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.

methods(`+`)
## [1] +.Date        +.gg*         +.glue*       +.POSIXt      +.vctrs_vctr*
## see '?methods' for accessing help and source code

たしかに、::でパッケージを呼び出した関数しか使えなかったらそもそも、qplot()でつくったggplotオブジェクトを描画するplot()の適当なメソッドがなくうまい具合に表示されないことになってしまう。

{purrr}パッケージのmap_df()系がsupersededになった理由について最近知った

{purrr}パッケージが今年頭辺りに1.0.0になったときにmap_df()系の出力がデータフレームにできるmapファミリー関数がsupresededになった。

データフレームを直接出力してくれるので便利であったがsupresededになって、代替案として新たな関数が用意されるのではなくmap()で出したリストをlist_rbind()などで結合すべしとのことで手間が一つ増えた印象だった。

ドキュメントには

The functions were superseded in purrr 1.0.0 because their names suggest they work like ⁠_lgl()⁠, ⁠_int()⁠, etc which require length 1 outputs, but actually they return results of any size because the results are combined without any size checks.

と書いてあるが、いまいちよく分からなかった。

そんなふうに思っていたらPosit社のYoutubeチャンネルにあるHadley氏の{purrr}について話している動画がRecomendされて来たので見る機会があった。

www.youtube.com

だたい11分10秒くらいからmap_df()系の話をしているのだけど、要約すると「『map系関数は入力した値の長さと同じ長さの値を返すべき』という設計に基づくとmapとして不適切だろ、というわけでsupresededな」ということのよう。

統一感とか設計思想って大事だよね、と思った。

Rのlme4パッケージで"関数 'chm_factor_ldetL2' はパッケージ 'Matrix' では提供されていません"とエラーが出た

1ヶ月ぶりくらいにlme4パッケージをつかって(一般化)線形混合モデルを計算しようと思ったら関数 'chm_factor_ldetL2' はパッケージ 'Matrix' では提供されていませんとエラーが出たので解決させたときの対応をメモしておく。

英語だと、Error in initializePtr() : function 'cholmod_factor_ldetL2' not provided by package 'Matrix'というメッセージになる。

library(lme4)
## 要求されたパッケージ Matrix をロード中です
library(lattice)
gm1 <- glmer(cbind(incidence, size - incidence) ~ period + (1 | herd), data = cbpp, family = binomial)
## initializePtr() でエラー: 
##  関数 'chm_factor_ldetL2' はパッケージ 'Matrix' では提供されていません 

少し前に実行したときには特に問題なく実行できていたのに、なんじゃらほい、と思った。glmer()だけでなくlmer()でも同じエラーが出た。

だいたいこういうエラーは誰かが同じように踏んでいるのでエラーメッセージ検索をかけると、英語のメッセージで検索するとすぐに

github.com

github.com

微妙にエラーメッセージが異なるが似た内容のエラー現象にいきあたる。

要するに「お前の使ってるMatrixパッケージが古いんじゃね?」的な話の様子。

いや、定期的に、RStudoのTools > Check for package update実行してますけど、と思ったけど言われた通り、

install.packages("Matrix")
install.packages("lme4")

とインストールし直してみたら解決した。

11月ころに出始めた症状のようで、たしかにここ1ヶ月位lme4パッケージ使ってなかったな、と思った。

追記

RStudioをRoot権限のない一般ユーザーとして起動してTools > Check for package updateでパッケージをアップデートしてもSystem Libraryのパッケージはアップデートの対象にならないだけでなく、アップデートの有無すら確認されないことがわかった。

indenkun.hatenablog.com

たぶんこの問題を踏んでいる人の多くはRStudioを一般ユーザーで起動しTools > Check for package updateをつかってパッケージのアップデートをしているのだと思う。

CRANからremoveされた他の人の作ったパッケージを再登録させた話(ポエム)

この記事はR言語 Advent Calendar 2023の2日目の記事です。Rに少しでも関連していれば誰でもなんでもOKとのことで、本稿はその趣旨を信じて次に記載する本稿の要旨の通り、R言語のHOW TOやトリビア的な内容ではありません(お祭りに参加したいけど手持ちネタがない人です)。基本的にはポエム的な内容なので敬体で記述していきます。

qiita.com

本稿の要旨

CRANからremoveされた{mvnmle}パッケージを開発者である元のメンテナーの許諾を得てCRANへの再登録を行ったので、その経緯と経過、この経験から感じたことについて書いています。

Rで使いたい(使っていた)パッケージがCRANからremoveされてしまったらどうする(まえがきのまえがき)

R言語でさまざまなデータや統計処理などを行うときには、追加でパッケージをインストールし読み込み利用することが多いと思います。特に、「◯◯という処理をするにはどうしたらいいだろう」と書籍やwebページにあたり、その中に記載されているパッケージをインストールしようと考える場面はそうした中ではしばしばあると思います。

このとき、やや古めの書籍やwebページの場合には紹介されているパッケージがCRAN(The Comprehensive R Archive Network)からremove*1されており、install.packages()を用いてCRANからインストールしようとすると、

##  Error: package '***' is not available

などエラーメッセージがでてきてしまいインストールできないという場面に遭遇することがあります。

GitHubなどの他のリポジトリで公開が継続されている場合にはそちらからインストールするという方法を取ることができますが、CRANにしか公開されていないパッケージの場合の解決策としては

  • CRANからremoveされたパッケージをArchiveからインストールする。

  • 同じような機能を有する現在CRANに登録されている(あるいはGitHubなどに公開されている)他のパッケージを探し、インストールする。

という方法*2が考えられます。

CRANからremoveされたパッケージをArchiveからインストールする方法としては、Posit社のInstalling older versions of packagesという記事にも書いているとおり、{remotes}パッケージ*3install_version()関数を用いてインストールする方法*4と、Archiveされたソースパッケージ(.tar.gz)をCRAN(Index of /src/contrib/Archive)からダウンロードしてソースパッケージをインストールする要領でinstall.package()を使ってインストールする方法があります(ソースパッケージをダウンロードする手間の問題くらいの差)。

また手間的にはそんなに変わりませんが、CRANの非公式のパッケージのミラーであるcran · GitHubから{remotes}install_github()をつかってインストールする、という方法もあります。

ただし、依存関係にあるパッケージもCRANからremoveされている場合には先にremoveされている依存関係にあるパッケージをインストール必要があったり、新しいRに対応しておらずうまく挙動しない可能性があるなど、Posit社の記事にもあるとおり潜在的な問題を有する可能性もあります。

また、CRANからremoveされたパッケージの代替案を探すという方法をとることができれば長期的な視座からはメンテナンスがなされているパッケージを使用できるということでとても良いのですが、そもそも代替案のパッケージを探すのに手間がかかる場合があることや、手法の勉強中などにとりあえず書籍やwebページの記載どおりに出力結果を得たい場合(例えば書籍に一行ずつの処理のコードが書いてあり、ライブラリを使った結果と一緒だよね、みたいなのを示してくる場合*5など)には全く同じ出力結果が欲しくなりますが、そういうパッケージが見つかるとは限らないというのが、パッケージの代替案を探すという手法の大きなデメリットです。

CRANからremoveされたパッケージの作者に連絡をとり再登録を図るという第三の選択肢について(はじめに)

CRANからremoveされたパッケージを利用したい場合に、

  • Archiveされたパッケージを利用する。

  • 代替となるパッケージを探す。

という2つの選択肢(再実装を加えると3つの選択肢)について上記で記載しましたが、第三の選択肢として、CRANからremoveされたパッケージの作者に連絡をとり再登録を図るという方法について本稿では書いていきます。

ところで、CRANからパッケージがremoveされるのは、パッケージ作者(メンテナー)自らが取り下げを行うこともできるようです*6が、多くの場合はCRANが定期的に行っているパッケージのR CMD checkの結果ERRORとなり、更新がなされない場合にCRANからremoveされArchiveとなります*7。これは、特にR本体のマイナーバージョン以上のアップデートでしばしば起こるとのことで*8、コードそのものよりもRのPolicyや細かい挙動の変更に伴って弾かれてしまっている例があるのではないかと思っています。その証左として、removeされたパッケージもArchiveからインストールして以前の通り利用できる(場合もある)のだと思います。

パッケージメンテナー自らがパッケージの取り下げを行った場合には何らかの複雑な事情を抱えている可能性があり、再登録は困難であると思いますが、それ以外の場合には上記のようにR CMD checkERRORさえ回避できれば再登録できそうです。

ここで選択肢としてもう一つ、CRANに登録されているパッケージは基本的にオープンソースソフトウェアライセンスなので、

  • removeされたパッケージをフォークして適切な修正を加えた上で新しいパッケージとしてCRANに登録し、利用する*9

という方法を取ることができます。

そもそも、

  • removeされたパッケージがR CMD checkERRORが解消されCRAN再登録され、利用する。

と幸せになれる気がします。これは、既存の資産としてそのパッケージをほぼそのまま利用できるだけでなく、そのパッケージを使って今まで処理していたものがそのままパッケージ名、関数名を変えずに今までの通りのコードを欠くことができ、今後も同様にコードを書くことができるというメリットがあります。

このメリットはCRANからremoveされたパッケージをフォークし再登録した場合には享受することができません。なぜなら、フォークして再登録してもCRANのPolicy上、現在と過去にCRAN(とBioconductor)に登録されたパッケージと同じ名前のパッケージを登録することができないという制約上*10、パッケージ名は必ず変更となるため既存のそのパッケージを使ったコードの少なくともパッケージ名は修正する必要があるためです。

また、CRANからremoveされたパッケージを再登録した場合、再登録した個人のメリットというよりRを利用している全体の利益として、書籍やwebページ等で紹介されているが現在はCRAN上からremoveされたパッケージが(いつの間にか)同じパッケージ名で再登録されれば、「あれ、このパッケージを使いたいのにCRANから削除されてる」などと思って困る人が一人でも減り、余計な手間(Archiveからインストールしたりするなど)が減り幸せになる人が増えるのではないかと思います。

今回、CRANからremoveされたパッケージで元のパッケージメンテナーに連絡をとり、CRANに再登録したという経験をしたため、あまりそういう記事を少なくとも邦文で読んだことがないので記事にしておこうと思います。

経緯と経過

欠測値の処理について考えていたときに、{mvnmle}パッケージのmlest()という関数があることを知りました*11

このパッケージを最初に使うにあたって、CRANからパッケージがremoveされていたので前述した手法を用いてArchiveからインストールして使用しており、個人的には特段の問題はありませんでしたが、いろいろ調べていると{mvnmle}がCRANからremoveされてインストールできなくて困っている的な話を見かける機会がありました(が、当該文書を見つけることができなかったので口頭で聞いたのかもしれません)。

また、今年のはじめ頃にr - Orphaned package etiquette - Stack Overflowという記事をみて、ちょうど前の作業が一段落ついて手が空いたこともあって、「そいうえば{mvnmle}もORPHANEDだった」と思い出して、なんとなく{mvnmle}の周辺事情を調べてみました。

ちなみに、r - Orphaned package etiquette - Stack Overflowの内容は、自分が使っているパッケージのメンテナーがORPHANEDになってしまったけど、将来メンテナンス不足でArchivesになるのは惜しいからメンテナー引き継ぎたいけどどうしたらいい?という質問内容で、とりあえず元のメンテナーにコンタクト取ったらいいんじゃない。という回答内容でした。

{mvnmle}パッケージの生まれはとても古くArchive*12を見ると最も古い版が2001年のもので、20年以上も前に作成されたことがわかりました。そしてremoveされた当時の0.1-11.1バージョンではメンテナーがORPHANEDとなっており、元のメンテナーがメンテナンス指定ない状態でした。ArchiveのDESCRIPTIONを辿っていくと2011年の0.1-10まではパッケージの開発者であるKevin Grossがメンテナーですが、2012年の0.1-11からメンテナーがORPHANEDとなっていました。

ORPHANEDとなった理由は、「メンテナーにR 2.14.0に対応するように依頼したが返事がなかったから」とDESCRIPTIONに書いてありました。そこからメンテナーがORPHANEDとなった0.1-11バージョンと、その次の2018年の0.1-11.1バージョンとで0.1-10バージョンから(おそらくCRANの中の人の手によって)軽微な修正が加えられて、最終的に2020年にR CMD checkの結果ERRORとなりCRANからremoveされていました。

R CMD checkERRORとなった理由は、当時の{mvnmle}パッケージのCRANのページにcheck results archiveがあり、正確な文言は忘れてしまったけれどRcpp::compileAttributes() Error - Stack Overflowと同じような内容で、多変量正規分布最尤推定を行う計算自体のコードには問題はなく、CRANへの対応は比較的に簡単に対応できそうだと考えました。

そこで、r - Orphaned package etiquette - Stack Overflowに記載されている通り、元のメンテナーに「{mvnmle}のメンテナーがORPHANEDになりその後、現在CRANからremoveされていますが、再投稿する予定ありますか?もし再投稿するのであれば、removeされた原因解決のお手伝いできるかと思います。その気がなければ、もし気を悪くしなければでいいのですが、私の方でremoveされた原因を解決して再投稿しても良いですか?」(要旨)とメールしました。

だいぶ長いことメンテナンスしてない初版が20年以上前のパッケージの件だから返信ないかもな、と思っていたら数日の間をあけて返信があり、「私の方では再投稿する予定はないから、もし再投稿したいのであればいいよ。」(要旨)と返事がありました。

そこで、CRANからremoveされたパッケージのソースファイル(tar.gzファイル)をダウンロードし、R CMD checkERRORとならないような最低限の処置を施し、その他は元のコードのまま(もちろん、R-hub package builderでもERRORが出ないことを確認し)CRANに再投稿してみました。CRANにパッケージを再投稿を行う手順については、新規にパッケージを投稿する手順と同じように行いました*13

そうしたところ、CRANからドキュメントの文献リストの文献はDOIやISBNをつけてくださいなど複数の指摘を受けました。

うけた指摘自体はCRANのRepository Policyに記載されている*14とおりなのですが、以前から存在するパッケージ、例えば{GPArotation}などは現在でもDOIやISBNが記載されていない文献リストが記載されており、過去のPolicyに適合し登録されたパッケージのアップデートなどではその点についてはreject理由とされていないようですが、一旦removeとなったパッケージについてはそうとは言われていませんが新規投稿と同じように現在のCRANのRepository Policyに合致しているかチェックされるようでした*15

そこで、コードそのものは書き換えないようにしつつ、それなりにいろいろな点をモディファイして再投稿を図りました。

その結果、挙動は変わらないままCRANに受理され、再登録されることとなりました。

めでたしめでたし。

ORPHANEDについて

今回、この記事を書くに当たり、RのパッケージのORPHANEDについて少し調べたところORPHANEDになったパッケージを誰かが引き継いでいる例はいくつか見つけることができました。

たとえば、先に上げたr - Orphaned package etiquette - Stack Overflowで話題にされていた{sapa}もそうです。また、先に脚注で少し触れた{nrom}も一度ORPHANEDとなって現在別のメンテナーによりメンテナンスされているようです。ほかにもさまざさまざまなパッケージメンテナーがORPHANEDとなり、他のメンテナーに引き継がれたものがあるようです。

ORPHANEDになる経緯は元のメンテナーが時間がなくなったり興味がなくなったりしたなどの経緯から自主的にORPHANEDにする場合と、CRANからの連絡にメンテナーからの返事がなかった場合になるようです*16。また、パッケージメンテナーがORPHANEDになったパッケージのメンテナーの引き継ぎはCRANとしては歓迎されているようです。

ORPHANEDになった(なる)経緯はわかったのですが、ORPHANEDになる条件は上記以上はわからなかったです。ORPHANEDになるとき(またその後も)に、当時のRのPackage Policyに適合する処置が(おそらくCRANによって)なされていますが、現在のCRANの運用的には再投稿が必要な修正がある場合にはパッケージメンテナーに連絡が行き軽微な修正であっても修正・再投稿がなされない場合にはCRANからremoveされるという運用になっているはずです*17

2. 石田基広徳島大学教授との対談 | R Radio for the Rest of us.石田基広氏が昔のCRANのパッケージ登録について比較的ファジーな運用がなされていたと話しており*18、私が確認できたORPHANEDになったパッケージが比較的古いパッケージが多いことから、想像の域は出ませんがパッケージメンテナーがORPHANEDになるパッケージの存在はCRANの運用が比較的牧歌的な時代の産物なのかもしれないと思っています。

もしこの点についてご存じの方がいたらお教えいただければ嬉しいです。

メンテナーを引き続くことについて

Posit社の社員が開発しているパッケージなどのごく一部のパッケージを除けば、CRANに登録されているパッケージの多くは基本的にボランティアが開発、メンテナンスしています。そのためさまざまな事情でCRANのPolicyに適合できずremoveされてしまうのはやむを得ません。これはパッケージメンテナーをORPHANEDから引き継いだ場合も例外ではなく、{sapa}はORPHANEDから次のメンテナーに引き継がれたあと、現在、CRANから昨年removeされています。

{mvnmle}パッケージの元のメンテナーからのメールにもあったとおり興味が変わったり、CRANのORPHANEDの文書にもある通り忙しくなったなどのさまざまな理由からパッケージをメンテナンスすることができなくなる可能性はありえます。なので、普段使っているパッケージがRのPolicy変更にともない突然にCRANからremoveされてしまう可能性は大いにありえます。

現状、パッケージを引き継ぐためにはORPHANEDとなっていないパッケージはCRANに元のメンテナーからメールしてもらう必要があります*19。なので、今回はパッケージメンテナーがORPHANEDとなっていたことと、元のメンテナーが親切にも返事をくれたことは大変幸いでしたが、CRANからremoveされてもパッケージメンテナーの引き継ぎに興味がなかったり、忙しかったりなどのさまざまな理由により返信をされない可能性もおおいにあると思います。ただし、元のメンテナーと連絡がつかないなど、場合にはメンテナーを元のメンテナーの了承を得ずにメンテナーを引き継ぐこともできるようです*20が、引き継げないパッケージは無理せずに他のパッケージを探すかCRANに登録されているパッケージは基本的にはオープンソースソフトウェアなのでフォークして使っていく(場合によってはCRANへの登録をはかっていく)*21のがいいのかもしれないと思いました。

Rのパッケージのメンテナーの引き継ぎについては、Rパッケージを査読し支援するコミュニティーであるrOpenSciはパッケージメンテナーをやめたい人とパッケージメンテナーを引き継ぎたい人をつなぐ活動も行っています*22。そして、実際にその成果も上がっています*23。ただし、科学的な解析解析に使われるパッケージのみがrOpenSciの守備範囲なのでそれ以外の膨大なパッケージは対象外となります*24

なんでこんなことを思っているかというと、今後、自分自身がパッケージをメンテナンスできなくなるときがきたときに、もし自分で意図してメンテナンスをやめてCRANからremoveした場合を除いては、「もし、CRAN Policyに適合できず、CRANからパッケージがremoveされた場合またされそうな場合は、それは私の望んだ結果ではなく誰でもパッケージメンテナーを引き継いでCRANへの再登録を図っていただいてかまいません。」などと宣言しておいて誰かが勝手に引き継ぎたかったら引き継いでもらえるような感じ(そういう制度があれば)にしておけたら、人間ですのでいつ何があるかわからないのでパッケージがある日突然使えなくなって困る人が減るのではないかと考えたためです。

ところで、パッケージメンテナーは、元のメンテナーからパッケージを引き継いただ場合、新たなRの仕様変更に追随しパッケージが動作するようなコードの修正や追加は期待されていますし、当然の役割と思います。ここで私が問題になると思うのは、メンテナーはCRAN側としてはパッケージに含まれるコードの変更や追加の全権を有して(パッケージ側の開発体制・プロセスのいかんはここでは問題とはしません)いますが、元メンテナーからメンテナーを引き継いだパッケージでも保守以上の部分、つまり新メンテナーが従来の挙動を変更するようなコード変更や機能追加は期待(許容)されるかのかという点です。

アクティブに開発されていたパッケージを、元メンテナーから引き継いだ場合などは元の開発方針を引き継いでアクティブに開発していけば良いと思いますし、メンテナー変わったから今後はコードの保全しかしないと宣言して動態保存のような保守管理に徹するという方針を取るのもよいと思います。ただし、アクティブに開発されていなかったパッケージのメンテナーを引き継いだ場合、どうするかというのはアクティブに開発されていたパッケージとは少し事情がかわると思います。前述したように引き継いだパッケージメンテナーがコードを修正・変更・追加しCRANに登録する権限を有しているので、従来の挙動を変更しCRANに登録することは可能です。可能な一方で、それは期待される役割なのだろうかという問題です。

まさに{mvnmle}がこの、アクティブに開発されていなかったパッケージに当たるのですか、この点をどうするかというのは、私個人の考えとしては基本的にはR上でパッケージの関数が動作するための保守に徹したほうがよいと思っています。アクティブに開発されていないパッケージでも、見逃されていた重大な計算上の誤り等があれば修正する必要があるかもしれませんが、従来から出ていたメッセージを制御したりするのは個人的には望まれておらず、従来どおりの動作をすることのほうが重要ではないかと思っています。ただし、これは個人的な見解でそれぞれの意見があるかもしれません。ただ、個人的な見解としては、アクティブに開発されていなかったパッケージに手を入れたいのであればメンテナーを引き継いでアクティブに開発し始めるよりも、フォークして新しいパッケージとして開発を進めたほうが良いのではないかと思っています。

CRANへの再登録後の経過

パッケージ登録の手続きなどは通常のCRANのパッケージ登録(再登録)の手順と同じなので、CRANにパッケージ登録をしたことがあれば(込み入った修正が必要な場合を除いて)それほど難渋することはないかと思いました。ただ、一度、CRANからremoveされ1年以上経過したパッケージを再登録して、誰が使うんだろうというところはもちろんあるかと思います。

{mvnmle}については再登録されたことについては再投稿、{cranlogs}パッケージのcran_downloads()をつかってパッケージのダウンロード数をみるとおおよそ平均16件/日程度ダウンロードされているようです*25

そのため、世界の誰かの一助になっているのではないかと思っています。

その他雑感

CRANからremoveされたパッケージを再登録したわけですが、やや「寝た子を起こしてしまった」感を感じています。平均16件/日のダウンロードがあるということは割りと需要がどこかしらかにあったというわけですが、需要があるなら再開発されてもよかったかはず。そうではなかったということは……。と考えてしまいます。

*1:removeについて、本稿ではそのままremoveと記載していきます。削除とするとCRAN上から抹消された印象を受けますが、Archivesとしてソースパッケージ(tar.gz)は残っており削除されたというよりも表舞台から除かれたという印象を個人的には抱いているからです。表舞台から除かれたという表現についてはCRANから消えた"Archived R package"をインストールしたい - Yoshi Nishikawa Blogでみて穏健な言い回しだなと感じ使っていますが、一般的かどうかはわかりません。

*2:もちろん、どういう処理をしたいのかわかっているのであれば自分で再実装するという方法ももちろんありえます。再実装した関数を含むパッケージをCRANに登録し周知することで誰かの助けになる可能性は大いにあると思います。

*3:記事中は{devtools}となっているが、本体は{remotes}のinstall_version()関数を呼び出しているだけ。

*4:上記の記事中は記載がありませんが、{remotes}のinstall_url()でArchiveのURLを直接しても変わらないです 。 r - Install the package that has been removed from the CRAN repository easily - Stack Overflow

*5:Jonathon D. Brown(2018). Advanced Statistics for the Behavioral Sciences A Computational Approach with R. Springer Cham. 354 - 356. https://doi.org/10.1007/978-3-319-93549-2

*6:Packages will not normally be removed from CRAN: however, they may be archived, including at the maintainer’s request. CRAN Repository Policy

*7:Packages for which R CMD check gives an ‘ERROR’ when a new R x.y.0 version is released will be archived (or in exceptional circumstances updated by the CRAN team) unless the maintainer has set a firm deadline for an upcoming update (and keeps to it). CRAN Repository Policy

*8:Maintainers will be asked to update packages which show any warnings or significant notes, especially at around the time of a new x.y.0 release. Packages which are not updated are liable to be archived. CRAN Repository Policy

*9:当然、元のパッケージのライセンスなどに注意が必要。

*10:Packages should be named in a way that does not conflict (irrespective of case) with any current or past CRAN package (the Archive area can be consulted), nor any current Bioconductor package. Package maintainers give the right to use that package name to CRAN when they submit, so the CRAN team may orphan a package and allow another maintainer to take it over. CRAN Repository Policy

*11:{mvnmle}は欠損値のあるデータの多変量正規分布の平均と分散共分散行列の最尤推定値を計算するためのパッケージです。他に同じような計算を行うパッケージとしては{nrom}があります

*12:Index of /src/contrib/Archive/mvnmle

*13:CRANへの投稿自体は、「cran package submission」と検索しでくる内容を参考にするとできる。R Packages (2e) - 22  Releasing to CRANが参考になります。だいたい{devtools}パッケージを使っとけばなんとかなる。 Thank you, Hadley . 自分の他のパッケージの投稿時の記録としては次の通りCRANにパッケージを登録したのでその思い出をメモしておく - 備忘ログ

*14:たとえば、文献リストのDOIややISBNの記載については"Citations in the ‘Description’ field of the DESCRIPTION file should be in author-year style, followed by a DOI or ISBN for published materials, or a URL otherwise. Preferably, the year and identifier would be enclosed, respectively, in parentheses and angle brackets."とあります。 CRAN Repository Policy

*15:もしかしたらCARNからパッケージがremoveされてからの期間がかなりあいているのも関係あるかもしれませんが、その点の詳細はわかりません。

*16:https://cran.r-project.org/src/contrib/Orphaned/README

*17:実際にはORPHANEDになっているパッケージもあるのかもしれませんが私が観測した範囲では近年新たにORPHANEDになったパッケージは見つけられず そこらへんの運用基準はわかりませんでした。

*18:2. 石田基広徳島大学教授との対談 | R Radio for the Rest of us.の5分程度から

*19:When a new maintainer wishes to take over a package, this should be accompanied by the written agreement of the previous maintainer (unless the package has been formally orphaned). CRAN Repository Policy

*20:[R-pkg-devel] How to fix Archived Package Rpdb?の結果、CRAN - Package Rpdbと再登録に至っている

*21:もちろん新たに実装し直すというのもありだと思います。本稿の趣旨はremoveされたパッケージの再登録なので。 {mvnmle}について調べているときに、中澤港氏が「メンテされていないパッケージを使うより,(中略)を読んで自分でコーディングした方が良いと思う。」と書かれていてぐうの音も出ないと思いました。 鐵人三國誌・アーカイヴ【第615回】 年明け初ミーティング(2021年1月7日)

*22:Chapter 11 Changing package maintainers | rOpenSci Packages: Development, Maintenance, and Peer Review

*23:rOpenSci | Relaunching the qualtRics package

*24:ほかにもなにかパッケージのメンテナーの引き継ぎを支援している活動があるかもしれませんが、私の観測範囲では見つけることができませんでした。

*25:いまはPosit社(旧RStdio社)のCRANパッケージミラーがRStudioユーザの初期設定のミラーであり、CRANパッケージのミラーとして選択している人が多いとは思いますが、唯一のミラーではないので実際にはもう少しだけうわぶれする可能性はあります。

RでLittleのMCAR検定を行う場合、{BaylorEdPsych}パッケージのLittleMCAR()は使うべきではない

欠測値(欠損値)のを含むデータについて、データが完全にランダムに欠落しているかどうか、つまりMCAR(Missing Completely at Random)であるかを検定するLittleのMCAR検定をRで行う場合、やや古めのウェブページ等では{BaylorEdPsych}パッケージというすでにCRANからremoveされたパッケージのLittleMCAR()関数を使う方法を示しているものがある。

しかし、LittleのMCAR検定を行う場合、{BaylorEdPsych}パッケージのLittleMCAR()は基本的には使うべきではない*1

これはCRANからremoveされたメンテナンスされてないパッケージ(および関数)だからというだけでなく、正しい統計量が計算できない場合が存在するからである

LittleのMCAR検定を行うためには多変量正規分布最尤推定を行う必要があるが、{BaylorEdPsych}パッケージのLittleMCAR(){mvnmle}パッケージのmlest()関数を用いている。

このmlest()関数は規定値(mlest()の既定値というよりnlm()の既定値の関係で)では100回まで計算し、それまでに解が収束すればそれを出力し、100回まで解が収束しない場合はその時点での解を出力する仕様となっている。

そこで問題となるのは、mlest()は思いのほか既定値の範囲内では解が収束しない場合が多いということである。

例えばmlest()のドキュメントにあるexampleの例で扱われる13行5列のmissvalsデータですら、解が収束するまで311回を要する。

library(mvnmle)
# iterationsが100となっている
mlest(missvals)
## Warning in stats::nlm(lf, startvals, ...): NA/Inf
## は正の最大値で置き換えられました

## Warning in stats::nlm(lf, startvals, ...): NA/Inf
## は正の最大値で置き換えられました

## $muhat
## [1]  5.279162 44.263596 10.349330 37.131345 94.477964
## 
## $sigmahat
##           [,1]        [,2]       [,3]       [,4]       [,5]
## [1,]  25.54034   13.547670 -23.684100  -13.87358   41.28384
## [2,]  13.54767  181.620955  -5.873532 -196.82020  144.87356
## [3,] -23.68410   -5.873532  55.856434  -39.06666  -61.74589
## [4,] -13.87358 -196.820204 -39.066663  272.76572 -119.67041
## [5,]  41.28384  144.873556 -61.745893 -119.67041  207.52799
## 
## $value
## [1] 229.4776
## 
## $gradient
##  [1] -0.1398058  0.6927837  1.9804463  1.9536193  1.1419774 -5.1137371
##  [7]  6.2012617 -9.6061015 -0.2843517  2.2540983  4.8672109 -4.3410681
## [13] -2.8165870  3.6930118 -1.6934243  5.6298682  3.8015192  7.0913520
## [19]  1.4127670 -2.1581259
## 
## $stop.code
## [1] 4
## 
## $iterations
## [1] 100
# 試しに(exampleの通り)iterlim = 400として計算回数の上限を上げてみる
mlest(missvals, iterlim = 400)
## Warning in stats::nlm(lf, startvals, ...): NA/Inf
## は正の最大値で置き換えられました

## Warning in stats::nlm(lf, startvals, ...): NA/Inf
## は正の最大値で置き換えられました

## $muhat
## [1]  6.655167 49.965258 11.769229 27.047091 95.423078
## 
## $sigmahat
##           [,1]       [,2]       [,3]        [,4]       [,5]
## [1,]  21.82557   20.86434 -24.900388  -11.473447   46.95304
## [2,]  20.86434  238.01242 -15.817386 -252.072282  195.60361
## [3,] -24.90039  -15.81739  37.869819   -9.599196  -47.55622
## [4,] -11.47345 -252.07228  -9.599196  294.182999 -190.59847
## [5,]  46.95304  195.60361 -47.556220 -190.598470  208.90486
## 
## $value
## [1] 173.9566
## 
## $gradient
##  [1]  6.478684e-06  3.084000e-06  2.686993e-06  1.978164e-06 -1.767027e-06
##  [6] -7.045778e-07  2.969584e-06  8.881344e-06 -4.205162e-05  1.562100e-05
## [11] -3.342251e-06 -1.177085e-05  7.482967e-05  6.159113e-06  2.863133e-05
## [16] -3.181941e-06  5.427961e-06  7.017551e-06 -4.230366e-06 -9.427307e-06
## 
## $stop.code
## [1] 1
## 
## $iterations
## [1] 311

もう一つ例を挙げると、組み込みデータで欠測値(欠損値)を含むairqualitymlest()では同様に100回では収束しない。こちらは154回程度で収束する。

# iterationsが100になっている
mlest(airquality)
## $muhat
## [1]  42.137122 185.930478   9.979428  77.870026   6.997105  15.801146
## 
## $sigmahat
##             [,1]        [,2]        [,3]       [,4]       [,5]         [,6]
## [1,] 1043.774622  898.167535 -65.2797197 209.522465  7.4784926   -6.8827520
## [2,]  898.167535 8052.761127 -17.0826526 232.911402 -7.3345985 -119.5057922
## [3,]  -65.279720  -17.082653  12.3310937 -15.161552 -0.8830713    0.8089320
## [4,]  209.522465  232.911402 -15.1615518  89.016469  5.6073380  -10.8861366
## [5,]    7.478493   -7.334598  -0.8830713   5.607338  1.9932388   -0.1025824
## [6,]   -6.882752 -119.505792   0.8089320 -10.886137 -0.1025824   78.0683925
## 
## $value
## [1] 4641.691
## 
## $gradient
##  [1] -0.162113846  0.026192818  0.070248002  0.251939852  0.590048078
##  [6]  0.050085350 -0.050411761 -0.077714705  0.018605852 -0.040893191
## [11]  0.036207894 -0.023638300 -0.003128662 -0.006800292  0.007657945
## [16] -0.048387847 -0.007636118 -0.570109478  0.001581611 -0.001259650
## [21]  0.068154804 -0.012631062  0.080040991 -0.009478754  1.124590199
## [26]  0.014077159  0.114649993
## 
## $stop.code
## [1] 4
## 
## $iterations
## [1] 100
mlest(airquality, iterlim = 200)
## $muhat
## [1]  42.167735 185.925880   9.980416  77.817784   6.989324  15.791587
## 
## $sigmahat
##             [,1]        [,2]        [,3]       [,4]        [,5]          [,6]
## [1,] 1043.974050  898.260489 -65.2747166 209.560887  7.46080107   -6.81740788
## [2,]  898.260489 8047.751854 -17.1140592 232.865524 -7.32823113 -119.43119444
## [3,]  -65.274717  -17.114059  12.3341302 -15.178082 -0.88421910    0.84345576
## [4,]  209.560887  232.865524 -15.1780821  89.026436  5.60815570  -10.88566017
## [5,]    7.460801   -7.328231  -0.8842191   5.608156  1.99352220   -0.09948103
## [6,]   -6.817408 -119.431194   0.8434558 -10.885660 -0.09948103   78.08865476
## 
## $value
## [1] 4641.679
## 
## $gradient
##  [1] -1.009924e-01  2.628237e-02  3.929072e-03 -5.460913e-02 -5.338821e-03
##  [6] -2.463106e-02 -8.983478e-02  1.252422e-01 -8.041472e-02 -2.128262e-02
## [11] -1.395802e-02 -9.496306e-02 -1.587978e-03 -2.612978e-03  1.055014e-04
## [16]  8.476491e-04  1.373337e-04  2.511115e-03 -3.738023e-04 -4.911271e-05
## [21] -1.971785e-03  9.822543e-04  1.492481e-03 -1.618901e-04  5.696165e-03
## [26] -3.336936e-03  4.702088e-04
## 
## $stop.code
## [1] 2
## 
## $iterations
## [1] 154

別に、{mvnmle}mlest()上は正しく解が収束するまで計算するように指定してやればいい(ドキュメントのDetailsにnlm()つかってるからiterlim変えたらいいと書いてあるし、exampleも示している)だけだが、{BaylorEdPsych}パッケージのLittleMCAR()はデータフレーム(または行列)しか受け付けず、LittleMCAR()関数内で使用しているmlest()はデータフレームを与えるのみのため既定値である100回までしか計算しない設定になっているため、最尤推定するときに解が収束するまで計算していない結果を使用している可能性がある。

試しに{BaylorEdPsych}パッケージのLittleMCAR()関数の最尤推定の結果を取り出している箇所である、

gmean <- mlest(x)$muhat
gcov <- mlest(x)$sigmahat

を次の通り、

gmean <- mlest(x, iterlim = 400)$muhat
gcov <- mlest(x, iterlim = 400)$sigmahat

と計算回数の上限を400回程度に修正したものをLittleMCAR2()として、{BaylorEdPsych}パッケージのLittleMCAR()と結果を比較してみる。{BaylorEdPsych}はCRANからremoveされた当時の最新版の0.5を使用する。LittleMCAR()は結果にデータそのものを返すのが、それを掲載すると結果が長くなるので結果のうちの一部だけを載せる。

library(BaylorEdPsych)
LittleMCAR(missvals)[1:4]
## Warning in stats::nlm(lf, startvals, ...): NA/Inf
## は正の最大値で置き換えられました

## Warning in stats::nlm(lf, startvals, ...): NA/Inf
## は正の最大値で置き換えられました

## Warning in stats::nlm(lf, startvals, ...): NA/Inf
## は正の最大値で置き換えられました

## Warning in stats::nlm(lf, startvals, ...): NA/Inf
## は正の最大値で置き換えられました

## this could take a while

## $chi.square
## [1] 14.5923
## 
## $df
## [1] 6
## 
## $p.value
## [1] 0.02367611
## 
## $missing.patterns
## [1] 3
LittleMCAR2(missvals)[1:4]
## Warning in stats::nlm(lf, startvals, ...): NA/Inf
## は正の最大値で置き換えられました

## Warning in stats::nlm(lf, startvals, ...): NA/Inf
## は正の最大値で置き換えられました

## Warning in stats::nlm(lf, startvals, ...): NA/Inf
## は正の最大値で置き換えられました

## Warning in stats::nlm(lf, startvals, ...): NA/Inf
## は正の最大値で置き換えられました

## this could take a while

## $chi.square
## [1] 11.16069
## 
## $df
## [1] 6
## 
## $p.value
## [1] 0.08353528
## 
## $missing.patterns
## [1] 3

二つの結果で統計量が変わっており、p値を見てみるとp = 0.05を基準として判断した場合、判断が変わってしまう。

もう一例、組み込みデータのairqualityの例を次に示す。

LittleMCAR(airquality)[1:4]
## this could take a while

## $chi.square
## [1] 35.14887
## 
## $df
## [1] 14
## 
## $p.value
## [1] 0.001397251
## 
## $missing.patterns
## [1] 4
LittleMCAR2(airquality)[1:4]
## this could take a while

## $chi.square
## [1] 35.12481
## 
## $df
## [1] 14
## 
## $p.value
## [1] 0.001408772
## 
## $missing.patterns
## [1] 4

この例ではp = 0.05を基準とした場合に判断そのものは変わらないが、やはり使った平均や共分散行列が異なっているので統計量の結果は異なっている。

このため、再度冒頭と同じことをいうがLittleのMCAR検定を行う場合、{BaylorEdPsych}パッケージのLittleMCAR()は基本的には使うべきではない。さらにいうと、CRANからremoveされメンテナンスされていないパッケージなので、この問題が今後解決する見込みは現在のところない。

解が収束していれば{BaylorEdPsych}パッケージのLittleMCAR()でも結果は同じだが、{BaylorEdPsych}パッケージのLittleMCAR()の結果で示される上記の情報からは、解が収束したのかどうか分からないというのも問題となる。

というわけで、現在としては、CRANに登録されている{naniar}パッケージのmcar_test()か、{misty}パッケージのna.test()を使ったほうが良い。ちなみに、{naniar}パッケージのmcar_test()も、{misty}パッケージのna.test()のどちらもドキュメントに、 https://web.archive.org/web/20201120030409/https://stats-bayes.com/post/2020/08/14/r-function-for-little-s-test-for-data-missing-completely-at-random/ というwebページを参考にしたとあり、このウェブページのコード自体は{BaylorEdPsych}パッケージのLittleMCAR()を参考にしている様子であり、基本的には同源である*2

どちらも上記に示したLittleMCAR2()と結果が一致する(つまり解が収束された結果を用いている)。

library(naniar)
mcar_test(missvals)
## # A tibble: 1 × 4
##   statistic    df p.value missing.patterns
##       <dbl> <dbl>   <dbl>            <int>
## 1      11.2     6  0.0835                3
mcar_test(airquality)
## # A tibble: 1 × 4
##   statistic    df p.value missing.patterns
##       <dbl> <dbl>   <dbl>            <int>
## 1      35.1    14 0.00142                4
library(misty)
## |-------------------------------------|
## | misty 0.5.4 (2023-11-14)            |
## | Miscellaneous Functions T. Yanagida |
## |-------------------------------------|
na.test(missvals)
##  Little's MCAR Test
## 
##    n nIncomp nPattern  chi2 df  pval 
##   13       7        3 11.16  6 0.084
na.test(airquality)
##  Little's MCAR Test
## 
##     n nIncomp nPattern  chi2 df  pval 
##   153      42        4 35.11 14 0.001

こちらも、最尤推定の計算回数を指定する引数はないが、{naniar}パッケージのmcar_test(){misty}パッケージのna.test()最尤推定するために用いられている{norm}パッケージのem.norm()の計算回数の上限が既定値で1000回で多く、かつ{mvnmle}パッケージのmlest()よりも少ない回数で解が収束していることが多いため、さしあたって上記のmissvalsや、airquality程度のデータなら問題にならないためである。

library(norm)
## This package has some major limitations
## (for example, it does not work reliably when
## the number of variables exceeds 30),
## and has been superseded by the norm2 package.
s <- prelim.norm(as.matrix(missvals))
thetahat <- em.norm(s)
## Iterations of EM: 
## 1...2...3...4...5...6...7...8...9...10...11...12...13...14...15...16...17...18...19...20...21...22...23...24...25...26...27...28...29...30...31...32...33...34...35...36...37...38...39...40...41...42...43...44...45...46...47...48...49...50...51...52...53...54...55...56...57...58...59...60...61...62...63...64...65...66...67...68...69...70...71...72...73...74...75...76...77...78...79...80...81...82...83...84...85...86...87...88...89...90...91...92...93...94...95...96...97...98...99...100...101...102...103...104...105...106...107...108...109...110...111...112...113...114...115...116...117...118...119...120...121...122...123...124...
getparam.norm(s, thetahat)
## $mu
## [1]  6.655166 49.965259 11.769231 27.071654 95.423077
## 
## $sigma
##           [,1]       [,2]       [,3]        [,4]       [,5]
## [1,]  21.82557   20.86433 -24.900388  -11.597718   46.95303
## [2,]  20.86433  238.01244 -15.817377 -252.031412  195.60363
## [3,] -24.90039  -15.81738  37.869822   -9.400406  -47.55621
## [4,] -11.59772 -252.03141  -9.400406  293.791401 -190.77273
## [5,]  46.95303  195.60363 -47.556213 -190.772730  208.90485

途中に出力されるIteration of EMというのが計算回数で124回で収束している({mvnmle}パッケージのmlest()は311回)。

ただし、{norm}はパッケージを読み込んだときに出るメッセージの通り30以上の変数を含む場合に正しい結果を返さなない可能性があり、注意が必要である。

ところで、CRAN(のミラーサイトのうちPosit社が管理するもの)からパッケージがどの程度ダウンロードされているかを調べる{carnlogs}パッケージのcran_downloads()を使って、ここ1年間のダウンロード件数を調べると

library(cranlogs)
cran_downloads("BaylorEdPsych", from = Sys.Date() - 365, to = Sys.Date()) |> 
  summary()
##       date                count          package         
##  Min.   :2022-11-21   Min.   : 0.000   Length:366        
##  1st Qu.:2023-02-20   1st Qu.: 0.000   Class :character  
##  Median :2023-05-22   Median : 0.000   Mode  :character  
##  Mean   :2023-05-22   Mean   : 1.016                     
##  3rd Qu.:2023-08-21   3rd Qu.: 1.000                     
##  Max.   :2023-11-21   Max.   :41.000

となり、毎日1件くらいはダウンロードされ、日によっては41件もダウンロードされていることがわかり、世界の何処かでは今も使われているパッケージになっている(このうち1件は自分だけど)。

ここからは想像でしかないが、LittleのMCAR検定についてwebページで調べて、(代替案があるのにそんなはずはと思うかもしれないが)わざわざCRANのArchivesからダウンロードして使っている人も少なからずいるのではないかと思っている(他の関数を使っている例もあるとは思うが)。

ちなみにこの件は{mvnmle}パッケージのmlest()の挙動を眺めていて計算回数が既定値で解が収束しないパターンが多いことに気づいたことがきっかけで、{BaylorEdPsych}パッケージのLittleMCAR()ソースコードを読んで気づいた。意気揚々と書いてみたが、一箇所解けると割りと簡単に気づけそうな問題なので、他のところで書いている人がいるかも知れない。あるいは上記の内容で何か大きな勘違いをしているのであれば教えてほしい。

ついでにいうと、上記の内容に基づくと、過去に{BaylorEdPsych}パッケージ({BaylorEdPsych}パッケージがCARNからremoveされる以前でも)のLittleMCAR()で計算した結果は統計量に誤りが合った可能性があると思う。

*1:もう少し強めにいうと絶対に使うべきではない

*2:そもそも、Little, R. J. A. (1988). A test of missing completely at random for multivariate data with missing values. Journal of the American Statistical Association, 83(404), 1198–1202.に由来するので計算の効率化や表示の方法などはそれぞれで異なるが、計算そのものは同じ形に収斂する。

欠損値(欠測値)を含むデータのmultivariate normal maximum-likelihood estimation、つまり多変量正規分布の平均ベクトルと共分散行列の最尤推定値の計算をRで行う方法のメモ

欠損値(欠測値)を含むデータのmultivariate normal maximum-likelihood estimation、つまり多変量正規分布の平均ベクトルと共分散行列の最尤推定値の計算をRで行う方法。メモ。

CRANにあるパッケージでは{mvnmle}{norm}で計算できる。ほかにも実現できるパッケージ等あるかもしれないが見つけられていない。

Jonathon D. Brown(2018). Advanced Statistics for the Behavioral Sciences A Computational Approach with R. Springer Cham. 354 - 356. [https://doi.org/10.1007/978-3-319-93549-2] で記載されているRでの実装版と比較する。

データは{mvnmle}パッケージの欠損値を含むappleデータを使う。

data("apple", package = "mvnmle")

{mvnmle}パッケージを使う

CRANから{mvnmle}をインストールして計算する。

# インストールしていない場合は
# install.packages("mvnmle")
# でインストールする
library(mvnmle)
mlest(apple)
## $muhat
## [1] 14.72227 49.33325
## 
## $sigmahat
##           [,1]      [,2]
## [1,]  89.53415 -90.69653
## [2,] -90.69653 114.69470
## 
## $value
## [1] 148.435
## 
## $gradient
## [1]  4.988478e-06  2.892682e-06  8.726424e-07  1.682947e-05 -1.073488e-04
## 
## $stop.code
## [1] 1
## 
## $iterations
## [1] 34

ただし、ちょっと大きなデータを触るとすぐにnlm()がwornig messageが出るが、計算結果は他の2例と変わらない。

# {mvnmle}のmissvalsデータをつかう
data("missvals")
# nlm()のiterlim(反復回数の上限回数)の既定値が100なので、400まであげておく
mlest(missvals, iterlim = 400)
## Warning in stats::nlm(lf, startvals, ...): NA/Inf
## は正の最大値で置き換えられました

## Warning in stats::nlm(lf, startvals, ...): NA/Inf
## は正の最大値で置き換えられました

## $muhat
## [1]  6.655167 49.965258 11.769229 27.047091 95.423078
## 
## $sigmahat
##           [,1]       [,2]       [,3]        [,4]       [,5]
## [1,]  21.82557   20.86434 -24.900388  -11.473447   46.95304
## [2,]  20.86434  238.01242 -15.817386 -252.072282  195.60361
## [3,] -24.90039  -15.81739  37.869819   -9.599196  -47.55622
## [4,] -11.47345 -252.07228  -9.599196  294.182999 -190.59847
## [5,]  46.95304  195.60361 -47.556220 -190.598470  208.90486
## 
## $value
## [1] 173.9566
## 
## $gradient
##  [1]  6.478684e-06  3.084000e-06  2.686993e-06  1.978164e-06 -1.767027e-06
##  [6] -7.045778e-07  2.969584e-06  8.881344e-06 -4.205162e-05  1.562100e-05
## [11] -3.342251e-06 -1.177085e-05  7.482967e-05  6.159113e-06  2.863133e-05
## [16] -3.181941e-06  5.427961e-06  7.017551e-06 -4.230366e-06 -9.427307e-06
## 
## $stop.code
## [1] 1
## 
## $iterations
## [1] 311
# 次にとりあげる{norm}をつかって同じ計算をする
s <- norm::prelim.norm(as.matrix(missvals))
thetahat <- norm::em.norm(s, showits = FALSE)
norm::getparam.norm(s, thetahat)
## $mu
## [1]  6.655166 49.965259 11.769231 27.071654 95.423077
## 
## $sigma
##           [,1]       [,2]       [,3]        [,4]       [,5]
## [1,]  21.82557   20.86433 -24.900388  -11.597718   46.95303
## [2,]  20.86433  238.01244 -15.817377 -252.031412  195.60363
## [3,] -24.90039  -15.81738  37.869822   -9.400406  -47.55621
## [4,] -11.59772 -252.03141  -9.400406  293.791401 -190.77273
## [5,]  46.95303  195.60363 -47.556213 -190.772730  208.90485
# 最後にとりあげるRでの実装での計算を行う。
emMLE(missvals$x1, missvals$x2, missvals$x3, missvals$x4, missvals$x5)
## $iter
## [1] 611
## 
## $mu
## [1]  6.655166 49.965259 11.769231 27.047089 95.423077
## 
## $sigma
##           [,1]       [,2]       [,3]        [,4]       [,5]
## [1,]  21.82557   20.86433 -24.900388  -11.473441   46.95303
## [2,]  20.86433  238.01244 -15.817377 -252.072312  195.60363
## [3,] -24.90039  -15.81738  37.869822   -9.599211  -47.55621
## [4,] -11.47344 -252.07231  -9.599211  294.183044 -190.59849
## [5,]  46.95303  195.60363 -47.556213 -190.598491  208.90485
## 
## $lnL
## [1] -41.0314
## 
## $dif
## [1] 9.094947e-13

{norm}パッケージを使う

CRANから{norm}をインストールして計算する。

# インストールしていない場合は
# install.packages("norm")
# でインストールする
library(norm)
## This package has some major limitations
## (for example, it does not work reliably when
## the number of variables exceeds 30),
## and has been superseded by the norm2 package.
s <- prelim.norm(as.matrix(apple))
thetahat <- em.norm(s, showits = FALSE)
getparam.norm(s, thetahat)
## $mu
## [1] 14.72222 49.33266
## 
## $sigma
##           [,1]      [,2]
## [1,]  89.53395 -90.69081
## [2,] -90.69081 114.68298

ちなみに、{norm}関数の多くのドキュメントの参考文献が「See Section 5.3 of Schafer (1994).」みたいないまいちわからない書き方がされているが、多分、mdataに参考文献として書いてあるSchafer, J.L. (1997). Analysis of Incomplete Multivariate Data (1st ed.). Chapman and Hall/CRC. https://doi.org/10.1201/9780367803025 が(目次しか見ていないが)本当の参考文献だと思う(関数ごとに年が微妙に異なるが)。

ついでにいうと、{norm}mi.inference()の参考文献がp. 76-77 of Rubin (1987)となっているが、これはRubin, D.B. (1987) Multiple Imputation for Nonresponse in Surveys. John Wiley & Sons Inc., New York. http://dx.doi.org/10.1002/9780470316696 のことだと思う。

さらにいうと、“This package … has been superseded by the norm2 package.”とパッケージを読み込んだときにメッセージが出るが、今年の8月に{norm2}はCRANからreomveされている。

(おまけ){norm2}パッケージを使う

CRANからremoveされている{norm2}パッケージをつかって計算してみる。{remotes}パッケージを使ってインストールする。remotes::install_url("https://cran.r-project.org/src/contrib/Archive/norm2/norm2_2.0.4.tar.gz")とするとCRAN現在までに登録された最終版(removeされている)がインストールできる。

library(norm2)
emNorm(apple) |> 
  summary()
## Predictor (X) variables:
##       Mean SD Observed Missing Pct.Missing
## CONST    1  0       18       0           0
## 
## Response (Y) variables:
##           Mean        SD Observed Missing Pct.Missing
## size  14.72222  9.736563       18       0     0.00000
## worms 45.00000 10.539967       12       6    33.33333
## 
## Missingness patterns for response (Y) variables
##    (. denotes observed value, m denotes missing value)
##    (variable names are displayed vertically)
##    (rightmost column is the frequency):
##  w
## so
## ir
## zm
## es
## .. 12
## .m  6
## 
## Method:                             EM
## Prior:                              "uniform"
## Convergence criterion:              1e-05
## Iterations:                         23
## Converged:                          TRUE
## Max. rel. difference:               9.3347e-06
## -2 Loglikelihood:                   148.435
## -2 Log-posterior density:           148.435
## Worst fraction missing information: 0.6141
## 
## Estimated coefficients (beta):
##           size    worms
## CONST 14.72222 49.33324
## 
## Estimated covariance matrix (sigma):
##            size     worms
## size   89.53395 -90.69589
## worms -90.69589 114.69325
emNorm(missvals) |> 
  summary()
## Predictor (X) variables:
##       Mean SD Observed Missing Pct.Missing
## CONST    1  0       13       0           0
## 
## Response (Y) variables:
##        Mean        SD Observed Missing Pct.Missing
## x1  6.00000  4.358899        9       4    30.76923
## x2 45.00000 15.953056        9       4    30.76923
## x3 11.76923  6.405126       13       0     0.00000
## x4 39.00000 16.492423        6       7    53.84615
## x5 95.42308 15.043723       13       0     0.00000
## 
## Missingness patterns for response (Y) variables
##    (. denotes observed value, m denotes missing value)
##    (variable names are displayed vertically)
##    (rightmost column is the frequency):
## xxxxx
## 12345
## ..... 6
## ...m. 3
## mm.m. 4
## 
## Method:                             EM
## Prior:                              "uniform"
## Convergence criterion:              1e-05
## Iterations:                         226
## Converged:                          TRUE
## Max. rel. difference:               9.5711e-06
## -2 Loglikelihood:                   173.9567
## -2 Log-posterior density:           173.9567
## Worst fraction missing information: 0.9559
## 
## Estimated coefficients (beta):
##             x1       x2       x3       x4       x5
## CONST 6.655166 49.96526 11.76923 27.04733 95.42308
## 
## Estimated covariance matrix (sigma):
##           x1         x2         x3          x4         x5
## x1  21.82557   20.86433 -24.900388  -11.474685   46.95303
## x2  20.86433  238.01244 -15.817377 -252.071903  195.60363
## x3 -24.90039  -15.81738  37.869822   -9.597222  -47.55621
## x4 -11.47468 -252.07190  -9.597222  294.179111 -190.60023
## x5  46.95303  195.60363 -47.556213 -190.600234  208.90485

Rによる実装(Advanced Statistics for the Behavioral Sciences A Computational Approach with R.)

Rでの実装(Advanced Statistics for the Behavioral Sciences A Computational Approach with R.にある、emMLE())と比較する。写経したコードを使うが、書籍のコードなので実装そのものはこの記事中はのせない。

なお、書籍はgoogle booksに登録されており、当該ページは読むことができる(現在のところ)。

emMLE(apple$size, apple$worms)
## $iter
## [1] 68
## 
## $mu
## [1] 14.72222 49.33333
## 
## $sigma
##           [,1]      [,2]
## [1,]  89.53395 -90.69673
## [2,] -90.69673 114.69496
## 
## $lnL
## [1] -46.64932
## 
## $dif
## [1] 6.110668e-13

計算速度の比較

appleデータを100回処理する時間を比較する。

# mvnmleの計算時間
mvnmle_time <- system.time(for(i in 1:100) mlest(apple))
# normの計算時間
norm_time <- system.time(for(i in 1:100){
  s <- prelim.norm(as.matrix(apple))
  thetahat <- em.norm(s, showits = FALSE)
  getparam.norm(s, thetahat)
})
# Rでの実装の計算時間(emMLE)
emMLE_time <- system.time(for(i in 1:100) emMLE(apple$size, apple$worms))
# norm2の計算時間
norm2_time <- system.time(for(i in 1:100) ans <- emNorm(apple))
# 計算結果をデータフレーム固める
d <- list(mvnmle_time, norm_time, emMLE_time, norm2_time) |> 
  Reduce(rbind, x = _) |> 
  as.data.frame(row.names = NA)
d <- cbind(data = c("mvnmle_time", "norm_time", "emMLE_time", "norm2_time"), d) 
d
##          data user.self sys.self elapsed user.child sys.child
## 1 mvnmle_time      0.41     0.00    0.41         NA        NA
## 2   norm_time      0.09     0.00    0.09         NA        NA
## 3  emMLE_time     24.67     1.51   27.05         NA        NA
## 4  norm2_time      0.08     0.00    0.08         NA        NA
barplot(user.self ~ data, data = d)

計算の一部をCやFortranに投げている{mvnmle}{norm}{norm2}はRでの実装よりも早い。

少し大きなデータ(少し大きなデータと言ってもmissvalueはわずか13行5列のデータだが)を取り扱うとその差は更に大きくなる。

# mvnmleの計算時間
mvnmle_time <- system.time(for(i in 1:100) suppressWarnings(mlest(missvals, iterlim = 400)))
# normの計算時間
norm_time <- system.time(for(i in 1:100){
  s <- prelim.norm(as.matrix(missvals))
  thetahat <- em.norm(s, showits = FALSE)
  getparam.norm(s, thetahat)
})
# Rでの実装の計算時間(emMLE)
emMLE_time <- system.time(for(i in 1:100) emMLE(missvals$x1, missvals$x2, missvals$x3, missvals$x4, missvals$x5))
# norm2の計算時間
norm2_time <- system.time(for(i in 1:100) ans <- emNorm(missvals))
# 計算結果をデータフレーム固める
d <- list(mvnmle_time, norm_time, emMLE_time, norm2_time) |> 
  Reduce(rbind, x = _) |> 
  as.data.frame(row.names = NA)
d <- cbind(data = c("mvnmle_time", "norm_time", "emMLE_time", "norm2_time"), d) 
d
##          data user.self sys.self elapsed user.child sys.child
## 1 mvnmle_time     11.25     0.10   11.64         NA        NA
## 2   norm_time      0.28     0.01    0.31         NA        NA
## 3  emMLE_time    164.59     8.36  174.75         NA        NA
## 4  norm2_time      0.34     0.00    0.35         NA        NA
barplot(user.self ~ data, data = d)

{norm}{norm2}がはやいが、{norm2}はCRANからはremoveされている。{norm}はパッケージを読み込んだときに出るメッセージの通り、潜在的な問題を抱えているらしい。

{mvnmle}は収束までの計算回数(iterations)が多い。あと、いろいろな条件を踏むと解がほかのパッケージで計算した結果やRで実装した方式で計算した結果とわずかに異なる場合があることに気づいた。もう少し調べていく。

Rでの実装は、小さなデータですら解が収束するまでの時間が結構かかる。またJonathon D. Brown(2018). Advanced Statistics for the Behavioral Sciences A Computational Approach with R. Springer Cham. 354 - 356. [https://doi.org/10.1007/978-3-319-93549-2] の実装だと、大きいデータだと計算できないことがある。

RでExcelのLOOKUP関数のように対応表として値を別のデータフレームを参照して列を追加する方法について

RでExcelのLOOKUP関数のように対応表として値をデータフレームでもっているの場合に、対応している値を参照して列を追加する方法について考えてみる。小ネタ。

次のQiitaの記事を読んで思った。

qiita.com

いろいろな手法が考えられるが、{dplyr}を使うのであればleft_join()が比較的シンプルに書けると思う。

NAは0で埋めたいようなので{tidyr}replace_na()を使うことにする。ついでに、表示される列名をSP_noにするために、rename()しておく。

具体的には次の通り。

library(dplyr)
## 
##  次のパッケージを付け加えます: 'dplyr'

##  以下のオブジェクトは 'package:stats' からマスクされています:
## 
##     filter, lag

##  以下のオブジェクトは 'package:base' からマスクされています:
## 
##     intersect, setdiff, setequal, union
library(tidyr)

df <- data.frame(
  SP_name = c("ブナ", "スギ", "ヒノキ", "", "スギ", "コナラ", "", "ヒノキ", "ブナ", "コナラ"),
  SP_no = NA
)
SP_table <- data.frame(
  name = c("スギ", "ヒノキ", "カラマツ", "ブナ", "コナラ", "ミズナラ"),
  no = 1:6
)

df <- left_join(df[1], SP_table, by = join_by(SP_name == name)) |> 
  replace_na(list(no = 0)) |> 
  rename(SP_no = no)
df
##    SP_name SP_no
## 1     ブナ     4
## 2     スギ     1
## 3   ヒノキ     2
## 4              0
## 5     スギ     1
## 6   コナラ     5
## 7              0
## 8   ヒノキ     2
## 9     ブナ     4
## 10  コナラ     5

もとの記事内で紹介されている手法でも特段に問題はないが、せっかくデータフレームで対応表を持っているのに何度も$でデータフレームの中身をベクトルで呼び出したりしているので、「あ、対照表まちがってたわー。他のデータフレームだったわー」とか「指定する列まちがってたわー」などあったときに書き直さなければならない箇所がやや多い気がする。