Pipeline Workflow#

CausalPy provides a composable pipeline API that chains causal inference steps into a single, reproducible workflow. Instead of manually calling experiment construction, sensitivity analysis, and report generation separately, you can define them as steps in a pipeline.

import pandas as pd

import causalpy as cp

Manual approach (before pipeline)#

Traditionally, a CausalPy analysis involves several sequential steps:

df = (
    cp.load_data("its")
    .assign(date=lambda x: pd.to_datetime(x["date"]))
    .set_index("date")
)
treatment_time = pd.to_datetime("2017-01-01")

seed = 42
model = cp.pymc_models.LinearRegression(sample_kwargs={"random_seed": seed})

# Step 1: Fit the experiment
result = cp.InterruptedTimeSeries(
    df,
    treatment_time,
    formula="y ~ 1 + t",
    model=model,
)

# Step 2: Get effect summary
summary = result.effect_summary()
print(summary.text)
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [beta, y_hat_sigma]

Sampling 4 chains for 1_000 tune and 1_000 draw iterations (4_000 + 4_000 draws total) took 1 seconds.
Sampling: [beta, y_hat, y_hat_sigma]
Sampling: [y_hat]
Sampling: [y_hat]
Sampling: [y_hat]
Sampling: [y_hat]
During the Post-period (2017-01-31 00:00:00 to 2019-12-31 00:00:00), the response variable had an average value of approx. 57.73. By contrast, in the absence of an intervention, we would have expected an average response of 56.34. The 95% interval of this counterfactual prediction is [51.79, 60.78]. Subtracting this prediction from the observed response yields an estimate of the causal effect the intervention had on the response variable. This effect is 1.40 with a 95% interval of [-3.04, 5.94].

Summing up the individual data points during the Post-period, the response variable had an overall value of 2078.37. By contrast, had the intervention not taken place, we would have expected a sum of 2028.08. The 95% interval of this prediction is [1864.38, 2187.92].

The 95% HDI of the effect [-3.04, 5.94] includes zero. The posterior probability of an increase is 0.730. Relative to the counterfactual, the effect represents a 2.65% change (95% HDI [-5.05%, 11.42%]).

This analysis assumes that the relationship between the time-based predictors and the response observed during the pre-intervention period remains stable throughout the post-intervention period. If the formula includes external covariates, it further assumes they were not themselves affected by the intervention. We recommend inspecting model fit, examining pre-intervention trends, and conducting sensitivity analyses (e.g., placebo tests) to support any causal conclusions drawn from this analysis.

Pipeline approach#

The pipeline wraps these steps into a single, declarative workflow. Each step is configured upfront, and the pipeline validates everything before running.

df = (
    cp.load_data("its")
    .assign(date=lambda x: pd.to_datetime(x["date"]))
    .set_index("date")
)

result = cp.Pipeline(
    data=df,
    steps=[
        cp.EstimateEffect(
            method=cp.InterruptedTimeSeries,
            treatment_time=pd.to_datetime("2017-01-01"),
            formula="y ~ 1 + t",
            model=cp.pymc_models.LinearRegression(sample_kwargs={"random_seed": 42}),
        ),
        cp.GenerateReport(include_plots=False),
    ],
).run()

print("Experiment type:", type(result.experiment).__name__)
print("Effect summary available:", result.effect_summary is not None)
print("Report generated:", result.report is not None)
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [beta, y_hat_sigma]

Sampling 4 chains for 1_000 tune and 1_000 draw iterations (4_000 + 4_000 draws total) took 1 seconds.
Sampling: [beta, y_hat, y_hat_sigma]
Sampling: [y_hat]
Sampling: [y_hat]
Sampling: [y_hat]
Sampling: [y_hat]
Experiment type: InterruptedTimeSeries
Effect summary available: True
Report generated: True

Adding sensitivity analysis#

The SensitivityAnalysis step runs a suite of diagnostic checks against the fitted experiment. Checks are pluggable, and you can choose which ones to run.

result = cp.Pipeline(
    data=df,
    steps=[
        cp.EstimateEffect(
            method=cp.InterruptedTimeSeries,
            treatment_time=pd.to_datetime("2017-01-01"),
            formula="y ~ 1 + t",
            model=cp.pymc_models.LinearRegression(sample_kwargs={"random_seed": 42}),
        ),
        cp.SensitivityAnalysis(
            checks=[
                cp.checks.PlaceboInTime(n_folds=2),
            ]
        ),
        cp.GenerateReport(include_plots=True),
    ],
).run()

print(f"Sensitivity checks run: {len(result.sensitivity_results)}")
for check_result in result.sensitivity_results:
    print(f"  - {check_result.check_name}: {check_result.text[:80]}...")
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [beta, y_hat_sigma]

Sampling 4 chains for 1_000 tune and 1_000 draw iterations (4_000 + 4_000 draws total) took 1 seconds.
Sampling: [beta, y_hat, y_hat_sigma]
Sampling: [y_hat]
Sampling: [y_hat]
Sampling: [y_hat]
Sampling: [y_hat]
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [beta, y_hat_sigma]

Sampling 4 chains for 1_000 tune and 1_000 draw iterations (4_000 + 4_000 draws total) took 1 seconds.
Sampling: [beta, y_hat, y_hat_sigma]
Sampling: [y_hat]
Sampling: [y_hat]
Sampling: [y_hat]
Sampling: [y_hat]
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [beta, y_hat_sigma]

Sampling 4 chains for 1_000 tune and 1_000 draw iterations (4_000 + 4_000 draws total) took 1 seconds.
Sampling: [beta, y_hat, y_hat_sigma]
Sampling: [y_hat]
Sampling: [y_hat]
Sampling: [y_hat]
Sampling: [y_hat]
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [mu_status_quo, tau_status_quo, fold_z]

Sampling 4 chains for 1_000 tune and 1_000 draw iterations (4_000 + 4_000 draws total) took 1 seconds.
There was 1 divergence after tuning. Increase `target_accept` or reparameterize.
Sampling: [theta_new]

../_images/670123408154a4a4f6416ef949ea748513ce9cf7d27617c5584c24b26206801c.png
Sensitivity checks run: 1
  - PlaceboInTime: Placebo-in-time analysis: 2 of 2 folds completed.
Hierarchical status-quo model:...

Available checks#

CausalPy provides a range of sensitivity checks, each applicable to specific experiment types:

Check

Applicable methods

Description

PlaceboInTime

ITS, SC

Shifts treatment time backward to test for spurious effects

PriorSensitivity

All Bayesian

Re-fits with different priors

ConvexHullCheck

SC

Validates treated values are within control range

PersistenceCheck

ITS (3-period)

Checks if effects persist after intervention ends

PreTreatmentPlaceboCheck

Staggered DiD

Validates parallel trends via pre-treatment effects

BandwidthSensitivity

RD, RKink

Re-fits with multiple bandwidths

LeaveOneOut

SC

Drops each control unit and refits

PlaceboInSpace

SC

Treats each control as placebo treated

McCraryDensityTest

RD

Tests for running variable manipulation

Pipeline result#

The PipelineResult object contains all accumulated outputs:

print("result.experiment      ->", type(result.experiment).__name__)
print("result.effect_summary  ->", type(result.effect_summary).__name__)
print("result.sensitivity_results ->", len(result.sensitivity_results), "checks")
print("result.report          ->", "HTML" if result.report else "None")
result.experiment      -> InterruptedTimeSeries
result.effect_summary  -> EffectSummary
result.sensitivity_results -> 1 checks
result.report          -> HTML

The effect summary provides both a table and prose:

if result.effect_summary is not None:
    print(result.effect_summary.text)
    display(result.effect_summary.table)
During the Post-period (2017-01-31 00:00:00 to 2019-12-31 00:00:00), the response variable had an average value of approx. 57.73. By contrast, in the absence of an intervention, we would have expected an average response of 56.34. The 95% interval of this counterfactual prediction is [51.79, 60.78]. Subtracting this prediction from the observed response yields an estimate of the causal effect the intervention had on the response variable. This effect is 1.40 with a 95% interval of [-3.04, 5.94].

Summing up the individual data points during the Post-period, the response variable had an overall value of 2078.37. By contrast, had the intervention not taken place, we would have expected a sum of 2028.08. The 95% interval of this prediction is [1864.38, 2187.92].

The 95% HDI of the effect [-3.04, 5.94] includes zero. The posterior probability of an increase is 0.730. Relative to the counterfactual, the effect represents a 2.65% change (95% HDI [-5.05%, 11.42%]).

This analysis assumes that the relationship between the time-based predictors and the response observed during the pre-intervention period remains stable throughout the post-intervention period. If the formula includes external covariates, it further assumes they were not themselves affected by the intervention. We recommend inspecting model fit, examining pre-intervention trends, and conducting sensitivity analyses (e.g., placebo tests) to support any causal conclusions drawn from this analysis.
mean median hdi_lower hdi_upper p_gt_0 relative_mean relative_hdi_lower relative_hdi_upper
average 1.396982 1.355452 -3.043179 5.944022 0.73 2.652909 -5.048524 11.424529
cumulative 50.291370 48.796265 -109.554427 213.984808 0.73 2.652909 -5.048524 11.424529

Viewing the HTML report#

The GenerateReport step renders the full analysis — effect summary, diagnostic plots, and sensitivity-check results — into a single HTML report stored in result.report. See the report generation notebook for the standalone (non-pipeline) API.

Hide code cell source
import html as html_module
import warnings

from IPython.display import HTML

with warnings.catch_warnings():
    warnings.filterwarnings(
        "ignore", "Consider using IPython.display.IFrame", UserWarning
    )
    report_widget = HTML(
        '<iframe srcdoc="'
        + html_module.escape(result.report)
        + '" width="100%" height="1000"'
        ' style="border: 1px solid #ddd; border-radius: 6px;"></iframe>'
    )
report_widget