Interrupted Time Series with post-intervention analysis#
For fixed-period intervention cases (like a marketing promotion, or public policy intervention), we can make our interrupted time series experiment aware of this by providing treatment_end_time as a keyword argument. This splits the post-intervention period into:
Intervention period: When treatment is active (from
treatment_timetotreatment_end_time)Post-intervention period: After treatment ends
This enables analysis of immediate effects, effect persistence, and decay patterns.
Note
For standard two-period ITS analysis (permanent interventions), see Bayesian Interrupted Time Series.
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import causalpy as cp
%load_ext autoreload
%autoreload 2
%config InlineBackend.figure_format = 'retina'
seed = 42
The autoreload extension is already loaded. To reload it, use:
%reload_ext autoreload
Example: Marketing Campaign#
We simulate a 12-week marketing campaign with an immediate effect (+25 units) and partial persistence after it ends (+8 units, ~30% persistence).
# Set up simulation parameters
rng = np.random.default_rng(seed)
n_weeks = 135 # 2 years of weekly data
dates = pd.date_range(start="2022-06-01", end="2024-12-31", freq="W")
# Baseline: trend + seasonality + noise
trend = np.linspace(100, 120, n_weeks)
season = 10 * np.sin(2 * np.pi * np.arange(n_weeks) / 52) # Annual seasonality
noise = rng.normal(0, 5, n_weeks)
baseline = trend + season + noise
# Add intervention effect
treatment_idx = n_weeks // 2 # Start at midpoint
treatment_end_idx = treatment_idx + 12 # 12 weeks duration
y = baseline.copy()
y[treatment_idx:treatment_end_idx] += 25 # During intervention
y[treatment_end_idx:] += 8 # Post-intervention (persistence)
# Create DataFrame
df = pd.DataFrame(
{
"y": y,
"t": np.arange(n_weeks),
"month": dates.month,
},
index=dates,
)
treatment_time = dates[treatment_idx]
treatment_end_time = dates[treatment_end_idx]
print(f"Treatment starts: {treatment_time}")
print(f"Treatment ends: {treatment_end_time}")
print(f"Intervention period: {treatment_end_idx - treatment_idx} weeks")
print(f"Post-intervention period: {n_weeks - treatment_end_idx} weeks")
Treatment starts: 2023-09-17 00:00:00
Treatment ends: 2023-12-10 00:00:00
Intervention period: 12 weeks
Post-intervention period: 56 weeks
Visualize the Data#
Let’s first visualize the raw time series data to get an intuitive sense of the intervention effect
# Plot the raw data with treatment periods marked
fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(df.index, df["y"], "o-", markersize=3, alpha=0.6, label="Observed")
ax.axvline(
treatment_time, color="red", linestyle="-", linewidth=2, label="Treatment starts"
)
ax.axvline(
treatment_end_time,
color="orange",
linestyle="--",
linewidth=2,
label="Treatment ends",
)
ax.set_xlabel("Date")
ax.set_ylabel("y")
ax.set_title("Time Series Data with Intervention Periods")
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Run the Analysis#
Specify treatment_end_time to enable three-period analysis:
result = cp.InterruptedTimeSeries(
df,
treatment_time=treatment_time,
treatment_end_time=treatment_end_time,
formula="y ~ 1 + t + C(month)",
model=cp.pymc_models.LinearRegression(
sample_kwargs={
"random_seed": seed,
"progressbar": False,
"chains": 2,
"draws": 1000,
}
),
)
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (2 chains in 2 jobs)
NUTS: [beta, y_hat_sigma]
Sampling 2 chains for 1_000 tune and 1_000 draw iterations (2_000 + 2_000 draws total) took 37 seconds.
We recommend running at least 4 chains for robust computation of convergence diagnostics
Sampling: [beta, y_hat, y_hat_sigma]
Sampling: [y_hat]
Sampling: [y_hat]
Sampling: [y_hat]
Sampling: [y_hat]
Visualization#
The three-period design visualization adds a vertical line to mark where the treatment ends:
Solid red line:
treatment_time(intervention start)Dashed orange line:
treatment_end_time(intervention end)
The plot shows three panels:
Top panel: Time series with observations, counterfactual predictions, and causal impact shading
Middle panel: Pointwise causal impact over time
Bottom panel: Cumulative causal impact
The vertical line at treatment_end_time clearly separates the intervention period from the post-intervention period, allowing you to visually assess effect persistence and decay.
fig, ax = result.plot()
plt.tight_layout()
plt.show()
C:\Users\jeanv\AppData\Local\Temp\ipykernel_19012\3091047611.py:2: UserWarning: The figure layout has changed to tight
plt.tight_layout()
Period-Specific Summaries#
Get separate summaries for each period using the period parameter:
# Intervention period
intervention_summary = result.effect_summary(period="intervention")
print(intervention_summary.text)
During intervention (2023-09-17 00:00:00 to 2023-12-03 00:00:00), the average effect was 24.53 (95% HDI [20.86, 27.93]), with a posterior probability of an increase of 1.000. The cumulative effect was 294.37 (95% HDI [250.34, 335.19]); probability of an increase 1.000. Relative to the counterfactual, this equals 21.11% on average (95% HDI [17.38%, 24.73%]).
# Post-intervention period
post_summary = result.effect_summary(period="post")
print(post_summary.text)
Post-intervention (2023-12-10 00:00:00 to 2024-12-29 00:00:00), the average effect was 6.38 (95% HDI [2.32, 10.77]), with a posterior probability of an increase of 0.996. The cumulative effect was 357.26 (95% HDI [129.70, 602.98]); probability of an increase 0.996. Relative to the counterfactual, this equals 5.52% on average (95% HDI [1.70%, 9.40%]).
Comparison Summary#
Use period='comparison' to get a comparative summary showing persistence metrics:
comparison_summary = result.effect_summary(period="comparison")
print(comparison_summary.text)
Effect persistence: The post-intervention effect (6.4, 95% HDI [2.3, 10.8]) was 26.0% of the intervention effect (24.5, 95% HDI [20.9, 27.9]), with a posterior probability of 1.00 that some effect persisted beyond the intervention period.
The comparison summary provides:
Post-intervention effect as percentage of intervention effect
Posterior probability that some effect persisted
HDI interval comparison between periods
Detailed Persistence Analysis#
The analyze_persistence() method automatically prints and returns a detailed summary of effect persistence:
persistence = result.analyze_persistence()
# The method automatically prints results. Access the returned dictionary:
print("\nAccessing results programmatically:")
print(f" Mean effect during: {persistence['mean_effect_during']:.2f}")
print(f" Mean effect post: {persistence['mean_effect_post']:.2f}")
print(
f" Persistence ratio: {persistence['persistence_ratio']:.3f} ({persistence['persistence_ratio'] * 100:.1f}%)"
)
print(f" Total effect during: {persistence['total_effect_during']:.2f}")
print(f" Total effect post: {persistence['total_effect_post']:.2f}")
============================================================
Effect Persistence Analysis
============================================================
During intervention period:
Mean effect: 24.53
95% HDI: [20.86, 27.93]
Total effect: 294.37
Post-intervention period:
Mean effect: 6.38
95% HDI: [2.32, 10.77]
Total effect: 357.26
Persistence ratio: 0.260
(26.0% of intervention effect persisted)
============================================================
Accessing results programmatically:
Mean effect during: 24.53
Mean effect post: 6.38
Persistence ratio: 0.260 (26.0%)
Total effect during: 294.37
Total effect post: 357.26
Summary#
The three-period design enables analysis of temporary interventions:
Immediate effects:
effect_summary(period="intervention")analyzes effects during the active interventionPersistence:
effect_summary(period="post")measures how effects persist after the intervention endsComparison:
effect_summary(period="comparison")provides a comparative summary with persistence metricsDetailed analysis:
analyze_persistence()automatically prints and returns a detailed summary with mean effects, persistence ratio (as decimal), and total effects
The persistence ratio (e.g., 0.30 = 30%) indicates how much of the intervention effect “carried over” into the post-intervention period. Note that the ratio can exceed 1.0 if the post-intervention effect is larger than the intervention effect.
In practice, persistence effects could be caused by various mechanisms. For example, in marketing contexts, persistence might reflect brand awareness effects that continue to influence consumer behavior even after the promotional campaign ends.