Metadata-Version: 2.4
Name: augsynth-py
Version: 0.3.1
Summary: Augmented Synthetic Control Methods and geo-experimentation tooling for Python.
Project-URL: Repository, https://github.com/mrcsvg/augsynth-py
Project-URL: Issues, https://github.com/mrcsvg/augsynth-py/issues
Project-URL: Changelog, https://github.com/mrcsvg/augsynth-py/blob/main/CHANGELOG.md
Author: augsynth-py contributors
License: MIT License
        
        Copyright (c) 2026 augsynth-py contributors
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: causal-inference,econometrics,geo-experiments,marketing-science,synthetic-control
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: cvxpy>=1.4.1
Requires-Dist: joblib>=1.3
Requires-Dist: numpy>=1.26
Requires-Dist: polars>=1.0
Requires-Dist: scipy>=1.12
Provides-Extra: dev
Requires-Dist: ipykernel; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pre-commit>=3.7; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
Requires-Dist: mkdocs>=1.6; extra == 'docs'
Provides-Extra: numpy1
Requires-Dist: cvxpy<1.8,>=1.4.1; extra == 'numpy1'
Requires-Dist: numpy<2,>=1.26; extra == 'numpy1'
Provides-Extra: validation
Requires-Dist: pandas>=2.0; extra == 'validation'
Requires-Dist: pyarrow>=14.0; extra == 'validation'
Requires-Dist: rpy2>=3.5; extra == 'validation'
Description-Content-Type: text/markdown

# augsynth-py

A Python implementation of Augmented Synthetic Control Methods and
geo-experimentation tooling. Methodologically faithful to the published
literature, validated against the R reference implementations.

> **Status: alpha.** The estimator API is functional and validated against R,
> but not yet stable across releases. Pin the exact version in production.

## What this is

Synthetic control methods for causal inference, with a focus on the use case
that matters most in modern marketing science: measuring the lift of geo-level
ad campaigns. The roadmap targets feature parity with:

- The R [`augsynth`](https://github.com/ebenmichael/augsynth) package
  (estimators).
- Meta's R [`GeoLift`](https://github.com/facebookincubator/GeoLift) package
  (orchestration: power analysis, market selection, multi-cell).

## Why another synthetic control package

There are good Python options for parts of this problem (`CausalPy`,
`pysyncon`, `tfcausalimpact`), but none provides Augmented Synthetic Control
Methods (Ben-Michael, Feller & Rothstein 2021) together with the
GeoLift-style orchestration layer in a single Python-native package.

## Installation

```bash
pip install augsynth-py
```

For the validation test suite (requires R and the `augsynth` R package):

```bash
pip install "augsynth-py[validation]"
```

### Environments pinned to numpy 1.x

Both numpy 1.26+ and numpy 2.x are supported. When augsynth-py shares an
environment with libraries that cannot move off numpy 1.x yet (`econml`,
`tslearn`/`numba`, and similar), install the `numpy1` extra, which resolves the
dependency set against the last releases built for that ABI:

```bash
pip install "augsynth-py[numpy1]"
```

`pip` only reconciles what a single command names, so when augsynth-py is added
to an environment that already holds such pins, state them explicitly — in the
same command or through a constraints file:

```bash
# constraints.txt
numpy>=1.26.4,<2
cvxpy>=1.4.1,<1.5

pip install -c constraints.txt augsynth-py
```

The oldest supported dependency set — numpy 1.26, scipy 1.12, cvxpy 1.4.1,
polars 1.0, joblib 1.3 on Python 3.11 — runs the unit suite on every push.
See [`docs/compatibility.md`](docs/compatibility.md) for the full matrix.

## Quickstart

```python
import numpy as np
import polars as pl

from augsynth_py import AugSynth, conformal_pvalue

# Simulated geo panel: 20 markets x 90 days, +10% lift in geo_00 from day 70.
rng = np.random.default_rng(7)
days = np.arange(90)
base = rng.uniform(80, 120, 20)
trend = rng.normal(0.1, 0.05, 20)
seasonal = 5 * np.sin(2 * np.pi * days / 7)

panel = pl.concat(
    pl.DataFrame(
        {
            "geo": f"geo_{i:02d}",
            "day": days,
            "sales": base[i] + trend[i] * days + seasonal + rng.normal(0, 1.0, 90),
        }
    )
    for i in range(20)
).with_columns(
    pl.when((pl.col("geo") == "geo_00") & (pl.col("day") >= 70))
    .then(pl.col("sales") * 1.10)
    .otherwise(pl.col("sales"))
    .alias("sales")
)

fit = AugSynth(lambda_=1.0).fit(
    panel,
    unit="geo",
    time="day",
    outcome="sales",
    treated="geo_00",
    treatment_time=70,
)

print(f"ATT: {fit.att_:.2f} ({fit.att_pct_:+.1%})")
# ATT: 10.29 (+9.8%)

# The noise in this simulated panel is iid; on real (autocorrelated) series
# keep the default permutation_type="block".
p = conformal_pvalue(fit, permutation_type="iid", rng=np.random.default_rng(0))
print(f"conformal p-value: {p:.4f}")
# conformal p-value: 0.0120
```

### Treating more than one market

`treated` takes either a single unit value or any iterable of them — `list`,
`tuple`, `set`, `np.ndarray`, `pl.Series`. An iterable designates a treated
*group*: the units are collapsed to their elementwise mean and dropped from the
donor pool, so `att_` is the effect on that group mean.

```python
group_fit = AugSynth(lambda_=1.0).fit(
    panel,
    unit="geo",
    time="day",
    outcome="sales",
    treated={"geo_00", "geo_01"},  # <- a group, not a single market
    treatment_time=70,
)

print(group_fit.units_[0])
# geo_00,geo_01

print(f"ATT on the group mean: {group_fit.att_:.2f}")
# ATT on the group mean: 4.83
# (lower than the 10.29 above because only geo_00 got the lift; geo_01 dilutes it)
```

`str` and `bytes` are the one exception: they are read as a single unit value,
not as a sequence of characters. To treat exactly one market, either form works
(`treated="geo_00"` or `treated=["geo_00"]`).

The aggregate lift across the group is `att_ * n_treated * n_post_periods` —
`att_` itself stays per-unit-per-period, so it is comparable to a single-market
fit.

What's available today:

- `Synth` — classical simplex-constrained synthetic control
  (outcome-only form, `augsynth(progfunc = "None", scm = TRUE)` analogue),
  with optional unit fixed effects.
- `AugSynth` — ridge-augmented synthetic control (Ben-Michael, Feller &
  Rothstein 2021), with leave-one-out CV for the ridge penalty by default.
- `conformal_pvalue` / `conformal_interval` — exact conformal inference for
  a constant post-period effect (Chernozhukov, Wuthrich & Zhu 2021), block
  and iid permutation schemes.
- Multi-treated fits: pass an iterable of units as `treated` to estimate the
  effect on the treated-group mean (see
  [Treating more than one market](#treating-more-than-one-market)).

The GeoLift-style orchestration layer (power analysis, market selection) is
the next milestone on the roadmap.

## Methodological references

The implementation is based on the published literature, not translated from
the R sources. Key references:

- Abadie, Diamond & Hainmueller (2010). Synthetic Control Methods for
  Comparative Case Studies. *JASA*.
- Ben-Michael, Feller & Rothstein (2021). The Augmented Synthetic Control
  Method. *JASA*.
- Xu (2017). Generalized Synthetic Control Method. *Political Analysis*.
- Chernozhukov, Wuthrich & Zhu (2021). An Exact and Robust Conformal Inference
  Method for Counterfactual and Synthetic Controls. *JASA*.

See `docs/methodology.md` for the mapping between code and equations.

## Validation against R

Every estimator in this package is validated numerically against the R
reference implementation. The validation suite lives in
`tests/validation_against_r/` and runs as a separate CI job.

If you find a discrepancy with the R output beyond documented tolerances,
please open an issue — that is a bug.

## Contributing

Read [`CLAUDE.md`](CLAUDE.md) first. It contains the architectural decisions,
coding conventions, and the validation rule that PRs must satisfy.

## License

MIT. See [`LICENSE`](LICENSE).
