marginaleffects 0.5.1__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 (130) hide show
  1. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/PKG-INFO +3 -4
  2. marginaleffects-0.6.1/benchmarks/benchmark_autodiff.py +228 -0
  3. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/autodiff/__init__.py +4 -13
  4. marginaleffects-0.6.1/marginaleffects/autodiff/glm/__init__.py +1 -0
  5. marginaleffects-0.6.1/marginaleffects/autodiff/lower.py +219 -0
  6. marginaleffects-0.6.1/marginaleffects/autodiff/ops.py +60 -0
  7. marginaleffects-0.6.1/marginaleffects/autodiff/pipeline.py +237 -0
  8. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/by.py +42 -29
  9. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/classes/model.py +3 -0
  10. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/comparisons.py +185 -85
  11. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/datagrid.py +1 -1
  12. marginaleffects-0.6.1/marginaleffects/hypothesis_compile.py +213 -0
  13. marginaleffects-0.6.1/marginaleffects/plan.py +183 -0
  14. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/predictions.py +96 -86
  15. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/settings.py +7 -0
  16. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/statsmodels/model.py +29 -61
  17. marginaleffects-0.6.1/marginaleffects/test/core.py +17 -0
  18. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/test/formula.py +1 -1
  19. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/uncertainty.py +8 -2
  20. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects.egg-info/PKG-INFO +3 -4
  21. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects.egg-info/SOURCES.txt +11 -8
  22. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects.egg-info/requires.txt +1 -3
  23. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/pyproject.toml +3 -5
  24. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_autodiff.py +35 -1
  25. marginaleffects-0.6.1/tests/test_autodiff_lower.py +180 -0
  26. marginaleffects-0.6.1/tests/test_autodiff_pipeline.py +240 -0
  27. marginaleffects-0.6.1/tests/test_comparison_plan.py +162 -0
  28. marginaleffects-0.6.1/tests/test_hypothesis_compile.py +51 -0
  29. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_jss.py +2 -5
  30. marginaleffects-0.6.1/tests/test_plan.py +82 -0
  31. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_plot_comparisons.py +2 -5
  32. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_plot_predictions.py +3 -5
  33. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_plot_slopes.py +13 -6
  34. marginaleffects-0.6.1/tests/test_prediction_plan.py +98 -0
  35. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_sklearn.py +43 -0
  36. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_statsmodels_quantreg.py +0 -1
  37. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/utilities.py +13 -0
  38. marginaleffects-0.5.1/benchmarks/benchmark_autodiff.py +0 -111
  39. marginaleffects-0.5.1/marginaleffects/autodiff/comparisons.py +0 -69
  40. marginaleffects-0.5.1/marginaleffects/autodiff/dispatch.py +0 -310
  41. marginaleffects-0.5.1/marginaleffects/autodiff/glm/__init__.py +0 -5
  42. marginaleffects-0.5.1/marginaleffects/autodiff/glm/comparisons.py +0 -195
  43. marginaleffects-0.5.1/marginaleffects/autodiff/glm/predictions.py +0 -131
  44. marginaleffects-0.5.1/marginaleffects/autodiff/linear/__init__.py +0 -4
  45. marginaleffects-0.5.1/marginaleffects/autodiff/linear/comparisons.py +0 -147
  46. marginaleffects-0.5.1/marginaleffects/autodiff/linear/predictions.py +0 -71
  47. marginaleffects-0.5.1/marginaleffects/autodiff/utils.py +0 -31
  48. marginaleffects-0.5.1/marginaleffects/test/core.py +0 -155
  49. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/README.md +0 -0
  50. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/benchmarks/__init__.py +0 -0
  51. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/__init__.py +0 -0
  52. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/autodiff/glm/families.py +0 -0
  53. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/classes/__init__.py +0 -0
  54. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/classes/result.py +0 -0
  55. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/datasets.py +0 -0
  56. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/docstrings/__init__.py +0 -0
  57. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/docstrings/params.py +0 -0
  58. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/docstrings/qmd.py +0 -0
  59. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/estimands.py +0 -0
  60. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/formula.py +0 -0
  61. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/linearmodels/__init__.py +0 -0
  62. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/linearmodels/model.py +0 -0
  63. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/plot/__init__.py +0 -0
  64. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/plot/common.py +0 -0
  65. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/plot/comparisons.py +0 -0
  66. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/plot/predictions.py +0 -0
  67. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/plot/slopes.py +0 -0
  68. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/pyfixest/__init__.py +0 -0
  69. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/pyfixest/model.py +0 -0
  70. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/sanitize/__init__.py +0 -0
  71. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/sanitize/by.py +0 -0
  72. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/sanitize/categorical.py +0 -0
  73. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/sanitize/comparison.py +0 -0
  74. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/sanitize/deprecated.py +0 -0
  75. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/sanitize/hypothesis_null.py +0 -0
  76. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/sanitize/newdata.py +0 -0
  77. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/sanitize/sanitize_model.py +0 -0
  78. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/sanitize/utils.py +0 -0
  79. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/sanitize/validation.py +0 -0
  80. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/sanitize/variables.py +0 -0
  81. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/sanitize/vcov.py +0 -0
  82. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/sklearn/__init__.py +0 -0
  83. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/sklearn/model.py +0 -0
  84. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/slopes.py +0 -0
  85. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/statsmodels/__init__.py +0 -0
  86. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/test/__init__.py +0 -0
  87. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/test/equivalence.py +0 -0
  88. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/test/joint.py +0 -0
  89. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/test/main.py +0 -0
  90. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/transform.py +0 -0
  91. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects/utils.py +0 -0
  92. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects.egg-info/dependency_links.txt +0 -0
  93. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/marginaleffects.egg-info/top_level.txt +0 -0
  94. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/setup.cfg +0 -0
  95. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/__init__.py +0 -0
  96. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/helpers.py +0 -0
  97. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_analytic.py +0 -0
  98. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_bugfix.py +0 -0
  99. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_by.py +0 -0
  100. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_categorical.py +0 -0
  101. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_categorical_validation.py +0 -0
  102. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_comparisons.py +0 -0
  103. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_comparisons_interaction.py +0 -0
  104. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_datagrid_01.py +0 -0
  105. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_datagrid_02.py +0 -0
  106. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_equivalence.py +0 -0
  107. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_formula.py +0 -0
  108. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_formulaic_utils.py +0 -0
  109. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_hypotheses.py +0 -0
  110. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_hypotheses_joint.py +0 -0
  111. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_hypothesis.py +0 -0
  112. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_linearmodels_panelols.py +0 -0
  113. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_missing.py +0 -0
  114. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_newdata.py +0 -0
  115. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_predictions.py +0 -0
  116. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_pyfixest.py +0 -0
  117. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_slopes.py +0 -0
  118. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_statsmodels.py +0 -0
  119. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_statsmodels_logit.py +0 -0
  120. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_statsmodels_mixedlm.py +0 -0
  121. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_statsmodels_mnlogit.py +0 -0
  122. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_statsmodels_negativebinomial.py +0 -0
  123. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_statsmodels_ols.py +0 -0
  124. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_statsmodels_ordinal.py +0 -0
  125. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_statsmodels_poisson.py +0 -0
  126. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_statsmodels_probit.py +0 -0
  127. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_statsmodels_vcov.py +0 -0
  128. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_statsmodels_wls.py +0 -0
  129. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_typical.py +0 -0
  130. {marginaleffects-0.5.1 → marginaleffects-0.6.1}/tests/test_utils.py +0 -0
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: marginaleffects
3
- Version: 0.5.1
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
+ Requires-Dist: jax>=0.4.0
9
10
  Requires-Dist: narwhals>=1.34.0
10
11
  Requires-Dist: numpy>=2.0.0
11
12
  Requires-Dist: patsy>=1.0.1
@@ -14,8 +15,6 @@ Requires-Dist: pydantic>=2.10.3
14
15
  Requires-Dist: plotnine>=0.14.5
15
16
  Requires-Dist: scipy>=1.14.1
16
17
  Requires-Dist: pyarrow>=19.0.1
17
- Provides-Extra: autodiff
18
- Requires-Dist: jax>=0.4.0; extra == "autodiff"
19
18
  Provides-Extra: test
20
19
  Requires-Dist: duckdb>=1.1.2; extra == "test"
21
20
  Requires-Dist: matplotlib>=3.7.1; extra == "test"
@@ -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)
@@ -29,18 +29,14 @@ if _JAX_AVAILABLE:
29
29
 
30
30
  jax.config.update("jax_enable_x64", True)
31
31
 
32
- # Import submodules to make them accessible
33
- from . import linear as linear
34
32
  from . import glm as glm
33
+ from . import pipeline as pipeline
35
34
 
36
- # Re-export types for convenience
37
- from .comparisons import ComparisonType as ComparisonType
38
35
  from .glm.families import Family as Family, Link as Link
39
36
 
40
37
  __all__ = [
41
- "linear",
42
38
  "glm",
43
- "ComparisonType",
39
+ "pipeline",
44
40
  "Family",
45
41
  "Link",
46
42
  ]
@@ -50,13 +46,8 @@ else:
50
46
  def __getattr__(self, name):
51
47
  _raise_jax_error()
52
48
 
53
- linear = _DummyModule()
54
49
  glm = _DummyModule()
55
-
56
- # Create dummy enums that raise errors
57
- class ComparisonType:
58
- def __getattribute__(self, name):
59
- _raise_jax_error()
50
+ pipeline = _DummyModule()
60
51
 
61
52
  class Family:
62
53
  def __getattribute__(self, name):
@@ -66,4 +57,4 @@ else:
66
57
  def __getattribute__(self, name):
67
58
  _raise_jax_error()
68
59
 
69
- __all__ = []
60
+ __all__ = ["glm", "pipeline", "Family", "Link"]
@@ -0,0 +1 @@
1
+ from . import families as families
@@ -0,0 +1,219 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import warnings
5
+
6
+ import numpy as np
7
+
8
+ from ..settings import is_autodiff_enabled, is_autodiff_forced
9
+ from ..uncertainty import get_se
10
+ from .ops import COMPARISON_OPS
11
+
12
+
13
+ @dataclass
14
+ class Lowered:
15
+ ok: bool
16
+ kwargs: dict | None = None
17
+ reason: str = ""
18
+
19
+
20
+ @dataclass
21
+ class AutodiffResult:
22
+ std_error: np.ndarray
23
+ jacobian: np.ndarray
24
+
25
+
26
+ def _fail(reason: str) -> Lowered:
27
+ return Lowered(False, kwargs=None, reason=reason)
28
+
29
+
30
+ def _model_args(model):
31
+ args = model.get_autodiff_args()
32
+ if args is None:
33
+ return None, _fail("")
34
+ if isinstance(args, str):
35
+ return None, _fail(args)
36
+ return args, None
37
+
38
+
39
+ def _coefs_or_failure(model):
40
+ coefs = np.asarray(model.get_coef(), dtype=float).reshape(-1)
41
+ if np.isnan(coefs).any():
42
+ return None, _fail("models with NA coefficients")
43
+ return coefs, None
44
+
45
+
46
+ def _design_or_failure(X, coefs, n_pred):
47
+ X = np.asarray(X, dtype=float)
48
+ if X.ndim != 2 or X.shape[1] != coefs.size or X.shape[0] != n_pred:
49
+ return None, _fail("this model/data configuration")
50
+ return X, None
51
+
52
+
53
+ def _hypothesis_or_failure(plan):
54
+ if plan.hyp is None:
55
+ return None, None
56
+ if plan.hyp.kind != "matrix":
57
+ return None, _fail("this form of the `hypothesis` argument")
58
+ return np.asarray(plan.hyp.H, dtype=float), None
59
+
60
+
61
+ def _has_nan(x) -> bool:
62
+ return x is not None and np.isnan(np.asarray(x, dtype=float)).any()
63
+
64
+
65
+ def lower_predictions(plan, model) -> Lowered:
66
+ args, failure = _model_args(model)
67
+ if failure is not None:
68
+ return failure
69
+ coefs, failure = _coefs_or_failure(model)
70
+ if failure is not None:
71
+ return failure
72
+ X, failure = _design_or_failure(plan.exog, coefs, plan.n_pred)
73
+ if failure is not None:
74
+ return failure
75
+ if plan.align is not None:
76
+ return _fail("models with grouped/multi-equation outcomes")
77
+ if plan.has_na:
78
+ return _fail("missing values in predictions")
79
+ H, failure = _hypothesis_or_failure(plan)
80
+ if failure is not None:
81
+ return failure
82
+
83
+ agg_segments = None
84
+ agg_num_segments = None
85
+ agg_weights = None
86
+ if plan.agg is not None:
87
+ agg_segments = np.empty(plan.n_pred, dtype=np.int32)
88
+ agg_weights = np.ones(plan.n_pred, dtype=float)
89
+ for i, group in enumerate(plan.agg):
90
+ agg_segments[group.idx] = i
91
+ if group.w is not None:
92
+ if _has_nan(group.w):
93
+ return _fail("missing values in weights")
94
+ agg_weights[group.idx] = np.asarray(group.w, dtype=float)
95
+ agg_num_segments = len(plan.agg)
96
+
97
+ kwargs = {
98
+ **args,
99
+ "X": X,
100
+ "agg_segments": agg_segments,
101
+ "agg_num_segments": agg_num_segments,
102
+ "agg_weights": agg_weights if agg_segments is not None else None,
103
+ "H": H,
104
+ }
105
+ return Lowered(True, kwargs=kwargs)
106
+
107
+
108
+ def lower_comparisons(plan, model) -> Lowered:
109
+ args, failure = _model_args(model)
110
+ if failure is not None:
111
+ return failure
112
+ coefs, failure = _coefs_or_failure(model)
113
+ if failure is not None:
114
+ return failure
115
+ X_hi, failure = _design_or_failure(plan.exog_hi, coefs, plan.n_pred)
116
+ if failure is not None:
117
+ return failure
118
+ X_lo, failure = _design_or_failure(plan.exog_lo, coefs, plan.n_pred)
119
+ if failure is not None:
120
+ return failure
121
+ if plan.align is not None:
122
+ return _fail("models with grouped/multi-equation outcomes")
123
+ if plan.has_na:
124
+ return _fail("missing values in predictions")
125
+
126
+ if any(group.fun_key is None for group in plan.groups):
127
+ return _fail("custom comparison functions")
128
+ if plan.need_y:
129
+ return _fail("elasticities")
130
+ for group in plan.groups:
131
+ if group.fun_key not in COMPARISON_OPS:
132
+ return _fail(f"comparison='{group.fun_key}'")
133
+
134
+ H, failure = _hypothesis_or_failure(plan)
135
+ if failure is not None:
136
+ return failure
137
+
138
+ if not plan.groups:
139
+ order = np.asarray([], dtype=int)
140
+ else:
141
+ order = np.concatenate([group.idx for group in plan.groups]).astype(int)
142
+ if order.size != plan.n_pred:
143
+ return _fail("this model/data configuration")
144
+
145
+ ops = []
146
+ for group in plan.groups:
147
+ spec = COMPARISON_OPS[group.fun_key]
148
+ w = None
149
+ if spec.weighted:
150
+ if _has_nan(group.w):
151
+ return _fail("missing values in weights")
152
+ w = None if group.w is None else np.asarray(group.w, dtype=float)
153
+ ops.append({"op": spec.pipeline_op, "n": len(group.idx), "w": w})
154
+
155
+ kwargs = {
156
+ **args,
157
+ "X_hi": X_hi[order],
158
+ "X_lo": X_lo[order],
159
+ "ops": ops,
160
+ "H": H,
161
+ }
162
+ return Lowered(True, kwargs=kwargs)
163
+
164
+
165
+ def _warn_unsupported(reason):
166
+ if reason:
167
+ warnings.warn(
168
+ "Automatic differentiation does not support "
169
+ f"{reason}. Reverting to finite differences.",
170
+ UserWarning,
171
+ stacklevel=3,
172
+ )
173
+
174
+
175
+ def autodiff_try(plan, model, V, estimate, kind):
176
+ if plan is None or V is None or not is_autodiff_enabled():
177
+ return None
178
+
179
+ warn_on_fallback = is_autodiff_forced()
180
+ lowered = (
181
+ lower_predictions(plan, model)
182
+ if kind == "predictions"
183
+ else lower_comparisons(plan, model)
184
+ )
185
+ if not lowered.ok:
186
+ if warn_on_fallback:
187
+ _warn_unsupported(lowered.reason)
188
+ return None
189
+
190
+ try:
191
+ from . import pipeline
192
+
193
+ result = pipeline.compute(beta=model.get_coef(), **lowered.kwargs)
194
+ except Exception as exc:
195
+ if warn_on_fallback:
196
+ warnings.warn(
197
+ "Automatic differentiation failed "
198
+ f"({exc}). Reverting to finite differences.",
199
+ UserWarning,
200
+ stacklevel=3,
201
+ )
202
+ return None
203
+
204
+ estimate = np.asarray(estimate, dtype=float).reshape(-1)
205
+ if not np.allclose(result["estimate"], estimate, rtol=1e-8, atol=1e-8):
206
+ if warn_on_fallback:
207
+ warnings.warn(
208
+ "Automatic differentiation estimates did not match the standard "
209
+ "pipeline. Reverting to finite differences.",
210
+ UserWarning,
211
+ stacklevel=3,
212
+ )
213
+ return None
214
+
215
+ # Coef/vcov positional alignment is guaranteed by each adapter vault.
216
+ J = np.asarray(result["jacobian"], dtype=float)
217
+ se = get_se(J, V)
218
+ se[se == 0] = np.nan
219
+ return AutodiffResult(std_error=se, jacobian=J)
@@ -0,0 +1,60 @@
1
+ """Shared comparison operation registry for autodiff lowering and execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+
10
+ ArrayFn = Callable[[list[Any]], Any]
11
+ WMeanFn = Callable[[Any, Any | None], Any]
12
+ EstimateFn = Callable[[Any, Any, Any | None, ArrayFn, WMeanFn], Any]
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class PipelineOp:
17
+ estimate: EstimateFn
18
+ scalar: bool
19
+
20
+ def output_size(self, n: int) -> int:
21
+ return 1 if self.scalar else n
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class ComparisonOp:
26
+ pipeline_op: str
27
+ weighted: bool
28
+
29
+
30
+ def _difference(hi, lo, _w, _array, _wmean):
31
+ return hi - lo
32
+
33
+
34
+ def _ratio(hi, lo, _w, _array, _wmean):
35
+ return hi / lo
36
+
37
+
38
+ def _differenceavg(hi, lo, w, array, wmean):
39
+ return array([wmean(hi, w) - wmean(lo, w)])
40
+
41
+
42
+ def _ratioavg(hi, lo, w, array, wmean):
43
+ return array([wmean(hi, w) / wmean(lo, w)])
44
+
45
+
46
+ PIPELINE_OPS = {
47
+ "difference": PipelineOp(estimate=_difference, scalar=False),
48
+ "ratio": PipelineOp(estimate=_ratio, scalar=False),
49
+ "differenceavg": PipelineOp(estimate=_differenceavg, scalar=True),
50
+ "ratioavg": PipelineOp(estimate=_ratioavg, scalar=True),
51
+ }
52
+
53
+ COMPARISON_OPS = {
54
+ "difference": ComparisonOp(pipeline_op="difference", weighted=False),
55
+ "ratio": ComparisonOp(pipeline_op="ratio", weighted=False),
56
+ "differenceavg": ComparisonOp(pipeline_op="differenceavg", weighted=False),
57
+ "ratioavg": ComparisonOp(pipeline_op="ratioavg", weighted=False),
58
+ "differenceavgwts": ComparisonOp(pipeline_op="differenceavg", weighted=True),
59
+ "ratioavgwts": ComparisonOp(pipeline_op="ratioavg", weighted=True),
60
+ }