Getting started

Use GeoStep when geographic units can be randomly allocated. Read the methodology before interpreting results. Install the package using the installation guide.

Run a complete parallel example

This artificial example generates a complete panel with an additive assignment effect. It illustrates the API, not expected performance on marketing data.

import numpy as np
import pandas as pd
from geostep import DiDAnalyzer, SimpleRandomizationDesigner
from geostep.reports import generate_display_results_report

rng = np.random.default_rng(42)
units = pd.DataFrame({'geo': np.arange(40)})
allocation = SimpleRandomizationDesigner(seed=42).design(units, geo_col='geo')
dates = pd.date_range('2024-01-01', periods=56, freq='D')
rows = []
for geo, assignment in allocation[['geo', 'assignment']].itertuples(index=False):
    for date in dates:
        outcome = 100 + geo + rng.normal(0, 10)
        if assignment == 'Treatment' and date >= pd.Timestamp('2024-01-29'):
            outcome += 5
        rows.append({'geo': geo, 'date': date, 'assignment': assignment, 'sales': outcome})
data = pd.DataFrame(rows)
analyser = DiDAnalyzer()
result = analyser.analyze(
    data, geo_col='geo', date_col='date', assignment_col='assignment', kpi_col='sales',
    pre_period_start='2024-01-01', pre_period_end='2024-01-28',
    test_period_start='2024-01-29', test_period_end='2024-02-25',
    random_seed=42,
)
generate_display_results_report(result)

The effect and confidence interval are in sales units per geo-period. Retain result.to_dict() alongside the allocation, input data hash and analysis plan. Never randomise retrospectively using collected test outcomes.

Choose a normalised contrast

LiftAnalyzer accepts the same inputs as DiDAnalyzer. It divides each geo’s change by that geo’s baseline mean. Its estimate is a difference in normalised changes; multiplying by 100 gives percentage points. It is not automatically relative sales lift. Baseline means must be strictly positive.

analyser.prepare_data(data, **analysis_options) returns the exact geo summaries used for the parallel analysis, including pre_avg, test_avg and change. Lift summaries also include lift_index. Pass these to geostep.visualizer.plot_lift_distribution when creating a Lift figure.

Configure uncertainty

from geostep.base import AnalyzerConfig
from geostep import DiDAnalyzer

analyser = DiDAnalyzer(AnalyzerConfig(
    confidence_level=0.95, bootstrap_reps=2000, bootstrap_seed=0,
))

On analyze, set use_bca_bootstrap=True for a BCa interval, or use_permutation_test=True, n_permutations=9999 for a conditional sharp-null test. Each result records the interval and p-value methods separately. Analysis results are never cached.

Preserve strata

Use StratifiedRandomizationDesigner with one prespecified pre-treatment summary per geo. Every stratum needs at least four geos. Join its assignment, probability and stratum columns to the collected outcome panel. A column named stratum is used automatically; otherwise pass stratum_col to the analyser. Do not remove strata after allocation.

For strata defined externally, use geostep.designer.randomize_within_strata(units, 'geo', seed=42, stratum_col='block'). The supplied table must contain one row per eligible geo and its frozen block.

Plan a historical power scenario

from geostep import run_power_analysis

# historical_data must be a complete regular daily or weekly panel.
# Use an actual pre-intervention dataset; do not reuse synthetic treated outcomes.
def assess_power(historical_data):
    return run_power_analysis(
        historical_data, geo_col='geo', date_col='date', kpi_col='sales',
        effect_sizes=[0.03, 0.05], test_weeks_list=[4, 6],
        pre_period_weeks=8, n_sims=500, random_seed=42,
        analyzer='did', n_jobs=1,
    )

Inspect status, n_valid, n_failed and failure_reasons before using power. Durations are calendar weeks. The function does not calculate staircase or CRT power. It reports conditional historical scenarios and Monte Carlo uncertainty.

Randomise a staircase schedule

from geostep import StaircaseDesigner

designer = StaircaseDesigner(
    num_sequences=4, clusters_per_sequence=5,
    control_periods=2, intervention_periods=2,
)
allocation = designer.randomize([f'geo_{i}' for i in range(20)], seed=42)

Collect outcomes according to this retained schedule. CRTAnalyzer.analyze requires geo_col, period_col, assignment_col and kpi_col. Retain sequence in the outcome data. Read the CRT approximation limits before analysis.

Run from the checkout

python run_pipeline.py --data your_panel.csv --analyzer did \
  --geo-col geo --date-col date --kpi-col sales \
  --pre-end 2024-01-28 --test-start 2024-01-29 --test-end 2024-02-25 \
  --confidence-level 0.95 --random-seed 42 --output-dir results/my_trial

For CRT use --analyzer crt --period-col period and provide the recorded geo-period outcome table. Parallel date-window flags do not define CRT periods. Use --stratum-col block for named parallel strata and --permutation-test for the optional parallel sharp-null test.

Continue with the business interpretation guide, API reference or advanced boundaries.