rStats.js

貢献の手引き

English 日本語

この文書は 英語版 の訳です。内容が食い違うときは英語版が正です。

rStats.js に関心を持っていただきありがとうございます。この文書では、このプロジェクトで結果を R と どう照合しているか、関数を追加・変更するときに何が必要かを説明します。

準備

テストスイートの実行には R もネットワーク接続も要りません。R から得た期待値は test/fixtures/*.json に保存してあります。

R との照合の仕組み

すべての関数を R の出力と比べています。関数を 1 つ追加するには、次の 3 つのファイルを追加します。

  1. oracle/<name>.R: 入力のグリッド (典型的な値、p = 1e-300 のような裾、境界、NaN/Inf、 巨大なパラメータ) を作り、R の結果を test/fixtures/<name>.json に書き出す。1 ケースにつき 1 行で、数値は %.17g で書式化した文字列にする。
  2. src/ に置く実装。その分野の src/entries/<area>.ts (サブパスの export) から export する (src/index.ts はすべての分野を再 export します)。
  3. test/<name>.test.ts: フィクスチャに対して実装を実行し、相対誤差を確かめる。

フィクスチャを生成し直すには、Docker 上の R が必要です。

npm run oracle -- <oracle name>     # 例: npm run oracle -- gamma

scripts/oracle.sh は、使い捨てのコンテナ (docker run --rm --network none) で Rscript を 実行します。イメージは既定で rstats-oracle:4.6.2 です (RSTATS_ORACLE_IMAGE で変更できます)。 このイメージは oracle/Dockerfile.r46 から作ります。R 4.6.2 と、oracle が読み込むパッケージ (jsonlite、Rmpfr、gmp、MASS、Matrix、R にない関数のための extraDistr、lme4、car、 afex と、oracle/Dockerfile.r46 に挙げたほかのパッケージ) が入ります。パッケージは日付で固定した スナップショットから取るので、作り直しても同じ版になります。作るときだけネットワークが要り、 oracle の実行には要りません。1.0.0 までの fixture を作った R 4.5.2 のイメージ (oracle/Dockerfile) は、変えずに残しています。

docker build -f oracle/Dockerfile.r46 -t rstats-oracle:4.6.2 oracle
docker build --platform linux/amd64 -f oracle/Dockerfile.r46 -t rstats-oracle-amd64:4.6.2 oracle

R のメッセージは LANGUAGE=en で英語に固定しているので、フィクスチャに記録されるエラー メッセージはロケールに左右されません。新しい oracle には、既存のフィクスチャと同じ名前を 使わないでください (write_fixture は黙って上書きします)。実行したら git status で 確かめてください。

仮説検定のフィクスチャは、各ケースの R の警告も記録します (oracle/_htest.R の write_htest_fixture(..., warnings = TRUE))。この警告だけを、Docker を使わず数値も変えずに足したり更新したりするには、oracle のケースを 手元の R で流します。node scripts/fixture-warnings.mjs <oracle の名前> ... (--check は書かずに報告だけ。 フィクスチャの引数を正確に R に渡し、結果をフィクスチャと照合します) と、警告だけの oracle には node scripts/fixture-warnings.mjs --run htestwarnings を使います。PATH に Rscript と、oracle が使う パッケージが要ります。

乱数のフィクスチャは、x86_64 版のイメージ (--platform linux/amd64 の rstats-oracle-amd64。 oracle には # oracle-platform: amd64 と印を付ける) で生成します。arm64 版では一部の関数で 最後のビットが異なるためです。線形代数のフィクスチャには、R が使った LAPACK と BLAS の バージョンを info ブロックに記録します (現在のフィクスチャは OpenBLAS 0.3.26 と LAPACK 3.12.0 で作成)。BLAS が違うと最後の数ビットが変わることがあるためです。

mpmath による高精度の値とも照合できます。scripts/mpmath/<name>.py が test/fixtures/<name>.mpmath.json を書き出します (python3 -I scripts/mpmath/<name>.py。 pip install mpmath が必要)。R のない環境での作業はこれから始め、oracle/<name>.R を実行すると テストは自動的に R との照合も行います。フィクスチャの数値は、先に倍精度に直して読みます (mp.mpf(float(s)))。mp.mpf(s) は十進の文字列をそのまま取るので、関数が受け取った入力とは 違う値になります。R のフィクスチャに Rmpfr の参照関数が無い特殊関数では、 scripts/mpmath/mismatch_ref.py が R と合わないケース (RSTATS_WRITE_MISMATCH=1 で書き出す) の mpmath の参照値を計算し、<name>.ref.json に統合します。

R がいつも正しいとは限りません。一致しないケースがあると、テストはそのケースを高精度の参照値 (Rmpfr または gmp) と比べ、rStats.js のほうが R より真の値に近ければ合格とします。rStats.js が 意図して R と異なるケースは、理由を書いたうえで除外し (knownDifference)、ドキュメントに 記録します。docs/ja/accuracy.md と docs/ja/differences-from-r.md を見てください。 docs/ja/performance.md では、double-double 演算、scripts/ddgen.py で 生成したコード、速度の測り方 (npm run bench)、double-double によって精度がどれだけ上がるかの 測り方 (npm run precision-report) を説明しています。

分布関数は、参照値の要らない性質でも検査しています (test/distributionProperties.test.ts。検査の 中身は test/helpers/properties.ts)。確率が [0, 1] に収まること、下側 + 上側 = 1、対数スケールと 線形スケールの一致、単調性、分位点の往復、密度と分布関数の差分の一致、時間、呼び出し順に結果が 依らないことを、1e-300 から 1e300 までのパラメータで確かめます。現時点で分かっている違反は理由を 付けて test/helpers/distributionKnown.ts に載せてあり、不要になった項目があるとテストは失敗します。 より長く探すには、ビルドしてから node scripts/fuzz-distributions.mjs <seed> <iterations> [family regex] [output.json] を実行します (オプションは冒頭のコメント)。分布ごとにワーカースレッドで 動かし、5 秒以内に戻らない呼び出しも報告して、違反を JSON で書き出します (--known を付けると 既知のものを除きます)。

設計の決まり

R と数学が食い違うときや、入力に答えがないときに何を返すかは、次の決まりで決めます。新しいコードでも修正でも これに従ってください。

  1. 結果が定義されない入力はエラー。 NaN や範囲外の引数、有限でない許容誤差や刻み、検定が定義されなくなる 自由度や標準誤差は、Error か RangeError を投げます。R が NaN や ±Inf を返す場合も同じです (テストでは R の期待値をそのエラーに置き換え、理由を書きます)。例外は分布の関数 (d/p/q/r) で、R と同じく不正な母数に NaN を返します。予測の新しいデータに NaN があれば、その行が NaN になります。オプションのオブジェクトに関数の 知らないキーがあれば、無視せずに RangeError にします (分布の関数も同じ)。綴りを誤ったオプションが、黙って 誤った結果を返さないようにするためです。
  2. R が正しい範囲は R と同じビット、R が誤る範囲だけ正しい値。 R との一致より正しい値を優先しますが、 直すのは R (やパッケージ) が誤る範囲に限ります。R と違う値は、理由とともに docs/ja/differences-from-r.md と英語版の両方に記録します。
  3. 受け皿は通常の範囲を変えない。 別の経路に入るのは、元の結果が有限でない、範囲外、または桁落ちしている ときだけです。通常の範囲の結果はビットまで変わりません。
  4. 確保量と時間の上限。 1 本の配列は 2^24 個まで、1 回の呼び出しは 10 秒ほどまでにします。超えるときは 確保の前に RangeError を投げ (正確な検定では exact: false を案内します)、上限を文書 (性能) に書きます。値と乱数の列を変えずに確保をなくせるなら、なくします。
  5. 乱数は R の列に従う。 R (や extraDistr) と同じ一様乱数を同じ数だけ引きます。値を変える場合も同じです。 母数が無限のときは、r は q が返す極限の値を返します。
  6. 無限の尺度は極限。 無限の尺度で d と p が極限を返すなら、q と r も極限を返します。尺度 0 は R と同じです。
  7. 2^53 を超える整数。 整数であるべき引数が 2^53 を超え、double で正確な整数でなくなるときは NaN です (文書にした例外は differences-from-r にあります)。
  8. 長さ 0 は R と同じ。 長さ 0 のベクトルには R と同じ結果を返します。
  9. 整数への変換は R と同じ。 整数であるべき引数は、R の as.integer と同じく切り捨ててから確かめます。 離散分布の分布関数は q 以下の値を数えます。
  10. そのほかの約束。 信頼水準が 1 に近く α/2 < 1e-8 のときは、上側の裾から分位点を求めます。検定・モデル・ 当てはめは、R の警告を結果の warnings に入れて返します。R の文言のまま、同じ警告は 1 回ずつ、R が出す順で、 警告がなければ空の配列です。結果が NaN になる理由を説明する警告 (NaNs produced) は入れます。R の分布関数が 自身の精度について出す警告 (full precision may not have been achieved) と、R の引数の型についての警告 (群を 因子に変換したなど) は入れません。既定では何も表示しません。setWarningHandler で設定した関数が、呼び出しが 結果を計算した後に、その呼び出しの警告を 1 つずつ、呼んだ関数の名前とともに受け取ります。別の関数を呼ぶ関数 (wilcoxTest を呼ぶ pairwiseWilcoxTest) は自分の名前で 1 回だけ渡し、当てはめの警告は当てはめたときに 1 回だけ渡します。anova(fit) や etaSquared(fit) は、それを結果に残しますが、渡すのは自分で足した警告だけ です (src/internal/warnings.ts)。値が溢れたりアンダーフローしたり しうる計算は、先に入力を 2 の冪で正規化します。

ライセンスの決まり

rStats.js は MIT ライセンスです。それを保つために、次のことを守ってください。

デモ

demo/ は、jsDelivr から dist/rstats.js を読み込む静的なページです (?src=local を付けると ローカルのビルドを読み込みます)。例は demo/examples.json で定義しています。例を変更したら npm run demo:r を実行して、demo/examples.R と demo/r-results.json の R の値を生成し直して ください。test/demo.test.ts がすべての例を R と照合し、test/browser/demo.mjs がページを 操作して確かめます。

スタイル

リリース

  1. NOT_IN_DEMO_YET (test/demo.test.ts) に挙げた export のデモ例を追加して、このリストを空に します (npm run demo:r が必要)。package.json の version、CHANGELOG.md の日付、demo/app.js の VERSION (デモが jsDelivr から読み込むタグ) を更新してコミットし、タグを付けて push します: git tag vX.Y.Z && git push origin vX.Y.Z。
  2. .github/workflows/release.yml がテストを実行してビルドし、dist/ も含むコミットへタグを 移します。これで jsDelivr が https://cdn.jsdelivr.net/gh/numero-uno-jpn/rStats.js@vX.Y.Z/dist/rstats.js を配信できる ようになります (ワークフローが終わったら、git fetch --tags --force でローカルのタグを 更新してください)。main ブランチには dist/ を一切含めません。
  3. 続いて、provenance 付きでその版を npm の公開待ちに入れます (npm stage publish)。トークンは 使いません。npmjs.com のパッケージ設定で、このリポジトリと release.yml を trusted publisher として指定しているためです。この trusted publisher には公開待ちに入れることだけを許し、直接の 公開は許していません。
  4. メンテナーが公開待ちの版を確かめて承認すると、公開されます。npmjs.com のパッケージの Staged Packages タブで Approve を押すか、npm stage approve <id> を実行します。どちらも npm アカウントの二要素認証が必要です。

バージョン 0.5.0 は手作業で公開しました (provenance には CI が必要なので npm publish --provenance=false)。以降のバージョンは、ワークフローと上の承認を経て公開します。 ローカルのマシンから公開するときも、npm アカウントの二要素認証が必要です。jsDelivr が配信できるように、 リポジトリは公開のままにしておく必要があります。