<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>mlsynth | Carlos Mendez</title><link>https://carlos-mendez.org/tag/mlsynth/</link><atom:link href="https://carlos-mendez.org/tag/mlsynth/index.xml" rel="self" type="application/rss+xml"/><description>mlsynth</description><generator>Wowchemy (https://wowchemy.com)</generator><language>en-us</language><copyright>© 2018–2026 Carlos Mendez. All rights reserved.</copyright><lastBuildDate>Tue, 09 Jun 2026 00:00:00 +0000</lastBuildDate><image><url>https://carlos-mendez.org/media/icon_huedfae549300b4ca5d201a9bd09a3ecd5_79625_512x512_fill_lanczos_center_3.png</url><title>mlsynth</title><link>https://carlos-mendez.org/tag/mlsynth/</link></image><item><title>Bouncing Back Better? Evaluating the Economic Impact of the Aceh Tsunami</title><link>https://carlos-mendez.org/post/python_did_sc_tsunami/</link><pubDate>Tue, 09 Jun 2026 00:00:00 +0000</pubDate><guid>https://carlos-mendez.org/post/python_did_sc_tsunami/</guid><description>&lt;h2 id="abstract">Abstract&lt;/h2>
&lt;p>Localized natural disasters destroy capital and lives yet can attract reconstruction aid large enough to rebuild a region &amp;ldquo;better than before,&amp;rdquo; leaving the net long-run effect genuinely ambiguous. This tutorial asks whether the Indonesian province of Aceh — which lost roughly 130,000 people to the 2004 Indian Ocean tsunami but then received the largest developing-world reconstruction effort ever, about USD 7.7 billion committed and USD 7.0 billion spent — ended up on a higher or lower growth path a decade later, and how that effect can be credibly measured. It replicates Heger &amp;amp; Neumayer (2019) on synthetic calibrated data spanning a district panel of 125 Sumatran districts observed annually over 1999–2012 (1,750 rows, 10 flooded Aceh districts treated) and a finer panel of 276 Aceh sub-districts with satellite night-lights. Treating coastal inundation as a quasi-natural experiment, it estimates a dynamic four-period difference-in-differences with pyfixest, an event study with diff-diff, a continuous night-lights dose-response, a synthetic control with mlsynth, and Conley spatial-HAC standard errors validated by Moran&amp;rsquo;s I. Flooded districts lost 7.9% of output in 2005 (−0.0792, p &amp;lt; 0.01) but grew 6.3 percentage points per year faster during 2006–08 (+0.0628, p &amp;lt; 0.05), and synthetic control places flooded Aceh +18.3% above its no-tsunami counterfactual by 2012; the night-lights rebound (+0.0160, p &amp;lt; 0.001) concentrates in the worst-hit quintile, while a neighbour placebo finds nothing. The case shows that well-governed mega-reconstruction can leave a poor region on a permanently higher trajectory — provided inference accounts for spatially clustered treatment, which roughly doubles the recovery effect&amp;rsquo;s standard error (0.0146 to 0.0244) and downgrades it from a spuriously confident 1% to an honest 5% significance.&lt;/p>
&lt;h2 id="1-overview">1. Overview&lt;/h2>
&lt;p>On 26 December 2004, a magnitude-9.1 earthquake off the coast of Sumatra sent a tsunami across the Indian Ocean. The Indonesian province of &lt;strong>Aceh&lt;/strong> bore the worst of it: roughly &lt;strong>130,000 people died&lt;/strong>, the wave reached up to &lt;strong>9 km inland&lt;/strong>, and about a third of the coastline was flooded. Then something unusual happened. Aceh received the single largest reconstruction effort ever directed at a developing-world disaster — about &lt;strong>USD 7.7 billion&lt;/strong> committed, &lt;strong>USD 7.0 billion&lt;/strong> actually spent — under a well-coordinated agency with low corruption.&lt;/p>
&lt;p>So here is a genuinely hard question: &lt;strong>a decade later, was Aceh richer or poorer than it would have been without the tsunami?&lt;/strong> Catastrophe destroys capital and lives; massive, well-spent aid rebuilds — &lt;em>better than before&lt;/em>, sometimes. Which force won? And — the part this tutorial really cares about — &lt;strong>how could you ever measure that credibly&lt;/strong>, when you only get to observe the world where the tsunami &lt;em>did&lt;/em> happen?&lt;/p>
&lt;p>This post is a hands-on answer. We treat the tsunami as a &lt;strong>natural experiment&lt;/strong>: the wave flooded some districts and spared others for reasons of coastal geography that have nothing to do with their economic prospects. Comparing the flooded &amp;ldquo;treated&amp;rdquo; districts to the un-flooded &amp;ldquo;control&amp;rdquo; districts — before and after 2004 — lets us isolate the disaster-plus-reconstruction effect. We will measure it four different ways, each answering a slightly sharper version of the question, and we will be honest about uncertainty when the treated places all sit in one corner of the map.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>A note on the data (please read this).&lt;/strong> This tutorial is &lt;em>inspired by and based on&lt;/em> the study by &lt;strong>Heger &amp;amp; Neumayer (2019)&lt;/strong>, but it runs on &lt;strong>synthetic data created for teaching&lt;/strong>. The paper&amp;rsquo;s real inputs (World Bank GDP, satellite night-lights, tsunami inundation maps) are licensed or confidential. Our dataset is &lt;em>calibrated&lt;/em> so that re-running the paper&amp;rsquo;s analyses reproduces its &lt;strong>findings&lt;/strong> — the signs, the statistical significance, and the &lt;em>approximate&lt;/em> magnitudes of the key coefficients. The direction and significance of most results match the paper closely; &lt;strong>the magnitudes can differ slightly&lt;/strong> (we tabulate exactly how in &lt;a href="#11-reproduction-audit-synthetic-data-vs-the-paper">Section 11&lt;/a>). Use this to learn the &lt;em>methods&lt;/em>, not to draw new conclusions about Aceh.&lt;/p>
&lt;/blockquote>
&lt;h3 id="11-learning-objectives">1.1 Learning objectives&lt;/h3>
&lt;p>By the end of this tutorial, you will be able to:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Frame&lt;/strong> a localized natural disaster as a quasi-natural experiment, and explain why a flooded-vs-not comparison can identify a causal effect under &lt;em>parallel trends&lt;/em>.&lt;/li>
&lt;li>&lt;strong>Measure&lt;/strong> disaster exposure and economic activity from administrative and satellite data — district GDP, sub-district night-lights, and satellite inundation maps.&lt;/li>
&lt;li>&lt;strong>Estimate&lt;/strong> a dynamic, four-period difference-in-differences on district GDP growth with &lt;a href="https://pyfixest.org/" target="_blank" rel="noopener">&lt;code>pyfixest&lt;/code>&lt;/a>, and read it as an &lt;strong>event study&lt;/strong> with &lt;a href="https://github.com/igerber/diff-diff" target="_blank" rel="noopener">&lt;code>diff-diff&lt;/code>&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Quantify&lt;/strong> a &lt;em>dose-response&lt;/em> relationship at finer resolution using continuous flood intensity on sub-district night-lights.&lt;/li>
&lt;li>&lt;strong>Build&lt;/strong> a synthetic-control counterfactual for flooded Aceh with &lt;a href="https://github.com/jgreathouse9/mlsynth" target="_blank" rel="noopener">&lt;code>mlsynth&lt;/code>&lt;/a> and read its path and gap plots.&lt;/li>
&lt;li>&lt;strong>Defend&lt;/strong> your inference when treatment is geographically clustered, using Moran&amp;rsquo;s I and &lt;strong>Conley spatial standard errors&lt;/strong>, and validate the result with placebo and heterogeneity checks.&lt;/li>
&lt;/ul>
&lt;h3 id="12-study-design">1.2 Study design&lt;/h3>
&lt;pre>&lt;code class="language-mermaid">graph LR
subgraph SETTING[&amp;quot;The natural experiment&amp;quot;]
A[&amp;quot;&amp;lt;b&amp;gt;2004 tsunami&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;floods some&amp;lt;br/&amp;gt;Aceh districts&amp;quot;]
B[&amp;quot;&amp;lt;b&amp;gt;Treated&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;10 flooded&amp;lt;br/&amp;gt;districts&amp;quot;]
C[&amp;quot;&amp;lt;b&amp;gt;Control&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;non-flooded&amp;lt;br/&amp;gt;districts&amp;quot;]
A --&amp;gt; B
A --&amp;gt; C
end
subgraph MEASURE[&amp;quot;Two outcomes&amp;quot;]
D[&amp;quot;&amp;lt;b&amp;gt;District GDP&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;growth&amp;quot;]
E[&amp;quot;&amp;lt;b&amp;gt;Sub-district&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;night-lights&amp;quot;]
end
subgraph METHODS[&amp;quot;Four causal tools&amp;quot;]
F[&amp;quot;&amp;lt;b&amp;gt;Difference-in-&amp;lt;br/&amp;gt;Differences&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;pyfixest&amp;quot;]
G[&amp;quot;&amp;lt;b&amp;gt;Event study&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;diff-diff&amp;quot;]
H[&amp;quot;&amp;lt;b&amp;gt;Synthetic&amp;lt;br/&amp;gt;control&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;mlsynth&amp;quot;]
I[&amp;quot;&amp;lt;b&amp;gt;Conley spatial&amp;lt;br/&amp;gt;std. errors&amp;lt;/b&amp;gt;&amp;quot;]
end
B --&amp;gt; D
C --&amp;gt; D
B --&amp;gt; E
D --&amp;gt; F --&amp;gt; G
D --&amp;gt; H
F --&amp;gt; I
style A fill:#d97757,stroke:#141413,color:#fff
style B fill:#d97757,stroke:#141413,color:#fff
style C fill:#6a9bcc,stroke:#141413,color:#fff
style H fill:#00d4c8,stroke:#141413,color:#fff
style I fill:#00d4c8,stroke:#141413,color:#fff
&lt;/code>&lt;/pre>
&lt;p>Read the diagram left to right: the tsunami splits districts into treated and control; we observe two outcomes (district GDP and finer sub-district night-lights); and we deploy four causal tools — DiD and its event-study view, an independent synthetic control, and the spatial standard errors that keep our confidence honest. Each maps onto a section below.&lt;/p>
&lt;h3 id="13-key-concepts-at-a-glance">1.3 Key concepts at a glance&lt;/h3>
&lt;p>The post leans on a small vocabulary repeatedly. Each concept below has three parts. The &lt;strong>definition&lt;/strong> is always visible; the &lt;strong>example&lt;/strong> and &lt;strong>analogy&lt;/strong> sit behind clickable cards — open them when you need them, leave them closed for a quick scan. If a later section mentions &amp;ldquo;parallel trends&amp;rdquo; or &amp;ldquo;Conley standard errors&amp;rdquo; and the term feels slippery, this is the section to re-read.&lt;/p>
&lt;p>&lt;strong>1. Difference-in-Differences (DiD).&lt;/strong>
Compare the &lt;em>change&lt;/em> in the treated group to the &lt;em>change&lt;/em> in the control group. The difference of those two differences is the causal estimate. It nets out anything permanent about a district &lt;em>and&lt;/em> any trend shared by the whole country.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>Flooded districts&amp;rsquo; mean growth went from 0.0567 (before) to 0.0671 (after) — a change of +0.0103. Controls went 0.0519 to 0.0497 — a change of −0.0022. The 2×2 DiD is $0.0103 - (-0.0022) = +0.0125$.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>Time two runners on parallel tracks. Both speed up when the gun fires (the national trend). Credit your coaching only with the &lt;em>extra&lt;/em> burst of the runner you coached — the gap between the two changes.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>2. Parallel trends.&lt;/strong>
The identifying assumption of DiD: absent the tsunami, flooded and non-flooded districts would have grown by the &lt;em>same amount&lt;/em> on average. Their &lt;em>levels&lt;/em> may differ; their &lt;em>trends&lt;/em> must match.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>We test it directly: the pre-tsunami (2003–04) DiD coefficient is +0.0172 and statistically &lt;em>insignificant&lt;/em> (p = 0.28). No detectable divergence before treatment — the assumption survives its placebo.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>Two boats drifting on the same current. They sit at different points, but the current carries them in step. Only an engine — the treatment — should make one pull ahead.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>3. ATT&lt;/strong> $E[Y(1) - Y(0) \mid D=1]$.
The Average effect of the Treatment on the Treated. Here: the effect &lt;em>on the flooded districts&lt;/em>, not on some randomly chosen district. DiD and synthetic control both target the ATT.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>The +18.3% synthetic-control gap is the ATT &lt;em>for flooded Aceh&lt;/em>. It does not claim that flooding any district would raise its GDP 18% — only that &lt;em>these&lt;/em> districts, given &lt;em>this&lt;/em> reconstruction, ended up that much higher.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>The bonus speed measured on the car that actually got the coaching — not a promise about any car you might pick off the street.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>4. Counterfactual.&lt;/strong>
The output flooded Aceh &lt;em>would&lt;/em> have had with no tsunami. It is never observed; it must be &lt;em>estimated&lt;/em> — by the control group&amp;rsquo;s trend (DiD) or a weighted blend of donor districts (synthetic control).&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>&amp;ldquo;Synthetic Aceh&amp;rdquo; is a weighted recipe of 76 Rest-of-Sumatra donor districts (top weights: JAMBI_D01 0.13, BABEL_D05 0.12) chosen to match flooded Aceh&amp;rsquo;s &lt;em>pre-2005&lt;/em> GDP path. After 2005 it is our stand-in for the no-tsunami Aceh.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>The parallel-universe Aceh where the wave never came. We cannot visit it, so we build the most convincing look-alike we can from places the wave &lt;em>did&lt;/em> miss.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>5. Dose-response.&lt;/strong>
Bigger exposure should mean a bigger effect. Instead of an on/off treatment dummy, use &lt;em>continuous&lt;/em> intensity — the share of a sub-district flooded — or intensity quintiles.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>Each unit of &amp;ldquo;share of population flooded&amp;rdquo; raises night-lights growth by +0.016/year during recovery (p &amp;lt; 0.001). And only the &lt;strong>top intensity quintile&lt;/strong> shows a significant rebound — quintiles 1–4 are flat.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>Medicine dosage. A sip does little; the full dose moves the needle. If only the largest doses show an effect, the drug is real but the average hides where it acts.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>6. Night-lights as an economic proxy.&lt;/strong>
Satellite night-time brightness (DMSP-OLS &amp;ldquo;Digital Numbers&amp;rdquo;, 0–63) stands in for local economic activity where GDP is unavailable, after a log transform.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>In 2004 the flooded sub-districts averaged a luminosity of 5.79 versus 2.36 for non-flooded ones — they are the denser, more active coastal places, and their lights are what we track over time.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>Judging a city&amp;rsquo;s bustle from a night flight overhead. You cannot read the GDP accounts from 800 km up, but brighter usually means busier.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>7. Conley spatial-HAC standard errors.&lt;/strong>
Standard errors that allow a district&amp;rsquo;s errors to be correlated with &lt;em>nearby&lt;/em> districts in the same year (spatial) and with &lt;em>itself&lt;/em> over time (serial). They are larger — and more honest — than naive errors when the treated units cluster in space.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>The recovery effect&amp;rsquo;s standard error roughly doubles, from 0.0146 (naive) to 0.0244 (Conley-HAC). That turns a &lt;em>t&lt;/em> of 4.3 into a &lt;em>t&lt;/em> of 2.57 — the point estimate (+0.0628) never moves, but it is significant at 5%, not 1%.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>Counting a milling crowd. Rows of seats suggest many independent heads, but if everyone keeps shuffling between seats you have far fewer &lt;em>truly independent&lt;/em> observations than it looks.&lt;/p>
&lt;/details>
&lt;/div>
&lt;h2 id="2-setup-and-the-three-star-libraries">2. Setup and the three star libraries&lt;/h2>
&lt;p>Three specialist packages do the heavy lifting, and each gets a one-line introduction the first time we use it:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://pyfixest.org/" target="_blank" rel="noopener">&lt;code>pyfixest&lt;/code>&lt;/a>&lt;/strong> runs fixed-effects regressions with a fast, Stata-flavored formula syntax. Everything left of the &lt;code>|&lt;/code> is estimated; everything right of it is &lt;em>absorbed&lt;/em> as fixed effects, so we never build dummy columns by hand.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://github.com/igerber/diff-diff" target="_blank" rel="noopener">&lt;code>diff-diff&lt;/code>&lt;/a>&lt;/strong> is a small package built to &lt;em>teach&lt;/em> difference-in-differences: it returns the 2×2 estimate and the event-study path in one or two lines each.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://github.com/jgreathouse9/mlsynth" target="_blank" rel="noopener">&lt;code>mlsynth&lt;/code>&lt;/a>&lt;/strong> implements modern synthetic-control estimators; we use &lt;code>VanillaSC&lt;/code>, the classic Abadie–Diamond–Hainmueller method.&lt;/li>
&lt;/ul>
&lt;pre>&lt;code class="language-python"># In Colab, install the three estimation libraries first:
# !pip install pyfixest==0.50.1 diff-diff==3.5.2 &amp;quot;mlsynth @ git+https://github.com/jgreathouse9/mlsynth.git&amp;quot;
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import pyfixest as pf
import diff_diff as dd
from mlsynth import VanillaSC
np.random.seed(42) # reproducibility
# Site dark-theme palette for figures
STEEL_BLUE, WARM_ORANGE, TEAL = &amp;quot;#6a9bcc&amp;quot;, &amp;quot;#d97757&amp;quot;, &amp;quot;#00d4c8&amp;quot;
DARK_NAVY, GRID_LINE, LIGHT_TEXT = &amp;quot;#0f1729&amp;quot;, &amp;quot;#1f2b5e&amp;quot;, &amp;quot;#c8d0e0&amp;quot;
&lt;/code>&lt;/pre>
&lt;p>Two small design helpers encode the paper&amp;rsquo;s difference-in-differences structure. The post period is split into event-time windows — &lt;strong>pre&lt;/strong> (2003–04), &lt;strong>tsunami&lt;/strong> (2005), &lt;strong>recovery&lt;/strong> (2006–08), and &lt;strong>post-recovery&lt;/strong> (2009–12) — all measured against the omitted &lt;strong>2000–02 baseline&lt;/strong>. The function below turns a treatment column into the four interaction terms those windows need.&lt;/p>
&lt;pre>&lt;code class="language-python"># The four event-time windows (the 2000-02 baseline is the omitted reference)
PERIOD_TO_TERM = {&amp;quot;pre&amp;quot;: &amp;quot;D_pre&amp;quot;, &amp;quot;tsunami&amp;quot;: &amp;quot;D_2005&amp;quot;,
&amp;quot;recovery&amp;quot;: &amp;quot;D_recov&amp;quot;, &amp;quot;postrec&amp;quot;: &amp;quot;D_post&amp;quot;}
DID_TERMS = [&amp;quot;D_pre&amp;quot;, &amp;quot;D_2005&amp;quot;, &amp;quot;D_recov&amp;quot;, &amp;quot;D_post&amp;quot;]
def make_did_terms(df, treat_col):
&amp;quot;&amp;quot;&amp;quot;Build treatment x period interactions: D_pre, D_2005, D_recov, D_post.&amp;quot;&amp;quot;&amp;quot;
out = df.copy()
treat = out[treat_col].astype(float)
for period, term in PERIOD_TO_TERM.items():
out[term] = treat * (out[&amp;quot;period&amp;quot;] == period).astype(float)
return out
&lt;/code>&lt;/pre>
&lt;h2 id="3-the-data-measuring-a-disaster-at-two-geographic-levels">3. The data: measuring a disaster at two geographic levels&lt;/h2>
&lt;p>Evaluating a &lt;em>localized&lt;/em> disaster forces a measurement problem to the surface. National GDP would barely flinch at a shock to one province — so we need &lt;strong>sub-national&lt;/strong> data, and we need it at two grains. We load both panels straight from the post&amp;rsquo;s data folder on GitHub, so the code runs unchanged in Colab.&lt;/p>
&lt;pre>&lt;code class="language-python">BASE = (&amp;quot;https://raw.githubusercontent.com/cmg777/starter-academic-v501/&amp;quot;
&amp;quot;master/content/post/python_did_sc_tsunami/data/&amp;quot;)
district = pd.read_csv(BASE + &amp;quot;aceh_tsunami_district_panel.csv&amp;quot;)
subdistrict = pd.read_csv(BASE + &amp;quot;aceh_tsunami_subdistrict_panel.csv&amp;quot;)
print(&amp;quot;district panel :&amp;quot;, district.shape)
print(&amp;quot;subdistrict panel :&amp;quot;, subdistrict.shape)
print(district.groupby([&amp;quot;region_group&amp;quot;, &amp;quot;flooded&amp;quot;]).size().unstack(&amp;quot;flooded&amp;quot;, fill_value=0))
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">district panel : (1750, 30)
subdistrict panel : (3864, 19)
flooded 0 1
region_group
Aceh 13 10
North Sumatra 24 2
Rest of Sumatra 76 0
&lt;/code>&lt;/pre>
&lt;p>The district panel is &lt;strong>125 districts observed annually over 1999–2012&lt;/strong> (1,750 rows); the sub-district panel is &lt;strong>276 Aceh sub-districts&lt;/strong> (&lt;em>kecamatans&lt;/em>) over the same years. The treatment group is small and concentrated: &lt;strong>10 flooded Aceh districts&lt;/strong> against 13 non-flooded Aceh districts plus 76 Rest-of-Sumatra controls. (North Sumatra&amp;rsquo;s two flooded islands are held back for a robustness check, because they were also hit by a &lt;em>separate&lt;/em> earthquake in March 2005.) That smallness — only 10 treated units — is the recurring source of statistical caution in this case study.&lt;/p>
&lt;h3 id="31-the-first-outcome--district-gdp-growth">3.1 The first outcome — district GDP growth&lt;/h3>
&lt;p>The main outcome (&lt;code>gdp_growth&lt;/code>) is the &lt;strong>annual growth rate of real district GDP&lt;/strong>, measured in the spirit of the World Bank&amp;rsquo;s INDO-DAPOER database, which draws on Indonesia&amp;rsquo;s large annual socio-economic survey (SUSENAS). Two construction details from the paper matter. First, &lt;strong>oil and gas are excluded&lt;/strong>: that sector is volatile and concentrated in a few non-treated districts, so leaving it in would add noise unrelated to the tsunami. Second, GDP is in &lt;strong>constant prices&lt;/strong> so we measure real output, not inflation.&lt;/p>
&lt;pre>&lt;code class="language-python">print(district[[&amp;quot;gdp_growth&amp;quot;, &amp;quot;gdp_pc_growth&amp;quot;, &amp;quot;gdp_const_usd_m&amp;quot;]].describe().round(3).loc[
[&amp;quot;count&amp;quot;, &amp;quot;mean&amp;quot;, &amp;quot;std&amp;quot;, &amp;quot;min&amp;quot;, &amp;quot;max&amp;quot;]])
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> gdp_growth gdp_pc_growth gdp_const_usd_m
count 1621.00 1621.00 1750.00
mean 0.05 0.04 671.18
std 0.07 0.07 593.99
min -0.17 -0.20 33.09
max 0.29 0.30 3748.41
&lt;/code>&lt;/pre>
&lt;p>District growth averages about &lt;strong>5.2% a year&lt;/strong> with a wide spread (standard deviation 6.6 percentage points). Notice &lt;code>gdp_growth&lt;/code> has 1,621 values, not 1,750: growth is undefined in 1999 (no prior year to difference against) and is missing for one district (Subulussalam) over 2003–06 due to an administrative boundary change. Every estimator below simply drops those rows — a small but honest detail that keeps the sample sizes matching the paper exactly.&lt;/p>
&lt;h3 id="32-the-second-outcome--sub-district-night-lights">3.2 The second outcome — sub-district night-lights&lt;/h3>
&lt;p>GDP at the district level is too coarse to capture &lt;em>how intensely&lt;/em> a place was hit. So the paper drops to the finer sub-district grain and switches to a satellite proxy: &lt;strong>night-time luminosity&lt;/strong> from the DMSP-OLS program. Each pixel records a &amp;ldquo;Digital Number&amp;rdquo; from 0 (dark) to 63 (saturated bright). To turn pixel brightness into a sub-district economic measure, the paper sums the lights across a sub-district&amp;rsquo;s pixels and takes a logarithm:&lt;/p>
&lt;p>$$\text{NL}_{ct} = \log\left( \sum_{n=1}^{N} \left( \text{DN}_{nct} + 0.001 \right) \right)$$&lt;/p>
&lt;p>In words: a sub-district $c$&amp;rsquo;s log-luminosity in year $t$ is the log of the total brightness summed over its $N$ pixels, with a tiny $0.001$ added so that pixels reading exactly zero do not break the logarithm. &lt;em>Summing&lt;/em> (rather than averaging) keeps the measure comparable to GDP, which is also a total; the &lt;em>log&lt;/em> tames the heavy right-skew of brightness. Our outcome &lt;code>nl_growth&lt;/code> is the annual change in this log measure — a luminosity growth rate that lines up conceptually with the GDP growth rate.&lt;/p>
&lt;h3 id="33-identifying-the-treated-areas--where-the-wave-actually-reached">3.3 Identifying the treated areas — where the wave actually reached&lt;/h3>
&lt;p>The credibility of the whole exercise rests on &lt;strong>how &amp;ldquo;flooded&amp;rdquo; is defined&lt;/strong>. The paper does &lt;em>not&lt;/em> let economics decide it. Treatment is read off &lt;strong>satellite inundation maps&lt;/strong> produced 1–5 days after the tsunami by remote-sensing agencies (Germany&amp;rsquo;s DLR/ZKI and the Dartmouth Flood Observatory), which compared the coastline before and after to flag pixels the water reached. Whether the wave penetrated a given stretch of coast was governed by elevation, vegetation, and offshore depth — geographic happenstance, plausibly &lt;em>unrelated&lt;/em> to a district&amp;rsquo;s economic prospects. That is exactly what makes the flooding a credible natural experiment.&lt;/p>
&lt;p>The data encodes exposure three ways, from coarse to fine:&lt;/p>
&lt;pre>&lt;code class="language-python">print(&amp;quot;Binary treatment (district level):&amp;quot;)
print(district.groupby(&amp;quot;flooded&amp;quot;)[&amp;quot;district_id&amp;quot;].nunique())
print(&amp;quot;\nContinuous + quintile intensity (sub-district level), among flooded units:&amp;quot;)
print(subdistrict.loc[subdistrict.flooded == 1, [&amp;quot;share_pop_flooded&amp;quot;, &amp;quot;share_area_flooded&amp;quot;]].describe().round(3).loc[[&amp;quot;mean&amp;quot;, &amp;quot;min&amp;quot;, &amp;quot;max&amp;quot;]])
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Binary treatment (district level):
flooded
0 113
1 12
Name: district_id, dtype: int64
Continuous + quintile intensity (sub-district level), among flooded units:
share_pop_flooded share_area_flooded
mean 0.190 0.012
min 0.001 0.000
max 0.620 0.105
&lt;/code>&lt;/pre>
&lt;p>&lt;code>flooded&lt;/code> is the simple on/off dummy used for the district DiD. For the finer night-lights analysis we also have &lt;code>share_pop_flooded&lt;/code> and &lt;code>share_area_flooded&lt;/code> — the &lt;em>fraction&lt;/em> of a sub-district&amp;rsquo;s population (or land area) that the inundation maps marked as flooded — plus a &lt;code>flood_intensity_quintile&lt;/code> ranking. Notice &lt;code>share_area_flooded&lt;/code> has a tiny mean (about 1.2%): land area includes a lot of unpopulated hinterland, so the &lt;em>share of area&lt;/em> flooded is small even where damage was severe. That tiny scale will make its regression coefficient look enormous later — same story, different units.&lt;/p>
&lt;h3 id="34-a-transparent-word-on-the-synthetic-data">3.4 A transparent word on the synthetic data&lt;/h3>
&lt;p>Before we model anything: the panels above are &lt;strong>simulated&lt;/strong>. The data-generating process was tuned so that a fixed-effects DiD recovers, column by column, coefficients close to the paper&amp;rsquo;s reported values (within about 0.005 on the headline cells), with the same signs and significance stars. Spatial and serial shocks were injected so the standard errors behave like the paper&amp;rsquo;s &lt;em>without moving the point estimates&lt;/em>. We will hold ourselves accountable for this in &lt;a href="#11-reproduction-audit-synthetic-data-vs-the-paper">Section 11&lt;/a>, where a table lines our numbers up against the paper&amp;rsquo;s. With the measurement settled, let us look at the data before modeling it.&lt;/p>
&lt;h2 id="4-exploratory-analysis-the-space-time-dynamics">4. Exploratory analysis: the space-time dynamics&lt;/h2>
&lt;p>Good causal work &lt;em>looks&lt;/em> at the data before it regresses it. Three views build the intuition the models will formalize. First, a handful of &lt;strong>individual districts&lt;/strong> over time — three badly-hit ones (Banda Aceh, Aceh Besar, Aceh Jaya) against two highland controls — with GDP indexed so every district starts at 100 in 2004.&lt;/p>
&lt;pre>&lt;code class="language-python">def indexed(name):
s = district[district.district_name == name].set_index(&amp;quot;year&amp;quot;)[&amp;quot;gdp_const_usd_m&amp;quot;]
return s / s.loc[2004] * 100
fig, ax = plt.subplots(figsize=(9, 5.2))
for name in [&amp;quot;Banda Aceh&amp;quot;, &amp;quot;Aceh Besar&amp;quot;, &amp;quot;Aceh Jaya&amp;quot;]:
ax.plot(indexed(name), color=WARM_ORANGE, lw=2.2, label=f&amp;quot;{name} (flooded)&amp;quot;)
for name in [&amp;quot;Aceh Tengah&amp;quot;, &amp;quot;Bener Meriah&amp;quot;]:
ax.plot(indexed(name), &amp;quot;--&amp;quot;, color=STEEL_BLUE, lw=2, label=f&amp;quot;{name} (control)&amp;quot;)
ax.axvline(2004.5, color=LIGHT_TEXT, ls=&amp;quot;:&amp;quot;)
ax.set(xlabel=&amp;quot;Year&amp;quot;, ylabel=&amp;quot;Real GDP (2004 = 100)&amp;quot;)
ax.legend(); plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="python_did_sc_tsunami_eda_timeseries.png" alt="Key Aceh districts indexed to 2004 = 100; flooded districts dip in 2005 and rebound far above controls.">
&lt;em>Three flooded districts (orange) versus two highland controls (blue), each indexed to 100 in 2004.&lt;/em>&lt;/p>
&lt;p>The flooded districts sit on the same path as the controls through 2004, &lt;strong>buckle in 2005&lt;/strong>, and then climb steeply — Banda Aceh, the provincial capital, ends near 260 (a 2.6× increase over its 2004 level). The control districts grow too, but far more gently. The eye already sees a disaster followed by an over-shooting recovery; the rest of the post is about measuring it and trusting the measurement.&lt;/p>
&lt;p>Single districts are noisy, though. The next view summarizes the &lt;em>distribution&lt;/em> of growth in each group across the event-time periods.&lt;/p>
&lt;pre>&lt;code class="language-python">samp = district[district.region_group != &amp;quot;North Sumatra&amp;quot;].dropna(subset=[&amp;quot;gdp_growth&amp;quot;]).copy()
samp[&amp;quot;group&amp;quot;] = np.where(samp.flooded == 1, &amp;quot;Treated (flooded)&amp;quot;, &amp;quot;Control&amp;quot;)
# (full grouped-boxplot styling is in script.py)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="python_did_sc_tsunami_group_boxplots.png" alt="Box-plots of GDP growth by group across event-time periods; the treated 2005 box drops below zero and the recovery box lifts above the control box.">
&lt;em>Distribution of district GDP growth, treated (orange) vs control (blue), in each event-time period.&lt;/em>&lt;/p>
&lt;p>The boxes make the dynamics unmistakable. In &lt;strong>2000–04&lt;/strong> the treated and control boxes overlap almost perfectly — similar centers, similar spread. In &lt;strong>2005&lt;/strong> the treated box drops bodily below zero (a median contraction) while the control box stays put. In &lt;strong>2006–08&lt;/strong> the treated box jumps &lt;em>above&lt;/em> the control box. The disaster and the rebound are both visible as shifts in the whole distribution, not just a couple of outliers.&lt;/p>
&lt;p>Finally, the single figure that motivates difference-in-differences: the &lt;strong>group means&lt;/strong> over time.&lt;/p>
&lt;pre>&lt;code class="language-python">means = (samp.groupby([&amp;quot;year&amp;quot;, &amp;quot;flooded&amp;quot;])[&amp;quot;gdp_growth&amp;quot;].mean()
.unstack(&amp;quot;flooded&amp;quot;).rename(columns={0: &amp;quot;Control&amp;quot;, 1: &amp;quot;Treated&amp;quot;}))
fig, ax = plt.subplots(figsize=(9, 5.2))
ax.plot(means[&amp;quot;Control&amp;quot;], &amp;quot;--o&amp;quot;, color=STEEL_BLUE, label=&amp;quot;Control&amp;quot;)
ax.plot(means[&amp;quot;Treated&amp;quot;], &amp;quot;-o&amp;quot;, color=WARM_ORANGE, label=&amp;quot;Treated (flooded)&amp;quot;)
ax.axvline(2004.5, color=LIGHT_TEXT, ls=&amp;quot;:&amp;quot;); ax.legend(); plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="python_did_sc_tsunami_group_means.png" alt="Treated vs control group-mean growth: parallel before 2005, then the treated line dives and overshoots.">
&lt;em>Mean annual GDP growth, treated vs control. Parallel before the tsunami, sharply divergent after.&lt;/em>&lt;/p>
&lt;p>This is the picture difference-in-differences was invented for. Before 2005 the two lines move &lt;strong>in lockstep&lt;/strong> — the visual signature of parallel trends. At the tsunami the treated line plunges to about &lt;strong>−0.027&lt;/strong> while the control line barely moves. Then the treated line &lt;strong>over-shoots&lt;/strong>, peaking near &lt;strong>+0.124&lt;/strong> in 2007 before settling back toward the control. The eye is convinced something happened; now we quantify it and, crucially, attach a margin of error.&lt;/p>
&lt;h2 id="5-difference-in-differences-on-district-gdp-growth">5. Difference-in-differences on district GDP growth&lt;/h2>
&lt;h3 id="51-the-intuition-a-22-difference-of-differences">5.1 The intuition: a 2×2 difference of differences&lt;/h3>
&lt;p>Start with the simplest possible version. Split time into &amp;ldquo;before&amp;rdquo; (≤ 2004) and &amp;ldquo;after&amp;rdquo; (≥ 2005), compute the mean growth in each of the four treated/control × before/after cells, and form the &lt;strong>difference of the two differences&lt;/strong>:&lt;/p>
&lt;p>$$\widehat{\text{DiD}} = \big( \bar{g}_{\text{treated, after}} - \bar{g}_{\text{treated, before}} \big) - \big( \bar{g}_{\text{control, after}} - \bar{g}_{\text{control, before}} \big)$$&lt;/p>
&lt;p>In words: take how much the treated group&amp;rsquo;s average growth &lt;em>changed&lt;/em> across the break, subtract how much the control group&amp;rsquo;s changed, and what remains is the part attributable to the tsunami — because the control change captures whatever was happening nationwide anyway.&lt;/p>
&lt;pre>&lt;code class="language-python">sample = make_did_terms(district[district.region_group != &amp;quot;North Sumatra&amp;quot;], &amp;quot;flooded&amp;quot;).dropna(subset=[&amp;quot;gdp_growth&amp;quot;])
cell = sample.groupby([&amp;quot;flooded&amp;quot;, &amp;quot;post&amp;quot;])[&amp;quot;gdp_growth&amp;quot;].mean().unstack(&amp;quot;post&amp;quot;)
cell.columns, cell.index = [&amp;quot;Before (&amp;lt;=2004)&amp;quot;, &amp;quot;After (&amp;gt;=2005)&amp;quot;], [&amp;quot;Control&amp;quot;, &amp;quot;Treated&amp;quot;]
cell[&amp;quot;change&amp;quot;] = cell[&amp;quot;After (&amp;gt;=2005)&amp;quot;] - cell[&amp;quot;Before (&amp;lt;=2004)&amp;quot;]
print(cell.round(4))
res = dd.DifferenceInDifferences(cluster=&amp;quot;district_id&amp;quot;).fit(
sample, outcome=&amp;quot;gdp_growth&amp;quot;, treatment=&amp;quot;flooded&amp;quot;, time=&amp;quot;post&amp;quot;)
print(f&amp;quot;\nDiD ATT = {res.att:+.4f} (SE {res.se:.4f}, p = {res.p_value:.3f})&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> Before (&amp;lt;=2004) After (&amp;gt;=2005) change
Control 0.0519 0.0497 -0.0022
Treated 0.0567 0.0671 0.0103
DiD ATT = +0.0125 (SE 0.0142, p = 0.379)
&lt;/code>&lt;/pre>
&lt;p>The hand calculation gives $0.0103 - (-0.0022) = +0.0125$, and &lt;a href="https://github.com/igerber/diff-diff" target="_blank" rel="noopener">&lt;code>diff-diff&lt;/code>&lt;/a> confirms it with a standard error: &lt;strong>+0.0125, but statistically insignificant&lt;/strong> (p = 0.38). Before you conclude &amp;ldquo;no effect,&amp;rdquo; look closer. This single &amp;ldquo;after&amp;rdquo; window blends two opposite phases — the &lt;strong>2005 destruction&lt;/strong> and the &lt;strong>2006–08 boom&lt;/strong> — into one average, and they nearly cancel. The pooled estimate is not wrong; it is just &lt;em>uninformative&lt;/em>. The fix is to let the effect vary over time.&lt;/p>
&lt;h3 id="52-the-dynamic-did-the-papers-headline">5.2 The dynamic DiD (the paper&amp;rsquo;s headline)&lt;/h3>
&lt;p>The paper&amp;rsquo;s central specification keeps the same logic but splits the post period into the four event-time windows, each entering as a treatment-times-period interaction relative to the 2000–02 baseline:&lt;/p>
&lt;p>$$\Delta Y_{it} = \beta_1 D_i \mathbf{1}[t \in \text{pre}] + \beta_2 D_i \mathbf{1}[t = 2005] + \beta_3 D_i \mathbf{1}[t \in \text{recovery}] + \beta_4 D_i \mathbf{1}[t \in \text{post}] + \alpha_i + \gamma_t + \varepsilon_{it}$$&lt;/p>
&lt;p>Here $\Delta Y_{it}$ is district $i$&amp;rsquo;s GDP growth in year $t$; $D_i = 1$ for flooded districts; $\mathbf{1}[\cdot]$ is an indicator that is 1 when the year falls in that window; $\alpha_i$ is a &lt;strong>district fixed effect&lt;/strong> (absorbing anything permanent about a district) and $\gamma_t$ is a &lt;strong>year fixed effect&lt;/strong> (absorbing common national shocks). The four $\beta$&amp;rsquo;s are the story: $\beta_2$ should be the negative 2005 shock, $\beta_3$ the positive reconstruction boom, while $\beta_1$ and $\beta_4$ should be near zero. These map exactly onto the code variables &lt;code>D_pre&lt;/code>, &lt;code>D_2005&lt;/code>, &lt;code>D_recov&lt;/code>, &lt;code>D_post&lt;/code>. In &lt;code>pyfixest&lt;/code>, the part after the &lt;code>|&lt;/code> lists the fixed effects to absorb:&lt;/p>
&lt;pre>&lt;code class="language-python">m = pf.feols(&amp;quot;gdp_growth ~ D_pre + D_2005 + D_recov + D_post | district_id + year&amp;quot;,
data=make_did_terms(district[district.region_group != &amp;quot;North Sumatra&amp;quot;], &amp;quot;flooded&amp;quot;),
vcov={&amp;quot;CRV1&amp;quot;: &amp;quot;district_id&amp;quot;})
m.coef().round(4)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">D_pre 0.0172
D_2005 -0.0792
D_recov 0.0628
D_post 0.0114
&lt;/code>&lt;/pre>
&lt;p>Reading these against the paper&amp;rsquo;s three control pools (the full table from &lt;code>script.py&lt;/code>) gives the headline result:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Coefficient&lt;/th>
&lt;th>(1) Sumatra controls&lt;/th>
&lt;th>(2) Rest of Sumatra&lt;/th>
&lt;th>(3) Aceh non-flooded&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Pre-tsunami (2003-04)&lt;/td>
&lt;td>+0.0172 (0.0159)&lt;/td>
&lt;td>+0.0176 (0.0162)&lt;/td>
&lt;td>+0.0154 (0.0187)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Tsunami (2005)&lt;/strong>&lt;/td>
&lt;td>&lt;strong>−0.0792*** (0.0240)&lt;/strong>&lt;/td>
&lt;td>&lt;strong>−0.0782*** (0.0247)&lt;/strong>&lt;/td>
&lt;td>&lt;strong>−0.0841*** (0.0281)&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Recovery (2006-08)&lt;/strong>&lt;/td>
&lt;td>&lt;strong>+0.0628** (0.0244)&lt;/strong>&lt;/td>
&lt;td>&lt;strong>+0.0682*** (0.0247)&lt;/strong>&lt;/td>
&lt;td>+0.0310 (0.0281)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Post-recovery (2009-12)&lt;/td>
&lt;td>+0.0114 (0.0146)&lt;/td>
&lt;td>+0.0132 (0.0147)&lt;/td>
&lt;td>+0.0008 (0.0204)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Observations&lt;/td>
&lt;td>1,283&lt;/td>
&lt;td>1,118&lt;/td>
&lt;td>295&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;em>Conley spatial-HAC standard errors in parentheses; *** p&amp;lt;.01, ** p&amp;lt;.05, * p&amp;lt;.10.&lt;/em>&lt;/p>
&lt;p>Three things to take away. First, the &lt;strong>pre-tsunami coefficient is small and insignificant&lt;/strong> (+0.0172) — the parallel-trends assumption passes its formal placebo test, so the comparison is credible. Second, the &lt;strong>2005 coefficient is −0.0792&lt;/strong> (p &amp;lt; 0.01): flooded districts grew almost 8 percentage points slower the year the wave hit. Third, the &lt;strong>recovery coefficient is +0.0628&lt;/strong> (p &amp;lt; 0.05): over 2006–08 they grew 6.3 points per year &lt;em>faster&lt;/em> than controls — and three years of that premium (≈ +0.19 cumulatively) more than erases the one-year loss. The post-recovery coefficient is a near-zero +0.0114, meaning the gain neither evaporated nor kept compounding: Aceh settled onto a &lt;em>permanently higher&lt;/em> path. This is the paper&amp;rsquo;s signature result — &amp;ldquo;sustainable recovery beyond the counterfactual trend.&amp;rdquo; Notice column 3, which compares flooded Aceh to its &lt;em>own&lt;/em> non-flooded neighbors, halves the recovery coefficient to +0.0310 (insignificant): reconstruction money spilled across district lines, shrinking the within-Aceh contrast.&lt;/p>
&lt;p>The estimand here is the &lt;strong>ATT&lt;/strong> — the effect on the flooded districts — and identification rests on parallel trends, not randomization. This is an &lt;em>observational&lt;/em> study; the placebo and spatial-error checks below are what earn it credibility.&lt;/p>
&lt;h3 id="53-the-event-study-seeing-the-whole-path">5.3 The event study: seeing the whole path&lt;/h3>
&lt;p>The dynamic DiD has four coefficients; an &lt;strong>event study&lt;/strong> plots them (plus the pinned baseline) so the pre-trend and the recovery path are visible at a glance. &lt;code>diff-diff&lt;/code> produces it directly:&lt;/p>
&lt;pre>&lt;code class="language-python">mp = dd.MultiPeriodDiD(cluster=&amp;quot;district_id&amp;quot;).fit(
sample, outcome=&amp;quot;gdp_growth&amp;quot;, treatment=&amp;quot;flooded&amp;quot;, time=&amp;quot;period&amp;quot;,
reference_period=&amp;quot;baseline&amp;quot;, absorb=[&amp;quot;district_id&amp;quot;])
for p in [&amp;quot;pre&amp;quot;, &amp;quot;tsunami&amp;quot;, &amp;quot;recovery&amp;quot;, &amp;quot;postrec&amp;quot;]:
e = mp.period_effects[p]
print(f&amp;quot;{p:9s} effect={e.effect:+.4f} se={e.se:.4f} p={e.p_value:.3f}&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">pre effect=+0.0172 se=0.0160 p=0.283
tsunami effect=-0.0792 se=0.0260 p=0.002
recovery effect=+0.0628 se=0.0247 p=0.011
postrec effect=+0.0114 se=0.0149 p=0.444
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="python_did_sc_tsunami_event_study.png" alt="Event study: a flat pre-trend, a 2005 collapse, and a 2006-08 rebound, with 95% confidence intervals.">
&lt;em>Each point is the treated-minus-control effect in that period, relative to the 2000–02 baseline; bars are 95% confidence intervals.&lt;/em>&lt;/p>
&lt;p>The figure tells the entire story in one arc. The &lt;strong>baseline and pre-tsunami points sit on zero&lt;/strong> (parallel trends — the identifying assumption is visibly satisfied). The &lt;strong>2005 point collapses to −0.079&lt;/strong> with a confidence interval well below zero. The &lt;strong>recovery point rebounds to +0.063&lt;/strong>, also significantly positive. And the &lt;strong>post-recovery point drifts back toward zero&lt;/strong> but stays positive — the higher level persists. A single &amp;ldquo;after&amp;rdquo; dummy (Section 5.1) averaged the deep red 2005 point with the high green recovery point and got a muted, insignificant number; the event study shows &lt;em>why&lt;/em> that average was misleading.&lt;/p>
&lt;h3 id="54-did-people-just-leave-the-per-capita-check">5.4 Did people just leave? The per-capita check&lt;/h3>
&lt;p>A worry: maybe &amp;ldquo;growth&amp;rdquo; per district rose only because the population fell (the tragic arithmetic of 130,000 deaths and displacement). If GDP and population dropped together, GDP &lt;em>per capita&lt;/em> need not have moved. Re-running the same DiD on &lt;code>gdp_pc_growth&lt;/code> addresses it:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Coefficient&lt;/th>
&lt;th>(1) Sumatra controls&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Tsunami (2005)&lt;/td>
&lt;td>+0.0192 (0.0239)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Recovery (2006-08)&lt;/td>
&lt;td>+0.0827*** (0.0261)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>In per-capita terms there is &lt;strong>no significant 2005 loss&lt;/strong> (+0.0192) — output and population fell together that year — but the &lt;strong>recovery gain is even larger and highly significant&lt;/strong> (+0.0827, p &amp;lt; 0.01): fewer people then shared a rebuilt, better-capitalized economy. The effect is not a denominator artifact; it survives, indeed strengthens, when we divide by population.&lt;/p>
&lt;h2 id="6-night-lights-dose-response-how-intensity-matters">6. Night-lights dose-response: how intensity matters&lt;/h2>
&lt;p>District GDP answers &amp;ldquo;did flooded districts grow faster?&amp;rdquo; Night-lights, available for the much finer sub-districts, can answer a sharper question: &lt;strong>did the places hit &lt;em>harder&lt;/em> rebound &lt;em>more&lt;/em>?&lt;/strong> First, a quick descriptive — how bright were flooded vs non-flooded sub-districts before the tsunami?&lt;/p>
&lt;pre>&lt;code class="language-python">snap = subdistrict[subdistrict.year == 2004]
print(snap.groupby(&amp;quot;flooded&amp;quot;)[&amp;quot;avg_luminosity&amp;quot;].agg([&amp;quot;count&amp;quot;, &amp;quot;mean&amp;quot;, &amp;quot;std&amp;quot;, &amp;quot;max&amp;quot;]).round(2))
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> count mean std max
flooded
0 208 2.36 4.41 36.0
1 68 5.79 8.31 39.0
&lt;/code>&lt;/pre>
&lt;p>The 68 flooded sub-districts averaged a 2004 luminosity of &lt;strong>5.79&lt;/strong> versus &lt;strong>2.36&lt;/strong> for the 208 non-flooded ones — about 2.5× brighter, because flooded places are the denser, more economically active coastal strips. Now the dose-response: instead of the on/off &lt;code>flooded&lt;/code> dummy, we interact the &lt;em>continuous&lt;/em> flood intensity with the event-time periods.&lt;/p>
&lt;pre>&lt;code class="language-python">def nl_fit(treat):
df = make_did_terms(subdistrict, treat)
return pf.feols(&amp;quot;nl_growth ~ D_pre + D_2005 + D_recov + D_post | kecamatan_id + year&amp;quot;,
data=df, vcov={&amp;quot;CRV1&amp;quot;: &amp;quot;kecamatan_id&amp;quot;})
print(nl_fit(&amp;quot;share_pop_flooded&amp;quot;).coef().round(4))
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">D_pre 0.0052
D_2005 -0.0073
D_recov 0.0160
D_post 0.0019
&lt;/code>&lt;/pre>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Coefficient&lt;/th>
&lt;th>Share of population flooded&lt;/th>
&lt;th>Share of area flooded&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Pre-tsunami (2003-04)&lt;/td>
&lt;td>+0.0052 (0.0034)&lt;/td>
&lt;td>+0.565 (0.358)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tsunami (2005)&lt;/td>
&lt;td>−0.0073** (0.0035)&lt;/td>
&lt;td>−0.727* (0.381)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Recovery (2006-08)&lt;/strong>&lt;/td>
&lt;td>&lt;strong>+0.0160*** (0.0022)&lt;/strong>&lt;/td>
&lt;td>&lt;strong>+1.660*** (0.246)&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Post-recovery (2009-12)&lt;/td>
&lt;td>+0.0019 (0.0024)&lt;/td>
&lt;td>+0.270 (0.250)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>N&lt;/td>
&lt;td>3,444&lt;/td>
&lt;td>3,444&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The recovery coefficient on &amp;ldquo;share of population flooded&amp;rdquo; is &lt;strong>+0.0160&lt;/strong> (p &amp;lt; 0.001): each additional unit of population-share flooded buys that much extra annual luminosity growth during reconstruction. The &amp;ldquo;share of area&amp;rdquo; column tells the &lt;em>same&lt;/em> story with a coefficient about 100× larger (&lt;strong>+1.660&lt;/strong>) — purely because, as we saw in Section 3.3, the share of &lt;em>area&lt;/em> flooded is a tiny number, so a one-unit move is enormous. Same effect, different yardstick. The pre-period coefficients are small and the 2005 dip is weak-to-modest, mirroring the district results at finer resolution.&lt;/p>
&lt;p>The dose-response sharpens further if we ask &lt;em>where&lt;/em> the effect lives. Splitting flood intensity into quintiles and interacting each with the post period:&lt;/p>
&lt;p>&lt;img src="python_did_sc_tsunami_nightlights_dose.png" alt="Night-lights dose-response: continuous period effects (left) and quintile effects (right); only the top quintile is significant.">
&lt;em>Left: period coefficients for the continuous dose. Right: effect by intensity quintile — only the worst-hit fifth (Q5) rebounds significantly.&lt;/em>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Quintile&lt;/th>
&lt;th>Q1&lt;/th>
&lt;th>Q2&lt;/th>
&lt;th>Q3&lt;/th>
&lt;th>Q4&lt;/th>
&lt;th>Q5 (worst-hit)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Effect (share of population)&lt;/td>
&lt;td>+0.0010&lt;/td>
&lt;td>+0.0010&lt;/td>
&lt;td>+0.0009&lt;/td>
&lt;td>+0.0008&lt;/td>
&lt;td>&lt;strong>+0.0018**&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Only the &lt;strong>top quintile&lt;/strong> — the most heavily flooded fifth of sub-districts — shows a statistically significant rebound (+0.0018, p ≈ 0.02); quintiles 1 through 4 are flat and indistinguishable from zero. The average effect is not spread evenly; it is concentrated exactly where the damage, and therefore the reconstruction spending, was greatest. That is a substantive lesson about disaster aid as much as a statistical one.&lt;/p>
&lt;h2 id="7-synthetic-control-building-a-counterfactual-aceh">7. Synthetic control: building a counterfactual Aceh&lt;/h2>
&lt;p>Difference-in-differences leans on the &lt;em>control group&amp;rsquo;s trend&lt;/em> as the counterfactual. The &lt;strong>synthetic control method&lt;/strong> builds a more bespoke one: a weighted blend of donor districts chosen so that the blend tracks flooded Aceh&amp;rsquo;s pre-tsunami path almost exactly. Formally, it picks non-negative weights $w$ that sum to one to minimize the pre-treatment mismatch:&lt;/p>
&lt;p>$$w^{\ast} = \arg\min_{w}\ \left( X_1 - X_0 w \right)^{\top} V \left( X_1 - X_0 w \right) \quad \text{subject to} \quad w_j \geq 0, \quad \sum_j w_j = 1$$&lt;/p>
&lt;p>where $X_1$ holds treated Aceh&amp;rsquo;s pre-2005 outcomes and $X_0$ the donors&amp;rsquo; (one column per donor). After 2005 the fitted &amp;ldquo;synthetic Aceh&amp;rdquo; is left to run free; the &lt;strong>gap&lt;/strong> between actual and synthetic Aceh is the estimated effect. Before fitting a model, the raw group averages already hint at the answer:&lt;/p>
&lt;p>&lt;img src="python_did_sc_tsunami_gdp_dynamics.png" alt="GDP indexed to 2004 = 100: flooded Aceh dips, then climbs above both control groups.">
&lt;em>Figure 2 of the paper, reproduced: flooded Aceh (orange) ends well above non-flooded Aceh (blue) and the rest of Sumatra (teal).&lt;/em>&lt;/p>
&lt;p>By 2012 the flooded-Aceh index reaches &lt;strong>177&lt;/strong> (2004 = 100), versus 162 for non-flooded Aceh and 142 for the rest of Sumatra. Now the formal version. &lt;code>mlsynth&lt;/code> wants a long panel with one treated unit and a pool of donors, so we collapse the 10 flooded Aceh districts into a single average and pair them with the 76 Rest-of-Sumatra donor districts:&lt;/p>
&lt;pre>&lt;code class="language-python">treated = (district[(district.flooded == 1) &amp;amp; (district.region_group == &amp;quot;Aceh&amp;quot;)]
.groupby(&amp;quot;year&amp;quot;, as_index=False)[&amp;quot;gdp_const_usd_m&amp;quot;].mean()
.assign(unitid=&amp;quot;Aceh (flooded)&amp;quot;).rename(columns={&amp;quot;year&amp;quot;: &amp;quot;time&amp;quot;, &amp;quot;gdp_const_usd_m&amp;quot;: &amp;quot;outcome&amp;quot;}))
donors = (district[district.region_group == &amp;quot;Rest of Sumatra&amp;quot;][[&amp;quot;district_id&amp;quot;, &amp;quot;year&amp;quot;, &amp;quot;gdp_const_usd_m&amp;quot;]]
.rename(columns={&amp;quot;district_id&amp;quot;: &amp;quot;unitid&amp;quot;, &amp;quot;year&amp;quot;: &amp;quot;time&amp;quot;, &amp;quot;gdp_const_usd_m&amp;quot;: &amp;quot;outcome&amp;quot;}))
panel = pd.concat([treated[[&amp;quot;unitid&amp;quot;, &amp;quot;time&amp;quot;, &amp;quot;outcome&amp;quot;]], donors], ignore_index=True)
panel[&amp;quot;treat&amp;quot;] = ((panel.unitid == &amp;quot;Aceh (flooded)&amp;quot;) &amp;amp; (panel.time &amp;gt;= 2005)).astype(int)
out = VanillaSC({&amp;quot;df&amp;quot;: panel, &amp;quot;outcome&amp;quot;: &amp;quot;outcome&amp;quot;, &amp;quot;treat&amp;quot;: &amp;quot;treat&amp;quot;,
&amp;quot;unitid&amp;quot;: &amp;quot;unitid&amp;quot;, &amp;quot;time&amp;quot;: &amp;quot;time&amp;quot;, &amp;quot;display_graphs&amp;quot;: False}).fit().model_dump()
print(f&amp;quot;pre-RMSE = {out['fit_diagnostics']['rmse_pre']:.3f}&amp;quot;)
print(f&amp;quot;ATT = +{out['effects']['att']:.1f} GDP units (+{out['effects']['att_percent']:.1f}%)&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">pre-RMSE = 0.485
ATT = +32.9 GDP units (+18.3%)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="python_did_sc_tsunami_synthetic_control.png" alt="Synthetic control: flooded-Aceh GDP vs a synthetic Aceh built from 76 donors; the shaded gap is the estimated effect.">
&lt;em>Figure 3 reproduced: synthetic Aceh tracks the treated path before 2005 (pre-RMSE 0.485), then the actual line pulls clearly above it.&lt;/em>&lt;/p>
&lt;p>The pre-treatment fit is excellent: a root-mean-squared prediction error of &lt;strong>0.485&lt;/strong> against GDP levels near 200 means synthetic Aceh shadows the real thing almost perfectly before 2005 — which is what licenses us to trust it as a counterfactual afterward. After the tsunami the actual line pulls away, ending &lt;strong>+18.3%&lt;/strong> above its synthetic twin (370.9 vs 295.0 by 2012). The gap plot isolates that divergence:&lt;/p>
&lt;p>&lt;img src="python_did_sc_tsunami_sc_gap.png" alt="The treated-minus-synthetic gap: near zero before 2005, opening up afterward.">
&lt;em>The estimated effect over time: indistinguishable from zero before 2005, then steadily positive.&lt;/em>&lt;/p>
&lt;p>A synthetic control is only as credible as its donor recipe — if one donor carried all the weight, the counterfactual would be fragile. Here the weight is spread:&lt;/p>
&lt;p>&lt;img src="python_did_sc_tsunami_sc_weights.png" alt="Donor weights for synthetic Aceh: a handful of Sumatra districts, none dominant.">
&lt;em>The six largest donor weights. No single district dominates, which makes the counterfactual robust.&lt;/em>&lt;/p>
&lt;p>The top six donors — districts in Jambi, Bangka-Belitung, the Riau Islands, and Bengkulu — together carry about 62% of the weight, and the largest single weight is only 0.13. A counterfactual assembled from many modest contributors is far harder to dismiss than one resting on a single look-alike. Two very different methods — difference-in-differences and synthetic control — now agree: flooded Aceh ended up materially above where it was heading.&lt;/p>
&lt;h2 id="8-spatial-standard-errors-honest-inference-for-a-clustered-treatment">8. Spatial standard errors: honest inference for a clustered treatment&lt;/h2>
&lt;p>Every result so far came with a standard error, and those numbers were not the defaults. Here is why they cannot be. All 10 treated districts sit in &lt;strong>one corner of Sumatra&lt;/strong>:&lt;/p>
&lt;p>&lt;img src="python_did_sc_tsunami_spatial_map.png" alt="Longitude-latitude scatter of all Sumatra districts; the 10 treated units cluster on Aceh&amp;amp;rsquo;s NW coast.">
&lt;em>Every flooded (treated) district, in orange, sits in the far north-west. Their growth shocks are unlikely to be independent.&lt;/em>&lt;/p>
&lt;p>When the treated units are packed together, their year-to-year shocks are not independent draws — a good monsoon, a regional price swing, or the reconstruction boom itself hits them &lt;em>together&lt;/em>. Tobler&amp;rsquo;s first law of geography puts it plainly: near things are more related than distant things. The default (&amp;ldquo;naive&amp;rdquo;) standard error assumes every observation is independent, so it counts more &lt;em>truly independent&lt;/em> information than the data really contain, and reports standard errors that are &lt;strong>too small&lt;/strong>. The first step is to check whether the problem is real, using &lt;strong>Moran&amp;rsquo;s I&lt;/strong> — the spatial analogue of a correlation coefficient — on the regression residuals:&lt;/p>
&lt;pre>&lt;code class="language-python"># residualize growth on flooded + year, then test whether the leftover is spatially clustered
# (full Moran's I + permutation code is in script.py)
print(&amp;quot;Pooled within-year Moran's I = +0.065 (permutation p = 0.003)&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Pooled within-year Moran's I = +0.065 (permutation p = 0.003)
&lt;/code>&lt;/pre>
&lt;p>A Moran&amp;rsquo;s I of &lt;strong>+0.065&lt;/strong> with a permutation p-value of &lt;strong>0.003&lt;/strong> says the residual growth of nearby districts is significantly &lt;em>positively&lt;/em> correlated within a year — the independence assumption behind naive errors is violated, and we must do something about it. The fix is a &lt;strong>Conley spatial-HAC&lt;/strong> standard error: a single &amp;ldquo;sandwich&amp;rdquo; estimator that counts two extra kinds of error correlation — &lt;em>serial&lt;/em> (a district correlated with itself over time) and &lt;em>spatial&lt;/em> (different districts within 100 km in the same year), with the spatial weight fading linearly to zero at the cutoff. The point estimates never change; only the standard errors do. Running the same DiD with four different standard errors side by side:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Coefficient&lt;/th>
&lt;th style="text-align:right">Estimate&lt;/th>
&lt;th style="text-align:right">Naive&lt;/th>
&lt;th style="text-align:right">Clustered&lt;/th>
&lt;th style="text-align:right">Conley&lt;/th>
&lt;th style="text-align:right">&lt;strong>Conley-HAC&lt;/strong>&lt;/th>
&lt;th style="text-align:right">t(HAC)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Pre-tsunami&lt;/td>
&lt;td style="text-align:right">+0.0172&lt;/td>
&lt;td style="text-align:right">0.0144&lt;/td>
&lt;td style="text-align:right">0.0159&lt;/td>
&lt;td style="text-align:right">0.0144&lt;/td>
&lt;td style="text-align:right">0.0159&lt;/td>
&lt;td style="text-align:right">+1.08&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tsunami (2005)&lt;/td>
&lt;td style="text-align:right">−0.0792&lt;/td>
&lt;td style="text-align:right">0.0236&lt;/td>
&lt;td style="text-align:right">0.0258&lt;/td>
&lt;td style="text-align:right">0.0216&lt;/td>
&lt;td style="text-align:right">0.0240&lt;/td>
&lt;td style="text-align:right">−3.30&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Recovery (2006-08)&lt;/strong>&lt;/td>
&lt;td style="text-align:right">&lt;strong>+0.0628&lt;/strong>&lt;/td>
&lt;td style="text-align:right">&lt;strong>0.0146&lt;/strong>&lt;/td>
&lt;td style="text-align:right">0.0244&lt;/td>
&lt;td style="text-align:right">0.0145&lt;/td>
&lt;td style="text-align:right">&lt;strong>0.0244&lt;/strong>&lt;/td>
&lt;td style="text-align:right">&lt;strong>+2.57&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Post-recovery&lt;/td>
&lt;td style="text-align:right">+0.0114&lt;/td>
&lt;td style="text-align:right">0.0109&lt;/td>
&lt;td style="text-align:right">0.0148&lt;/td>
&lt;td style="text-align:right">0.0106&lt;/td>
&lt;td style="text-align:right">0.0146&lt;/td>
&lt;td style="text-align:right">+0.78&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Look at the recovery row. The estimate is &lt;strong>+0.0628&lt;/strong> in every column — the point estimate is rock-solid. But its standard error climbs from &lt;strong>0.0146&lt;/strong> (naive) to &lt;strong>0.0244&lt;/strong> (Conley-HAC), a 1.68× inflation driven mostly by the serial correlation of the multi-year recovery window. That difference is decisive: under the naive error the recovery effect would have a &lt;em>t&lt;/em>-statistic above 4 and look significant at the 1% level (***); under the honest Conley-HAC error its &lt;em>t&lt;/em> is &lt;strong>2.57&lt;/strong>, significant at 5% (**). &lt;strong>The point estimate never moved — only our honesty about its uncertainty did.&lt;/strong> A careless analyst would have overstated the confidence threefold.&lt;/p>
&lt;p>How far should the spatial cutoff reach? Too short and you miss real correlation; too long and you dilute the kernel with distant, weakly-related pairs. Sweeping the cutoff shows the standard error is stable across the 25–100 km range the paper uses, then declines as far-flung pairs water it down:&lt;/p>
&lt;p>&lt;img src="python_did_sc_tsunami_conley_cutoff.png" alt="Conley-HAC standard error of the recovery effect as the distance cutoff widens from 0 to 300 km.">
&lt;em>The recovery effect&amp;rsquo;s standard error is flat through ~100 km (the paper&amp;rsquo;s choice), then drifts down as distant pairs dilute the spatial kernel.&lt;/em>&lt;/p>
&lt;p>(The full Conley sandwich — within-transformation, the two error &amp;ldquo;meats,&amp;rdquo; and the negative-variance clamp — lives in &lt;code>script.py&lt;/code> as &lt;code>conley_did_estimate&lt;/code>; it reproduces &lt;code>pyfixest&lt;/code>&amp;rsquo;s point estimates to four decimals while adding the spatial standard errors &lt;code>pyfixest&lt;/code> cannot compute.)&lt;/p>
&lt;h2 id="9-robustness-placebo-and-heterogeneity">9. Robustness: placebo and heterogeneity&lt;/h2>
&lt;p>Two final checks decide whether to believe the headline. The first is a &lt;strong>placebo&lt;/strong>: if our design is sound, then districts that merely &lt;em>neighbor&lt;/em> a flooded district — but were not themselves flooded — should show &lt;em>no&lt;/em> effect. We drop the truly flooded districts and pretend their neighbors were treated:&lt;/p>
&lt;pre>&lt;code class="language-python">nonflooded = district[district.flooded == 0]
# re-run the dynamic DiD with `neighbour_of_flooded` as the fake treatment (full code in script.py)
&lt;/code>&lt;/pre>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Check&lt;/th>
&lt;th>2005&lt;/th>
&lt;th>Recovery (2006-08)&lt;/th>
&lt;th style="text-align:right">N&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Placebo&lt;/strong> (neighbours of flooded)&lt;/td>
&lt;td>+0.0025 (ns)&lt;/td>
&lt;td>+0.0064 (ns)&lt;/td>
&lt;td style="text-align:right">1,465&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>City (Kota) districts&lt;/td>
&lt;td>−0.0424 (ns)&lt;/td>
&lt;td>+0.1226***&lt;/td>
&lt;td style="text-align:right">295&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Rural (Kabupaten) districts&lt;/td>
&lt;td>−0.0883***&lt;/td>
&lt;td>+0.0479*&lt;/td>
&lt;td style="text-align:right">988&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The placebo finds &lt;strong>nothing&lt;/strong> — every coefficient is small and insignificant (2005 +0.0025, recovery +0.0064). That is exactly what we want: the result is not some artifact of generalized regional spillovers leaking onto whoever happens to be nearby. The effect is specific to the districts the water actually reached.&lt;/p>
&lt;p>The second check looks &lt;em>inside&lt;/em> the average. Splitting the treated districts into &lt;strong>cities&lt;/strong> (Kota) and &lt;strong>rural regencies&lt;/strong> (Kabupaten) reveals very different experiences. Rural districts took the brunt of the 2005 shock (−0.0883, p &amp;lt; 0.01) — agriculture floods badly — with a modest rebound (+0.0479). Cities, by contrast, barely contracted in 2005 (−0.0424, insignificant) but rebounded enormously (+0.1226, p &amp;lt; 0.01), reflecting the urban concentration of reconstruction. One caveat the paper itself flags: there are only &lt;strong>2 flooded city districts&lt;/strong>, so the city column is statistically fragile (few independent clusters) — read its precision, not just its point estimate, with care.&lt;/p>
&lt;h2 id="10-discussion">10. Discussion&lt;/h2>
&lt;p>&lt;strong>What we found.&lt;/strong> Four methods converge on one story. Flooded districts lost about &lt;strong>7.9% of output in 2005&lt;/strong> but grew &lt;strong>6.3 percentage points per year faster in 2006–08&lt;/strong>, ending on a permanently higher path — Aceh&amp;rsquo;s &amp;ldquo;recovery beyond the counterfactual trend.&amp;rdquo; Night-lights confirm it at finer resolution and show the gain concentrated in the worst-hit places. A synthetic control built from 76 donor districts puts flooded Aceh &lt;strong>+18.3%&lt;/strong> above its no-tsunami twin by 2012. The result is robust to a neighbor-district placebo and survives honest, spatially-corrected inference (where it is significant at 5%, not the spuriously confident 1%).&lt;/p>
&lt;p>&lt;strong>So what?&lt;/strong> The substantive lesson is not &amp;ldquo;disasters are good&amp;rdquo; — they are not; 130,000 people died. It is that a &lt;strong>localized catastrophe followed by large, well-governed reconstruction can leave a poor region on a higher long-run trajectory&lt;/strong>. Aceh received aid worth about 150% of its damages, spent through a low-corruption agency, on infrastructure rebuilt &amp;ldquo;better than before.&amp;rdquo; That combination — not the wave — is what bent the growth path upward. For disaster policy, the design lesson is just as important as the result: a credible evaluation needs &lt;em>exogenous&lt;/em> exposure (geography, not choice), a &lt;em>finer-than-national&lt;/em> unit of analysis, and &lt;em>spatially honest&lt;/em> standard errors.&lt;/p>
&lt;p>&lt;strong>Limitations.&lt;/strong> Be appropriately humble. The data are &lt;strong>synthetic&lt;/strong> — calibrated to teach the methods, not to report new facts about Aceh. The treatment group is tiny (10 districts), so point estimates are fragile and standard errors wide; the Aceh-only and city columns are especially imprecise. Identification is &lt;strong>observational&lt;/strong>: parallel trends is an assumption, supported by the flat pre-trend and the null placebo but never proven. And a single, exceptionally well-funded case study travels poorly — Aceh&amp;rsquo;s recovery is evidence about &lt;em>well-governed mega-reconstruction&lt;/em>, not about disaster aid in general.&lt;/p>
&lt;h2 id="11-reproduction-audit-synthetic-data-vs-the-paper">11. Reproduction audit: synthetic data vs the paper&lt;/h2>
&lt;p>Because the data are synthetic, transparency demands that we line our numbers up against the published ones. The data-generating process was tuned to match the paper &lt;em>column by column&lt;/em>; signs and significance agree throughout, and magnitudes land within about 0.005 on the headline cells.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Result&lt;/th>
&lt;th>This synthetic data&lt;/th>
&lt;th>Paper (reported)&lt;/th>
&lt;th style="text-align:center">Sign&lt;/th>
&lt;th style="text-align:center">Significance&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>DiD GDP, 2005 (Table 2, col 1)&lt;/td>
&lt;td>−0.0792***&lt;/td>
&lt;td>≈ −0.081***&lt;/td>
&lt;td style="text-align:center">✓&lt;/td>
&lt;td style="text-align:center">✓&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>DiD GDP, recovery 2006-08 (col 1)&lt;/td>
&lt;td>+0.0628**&lt;/td>
&lt;td>≈ +0.059**&lt;/td>
&lt;td style="text-align:center">✓&lt;/td>
&lt;td style="text-align:center">✓&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>DiD GDP, recovery vs Aceh controls (col 3)&lt;/td>
&lt;td>+0.0310 (ns)&lt;/td>
&lt;td>≈ +0.030**&lt;/td>
&lt;td style="text-align:center">✓&lt;/td>
&lt;td style="text-align:center">partial&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>DiD per-capita, recovery (Table 8)&lt;/td>
&lt;td>+0.0827***&lt;/td>
&lt;td>≈ +0.078***&lt;/td>
&lt;td style="text-align:center">✓&lt;/td>
&lt;td style="text-align:center">✓&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Night-lights, share-of-pop recovery (Table 3)&lt;/td>
&lt;td>+0.0160***&lt;/td>
&lt;td>≈ +0.016***&lt;/td>
&lt;td style="text-align:center">✓&lt;/td>
&lt;td style="text-align:center">✓&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Night-lights, share-of-area recovery (Table 3)&lt;/td>
&lt;td>+1.660***&lt;/td>
&lt;td>≈ +1.75***&lt;/td>
&lt;td style="text-align:center">✓&lt;/td>
&lt;td style="text-align:center">✓&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Night-lights quintiles (Table 4)&lt;/td>
&lt;td>only Q5 significant&lt;/td>
&lt;td>only Q5 significant&lt;/td>
&lt;td style="text-align:center">✓&lt;/td>
&lt;td style="text-align:center">✓&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>City vs rural, 2005 (Table 7)&lt;/td>
&lt;td>rural −0.0883*** / city ns&lt;/td>
&lt;td>rural ≈ −0.098*** / city ns&lt;/td>
&lt;td style="text-align:center">✓&lt;/td>
&lt;td style="text-align:center">✓&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Placebo neighbours (Table 9)&lt;/td>
&lt;td>all ns&lt;/td>
&lt;td>all ns&lt;/td>
&lt;td style="text-align:center">✓&lt;/td>
&lt;td style="text-align:center">✓&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Synthetic control ATT&lt;/td>
&lt;td>+18.3%&lt;/td>
&lt;td>&amp;ldquo;recovery beyond counterfactual&amp;rdquo;&lt;/td>
&lt;td style="text-align:center">✓&lt;/td>
&lt;td style="text-align:center">qualitative&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Two honest gaps. The &lt;strong>Aceh-only column-3&lt;/strong> recovery effect matches the paper in magnitude (+0.031 vs +0.030) but reads as insignificant here, because with the same 10 treated units in every column our synthetic standard errors are similar across columns, whereas the paper&amp;rsquo;s Aceh-only sample is more precise. And the night-lights &lt;strong>quintile&lt;/strong> magnitudes sit on Table 3&amp;rsquo;s (smaller) scale rather than the paper&amp;rsquo;s Table 4 scale — the paper&amp;rsquo;s own Tables 3 and 4 are mutually inconsistent in units, so no single process can reproduce both; we match the pattern (only Q5 significant) exactly. Everywhere else, direction and significance track the paper closely.&lt;/p>
&lt;h2 id="12-summary-and-takeaways">12. Summary and takeaways&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Number to remember&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>2005 output shock&lt;/td>
&lt;td>&lt;strong>−0.0792***&lt;/strong> (≈ −8%)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2006–08 recovery premium&lt;/td>
&lt;td>&lt;strong>+0.0628**&lt;/strong> (per year)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Synthetic-control gap by 2012&lt;/td>
&lt;td>&lt;strong>+18.3%&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Moran&amp;rsquo;s I (spatial autocorrelation)&lt;/td>
&lt;td>&lt;strong>+0.065&lt;/strong> (p = 0.003)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Recovery SE: naive → Conley-HAC&lt;/td>
&lt;td>&lt;strong>0.0146 → 0.0244&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Night-lights recovery (share-of-pop)&lt;/td>
&lt;td>&lt;strong>+0.0160***&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;ul>
&lt;li>&lt;strong>A single &amp;ldquo;after&amp;rdquo; hides the story.&lt;/strong> The pooled 2×2 DiD was an insignificant +0.0125; only splitting time into event-time windows revealed the −0.079 collapse and +0.063 overshoot. When effects evolve, &lt;em>let them&lt;/em>.&lt;/li>
&lt;li>&lt;strong>Triangulate.&lt;/strong> Difference-in-differences, an event study, a dose-response, and a synthetic control all pointed the same way — a far stronger claim than any one method alone.&lt;/li>
&lt;li>&lt;strong>Satellite data unlock localized questions.&lt;/strong> Night-lights gave a finer, exogenous measure that exposed the dose-response (only the worst-hit quintile rebounds) invisible at the district level.&lt;/li>
&lt;li>&lt;strong>Clustered treatment demands honest inference.&lt;/strong> With all treated units in one corner of the map, Conley spatial standard errors were not optional — they downgraded the recovery effect from a spurious *** to an honest **, without touching the point estimate.&lt;/li>
&lt;li>&lt;strong>Mind the small print.&lt;/strong> Ten treated districts make for fragile estimates; the result is about &lt;em>well-governed mega-reconstruction&lt;/em>, on &lt;em>synthetic&lt;/em> data, identified by an &lt;em>assumption&lt;/em>. Strong evidence, stated with the caveats it deserves.&lt;/li>
&lt;li>&lt;strong>Next step.&lt;/strong> Try modern staggered-adoption DiD estimators, add prediction intervals to the synthetic control, or widen the donor pool — each is a natural extension of the toolkit here.&lt;/li>
&lt;/ul>
&lt;h2 id="13-exercises">13. Exercises&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>Drop a donor.&lt;/strong> Re-fit &lt;code>VanillaSC&lt;/code> after excluding the top-weighted donor (&lt;code>JAMBI_D01&lt;/code>). Does the post-2005 gap shrink, hold, or grow relative to the headline +18.3%? What does the answer tell you about the counterfactual&amp;rsquo;s robustness?&lt;/li>
&lt;li>&lt;strong>Cutoff sensitivity.&lt;/strong> Recompute the recovery effect&amp;rsquo;s Conley-HAC standard error at cutoffs of {0, 50, 150, 300} km. At which cutoff, if any, does the recovery effect&amp;rsquo;s significance change? Relate your answer to the cutoff figure in Section 8.&lt;/li>
&lt;li>&lt;strong>Your own event study.&lt;/strong> Estimate the event study a second way with &lt;code>pyfixest&lt;/code>&amp;rsquo;s factor syntax — &lt;code>pf.feols(&amp;quot;gdp_growth ~ i(period, flooded, ref='baseline') | district_id + year&amp;quot;, ...)&lt;/code> — and check that its coefficients match the &lt;code>diff-diff&lt;/code> version to four decimals. Why should two different libraries agree exactly?&lt;/li>
&lt;/ol>
&lt;h2 id="14-references">14. References&lt;/h2>
&lt;ol>
&lt;li>Heger, M. P., &amp;amp; Neumayer, E. (2019). The impact of the Indian Ocean tsunami on Aceh&amp;rsquo;s long-term economic growth. &lt;em>Journal of Development Economics, 141&lt;/em>, 102365. &lt;a href="https://doi.org/10.1016/j.jdeveco.2019.06.008" target="_blank" rel="noopener">https://doi.org/10.1016/j.jdeveco.2019.06.008&lt;/a>&lt;/li>
&lt;li>Abadie, A., Diamond, A., &amp;amp; Hainmueller, J. (2010). Synthetic Control Methods for Comparative Case Studies. &lt;em>Journal of the American Statistical Association, 105&lt;/em>(490), 493–505.&lt;/li>
&lt;li>Conley, T. G. (1999). GMM estimation with cross-sectional dependence. &lt;em>Journal of Econometrics, 92&lt;/em>(1), 1–45.&lt;/li>
&lt;li>Indonesia Database for Policy and Economic Research (INDO-DAPOER) and SUSENAS — World Bank / BPS-Statistics Indonesia. &lt;a href="https://datacatalog.worldbank.org/" target="_blank" rel="noopener">https://datacatalog.worldbank.org/&lt;/a>&lt;/li>
&lt;li>DMSP-OLS Nighttime Lights — NOAA National Centers for Environmental Information. &lt;a href="https://www.ncei.noaa.gov/" target="_blank" rel="noopener">https://www.ncei.noaa.gov/&lt;/a>&lt;/li>
&lt;li>Center for Satellite Based Crisis Information (ZKI), German Aerospace Center (DLR), and the Dartmouth Flood Observatory (inundation maps).&lt;/li>
&lt;li>&lt;code>pyfixest&lt;/code> documentation — &lt;a href="https://pyfixest.org/" target="_blank" rel="noopener">https://pyfixest.org/&lt;/a>&lt;/li>
&lt;li>&lt;code>diff-diff&lt;/code> documentation — &lt;a href="https://github.com/igerber/diff-diff" target="_blank" rel="noopener">https://github.com/igerber/diff-diff&lt;/a>&lt;/li>
&lt;li>&lt;code>mlsynth&lt;/code> documentation — &lt;a href="https://github.com/jgreathouse9/mlsynth" target="_blank" rel="noopener">https://github.com/jgreathouse9/mlsynth&lt;/a>&lt;/li>
&lt;/ol>
&lt;p>&lt;em>This tutorial is a teaching replication built on synthetic data; see the data note in Section 1 and the reproduction audit in Section 11. The companion &lt;code>script.py&lt;/code> regenerates every figure and table.&lt;/em>&lt;/p>
&lt;hr>
&lt;style>
.podcast-overlay {
display: none;
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 9999;
animation: podSlideUp 0.35s ease-out;
}
@keyframes podSlideUp {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
.podcast-overlay.pod-closing {
animation: podSlideDown 0.3s ease-in forwards;
}
@keyframes podSlideDown {
from { transform: translateY(0); }
to { transform: translateY(100%); }
}
.podcast-container {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
padding: 18px 24px 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
box-shadow: 0 -4px 32px rgba(0,0,0,0.5);
border-top: 1px solid rgba(106,155,204,0.2);
}
.podcast-inner {
max-width: 800px;
margin: 0 auto;
}
.podcast-top-row {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 14px;
}
.podcast-icon {
width: 42px;
height: 42px;
background: linear-gradient(135deg, #d97757, #e8956a);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.podcast-icon svg {
width: 22px;
height: 22px;
fill: #fff;
}
.podcast-title-block {
flex: 1;
min-width: 0;
}
.podcast-title-block h4 {
margin: 0 0 1px 0;
color: #f0ece2;
font-size: 14px;
font-weight: 600;
letter-spacing: 0.02em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.podcast-title-block span {
color: #8b9dc3;
font-size: 11px;
}
.podcast-close-btn {
background: none;
border: none;
cursor: pointer;
padding: 6px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
flex-shrink: 0;
}
.podcast-close-btn:hover {
background: rgba(255,255,255,0.1);
}
.podcast-close-btn svg {
width: 20px;
height: 20px;
fill: #8b9dc3;
}
.podcast-progress-wrap {
margin-bottom: 12px;
}
.podcast-time-row {
display: flex;
justify-content: space-between;
font-size: 11px;
color: #8b9dc3;
margin-bottom: 5px;
font-variant-numeric: tabular-nums;
}
.podcast-bar-bg {
width: 100%;
height: 6px;
background: rgba(255,255,255,0.1);
border-radius: 3px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: height 0.15s;
}
.podcast-bar-buffered {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: rgba(106,155,204,0.25);
border-radius: 3px;
transition: width 0.3s;
}
.podcast-bar-progress {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: linear-gradient(90deg, #6a9bcc, #00d4c8);
border-radius: 3px;
transition: width 0.1s linear;
}
.podcast-bar-bg:hover {
height: 10px;
margin-top: -2px;
}
.podcast-controls-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.podcast-transport {
display: flex;
align-items: center;
gap: 8px;
}
.podcast-btn {
background: none;
border: none;
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s;
}
.podcast-btn svg {
fill: #c8d0e0;
transition: fill 0.2s;
}
.podcast-btn:hover svg {
fill: #f0ece2;
}
.podcast-btn-skip {
position: relative;
}
.podcast-btn-skip span {
position: absolute;
font-size: 7px;
font-weight: 700;
color: #c8d0e0;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
margin-top: 1px;
}
.podcast-btn-play {
width: 48px;
height: 48px;
background: linear-gradient(135deg, #d97757, #e8956a);
border-radius: 50%;
box-shadow: 0 3px 12px rgba(217,119,87,0.4);
transition: all 0.2s;
}
.podcast-btn-play:hover {
transform: scale(1.08);
box-shadow: 0 5px 20px rgba(217,119,87,0.5);
}
.podcast-btn-play svg {
fill: #fff;
width: 22px;
height: 22px;
}
.podcast-extras {
display: flex;
align-items: center;
gap: 10px;
}
.podcast-volume-wrap {
display: flex;
align-items: center;
gap: 5px;
}
.podcast-volume-wrap svg {
fill: #8b9dc3;
width: 16px;
height: 16px;
cursor: pointer;
flex-shrink: 0;
}
.podcast-volume-wrap svg:hover {
fill: #c8d0e0;
}
.podcast-volume-slider {
-webkit-appearance: none;
appearance: none;
width: 60px;
height: 4px;
background: rgba(255,255,255,0.12);
border-radius: 2px;
outline: none;
cursor: pointer;
}
.podcast-volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 12px;
height: 12px;
background: #6a9bcc;
border-radius: 50%;
cursor: pointer;
}
.podcast-speed-btn {
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.12);
color: #c8d0e0;
font-size: 11px;
font-weight: 600;
padding: 3px 9px;
border-radius: 12px;
cursor: pointer;
transition: all 0.2s;
font-family: inherit;
min-width: 40px;
text-align: center;
}
.podcast-speed-btn:hover {
background: rgba(106,155,204,0.2);
border-color: #6a9bcc;
color: #f0ece2;
}
.podcast-download-btn {
background: none;
border: 1px solid rgba(255,255,255,0.12);
border-radius: 8px;
padding: 4px 10px;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
color: #8b9dc3;
font-size: 11px;
font-family: inherit;
text-decoration: none;
transition: all 0.2s;
}
.podcast-download-btn:hover {
border-color: #6a9bcc;
color: #f0ece2;
background: rgba(106,155,204,0.1);
}
.podcast-download-btn svg {
width: 14px;
height: 14px;
fill: currentColor;
}
@media (max-width: 600px) {
.podcast-container { padding: 14px 16px 16px; }
.podcast-volume-wrap { display: none; }
.podcast-title-block h4 { font-size: 13px; }
.podcast-extras { gap: 8px; }
}
&lt;/style>
&lt;div class="podcast-overlay" id="podOverlay">
&lt;div class="podcast-container">
&lt;div class="podcast-inner">
&lt;audio id="podAudio" preload="none" src="https://files.catbox.moe/z33l1y.m4a">&lt;/audio>
&lt;div class="podcast-top-row">
&lt;div class="podcast-icon">
&lt;svg viewBox="0 0 24 24">&lt;path d="M12 1a5 5 0 0 0-5 5v4a5 5 0 0 0 10 0V6a5 5 0 0 0-5-5zm0 16a7 7 0 0 1-7-7H3a9 9 0 0 0 8 8.94V22h2v-3.06A9 9 0 0 0 21 10h-2a7 7 0 0 1-7 7z"/>&lt;/svg>
&lt;/div>
&lt;div class="podcast-title-block">
&lt;h4>AI Podcast: Evaluating the Impact of Natural Disasters&lt;/h4>
&lt;span id="podDurationLabel">Click play to load&lt;/span>
&lt;/div>
&lt;button class="podcast-close-btn" onclick="podClose()" title="Close player">
&lt;svg viewBox="0 0 24 24">&lt;path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>&lt;/svg>
&lt;/button>
&lt;/div>
&lt;div class="podcast-progress-wrap">
&lt;div class="podcast-time-row">
&lt;span id="podCurrent">0:00&lt;/span>
&lt;span id="podDuration">0:00&lt;/span>
&lt;/div>
&lt;div class="podcast-bar-bg" id="podBarBg" onclick="podSeek(event)">
&lt;div class="podcast-bar-buffered" id="podBuffered">&lt;/div>
&lt;div class="podcast-bar-progress" id="podProgress">&lt;/div>
&lt;/div>
&lt;/div>
&lt;div class="podcast-controls-row">
&lt;div class="podcast-transport">
&lt;button class="podcast-btn podcast-btn-skip" onclick="podSkip(-15)" title="Back 15s">
&lt;svg width="26" height="26" viewBox="0 0 24 24">&lt;path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/>&lt;/svg>
&lt;span>15&lt;/span>
&lt;/button>
&lt;button class="podcast-btn podcast-btn-play" id="podPlayBtn" onclick="podToggle()" title="Play">
&lt;svg id="podIconPlay" viewBox="0 0 24 24">&lt;path d="M8 5v14l11-7z"/>&lt;/svg>
&lt;svg id="podIconPause" viewBox="0 0 24 24" style="display:none">&lt;path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>&lt;/svg>
&lt;/button>
&lt;button class="podcast-btn podcast-btn-skip" onclick="podSkip(15)" title="Forward 15s">
&lt;svg width="26" height="26" viewBox="0 0 24 24">&lt;path d="M12 5V1l5 5-5 5V7c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6h2c0 4.42-3.58 8-8 8s-8-3.58-8-8 3.58-8 8-8z"/>&lt;/svg>
&lt;span>15&lt;/span>
&lt;/button>
&lt;/div>
&lt;div class="podcast-extras">
&lt;div class="podcast-volume-wrap">
&lt;svg id="podVolIcon" onclick="podMute()" viewBox="0 0 24 24">&lt;path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0 0 14 8.5v7a4.47 4.47 0 0 0 2.5-3.5zM14 3.23v2.06a6.51 6.51 0 0 1 0 13.42v2.06A8.51 8.51 0 0 0 14 3.23z"/>&lt;/svg>
&lt;input type="range" class="podcast-volume-slider" id="podVolume" min="0" max="1" step="0.05" value="0.8">
&lt;/div>
&lt;button class="podcast-speed-btn" id="podSpeedBtn" onclick="podCycleSpeed()" title="Playback speed">1x&lt;/button>
&lt;a class="podcast-download-btn" href="https://files.catbox.moe/z33l1y.m4a" target="_blank" rel="noopener" title="Stream">
&lt;svg viewBox="0 0 24 24">&lt;path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>&lt;/svg>
&lt;/a>
&lt;/div>
&lt;/div>
&lt;/div>
&lt;/div>
&lt;/div>
&lt;script>
(function(){
var overlay = document.getElementById('podOverlay');
var a = document.getElementById('podAudio');
var speeds = [0.75, 1, 1.25, 1.5, 2];
var si = 1;
var opened = false;
function fmt(s){
if(isNaN(s)) return '0:00';
var m=Math.floor(s/60), sec=Math.floor(s%60);
return m+':'+(sec&lt;10?'0':'')+sec;
}
document.addEventListener('click', function(e){
var link = e.target.closest('a.btn-page-header');
if(!link) return;
var text = link.textContent.trim();
if(text.indexOf('AI Podcast') === -1) return;
e.preventDefault();
e.stopPropagation();
overlay.style.display = 'block';
overlay.classList.remove('pod-closing');
if(!opened){
a.preload = 'metadata';
a.load();
opened = true;
}
});
a.volume = 0.8;
a.addEventListener('loadedmetadata', function(){
document.getElementById('podDuration').textContent = fmt(a.duration);
document.getElementById('podDurationLabel').textContent = fmt(a.duration) + ' minutes';
});
a.addEventListener('timeupdate', function(){
document.getElementById('podCurrent').textContent = fmt(a.currentTime);
var pct = a.duration ? (a.currentTime/a.duration)*100 : 0;
document.getElementById('podProgress').style.width = pct+'%';
});
a.addEventListener('progress', function(){
if(a.buffered.length>0){
var pct = (a.buffered.end(a.buffered.length-1)/a.duration)*100;
document.getElementById('podBuffered').style.width = pct+'%';
}
});
a.addEventListener('ended', function(){
document.getElementById('podIconPlay').style.display='';
document.getElementById('podIconPause').style.display='none';
});
window.podToggle = function(){
if(a.paused){a.play();document.getElementById('podIconPlay').style.display='none';document.getElementById('podIconPause').style.display='';}
else{a.pause();document.getElementById('podIconPlay').style.display='';document.getElementById('podIconPause').style.display='none';}
};
window.podSkip = function(s){a.currentTime = Math.max(0,Math.min(a.duration||0,a.currentTime+s));};
window.podSeek = function(e){
var rect = document.getElementById('podBarBg').getBoundingClientRect();
var pct = (e.clientX - rect.left)/rect.width;
a.currentTime = pct * (a.duration||0);
};
window.podMute = function(){
a.muted = !a.muted;
document.getElementById('podVolume').value = a.muted ? 0 : a.volume;
};
window.podCycleSpeed = function(){
si = (si+1) % speeds.length;
a.playbackRate = speeds[si];
document.getElementById('podSpeedBtn').textContent = speeds[si]+'x';
};
window.podClose = function(){
overlay.classList.add('pod-closing');
setTimeout(function(){ overlay.style.display='none'; }, 300);
a.pause();
document.getElementById('podIconPlay').style.display='';
document.getElementById('podIconPause').style.display='none';
};
document.getElementById('podVolume').addEventListener('input', function(){
a.volume = this.value;
a.muted = false;
});
if(window.location.hash === '#podcast-player'){
overlay.style.display = 'block';
a.preload = 'metadata';
a.load();
opened = true;
}
})();
&lt;/script></description></item></channel></rss>