augsynth-py 0.3.0__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.
- augsynth_py-0.3.0/.gitignore +91 -0
- augsynth_py-0.3.0/CHANGELOG.md +42 -0
- augsynth_py-0.3.0/CLAUDE.md +211 -0
- augsynth_py-0.3.0/LICENSE +21 -0
- augsynth_py-0.3.0/PKG-INFO +198 -0
- augsynth_py-0.3.0/README.md +137 -0
- augsynth_py-0.3.0/docs/clean-room-audit-2026-05-16.md +282 -0
- augsynth_py-0.3.0/docs/clean-room-audit-2026-05-26-augsynth.md +553 -0
- augsynth_py-0.3.0/docs/clean-room-audit-2026-07-07-inference.md +469 -0
- augsynth_py-0.3.0/docs/known-issues.md +87 -0
- augsynth_py-0.3.0/docs/methodology.md +573 -0
- augsynth_py-0.3.0/docs/releasing.md +68 -0
- augsynth_py-0.3.0/pyproject.toml +194 -0
- augsynth_py-0.3.0/src/augsynth_py/__init__.py +7 -0
- augsynth_py-0.3.0/src/augsynth_py/_version.py +3 -0
- augsynth_py-0.3.0/src/augsynth_py/exceptions.py +24 -0
- augsynth_py-0.3.0/src/augsynth_py/inference.py +529 -0
- augsynth_py-0.3.0/src/augsynth_py/py.typed +0 -0
- augsynth_py-0.3.0/src/augsynth_py/synth/__init__.py +12 -0
- augsynth_py-0.3.0/src/augsynth_py/synth/_panel.py +254 -0
- augsynth_py-0.3.0/src/augsynth_py/synth/augmented.py +747 -0
- augsynth_py-0.3.0/src/augsynth_py/synth/classical.py +304 -0
- augsynth_py-0.3.0/tests/__init__.py +0 -0
- augsynth_py-0.3.0/tests/conftest.py +16 -0
- augsynth_py-0.3.0/tests/unit/__init__.py +0 -0
- augsynth_py-0.3.0/tests/unit/test_augsynth.py +897 -0
- augsynth_py-0.3.0/tests/unit/test_classical_synth.py +244 -0
- augsynth_py-0.3.0/tests/unit/test_imbalance.py +173 -0
- augsynth_py-0.3.0/tests/unit/test_inference_core.py +84 -0
- augsynth_py-0.3.0/tests/unit/test_inference_interval.py +297 -0
- augsynth_py-0.3.0/tests/unit/test_inference_pvalue.py +144 -0
- augsynth_py-0.3.0/tests/unit/test_multitreated.py +100 -0
- augsynth_py-0.3.0/tests/unit/test_panel.py +107 -0
- augsynth_py-0.3.0/tests/unit/test_smoke.py +21 -0
- augsynth_py-0.3.0/tests/validation_against_r/README.md +57 -0
- augsynth_py-0.3.0/tests/validation_against_r/__init__.py +0 -0
- augsynth_py-0.3.0/tests/validation_against_r/conftest.py +153 -0
- augsynth_py-0.3.0/tests/validation_against_r/test_augsynth.py +476 -0
- augsynth_py-0.3.0/tests/validation_against_r/test_classical_synth.py +110 -0
- augsynth_py-0.3.0/tests/validation_against_r/test_conformal.py +439 -0
- augsynth_py-0.3.0/tests/validation_against_r/test_imbalance.py +245 -0
- augsynth_py-0.3.0/tests/validation_against_r/test_multitreated.py +100 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
|
|
7
|
+
# Distribution / packaging
|
|
8
|
+
.Python
|
|
9
|
+
build/
|
|
10
|
+
develop-eggs/
|
|
11
|
+
dist/
|
|
12
|
+
downloads/
|
|
13
|
+
eggs/
|
|
14
|
+
.eggs/
|
|
15
|
+
lib/
|
|
16
|
+
lib64/
|
|
17
|
+
parts/
|
|
18
|
+
sdist/
|
|
19
|
+
var/
|
|
20
|
+
wheels/
|
|
21
|
+
share/python-wheels/
|
|
22
|
+
*.egg-info/
|
|
23
|
+
.installed.cfg
|
|
24
|
+
*.egg
|
|
25
|
+
MANIFEST
|
|
26
|
+
|
|
27
|
+
# Virtual environments
|
|
28
|
+
.env
|
|
29
|
+
.venv
|
|
30
|
+
env/
|
|
31
|
+
venv/
|
|
32
|
+
ENV/
|
|
33
|
+
|
|
34
|
+
# Testing / coverage
|
|
35
|
+
.pytest_cache/
|
|
36
|
+
.coverage
|
|
37
|
+
.coverage.*
|
|
38
|
+
htmlcov/
|
|
39
|
+
.tox/
|
|
40
|
+
.nox/
|
|
41
|
+
coverage.xml
|
|
42
|
+
*.cover
|
|
43
|
+
*.py,cover
|
|
44
|
+
.hypothesis/
|
|
45
|
+
|
|
46
|
+
# Type checking
|
|
47
|
+
.mypy_cache/
|
|
48
|
+
.dmypy.json
|
|
49
|
+
.pyre/
|
|
50
|
+
.pytype/
|
|
51
|
+
.ruff_cache/
|
|
52
|
+
|
|
53
|
+
# IDEs
|
|
54
|
+
.vscode/
|
|
55
|
+
.idea/
|
|
56
|
+
*.swp
|
|
57
|
+
*.swo
|
|
58
|
+
*~
|
|
59
|
+
|
|
60
|
+
# Jupyter
|
|
61
|
+
.ipynb_checkpoints/
|
|
62
|
+
*.ipynb_checkpoints
|
|
63
|
+
|
|
64
|
+
# OS
|
|
65
|
+
.DS_Store
|
|
66
|
+
Thumbs.db
|
|
67
|
+
|
|
68
|
+
# Project-specific
|
|
69
|
+
notebooks/scratch/
|
|
70
|
+
notebooks/*.ipynb
|
|
71
|
+
!notebooks/README.md
|
|
72
|
+
|
|
73
|
+
# R artifacts (in case someone runs R locally for validation)
|
|
74
|
+
.Rhistory
|
|
75
|
+
.RData
|
|
76
|
+
.Ruserdata
|
|
77
|
+
|
|
78
|
+
# Claude Code (local agent state, settings, scratch plans)
|
|
79
|
+
.claude/
|
|
80
|
+
.claudeignore
|
|
81
|
+
CLAUDE.local.md
|
|
82
|
+
docs/plans/
|
|
83
|
+
|
|
84
|
+
# Paper working material (local only — not part of the package)
|
|
85
|
+
paper/
|
|
86
|
+
|
|
87
|
+
# Git worktrees (local isolated workspaces)
|
|
88
|
+
.worktrees/
|
|
89
|
+
|
|
90
|
+
# papers/: only SOURCES.md is tracked; keep local reading copies untracked
|
|
91
|
+
papers/*.pdf
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
|
|
7
|
+
(with the usual 0.x caveat: minor versions may break the API).
|
|
8
|
+
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
## [0.3.0] - 2026-08-03
|
|
12
|
+
|
|
13
|
+
First public release on PyPI. Everything below is new in the sense that no
|
|
14
|
+
earlier version was ever published; internally this tree evolved through the
|
|
15
|
+
v0.1 (classical SCM) and v0.2 (ridge ASCM) design milestones.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- `Synth`: classical simplex-constrained synthetic control
|
|
20
|
+
(Abadie, Diamond & Hainmueller 2010; outcome-only Doudchenko & Imbens 2016
|
|
21
|
+
form, analogue of R `augsynth(progfunc = "None", scm = TRUE)`), with
|
|
22
|
+
optional unit fixed effects (`fixedeff=True` default).
|
|
23
|
+
- `AugSynth`: ridge-augmented synthetic control (Ben-Michael, Feller &
|
|
24
|
+
Rothstein 2021, §2.3–2.4), with leave-one-out cross-validation of the
|
|
25
|
+
ridge penalty (auto-grid or user grid) or a fixed `lambda_`.
|
|
26
|
+
- `conformal_pvalue` / `conformal_interval`: exact conformal inference for a
|
|
27
|
+
constant post-period effect (Chernozhukov, Wuthrich & Zhu 2021, §3), with
|
|
28
|
+
moving-block (default) and iid permutation schemes and refit-under-the-null
|
|
29
|
+
residuals.
|
|
30
|
+
- Multi-treated fits: `treated` accepts an iterable of units; the effect is
|
|
31
|
+
estimated on the treated-group mean.
|
|
32
|
+
- `conformal_interval` detects a non-contiguous acceptance region (accepted
|
|
33
|
+
grid points with interior rejected gaps — demonstrated on the Basque panel,
|
|
34
|
+
where the gap contains the point estimate itself) and emits a `UserWarning`
|
|
35
|
+
clarifying that the returned bounds are the conservative min/max envelope
|
|
36
|
+
(clean-room audit D-6, Recommendation 4).
|
|
37
|
+
- Numerical parity suite against R `augsynth` (`tests/validation_against_r/`),
|
|
38
|
+
including exact p-value-curve parity on the Basque panel.
|
|
39
|
+
- PEP 561 `py.typed` marker; `mypy --strict` clean on `src/`.
|
|
40
|
+
|
|
41
|
+
[Unreleased]: https://github.com/mrcsvg/augsynth-py/compare/v0.3.0...HEAD
|
|
42
|
+
[0.3.0]: https://github.com/mrcsvg/augsynth-py/releases/tag/v0.3.0
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# augsynth-py
|
|
2
|
+
|
|
3
|
+
A Python implementation of Augmented Synthetic Control Methods (ASCM) and related
|
|
4
|
+
geo-experimentation methodology. The end goal is feature parity with the R
|
|
5
|
+
`augsynth` package and a higher-level orchestration layer comparable to Meta's
|
|
6
|
+
GeoLift (R).
|
|
7
|
+
|
|
8
|
+
This file is the entry point for AI coding assistants working on this repo.
|
|
9
|
+
Read it before starting any task.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Project goal
|
|
14
|
+
|
|
15
|
+
Provide a Python-native, methodologically faithful implementation of:
|
|
16
|
+
|
|
17
|
+
1. Classical synthetic control (Abadie, Diamond & Hainmueller, 2010).
|
|
18
|
+
2. Augmented synthetic control with ridge augmentation (Ben-Michael, Feller & Rothstein, 2021).
|
|
19
|
+
3. Generalized synthetic control (Xu, 2017).
|
|
20
|
+
4. Power analysis and market selection for geo-experiments (the orchestration
|
|
21
|
+
layer that GeoLift provides on top of `augsynth`).
|
|
22
|
+
|
|
23
|
+
The package must be usable in production data science workflows. Methodology
|
|
24
|
+
correctness is the top priority, performance is secondary, ergonomics third.
|
|
25
|
+
|
|
26
|
+
## Implementation strategy: clean-room from papers
|
|
27
|
+
|
|
28
|
+
All algorithms are implemented **from the published papers**, not by translating
|
|
29
|
+
the R `augsynth` source code. This keeps the project under a permissive license
|
|
30
|
+
(MIT) and forces a deeper understanding of the methods.
|
|
31
|
+
|
|
32
|
+
Reference implementations in R (`augsynth`, `gsynth`, `Synth`, `GeoLift`) are
|
|
33
|
+
used **only as oracles for validation tests**, never as a source to translate
|
|
34
|
+
from. Do not copy R source code into this repo.
|
|
35
|
+
|
|
36
|
+
## Canonical references
|
|
37
|
+
|
|
38
|
+
The implementation must be traceable to these papers. New code should cite the
|
|
39
|
+
relevant paper and equation in docstrings.
|
|
40
|
+
|
|
41
|
+
- Abadie, Diamond & Hainmueller (2010). *Synthetic Control Methods for
|
|
42
|
+
Comparative Case Studies*. JASA.
|
|
43
|
+
- Abadie (2021). *Using Synthetic Controls: Feasibility, Data Requirements,
|
|
44
|
+
and Methodological Aspects*. JEL.
|
|
45
|
+
- Ben-Michael, Feller & Rothstein (2021). *The Augmented Synthetic Control
|
|
46
|
+
Method*. JASA.
|
|
47
|
+
- Xu (2017). *Generalized Synthetic Control Method*. Political Analysis.
|
|
48
|
+
- Doudchenko & Imbens (2016). *Balancing, Regression, Difference-in-Differences
|
|
49
|
+
and Synthetic Control Methods: A Synthesis*. NBER WP 22791.
|
|
50
|
+
- Chernozhukov, Wuthrich & Zhu (2021). *An Exact and Robust Conformal Inference
|
|
51
|
+
Method for Counterfactual and Synthetic Controls*. JASA.
|
|
52
|
+
|
|
53
|
+
## Architectural decisions (already made — do not relitigate without reason)
|
|
54
|
+
|
|
55
|
+
| Concern | Choice | Why |
|
|
56
|
+
|----------------------|-----------------------------------|--------------------------------------------------------|
|
|
57
|
+
| Language | Python 3.11+ | Practical adoption, scientific ecosystem |
|
|
58
|
+
| Numerics | `numpy`, `scipy` | Native BLAS/LAPACK, stable APIs |
|
|
59
|
+
| Convex optimization | `cvxpy` (OSQP / Clarabel backends)| Cleanest expression of constrained QPs |
|
|
60
|
+
| Tabular data | `polars` | Fast, sane API, scales to panel datasets |
|
|
61
|
+
| Parallelism | `joblib` | Power analysis is embarrassingly parallel |
|
|
62
|
+
| Build backend | `hatchling` | Modern, simple, PEP 621 native |
|
|
63
|
+
| Layout | `src/` layout | Avoids accidental imports during testing |
|
|
64
|
+
| Lint + format | `ruff` | One tool, fast |
|
|
65
|
+
| Type checking | `mypy --strict` on `src/` | Catch shape/contract bugs early |
|
|
66
|
+
| Tests | `pytest` | Standard |
|
|
67
|
+
| Validation oracle | R `augsynth` via `rpy2` | Ground truth for parity tests |
|
|
68
|
+
|
|
69
|
+
**Do not introduce new dependencies without justification in the PR.**
|
|
70
|
+
**Do not reach for Rust/C extensions before profiling identifies a real hot
|
|
71
|
+
loop.** This is a project rule, not a suggestion. Premature optimization in a
|
|
72
|
+
foreign language has killed similar projects.
|
|
73
|
+
|
|
74
|
+
## Validation rule (non-negotiable)
|
|
75
|
+
|
|
76
|
+
Every public estimator must have at least one test in
|
|
77
|
+
`tests/validation_against_r/` that:
|
|
78
|
+
|
|
79
|
+
1. Runs the same input through both the Python implementation and the R
|
|
80
|
+
reference (`augsynth` / `gsynth` / `Synth`) via `rpy2`.
|
|
81
|
+
2. Asserts numerical agreement on weights, counterfactuals, and ATT estimates
|
|
82
|
+
within a documented tolerance (default `atol=1e-6, rtol=1e-5`; relax with
|
|
83
|
+
justification only).
|
|
84
|
+
3. Uses a fixture from the `GeoLift_PreTest` panel (40 US cities, 90 days)
|
|
85
|
+
loaded once per session.
|
|
86
|
+
|
|
87
|
+
PRs that add a new estimator without a parity test should be rejected.
|
|
88
|
+
|
|
89
|
+
## Roadmap and scope
|
|
90
|
+
|
|
91
|
+
Shipped so far: classical `Synth` (v0.1), ridge-augmented `AugSynth` with
|
|
92
|
+
LOO-CV (v0.2), conformal inference per CWZ 2021 (v0.3) — each with unit +
|
|
93
|
+
R-parity tests and a clean-room audit in `docs/`.
|
|
94
|
+
|
|
95
|
+
Critical path toward end-to-end geo-experiments (the project's reason to
|
|
96
|
+
exist), in order:
|
|
97
|
+
|
|
98
|
+
1. **v0.4 — power analysis / MDE** (`augsynth_py.power`): effect injection +
|
|
99
|
+
refit + detection rate via conformal inference; parallel grid with joblib.
|
|
100
|
+
Parity oracle: `GeoLiftPower`.
|
|
101
|
+
2. **v0.5 — market selection**: candidate ranking over treatment sets ×
|
|
102
|
+
durations, `GeoLiftMarketSelection` analogue. Completes the single-cell
|
|
103
|
+
GeoLift flow.
|
|
104
|
+
3. **v0.6+ — multi-cell experiments**; then, if demand appears, auxiliary
|
|
105
|
+
covariates $X_i$ and GSC (Xu 2017, incl. BFR 2021 §4 augmentation).
|
|
106
|
+
|
|
107
|
+
Deferred until further notice (do not start without maintainer sign-off):
|
|
108
|
+
|
|
109
|
+
- AugSynth with auxiliary covariates / predictors $X_i$ — needs `Synth`
|
|
110
|
+
predictor support first.
|
|
111
|
+
- Matrix-completion augmentation (Athey et al. 2021) — would land as a
|
|
112
|
+
sibling estimator.
|
|
113
|
+
- GSC / gsynth (Xu 2017) and BFR 2021 §4 GSC augmentation.
|
|
114
|
+
|
|
115
|
+
Permanently out of scope:
|
|
116
|
+
|
|
117
|
+
- Bayesian variants — that's CausalPy's territory.
|
|
118
|
+
- GPU acceleration — not needed.
|
|
119
|
+
- Rust extensions — not needed, see above.
|
|
120
|
+
|
|
121
|
+
## Repository layout
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
augsynth-py/
|
|
125
|
+
├── CLAUDE.md <- you are here
|
|
126
|
+
├── README.md
|
|
127
|
+
├── CHANGELOG.md <- Keep a Changelog format
|
|
128
|
+
├── LICENSE <- MIT
|
|
129
|
+
├── pyproject.toml
|
|
130
|
+
├── src/augsynth_py/
|
|
131
|
+
│ ├── __init__.py <- public exports
|
|
132
|
+
│ ├── _version.py <- single source of truth for the version
|
|
133
|
+
│ ├── exceptions.py <- domain exceptions
|
|
134
|
+
│ ├── inference.py <- conformal inference (CWZ 2021)
|
|
135
|
+
│ ├── py.typed <- PEP 561 marker
|
|
136
|
+
│ └── synth/ <- estimator implementations
|
|
137
|
+
├── tests/
|
|
138
|
+
│ ├── conftest.py <- shared fixtures
|
|
139
|
+
│ ├── unit/ <- pure-Python unit tests, no R required
|
|
140
|
+
│ └── validation_against_r/ <- parity tests, require R + augsynth
|
|
141
|
+
├── docs/
|
|
142
|
+
│ ├── methodology.md <- maps code to paper sections/equations
|
|
143
|
+
│ ├── known-issues.md <- open investigations register
|
|
144
|
+
│ ├── releasing.md <- PyPI release runbook
|
|
145
|
+
│ └── clean-room-audit-*.md <- one audit per estimator/module
|
|
146
|
+
├── papers/
|
|
147
|
+
│ └── SOURCES.md <- link index (PDFs are not tracked)
|
|
148
|
+
├── notebooks/ <- exploratory notebooks, not shipped
|
|
149
|
+
└── .github/workflows/
|
|
150
|
+
├── ci.yml <- lint, type, unit tests
|
|
151
|
+
├── validation.yml <- parity tests against R
|
|
152
|
+
└── release.yml <- tag-triggered PyPI trusted publishing
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Code conventions
|
|
156
|
+
|
|
157
|
+
- Type hints everywhere in `src/`. Public APIs use `numpy.typing.NDArray` for
|
|
158
|
+
arrays.
|
|
159
|
+
- Docstrings: NumPy style. Every public function cites paper + equation when
|
|
160
|
+
implementing a known method.
|
|
161
|
+
- No `print()` in library code. Use the `logging` module at INFO/DEBUG.
|
|
162
|
+
- No global mutable state. Estimators are classes with `fit()` populating
|
|
163
|
+
trailing-underscore fitted attributes (`att_`, `weights_`, ...); a separate
|
|
164
|
+
`predict()` has not been needed so far — introduce one only with maintainer
|
|
165
|
+
sign-off on the API shape.
|
|
166
|
+
- Random seeds: every stochastic function takes `rng: np.random.Generator`,
|
|
167
|
+
never reads global state.
|
|
168
|
+
- Errors: raise `ValueError` for bad input, custom exceptions in
|
|
169
|
+
`augsynth_py.exceptions` for domain errors.
|
|
170
|
+
|
|
171
|
+
## Testing conventions
|
|
172
|
+
|
|
173
|
+
- `tests/unit/` runs in CI on every push, no R required.
|
|
174
|
+
- `tests/validation_against_r/` runs in a separate CI job that installs R
|
|
175
|
+
and the reference packages. Use `pytest.importorskip("rpy2")` so local
|
|
176
|
+
developers without R can still run the unit suite.
|
|
177
|
+
- Parity fixtures are loaded from the R packages themselves via `rpy2`
|
|
178
|
+
(`GeoLift_PreTest` in `tests/validation_against_r/conftest.py`) or from
|
|
179
|
+
public CSVs under `notebooks/_data/` (Basque panel). There is no
|
|
180
|
+
`tests/fixtures/` directory today; create one only if a fixture cannot
|
|
181
|
+
live in either of those places.
|
|
182
|
+
- Snapshot tests for plotting outputs are NOT used; we test data, not pixels.
|
|
183
|
+
|
|
184
|
+
## Common task recipes
|
|
185
|
+
|
|
186
|
+
### Adding a new estimator
|
|
187
|
+
|
|
188
|
+
1. Create `src/augsynth_py/synth/<name>.py` with a class implementing `fit` and
|
|
189
|
+
`predict`.
|
|
190
|
+
2. Cite the paper and equation in the class docstring.
|
|
191
|
+
3. Add unit tests in `tests/unit/test_<name>.py` covering edge cases (single
|
|
192
|
+
treated unit, perfect pre-period fit, degenerate donor pool).
|
|
193
|
+
4. Add a parity test in `tests/validation_against_r/test_<name>.py` against the
|
|
194
|
+
R reference.
|
|
195
|
+
5. Document the estimator in `docs/methodology.md` with the mapping to paper
|
|
196
|
+
sections.
|
|
197
|
+
6. Export from `src/augsynth_py/__init__.py`.
|
|
198
|
+
|
|
199
|
+
### Updating dependencies
|
|
200
|
+
|
|
201
|
+
- Edit `pyproject.toml`, not `requirements.txt` (we don't have one).
|
|
202
|
+
- Justify the dependency in the PR description.
|
|
203
|
+
- Prefer `scipy` / `numpy` primitives over new packages.
|
|
204
|
+
|
|
205
|
+
## Things to confirm with the maintainer before doing
|
|
206
|
+
|
|
207
|
+
- Adding a dependency.
|
|
208
|
+
- Changing the public API of any class already exported from `__init__.py`.
|
|
209
|
+
- Relaxing a validation tolerance.
|
|
210
|
+
- Adding a Rust / Cython / C extension.
|
|
211
|
+
- Adding GPU support.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 augsynth-py contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: augsynth-py
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Augmented Synthetic Control Methods and geo-experimentation tooling for Python.
|
|
5
|
+
Project-URL: Repository, https://github.com/mrcsvg/augsynth-py
|
|
6
|
+
Project-URL: Issues, https://github.com/mrcsvg/augsynth-py/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/mrcsvg/augsynth-py/blob/main/CHANGELOG.md
|
|
8
|
+
Author: augsynth-py contributors
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 augsynth-py contributors
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: causal-inference,econometrics,geo-experiments,marketing-science,synthetic-control
|
|
32
|
+
Classifier: Development Status :: 3 - Alpha
|
|
33
|
+
Classifier: Intended Audience :: Science/Research
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Programming Language :: Python :: 3
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
38
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
39
|
+
Classifier: Typing :: Typed
|
|
40
|
+
Requires-Python: >=3.11
|
|
41
|
+
Requires-Dist: cvxpy>=1.5
|
|
42
|
+
Requires-Dist: joblib>=1.3
|
|
43
|
+
Requires-Dist: numpy>=1.26
|
|
44
|
+
Requires-Dist: polars>=1.0
|
|
45
|
+
Requires-Dist: scipy>=1.12
|
|
46
|
+
Provides-Extra: dev
|
|
47
|
+
Requires-Dist: ipykernel; extra == 'dev'
|
|
48
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
49
|
+
Requires-Dist: pre-commit>=3.7; extra == 'dev'
|
|
50
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
51
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
52
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
53
|
+
Provides-Extra: docs
|
|
54
|
+
Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
|
|
55
|
+
Requires-Dist: mkdocs>=1.6; extra == 'docs'
|
|
56
|
+
Provides-Extra: validation
|
|
57
|
+
Requires-Dist: pandas>=2.0; extra == 'validation'
|
|
58
|
+
Requires-Dist: pyarrow>=14.0; extra == 'validation'
|
|
59
|
+
Requires-Dist: rpy2>=3.5; extra == 'validation'
|
|
60
|
+
Description-Content-Type: text/markdown
|
|
61
|
+
|
|
62
|
+
# augsynth-py
|
|
63
|
+
|
|
64
|
+
A Python implementation of Augmented Synthetic Control Methods and
|
|
65
|
+
geo-experimentation tooling. Methodologically faithful to the published
|
|
66
|
+
literature, validated against the R reference implementations.
|
|
67
|
+
|
|
68
|
+
> **Status: alpha.** The estimator API is functional and validated against R,
|
|
69
|
+
> but not yet stable across releases. Pin the exact version in production.
|
|
70
|
+
|
|
71
|
+
## What this is
|
|
72
|
+
|
|
73
|
+
Synthetic control methods for causal inference, with a focus on the use case
|
|
74
|
+
that matters most in modern marketing science: measuring the lift of geo-level
|
|
75
|
+
ad campaigns. The roadmap targets feature parity with:
|
|
76
|
+
|
|
77
|
+
- The R [`augsynth`](https://github.com/ebenmichael/augsynth) package
|
|
78
|
+
(estimators).
|
|
79
|
+
- Meta's R [`GeoLift`](https://github.com/facebookincubator/GeoLift) package
|
|
80
|
+
(orchestration: power analysis, market selection, multi-cell).
|
|
81
|
+
|
|
82
|
+
## Why another synthetic control package
|
|
83
|
+
|
|
84
|
+
There are good Python options for parts of this problem (`CausalPy`,
|
|
85
|
+
`pysyncon`, `tfcausalimpact`), but none provides Augmented Synthetic Control
|
|
86
|
+
Methods (Ben-Michael, Feller & Rothstein 2021) together with the
|
|
87
|
+
GeoLift-style orchestration layer in a single Python-native package.
|
|
88
|
+
|
|
89
|
+
## Installation
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pip install augsynth-py
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
For the validation test suite (requires R and the `augsynth` R package):
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
pip install "augsynth-py[validation]"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Quickstart
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
import numpy as np
|
|
105
|
+
import polars as pl
|
|
106
|
+
|
|
107
|
+
from augsynth_py import AugSynth, conformal_pvalue
|
|
108
|
+
|
|
109
|
+
# Simulated geo panel: 20 markets x 90 days, +10% lift in geo_00 from day 70.
|
|
110
|
+
rng = np.random.default_rng(7)
|
|
111
|
+
days = np.arange(90)
|
|
112
|
+
base = rng.uniform(80, 120, 20)
|
|
113
|
+
trend = rng.normal(0.1, 0.05, 20)
|
|
114
|
+
seasonal = 5 * np.sin(2 * np.pi * days / 7)
|
|
115
|
+
|
|
116
|
+
panel = pl.concat(
|
|
117
|
+
pl.DataFrame(
|
|
118
|
+
{
|
|
119
|
+
"geo": f"geo_{i:02d}",
|
|
120
|
+
"day": days,
|
|
121
|
+
"sales": base[i] + trend[i] * days + seasonal + rng.normal(0, 1.0, 90),
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
for i in range(20)
|
|
125
|
+
).with_columns(
|
|
126
|
+
pl.when((pl.col("geo") == "geo_00") & (pl.col("day") >= 70))
|
|
127
|
+
.then(pl.col("sales") * 1.10)
|
|
128
|
+
.otherwise(pl.col("sales"))
|
|
129
|
+
.alias("sales")
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
fit = AugSynth(lambda_=1.0).fit(
|
|
133
|
+
panel,
|
|
134
|
+
unit="geo",
|
|
135
|
+
time="day",
|
|
136
|
+
outcome="sales",
|
|
137
|
+
treated="geo_00",
|
|
138
|
+
treatment_time=70,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
print(f"ATT: {fit.att_:.2f} ({fit.att_pct_:+.1%})")
|
|
142
|
+
# ATT: 10.29 (+9.8%)
|
|
143
|
+
|
|
144
|
+
# The noise in this simulated panel is iid; on real (autocorrelated) series
|
|
145
|
+
# keep the default permutation_type="block".
|
|
146
|
+
p = conformal_pvalue(fit, permutation_type="iid", rng=np.random.default_rng(0))
|
|
147
|
+
print(f"conformal p-value: {p:.4f}")
|
|
148
|
+
# conformal p-value: 0.0120
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
What's available today:
|
|
152
|
+
|
|
153
|
+
- `Synth` — classical simplex-constrained synthetic control
|
|
154
|
+
(outcome-only form, `augsynth(progfunc = "None", scm = TRUE)` analogue),
|
|
155
|
+
with optional unit fixed effects.
|
|
156
|
+
- `AugSynth` — ridge-augmented synthetic control (Ben-Michael, Feller &
|
|
157
|
+
Rothstein 2021), with leave-one-out CV for the ridge penalty by default.
|
|
158
|
+
- `conformal_pvalue` / `conformal_interval` — exact conformal inference for
|
|
159
|
+
a constant post-period effect (Chernozhukov, Wuthrich & Zhu 2021), block
|
|
160
|
+
and iid permutation schemes.
|
|
161
|
+
- Multi-treated fits: pass an iterable of units as `treated` to estimate the
|
|
162
|
+
effect on the treated-group mean.
|
|
163
|
+
|
|
164
|
+
The GeoLift-style orchestration layer (power analysis, market selection) is
|
|
165
|
+
the next milestone on the roadmap.
|
|
166
|
+
|
|
167
|
+
## Methodological references
|
|
168
|
+
|
|
169
|
+
The implementation is based on the published literature, not translated from
|
|
170
|
+
the R sources. Key references:
|
|
171
|
+
|
|
172
|
+
- Abadie, Diamond & Hainmueller (2010). Synthetic Control Methods for
|
|
173
|
+
Comparative Case Studies. *JASA*.
|
|
174
|
+
- Ben-Michael, Feller & Rothstein (2021). The Augmented Synthetic Control
|
|
175
|
+
Method. *JASA*.
|
|
176
|
+
- Xu (2017). Generalized Synthetic Control Method. *Political Analysis*.
|
|
177
|
+
- Chernozhukov, Wuthrich & Zhu (2021). An Exact and Robust Conformal Inference
|
|
178
|
+
Method for Counterfactual and Synthetic Controls. *JASA*.
|
|
179
|
+
|
|
180
|
+
See `docs/methodology.md` for the mapping between code and equations.
|
|
181
|
+
|
|
182
|
+
## Validation against R
|
|
183
|
+
|
|
184
|
+
Every estimator in this package is validated numerically against the R
|
|
185
|
+
reference implementation. The validation suite lives in
|
|
186
|
+
`tests/validation_against_r/` and runs as a separate CI job.
|
|
187
|
+
|
|
188
|
+
If you find a discrepancy with the R output beyond documented tolerances,
|
|
189
|
+
please open an issue — that is a bug.
|
|
190
|
+
|
|
191
|
+
## Contributing
|
|
192
|
+
|
|
193
|
+
Read [`CLAUDE.md`](CLAUDE.md) first. It contains the architectural decisions,
|
|
194
|
+
coding conventions, and the validation rule that PRs must satisfy.
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT. See [`LICENSE`](LICENSE).
|