lme-python 0.1.1__tar.gz → 0.1.2__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.1 → lme_python-0.1.2}/Cargo.lock +1 -1
- {lme_python-0.1.1 → lme_python-0.1.2}/Cargo.toml +1 -1
- {lme_python-0.1.1 → lme_python-0.1.2}/PKG-INFO +1 -1
- {lme_python-0.1.1 → lme_python-0.1.2}/benches/bench_math.rs +1 -1
- {lme_python-0.1.1 → lme_python-0.1.2}/python/Cargo.lock +2 -2
- {lme_python-0.1.1 → lme_python-0.1.2}/python/Cargo.toml +1 -1
- {lme_python-0.1.1 → lme_python-0.1.2}/python/PYTHON_GUIDE.md +37 -3
- lme_python-0.1.2/python/tests/test_basic.py +164 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/src/family.rs +15 -2
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_coverage_edge_cases.rs +0 -2
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_robust.rs +0 -1
- lme_python-0.1.1/python/tests/test_basic.py +0 -27
- {lme_python-0.1.1 → lme_python-0.1.2}/.github/workflows/ci.yml +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/.github/workflows/python-release.yml +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/.gitignore +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/CHANGELOG.md +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/GUIDE.md +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/LICENSE +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/README.md +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/COMPARISONS.md +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/comparison_chart.png +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/glmm_cbpp.R +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/glmm_cbpp.jl +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/glmm_cbpp.py +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/glmm_cbpp.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/glmm_grouseticks.R +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/glmm_grouseticks.jl +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/glmm_grouseticks.py +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/glmm_grouseticks.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/lmm_dyestuff.R +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/lmm_dyestuff.jl +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/lmm_dyestuff.py +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/lmm_dyestuff.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/lmm_pastes.R +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/lmm_pastes.jl +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/lmm_pastes.py +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/lmm_pastes.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/lmm_penicillin.R +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/lmm_penicillin.jl +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/lmm_penicillin.py +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/lmm_penicillin.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/sleepstudy.R +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/sleepstudy.jl +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/sleepstudy.py +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/sleepstudy.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/sleepstudy_ml.R +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/sleepstudy_ml.jl +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/sleepstudy_ml.py +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/examples/sleepstudy_ml.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/pyproject.toml +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/python/src/lib.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/scripts/ast_explorations/test_fiasto.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/scripts/ast_explorations/test_fiasto_offset.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/scripts/dump_dyestuff.R +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/scripts/dump_pastes.R +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/scripts/make_penicillin_csv.py +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/scripts/plot_comparisons.py +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/src/anova.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/src/formula.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/src/glmm_math.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/src/kenward_roger.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/src/lib.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/src/math.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/src/model_matrix.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/src/optimizer.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/src/robust.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/src/satterthwaite.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/data/cbpp_binary.csv +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/data/dyestuff.csv +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/data/glmm_binomial.json +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/data/glmm_poisson.json +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/data/grouseticks.csv +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/data/intercept_only.json +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/data/mock_crossed.json +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/data/mock_ml.json +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/data/pastes.csv +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/data/penicillin.csv +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/data/penicillin.json +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/data/random_slopes.json +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/data/sleepstudy.csv +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/generate_mock_data.py +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/generate_test_data.R +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_anova.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_conditional_real.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_confint_simulate.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_coverage_gaps.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_crossed_mock.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_e2e_lmer.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_edge_cases.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_errors.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_formula.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_gaps.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_glmm.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_intercept_only.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_kenward_roger.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_ml_optimization.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_no_intercept.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_numerical_parity.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_predict.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_random_slopes.rs +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_sandwich_math.R +0 -0
- {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_satterthwaite.rs +0 -0
|
@@ -126,7 +126,7 @@ fn bench_deviance_evaluation(c: &mut Criterion) {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
fn load_csv(path: &str) -> DataFrame {
|
|
129
|
-
let mut file = File::open(path).
|
|
129
|
+
let mut file = File::open(path).unwrap_or_else(|_| panic!("Could not open {}", path));
|
|
130
130
|
CsvReadOptions::default()
|
|
131
131
|
.with_has_header(true)
|
|
132
132
|
.into_reader_with_file_handle(&mut file)
|
|
@@ -1291,7 +1291,7 @@ checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
|
|
1291
1291
|
|
|
1292
1292
|
[[package]]
|
|
1293
1293
|
name = "lme-rs"
|
|
1294
|
-
version = "0.1.
|
|
1294
|
+
version = "0.1.2"
|
|
1295
1295
|
dependencies = [
|
|
1296
1296
|
"anyhow",
|
|
1297
1297
|
"argmin",
|
|
@@ -1313,7 +1313,7 @@ dependencies = [
|
|
|
1313
1313
|
|
|
1314
1314
|
[[package]]
|
|
1315
1315
|
name = "lme_python"
|
|
1316
|
-
version = "0.1.
|
|
1316
|
+
version = "0.1.2"
|
|
1317
1317
|
dependencies = [
|
|
1318
1318
|
"lme-rs",
|
|
1319
1319
|
"polars",
|
|
@@ -166,14 +166,37 @@ df = pl.read_csv("tests/data/sleepstudy.csv")
|
|
|
166
166
|
model = lme_python.lmer("Reaction ~ Days + (Days | Subject)", data=df)
|
|
167
167
|
print(model)
|
|
168
168
|
|
|
169
|
-
#
|
|
169
|
+
# Population-level predictions (fixed effects only)
|
|
170
170
|
newdata = pl.DataFrame({
|
|
171
171
|
"Days": [0.0, 1.0, 5.0, 10.0],
|
|
172
172
|
"Subject": ["308", "308", "308", "308"],
|
|
173
173
|
})
|
|
174
|
-
|
|
175
|
-
print(f"Predictions: {
|
|
174
|
+
preds_pop = model.predict(newdata)
|
|
175
|
+
print(f"Pop Predictions: {preds_pop}")
|
|
176
176
|
# [251.405, 261.872, 303.742, 356.078]
|
|
177
|
+
|
|
178
|
+
# Conditional predictions (fixed + random effects)
|
|
179
|
+
# Set allow_new_levels=True if predicting for unseen groups
|
|
180
|
+
preds_cond = model.predict_conditional(newdata, allow_new_levels=True)
|
|
181
|
+
print(f"Cond Predictions: {preds_cond}")
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Confidence Intervals and Standard Errors
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
import polars as pl
|
|
188
|
+
import lme_python
|
|
189
|
+
|
|
190
|
+
df = pl.read_csv("tests/data/sleepstudy.csv")
|
|
191
|
+
model = lme_python.lmer("Reaction ~ Days + (Days | Subject)", data=df)
|
|
192
|
+
|
|
193
|
+
# Standard errors for fixed effects
|
|
194
|
+
print(f"Standard Errors: {model.std_errors}")
|
|
195
|
+
|
|
196
|
+
# 95% Wald confidence intervals for fixed effects
|
|
197
|
+
# Returns list of tuples: [(lower1, upper1), (lower2, upper2), ...]
|
|
198
|
+
ci = model.confint(level=0.95)
|
|
199
|
+
print(f"95% CI: {ci}")
|
|
177
200
|
```
|
|
178
201
|
|
|
179
202
|
### Intercept-Only Model
|
|
@@ -206,6 +229,17 @@ model = lme_python.glmer(
|
|
|
206
229
|
family_name="binomial"
|
|
207
230
|
)
|
|
208
231
|
print(model)
|
|
232
|
+
|
|
233
|
+
# For GLMMs, predict_response() returns probabilities (Binomial) or rates (Poisson)
|
|
234
|
+
newdata = pl.DataFrame({
|
|
235
|
+
"period2": [1.0, 0.0],
|
|
236
|
+
"period3": [0.0, 1.0],
|
|
237
|
+
"period4": [0.0, 0.0],
|
|
238
|
+
"herd": ["1", "1"]
|
|
239
|
+
})
|
|
240
|
+
# Returns predictions on the response scale [0, 1]
|
|
241
|
+
probs = model.predict_response(newdata)
|
|
242
|
+
print(f"Probabilities: {probs}")
|
|
209
243
|
```
|
|
210
244
|
|
|
211
245
|
### Nested Random Effects
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import polars as pl
|
|
3
|
+
import lme_python
|
|
4
|
+
import math
|
|
5
|
+
|
|
6
|
+
def test_lmer_sleepstudy():
|
|
7
|
+
# Load identical data used in rust tests
|
|
8
|
+
df = pl.read_csv("../tests/data/sleepstudy.csv")
|
|
9
|
+
|
|
10
|
+
# Fit model natively via rust
|
|
11
|
+
model = lme_python.lmer("Reaction ~ Days + (Days | Subject)", data=df, reml=True)
|
|
12
|
+
summary = model.summary()
|
|
13
|
+
|
|
14
|
+
print("\n--- Model Summary ---")
|
|
15
|
+
print(summary)
|
|
16
|
+
|
|
17
|
+
# Check that REML objective is embedded in the summary string
|
|
18
|
+
assert "1743.6" in summary
|
|
19
|
+
assert "611.9033" in summary # Intercept variance
|
|
20
|
+
|
|
21
|
+
# Check properties
|
|
22
|
+
assert model.converged is True
|
|
23
|
+
assert model.num_obs == 180
|
|
24
|
+
assert len(model.coefficients) == 2
|
|
25
|
+
assert model.fixed_names == ["(Intercept)", "Days"]
|
|
26
|
+
assert model.sigma2 is not None
|
|
27
|
+
assert abs(model.sigma2 - 654.94) < 0.1
|
|
28
|
+
assert model.log_likelihood is not None
|
|
29
|
+
assert model.aic is not None
|
|
30
|
+
assert model.bic is not None
|
|
31
|
+
assert model.deviance is not None
|
|
32
|
+
|
|
33
|
+
# Test confint
|
|
34
|
+
ci = model.confint(0.95)
|
|
35
|
+
assert len(ci) == 2
|
|
36
|
+
assert ci[0][0] < model.coefficients[0] < ci[0][1]
|
|
37
|
+
|
|
38
|
+
# Test predictions
|
|
39
|
+
preds = model.predict(df)
|
|
40
|
+
assert len(preds) == 180
|
|
41
|
+
# First prediction for sleepstudy should match R output closely (population level)
|
|
42
|
+
assert abs(preds[0] - 251.405105) < 1e-4
|
|
43
|
+
|
|
44
|
+
# Test conditional predictions
|
|
45
|
+
cond_preds = model.predict_conditional(df, allow_new_levels=False)
|
|
46
|
+
assert len(cond_preds) == 180
|
|
47
|
+
assert abs(cond_preds[0] - preds[0]) > 0.1 # Subject level effect should make it differ from population
|
|
48
|
+
|
|
49
|
+
# Test residuals and fitted
|
|
50
|
+
assert len(model.residuals) == 180
|
|
51
|
+
assert len(model.fitted) == 180
|
|
52
|
+
|
|
53
|
+
def test_glmer_poisson():
|
|
54
|
+
df = pl.read_csv("../tests/data/grouseticks.csv")
|
|
55
|
+
# Ticks ~ Year + Height + (1|BROOD)
|
|
56
|
+
model = lme_python.glmer("TICKS ~ YEAR + HEIGHT + (1 | BROOD)", data=df, family_name="poisson")
|
|
57
|
+
|
|
58
|
+
assert model.converged is True
|
|
59
|
+
assert model.sigma2 is None # Poisson has no dispersion
|
|
60
|
+
assert len(model.coefficients) == 3
|
|
61
|
+
|
|
62
|
+
preds_link = model.predict(df)
|
|
63
|
+
preds_resp = model.predict_response(df)
|
|
64
|
+
|
|
65
|
+
# Link scale (log) should be different from response scale
|
|
66
|
+
assert preds_link[0] != preds_resp[0]
|
|
67
|
+
assert abs(math.exp(preds_link[0]) - preds_resp[0]) < 1e-6
|
|
68
|
+
|
|
69
|
+
def test_glmer_binomial():
|
|
70
|
+
df = pl.read_csv("../tests/data/cbpp_binary.csv")
|
|
71
|
+
model = lme_python.glmer("y ~ period2 + period3 + period4 + (1 | herd)", data=df, family_name="binomial")
|
|
72
|
+
|
|
73
|
+
assert model.converged is True
|
|
74
|
+
assert model.sigma2 is None # Binomial has no dispersion
|
|
75
|
+
assert len(model.coefficients) == 4
|
|
76
|
+
|
|
77
|
+
preds_resp = model.predict_response(df)
|
|
78
|
+
# Binomial response predictions should be probabilities [0, 1]
|
|
79
|
+
assert all(0.0 <= p <= 1.0 for p in preds_resp)
|
|
80
|
+
|
|
81
|
+
def test_invalid_family():
|
|
82
|
+
df = pl.read_csv("../tests/data/sleepstudy.csv")
|
|
83
|
+
with pytest.raises(ValueError, match="Unsupported or invalid family"):
|
|
84
|
+
lme_python.glmer("Reaction ~ Days + (Days | Subject)", data=df, family_name="invalid_family")
|
|
85
|
+
|
|
86
|
+
def test_missing_column():
|
|
87
|
+
df = pl.read_csv("../tests/data/sleepstudy.csv")
|
|
88
|
+
with pytest.raises(ValueError):
|
|
89
|
+
# MissingCol is not in sleepstudy
|
|
90
|
+
lme_python.lmer("Reaction ~ MissingCol + (1 | Subject)", data=df)
|
|
91
|
+
|
|
92
|
+
def test_lmer_ml_estimation():
|
|
93
|
+
df = pl.read_csv("../tests/data/sleepstudy.csv")
|
|
94
|
+
model = lme_python.lmer("Reaction ~ Days + (Days | Subject)", data=df, reml=False)
|
|
95
|
+
assert model.converged is True
|
|
96
|
+
# The REML objective is larger than ML objective for the same model
|
|
97
|
+
assert "REML criterion" not in model.summary()
|
|
98
|
+
|
|
99
|
+
def test_glmer_gamma():
|
|
100
|
+
df = pl.read_csv("../tests/data/dyestuff.csv")
|
|
101
|
+
model = lme_python.glmer("Yield ~ 1 + (1 | Batch)", data=df, family_name="gamma")
|
|
102
|
+
assert model.converged is True
|
|
103
|
+
assert model.sigma2 is not None # Gamma has dispersion
|
|
104
|
+
assert len(model.coefficients) == 1
|
|
105
|
+
|
|
106
|
+
def test_predictions_with_new_data():
|
|
107
|
+
df = pl.read_csv("../tests/data/sleepstudy.csv")
|
|
108
|
+
model = lme_python.lmer("Reaction ~ Days + (Days | Subject)", data=df, reml=True)
|
|
109
|
+
|
|
110
|
+
# Subset to just 2 rows
|
|
111
|
+
subset_df = df.head(2)
|
|
112
|
+
preds = model.predict(subset_df)
|
|
113
|
+
assert len(preds) == 2
|
|
114
|
+
|
|
115
|
+
def test_conditional_predictions_unseen_levels():
|
|
116
|
+
df = pl.read_csv("../tests/data/sleepstudy.csv")
|
|
117
|
+
model = lme_python.lmer("Reaction ~ Days + (Days | Subject)", data=df, reml=True)
|
|
118
|
+
|
|
119
|
+
# Create a dataframe with an entirely unseen subject level
|
|
120
|
+
new_level_df = pl.DataFrame({
|
|
121
|
+
"Reaction": [250.0],
|
|
122
|
+
"Days": [0],
|
|
123
|
+
"Subject": ["UNSEEN_SUBJECT_999"]
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
# Should throw an error natively when allow_new_levels=False
|
|
127
|
+
with pytest.raises(ValueError):
|
|
128
|
+
model.predict_conditional(new_level_df, allow_new_levels=False)
|
|
129
|
+
|
|
130
|
+
# Should fallback to population level securely when allow_new_levels=True
|
|
131
|
+
cond_preds = model.predict_conditional(new_level_df, allow_new_levels=True)
|
|
132
|
+
pop_preds = model.predict(new_level_df)
|
|
133
|
+
assert len(cond_preds) == 1
|
|
134
|
+
assert abs(cond_preds[0] - pop_preds[0]) < 1e-6
|
|
135
|
+
|
|
136
|
+
def test_predict_missing_columns():
|
|
137
|
+
df = pl.read_csv("../tests/data/sleepstudy.csv")
|
|
138
|
+
model = lme_python.lmer("Reaction ~ Days + (Days | Subject)", data=df, reml=True)
|
|
139
|
+
|
|
140
|
+
bad_df = pl.DataFrame({"Reaction": [250.0], "Subject": ["308"]}) # Missing 'Days'
|
|
141
|
+
with pytest.raises(ValueError):
|
|
142
|
+
model.predict(bad_df)
|
|
143
|
+
|
|
144
|
+
def test_std_errors():
|
|
145
|
+
df = pl.read_csv("../tests/data/sleepstudy.csv")
|
|
146
|
+
model = lme_python.lmer("Reaction ~ Days + (Days | Subject)", data=df, reml=True)
|
|
147
|
+
|
|
148
|
+
se = model.std_errors
|
|
149
|
+
assert se is not None
|
|
150
|
+
assert len(se) == 2
|
|
151
|
+
assert se[0] > 0
|
|
152
|
+
assert se[1] > 0
|
|
153
|
+
|
|
154
|
+
if __name__ == "__main__":
|
|
155
|
+
test_lmer_sleepstudy()
|
|
156
|
+
test_glmer_poisson()
|
|
157
|
+
test_glmer_binomial()
|
|
158
|
+
test_lmer_ml_estimation()
|
|
159
|
+
test_glmer_gamma()
|
|
160
|
+
test_predictions_with_new_data()
|
|
161
|
+
test_conditional_predictions_unseen_levels()
|
|
162
|
+
test_predict_missing_columns()
|
|
163
|
+
test_std_errors()
|
|
164
|
+
print("All tests passed natively")
|
|
@@ -286,6 +286,10 @@ impl GlmFamily for BinomialFamily {
|
|
|
286
286
|
let mut d = Array1::zeros(n);
|
|
287
287
|
for i in 0..n {
|
|
288
288
|
let yi = y[i];
|
|
289
|
+
if !(0.0..=1.0).contains(&yi) {
|
|
290
|
+
d[i] = f64::NAN;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
289
293
|
let mi = mu[i].clamp(f64::EPSILON, 1.0 - f64::EPSILON);
|
|
290
294
|
let wi = wt[i];
|
|
291
295
|
let mut dev_i = 0.0;
|
|
@@ -351,6 +355,10 @@ impl GlmFamily for PoissonFamily {
|
|
|
351
355
|
let mut d = Array1::zeros(n);
|
|
352
356
|
for i in 0..n {
|
|
353
357
|
let yi = y[i];
|
|
358
|
+
if yi < 0.0 {
|
|
359
|
+
d[i] = f64::NAN;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
354
362
|
let mi = mu[i].max(f64::EPSILON);
|
|
355
363
|
let wi = wt[i];
|
|
356
364
|
let mut dev_i = -(yi - mi);
|
|
@@ -467,10 +475,15 @@ impl GlmFamily for GammaFamily {
|
|
|
467
475
|
let n = y.len();
|
|
468
476
|
let mut d = Array1::zeros(n);
|
|
469
477
|
for i in 0..n {
|
|
470
|
-
let yi = y[i]
|
|
478
|
+
let yi = y[i];
|
|
479
|
+
if yi <= 0.0 {
|
|
480
|
+
d[i] = f64::NAN;
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
let yi_clamped = yi.max(f64::EPSILON);
|
|
471
484
|
let mi = mu[i].max(f64::EPSILON);
|
|
472
485
|
let wi = wt[i];
|
|
473
|
-
d[i] = 2.0 * wi * (-(
|
|
486
|
+
d[i] = 2.0 * wi * (-(yi_clamped / mi).ln() + (yi_clamped - mi) / mi);
|
|
474
487
|
}
|
|
475
488
|
d
|
|
476
489
|
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
import polars as pl
|
|
3
|
-
import lme_python
|
|
4
|
-
|
|
5
|
-
def test_lmer_sleepstudy():
|
|
6
|
-
# Load identical data used in rust tests
|
|
7
|
-
df = pl.read_csv("../tests/data/sleepstudy.csv")
|
|
8
|
-
|
|
9
|
-
# Fit model natively via rust
|
|
10
|
-
model = lme_python.lmer("Reaction ~ Days + (Days | Subject)", data=df, reml=True)
|
|
11
|
-
summary = model.summary()
|
|
12
|
-
|
|
13
|
-
print("\n--- Model Summary ---")
|
|
14
|
-
print(summary)
|
|
15
|
-
|
|
16
|
-
# Check that REML objective is embedded in the summary string
|
|
17
|
-
assert "1743.6" in summary
|
|
18
|
-
assert "611.9033" in summary # Intercept variance
|
|
19
|
-
|
|
20
|
-
# Test predictions
|
|
21
|
-
preds = model.predict(df)
|
|
22
|
-
assert len(preds) == 180
|
|
23
|
-
# First prediction for sleepstudy should match R output closely
|
|
24
|
-
assert abs(preds[0] - 251.405105) < 1e-4
|
|
25
|
-
|
|
26
|
-
if __name__ == "__main__":
|
|
27
|
-
test_lmer_sleepstudy()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|