Approach (brief): build a daily panel with a treatment indicator (0 before day 11 for both groups, 1 thereafter for treated group only), model the outcome as a rate (conversions / users) and fit a time-series regression that (a) includes a lagged dependent variable to capture persistence, (b) includes a treatment-by-post indicator to estimate effect, and (c) accounts for AR(1) residuals. Estimate robust (HAC/Newey-West) standard errors to correct for remaining autocorrelation when forming CIs.Implementation (Python):python
import pandas as pd
import numpy as np
import statsmodels.api as sm
from statsmodels.regression.linear_model import GLSAR
from statsmodels.stats.sandwich_covariance import cov_hac
# assume df_control and df_treat provided with ['date','conversions','users']
df_control = df_control.copy(); df_treat = df_treat.copy()
df_control['group'] = 'control'; df_treat['group'] = 'treatment'
df = pd.concat([df_control, df_treat], ignore_index=True)
df['date'] = pd.to_datetime(df['date'])
df = df.sort_values(['group','date']).reset_index(drop=True)
# outcome = conversion rate
df['rate'] = df['conversions'] / df['users']
# create day index and post indicator (treatment starts on day 11)
start = df['date'].min()
df['day'] = (df['date'] - start).dt.days + 1
df['post'] = (df['day'] >= 11).astype(int)
df['treat'] = (df['group'] == 'treatment').astype(int)
df['treat_post'] = df['treat'] * df['post']
# create lagged outcome by group to avoid leakage across groups
df['rate_lag1'] = df.groupby('group')['rate'].shift(1)
df = df.dropna(subset=['rate_lag1']) # drop first day per group
# regressors: intercept, lag, treat, post, treat_post (DiD with lag)
X = sm.add_constant(df[['rate_lag1','treat','post','treat_post']])
y = df['rate']
# GLSAR to model AR(1) errors
model = GLSAR(y, X, rho=1) # start with rho=1 iteration
res = model.iterative_fit(5) # iterate to refine rho estimate
# robust covariance: Newey-West (HAC) with lag=1 or more based on autocorr
max_lag = 1
hac_cov = cov_hac(res, nlags=max_lag)
se_hac = np.sqrt(np.diag(hac_cov))
# coefficient, robust SE, 95% CI for treatment effect (treat_post)
coef = res.params['treat_post']
se = se_hac[list(res.params.index).index('treat_post')]
ci_lower = coef - 1.96 * se
ci_upper = coef + 1.96 * se
print("Estimated treatment effect (treat_post):", coef)
print("HAC SE:", se, "95% CI:", (ci_lower, ci_upper))
Key points / reasoning:- Modeling rate stabilizes variance; can also use logistic/Poisson GLM if counts and exposures vary widely.- Including lagged outcome controls temporal dependence and helps avoid biased effect estimates when outcomes are persistent.- GLSAR explicitly models AR(1) residuals for efficiency; iterative_fit estimates rho.- Newey-West (HAC) covariance provides consistent standard errors under heteroskedasticity and autocorrelation — use appropriate nlags (e.g., floor(4*(T/100)^(2/9)) or inspect ACF).- Check diagnostics: ACF/PACF of residuals, Durbin-Watson, and consider cluster-robust SEs if multiple independent units exist.Edge cases & alternatives:- If small sample (30 days), NLags choice matters; bootstrap (block bootstrap) is an alternative to get CIs.- If rates near 0/1 or counts small, use GLM with AR errors (e.g., state-space models or poisson regression with autoregressive latent process).