Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Difference in difference

Imports

%load_ext autoreload
%autoreload 2

import logging

import pandas as pd

try:
    from IPython.display import display
except ImportError:
    display = print  # type: ignore
import helpers.hnotebook as hnotebo
import helpers.htutorial as ut

ut.config_notebook()

# Initialize logger.
logging.basicConfig(level=logging.INFO)
_LOG = logging.getLogger(__name__)
hnotebo.init_loggers(_LOG, set_all_loggers_to_print=True)
pymc is not installed
arviz is not installed
preliz is not installed
sns is not installed
Python 3.12.13
Linux 3a71c0a7bf16 6.12.67-linuxkit #1 SMP Sun Jan 25 02:26:28 UTC 2026 aarch64 GNU/Linux

Part 1: Loading Data

Cell 1.1: Loading the marketing panel data

Goal:

  • Load a marketing panel dataset used to estimate the effect of an offline campaign via difference-in-differences

The data is in a panel format:

  • Each row is a (date, city) pair
  • downloads: the outcome to predict
  • treated: indicator of whether the city received the intervention
  • tau: the (known, ground-truth) treatment effect
dir_name = "L08_data"
!ls $dir_name

out_dir_name = "figures/"
mkt_data = pd.read_csv(f"{dir_name}/short_offline_mkt_south.csv").astype(
    {"date": "datetime64[ns]"}
)
print("mkt_data.shape=", mkt_data.shape)
display(mkt_data.head())

Part 2: Canonical Difference-in-Differences

Cell 2.1: Checking the pre/post intervention windows

Goal:

  • Check the date range covered by each of the 4 (treated, post) groups, to confirm the panel spans both a pre- and a post-intervention period
# Compute pre- and post-intervention period.
pre_post_windows = (
    mkt_data.assign(w=lambda d: d["treated"] * d["post"])
    .groupby(["w"])
    .agg({"date": ["min", "max"]})
)
display(pre_post_windows)

Cell 2.2: Computing group-time average outcomes

Goal:

  • Compute the average outcome for each of the 4 (treated, post) groups, the building block of the difference-in-differences estimator
did_data = mkt_data.groupby(["treated", "post"]).agg(
    {"downloads": "mean", "date": "min"}
)
display(did_data)
mkt_data= (1632, 7)
Loading...

Cell 2.3: Computing the average treatment effect on the treated

Goal:

  • Estimate the ATT as the treated group’s actual post-period outcome minus its counterfactual (parallel-trends) outcome

Implementation:

  • y0_est: the treated group’s pre-period outcome, plus the control group’s pre-to-post change
  • att = <treated, post-period outcome> - y0_est
y0_est = (
    did_data.loc[1].loc[0, "downloads"]  # Treated baseline.
    + did_data.loc[0].diff().loc[1, "downloads"]  # Control evolution.
)

att = did_data.loc[1].loc[1, "downloads"] - y0_est
print("att=", att)
Loading...

Cell 2.4: Comparing against the known ground-truth effect

Goal:

  • Compare the estimated ATT against the dataset’s known, ground-truth tau, to check how close the difference-in-differences estimator got
print("mean tau=", mkt_data.query("post==1").query("treated==1")["tau"].mean())