| English | 日本語 |
この文書は 英語版 の訳です。内容が食い違うときは英語版が正です。
rStats.js に関心を持っていただきありがとうございます。この文書では、このプロジェクトで結果を R と どう照合しているか、関数を追加・変更するときに何が必要かを説明します。
npm cinpm run typecheck、npm test、npm run buildテストスイートの実行には R もネットワーク接続も要りません。R から得た期待値は
test/fixtures/*.json に保存してあります。
すべての関数を R の出力と比べています。関数を 1 つ追加するには、次の 3 つのファイルを追加します。
oracle/<name>.R: 入力のグリッド (典型的な値、p = 1e-300 のような裾、境界、NaN/Inf、
巨大なパラメータ) を作り、R の結果を test/fixtures/<name>.json に書き出す。1 ケースにつき
1 行で、数値は %.17g で書式化した文字列にする。src/ に置く実装。その分野の src/entries/<area>.ts (サブパスの export) から export する
(src/index.ts はすべての分野を再 export します)。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 と数学が食い違うときや、入力に答えがないときに何を返すかは、次の決まりで決めます。新しいコードでも修正でも これに従ってください。
Error か RangeError を投げます。R が NaN や ±Inf を返す場合も同じです (テストでは
R の期待値をそのエラーに置き換え、理由を書きます)。例外は分布の関数 (d/p/q/r) で、R と同じく不正な母数に
NaN を返します。予測の新しいデータに NaN があれば、その行が NaN になります。オプションのオブジェクトに関数の
知らないキーがあれば、無視せずに RangeError にします (分布の関数も同じ)。綴りを誤ったオプションが、黙って
誤った結果を返さないようにするためです。exact: false を案内します)、上限を文書
(性能) に書きます。値と乱数の列を変えずに確保をなくせるなら、なくします。r は q が返す極限の値を返します。d と p が極限を返すなら、q と r も極限を返します。尺度 0 は
R と同じです。as.integer と同じく切り捨ててから確かめます。
離散分布の分布関数は q 以下の値を数えます。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 ライセンスです。それを保つために、次のことを守ってください。
src/nmath、src/appl、
推奨パッケージ、TOMS 708 もこれに含みます。THIRD_PARTY_NOTICES.md に追加してください。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 がページを
操作して確かめます。
glmResiduals(fit, "pearson")) は、位置とオプションの両方で受け付け (オーバーロード)、両方で
渡したら RangeError にします。例外は optimHess(par, fn, gr, control) で、R と同じく gr と control を位置
だけで受け取ります。control 自体が素のオブジェクトなので、gr の後のオプションオブジェクトと区別できないためです。src/internal/checkOptions.ts の
checkOptions)。検査とキーの一覧は、オプションの型から生成します。export する関数を足したり、オプションの型を
変えたりしたら、node scripts/option-keys.mjs --write を流し、変わったものをコミットしてください (ソースが
その出力と違う間は test/unknownOptions.test.ts が落ちます)。印の付いた生成された行は手で直しません。
オプションとデータフレームが同じ位置に来るところと、独自の検査を持つ関数は、スクリプトの中で指定します。*.md と docs/) は
英語で書きます。docs/ja/ と *.ja.md のファイルは日本語訳で、英語版が正です。一方を変えたら、
その訳も同じように変えてください。@example を付けます。その // 値 のコメントは
test/docExamples.test.ts が確かめます。R の何に当たるかを、決まった形の 1 文で書きます
(Equivalent to R's `t.test()`.、Equivalent to `car::Anova()` in R.、Not an R function.。
test/rNames.test.ts が確かめます)。export やこれらのコメントを変えたら、node scripts/r-index.mjs > docs/r-functions.md と
node scripts/r-index.mjs --ja > docs/ja/r-functions.md で索引のページを作り直してください (日本語の説明は
docs/ja/r-functions.json に関数ごとに 1 つ。どちらのページもテストが確かめます)。npx typedoc@0.28.20 の
警告は 0 件にします。TypeError、値が範囲外のとき (ない列、
空の選択、知らない選択肢) は RangeError、数値計算が失敗したときと、結果が定義されない入力 (観測が足りない
など) は Error、構文として読めない式は SyntaxError にします。メッセージは英語です。NaN を返さずにエラーに
する場合と、確保量と時間の上限は、設計の決まり にあります。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。.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/ を一切含めません。npm stage publish)。トークンは
使いません。npmjs.com のパッケージ設定で、このリポジトリと release.yml を trusted publisher
として指定しているためです。この trusted publisher には公開待ちに入れることだけを許し、直接の
公開は許していません。npm stage approve <id> を実行します。どちらも npm
アカウントの二要素認証が必要です。バージョン 0.5.0 は手作業で公開しました (provenance には CI が必要なので
npm publish --provenance=false)。以降のバージョンは、ワークフローと上の承認を経て公開します。
ローカルのマシンから公開するときも、npm アカウントの二要素認証が必要です。jsDelivr が配信できるように、
リポジトリは公開のままにしておく必要があります。