From 42bdd21ba5d398d074045fa1867f1d629ad3f32e Mon Sep 17 00:00:00 2001 From: igerber Date: Mon, 16 Feb 2026 20:49:37 -0500 Subject: [PATCH] Add LinkedIn carousel PDF and generator for v2.4 release 10-slide carousel featuring Two-Stage DiD (Gardner 2022) with teal accent color, LaTeX-rendered equations via matplotlib, and adapted layouts from the v2.3 carousel template. Co-Authored-By: Claude Opus 4.6 --- carousel/diff-diff-v24-carousel.pdf | Bin 0 -> 52220 bytes carousel/generate_v24_pdf.py | 954 ++++++++++++++++++++++++++++ 2 files changed, 954 insertions(+) create mode 100644 carousel/diff-diff-v24-carousel.pdf create mode 100644 carousel/generate_v24_pdf.py diff --git a/carousel/diff-diff-v24-carousel.pdf b/carousel/diff-diff-v24-carousel.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e664763611689c20914b163e9fd12761286a5898 GIT binary patch literal 52220 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

P1#7Zx1)$&OmVl7c=_$ai~sV2F|lNc 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�W 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 literal 0 HcmV?d00001 diff --git a/carousel/generate_v24_pdf.py b/carousel/generate_v24_pdf.py new file mode 100644 index 00000000..ff93a2e1 --- /dev/null +++ b/carousel/generate_v24_pdf.py @@ -0,0 +1,954 @@ +#!/usr/bin/env python3 +"""Generate LinkedIn carousel PDF for diff-diff v2.4 release.""" + +import math +import os +import tempfile +from pathlib import Path + +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt # noqa: E402 +from PIL import Image as PILImage # noqa: E402 + +from fpdf import FPDF # noqa: E402 + +# Use Computer Modern math font (LaTeX-like) +plt.rcParams["mathtext.fontset"] = "cm" + +# LinkedIn carousel dimensions (4:5 aspect ratio) +WIDTH = 270 # mm +HEIGHT = 337.5 # mm + +# Colors - Light theme with teal accent +MID_BLUE = (59, 130, 246) # #3b82f6 +NAVY = (15, 23, 42) # #0f172a +WHITE = (255, 255, 255) +RED = (220, 38, 38) # #dc2626 +GREEN = (22, 163, 74) # #16a34a +GRAY = (100, 116, 139) # #64748b +LIGHT_GRAY = (148, 163, 184) # #94a3b8 +TEAL = (8, 145, 178) # #0891b2 - v2.4 accent +DARK_SLATE = (30, 41, 59) # #1e293b - code block bg + +# Navy as hex for matplotlib +NAVY_HEX = "#0f172a" + + +class CarouselV24PDF(FPDF): + def __init__(self): + super().__init__(orientation="P", unit="mm", format=(WIDTH, HEIGHT)) + self.set_auto_page_break(False) + self._temp_files = [] + + def cleanup(self): + """Remove temporary equation image files.""" + for f in self._temp_files: + try: + os.unlink(f) + except OSError: + pass + + # ── Equation Rendering ──────────────────────────────────────────── + + def _render_equations(self, latex_lines, fontsize=26): + """Render one or more LaTeX equations to a single PNG image. + + Args: + latex_lines: list of LaTeX math strings (each wrapped in $...$) + fontsize: matplotlib font size + + Returns: + (path, pixel_width, pixel_height) + """ + n = len(latex_lines) + fig_h = max(0.7, 0.55 * n + 0.15) + fig = plt.figure(figsize=(10, fig_h)) + + for i, line in enumerate(latex_lines): + y_frac = 1.0 - (2 * i + 1) / (2 * n) + fig.text( + 0.5, y_frac, line, + fontsize=fontsize, ha="center", va="center", + color=NAVY_HEX, + ) + + fig.patch.set_alpha(0) + fd, path = tempfile.mkstemp(suffix=".png") + os.close(fd) + fig.savefig(path, dpi=250, bbox_inches="tight", pad_inches=0.06, + transparent=True) + plt.close(fig) + + with PILImage.open(path) as img: + pw, ph = img.size + + self._temp_files.append(path) + return path, pw, ph + + def _place_equation(self, path, pw, ph, box_x, _box_y, box_w, + content_top, content_bottom): + """Place an equation image centered in a region of a box.""" + max_w = box_w * 0.82 + aspect = ph / pw + display_w = max_w + display_h = display_w * aspect + + # Shrink if too tall for the available space + avail_h = content_bottom - content_top + if display_h > avail_h: + display_h = avail_h + display_w = display_h / aspect + + eq_x = box_x + (box_w - display_w) / 2 + eq_y = content_top + (avail_h - display_h) / 2 + self.image(path, eq_x, eq_y, display_w) + + # ── Helper Methods ──────────────────────────────────────────────── + + def add_connector_graphic(self, position="right"): + """Add decorative connector graphic to bottom corner.""" + if position == "right": + cx = WIDTH + 20 + cy = HEIGHT - 40 + else: + cx = -20 + cy = HEIGHT - 40 + + self.set_draw_color(*MID_BLUE) + for i, radius in enumerate([60, 80, 100]): + self.set_line_width(2.5 - i * 0.5) + segments = 30 + if position == "right": + start_angle = math.pi * 0.5 + end_angle = math.pi * 1.0 + else: + start_angle = 0 + end_angle = math.pi * 0.5 + + for j in range(segments): + t1 = start_angle + (end_angle - start_angle) * j / segments + t2 = start_angle + (end_angle - start_angle) * (j + 1) / segments + x1 = cx + radius * math.cos(t1) + y1 = cy + radius * math.sin(t1) + x2 = cx + radius * math.cos(t2) + y2 = cy + radius * math.sin(t2) + self.line(x1, y1, x2, y2) + + self.set_fill_color(*MID_BLUE) + if position == "right": + dot_positions = [(35, HEIGHT - 60), (50, HEIGHT - 45), (30, HEIGHT - 35)] + else: + dot_positions = [ + (WIDTH - 35, HEIGHT - 60), + (WIDTH - 50, HEIGHT - 45), + (WIDTH - 30, HEIGHT - 35), + ] + for i, (dx, dy) in enumerate(dot_positions): + dot_radius = 3 - i * 0.5 + self.ellipse( + dx - dot_radius, dy - dot_radius, dot_radius * 2, dot_radius * 2, "F" + ) + + def light_gradient_background(self): + """Draw light gradient background (top #e1f0ff fading to white).""" + steps = 50 + for i in range(steps): + ratio = i / steps + r = int(225 + (255 - 225) * ratio) + g = int(240 + (255 - 240) * ratio) + b = 255 + self.set_fill_color(r, g, b) + y = i * HEIGHT / steps + self.rect(0, y, WIDTH, HEIGHT / steps + 1, "F") + + def add_footer(self): + """Add footer with logo.""" + self.set_xy(0, HEIGHT - 25) + self.set_font("Helvetica", "B", 14) + self.set_text_color(*GRAY) + self.cell(WIDTH, 10, "diff-diff", align="C") + + def centered_text(self, y, text, size=28, bold=True, color=NAVY): + """Add centered text.""" + self.set_xy(0, y) + self.set_font("Helvetica", "B" if bold else "", size) + self.set_text_color(*color) + self.cell(WIDTH, size * 0.5, text, align="C") + + def add_list_item(self, y, icon, text, icon_color, text_size=22): + """Add a list item with icon.""" + margin = 50 + self.set_xy(margin, y) + self.set_font("Helvetica", "B", text_size + 2) + self.set_text_color(*icon_color) + self.cell(25, 12, icon, align="C") + self.set_text_color(*NAVY) + self.set_font("Helvetica", "", text_size) + self.cell(WIDTH - margin * 2 - 25, 12, text) + + def draw_split_logo(self, y, size=18): + """Draw the split-color diff-diff logo.""" + self.set_xy(0, y) + self.set_font("Helvetica", "B", size) + self.set_text_color(*NAVY) + self.cell(WIDTH / 2 - 5, 10, "diff", align="R") + self.set_text_color(*MID_BLUE) + self.cell(10, 10, "-", align="C") + self.set_text_color(*NAVY) + self.cell(WIDTH / 2 - 5, 10, "diff", align="L") + + # ── Slide 1: Hook ───────────────────────────────────────────────── + + def slide_hook(self): + """Slide 1: diff-diff v2.4 hook.""" + self.add_page() + self.light_gradient_background() + + self.draw_split_logo(55, size=60) + self.centered_text(120, "v2.4", size=50, color=TEAL) + + self.centered_text(170, "Your variance estimator", size=26) + self.centered_text(193, "is lying to you.", size=26) + + teasers = [ + "Gardner (2022) Two-Stage DiD", + "GMM sandwich variance that tells the truth", + "Per-observation treatment effects", + ] + y_start = 230 + for i, teaser in enumerate(teasers): + self.set_xy(0, y_start + i * 22) + self.set_font("Helvetica", "", 17) + self.set_text_color(*GRAY) + self.cell(WIDTH, 10, teaser, align="C") + + self.add_footer() + + # ── Slide 2: Recap ──────────────────────────────────────────────── + + def slide_recap(self): + """Slide 2: Quick catch-up on what diff-diff is.""" + self.add_page() + self.light_gradient_background() + + self.centered_text(40, "What is", size=38) + self.centered_text(73, "diff-diff?", size=38, color=MID_BLUE) + + items = [ + "Complete DiD toolkit for Python", + "sklearn-like API, statsmodels-style output", + "10 methods, 12 tutorials, validated vs R", + "The most complete DiD toolkit in any language", + ] + y_start = 130 + for i, item in enumerate(items): + self.add_list_item(y_start + i * 35, "+", item, GREEN, text_size=21) + + self.centered_text( + 285, "Now with the Two-Stage DiD estimator.", + size=16, bold=False, color=GRAY, + ) + self.add_footer() + + # ── Slide 3: The TWFE Problem ───────────────────────────────────── + + def slide_twfe_problem(self): + """Slide 3: The TWFE problem — treated outcomes contaminate counterfactual.""" + self.add_page() + self.light_gradient_background() + + self.centered_text(30, "The TWFE", size=38) + self.centered_text(63, "Problem", size=38, color=RED) + + # Panel data grid: 4 units x 6 periods showing staggered treatment + margin = 40 + grid_y = 100 + n_units = 4 + n_periods = 6 + cell_w = (WIDTH - margin * 2) / n_periods + cell_h = 18 # compact cells + + # Period labels + self.set_font("Helvetica", "B", 11) + self.set_text_color(*GRAY) + for p in range(n_periods): + self.set_xy(margin + p * cell_w, grid_y - 12) + self.cell(cell_w, 10, f"t={p + 1}", align="C") + + # Unit labels + for u in range(n_units): + self.set_xy(margin - 28, grid_y + u * cell_h + 2) + self.set_text_color(*NAVY) + self.set_font("Helvetica", "", 10) + self.cell(26, 10, f"Unit {u + 1}", align="R") + + # Treatment onset: unit 1 at t=3, unit 2 at t=4, units 3-4 never treated + treat_onset = {0: 3, 1: 4, 2: None, 3: None} + + for u in range(n_units): + for p in range(n_periods): + x = margin + p * cell_w + y = grid_y + u * cell_h + onset = treat_onset[u] + is_treated = onset is not None and (p + 1) >= onset + + if is_treated: + self.set_fill_color(254, 202, 202) + self.set_draw_color(220, 38, 38) + else: + self.set_fill_color(219, 234, 254) + self.set_draw_color(147, 197, 253) + + self.set_line_width(0.5) + self.rect(x, y, cell_w, cell_h, "DF") + + grid_bottom = grid_y + n_units * cell_h + + # Diagram label + self.set_xy(margin, grid_bottom + 4) + self.set_font("Helvetica", "I", 12) + self.set_text_color(*GRAY) + self.cell( + WIDTH - margin * 2, 10, + "TWFE estimates FEs using ALL data, including treated outcomes", + align="C", + ) + + # Legend + legend_y = grid_bottom + 18 + legend_x = margin + 25 + self.set_fill_color(219, 234, 254) + self.rect(legend_x, legend_y, 10, 8, "F") + self.set_xy(legend_x + 13, legend_y - 1) + self.set_font("Helvetica", "", 11) + self.set_text_color(*NAVY) + self.cell(45, 10, "Untreated") + self.set_fill_color(254, 202, 202) + self.rect(legend_x + 75, legend_y, 10, 8, "F") + self.set_xy(legend_x + 88, legend_y - 1) + self.cell(45, 10, "Treated") + + # Explanation + explain_y = legend_y + 18 + self.centered_text( + explain_y, + "Treated outcomes contaminate the counterfactual", + size=16, bold=True, color=NAVY, + ) + self.centered_text( + explain_y + 18, + "Heterogeneous effects create negative weights", + size=14, bold=False, color=GRAY, + ) + + # Callout box + callout_y = explain_y + 40 + callout_margin = 40 + callout_w = WIDTH - callout_margin * 2 + self.set_fill_color(240, 253, 250) + self.set_draw_color(*TEAL) + self.set_line_width(0.8) + self.rect(callout_margin, callout_y, callout_w, 26, "DF") + self.set_xy(callout_margin, callout_y + 6) + self.set_font("Helvetica", "B", 15) + self.set_text_color(*TEAL) + self.cell( + callout_w, 12, + "Solution: estimate the model on untreated data only.", + align="C", + ) + + self.add_footer() + + # ── Slide 4: Two-Stage Procedure ────────────────────────────────── + + def slide_two_stage_intro(self): + """Slide 4: Two-Stage DiD procedure with 2 numbered step boxes.""" + self.add_page() + self.light_gradient_background() + + self.centered_text(30, "Two-Stage DiD", size=36, color=NAVY) + + # Citation block + self.set_xy(0, 65) + self.set_font("Helvetica", "I", 15) + self.set_text_color(*GRAY) + self.cell( + WIDTH, 8, + "Gardner (2022) | Butts & Gardner (R Journal, 2022)", + align="C", + ) + + # Two numbered step boxes + margin = 35 + box_width = WIDTH - margin * 2 + box_height = 42 + circle_r = 14 + step_y_start = 95 + total_step_unit = 65 # box_height + gap between boxes + + steps = [ + "Estimate unit + time FEs on untreated observations only", + "Residualize ALL outcomes, regress on treatment", + ] + + for i, step_text in enumerate(steps): + y = step_y_start + i * total_step_unit + + # Step number circle + circle_x = margin + circle_r + circle_y = y + box_height / 2 + + self.set_fill_color(*TEAL) + self.ellipse( + circle_x - circle_r, circle_y - circle_r, + circle_r * 2, circle_r * 2, "F", + ) + self.set_xy(circle_x - circle_r, circle_y - 6) + self.set_font("Helvetica", "B", 18) + self.set_text_color(*WHITE) + self.cell(circle_r * 2, 12, str(i + 1), align="C") + + # Step text box + text_x = margin + circle_r * 2 + 10 + text_width = box_width - circle_r * 2 - 10 + + self.set_fill_color(*WHITE) + self.set_draw_color(*TEAL) + self.set_line_width(0.8) + self.rect(text_x, y, text_width, box_height, "DF") + + self.set_font("Helvetica", "", 15) + self.set_text_color(*NAVY) + self.set_xy(text_x + 10, y + (box_height - 10) / 2) + self.cell(text_width - 20, 10, step_text) + + # Downward arrow between boxes + arrow_x = margin + circle_r + arrow_top = step_y_start + box_height + 2 + arrow_bottom = step_y_start + total_step_unit - 2 + self.set_draw_color(*TEAL) + self.set_line_width(1.2) + self.line(arrow_x, arrow_top, arrow_x, arrow_bottom) + # Arrowhead + head_size = 5 + self.line(arrow_x - head_size, arrow_bottom - head_size, arrow_x, arrow_bottom) + self.line(arrow_x + head_size, arrow_bottom - head_size, arrow_x, arrow_bottom) + + # Footer text + footer_y = step_y_start + 2 * total_step_unit + 8 + self.centered_text( + footer_y, + "Clean counterfactual from untreated data.", + size=15, bold=False, color=GRAY, + ) + self.centered_text( + footer_y + 18, + "Unbiased treatment effects from the residuals.", + size=15, bold=False, color=GRAY, + ) + + # Caveat + self.centered_text( + footer_y + 45, + "Requires parallel trends + no anticipation + absorbing treatment.", + size=11, bold=False, color=LIGHT_GRAY, + ) + + self.add_footer() + + # ── Slide 5: The Math ───────────────────────────────────────────── + + def slide_math(self): + """Slide 5: Three equation boxes with LaTeX-rendered equations.""" + self.add_page() + self.light_gradient_background() + + self.centered_text(30, "The Math", size=38) + self.centered_text(63, "Two stages, three equations", size=18, bold=False, color=GRAY) + + margin = 30 + box_w = WIDTH - margin * 2 + badge_w = 65 + badge_h = 18 + + # Pre-render all equations + eq1_path, eq1_pw, eq1_ph = self._render_equations( + [r"$Y_{it} = \alpha_i + \delta_t + \varepsilon_{it}$"] + ) + eq2_path, eq2_pw, eq2_ph = self._render_equations([ + r"$\tilde{Y}_{it} = Y_{it} - \hat{\alpha}_i - \hat{\delta}_t$", + r"$\tilde{Y}_{it} = \tau \cdot D_{it} + u_{it}$", + ]) + eq3_path, eq3_pw, eq3_ph = self._render_equations( + [r"$V = (D^\prime\! D)^{-1}\left[\sum_c S_c\, S_c^\prime\right](D^\prime\! D)^{-1}$"], + fontsize=24, + ) + + # Box definitions: (badge_label, eq_path/pw/ph, annotation, box_height) + boxes = [ + { + "badge": "Stage 1", + "eq": (eq1_path, eq1_pw, eq1_ph), + "annotation": "(on D_it = 0 only)", + "height": 48, + }, + { + "badge": "Stage 2", + "eq": (eq2_path, eq2_pw, eq2_ph), + "annotation": "(on ALL observations)", + "height": 62, + }, + { + "badge": "Variance", + "eq": (eq3_path, eq3_pw, eq3_ph), + "annotation": None, + "height": 48, + }, + ] + + y_cursor = 85 + box_gap = 8 + + for box in boxes: + bh = box["height"] + + # White box with teal border + self.set_fill_color(*WHITE) + self.set_draw_color(*TEAL) + self.set_line_width(0.8) + self.rect(margin, y_cursor, box_w, bh, "DF") + + # Teal badge overlapping top edge + badge_x = margin + 8 + badge_y = y_cursor - badge_h / 2 + self.set_fill_color(*TEAL) + self.rect(badge_x, badge_y, badge_w, badge_h, "F") + self.set_xy(badge_x, badge_y + 3) + self.set_font("Helvetica", "B", 11) + self.set_text_color(*WHITE) + self.cell(badge_w, 12, box["badge"], align="C") + + # Determine content region (between badge and annotation) + content_top = y_cursor + badge_h / 2 + 2 + if box["annotation"]: + ann_y = y_cursor + bh - 14 + content_bottom = ann_y - 2 + else: + content_bottom = y_cursor + bh - 6 + + # Place equation image centered + eq_path, eq_pw, eq_ph = box["eq"] + self._place_equation( + eq_path, eq_pw, eq_ph, + margin, y_cursor, box_w, + content_top, content_bottom, + ) + + # Annotation text + if box["annotation"]: + self.set_xy(margin, ann_y) + self.set_font("Helvetica", "I", 12) + self.set_text_color(*GRAY) + self.cell(box_w, 10, box["annotation"], align="C") + + y_cursor += bh + box_gap + + # GMM annotation below all boxes + self.centered_text( + y_cursor + 2, + "GMM sandwich corrects for Stage 1 uncertainty", + size=14, bold=True, color=TEAL, + ) + + self.add_footer() + + # ── Slide 6: Honest Standard Errors ─────────────────────────────── + + def slide_honest_se(self): + """Slide 6: Three CI bars comparing TWFE, GMM, and naive OLS.""" + self.add_page() + self.light_gradient_background() + + self.centered_text(30, "Honest", size=38) + self.centered_text(63, "Standard Errors", size=38, color=TEAL) + + self.centered_text( + 100, "Not all confidence intervals tell the truth", + size=16, bold=False, color=GRAY, + ) + + center_x = WIDTH / 2 + bar_height = 22 + bar_y_start = 122 + bar_gap = 14 + + # Vertical dashed center line + self.set_draw_color(*NAVY) + self.set_line_width(0.5) + dash_len = 4 + gap_len = 3 + line_top = bar_y_start - 6 + line_bottom = bar_y_start + 3 * (bar_height + bar_gap) - bar_gap + 6 + for y_pos in range(int(line_top), int(line_bottom), dash_len + gap_len): + self.line(center_x, y_pos, center_x, + min(y_pos + dash_len, line_bottom)) + + dot_r = 4 + bars = [ + {"half_width": 95, "color": RED, "label": "Naive TWFE", + "annotation": "biased", "marker": None}, + {"half_width": 65, "color": TEAL, "label": "GMM Sandwich", + "annotation": "correct coverage", "marker": "check"}, + {"half_width": 40, "color": GRAY, "label": "Naive Stage 2 OLS", + "annotation": "false precision", "marker": "x"}, + ] + + for i, bar in enumerate(bars): + y = bar_y_start + i * (bar_height + bar_gap) + hw = bar["half_width"] + + # Bar + self.set_fill_color(*bar["color"]) + self.rect(center_x - hw, y, hw * 2, bar_height, "F") + + # Point estimate dot + self.set_fill_color(*WHITE) + self.ellipse( + center_x - dot_r, y + bar_height / 2 - dot_r, + dot_r * 2, dot_r * 2, "F", + ) + + # Marker to the right of the bar + if bar["marker"] == "check": + mark_x = center_x + hw + 6 + self.set_xy(mark_x, y + 2) + self.set_font("Helvetica", "B", 16) + self.set_text_color(*GREEN) + self.cell(15, 14, "OK") + elif bar["marker"] == "x": + mark_x = center_x + hw + 6 + self.set_xy(mark_x, y + 2) + self.set_font("Helvetica", "B", 16) + self.set_text_color(*RED) + self.cell(15, 14, "X") + + # Label to the left + self.set_xy(0, y + 3) + self.set_font("Helvetica", "B", 12) + self.set_text_color(*bar["color"]) + self.cell(center_x - hw - 6, 14, bar["label"], align="R") + + # Annotation to the right (after marker) + ann_x = center_x + hw + (23 if bar["marker"] else 6) + self.set_xy(ann_x, y + 3) + self.set_font("Helvetica", "I", 11) + self.set_text_color(*bar["color"]) + self.cell(60, 14, bar["annotation"]) + + # Notes + note_y = bar_y_start + 3 * (bar_height + bar_gap) + 2 + self.centered_text( + note_y, + "Naive OLS ignores that alpha-hat and delta-hat are estimated.", + size=13, bold=False, color=GRAY, + ) + self.centered_text( + note_y + 15, + "The GMM correction accounts for first-stage uncertainty.", + size=13, bold=False, color=GRAY, + ) + + # Callout + callout_y = note_y + 34 + callout_margin = 50 + callout_w = WIDTH - callout_margin * 2 + self.set_fill_color(240, 253, 250) + self.set_draw_color(*TEAL) + self.set_line_width(0.8) + self.rect(callout_margin, callout_y, callout_w, 24, "DF") + self.set_xy(callout_margin, callout_y + 5) + self.set_font("Helvetica", "B", 14) + self.set_text_color(*TEAL) + self.cell( + callout_w, 12, + "Narrower isn't better if it's wrong. GMM gets it right.", + align="C", + ) + + # Caveat (single line) + self.centered_text( + callout_y + 30, + "Under homogeneous effects. Compare with ImputationDiD for robustness.", + size=10, bold=False, color=LIGHT_GRAY, + ) + + self.add_footer() + + # ── Slide 7: Per-Observation Treatment Effects ──────────────────── + + def slide_per_obs_effects(self): + """Slide 7: DataFrame-style table showing per-observation effects.""" + self.add_page() + self.light_gradient_background() + + self.centered_text(30, "Per-Observation", size=36) + self.centered_text(63, "Treatment Effects", size=36, color=TEAL) + + self.centered_text( + 98, "Every treated unit-period gets its own tau-hat", + size=16, bold=False, color=GRAY, + ) + + # DataFrame-style table + margin = 40 + table_x = margin + table_y = 118 + table_width = WIDTH - margin * 2 + row_height = 22 + n_cols = 4 + col_width = table_width / n_cols + + headers = ["unit", "time", "tau_hat", "weight"] + data_rows = [ + ["firm_3", "2019", "2.14", "0.0033"], + ["firm_3", "2020", "1.87", "0.0033"], + ["firm_7", "2020", "3.21", "0.0033"], + ["firm_7", "2021", "2.95", "0.0033"], + ["...", "...", "...", "..."], + ] + + # Header row + self.set_fill_color(*DARK_SLATE) + self.rect(table_x, table_y, table_width, row_height, "F") + self.set_font("Courier", "B", 13) + self.set_text_color(*WHITE) + for c, header in enumerate(headers): + self.set_xy(table_x + c * col_width, table_y + 4) + self.cell(col_width, 12, header, align="C") + + # Data rows + for r, row_data in enumerate(data_rows): + y = table_y + (r + 1) * row_height + + if r % 2 == 0: + self.set_fill_color(245, 250, 255) + else: + self.set_fill_color(*WHITE) + self.rect(table_x, y, table_width, row_height, "F") + + self.set_draw_color(220, 230, 240) + self.set_line_width(0.3) + self.rect(table_x, y, table_width, row_height, "D") + + self.set_font("Courier", "", 12) + self.set_text_color(*NAVY) + for c, val in enumerate(row_data): + self.set_xy(table_x + c * col_width, y + 4) + self.cell(col_width, 12, val, align="C") + + # Notes below table + table_bottom = table_y + (len(data_rows) + 1) * row_height + note_y = table_bottom + 10 + self.centered_text( + note_y, + "Aggregate to: static ATT, event study, or by cohort", + size=16, bold=True, color=NAVY, + ) + self.centered_text( + note_y + 20, + "Or analyze individual treatment effect heterogeneity", + size=15, bold=False, color=GRAY, + ) + + self.add_footer() + + # ── Slide 8: Code Example ───────────────────────────────────────── + + def slide_code(self): + """Slide 8: Drop-in replacement code example.""" + self.add_page() + self.light_gradient_background() + + self.centered_text(30, "Drop-in", size=36) + self.centered_text(60, "Replacement", size=36, color=MID_BLUE) + + margin = 30 + code_y = 95 + code_lines = [ + ("# Switch in one line", 1.0), + ("from diff_diff import TwoStageDiD", 1.0), + ("", 0.5), + ("est = TwoStageDiD()", 1.0), + ("results = est.fit(", 1.0), + (" data,", 1.0), + (" outcome='sales',", 1.0), + (" unit='firm_id',", 1.0), + (" time='year',", 1.0), + (" first_treat='first_treat',", 1.0), + (" aggregate='event_study'", 1.0), + (")", 1.0), + ("", 0.5), + ("# Per-observation effects", 1.0), + ("results.treatment_effects.head()", 1.0), + ] + line_height = 11 + total_lines = sum(h for _, h in code_lines) + code_height = total_lines * line_height + 20 + + self.set_fill_color(*DARK_SLATE) + self.rect(margin, code_y, WIDTH - margin * 2, code_height, "F") + + self.set_font("Courier", "", 13) + self.set_text_color(*WHITE) + cumulative_y = 0.0 + for line_text, height_mult in code_lines: + self.set_xy(margin + 15, code_y + 10 + cumulative_y) + self.cell(0, 10, line_text) + cumulative_y += line_height * height_mult + + subtitle_y = code_y + code_height + 12 + self.centered_text( + subtitle_y, + "Same fit() API as CallawaySantAnna and ImputationDiD.", + size=15, bold=False, color=GRAY, + ) + self.centered_text( + subtitle_y + 17, + "Identical point estimates to ImputationDiD.", + size=15, bold=False, color=GRAY, + ) + self.add_footer() + + # ── Slide 9: Full Toolkit (5x2 Grid) ───────────────────────────── + + def slide_full_toolkit(self): + """Slide 9: 10 methods, one library (5x2 grid).""" + self.add_page() + self.light_gradient_background() + + self.centered_text(20, "Every Method", size=36) + self.centered_text(50, "You Need", size=36, color=MID_BLUE) + + margin = 25 + box_width = (WIDTH - margin * 3) / 2 + box_height = 34 + gap_y = 3 + y_start = 80 + + methods = [ + ("Basic DiD / TWFE", "Classic 2x2 and panel", False), + ("Callaway-Sant'Anna", "Staggered adoption (2021)", False), + ("Sun-Abraham", "Interaction-weighted (2021)", False), + ("Imputation DiD", "Borusyak et al. (2024)", False), + ("Two-Stage DiD", "Gardner (2022)", True), + ("Synthetic DiD", "Arkhangelsky et al. (2021)", False), + ("Triple Difference", "DDD with proper covariates", False), + ("TROP", "Factor-adjusted DiD (2025)", False), + ("Honest DiD", "Rambachan-Roth sensitivity", False), + ("Bacon Decomposition", "TWFE diagnostic weights", False), + ] + + for i, (title, desc, is_new) in enumerate(methods): + col = i % 2 + row = i // 2 + x = margin + col * (box_width + margin) + y = y_start + row * (box_height + gap_y) + + self.set_fill_color(*WHITE) + if is_new: + self.set_draw_color(*TEAL) + self.set_line_width(1.2) + else: + self.set_draw_color(*MID_BLUE) + self.set_line_width(0.8) + self.rect(x, y, box_width, box_height, "DF") + + # Title + self.set_xy(x + 5, y + 3) + self.set_font("Helvetica", "B", 14) + self.set_text_color(*TEAL if is_new else MID_BLUE) + display_title = title + " [NEW]" if is_new else title + self.cell(box_width - 10, 10, display_title, align="C") + + # Description + self.set_xy(x + 5, y + 19) + self.set_font("Helvetica", "", 11) + self.set_text_color(*GRAY) + self.cell(box_width - 10, 10, desc, align="C") + + # Subtitle below grid + grid_bottom = y_start + 5 * (box_height + gap_y) + self.centered_text( + grid_bottom + 2, + "The most complete DiD toolkit in any language.", + size=15, bold=False, color=GRAY, + ) + self.add_footer() + + # ── Slide 10: CTA ───────────────────────────────────────────────── + + def slide_cta(self): + """Slide 10: Upgrade today.""" + self.add_page() + self.light_gradient_background() + + self.centered_text(45, "Upgrade to", size=38) + self.centered_text(78, "v2.4", size=38, color=TEAL) + + box_width = 195 + box_x = (WIDTH - box_width) / 2 + box_y = 125 + box_h = 36 + self.set_fill_color(*MID_BLUE) + self.rect(box_x, box_y, box_width, box_h, "F") + + self.set_xy(box_x, box_y + 10) + self.set_font("Courier", "B", 15) + self.set_text_color(*WHITE) + self.cell(box_width, 14, "$ pip install --upgrade diff-diff", align="C") + + self.centered_text(200, "github.com/igerber/diff-diff", size=20, color=MID_BLUE) + + self.centered_text( + 232, "Full documentation & 12 tutorials included", + size=16, bold=False, color=GRAY, + ) + self.centered_text( + 252, "MIT Licensed | Open Source", + size=16, bold=False, color=GRAY, + ) + + self.draw_split_logo(278, size=28) + + self.centered_text( + 298, "Difference-in-Differences for Python", + size=14, bold=False, color=GRAY, + ) + + + +def main(): + pdf = CarouselV24PDF() + + pdf.slide_hook() + pdf.slide_recap() + pdf.slide_twfe_problem() + pdf.slide_two_stage_intro() + pdf.slide_math() + pdf.slide_honest_se() + pdf.slide_per_obs_effects() + pdf.slide_code() + pdf.slide_full_toolkit() + pdf.slide_cta() + + output_path = Path(__file__).parent / "diff-diff-v24-carousel.pdf" + pdf.output(str(output_path)) + print(f"PDF saved to: {output_path}") + + pdf.cleanup() + + +if __name__ == "__main__": + main()