Geo experiment design and analysis
Project description
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
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. 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.
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
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.)
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}")
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 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:
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 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:
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.
Acknowledgements
causal-marketing reimplements, in Python, methods from two MIT-licensed projects, and bundles their example data for testing and the walkthrough:
- GeoLift (Meta Platforms): the market-selection method. Its example data lets you check results against Meta's published walkthrough without installing R.
- augsynth (Eli Ben-Michael): the augmented synthetic-control estimator.
Full third-party license notices are in 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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file causal_marketing-0.1.0.tar.gz.
File metadata
- Download URL: causal_marketing-0.1.0.tar.gz
- Upload date:
- Size: 148.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
07aa6575fd507cb14b8d69b137b88c4de92ac0ef015e588eb4bcb48fb58370c7
|
|
| MD5 |
94e7a706f75933aba20463dd7595c59b
|
|
| BLAKE2b-256 |
31c14b18c5e3b717acfffac8991e224f8017456fc9e311c16495c002345795da
|
File details
Details for the file causal_marketing-0.1.0-py3-none-any.whl.
File metadata
- Download URL: causal_marketing-0.1.0-py3-none-any.whl
- Upload date:
- Size: 104.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3c50bfb309f9bfe9e9ad22ab1cf60719e68202bae05a9d2585de64ef2cf86453
|
|
| MD5 |
79235aac49bafc924ff5126ec54ad553
|
|
| BLAKE2b-256 |
34b669e6f600f0580d1707861b9805d0882df30324e7a45bde0abdc3540b005e
|