marginaleffects 0.6.0__tar.gz → 0.6.1__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. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/PKG-INFO +2 -2
  2. marginaleffects-0.6.1/benchmarks/benchmark_autodiff.py +228 -0
  3. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/comparisons.py +2 -4
  4. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/plan.py +13 -0
  5. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/predictions.py +7 -5
  6. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects.egg-info/PKG-INFO +2 -2
  7. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/pyproject.toml +2 -2
  8. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_sklearn.py +43 -0
  9. marginaleffects-0.6.0/benchmarks/benchmark_autodiff.py +0 -111
  10. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/README.md +0 -0
  11. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/benchmarks/__init__.py +0 -0
  12. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/__init__.py +0 -0
  13. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/autodiff/__init__.py +0 -0
  14. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/autodiff/glm/__init__.py +0 -0
  15. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/autodiff/glm/families.py +0 -0
  16. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/autodiff/lower.py +0 -0
  17. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/autodiff/ops.py +0 -0
  18. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/autodiff/pipeline.py +0 -0
  19. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/by.py +0 -0
  20. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/classes/__init__.py +0 -0
  21. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/classes/model.py +0 -0
  22. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/classes/result.py +0 -0
  23. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/datagrid.py +0 -0
  24. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/datasets.py +0 -0
  25. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/docstrings/__init__.py +0 -0
  26. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/docstrings/params.py +0 -0
  27. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/docstrings/qmd.py +0 -0
  28. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/estimands.py +0 -0
  29. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/formula.py +0 -0
  30. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/hypothesis_compile.py +0 -0
  31. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/linearmodels/__init__.py +0 -0
  32. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/linearmodels/model.py +0 -0
  33. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/plot/__init__.py +0 -0
  34. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/plot/common.py +0 -0
  35. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/plot/comparisons.py +0 -0
  36. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/plot/predictions.py +0 -0
  37. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/plot/slopes.py +0 -0
  38. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/pyfixest/__init__.py +0 -0
  39. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/pyfixest/model.py +0 -0
  40. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/sanitize/__init__.py +0 -0
  41. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/sanitize/by.py +0 -0
  42. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/sanitize/categorical.py +0 -0
  43. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/sanitize/comparison.py +0 -0
  44. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/sanitize/deprecated.py +0 -0
  45. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/sanitize/hypothesis_null.py +0 -0
  46. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/sanitize/newdata.py +0 -0
  47. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/sanitize/sanitize_model.py +0 -0
  48. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/sanitize/utils.py +0 -0
  49. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/sanitize/validation.py +0 -0
  50. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/sanitize/variables.py +0 -0
  51. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/sanitize/vcov.py +0 -0
  52. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/settings.py +0 -0
  53. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/sklearn/__init__.py +0 -0
  54. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/sklearn/model.py +0 -0
  55. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/slopes.py +0 -0
  56. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/statsmodels/__init__.py +0 -0
  57. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/statsmodels/model.py +0 -0
  58. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/test/__init__.py +0 -0
  59. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/test/core.py +0 -0
  60. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/test/equivalence.py +0 -0
  61. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/test/formula.py +0 -0
  62. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/test/joint.py +0 -0
  63. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/test/main.py +0 -0
  64. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/transform.py +0 -0
  65. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/uncertainty.py +0 -0
  66. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/utils.py +0 -0
  67. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects.egg-info/SOURCES.txt +0 -0
  68. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects.egg-info/dependency_links.txt +0 -0
  69. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects.egg-info/requires.txt +0 -0
  70. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects.egg-info/top_level.txt +0 -0
  71. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/setup.cfg +0 -0
  72. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/__init__.py +0 -0
  73. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/helpers.py +0 -0
  74. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_analytic.py +0 -0
  75. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_autodiff.py +0 -0
  76. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_autodiff_lower.py +0 -0
  77. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_autodiff_pipeline.py +0 -0
  78. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_bugfix.py +0 -0
  79. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_by.py +0 -0
  80. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_categorical.py +0 -0
  81. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_categorical_validation.py +0 -0
  82. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_comparison_plan.py +0 -0
  83. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_comparisons.py +0 -0
  84. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_comparisons_interaction.py +0 -0
  85. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_datagrid_01.py +0 -0
  86. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_datagrid_02.py +0 -0
  87. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_equivalence.py +0 -0
  88. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_formula.py +0 -0
  89. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_formulaic_utils.py +0 -0
  90. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_hypotheses.py +0 -0
  91. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_hypotheses_joint.py +0 -0
  92. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_hypothesis.py +0 -0
  93. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_hypothesis_compile.py +0 -0
  94. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_jss.py +0 -0
  95. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_linearmodels_panelols.py +0 -0
  96. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_missing.py +0 -0
  97. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_newdata.py +0 -0
  98. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_plan.py +0 -0
  99. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_plot_comparisons.py +0 -0
  100. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_plot_predictions.py +0 -0
  101. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_plot_slopes.py +0 -0
  102. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_prediction_plan.py +0 -0
  103. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_predictions.py +0 -0
  104. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_pyfixest.py +0 -0
  105. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_slopes.py +0 -0
  106. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_statsmodels.py +0 -0
  107. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_statsmodels_logit.py +0 -0
  108. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_statsmodels_mixedlm.py +0 -0
  109. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_statsmodels_mnlogit.py +0 -0
  110. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_statsmodels_negativebinomial.py +0 -0
  111. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_statsmodels_ols.py +0 -0
  112. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_statsmodels_ordinal.py +0 -0
  113. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_statsmodels_poisson.py +0 -0
  114. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_statsmodels_probit.py +0 -0
  115. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_statsmodels_quantreg.py +0 -0
  116. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_statsmodels_vcov.py +0 -0
  117. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_statsmodels_wls.py +0 -0
  118. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_typical.py +0 -0
  119. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_utils.py +0 -0
  120. {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/utilities.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: marginaleffects
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary: Predictions, counterfactual comparisons, slopes, and hypothesis tests for statistical models.
5
5
  License-Expression: GPL-3.0-or-later
6
- Requires-Python: >=3.10
6
+ Requires-Python: >=3.12
7
7
  Description-Content-Type: text/markdown
8
8
  Requires-Dist: formulaic>=1.0.2
9
9
  Requires-Dist: jax>=0.4.0
@@ -0,0 +1,228 @@
1
+ """
2
+ Benchmark comparing autodiff (JAX) vs finite differences.
3
+
4
+ Run with:
5
+ uv run --all-extras python benchmarks/benchmark_autodiff.py
6
+ """
7
+
8
+ import time
9
+
10
+ import numpy as np
11
+ import polars as pl
12
+ import statsmodels.api as sm
13
+ from marginaleffects import (
14
+ autodiff,
15
+ avg_comparisons,
16
+ avg_predictions,
17
+ comparisons,
18
+ predictions,
19
+ )
20
+ from marginaleffects.uncertainty import get_jacobian, get_se
21
+
22
+
23
+ def generate_data(
24
+ n_obs: int = 5000, n_predictors: int = 50, seed: int = 42
25
+ ) -> pl.DataFrame:
26
+ """Generate simulated data."""
27
+ rng = np.random.default_rng(seed)
28
+ X = rng.normal(size=(n_obs, n_predictors))
29
+ beta = rng.normal(size=n_predictors + 1)
30
+ y = beta[0] + X @ beta[1:] + rng.normal(size=n_obs) * 0.5
31
+ data = {"y": y}
32
+ for i in range(n_predictors):
33
+ data[f"x{i}"] = X[:, i]
34
+ return pl.DataFrame(data)
35
+
36
+
37
+ def _time_call(func, n_runs: int):
38
+ out = []
39
+ for _ in range(n_runs):
40
+ start = time.perf_counter()
41
+ func()
42
+ out.append(time.perf_counter() - start)
43
+ return out
44
+
45
+
46
+ def benchmark_public_api(func, model, n_runs: int = 10, **kwargs):
47
+ """
48
+ Benchmark public API calls with and without autodiff.
49
+
50
+ This includes shared overhead such as model sanitization, formula/design
51
+ matrix construction, vcov retrieval, confidence intervals, and result
52
+ formatting.
53
+ """
54
+ results = {"autodiff": [], "finite_diff": []}
55
+
56
+ autodiff(True)
57
+ func(model, **kwargs)
58
+ autodiff(False)
59
+ func(model, **kwargs)
60
+
61
+ autodiff(True)
62
+ results["autodiff"] = _time_call(lambda: func(model, **kwargs), n_runs=n_runs)
63
+
64
+ autodiff(False)
65
+ results["finite_diff"] = _time_call(lambda: func(model, **kwargs), n_runs=n_runs)
66
+
67
+ autodiff(None)
68
+ return results
69
+
70
+
71
+ def _engine_cases(X: np.ndarray, beta: np.ndarray):
72
+ from marginaleffects.autodiff import pipeline
73
+
74
+ V = np.eye(beta.size)
75
+ n_obs = X.shape[0]
76
+ segments = np.zeros(n_obs, dtype=np.int32)
77
+
78
+ X_hi = X.copy()
79
+ X_lo = X.copy()
80
+ X_hi[:, 1] += 0.5
81
+
82
+ def jax_predictions():
83
+ result = pipeline.compute(beta=beta, model_type="linear", X=X)
84
+ return get_se(result["jacobian"], V)
85
+
86
+ def fd_predictions():
87
+ J = get_jacobian(lambda b: X @ b, beta)
88
+ return get_se(J, V)
89
+
90
+ def jax_avg_predictions():
91
+ result = pipeline.compute(
92
+ beta=beta,
93
+ model_type="linear",
94
+ X=X,
95
+ agg_segments=segments,
96
+ agg_num_segments=1,
97
+ )
98
+ return get_se(result["jacobian"], V)
99
+
100
+ def fd_avg_predictions():
101
+ J = get_jacobian(lambda b: np.array([(X @ b).mean()]), beta)
102
+ return get_se(J, V)
103
+
104
+ def jax_comparisons():
105
+ result = pipeline.compute(
106
+ beta=beta,
107
+ model_type="linear",
108
+ X_hi=X_hi,
109
+ X_lo=X_lo,
110
+ ops=[{"op": "difference", "n": n_obs, "w": None}],
111
+ )
112
+ return get_se(result["jacobian"], V)
113
+
114
+ def fd_comparisons():
115
+ J = get_jacobian(lambda b: (X_hi @ b) - (X_lo @ b), beta)
116
+ return get_se(J, V)
117
+
118
+ def jax_avg_comparisons():
119
+ result = pipeline.compute(
120
+ beta=beta,
121
+ model_type="linear",
122
+ X_hi=X_hi,
123
+ X_lo=X_lo,
124
+ ops=[{"op": "differenceavg", "n": n_obs, "w": None}],
125
+ )
126
+ return get_se(result["jacobian"], V)
127
+
128
+ def fd_avg_comparisons():
129
+ J = get_jacobian(
130
+ lambda b: np.array([np.mean((X_hi @ b) - (X_lo @ b))]),
131
+ beta,
132
+ )
133
+ return get_se(J, V)
134
+
135
+ return {
136
+ "OLS predictions()": (jax_predictions, fd_predictions),
137
+ "OLS avg_predictions()": (jax_avg_predictions, fd_avg_predictions),
138
+ "OLS comparisons()": (jax_comparisons, fd_comparisons),
139
+ "OLS avg_comparisons()": (jax_avg_comparisons, fd_avg_comparisons),
140
+ }
141
+
142
+
143
+ def benchmark_engine(X: np.ndarray, beta: np.ndarray, n_runs: int = 20):
144
+ """
145
+ Benchmark only the Jacobian engine on cached design matrices.
146
+
147
+ This isolates the finite-difference loop from JAX autodiff, excluding public
148
+ API setup and result formatting.
149
+ """
150
+ out = {}
151
+ for name, (jax_func, fd_func) in _engine_cases(X, beta).items():
152
+ # Warm-up. The JAX call also compiles the traced function.
153
+ jax_func()
154
+ fd_func()
155
+ out[name] = {
156
+ "autodiff": _time_call(jax_func, n_runs=n_runs),
157
+ "finite_diff": _time_call(fd_func, n_runs=n_runs),
158
+ }
159
+ return out
160
+
161
+
162
+ def print_results(name: str, results: dict):
163
+ """Print benchmark results."""
164
+ ad_mean = np.mean(results["autodiff"]) * 1000
165
+ fd_mean = np.mean(results["finite_diff"]) * 1000
166
+ speedup = fd_mean / ad_mean
167
+ print(f"{name:30s} {ad_mean:6.1f} ms {fd_mean:6.1f} ms {speedup:.2f}x")
168
+
169
+
170
+ if __name__ == "__main__":
171
+ try:
172
+ from marginaleffects.autodiff import _JAX_AVAILABLE
173
+
174
+ if not _JAX_AVAILABLE:
175
+ print("JAX not available")
176
+ exit(1)
177
+ except ImportError:
178
+ print("autodiff module not found")
179
+ exit(1)
180
+
181
+ data = generate_data()
182
+ formula = "y ~ " + " + ".join([f"x{i}" for i in range(50)])
183
+ X = data.select([f"x{i}" for i in range(50)]).to_numpy()
184
+ X = np.column_stack([np.ones(X.shape[0]), X])
185
+ beta = np.random.default_rng(42).normal(size=X.shape[1])
186
+
187
+ print("Autodiff Public API Benchmark (5000 obs, 50 predictors)")
188
+ print("Includes model wrapping, formula/design matrices, vcov, and formatting")
189
+ print("=" * 78)
190
+ print(f"{'Function':30s} {'JAX':>8s} {'Finite':>8s} Speedup")
191
+ print("-" * 78)
192
+
193
+ ols = sm.OLS.from_formula(formula, data.to_pandas()).fit()
194
+ print_results("OLS predictions()", benchmark_public_api(predictions, ols))
195
+ print_results("OLS avg_predictions()", benchmark_public_api(avg_predictions, ols))
196
+ print_results(
197
+ "OLS comparisons()",
198
+ benchmark_public_api(comparisons, ols, variables="x0"),
199
+ )
200
+ print_results(
201
+ "OLS avg_comparisons()",
202
+ benchmark_public_api(avg_comparisons, ols, variables="x0"),
203
+ )
204
+
205
+ glm = sm.GLM.from_formula(
206
+ formula, data.to_pandas(), family=sm.families.Gaussian()
207
+ ).fit()
208
+ print_results("GLM predictions()", benchmark_public_api(predictions, glm))
209
+ print_results("GLM avg_predictions()", benchmark_public_api(avg_predictions, glm))
210
+ print_results(
211
+ "GLM comparisons()",
212
+ benchmark_public_api(comparisons, glm, variables="x0"),
213
+ )
214
+ print_results(
215
+ "GLM avg_comparisons()",
216
+ benchmark_public_api(avg_comparisons, glm, variables="x0"),
217
+ )
218
+ print("=" * 78)
219
+
220
+ print()
221
+ print("Autodiff Jacobian Engine Benchmark (cached 5000x51 design matrix)")
222
+ print("Excludes shared public API setup; row-level quantities have many outputs")
223
+ print("=" * 78)
224
+ print(f"{'Function':30s} {'JAX':>8s} {'Finite':>8s} Speedup")
225
+ print("-" * 78)
226
+ for name, results in benchmark_engine(X, beta).items():
227
+ print_results(name, results)
228
+ print("=" * 78)
@@ -20,6 +20,7 @@ from .plan import (
20
20
  ComparisonPlan,
21
21
  comparison_plan_apply,
22
22
  comparison_plan_predict,
23
+ plan_values_allclose,
23
24
  )
24
25
  from .utils import (
25
26
  get_pad,
@@ -546,12 +547,9 @@ def _comparisons_build(
546
547
 
547
548
  hi, lo, y = plan_predictions
548
549
  replay = comparison_plan_apply(plan, hi, lo, y if plan.need_y else None)
549
- if not np.allclose(
550
+ if not plan_values_allclose(
550
551
  replay,
551
552
  tmp["estimate"].to_numpy(),
552
- rtol=1e-12,
553
- atol=1e-12,
554
- equal_nan=True,
555
553
  ):
556
554
  raise RuntimeError(
557
555
  "marginaleffects internal error: comparison plan baseline check failed"
@@ -98,6 +98,19 @@ def _estimand_to_numpy(x) -> np.ndarray:
98
98
  return _as_float_array(x)
99
99
 
100
100
 
101
+ def plan_values_allclose(replay, target) -> bool:
102
+ """Compare plan replay values to baseline values at backend precision."""
103
+ replay = np.asarray(replay)
104
+ target = np.asarray(target)
105
+ tol = 1e-12
106
+ if any(
107
+ np.issubdtype(x.dtype, np.floating) and x.dtype.itemsize <= 4
108
+ for x in (replay, target)
109
+ ):
110
+ tol = 1e-6
111
+ return np.allclose(replay, target, rtol=tol, atol=tol, equal_nan=True)
112
+
113
+
101
114
  def prediction_plan_predict(plan: PredictionPlan, model, coefs) -> np.ndarray:
102
115
  out = model.get_predict(params=np.asarray(coefs), newdata=plan.exog)
103
116
  return _as_float_array(out["estimate"].to_numpy())
@@ -6,7 +6,12 @@ from .by import get_by_plan
6
6
  from .hypothesis_compile import hypothesis_compile
7
7
  from .uncertainty import get_jacobian, get_se
8
8
  from .classes import MarginaleffectsResult
9
- from .plan import PredictionPlan, prediction_plan_apply, prediction_plan_predict
9
+ from .plan import (
10
+ PredictionPlan,
11
+ plan_values_allclose,
12
+ prediction_plan_apply,
13
+ prediction_plan_predict,
14
+ )
10
15
  from .utils import prepare_base_inputs, finalize_result, call_avg
11
16
  from .sanitize import handle_deprecated_hypotheses_argument
12
17
  from .docstrings import doc
@@ -146,12 +151,9 @@ def _predictions_build(model, exog, newdata, by, wts, hypothesis):
146
151
  replay = prediction_plan_apply(
147
152
  plan, prediction_plan_predict(plan, model, model.get_coef())
148
153
  )
149
- if not np.allclose(
154
+ if not plan_values_allclose(
150
155
  replay,
151
156
  out["estimate"].to_numpy(),
152
- rtol=1e-12,
153
- atol=1e-12,
154
- equal_nan=True,
155
157
  ):
156
158
  raise RuntimeError(
157
159
  "marginaleffects internal error: prediction plan baseline check failed"
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: marginaleffects
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary: Predictions, counterfactual comparisons, slopes, and hypothesis tests for statistical models.
5
5
  License-Expression: GPL-3.0-or-later
6
- Requires-Python: >=3.10
6
+ Requires-Python: >=3.12
7
7
  Description-Content-Type: text/markdown
8
8
  Requires-Dist: formulaic>=1.0.2
9
9
  Requires-Dist: jax>=0.4.0
@@ -1,10 +1,10 @@
1
1
  [project]
2
2
  name = "marginaleffects"
3
- version = "0.6.0"
3
+ version = "0.6.1"
4
4
  license = "GPL-3.0-or-later"
5
5
  description = "Predictions, counterfactual comparisons, slopes, and hypothesis tests for statistical models."
6
6
  readme = "README.md"
7
- requires-python = ">=3.10"
7
+ requires-python = ">=3.12"
8
8
  dependencies = [
9
9
  "formulaic >=1.0.2",
10
10
  "jax >=0.4.0",
@@ -146,3 +146,46 @@ def test_categorical_formula_equivalence():
146
146
  branch_categorical = pred_categorical["branch"].to_list()
147
147
  branch_direct = pred_direct["branch"].to_list()
148
148
  assert branch_categorical == branch_direct
149
+
150
+
151
+ def test_callable_selector_xgboost_float32_predictions_by():
152
+ """Callable sklearn selectors can replay float32 predictions with by groups."""
153
+ import polars.selectors as cs
154
+ from sklearn.compose import make_column_transformer
155
+ from sklearn.pipeline import make_pipeline
156
+ from sklearn.preprocessing import FunctionTransformer, OneHotEncoder
157
+ from xgboost import XGBRegressor
158
+
159
+ rng = np.random.default_rng(1)
160
+ data = pl.DataFrame(
161
+ {
162
+ "price": rng.normal(size=80),
163
+ "unit_type": np.where(np.arange(80) % 2 == 0, "a", "b"),
164
+ "neighborhood": np.where(np.arange(80) % 3 == 0, "n1", "n2"),
165
+ "bedrooms": rng.integers(1, 4, size=80),
166
+ "x": rng.normal(size=80),
167
+ }
168
+ )
169
+ catvar = data.select(~cs.numeric()).columns
170
+ data = data.with_columns([pl.col(c).cast(pl.Categorical) for c in catvar])
171
+
172
+ preprocessor = make_column_transformer(
173
+ (OneHotEncoder(), catvar),
174
+ remainder=FunctionTransformer(lambda x: x.to_numpy()),
175
+ )
176
+ pipeline = make_pipeline(
177
+ preprocessor,
178
+ XGBRegressor(n_estimators=10, max_depth=2, random_state=1),
179
+ )
180
+
181
+ def selector(data):
182
+ y = data.select(cs.by_name("price", require_all=False))
183
+ X = data.select(~cs.by_name("price", require_all=False))
184
+ return y, X
185
+
186
+ mod = me.fit_sklearn(selector, data=data[:60], engine=pipeline)
187
+ out = me.avg_predictions(mod, newdata=data[60:], by="unit_type")
188
+
189
+ assert isinstance(out, MarginaleffectsResult)
190
+ assert out.height == 2
191
+ assert set(out["unit_type"].to_list()) == {"a", "b"}
@@ -1,111 +0,0 @@
1
- """
2
- Benchmark comparing autodiff (JAX) vs finite differences.
3
-
4
- Run with:
5
- uv run --all-extras python benchmarks/benchmark_autodiff.py
6
- """
7
-
8
- import time
9
- import numpy as np
10
- import polars as pl
11
- import statsmodels.api as sm
12
- from marginaleffects import (
13
- predictions,
14
- avg_predictions,
15
- comparisons,
16
- avg_comparisons,
17
- autodiff,
18
- )
19
-
20
-
21
- def generate_data(
22
- n_obs: int = 5000, n_predictors: int = 50, seed: int = 42
23
- ) -> pl.DataFrame:
24
- """Generate simulated data."""
25
- np.random.seed(seed)
26
- X = np.random.randn(n_obs, n_predictors)
27
- beta = np.random.randn(n_predictors + 1)
28
- y = beta[0] + X @ beta[1:] + np.random.randn(n_obs) * 0.5
29
- data = {"y": y}
30
- for i in range(n_predictors):
31
- data[f"x{i}"] = X[:, i]
32
- return pl.DataFrame(data)
33
-
34
-
35
- def benchmark(func, model, n_runs: int = 10, **kwargs):
36
- """Benchmark a function with and without autodiff."""
37
- results = {"autodiff": [], "finite_diff": []}
38
-
39
- # Warm-up
40
- autodiff(True)
41
- func(model, **kwargs)
42
- autodiff(False)
43
- func(model, **kwargs)
44
-
45
- # Benchmark with autodiff
46
- autodiff(True)
47
- for _ in range(n_runs):
48
- start = time.perf_counter()
49
- func(model, **kwargs)
50
- results["autodiff"].append(time.perf_counter() - start)
51
-
52
- # Benchmark without autodiff
53
- autodiff(False)
54
- for _ in range(n_runs):
55
- start = time.perf_counter()
56
- func(model, **kwargs)
57
- results["finite_diff"].append(time.perf_counter() - start)
58
-
59
- autodiff(None)
60
- return results
61
-
62
-
63
- def print_results(name: str, results: dict):
64
- """Print benchmark results."""
65
- ad_mean = np.mean(results["autodiff"]) * 1000
66
- fd_mean = np.mean(results["finite_diff"]) * 1000
67
- speedup = fd_mean / ad_mean
68
- print(f"{name:30s} {ad_mean:6.1f} ms {fd_mean:6.1f} ms {speedup:.2f}x")
69
-
70
-
71
- if __name__ == "__main__":
72
- # Check JAX
73
- try:
74
- from marginaleffects.autodiff import _JAX_AVAILABLE
75
-
76
- if not _JAX_AVAILABLE:
77
- print("JAX not available")
78
- exit(1)
79
- except ImportError:
80
- print("autodiff module not found")
81
- exit(1)
82
-
83
- print("Autodiff Benchmark (5000 obs, 50 predictors)")
84
- print("=" * 65)
85
- print(f"{'Function':30s} {'JAX':>8s} {'Finite':>8s} Speedup")
86
- print("-" * 65)
87
-
88
- data = generate_data()
89
- formula = "y ~ " + " + ".join([f"x{i}" for i in range(50)])
90
-
91
- # OLS
92
- ols = sm.OLS.from_formula(formula, data.to_pandas()).fit()
93
- print_results("OLS predictions()", benchmark(predictions, ols))
94
- print_results("OLS avg_predictions()", benchmark(avg_predictions, ols))
95
- print_results("OLS comparisons()", benchmark(comparisons, ols, variables="x0"))
96
- print_results(
97
- "OLS avg_comparisons()", benchmark(avg_comparisons, ols, variables="x0")
98
- )
99
-
100
- # GLM
101
- glm = sm.GLM.from_formula(
102
- formula, data.to_pandas(), family=sm.families.Gaussian()
103
- ).fit()
104
- print_results("GLM predictions()", benchmark(predictions, glm))
105
- print_results("GLM avg_predictions()", benchmark(avg_predictions, glm))
106
- print_results("GLM comparisons()", benchmark(comparisons, glm, variables="x0"))
107
- print_results(
108
- "GLM avg_comparisons()", benchmark(avg_comparisons, glm, variables="x0")
109
- )
110
-
111
- print("=" * 65)