User Guide¶
This guide walks through the GeoLift workflow for measuring campaign impact using causal inference.
Overview¶
GeoLift measures the true sales lift and ROI from your regional marketing campaigns using advanced causal inference methods. It follows a proven 3-step workflow to ensure reliable results.
The 3-Step Workflow¶
Step 1: Find a fair comparison (Donor evaluation)¶
Before measuring impact, we need to identify control markets that behave similarly to your test markets.
Data Requirements¶
Your dataset should include:
Time Series Data: At least 12-24 weeks of pre-campaign data
Geographic Units: Markets, DMAs, states, or regions
Outcome Metrics: Sales, conversions, or other KPIs
Treatment Assignment: Which markets received the campaign
Running donor evaluation¶
Use the bundled recipe and the YAML under data-config/:
python recipes/donor_evaluator.py --config data-config/donor_eval_config.yaml
Outputs (by default): outputs/multicell_donor_eval/
donor_eval_results.csvanddonor_map_*.png
What to Look For¶
High Correlation: Control markets should track closely with treatment markets pre-campaign
Similar Trends: Parallel movement in the pre-period
Geographic Diversity: Avoid clustering all controls in one region
Step 2: Check if the test is strong enough (Power analysis)¶
Power analysis determines if your experiment can detect meaningful effects.
# GPU optional, CPU parallelisation supported
python recipes/power_calculator_sparsesc.py \
--config data-config/power_analysis_config.yaml \
--use-gpu --jobs -1
Outputs: outputs/multicell_power_analysis/
power_curves.png,power_analysis_results.csv
Power Analysis Outputs¶
Minimum Detectable Effect (MDE): Smallest lift you can reliably detect
Recommended Duration: How long to run the campaign for reliable results
Sample Size Requirements: Number of markets needed
Step 3: Measure the lift (inference)¶
Run the main causal inference analysis to measure campaign impact.
python recipes/geolift_multi_cell.py --config data-config/geolift_analysis_config.yaml
Outputs: outputs/multicell_geolift_analysis/
geolift_results.json,geolift_diagnostics.json,uplift_timeseries.png
Understanding Your Results¶
Key Metrics Explained¶
Causal Impact¶
Absolute Lift: Raw units of incremental impact
Relative Lift: Percentage increase over baseline
Confidence Intervals: Range of plausible effect sizes
Statistical Significance¶
P-value: Probability results occurred by chance
Confidence Level: How certain we are about the effect
Statistical Power: Ability to detect true effects
Business Impact¶
Incremental ROI: Return on marketing investment
Cost Per Incremental Unit: Efficiency of campaign spend
Payback Period: Time to recover campaign investment
Interpreting Results¶
# Access key metrics from results dictionary
if results:
print(f"Average Treatment Effect: {results.get('att', 'N/A')}")
print(f"P-value: {results.get('p_value', 'N/A')}")
print(f"Confidence Interval: [{results.get('ci_lower', 'N/A')}, {results.get('ci_upper', 'N/A')}]")
# Check statistical significance
if results.get('p_value', 1.0) < 0.05:
print("Result is statistically significant at 95% confidence level")
# Results are also saved in output_dir as JSON files:
# - geolift_results.json: Main results
# - geolift_diagnostics.json: Detailed diagnostics
Visual Diagnostics¶
If you run the recipe with create_plots: true, the analyser will save uplift_timeseries.png.
Data Preparation Best Practices¶
Data Quality Requirements¶
Completeness: No missing values in key periods
Consistency: Same measurement methodology throughout
Granularity: Weekly or daily data preferred over monthly
Baseline Period: At least 12 weeks of pre-campaign data
Common Data Issues¶
Seasonality: Account for holidays and seasonal patterns
External Events: Note major market disruptions
Data Breaks: Ensure consistent measurement methodology
Outliers: Identify and handle extreme values appropriately
Data validation¶
Ensure input data are consistent and complete (no missing key columns or periods). Use the load_and_prepare_data utility for file‑based workflows.
Configuration Options¶
Analysis parameters¶
# config.yaml
analysis:
treatment_start_date: "2023-06-01"
treatment_end_date: "2023-08-31"
pre_period_weeks: 24
outcome_column: "sales"
inference:
method: "bootstrap" # bootstrap, placebo, jackknife
n_bootstrap: 1000
confidence_level: 0.95
# Bootstrap uses stationary (circular) block bootstrap with automatic
# block-length selection. Typical block lengths: 3-5 periods for marketing
# time series with moderate autocorrelation.
validation:
min_pre_period_weeks: 12
max_missing_data_pct: 0.05
outlier_threshold: 3.0
Advanced Options¶
# Using direct data initialisation for more control (advanced)
import pandas as pd
# Load and prepare your data
data = pd.read_csv('campaign_data.csv')
# Pivot to get units as rows, time periods as columns
outcomes_df = data.pivot(index='geo', columns='date', values='sales')
# Define treatment periods
unit_treatment_periods = pd.Series({
502: '2023-06-01', # Treatment unit
503: '2023-06-01', # Treatment unit
501: None, # Control unit
504: None, # Control unit
505: None # Control unit
})
# Initialize with direct data
# Note: SparseSC uses time-blocked cross-validation on the pre-treatment period
# (not unit-folding CV) to select regularisation penalties. This prevents data
# leakage and aligns model selection with the counterfactual prediction objective.
analyzer = GeoLiftAnalyzer(
outcomes_df=outcomes_df,
unit_treatment_periods=unit_treatment_periods,
intervention_date='2023-06-01',
config={
'sparse_sc_model_type': 'retrospective',
'sparse_sc_max_n_pl': 5000,
'sparse_sc_level': 0.95
}
)
results = analyzer.run_analysis()
Reporting and Export¶
Generate Business Reports¶
import json
import pandas as pd
# Load saved results from JSON files
with open('outputs/geolift_analysis/geolift_results.json', 'r') as f:
saved_results = json.load(f)
with open('outputs/geolift_analysis/geolift_diagnostics.json', 'r') as f:
saved_diagnostics = json.load(f)
# Create custom summary
summary_data = {
'campaign_name': 'Q3 Brand Campaign',
'lift_estimate': saved_results.get('att'),
'lift_ci_lower': saved_results.get('ci_lower'),
'lift_ci_upper': saved_results.get('ci_upper'),
'p_value': saved_results.get('p_value'),
'campaign_cost': 50000,
'incremental_revenue': saved_results.get('att', 0) * 100 # Assuming $100 per unit
}
# Calculate ROI
if summary_data['incremental_revenue'] and summary_data['campaign_cost']:
summary_data['roi'] = summary_data['incremental_revenue'] / summary_data['campaign_cost']
# Export to CSV
pd.DataFrame([summary_data]).to_csv('campaign_summary.csv', index=False)
# Create detailed report
detailed_report = pd.DataFrame({
'Metric': ['ATT', 'P-value', 'CI Lower', 'CI Upper'],
'Value': [
saved_results.get('att'),
saved_results.get('p_value'),
saved_results.get('ci_lower'),
saved_results.get('ci_upper')
]
})
detailed_report.to_csv('detailed_results.csv', index=False)
Troubleshooting Common Issues¶
Poor Pre-Period Fit¶
Problem: Synthetic control doesn’t match treatment markets well before campaign Solutions:
Extend pre-period data
Remove outlier periods
Try different donor selection criteria
Low Statistical Power¶
Problem: Cannot detect meaningful effects Solutions:
Extend campaign duration
Include more treatment markets
Use more sensitive outcome metrics
Implausible Results¶
Problem: Effect sizes seem too large or small Solutions:
Check data quality and definitions
Validate treatment assignment
Review external factors during campaign period
Next Steps¶
Need technical details? → See API Reference
Want to understand the math? → Check Advanced Topics
Having specific issues? → Review FAQ
Ready for production? → See deployment guides in Advanced Topics