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.
Files changed (42) hide show
  1. augsynth_py-0.3.0/.gitignore +91 -0
  2. augsynth_py-0.3.0/CHANGELOG.md +42 -0
  3. augsynth_py-0.3.0/CLAUDE.md +211 -0
  4. augsynth_py-0.3.0/LICENSE +21 -0
  5. augsynth_py-0.3.0/PKG-INFO +198 -0
  6. augsynth_py-0.3.0/README.md +137 -0
  7. augsynth_py-0.3.0/docs/clean-room-audit-2026-05-16.md +282 -0
  8. augsynth_py-0.3.0/docs/clean-room-audit-2026-05-26-augsynth.md +553 -0
  9. augsynth_py-0.3.0/docs/clean-room-audit-2026-07-07-inference.md +469 -0
  10. augsynth_py-0.3.0/docs/known-issues.md +87 -0
  11. augsynth_py-0.3.0/docs/methodology.md +573 -0
  12. augsynth_py-0.3.0/docs/releasing.md +68 -0
  13. augsynth_py-0.3.0/pyproject.toml +194 -0
  14. augsynth_py-0.3.0/src/augsynth_py/__init__.py +7 -0
  15. augsynth_py-0.3.0/src/augsynth_py/_version.py +3 -0
  16. augsynth_py-0.3.0/src/augsynth_py/exceptions.py +24 -0
  17. augsynth_py-0.3.0/src/augsynth_py/inference.py +529 -0
  18. augsynth_py-0.3.0/src/augsynth_py/py.typed +0 -0
  19. augsynth_py-0.3.0/src/augsynth_py/synth/__init__.py +12 -0
  20. augsynth_py-0.3.0/src/augsynth_py/synth/_panel.py +254 -0
  21. augsynth_py-0.3.0/src/augsynth_py/synth/augmented.py +747 -0
  22. augsynth_py-0.3.0/src/augsynth_py/synth/classical.py +304 -0
  23. augsynth_py-0.3.0/tests/__init__.py +0 -0
  24. augsynth_py-0.3.0/tests/conftest.py +16 -0
  25. augsynth_py-0.3.0/tests/unit/__init__.py +0 -0
  26. augsynth_py-0.3.0/tests/unit/test_augsynth.py +897 -0
  27. augsynth_py-0.3.0/tests/unit/test_classical_synth.py +244 -0
  28. augsynth_py-0.3.0/tests/unit/test_imbalance.py +173 -0
  29. augsynth_py-0.3.0/tests/unit/test_inference_core.py +84 -0
  30. augsynth_py-0.3.0/tests/unit/test_inference_interval.py +297 -0
  31. augsynth_py-0.3.0/tests/unit/test_inference_pvalue.py +144 -0
  32. augsynth_py-0.3.0/tests/unit/test_multitreated.py +100 -0
  33. augsynth_py-0.3.0/tests/unit/test_panel.py +107 -0
  34. augsynth_py-0.3.0/tests/unit/test_smoke.py +21 -0
  35. augsynth_py-0.3.0/tests/validation_against_r/README.md +57 -0
  36. augsynth_py-0.3.0/tests/validation_against_r/__init__.py +0 -0
  37. augsynth_py-0.3.0/tests/validation_against_r/conftest.py +153 -0
  38. augsynth_py-0.3.0/tests/validation_against_r/test_augsynth.py +476 -0
  39. augsynth_py-0.3.0/tests/validation_against_r/test_classical_synth.py +110 -0
  40. augsynth_py-0.3.0/tests/validation_against_r/test_conformal.py +439 -0
  41. augsynth_py-0.3.0/tests/validation_against_r/test_imbalance.py +245 -0
  42. 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).