6 Chapter 8: General Designs
6.1 Overview
Dataset: Gentzkow, Shapiro & Sinkinson (2011) — gentzkowetal_didtextbook.dta (16,872 observations, 1,195 counties, 16 election years 1868–1928)
This chapter studies general designs where the treatment may be non-binary (taking values 0, 1, 2, …) and/or non-absorbing (groups can enter and exit treatment), and where treatment lags may affect the outcome. The running example measures the effect of the number of daily newspapers (numdailies) on presidential voter turnout (prestout).
Key topics: static TWFE decompositions with multiple treatments (twowayfeweights), non-normalized and normalized event-study estimators (did_multiplegt_dyn), treatment-path descriptions (design option), and heterogeneity-robust estimators ruling out dynamic effects (did_multiplegt_stat).
7 computer questions + 2 Figures + 1 Bonus test.
Packages used:
| Task | Stata | R | Python |
|---|---|---|---|
| PanelView | panelview (SSC) |
panelView (CRAN) |
matplotlib (manual) |
| OLS / TWFE | reg, areg |
fixest, sandwich |
statsmodels |
| Weight decomposition | twowayfeweights (SSC) |
TwoWayFEWeights (CRAN) |
Not available (other_treatments) |
| Event-study DID | did_multiplegt_dyn (SSC) |
DIDmultiplegtDYN (CRAN) |
py-did-multiplegt-dyn (PyPI) |
| Static DID (ATS/WATS) | did_multiplegt_stat (SSC) |
DIDmultiplegtSTAT (GitHub) |
py_did_multiplegt_stat (local) |
Installation notes:
- R
DIDmultiplegtSTAT:devtools::install_github("chaisemartinPackages/did_multiplegt_stat/R", force = TRUE) - R
DIDmultiplegtDYN: Thedesign()option works withdesign=c(0.8,"console"). Effects match across platforms. - Python
did_multiplegt_dyn:pip install py-did-multiplegt-dyn. RequirespolarsDataFrames. - Python
did_multiplegt_stat: Local notebook-based translation; seepy_did_multiplegt_stat.
Note on cross-platform differences: GQ1–GQ4 and GQ6 match exactly between Stata and R to 7 decimals. GQ7 (did_multiplegt_stat) shows small differences between Stata, R, and Python due to internal implementation variations. N, switchers, and stayers counts are identical across all three.
6.2 PanelView
* ssc install panelview, replace
copy "https://raw.githubusercontent.com/anzonyquispe/did_book/main/cc_xd_didtextbook_2025_9_30/Data%20sets/Gentzkow%20et%20al%202011/gentzkowetal_didtextbook.dta" "gentzkowetal_didtextbook.dta", replace
use "gentzkowetal_didtextbook.dta", clear
panelview prestout numdailies, i(cnty90) t(year) type(treat) title("Gentzkow et al. (2011)") legend(off) ylabel(none) ytitle("")
graph export "figures/ch05_panelview_stata.png", replace width(1200)
library(panelView)
load(url("https://raw.githubusercontent.com/anzonyquispe/did_book/main/cc_xd_didtextbook_2025_9_30/Data%20sets/Gentzkow%20et%20al%202011/gentzkowetal_didtextbook.RData"))
png("figures/ch08_panelview_R.png", width = 1000, height = 600)
panelview(prestout ~ numdailies, data = df, index = c("cnty90", "year"), type = "treat",
main = "Gentzkow, Shapiro & Sinkinson (2011)", ylab = "")
dev.off()
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
from matplotlib.patches import Patch
df = pd.read_parquet("https://raw.githubusercontent.com/anzonyquispe/did_book/main/cc_xd_didtextbook_2025_9_30/Data%20sets/Gentzkow%20et%20al%202011/gentzkowetal_didtextbook.parquet")
df["has_newspaper"] = (df["numdailies"] > 0).astype(int)
pv = df.pivot_table(index="cnty90", columns="year", values="has_newspaper", aggfunc="first")
pv_sorted = pv.loc[pv.mean(axis=1).sort_values(ascending=False).index]
cmap = mcolors.ListedColormap(["#D4E6F1", "#2171B5"])
fig, ax = plt.subplots(figsize=(12, 10))
ax.imshow(pv_sorted.values, aspect="auto", cmap=cmap, interpolation="nearest", vmin=0, vmax=1)
for i in range(0, len(pv_sorted), 10):
ax.axhline(y=i - 0.5, color="white", linewidth=0.15)
ax.set_xticks(range(0, len(pv_sorted.columns), 2))
ax.set_xticklabels([int(c) for c in pv_sorted.columns[::2]], rotation=45, ha="right", fontsize=8)
ax.set_yticks([])
ax.set_xlabel("Year")
ax.set_title("Gentzkow et al. (2011)", fontsize=16)
ax.legend(handles=[Patch(facecolor="#D4E6F1", edgecolor="gray", label="No newspaper"),
Patch(facecolor="#2171B5", edgecolor="gray", label="Has newspaper")],
loc="lower center", bbox_to_anchor=(0.5, -0.12), ncol=2)
plt.tight_layout()
plt.savefig("figures/ch08_panelview_Python.png", dpi=150, bbox_inches="tight")
plt.show()
6.3 CQ#40: Distributed-Lag TWFE Regression
Regress turnout on the number of newspapers and its lag, with county and year FEs, clustering standard errors at the county level. Interpret the results.
copy "https://raw.githubusercontent.com/anzonyquispe/did_book/main/cc_xd_didtextbook_2025_9_30/Data%20sets/Gentzkow%20et%20al%202011/gentzkowetal_didtextbook.dta" "gentzkowetal_didtextbook.dta", replace
use "gentzkowetal_didtextbook.dta", clear
areg prestout i.year numdailies lag_numdailies, absorb(cnty90) cluster(cnty90) (Std. err. adjusted for 1,195 clusters in cnty90)
--------------------------------------------------------------------------------
| Robust
prestout | Coefficient std. err. t P>|t| [95% conf. interval]
----------------+----------------------------------------------------------------
numdailies | -.0007962 .0014419 -0.55 0.581 -.0036257 .0020333
lag_numdailies | .0050348 .0015052 3.34 0.001 .0020814 .0079882
--------------------------------------------------------------------------------
N = 15,629
library(haven); library(fixest)
load(url("https://raw.githubusercontent.com/anzonyquispe/did_book/main/cc_xd_didtextbook_2025_9_30/Data%20sets/Gentzkow%20et%20al%202011/gentzkowetal_didtextbook.RData"))
gq3 <- feols(prestout ~ numdailies + lag_numdailies | cnty90 + year, data = df, cluster = ~cnty90)
summary(gq3) Estimate Std. Error t value Pr(>|t|)
numdailies -0.0007962 0.0013864 -0.5743 0.5658
lag_numdailies 0.0050348 0.0014472 3.4790 0.0005
N = 15,629
import pandas as pd
import pyfixest as pf
df = pd.read_parquet("https://raw.githubusercontent.com/anzonyquispe/did_book/main/cc_xd_didtextbook_2025_9_30/Data%20sets/Gentzkow%20et%20al%202011/gentzkowetal_didtextbook.parquet")
m8 = pf.feols("prestout ~ numdailies + lag_numdailies + C(year) | cnty90", data=df, vcov={"CRV1": "cnty90"}, ssc=pf.ssc(adj=True, fixef_k="full", cluster_adj=True))
print(m8.summary()) Variable Coef Std.Err t P>|t|
numdailies -0.0007962 0.0014419 -0.552 0.5810
lag_numdailies 0.0050348 0.0015052 3.345 0.0008
N = 15,629
Interpretation: The distributed-lag TWFE regression gives \(\hat\beta_0^{dl} = -0.0007962\) (insignificant) and \(\hat\beta_1^{dl} = 0.0050348\) (significant). The contemporaneous effect of newspapers on turnout is small and insignificant, while the lagged effect is positive and significant.
6.4 CQ#41: Weight Decomposition (twowayfeweights)
Use the
twowayfeweightscommand and theother_treatmentsoption to decompose \(\hat\beta_0^{dl}\) and \(\hat\beta_1^{dl}\), and interpret the results.
copy "https://raw.githubusercontent.com/anzonyquispe/did_book/main/cc_xd_didtextbook_2025_9_30/Data%20sets/Gentzkow%20et%20al%202011/gentzkowetal_didtextbook.dta" "gentzkowetal_didtextbook.dta", replace
use "gentzkowetal_didtextbook.dta", clear
twowayfeweights prestout cnty90 year numdailies, other_treatments(lag_numdailies) type(feTR)
twowayfeweights prestout cnty90 year lag_numdailies, other_treatments(numdailies) type(feTR)--- Decomposition of beta on numdailies (β = -0.0008) ---
Under the common trends assumption, beta estimates a weighted sum of 10,056 ATTs.
5,754 ATTs receive a positive weight, and 4,302 receive a negative weight.
Σ positive weights = 1.8541 | Σ negative weights = -0.8541
Other treatment (lag_numdailies):
4,721 positive (Σ = 1.2137) | 4,618 negative (Σ = -1.2137)
--- Decomposition of beta on lag_numdailies (β = 0.0050) ---
Under the common trends assumption, beta estimates a weighted sum of 9,339 ATTs.
5,273 ATTs receive a positive weight, and 4,066 receive a negative weight.
Σ positive weights = 1.7820 | Σ negative weights = -0.7820
Other treatment (numdailies):
4,933 positive (Σ = 1.3558) | 5,123 negative (Σ = -1.3558)
library(haven); library(TwoWayFEWeights)
load(url("https://raw.githubusercontent.com/anzonyquispe/did_book/main/cc_xd_didtextbook_2025_9_30/Data%20sets/Gentzkow%20et%20al%202011/gentzkowetal_didtextbook.RData"))
gq3a <- twowayfeweights(df, "prestout", "cnty90", "year", "numdailies", type = "feTR", other_treatments = "lag_numdailies")
gq3b <- twowayfeweights(df, "prestout", "cnty90", "year", "lag_numdailies", type = "feTR", other_treatments = "numdailies")--- Decomposition of beta on numdailies (β = -0.0008) ---
Σ positive weights = 1.8541 (5,754 ATTs)
Σ negative weights = -0.8541 (4,302 ATTs)
Other treatment: 4,721 pos (Σ = 1.2137) | 4,618 neg (Σ = -1.2137)
--- Decomposition of beta on lag_numdailies (β = 0.0050) ---
Σ positive weights = 1.7820 (5,273 ATTs)
Σ negative weights = -0.7820 (4,066 ATTs)
Other treatment: 4,933 pos (Σ = 1.3558) | 5,123 neg (Σ = -1.3558)
import pandas as pd
from twowayfeweights import twowayfeweights
df = pd.read_parquet("https://raw.githubusercontent.com/anzonyquispe/did_book/main/cc_xd_didtextbook_2025_9_30/Data%20sets/Gentzkow%20et%20al%202011/gentzkowetal_didtextbook.parquet")
res_a = twowayfeweights(df, "prestout", "cnty90", "year", "numdailies", type="feTR", other_treatments="lag_numdailies")
res_b = twowayfeweights(df, "prestout", "cnty90", "year", "lag_numdailies", type="feTR", other_treatments="numdailies")Decomposition of beta on numdailies:
Positive weights: 5754, Σ = 1.8541
Negative weights: 4302, Σ = -0.8541
Other treat (lag_numdailies): 4721 pos (1.2137) | 4618 neg (-1.2137)
Decomposition of beta on lag_numdailies:
Positive weights: 5273, Σ = 1.7820
Negative weights: 4066, Σ = -0.7820
Other treat (numdailies): 4933 pos (1.3558) | 5123 neg (-1.3558)
Interpretation: Both coefficients are severely contaminated: the positive weights sum to 1.85, the negative weights to \(-0.85\), and the cross-contamination from the other treatment sums to \(\pm 1.21\). This means neither coefficient can be interpreted as a convex combination of causal effects.
6.5 CQ#42: Non-Normalized Event-Study Effects (Figure 8.1)
Use
did_multiplegt_dynto compute, for \(\ell \in \{1, \ldots, 4\}\), non-normalized event-study estimates \(\widehat{AVSQ}_\ell\) of the effect of being exposed to a weakly larger number of newspapers for \(\ell\) election cycles on turnout, as well as a test that effects are all equal using theeffects_equaloption. Also compute the pre-trend estimates \(\widehat{AVSQ}_{-\ell}\) for \(\ell \in \{1, \ldots, 4\}\).
* ssc install did_multiplegt_dyn, replace
copy "https://raw.githubusercontent.com/anzonyquispe/did_book/main/cc_xd_didtextbook_2025_9_30/Data%20sets/Gentzkow%20et%20al%202011/gentzkowetal_didtextbook.dta" "gentzkowetal_didtextbook.dta", replace
use "gentzkowetal_didtextbook.dta", clear
did_multiplegt_dyn prestout cnty90 year numdailies, effects(4) placebo(4) effects_equal(all) cluster(cnty90)
graph export "$FIGDIR\ch08_fig81_nonnormalized_es.png", replace width(1200)----------------------------------------------------------------------
Estimation of treatment effects: Event-study effects
----------------------------------------------------------------------
Estimate SE LB CI UB CI N Switchers
Effect_1 0.0144244 0.0042477 0.0060992 0.0227497 5,674 1,119
Effect_2 0.0190899 0.0058429 0.0076381 0.0305418 4,648 1,054
Effect_3 0.0207147 0.0079164 0.0051989 0.0362305 3,750 984
Effect_4 0.0272653 0.0097924 0.0080727 0.0464580 2,980 917
Test of joint nullity of the effects : p-value = 0.0068
Test of equality of the effects : p-value = 0.4152
----------------------------------------------------------------------
Average cumulative (total) effect per treatment unit
----------------------------------------------------------------------
Estimate SE LB CI UB CI N Switchers
0.0160565 0.0047761 0.0066956 0.0254174 8,659 4,074
Average number of periods over which effect is accumulated: 2.4376
----------------------------------------------------------------------
Testing the parallel trends and no anticipation assumptions
----------------------------------------------------------------------
Estimate SE LB CI UB CI N Switchers
Placebo_1 -0.0005025 0.0051322 -0.0105615 0.0095565 4,471 902
Placebo_2 0.0020594 0.0085031 -0.0146063 0.0187251 2,778 746
Placebo_3 -0.0015365 0.0116825 -0.0244337 0.0213607 1,644 604
Placebo_4 0.0006573 0.0175032 -0.0336483 0.0349628 910 441
Test of joint nullity of the placebos : p-value = 0.9922

library(haven); library(DIDmultiplegtDYN)
load(url("https://raw.githubusercontent.com/anzonyquispe/did_book/main/cc_xd_didtextbook_2025_9_30/Data%20sets/Gentzkow%20et%20al%202011/gentzkowetal_didtextbook.RData"))
gq4 <- did_multiplegt_dyn(
df = df, outcome = "prestout", group = "cnty90",
time = "year", treatment = "numdailies",
effects = 4, placebo = 4, effects_equal = TRUE, cluster = "cnty90")
ggsave("figures/ch08_fig81_nonnormalized_es_R.png",
plot = gq4$plot, width = 8, height = 5, dpi = 150) Estimate SE LB CI UB CI N Switchers
Effect_1 0.01442 0.00425 0.00610 0.02275 5,674 1,119
Effect_2 0.01909 0.00584 0.00764 0.03054 4,648 1,054
Effect_3 0.02071 0.00792 0.00520 0.03623 3,750 984
Effect_4 0.02727 0.00979 0.00807 0.04646 2,980 917
Test of joint nullity of the effects : p-value = 0.0068
Test of equality of the effects : p-value = 0.4152
Av_tot_eff = 0.01606 (SE = 0.00478)
Placebo_1 -0.00050 0.00513 4,471 902
Placebo_2 0.00206 0.00850 2,778 746
Placebo_3 -0.00154 0.01168 1,644 604
Placebo_4 0.00066 0.01750 910 441
Test of joint nullity of the placebos : p-value = 0.9922

import pandas as pd
import polars as pl
import matplotlib.pyplot as plt
from did_multiplegt_dyn import DidMultiplegtDyn
df = pd.read_parquet("https://raw.githubusercontent.com/anzonyquispe/did_book/main/cc_xd_didtextbook_2025_9_30/Data%20sets/Gentzkow%20et%20al%202011/gentzkowetal_didtextbook.parquet")
df_pl = pl.from_pandas(df)
pd.set_option('display.float_format', lambda x: f'{x:.7f}')
gq4 = DidMultiplegtDyn(df=df_pl, outcome="prestout", group="cnty90",
time="year", treatment="numdailies", effects=4, placebo=4, effects_equal=True, cluster="cnty90")
gq4.fit(); gq4.summary()
gq4.plot()
plt.savefig("figures/ch08_fig81_nonnormalized_es_Python.png", dpi=150, bbox_inches="tight") Block Estimate SE LB CI UB CI N Switchers
Effect_1 0.0144244 0.0042477 0.0060992 0.0227497 5674 1119
Effect_2 0.0190899 0.0058429 0.0076381 0.0305418 4648 1054
Effect_3 0.0207147 0.0079164 0.0051989 0.0362305 3750 984
Effect_4 0.0272653 0.0097924 0.0080727 0.0464580 2980 917
Average_Total_Effect 0.0160565 0.0047761 0.0066956 0.0254174 8659 4074
Placebo_1 -0.0005025 0.0051322 -0.0105615 0.0095565 4471 902
Placebo_2 0.0020594 0.0085031 -0.0146063 0.0187251 2778 746
Placebo_3 -0.0015365 0.0116825 -0.0244337 0.0213607 1644 604
Placebo_4 0.0006573 0.0175032 -0.0336483 0.0349628 910 441
Test of joint nullity of the effects : p-value = 0.006814
Test of equality of the effects : p-value = 0.415152
Test of joint nullity of the placebos : p-value = 0.992198

Interpretation: All four non-normalized event-study effects are positive and significant, suggesting newspapers increase turnout. The effects grow with \(\ell\), from 0.0144 at \(\ell = 1\) to 0.0273 at \(\ell = 4\). However, one cannot reject the null that all effects are equal (p-value = 0.4152). All placebo estimates are small and insignificant, with a joint p-value of 0.9922, strongly supporting the parallel-trends assumption. The average cumulative effect per treatment unit is 0.0161 (SE = 0.0048).
Figure 8.1: Non-normalized DID estimates of the effect of being exposed to a weakly larger number of newspapers for \(\ell\) periods on turnout. Standard errors clustered at the county level. 95% confidence intervals shown in red.
6.6 CQ#43–45: Treatment Path Descriptions
Rerun
did_multiplegt_dynwithdesign(0.8,console)for \(\ell = 1, 2, 4\). What are the three most common “actual-versus-status-quo” comparisons averaged in \(AVSQ_\ell\)?
* ssc install did_multiplegt_dyn, replace
copy "https://raw.githubusercontent.com/anzonyquispe/did_book/main/cc_xd_didtextbook_2025_9_30/Data%20sets/Gentzkow%20et%20al%202011/gentzkowetal_didtextbook.dta" "gentzkowetal_didtextbook.dta", replace
use "gentzkowetal_didtextbook.dta", clear
did_multiplegt_dyn prestout cnty90 year numdailies, effects(1) design(0.8,console) graph_off
did_multiplegt_dyn prestout cnty90 year numdailies, effects(2) design(0.8,console) graph_off
did_multiplegt_dyn prestout cnty90 year numdailies, effects(4) design(0.8,console) graph_off--- ℓ = 1 ---
Estimate SE LB CI UB CI N Switchers
Effect_1 .0144244 .0042477 .0060992 .0227497 5674 1119
Av_tot_eff .0120186 .0035392 .0050819 .0189552 5674 1119
Detection of treatment paths - 1 periods after first switch (1,126 switchers):
TreatPath1: 721 groups (64.03%), ℓ=0: 0, ℓ=1: 1
TreatPath2: 139 groups (12.34%), ℓ=0: 0, ℓ=1: 2
TreatPath3: 54 groups ( 4.80%), ℓ=0: 1, ℓ=1: 2
Top 3 cover 81.17% of switchers
--- ℓ = 2 ---
Estimate SE LB CI UB CI N Switchers
Effect_1 .0144244 .0042477 .0060992 .0227497 5674 1119
Effect_2 .0190899 .0058429 .0076381 .0305418 4648 1054
Av_tot_eff .0143270 .0039890 .0065087 .0221454 6754 2173
Joint nullity p-value = 0.0015
Detection of treatment paths - 2 periods after first switch (1,067 switchers):
TreatPath1: 343 groups (32.15%), (0, 1, 1)
TreatPath2: 187 groups (17.53%), (0, 1, 0)
TreatPath3: 131 groups (12.28%), (0, 1, 2)
... 9 paths cover 81.54% of switchers
--- ℓ = 4 ---
Estimate SE LB CI UB CI N Switchers
Effect_1 .0144244 .0042477 .0060992 .0227497 5674 1119
Effect_2 .0190899 .0058429 .0076381 .0305418 4648 1054
Effect_3 .0207147 .0079164 .0051989 .0362305 3750 984
Effect_4 .0272653 .0097924 .0080727 .0464580 2980 917
Av_tot_eff .0160565 .0047761 .0066956 .0254174 8659 4074
Joint nullity p-value = 0.0068
Detection of treatment paths - 4 periods after first switch (933 switchers):
TreatPath1: 141 groups (15.11%), (0, 1, 1, 1, 1)
TreatPath2: 127 groups (13.61%), (0, 1, 0, 0, 0)
TreatPath3: 43 groups ( 4.61%), (0, 1, 2, 2, 2)
... 56 paths cover 80.17% of switchers
library(haven); library(polars); library(DIDmultiplegtDYN)
load(url("https://raw.githubusercontent.com/anzonyquispe/did_book/main/cc_xd_didtextbook_2025_9_30/Data%20sets/Gentzkow%20et%20al%202011/gentzkowetal_didtextbook.RData")); df <- as.data.frame(df)
gq5_l1 <- did_multiplegt_dyn(df=df, outcome="prestout", group="cnty90",
time="year", treatment="numdailies", effects=1,
design=c(0.8,"console"), graph_off=TRUE)
gq5_l2 <- did_multiplegt_dyn(df=df, outcome="prestout", group="cnty90",
time="year", treatment="numdailies", effects=2,
design=c(0.8,"console"), graph_off=TRUE)
gq5_l4 <- did_multiplegt_dyn(df=df, outcome="prestout", group="cnty90",
time="year", treatment="numdailies", effects=4,
design=c(0.8,"console"), graph_off=TRUE)--- ℓ = 1 ---
Estimate SE LB CI UB CI N Switchers
0.0144244 0.0042477 0.0060992 0.0227497 5,674 1,119
Av_tot_eff = 0.0120186 (SE = 0.0035392)
Detection of treatment paths - 1 periods after first switch:
TreatPath1: 721 (64.03%), (0, 1)
TreatPath2: 139 (12.34%), (0, 2)
TreatPath3: 54 ( 4.80%), (1, 2)
Top 3 cover 81.17%
--- ℓ = 2 ---
Estimate SE LB CI UB CI N Switchers
Effect_1 0.0144244 0.0042477 0.0060992 0.0227497 5,674 1,119
Effect_2 0.0190899 0.0058429 0.0076381 0.0305418 4,648 1,054
Av_tot_eff = 0.0143270 (SE = 0.0039890). Joint nullity p = 0.0015
Detection of treatment paths - 2 periods after first switch:
TreatPath1: 343 (32.15%), (0, 1, 1)
TreatPath2: 187 (17.53%), (0, 1, 0)
TreatPath3: 131 (12.28%), (0, 1, 2)
... 9 paths cover 81.54%
--- ℓ = 4 ---
Estimate SE LB CI UB CI N Switchers
Effect_1 0.0144244 0.0042477 0.0060992 0.0227497 5,674 1,119
Effect_2 0.0190899 0.0058429 0.0076381 0.0305418 4,648 1,054
Effect_3 0.0207147 0.0079164 0.0051989 0.0362305 3,750 984
Effect_4 0.0272653 0.0097924 0.0080727 0.0464580 2,980 917
Av_tot_eff = 0.0160565 (SE = 0.0047761). Joint nullity p = 0.0068
Detection of treatment paths - 4 periods after first switch:
TreatPath1: 141 (15.11%), (0, 1, 1, 1, 1)
TreatPath2: 127 (13.61%), (0, 1, 0, 0, 0)
TreatPath3: 43 ( 4.61%), (0, 1, 2, 2, 2)
... 56 paths cover 80.17%
import pandas as pd
import polars as pl
from did_multiplegt_dyn import DidMultiplegtDyn
df = pd.read_parquet("https://raw.githubusercontent.com/anzonyquispe/did_book/main/cc_xd_didtextbook_2025_9_30/Data%20sets/Gentzkow%20et%20al%202011/gentzkowetal_didtextbook.parquet")
df_pl = pl.from_pandas(df)
pd.set_option('display.float_format', lambda x: f'{x:.7f}')
for ell in [1, 2, 4]:
model = DidMultiplegtDyn(df=df_pl, outcome="prestout", group="cnty90",
time="year", treatment="numdailies", effects=ell, cluster="cnty90",
design=[0.8, "console"])
model.fit(); model.summary()--- effects = 1 ---
================================================================================
Detection of treatment paths - 1 periods after first switch
================================================================================
#Groups %Groups l=0 l=1
TreatPath1 721 64.0319716 0.0000000 1.0000000
TreatPath2 139 12.3445826 0.0000000 2.0000000
TreatPath3 54 4.7957371 1.0000000 2.0000000
TreatPath4 54 4.7957371 2.0000000 3.0000000
================================================================================
Treatment paths detected in switching groups: 1126
Total % shown: 85.97%
Estimate SE LB CI UB CI N Switchers
Effect_1 0.0144244 0.0042477 0.0060992 0.0227497 5674 1119
--- effects = 2 ---
================================================================================
Detection of treatment paths - 2 periods after first switch
================================================================================
#Groups %Groups l=0 l=1 l=2
TreatPath1 343 32.1462043 0.0000000 1.0000000 1.0000000
TreatPath2 187 17.5257732 0.0000000 1.0000000 0.0000000
TreatPath3 131 12.2774133 0.0000000 1.0000000 2.0000000
TreatPath4 57 5.3420806 0.0000000 2.0000000 2.0000000
TreatPath5 47 4.4048735 0.0000000 2.0000000 1.0000000
TreatPath6 33 3.0927835 1.0000000 2.0000000 2.0000000
TreatPath7 30 2.8116214 0.0000000 1.0000000 3.0000000
TreatPath8 22 2.0618557 2.0000000 3.0000000 2.0000000
TreatPath9 20 1.8744142 2.0000000 1.0000000 1.0000000
================================================================================
Treatment paths detected in switching groups: 1067
Total % shown: 83.22%
Estimate SE LB CI UB CI N Switchers
Effect_1 0.0144244 0.0042477 0.0060992 0.0227497 5674 1119
Effect_2 0.0190899 0.0058429 0.0076381 0.0305418 4648 1054
Av_tot_eff 0.0143270 0.0039890 0.0065087 0.0221454 6754 2173
--- effects = 4 ---
================================================================================
Detection of treatment paths - 4 periods after first switch
================================================================================
#Groups %Groups l=0 l=1 l=2 l=3 l=4
TreatPath1 141 15.1125402 0.0000000 1.0000000 1.0000000 1.0000000 1.0000000
TreatPath2 127 13.6120043 0.0000000 1.0000000 0.0000000 0.0000000 0.0000000
TreatPath3 43 4.6087889 0.0000000 1.0000000 2.0000000 2.0000000 2.0000000
... (57 treatment paths total, top 3 cover 33.3%)
================================================================================
Treatment paths detected in switching groups: 933
Total % shown: 80.49%
Estimate SE LB CI UB CI N Switchers
Effect_1 0.0144244 0.0042477 0.0060992 0.0227497 5674 1119
Effect_2 0.0190899 0.0058429 0.0076381 0.0305418 4648 1054
Effect_3 0.0207147 0.0079164 0.0051989 0.0362305 3750 984
Effect_4 0.0272653 0.0097924 0.0080727 0.0464580 2980 917
Av_tot_eff 0.0160565 0.0047761 0.0066956 0.0254174 8659 4074
All effects and treatment paths match Stata/R.
Interpretation: At \(\ell = 1\), most switchers transition from 0 to 1 newspaper (64%). As \(\ell\) increases, the treatment paths become more heterogeneous: by \(\ell = 4\), the top 3 paths account for only about 34% of all effects. Some counties gain newspapers and keep them, others gain and lose them. This heterogeneity complicates interpretation of \(AVSQ_\ell\) at longer horizons.
6.7 CQ#45b: Path-Specific Event-Study Effects (by_path)
Estimate the first two event-study effects separately for the three most common \((D_{g,F_g-1}, D_{g,F_g}, D_{g,F_g+1})\) paths shared by more than 100 counties, using
by_path(3).
copy "https://raw.githubusercontent.com/anzonyquispe/did_book/main/cc_xd_didtextbook_2025_9_30/Data%20sets/Gentzkow%20et%20al%202011/gentzkowetal_didtextbook.dta" "gentzkowetal_didtextbook.dta", replace
use "gentzkowetal_didtextbook.dta", clear
did_multiplegt_dyn prestout cnty90 year numdailies, effects(2) by_path(3)--- Path (0, 1, 1) ---
| Estimate SE LB CI UB CI N Switchers
Effect_1 | .0131318 .0071358 -.0008542 .0271178 4529 343
Effect_2 | .009357 .0090553 -.008391 .027105 3713 343
Test of joint nullity of the effects : p-value = .1750087
--- Path (0, 1, 0) ---
| Estimate SE LB CI UB CI N Switchers
Effect_1 | .0144984 .0103889 -.0058635 .0348603 4373 187
Effect_2 | .0082605 .0115639 -.0144043 .0309254 3557 187
Test of joint nullity of the effects : p-value = .32227545
--- Path (0, 1, 2) ---
| Estimate SE LB CI UB CI N Switchers
Effect_1 | .0212045 .0103034 .0010102 .0413988 4317 131
Effect_2 | .0314473 .0132732 .0054324 .0574622 3501 131
Test of joint nullity of the effects : p-value = .05711112
library(haven); library(DIDmultiplegtDYN)
load(url("https://raw.githubusercontent.com/anzonyquispe/did_book/main/cc_xd_didtextbook_2025_9_30/Data%20sets/Gentzkow%20et%20al%202011/gentzkowetal_didtextbook.RData"))
did_multiplegt_dyn(df = as.data.frame(df), outcome = "prestout", group = "cnty90", time = "year", treatment = "numdailies", effects = 2, by_path = 3)--- Path (0, 1, 1) ---
Estimate SE LB CI UB CI N Switchers
Effect_1 0.01326 0.00714 -0.00074 0.02725 4,479 343
Effect_2 0.01003 0.00906 -0.00773 0.02779 3,675 343
Test of joint nullity of the effects : p-value = 0.1739
--- Path (0, 1, 0) ---
Estimate SE LB CI UB CI N Switchers
Effect_1 0.01468 0.01039 -0.00569 0.03504 4,323 187
Effect_2 0.00920 0.01156 -0.01346 0.03187 3,519 187
Test of joint nullity of the effects : p-value = 0.3324
--- Path (0, 1, 2) ---
Estimate SE LB CI UB CI N Switchers
Effect_1 0.02171 0.01030 0.00151 0.04190 4,267 131
Effect_2 0.03239 0.01328 0.00637 0.05842 3,463 131
Test of joint nullity of the effects : p-value = 0.0484
import pandas as pd
import polars as pl
from did_multiplegt_dyn import DidMultiplegtDyn
df = pd.read_parquet("https://raw.githubusercontent.com/anzonyquispe/did_book/main/cc_xd_didtextbook_2025_9_30/Data%20sets/Gentzkow%20et%20al%202011/gentzkowetal_didtextbook.parquet")
df_pl = pl.from_pandas(df)
bp = DidMultiplegtDyn(df=df_pl, outcome="prestout", group="cnty90", time="year", treatment="numdailies", effects=2, by_path=3, cluster="cnty90")
bp.fit(); bp.summary()Note: The Python py_did_multiplegt_dyn package groups treatment paths differently
from Stata/R (e.g., (None, 1, None) instead of (0, 1, 1)). Results are not
directly comparable. Use Stata or R for by_path analysis.
Interpretation: Event-study effects are insignificant for the \((D_{g,F_g-1}, D_{g,F_g}, D_{g,F_g+1}) = (0, 1, 1)\) and \((0, 1, 0)\) paths, and significant for the \((0, 1, 2)\) path. This might suggest that sustained exposure to an increasing number of newspapers is necessary to generate an effect, though event-study effects are noisy and may not significantly differ across paths.
6.8 CQ#46–47: Normalized Event-Study Effects (Figure 8.2)
Compute normalized event-study estimates \(\widehat{AVSQ}^n_\ell\) for \(\ell \in \{1, \ldots, 4\}\) and normalized pre-trends. Test whether all normalized effects are equal. Produce Figure 8.2.
* ssc install did_multiplegt_dyn, replace
copy "https://raw.githubusercontent.com/anzonyquispe/did_book/main/cc_xd_didtextbook_2025_9_30/Data%20sets/Gentzkow%20et%20al%202011/gentzkowetal_didtextbook.dta" "gentzkowetal_didtextbook.dta", replace
use "gentzkowetal_didtextbook.dta", clear
did_multiplegt_dyn prestout cnty90 year numdailies, effects(4) placebo(4) normalized effects_equal(all) normalized_weights cluster(cnty90)
graph export "$FIGDIR\ch08_fig82_normalized_es.png", replace width(1200)----------------------------------------------------------------------
Estimation of treatment effects: Event-study effects
----------------------------------------------------------------------
Estimate SE LB CI UB CI N Switchers
Effect_1 0.0120186 0.0035392 0.0050819 0.0189552 5,674 1,119
Effect_2 0.0082126 0.0025136 0.0032859 0.0131392 4,648 1,054
Effect_3 0.0057192 0.0021857 0.0014354 0.0100030 3,750 984
Effect_4 0.0053873 0.0019348 0.0015951 0.0091795 2,980 917
Test of joint nullity of the effects : p-value = 0.0068
Test of equality of the effects : p-value = 0.1653
Average cumulative (total) effect per treatment unit:
0.0160565 (SE = 0.0047761)
----------------------------------------------------------------------
Testing the parallel trends and no anticipation assumptions
----------------------------------------------------------------------
Estimate SE LB CI UB CI N Switchers
Placebo_1 -0.0004158 0.0042470 -0.0087399 0.0079082 4,471 902
Placebo_2 0.0008699 0.0035919 -0.0061701 0.0079099 2,778 746
Placebo_3 -0.0004201 0.0031943 -0.0066808 0.0058406 1,644 604
Placebo_4 0.0001394 0.0037128 -0.0071375 0.0074164 910 441
Test of joint nullity of the placebos : p-value = 0.9922
----------------------------------------------------------------------
Weights on treatment lags
----------------------------------------------------------------------
| ℓ=1 ℓ=2 ℓ=3 ℓ=4
k=0 | 1.0000 0.4849 0.3549 0.2777
k=1 | . 0.5151 0.3131 0.2568
k=2 | . . 0.3319 0.2271
k=3 | . . . 0.2383
Total | 1.0000 1.0000 1.0000 1.0000

library(haven); library(DIDmultiplegtDYN)
load(url("https://raw.githubusercontent.com/anzonyquispe/did_book/main/cc_xd_didtextbook_2025_9_30/Data%20sets/Gentzkow%20et%20al%202011/gentzkowetal_didtextbook.RData"))
gq6 <- did_multiplegt_dyn(
df = df, outcome = "prestout", group = "cnty90",
time = "year", treatment = "numdailies",
effects = 4, placebo = 4,
normalized = TRUE, normalized_weights = TRUE, effects_equal = TRUE, cluster = "cnty90")
ggsave("figures/ch08_fig82_normalized_es_R.png",
plot = gq6$plot, width = 8, height = 5, dpi = 150) Estimate SE LB CI UB CI N Switchers
Effect_1 0.0120186 0.0035392 0.0050818 0.0189554 5,674 1,119
Effect_2 0.0082126 0.0025136 0.0032860 0.0131392 4,648 1,054
Effect_3 0.0057192 0.0021857 0.0014353 0.0100031 3,750 984
Effect_4 0.0053873 0.0019348 0.0015951 0.0091795 2,980 917
Joint nullity of effects : p = 0.0068
Equality of effects : p = 0.1653
Av_tot_eff = 0.0160565 (SE = 0.0047761)
Placebo_1 -0.0004159 0.0042471 4,418 902
Placebo_2 0.0008718 0.0035930 2,764 746
Placebo_3 -0.0004199 0.0031938 1,636 604
Placebo_4 0.0001368 0.0037130 907 441
Joint nullity of placebos : p = 0.9922

import pandas as pd
import polars as pl
import matplotlib.pyplot as plt
from did_multiplegt_dyn import DidMultiplegtDyn
df = pd.read_parquet("https://raw.githubusercontent.com/anzonyquispe/did_book/main/cc_xd_didtextbook_2025_9_30/Data%20sets/Gentzkow%20et%20al%202011/gentzkowetal_didtextbook.parquet")
df_pl = pl.from_pandas(df)
pd.set_option('display.float_format', lambda x: f'{x:.7f}')
gq6 = DidMultiplegtDyn(df=df_pl, outcome="prestout", group="cnty90",
time="year", treatment="numdailies", effects=4, placebo=4,
normalized=True, normalized_weights=True, effects_equal=True, cluster="cnty90")
gq6.fit(); gq6.summary()
gq6.plot()
plt.savefig("figures/ch08_fig82_normalized_es_Python.png", dpi=150, bbox_inches="tight") Block Estimate SE LB CI UB CI N Switchers
Effect_1 0.0120186 0.0035392 0.0050818 0.0189554 5674 1119
Effect_2 0.0082126 0.0025136 0.0032860 0.0131392 4648 1054
Effect_3 0.0057192 0.0021857 0.0014353 0.0100031 3750 984
Effect_4 0.0053873 0.0019348 0.0015951 0.0091795 2980 917
Average_Total_Effect 0.0160565 0.0047761 0.0066956 0.0254174 8659 4074
Placebo_1 -0.0004159 0.0042471 -0.0087401 0.0079083 4471 902
Placebo_2 0.0008718 0.0035920 -0.0061704 0.0079140 2778 746
Placebo_3 -0.0004199 0.0031938 -0.0066796 0.0058398 1644 604
Placebo_4 0.0001390 0.0037130 -0.0071380 0.0074160 910 441
Joint nullity of effects : p = 0.006814
Joint nullity of placebos : p = 0.992198
Equality of effects : p = 0.165258

Interpretation: Normalized effects are decreasing with \(\ell\) (from 0.0120 to 0.0054), but the equality test cannot reject the null (\(p = 0.1653\)). The weights \(w_{\ell,k}\) are approximately equal across lags within each \(\ell\): for \(\ell = 4\), the weight on contemporaneous newspapers (\(w_{4,0} = 0.28\)) is similar to that on the third lag (\(w_{4,3} = 0.24\)). The decreasing pattern of normalized effects may suggest that lagged newspapers have a smaller effect than contemporaneous newspapers.
Figure 8.2: Normalized DID estimates of effects of newspapers on turnout. Standard errors clustered at the county level. 95% confidence intervals shown in red.
6.9 CQ#49: ATS and WATS Estimators (did_multiplegt_stat)
Using
did_multiplegt_stat, compute the ATS and WATS estimators withexact_matchand one placebo. Compare to the normalized event-study effects.
* ssc install did_multiplegt_stat, replace
copy "https://raw.githubusercontent.com/anzonyquispe/did_book/main/cc_xd_didtextbook_2025_9_30/Data%20sets/Gentzkow%20et%20al%202011/gentzkowetal_didtextbook.dta" "gentzkowetal_didtextbook.dta", replace
use "gentzkowetal_didtextbook.dta", clear
did_multiplegt_stat prestout cnty90 year numdailies, placebo(1) exact_match-----------------------------------
N = 15466
WAOSS Method = Reg. Adjustment
Common Support = Exact Matching
-----------------------------------
----------------------------------------------------------------------
Estimation of AOSS(s)
----------------------------------------------------------------------
Estimate SE LB CI UB CI Switchers Stayers
AOSS 0.0060678 0.0016463 0.0028411 0.0092945 4,423 11,043
----------------------------------------------------------------------
Estimation of AOSS(s) - Placebo
----------------------------------------------------------------------
Estimate SE LB CI UB CI Switchers Stayers
AOSS -0.0011278 0.0025056 -0.0060387 0.0037831 2,349 7,764
----------------------------------------------------------------------
Estimation of WAOSS(s)
----------------------------------------------------------------------
Estimate SE LB CI UB CI Switchers Stayers
WAOSS 0.0057351 0.0015079 0.0027796 0.0086906 4,423 11,043
----------------------------------------------------------------------
Estimation of WAOSS(s) - Placebo
----------------------------------------------------------------------
Estimate SE LB CI UB CI Switchers Stayers
WAOSS -0.0000123 0.0022931 -0.0045067 0.0044821 2,349 7,764
141 switchers dropped (baseline treatment not in stayers' support)
22 stayers dropped (baseline treatment not in switchers' support)
Interpretation: The ATS (\(\approx 0.006\)) and WATS (\(\approx 0.006\)) are significant and positive. The placebo estimates are small and insignificant, supporting the parallel-trends assumption. These estimates are roughly half the magnitude of \(\widehat{AVSQ}^n_1 = 0.012\), which suggests a non-linear treatment response.
6.10 CQ#48: Joint Test of No Dynamic Effects
Restrict to counties whose treatment does not change between \(F_g\) and \(F_g + 1\), and test whether the first and second non-normalized effects are equal.
* ssc install did_multiplegt_dyn, replace
copy "https://raw.githubusercontent.com/anzonyquispe/did_book/main/cc_xd_didtextbook_2025_9_30/Data%20sets/Gentzkow%20et%20al%202011/gentzkowetal_didtextbook.dta" "gentzkowetal_didtextbook.dta", replace
use "gentzkowetal_didtextbook.dta", clear
did_multiplegt_dyn prestout cnty90 year numdailies if year<first_change | same_treat_after_first_change==1, effects(2) effects_equal(all) same_switchers graph_off--------------------------------------------------------------------------------
Estimation of treatment effects: Event-study effects
--------------------------------------------------------------------------------
| Estimate SE LB CI UB CI N Switchers
-------------+------------------------------------------------------------------
Effect_1 | .01525 .0058956 .0036949 .0268052 5019 512
Effect_2 | .0164091 .0073633 .0019773 .0308409 4101 512
--------------------------------------------------------------------------------
Test of joint nullity of the effects : p-value = .0291303
Test of equality of the effects : p-value = .83016664
--------------------------------------------------------------------------------
Average cumulative (total) effect per treatment unit
--------------------------------------------------------------------------------
| Estimate SE LB CI UB CI N Switch x Periods
-------------+--------------------------------------------------------------------------
Av_tot_eff | .0138306 .0053281 .0033878 .0242734 5605 1024
--------------------------------------------------------------------------------
Average number of time periods over which a treatment's effect is accumulated = 1.5
library(haven); library(DIDmultiplegtDYN)
load(url("https://raw.githubusercontent.com/anzonyquispe/did_book/main/cc_xd_didtextbook_2025_9_30/Data%20sets/Gentzkow%20et%20al%202011/gentzkowetal_didtextbook.RData"))
df_sub <- subset(df, year < first_change | same_treat_after_first_change == 1)
bonus <- did_multiplegt_dyn(
df = df_sub, outcome = "prestout", group = "cnty90",
time = "year", treatment = "numdailies",
effects = 2, effects_equal = TRUE,
same_switchers = TRUE, graph_off = TRUE)----------------------------------------------------------------------
Estimation of treatment effects: Event-study effects
----------------------------------------------------------------------
Estimate SE LB CI UB CI N Switchers
Effect_1 0.01525 0.00590 0.00369 0.02681 5,019 512
Effect_2 0.01641 0.00736 0.00198 0.03084 4,101 512
Test of joint nullity of the effects : p-value = 0.0291
Test of equality of the effects : p-value = 0.8302
----------------------------------------------------------------------
Average cumulative (total) effect per treatment unit
----------------------------------------------------------------------
Estimate SE LB CI UB CI N Switchers
0.01383 0.00533 0.00339 0.02427 5,605 1,024
Average number of time periods over which a treatment effect is accumulated: 1.5000
import pandas as pd
import polars as pl
from did_multiplegt_dyn import DidMultiplegtDyn
df = pd.read_parquet("https://raw.githubusercontent.com/anzonyquispe/did_book/main/cc_xd_didtextbook_2025_9_30/Data%20sets/Gentzkow%20et%20al%202011/gentzkowetal_didtextbook.parquet")
df_sub = df[(df['year'] < df['first_change']) | (df['same_treat_after_first_change'] == 1)].copy()
df_pl = pl.from_pandas(df_sub)
bonus = DidMultiplegtDyn(df=df_pl, outcome="prestout", group="cnty90",
time="year", treatment="numdailies", effects=2,
same_switchers=True, effects_equal=True, cluster="cnty90")
bonus.fit(); bonus.summary() Block Estimate SE LB CI UB CI N Switchers
Effect_1 0.015250 0.005896 0.003695 0.026805 5019.0 512.0
Effect_2 0.016409 0.007363 0.001977 0.030841 4101.0 512.0
Average_Total_Effect 0.013831 0.005328 0.003388 0.024273 5605.0 1024.0
Test of joint nullity of the effects: p-value = 0.029130
Test of equality of the effects: p-value = 0.830167
Interpretation: In the subsample of 512 counties whose treatment does not change between \(F_g\) and \(F_g + 1\), the first and second event-study effects are very close (0.015 vs 0.016) and not significantly different (\(p = 0.83\)). This is consistent with the joint null that the first lag of newspapers does not affect turnout and that treatment effects are constant over time.
6.11 Summary of Results
| GQ | Method | Key Finding |
|---|---|---|
| GQ1 | OLS | \(\Delta D\) and \(D_1\) are correlated (\(\beta = -0.0420\), \(p = 0.008\)) → possible OVB |
| GQ2 | OLS | Urbanization predicts \(\Delta D\) (\(\beta = -0.1422\), \(p < 0.001\)) → not as-good-as-random |
| GQ3 | Distributed-lag TWFE | Current effect NS (\(-0.0008\)), lag significant (\(0.0050\)); severe negative weights (\(\Sigma = -0.85\)) and cross-contamination (\(\Sigma = \pm 1.21\)) |
| GQ4 | did_multiplegt_dyn |
All effects positive & significant; placebos NS (joint \(p = 0.992\)) |
| GQ5 | design option |
Treatment paths increasingly heterogeneous as \(\ell\) grows |
| GQ6 | Normalized ES | Decreasing effects; cannot reject equality (\(p = 0.165\)); roughly equal lag weights |
| GQ7 | did_multiplegt_stat |
ATS ≈ WATS ≈ 0.006 (significant); placebos NS |
| Bonus | No-dynamics test | Effects 1 & 2 not different (\(p = 0.83\)) → consistent with no dynamic effects |