lme-python 0.1.3__tar.gz → 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. {lme_python-0.1.3 → lme_python-0.1.4}/.github/workflows/benchmarks.yml +63 -0
  2. {lme_python-0.1.3 → lme_python-0.1.4}/.gitignore +2 -0
  3. {lme_python-0.1.3 → lme_python-0.1.4}/BENCHMARKS.md +29 -1
  4. {lme_python-0.1.3 → lme_python-0.1.4}/CHANGELOG.md +12 -0
  5. {lme_python-0.1.3 → lme_python-0.1.4}/CONTRIBUTING.md +3 -3
  6. {lme_python-0.1.3 → lme_python-0.1.4}/Cargo.lock +1 -1
  7. {lme_python-0.1.3 → lme_python-0.1.4}/Cargo.toml +29 -1
  8. {lme_python-0.1.3 → lme_python-0.1.4}/GUIDE.md +5 -5
  9. {lme_python-0.1.3 → lme_python-0.1.4}/PKG-INFO +1 -1
  10. {lme_python-0.1.3 → lme_python-0.1.4}/README.md +15 -8
  11. {lme_python-0.1.3 → lme_python-0.1.4}/RELEASING.md +6 -6
  12. lme_python-0.1.4/docs/benchmarks/app.js +207 -0
  13. lme_python-0.1.4/docs/benchmarks/index.html +92 -0
  14. lme_python-0.1.4/docs/benchmarks/styles.css +305 -0
  15. lme_python-0.1.4/docs/index.html +63 -0
  16. {lme_python-0.1.3 → lme_python-0.1.4}/python/Cargo.lock +2 -2
  17. {lme_python-0.1.3 → lme_python-0.1.4}/python/Cargo.toml +1 -1
  18. {lme_python-0.1.3 → lme_python-0.1.4}/python/PYTHON_GUIDE.md +62 -2
  19. lme_python-0.1.4/python/examples/glmer_cbpp.py +92 -0
  20. lme_python-0.1.4/python/examples/glmer_grouseticks.py +99 -0
  21. lme_python-0.1.4/python/examples/lm_basics.py +84 -0
  22. lme_python-0.1.4/python/examples/lmer_sleepstudy.py +96 -0
  23. lme_python-0.1.4/python/examples/model_comparison.py +117 -0
  24. lme_python-0.1.4/python/src/lib.rs +642 -0
  25. lme_python-0.1.4/python/tests/test_basic.py +406 -0
  26. lme_python-0.1.4/scripts/build_benchmark_site.py +210 -0
  27. {lme_python-0.1.3 → lme_python-0.1.4}/scripts/plot_comparisons.py +2 -2
  28. {lme_python-0.1.3 → lme_python-0.1.4}/scripts/run_cross_language_benchmarks.py +12 -12
  29. {lme_python-0.1.3 → lme_python-0.1.4}/src/family.rs +2 -2
  30. {lme_python-0.1.3 → lme_python-0.1.4}/src/glmm_math.rs +57 -9
  31. {lme_python-0.1.3 → lme_python-0.1.4}/src/lib.rs +198 -18
  32. {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_confint_simulate.rs +51 -1
  33. {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_glmm.rs +47 -0
  34. lme_python-0.1.3/python/src/lib.rs +0 -201
  35. lme_python-0.1.3/python/tests/test_basic.py +0 -164
  36. {lme_python-0.1.3 → lme_python-0.1.4}/.github/workflows/ci.yml +0 -0
  37. {lme_python-0.1.3 → lme_python-0.1.4}/.github/workflows/python-release.yml +0 -0
  38. {lme_python-0.1.3 → lme_python-0.1.4}/.github/workflows/repo-metadata.yml +0 -0
  39. {lme_python-0.1.3 → lme_python-0.1.4}/LICENSE +0 -0
  40. {lme_python-0.1.3 → lme_python-0.1.4}/benches/bench_math.rs +0 -0
  41. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/COMPARISONS.md +0 -0
  42. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/comparison_chart.png +0 -0
  43. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/glmm_cbpp.R +0 -0
  44. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/glmm_cbpp.jl +0 -0
  45. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/glmm_cbpp.py +0 -0
  46. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/glmm_cbpp.rs +0 -0
  47. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/glmm_grouseticks.R +0 -0
  48. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/glmm_grouseticks.jl +0 -0
  49. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/glmm_grouseticks.py +0 -0
  50. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/glmm_grouseticks.rs +0 -0
  51. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/lmm_dyestuff.R +0 -0
  52. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/lmm_dyestuff.jl +0 -0
  53. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/lmm_dyestuff.py +0 -0
  54. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/lmm_dyestuff.rs +0 -0
  55. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/lmm_pastes.R +0 -0
  56. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/lmm_pastes.jl +0 -0
  57. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/lmm_pastes.py +0 -0
  58. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/lmm_pastes.rs +0 -0
  59. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/lmm_penicillin.R +0 -0
  60. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/lmm_penicillin.jl +0 -0
  61. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/lmm_penicillin.py +0 -0
  62. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/lmm_penicillin.rs +0 -0
  63. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/sleepstudy.R +0 -0
  64. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/sleepstudy.jl +0 -0
  65. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/sleepstudy.py +0 -0
  66. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/sleepstudy.rs +0 -0
  67. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/sleepstudy_ml.R +0 -0
  68. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/sleepstudy_ml.jl +0 -0
  69. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/sleepstudy_ml.py +0 -0
  70. {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/sleepstudy_ml.rs +0 -0
  71. {lme_python-0.1.3 → lme_python-0.1.4}/pyproject.toml +0 -0
  72. {lme_python-0.1.3 → lme_python-0.1.4}/scripts/ast_explorations/test_fiasto.rs +0 -0
  73. {lme_python-0.1.3 → lme_python-0.1.4}/scripts/ast_explorations/test_fiasto_offset.rs +0 -0
  74. {lme_python-0.1.3 → lme_python-0.1.4}/scripts/dump_dyestuff.R +0 -0
  75. {lme_python-0.1.3 → lme_python-0.1.4}/scripts/dump_pastes.R +0 -0
  76. {lme_python-0.1.3 → lme_python-0.1.4}/scripts/make_penicillin_csv.py +0 -0
  77. {lme_python-0.1.3 → lme_python-0.1.4}/scripts/sync_github_repo_metadata.py +0 -0
  78. {lme_python-0.1.3 → lme_python-0.1.4}/src/anova.rs +0 -0
  79. {lme_python-0.1.3 → lme_python-0.1.4}/src/formula.rs +0 -0
  80. {lme_python-0.1.3 → lme_python-0.1.4}/src/kenward_roger.rs +0 -0
  81. {lme_python-0.1.3 → lme_python-0.1.4}/src/math.rs +0 -0
  82. {lme_python-0.1.3 → lme_python-0.1.4}/src/model_matrix.rs +0 -0
  83. {lme_python-0.1.3 → lme_python-0.1.4}/src/optimizer.rs +0 -0
  84. {lme_python-0.1.3 → lme_python-0.1.4}/src/robust.rs +0 -0
  85. {lme_python-0.1.3 → lme_python-0.1.4}/src/satterthwaite.rs +0 -0
  86. {lme_python-0.1.3 → lme_python-0.1.4}/tests/data/cbpp_binary.csv +0 -0
  87. {lme_python-0.1.3 → lme_python-0.1.4}/tests/data/dyestuff.csv +0 -0
  88. {lme_python-0.1.3 → lme_python-0.1.4}/tests/data/glmm_binomial.json +0 -0
  89. {lme_python-0.1.3 → lme_python-0.1.4}/tests/data/glmm_poisson.json +0 -0
  90. {lme_python-0.1.3 → lme_python-0.1.4}/tests/data/grouseticks.csv +0 -0
  91. {lme_python-0.1.3 → lme_python-0.1.4}/tests/data/intercept_only.json +0 -0
  92. {lme_python-0.1.3 → lme_python-0.1.4}/tests/data/mock_crossed.json +0 -0
  93. {lme_python-0.1.3 → lme_python-0.1.4}/tests/data/mock_ml.json +0 -0
  94. {lme_python-0.1.3 → lme_python-0.1.4}/tests/data/pastes.csv +0 -0
  95. {lme_python-0.1.3 → lme_python-0.1.4}/tests/data/penicillin.csv +0 -0
  96. {lme_python-0.1.3 → lme_python-0.1.4}/tests/data/penicillin.json +0 -0
  97. {lme_python-0.1.3 → lme_python-0.1.4}/tests/data/random_slopes.json +0 -0
  98. {lme_python-0.1.3 → lme_python-0.1.4}/tests/data/sleepstudy.csv +0 -0
  99. {lme_python-0.1.3 → lme_python-0.1.4}/tests/generate_mock_data.py +0 -0
  100. {lme_python-0.1.3 → lme_python-0.1.4}/tests/generate_test_data.R +0 -0
  101. {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_anova.rs +0 -0
  102. {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_conditional_real.rs +0 -0
  103. {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_coverage_edge_cases.rs +0 -0
  104. {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_coverage_gaps.rs +0 -0
  105. {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_crossed_mock.rs +0 -0
  106. {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_e2e_lmer.rs +0 -0
  107. {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_edge_cases.rs +0 -0
  108. {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_errors.rs +0 -0
  109. {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_formula.rs +0 -0
  110. {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_gaps.rs +0 -0
  111. {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_intercept_only.rs +0 -0
  112. {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_kenward_roger.rs +0 -0
  113. {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_ml_optimization.rs +0 -0
  114. {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_no_intercept.rs +0 -0
  115. {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_numerical_parity.rs +0 -0
  116. {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_predict.rs +0 -0
  117. {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_random_slopes.rs +0 -0
  118. {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_robust.rs +0 -0
  119. {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_sandwich_math.R +0 -0
  120. {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_satterthwaite.rs +0 -0
@@ -17,6 +17,8 @@ on:
17
17
 
18
18
  permissions:
19
19
  contents: write
20
+ pages: write
21
+ id-token: write
20
22
 
21
23
  env:
22
24
  CARGO_TERM_COLOR: always
@@ -169,3 +171,64 @@ jobs:
169
171
  run: |
170
172
  echo "Cross-language benchmark script failed with exit code ${{ steps.cross_language_run.outputs.status }}." >&2
171
173
  exit 1
174
+
175
+ pages:
176
+ name: Publish Benchmark Dashboard
177
+ runs-on: ubuntu-latest
178
+ needs:
179
+ - criterion
180
+ - cross-language
181
+ if: needs.criterion.result == 'success' && needs.cross-language.result == 'success' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/'))
182
+ environment:
183
+ name: github-pages
184
+ url: ${{ steps.deploy.outputs.page_url }}
185
+ steps:
186
+ - uses: actions/checkout@v6
187
+
188
+ - uses: actions/setup-python@v6
189
+ with:
190
+ python-version: "3.11"
191
+
192
+ - name: Download cross-language artifact
193
+ uses: actions/download-artifact@v5
194
+ with:
195
+ name: cross-language-${{ github.sha }}
196
+ path: site-input
197
+
198
+ - name: Resolve release URL
199
+ id: release_url
200
+ run: |
201
+ if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
202
+ echo "value=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/tag/${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
203
+ else
204
+ echo "value=" >> "$GITHUB_OUTPUT"
205
+ fi
206
+
207
+ - name: Build benchmark dashboard
208
+ run: |
209
+ set -euo pipefail
210
+ mkdir -p site
211
+ cp -R docs/. site/
212
+ mkdir -p site/benchmarks/data
213
+ cross_json="$(find site-input -name 'cross-language-*.json' -print -quit)"
214
+ python scripts/build_benchmark_site.py \
215
+ --cross-language-json "$cross_json" \
216
+ --output-dir "site/benchmarks/data" \
217
+ --run-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
218
+ --release-url "${{ steps.release_url.outputs.value }}" \
219
+ --criterion-asset-name "criterion-${GITHUB_SHA}.tar.gz" \
220
+ --cross-language-asset-name "cross-language-${GITHUB_SHA}.json" \
221
+ --ref-name "${GITHUB_REF_NAME}"
222
+
223
+ - uses: actions/configure-pages@v5
224
+ with:
225
+ token: ${{ secrets.REPO_ADMIN_TOKEN != '' && secrets.REPO_ADMIN_TOKEN || github.token }}
226
+ enablement: ${{ secrets.REPO_ADMIN_TOKEN != '' }}
227
+
228
+ - uses: actions/upload-pages-artifact@v3
229
+ with:
230
+ path: site
231
+
232
+ - name: Deploy to GitHub Pages
233
+ id: deploy
234
+ uses: actions/deploy-pages@v4
@@ -6,6 +6,8 @@ julia-portable.zip
6
6
  flamegraph.svg
7
7
  artifacts/
8
8
  benchmark-results/
9
+ benchmark-site/
10
+ site/
9
11
 
10
12
  # Python bindings
11
13
  python/target/
@@ -82,7 +82,7 @@ Recommended rules:
82
82
  - note when optimizers are not directly comparable across ecosystems
83
83
  - keep output artifacts or raw logs so later releases can be compared to the same baseline
84
84
 
85
- For this repo, the most defensible cross-language benchmark set would start with the same reference datasets already used in `examples/COMPARISONS.md`:
85
+ For this repo, the most defensible cross-language benchmark set would start with the same reference datasets already used in `comparisons/COMPARISONS.md`:
86
86
 
87
87
  - `sleepstudy`
88
88
  - `dyestuff`
@@ -122,6 +122,34 @@ The repository also includes a dedicated workflow in [.github/workflows/benchmar
122
122
  - runs the cross-language benchmark script
123
123
  - uploads the resulting artifacts in CI
124
124
  - attaches them to GitHub Releases on tag pushes
125
+ - publishes the latest benchmark dashboard to GitHub Pages at `https://x4g4p3x.github.io/lme-rs/benchmarks/`
126
+
127
+ ## Latest published results
128
+
129
+ The latest published benchmark artifacts are attached to the [v0.1.3 release](https://github.com/x4g4p3x/lme-rs/releases/tag/v0.1.3).
130
+
131
+ That release currently includes:
132
+
133
+ - `criterion-e8a82e0b97b88c5549bc61e89c22dc12c9060a02.tar.gz`
134
+ - `cross-language-e8a82e0b97b88c5549bc61e89c22dc12c9060a02.json`
135
+
136
+ The cross-language JSON includes representative timings for:
137
+
138
+ - `sleepstudy`
139
+ - `pastes`
140
+ - `cbpp`
141
+ - `grouseticks`
142
+
143
+ across:
144
+
145
+ - Rust
146
+ - Python
147
+ - R
148
+ - Julia
149
+
150
+ Treat those numbers as versioned release artifacts, not as universal constants. They were produced on GitHub-hosted runners with the workflow's pinned toolchain setup, so they are useful for release-to-release comparison and public transparency, but not as machine-independent proof of absolute speed.
151
+
152
+ If you want to cite benchmark results in release notes or external docs, prefer linking the release asset directly instead of copying raw numbers into the README. That keeps the landing page stable while still making the measured outputs inspectable.
125
153
 
126
154
  ## How to interpret results
127
155
 
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.4] - 2026-03-22
9
+
10
+ ### Added
11
+
12
+ - Formula-string entry point `lm()` and corresponding Python binding for fitting ordinary least squares models using DataFrames.
13
+ - New Python examples directory `python/examples` with five runnable demonstration scripts mapping to common mixed-modeling workflows.
14
+
15
+ ### Changed
16
+
17
+ - Renamed `examples/` directory to `comparisons/` to clarify that it contains cross-language parity scripts, updating all build and documentation references accordingly.
18
+ - Promoted Kenward-Roger degrees of freedom from "provisional" status after validating numerical parity (within 0.01 df) against R's `pbkrtest`.
19
+
8
20
  ## [0.1.3] - 2026-03-12
9
21
 
10
22
  ### Added
@@ -6,7 +6,7 @@ This repository contains:
6
6
 
7
7
  - the Rust crate in the repository root
8
8
  - the Python bindings in `python/`
9
- - cross-language comparison examples in `examples/`
9
+ - cross-language comparison scripts in `comparisons/`
10
10
  - regression fixtures and integration tests in `tests/`
11
11
 
12
12
  ## Local setup
@@ -50,7 +50,7 @@ If you change fitting logic, optimizer behavior, variance calculations, or infer
50
50
 
51
51
  - add or update Rust tests in `tests/`
52
52
  - prefer fixture-backed tests for parity-sensitive behavior
53
- - update `examples/COMPARISONS.md` when the reference output changes materially
53
+ - update `comparisons/COMPARISONS.md` when the reference output changes materially
54
54
  - validate hard cases against R `lme4` where practical
55
55
 
56
56
  Relevant files and directories:
@@ -58,7 +58,7 @@ Relevant files and directories:
58
58
  - `src/` for the crate implementation
59
59
  - `tests/data/` for fixture inputs
60
60
  - `tests/generate_test_data.R` for R-backed fixture generation
61
- - `examples/` for end-to-end language comparisons
61
+ - `comparisons/` for cross-language parity scripts (R, Python/statsmodels, Julia)
62
62
 
63
63
  ## Working on documentation
64
64
 
@@ -1528,7 +1528,7 @@ checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
1528
1528
 
1529
1529
  [[package]]
1530
1530
  name = "lme-rs"
1531
- version = "0.1.3"
1531
+ version = "0.1.4"
1532
1532
  dependencies = [
1533
1533
  "anyhow",
1534
1534
  "argmin",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "lme-rs"
3
- version = "0.1.3"
3
+ version = "0.1.4"
4
4
  edition = "2021"
5
5
  authors = ["Martin Huck <x4g4p3x@users.noreply.github.com>"]
6
6
  description = "Rust port of R's lme4: linear mixed-effects models with 1:1 numerical compatibility"
@@ -40,6 +40,34 @@ debug = true
40
40
  name = "bench_math"
41
41
  harness = false
42
42
 
43
+ [[example]]
44
+ name = "glmm_cbpp"
45
+ path = "comparisons/glmm_cbpp.rs"
46
+
47
+ [[example]]
48
+ name = "glmm_grouseticks"
49
+ path = "comparisons/glmm_grouseticks.rs"
50
+
51
+ [[example]]
52
+ name = "lmm_dyestuff"
53
+ path = "comparisons/lmm_dyestuff.rs"
54
+
55
+ [[example]]
56
+ name = "lmm_pastes"
57
+ path = "comparisons/lmm_pastes.rs"
58
+
59
+ [[example]]
60
+ name = "lmm_penicillin"
61
+ path = "comparisons/lmm_penicillin.rs"
62
+
63
+ [[example]]
64
+ name = "sleepstudy"
65
+ path = "comparisons/sleepstudy.rs"
66
+
67
+ [[example]]
68
+ name = "sleepstudy_ml"
69
+ path = "comparisons/sleepstudy_ml.rs"
70
+
43
71
  [target.'cfg(target_arch = "x86_64")'.dependencies]
44
72
  ndarray-linalg = { version = "0.17", features = ["intel-mkl-static"] }
45
73
 
@@ -270,7 +270,7 @@ fit.with_kenward_roger(&df)?;
270
270
  println!("{}", fit);
271
271
  ```
272
272
 
273
- This API exists and is tested at the interface level, but the current implementation is still provisional. Treat it as an experimental path rather than as a drop-in replacement for the full `pbkrtest` stack.
273
+ The Kenward-Roger path produces denominator degrees of freedom that match R's `pbkrtest` on the covered LMM configurations. As with Satterthwaite, results are derived via numerical differentiation of the REML objective.
274
274
 
275
275
  ### Robust standard errors
276
276
 
@@ -317,7 +317,7 @@ println!("{}", lrt);
317
317
 
318
318
  `lme-rs` is designed to track `lme4` behavior closely on the covered workflows. The strongest evidence for parity is in the repository tests and comparison fixtures, not in a blanket claim that every `lme4` feature is already mirrored.
319
319
 
320
- See [examples/COMPARISONS.md](examples/COMPARISONS.md) for concrete side-by-side output.
320
+ See [comparisons/COMPARISONS.md](comparisons/COMPARISONS.md) for concrete side-by-side output.
321
321
 
322
322
  ### GLMM information criteria
323
323
 
@@ -329,7 +329,7 @@ Fixed-effects ANOVA support currently means Type III tests over the current 1-Do
329
329
 
330
330
  ### Kenward-Roger status
331
331
 
332
- The public API exposes Kenward-Roger, but the implementation is still provisional. If your workflow is publication-critical, validate against your R reference outputs before relying on it.
332
+ `with_kenward_roger()` produces denominator degrees of freedom that match R's `pbkrtest` on the covered LMM configurations. Numerical precision is consistent with the finite-difference Hessian used by the implementation; results have been validated against the `sleepstudy` reference to within 0.01 df. As always, validating against an R reference is sensible for any publication-critical workflow.
333
333
 
334
334
  ### Python bindings
335
335
 
@@ -367,7 +367,7 @@ If you see dimension mismatch or underdetermined-system errors, inspect the effe
367
367
  - whether you fit random slopes as well as intercepts
368
368
  - how well-scaled the predictors are for optimization
369
369
 
370
- For concrete examples and parity outputs, use the scripts and datasets in `examples/` and `tests/data/`.
370
+ For concrete parity outputs, use the scripts and datasets in `comparisons/` and `tests/data/`.
371
371
 
372
372
  ## API Surface Summary
373
373
 
@@ -392,7 +392,7 @@ For concrete examples and parity outputs, use the scripts and datasets in `examp
392
392
  | `confint(level)` | Wald confidence intervals |
393
393
  | `simulate(nsim)` | parametric simulation |
394
394
  | `with_satterthwaite(data)` | Satterthwaite degrees of freedom and p-values |
395
- | `with_kenward_roger(data)` | provisional Kenward-Roger path |
395
+ | `with_kenward_roger(data)` | Kenward-Roger denominator degrees of freedom and p-values |
396
396
  | `with_robust_se(data, cluster_col)` | robust or cluster-robust standard errors |
397
397
  | `anova(ddf_method)` | Type III fixed-effects ANOVA table |
398
398
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lme_python
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
@@ -14,7 +14,7 @@
14
14
  - `glmer()` for binomial, poisson, gaussian, and gamma mixed models
15
15
  - Wilkinson formulas with nested and crossed random effects
16
16
  - Population-level and conditional prediction APIs
17
- - Wald confidence intervals, parametric simulation, robust standard errors, Satterthwaite degrees of freedom, and a provisional Kenward-Roger path
17
+ - Wald confidence intervals, parametric simulation, robust standard errors, Satterthwaite degrees of freedom, and Kenward-Roger denominator degrees of freedom
18
18
  - Likelihood ratio tests between nested models and Type III ANOVA tables for 1-DoF fixed effects
19
19
 
20
20
  ## Quick start
@@ -24,7 +24,7 @@ cargo add lme-rs
24
24
  ```
25
25
 
26
26
  ```rust
27
- use lme_rs::lmer;
27
+ use lme_rs::{lm_df, lmer};
28
28
  use polars::prelude::*;
29
29
 
30
30
  fn main() -> anyhow::Result<()> {
@@ -34,8 +34,13 @@ fn main() -> anyhow::Result<()> {
34
34
  .into_reader_with_file_handle(&mut file)
35
35
  .finish()?;
36
36
 
37
- let fit = lmer("Reaction ~ Days + (Days | Subject)", &df, true)?;
38
- println!("{}", fit);
37
+ // Fixed-effects-only OLS (formula + DataFrame, no random effects)
38
+ let ols = lm_df("Reaction ~ Days", &df)?;
39
+ println!("{}", ols);
40
+
41
+ // Linear mixed model
42
+ let mixed = lmer("Reaction ~ Days + (Days | Subject)", &df, true)?;
43
+ println!("{}", mixed);
39
44
 
40
45
  Ok(())
41
46
  }
@@ -51,14 +56,14 @@ fn main() -> anyhow::Result<()> {
51
56
 
52
57
  ## Current status
53
58
 
54
- The core modeling surface is in place and exercised by the test suite, examples, and cross-language comparisons in [examples/COMPARISONS.md](examples/COMPARISONS.md). The crate is usable today, but some features are intentionally narrower than the R ecosystem wrappers they resemble.
59
+ The core modeling surface is in place and exercised by the test suite, examples, and cross-language comparisons in [comparisons/COMPARISONS.md](comparisons/COMPARISONS.md). The crate is usable today, but some features are intentionally narrower than the R ecosystem wrappers they resemble.
55
60
 
56
61
  ## Limitations and compatibility notes
57
62
 
58
63
  - Numerical parity is the goal for the covered LMM and GLMM workflows, but the guarantee is scoped to the models and examples exercised by the repository tests and comparison fixtures.
59
64
  - `glmer()` uses a Laplace approximation. Absolute AIC, BIC, and log-likelihood values can differ from R because `lme-rs` optimizes a deviance expression that omits data-dependent constants. Coefficients and variance parameters are the quantities to compare.
60
65
  - Fixed-effects ANOVA support is currently Type III only, and only for the current 1-DoF fixed-effect design produced by the parser.
61
- - `with_kenward_roger()` is available, but the current implementation should be treated as provisional rather than as a mature independent reimplementation of `pbkrtest`.
66
+ - `with_kenward_roger()` produces denominator degrees of freedom that match R's `pbkrtest` to within the precision of numerical differentiation on the covered LMM models.
62
67
  - The Rust crate exposes a broader surface than the Python bindings. The Python package is useful, but it is not yet a full mirror of the Rust API.
63
68
  - Built-in GLMM families currently use their default links through the public `glmer()` API.
64
69
 
@@ -67,15 +72,17 @@ The core modeling surface is in place and exercised by the test suite, examples,
67
72
  - Rust API docs: [docs.rs](https://docs.rs/lme-rs/latest/lme_rs/)
68
73
  - Rust usage guide: [GUIDE.md](GUIDE.md)
69
74
  - Python bindings guide: [python/PYTHON_GUIDE.md](python/PYTHON_GUIDE.md)
70
- - Cross-language numerical comparisons: [examples/COMPARISONS.md](examples/COMPARISONS.md)
75
+ - Cross-language numerical comparisons: [comparisons/COMPARISONS.md](comparisons/COMPARISONS.md)
76
+ - Published benchmark dashboard: [GitHub Pages](https://x4g4p3x.github.io/lme-rs/benchmarks/)
71
77
  - Benchmark scope and methodology: [BENCHMARKS.md](BENCHMARKS.md)
78
+ - Latest benchmark artifacts: [v0.1.3 release](https://github.com/x4g4p3x/lme-rs/releases/tag/v0.1.3)
72
79
  - Release history: [CHANGELOG.md](CHANGELOG.md)
73
80
  - Contributor setup: [CONTRIBUTING.md](CONTRIBUTING.md)
74
81
  - Release workflow: [RELEASING.md](RELEASING.md)
75
82
 
76
83
  ## Examples
77
84
 
78
- The `examples/` directory includes end-to-end fits for common reference datasets:
85
+ The `comparisons/` directory contains cross-language reference fits for common datasets:
79
86
 
80
87
  - `sleepstudy`
81
88
  - `dyestuff`
@@ -62,7 +62,7 @@ Update all user-visible versioned surfaces together.
62
62
 
63
63
  - update `CHANGELOG.md`
64
64
  - update any README or guide snippets that mention a specific version
65
- - review `examples/COMPARISONS.md` if release notes depend on changed outputs or behavior
65
+ - review `comparisons/COMPARISONS.md` if release notes depend on changed outputs or behavior
66
66
 
67
67
  ## Benchmarks before release
68
68
 
@@ -83,14 +83,14 @@ python scripts/run_cross_language_benchmarks.py
83
83
  ## Git workflow
84
84
 
85
85
  1. Ensure the worktree is clean enough to understand what is being released.
86
- 2. Commit the release changes.
87
- 3. Create an annotated tag:
86
+ 1. Commit the release changes.
87
+ 1. Create an annotated tag:
88
88
 
89
89
  ```bash
90
90
  git tag -a v0.1.3 -m "Release v0.1.3"
91
91
  ```
92
92
 
93
- 4. Push the branch and the tag:
93
+ 1. Push the branch and the tag:
94
94
 
95
95
  ```bash
96
96
  git push origin master
@@ -124,7 +124,7 @@ If a release changes the crate description, homepage, keywords, or categories, v
124
124
 
125
125
  ## Post-release verification
126
126
 
127
- ### Rust crate
127
+ ### Rust crate (post-release)
128
128
 
129
129
  Verify the new crate version appears on crates.io and docs.rs.
130
130
 
@@ -134,7 +134,7 @@ Check:
134
134
  - README rendering on crates.io is sane
135
135
  - docs.rs builds and links resolve
136
136
 
137
- ### Python package
137
+ ### Python package (post-release)
138
138
 
139
139
  Verify the PyPI release and GitHub Release artifacts.
140
140
 
@@ -0,0 +1,207 @@
1
+ function formatSeconds(seconds) {
2
+ if (seconds >= 10) {
3
+ return `${seconds.toFixed(2)} s`;
4
+ }
5
+ if (seconds >= 1) {
6
+ return `${seconds.toFixed(3)} s`;
7
+ }
8
+ return `${(seconds * 1000).toFixed(2)} ms`;
9
+ }
10
+
11
+ function formatRatio(value) {
12
+ if (value == null) {
13
+ return "n/a";
14
+ }
15
+ return `${value.toFixed(2)}x`;
16
+ }
17
+
18
+ function titleCase(value) {
19
+ return value.charAt(0).toUpperCase() + value.slice(1);
20
+ }
21
+
22
+ function renderHeroLinks(data) {
23
+ const heroLinks = document.getElementById("hero-links");
24
+ const links = [];
25
+
26
+ if (data.run_url) {
27
+ links.push({ href: data.run_url, label: "Workflow run" });
28
+ }
29
+ if (data.release_url) {
30
+ links.push({ href: data.release_url, label: "GitHub release" });
31
+ if (data.asset_urls.cross_language) {
32
+ links.push({
33
+ href: data.asset_urls.cross_language,
34
+ label: "Cross-language JSON",
35
+ });
36
+ }
37
+ if (data.asset_urls.criterion) {
38
+ links.push({
39
+ href: data.asset_urls.criterion,
40
+ label: "Criterion archive",
41
+ });
42
+ }
43
+ }
44
+
45
+ heroLinks.replaceChildren(
46
+ ...links.map((link) => {
47
+ const anchor = document.createElement("a");
48
+ anchor.href = link.href;
49
+ anchor.textContent = link.label;
50
+ anchor.target = "_blank";
51
+ anchor.rel = "noreferrer";
52
+ return anchor;
53
+ }),
54
+ );
55
+ }
56
+
57
+ function renderSummary(data) {
58
+ const runMeta = document.getElementById("run-meta");
59
+ const generatedAt = new Date(data.generated_at).toLocaleString(undefined, {
60
+ dateStyle: "medium",
61
+ timeStyle: "short",
62
+ });
63
+ runMeta.textContent = `Generated ${generatedAt} for ${data.ref_name || data.git_sha.slice(0, 7)}.`;
64
+
65
+ const statsGrid = document.getElementById("stats-grid");
66
+ const stats = [
67
+ ["Cases", data.config.cases.length],
68
+ ["Implementations", data.config.implementations.length],
69
+ ["Warmups", data.config.warmups],
70
+ ["Repeats", data.config.repeats],
71
+ ["CPU count", data.machine_info.cpu_count],
72
+ ["Git SHA", data.git_sha.slice(0, 12)],
73
+ ];
74
+
75
+ statsGrid.replaceChildren(
76
+ ...stats.map(([label, value]) => {
77
+ const card = document.createElement("div");
78
+ card.className = "stat-card";
79
+
80
+ const labelNode = document.createElement("span");
81
+ labelNode.className = "stat-label";
82
+ labelNode.textContent = label;
83
+
84
+ const valueNode = document.createElement("div");
85
+ valueNode.className = "stat-value";
86
+ valueNode.textContent = String(value);
87
+
88
+ card.append(labelNode, valueNode);
89
+ return card;
90
+ }),
91
+ );
92
+ }
93
+
94
+ function renderRatioSummary(data) {
95
+ const container = document.getElementById("ratio-summary");
96
+ const nonRust = data.implementation_summary.filter(
97
+ (item) => item.implementation !== "rust",
98
+ );
99
+ const maxRatio = Math.max(
100
+ ...nonRust.map((item) => item.geometric_mean_relative_to_rust || 0),
101
+ 1,
102
+ );
103
+
104
+ container.replaceChildren(
105
+ ...data.implementation_summary.map((item) => {
106
+ const row = document.createElement("div");
107
+ row.className = "ratio-row";
108
+
109
+ const label = document.createElement("div");
110
+ label.textContent = titleCase(item.implementation);
111
+
112
+ const track = document.createElement("div");
113
+ track.className = "ratio-track";
114
+ const fill = document.createElement("div");
115
+ fill.className = "ratio-fill";
116
+ const ratio = item.geometric_mean_relative_to_rust || 1;
117
+ fill.style.width = `${Math.max((ratio / maxRatio) * 100, 3)}%`;
118
+ track.append(fill);
119
+
120
+ const value = document.createElement("div");
121
+ if (item.implementation === "rust") {
122
+ value.textContent = "baseline";
123
+ } else {
124
+ value.textContent = `${formatRatio(ratio)} slower`;
125
+ }
126
+
127
+ row.append(label, track, value);
128
+ return row;
129
+ }),
130
+ );
131
+ }
132
+
133
+ function renderCases(data) {
134
+ const caseGrid = document.getElementById("case-grid");
135
+ const caseTemplate = document.getElementById("case-card-template");
136
+ const rowTemplate = document.getElementById("bar-row-template");
137
+
138
+ caseGrid.replaceChildren(
139
+ ...data.cases.map((caseData) => {
140
+ const fragment = caseTemplate.content.cloneNode(true);
141
+ fragment.querySelector(".case-title").textContent = caseData.case;
142
+ fragment.querySelector(".case-fastest").textContent =
143
+ caseData.fastest_implementation
144
+ ? `Fastest in this run: ${titleCase(caseData.fastest_implementation)}`
145
+ : "No successful measurements";
146
+
147
+ const bars = fragment.querySelector(".bars");
148
+ caseData.entries.forEach((entry) => {
149
+ const row = rowTemplate.content.cloneNode(true);
150
+ row.querySelector(".bar-label").textContent = entry.implementation;
151
+ const fill = row.querySelector(".bar-fill");
152
+ fill.classList.add(entry.implementation);
153
+ fill.style.width = `${Math.max(entry.width_fraction * 100, 2)}%`;
154
+ const value = row.querySelector(".bar-value");
155
+ const ratioSuffix =
156
+ entry.relative_to_rust && entry.implementation !== "rust"
157
+ ? ` (${formatRatio(entry.relative_to_rust)} slower)`
158
+ : "";
159
+ value.textContent = `${formatSeconds(entry.median_seconds)}${ratioSuffix}`;
160
+ bars.append(row);
161
+ });
162
+
163
+ return fragment;
164
+ }),
165
+ );
166
+ }
167
+
168
+ function renderEnvironment(data) {
169
+ const list = document.getElementById("environment-list");
170
+ const entries = [
171
+ ["Platform", data.machine_info.platform],
172
+ ["Machine", data.machine_info.machine],
173
+ ["Rust", data.runtime_versions.rustc],
174
+ ["Python", data.runtime_versions.python],
175
+ ["R", data.runtime_versions.Rscript],
176
+ ["Julia", data.runtime_versions.julia],
177
+ ];
178
+
179
+ list.replaceChildren(
180
+ ...entries.flatMap(([term, description]) => {
181
+ const dt = document.createElement("dt");
182
+ dt.textContent = term;
183
+ const dd = document.createElement("dd");
184
+ dd.textContent = description || "n/a";
185
+ return [dt, dd];
186
+ }),
187
+ );
188
+ }
189
+
190
+ async function main() {
191
+ const response = await fetch("./data/latest.json", { cache: "no-store" });
192
+ if (!response.ok) {
193
+ throw new Error(`Failed to load benchmark data: ${response.status}`);
194
+ }
195
+
196
+ const data = await response.json();
197
+ renderHeroLinks(data);
198
+ renderSummary(data);
199
+ renderRatioSummary(data);
200
+ renderCases(data);
201
+ renderEnvironment(data);
202
+ }
203
+
204
+ main().catch((error) => {
205
+ const runMeta = document.getElementById("run-meta");
206
+ runMeta.textContent = error.message;
207
+ });