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.
- {lme_python-0.1.3 → lme_python-0.1.4}/.github/workflows/benchmarks.yml +63 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/.gitignore +2 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/BENCHMARKS.md +29 -1
- {lme_python-0.1.3 → lme_python-0.1.4}/CHANGELOG.md +12 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/CONTRIBUTING.md +3 -3
- {lme_python-0.1.3 → lme_python-0.1.4}/Cargo.lock +1 -1
- {lme_python-0.1.3 → lme_python-0.1.4}/Cargo.toml +29 -1
- {lme_python-0.1.3 → lme_python-0.1.4}/GUIDE.md +5 -5
- {lme_python-0.1.3 → lme_python-0.1.4}/PKG-INFO +1 -1
- {lme_python-0.1.3 → lme_python-0.1.4}/README.md +15 -8
- {lme_python-0.1.3 → lme_python-0.1.4}/RELEASING.md +6 -6
- lme_python-0.1.4/docs/benchmarks/app.js +207 -0
- lme_python-0.1.4/docs/benchmarks/index.html +92 -0
- lme_python-0.1.4/docs/benchmarks/styles.css +305 -0
- lme_python-0.1.4/docs/index.html +63 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/python/Cargo.lock +2 -2
- {lme_python-0.1.3 → lme_python-0.1.4}/python/Cargo.toml +1 -1
- {lme_python-0.1.3 → lme_python-0.1.4}/python/PYTHON_GUIDE.md +62 -2
- lme_python-0.1.4/python/examples/glmer_cbpp.py +92 -0
- lme_python-0.1.4/python/examples/glmer_grouseticks.py +99 -0
- lme_python-0.1.4/python/examples/lm_basics.py +84 -0
- lme_python-0.1.4/python/examples/lmer_sleepstudy.py +96 -0
- lme_python-0.1.4/python/examples/model_comparison.py +117 -0
- lme_python-0.1.4/python/src/lib.rs +642 -0
- lme_python-0.1.4/python/tests/test_basic.py +406 -0
- lme_python-0.1.4/scripts/build_benchmark_site.py +210 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/scripts/plot_comparisons.py +2 -2
- {lme_python-0.1.3 → lme_python-0.1.4}/scripts/run_cross_language_benchmarks.py +12 -12
- {lme_python-0.1.3 → lme_python-0.1.4}/src/family.rs +2 -2
- {lme_python-0.1.3 → lme_python-0.1.4}/src/glmm_math.rs +57 -9
- {lme_python-0.1.3 → lme_python-0.1.4}/src/lib.rs +198 -18
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_confint_simulate.rs +51 -1
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_glmm.rs +47 -0
- lme_python-0.1.3/python/src/lib.rs +0 -201
- lme_python-0.1.3/python/tests/test_basic.py +0 -164
- {lme_python-0.1.3 → lme_python-0.1.4}/.github/workflows/ci.yml +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/.github/workflows/python-release.yml +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/.github/workflows/repo-metadata.yml +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/LICENSE +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/benches/bench_math.rs +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/COMPARISONS.md +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/comparison_chart.png +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/glmm_cbpp.R +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/glmm_cbpp.jl +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/glmm_cbpp.py +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/glmm_cbpp.rs +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/glmm_grouseticks.R +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/glmm_grouseticks.jl +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/glmm_grouseticks.py +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/glmm_grouseticks.rs +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/lmm_dyestuff.R +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/lmm_dyestuff.jl +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/lmm_dyestuff.py +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/lmm_dyestuff.rs +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/lmm_pastes.R +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/lmm_pastes.jl +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/lmm_pastes.py +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/lmm_pastes.rs +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/lmm_penicillin.R +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/lmm_penicillin.jl +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/lmm_penicillin.py +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/lmm_penicillin.rs +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/sleepstudy.R +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/sleepstudy.jl +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/sleepstudy.py +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/sleepstudy.rs +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/sleepstudy_ml.R +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/sleepstudy_ml.jl +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/sleepstudy_ml.py +0 -0
- {lme_python-0.1.3/examples → lme_python-0.1.4/comparisons}/sleepstudy_ml.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/pyproject.toml +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/scripts/ast_explorations/test_fiasto.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/scripts/ast_explorations/test_fiasto_offset.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/scripts/dump_dyestuff.R +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/scripts/dump_pastes.R +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/scripts/make_penicillin_csv.py +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/scripts/sync_github_repo_metadata.py +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/src/anova.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/src/formula.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/src/kenward_roger.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/src/math.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/src/model_matrix.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/src/optimizer.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/src/robust.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/src/satterthwaite.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/data/cbpp_binary.csv +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/data/dyestuff.csv +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/data/glmm_binomial.json +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/data/glmm_poisson.json +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/data/grouseticks.csv +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/data/intercept_only.json +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/data/mock_crossed.json +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/data/mock_ml.json +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/data/pastes.csv +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/data/penicillin.csv +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/data/penicillin.json +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/data/random_slopes.json +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/data/sleepstudy.csv +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/generate_mock_data.py +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/generate_test_data.R +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_anova.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_conditional_real.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_coverage_edge_cases.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_coverage_gaps.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_crossed_mock.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_e2e_lmer.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_edge_cases.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_errors.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_formula.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_gaps.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_intercept_only.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_kenward_roger.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_ml_optimization.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_no_intercept.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_numerical_parity.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_predict.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_random_slopes.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_robust.rs +0 -0
- {lme_python-0.1.3 → lme_python-0.1.4}/tests/test_sandwich_math.R +0 -0
- {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
|
|
@@ -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 `
|
|
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
|
|
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 `
|
|
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
|
-
- `
|
|
61
|
+
- `comparisons/` for cross-language parity scripts (R, Python/statsmodels, Julia)
|
|
62
62
|
|
|
63
63
|
## Working on documentation
|
|
64
64
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "lme-rs"
|
|
3
|
-
version = "0.1.
|
|
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
|
-
|
|
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 [
|
|
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
|
-
|
|
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
|
|
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)` |
|
|
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
|
|
|
@@ -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
|
|
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
|
-
|
|
38
|
-
|
|
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 [
|
|
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()`
|
|
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: [
|
|
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 `
|
|
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 `
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
+
});
|