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.
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/PKG-INFO +2 -2
- marginaleffects-0.6.1/benchmarks/benchmark_autodiff.py +228 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/comparisons.py +2 -4
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/plan.py +13 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/predictions.py +7 -5
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects.egg-info/PKG-INFO +2 -2
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/pyproject.toml +2 -2
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_sklearn.py +43 -0
- marginaleffects-0.6.0/benchmarks/benchmark_autodiff.py +0 -111
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/README.md +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/benchmarks/__init__.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/__init__.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/autodiff/__init__.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/autodiff/glm/__init__.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/autodiff/glm/families.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/autodiff/lower.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/autodiff/ops.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/autodiff/pipeline.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/by.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/classes/__init__.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/classes/model.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/classes/result.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/datagrid.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/datasets.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/docstrings/__init__.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/docstrings/params.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/docstrings/qmd.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/estimands.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/formula.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/hypothesis_compile.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/linearmodels/__init__.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/linearmodels/model.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/plot/__init__.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/plot/common.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/plot/comparisons.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/plot/predictions.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/plot/slopes.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/pyfixest/__init__.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/pyfixest/model.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/sanitize/__init__.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/sanitize/by.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/sanitize/categorical.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/sanitize/comparison.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/sanitize/deprecated.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/sanitize/hypothesis_null.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/sanitize/newdata.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/sanitize/sanitize_model.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/sanitize/utils.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/sanitize/validation.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/sanitize/variables.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/sanitize/vcov.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/settings.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/sklearn/__init__.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/sklearn/model.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/slopes.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/statsmodels/__init__.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/statsmodels/model.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/test/__init__.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/test/core.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/test/equivalence.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/test/formula.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/test/joint.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/test/main.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/transform.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/uncertainty.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects/utils.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects.egg-info/SOURCES.txt +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects.egg-info/dependency_links.txt +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects.egg-info/requires.txt +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects.egg-info/top_level.txt +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/setup.cfg +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/__init__.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/helpers.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_analytic.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_autodiff.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_autodiff_lower.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_autodiff_pipeline.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_bugfix.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_by.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_categorical.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_categorical_validation.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_comparison_plan.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_comparisons.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_comparisons_interaction.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_datagrid_01.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_datagrid_02.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_equivalence.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_formula.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_formulaic_utils.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_hypotheses.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_hypotheses_joint.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_hypothesis.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_hypothesis_compile.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_jss.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_linearmodels_panelols.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_missing.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_newdata.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_plan.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_plot_comparisons.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_plot_predictions.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_plot_slopes.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_prediction_plan.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_predictions.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_pyfixest.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_slopes.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_statsmodels.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_statsmodels_logit.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_statsmodels_mixedlm.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_statsmodels_mnlogit.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_statsmodels_negativebinomial.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_statsmodels_ols.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_statsmodels_ordinal.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_statsmodels_poisson.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_statsmodels_probit.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_statsmodels_quantreg.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_statsmodels_vcov.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_statsmodels_wls.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_typical.py +0 -0
- {marginaleffects-0.6.0 → marginaleffects-0.6.1}/tests/test_utils.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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)
|
|
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
|
{marginaleffects-0.6.0 → marginaleffects-0.6.1}/marginaleffects.egg-info/dependency_links.txt
RENAMED
|
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
|