Metadata-Version: 2.4
Name: causal-marketing
Version: 0.1.0
Summary: Geo experiment design and analysis
Author: causal-marketing contributors
License-Expression: MIT
Project-URL: Homepage, https://github.com/ssemov/causal-marketing
Project-URL: Repository, https://github.com/ssemov/causal-marketing
Project-URL: Issues, https://github.com/ssemov/causal-marketing/issues
Keywords: causal inference,geo experiments,synthetic control,experiment design,marketing measurement,incrementality
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Requires-Dist: numpy>=2.0
Requires-Dist: pandas>=2.0
Requires-Dist: scipy>=1.11
Requires-Dist: osqp
Requires-Dist: pydantic>=2.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Dynamic: license-file

# causal-marketing

A Python library for marketing causal inference, handling both the design and the analysis of marketing experiments. Experiment design is treated as an optimization problem you specify with a marketing-native API. Built for data science teams estimating incremental impact. No R dependency required.

## Installation

```bash
pip install causal-marketing
```

## Quick Start

You declare your objective and constraints, and the library searches over the
designs and analyses consistent with them, and optimizes.

The example below reproduces the published [Meta GeoLift walkthrough](https://facebookincubator.github.io/GeoLift/docs/GettingStarted/Walkthrough/). GeoLift's approach fits into this framework as a special case: it searches over groups of correlated markets and optimizes for detectability. It can be executed with the built-in example data we provide, the same data used in that walkthrough, included for convenience.

```python
from causal_marketing import (
    Budget,
    Eligibility,
    GeoLiftMarketSelection,
    GeoLiftSearchSpec,
    load_geolift_example,
)

example = load_geolift_example()          # built-in 40-city panel; no setup
pretest, test = example.pretest, example.test

spec = GeoLiftSearchSpec(
    primary_metric="Y",
    estimator="augsynth",
    estimator_parameters={"seed": 42},
    assigner="matched_markets",                # Meta's GeoLiftMarketSelection
    assigner_parameters={"N": [2, 3, 4, 5]},   # treatment-group sizes
    eligibility=Eligibility(treatment=[{"chicago"}], exclude={"honolulu"}),
    budget=Budget(cpic=7.5, max_investment=100_000),
    treatment_periods=[10, 15],
    effect_sizes=[0.0, 0.05, 0.10, 0.15, 0.20],
    alpha=0.1,
)
selection = GeoLiftMarketSelection(pretest, spec)
selection.search()

cols = ["ID", "location", "duration", "EffectSize", "Power", "AvgATT", "Investment", "rank"]
top6 = selection.data.head(6)[cols].round({"EffectSize": 2, "Power": 2, "AvgATT": 1, "Investment": 0})
print("Ranked candidate designs (top 6):")
print(top6.to_string(index=False))        # ranked best-first
```

```text
Ranked candidate designs (top 6):
 ID                                           location  duration  EffectSize  Power  AvgATT  Investment  rank
  1             chicago, cincinnati, houston, portland        15        0.05    1.0   159.4     74118.0     1
  2                                  chicago, portland        15        0.10    1.0   290.0     64564.0     1
  3             chicago, cincinnati, houston, portland        10        0.10    1.0   316.6     99028.0     3
  4                                  chicago, portland        10        0.10    1.0   300.9     43646.0     3
  5                         chicago, houston, portland        10        0.10    1.0   350.3     75389.0     5
  6 chicago, cincinnati, houston, nashville, san diego        15        0.05    1.0   146.8     95756.0     6
```

Following GeoLift, we pick design 2 (chicago + portland): it detects a 10% effect at a lower investment than design 1. (`AvgATT` is the average incremental units a design would generate; `Investment` is the spend it needs to be well-powered.)

```python
design = selection.get_design_by_id(2)    # Chicago + Portland
df, extras = design.analyze(test, treatment_start_date="2021-04-01")

print("Analyzed design:", ", ".join(design.treated))
print(f"  ATT (incremental units/day): {df['point_estimate'].iloc[0]:.3f}")
print(f"  p-value                    : {df['p_value'].iloc[0]:.3f}")
print(f"  scaled L2 imbalance        : {extras['scaled_l2_imbalance']:.4f}")
```

```text
Analyzed design: chicago, portland
  ATT (incremental units/day): 155.556
  p-value                    : 0.013
  scaled L2 imbalance        : 0.1636
```

If you're working with your own data instead, you could build a `Dataset` object from a long-format data frame (`Dataset(df, unit_col="location", time_col="date")`) and pass it in the same way.

## What makes it different

The library separates the **design** of an experiment (how you assign treatment) from the **analysis** (how you analyze the results). This lets you more cleanly understand the connection between experiment design and analysis, and explore complementarities between them, like pairing different assignment strategies with different estimators. There are two ways to choose treatment markets:

**Option 1: search and rank (the Quick Start above).** Score candidate market splits and keep the best by power. This reproduces Meta's [GeoLift](https://facebookincubator.github.io/GeoLift/docs/GettingStarted/Walkthrough/) market selection: `assigner="matched_markets"`, `estimator="augsynth"`.

**Option 2: randomization.** Draw the treatment markets at random and analyze with the same estimator. The only change from Option 1 is the assigner:

```python
design = GeoLiftDesign(
    primary_metric="Y", estimator="augsynth", runtime_weeks=15,
    estimator_parameters={"seed": 42},
    assigner="random", assigner_parameters={"n_treatment": 10, "seed": 42},
)
design.assign(pretest)                            # 10 treatment, 10 control, drawn at random
df, extras = design.analyze(test, treatment_start_date="2021-04-01")
```

[`examples/random_assignment.py`](examples/random_assignment.py) runs this as a 10-treatment / 10-control split of 20 example cities (chicago and portland excluded, so the random arm carries no planted effect), and prints the balance, the pre-period fit next to the matched design, and the estimate:

```text
Random 10 / 10 split (chicago and portland excluded), seed=42:
  pre-test mean outcome  -- treatment: 3700, control: 5041
  pre-test correlation (treatment vs control): 0.976

Pre-period fit (L2 imbalance) -- scaled L2 = raw / naive:
  design                       raw L2  naive L2  scaled L2
  matched chicago+portland      909.5    5557.9     0.1636
  random 10 / 10               1015.1    4046.0     0.2509

Analysis of the random split (illustrative):
  ATT (incremental units/day): 117.379
  p-value                    : 0.442
```

The scaled L2 imbalance is the synthetic control's pre-period reconstruction error as a fraction of a naive equal-weights average (0 = perfect, 1 = no better than naive).

## Requirements

- Python 3.13
- numpy >= 2.0, pandas >= 2.0, scipy >= 1.11, osqp, pydantic >= 2.0

## License

MIT. See [LICENSE](LICENSE).

## Acknowledgements

causal-marketing reimplements, in Python, methods from two MIT-licensed projects, and bundles their example data for testing and the walkthrough:

- **[GeoLift](https://github.com/facebookincubator/GeoLift)** (Meta Platforms): the market-selection method. Its example data lets you check results against Meta's published walkthrough without installing R.
- **[augsynth](https://github.com/ebenmichael/augsynth)** (Eli Ben-Michael): the augmented synthetic-control estimator.

Full third-party license notices are in [`NOTICE`](NOTICE).

### References

- Ben-Michael, E., Feller, A., & Rothstein, J. (2021). The Augmented Synthetic Control Method. *Journal of the American Statistical Association*.
- Chernozhukov, V., Wüthrich, K., & Zhu, Y. (2021). An Exact and Robust Conformal Inference Method for Counterfactual and Synthetic Control. *Journal of the American Statistical Association*.
