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.
Files changed (102) hide show
  1. {lme_python-0.1.1 → lme_python-0.1.2}/Cargo.lock +1 -1
  2. {lme_python-0.1.1 → lme_python-0.1.2}/Cargo.toml +1 -1
  3. {lme_python-0.1.1 → lme_python-0.1.2}/PKG-INFO +1 -1
  4. {lme_python-0.1.1 → lme_python-0.1.2}/benches/bench_math.rs +1 -1
  5. {lme_python-0.1.1 → lme_python-0.1.2}/python/Cargo.lock +2 -2
  6. {lme_python-0.1.1 → lme_python-0.1.2}/python/Cargo.toml +1 -1
  7. {lme_python-0.1.1 → lme_python-0.1.2}/python/PYTHON_GUIDE.md +37 -3
  8. lme_python-0.1.2/python/tests/test_basic.py +164 -0
  9. {lme_python-0.1.1 → lme_python-0.1.2}/src/family.rs +15 -2
  10. {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_coverage_edge_cases.rs +0 -2
  11. {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_robust.rs +0 -1
  12. lme_python-0.1.1/python/tests/test_basic.py +0 -27
  13. {lme_python-0.1.1 → lme_python-0.1.2}/.github/workflows/ci.yml +0 -0
  14. {lme_python-0.1.1 → lme_python-0.1.2}/.github/workflows/python-release.yml +0 -0
  15. {lme_python-0.1.1 → lme_python-0.1.2}/.gitignore +0 -0
  16. {lme_python-0.1.1 → lme_python-0.1.2}/CHANGELOG.md +0 -0
  17. {lme_python-0.1.1 → lme_python-0.1.2}/GUIDE.md +0 -0
  18. {lme_python-0.1.1 → lme_python-0.1.2}/LICENSE +0 -0
  19. {lme_python-0.1.1 → lme_python-0.1.2}/README.md +0 -0
  20. {lme_python-0.1.1 → lme_python-0.1.2}/examples/COMPARISONS.md +0 -0
  21. {lme_python-0.1.1 → lme_python-0.1.2}/examples/comparison_chart.png +0 -0
  22. {lme_python-0.1.1 → lme_python-0.1.2}/examples/glmm_cbpp.R +0 -0
  23. {lme_python-0.1.1 → lme_python-0.1.2}/examples/glmm_cbpp.jl +0 -0
  24. {lme_python-0.1.1 → lme_python-0.1.2}/examples/glmm_cbpp.py +0 -0
  25. {lme_python-0.1.1 → lme_python-0.1.2}/examples/glmm_cbpp.rs +0 -0
  26. {lme_python-0.1.1 → lme_python-0.1.2}/examples/glmm_grouseticks.R +0 -0
  27. {lme_python-0.1.1 → lme_python-0.1.2}/examples/glmm_grouseticks.jl +0 -0
  28. {lme_python-0.1.1 → lme_python-0.1.2}/examples/glmm_grouseticks.py +0 -0
  29. {lme_python-0.1.1 → lme_python-0.1.2}/examples/glmm_grouseticks.rs +0 -0
  30. {lme_python-0.1.1 → lme_python-0.1.2}/examples/lmm_dyestuff.R +0 -0
  31. {lme_python-0.1.1 → lme_python-0.1.2}/examples/lmm_dyestuff.jl +0 -0
  32. {lme_python-0.1.1 → lme_python-0.1.2}/examples/lmm_dyestuff.py +0 -0
  33. {lme_python-0.1.1 → lme_python-0.1.2}/examples/lmm_dyestuff.rs +0 -0
  34. {lme_python-0.1.1 → lme_python-0.1.2}/examples/lmm_pastes.R +0 -0
  35. {lme_python-0.1.1 → lme_python-0.1.2}/examples/lmm_pastes.jl +0 -0
  36. {lme_python-0.1.1 → lme_python-0.1.2}/examples/lmm_pastes.py +0 -0
  37. {lme_python-0.1.1 → lme_python-0.1.2}/examples/lmm_pastes.rs +0 -0
  38. {lme_python-0.1.1 → lme_python-0.1.2}/examples/lmm_penicillin.R +0 -0
  39. {lme_python-0.1.1 → lme_python-0.1.2}/examples/lmm_penicillin.jl +0 -0
  40. {lme_python-0.1.1 → lme_python-0.1.2}/examples/lmm_penicillin.py +0 -0
  41. {lme_python-0.1.1 → lme_python-0.1.2}/examples/lmm_penicillin.rs +0 -0
  42. {lme_python-0.1.1 → lme_python-0.1.2}/examples/sleepstudy.R +0 -0
  43. {lme_python-0.1.1 → lme_python-0.1.2}/examples/sleepstudy.jl +0 -0
  44. {lme_python-0.1.1 → lme_python-0.1.2}/examples/sleepstudy.py +0 -0
  45. {lme_python-0.1.1 → lme_python-0.1.2}/examples/sleepstudy.rs +0 -0
  46. {lme_python-0.1.1 → lme_python-0.1.2}/examples/sleepstudy_ml.R +0 -0
  47. {lme_python-0.1.1 → lme_python-0.1.2}/examples/sleepstudy_ml.jl +0 -0
  48. {lme_python-0.1.1 → lme_python-0.1.2}/examples/sleepstudy_ml.py +0 -0
  49. {lme_python-0.1.1 → lme_python-0.1.2}/examples/sleepstudy_ml.rs +0 -0
  50. {lme_python-0.1.1 → lme_python-0.1.2}/pyproject.toml +0 -0
  51. {lme_python-0.1.1 → lme_python-0.1.2}/python/src/lib.rs +0 -0
  52. {lme_python-0.1.1 → lme_python-0.1.2}/scripts/ast_explorations/test_fiasto.rs +0 -0
  53. {lme_python-0.1.1 → lme_python-0.1.2}/scripts/ast_explorations/test_fiasto_offset.rs +0 -0
  54. {lme_python-0.1.1 → lme_python-0.1.2}/scripts/dump_dyestuff.R +0 -0
  55. {lme_python-0.1.1 → lme_python-0.1.2}/scripts/dump_pastes.R +0 -0
  56. {lme_python-0.1.1 → lme_python-0.1.2}/scripts/make_penicillin_csv.py +0 -0
  57. {lme_python-0.1.1 → lme_python-0.1.2}/scripts/plot_comparisons.py +0 -0
  58. {lme_python-0.1.1 → lme_python-0.1.2}/src/anova.rs +0 -0
  59. {lme_python-0.1.1 → lme_python-0.1.2}/src/formula.rs +0 -0
  60. {lme_python-0.1.1 → lme_python-0.1.2}/src/glmm_math.rs +0 -0
  61. {lme_python-0.1.1 → lme_python-0.1.2}/src/kenward_roger.rs +0 -0
  62. {lme_python-0.1.1 → lme_python-0.1.2}/src/lib.rs +0 -0
  63. {lme_python-0.1.1 → lme_python-0.1.2}/src/math.rs +0 -0
  64. {lme_python-0.1.1 → lme_python-0.1.2}/src/model_matrix.rs +0 -0
  65. {lme_python-0.1.1 → lme_python-0.1.2}/src/optimizer.rs +0 -0
  66. {lme_python-0.1.1 → lme_python-0.1.2}/src/robust.rs +0 -0
  67. {lme_python-0.1.1 → lme_python-0.1.2}/src/satterthwaite.rs +0 -0
  68. {lme_python-0.1.1 → lme_python-0.1.2}/tests/data/cbpp_binary.csv +0 -0
  69. {lme_python-0.1.1 → lme_python-0.1.2}/tests/data/dyestuff.csv +0 -0
  70. {lme_python-0.1.1 → lme_python-0.1.2}/tests/data/glmm_binomial.json +0 -0
  71. {lme_python-0.1.1 → lme_python-0.1.2}/tests/data/glmm_poisson.json +0 -0
  72. {lme_python-0.1.1 → lme_python-0.1.2}/tests/data/grouseticks.csv +0 -0
  73. {lme_python-0.1.1 → lme_python-0.1.2}/tests/data/intercept_only.json +0 -0
  74. {lme_python-0.1.1 → lme_python-0.1.2}/tests/data/mock_crossed.json +0 -0
  75. {lme_python-0.1.1 → lme_python-0.1.2}/tests/data/mock_ml.json +0 -0
  76. {lme_python-0.1.1 → lme_python-0.1.2}/tests/data/pastes.csv +0 -0
  77. {lme_python-0.1.1 → lme_python-0.1.2}/tests/data/penicillin.csv +0 -0
  78. {lme_python-0.1.1 → lme_python-0.1.2}/tests/data/penicillin.json +0 -0
  79. {lme_python-0.1.1 → lme_python-0.1.2}/tests/data/random_slopes.json +0 -0
  80. {lme_python-0.1.1 → lme_python-0.1.2}/tests/data/sleepstudy.csv +0 -0
  81. {lme_python-0.1.1 → lme_python-0.1.2}/tests/generate_mock_data.py +0 -0
  82. {lme_python-0.1.1 → lme_python-0.1.2}/tests/generate_test_data.R +0 -0
  83. {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_anova.rs +0 -0
  84. {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_conditional_real.rs +0 -0
  85. {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_confint_simulate.rs +0 -0
  86. {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_coverage_gaps.rs +0 -0
  87. {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_crossed_mock.rs +0 -0
  88. {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_e2e_lmer.rs +0 -0
  89. {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_edge_cases.rs +0 -0
  90. {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_errors.rs +0 -0
  91. {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_formula.rs +0 -0
  92. {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_gaps.rs +0 -0
  93. {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_glmm.rs +0 -0
  94. {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_intercept_only.rs +0 -0
  95. {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_kenward_roger.rs +0 -0
  96. {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_ml_optimization.rs +0 -0
  97. {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_no_intercept.rs +0 -0
  98. {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_numerical_parity.rs +0 -0
  99. {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_predict.rs +0 -0
  100. {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_random_slopes.rs +0 -0
  101. {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_sandwich_math.R +0 -0
  102. {lme_python-0.1.1 → lme_python-0.1.2}/tests/test_satterthwaite.rs +0 -0
@@ -1528,7 +1528,7 @@ checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
1528
1528
 
1529
1529
  [[package]]
1530
1530
  name = "lme-rs"
1531
- version = "0.1.1"
1531
+ version = "0.1.2"
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.1"
3
+ version = "0.1.2"
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lme_python
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
@@ -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).expect(&format!("Could not 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.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.1"
1316
+ version = "0.1.2"
1317
1317
  dependencies = [
1318
1318
  "lme-rs",
1319
1319
  "polars",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "lme_python"
3
- version = "0.1.1"
3
+ version = "0.1.2"
4
4
  edition = "2021"
5
5
  authors = ["Martin Huck <x4g4p3x@users.noreply.github.com>"]
6
6
  description = "Python bindings for lme-rs"
@@ -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
- # Predict for new data
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
- preds = model.predict(newdata)
175
- print(f"Predictions: {preds}")
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].max(f64::EPSILON);
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 * (-(yi / mi).ln() + (yi - mi) / mi);
486
+ d[i] = 2.0 * wi * (-(yi_clamped / mi).ln() + (yi_clamped - mi) / mi);
474
487
  }
475
488
  d
476
489
  }
@@ -1,6 +1,4 @@
1
1
  use lme_rs::family::Family;
2
- use lme_rs::optimizer::{optimize_theta_glmm, optimize_theta_nd};
3
- use ndarray::{array, Array2};
4
2
  use polars::prelude::*;
5
3
 
6
4
  #[test]
@@ -1,5 +1,4 @@
1
1
  use lme_rs::lmer;
2
- use ndarray::Array1;
3
2
  use polars::prelude::*;
4
3
  use serde::Deserialize;
5
4
 
@@ -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