plt.rcParams.update(
{
"font.size": 10,
"axes.spines.top": False,
"axes.spines.right": False,
}
)
fig = plt.figure(figsize=(14, 9.2), constrained_layout=True)
grid = fig.add_gridspec(2, 2, width_ratios=[1.05, 1.15], height_ratios=[0.58, 0.42])
ax0 = fig.add_subplot(grid[0, 0])
ax0.imshow(W_sorted, aspect="auto", interpolation="nearest", cmap="Greys", vmin=0, vmax=1)
ax0.set_title("Adoption pattern: indirect democracy")
ax0.set_xlabel("Year")
ax0.set_ylabel("Municipalities sorted by first treatment")
ax0.set_xticks(np.arange(len(years)))
ax0.set_xticklabels(years, rotation=45, ha="right")
ax0.axhline(never_start - 0.5, color="tab:blue", lw=1.2, ls="--")
ax0.text(len(years) - 0.1, never_start + 15, "never treated", color="tab:blue", ha="right", va="top")
ax1 = fig.add_subplot(grid[1, 0])
ax1.bar(np.arange(len(cohort_counts)), cohort_counts.values, color="0.25")
ax1.set_title("First treatment cohort counts")
ax1.set_ylabel("municipalities")
ax1.set_xlabel("first treated year")
ax1.set_xticks(np.arange(len(cohort_counts)))
ax1.set_xticklabels(cohort_counts.index.astype(str), rotation=45, ha="right")
ax2 = fig.add_subplot(grid[0, 1])
for name, result in crabby_results.items():
mask = (
(result["event_time"] >= -8)
& (result["event_time"] <= 8)
& (result["n"] >= 10)
)
ax2.plot(
result["event_time"][mask],
result["estimate"][mask],
marker="o",
lw=2,
color=colors[name],
label=f"{name} (ATT={result['att']:.2f})",
)
finite = mask & np.isfinite(result["lower"]) & np.isfinite(result["upper"])
ax2.fill_between(
result["event_time"][finite],
result["lower"][finite],
result["upper"][finite],
color=colors[name],
alpha=0.08,
linewidth=0,
)
ax2.axhline(0, color="black", lw=1)
ax2.axvline(-0.5, color="0.4", lw=1, ls="--")
ax2.set_title("crabbymetrics panel estimators: weighted event-study summaries")
ax2.set_xlabel("Event time")
ax2.set_ylabel("Effect on nat_rate_ord")
ax2.grid(axis="y", alpha=0.25)
ax2.legend(frameon=False, fontsize=8, loc="upper left")
ax3 = fig.add_subplot(grid[1, 1])
for label, data, color in [
(f"Vanilla TWFE binned ES (ATT={twfe_att:.2f})", twfe_event, "tab:red"),
("Sun-Abraham / saturated", sa_event, "tab:purple"),
]:
plot_data = data[(data["event_time"] >= -8) & (data["event_time"] <= 8)].copy()
x = plot_data["event_time"].to_numpy(float)
y = plot_data["estimate"].to_numpy(float)
lo = plot_data["lower"].to_numpy(float)
hi = plot_data["upper"].to_numpy(float)
ax3.plot(x, y, marker="o", lw=2, color=color, label=label)
ax3.fill_between(x, lo, hi, color=color, alpha=0.12, linewidth=0)
ax3.axhline(0, color="black", lw=1)
ax3.axvline(-0.5, color="0.4", lw=1, ls="--")
ax3.set_title("pyfixest 2WFE flavors, clustered by municipality")
ax3.set_xlabel("Event time")
ax3.set_ylabel("Effect on nat_rate_ord")
ax3.grid(axis="y", alpha=0.25)
ax3.legend(frameon=False, fontsize=8, loc="upper left")
fig.suptitle(
"Hainmueller--Hangartner panel: staggered adoption, crabbymetrics, and pyfixest event studies",
fontsize=14,
fontweight="bold",
)
fig.text(
0.01,
0.005,
f"Full panel: {Y_all.shape[0]} municipalities x {Y_all.shape[1]} years. "
f"Estimator sample drops {int((first_idx_all == 0).sum())} baseline-treated 1991 units. "
"Vanilla TWFE bins relative time to [-8, 8]; reference event time is -1.",
fontsize=8,
color="0.35",
)
plt.show()