Skip to main content

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

causal_marketing-0.1.0.tar.gz (148.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

causal_marketing-0.1.0-py3-none-any.whl (104.5 kB view details)

Uploaded Python 3

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

Hashes for causal_marketing-0.1.0.tar.gz
Algorithm Hash digest
SHA256 07aa6575fd507cb14b8d69b137b88c4de92ac0ef015e588eb4bcb48fb58370c7
MD5 94e7a706f75933aba20463dd7595c59b
BLAKE2b-256 31c14b18c5e3b717acfffac8991e224f8017456fc9e311c16495c002345795da

See more details on using hashes here.

File details

Details for the file causal_marketing-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for causal_marketing-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3c50bfb309f9bfe9e9ad22ab1cf60719e68202bae05a9d2585de64ef2cf86453
MD5 79235aac49bafc924ff5126ec54ad553
BLAKE2b-256 34b669e6f600f0580d1707861b9805d0882df30324e7a45bde0abdc3540b005e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page