<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Spatial inequality | Carlos Mendez</title><link>https://carlos-mendez.org/category/spatial-inequality/</link><atom:link href="https://carlos-mendez.org/category/spatial-inequality/index.xml" rel="self" type="application/rss+xml"/><description>Spatial inequality</description><generator>Wowchemy (https://wowchemy.com)</generator><language>en-us</language><copyright>Carlos Mendez</copyright><lastBuildDate>Wed, 29 Apr 2026 00:00:00 +0000</lastBuildDate><image><url>https://carlos-mendez.org/media/icon_huedfae549300b4ca5d201a9bd09a3ecd5_79625_512x512_fill_lanczos_center_3.png</url><title>Spatial inequality</title><link>https://carlos-mendez.org/category/spatial-inequality/</link></image><item><title>Beta and Sigma Convergence Across Countries: A Stata Tutorial</title><link>https://carlos-mendez.org/post/stata_convergence/</link><pubDate>Wed, 29 Apr 2026 00:00:00 +0000</pubDate><guid>https://carlos-mendez.org/post/stata_convergence/</guid><description>&lt;h2 id="1-overview">1. Overview&lt;/h2>
&lt;p>Are poorer countries catching up to richer ones? This is one of the most fundamental questions in development economics. If convergence holds, then the vast income gaps we observe today should eventually close on their own as low-income economies grow faster than high-income ones. If it does not hold, then without deliberate policy intervention, the gap will persist &amp;mdash; or even widen.&lt;/p>
&lt;p>For decades, the empirical evidence was discouraging. From 1960 to 2000, there was no sign that poorer countries were growing faster. If anything, richer countries pulled further ahead. But Patel, Sandefur, and Subramanian (2021) documented a striking reversal: since around the year 2000, the world has entered a &lt;strong>new era of unconditional convergence&lt;/strong>, with poorer countries finally growing faster than richer ones &amp;mdash; no controls for institutions, human capital, or policy needed.&lt;/p>
&lt;p>This tutorial walks through the complete convergence toolkit in Stata, from the simplest two-period regression to advanced heatmaps covering every possible time window. We use Penn World Tables 10.0 data for a &lt;strong>balanced panel of 84 countries&lt;/strong> with data available since 1960 and ask: &lt;strong>How fast is convergence happening, and is the global income distribution actually narrowing?&lt;/strong> The answer involves two distinct concepts &amp;mdash; &lt;em>beta convergence&lt;/em> (do poor countries grow faster?) and &lt;em>sigma convergence&lt;/em> (is the income spread shrinking?) &amp;mdash; and the surprising finding that one does not guarantee the other.&lt;/p>
&lt;p>A distinctive feature of this tutorial is its comparative approach to measuring convergence speed. We first show how to extract the speed of convergence from standard OLS output using a simple algebraic conversion, then introduce Nonlinear Least Squares (NLS) as a direct estimation method. Students learn that both approaches yield the same structural parameter &amp;mdash; building intuition before complexity.&lt;/p>
&lt;h3 id="learning-objectives">Learning objectives&lt;/h3>
&lt;ul>
&lt;li>Estimate beta convergence using OLS and interpret the sign of the slope coefficient&lt;/li>
&lt;li>Identify the structural break between the era of divergence (1960&amp;ndash;2000) and the era of convergence (2000&amp;ndash;2019)&lt;/li>
&lt;li>Compute the speed of convergence and half-life from OLS output using an algebraic conversion&lt;/li>
&lt;li>Understand what Nonlinear Least Squares (NLS) is, why it is needed, and how to estimate it in Stata&lt;/li>
&lt;li>Compare OLS-derived and NLS-derived convergence estimates&lt;/li>
&lt;li>Construct rolling-window visualizations for both OLS and NLS to assess robustness&lt;/li>
&lt;li>Measure sigma convergence using the variance of log GDP per capita&lt;/li>
&lt;li>Understand why beta convergence is necessary but not sufficient for sigma convergence&lt;/li>
&lt;li>Build convergence heatmaps to visualize every possible time window&lt;/li>
&lt;/ul>
&lt;h3 id="key-concepts-at-a-glance">Key concepts at a glance&lt;/h3>
&lt;p>The post leans on a small vocabulary repeatedly. The rest of the tutorial assumes you can move between these terms quickly. 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 collapsed for a quick scan. If a later section mentions &amp;ldquo;structural break&amp;rdquo; or &amp;ldquo;half-life&amp;rdquo; and the term feels slippery, this is the section to re-read.&lt;/p>
&lt;p>&lt;strong>1. Beta convergence&lt;/strong> $\lambda$.
The OLS slope coefficient when annualized growth is regressed on log initial income. A &lt;em>negative&lt;/em> $\lambda$ means poorer countries grew faster than richer ones — they &amp;ldquo;caught up&amp;rdquo;. A &lt;em>positive&lt;/em> $\lambda$ means the opposite: divergence.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>Over 2000–2019, $\lambda = -0.00352$ (p = 0.019). Convergence has emerged. Over 1960–2000, $\lambda = +0.00437$ (p = 0.007) — divergence. The full-period (1960–2019) coefficient is essentially zero (0.00057, p = 0.661). The two regimes cancel.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>A catching-up race. If the runner who started at the back is moving faster, the gap to the leader is closing. Beta convergence asks whether poor countries are running faster than rich ones — does the rear runner have more horsepower?&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>2. Sigma convergence&lt;/strong> $\sigma_t^2$.
The variance (or standard deviation) of log GDP per capita across countries at time $t$. Convergence in the sigma sense means $\sigma_t$ is &lt;em>falling&lt;/em> over time — the cross-country distribution of incomes is narrowing.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>In our 84-country sample, the variance of log &lt;code>gdppc&lt;/code> rose from 0.924 in 1960 to 1.918 in 2008 (peak), then eased to 1.764 by 2019. The world &lt;em>did not&lt;/em> sigma-converge over 1960–2019. Beta convergence after 2000 is a necessary precondition for future sigma convergence, not a guarantee.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>A flock of birds. Sigma convergence asks whether the flock is tightening — are the laggards catching the leaders? The flock can briefly tighten even when individual birds are accelerating away from each other.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>3. Speed of convergence&lt;/strong> $\beta$.
The structural parameter from the Barro–Sala-i-Martin model. Different from the OLS $\lambda$. Computed via $\beta = -\ln(1 + \lambda T)/T$, where $T$ is the period length. Bigger $\beta$ means a faster catch-up engine.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>Plugging $\lambda = -0.00352$ and $T = 19$ years into the conversion gives $\beta = 0.00365$. Less than half a percent per year. The catching-up engine, once it turned on after 2000, runs at idle.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>Horsepower of the catch-up engine. The OLS slope $\lambda$ is the speedometer reading. The structural $\beta$ is what the engine can actually deliver — the underlying capacity to close gaps.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>4. Half-life&lt;/strong> $\tau = \ln(2)/\beta$.
The number of years required to close half of the existing income gap at the current convergence speed. A natural reading of $\beta$ on a human time scale.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>With $\beta = 0.00365$, the half-life is &lt;strong>190 years&lt;/strong>. Half of the world&amp;rsquo;s current income gap will close in 190 years if convergence continues at this pace. Compare to the canonical 70-year half-life from cross-country growth regressions of the 1990s; the modern world converges much more slowly.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>Radioactive decay&amp;rsquo;s half-life. After one half-life, half the atoms are gone; after two, three-quarters; and so on. Income-gap half-life works the same way — but at 190 years, even a generation makes only a small dent.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>5. Structural break.&lt;/strong>
A point in time where the convergence coefficient changes its sign or magnitude. Identified by Chow tests, by visual inspection of rolling estimates, or by direct interaction with a year dummy.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>This dataset shows a clear break around 2000. Before: $\lambda = +0.00437$ (divergence). After: $\lambda = -0.00352$ (convergence). The full-period $\lambda$ averages the two regimes and looks like nothing happened — a textbook example of why pooled estimates can mislead.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>A thermostat flipping. Before the flip, the heater is on and the room is warming. After, the cooler is on and the room is cooling. Averaging the two periods reads as &amp;ldquo;no temperature change&amp;rdquo; — the flip is the story.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>6. Nonlinear Least Squares (NLS).&lt;/strong>
A direct estimator of the structural $\beta$ when it appears inside an exponential. Avoids the OLS-to-$\beta$ algebraic conversion. Stata&amp;rsquo;s &lt;code>nl&lt;/code> command fits the nonlinear regression $g_i = (1 - e^{-\beta T})/T \cdot \ln(y_{i,0}) + \varepsilon_i$ in one shot.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>NLS on the 2000–2019 sample returns $\beta = 0.00365$ — the same as the OLS conversion. When the relationship is well-behaved, both routes coincide; the gap is a useful sanity check.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>Direct measurement vs proxy measurement. OLS-then-convert is the proxy: measure something simple ($\lambda$), then compute the structural quantity. NLS is the direct route: measure $\beta$ in one step.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>7. Rolling window.&lt;/strong>
Re-estimate the regression over every possible start year, holding the end year fixed. Each window produces one estimate. The sequence of estimates traces out how convergence has evolved.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>This post&amp;rsquo;s rolling window for $\lambda$ slides the start year from 1960 to 2000 with end year fixed at 2019. The line crosses zero around 1995, becomes solidly negative after 2000, and stabilizes near $-0.0035$ for the most recent windows.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>A sliding microscope across a slide. At each position you take a snapshot. The full sequence of snapshots is the rolling estimate — it shows how the local picture changes as you move along.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>8. Cross-country dispersion&lt;/strong> $\sigma_t$.
The standard deviation of log GDP per capita across countries at time $t$. The &amp;ldquo;$\sigma$&amp;rdquo; in $\sigma$-convergence. Tracks the &lt;em>width&lt;/em> of the world income distribution year by year.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>The variance of log &lt;code>gdppc&lt;/code> rose 90.8% from 0.924 in 1960 to 1.764 in 2019, with a peak of 1.918 in 2008. The dispersion narrative is the opposite of the post-2000 beta-convergence narrative: the rear runner is now faster, but the flock has not yet tightened.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>Standard deviation of incomes in a class. If everyone earns roughly the same, $\sigma$ is small. If a few earn very much and many earn very little, $\sigma$ is large. Sigma convergence asks whether $\sigma$ is shrinking over time.&lt;/p>
&lt;/details>
&lt;/div>
&lt;hr>
&lt;h2 id="2-analytical-roadmap">2. Analytical roadmap&lt;/h2>
&lt;p>The tutorial progresses from the simplest possible convergence test to the most comprehensive. Each section builds on the previous one, adding complexity and robustness.&lt;/p>
&lt;pre>&lt;code class="language-mermaid">graph LR
A[&amp;quot;&amp;lt;b&amp;gt;Simple OLS&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;1960-2019&amp;lt;br/&amp;gt;&amp;lt;i&amp;gt;Section 4&amp;lt;/i&amp;gt;&amp;quot;]
B[&amp;quot;&amp;lt;b&amp;gt;Two Eras&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Structural Break&amp;lt;br/&amp;gt;&amp;lt;i&amp;gt;Section 5&amp;lt;/i&amp;gt;&amp;quot;]
C[&amp;quot;&amp;lt;b&amp;gt;Speed from OLS&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;λ → β conversion&amp;lt;br/&amp;gt;&amp;lt;i&amp;gt;Section 6&amp;lt;/i&amp;gt;&amp;quot;]
D[&amp;quot;&amp;lt;b&amp;gt;NLS Framework&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Direct estimation&amp;lt;br/&amp;gt;&amp;lt;i&amp;gt;Sections 7-9&amp;lt;/i&amp;gt;&amp;quot;]
E[&amp;quot;&amp;lt;b&amp;gt;Rolling Windows&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;λ, then β&amp;lt;br/&amp;gt;&amp;lt;i&amp;gt;Sections 10-11&amp;lt;/i&amp;gt;&amp;quot;]
F[&amp;quot;&amp;lt;b&amp;gt;Sigma&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Convergence&amp;lt;br/&amp;gt;&amp;lt;i&amp;gt;Sections 12-14&amp;lt;/i&amp;gt;&amp;quot;]
G[&amp;quot;&amp;lt;b&amp;gt;Heatmaps&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;OLS &amp;amp; NLS&amp;lt;br/&amp;gt;&amp;lt;i&amp;gt;Section 15&amp;lt;/i&amp;gt;&amp;quot;]
A --&amp;gt; B --&amp;gt; C --&amp;gt; D --&amp;gt; E --&amp;gt; F --&amp;gt; G
style A fill:#6a9bcc,stroke:#141413,color:#fff
style B fill:#d97757,stroke:#141413,color:#fff
style C fill:#00d4c8,stroke:#141413,color:#141413
style D fill:#6a9bcc,stroke:#141413,color:#fff
style E fill:#d97757,stroke:#141413,color:#fff
style F fill:#00d4c8,stroke:#141413,color:#141413
style G fill:#6a9bcc,stroke:#141413,color:#fff
&lt;/code>&lt;/pre>
&lt;p>We start with the simplest OLS test (does initial income predict growth?), then split the sample to reveal a structural break. Next, we show how to extract the speed of convergence from OLS output using a straightforward algebraic conversion. We then introduce Nonlinear Least Squares (NLS) as a direct estimation method and compare the two approaches. A pedagogical introduction to rolling windows starts with the raw OLS coefficient $\lambda$ before progressing to the structural $\beta$, including a full walkthrough of how confidence intervals are constructed and transformed. We then shift from beta to sigma convergence, show why one does not imply the other, and track the income distribution over time. Finally, convergence heatmaps covering every possible time window provide the most comprehensive robustness check.&lt;/p>
&lt;hr>
&lt;h2 id="3-setup-and-data-preparation">3. Setup and data preparation&lt;/h2>
&lt;p>We use the Penn World Tables version 10.0 (Feenstra, Inklaar, and Timmer, 2015), the standard dataset for cross-country income comparisons. It provides expenditure-side real GDP in purchasing power parity (PPP) terms, which makes incomes comparable across countries with different price levels. Following Patel et al. (2021), we exclude oil-producing countries (whose income reflects resource rents rather than productive convergence) and very small countries (population under 1 million). We further restrict the sample to a &lt;strong>balanced panel of 84 countries&lt;/strong> with GDP per capita data available since 1960, ensuring that the same set of countries is used consistently across all sections of the tutorial.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Load Penn World Tables 10.0
use &amp;quot;https://raw.githubusercontent.com/cmg777/starter-academic-v501/master/content/post/stata_convergence/pwt100.dta&amp;quot;, clear
rename countrycode ccode
keep country ccode year pop rgdpe
* Compute GDP per capita (PPP, 2017 US$)
gen gdppc = rgdpe / pop
drop if missing(gdppc) | missing(pop)
* Exclude oil-producing countries (IMF classification, 25 countries)
gen oil = inlist(ccode, &amp;quot;DZA&amp;quot;, &amp;quot;AGO&amp;quot;, &amp;quot;AZE&amp;quot;, &amp;quot;BHR&amp;quot;, &amp;quot;BRN&amp;quot;, &amp;quot;TCD&amp;quot;, &amp;quot;COG&amp;quot;) | ///
inlist(ccode, &amp;quot;ECU&amp;quot;, &amp;quot;GNQ&amp;quot;, &amp;quot;GAB&amp;quot;, &amp;quot;IRN&amp;quot;, &amp;quot;IRQ&amp;quot;, &amp;quot;KAZ&amp;quot;, &amp;quot;KWT&amp;quot;) | ///
inlist(ccode, &amp;quot;NGA&amp;quot;, &amp;quot;OMN&amp;quot;, &amp;quot;QAT&amp;quot;, &amp;quot;RUS&amp;quot;, &amp;quot;SAU&amp;quot;, &amp;quot;TTO&amp;quot;, &amp;quot;TKM&amp;quot;) | ///
inlist(ccode, &amp;quot;ARE&amp;quot;, &amp;quot;VEN&amp;quot;, &amp;quot;YEM&amp;quot;, &amp;quot;LBY&amp;quot;, &amp;quot;TLS&amp;quot;, &amp;quot;SDN&amp;quot;)
drop if oil == 1
drop oil
* Exclude small countries (population &amp;lt; 1 million)
drop if pop &amp;lt; 1
* Restrict to 1960 onwards
drop if year &amp;lt; 1960
* Restrict to balanced panel: countries with data in 1960
bys ccode: egen has1960 = max(year == 1960 &amp;amp; !missing(gdppc))
keep if has1960 == 1
drop has1960
summarize gdppc, detail
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> Real GDP per capita (PPP, 2017 US$)
-------------------------------------------------------------
Percentiles Smallest
1% 498.6677 368.2704
5% 805.8461 425.7048
10% 1048.736 498.6677 Obs 5,040
25% 1927.449 523.0073 Sum of wgt. 5,040
50% 4873.137 Mean 10811.48
Largest Std. dev. 14375.5
75% 14282.34 88681.06
90% 30734.83 89403.9 Variance 2.07e+08
95% 35014 90413.35 Skewness 2.158023
99% 55579.96 102937.7 Kurtosis 8.099127
Number of unique countries: 84
&lt;/code>&lt;/pre>
&lt;p>The cleaned dataset contains 5,040 country-year observations across 84 unique countries spanning 1960&amp;ndash;2019. GDP per capita ranges from \$368 (the poorest country-year) to \$102,938 (the richest), with a median of \$4,873 and a mean of \$10,811. The large gap between mean and median &amp;mdash; reinforced by a skewness of 2.16 &amp;mdash; reflects the heavy right tail of the world income distribution: a small number of very rich countries pull the average far above the typical country. Because we restrict to countries with data available since 1960, this is a balanced panel: the same 84 countries appear in every year, eliminating composition effects that would arise if the sample grew over time.&lt;/p>
&lt;hr>
&lt;h2 id="4-beta-convergence-the-simplest-test">4. Beta convergence: the simplest test&lt;/h2>
&lt;p>Beta convergence &amp;mdash; sometimes called &lt;em>absolute&lt;/em> or &lt;em>unconditional&lt;/em> convergence &amp;mdash; asks a simple question: do countries that start poorer grow faster? If they do, the income gap should eventually close without any need to control for differences in institutions, education, or policy. We test this using ordinary least squares (OLS) regression of the average annual growth rate on the log of initial income. Think of it like a race: if the runners at the back are faster than those at the front, the pack will eventually bunch together.&lt;/p>
&lt;p>The regression equation is:&lt;/p>
&lt;p>$$g_i = \alpha + \lambda \cdot \ln(y_{i,0}) + \varepsilon_i$$&lt;/p>
&lt;p>In words, this says that the annualized growth rate of country $i$ ($g_i$) depends linearly on the log of its initial GDP per capita ($\ln(y_{i,0})$). A negative $\lambda$ means convergence: countries that start with lower income grow faster. A positive or zero $\lambda$ means divergence or no convergence. In the code, $g_i$ corresponds to the variable &lt;code>growth&lt;/code> and $\ln(y_{i,0})$ corresponds to &lt;code>initial&lt;/code>.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Reshape to wide: one row per country
reshape wide gdppc, i(ccode country) j(year)
* Annualized growth rate over 59 years
local s = 2019 - 1960
gen growth = (1/`s') * ln(gdppc2019 / gdppc1960)
* Log initial income
gen initial = ln(gdppc1960)
drop if missing(growth) | missing(initial)
* OLS regression with robust standard errors
reg growth initial, robust
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Linear regression Number of obs = 84
F(1, 82) = 0.19
Prob &amp;gt; F = 0.6606
R-squared = 0.0013
Root MSE = .01502
------------------------------------------------------------------------------
| Robust
growth | Coefficient std. err. t P&amp;gt;|t| [95% conf. interval]
-------------+----------------------------------------------------------------
initial | .0005689 .0012908 0.44 0.661 -.0019988 .0031366
_cons | .0176868 .0112996 1.57 0.121 -.0047917 .0401653
------------------------------------------------------------------------------
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_convergence_scatter_1960_2019.png" alt="Beta convergence test for 1960-2019: scatter plot of annualized growth versus log initial income, showing a flat fitted line with no evidence of convergence.">&lt;/p>
&lt;p>Over the full 1960&amp;ndash;2019 period, the OLS coefficient on initial income is 0.00057 &amp;mdash; positive, tiny, and statistically insignificant (p = 0.661, t = 0.44). The R-squared is just 0.13%, meaning initial income in 1960 has essentially zero predictive power for subsequent growth. The 84 countries grew at an average rate of about 2.2% per year, but this growth was completely unrelated to starting income levels. In the scatter plot, the fitted line is essentially flat. This &amp;ldquo;null result&amp;rdquo; seems to settle the question: no convergence over six decades. But this conclusion is misleading, because it masks a dramatic structural break that the next section reveals.&lt;/p>
&lt;hr>
&lt;h2 id="5-the-structural-break-divergence-vs-convergence">5. The structural break: divergence vs. convergence&lt;/h2>
&lt;p>A single regression over 60 years hides a crucial story. The world changed in the mid-1990s. By splitting the sample at the year 2000, we can see two distinct eras: one where the income gap widened (divergence) and one where it began to close (convergence).&lt;/p>
&lt;pre>&lt;code class="language-stata">* Era of Divergence: 1960 to 2000
gen growth_era1 = (1/40) * ln(gdppc2000 / gdppc1960)
gen initial_era1 = ln(gdppc1960)
reg growth_era1 initial_era1, robust
* Era of Convergence: 2000 to 2019
gen growth_era2 = (1/19) * ln(gdppc2019 / gdppc2000)
gen initial_era2 = ln(gdppc2000)
reg growth_era2 initial_era2, robust
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">--- Era 1: 1960 to 2000 (the 'divergence era') ---
Linear regression Number of obs = 84
Prob &amp;gt; F = 0.0072
R-squared = 0.0436
growth_era1 | Coefficient std. err. t P&amp;gt;|t| [95% conf. interval]
-------------+----------------------------------------------------------------
initial_era1 | .004366 .0015843 2.76 0.007 .0012143 .0075176
--- Era 2: 2000 to 2019 (the 'convergence era') ---
Linear regression Number of obs = 84
Prob &amp;gt; F = 0.0187
R-squared = 0.0688
growth_era2 | Coefficient std. err. t P&amp;gt;|t| [95% conf. interval]
-------------+----------------------------------------------------------------
initial_era2 | -.0035228 .0014686 -2.40 0.019 -.0064442 -.0006013
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_convergence_scatter_two_eras.png" alt="Side-by-side scatter plots comparing the era of divergence (1960-2000, warm orange) with the era of convergence (2000-2019, steel blue), showing the slope flip from positive to negative.">&lt;/p>
&lt;p>The results reveal a dramatic reversal. During 1960&amp;ndash;2000, the OLS coefficient is positive and significant ($\lambda$ = 0.00437, p = 0.007): richer countries grew faster, and the income gap widened. During 2000&amp;ndash;2019, the coefficient flips to negative and significant ($\lambda$ = -0.00352, p = 0.019): poorer countries are now growing faster. The total swing of 0.0079 represents a complete reversal from divergence to convergence. This is what Patel et al. (2021) call &amp;ldquo;the new era of unconditional convergence.&amp;rdquo; But how fast is this convergence happening? The next section shows how to measure speed and half-life using nothing more than the OLS coefficient we already have.&lt;/p>
&lt;hr>
&lt;h2 id="6-speed-of-convergence-and-half-life-from-ols">6. Speed of convergence and half-life from OLS&lt;/h2>
&lt;p>Knowing that convergence exists is only the first step. We also want to know: &lt;strong>how fast are poor countries catching up?&lt;/strong> The OLS coefficient $\lambda$ tells us the direction, but its magnitude depends on the length of the growth period ($s$), making it hard to compare across time windows. We need a &lt;strong>structural parameter&lt;/strong> $\beta$ &amp;mdash; the speed of convergence &amp;mdash; that is invariant to period length.&lt;/p>
&lt;p>The good news: we can extract $\beta$ directly from the OLS coefficient using a simple algebraic conversion. The relationship comes from the Barro and Sala-i-Martin (1992) convergence model, which implies that the OLS coefficient $\lambda$ and the structural speed $\beta$ are related by:&lt;/p>
&lt;p>$$\lambda = -\frac{1 - e^{-\beta s}}{s}$$&lt;/p>
&lt;p>In words, the OLS slope is a nonlinear function of the speed of convergence $\beta$ and the time span $s$. We can solve this equation for $\beta$ in four steps:&lt;/p>
&lt;p>&lt;strong>Step 1.&lt;/strong> Multiply both sides by $s$:&lt;/p>
&lt;p>$$\lambda s = -(1 - e^{-\beta s})$$&lt;/p>
&lt;p>&lt;strong>Step 2.&lt;/strong> Rearrange:&lt;/p>
&lt;p>$$e^{-\beta s} = 1 + \lambda s$$&lt;/p>
&lt;p>&lt;strong>Step 3.&lt;/strong> Take the natural log and solve for $\beta$:&lt;/p>
&lt;p>$$\beta = \frac{-\ln(1 + \lambda s)}{s}$$&lt;/p>
&lt;p>&lt;strong>Step 4.&lt;/strong> Compute the half-life &amp;mdash; how many years to close half the income gap:&lt;/p>
&lt;p>$$\tau = \frac{\ln(2)}{\beta}$$&lt;/p>
&lt;p>The classic benchmark from the convergence literature is $\beta \approx 0.02$ (2% per year) with a half-life of about 35 years (Barro and Sala-i-Martin, 1992; Sala-i-Martin, 1996). But that was for &lt;em>conditional&lt;/em> convergence &amp;mdash; controlling for human capital, institutions, and other factors. Unconditional convergence, which requires no controls, is much slower.&lt;/p>
&lt;pre>&lt;code class="language-stata">* For each period: run OLS, get λ, convert to β = -ln(1+λs)/s, compute half-life
foreach period in &amp;quot;1960-2019&amp;quot; &amp;quot;1960-2000&amp;quot; &amp;quot;1980-2019&amp;quot; &amp;quot;1990-2019&amp;quot; &amp;quot;1995-2019&amp;quot; &amp;quot;2000-2019&amp;quot; {
reg outcome initial_inc, robust
local lambda = _b[initial_inc]
* Convert OLS λ to structural β
local beta = -ln(1 + `lambda' * `s') / `s'
* Half-life
local halflife = ln(2) / `beta'
}
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Speed of Convergence from OLS: λ → β → Half-Life
period lambda_ols beta_ols speed_ols halflife_ols n
1960-2000 .00436597 -.00402402 -.4024021 . 84
1960-2019 .00056889 -.00055955 -.0559547 . 84
1980-2019 .00113216 -.00110461 -.110461 . 84
1990-2019 -.00008191 .00008131 .0081305 8525.66 84
1995-2019 -.00178267 .00181768 .1817678 381.3365 84
2000-2019 -.00352278 .0036462 .3646201 190.0984 84
Benchmarks (Barro &amp;amp; Sala-i-Martin 1992, conditional convergence):
Speed: 2.00% per year
Half-life: 35 years
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_convergence_speed_ols.png" alt="Speed of unconditional convergence from OLS across six periods, with the 2% conditional convergence benchmark shown as a dashed line.">&lt;/p>
&lt;p>The table reveals a clear acceleration. For 1960&amp;ndash;2000, the structural $\beta$ is negative (-0.00402), confirming divergence at a rate of 0.40% per year &amp;mdash; incomes were spreading apart. As the start year moves forward, convergence emerges and strengthens: essentially zero for 1990&amp;ndash;2019, 0.18% per year for 1995&amp;ndash;2019, and 0.36% for 2000&amp;ndash;2019. The 2000&amp;ndash;2019 estimate of $\beta$ = 0.00365 with a half-life of 190 years means that at the current pace, the average developing country would close only half the gap to its steady-state income in nearly two centuries. This is roughly five times slower than the 35-year benchmark for conditional convergence. Unconditional convergence is statistically real, but it is extremely slow.&lt;/p>
&lt;p>We computed these results using nothing more than OLS and an algebraic formula. But there is a more direct way to estimate $\beta$ &amp;mdash; one that does not require any conversion. The next section introduces Nonlinear Least Squares.&lt;/p>
&lt;hr>
&lt;h2 id="7-what-is-nonlinear-least-squares-nls">7. What is Nonlinear Least Squares (NLS)?&lt;/h2>
&lt;p>The OLS-to-$\beta$ conversion in Section 6 works, but it goes &lt;strong>backwards&lt;/strong>: we estimate $\lambda$ first, then convert to $\beta$. Can we estimate $\beta$ &lt;strong>directly&lt;/strong>? Yes &amp;mdash; using Nonlinear Least Squares (NLS).&lt;/p>
&lt;h3 id="why-cant-ols-estimate-beta-directly">Why can&amp;rsquo;t OLS estimate $\beta$ directly?&lt;/h3>
&lt;p>The Barro-Sala-i-Martin (1992) convergence equation is:&lt;/p>
&lt;p>$$\frac{1}{s} \ln\left(\frac{y_{i,t+s}}{y_{i,t}}\right) = \alpha - \frac{1 - e^{-\beta s}}{s} \cdot \ln(y_{i,t}) + \varepsilon_i$$&lt;/p>
&lt;p>The parameter $\beta$ appears &lt;strong>inside an exponential&lt;/strong>: $e^{-\beta s}$. OLS requires that parameters enter the equation &lt;em>linearly&lt;/em> &amp;mdash; as coefficients that multiply variables. Since $\beta$ is trapped inside $\exp()$, OLS cannot estimate it directly. Instead, OLS estimates the entire expression $-\frac{1 - e^{-\beta s}}{s}$ as a single coefficient $\lambda$, and we must back out $\beta$ algebraically.&lt;/p>
&lt;h3 id="what-does-nls-do">What does NLS do?&lt;/h3>
&lt;p>Like OLS, NLS minimizes the sum of squared residuals:&lt;/p>
&lt;p>$$\min_{\alpha, \beta} \sum_{i=1}^{N} \left[ g_i - f(\ln y_{i,0}; \alpha, \beta) \right]^2$$&lt;/p>
&lt;p>But unlike OLS, the function $f()$ can be &lt;strong>any nonlinear function&lt;/strong> of the parameters. NLS uses an iterative algorithm:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Start&lt;/strong> with an initial guess for $\beta$ (e.g., $\beta_0 = 0.02$, the classic benchmark)&lt;/li>
&lt;li>&lt;strong>Compute&lt;/strong> predicted values and residuals given the current guess&lt;/li>
&lt;li>&lt;strong>Adjust&lt;/strong> $\beta$ in the direction that reduces the sum of squared residuals&lt;/li>
&lt;li>&lt;strong>Repeat&lt;/strong> until the improvement is negligible (the algorithm has &amp;ldquo;converged&amp;rdquo;)&lt;/li>
&lt;/ol>
&lt;h3 id="how-to-estimate-nls-in-stata">How to estimate NLS in Stata&lt;/h3>
&lt;p>Stata&amp;rsquo;s &lt;code>nl&lt;/code> command performs NLS estimation. The syntax places the entire nonlinear equation inside parentheses, with parameters in curly braces:&lt;/p>
&lt;pre>&lt;code class="language-stata">* NLS estimation for 2000-2019
local s = 19
nl (outcome = {b0=1} - (1 - exp(-1*{b1=0.02}*`s'))/`s' * initial_inc), vce(robust)
&lt;/code>&lt;/pre>
&lt;p>Reading the syntax:&lt;/p>
&lt;ul>
&lt;li>&lt;code>{b0=1}&lt;/code> &amp;mdash; the intercept $\alpha$, with initial guess = 1&lt;/li>
&lt;li>&lt;code>{b1=0.02}&lt;/code> &amp;mdash; the speed of convergence $\beta$, with initial guess = 0.02 (the 2% benchmark)&lt;/li>
&lt;li>&lt;code>*19&lt;/code> &amp;mdash; $s$ = 19 years (2000 to 2019)&lt;/li>
&lt;li>&lt;code>initial_inc&lt;/code> &amp;mdash; $\ln(y_{2000})$, the independent variable&lt;/li>
&lt;li>&lt;code>vce(robust)&lt;/code> &amp;mdash; heteroskedasticity-robust standard errors&lt;/li>
&lt;/ul>
&lt;pre>&lt;code class="language-text">Nonlinear regression Number of obs = 84
R-squared = 0.0704
Root MSE = .0215709
------------------------------------------------------------------------------
| Robust
outcome | Coefficient std. err. t P&amp;gt;|t| [95% conf. interval]
-------------+----------------------------------------------------------------
/b0 | .0580907 .014098 4.12 0.000 .0300452 .0861362
/b1 | .0036462 .0015739 2.32 0.023 .0005152 .0067772
------------------------------------------------------------------------------
HOW TO READ THE OUTPUT:
/b1 = 0.00365 → This is β (speed of convergence)
Speed = 0.36% per year
Half-life = 190.1 years
COMPARISON with OLS conversion:
OLS λ = -0.00352
OLS → β = -ln(1 + -0.00352 × 19) / 19 = 0.00365
NLS β = 0.00365
Difference = 0.0000000
&lt;/code>&lt;/pre>
&lt;h3 id="why-use-nls">Why use NLS?&lt;/h3>
&lt;p>The &lt;strong>advantage of NLS&lt;/strong> is that standard errors and p-values apply directly to $\beta$ itself. With OLS, the standard error applies to $\lambda$, and transforming it to $\beta$ requires the delta method &amp;mdash; an additional mathematical step. NLS gives you $\beta$, its standard error, and a p-value in one shot. The &lt;strong>advantage of OLS&lt;/strong> is simplicity: it is faster, always converges, and gives identical point estimates after conversion.&lt;/p>
&lt;hr>
&lt;h2 id="8-speed-of-convergence-and-half-life-from-nls">8. Speed of convergence and half-life from NLS&lt;/h2>
&lt;p>Now we estimate $\beta$ directly via NLS for the same six periods as Section 6. The results should match the OLS conversion, confirming that both methods recover the same structural parameter.&lt;/p>
&lt;pre>&lt;code class="language-stata">* NLS estimation for each period
foreach period in &amp;quot;1960-2019&amp;quot; ... &amp;quot;2000-2019&amp;quot; {
nl (outcome = {b0=1} - (1 - exp(-1*{b1=0.00}*`s'))/`s' * initial_inc), vce(robust)
}
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Speed of Convergence from NLS (Direct Estimation of β):
period beta_nls se_nls speed_nls halflife_nls n
1960-2000 -.00402402 .0013502 -.4024021 . 84
1960-2019 -.00055955 .0012508 -.0559547 . 84
1980-2019 -.00110461 .0013178 -.110461 . 84
1990-2019 .00008131 .0014044 .0081305 8525.66 84
1995-2019 .00181768 .0014633 .1817678 381.3365 84
2000-2019 .00364620 .0015739 .3646201 190.0984 84
Benchmarks (Barro &amp;amp; Sala-i-Martin 1992, conditional convergence):
Speed: 2.00% per year
Half-life: 35 years
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_convergence_speed_nls.png" alt="Speed of unconditional convergence from NLS across six periods, with the 2% conditional convergence benchmark shown as a dashed line.">&lt;/p>
&lt;p>The NLS results confirm the same pattern as the OLS conversion. For 2000&amp;ndash;2019, NLS estimates $\beta$ = 0.00365 (SE = 0.00157, p = 0.023), identical to the OLS-derived value. The speed of 0.36% per year and half-life of 190 years are consistent across both methods. Notice that NLS provides a direct p-value for $\beta$: p = 0.023 confirms that unconditional convergence since 2000 is statistically significant at the 5% level. For 1960&amp;ndash;2000, the NLS estimate of $\beta$ = -0.00402 (p = 0.004) confirms statistically significant &lt;em>divergence&lt;/em>.&lt;/p>
&lt;hr>
&lt;h2 id="9-ols-vs-nls-comparison">9. OLS vs NLS comparison&lt;/h2>
&lt;p>How do the two methods compare side by side? The point estimates should be nearly identical, since both minimize the same sum of squared residuals &amp;mdash; the only difference is whether $\beta$ is estimated directly (NLS) or recovered algebraically from $\lambda$ (OLS).&lt;/p>
&lt;pre>&lt;code class="language-text">OLS vs NLS: Side-by-Side Comparison
period lambda_ols beta_ols beta_nls diff speed_ols speed_nls n
1960-2000 .00436597 -.00402402 -.00402402 1.110e-16 -.4024021 -.4024021 84
1960-2019 .00056889 -.00055955 -.00055955 1.388e-17 -.0559547 -.0559547 84
1980-2019 .00113216 -.00110461 -.00110461 4.337e-17 -.110461 -.110461 84
1990-2019 -.00008191 .00008131 .00008131 1.735e-17 .0081305 .0081305 84
1995-2019 -.00178267 .00181768 .00181768 4.337e-17 .1817678 .1817678 84
2000-2019 -.00352278 .00364620 .00364620 4.337e-17 .3646201 .3646201 84
&lt;/code>&lt;/pre>
&lt;p>The differences are on the order of $10^{-17}$ &amp;mdash; effectively zero, confirming that the OLS conversion $\beta = -\ln(1 + \lambda s)/s$ and NLS direct estimation recover the same structural parameter. This equivalence holds because the Barro-Sala-i-Martin equation is a reparameterization of the linear model, not a fundamentally different specification. The choice between OLS and NLS is therefore about &lt;strong>convenience&lt;/strong>, not correctness:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Use OLS&lt;/strong> when you want simplicity, speed, and guaranteed convergence of the estimation algorithm.&lt;/li>
&lt;li>&lt;strong>Use NLS&lt;/strong> when you want standard errors and p-values directly for $\beta$ without applying the delta method.&lt;/li>
&lt;/ul>
&lt;p>Both approaches are correct. In the rolling-window and heatmap sections that follow, we present results from both methods.&lt;/p>
&lt;hr>
&lt;h2 id="10-introduction-to-rolling-windows">10. Introduction to rolling windows&lt;/h2>
&lt;p>So far we have estimated convergence for specific time periods (1960&amp;ndash;2019, 1960&amp;ndash;2000, 2000&amp;ndash;2019). But convergence is not a fixed property &amp;mdash; it evolves over time. A &lt;strong>rolling window&lt;/strong> lets us watch this evolution by estimating a separate regression for every possible start year, always ending in 2019. Each start year produces one regression, one coefficient, and one dot on the plot.&lt;/p>
&lt;pre>&lt;code class="language-mermaid">graph TD
A[&amp;quot;Start = 1960, End = 2019&amp;lt;br/&amp;gt;(59 years)&amp;quot;] --&amp;gt; R1[&amp;quot;OLS → λ₁&amp;quot;]
B[&amp;quot;Start = 1961, End = 2019&amp;lt;br/&amp;gt;(58 years)&amp;quot;] --&amp;gt; R2[&amp;quot;OLS → λ₂&amp;quot;]
C[&amp;quot;Start = 1962, End = 2019&amp;lt;br/&amp;gt;(57 years)&amp;quot;] --&amp;gt; R3[&amp;quot;OLS → λ₃&amp;quot;]
D[&amp;quot;...&amp;quot;] --&amp;gt; R4[&amp;quot;...&amp;quot;]
E[&amp;quot;Start = 2010, End = 2019&amp;lt;br/&amp;gt;(9 years)&amp;quot;] --&amp;gt; R5[&amp;quot;OLS → λ₅₁&amp;quot;]
R1 --&amp;gt; P[&amp;quot;Plot all 51 λ values&amp;lt;br/&amp;gt;against start year&amp;quot;]
R2 --&amp;gt; P
R3 --&amp;gt; P
R4 --&amp;gt; P
R5 --&amp;gt; P
style A fill:#6a9bcc,stroke:#141413,color:#fff
style B fill:#6a9bcc,stroke:#141413,color:#fff
style C fill:#6a9bcc,stroke:#141413,color:#fff
style E fill:#6a9bcc,stroke:#141413,color:#fff
style P fill:#d97757,stroke:#141413,color:#fff
&lt;/code>&lt;/pre>
&lt;p>We start with the simplest rolling window: the raw OLS slope coefficient $\lambda$. This requires nothing beyond the &lt;code>reg&lt;/code> command we already know.&lt;/p>
&lt;h3 id="rolling-ols-lambda">Rolling OLS lambda&lt;/h3>
&lt;p>For each start year from 1960 to 2010, we run the same OLS regression as in Section 4 &amp;mdash; growth on initial income &amp;mdash; and collect the slope coefficient $\lambda$ along with its 95% confidence interval. The CI uses the standard OLS formula:&lt;/p>
&lt;p>$$\lambda \pm t_{N-2, 0.025} \times \text{SE}(\lambda)$$&lt;/p>
&lt;p>where $t_{N-2, 0.025}$ is the critical value from the t-distribution with $N-2$ degrees of freedom (82 for our 84-country sample).&lt;/p>
&lt;pre>&lt;code class="language-stata">* For each start year, run OLS and store lambda + CI
forval startyear = 1960(1)2010 {
local s = 2019 - `startyear'
gen outcome = (1/`s') * ln(gdppc2019 / gdppc`startyear')
gen initial_inc = ln(gdppc`startyear')
reg outcome initial_inc, robust
* Store lambda and its 95% CI
local lambda = _b[initial_inc]
local se = _se[initial_inc]
local lambda_lb = `lambda' - invttail(e(df_r), 0.025) * `se'
local lambda_ub = `lambda' + invttail(e(df_r), 0.025) * `se'
drop outcome initial_inc
}
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Rolling OLS Lambda: Key Findings
startyear lambda se lower upper n
1960 .0005689 .0012908 -.0019988 .0031366 84
1970 .0009814 .0012959 -.0015964 .0035592 84
1980 .0011322 .0013758 -.0016047 .0038690 84
1990 -.0000819 .0014043 -.0028757 .0027119 84
1995 -.0017827 .0014030 -.0045739 .0010085 84
2000 -.0035228 .0014686 -.0064442 -.0006013 84
2005 -.0041503 .0017255 -.0075825 -.0007181 84
2010 -.0030074 .0018375 -.0066619 .0006471 84
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_convergence_rolling_lambda.png" alt="Rolling OLS lambda (raw slope coefficient) from each start year (1960-2010) to 2019, with 95% confidence intervals.">&lt;/p>
&lt;p>The rolling $\lambda$ tells the convergence story in its rawest form. For start years in the 1960s&amp;ndash;1980s, $\lambda$ is positive (above the dashed zero line): richer countries grew faster, meaning divergence. Around 1990, $\lambda$ crosses zero and becomes increasingly negative: poorer countries are now growing faster. The 95% CI bars show that $\lambda$ is statistically distinguishable from zero (the entire CI is below zero) for start years from about 1998 onward. Notice that the sign convention for $\lambda$ is the &lt;strong>opposite&lt;/strong> of $\beta$: negative $\lambda$ means convergence, while positive $\beta$ means convergence.&lt;/p>
&lt;h3 id="from-lambda-to-beta-transforming-the-confidence-interval">From lambda to beta: transforming the confidence interval&lt;/h3>
&lt;p>To convert the rolling $\lambda$ to the structural speed of convergence $\beta$, we apply the formula from Section 6: $\beta = -\ln(1 + \lambda s)/s$. But what about the confidence interval? We cannot simply plug the CI formula for $\lambda$ into the $\beta$ formula, because the transformation is &lt;strong>nonlinear&lt;/strong> and &lt;strong>monotone decreasing&lt;/strong> &amp;mdash; a more negative $\lambda$ (stronger convergence) maps to a &lt;em>larger&lt;/em> positive $\beta$. This means the bounds &lt;strong>flip&lt;/strong> during transformation.&lt;/p>
&lt;p>Let&amp;rsquo;s walk through this with the actual 2000&amp;ndash;2019 estimates:&lt;/p>
&lt;p>&lt;strong>Step 1.&lt;/strong> The OLS CI for $\lambda$ (from the regression output):&lt;/p>
&lt;p>$$\lambda = -0.00352, \quad \text{SE} = 0.00147, \quad s = 19$$&lt;/p>
&lt;p>$$\text{CI for } \lambda: \quad [-0.00352 - 1.989 \times 0.00147, \quad -0.00352 + 1.989 \times 0.00147] = [-0.00645, \quad -0.00060]$$&lt;/p>
&lt;p>&lt;strong>Step 2.&lt;/strong> Transform each bound through $\beta = -\ln(1 + \lambda s)/s$:&lt;/p>
&lt;p>$$\text{Lower } \lambda = -0.00645 \quad \Rightarrow \quad \beta = \frac{-\ln(1 + (-0.00645)(19))}{19} = \frac{-\ln(0.8775)}{19} = \frac{0.1307}{19} = 0.00688$$&lt;/p>
&lt;p>$$\text{Upper } \lambda = -0.00060 \quad \Rightarrow \quad \beta = \frac{-\ln(1 + (-0.00060)(19))}{19} = \frac{-\ln(0.9886)}{19} = \frac{0.01147}{19} = 0.00060$$&lt;/p>
&lt;p>&lt;strong>Step 3.&lt;/strong> Notice the flip: the &lt;strong>lower&lt;/strong> $\lambda$ bound (-0.00645) produced the &lt;strong>upper&lt;/strong> $\beta$ bound (0.00688), and the &lt;strong>upper&lt;/strong> $\lambda$ bound (-0.00060) produced the &lt;strong>lower&lt;/strong> $\beta$ bound (0.00060). So:&lt;/p>
&lt;p>$$\text{CI for } \beta: \quad [0.00060, \quad 0.00688]$$&lt;/p>
&lt;p>This happens because $\beta = -\ln(1 + \lambda s)/s$ is a &lt;strong>monotone decreasing&lt;/strong> function of $\lambda$: as $\lambda$ decreases (becomes more negative), $\beta$ increases (stronger convergence). In the code, we handle this by simply swapping the transformed bounds:&lt;/p>
&lt;pre>&lt;code class="language-stata">* Transform lambda CI to beta CI (bounds flip)
local beta_lb = -ln(1 + `lambda_ub' * `s') / `s' // upper lambda → lower beta
local beta_ub = -ln(1 + `lambda_lb' * `s') / `s' // lower lambda → upper beta
&lt;/code>&lt;/pre>
&lt;p>With this understanding, we can now construct rolling windows for the structural speed $\beta$ using both OLS (with the conversion) and NLS (direct estimation).&lt;/p>
&lt;hr>
&lt;h2 id="11-rolling-beta-convergence-over-time">11. Rolling beta convergence over time&lt;/h2>
&lt;p>We now apply the rolling-window approach to the structural speed of convergence $\beta$, using both methods from Sections 6&amp;ndash;9. For each start year from 1960 to 2010, with end year fixed at 2019, we estimate $\beta$ via:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>OLS:&lt;/strong> estimate $\lambda$, convert to $\beta = -\ln(1+\lambda s)/s$, transform CI bounds (with the flip)&lt;/li>
&lt;li>&lt;strong>NLS:&lt;/strong> estimate $\beta$ directly, CI comes straight from the standard error&lt;/li>
&lt;/ul>
&lt;h3 id="ols-rolling-beta">OLS rolling beta&lt;/h3>
&lt;pre>&lt;code class="language-stata">* For each start year, estimate OLS and convert λ → β
forval startyear = 1960(1)2010 {
local s = 2019 - `startyear'
reg outcome initial_inc, robust
local lambda = _b[initial_inc]
local se = _se[initial_inc]
* Convert lambda to beta
local beta = -ln(1 + `lambda' * `s') / `s'
* Convert CI (bounds flip due to monotone decreasing transformation)
local lambda_lb = `lambda' - invttail(e(df_r), 0.025) * `se'
local lambda_ub = `lambda' + invttail(e(df_r), 0.025) * `se'
local beta_lb = -ln(1 + `lambda_ub' * `s') / `s' // upper λ → lower β
local beta_ub = -ln(1 + `lambda_lb' * `s') / `s' // lower λ → upper β
}
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Rolling OLS Beta Convergence: Key Findings
startyear beta speed_pct halflife n
1960 -.0005596 -.0559555 . 84
1970 -.000968 -.0967977 . 84
1980 -.001105 -.1104993 . 84
1990 .0000813 .0081259 8530.064 84
1995 .0018177 .1817691 381.334 84
2000 .0036462 .3646227 190.100 84
2005 .0044101 .4410113 157.172 84
2010 .0030897 .3089731 224.339 84
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_convergence_rolling_beta_ols.png" alt="Rolling OLS beta coefficient (converted from lambda) from each start year (1960-2010) to 2019, with 95% confidence intervals.">&lt;/p>
&lt;h3 id="nls-rolling-beta">NLS rolling beta&lt;/h3>
&lt;pre>&lt;code class="language-stata">* For each start year, estimate NLS β directly
forval startyear = 1960(1)2010 {
local s = 2019 - `startyear'
nl (outcome = {b0=1} - (1 - exp(-1*{b1=0.00}*`s'))/`s' * initial_inc), vce(robust)
* CI comes directly: beta ± t × SE(beta)
}
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Rolling NLS Beta Convergence: Key Findings
startyear beta speed_pct halflife n
1960 -.0005596 -.0559555 . 84
1970 -.000968 -.0967977 . 84
1980 -.001105 -.1104993 . 84
1990 .0000813 .0081259 8530.064 84
1995 .0018177 .1817691 381.334 84
2000 .0036462 .3646227 190.100 84
2005 .0044101 .4410113 157.172 84
2010 .0030897 .3089731 224.339 84
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_convergence_rolling_beta_nls.png" alt="Rolling NLS beta coefficient from each start year (1960-2010) to 2019, with 95% confidence intervals.">&lt;/p>
&lt;p>The rolling $\beta$ tells a clear story of transition, and the OLS and NLS results are identical in every row. For start years in the 1960s through mid-1980s, $\beta$ is negative &amp;mdash; divergence. It then climbs steadily through the 1990s, crosses zero around 1990, and peaks at 0.00441 for start year 2005 (speed = 0.44%/yr, half-life = 157 years). For the most recent start years (2009&amp;ndash;2010), the coefficient pulls back slightly to 0.00309 (half-life = 224 years), suggesting that convergence may have moderated &amp;mdash; possibly reflecting effects of the 2008 financial crisis. The two figures look identical because the OLS conversion and NLS give the same point estimates; the only difference is that the NLS confidence intervals are derived directly from $\beta$&amp;rsquo;s standard error, while the OLS intervals are transformed from $\lambda$&amp;rsquo;s (with the bound-flipping described in Section 10). With convergence dynamics established, we now turn to a different question: is the actual spread of income across countries narrowing?&lt;/p>
&lt;hr>
&lt;h2 id="12-sigma-convergence-is-the-spread-narrowing">12. Sigma convergence: is the spread narrowing?&lt;/h2>
&lt;p>Beta convergence asks whether poorer countries grow faster. &lt;strong>Sigma convergence&lt;/strong> asks a different question: is the &lt;em>dispersion&lt;/em> of income across countries getting smaller? We measure dispersion using the variance of log GDP per capita. If the variance decreases over time, incomes are bunching together (sigma convergence). If it increases, incomes are spreading apart (sigma divergence).&lt;/p>
&lt;pre>&lt;code class="language-stata">* Variance of log GDP per capita in 1960
gen logy = ln(gdppc)
ci variances logy if year == 1960
* Variance of log GDP per capita in 2019
ci variances logy if year == 2019
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">--- Cross-country dispersion in 1960 ---
Variable | Obs Variance [95% conf. interval]
logy | 84 .9244376 .6969585 1.285409
Std. Dev. = 0.9615
--- Cross-country dispersion in 2019 ---
Variable | Obs Variance [95% conf. interval]
logy | 84 1.763502 1.329631 2.452057
Std. Dev. = 1.3280
Sigma Convergence Test: 1960 vs 2019:
Change in variance: 0.8391 ( 90.8%)
Variance INCREASED: evidence of sigma-DIVERGENCE.
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_convergence_sigma_two_periods.png" alt="Bar chart comparing the variance of log GDP per capita in 1960 versus 2019, with 95% confidence intervals. Both bars use the same 84 countries.">&lt;/p>
&lt;p>The error bars in the figure show 95% confidence intervals for the variance, computed using the &lt;strong>chi-squared distribution&lt;/strong>. Stata&amp;rsquo;s &lt;code>ci variances&lt;/code> command uses the formula:&lt;/p>
&lt;p>$$\text{CI for } \sigma^2 = \left[\frac{(N-1) s^2}{\chi^2_{\alpha/2, N-1}}, \quad \frac{(N-1) s^2}{\chi^2_{1-\alpha/2, N-1}}\right]$$&lt;/p>
&lt;p>where $s^2$ is the sample variance, $N$ = 84 countries, and $\chi^2_{\alpha/2, N-1}$ is the critical value from the chi-squared distribution with $N-1$ = 83 degrees of freedom. This is the standard CI for a variance under the assumption that the data (log GDP per capita) is approximately normally distributed. Unlike the symmetric OLS confidence interval ($\hat{\theta} \pm t \times \text{SE}$), the chi-squared CI is &lt;strong>asymmetric&lt;/strong> &amp;mdash; the upper tail extends further than the lower tail, reflecting the right-skewed nature of the chi-squared distribution. This asymmetry is visible in the error bars: the upper whisker is longer than the lower one.&lt;/p>
&lt;p>Comparing the two endpoints, the variance of log GDP per capita &lt;em>increased&lt;/em> by 90.8%, from 0.924 in 1960 to 1.764 in 2019. The standard deviation rose from 0.96 to 1.33. In 2019, a one-standard-deviation move along the world income distribution corresponds to a roughly 3.8-fold difference in living standards ($e^{1.33}$ = 3.78), up from a 2.6-fold difference in 1960 ($e^{0.96}$ = 2.61). This is clear evidence of sigma &lt;em>divergence&lt;/em> over the full period: the world income distribution widened substantially, even though beta convergence exists in the recent era. How can poorer countries be growing faster &lt;em>and&lt;/em> the income spread be widening at the same time? The next section explains this apparent paradox.&lt;/p>
&lt;hr>
&lt;h2 id="13-why-beta-convergence-is-not-enough">13. Why beta convergence is not enough&lt;/h2>
&lt;p>The seeming contradiction &amp;mdash; beta convergence without sigma convergence &amp;mdash; is not a paradox but a well-known theoretical result. Young, Higgins, and Levy (2008) proved that &lt;strong>beta convergence is necessary but not sufficient for sigma convergence&lt;/strong>. Think of it like a race with wind gusts: even if the runners at the back are faster on average (beta convergence), random gusts can push some runners forward and others backward, keeping the pack spread out (no sigma convergence). The catch-up tendency must be strong enough to overcome the dispersing force of random shocks before the distribution actually narrows.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Decade-by-decade OLS λ and variance of log income
foreach decade in 1960 1970 1980 1990 2000 2010 {
* OLS slope of growth on initial income
reg g_temp i_temp, robust
* Variance of log income at start of decade
summarize logy_temp
}
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> Decade | OLS λ | σ² start | Interpretation
1960-1970 | 0.00594 | 0.9244 | λ≥0: divergence
1970-1980 | 0.00555 | 1.0818 | λ≥0: divergence
1980-1990 | 0.00686 | 1.2893 | λ≥0: divergence
1990-2000 | 0.00882 | 1.5384 | λ≥0: divergence
2000-2010 | -0.00379 | 1.8937 | λ&amp;lt;0: convergence
2010-2019 | -0.00305 | 1.8262 | λ&amp;lt;0: convergence
&lt;/code>&lt;/pre>
&lt;p>The decade-by-decade view confirms the theory in action. The OLS $\lambda$ turns negative (convergence) in 2000&amp;ndash;2010, but the variance of log income does not begin declining until after 2008 &amp;mdash; it peaks at 1.918 in 2008 before falling to 1.826 by 2010 and 1.764 by 2019. This creates an approximately &lt;strong>8-year lag&lt;/strong>: poorer countries started growing faster around 2000, but the overall income distribution only began narrowing around 2008. For nearly a decade, random growth shocks &amp;mdash; economic crises, commodity price swings, conflict &amp;mdash; offset the systematic catch-up tendency before the convergence force became strong enough to dominate. Now that we have established both the existence and the timing of convergence, the next section tracks sigma convergence year by year.&lt;/p>
&lt;hr>
&lt;h2 id="14-sigma-convergence-over-time">14. Sigma convergence over time&lt;/h2>
&lt;p>We now track the dispersion of income every year from 1960 to 2019. Because we use a balanced panel of 84 countries, the sample composition is constant throughout &amp;mdash; there is no need for a separate &amp;ldquo;fixed sample&amp;rdquo; series to control for changing coverage.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Variance of log GDP per capita each year (84-country balanced panel)
forval yr = 1960(1)2019 {
gen logy = ln(gdppc`yr')
ci variances logy
drop logy
}
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Sigma Convergence Over Time: Key Years
year variance n
1960 .9244376 84
1970 1.081847 84
1980 1.289282 84
1990 1.53844 84
2000 1.893675 84
2008 1.918209 84 (peak)
2010 1.826223 84
2019 1.763502 84
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_convergence_sigma_evolution.png" alt="Year-by-year variance of log GDP per capita for the 84-country balanced panel, with 95% confidence intervals. The variance peaks around 2008 and declines thereafter.">&lt;/p>
&lt;p>The error bars at each year are the chi-squared confidence intervals described in Section 12. Because our balanced panel has a constant $N$ = 84, the width of the CI at each year depends only on the variance itself: years with larger variance have wider bars in absolute terms. The bars do not reflect changes in sample size (which is constant throughout).&lt;/p>
&lt;p>The variance series tells a two-act story. &lt;strong>Act one (1960&amp;ndash;2008):&lt;/strong> variance rose almost continuously from 0.924 to a peak of 1.918, an increase of 108% over nearly five decades. &lt;strong>Act two (2008&amp;ndash;2019):&lt;/strong> variance declined from 1.918 to 1.764, a drop of 8.1%. Sigma convergence is a genuinely recent phenomenon, emerging only after the mid-2000s. Even so, the 2019 variance (1.764) remains 91% higher than the 1960 value (0.924). The recent narrowing is real but has barely begun to undo decades of divergence. The next section provides the most comprehensive view of convergence by examining every possible time window.&lt;/p>
&lt;hr>
&lt;h2 id="15-the-convergence-heatmap">15. The convergence heatmap&lt;/h2>
&lt;p>The heatmap is the most comprehensive visualization of convergence dynamics. For every possible start-year and end-year combination from 1960 to 2019, we estimate a separate regression &amp;mdash; approximately 1,770 regressions &amp;mdash; and color-code the result. Blue indicates convergence ($\beta &amp;gt; 0$) and red indicates divergence ($\beta &amp;lt; 0$). We produce two heatmaps: one using the OLS $\lambda \to \beta$ conversion and one using NLS direct estimation, following Patel et al. (2021) Figure 2.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Loop over ALL start/end year combinations
forval startyear = 1960(1)2018 {
forval outcomeyear = `startyear'+1 (1) 2019 {
* OLS: estimate λ, convert to β = -ln(1+λs)/s
reg outcome initial_inc, robust
* NLS: estimate β directly
nl (outcome = {b0=1} - (1 - exp(-1*{b1=0.00}*`s'))/`s' * initial_inc), vce(robust)
}
}
&lt;/code>&lt;/pre>
&lt;h3 id="ols-heatmap">OLS heatmap&lt;/h3>
&lt;p>&lt;img src="stata_convergence_heatmap_ols.png" alt="Convergence heatmap using OLS (lambda to beta conversion): every start-year and end-year combination color-coded by structural beta. Blue indicates convergence in recent periods; red indicates divergence in earlier periods.">&lt;/p>
&lt;h3 id="nls-heatmap">NLS heatmap&lt;/h3>
&lt;p>&lt;img src="stata_convergence_heatmap_nls.png" alt="Convergence heatmap using NLS (direct estimation): every start-year and end-year combination color-coded by NLS beta. The pattern is virtually identical to the OLS heatmap.">&lt;/p>
&lt;p>The pattern is strikingly clear and identical across both methods. The upper-right triangle (periods ending in 2010&amp;ndash;2019) is dominated by blue, while the central and lower-left regions (periods ending before 2000) are dominated by red. The deepest red ($\beta &amp;lt; -0.0055$) is concentrated in short windows during the 1970s&amp;ndash;1980s, when divergence was strongest. The deepest blue ($\beta &amp;gt; 0.0035$) appears for windows ending in 2015&amp;ndash;2019 and starting after 1990. The transition from red to blue occurs gradually along diagonals, with the crossover point moving from the upper right toward the center. This confirms that the convergence finding is not an artifact of choosing specific endpoints &amp;mdash; it appears robustly across many time windows. Along the diagonal (short intervals), estimates are noisier due to shorter periods. The two heatmaps are virtually indistinguishable, providing a final confirmation that OLS conversion and NLS direct estimation yield the same results.&lt;/p>
&lt;hr>
&lt;h2 id="16-discussion">16. Discussion&lt;/h2>
&lt;p>We set out to ask whether the world has entered a new era of unconditional convergence and how fast it is happening. The evidence is clear: &lt;strong>yes, unconditional convergence is real since approximately 2000, but it is very slow.&lt;/strong>&lt;/p>
&lt;p>The speed of convergence for 2000&amp;ndash;2019 is 0.36% per year ($\beta$ = 0.00365, p = 0.023), with a half-life of 190 years &amp;mdash; both OLS conversion and NLS direct estimation give this identical result. To put this in perspective, at this pace, a country currently at one-tenth of US income per capita would need nearly two centuries to close just half the gap &amp;mdash; not to catch up entirely, but merely to halve the distance. This is roughly five times slower than the classic 2%/year benchmark for conditional convergence (Barro and Sala-i-Martin, 1992), which controls for human capital, institutions, and savings rates. The fact that unconditional convergence exists &lt;em>at all&lt;/em> is remarkable, but its pace should temper optimism about automatic catch-up.&lt;/p>
&lt;p>The sigma convergence results add an important nuance. Even though poorer countries have been growing faster since around 2000, the actual spread of world incomes only began narrowing after 2008 &amp;mdash; an 8-year lag. And even with this recent narrowing, the 2019 income distribution is still 91% wider than in 1960. A policymaker looking at these results would conclude that convergence forces alone are far too slow to eliminate global poverty or close income gaps within any reasonable planning horizon. Active investment in education, infrastructure, institutions, and technology transfer remains essential.&lt;/p>
&lt;p>A methodological contribution of this tutorial is demonstrating that the OLS $\lambda \to \beta$ conversion and NLS direct estimation are algebraically equivalent, producing identical point estimates. The choice between methods is one of convenience: OLS for simplicity, NLS for direct inference on $\beta$. Students can start with the familiar OLS framework and add NLS when they need standard errors for the structural parameter.&lt;/p>
&lt;hr>
&lt;h2 id="17-summary-and-next-steps">17. Summary and next steps&lt;/h2>
&lt;h3 id="key-takeaways">Key takeaways&lt;/h3>
&lt;ol>
&lt;li>&lt;strong>No convergence over 1960&amp;ndash;2019 as a whole&lt;/strong> (OLS $\lambda$ = 0.00057, p = 0.661), but this null result conceals a dramatic structural break around the year 2000.&lt;/li>
&lt;li>&lt;strong>Unconditional convergence since 2000&lt;/strong> at a speed of 0.36% per year ($\beta$ = 0.00365, half-life = 190 years, N = 84, p = 0.023). This is statistically significant but five times slower than conditional convergence.&lt;/li>
&lt;li>&lt;strong>OLS and NLS give identical results.&lt;/strong> The algebraic conversion $\beta = -\ln(1 + \lambda s)/s$ recovers the same structural parameter as direct NLS estimation, confirming both methods are valid.&lt;/li>
&lt;li>&lt;strong>Sigma convergence lags beta convergence by ~8 years.&lt;/strong> The income variance peaked at 1.918 in 2008 and declined 8.1% by 2019. Random growth shocks delayed the narrowing of the distribution even as poorer countries grew faster on average.&lt;/li>
&lt;li>&lt;strong>The income distribution remains 91% wider than in 1960.&lt;/strong> Despite post-2008 sigma convergence, the 2019 variance of log GDP per capita (1.764) far exceeds the 1960 value (0.924). A one-standard-deviation move in the 2019 distribution corresponds to a 3.8-fold difference in living standards.&lt;/li>
&lt;/ol>
&lt;h3 id="limitations">Limitations&lt;/h3>
&lt;ul>
&lt;li>The analysis uses a balanced panel of 84 countries with data available since 1960, excluding 40 countries that entered PWT coverage after 1960. These excluded countries are disproportionately from Africa and small island states, so the results may not generalize to the full set of developing countries.&lt;/li>
&lt;li>The convergence regressions explain very little of the cross-country growth variation (R-squared from 0.001 to 0.069). The research question is about the sign and significance of the relationship, not prediction.&lt;/li>
&lt;li>The most recent rolling-window estimates (start years 2009&amp;ndash;2010) show some moderation in convergence speed, but shorter growth windows also mean more noise.&lt;/li>
&lt;li>Results depend on the choice of income measure (expenditure-side real GDP at chained PPPs) and sample restrictions (excluding oil producers and small countries).&lt;/li>
&lt;/ul>
&lt;h3 id="next-steps">Next steps&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Conditional convergence:&lt;/strong> Add controls for human capital, institutional quality, and savings rates to see whether the speed approaches the 2% benchmark.&lt;/li>
&lt;li>&lt;strong>Club convergence:&lt;/strong> Test whether countries converge to different steady states rather than a single global equilibrium (Phillips and Sul, 2007).&lt;/li>
&lt;li>&lt;strong>Within-country convergence:&lt;/strong> Apply the same framework to regions within a country to study subnational income dynamics.&lt;/li>
&lt;li>&lt;strong>Post-COVID update:&lt;/strong> Extend the analysis past 2019 to assess whether the pandemic disrupted or accelerated convergence.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="18-exercises">18. Exercises&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Change the breakpoint.&lt;/strong> Instead of splitting at the year 2000, try splitting at 1990 or 1995. Does the convergence coefficient in the recent era change? At what breakpoint does the coefficient first become significantly negative?&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Conditional convergence.&lt;/strong> Add log population and a measure of education (years of schooling, available in PWT 10.0 as &lt;code>hc&lt;/code>) as controls to the NLS specification. How much does the speed of convergence increase? Does the half-life approach the 35-year conditional benchmark?&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Alternative samples.&lt;/strong> Re-run the 2000&amp;ndash;2019 NLS regression including oil producers. Then try including small countries. How sensitive is the convergence result to these sample restrictions?&lt;/p>
&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="19-references">19. References&lt;/h2>
&lt;ol>
&lt;li>&lt;a href="https://doi.org/10.1016/j.jdeveco.2021.102687" target="_blank" rel="noopener">Patel, D., Sandefur, J., and Subramanian, A. (2021). The New Era of Unconditional Convergence. &lt;em>Journal of Development Economics&lt;/em>, 152, 102687.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://doi.org/10.1086/261816" target="_blank" rel="noopener">Barro, R. J. and Sala-i-Martin, X. (1992). Convergence. &lt;em>Journal of Political Economy&lt;/em>, 100(2), 223&amp;ndash;251.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://doi.org/10.1257/aer.20130954" target="_blank" rel="noopener">Feenstra, R. C., Inklaar, R., and Timmer, M. P. (2015). The Next Generation of the Penn World Table. &lt;em>American Economic Review&lt;/em>, 105(10), 3150&amp;ndash;3182.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://doi.org/10.2307/2235375" target="_blank" rel="noopener">Sala-i-Martin, X. (1996). The Classical Approach to Convergence Analysis. &lt;em>The Economic Journal&lt;/em>, 106(437), 1019&amp;ndash;1036.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://doi.org/10.1111/j.1538-4616.2008.00148.x" target="_blank" rel="noopener">Young, A. T., Higgins, M. J., and Levy, D. (2008). Sigma Convergence versus Beta Convergence: Evidence from U.S. County-Level Data. &lt;em>Journal of Money, Credit and Banking&lt;/em>, 40(5), 1083&amp;ndash;1093.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.rug.nl/ggdc/productivity/pwt/" target="_blank" rel="noopener">Penn World Tables 10.0 &amp;ndash; University of Groningen&lt;/a>&lt;/li>
&lt;/ol></description></item><item><title>Converging to Convergence: Understanding the Main Ideas of the Convergence Literature</title><link>https://carlos-mendez.org/post/stata_convergence2/</link><pubDate>Wed, 29 Apr 2026 00:00:00 +0000</pubDate><guid>https://carlos-mendez.org/post/stata_convergence2/</guid><description>&lt;h2 id="1-overview">1. Overview&lt;/h2>
&lt;p>For decades, one of the most important questions in economics has been: are poor countries catching up to rich ones? The answer has changed dramatically over time. In the 1960s, richer countries actually grew &lt;em>faster&lt;/em> than poorer ones &amp;mdash; a pattern called &lt;strong>divergence&lt;/strong>. By the 2000s, this had reversed: poor countries were growing significantly faster, a phenomenon known as &lt;strong>unconditional convergence&lt;/strong> (also called absolute convergence). What caused this shift?&lt;/p>
&lt;p>This tutorial walks through the key ideas of the convergence literature by reproducing the main findings of Kremer, Willis, and You (2021), &amp;ldquo;Converging to Convergence.&amp;rdquo; The paper provides an elegant explanation: the world has &amp;ldquo;converged to convergence&amp;rdquo; because growth correlates &amp;mdash; the policies, institutions, and human capital variables that predict economic growth &amp;mdash; have themselves converged across countries. As poor countries improved their institutions and policies, the gap between unconditional convergence (a simple comparison of growth rates across income levels) and conditional convergence (controlling for institutions) closed. The central tool for understanding this is the &lt;strong>omitted variable bias (OVB) formula&lt;/strong>, which decomposes exactly &lt;em>how much&lt;/em> each growth correlate contributes to the convergence gap.&lt;/p>
&lt;p>We use the authors' replication dataset, which combines Penn World Table 10.0 GDP data with over 50 institutional, policy, and cultural variables for approximately 160 countries from 1960 to 2017. The analysis is entirely &lt;strong>descriptive&lt;/strong> &amp;mdash; we document cross-country correlations and trends, but do not make causal claims.&lt;/p>
&lt;h3 id="learning-objectives">Learning objectives&lt;/h3>
&lt;ul>
&lt;li>Understand beta-convergence and sigma-convergence and how to test for each&lt;/li>
&lt;li>Track the trend in convergence over time using year-interacted regressions&lt;/li>
&lt;li>Decompose convergence into contributions from income quartiles and geographic regions&lt;/li>
&lt;li>Apply the omitted variable bias (OVB) formula to explain why unconditional convergence emerged&lt;/li>
&lt;li>Distinguish between correlate-income slopes (delta), growth-correlate slopes (lambda), and their product&lt;/li>
&lt;li>Evaluate whether the 1990s growth regression literature holds up as an out-of-sample test&lt;/li>
&lt;/ul>
&lt;h3 id="analytical-roadmap">Analytical roadmap&lt;/h3>
&lt;p>The diagram below shows the logical progression of the tutorial. We first establish the facts, then explain them.&lt;/p>
&lt;pre>&lt;code class="language-mermaid">graph LR
A[&amp;quot;&amp;lt;b&amp;gt;Establish the&amp;lt;br/&amp;gt;Facts&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;i&amp;gt;Sections 3--6&amp;lt;/i&amp;gt;&amp;quot;]
B[&amp;quot;&amp;lt;b&amp;gt;Correlate&amp;lt;br/&amp;gt;Convergence&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;i&amp;gt;Section 7&amp;lt;/i&amp;gt;&amp;quot;]
C[&amp;quot;&amp;lt;b&amp;gt;OVB&amp;lt;br/&amp;gt;Framework&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;i&amp;gt;Sections 8--10&amp;lt;/i&amp;gt;&amp;quot;]
D[&amp;quot;&amp;lt;b&amp;gt;The&amp;lt;br/&amp;gt;Punchline&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;i&amp;gt;Section 11&amp;lt;/i&amp;gt;&amp;quot;]
A --&amp;gt; B
B --&amp;gt; C
C --&amp;gt; D
style A fill:#6a9bcc,stroke:#141413,color:#fff
style B fill:#d97757,stroke:#141413,color:#fff
style C fill:#00d4c8,stroke:#141413,color:#141413
style D fill:#141413,stroke:#d97757,color:#fff
&lt;/code>&lt;/pre>
&lt;p>We start by documenting the emergence of convergence (scatter plots, rolling coefficients, sigma-convergence, quartile decompositions). Then we show that growth correlates have themselves converged. Finally, the OVB framework links these two facts, revealing that the gap between unconditional and conditional convergence closed because growth regression coefficients for policy variables collapsed.&lt;/p>
&lt;h3 id="key-concepts-at-a-glance">Key concepts at a glance&lt;/h3>
&lt;p>The post leans on a small vocabulary repeatedly. The rest of the tutorial assumes you can move between these terms quickly. 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 collapsed for a quick scan. If a later section mentions &amp;ldquo;OVB decomposition&amp;rdquo; or &amp;ldquo;lambda flattening&amp;rdquo; and the term feels slippery, this is the section to re-read.&lt;/p>
&lt;p>&lt;strong>1. Beta convergence: unconditional vs conditional&lt;/strong> $\beta$ vs $\beta^&lt;em>$.
The unconditional $\beta$ is the slope of growth on log initial income with no controls. The conditional $\beta^&lt;/em>$ is the same slope after controlling for growth correlates. Both negative means poorer countries are catching up — even those with similar institutions.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>For the &lt;code>polity2&lt;/code> sample in 2005, the unconditional $\beta = -0.767$ and the conditional $\beta^* = -0.807$. The two are within 0.04 of each other. Twenty years earlier (1985), the gap was 0.44 — institutions explained most of the apparent divergence.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>&amp;ldquo;Catching up overall&amp;rdquo; vs &amp;ldquo;catching up given the same institutions&amp;rdquo;. Imagine two race tracks: one mixes all runners, the other separates them by training regimen. If both show poor runners gaining, the catching-up is real.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>2. Sigma convergence&lt;/strong> $\sigma_t$.
The cross-country standard deviation of log GDP per capita at year $t$. Tracks the &lt;em>width&lt;/em> of the world income distribution. A narrowing distribution is sigma convergence.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>$\sigma$ rose from 0.947 in 1960 to 1.217 in 2000 (peak), then eased to 1.173 by 2017. Income dispersion is no longer widening but has not yet narrowed substantially. Beta convergence has just begun the work that sigma convergence will eventually reflect.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>A flock of birds. Sigma asks whether the flock is tightening. Beta tells you which birds are flying faster. They are related but not the same: the laggard birds can accelerate without the flock yet looking tighter.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>3. OVB decomposition&lt;/strong> $\beta - \beta^* = \delta \cdot \lambda$.
The omitted-variable-bias identity. The gap between unconditional and conditional convergence equals the product of two slopes: $\delta$ (correlate-on-income) and $\lambda$ (correlate-on-growth). When the gap closes, at least one of $\delta$ or $\lambda$ must have shrunk.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>For the &lt;code>polity2&lt;/code> example, the gap closed from 0.440 (1985) to 0.040 (2005). The product $\delta \cdot \lambda$ went from $0.440$ to $0.040$. Inspecting the components: $\lambda$ collapsed from 0.891 to 0.183 — the growth regression coefficient flattened.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>Double-entry bookkeeping. The total bias on the convergence books equals the sum of two ledger entries. If the total drops, one of the ledger entries must have dropped — and the OVB identity tells you which one.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>4. Growth correlates.&lt;/strong>
The policy and institutional variables economists used to put on the right-hand side of growth regressions in the 1990s: inflation, investment, schooling, openness, political rights, rule of law, and so on. Each is meant to capture a &amp;ldquo;fundamental&amp;rdquo; of long-run growth.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>This post tracks &lt;code>polity2&lt;/code>, &lt;code>FH_political_rights&lt;/code>, &lt;code>investment&lt;/code>, &lt;code>inflation&lt;/code>, and &lt;code>barrolee2060&lt;/code> (schooling) as the headline correlates. Each has a story in the post: &lt;code>investment&lt;/code> shows the strongest cross-country correlation with income; political rights show the most pronounced correlate-income flattening.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>Ingredients in a recipe. Some recipes call for many ingredients (high-inflation, low-savings, weak-rights), others for few. Growth correlates are the ingredients we suspect explain why some economies cook up more output than others.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>5. Correlate–income slope&lt;/strong> $\delta$.
The regression of a correlate on log income. How much richer countries have &lt;em>more&lt;/em> of the correlate. A large positive $\delta$ for &lt;code>polity2&lt;/code> means richer countries are more democratic.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>For &lt;code>polity2&lt;/code>, $\delta$ has stayed around 0.5–0.6 over decades. Richer countries have always tended to be more democratic. The correlate-income slope is &lt;em>not&lt;/em> what flattened in the 1990s–2000s; it is the other half of the OVB product.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>How well-stocked the kitchen is. A wealthy kitchen has more ingredients on hand. The correlate-income slope $\delta$ measures the kitchen-stocking gradient: as a country gets richer, how much better-stocked does its kitchen become?&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>6. Growth-regression slope&lt;/strong> $\lambda$.
The coefficient on a correlate when growth is regressed on the correlate (controlling for log income). How much each correlate contributes to growth, holding initial income fixed. A large $\lambda$ means the correlate matters; a small $\lambda$ means it does not.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>For &lt;code>polity2&lt;/code> in 1985, $\lambda = 0.891$. By 2005, $\lambda = 0.183$. The growth payoff to good political institutions has flattened dramatically over two decades.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>How much each ingredient matters in the recipe. A pinch of saffron used to be transformative. Now everyone uses it; the marginal effect is much smaller. Lambda is &amp;ldquo;marginal effect of the ingredient&amp;rdquo;; not &amp;ldquo;amount of ingredient on hand&amp;rdquo;.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>7. Lambda flattening.&lt;/strong>
The empirical observation that growth-regression coefficients $\lambda$ on short-run correlates have collapsed since the 1990s. The collapse is the &lt;em>real&lt;/em> story: it is what made unconditional convergence emerge.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>Across the post&amp;rsquo;s correlate set, $\lambda$ for several short-run policy variables fell from 0.5–1.0 (1985) to 0.1–0.3 (2005). The longer-run correlates (like schooling) are stickier. The lambda flattening shrinks the OVB product and brings $\beta$ and $\beta^*$ into alignment.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>Ingredients losing their punch as kitchens equalize. When every kitchen has good knives and a working oven, the kitchens with the &lt;em>best&lt;/em> knives no longer dominate. Lambda flattening is that universal-baseline effect.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>8. Quartile and regional decomposition.&lt;/strong>
A descriptive break-down of beta convergence by initial-income quartile or by region. Asks: which subgroup is doing the catching-up? A few quartiles or regions usually do most of the work.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>This post&amp;rsquo;s regional decomposition (Sub-Saharan Africa, East Asia, Latin America, OECD, etc.) attributes most of the post-2000 catch-up to East Asia and parts of South Asia. Within-quartile, the bottom two quartiles drive the recent convergence; the top two have stayed flat.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>Breaking the average down by income tier. The class average improved; was it because everyone improved, or because the bottom of the class caught up? Quartile decomposition answers exactly that question.&lt;/p>
&lt;/details>
&lt;/div>
&lt;hr>
&lt;h2 id="2-setup-and-data-loading">2. Setup and data loading&lt;/h2>
&lt;p>We begin by loading the Kremer et al. (2021) replication dataset, which has already been cleaned to exclude very small countries (population below 200,000) and resource-dependent economies (natural resource rents above 75% of GDP). We also merge regional classifications from the World Development Indicators.&lt;/p>
&lt;pre>&lt;code class="language-stata">clear all
set more off
set seed 42
set scheme s2color
* Load the main dataset
use &amp;quot;https://raw.githubusercontent.com/cmg777/starter-academic-v501/master/content/post/stata_convergence2/main_data.dta&amp;quot;, clear
* Display panel structure
codebook country_id, compact
tab year if loggdp != ., missing
summarize loggdp loggdp_growth_10
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Panel structure:
country_id: 174 unique countries, range 2--218
Years covered: 1960 to 2017
Countries with GDP data: 160
Key income variables:
Variable | Obs Mean Std. dev. Min Max
-----------+---------------------------------------------------------
loggdp | 8,328 8.712741 1.186573 5.368557 12.61823
loggdp_g~10| 6,888 1.962031 2.78512 -12.33628 22.12787
&lt;/code>&lt;/pre>
&lt;p>The dataset is an unbalanced panel of 160 countries observed over 58 years (1960&amp;ndash;2017), with 8,328 country-year observations containing GDP data. The panel expands in two jumps &amp;mdash; from 109 countries in 1960 to 137 in 1970 (decolonization) and to 160 in 1990 (post-Soviet states). Average log GDP per capita is 8.71, with a standard deviation of 1.19 log points reflecting enormous cross-country income inequality. The 10-year forward-looking growth rate &amp;mdash; the main outcome variable &amp;mdash; averages 1.96% per year with a range from -12.3% (economic collapses) to 22.1% (growth miracles).&lt;/p>
&lt;p>We then define variable groups following the paper&amp;rsquo;s classification of growth correlates into four categories.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Solow fundamentals (steady-state determinants)
local solow investment population_growth barrolee2060
* Short-run correlates (policies/institutions that can change quickly)
local short_run polity2 FH_political_rights FH_civil_liberties ///
pri_inv gov_spending inflation WDI_credit credit /* +19 more */
* Long-run correlates (geography and historical institutions)
local long_run population_1900 legor_uk legor_fr logem4 meantemp /* +7 more */
* Culture (Hofstede cultural dimensions)
local culture VSM_power_dist VSM_individualism VSM_masculinity /* +3 more */
&lt;/code>&lt;/pre>
&lt;p>The classification matters because the paper&amp;rsquo;s central finding is that &lt;strong>short-run correlates&lt;/strong> behave very differently from &lt;strong>Solow fundamentals&lt;/strong> in growth regressions. We will return to this distinction in Sections 9 and 10.&lt;/p>
&lt;hr>
&lt;h2 id="3-has-the-world-been-converging-scatter-plots-by-decade">3. Has the world been converging? Scatter plots by decade&lt;/h2>
&lt;p>The simplest test for convergence is visual: plot 10-year economic growth against initial income level and check the slope. &lt;strong>Beta-convergence&lt;/strong> &amp;mdash; named after the slope coefficient $\beta$ in the regression of growth on income &amp;mdash; means that poorer countries grow faster. A negative slope indicates convergence; a positive slope indicates divergence.&lt;/p>
&lt;p>We run this regression for each decade separately, from the 1960s through 2007.&lt;/p>
&lt;pre>&lt;code class="language-stata">foreach yr in 1960 1970 1980 1990 2000 2007 {
quietly reg loggdp_growth_10 loggdp if year == `yr', robust
* Store coefficients for each decade
}
* Combine 6 scatter panels into one figure
graph combine G1 G2 G3 G4 G5 G6, rows(2) cols(3) ///
graphregion(color(white)) ///
title(&amp;quot;Income Convergence by Decade&amp;quot;, size(medium))
graph export &amp;quot;stata_convergence2_scatter_by_decade.png&amp;quot;, replace width(2400)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_convergence2_scatter_by_decade.png" alt="Six-panel scatter plot showing 10-year growth versus log GDP per capita for each decade from the 1960s through 2007. The fitted slope shifts from positive (divergence) to steeply negative (convergence).">&lt;/p>
&lt;pre>&lt;code class="language-text">Beta by decade:
decade | beta se pval n_obs
--------+----------------------------------------
1960 | 0.532 0.191 0.006 109
1970 | -0.075 0.292 0.799 137
1980 | 0.106 0.246 0.667 137
1990 | -0.127 0.220 0.564 160
2000 | -0.651 0.168 0.000 160
2007 | -0.764 0.146 0.000 160
&lt;/code>&lt;/pre>
&lt;p>The scatter plots reveal a dramatic historical reversal. In the 1960s, $\beta = +0.53$ (p = 0.006), meaning richer countries grew significantly faster &amp;mdash; a world of divergence. Through the 1970s&amp;ndash;1990s, the coefficient hovered near zero, statistically indistinguishable from zero in every decade. By the 2000s, a strongly negative $\beta = -0.65$ (p &amp;lt; 0.001) emerged, deepening to -0.76 by 2007. This shift from divergence to convergence &amp;mdash; spanning roughly 1.3 percentage points of GDP growth per log point of income &amp;mdash; represents a fundamental transformation in the global growth landscape.&lt;/p>
&lt;p>But is this trend systematic, or just an artifact of picking the right decades? The next section tests whether convergence has been &lt;em>trending&lt;/em> continuously.&lt;/p>
&lt;hr>
&lt;h2 id="4-the-trend-in-beta-convergence">4. The trend in beta-convergence&lt;/h2>
&lt;p>Rather than comparing snapshots, we track the convergence coefficient &lt;strong>continuously&lt;/strong> over time. This is the paper&amp;rsquo;s key innovation: studying the &lt;em>trend&lt;/em> in convergence, not just testing whether convergence exists at a single point in time.&lt;/p>
&lt;p>The specification interacts log GDP per capita with year dummies, giving a separate $\beta_t$ for each year:&lt;/p>
&lt;p>$$\text{Growth}_{i,t \to t+10} = \beta_t \cdot \log(\text{GDPpc}_{i,t}) + \mu_t + \varepsilon_{i,t}$$&lt;/p>
&lt;p>In words, this equation says that 10-year forward-looking growth is a linear function of initial income, with a slope $\beta_t$ that varies by year and year fixed effects $\mu_t$ absorbing common shocks. A negative $\beta_t$ means convergence in year $t$; a positive $\beta_t$ means divergence.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Estimate year-by-year beta coefficients using year-interacted regression
areg loggdp_growth_10 c.loggdp#i.year, absorb(year) robust cluster(country_id)
* Extract coefficients and plot with 95% CI
twoway (rarea ci_upper ci_lower year, fcolor(&amp;quot;106 155 204%30&amp;quot;) lwidth(none)) ///
(line beta year, lcolor(&amp;quot;106 155 204&amp;quot;) lwidth(medthick)) ///
(function y = 0, range(1960 2009) lcolor(&amp;quot;217 119 87&amp;quot;) lpattern(dash)), ///
xtitle(&amp;quot;Year&amp;quot;) ytitle(&amp;quot;Beta-convergence coefficient&amp;quot;) ///
title(&amp;quot;Trend in Beta-Convergence, 1960-2007&amp;quot;, size(medium))
graph export &amp;quot;stata_convergence2_beta_trend.png&amp;quot;, replace width(2400)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_convergence2_beta_trend.png" alt="Rolling beta-convergence coefficient from 1960 to 2008 with 95% confidence interval. The coefficient trends downward from about +0.5 in the 1960s to about -0.8 by 2008, crossing zero around the late 1990s.">&lt;/p>
&lt;p>We also estimate a linear trend specification (Table 1) to test whether the downward movement is statistically significant.&lt;/p>
&lt;pre>&lt;code class="language-text">Table 1: Converging to Convergence
-------------------------------------------------
(1) (2) (3)
Pooled Trend By Decade
-------------------------------------------------
loggdp -0.270** 0.449**
(0.118) (0.224)
loggdp_X~r -0.025***
(0.006)
loggdp~60s 0.532***
(0.191)
loggdp~00s -0.651***
(0.168)
loggdp~07s -0.764***
(0.146)
-------------------------------------------------
N 863 863 863
Year FE Y Y Y
-------------------------------------------------
&lt;/code>&lt;/pre>
&lt;p>The trend coefficient of &lt;strong>-0.025 per year&lt;/strong> (p &amp;lt; 0.01) confirms that convergence has been a systematic trend, not just a snapshot. The convergence coefficient has decreased by 0.025 per year since 1960 &amp;mdash; or equivalently, has shifted by about 1.2 percentage points per half-century. The rolling year-by-year beta (Figure 2) shows this was not smooth: $\beta$ fluctuated around zero through the 1970s&amp;ndash;1980s, then dropped sharply through the 1990s and 2000s, becoming consistently and significantly negative after 1999.&lt;/p>
&lt;p>This raises a natural follow-up question: if countries are growing at rates that should reduce income gaps (beta-convergence), has income dispersion actually &lt;em>narrowed&lt;/em>?&lt;/p>
&lt;hr>
&lt;h2 id="5-sigma-convergence-is-income-dispersion-narrowing">5. Sigma-convergence: is income dispersion narrowing?&lt;/h2>
&lt;p>&lt;strong>Beta-convergence&lt;/strong> (poorer countries growing faster) and &lt;strong>sigma-convergence&lt;/strong> (declining cross-country income dispersion) are related but distinct concepts. Beta-convergence is &lt;em>necessary&lt;/em> but not &lt;em>sufficient&lt;/em> for sigma-convergence &amp;mdash; like a river flowing downhill, catch-up growth must be strong enough to overcome random shocks that push countries apart. We measure sigma as the standard deviation of log GDP per capita across countries in each year.&lt;/p>
&lt;pre>&lt;code class="language-stata">preserve
collapse (sd) sigma = loggdp, by(year)
twoway (line sigma year, lcolor(&amp;quot;106 155 204&amp;quot;) lwidth(medthick)), ///
xtitle(&amp;quot;Year&amp;quot;) ytitle(&amp;quot;SD of log GDP per capita&amp;quot;) ///
title(&amp;quot;Sigma-Convergence: Cross-Country Income Dispersion&amp;quot;, size(medium))
graph export &amp;quot;stata_convergence2_sigma.png&amp;quot;, replace width(2400)
restore
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_convergence2_sigma.png" alt="Standard deviation of log GDP per capita across countries from 1960 to 2017. Sigma rises steadily from about 0.95 in 1960 to a peak of 1.22 around 2000, then declines.">&lt;/p>
&lt;pre>&lt;code class="language-text">Sigma (SD of log GDP per capita):
Year | Sigma
-------+---------
1960 | 0.947
1970 | 1.086
1980 | 1.139
1990 | 1.146
2000 | 1.217 (peak)
2010 | 1.173
2017 | 1.173
&lt;/code>&lt;/pre>
&lt;p>The standard deviation of log GDP per capita rose steadily from 0.95 in 1960 to a peak of 1.22 in 2000, reflecting four decades of widening global inequality. After 2000, sigma began declining, reaching 1.13 by 2015 before ticking back up slightly to 1.17 in 2017. This pattern is consistent with beta-convergence leading sigma-convergence by roughly a decade: beta turned significantly negative around 1999, and sigma began declining shortly after 2000. The lag occurs because sigma-convergence requires catch-up growth fast enough to offset the random shocks that push countries apart &amp;mdash; a more demanding condition than simple beta-convergence.&lt;/p>
&lt;p>Now that we have established the headline fact &amp;mdash; convergence emerged around 2000 &amp;mdash; we need to understand &lt;em>who&lt;/em> is driving it. Is it catch-up growth at the bottom, stagnation at the top, or both?&lt;/p>
&lt;hr>
&lt;h2 id="6-who-drives-convergence">6. Who drives convergence?&lt;/h2>
&lt;h3 id="61-income-quartile-decomposition">6.1 Income quartile decomposition&lt;/h3>
&lt;p>We decompose the convergence trend by sorting countries into income quartiles and tracking each group&amp;rsquo;s average growth rate over time. This reveals whether convergence reflects catch-up growth by the poorest countries, a growth slowdown among the richest, or both.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Compute mean 10-year growth by income quartile and year
xtile quartile = loggdp, nq(4)
collapse (mean) mean_growth = loggdp_growth_10, by(quartile year)
* Plot 4 lines, one per quartile
twoway (line mean_growth year if quartile == 1, lcolor(&amp;quot;255 141 61&amp;quot;)) ///
(line mean_growth year if quartile == 2, lcolor(&amp;quot;246 199 0&amp;quot;)) ///
(line mean_growth year if quartile == 3, lcolor(&amp;quot;146 195 51&amp;quot;)) ///
(line mean_growth year if quartile == 4, lcolor(&amp;quot;106 155 204&amp;quot;)), ///
legend(label(1 &amp;quot;Q1 (Poorest)&amp;quot;) label(2 &amp;quot;Q2&amp;quot;) label(3 &amp;quot;Q3&amp;quot;) label(4 &amp;quot;Q4 (Richest)&amp;quot;))
graph export &amp;quot;stata_convergence2_growth_by_quartile.png&amp;quot;, replace width(2400)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_convergence2_growth_by_quartile.png" alt="Mean 10-year growth by income quartile over time. The richest quartile shifts from fastest-growing in the 1960s to slowest-growing by 2007, while the poorest quartile accelerates.">&lt;/p>
&lt;pre>&lt;code class="language-text">Mean 10-year growth by quartile:
Q1(Poorest) Q2 Q3 Q4(Richest)
1960 2.46 2.20 2.93 3.49
1985 0.49 0.99 1.46 1.76
2000 3.31 3.60 3.29 1.26
2007 3.02 2.18 1.60 0.31
&lt;/code>&lt;/pre>
&lt;p>Convergence since 2000 is driven by both catch-up growth at the bottom AND a growth slowdown at the top. In the 1960s, the richest quartile (Q4) grew fastest at 3.49% per year, while the poorest (Q1) grew at only 2.46%. By 2007, this ordering had completely reversed: Q1 grew at 3.02% while Q4 grew at just 0.31%. The richest quartile experienced the most dramatic decline, going from the fastest-growing group in the 1960s to the slowest by the 2000s. Think of it like a marathon where the leaders have slowed down while the runners at the back have sped up &amp;mdash; the pack is compressing from both directions.&lt;/p>
&lt;h3 id="62-regional-robustness">6.2 Regional robustness&lt;/h3>
&lt;p>A natural concern is that convergence might be driven by a single region &amp;mdash; perhaps it disappears if we exclude China and the rest of Asia. We check by estimating the rolling beta trend while excluding each major region one at a time.&lt;/p>
&lt;pre>&lt;code class="language-stata">* For each region, estimate beta trend excluding that region
foreach reg in 1 2 3 4 {
areg loggdp_growth_10 c.loggdp#i.year if region_group != `reg', ///
absorb(year) robust cluster(country_id)
* Extract and store coefficients
}
graph export &amp;quot;stata_convergence2_beta_excluding_regions.png&amp;quot;, replace width(2400)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_convergence2_beta_excluding_regions.png" alt="Rolling beta trend with each of four major regions excluded one at a time. Convergence is robust to excluding any single region.">&lt;/p>
&lt;p>Convergence holds when excluding any single region. Excluding Sub-Saharan Africa makes convergence even stronger ($\beta$ reaches -1.25 by 2000), consistent with Africa&amp;rsquo;s economic difficulties during the 1970s&amp;ndash;1990s dragging the global average toward zero. Excluding Europe/North America yields a somewhat weaker but still clearly negative trend. The finding is genuinely global.&lt;/p>
&lt;p>We have now established the core empirical facts: convergence emerged around 2000, it reflects forces on both ends of the income distribution, and it is not driven by any single region. The next step is to ask &lt;em>why&lt;/em>. The paper&amp;rsquo;s key insight is that the answer lies in the behavior of growth correlates.&lt;/p>
&lt;hr>
&lt;h2 id="7-have-growth-correlates-converged">7. Have growth correlates converged?&lt;/h2>
&lt;p>The 1990s growth literature identified dozens of variables that predict economic growth: investment, education, democracy, governance, financial development, inflation, and many others. A key insight of Kremer et al. (2021) is that these variables are not static &amp;mdash; they have been converging across countries just like income itself.&lt;/p>
&lt;p>We test this by regressing the change in each correlate (from 1985 to 2015) on its initial level in 1985. A negative slope means &lt;strong>correlate convergence&lt;/strong> &amp;mdash; countries that started with worse values experienced the largest improvements.&lt;/p>
&lt;pre>&lt;code class="language-stata">* For each correlate: change = beta * initial_level + epsilon
* Example for Polity 2 (democracy score)
gen change = 100 * ((polity2_2015 - polity2_1985) / 30)
reg change polity2_1985, robust
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_convergence2_correlate_convergence.png" alt="Six-panel scatter showing convergence in six representative growth correlates: population growth, investment, education, democracy, government spending, and financial credit. All six show negative slopes indicating convergence.">&lt;/p>
&lt;pre>&lt;code class="language-text">Correlate beta-convergence (change 1985-2015 regressed on level 1985):
Variable | beta se n_obs pval
-----------------------+------------------------------------
investment | -2.978 0.395 118 0.000
population_growth | -1.530 0.277 172 0.000
polity2 | -2.029 0.168 131 0.000
FH_political_rights | -1.394 0.206 139 0.000
gov_spending | -1.611 0.305 114 0.000
inflation | -3.070 0.103 128 0.000
barrolee2060 | -0.158 0.105 136 0.136
&lt;/code>&lt;/pre>
&lt;p>Growth correlates have themselves been converging since 1985. The strongest convergence is in inflation ($\beta = -3.07$), investment ($\beta = -2.98$), and democracy as measured by Polity 2 ($\beta = -2.03$) &amp;mdash; all significant at the 0.1% level. This means that the cross-country distribution of policies and institutions has been compressing: countries with initially worse institutions experienced the largest improvements. The notable exception is Barro-Lee education ($\beta = -0.16$, p = 0.14), where convergence is slower and not statistically significant.&lt;/p>
&lt;p>This finding is crucial because it connects two previously separate literatures. The convergence literature asks whether poor countries are catching up in &lt;em>income&lt;/em>. The institutions literature documents whether countries are catching up in &lt;em>policies&lt;/em>. The answer to both is yes &amp;mdash; and the next sections show these are not coincidences but are linked by the omitted variable bias formula.&lt;/p>
&lt;hr>
&lt;h2 id="8-the-ovb-framework-why-does-convergence-emerge">8. The OVB framework: why does convergence emerge?&lt;/h2>
&lt;p>This section introduces the central analytical framework of the paper. The &lt;strong>omitted variable bias (OVB) formula&lt;/strong> provides an exact decomposition of the gap between unconditional convergence (a simple comparison of growth and income) and conditional convergence (controlling for institutions). Understanding this decomposition is the key to answering &lt;em>why&lt;/em> unconditional convergence emerged.&lt;/p>
&lt;h3 id="81-three-regressions">8.1 Three regressions&lt;/h3>
&lt;p>Consider any growth correlate &amp;mdash; say, democracy (Polity 2 score). Three regressions define the framework:&lt;/p>
&lt;p>&lt;strong>Regression 1 &amp;mdash; Unconditional convergence ($\beta$):&lt;/strong> Regress growth on income alone.&lt;/p>
&lt;p>$$\text{Growth}_i = \alpha + \beta \cdot \log(\text{GDPpc}_i) + \varepsilon_i$$&lt;/p>
&lt;p>If $\beta &amp;lt; 0$, poorer countries grow faster (convergence). If $\beta &amp;gt; 0$, richer countries grow faster (divergence).&lt;/p>
&lt;p>&lt;strong>Regression 2 &amp;mdash; Conditional convergence ($\beta^{\ast}$):&lt;/strong> Regress growth on income &lt;em>and&lt;/em> the correlate.&lt;/p>
&lt;p>$$\text{Growth}_i = \alpha + \beta^{\ast} \cdot \log(\text{GDPpc}_i) + \lambda \cdot \text{Inst}_i + \varepsilon_i$$&lt;/p>
&lt;p>$\beta^{\ast}$ is the convergence coefficient &lt;em>controlling for&lt;/em> institutions. The coefficient $\lambda$ captures how much the correlate predicts growth, holding income constant. In the 1990s, $\beta^{\ast}$ was typically negative (conditional convergence) even when $\beta$ was not (no unconditional convergence).&lt;/p>
&lt;p>&lt;strong>Regression 3 &amp;mdash; Correlate-income slope ($\delta$):&lt;/strong> Regress the correlate on income.&lt;/p>
&lt;p>$$\text{Inst}_i = \nu + \delta \cdot \log(\text{GDPpc}_i) + u_i$$&lt;/p>
&lt;p>$\delta$ captures how strongly the correlate correlates with income. If $\delta &amp;gt; 0$, richer countries have better institutions &amp;mdash; the &amp;ldquo;modernization hypothesis.&amp;rdquo;&lt;/p>
&lt;h3 id="82-the-key-equation">8.2 The key equation&lt;/h3>
&lt;p>The OVB formula links these three regressions with an exact algebraic identity:&lt;/p>
&lt;p>$$\beta - \beta^{\ast} = \delta \times \lambda$$&lt;/p>
&lt;p>In words, this says that the gap between unconditional and conditional convergence equals the product of two things: (1) how much richer countries have better institutions ($\delta$), and (2) how much those institutions predict growth ($\lambda$). This is not an approximation &amp;mdash; it is an algebraic identity that holds exactly in any linear regression.&lt;/p>
&lt;p>&lt;strong>Why this matters.&lt;/strong> The decomposition tells us there are exactly three ways unconditional convergence can change over time:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Conditional convergence itself changes&lt;/strong> ($\beta^{\ast}$ shifts) &amp;mdash; e.g., technology diffusion accelerates&lt;/li>
&lt;li>&lt;strong>Correlate-income slopes change&lt;/strong> ($\delta$ shifts) &amp;mdash; e.g., rich and poor countries become equally democratic&lt;/li>
&lt;li>&lt;strong>Growth regression coefficients change&lt;/strong> ($\lambda$ shifts) &amp;mdash; e.g., democracy stops predicting growth&lt;/li>
&lt;/ol>
&lt;p>The paper&amp;rsquo;s central finding: it is mainly &lt;strong>mechanism 3&lt;/strong> &amp;mdash; $\lambda$ flattened &amp;mdash; that explains the emergence of unconditional convergence.&lt;/p>
&lt;h3 id="83-worked-example-democracy-polity-2">8.3 Worked example: democracy (Polity 2)&lt;/h3>
&lt;p>Before generalizing, we build intuition with one correlate. Polity 2 measures democracy on a scale from -10 (autocracy) to +10 (full democracy), normalized by its 1985 standard deviation so that coefficients are in comparable units.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Normalize polity2 by its 1985 SD
gen polity2_norm = polity2 / `sd_polity2'
* --- Period: 1985 ---
* Regression 1 (Unconditional):
reg loggdp_growth_10 loggdp if year == 1985 &amp;amp; polity2_norm != ., robust
* Regression 2 (Conditional):
reg loggdp_growth_10 loggdp polity2_norm if year == 1985, robust
* Regression 3 (Income-Institution slope):
reg polity2_norm loggdp if year == 1985, robust
* Repeat for 2005
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">---- Period: 1985 ----
Regression 1 (Unconditional): beta = 0.328 (SE = 0.199, N = 124)
Regression 2 (Conditional): beta* = -0.111, lambda = 0.891
Regression 3 (Income-Inst): delta = 0.494
OVB DECOMPOSITION:
beta - beta* = 0.440 (actual gap)
delta x lambda = 0.440 (predicted by OVB formula)
delta = 0.494 (richer countries more democratic?)
lambda = 0.891 (democracy predicts growth?)
---- Period: 2005 ----
Regression 1 (Unconditional): beta = -0.767 (SE = 0.149, N = 147)
Regression 2 (Conditional): beta* = -0.807, lambda = 0.183
Regression 3 (Income-Inst): delta = 0.216
OVB DECOMPOSITION:
beta - beta* = 0.040 (actual gap)
delta x lambda = 0.040 (predicted by OVB formula)
delta = 0.216 (richer countries more democratic?)
lambda = 0.183 (democracy predicts growth?)
COMPARISON ACROSS TIME:
delta (1985) = 0.494 --&amp;gt; delta (2005) = 0.216 [STABLE]
lambda (1985) = 0.891 --&amp;gt; lambda (2005) = 0.183 [SHRANK]
gap (1985) = 0.440 --&amp;gt; gap (2005) = 0.040 [CLOSED]
&lt;/code>&lt;/pre>
&lt;p>This single example encapsulates the paper&amp;rsquo;s entire argument. In 1985, unconditional $\beta$ was +0.33 (divergence), but controlling for democracy revealed conditional convergence at $\beta^{\ast} = -0.11$. The gap of 0.44 is exactly predicted by $\delta \times \lambda = 0.494 \times 0.891 = 0.44$ &amp;mdash; the OVB formula holds exactly because it is an algebraic identity. By 2005, $\lambda$ collapsed from 0.89 to 0.18 &amp;mdash; democracy went from being a powerful growth predictor (one SD higher Polity 2 associated with 0.89% faster annual growth) to a near-zero predictor. The resulting gap shrank from 0.44 to 0.04 &amp;mdash; a &lt;strong>91% reduction&lt;/strong>. The correlate-income slope $\delta$ also fell (from 0.49 to 0.22), but the primary driver was the collapse in $\lambda$.&lt;/p>
&lt;p>Think of it like a recipe that calls for two ingredients. The gap ($\delta \times \lambda$) was large in 1985 because both ingredients were present: richer countries had much better democracy ($\delta$ large) &lt;em>and&lt;/em> democracy strongly predicted growth ($\lambda$ large). By 2005, the second ingredient ($\lambda$) had nearly vanished &amp;mdash; it no longer mattered for growth predictions whether a country was democratic or not &amp;mdash; so the recipe produced almost nothing.&lt;/p>
&lt;p>Now we generalize: does this pattern hold across &lt;em>all&lt;/em> growth correlates, not just democracy?&lt;/p>
&lt;hr>
&lt;h2 id="9-are-correlate-income-slopes-stable-delta">9. Are correlate-income slopes stable? (Delta)&lt;/h2>
&lt;p>The OVB formula has two components: $\delta$ (the correlate-income slope) and $\lambda$ (the growth-correlate slope). We examine each in turn. If $\delta$ &amp;mdash; the relationship between income and institutions &amp;mdash; has changed dramatically, that could explain the closing gap. But the paper finds that $\delta$ has been remarkably stable.&lt;/p>
&lt;p>For each correlate, we compute $\delta$ in 1985 and in 2015, then scatter one against the other. Points on the 45-degree line mean $\delta$ has not changed; points below it mean the relationship weakened.&lt;/p>
&lt;pre>&lt;code class="language-stata">* For each correlate: regress Inst on loggdp in 1985 and 2015
* All correlates normalized by their 1985 SD
* Panel A: Solow fundamentals + short-run correlates
* Panel B: Long-run correlates + culture
graph combine delta_A delta_B, rows(1) cols(2) ///
graphregion(color(white)) ///
title(&amp;quot;Stability of Correlate-Income Slopes&amp;quot;, size(medium))
graph export &amp;quot;stata_convergence2_delta_stability.png&amp;quot;, replace width(2400)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_convergence2_delta_stability.png" alt="Two-panel scatter of correlate-income slopes (delta) in 2015 versus 1985. Points cluster tightly along the 45-degree line for all variable groups.">&lt;/p>
&lt;pre>&lt;code class="language-text">Delta fitted line slopes (delta_2015 vs delta_1985):
Solow fundamentals: slope = 0.878
Short-Run correlates: slope = 0.886
Long-Run correlates: slope = 1.024
Culture: slope = 0.884
&lt;/code>&lt;/pre>
&lt;p>The correlate-income relationships are remarkably stable. Fitted lines cluster tightly around the 45-degree line: Solow fundamentals 0.88, short-run correlates 0.89, long-run correlates 1.02, culture 0.88. This means the cross-country association between income and institutions has barely changed over 30 years. Richer countries still have better democracy, more investment, lower population growth, and stronger financial sectors in essentially the same proportions as in 1985. The &amp;ldquo;modernization hypothesis&amp;rdquo; &amp;mdash; that economic development goes hand-in-hand with institutional improvement &amp;mdash; passes its out-of-sample test.&lt;/p>
&lt;p>Crucially, this stability means that the $\delta$ component is &lt;strong>not&lt;/strong> responsible for the closing gap between unconditional and conditional convergence. The answer must lie in the other component: $\lambda$.&lt;/p>
&lt;hr>
&lt;h2 id="10-growth-regressions-then-vs-now-the-lambda-flattening">10. Growth regressions then vs. now: the lambda flattening&lt;/h2>
&lt;p>In the 1990s, a massive literature ran growth regressions of the form: Growth = $\alpha + \beta^{\ast} \times$ Income $+ \lambda \times$ Correlate $+ \varepsilon$. These regressions identified which policies and institutions predict growth and formed the empirical backbone of the &amp;ldquo;Washington Consensus&amp;rdquo; &amp;mdash; the set of policy recommendations that international institutions gave to developing countries. The key question: &lt;strong>do these regressions hold up with 25 years of new data?&lt;/strong>&lt;/p>
&lt;p>For each correlate, we estimate $\lambda$ (the growth-correlate slope) in the base year (~1985) and in 2005, using a fixed sample of countries with data in both periods.&lt;/p>
&lt;pre>&lt;code class="language-stata">* For each correlate, run the growth regression in base year and 2005
* Growth = alpha + beta* x loggdp + lambda x correlate + epsilon
* Fixed country sample per correlate
* Scatter lambda_2005 vs lambda_1985
reg lambda_2005 lambda_1985 if flag_solow == 1
* -&amp;gt; slope = 0.861, R-sq = 0.947
reg lambda_2005 lambda_1985 if flag_solow == 0 &amp;amp; flag_long_run == 0
* -&amp;gt; slope = 0.189, R-sq = 0.063
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_convergence2_lambda_flattening.png" alt="Two-panel scatter of growth regression coefficients (lambda) in 2005 versus 1985. Solow fundamentals cluster near the 45-degree line; short-run correlates are scattered near zero.">&lt;/p>
&lt;pre>&lt;code class="language-text">Lambda fitted line slopes (lambda_2005 vs lambda_1985):
Solow fundamentals: slope = 0.861, R-sq = 0.947
Short-run correlates: slope = 0.189, R-sq = 0.063
Long-Run correlates: slope = 0.296
Culture: slope = 0.685
&lt;/code>&lt;/pre>
&lt;p>This is the most striking empirical result of the paper. &lt;strong>Solow fundamentals&lt;/strong> (investment, population growth, education) show high persistence: a fitted slope of 0.86 with R-squared of 0.95, meaning these deep structural variables predict growth almost as well in 2005 as in 1985. In dramatic contrast, &lt;strong>short-run correlates&lt;/strong> (democracy, governance, fiscal policy, financial development) show near-zero persistence: a slope of 0.19 with R-squared of only 0.06. There is essentially no correlation between which policy variables predicted growth in 1985 and which predict growth in 2005.&lt;/p>
&lt;p>The Washington Consensus growth regressions &amp;mdash; which identified specific policies and institutions as growth drivers &amp;mdash; have &lt;strong>failed their out-of-sample test&lt;/strong>. Variables like Polity 2 ($\lambda$ fell from 0.89 to 0.34), FH Political Rights (1.11 to 0.19), and FH Civil Liberties (0.96 to 0.17) went from strong growth predictors to near-zero predictors. Long-run correlates and culture occupy an intermediate position (slopes 0.30 and 0.69 respectively).&lt;/p>
&lt;p>Why did this happen? There are at least three possible explanations: (a) as correlates converged (Section 7), the reduced cross-country variation made coefficient estimation noisier; (b) the original regressions may have been overfitted to a specific historical sample; (c) the relationship between institutions and growth may be non-linear &amp;mdash; institutions matter most when differences are large, and less when all countries have reasonably good policies. The analysis cannot distinguish between these, but the empirical fact is clear: $\lambda$ collapsed.&lt;/p>
&lt;p>Since $\delta$ is stable (Section 9) and $\lambda$ collapsed (this section), their product $\delta \times \lambda$ must have shrunk toward zero. The next section confirms this.&lt;/p>
&lt;hr>
&lt;h2 id="11-the-punchline-absolute-convergence-converges-to-conditional">11. The punchline: absolute convergence converges to conditional&lt;/h2>
&lt;h3 id="111-the-ovb-gap-is-closing">11.1 The OVB gap is closing&lt;/h3>
&lt;p>The product $\delta \times \lambda$ quantifies how much each correlate biases the unconditional convergence coefficient. We scatter $\delta \times \lambda$ in 2005 against its value in 1985 to see whether this &amp;ldquo;explanatory gap&amp;rdquo; has closed.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Scatter delta*lambda in 2005 vs 1985
reg dl_2005 dl_1985 if flag_solow == 0 &amp;amp; flag_long_run == 0
* -&amp;gt; slope = 0.090 (short-run correlates: gap essentially vanished)
reg dl_2005 dl_1985 if flag_solow == 1
* -&amp;gt; slope = 0.740 (Solow fundamentals: gap partially retained)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_convergence2_ovb_gap.png" alt="Two-panel scatter of delta times lambda products in 2005 versus 1985. Short-run correlate products have collapsed to near zero.">&lt;/p>
&lt;pre>&lt;code class="language-text">OVB gap fitted line slopes (dl_2005 vs dl_1985):
Panel A:
Solow fundamentals: slope = 0.740
Short-Run correlates: slope = 0.090
Panel B:
Long-Run correlates: slope = 0.480
Culture: slope = 0.739
&lt;/code>&lt;/pre>
&lt;p>The OVB gap for short-run correlates has shrunk to nearly zero (fitted slope 0.09). In 1985, omitting these policy and institutional variables made unconditional convergence look substantially worse than conditional convergence. By 2005, the two are nearly identical. Solow fundamentals retained more of their explanatory power (slope 0.74), reflecting the stability of both their $\delta$ and $\lambda$ components. This confirms the paper&amp;rsquo;s central thesis: unconditional convergence emerged not because the income-correlate relationship changed ($\delta$ is stable) but because policy variables stopped predicting growth ($\lambda$ flattened).&lt;/p>
&lt;h3 id="112-the-closing-gap-over-time">11.2 The closing gap over time&lt;/h3>
&lt;p>The definitive test uses multivariate regressions. We fix a sample of 73 countries with complete data on 10 correlates (Polity 2, FH political rights, FH civil liberties, private investment, government spending, inflation, WDI credit, credit by financial sector, Barro-Lee education, and education gender gap). For each year from 1985 to 2007, we estimate both unconditional $\beta$ (income only) and conditional $\beta^{\ast}$ (income plus all 10 correlates).&lt;/p>
&lt;pre>&lt;code class="language-stata">* Fix sample: 73 countries with complete data on all 10 correlates in 1985
local var_all polity2 FH_political_rights FH_civil_liberties pri_inv ///
gov_spending inflation WDI_credit credit barrolee2060 edugap
forval yr = 1985/2007 {
* Unconditional: reg growth loggdp, robust cluster(country_id)
* Conditional: reg growth loggdp `var_all', robust cluster(country_id)
}
* Plot the closing gap
twoway (line beta_unconditional year, lcolor(&amp;quot;20 20 19&amp;quot;) lwidth(medthick)) ///
(line beta_conditional year, lcolor(&amp;quot;106 155 204&amp;quot;) lwidth(medthick)) ///
(line zero year, lcolor(&amp;quot;217 119 87&amp;quot;) lpattern(dot)), ///
legend(label(1 &amp;quot;Absolute Convergence&amp;quot;) label(2 &amp;quot;Conditional Convergence&amp;quot;))
graph export &amp;quot;stata_convergence2_absolute_vs_conditional.png&amp;quot;, replace width(2400)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_convergence2_absolute_vs_conditional.png" alt="Time series of unconditional beta and conditional beta-star from 1985 to 2007. The two lines converge as unconditional beta falls from +0.42 to -0.65 while conditional beta-star fluctuates around -0.5 to -1.3.">&lt;/p>
&lt;pre>&lt;code class="language-text">Year | beta_unconditional beta_conditional gap
------+-------------------------------------------
1985 | 0.420 -1.072 1.492
1990 | 0.377 -0.560 0.937
1995 | 0.081 -0.155 0.236
2000 | -0.387 -0.540 0.153
2005 | -0.556 -0.969 0.413
2007 | -0.646 -1.274 0.629
&lt;/code>&lt;/pre>
&lt;p>This is the paper&amp;rsquo;s title finding. In 1985, unconditional $\beta$ was +0.42 (divergence) while conditional $\beta^{\ast}$ was -1.07 (strong convergence when controlling for institutions) &amp;mdash; a gap of 1.49. By 2000, unconditional $\beta$ had fallen to -0.39 while conditional $\beta^{\ast}$ was -0.54, narrowing the gap to just 0.15. The gap narrowed dramatically from 1.49 (1985) to 0.15 (2000), then widened somewhat as conditional $\beta^{\ast}$ deepened faster, but both lines are firmly negative by 2000.&lt;/p>
&lt;p>The Solow model&amp;rsquo;s prediction of conditional convergence held all along &amp;mdash; what changed is that the real world caught up. As the OVB from excluding correlates shrank toward zero, unconditional convergence &amp;ldquo;converged to&amp;rdquo; conditional convergence.&lt;/p>
&lt;h3 id="113-multivariate-evidence-table-5">11.3 Multivariate evidence (Table 5)&lt;/h3>
&lt;p>The multivariate regressions crystallize the structural change by showing how adding correlates affects the convergence coefficient in each period.&lt;/p>
&lt;pre>&lt;code class="language-text"> abs_1985 solow_1985 short_1985 full_1985 abs_2005 solow_2005 short_2005 full_2005
loggdp 0.420 -0.447 -0.435 -0.816 -0.556 -1.176 -0.557 -1.040
(0.252) (0.661) (0.457) (0.619) (0.203) (0.309) (0.327) (0.393)
R2 0.028 0.155 0.152 0.228 0.101 0.247 0.258 0.355
N 73 73 73 73 73 73 73 73
&lt;/code>&lt;/pre>
&lt;p>In 1985, absolute convergence alone gives $\beta = +0.42$ (divergence, R-squared = 0.03 &amp;mdash; essentially no linear relationship). Adding Solow fundamentals flips the sign to $\beta^{\ast} = -0.45$, and the full model gives $\beta^{\ast} = -0.82$. In 2005, the picture changes fundamentally: absolute convergence is already strong at $\beta = -0.56$ (R-squared = 0.10). Adding short-run correlates alone barely changes the coefficient (from -0.56 to -0.56), confirming that policy variables no longer have explanatory power beyond what income already captures. Correlates still improve overall fit (R-squared rises from 0.10 to 0.35), but they no longer alter the convergence coefficient.&lt;/p>
&lt;hr>
&lt;h2 id="12-robustness-does-the-averaging-period-matter">12. Robustness: does the averaging period matter?&lt;/h2>
&lt;p>The main results use 10-year forward-looking growth rates. One concern is that 10-year averaging may smooth out noise in a way that creates artificial trends. We check by re-estimating the rolling beta-convergence trend using 1-year, 2-year, 5-year, and 10-year growth averages.&lt;/p>
&lt;pre>&lt;code class="language-stata">* For each averaging period t = 1, 2, 5, 10:
gen loggdp_growth_t = 100 * ((F[t].logrgdpna - logrgdpna) / t)
areg loggdp_growth_t c.loggdp#i.year, absorb(year) robust cluster(country_id)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_convergence2_robustness_averaging.png" alt="Four-panel comparison of beta trends using 1-, 2-, 5-, and 10-year growth averages. All four panels show the same downward trend; shorter averages are noisier.">&lt;/p>
&lt;pre>&lt;code class="language-text">Results:
1-year average: high noise, downward trend visible but obscured by fluctuations
2-year average: moderate noise, downward trend clearer
5-year average: smooth, clear downward trend from ~0 to ~-0.5 by late 2000s
10-year average: smoothest, clearest trend from +0.5 to -0.76 by 2007
&lt;/code>&lt;/pre>
&lt;p>The convergence trend is robust across all averaging periods. As expected, shorter periods produce noisier estimates &amp;mdash; the 1-year panel is dominated by year-to-year fluctuations &amp;mdash; while longer averages yield smoother trends. All four specifications agree that the crossover from divergence to convergence occurs around 1990&amp;ndash;2000, confirming that the finding is not an artifact of the 10-year growth rate choice.&lt;/p>
&lt;hr>
&lt;h2 id="13-discussion">13. Discussion&lt;/h2>
&lt;p>Let us return to the question posed in the Overview: &lt;strong>why did unconditional convergence emerge since 2000?&lt;/strong>&lt;/p>
&lt;p>The OVB framework provides a clear and quantitative answer. The gap between unconditional convergence ($\beta$) and conditional convergence ($\beta^{\ast}$) is exactly equal to the product $\delta \times \lambda$. This gap closed because $\lambda$ &amp;mdash; the coefficient on growth correlates in growth regressions &amp;mdash; collapsed for short-run policy and institutional variables (slope = 0.19, R-squared = 0.06). Meanwhile, $\delta$ &amp;mdash; the relationship between income and institutions &amp;mdash; remained remarkably stable (slopes around 0.88 on the 45-degree line). In concrete terms: richer countries still have better institutions in the same proportions as 30 years ago, but those institutional advantages no longer translate into faster growth. As a result, unconditional convergence caught up to conditional convergence.&lt;/p>
&lt;p>This has important implications for how we think about economic development. The 1990s &amp;ldquo;Washington Consensus&amp;rdquo; was built on the empirical finding that good policies and institutions predict faster growth. Our out-of-sample test shows that many of these relationships did not persist into the 2000s &amp;mdash; at least not for short-run policy variables. Solow fundamentals (investment, population growth, education) remained robust growth predictors, consistent with the Solow model&amp;rsquo;s enduring relevance. But governance indices, fiscal indicators, and financial variables that were &amp;ldquo;significant&amp;rdquo; in 1990s regressions no longer predict growth. This raises questions about the stability of policy advice based on cross-country growth regressions.&lt;/p>
&lt;p>&lt;strong>Caveats.&lt;/strong> Several important limitations apply. First, the analysis is entirely descriptive &amp;mdash; cross-country regressions do not establish causal relationships. The flattening of $\lambda$ could reflect genuine changes in causal relationships, convergence in unobserved variables, or reduced cross-country variation making coefficient estimation noisier. Second, the panel is unbalanced (109 countries in 1960 vs. 160 by 1990), and sample composition changes could mechanically affect estimates. Third, some correlates have small samples (fewer than 60 observations), limiting statistical precision. Finally, the 10-year growth variable is forward-looking, so the last usable observation is 2007/2008, missing the Global Financial Crisis, the post-GFC recovery, and COVID-19. Whether convergence persisted through these shocks is an open question.&lt;/p>
&lt;hr>
&lt;h2 id="14-summary-and-key-takeaways">14. Summary and key takeaways&lt;/h2>
&lt;p>This tutorial reproduced the key findings of Kremer, Willis, and You (2021), documenting the emergence of unconditional convergence and explaining it through the OVB decomposition framework. The analysis used 160 countries over 58 years with 50+ growth correlates.&lt;/p>
&lt;h3 id="the-story-in-four-facts">The story in four facts&lt;/h3>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Unconditional convergence emerged around 2000.&lt;/strong> The $\beta$-convergence coefficient shifted from +0.53 in the 1960s (divergence, p = 0.006) to -0.76 by 2007 (convergence, p &amp;lt; 0.001), with a systematic trend of -0.025 per year.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Growth correlates converged.&lt;/strong> Inflation ($\beta = -3.07$), investment ($\beta = -2.98$), and democracy ($\beta = -2.03$) all showed strong convergence. Countries with initially worse institutions experienced the largest improvements.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Growth regression coefficients collapsed for policy variables.&lt;/strong> Solow fundamentals maintained high stability ($\lambda$ slope = 0.86, R-squared = 0.95), but short-run correlates showed near-zero persistence ($\lambda$ slope = 0.19, R-squared = 0.06). The 1990s growth regressions failed their out-of-sample test.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>The gap between absolute and conditional convergence closed.&lt;/strong> The Polity 2 worked example shows the gap fell from 0.44 to 0.04 (a 91% reduction). In the multivariate analysis, the gap narrowed from 1.49 (1985) to 0.15 (2000).&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h3 id="limitations">Limitations&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Descriptive, not causal:&lt;/strong> The OVB framework decomposes observed correlations, not causal relationships&lt;/li>
&lt;li>&lt;strong>Pre-2008 endpoint:&lt;/strong> The analysis does not cover the Global Financial Crisis or COVID-19&lt;/li>
&lt;li>&lt;strong>Small samples for some correlates:&lt;/strong> Culture and tariff variables have fewer than 60 observations&lt;/li>
&lt;li>&lt;strong>Normalization sensitivity:&lt;/strong> All correlate coefficients are normalized by their 1985 standard deviation&lt;/li>
&lt;/ul>
&lt;h3 id="next-steps">Next steps&lt;/h3>
&lt;ul>
&lt;li>Extend the analysis through the 2010s using updated PWT data to test whether convergence survived the post-GFC period&lt;/li>
&lt;li>Explore non-linear specifications to test whether $\lambda$ flattened because of reduced correlate variation&lt;/li>
&lt;li>Apply the OVB decomposition to regional subsamples (e.g., does the mechanism differ for Sub-Saharan Africa vs. East Asia?)&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="15-exercises">15. Exercises&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Your own worked example.&lt;/strong> Choose a different correlate from the dataset (e.g., investment or FH political rights) and replicate the OVB worked example from Section 8.3. Compute $\beta$, $\beta^{\ast}$, $\delta$, $\lambda$, and verify the identity $\beta - \beta^{\ast} = \delta \times \lambda$ for both 1985 and 2005. Did the gap close for your chosen variable? Was the primary driver the change in $\delta$ or $\lambda$?&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Balanced panel sensitivity.&lt;/strong> Re-estimate the rolling beta-convergence trend (Section 4) using only countries that have GDP data from 1960 onward (a balanced panel of approximately 109 countries). Does the convergence trend look different when you exclude countries that enter the sample later? What does this tell you about the role of sample composition changes?&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Alternative classification.&lt;/strong> The paper classifies variables as &amp;ldquo;Solow fundamentals&amp;rdquo; or &amp;ldquo;short-run correlates.&amp;rdquo; Move education (barrolee2060) from the Solow group to the short-run group and re-estimate the lambda stability scatters (Section 10). Does the Solow fitted line slope change substantially? What does this tell you about the robustness of the paper&amp;rsquo;s classification scheme?&lt;/p>
&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="references">References&lt;/h2>
&lt;ol>
&lt;li>&lt;a href="https://www.nber.org/papers/w29484" target="_blank" rel="noopener">Kremer, M., Willis, J., &amp;amp; You, Y. (2021). Converging to Convergence. &lt;em>NBER Working Paper 29484&lt;/em>&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://doi.org/10.2307/2937943" target="_blank" rel="noopener">Barro, R. (1991). Economic Growth in a Cross Section of Countries. &lt;em>Quarterly Journal of Economics&lt;/em>, 106(2), 407&amp;ndash;443&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://doi.org/10.1086/261816" target="_blank" rel="noopener">Barro, R. &amp;amp; Sala-i-Martin, X. (1992). Convergence. &lt;em>Journal of Political Economy&lt;/em>, 100(2), 223&amp;ndash;251&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://doi.org/10.1016/j.jdeveco.2021.102687" target="_blank" rel="noopener">Patel, D., Sandefur, J., &amp;amp; Subramanian, A. (2021). The New Era of Unconditional Convergence. &lt;em>Journal of Development Economics&lt;/em>, 152, 102687&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://doi.org/10.1016/S1574-0684%2805%2901008-7" target="_blank" rel="noopener">Durlauf, S., Johnson, P., &amp;amp; Temple, J. (2005). Growth Econometrics. &lt;em>Handbook of Economic Growth&lt;/em>, Volume 1A&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.rug.nl/ggdc/productivity/pwt/" target="_blank" rel="noopener">Penn World Table 10.0 &amp;mdash; Groningen Growth and Development Centre&lt;/a>&lt;/li>
&lt;/ol>
&lt;h3 id="acknowledgements">Acknowledgements&lt;/h3>
&lt;p>AI tools (Claude Code) were used to make the contents of this post more accessible to students. Nevertheless, the content in this post may still have errors. Caution is needed when applying the contents of this post to true research projects.&lt;/p></description></item><item><title>Regional Inequality and the Kuznets Curve: Panel Fixed Effects in Python</title><link>https://carlos-mendez.org/post/python_fe_kuznets/</link><pubDate>Mon, 27 Apr 2026 00:00:00 +0000</pubDate><guid>https://carlos-mendez.org/post/python_fe_kuznets/</guid><description>&lt;h2 id="1-overview">1. Overview&lt;/h2>
&lt;p>Does economic growth reduce inequality within countries, or does it make some regions richer while others fall behind? In 1955, Simon Kuznets hypothesized an inverted-U relationship: inequality rises during early industrialization as workers move from farms to factories, then falls as the benefits of growth diffuse more broadly. This &amp;ldquo;Kuznets curve&amp;rdquo; became one of the most tested hypotheses in development economics &amp;mdash; and one of the most debated.&lt;/p>
&lt;p>Using satellite nighttime light data to measure regional inequality across 180 countries from 1992 to 2012, Lessmann and Seidel (2017) found something surprising: the relationship is not an inverted-U at all. It is &lt;strong>N-shaped&lt;/strong>. Inequality rises at low income levels, falls through middle-income development, then rises &lt;em>again&lt;/em> at the very highest income levels. The classic Kuznets curve misses this second upturn because most early studies lacked data from the richest nations.&lt;/p>
&lt;p>In this tutorial we replicate their key findings using &lt;a href="https://pyfixest.org/" target="_blank" rel="noopener">PyFixest&lt;/a> for panel fixed effects estimation and &lt;a href="https://posit-dev.github.io/great-tables/" target="_blank" rel="noopener">Great Tables&lt;/a> for publication-quality regression tables. We progress from naive pooled OLS &amp;mdash; which mixes between-country and within-country variation &amp;mdash; through two-way fixed effects (TWFE) that isolate how inequality changes as the &lt;em>same country&lt;/em> develops over time. We then compute turning points of the fitted N-shaped polynomial and investigate what determinants &amp;mdash; resources, trade, mobility, education, and ethnicity &amp;mdash; drive regional inequality beyond the Kuznets curve.&lt;/p>
&lt;p>The case study question is: &lt;strong>Is the relationship between regional inequality and economic development inverted-U or N-shaped, and what factors beyond income drive regional disparities?&lt;/strong>&lt;/p>
&lt;p>&lt;strong>Learning objectives:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Understand why polynomial specifications are necessary for testing the Kuznets hypothesis&lt;/li>
&lt;li>Implement pooled OLS and two-way fixed effects regressions using PyFixest&lt;/li>
&lt;li>Compute and interpret turning points of a cubic polynomial in the context of development economics&lt;/li>
&lt;li>Compare pooled OLS and TWFE estimates to assess the impact of omitted variable bias&lt;/li>
&lt;li>Identify the key determinants of regional inequality using panel fixed effects with clustered standard errors&lt;/li>
&lt;/ul>
&lt;p>The following diagram outlines the analytical pipeline:&lt;/p>
&lt;pre>&lt;code class="language-mermaid">graph TD
A[&amp;quot;&amp;lt;b&amp;gt;Data&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;180 countries, 1992-2012&amp;quot;] --&amp;gt; B[&amp;quot;&amp;lt;b&amp;gt;Visual EDA&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Scatter plots + polynomial fits&amp;quot;]
B --&amp;gt; C[&amp;quot;&amp;lt;b&amp;gt;Pooled OLS&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Linear / Quadratic / Cubic&amp;quot;]
C --&amp;gt; D[&amp;quot;&amp;lt;b&amp;gt;Why Fixed Effects?&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Country trajectories differ&amp;quot;]
D --&amp;gt; E[&amp;quot;&amp;lt;b&amp;gt;Two-Way FE&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Country + Year FE&amp;quot;]
E --&amp;gt; F[&amp;quot;&amp;lt;b&amp;gt;Turning Points&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;3 development phases&amp;quot;]
E --&amp;gt; G[&amp;quot;&amp;lt;b&amp;gt;Determinants&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;What drives inequality?&amp;quot;]
G --&amp;gt; H[&amp;quot;&amp;lt;b&amp;gt;Robustness&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Coefficient stability&amp;quot;]
style A fill:#6a9bcc,stroke:#141413,color:#fff
style B fill:#6a9bcc,stroke:#141413,color:#fff
style C fill:#d97757,stroke:#141413,color:#fff
style D fill:#d97757,stroke:#141413,color:#fff
style E fill:#00d4c8,stroke:#141413,color:#fff
style F fill:#00d4c8,stroke:#141413,color:#fff
style G fill:#1a3a8a,stroke:#141413,color:#fff
style H fill:#1a3a8a,stroke:#141413,color:#fff
&lt;/code>&lt;/pre>
&lt;p>The pipeline progresses from exploratory analysis (blue) through baseline estimation (orange) to the core fixed effects results (teal) and determinant analysis (dark blue). Each stage builds on the previous: the visual patterns motivate the polynomial specification, the spaghetti plot motivates fixed effects, and the robust N-shape motivates the search for determinants.&lt;/p>
&lt;h3 id="key-concepts-at-a-glance">Key concepts at a glance&lt;/h3>
&lt;p>The post leans on a small vocabulary repeatedly. The rest of the tutorial assumes you can move between these terms quickly. 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 collapsed for a quick scan. If a later section mentions &amp;ldquo;turning points&amp;rdquo; or &amp;ldquo;within R²&amp;rdquo; and the term feels slippery, this is the section to re-read.&lt;/p>
&lt;p>&lt;strong>1. Kuznets curve.&lt;/strong>
The theoretical inverted-U relationship between economic development and income inequality, proposed by Simon Kuznets in 1955. Inequality should rise as countries industrialize, peak at intermediate income levels, then fall as services and welfare states emerge. The post tests whether modern panel data confirm or refute this pattern.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>Plotting &lt;code>gini&lt;/code> against &lt;code>log_GDPpc&lt;/code> for the 880 country-period observations, the unconditional pattern is closer to N-shaped than to a clean inverted-U. The Kuznets prediction is the null the post tests against.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>The textbook story. Like the Phillips curve in macroeconomics — a famous theoretical curve that the data sometimes confirm and sometimes contradict. Modern data is the audit on whether the curve still holds.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>2. N-shaped relationship&lt;/strong> $\beta_1 + 2\beta_2 \ln Y + 3\beta_3 (\ln Y)^2 = 0$.
A non-monotonic pattern with two turning points. Inequality rises with development, falls, then rises again at very high incomes. Captured by a cubic polynomial in log GDP. The N-shape is the post&amp;rsquo;s headline finding once fixed effects are imposed.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>The cubic TWFE estimates yield $\beta_1 = 0.293$, $\beta_2 = -0.032$, $\beta_3 = 0.001$. The derivative crosses zero twice, producing two turning points at \$2,287 and \$77,205. Below the first and above the second turning point, inequality is rising in income.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>A story with two acts. Act 1: inequality rises through industrialization. Act 2: inequality falls through welfare expansion. Modern data adds Act 3: at very high incomes, inequality rises again. The N captures all three acts.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>3. Two-Way Fixed Effects (TWFE)&lt;/strong> $\alpha_i + \delta_t$.
A panel estimator that absorbs both country fixed effects $\alpha_i$ and time-period fixed effects $\delta_t$. Identification comes from within-country deviations from country and period means. Removes time-invariant country features and global period shocks.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>This post&amp;rsquo;s headline cubic specification is TWFE. The estimator absorbs 180 country effects and 5 period effects, leaving only within-country, within-period variation to identify the polynomial coefficients.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>Wiping the negative twice. The first wipe removes country-specific stains (geography, institutions, culture). The second wipe removes period-specific glare (a global recession, a global commodity boom). What remains is the country&amp;rsquo;s &lt;em>change&lt;/em> relative to its own typical trajectory.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>4. Polynomial specification&lt;/strong> $\beta_1 \ln Y + \beta_2 (\ln Y)^2 + \beta_3 (\ln Y)^3$.
Including powers of the regressor lets the relationship bend. Linear (just $\ln Y$) imposes monotonicity. Quadratic ($\ln Y$ and $(\ln Y)^2$) imposes a single inverted-U. Cubic adds a second turn. The post compares all three.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>The post fits linear, quadratic, and cubic versions of the TWFE model. The cubic is preferred on AIC and on coefficient significance: all three of $\beta_1, \beta_2, \beta_3$ are significant at $p &amp;lt; 0.001$, $p &amp;lt; 0.001$, and $p = 0.001$ respectively.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>Trying first-, second-, and third-order curves to fit a scatter. A line fits a straight road. A parabola fits a hill. A cubic fits a roller-coaster with two peaks. You pick the simplest curve that the data actually demand.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>5. Turning points&lt;/strong> $\partial \mathrm{Gini} / \partial \ln Y = 0$.
Income levels where the polynomial derivative crosses zero. The slope of inequality with respect to income changes sign at each turning point. Computed by solving the quadratic $\beta_1 + 2\beta_2 \ln Y + 3\beta_3 (\ln Y)^2 = 0$.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>With the cubic estimates, the two turning points sit at $\ln Y = 7.735$ (≈ \$2,287) and $\ln Y = 11.254$ (≈ \$77,205). Below \$2,287 inequality rises with income; between \$2,287 and \$77,205 it falls; above \$77,205 it rises again.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>Where the rollercoaster changes direction. Two turning points means two crests-or-troughs in the ride. The N-shape says: rise, fall, rise. Each turning point is a moment where the cart momentarily stops climbing or falling.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>6. Within R² vs overall R².&lt;/strong>
Two ways to summarize the fit of a panel regression. &lt;em>Overall R²&lt;/em> uses both within and between variation in $y$. &lt;em>Within R²&lt;/em> uses only the variation that survives demeaning. The within R² is what the FE model actually explains.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>The cubic TWFE has overall R² = 0.975 — most of which comes from the unit and time fixed effects mechanically explaining variation in &lt;code>gini&lt;/code>. The within R² is 0.142 — the polynomial in &lt;code>log_GDPpc&lt;/code> explains 14% of the within-country, within-period variation. The within R² is the honest number.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>How well you predict the &lt;em>changes&lt;/em>. A great forecast of the past does not mean you understand what makes the future different. Within R² is the forecast on actual changes. Overall R² flatters the model with the easy parts.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>7. Omitted variable bias (OVB).&lt;/strong>
Bias from leaving out a confounder that correlates with both $\ln Y$ and &lt;code>gini&lt;/code>. Pooled OLS ignores fixed country traits that drive both. TWFE removes time-invariant country traits. The 5x jump in coefficient magnitude between POLS and TWFE is an OVB diagnostic.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>Pooled OLS R² is 0.176 — most of the explanation comes from confounded between-country variation. TWFE within R² is 0.142 — almost all from within-country variation. The OVB hidden in pooled OLS is what motivates the FE specification.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>A stain on the camera lens. Pooled OLS thinks the dark spot in every photo is part of the subject. TWFE recognizes it is on the lens and wipes it off. What was attributed to &amp;ldquo;low GDP per capita&amp;rdquo; was actually country-specific shadow.&lt;/p>
&lt;/details>
&lt;/div>
&lt;h2 id="2-setup-and-imports">2. Setup and imports&lt;/h2>
&lt;p>Before running the analysis, install the required packages if needed:&lt;/p>
&lt;pre>&lt;code class="language-python">pip install pyfixest great_tables
&lt;/code>&lt;/pre>
&lt;p>The following code imports PyFixest and standard data science libraries. &lt;a href="https://pyfixest.org/reference/estimation.feols.html" target="_blank" rel="noopener">pf.feols()&lt;/a> is the main estimation function, accepting R-style formulas with a pipe &lt;code>|&lt;/code> separator for fixed effects. &lt;a href="https://posit-dev.github.io/great-tables/" target="_blank" rel="noopener">Great Tables&lt;/a> creates publication-quality tables rendered as PNG images.&lt;/p>
&lt;pre>&lt;code class="language-python">import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import pyfixest as pf
from great_tables import GT, md, style, loc
# Reproducibility
RANDOM_SEED = 42
np.random.seed(RANDOM_SEED)
# Site color palette
STEEL_BLUE = &amp;quot;#6a9bcc&amp;quot;
WARM_ORANGE = &amp;quot;#d97757&amp;quot;
NEAR_BLACK = &amp;quot;#141413&amp;quot;
TEAL = &amp;quot;#00d4c8&amp;quot;
# Data URLs
URL_TAB03 = &amp;quot;https://github.com/quarcs-lab/data-open/raw/master/pGDP/simpleTAB03.dta&amp;quot;
URL_TAB04 = &amp;quot;https://github.com/quarcs-lab/data-open/raw/master/pGDP/simpleTAB04.dta&amp;quot;
&lt;/code>&lt;/pre>
&lt;details>
&lt;summary>&lt;strong>Dark theme figure styling&lt;/strong> (click to expand)&lt;/summary>
&lt;pre>&lt;code class="language-python"># Dark theme palette (consistent with site navbar/dark sections)
DARK_NAVY = &amp;quot;#0f1729&amp;quot;
GRID_LINE = &amp;quot;#1f2b5e&amp;quot;
LIGHT_TEXT = &amp;quot;#c8d0e0&amp;quot;
WHITE_TEXT = &amp;quot;#e8ecf2&amp;quot;
# Plot defaults — minimal, spine-free, dark background
plt.rcParams.update({
&amp;quot;figure.facecolor&amp;quot;: DARK_NAVY,
&amp;quot;axes.facecolor&amp;quot;: DARK_NAVY,
&amp;quot;axes.edgecolor&amp;quot;: DARK_NAVY,
&amp;quot;axes.linewidth&amp;quot;: 0,
&amp;quot;axes.labelcolor&amp;quot;: LIGHT_TEXT,
&amp;quot;axes.titlecolor&amp;quot;: WHITE_TEXT,
&amp;quot;axes.spines.top&amp;quot;: False,
&amp;quot;axes.spines.right&amp;quot;: False,
&amp;quot;axes.spines.left&amp;quot;: False,
&amp;quot;axes.spines.bottom&amp;quot;: False,
&amp;quot;axes.grid&amp;quot;: True,
&amp;quot;grid.color&amp;quot;: GRID_LINE,
&amp;quot;grid.linewidth&amp;quot;: 0.6,
&amp;quot;grid.alpha&amp;quot;: 0.8,
&amp;quot;xtick.color&amp;quot;: LIGHT_TEXT,
&amp;quot;ytick.color&amp;quot;: LIGHT_TEXT,
&amp;quot;xtick.major.size&amp;quot;: 0,
&amp;quot;ytick.major.size&amp;quot;: 0,
&amp;quot;text.color&amp;quot;: WHITE_TEXT,
&amp;quot;font.size&amp;quot;: 12,
&amp;quot;legend.frameon&amp;quot;: False,
&amp;quot;legend.fontsize&amp;quot;: 11,
&amp;quot;legend.labelcolor&amp;quot;: LIGHT_TEXT,
&amp;quot;figure.edgecolor&amp;quot;: DARK_NAVY,
&amp;quot;savefig.facecolor&amp;quot;: DARK_NAVY,
&amp;quot;savefig.edgecolor&amp;quot;: DARK_NAVY,
})
&lt;/code>&lt;/pre>
&lt;/details>
&lt;h2 id="3-data-loading-and-panel-structure">3. Data loading and panel structure&lt;/h2>
&lt;h3 id="31-the-kuznets-curve-dataset">3.1 The Kuznets curve dataset&lt;/h3>
&lt;p>The dataset comes from Lessmann and Seidel (2017), who measured regional inequality within countries using satellite nighttime light data. The dependent variable is a population-weighted &lt;em>Gini coefficient&lt;/em> &amp;mdash; a number between 0 (perfect equality across regions) and 1 (all income concentrated in one region) &amp;mdash; computed from subnational GDP estimates derived from nighttime light intensity. We load it directly from a Stata &lt;code>.dta&lt;/code> file hosted on GitHub using &lt;a href="https://pandas.pydata.org/docs/reference/api/pandas.read_stata.html" target="_blank" rel="noopener">pd.read_stata()&lt;/a>.&lt;/p>
&lt;pre>&lt;code class="language-python">df3 = pd.read_stata(URL_TAB03)
print(f&amp;quot;Shape: {df3.shape}&amp;quot;)
print(f&amp;quot;Columns: {list(df3.columns)}&amp;quot;)
print(f&amp;quot;\nDescriptive statistics:&amp;quot;)
print(df3.describe().round(4))
print(f&amp;quot;\nPanel structure:&amp;quot;)
print(f&amp;quot; Countries: {df3['id'].nunique()}&amp;quot;)
print(f&amp;quot; Time periods: {sorted(df3['year'].unique())}&amp;quot;)
print(f&amp;quot;\nObservations per period:&amp;quot;)
print(df3.groupby('year')['id'].count())
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Shape: (880, 7)
Columns: ['id', 'year', 'country', 'gini', 'log_GDPpc', 'log_GDPpc2', 'log_GDPpc3']
Descriptive statistics:
id year gini log_GDPpc log_GDPpc2 log_GDPpc3
count 880.0000 880.0000 880.0000 880.0000 880.0000 880.0000
mean 89.9932 3.0318 0.0641 8.7599 78.2732 712.3774
std 51.9770 1.4090 0.0332 1.2403 21.6226 288.5019
min 1.0000 1.0000 0.0019 5.2458 27.5184 144.3558
25% 45.0000 2.0000 0.0381 7.7617 60.2448 467.6052
50% 89.5000 3.0000 0.0605 8.8514 78.3474 693.4843
75% 134.0000 4.0000 0.0847 9.7595 95.2473 929.5637
max 180.0000 5.0000 0.1601 11.6716 136.2253 1589.9617
Panel structure:
Countries: 180
Time periods: [1.0, 2.0, 3.0, 4.0, 5.0]
Observations per period:
Period 1: 168 | Period 2: 175 | Period 3: 178 | Period 4: 179 | Period 5: 180
&lt;/code>&lt;/pre>
&lt;p>The dataset contains 880 country-period observations spanning 180 countries across 5 time periods (5-year averages from 1990&amp;ndash;1994 through 2010&amp;ndash;2013, covering data from 1992&amp;ndash;2012). The panel is slightly &lt;em>unbalanced&lt;/em> &amp;mdash; meaning not every country is observed in every period &amp;mdash; with 168 countries in the first period growing to 180 by the last. The mean regional Gini is 0.064 with substantial variation (SD = 0.033, range 0.002 to 0.160), indicating that some countries have highly equal regional income distributions while others show pronounced disparities. Log GDP per capita ranges from 5.25 (about \$190, the poorest nations) to 11.67 (about \$117,000, oil-rich Gulf states), capturing the full development spectrum. The polynomial terms (&lt;code>log_GDPpc2&lt;/code>, &lt;code>log_GDPpc3&lt;/code>) are pre-computed in the dataset to ensure consistency with the original Stata analysis. Let us now visualize the data to see if the Kuznets pattern is visible.&lt;/p>
&lt;h3 id="32-the-determinants-dataset">3.2 The determinants dataset&lt;/h3>
&lt;p>A second dataset adds 14 covariates capturing resources, trade, mobility, governance, and ethnicity &amp;mdash; the factors that may drive regional inequality beyond the Kuznets curve.&lt;/p>
&lt;pre>&lt;code class="language-python">df4 = pd.read_stata(URL_TAB04)
print(f&amp;quot;Shape: {df4.shape}&amp;quot;)
print(f&amp;quot;Key variables: gini, lnGDPpc (+ squared/cubed), rents, land, trade,&amp;quot;)
print(f&amp;quot; fdi, gasoline, areaXgasoline, aid, school, ethnic_gini&amp;quot;)
print(f&amp;quot;\nNotable missing values:&amp;quot;)
print(f&amp;quot; aid: {df4['aid'].notna().sum()} / 880 ({df4['aid'].isna().mean():.0%} missing)&amp;quot;)
print(f&amp;quot; school: {df4['school'].notna().sum()} / 880 ({df4['school'].isna().mean():.0%} missing)&amp;quot;)
print(f&amp;quot; ethnic_gini: {df4['ethnic_gini'].notna().sum()} / 880 &amp;quot;
f&amp;quot;({df4['ethnic_gini'].isna().mean():.0%} missing)&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Shape: (880, 21)
Key variables: gini, lnGDPpc (+ squared/cubed), rents, land, trade,
fdi, gasoline, areaXgasoline, aid, school, ethnic_gini
Notable missing values:
aid: 711 / 880 (19% missing)
school: 748 / 880 (15% missing)
ethnic_gini: 845 / 880 (4% missing)
&lt;/code>&lt;/pre>
&lt;p>The determinants dataset includes the same 880 observations but adds 14 covariates. Missing data is most pronounced for foreign aid (19% missing) and school enrollment (15% missing), which will reduce sample sizes in some determinant models. We return to this dataset after establishing the Kuznets curve with fixed effects.&lt;/p>
&lt;h2 id="4-visual-exploration-is-there-a-kuznets-curve">4. Visual exploration: Is there a Kuznets curve?&lt;/h2>
&lt;h3 id="41-pooled-scatter-with-polynomial-fits">4.1 Pooled scatter with polynomial fits&lt;/h3>
&lt;p>Before estimating any regression, it helps to see the raw data. We plot every country-period observation of regional inequality against log GDP per capita, overlaying three polynomial fit lines: linear (dashed gray), quadratic (dashed teal), and cubic (solid orange). If the classic Kuznets inverted-U holds, the quadratic should capture the pattern. If the relationship bends twice &amp;mdash; first up, then down, then up again &amp;mdash; we need the cubic.&lt;/p>
&lt;p>Think of a cubic polynomial as fitting a roller coaster track through the data: it can climb, descend, and rise again, capturing patterns that a straight line or simple curve would miss entirely.&lt;/p>
&lt;pre>&lt;code class="language-python">fig, ax = plt.subplots(figsize=(10, 6))
x = df3[&amp;quot;log_GDPpc&amp;quot;].values
y = df3[&amp;quot;gini&amp;quot;].values
ax.scatter(x, y, alpha=0.35, s=18, color=STEEL_BLUE, edgecolors=DARK_NAVY)
# Fit and overlay three polynomial curves
x_grid = np.linspace(x.min(), x.max(), 200)
for deg, color, ls, lw, label in [
(1, LIGHT_TEXT, &amp;quot;--&amp;quot;, 1.5, &amp;quot;Linear&amp;quot;),
(2, TEAL, &amp;quot;--&amp;quot;, 1.8, &amp;quot;Quadratic (inverted-U)&amp;quot;),
(3, WARM_ORANGE, &amp;quot;-&amp;quot;, 2.5, &amp;quot;Cubic (N-shape)&amp;quot;),
]:
coeffs = np.polyfit(x, y, deg)
ax.plot(x_grid, np.polyval(coeffs, x_grid), color=color, ls=ls, lw=lw, label=label)
ax.set_xlabel(&amp;quot;Log GDP per capita (PPP, constant US$)&amp;quot;)
ax.set_ylabel(&amp;quot;Regional Inequality (Population-weighted Gini)&amp;quot;)
ax.set_title(&amp;quot;Regional Inequality vs National Development\n&amp;quot;
&amp;quot;180 Countries, 1992-2012 (pooled)&amp;quot;)
ax.legend()
plt.savefig(&amp;quot;kuznets_scatter_pooled.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="kuznets_scatter_pooled.png" alt="Scatter plot of regional Gini versus log GDP per capita with linear, quadratic, and cubic fit lines, showing the cubic N-shaped fit captures the data pattern best.">&lt;/p>
&lt;p>The scatter reveals a clear pattern: regional inequality is highest among the poorest and richest nations, with lower inequality in the middle-income range. The linear fit (dashed gray) captures a downward trend but misses the curvature entirely. The quadratic fit (dashed teal) bends once but does not capture the upturn at high incomes. The cubic fit (solid orange) traces an N-shape &amp;mdash; rising, falling, then rising again &amp;mdash; that most closely follows the data cloud. This visual evidence motivates testing a cubic polynomial specification formally. But is this pattern stable across time periods?&lt;/p>
&lt;h3 id="42-stability-across-periods">4.2 Stability across periods&lt;/h3>
&lt;p>To check whether the N-shape is a persistent feature of the data or an artifact of a single time window, we plot the same scatter separately for each of the five periods:&lt;/p>
&lt;pre>&lt;code class="language-python">periods = sorted(df3[&amp;quot;year&amp;quot;].unique())
fig, axes = plt.subplots(1, len(periods), figsize=(20, 5), sharey=True)
# Map numeric periods to actual year ranges (Lessmann &amp;amp; Seidel 2017)
period_labels = {1: &amp;quot;1990--1994&amp;quot;, 2: &amp;quot;1995--1999&amp;quot;, 3: &amp;quot;2000--2004&amp;quot;,
4: &amp;quot;2005--2009&amp;quot;, 5: &amp;quot;2010--2013&amp;quot;}
for ax, period in zip(axes, periods):
sub = df3[df3[&amp;quot;year&amp;quot;] == period]
ax.scatter(sub[&amp;quot;log_GDPpc&amp;quot;], sub[&amp;quot;gini&amp;quot;], alpha=0.4, s=20, color=STEEL_BLUE)
cp = np.polyfit(sub[&amp;quot;log_GDPpc&amp;quot;], sub[&amp;quot;gini&amp;quot;], 3)
xg = np.linspace(sub[&amp;quot;log_GDPpc&amp;quot;].min(), sub[&amp;quot;log_GDPpc&amp;quot;].max(), 100)
ax.plot(xg, np.polyval(cp, xg), color=WARM_ORANGE, lw=2)
ax.set_title(period_labels.get(int(period), f&amp;quot;Period {int(period)}&amp;quot;))
ax.set_xlabel(&amp;quot;Log GDP pc&amp;quot;)
axes[0].set_ylabel(&amp;quot;Regional Gini&amp;quot;)
plt.savefig(&amp;quot;kuznets_scatter_by_period.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="kuznets_scatter_by_period.png" alt="Five-panel faceted scatter showing the Gini-GDP relationship separately for each period (1990&amp;ndash;1994 through 2010&amp;ndash;2013), with cubic fit lines.">&lt;/p>
&lt;p>The N-shaped pattern appears in all five periods from 1990&amp;ndash;1994 through 2010&amp;ndash;2013, ruling out the possibility that the result is driven by a single unusual time window. The cubic fit line bends in the same direction across every panel, suggesting a stable structural relationship. Now let us formalize this with regression analysis, starting with the simplest pooled OLS specification.&lt;/p>
&lt;h2 id="5-pooled-ols-baseline-linear-quadratic-and-cubic">5. Pooled OLS baseline: Linear, quadratic, and cubic&lt;/h2>
&lt;p>We begin by estimating three pooled OLS regressions of increasing polynomial complexity. The &lt;em>pooled&lt;/em> specification treats every country-period observation as an independent draw, ignoring the panel structure entirely. This serves as a baseline that we will improve upon with fixed effects.&lt;/p>
&lt;p>The cubic polynomial specification is:&lt;/p>
&lt;p>$$\text{Gini}_i = \beta_0 + \beta_1 \ln(\text{GDP}_i) + \beta_2 [\ln(\text{GDP}_i)]^2 + \beta_3 [\ln(\text{GDP}_i)]^3 + \epsilon_i$$&lt;/p>
&lt;p>In words, this equation models regional inequality as a polynomial function of log GDP per capita. The coefficient $\beta_1$ captures the linear association. The term $\beta_2$ allows the relationship to bend once (inverted-U if negative), and $\beta_3$ allows it to bend a second time (N-shape if positive). In the code, these correspond to &lt;code>log_GDPpc&lt;/code>, &lt;code>log_GDPpc2&lt;/code>, and &lt;code>log_GDPpc3&lt;/code>.&lt;/p>
&lt;p>We use &lt;code>pf.feols()&lt;/code> to estimate all three models with &lt;em>clustered standard errors&lt;/em> &amp;mdash; standard errors that account for the fact that observations from the same country are not independent. The &lt;code>vcov={&amp;quot;CRV1&amp;quot;: &amp;quot;id&amp;quot;}&lt;/code> argument clusters at the country level.&lt;/p>
&lt;pre>&lt;code class="language-python"># Pooled OLS: linear, quadratic, cubic
ols_linear = pf.feols(&amp;quot;gini ~ log_GDPpc&amp;quot;, data=df3, vcov={&amp;quot;CRV1&amp;quot;: &amp;quot;id&amp;quot;})
ols_quad = pf.feols(&amp;quot;gini ~ log_GDPpc + log_GDPpc2&amp;quot;, data=df3, vcov={&amp;quot;CRV1&amp;quot;: &amp;quot;id&amp;quot;})
ols_cubic = pf.feols(&amp;quot;gini ~ log_GDPpc + log_GDPpc2 + log_GDPpc3&amp;quot;, data=df3,
vcov={&amp;quot;CRV1&amp;quot;: &amp;quot;id&amp;quot;})
# Compare coefficients across specifications
print(&amp;quot;Pooled OLS Coefficient Comparison:&amp;quot;)
print(f&amp;quot;{'Variable':&amp;lt;14} {'Linear':&amp;gt;10} {'Quadratic':&amp;gt;12} {'Cubic':&amp;gt;10}&amp;quot;)
print(&amp;quot;-&amp;quot; * 48)
for var in [&amp;quot;log_GDPpc&amp;quot;, &amp;quot;log_GDPpc2&amp;quot;, &amp;quot;log_GDPpc3&amp;quot;]:
vals = []
for m in [ols_linear, ols_quad, ols_cubic]:
vals.append(f&amp;quot;{m.coef()[var]:.4f}&amp;quot; if var in m.coef().index else &amp;quot;---&amp;quot;)
print(f&amp;quot;{var:&amp;lt;14} {vals[0]:&amp;gt;10} {vals[1]:&amp;gt;12} {vals[2]:&amp;gt;10}&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Pooled OLS Coefficient Comparison:
Variable Linear Quadratic Cubic
------------------------------------------------
log_GDPpc -0.0108 0.0148 0.2405
log_GDPpc2 --- -0.0015 -0.0279
log_GDPpc3 --- --- 0.0010
R-squared: 0.164 0.170 0.176
&lt;/code>&lt;/pre>
&lt;p>The linear model shows a significant negative association between development and inequality (coefficient -0.011, p &amp;lt; 0.001), but explains only 16.4% of the variation. Adding the quadratic term barely improves fit (R-squared rises to 0.170) and neither term is individually significant, suggesting the simple inverted-U does not hold in the pooled data. The cubic specification reveals the N-shaped pattern (coefficients: 0.241, -0.028, 0.001) with all terms marginally significant (p-values around 0.07&amp;ndash;0.09), but these are pooled estimates that confound between-country and within-country variation. The low R-squared of 0.176 confirms that cross-sectional variation dominates. Why does pooled OLS produce such noisy estimates? The answer lies in country heterogeneity.&lt;/p>
&lt;h2 id="6-why-fixed-effects-the-omitted-variable-problem">6. Why fixed effects? The omitted variable problem&lt;/h2>
&lt;p>Pooled OLS treats all country-period observations as independent draws. But countries differ in geography, institutions, colonial history, and culture &amp;mdash; factors that affect &lt;em>both&lt;/em> inequality &lt;em>and&lt;/em> development. If these unobserved factors correlate with GDP per capita, the pooled OLS coefficients are biased. This is called &lt;em>omitted variable bias&lt;/em> &amp;mdash; the regression attributes variation to GDP that is really driven by unobserved country characteristics.&lt;/p>
&lt;p>Think of it this way: if you want to measure whether nutrition affects height, you cannot just compare children from different families &amp;mdash; taller families tend to eat differently from shorter ones. You need to look at how height changes &lt;em>within the same family&lt;/em> when nutrition changes. &lt;em>Fixed effects&lt;/em> does exactly this for countries: it adds a separate intercept for each country, effectively controlling for all time-invariant country characteristics.&lt;/p>
&lt;p>The spaghetti plot below makes this concrete. Each line traces a single country&amp;rsquo;s trajectory over time, while the dashed curve shows the pooled cross-sectional pattern.&lt;/p>
&lt;pre>&lt;code class="language-python"># Select 20 countries spread across the GDP distribution
country_obs = df3.groupby(&amp;quot;id&amp;quot;).agg(
n_periods=(&amp;quot;year&amp;quot;, &amp;quot;count&amp;quot;), mean_gdp=(&amp;quot;log_GDPpc&amp;quot;, &amp;quot;mean&amp;quot;)
).reset_index()
country_obs = country_obs[country_obs[&amp;quot;n_periods&amp;quot;] &amp;gt;= 3].sort_values(&amp;quot;mean_gdp&amp;quot;)
idx = np.linspace(0, len(country_obs) - 1, 20, dtype=int)
selected_ids = country_obs.iloc[idx][&amp;quot;id&amp;quot;].values
fig, ax = plt.subplots(figsize=(10, 6))
for cid in selected_ids:
sub = df3[df3[&amp;quot;id&amp;quot;] == cid].sort_values(&amp;quot;log_GDPpc&amp;quot;)
ax.plot(sub[&amp;quot;log_GDPpc&amp;quot;], sub[&amp;quot;gini&amp;quot;], color=LIGHT_TEXT, alpha=0.25,
lw=1.2, marker=&amp;quot;o&amp;quot;, ms=3)
# Highlight 6 diverse countries
highlight_ids = country_obs.iloc[
np.linspace(0, len(country_obs) - 1, 6, dtype=int)
][&amp;quot;id&amp;quot;].values
colors = [WARM_ORANGE, TEAL, STEEL_BLUE, &amp;quot;#e8956a&amp;quot;, &amp;quot;#8ec8e8&amp;quot;, &amp;quot;#66e8df&amp;quot;]
for i, cid in enumerate(highlight_ids):
sub = df3[df3[&amp;quot;id&amp;quot;] == cid].sort_values(&amp;quot;log_GDPpc&amp;quot;)
ax.plot(sub[&amp;quot;log_GDPpc&amp;quot;], sub[&amp;quot;gini&amp;quot;], color=colors[i], lw=2.5,
marker=&amp;quot;o&amp;quot;, ms=5, label=sub[&amp;quot;country&amp;quot;].iloc[0])
ax.set_xlabel(&amp;quot;Log GDP per capita&amp;quot;)
ax.set_ylabel(&amp;quot;Regional Gini&amp;quot;)
ax.set_title(&amp;quot;Individual Country Trajectories vs Pooled Pattern\n&amp;quot;
&amp;quot;Each line = one country over time&amp;quot;)
ax.legend(ncol=2)
plt.savefig(&amp;quot;kuznets_spaghetti.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="kuznets_spaghetti.png" alt="Individual country trajectories showing that Liberia, Kenya, Republic of Congo, Algeria, Bahamas, and Qatar follow distinct paths, different from the pooled cross-sectional pattern.">&lt;/p>
&lt;p>The spaghetti plot reveals the key insight: individual countries follow their own trajectories that differ substantially from the cross-sectional pattern. Liberia (far left) has high inequality at low GDP, while Qatar (far right) has high inequality at high GDP &amp;mdash; but within each country, the trajectory over time looks nothing like the pooled cubic fit. A country at log GDP = 8 may have very different inequality than another at the same GDP level because of country-specific factors like geography, ethnic composition, and colonial history. Fixed effects remove these country-specific levels and focus only on how inequality changes &lt;em>within&lt;/em> each country as it develops. Let us now estimate the fixed effects models.&lt;/p>
&lt;h2 id="7-two-way-fixed-effects-replicating-table-3">7. Two-way fixed effects: Replicating Table 3&lt;/h2>
&lt;p>&lt;em>Two-way fixed effects&lt;/em> (TWFE) adds two sets of dummy variables to the regression: country fixed effects ($\alpha_i$) absorb all time-invariant country characteristics, and year fixed effects ($\gamma_t$) absorb common global shocks like financial crises or commodity price swings. The model becomes:&lt;/p>
&lt;p>$$\text{Gini}_{it} = \beta_1 \ln(\text{GDP}_{it}) + \beta_2 [\ln(\text{GDP}_{it})]^2 + \beta_3 [\ln(\text{GDP}_{it})]^3 + \alpha_i + \gamma_t + \epsilon_{it}$$&lt;/p>
&lt;p>In words, this equation isolates the &lt;em>within-country, within-time-period&lt;/em> relationship between development and inequality. The country fixed effects $\alpha_i$ ensure we compare each country to itself over time, not to other countries. The year fixed effects $\gamma_t$ ensure we do not conflate the Kuznets relationship with global trends. In PyFixest, we specify fixed effects after a pipe &lt;code>|&lt;/code> in the formula: &lt;code>gini ~ log_GDPpc | id + year&lt;/code> means regress &lt;code>gini&lt;/code> on &lt;code>log_GDPpc&lt;/code>, absorbing &lt;code>id&lt;/code> (country) and &lt;code>year&lt;/code> fixed effects.&lt;/p>
&lt;pre>&lt;code class="language-python"># Three TWFE specifications: linear, quadratic, cubic
fe_linear = pf.feols(&amp;quot;gini ~ log_GDPpc | id + year&amp;quot;, data=df3, vcov={&amp;quot;CRV1&amp;quot;: &amp;quot;id&amp;quot;})
fe_quad = pf.feols(&amp;quot;gini ~ log_GDPpc + log_GDPpc2 | id + year&amp;quot;, data=df3,
vcov={&amp;quot;CRV1&amp;quot;: &amp;quot;id&amp;quot;})
fe_cubic = pf.feols(&amp;quot;gini ~ log_GDPpc + log_GDPpc2 + log_GDPpc3 | id + year&amp;quot;,
data=df3, vcov={&amp;quot;CRV1&amp;quot;: &amp;quot;id&amp;quot;})
print(&amp;quot;TWFE Cubic Model (Model 3):&amp;quot;)
print(f&amp;quot; log_GDPpc: {fe_cubic.coef()['log_GDPpc']:.3f} &amp;quot;
f&amp;quot;(SE {fe_cubic.se()['log_GDPpc']:.3f}, p &amp;lt; 0.001) ***&amp;quot;)
print(f&amp;quot; log_GDPpc2: {fe_cubic.coef()['log_GDPpc2']:.3f} &amp;quot;
f&amp;quot;(SE {fe_cubic.se()['log_GDPpc2']:.3f}, p &amp;lt; 0.001) ***&amp;quot;)
print(f&amp;quot; log_GDPpc3: {fe_cubic.coef()['log_GDPpc3']:.3f} &amp;quot;
f&amp;quot;(SE {fe_cubic.se()['log_GDPpc3']:.3f}, p = 0.001) ***&amp;quot;)
print(f&amp;quot; R-squared: {fe_cubic._r2:.3f} | R-squared Within: {fe_cubic._r2_within:.3f}&amp;quot;)
print(f&amp;quot; Observations: {fe_cubic._N}&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">TWFE Cubic Model (Model 3):
log_GDPpc: 0.293 (SE 0.078, p &amp;lt; 0.001) ***
log_GDPpc2: -0.032 (SE 0.009, p &amp;lt; 0.001) ***
log_GDPpc3: 0.001 (SE 0.000, p = 0.001) ***
R-squared: 0.975 | R-squared Within: 0.142
Observations: 879
&lt;/code>&lt;/pre>
&lt;p>Adding country and year fixed effects transforms the results dramatically. All three polynomial terms become highly significant (p &amp;lt; 0.001 for each), confirming the N-shaped relationship &lt;em>within countries over time&lt;/em>. The overall R-squared of 0.975 indicates that country fixed effects absorb the vast majority of cross-sectional variation &amp;mdash; 97.5% of total variation is explained once we account for which country and which period we are observing. The &lt;em>within-R-squared&lt;/em> of 0.142 tells us that the cubic polynomial explains about 14.2% of the within-country variation in inequality, which is substantial given the short time dimension (5 periods). Compared to pooled OLS, the TWFE coefficients are slightly larger in magnitude (0.293 vs 0.241 for the linear term) and &amp;mdash; crucially &amp;mdash; the significance improves from marginal (p ~ 0.07) to highly significant (p &amp;lt; 0.001), demonstrating how fixed effects resolve omitted variable bias.&lt;/p>
&lt;p>The Great Tables regression table below summarizes all three TWFE specifications in publication-quality format:&lt;/p>
&lt;p>&lt;img src="kuznets_table3.png" alt="Publication-quality regression table for the three TWFE Kuznets curve models showing linear, quadratic, and cubic specifications with clustered standard errors.">&lt;/p>
&lt;h3 id="71-the-linear-twfe-model-is-uninformative">7.1 The linear TWFE model is uninformative&lt;/h3>
&lt;p>A key pedagogical finding emerges when we compare the three TWFE specifications side by side:&lt;/p>
&lt;pre>&lt;code class="language-python">print(&amp;quot;Linear TWFE:&amp;quot;)
print(f&amp;quot; log_GDPpc: {fe_linear.coef()['log_GDPpc']:.3f} &amp;quot;
f&amp;quot;(SE {fe_linear.se()['log_GDPpc']:.3f}, &amp;quot;
f&amp;quot;p = {fe_linear.pvalue()['log_GDPpc']:.3f})&amp;quot;)
print(f&amp;quot; R-squared Within: {fe_linear._r2_within:.3f}&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Linear TWFE:
log_GDPpc: -0.003 (SE 0.003, p = 0.265)
R-squared Within: 0.009
&lt;/code>&lt;/pre>
&lt;p>The linear TWFE model yields a coefficient of -0.003 that is statistically insignificant (p = 0.265) with a within-R-squared of only 0.009. A researcher who only estimated the linear specification would conclude that development has no relationship with inequality within countries &amp;mdash; a misleading result. The true relationship is nonlinear: inequality rises with early development and falls later, so the linear approximation averages these opposing effects to roughly zero. This demonstrates why polynomial specifications are essential when testing the Kuznets hypothesis. Now let us compute where exactly the N-shaped curve bends.&lt;/p>
&lt;h2 id="8-the-n-shaped-curve-computing-turning-points">8. The N-shaped curve: Computing turning points&lt;/h2>
&lt;p>The cubic TWFE model implies that inequality first rises, then falls, then rises again with development. To find where the curve changes direction, we take the first derivative of the polynomial and set it to zero:&lt;/p>
&lt;p>$$\frac{\partial \text{Gini}}{\partial \ln(\text{GDP})} = \beta_1 + 2\beta_2 \ln(\text{GDP}) + 3\beta_3 [\ln(\text{GDP})]^2 = 0$$&lt;/p>
&lt;p>In words, this equation asks: at what income level does the slope of the inequality-development relationship switch sign? Solving this quadratic equation yields two &lt;em>turning points&lt;/em> &amp;mdash; the first where inequality peaks and the second where it reaches a trough before rising again.&lt;/p>
&lt;pre>&lt;code class="language-python"># Extract cubic TWFE coefficients
b1 = fe_cubic.coef()[&amp;quot;log_GDPpc&amp;quot;] # 0.2931
b2 = fe_cubic.coef()[&amp;quot;log_GDPpc2&amp;quot;] # -0.0320
b3 = fe_cubic.coef()[&amp;quot;log_GDPpc3&amp;quot;] # 0.0011
# Solve: 3*b3*x^2 + 2*b2*x + b1 = 0
roots = np.roots([3 * b3, 2 * b2, b1])
real_roots = np.sort(roots[np.isreal(roots)].real)
turning_usd = np.exp(real_roots)
print(f&amp;quot;Cubic TWFE coefficients: b1 = {b1:.6f}, b2 = {b2:.6f}, b3 = {b3:.6f}&amp;quot;)
print(f&amp;quot;Turning points (log scale): [{real_roots[0]:.3f}, {real_roots[1]:.3f}]&amp;quot;)
print(f&amp;quot;Turning points (USD PPP): [${turning_usd[0]:,.0f}, ${turning_usd[1]:,.0f}]&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Cubic TWFE coefficients: b1 = 0.293112, b2 = -0.031969, b3 = 0.001122
Turning points (log scale): [7.735, 11.254]
Turning points (USD PPP): [$2,287, $77,205]
&lt;/code>&lt;/pre>
&lt;p>The two turning points define three development phases. The first turning point at \$2,287 GDP per capita marks where regional inequality peaks: below this threshold &amp;mdash; very poor countries like Liberia and the DRC &amp;mdash; development initially concentrates income in a leading region, widening the gap. Between \$2,287 and \$77,205 &amp;mdash; the vast majority of countries, from Kenya through most of Europe &amp;mdash; further development is associated with falling regional inequality as lagging regions catch up. The second turning point at \$77,205 suggests that the richest nations (essentially Qatar, Luxembourg, and similar outliers) may see inequality rise again as knowledge-economy agglomeration re-concentrates activity. These values closely replicate the paper&amp;rsquo;s reported thresholds of approximately \$2,288 and \$77,128, with minor differences due to rounding in the original Stata analysis.&lt;/p>
&lt;p>The figure below visualizes the fitted N-shaped polynomial with shaded regions marking each development phase:&lt;/p>
&lt;p>&lt;img src="kuznets_fitted_curve.png" alt="Fitted N-shaped Kuznets curve with shaded rising and falling regions, turning points annotated, and a dual x-axis showing both log and USD values.">&lt;/p>
&lt;p>The three development phases are visually clear: rising inequality for the poorest nations (left orange region), convergence through middle income (blue region), and a secondary upturn at very high income (right orange region). The dual x-axis lets the reader map from log GDP &amp;mdash; the scale used in the regression &amp;mdash; to familiar dollar amounts. Next, let us compare the pooled OLS and TWFE estimates side by side.&lt;/p>
&lt;h2 id="9-pooled-ols-vs-twfe-correcting-for-omitted-variable-bias">9. Pooled OLS vs TWFE: Correcting for omitted variable bias&lt;/h2>
&lt;p>How much does controlling for country heterogeneity change the estimates? The table below compares the cubic polynomial coefficients from pooled OLS and TWFE:&lt;/p>
&lt;pre>&lt;code class="language-python">print(&amp;quot;Pooled OLS vs TWFE (cubic):&amp;quot;)
print(f&amp;quot;{'Variable':&amp;lt;14} {'Pooled OLS':&amp;gt;12} {'TWFE':&amp;gt;12}&amp;quot;)
print(&amp;quot;-&amp;quot; * 40)
for var in [&amp;quot;log_GDPpc&amp;quot;, &amp;quot;log_GDPpc2&amp;quot;, &amp;quot;log_GDPpc3&amp;quot;]:
print(f&amp;quot;{var:&amp;lt;14} {ols_cubic.coef()[var]:&amp;gt;12.4f} {fe_cubic.coef()[var]:&amp;gt;12.4f}&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Pooled OLS vs TWFE (cubic):
Variable Pooled OLS TWFE
----------------------------------------
log_GDPpc 0.2405 0.2931
log_GDPpc2 -0.0279 -0.0320
log_GDPpc3 0.0010 0.0011
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="kuznets_ols_vs_fe.png" alt="Horizontal bar chart comparing pooled OLS and TWFE coefficients for the cubic Kuznets specification with 95% confidence intervals.">&lt;/p>
&lt;p>TWFE coefficients are slightly larger in magnitude than their pooled OLS counterparts (0.293 vs 0.241 for the linear term), and the confidence intervals are substantially tighter. The pooled OLS estimates are only marginally significant (p ~ 0.07), while the TWFE estimates are all significant at the 0.1% level. This demonstrates that fixed effects both &lt;em>correct bias&lt;/em> (by removing confounding from time-invariant country characteristics) and &lt;em>improve precision&lt;/em> (by reducing residual variance). The N-shape is not a cross-sectional artifact &amp;mdash; it is a robust within-country phenomenon. Having established the Kuznets curve, we now turn to a broader question: what factors beyond income drive regional inequality?&lt;/p>
&lt;h2 id="10-determinants-of-regional-inequality">10. Determinants of regional inequality&lt;/h2>
&lt;h3 id="101-exploring-correlations">10.1 Exploring correlations&lt;/h3>
&lt;p>The determinants dataset adds nine variables capturing different channels through which factors might affect regional inequality: resource wealth, international trade, factor mobility, human capital, and ethnic composition. Before running regressions, we examine the correlation structure:&lt;/p>
&lt;pre>&lt;code class="language-python">det_vars = [&amp;quot;gini&amp;quot;, &amp;quot;lnGDPpc&amp;quot;, &amp;quot;rents&amp;quot;, &amp;quot;land&amp;quot;, &amp;quot;trade&amp;quot;, &amp;quot;fdi&amp;quot;,
&amp;quot;gasoline&amp;quot;, &amp;quot;aid&amp;quot;, &amp;quot;school&amp;quot;, &amp;quot;ethnic_gini&amp;quot;]
corr = df4[det_vars].corr()
fig, ax = plt.subplots(figsize=(10, 8))
im = ax.imshow(corr.values, cmap=&amp;quot;RdBu_r&amp;quot;, vmin=-1, vmax=1, aspect=&amp;quot;auto&amp;quot;)
for i in range(len(det_vars)):
for j in range(len(det_vars)):
ax.text(j, i, f&amp;quot;{corr.values[i, j]:.2f}&amp;quot;, ha=&amp;quot;center&amp;quot;, va=&amp;quot;center&amp;quot;,
fontsize=8)
ax.set_title(&amp;quot;Correlation Matrix: Determinants of Regional Inequality&amp;quot;)
plt.savefig(&amp;quot;kuznets_correlation_heatmap.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="kuznets_correlation_heatmap.png" alt="Correlation heatmap of all determinant variables with annotated coefficients, showing ethnic Gini has the strongest positive correlation with regional inequality.">&lt;/p>
&lt;p>The ethnic Gini has the strongest positive correlation with regional inequality (r = 0.49), suggesting that countries with large income gaps between ethnic groups also tend to have large income gaps between regions. School enrollment has the strongest negative correlation (r = -0.41), consistent with education promoting regional convergence. Trade openness and GDP per capita are positively correlated (r = 0.38), which means pooled regressions of inequality on trade may partly reflect development effects. The fixed effects regressions below address this by controlling for the Kuznets polynomial and country heterogeneity simultaneously.&lt;/p>
&lt;h3 id="102-determinant-regressions-replicating-table-4">10.2 Determinant regressions: Replicating Table 4&lt;/h3>
&lt;p>We estimate five TWFE models, each adding a different group of determinants while keeping the cubic polynomial and country/year fixed effects. This replicates Table 4 of Lessmann and Seidel (2017):&lt;/p>
&lt;pre>&lt;code class="language-python">det1 = pf.feols(&amp;quot;gini ~ lnGDPpc + lnGDPpc2 + lnGDPpc3 + rents + land | id + year&amp;quot;,
data=df4, vcov={&amp;quot;CRV1&amp;quot;: &amp;quot;id&amp;quot;}) # Resources
det2 = pf.feols(&amp;quot;gini ~ lnGDPpc + lnGDPpc2 + lnGDPpc3 + trade + fdi | id + year&amp;quot;,
data=df4, vcov={&amp;quot;CRV1&amp;quot;: &amp;quot;id&amp;quot;}) # Trade
det3 = pf.feols(&amp;quot;gini ~ lnGDPpc + lnGDPpc2 + lnGDPpc3 + gasoline + areaXgasoline &amp;quot;
&amp;quot;| id + year&amp;quot;, data=df4, vcov={&amp;quot;CRV1&amp;quot;: &amp;quot;id&amp;quot;}) # Mobility
det4 = pf.feols(&amp;quot;gini ~ lnGDPpc + lnGDPpc2 + lnGDPpc3 + aid + school | id + year&amp;quot;,
data=df4, vcov={&amp;quot;CRV1&amp;quot;: &amp;quot;id&amp;quot;}) # Aid/Education
det5 = pf.feols(&amp;quot;gini ~ lnGDPpc + lnGDPpc2 + lnGDPpc3 + ethnic_gini | id + year&amp;quot;,
data=df4, vcov={&amp;quot;CRV1&amp;quot;: &amp;quot;id&amp;quot;}) # Ethnicity
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="kuznets_table4.png" alt="Publication-quality regression table for the five determinants models, each adding a different group of covariates to the cubic Kuznets specification.">&lt;/p>
&lt;p>Seven of nine determinants are statistically significant at the 10% level. Ethnic income inequality is the single strongest driver (coefficient 0.071, p &amp;lt; 0.001): a one-unit increase in the ethnic Gini is associated with a 7.1-percentage-point increase in regional inequality, holding the Kuznets curve constant. This is economically large given that the mean regional Gini is only 0.064. Arable land has the second-largest effect in absolute value but with the opposite sign (-0.053, p &amp;lt; 0.001), indicating that agricultural economies tend toward more equal regional development, likely because farming activity is geographically dispersed.&lt;/p>
&lt;p>Resource rents increase inequality (0.018, p = 0.008), consistent with the &amp;ldquo;resource curse&amp;rdquo; &amp;mdash; the pattern where natural resource wealth concentrates extractive income in specific regions. Trade openness modestly increases inequality (0.005, p = 0.007), suggesting that internationally connected regions pull ahead. Foreign aid increases inequality (0.015, p = 0.028), possibly because aid flows concentrate in capital cities. School enrollment reduces inequality (-0.014, p = 0.053), consistent with human capital diffusion promoting convergence.&lt;/p>
&lt;p>FDI and gasoline price alone are not significant, though the interaction of gasoline price with country area is (0.006, p = 0.049), indicating that transport costs matter more in geographically large countries. But do these additional controls change the Kuznets curve itself?&lt;/p>
&lt;h3 id="103-coefficient-stability-across-specifications">10.3 Coefficient stability across specifications&lt;/h3>
&lt;p>A critical robustness check is whether the N-shaped Kuznets curve survives the addition of controls. If the polynomial coefficients change dramatically when we add determinants, the N-shape may be spurious &amp;mdash; driven by omitted variables that correlate with both GDP and inequality:&lt;/p>
&lt;pre>&lt;code class="language-python">specs = [&amp;quot;Baseline (Table 3)&amp;quot;, &amp;quot;Resources&amp;quot;, &amp;quot;Trade&amp;quot;,
&amp;quot;Mobility&amp;quot;, &amp;quot;Aid/Educ.&amp;quot;, &amp;quot;Ethnicity&amp;quot;]
print(f&amp;quot;{'Specification':&amp;lt;20} {'ln(GDP)':&amp;gt;10} {'ln(GDP)^2':&amp;gt;12} {'ln(GDP)^3':&amp;gt;12}&amp;quot;)
print(&amp;quot;-&amp;quot; * 56)
for name, coefs in zip(specs, [
(0.2931, -0.0320, 0.0011), (0.3498, -0.0380, 0.0013),
(0.2054, -0.0222, 0.0008), (0.1711, -0.0186, 0.0007),
(0.2264, -0.0232, 0.0007), (0.1492, -0.0153, 0.0005),
]):
print(f&amp;quot;{name:&amp;lt;20} {coefs[0]:&amp;gt;10.4f} {coefs[1]:&amp;gt;12.4f} {coefs[2]:&amp;gt;12.4f}&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Specification ln(GDP) ln(GDP)^2 ln(GDP)^3
--------------------------------------------------------
Baseline (Table 3) 0.2931 -0.0320 0.0011
Resources 0.3498 -0.0380 0.0013
Trade 0.2054 -0.0222 0.0008
Mobility 0.1711 -0.0186 0.0007
Aid/Educ. 0.2264 -0.0232 0.0007
Ethnicity 0.1492 -0.0153 0.0005
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="kuznets_coefficient_stability.png" alt="Three-panel dot plot showing the linear, quadratic, and cubic coefficients across all six specifications with 95% confidence intervals.">&lt;/p>
&lt;p>The sign pattern (+, -, +) for the three polynomial terms is preserved across all six specifications, confirming the robustness of the N-shaped Kuznets curve. However, the magnitudes attenuate noticeably when ethnic inequality is included: the linear term drops from 0.293 to 0.149, and the cubic term halves from 0.0011 to 0.0005. This suggests that part of what appears as a &amp;ldquo;development effect&amp;rdquo; on regional inequality is actually driven by ethnic income disparities that correlate with development levels. The Resources specification actually &lt;em>strengthens&lt;/em> the polynomial coefficients (0.350, -0.038, 0.001), indicating that controlling for resource rents and arable land sharpens the Kuznets curve rather than weakening it. The cubic term remains positive in all specifications but loses significance in the Aid/Education model (p = 0.180), where the smaller sample (N = 585) reduces statistical power.&lt;/p>
&lt;h3 id="104-determinant-effects-at-a-glance">10.4 Determinant effects at a glance&lt;/h3>
&lt;p>Finally, the bar chart below ranks all nine determinants by their coefficient magnitude, color-coded by whether they increase (orange) or decrease (blue) regional inequality:&lt;/p>
&lt;p>&lt;img src="kuznets_determinants_barplot.png" alt="Horizontal bar chart of determinant coefficients, color-coded by direction. Ethnic Gini dominates all other determinants. Solid bars indicate significance at p less than 0.10, faded bars indicate non-significance.">&lt;/p>
&lt;p>The ethnic Gini dominates all other determinants, with a coefficient (0.071) that is 3.9 times larger than the next biggest positive effect (resource rents at 0.018) and 1.3 times larger than the largest effect in absolute value (arable land at -0.053). Arable land and school enrollment are the only factors that significantly &lt;em>reduce&lt;/em> regional inequality, suggesting that geographically dispersed economic activity and broad-based human capital investment are the two channels through which countries can promote more equal regional development. The policy implication is clear: governments concerned about regional disparities should invest in education and be cautious about over-relying on resource extraction or trade liberalization, which tend to concentrate economic activity in specific regions.&lt;/p>
&lt;h2 id="11-discussion">11. Discussion&lt;/h2>
&lt;p>We can now answer the case study question: &lt;strong>the relationship between regional inequality and economic development is N-shaped, not inverted-U.&lt;/strong> The cubic TWFE model yields highly significant coefficients (0.293, -0.032, 0.001, all p &amp;lt; 0.001) that define three development phases. Below \$2,287 GDP per capita, initial development concentrates economic activity and widens regional gaps. Between \$2,287 and \$77,205, the convergence story dominates &amp;mdash; lagging regions catch up as infrastructure, education, and market access spread. Above \$77,205, inequality may rise again as knowledge-economy agglomeration re-concentrates activity, though this second upturn is estimated from very few observations (Qatar, Luxembourg, Norway).&lt;/p>
&lt;p>The fixed effects framework proved essential. A researcher who estimated only the linear specification would conclude that development has no effect on inequality (coefficient -0.003, p = 0.265). This is wrong &amp;mdash; the true relationship is nonlinear, and the opposing effects at different development stages cancel out in a linear model.&lt;/p>
&lt;p>Among determinants, ethnic income inequality stands out as the most powerful driver of regional disparities. When ethnic inequality is controlled, the Kuznets polynomial attenuates substantially (the linear term drops from 0.293 to 0.149), raising the question of whether the Kuznets curve is partly an artifact of ethnic composition correlating with income levels. This finding has direct policy relevance: addressing ethnic income gaps may be a more effective lever for reducing regional inequality than broad economic growth alone.&lt;/p>
&lt;p>Several caveats apply. The second turning point at \$77,205 is beyond most of the data and should be interpreted cautiously. Missing data reduces sample sizes for some determinant models (the Aid/Education model drops to 585 observations from 880). The within-R-squared ranges from 0.01 to 0.28 depending on the specification, meaning that a substantial share of within-country inequality variation remains unexplained. Most importantly, this analysis is &lt;em>descriptive&lt;/em>, not causal. Fixed effects control for time-invariant confounders but cannot address time-varying confounders. The &amp;ldquo;determinants&amp;rdquo; should be interpreted as associations conditional on the Kuznets curve and country/year fixed effects, not as causal effects.&lt;/p>
&lt;h2 id="12-summary-and-next-steps">12. Summary and next steps&lt;/h2>
&lt;p>&lt;strong>Key takeaways:&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>The Kuznets curve is N-shaped, not inverted-U.&lt;/strong> The cubic TWFE model with country and year fixed effects yields coefficients of 0.293, -0.032, and 0.001 (all p &amp;lt; 0.001), with a within-R-squared of 0.142 compared to just 0.009 for the linear specification. The N-shape is robust across all six model specifications.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Turning points anchor three development phases.&lt;/strong> Regional inequality peaks at \$2,287 GDP per capita and reaches a trough at \$77,205, defining a broad convergence zone where most of the world&amp;rsquo;s countries fall. The pattern is stable across all five time periods in the data.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Ethnic income inequality is the strongest determinant of regional disparities.&lt;/strong> With a coefficient of 0.071 (p &amp;lt; 0.001), it is 3.9 times larger than the next biggest positive effect. Controlling for it halves the Kuznets polynomial coefficients, suggesting that ethnic composition partly drives the apparent development-inequality relationship.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Fixed effects are essential for uncovering the Kuznets relationship.&lt;/strong> Pooled OLS cubic coefficients are only marginally significant (p ~ 0.07), while TWFE coefficients are highly significant (p &amp;lt; 0.001). The linear TWFE model is completely uninformative (p = 0.265), demonstrating that both the polynomial specification &lt;em>and&lt;/em> the fixed effects are needed to reveal the true pattern.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Limitations:&lt;/strong> The analysis covers 1992&amp;ndash;2012; patterns may differ with more recent data. The second turning point (\$77,205) relies on very few observations. The panel has only 5 periods, limiting within-country variation. All results are associations, not causal effects.&lt;/p>
&lt;p>&lt;strong>Next steps:&lt;/strong> Extend the analysis to more recent satellite data (e.g., VIIRS nighttime lights). Test whether the N-shape holds at the subnational level within individual countries. Explore instrumental variables or shift-share designs to identify causal effects of trade, FDI, or aid on regional inequality.&lt;/p>
&lt;h2 id="13-exercises">13. Exercises&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Quadratic vs cubic test.&lt;/strong> Re-estimate the TWFE model with just the quadratic polynomial (&lt;code>gini ~ log_GDPpc + log_GDPpc2 | id + year&lt;/code>). How does the within-R-squared compare to the cubic model (0.142)? Is the cubic term ($\beta_3$) individually significant? What would you conclude about the Kuznets hypothesis from the quadratic specification alone?&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Subsample analysis.&lt;/strong> Split the sample into OECD and non-OECD countries. Re-estimate the cubic TWFE model for each subsample. Does the N-shape hold in both groups, or is it driven primarily by one? What happens to the turning points?&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Full determinants model.&lt;/strong> Estimate a single TWFE model that includes &lt;em>all&lt;/em> nine determinants simultaneously (rather than in separate models). How do the coefficients change compared to Table 4? Which variables remain significant? What does multicollinearity among the determinants do to the standard errors?&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="14-references">14. References&lt;/h2>
&lt;ol>
&lt;li>&lt;a href="https://doi.org/10.1016/j.euroecorev.2016.11.009" target="_blank" rel="noopener">Lessmann, C., &amp;amp; Seidel, A. (2017). Regional inequality, convergence, and its determinants &amp;mdash; A view from outer space. &lt;em>European Economic Review&lt;/em>, 92, 110&amp;ndash;132.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/quarcs-lab/data-open/tree/master/pGDP" target="_blank" rel="noopener">Population-Weighted Regional Inequality Dataset &amp;mdash; quarcs-lab/data-open (GitHub)&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://pyfixest.org/" target="_blank" rel="noopener">PyFixest &amp;mdash; Fast High-Dimensional Fixed Effects Estimation in Python&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://posit-dev.github.io/great-tables/" target="_blank" rel="noopener">Great Tables &amp;mdash; Publication-Quality Tables in Python&lt;/a>&lt;/li>
&lt;/ol></description></item><item><title>Exploratory Spatial Data Analysis: Spatial Clusters and Dynamics of Human Development in South America</title><link>https://carlos-mendez.org/post/python_esda2/</link><pubDate>Sun, 22 Mar 2026 00:00:00 +0000</pubDate><guid>https://carlos-mendez.org/post/python_esda2/</guid><description>&lt;h2 id="1-overview">1. Overview&lt;/h2>
&lt;p>When we look at a map of human development across South America, a pattern immediately stands out: prosperous regions tend to cluster together, and so do lagging regions. But is this clustering statistically significant, or could it arise by chance? And how have these spatial clusters evolved over time?&lt;/p>
&lt;p>&lt;strong>Exploratory Spatial Data Analysis (ESDA)&lt;/strong> provides the tools to answer these questions. ESDA is a set of techniques for visualizing spatial distributions, identifying patterns of spatial clustering, and detecting spatial outliers. Unlike standard exploratory data analysis, which treats observations as independent, ESDA explicitly accounts for the geographic location of each observation and the relationships between neighbors.&lt;/p>
&lt;p>This tutorial uses the &lt;a href="https://globaldatalab.org/shdi/" target="_blank" rel="noopener">Subnational Human Development Index&lt;/a> (SHDI) from &lt;a href="https://doi.org/10.1038/sdata.2019.38" target="_blank" rel="noopener">Smits and Permanyer (2019)&lt;/a> for &lt;strong>153 sub-national regions across 12 South American countries&lt;/strong> in 2013 and 2019 &amp;mdash; the same dataset from the &lt;a href="https://carlos-mendez.org/post/python_pca2/">Pooled PCA tutorial&lt;/a>. We progress from simple scatter plots and choropleth maps to formal tests of spatial dependence (Moran&amp;rsquo;s I), local cluster identification (LISA maps), and space-time dynamics. By the end, you will be able to answer: &lt;strong>do nearby regions in South America share similar development levels, and how have these spatial clusters evolved between 2013 and 2019?&lt;/strong>&lt;/p>
&lt;p>&lt;strong>Learning objectives:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Understand the concept of spatial autocorrelation and why it matters for regional analysis&lt;/li>
&lt;li>Create choropleth maps and scatter plots to visualize spatial distributions&lt;/li>
&lt;li>Build and interpret a spatial weights matrix using Queen contiguity&lt;/li>
&lt;li>Compute and interpret global Moran&amp;rsquo;s I for spatial dependence testing&lt;/li>
&lt;li>Identify local spatial clusters (HH, LL) and outliers (HL, LH) using LISA statistics&lt;/li>
&lt;li>Explore space-time dynamics of spatial clusters using directional Moran scatter plots&lt;/li>
&lt;li>Compare country-level development trajectories within the spatial framework&lt;/li>
&lt;/ul>
&lt;h3 id="key-concepts-at-a-glance">Key concepts at a glance&lt;/h3>
&lt;p>The post leans on a small vocabulary repeatedly. The rest of the tutorial assumes you can move between these terms quickly. 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 collapsed for a quick scan. If a later section mentions &amp;ldquo;Moran&amp;rsquo;s I&amp;rdquo; or &amp;ldquo;LISA&amp;rdquo; and the term feels slippery, this is the section to re-read.&lt;/p>
&lt;p>&lt;strong>1. Spatial weights matrix&lt;/strong> $W$, $w_{ij}$.
An $n \times n$ matrix encoding which units are &amp;ldquo;neighbours&amp;rdquo; of which. Queen contiguity sets $w_{ij} = 1$ if regions $i$ and $j$ share an edge or vertex.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>In this post, &lt;code>libpysal.weights.Queen.from_dataframe(gdf)&lt;/code> builds a Queen-contiguity weights matrix for the 153 South American regions. Most regions have 4–6 neighbours; islands have zero.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>A friendship graph between regions — who shares a fence with whom.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>2. Global spatial autocorrelation (Moran&amp;rsquo;s I)&lt;/strong> $I = \frac{n}{\sum w} \cdot \frac{\sum_i \sum_j w_{ij}(y_i - \bar y)(y_j - \bar y)}{\sum (y_i - \bar y)^2}$.
A scalar summary of how much like-values cluster geographically. Positive $I$ = clustering; near zero = random; negative = checkerboard.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>Moran&amp;rsquo;s I on &lt;code>SHDI&lt;/code> is 0.5680 in 2013 and 0.6320 in 2019. Strong positive autocorrelation in both years — and the clustering &lt;em>strengthened&lt;/em>. Permutation $p$ = 0.0010 for both: extremely unlikely under a null of randomness.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>How strongly opinions cluster among friends.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>3. Local spatial autocorrelation (LISA)&lt;/strong> $I_i = z_i \sum_j w_{ij} z_j$.
Decomposes the global Moran&amp;rsquo;s I into a per-unit local statistic. Identifies &lt;em>which&lt;/em> regions belong to clusters, not just whether clusters exist.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>LISA in 2019 flags 30 high-high regions, 37 low-low regions, and 6 outliers (5 HL, 1 LH). 80 are statistically not significant. The clusters cover roughly half the map.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>The local cliques inside the social network — &lt;em>who&lt;/em> gathers, not just whether anyone does.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>4. Cluster typology&lt;/strong> HH, LL, HL, LH.
Each significant LISA observation belongs to one of four types: HH (high value, high neighbours = hot spot), LL (cold spot), HL (high value, low neighbours = high outlier), LH (low value, high neighbours = low outlier).&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>The post&amp;rsquo;s LISA map shows HH clusters concentrated in southern Chile and southeast Brazil, LL clusters in Guyana and northern Bolivia. Outliers are rare (5 HL, 1 LH in 2019).&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>Popular kids surrounded by popular kids vs the lone rebel surrounded by the in-crowd.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>5. Choropleth map&lt;/strong>.
A map where each region is shaded by the value of a variable. Quantile and equal-interval are the two most common classification schemes.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>This post draws choropleths of &lt;code>SHDI&lt;/code> for 2013 and 2019 with 5-class quantile breaks. The colour scale exposes regional inequality at a glance.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>A heat-map of the country.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>6. Spatial spillover&lt;/strong>.
The phenomenon that a region&amp;rsquo;s outcome is shaped by its neighbours' outcomes (or covariates). The reason regions are not independent observations.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>In this post, regions adjacent to Venezuelan ones experienced an SHDI decline of -0.0653 on average — the crisis spilled into neighbouring economies. Bolivia, by contrast, gained +0.0333.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>Your neighbours' garage band wakes you up too — your sleep is not independent of theirs.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>7. Space-time dynamics&lt;/strong>.
Comparing LISA results at $t_1$ vs $t_2$ to see how clusters move, expand, or fade. The directional Moran scatter plot summarizes the transitions.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>Between 2013 and 2019, 88% of Venezuelan regions moved into the LL cluster — a hot-spot collapse. The number of LL regions grew from 29 to 37; HH stayed roughly constant at 30–31.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>The social network changes year to year — cliques form and dissolve.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>8. Permutation inference&lt;/strong>.
$p$-values computed by randomly shuffling the outcome across regions thousands of times and asking how often the simulated Moran&amp;rsquo;s I exceeds the observed one. No normality assumption needed.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">
&lt;summary>Example&lt;/summary>
&lt;p>For both 2013 and 2019, 999 random permutations of &lt;code>SHDI&lt;/code> yield $p$ = 0.0010 (the smallest possible with 999 draws). The observed clustering is statistically extreme by any standard.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">
&lt;summary>Analogy&lt;/summary>
&lt;p>Shuffling the seating chart at random to ask whether cliques would form by chance.&lt;/p>
&lt;/details>
&lt;/div>
&lt;h2 id="2-the-esda-pipeline">2. The ESDA pipeline&lt;/h2>
&lt;p>The analysis follows a natural progression from visualization to formal testing. Each step builds on the previous one, moving from &amp;ldquo;what does the data look like?&amp;rdquo; to &amp;ldquo;is the spatial pattern statistically significant?&amp;rdquo; to &amp;ldquo;where exactly are the clusters?&amp;rdquo;&lt;/p>
&lt;pre>&lt;code class="language-mermaid">graph LR
A[&amp;quot;&amp;lt;b&amp;gt;Step 1&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Load &amp;amp;&amp;lt;br/&amp;gt;Explore&amp;quot;] --&amp;gt; B[&amp;quot;&amp;lt;b&amp;gt;Step 2&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Visualize&amp;lt;br/&amp;gt;Maps&amp;quot;]
B --&amp;gt; C[&amp;quot;&amp;lt;b&amp;gt;Step 3&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Spatial&amp;lt;br/&amp;gt;Weights&amp;quot;]
C --&amp;gt; D[&amp;quot;&amp;lt;b&amp;gt;Step 4&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Global&amp;lt;br/&amp;gt;Moran's I&amp;quot;]
D --&amp;gt; E[&amp;quot;&amp;lt;b&amp;gt;Step 5&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Local&amp;lt;br/&amp;gt;LISA&amp;quot;]
E --&amp;gt; F[&amp;quot;&amp;lt;b&amp;gt;Step 6&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Space-Time&amp;lt;br/&amp;gt;Dynamics&amp;quot;]
style A fill:#141413,stroke:#6a9bcc,color:#fff
style B fill:#d97757,stroke:#141413,color:#fff
style C fill:#6a9bcc,stroke:#141413,color:#fff
style D fill:#6a9bcc,stroke:#141413,color:#fff
style E fill:#00d4c8,stroke:#141413,color:#fff
style F fill:#1a3a8a,stroke:#141413,color:#fff
&lt;/code>&lt;/pre>
&lt;p>Steps 1&amp;ndash;2 are purely visual &amp;mdash; they build intuition about where high and low values are concentrated. Step 3 formalizes the notion of &amp;ldquo;neighbors&amp;rdquo; through a spatial weights matrix. Steps 4&amp;ndash;5 use that matrix to compute statistics that quantify spatial clustering, first globally (one number for the whole map) and then locally (one number per region). Step 6 connects the spatial and temporal dimensions by tracking how regions move through the Moran scatter plot between periods.&lt;/p>
&lt;h2 id="3-setup-and-imports">3. Setup and imports&lt;/h2>
&lt;p>The analysis uses &lt;a href="https://geopandas.org/" target="_blank" rel="noopener">GeoPandas&lt;/a> for spatial data handling, &lt;a href="https://pysal.org/" target="_blank" rel="noopener">PySAL&lt;/a> for spatial statistics, and &lt;a href="https://splot.readthedocs.io/" target="_blank" rel="noopener">splot&lt;/a> for specialized spatial visualizations.&lt;/p>
&lt;pre>&lt;code class="language-python">import numpy as np
import pandas as pd
import geopandas as gpd
import matplotlib.pyplot as plt
from libpysal.weights import Queen
from libpysal.weights import lag_spatial
from esda.moran import Moran, Moran_Local
from splot.esda import moran_scatterplot, lisa_cluster
from splot.libpysal import plot_spatial_weights
from adjustText import adjust_text
import mapclassify
# Reproducibility
RANDOM_SEED = 42
# Site color palette
STEEL_BLUE = &amp;quot;#6a9bcc&amp;quot;
WARM_ORANGE = &amp;quot;#d97757&amp;quot;
NEAR_BLACK = &amp;quot;#141413&amp;quot;
TEAL = &amp;quot;#00d4c8&amp;quot;
&lt;/code>&lt;/pre>
&lt;details>
&lt;summary>Dark theme figure styling (click to expand)&lt;/summary>
&lt;pre>&lt;code class="language-python"># Dark theme palette (consistent with site navbar/dark sections)
DARK_NAVY = &amp;quot;#0f1729&amp;quot;
GRID_LINE = &amp;quot;#1f2b5e&amp;quot;
LIGHT_TEXT = &amp;quot;#c8d0e0&amp;quot;
WHITE_TEXT = &amp;quot;#e8ecf2&amp;quot;
# Plot defaults — minimal, spine-free, dark background
plt.rcParams.update({
&amp;quot;figure.facecolor&amp;quot;: DARK_NAVY,
&amp;quot;axes.facecolor&amp;quot;: DARK_NAVY,
&amp;quot;axes.edgecolor&amp;quot;: DARK_NAVY,
&amp;quot;axes.linewidth&amp;quot;: 0,
&amp;quot;axes.labelcolor&amp;quot;: LIGHT_TEXT,
&amp;quot;axes.titlecolor&amp;quot;: WHITE_TEXT,
&amp;quot;axes.spines.top&amp;quot;: False,
&amp;quot;axes.spines.right&amp;quot;: False,
&amp;quot;axes.spines.left&amp;quot;: False,
&amp;quot;axes.spines.bottom&amp;quot;: False,
&amp;quot;axes.grid&amp;quot;: True,
&amp;quot;grid.color&amp;quot;: GRID_LINE,
&amp;quot;grid.linewidth&amp;quot;: 0.6,
&amp;quot;grid.alpha&amp;quot;: 0.8,
&amp;quot;xtick.color&amp;quot;: LIGHT_TEXT,
&amp;quot;ytick.color&amp;quot;: LIGHT_TEXT,
&amp;quot;xtick.major.size&amp;quot;: 0,
&amp;quot;ytick.major.size&amp;quot;: 0,
&amp;quot;text.color&amp;quot;: WHITE_TEXT,
&amp;quot;font.size&amp;quot;: 12,
&amp;quot;legend.frameon&amp;quot;: False,
&amp;quot;legend.fontsize&amp;quot;: 11,
&amp;quot;legend.labelcolor&amp;quot;: LIGHT_TEXT,
&amp;quot;figure.edgecolor&amp;quot;: DARK_NAVY,
&amp;quot;savefig.facecolor&amp;quot;: DARK_NAVY,
&amp;quot;savefig.edgecolor&amp;quot;: DARK_NAVY,
})
&lt;/code>&lt;/pre>
&lt;/details>
&lt;h2 id="4-data-loading-and-exploration">4. Data loading and exploration&lt;/h2>
&lt;p>The dataset is a GeoJSON file containing polygon geometries and development indicators for 153 sub-national regions across South America. It is a spatial version of the data from the &lt;a href="https://carlos-mendez.org/post/python_pca2/">Pooled PCA tutorial&lt;/a>, sourced from the &lt;a href="https://globaldatalab.org/shdi/" target="_blank" rel="noopener">Global Data Lab&lt;/a> (&lt;a href="https://doi.org/10.1038/sdata.2019.38" target="_blank" rel="noopener">Smits and Permanyer, 2019&lt;/a>). Each region has the Subnational Human Development Index (SHDI) and its three component indices &amp;mdash; Health, Education, and Income &amp;mdash; for 2013 and 2019.&lt;/p>
&lt;pre>&lt;code class="language-python">DATA_URL = &amp;quot;https://raw.githubusercontent.com/cmg777/starter-academic-v501/master/content/post/python_esda2/data.geojson&amp;quot;
gdf = gpd.read_file(DATA_URL)
print(f&amp;quot;Loaded: {gdf.shape[0]} rows, {gdf.shape[1]} columns&amp;quot;)
print(f&amp;quot;Countries: {gdf['country'].nunique()}&amp;quot;)
print(f&amp;quot;CRS: {gdf.crs}&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Loaded: 153 rows, 25 columns
Countries: 12
CRS: EPSG:4326
&lt;/code>&lt;/pre>
&lt;p>Before computing change columns, we prepare the data for labeling. Some region names in the raw data are very long (e.g., &amp;ldquo;Chubut, Neuquen, Rio Negro, Santa Cruz, Tierra del Fuego&amp;rdquo;), so we simplify them. We also create a &lt;code>region_country&lt;/code> column that appends the ISO country code to each region name &amp;mdash; this makes labels immediately informative when regions from different countries appear on the same plot.&lt;/p>
&lt;pre>&lt;code class="language-python"># Country name → ISO 3166-1 alpha-3 code
COUNTRY_ISO = {
&amp;quot;Argentina&amp;quot;: &amp;quot;ARG&amp;quot;, &amp;quot;Bolivia&amp;quot;: &amp;quot;BOL&amp;quot;, &amp;quot;Brazil&amp;quot;: &amp;quot;BRA&amp;quot;,
&amp;quot;Chili&amp;quot;: &amp;quot;CHL&amp;quot;, &amp;quot;Colombia&amp;quot;: &amp;quot;COL&amp;quot;, &amp;quot;Ecuador&amp;quot;: &amp;quot;ECU&amp;quot;,
&amp;quot;Guyana&amp;quot;: &amp;quot;GUY&amp;quot;, &amp;quot;Paraguay&amp;quot;: &amp;quot;PRY&amp;quot;, &amp;quot;Peru&amp;quot;: &amp;quot;PER&amp;quot;,
&amp;quot;Suriname&amp;quot;: &amp;quot;SUR&amp;quot;, &amp;quot;Uruguay&amp;quot;: &amp;quot;URY&amp;quot;, &amp;quot;Venezuela&amp;quot;: &amp;quot;VEN&amp;quot;,
}
gdf[&amp;quot;country_iso&amp;quot;] = gdf[&amp;quot;country&amp;quot;].map(COUNTRY_ISO)
# Simplify long region names
RENAME = {
&amp;quot;Catamarca, La Rioja, San Juan&amp;quot;: &amp;quot;Catamarca-La Rioja&amp;quot;,
&amp;quot;Corrientes, Entre Rios, Misiones&amp;quot;: &amp;quot;Corrientes-Misiones&amp;quot;,
&amp;quot;Chubut, Neuquen, Rio Negro, Santa Cruz, Tierra del Fuego&amp;quot;: &amp;quot;Patagonia&amp;quot;,
&amp;quot;La Pampa, San Luis, Mendoza&amp;quot;: &amp;quot;La Pampa-Mendoza&amp;quot;,
&amp;quot;Santiago del Estero, Tucuman&amp;quot;: &amp;quot;Tucuman-Sgo Estero&amp;quot;,
&amp;quot;Tarapaca (incl Arica and Parinacota)&amp;quot;: &amp;quot;Tarapaca&amp;quot;,
&amp;quot;Valparaiso (former Aconcagua)&amp;quot;: &amp;quot;Valparaiso&amp;quot;,
&amp;quot;Los Lagos (incl Los Rios)&amp;quot;: &amp;quot;Los Lagos&amp;quot;,
&amp;quot;Magallanes and La Antartica Chilena&amp;quot;: &amp;quot;Magallanes&amp;quot;,
&amp;quot;Antioquia (incl Medellin)&amp;quot;: &amp;quot;Antioquia&amp;quot;,
&amp;quot;Atlantico (incl Barranquilla)&amp;quot;: &amp;quot;Atlantico&amp;quot;,
&amp;quot;Bolivar (Sur and Norte)&amp;quot;: &amp;quot;Bolivar&amp;quot;,
&amp;quot;Essequibo Islands-West Demerara&amp;quot;: &amp;quot;Essequibo-W Demerara&amp;quot;,
&amp;quot;East Berbice-Corentyne&amp;quot;: &amp;quot;E Berbice-Corentyne&amp;quot;,
&amp;quot;Upper Takutu-Upper Essequibo&amp;quot;: &amp;quot;Upper Takutu-Essequibo&amp;quot;,
&amp;quot;Upper Demerara-Berbice&amp;quot;: &amp;quot;Upper Demerara&amp;quot;,
&amp;quot;Cuyuni-Mazaruni-Upper Essequibo&amp;quot;: &amp;quot;Cuyuni-Mazaruni&amp;quot;,
&amp;quot;Region Metropolitana&amp;quot;: &amp;quot;R. Metropolitana&amp;quot;,
&amp;quot;Federal District&amp;quot;: &amp;quot;Federal Dist.&amp;quot;,
&amp;quot;City of Buenos Aires&amp;quot;: &amp;quot;C. Buenos Aires&amp;quot;,
&amp;quot;Brokopondo and Sipaliwini&amp;quot;: &amp;quot;Brokopondo-Sipaliwini&amp;quot;,
&amp;quot;Montevideo and Metropolitan area&amp;quot;: &amp;quot;Montevideo&amp;quot;,
}
gdf[&amp;quot;region&amp;quot;] = gdf[&amp;quot;region&amp;quot;].replace(RENAME)
# Create region_country label column
gdf[&amp;quot;region_country&amp;quot;] = gdf[&amp;quot;region&amp;quot;] + &amp;quot; (&amp;quot; + gdf[&amp;quot;country_iso&amp;quot;] + &amp;quot;)&amp;quot;
&lt;/code>&lt;/pre>
&lt;p>We then compute the change in SHDI and its components between the two periods.&lt;/p>
&lt;pre>&lt;code class="language-python">gdf[&amp;quot;shdi_change&amp;quot;] = gdf[&amp;quot;shdi2019&amp;quot;] - gdf[&amp;quot;shdi2013&amp;quot;]
gdf[&amp;quot;health_change&amp;quot;] = gdf[&amp;quot;healthindex2019&amp;quot;] - gdf[&amp;quot;healthindex2013&amp;quot;]
gdf[&amp;quot;educ_change&amp;quot;] = gdf[&amp;quot;edindex2019&amp;quot;] - gdf[&amp;quot;edindex2013&amp;quot;]
gdf[&amp;quot;income_change&amp;quot;] = gdf[&amp;quot;incindex2019&amp;quot;] - gdf[&amp;quot;incindex2013&amp;quot;]
print(gdf[[&amp;quot;shdi2013&amp;quot;, &amp;quot;shdi2019&amp;quot;, &amp;quot;shdi_change&amp;quot;]].describe().round(4).to_string())
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> shdi2013 shdi2019 shdi_change
count 153.0000 153.0000 153.0000
mean 0.7424 0.7477 0.0053
std 0.0594 0.0613 0.0319
min 0.5540 0.5580 -0.0670
25% 0.7070 0.7150 0.0090
50% 0.7430 0.7440 0.0150
75% 0.7740 0.7840 0.0250
max 0.8780 0.8830 0.0450
&lt;/code>&lt;/pre>
&lt;p>The dataset covers 153 regions across 12 South American countries. Mean SHDI increased modestly from 0.7424 in 2013 to 0.7477 in 2019 (+0.0053), but the change varied widely: from a maximum decline of -0.0670 to a maximum improvement of +0.0450. The standard deviation of SHDI also increased slightly (0.0594 to 0.0613), hinting that regional disparities may have widened.&lt;/p>
&lt;h2 id="5-exploratory-scatter-plots">5. Exploratory scatter plots&lt;/h2>
&lt;h3 id="51-hdi-scatter-2013-vs-2019">5.1 HDI scatter: 2013 vs 2019&lt;/h3>
&lt;p>A scatter plot of SHDI in 2013 against SHDI in 2019 provides a quick overview of temporal dynamics. Points above the 45-degree line represent regions that improved; points below represent regions that declined.&lt;/p>
&lt;pre>&lt;code class="language-python">fig, ax = plt.subplots(figsize=(8, 7))
ax.scatter(gdf[&amp;quot;shdi2013&amp;quot;], gdf[&amp;quot;shdi2019&amp;quot;],
color=STEEL_BLUE, edgecolors=DARK_NAVY, s=45, alpha=0.75, zorder=3)
lims = [min(gdf[&amp;quot;shdi2013&amp;quot;].min(), gdf[&amp;quot;shdi2019&amp;quot;].min()) - 0.01,
max(gdf[&amp;quot;shdi2013&amp;quot;].max(), gdf[&amp;quot;shdi2019&amp;quot;].max()) + 0.01]
ax.plot(lims, lims, color=WARM_ORANGE, linewidth=1.5, linestyle=&amp;quot;--&amp;quot;,
label=&amp;quot;45° line (no change)&amp;quot;, zorder=2)
ax.set_xlabel(&amp;quot;SHDI 2013&amp;quot;)
ax.set_ylabel(&amp;quot;SHDI 2019&amp;quot;)
ax.set_title(&amp;quot;Subnational HDI: 2013 vs 2019&amp;quot;)
ax.legend()
# Label extreme regions (biggest gains, biggest losses, highest, lowest)
residual = gdf[&amp;quot;shdi2019&amp;quot;] - gdf[&amp;quot;shdi2013&amp;quot;]
extremes = set()
extremes.update(residual.nlargest(3).index.tolist())
extremes.update(residual.nsmallest(3).index.tolist())
extremes.update(gdf[&amp;quot;shdi2019&amp;quot;].nlargest(2).index.tolist())
extremes.update(gdf[&amp;quot;shdi2019&amp;quot;].nsmallest(2).index.tolist())
texts = []
for i in extremes:
texts.append(ax.text(gdf.loc[i, &amp;quot;shdi2013&amp;quot;], gdf.loc[i, &amp;quot;shdi2019&amp;quot;],
gdf.loc[i, &amp;quot;region_country&amp;quot;], fontsize=8, color=LIGHT_TEXT))
adjust_text(texts, ax=ax, arrowprops=dict(arrowstyle=&amp;quot;-&amp;quot;, color=LIGHT_TEXT,
alpha=0.5, lw=0.5))
plt.savefig(&amp;quot;esda2_scatter_hdi.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;)
plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="esda2_scatter_hdi.png" alt="Scatter plot of SHDI 2013 vs SHDI 2019 with 45-degree reference line and labeled extreme regions.">&lt;/p>
&lt;p>Of 153 regions, &lt;strong>126 improved&lt;/strong> their SHDI between 2013 and 2019, while &lt;strong>27 declined&lt;/strong>. The labels identify key cases: at the top, &lt;strong>C. Buenos Aires (ARG)&lt;/strong> and &lt;strong>R. Metropolitana (CHL)&lt;/strong> lead with SHDI above 0.88. At the bottom, &lt;strong>Potaro-Siparuni (GUY)&lt;/strong> and &lt;strong>Barima-Waini (GUY)&lt;/strong> remain the least developed. The biggest decliners &amp;mdash; &lt;strong>Federal Dist. (VEN)&lt;/strong>, &lt;strong>Carabobo (VEN)&lt;/strong>, and &lt;strong>Aragua (VEN)&lt;/strong> &amp;mdash; are all Venezuelan states, falling well below the 45-degree line. The biggest improvers &amp;mdash; &lt;strong>Meta (COL)&lt;/strong>, &lt;strong>Vichada (COL)&lt;/strong>, and &lt;strong>Brokopondo-Sipaliwini (SUR)&lt;/strong> &amp;mdash; rose above the line, with gains up to +0.045 points.&lt;/p>
&lt;h3 id="52-component-scatter-plots">5.2 Component scatter plots&lt;/h3>
&lt;p>The SHDI is a composite of three sub-indices: Health, Education, and Income. Breaking down the change by component reveals which dimensions drove the aggregate patterns.&lt;/p>
&lt;pre>&lt;code class="language-python">fig, axes = plt.subplots(1, 3, figsize=(18, 5.5))
components = [
(&amp;quot;healthindex2013&amp;quot;, &amp;quot;healthindex2019&amp;quot;, &amp;quot;Health Index&amp;quot;),
(&amp;quot;edindex2013&amp;quot;, &amp;quot;edindex2019&amp;quot;, &amp;quot;Education Index&amp;quot;),
(&amp;quot;incindex2013&amp;quot;, &amp;quot;incindex2019&amp;quot;, &amp;quot;Income Index&amp;quot;),
]
for ax, (col13, col19, label) in zip(axes, components):
ax.scatter(gdf[col13], gdf[col19],
color=STEEL_BLUE, edgecolors=DARK_NAVY, s=40, alpha=0.7, zorder=3)
lims = [min(gdf[col13].min(), gdf[col19].min()) - 0.02,
max(gdf[col13].max(), gdf[col19].max()) + 0.02]
ax.plot(lims, lims, color=WARM_ORANGE, linewidth=1.5, linestyle=&amp;quot;--&amp;quot;, zorder=2)
ax.set_xlabel(f&amp;quot;{label} 2013&amp;quot;)
ax.set_ylabel(f&amp;quot;{label} 2019&amp;quot;)
ax.set_title(label)
# Label extreme regions per component
comp_residual = gdf[col19] - gdf[col13]
comp_extremes = set()
comp_extremes.update(comp_residual.nlargest(2).index.tolist())
comp_extremes.update(comp_residual.nsmallest(2).index.tolist())
texts = []
for i in comp_extremes:
texts.append(ax.text(gdf.loc[i, col13], gdf.loc[i, col19],
gdf.loc[i, &amp;quot;region_country&amp;quot;], fontsize=7, color=LIGHT_TEXT))
adjust_text(texts, ax=ax, arrowprops=dict(arrowstyle=&amp;quot;-&amp;quot;, color=LIGHT_TEXT,
alpha=0.5, lw=0.5))
fig.suptitle(&amp;quot;HDI components: 2013 vs 2019&amp;quot;, fontsize=14, y=1.02)
plt.tight_layout()
plt.savefig(&amp;quot;esda2_scatter_components.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;)
plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="esda2_scatter_components.png" alt="Three-panel scatter plot comparing Health, Education, and Income indices between 2013 and 2019.">&lt;/p>
&lt;p>The three components tell very different stories. Health and Education improved almost universally &amp;mdash; the vast majority of points lie above the 45-degree line. Income, however, tells a starkly different story: &lt;strong>71 of 153 regions (46.4%) experienced a decline&lt;/strong> in their income index between 2013 and 2019. This mixed signal &amp;mdash; education and health gains partially offset by income losses &amp;mdash; explains why the aggregate SHDI improvement was so modest (+0.005 on average). The income panel also shows wider scatter, indicating greater heterogeneity in economic trajectories across the continent.&lt;/p>
&lt;h2 id="6-choropleth-maps">6. Choropleth maps&lt;/h2>
&lt;h3 id="61-hdi-levels-across-south-america">6.1 HDI levels across South America&lt;/h3>
&lt;p>The scatter plots tell us &lt;em>what&lt;/em> changed, but not &lt;em>where&lt;/em>. Choropleth maps add the geographic dimension by coloring each region according to its SHDI value. To make the two years directly comparable, we use &lt;a href="https://pysal.org/mapclassify/generated/mapclassify.FisherJenks.html" target="_blank" rel="noopener">Fisher-Jenks natural breaks&lt;/a> computed from 2013 and held constant for 2019. Fisher-Jenks is a classification method that finds natural groupings in data by minimizing within-class variance &amp;mdash; it places break points where the data naturally separates into clusters. This way, a color change between maps reflects a genuine shift in development class, not a shifting classification scheme. The legend shows the number of regions in each class, making it easy to see how the distribution shifted.&lt;/p>
&lt;pre>&lt;code class="language-python">import mapclassify
from matplotlib.patches import Patch
# Fisher-Jenks breaks from 2013 (5 classes)
fj = mapclassify.FisherJenks(gdf[&amp;quot;shdi2013&amp;quot;].values, k=5)
breaks = fj.bins.tolist()
# Extend upper break to cover 2019 max
max_val = max(gdf[&amp;quot;shdi2013&amp;quot;].max(), gdf[&amp;quot;shdi2019&amp;quot;].max())
if max_val &amp;gt; breaks[-1]:
breaks[-1] = float(round(max_val + 0.001, 3))
# Apply same breaks to 2019
fj_2019 = mapclassify.UserDefined(gdf[&amp;quot;shdi2019&amp;quot;].values, bins=breaks)
# Class transitions
classes_2013 = fj.yb
classes_2019 = fj_2019.yb
improved = (classes_2019 &amp;gt; classes_2013).sum()
stayed = (classes_2019 == classes_2013).sum()
declined = (classes_2019 &amp;lt; classes_2013).sum()
print(f&amp;quot;Breaks (from 2013): {[round(b, 3) for b in breaks]}&amp;quot;)
print(f&amp;quot; Improved (moved up): {improved}&amp;quot;)
print(f&amp;quot; Stayed same: {stayed}&amp;quot;)
print(f&amp;quot; Declined (moved down): {declined}&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Breaks (from 2013): [0.622, 0.693, 0.734, 0.789, 0.884]
Improved (moved up): 43
Stayed same: 86
Declined (moved down): 24
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-python"># Class labels
class_labels = []
lower = round(gdf[&amp;quot;shdi2013&amp;quot;].min(), 2)
for b in breaks:
class_labels.append(f&amp;quot;{lower:.2f} – {b:.2f}&amp;quot;)
lower = round(b, 2)
fig, axes = plt.subplots(1, 2, figsize=(16, 12))
cmap = plt.cm.coolwarm
norm = plt.Normalize(vmin=0, vmax=len(breaks) - 1)
for ax, year_col, title, year_fj in [
(axes[0], &amp;quot;shdi2013&amp;quot;, &amp;quot;SHDI 2013&amp;quot;, fj),
(axes[1], &amp;quot;shdi2019&amp;quot;, &amp;quot;SHDI 2019&amp;quot;, fj_2019),
]:
colors = [cmap(norm(c)) for c in year_fj.yb]
gdf.plot(ax=ax, color=colors, edgecolor=GRID_LINE, linewidth=0.3)
ax.set_title(title, fontsize=14, pad=10)
ax.set_axis_off()
# Legend with region counts per class
counts = np.bincount(year_fj.yb, minlength=len(breaks))
handles = [Patch(facecolor=cmap(norm(i)), edgecolor=GRID_LINE,
label=f&amp;quot;{cl} (n={c})&amp;quot;)
for i, (cl, c) in enumerate(zip(class_labels, counts))]
ax.legend(handles=handles, title=&amp;quot;SHDI Class&amp;quot;, loc=&amp;quot;lower right&amp;quot;,
fontsize=10, title_fontsize=11)
# Label extreme regions on both maps
map_extremes = gdf[&amp;quot;shdi2019&amp;quot;].nlargest(3).index.tolist() + \
gdf[&amp;quot;shdi2019&amp;quot;].nsmallest(3).index.tolist()
for ax_map in axes:
texts = []
for i in map_extremes:
centroid = gdf.geometry.iloc[i].centroid
texts.append(ax_map.text(centroid.x, centroid.y,
gdf.loc[i, &amp;quot;region_country&amp;quot;],
fontsize=7, color=WHITE_TEXT, weight=&amp;quot;bold&amp;quot;))
adjust_text(texts, ax=ax_map, arrowprops=dict(arrowstyle=&amp;quot;-|&amp;gt;&amp;quot;,
color=LIGHT_TEXT, alpha=0.9, lw=1.2, mutation_scale=8))
plt.savefig(&amp;quot;esda2_choropleth_hdi.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;)
plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="esda2_choropleth_hdi.png" alt="Side-by-side choropleth maps of SHDI in 2013 and 2019 with Fisher-Jenks classification, showing region counts per class.">&lt;/p>
&lt;p>The Fisher-Jenks classification reveals both persistence and change in South America&amp;rsquo;s development geography. Using the same 2013 breaks for both maps, &lt;strong>43 regions moved up&lt;/strong> at least one class between 2013 and 2019, &lt;strong>86 stayed&lt;/strong> in the same class, and &lt;strong>24 declined&lt;/strong>. The legend counts make the shifts visible: the lowest class shrank from n=6 to n=4, while the middle classes absorbed most of the movement. The Southern Cone and southern Brazil consistently occupy the highest class (red tones), while the Amazon basin, Guyana, and parts of Venezuela anchor the lowest class (blue tones). This visual clustering is precisely what spatial autocorrelation statistics will later quantify &amp;mdash; high values are surrounded by high values, and low values are surrounded by low values.&lt;/p>
&lt;h3 id="62-mapping-hdi-change">6.2 Mapping HDI change&lt;/h3>
&lt;p>A map of SHDI change (2019 minus 2013) reveals the geographic distribution of gains and losses, using a diverging color scale centered at zero.&lt;/p>
&lt;pre>&lt;code class="language-python">fig, ax = plt.subplots(1, 1, figsize=(10, 10))
abs_max = max(abs(gdf[&amp;quot;shdi_change&amp;quot;].min()), abs(gdf[&amp;quot;shdi_change&amp;quot;].max()))
gdf.plot(column=&amp;quot;shdi_change&amp;quot;, cmap=&amp;quot;RdYlGn&amp;quot;, ax=ax, legend=False,
edgecolor=DARK_NAVY, linewidth=0.3, vmin=-abs_max, vmax=abs_max)
ax.set_title(&amp;quot;Change in SHDI (2019 - 2013)&amp;quot;, fontsize=14, pad=10)
ax.set_axis_off()
# Label biggest gainers and losers
change_top = gdf[&amp;quot;shdi_change&amp;quot;].nlargest(3).index.tolist()
change_bot = gdf[&amp;quot;shdi_change&amp;quot;].nsmallest(3).index.tolist()
texts = []
for i in change_top + change_bot:
centroid = gdf.geometry.iloc[i].centroid
texts.append(ax.text(centroid.x, centroid.y, gdf.loc[i, &amp;quot;region&amp;quot;],
fontsize=7, color=WHITE_TEXT, weight=&amp;quot;bold&amp;quot;))
adjust_text(texts, ax=ax, arrowprops=dict(arrowstyle=&amp;quot;-|&amp;gt;&amp;quot;,
color=LIGHT_TEXT, alpha=0.9, lw=1.2,
mutation_scale=8))
sm = plt.cm.ScalarMappable(cmap=&amp;quot;RdYlGn&amp;quot;,
norm=plt.Normalize(vmin=-abs_max, vmax=abs_max))
cbar = fig.colorbar(sm, ax=ax, orientation=&amp;quot;horizontal&amp;quot;,
fraction=0.03, pad=0.02, aspect=40)
cbar.set_label(&amp;quot;SHDI change (2019 - 2013)&amp;quot;)
plt.savefig(&amp;quot;esda2_choropleth_change.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;)
plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="esda2_choropleth_change.png" alt="Choropleth map of SHDI change between 2013 and 2019, with diverging red-green color scale and labeled extremes.">&lt;/p>
&lt;p>The change map reveals that &lt;strong>development losses are geographically concentrated&lt;/strong>, not randomly scattered. The labels pinpoint the extremes: &lt;strong>Federal Dist. (VEN)&lt;/strong>, &lt;strong>Carabobo (VEN)&lt;/strong>, and &lt;strong>Aragua (VEN)&lt;/strong> show the deepest red (declines of up to -0.067 points), while &lt;strong>Vichada (COL)&lt;/strong>, &lt;strong>Meta (COL)&lt;/strong>, and &lt;strong>Brokopondo-Sipaliwini (SUR)&lt;/strong> show the brightest green (improvements of up to +0.045). The geographic concentration of gains and losses suggests that spatial proximity plays a role in development trajectories &amp;mdash; a hypothesis that we formalize in the next sections.&lt;/p>
&lt;h2 id="7-spatial-weights">7. Spatial weights&lt;/h2>
&lt;h3 id="71-what-is-a-spatial-weights-matrix">7.1 What is a spatial weights matrix?&lt;/h3>
&lt;p>To test for spatial clustering formally, we first need to define what &amp;ldquo;neighbor&amp;rdquo; means. A &lt;strong>spatial weights matrix&lt;/strong> $W$ is an $n \times n$ matrix where each entry $w_{ij}$ encodes the spatial relationship between regions $i$ and $j$. If two regions are neighbors, $w_{ij} &amp;gt; 0$; if not, $w_{ij} = 0$.&lt;/p>
&lt;p>The most common approach for polygon data is &lt;strong>contiguity-based weights&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Queen contiguity:&lt;/strong> Two regions are neighbors if they share any boundary point (even a single corner). Named after the queen in chess, which can move in any direction.&lt;/li>
&lt;li>&lt;strong>Rook contiguity:&lt;/strong> Two regions are neighbors only if they share an edge (not just a corner). More restrictive than Queen.&lt;/li>
&lt;/ul>
&lt;p>We use Queen contiguity because it captures the broadest definition of adjacency, which is appropriate for irregular administrative boundaries.&lt;/p>
&lt;h3 id="72-building-queen-contiguity-weights">7.2 Building Queen contiguity weights&lt;/h3>
&lt;p>PySAL&amp;rsquo;s &lt;a href="https://pysal.org/libpysal/generated/libpysal.weights.contiguity.Queen.html" target="_blank" rel="noopener">&lt;code>Queen.from_dataframe()&lt;/code>&lt;/a> builds the weights matrix directly from a GeoDataFrame. After construction, we &lt;strong>row-standardize&lt;/strong> the matrix so that each region&amp;rsquo;s neighbor weights sum to 1. This makes the spatial lag (the weighted average of neighbors' values) directly interpretable as the mean neighbor value.&lt;/p>
&lt;pre>&lt;code class="language-python">from libpysal.weights import Queen
W = Queen.from_dataframe(gdf)
W.transform = &amp;quot;r&amp;quot; # Row-standardize
print(f&amp;quot;Number of regions: {W.n}&amp;quot;)
print(f&amp;quot;Min neighbors: {W.min_neighbors}&amp;quot;)
print(f&amp;quot;Max neighbors: {W.max_neighbors}&amp;quot;)
print(f&amp;quot;Mean neighbors: {W.mean_neighbors:.2f}&amp;quot;)
print(f&amp;quot;Islands: {W.islands}&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Number of regions: 153
Min neighbors: 0
Max neighbors: 11
Mean neighbors: 4.93
Islands: [87, 145]
&lt;/code>&lt;/pre>
&lt;p>The Queen contiguity matrix connects 153 regions with an average of 4.93 neighbors each (minimum 0, maximum 11). Two regions have &lt;strong>no neighbors&lt;/strong> (islands): &lt;strong>San Andres (COL)&lt;/strong> (index 87) and &lt;strong>Nueva Esparta (VEN)&lt;/strong> (index 145) &amp;mdash; both are island territories separated from the mainland by water. PySAL excludes these isolates from spatial autocorrelation calculations, as they have no defined spatial relationship with other regions. Row-standardization ensures that each region&amp;rsquo;s spatial lag is the simple average of its neighbors' values, regardless of how many neighbors it has.&lt;/p>
&lt;h3 id="73-visualizing-the-connectivity-structure">7.3 Visualizing the connectivity structure&lt;/h3>
&lt;p>The &lt;a href="https://splot.readthedocs.io/en/latest/generated/splot.libpysal.plot_spatial_weights.html" target="_blank" rel="noopener">&lt;code>plot_spatial_weights()&lt;/code>&lt;/a> function from splot overlays the weights network on the map, drawing lines between each region&amp;rsquo;s centroid and its neighbors' centroids.&lt;/p>
&lt;pre>&lt;code class="language-python">fig, ax = plt.subplots(figsize=(10, 10))
gdf.plot(ax=ax, facecolor=&amp;quot;none&amp;quot;, edgecolor=GRID_LINE, linewidth=0.5)
plot_spatial_weights(W, gdf, ax=ax)
ax.set_title(&amp;quot;Queen contiguity weights&amp;quot;, fontsize=14, pad=10)
ax.set_axis_off()
plt.savefig(&amp;quot;esda2_spatial_weights.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;)
plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="esda2_spatial_weights.png" alt="Map of South America with Queen contiguity network overlaid, showing lines connecting neighboring region centroids.">&lt;/p>
&lt;p>The network visualization shows the connectivity structure underlying all spatial statistics in this tutorial. Denser networks appear in areas with many small regions (e.g., southern Brazil, northern Argentina), while sparser connections appear in areas with large administrative units (e.g., the Amazon basin). The two island territories (San Andres and Nueva Esparta) appear as isolated dots with no connecting lines. This network is the foundation for computing spatial lags &amp;mdash; the weighted average of neighbors' values &amp;mdash; which is the building block of Moran&amp;rsquo;s I.&lt;/p>
&lt;h2 id="8-global-spatial-autocorrelation">8. Global spatial autocorrelation&lt;/h2>
&lt;h3 id="81-morans-i-concept-and-intuition">8.1 Moran&amp;rsquo;s I: concept and intuition&lt;/h3>
&lt;p>&lt;strong>Moran&amp;rsquo;s I&lt;/strong> is the most widely used measure of global spatial autocorrelation. It answers a simple question: &lt;strong>do similar values tend to cluster together more than expected by chance?&lt;/strong> Think of it like temperature on a weather map &amp;mdash; if it is hot in one city, nearby cities are likely hot too. Moran&amp;rsquo;s I measures how strongly this &amp;ldquo;neighbor similarity&amp;rdquo; holds for development levels across South American regions.&lt;/p>
&lt;p>The statistic is defined as:&lt;/p>
&lt;p>$$I = \frac{n}{\sum_{i} \sum_{j} w_{ij}} \cdot \frac{\sum_{i} \sum_{j} w_{ij} (x_i - \bar{x})(x_j - \bar{x})}{\sum_{i} (x_i - \bar{x})^2}$$&lt;/p>
&lt;p>where $n$ is the number of regions, $w_{ij}$ are the spatial weights, $x_i$ is the value at region $i$, and $\bar{x}$ is the overall mean. In plain language: Moran&amp;rsquo;s I compares the product of deviations from the mean for each pair of neighbors. If high-value regions tend to be next to high-value regions (and low next to low), these products are positive, and $I$ is positive.&lt;/p>
&lt;ul>
&lt;li>$I \approx +1$: strong positive spatial autocorrelation (clustering of similar values)&lt;/li>
&lt;li>$I \approx 0$: no spatial pattern (random arrangement)&lt;/li>
&lt;li>$I \approx -1$: strong negative spatial autocorrelation (checkerboard pattern)&lt;/li>
&lt;/ul>
&lt;p>The expected value under spatial randomness is $E(I) = -1/(n-1)$, which approaches zero for large $n$.&lt;/p>
&lt;h3 id="82-morans-i-for-hdi-2013-and-2019">8.2 Moran&amp;rsquo;s I for HDI (2013 and 2019)&lt;/h3>
&lt;p>We compute Moran&amp;rsquo;s I with 999 random permutations to generate a reference distribution and assess statistical significance. A &lt;strong>permutation test&lt;/strong> works by randomly shuffling all the SHDI values across the map 999 times &amp;mdash; like dealing cards to random seats. If the real Moran&amp;rsquo;s I is more extreme than almost all the shuffled values, we can be confident the spatial pattern is real, not coincidence.&lt;/p>
&lt;pre>&lt;code class="language-python">from esda.moran import Moran
moran_2013 = Moran(gdf[&amp;quot;shdi2013&amp;quot;], W, permutations=999)
moran_2019 = Moran(gdf[&amp;quot;shdi2019&amp;quot;], W, permutations=999)
print(f&amp;quot;SHDI 2013: I = {moran_2013.I:.4f}, p-value = {moran_2013.p_sim:.4f}, &amp;quot;
f&amp;quot;z-score = {moran_2013.z_sim:.4f}&amp;quot;)
print(f&amp;quot;SHDI 2019: I = {moran_2019.I:.4f}, p-value = {moran_2019.p_sim:.4f}, &amp;quot;
f&amp;quot;z-score = {moran_2019.z_sim:.4f}&amp;quot;)
print(f&amp;quot;Expected I (random): {moran_2013.EI:.4f}&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">SHDI 2013: I = 0.5680, p-value = 0.0010, z-score = 10.7661
SHDI 2019: I = 0.6320, p-value = 0.0010, z-score = 11.9890
Expected I (random): -0.0066
&lt;/code>&lt;/pre>
&lt;p>Moran&amp;rsquo;s I for SHDI is &lt;strong>strongly positive and highly significant&lt;/strong> in both years. In 2013, $I = 0.5680$ (p = 0.001, z = 10.77), and in 2019, $I = 0.6320$ (p = 0.001, z = 11.99). Both values are far above the expected value under spatial randomness ($E(I) = -0.0066$), confirming that regions with similar development levels are spatially clustered. Notably, &lt;strong>spatial autocorrelation strengthened&lt;/strong> from 2013 to 2019 ($I$ increased from 0.568 to 0.632), suggesting that development clusters became more pronounced over the period &amp;mdash; the spatial divide deepened.&lt;/p>
&lt;h3 id="83-moran-scatter-plot">8.3 Moran scatter plot&lt;/h3>
&lt;p>The &lt;strong>Moran scatter plot&lt;/strong> visualizes the spatial relationship by plotting each region&amp;rsquo;s standardized value ($z_i$) against the spatial lag of its neighbors ($Wz_i$). The slope of the regression line through the scatter equals Moran&amp;rsquo;s I. The four quadrants identify the type of spatial association for each region:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>HH (top-right):&lt;/strong> High values surrounded by high neighbors&lt;/li>
&lt;li>&lt;strong>LL (bottom-left):&lt;/strong> Low values surrounded by low neighbors&lt;/li>
&lt;li>&lt;strong>LH (top-left):&lt;/strong> Low values surrounded by high neighbors (spatial outlier)&lt;/li>
&lt;li>&lt;strong>HL (bottom-right):&lt;/strong> High values surrounded by low neighbors (spatial outlier)&lt;/li>
&lt;/ul>
&lt;pre>&lt;code class="language-python">from scipy import stats as scipy_stats
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
for ax, moran_obj, year in [
(axes[0], moran_2013, &amp;quot;2013&amp;quot;),
(axes[1], moran_2019, &amp;quot;2019&amp;quot;),
]:
# Standardize values and compute spatial lag
y = gdf[f&amp;quot;shdi{year}&amp;quot;].values
z = (y - y.mean()) / y.std()
wz = lag_spatial(W, z)
ax.scatter(z, wz, color=STEEL_BLUE, s=35, alpha=0.7,
edgecolors=GRID_LINE, linewidths=0.3, zorder=3)
# Regression line (slope = Moran's I)
slope, intercept, _, _, _ = scipy_stats.linregress(z, wz)
x_range = np.array([z.min(), z.max()])
ax.plot(x_range, intercept + slope * x_range, color=WARM_ORANGE,
linewidth=1.5, zorder=2)
# Quadrant dividers at origin
ax.axhline(0, color=LIGHT_TEXT, linewidth=0.8, alpha=0.5, zorder=1)
ax.axvline(0, color=LIGHT_TEXT, linewidth=0.8, alpha=0.5, zorder=1)
# Quadrant labels
xlim, ylim = ax.get_xlim(), ax.get_ylim()
pad_x = (xlim[1] - xlim[0]) * 0.05
pad_y = (ylim[1] - ylim[0]) * 0.05
ax.text(xlim[1] - pad_x, ylim[1] - pad_y, &amp;quot;HH&amp;quot;, fontsize=13,
ha=&amp;quot;right&amp;quot;, va=&amp;quot;top&amp;quot;, color=LIGHT_TEXT, alpha=0.5)
ax.text(xlim[0] + pad_x, ylim[1] - pad_y, &amp;quot;LH&amp;quot;, fontsize=13,
ha=&amp;quot;left&amp;quot;, va=&amp;quot;top&amp;quot;, color=LIGHT_TEXT, alpha=0.5)
ax.text(xlim[0] + pad_x, ylim[0] + pad_y, &amp;quot;LL&amp;quot;, fontsize=13,
ha=&amp;quot;left&amp;quot;, va=&amp;quot;bottom&amp;quot;, color=LIGHT_TEXT, alpha=0.5)
ax.text(xlim[1] - pad_x, ylim[0] + pad_y, &amp;quot;HL&amp;quot;, fontsize=13,
ha=&amp;quot;right&amp;quot;, va=&amp;quot;bottom&amp;quot;, color=LIGHT_TEXT, alpha=0.5)
ax.set_xlabel(f&amp;quot;SHDI {year} (standardized)&amp;quot;)
ax.set_ylabel(f&amp;quot;Spatial lag of SHDI {year}&amp;quot;)
ax.set_title(f&amp;quot;({'a' if year == '2013' else 'b'}) Moran scatter plot &amp;quot;
f&amp;quot;— {year} (I = {moran_obj.I:.4f})&amp;quot;)
plt.tight_layout()
plt.savefig(&amp;quot;esda2_moran_global.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;)
plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="esda2_moran_global.png" alt="Two-panel Moran scatter plot for SHDI in 2013 and 2019.">&lt;/p>
&lt;p>Both Moran scatter plots show a clear positive slope, with the majority of regions falling in the &lt;strong>HH and LL quadrants&lt;/strong> (positive spatial autocorrelation). The steeper slope in the 2019 panel visually confirms the increase in Moran&amp;rsquo;s I from 0.5680 to 0.6320. Regions in the HH quadrant (top-right) represent the Southern Cone prosperity cluster, while regions in the LL quadrant (bottom-left) represent the Amazon/Guyana deprivation cluster. The relatively few points in the LH and HL quadrants are spatial outliers &amp;mdash; regions whose development level diverges sharply from their neighbors.&lt;/p>
&lt;h2 id="9-local-spatial-autocorrelation-lisa">9. Local spatial autocorrelation (LISA)&lt;/h2>
&lt;h3 id="91-from-global-to-local-why-lisa-matters">9.1 From global to local: why LISA matters&lt;/h3>
&lt;p>Global Moran&amp;rsquo;s I gives us &lt;strong>one number&lt;/strong> for the entire map, confirming that spatial clustering exists. But it does not tell us &lt;strong>where&lt;/strong> the clusters are located. &lt;strong>Local Indicators of Spatial Association (LISA)&lt;/strong> decompose the global statistic into a contribution from each individual region (&lt;a href="https://doi.org/10.1111/j.1538-4632.1995.tb00338.x" target="_blank" rel="noopener">Anselin, 1995&lt;/a>).&lt;/p>
&lt;p>The local Moran statistic for region $i$ is:&lt;/p>
&lt;p>$$I_i = z_i \sum_{j} w_{ij} z_j$$&lt;/p>
&lt;p>where $z_i = (x_i - \bar{x}) / s$ is the standardized value at region $i$ and $\sum_{j} w_{ij} z_j$ is its spatial lag (the weighted average of neighbors' standardized values). In plain language: each region&amp;rsquo;s local statistic is the product of its own deviation from the mean and the average deviation of its neighbors. In the code, $x_i$ corresponds to &lt;code>gdf[&amp;quot;shdi2019&amp;quot;]&lt;/code> and $w_{ij}$ to the row-standardized Queen weights &lt;code>W&lt;/code>.&lt;/p>
&lt;p>Each region receives a local Moran&amp;rsquo;s I statistic and is classified into one of four types based on its quadrant in the Moran scatter plot:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>HH (High-High):&lt;/strong> A high-value region surrounded by high-value neighbors &amp;mdash; a &amp;ldquo;hot spot&amp;rdquo; or prosperity cluster&lt;/li>
&lt;li>&lt;strong>LL (Low-Low):&lt;/strong> A low-value region surrounded by low-value neighbors &amp;mdash; a &amp;ldquo;cold spot&amp;rdquo; or deprivation trap&lt;/li>
&lt;li>&lt;strong>HL (High-Low):&lt;/strong> A high-value region surrounded by low-value neighbors &amp;mdash; a positive spatial outlier&lt;/li>
&lt;li>&lt;strong>LH (Low-High):&lt;/strong> A low-value region surrounded by high-value neighbors &amp;mdash; a negative spatial outlier&lt;/li>
&lt;/ul>
&lt;p>Statistical significance is assessed via permutation tests. Only regions with p-values below a chosen threshold (here, $p &amp;lt; 0.10$) are classified as belonging to a cluster.&lt;/p>
&lt;h3 id="92-lisa-for-hdi-2019">9.2 LISA for HDI 2019&lt;/h3>
&lt;p>We compute the local Moran&amp;rsquo;s I for SHDI in 2019 and visualize the results as a Moran scatter plot with significant regions colored by quadrant (left panel) and a cluster map (right panel).&lt;/p>
&lt;pre>&lt;code class="language-python">localMoran_2019 = Moran_Local(gdf[&amp;quot;shdi2019&amp;quot;], W, permutations=999, seed=12345)
wlag_2019 = lag_spatial(W, gdf[&amp;quot;shdi2019&amp;quot;].values)
sig_2019 = localMoran_2019.p_sim &amp;lt; 0.10
q_labels = {1: &amp;quot;HH&amp;quot;, 2: &amp;quot;LH&amp;quot;, 3: &amp;quot;LL&amp;quot;, 4: &amp;quot;HL&amp;quot;}
for q_val, q_name in q_labels.items():
count = ((localMoran_2019.q == q_val) &amp;amp; sig_2019).sum()
print(f&amp;quot; {q_name}: {count}&amp;quot;)
print(f&amp;quot; Not significant: {(~sig_2019).sum()}&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> HH: 30
LH: 1
LL: 37
HL: 5
Not significant: 80
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-python">LISA_COLORS = {1: &amp;quot;#d7191c&amp;quot;, 2: &amp;quot;#89cff0&amp;quot;, 3: &amp;quot;#2c7bb6&amp;quot;, 4: &amp;quot;#fdae61&amp;quot;}
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(14, 6))
# (a) LISA scatter plot with colored quadrants
ax = axes[0]
slope, intercept, _, _, _ = scipy_stats.linregress(gdf[&amp;quot;shdi2019&amp;quot;].values, wlag_2019)
# Non-significant points (grey)
ns_mask = ~sig_2019
ax.scatter(gdf.loc[ns_mask, &amp;quot;shdi2019&amp;quot;], wlag_2019[ns_mask],
color=&amp;quot;#bababa&amp;quot;, s=30, alpha=0.4, edgecolors=GRID_LINE,
linewidths=0.3, label=&amp;quot;ns&amp;quot;, zorder=2)
# Significant points colored by quadrant
for q_val, q_name in q_labels.items():
mask = (localMoran_2019.q == q_val) &amp;amp; sig_2019
if mask.any():
ax.scatter(gdf.loc[mask, &amp;quot;shdi2019&amp;quot;], wlag_2019[mask],
color=LISA_COLORS[q_val], s=40, alpha=0.8,
edgecolors=GRID_LINE, linewidths=0.3,
label=q_name, zorder=3)
# Regression line
x_range = np.array([gdf[&amp;quot;shdi2019&amp;quot;].min(), gdf[&amp;quot;shdi2019&amp;quot;].max()])
ax.plot(x_range, intercept + slope * x_range, color=WARM_ORANGE,
linewidth=1.2, zorder=1)
# Crosshairs at mean
ax.axhline(wlag_2019.mean(), color=GRID_LINE, linewidth=0.8, linestyle=&amp;quot;--&amp;quot;, zorder=0)
ax.axvline(gdf[&amp;quot;shdi2019&amp;quot;].mean(), color=GRID_LINE, linewidth=0.8, linestyle=&amp;quot;--&amp;quot;, zorder=0)
ax.set_xlabel(&amp;quot;SHDI 2019&amp;quot;)
ax.set_ylabel(&amp;quot;Spatial lag of SHDI 2019&amp;quot;)
ax.set_title(f&amp;quot;(a) Moran scatter plot (I = {moran_2019.I:.4f})&amp;quot;)
# (b) LISA cluster map
lisa_cluster(localMoran_2019, gdf, p=0.10,
legend_kwds={&amp;quot;bbox_to_anchor&amp;quot;: (0.02, 0.90)}, ax=axes[1])
axes[1].set_facecolor(DARK_NAVY)
axes[1].set_title(&amp;quot;(b) LISA clusters (p &amp;lt; 0.10)&amp;quot;)
# Label extreme LISA regions on both panels
label_idx = []
hh_mask = (localMoran_2019.q == 1) &amp;amp; sig_2019
if hh_mask.any():
label_idx += gdf.loc[hh_mask, &amp;quot;shdi2019&amp;quot;].nlargest(3).index.tolist()
ll_mask = (localMoran_2019.q == 3) &amp;amp; sig_2019
if ll_mask.any():
label_idx += gdf.loc[ll_mask, &amp;quot;shdi2019&amp;quot;].nsmallest(3).index.tolist()
hl_mask = (localMoran_2019.q == 4) &amp;amp; sig_2019
if hl_mask.any():
label_idx.append(gdf.loc[hl_mask, &amp;quot;shdi2019&amp;quot;].idxmax())
lh_mask = (localMoran_2019.q == 2) &amp;amp; sig_2019
if lh_mask.any():
label_idx.append(gdf.loc[lh_mask, &amp;quot;shdi2019&amp;quot;].idxmin())
# Scatter labels
texts = [axes[0].text(gdf.loc[i, &amp;quot;shdi2019&amp;quot;], wlag_2019[i], gdf.loc[i, &amp;quot;region&amp;quot;],
fontsize=7, color=LIGHT_TEXT) for i in label_idx]
adjust_text(texts, ax=axes[0], arrowprops=dict(arrowstyle=&amp;quot;-&amp;quot;, color=LIGHT_TEXT,
alpha=0.5, lw=0.5))
# Map labels
texts = [axes[1].text(gdf.geometry.iloc[i].centroid.x, gdf.geometry.iloc[i].centroid.y,
gdf.loc[i, &amp;quot;region_country&amp;quot;], fontsize=7, color=WHITE_TEXT, weight=&amp;quot;bold&amp;quot;)
for i in label_idx]
adjust_text(texts, ax=axes[1], arrowprops=dict(arrowstyle=&amp;quot;-|&amp;gt;&amp;quot;, color=LIGHT_TEXT,
alpha=0.9, lw=1.2, mutation_scale=8))
plt.tight_layout()
plt.savefig(&amp;quot;esda2_lisa_2019.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;)
plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="esda2_lisa_2019.png" alt="Two-panel LISA analysis for SHDI 2019: Moran scatter plot with labeled extreme regions (left) and LISA cluster map (right).">&lt;/p>
&lt;p>At the 10% significance level, the 2019 LISA analysis identifies &lt;strong>30 HH regions&lt;/strong>, &lt;strong>37 LL regions&lt;/strong>, &lt;strong>5 HL outliers&lt;/strong>, &lt;strong>1 LH outlier&lt;/strong>, and &lt;strong>80 non-significant regions&lt;/strong>. The labels highlight the extremes of each cluster type. The &lt;strong>three highest HH regions&lt;/strong> &amp;mdash; R. Metropolitana (CHL, SHDI = 0.883), C. Buenos Aires (ARG, 0.882), and Antofagasta (CHL, 0.875) &amp;mdash; anchor the Southern Cone prosperity core. The &lt;strong>three lowest LL regions&lt;/strong> &amp;mdash; Potaro-Siparuni (GUY, 0.558), Barima-Waini (GUY, 0.592), and Upper Takutu-Essequibo (GUY, 0.601) &amp;mdash; anchor the deprivation cluster in northern South America. &lt;strong>San Andres (COL)&lt;/strong> (0.789) appears as an HL outlier: a high-development island surrounded by lower-development mainland neighbors. &lt;strong>Potosi (BOL)&lt;/strong> (0.631) is the lone LH outlier: a lagging region surrounded by better-performing neighbors.&lt;/p>
&lt;h3 id="93-lisa-for-hdi-2013">9.3 LISA for HDI 2013&lt;/h3>
&lt;p>Repeating the analysis for 2013 allows us to compare how clusters have evolved over time.&lt;/p>
&lt;pre>&lt;code class="language-python">localMoran_2013 = Moran_Local(gdf[&amp;quot;shdi2013&amp;quot;], W, permutations=999, seed=12345)
wlag_2013 = lag_spatial(W, gdf[&amp;quot;shdi2013&amp;quot;].values)
sig_2013 = localMoran_2013.p_sim &amp;lt; 0.10
for q_val, q_name in q_labels.items():
count = ((localMoran_2013.q == q_val) &amp;amp; sig_2013).sum()
print(f&amp;quot; {q_name}: {count}&amp;quot;)
print(f&amp;quot; Not significant: {(~sig_2013).sum()}&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> HH: 31
LH: 0
LL: 29
HL: 5
Not significant: 88
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-python">fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(14, 6))
# (a) LISA scatter plot with colored quadrants
ax = axes[0]
slope, intercept, _, _, _ = scipy_stats.linregress(gdf[&amp;quot;shdi2013&amp;quot;].values, wlag_2013)
ns_mask = ~sig_2013
ax.scatter(gdf.loc[ns_mask, &amp;quot;shdi2013&amp;quot;], wlag_2013[ns_mask],
color=&amp;quot;#bababa&amp;quot;, s=30, alpha=0.4, edgecolors=GRID_LINE,
linewidths=0.3, label=&amp;quot;ns&amp;quot;, zorder=2)
for q_val, q_name in q_labels.items():
mask = (localMoran_2013.q == q_val) &amp;amp; sig_2013
if mask.any():
ax.scatter(gdf.loc[mask, &amp;quot;shdi2013&amp;quot;], wlag_2013[mask],
color=LISA_COLORS[q_val], s=40, alpha=0.8,
edgecolors=GRID_LINE, linewidths=0.3,
label=q_name, zorder=3)
x_range = np.array([gdf[&amp;quot;shdi2013&amp;quot;].min(), gdf[&amp;quot;shdi2013&amp;quot;].max()])
ax.plot(x_range, intercept + slope * x_range, color=WARM_ORANGE,
linewidth=1.2, zorder=1)
ax.axhline(wlag_2013.mean(), color=GRID_LINE, linewidth=0.8, linestyle=&amp;quot;--&amp;quot;, zorder=0)
ax.axvline(gdf[&amp;quot;shdi2013&amp;quot;].mean(), color=GRID_LINE, linewidth=0.8, linestyle=&amp;quot;--&amp;quot;, zorder=0)
ax.set_xlabel(&amp;quot;SHDI 2013&amp;quot;)
ax.set_ylabel(&amp;quot;Spatial lag of SHDI 2013&amp;quot;)
ax.set_title(f&amp;quot;(a) Moran scatter plot (I = {moran_2013.I:.4f})&amp;quot;)
# (b) LISA cluster map
lisa_cluster(localMoran_2013, gdf, p=0.10,
legend_kwds={&amp;quot;bbox_to_anchor&amp;quot;: (0.02, 0.90)}, ax=axes[1])
axes[1].set_facecolor(DARK_NAVY)
axes[1].set_title(&amp;quot;(b) LISA clusters (p &amp;lt; 0.10)&amp;quot;)
# Label extreme LISA regions (3 HH, 3 LL, 1 HL; no LH in 2013)
label_idx = []
hh_mask = (localMoran_2013.q == 1) &amp;amp; sig_2013
if hh_mask.any():
label_idx += gdf.loc[hh_mask, &amp;quot;shdi2013&amp;quot;].nlargest(3).index.tolist()
ll_mask = (localMoran_2013.q == 3) &amp;amp; sig_2013
if ll_mask.any():
label_idx += gdf.loc[ll_mask, &amp;quot;shdi2013&amp;quot;].nsmallest(3).index.tolist()
hl_mask = (localMoran_2013.q == 4) &amp;amp; sig_2013
if hl_mask.any():
label_idx.append(gdf.loc[hl_mask, &amp;quot;shdi2013&amp;quot;].idxmax())
lh_mask = (localMoran_2013.q == 2) &amp;amp; sig_2013
if lh_mask.any():
label_idx.append(gdf.loc[lh_mask, &amp;quot;shdi2013&amp;quot;].idxmin())
texts = [axes[0].text(gdf.loc[i, &amp;quot;shdi2013&amp;quot;], wlag_2013[i], gdf.loc[i, &amp;quot;region&amp;quot;],
fontsize=7, color=LIGHT_TEXT) for i in label_idx]
adjust_text(texts, ax=axes[0], arrowprops=dict(arrowstyle=&amp;quot;-&amp;quot;, color=LIGHT_TEXT,
alpha=0.5, lw=0.5))
texts = [axes[1].text(gdf.geometry.iloc[i].centroid.x, gdf.geometry.iloc[i].centroid.y,
gdf.loc[i, &amp;quot;region_country&amp;quot;], fontsize=7, color=WHITE_TEXT, weight=&amp;quot;bold&amp;quot;)
for i in label_idx]
adjust_text(texts, ax=axes[1], arrowprops=dict(arrowstyle=&amp;quot;-|&amp;gt;&amp;quot;, color=LIGHT_TEXT,
alpha=0.9, lw=1.2, mutation_scale=8))
plt.tight_layout()
plt.savefig(&amp;quot;esda2_lisa_2013.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;)
plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="esda2_lisa_2013.png" alt="Two-panel LISA analysis for SHDI 2013: Moran scatter plot with labeled regions (left) and LISA cluster map (right).">&lt;/p>
&lt;p>The 2013 LISA analysis identifies &lt;strong>31 HH regions&lt;/strong>, &lt;strong>29 LL regions&lt;/strong>, &lt;strong>5 HL outliers&lt;/strong>, &lt;strong>0 LH outliers&lt;/strong>, and &lt;strong>88 non-significant regions&lt;/strong>. The same three HH leaders appear: C. Buenos Aires (ARG, 0.878), R. Metropolitana (CHL, 0.857), and Antofagasta (CHL, 0.852). The same three LL anchors persist: Potaro-Siparuni (GUY, 0.554), Barima-Waini (GUY, 0.577), and Upper Takutu-Essequibo (GUY, 0.585). The HL outlier in 2013 is &lt;strong>Nueva Esparta (VEN)&lt;/strong> (0.797) &amp;mdash; an island state that performed well despite its mainland neighbors. Comparing with 2019, the most striking change is the &lt;strong>expansion of the LL cluster&lt;/strong> from 29 to 37 regions, while the HH cluster remained roughly stable (31 to 30). This asymmetric evolution is consistent with the income decline concentrated in Venezuela, which pulled more regions into the deprivation cluster.&lt;/p>
&lt;h3 id="94-comparing-lisa-clusters-across-time">9.4 Comparing LISA clusters across time&lt;/h3>
&lt;p>A transition table reveals how regions moved between LISA categories from 2013 to 2019.&lt;/p>
&lt;pre>&lt;code class="language-python">sig_2013 = localMoran_2013.p_sim &amp;lt; 0.10
sig_2019 = localMoran_2019.p_sim &amp;lt; 0.10
q_labels = {1: &amp;quot;HH&amp;quot;, 2: &amp;quot;LH&amp;quot;, 3: &amp;quot;LL&amp;quot;, 4: &amp;quot;HL&amp;quot;}
labels_2013 = [&amp;quot;ns&amp;quot; if not sig_2013[i] else q_labels[localMoran_2013.q[i]]
for i in range(len(gdf))]
labels_2019 = [&amp;quot;ns&amp;quot; if not sig_2019[i] else q_labels[localMoran_2019.q[i]]
for i in range(len(gdf))]
transition_df = pd.crosstab(
pd.Series(labels_2013, name=&amp;quot;2013&amp;quot;),
pd.Series(labels_2019, name=&amp;quot;2019&amp;quot;)
)
print(transition_df.to_string())
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">2019 HH HL LH LL ns
2013
HH 27 0 0 0 4
HL 0 2 0 2 1
LL 0 2 0 18 9
ns 3 1 1 17 66
&lt;/code>&lt;/pre>
&lt;p>The transition table reveals strong &lt;strong>cluster persistence&lt;/strong>. Of the 31 regions in the HH cluster in 2013, &lt;strong>27 remained HH&lt;/strong> in 2019 (87% persistence), while only 4 became non-significant. Of the 29 LL regions in 2013, &lt;strong>18 remained LL&lt;/strong> (62% persistence). The most notable transition is from non-significant to LL: &lt;strong>17 regions&lt;/strong> that were not part of any significant cluster in 2013 joined the low-development cluster by 2019. This expansion of the LL cluster, combined with the high persistence of HH, paints a picture of entrenched spatial inequality &amp;mdash; prosperity clusters are stable, and deprivation clusters are growing.&lt;/p>
&lt;h2 id="10-space-time-dynamics">10. Space-time dynamics&lt;/h2>
&lt;h3 id="101-directional-moran-scatter-plot">10.1 Directional Moran scatter plot&lt;/h3>
&lt;p>The LISA transition table tracks changes in statistical significance, but regions can also move &lt;em>within&lt;/em> the Moran scatter plot even without crossing significance thresholds. A &lt;strong>directional Moran scatter plot&lt;/strong> shows the movement vector for each region from its 2013 position to its 2019 position in the (standardized value, spatial lag) space. The arrows reveal the direction and magnitude of change in both a region&amp;rsquo;s own development and its neighbors' development.&lt;/p>
&lt;p>To make the two periods comparable, we standardize both years using the &lt;strong>pooled mean and standard deviation&lt;/strong> (across both periods combined), following the same logic as the &lt;a href="https://carlos-mendez.org/post/python_pca2/">Pooled PCA tutorial&lt;/a>.&lt;/p>
&lt;pre>&lt;code class="language-python">from libpysal.weights import lag_spatial
# Standardize using pooled parameters
mean_all = np.mean(np.concatenate([gdf[&amp;quot;shdi2013&amp;quot;].values, gdf[&amp;quot;shdi2019&amp;quot;].values]))
std_all = np.std(np.concatenate([gdf[&amp;quot;shdi2013&amp;quot;].values, gdf[&amp;quot;shdi2019&amp;quot;].values]))
z_2013 = (gdf[&amp;quot;shdi2013&amp;quot;].values - mean_all) / std_all
z_2019 = (gdf[&amp;quot;shdi2019&amp;quot;].values - mean_all) / std_all
# Spatial lags
wz_2013 = lag_spatial(W, z_2013)
wz_2019 = lag_spatial(W, z_2019)
fig, ax = plt.subplots(figsize=(9, 8))
for i in range(len(gdf)):
ax.annotate(&amp;quot;&amp;quot;, xy=(z_2019[i], wz_2019[i]),
xytext=(z_2013[i], wz_2013[i]),
arrowprops=dict(arrowstyle=&amp;quot;-&amp;gt;&amp;quot;, color=STEEL_BLUE,
alpha=0.5, lw=0.8))
ax.scatter(z_2013, wz_2013, color=WARM_ORANGE, s=20, alpha=0.6,
label=&amp;quot;2013&amp;quot;, zorder=4)
ax.scatter(z_2019, wz_2019, color=TEAL, s=20, alpha=0.6,
label=&amp;quot;2019&amp;quot;, zorder=4)
ax.axhline(0, color=GRID_LINE, linewidth=1)
ax.axvline(0, color=GRID_LINE, linewidth=1)
ax.set_xlabel(&amp;quot;SHDI (standardized)&amp;quot;)
ax.set_ylabel(&amp;quot;Spatial lag of SHDI&amp;quot;)
ax.set_title(&amp;quot;Directional Moran scatter plot: movements from 2013 to 2019&amp;quot;)
ax.legend()
plt.savefig(&amp;quot;esda2_directional_moran.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;)
plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="esda2_directional_moran.png" alt="Directional Moran scatter plot showing movement vectors from 2013 to 2019 positions for each region.">&lt;/p>
&lt;pre>&lt;code class="language-python"># Classify quadrant transitions
q_2013 = np.where((z_2013 &amp;gt;= 0) &amp;amp; (wz_2013 &amp;gt;= 0), &amp;quot;HH&amp;quot;,
np.where((z_2013 &amp;lt; 0) &amp;amp; (wz_2013 &amp;gt;= 0), &amp;quot;LH&amp;quot;,
np.where((z_2013 &amp;lt; 0) &amp;amp; (wz_2013 &amp;lt; 0), &amp;quot;LL&amp;quot;, &amp;quot;HL&amp;quot;)))
q_2019 = np.where((z_2019 &amp;gt;= 0) &amp;amp; (wz_2019 &amp;gt;= 0), &amp;quot;HH&amp;quot;,
np.where((z_2019 &amp;lt; 0) &amp;amp; (wz_2019 &amp;gt;= 0), &amp;quot;LH&amp;quot;,
np.where((z_2019 &amp;lt; 0) &amp;amp; (wz_2019 &amp;lt; 0), &amp;quot;LL&amp;quot;, &amp;quot;HL&amp;quot;)))
transition_moran = pd.crosstab(
pd.Series(q_2013, name=&amp;quot;2013&amp;quot;),
pd.Series(q_2019, name=&amp;quot;2019&amp;quot;)
)
print(transition_moran.to_string())
stayed = (q_2013 == q_2019).sum()
moved = (q_2013 != q_2019).sum()
print(f&amp;quot;\nStayed in same quadrant: {stayed} ({stayed/len(gdf)*100:.1f}%)&amp;quot;)
print(f&amp;quot;Moved to different quadrant: {moved} ({moved/len(gdf)*100:.1f}%)&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">2019 HH HL LH LL
2013
HH 41 1 2 10
HL 9 6 0 5
LH 0 0 2 3
LL 7 10 11 46
Stayed in same quadrant: 95 (62.1%)
Moved to different quadrant: 58 (37.9%)
&lt;/code>&lt;/pre>
&lt;p>The directional Moran scatter plot reveals the space-time dynamics of South American development. &lt;strong>95 regions (62.1%)&lt;/strong> remained in the same Moran scatter plot quadrant between 2013 and 2019, while &lt;strong>58 (37.9%)&lt;/strong> crossed quadrant boundaries. The most stable quadrants are HH (41 of 54 stayed, 76%) and LL (46 of 74 stayed, 62%), confirming that both prosperity and deprivation clusters are persistent. The most common transitions are LL to LH (11 regions) and HL to HH (9 regions), suggesting some upward mobility at the boundary of the prosperity cluster. However, the 10 HH-to-LL transitions highlight that the Venezuelan crisis pulled previously well-performing regions into the low-development quadrant &amp;mdash; a dramatic downward trajectory that affected both the regions themselves and their neighbors.&lt;/p>
&lt;h3 id="102-country-focus-venezuela-vs-bolivia">10.2 Country focus: Venezuela vs Bolivia&lt;/h3>
&lt;p>Venezuela and Bolivia offer a stark contrast in subnational development trajectories. In 2013, Venezuela&amp;rsquo;s regions were spread across the upper half of the Moran scatter plot &amp;mdash; 13 of 24 regions sat in the HH quadrant, reflecting relatively high development levels and high-development neighbors. Bolivia&amp;rsquo;s 9 regions, by contrast, were concentrated in the lower-left corner (8 in LL, 1 in LH). By 2019, these two countries had moved in opposite directions. We isolate them in the directional Moran scatter plot to compare their movement vectors.&lt;/p>
&lt;pre>&lt;code class="language-python"># Filter Venezuela and Bolivia regions
ven_mask = gdf[&amp;quot;country&amp;quot;] == &amp;quot;Venezuela&amp;quot;
bol_mask = gdf[&amp;quot;country&amp;quot;] == &amp;quot;Bolivia&amp;quot;
# Shared axis limits (from the full dataset, for comparability)
all_z = np.concatenate([z_2013, z_2019])
all_wz = np.concatenate([wz_2013, wz_2019])
pad = 0.3
shared_xlim = (all_z.min() - pad, all_z.max() + pad)
shared_ylim = (all_wz.min() - pad, all_wz.max() + pad)
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(16, 7))
for ax, mask, title in [
(axes[0], bol_mask, &amp;quot;(a) Bolivia&amp;quot;),
(axes[1], ven_mask, &amp;quot;(b) Venezuela&amp;quot;),
]:
# Background: all regions (grey, faded)
for i in range(len(gdf)):
ax.annotate(&amp;quot;&amp;quot;, xy=(z_2019[i], wz_2019[i]),
xytext=(z_2013[i], wz_2013[i]),
arrowprops=dict(arrowstyle=&amp;quot;-&amp;gt;&amp;quot;, color=GRID_LINE,
alpha=0.15, lw=0.5))
ax.scatter(z_2013, wz_2013, color=GRID_LINE, s=10, alpha=0.15, zorder=2)
ax.scatter(z_2019, wz_2019, color=GRID_LINE, s=10, alpha=0.15, zorder=2)
# Highlighted country
for i in gdf.index[mask]:
ax.annotate(&amp;quot;&amp;quot;, xy=(z_2019[i], wz_2019[i]),
xytext=(z_2013[i], wz_2013[i]),
arrowprops=dict(arrowstyle=&amp;quot;-&amp;gt;&amp;quot;, color=STEEL_BLUE,
alpha=0.7, lw=1.0))
ax.scatter(z_2013[mask], wz_2013[mask], color=WARM_ORANGE, s=30,
alpha=0.8, edgecolors=GRID_LINE, linewidths=0.3,
label=&amp;quot;2013&amp;quot;, zorder=5)
ax.scatter(z_2019[mask], wz_2019[mask], color=TEAL, s=30,
alpha=0.8, edgecolors=GRID_LINE, linewidths=0.3,
label=&amp;quot;2019&amp;quot;, zorder=5)
# Labels at 2019 positions
texts = []
for i in gdf.index[mask]:
texts.append(ax.text(z_2019[i], wz_2019[i], gdf.loc[i, &amp;quot;region&amp;quot;],
fontsize=7, color=LIGHT_TEXT))
adjust_text(texts, ax=ax, arrowprops=dict(arrowstyle=&amp;quot;-&amp;quot;, color=LIGHT_TEXT,
alpha=0.5, lw=0.5))
# Quadrant lines and labels
ax.axhline(0, color=GRID_LINE, linewidth=1, zorder=1)
ax.axvline(0, color=GRID_LINE, linewidth=1, zorder=1)
ax.set_xlim(shared_xlim)
ax.set_ylim(shared_ylim)
ox = (shared_xlim[1] - shared_xlim[0]) * 0.05
oy = (shared_ylim[1] - shared_ylim[0]) * 0.05
for lbl, ha, va, x, y in [
(&amp;quot;HH&amp;quot;, &amp;quot;right&amp;quot;, &amp;quot;top&amp;quot;, shared_xlim[1] - ox, shared_ylim[1] - oy),
(&amp;quot;LH&amp;quot;, &amp;quot;left&amp;quot;, &amp;quot;top&amp;quot;, shared_xlim[0] + ox, shared_ylim[1] - oy),
(&amp;quot;LL&amp;quot;, &amp;quot;left&amp;quot;, &amp;quot;bottom&amp;quot;, shared_xlim[0] + ox, shared_ylim[0] + oy),
(&amp;quot;HL&amp;quot;, &amp;quot;right&amp;quot;, &amp;quot;bottom&amp;quot;, shared_xlim[1] - ox, shared_ylim[0] + oy),
]:
ax.text(x, y, lbl, fontsize=14, ha=ha, va=va,
color=LIGHT_TEXT, alpha=0.6)
ax.set_xlabel(&amp;quot;SHDI (standardized)&amp;quot;)
ax.set_ylabel(&amp;quot;Spatial lag of SHDI&amp;quot;)
ax.set_title(title)
ax.legend(fontsize=8)
plt.tight_layout()
plt.savefig(&amp;quot;esda2_directional_ven_bol.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;)
plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="esda2_directional_ven_bol.png" alt="Side-by-side directional Moran scatter plots for Bolivia (left) and Venezuela (right), showing movement vectors from 2013 to 2019.">&lt;/p>
&lt;pre>&lt;code class="language-python"># Summary statistics for Venezuela and Bolivia
for country, mask in [(&amp;quot;Venezuela&amp;quot;, ven_mask), (&amp;quot;Bolivia&amp;quot;, bol_mask)]:
n = mask.sum()
mean_change = gdf.loc[mask, &amp;quot;shdi_change&amp;quot;].mean()
min_change = gdf.loc[mask, &amp;quot;shdi_change&amp;quot;].min()
max_change = gdf.loc[mask, &amp;quot;shdi_change&amp;quot;].max()
# Quadrant transitions
q13 = q_2013[mask]
q19 = q_2019[mask]
stayed = (q13 == q19).sum()
moved = (q13 != q19).sum()
print(f&amp;quot;\n{country} ({n} regions):&amp;quot;)
print(f&amp;quot; Mean SHDI change: {mean_change:+.4f}&amp;quot;)
print(f&amp;quot; Range: [{min_change:+.4f}, {max_change:+.4f}]&amp;quot;)
print(f&amp;quot; Quadrant stability: {stayed} stayed, {moved} moved&amp;quot;)
print(f&amp;quot; 2013 quadrants: {', '.join(f'{q}={c}' for q, c in zip(*np.unique(q13, return_counts=True)))}&amp;quot;)
print(f&amp;quot; 2019 quadrants: {', '.join(f'{q}={c}' for q, c in zip(*np.unique(q19, return_counts=True)))}&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Venezuela (24 regions):
Mean SHDI change: -0.0653
Range: [-0.0670, -0.0640]
Quadrant stability: 3 stayed, 21 moved
2013 quadrants: HH=13, HL=5, LH=3, LL=3
2019 quadrants: HL=1, LH=2, LL=21
Bolivia (9 regions):
Mean SHDI change: +0.0333
Range: [+0.0300, +0.0350]
Quadrant stability: 7 stayed, 2 moved
2013 quadrants: LH=1, LL=8
2019 quadrants: HL=1, LH=2, LL=6
&lt;/code>&lt;/pre>
&lt;p>Panel (a) shows Bolivia&amp;rsquo;s modest but consistent rightward movement. All 9 regions started in the lower-left portion of the plot (8 in LL, 1 in LH) and shifted rightward by 2019, reflecting genuine improvement in own-region development. The mean SHDI change was &lt;strong>+0.033&lt;/strong>, with a remarkably tight range ([+0.030, +0.035]) indicating that the gains were broad-based across all Bolivian regions. &lt;strong>Seven of 9 regions (78%) remained in the same quadrant&lt;/strong>, with 2 moving out of LL &amp;mdash; one to LH and one to HL. The arrows are short and point consistently to the right, meaning Bolivia improved its own development levels without substantially changing the spatial lag (its neighbors' conditions remained similar). This pattern suggests steady, internally driven progress that has not yet been large enough to escape the low-development spatial cluster.&lt;/p>
&lt;p>Panel (b) tells the opposite story. &lt;strong>Venezuela&amp;rsquo;s 24 regions experienced the most dramatic downward shift&lt;/strong> in the entire dataset, with a mean SHDI change of &lt;strong>-0.065&lt;/strong>. In 2013, Venezuelan regions were spread across the upper portion of the plot &amp;mdash; 13 in HH, 5 in HL, 3 in LH, and only 3 in LL. By 2019, the picture had completely inverted: &lt;strong>21 of 24 regions (88%) crossed quadrant boundaries&lt;/strong>, with 21 ending in the LL quadrant. The arrows sweep uniformly downward and to the left, reflecting both the collapse of each region&amp;rsquo;s own development level and the negative spillover onto its neighbors' spatial lags. The narrow range of change ([-0.067, -0.064]) reveals that the crisis was not localized to a few regions &amp;mdash; it was a near-uniform national collapse that dragged every Venezuelan region, regardless of its 2013 starting point, into the low-development quadrant.&lt;/p>
&lt;p>The juxtaposition is instructive. Bolivia&amp;rsquo;s arrows are short, rightward, and clustered &amp;mdash; a country making incremental gains within a stable spatial structure. Venezuela&amp;rsquo;s arrows are long, southwest-pointing, and tightly bundled &amp;mdash; a country experiencing systemic collapse that erased decades of development advantage in just six years. The contrast highlights how economic crises can propagate spatially: Venezuela&amp;rsquo;s decline did not just reduce its own regions' development, it also pulled down the spatial lags of neighboring Colombian and Brazilian border regions, contributing to the expansion of the LL cluster documented in Section 9.&lt;/p>
&lt;h2 id="11-discussion">11. Discussion&lt;/h2>
&lt;p>&lt;strong>Spatial autocorrelation in South American human development is strong and persistent.&lt;/strong> Global Moran&amp;rsquo;s I increased from 0.568 in 2013 to 0.632 in 2019 (both p = 0.001), indicating that the spatial clustering of development levels strengthened over the period. This means the development gap between prosperous and lagging regions is not only large but spatially structured &amp;mdash; high-development regions form a contiguous band across the Southern Cone, while low-development regions form an equally contiguous band across the Amazon basin and northern South America.&lt;/p>
&lt;p>The LISA analysis pinpoints these clusters with precision. In 2019, 30 regions form a significant HH cluster (high development surrounded by high-development neighbors) and 37 regions form a significant LL cluster (low development surrounded by low-development neighbors). The LL cluster expanded from 29 to 37 regions between 2013 and 2019, driven primarily by Venezuela&amp;rsquo;s economic crisis and its spillover effects on neighboring regions. The HH cluster remained stable (31 to 30), with 87% persistence &amp;mdash; a sign that prosperity corridors in the Southern Cone are structurally entrenched.&lt;/p>
&lt;p>The space-time analysis reveals that 62% of regions stayed in the same Moran scatter plot quadrant, but the 38% that moved tell an important story. The most concerning transitions are the 10 regions that moved from HH to LL and the 17 previously non-significant regions that joined the LL LISA cluster. These movements are concentrated in Venezuela and its neighbors, illustrating how economic shocks can propagate spatially.&lt;/p>
&lt;p>The &lt;strong>Venezuela&amp;ndash;Bolivia comparison&lt;/strong> crystallizes the two forces shaping South America&amp;rsquo;s spatial development landscape. Venezuela&amp;rsquo;s 24 regions collapsed nearly uniformly (mean SHDI change of -0.065, with 88% crossing quadrant boundaries), transforming a country that was largely in the HH quadrant in 2013 into one almost entirely in the LL quadrant by 2019. Bolivia&amp;rsquo;s 9 regions, starting from a much lower base, improved steadily (+0.033) with 78% quadrant stability. These divergent trajectories illustrate that spatial clusters are not static: they can expand rapidly through crisis-driven contagion (Venezuela pulling its neighbors downward) or contract slowly through sustained internal improvement (Bolivia gradually lifting its regions rightward in the Moran scatter plot). The fact that Venezuela&amp;rsquo;s decline was spatially contagious &amp;mdash; dragging down the spatial lags of neighboring Colombian and Brazilian border regions &amp;mdash; while Bolivia&amp;rsquo;s improvement remained spatially contained underscores an asymmetry: negative shocks propagate faster and farther across borders than positive ones.&lt;/p>
&lt;p>For policy, these findings suggest that &lt;strong>spatially targeted interventions&lt;/strong> may be more effective than uniform national programs. The persistent LL clusters represent development traps where a region&amp;rsquo;s own conditions are reinforced by the equally poor conditions of its neighbors. Breaking these traps may require coordinated cross-regional or cross-border programs that address the spatial dimension of underdevelopment. Bolivia&amp;rsquo;s experience suggests that broad-based national improvement can lift all regions, but escaping the low-development spatial cluster may require the additional step of improving neighbors' conditions simultaneously &amp;mdash; a challenge that calls for cross-border cooperation.&lt;/p>
&lt;h2 id="12-summary-and-next-steps">12. Summary and next steps&lt;/h2>
&lt;p>&lt;strong>Key takeaways:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Method insight:&lt;/strong> ESDA reveals spatial patterns invisible in aspatial analysis. The same dataset that shows a modest aggregate improvement (+0.005 SHDI) conceals a deepening spatial divide &amp;mdash; Moran&amp;rsquo;s I increased from 0.568 to 0.632, meaning spatial clustering strengthened between 2013 and 2019.&lt;/li>
&lt;li>&lt;strong>Data insight:&lt;/strong> 30 HH and 37 LL regions form statistically significant clusters at the 10% level. The LL cluster expanded by 8 regions (from 29 to 37), while the HH cluster remained stable. Cluster persistence is high: 87% for HH and 62% for LL, indicating entrenched spatial inequality.&lt;/li>
&lt;li>&lt;strong>Country insight:&lt;/strong> Venezuela and Bolivia illustrate contrasting development dynamics. Venezuela&amp;rsquo;s 24 regions collapsed nearly uniformly (mean -0.065), with 88% crossing quadrant boundaries from the upper to the lower portion of the Moran scatter plot. Bolivia&amp;rsquo;s 9 regions improved steadily (+0.033) with 78% quadrant stability, showing broad-based gains that have not yet been large enough to escape the LL spatial cluster.&lt;/li>
&lt;li>&lt;strong>Limitation:&lt;/strong> Queen contiguity assumes shared borders, which excludes island territories (San Andres, Nueva Esparta) and may not capture cross-water economic linkages. With only two time periods (2013 and 2019), we cannot distinguish permanent structural clusters from temporary effects of the Venezuelan crisis. The p = 0.10 significance threshold is relatively permissive.&lt;/li>
&lt;li>&lt;strong>Next step:&lt;/strong> Extend the analysis with spatial regression models (spatial lag and spatial error models) to test whether a region&amp;rsquo;s development is directly influenced by its neighbors' development, or whether the clustering is driven by shared underlying factors. Bivariate LISA could reveal whether income clusters coincide with education clusters. Adding more time periods (2000&amp;ndash;2019) from the full Global Data Lab series would enable Spatial Markov chain analysis of cluster transition probabilities.&lt;/li>
&lt;/ul>
&lt;h2 id="13-exercises">13. Exercises&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Income clusters.&lt;/strong> Repeat the LISA analysis for the income index (&lt;code>incindex2019&lt;/code>) instead of SHDI. Are income clusters in the same locations as HDI clusters? How many regions belong to both an income LL and an HDI LL cluster?&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Alternative weights.&lt;/strong> Build k-nearest neighbors weights (&lt;code>KNN&lt;/code> from &lt;code>libpysal.weights&lt;/code>) with $k = 5$ and Rook contiguity (&lt;code>Rook&lt;/code> from &lt;code>libpysal.weights&lt;/code>) instead of Queen contiguity. How does Moran&amp;rsquo;s I change under each specification? Does the KNN approach resolve the island problem?&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Bivariate Moran.&lt;/strong> Use &lt;a href="https://pysal.org/esda/generated/esda.Moran_BV.html" target="_blank" rel="noopener">&lt;code>Moran_BV&lt;/code>&lt;/a> from esda to compute the bivariate Moran&amp;rsquo;s I between education and income indices. Are regions with high education surrounded by regions with high income, or are the two dimensions spatially independent?&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Spatial autocorrelation of change.&lt;/strong> Compute Moran&amp;rsquo;s I for &lt;code>shdi_change&lt;/code> instead of the level variables. Is the &lt;em>change&lt;/em> in SHDI between 2013 and 2019 itself spatially clustered? Compare the result with the change choropleth from Section 6.2. Hint: &lt;code>Moran(gdf[&amp;quot;shdi_change&amp;quot;], W, permutations=999)&lt;/code>.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Component-level Moran&amp;rsquo;s I.&lt;/strong> Compute Moran&amp;rsquo;s I for the health, education, and income indices separately in both 2013 and 2019. Which component shows the strongest spatial autocorrelation? Does the income index &amp;mdash; which declined in 46% of regions &amp;mdash; show a different spatial pattern than health or education?&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Multiple testing sensitivity.&lt;/strong> Re-run the 2019 LISA analysis at $p &amp;lt; 0.05$ instead of $p &amp;lt; 0.10$. How many HH and LL regions survive the stricter threshold? Research the Bonferroni correction ($0.05 / 153 \approx 0.0003$) and the False Discovery Rate (FDR) procedure &amp;mdash; how would these affect the cluster counts?&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Neighbor count distribution.&lt;/strong> Plot a histogram of the number of neighbors per region from the Queen weights matrix (use &lt;code>W.cardinalities&lt;/code>). What is the shape of the distribution? Which regions have the most and fewest neighbors, and why?&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Is the Moran&amp;rsquo;s I increase significant?&lt;/strong> Moran&amp;rsquo;s I rose from 0.568 to 0.632 between 2013 and 2019. But does this difference pass a significance test? Try a bootstrap approach: pool the 2013 and 2019 SHDI values, randomly assign them to the two periods 999 times, and compute the difference in Moran&amp;rsquo;s I each time. Where does the observed difference (0.064) fall in the bootstrap distribution?&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Moran&amp;rsquo;s I excluding Venezuela.&lt;/strong> Recompute Moran&amp;rsquo;s I for 2013 and 2019 after dropping Venezuela&amp;rsquo;s 24 regions (rebuild the Queen weights on the subset GeoDataFrame). Does the increase in spatial autocorrelation survive? If not, the &amp;ldquo;deepening spatial divide&amp;rdquo; may be driven by a single country&amp;rsquo;s crisis rather than a continent-wide trend.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>LISA significance map.&lt;/strong> Create a choropleth map coloring each region by its LISA p-value (&lt;code>localMoran_2019.p_sim&lt;/code>) using a sequential colormap. How many regions have $p &amp;lt; 0.01$ vs $p &amp;lt; 0.05$ vs $p &amp;lt; 0.10$? Are the deeply significant regions ($p &amp;lt; 0.01$) concentrated in the same locations as the cluster map from Section 9.2?&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="14-references">14. References&lt;/h2>
&lt;ol>
&lt;li>&lt;a href="https://doi.org/10.1111/j.1538-4632.1995.tb00338.x" target="_blank" rel="noopener">Anselin, L. (1995). Local Indicators of Spatial Association &amp;mdash; LISA. &lt;em>Geographical Analysis&lt;/em>, 27(2), 93&amp;ndash;115.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://doi.org/10.1038/sdata.2019.38" target="_blank" rel="noopener">Smits, J. and Permanyer, I. (2019). The Subnational Human Development Database. &lt;em>Scientific Data&lt;/em>, 6, 190038.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://rrs.scholasticahq.com/article/8285" target="_blank" rel="noopener">Rey, S. J. and Anselin, L. (2007). PySAL: A Python Library of Spatial Analytical Methods. &lt;em>Review of Regional Studies&lt;/em>, 37(1), 5&amp;ndash;27.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://globaldatalab.org/shdi/" target="_blank" rel="noopener">Global Data Lab &amp;mdash; Subnational Human Development Index&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://pysal.org/esda/" target="_blank" rel="noopener">PySAL ESDA documentation&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://splot.readthedocs.io/" target="_blank" rel="noopener">splot documentation&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://carlos-mendez.org/post/python_pca2/">Mendez, C. (2026). Pooled PCA for Building Development Indicators Across Time.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://carlos-mendez.org/publication/20210318-economia/" target="_blank" rel="noopener">Mendez, C. and Gonzales, E. (2021). Human Capital Constraints, Spatial Dependence, and Regionalization in Bolivia. &lt;em>Economia&lt;/em>, 44(87).&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://carlos-mendez.org/post/python_monitor_regional_development/">Mendez, C. (2026). Monitoring Regional Development with Python.&lt;/a>&lt;/li>
&lt;/ol>
&lt;h4 id="acknowledgements">Acknowledgements&lt;/h4>
&lt;p>AI tools (Claude Code, Gemini, NotebookLM) were used to make the contents of this post more accessible to students. Nevertheless, the content in this post may still have errors. Caution is needed when applying the contents of this post to true research projects.&lt;/p></description></item><item><title>Multiscale Geographically Weighted Regression: Spatially Varying Economic Convergence in Indonesia</title><link>https://carlos-mendez.org/post/python_mgwr/</link><pubDate>Sun, 22 Mar 2026 00:00:00 +0000</pubDate><guid>https://carlos-mendez.org/post/python_mgwr/</guid><description>&lt;h2 id="1-overview">1. Overview&lt;/h2>
&lt;p>When we ask &amp;ldquo;do poorer regions catch up to richer ones?&amp;rdquo;, the standard approach is to run a single regression across all regions and report one coefficient. But what if the answer depends on &lt;em>where&lt;/em> you look? A negative coefficient in Sumatra does not mean the same process is at work in Papua. A global regression forces every district onto the same line &amp;mdash; and in doing so, it may hide the most interesting part of the story.&lt;/p>
&lt;p>&lt;strong>Multiscale Geographically Weighted Regression (MGWR)&lt;/strong> addresses this by estimating a separate set of coefficients at every location, weighted by proximity. Its key innovation over standard GWR is that each variable is allowed to operate at its own spatial scale. The intercept (representing baseline growth conditions) might vary smoothly across large regions, while the convergence coefficient might shift sharply between neighboring districts. MGWR discovers these scales from the data rather than imposing a single bandwidth on all variables.&lt;/p>
&lt;p>This tutorial applies MGWR to &lt;strong>514 Indonesian districts&lt;/strong> to answer: &lt;strong>does economic catching-up happen at the same pace everywhere in Indonesia, or does geography shape how fast poorer districts close the gap?&lt;/strong> We progress from a global regression baseline through MGWR estimation and coefficient mapping, revealing that the global R² of 0.214 jumps to 0.762 once we allow the relationship to vary across space.&lt;/p>
&lt;p>&lt;strong>Learning objectives:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Understand why a single regression coefficient may hide important spatial variation&lt;/li>
&lt;li>Estimate location-specific relationships with spatially varying coefficients&lt;/li>
&lt;li>Apply MGWR to allow each variable to operate at its own spatial scale&lt;/li>
&lt;li>Map and interpret spatially varying coefficients across Indonesia&lt;/li>
&lt;li>Compare global OLS vs MGWR model fit and diagnostics&lt;/li>
&lt;/ul>
&lt;h3 id="key-concepts-at-a-glance">Key concepts at a glance&lt;/h3>
&lt;p>The post leans on a small vocabulary repeatedly. The rest of the tutorial assumes you can move between these terms quickly. 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 collapsed for a quick scan. If a later section mentions &amp;ldquo;bandwidth&amp;rdquo; or &amp;ldquo;spatial heterogeneity&amp;rdquo; and the term feels slippery, this is the section to re-read.&lt;/p>
&lt;p>&lt;strong>1. Local regression&lt;/strong> $\hat\beta(s)$ varies by location. One regression per location $s$, weighted by spatial proximity. Coefficients become functions of geographic position rather than fixed numbers.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">&lt;summary>Example&lt;/summary>
&lt;p>In this post the convergence coefficient $\hat\beta$ on &lt;code>ln_gdppc2010&lt;/code> varies across the 514 Indonesian districts — from -1.74 (strong catching-up) to +0.42 (divergence).&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">&lt;summary>Analogy&lt;/summary>
&lt;p>Drawing a different best-fit line at each map dot, not one global line for the whole country.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>2. Bandwidth (kernel)&lt;/strong> $h$. The number of nearest neighbours each local regression uses. Smaller $h$ = more localized, noisier estimates; larger $h$ = smoother but flatter.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">&lt;summary>Example&lt;/summary>
&lt;p>This post selects an optimal bandwidth of 44 districts (out of 514) for both regressors. Each local regression at a given district uses its 44 nearest neighbours.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">&lt;summary>Analogy&lt;/summary>
&lt;p>The radius of the circle of friends a local model listens to before deciding.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>3. Spatial heterogeneity&lt;/strong> $\beta_i \neq \beta_j$. Coefficients differ across space. The relationship between predictors and outcome is not constant geographically.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">&lt;summary>Example&lt;/summary>
&lt;p>In this post catching-up is &lt;em>strong&lt;/em> in 149 of 514 districts (29% with significant negative β) but &lt;em>insignificant or positive&lt;/em> in the other 365 districts. Convergence is not a single Indonesia-wide story.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">&lt;summary>Analogy&lt;/summary>
&lt;p>Different family recipes in different villages — not the same dish everywhere.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>4. GWR vs MGWR&lt;/strong> one $h$ vs $h$ per regressor. GWR uses a single bandwidth for &lt;em>all&lt;/em> coefficients. MGWR allows each coefficient to have its own bandwidth, capturing the fact that different processes operate at different spatial scales.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">&lt;summary>Example&lt;/summary>
&lt;p>In this post both &lt;code>ln_gdppc2010&lt;/code> and the intercept happen to share bandwidth = 44, but in general MGWR could have e.g. bandwidth 30 for one variable and 200 for another. The constraint relaxation is the methodological advance.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">&lt;summary>Analogy&lt;/summary>
&lt;p>One volume knob for everyone vs each instrument with its own knob.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>5. Local R²&lt;/strong> $R^2_i$. The R² of the local regression at district $i$. Maps to a colour scale to show &lt;em>where&lt;/em> the model fits well and &lt;em>where&lt;/em> it struggles.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">&lt;summary>Example&lt;/summary>
&lt;p>This post maps local R² across Indonesia. Fits are strong in dense Java districts and weaker in sparse, remote eastern islands where the 44 nearest neighbours span huge geographic distances.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">&lt;summary>Analogy&lt;/summary>
&lt;p>&amp;ldquo;How well-played is the song in &lt;em>this&lt;/em> village&amp;rdquo;.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>6. AICc model selection&lt;/strong> lower AICc = better. The corrected Akaike Information Criterion penalizes model complexity. The standard MGWR-vs-OLS comparison.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">&lt;summary>Example&lt;/summary>
&lt;p>In this post global OLS has AICc = 1341.25 while MGWR has AICc = 838.41 — a difference of more than 500 strongly favours the spatially varying model.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">&lt;summary>Analogy&lt;/summary>
&lt;p>The picky food critic comparing the two restaurants and giving a definitive verdict.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>7. β-convergence&lt;/strong> $g_i = \alpha + \beta \ln Y_{i,0} + \varepsilon_i$. The classic growth-economics test: poor regions catching up with rich ones leads to a &lt;em>negative&lt;/em> β coefficient on initial income.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">&lt;summary>Example&lt;/summary>
&lt;p>This post&amp;rsquo;s global β = -0.1948 (mild catching-up overall). MGWR reveals β ranges from -1.74 (strong local convergence) to +0.42 (local divergence). The story is heterogeneous and the global average hides this.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">&lt;summary>Analogy&lt;/summary>
&lt;p>Poor districts catching up with rich ones. A negative slope means the gap shrinks; a positive slope means the gap widens.&lt;/p>
&lt;/details>
&lt;/div>
&lt;p>&lt;strong>8. Effective number of parameters&lt;/strong> trace of hat matrix. MGWR has more flexibility than OLS but less than fitting one regression per district. The &amp;ldquo;effective&amp;rdquo; parameter count quantifies this middle ground.&lt;/p>
&lt;div class="concept-pair">
&lt;details class="concept-card concept-example">&lt;summary>Example&lt;/summary>
&lt;p>This post&amp;rsquo;s MGWR uses 52.076 effective parameters — far more than OLS&amp;rsquo;s 2 but far less than 514×2 = 1,028 (one regression per district). MGWR finds the right level of model complexity automatically.&lt;/p>
&lt;/details>
&lt;details class="concept-card concept-analogy">&lt;summary>Analogy&lt;/summary>
&lt;p>A soft count of how many independent knobs the model really has.&lt;/p>
&lt;/details>
&lt;/div>
&lt;h2 id="2-the-modeling-pipeline">2. The modeling pipeline&lt;/h2>
&lt;p>The analysis follows a natural progression: start with a simple global model, visualize the spatial patterns it cannot capture, then let MGWR reveal the local structure.&lt;/p>
&lt;pre>&lt;code class="language-mermaid">graph LR
A[&amp;quot;&amp;lt;b&amp;gt;Step 1&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Load &amp;amp;&amp;lt;br/&amp;gt;Explore&amp;quot;] --&amp;gt; B[&amp;quot;&amp;lt;b&amp;gt;Step 2&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Map&amp;lt;br/&amp;gt;Variables&amp;quot;]
B --&amp;gt; C[&amp;quot;&amp;lt;b&amp;gt;Step 3&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Global&amp;lt;br/&amp;gt;OLS&amp;quot;]
C --&amp;gt; D[&amp;quot;&amp;lt;b&amp;gt;Step 4&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;MGWR&amp;lt;br/&amp;gt;Estimation&amp;quot;]
D --&amp;gt; E[&amp;quot;&amp;lt;b&amp;gt;Step 5&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Map&amp;lt;br/&amp;gt;Coefficients&amp;quot;]
E --&amp;gt; F[&amp;quot;&amp;lt;b&amp;gt;Step 6&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Significance&amp;lt;br/&amp;gt;&amp;amp; Compare&amp;quot;]
style A fill:#141413,stroke:#6a9bcc,color:#fff
style B fill:#d97757,stroke:#141413,color:#fff
style C fill:#6a9bcc,stroke:#141413,color:#fff
style D fill:#00d4c8,stroke:#141413,color:#fff
style E fill:#00d4c8,stroke:#141413,color:#fff
style F fill:#1a3a8a,stroke:#141413,color:#fff
&lt;/code>&lt;/pre>
&lt;h2 id="3-setup-and-imports">3. Setup and imports&lt;/h2>
&lt;p>The analysis uses &lt;a href="https://mgwr.readthedocs.io/" target="_blank" rel="noopener">mgwr&lt;/a> for multiscale regression, &lt;a href="https://geopandas.org/" target="_blank" rel="noopener">GeoPandas&lt;/a> for spatial data, and &lt;a href="https://pysal.org/mapclassify/" target="_blank" rel="noopener">mapclassify&lt;/a> for choropleth classification.&lt;/p>
&lt;pre>&lt;code class="language-python">import numpy as np
import pandas as pd
import geopandas as gpd
import matplotlib.pyplot as plt
from matplotlib.patches import Patch
import mapclassify
from scipy import stats
from mgwr.gwr import MGWR
from mgwr.sel_bw import Sel_BW
import warnings
warnings.filterwarnings(&amp;quot;ignore&amp;quot;)
# Site color palette
STEEL_BLUE = &amp;quot;#6a9bcc&amp;quot;
WARM_ORANGE = &amp;quot;#d97757&amp;quot;
NEAR_BLACK = &amp;quot;#141413&amp;quot;
TEAL = &amp;quot;#00d4c8&amp;quot;
&lt;/code>&lt;/pre>
&lt;details>
&lt;summary>Dark theme figure styling (click to expand)&lt;/summary>
&lt;pre>&lt;code class="language-python">DARK_NAVY = &amp;quot;#0f1729&amp;quot;
GRID_LINE = &amp;quot;#1f2b5e&amp;quot;
LIGHT_TEXT = &amp;quot;#c8d0e0&amp;quot;
WHITE_TEXT = &amp;quot;#e8ecf2&amp;quot;
plt.rcParams.update({
&amp;quot;figure.facecolor&amp;quot;: DARK_NAVY,
&amp;quot;axes.facecolor&amp;quot;: DARK_NAVY,
&amp;quot;axes.edgecolor&amp;quot;: DARK_NAVY,
&amp;quot;axes.linewidth&amp;quot;: 0,
&amp;quot;axes.labelcolor&amp;quot;: LIGHT_TEXT,
&amp;quot;axes.titlecolor&amp;quot;: WHITE_TEXT,
&amp;quot;axes.spines.top&amp;quot;: False,
&amp;quot;axes.spines.right&amp;quot;: False,
&amp;quot;axes.spines.left&amp;quot;: False,
&amp;quot;axes.spines.bottom&amp;quot;: False,
&amp;quot;axes.grid&amp;quot;: True,
&amp;quot;grid.color&amp;quot;: GRID_LINE,
&amp;quot;grid.linewidth&amp;quot;: 0.6,
&amp;quot;grid.alpha&amp;quot;: 0.8,
&amp;quot;xtick.color&amp;quot;: LIGHT_TEXT,
&amp;quot;ytick.color&amp;quot;: LIGHT_TEXT,
&amp;quot;xtick.major.size&amp;quot;: 0,
&amp;quot;ytick.major.size&amp;quot;: 0,
&amp;quot;text.color&amp;quot;: WHITE_TEXT,
&amp;quot;font.size&amp;quot;: 12,
&amp;quot;legend.frameon&amp;quot;: False,
&amp;quot;legend.fontsize&amp;quot;: 11,
&amp;quot;legend.labelcolor&amp;quot;: LIGHT_TEXT,
&amp;quot;figure.edgecolor&amp;quot;: DARK_NAVY,
&amp;quot;savefig.facecolor&amp;quot;: DARK_NAVY,
&amp;quot;savefig.edgecolor&amp;quot;: DARK_NAVY,
})
&lt;/code>&lt;/pre>
&lt;/details>
&lt;h2 id="4-data-loading-and-exploration">4. Data loading and exploration&lt;/h2>
&lt;p>The dataset covers &lt;strong>514 Indonesian districts&lt;/strong> with GDP per capita in 2010 and the subsequent growth rate through 2018. Indonesia is an ideal setting for studying spatial heterogeneity: it spans over 17,000 islands across 5,000 km of ocean, with enormous variation in economic structure, geography, and institutional capacity.&lt;/p>
&lt;p>The core idea behind convergence is straightforward: if poorer districts tend to grow faster than richer ones, the income gap narrows over time. In a regression framework, this means we expect a &lt;strong>negative relationship&lt;/strong> between initial income (log GDP per capita in 2010) and subsequent growth. The question is whether that negative relationship holds uniformly across the archipelago &amp;mdash; or whether it is stronger in some places and weaker (or even reversed) in others.&lt;/p>
&lt;pre>&lt;code class="language-python">CSV_URL = (&amp;quot;https://github.com/quarcs-lab/data-quarcs/raw/refs/heads/&amp;quot;
&amp;quot;master/indonesia514/dataBeta.csv&amp;quot;)
GEO_URL = (&amp;quot;https://github.com/quarcs-lab/data-quarcs/raw/refs/heads/&amp;quot;
&amp;quot;master/indonesia514/mapIdonesia514-opt.geojson&amp;quot;)
df = pd.read_csv(CSV_URL)
geo = gpd.read_file(GEO_URL)
gdf = geo.merge(df, on=&amp;quot;districtID&amp;quot;, how=&amp;quot;left&amp;quot;)
print(f&amp;quot;Loaded: {gdf.shape[0]} districts, {gdf.shape[1]} columns&amp;quot;)
print(gdf[[&amp;quot;ln_gdppc2010&amp;quot;, &amp;quot;g&amp;quot;]].describe().round(4).to_string())
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Loaded: 514 districts, 16 columns
ln_gdppc2010 g
count 514.0000 514.0000
mean 9.8371 0.3860
std 0.7603 0.3205
min 7.1657 -2.0452
25% 9.3983 0.2583
50% 9.7626 0.3453
75% 10.1739 0.4158
max 13.4438 2.0563
&lt;/code>&lt;/pre>
&lt;p>The 514 districts span a wide range of initial income: log GDP per capita ranges from 7.17 (the poorest district, roughly \$1,300 per capita) to 13.44 (the richest, roughly \$690,000 &amp;mdash; likely a resource-extraction enclave). Growth rates also vary enormously, from -2.05 (severe contraction) to +2.06 (rapid expansion), with a mean of 0.39. This high variance in both variables suggests that a single regression line will struggle to capture the full picture.&lt;/p>
&lt;h2 id="5-exploratory-maps">5. Exploratory maps&lt;/h2>
&lt;p>Before fitting any model, we map the two key variables to see whether spatial patterns are visible to the naked eye. If initial income and growth are geographically clustered, that is already a hint that spatial models will outperform global ones.&lt;/p>
&lt;pre>&lt;code class="language-python">fig, axes = plt.subplots(2, 1, figsize=(14, 14))
for ax, col, title in [
(axes[0], &amp;quot;ln_gdppc2010&amp;quot;, &amp;quot;(a) Log GDP per capita, 2010&amp;quot;),
(axes[1], &amp;quot;g&amp;quot;, &amp;quot;(b) GDP growth rate, 2010–2018&amp;quot;),
]:
fj = mapclassify.FisherJenks(gdf[col].dropna().values, k=5)
classified = mapclassify.UserDefined(gdf[col].values, bins=fj.bins.tolist())
cmap = plt.cm.coolwarm
norm = plt.Normalize(vmin=0, vmax=4)
colors = [cmap(norm(c)) for c in classified.yb]
gdf.plot(ax=ax, color=colors, edgecolor=GRID_LINE, linewidth=0.2)
ax.set_title(title, fontsize=14, pad=10)
ax.set_axis_off()
plt.tight_layout()
plt.savefig(&amp;quot;mgwr_map_xy.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;)
plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="mgwr_map_xy.png" alt="Two-panel choropleth map of Indonesia showing log GDP per capita in 2010 and GDP growth rate 2010-2018.">&lt;/p>
&lt;p>The maps reveal clear spatial structure. Initial income (panel a) is highest in Jakarta and resource-rich districts in Kalimantan and Papua (warm red), while the lowest-income districts cluster in eastern Nusa Tenggara and parts of Maluku (cool blue). Growth rates (panel b) show a different pattern: some of the poorest districts in Papua and Sulawesi experienced rapid growth (suggesting catching-up), while several high-income resource districts saw contraction. The fact that these patterns are geographically organized &amp;mdash; not randomly scattered &amp;mdash; motivates the use of spatially varying models.&lt;/p>
&lt;h2 id="6-global-regression-baseline">6. Global regression baseline&lt;/h2>
&lt;p>The simplest test for economic convergence fits a single regression line through all 514 districts. If the slope is negative, poorer districts (low initial income) tend to grow faster than richer ones.&lt;/p>
&lt;p>$$g_i = \alpha + \beta \cdot \ln(y_{i,2010}) + \varepsilon_i$$&lt;/p>
&lt;p>where $g_i$ is the growth rate, $\ln(y_{i,2010})$ is log initial income, and $\beta &amp;lt; 0$ indicates convergence. In the code, $g_i$ corresponds to the column &lt;code>g&lt;/code> and $\ln(y_{i,2010})$ to &lt;code>ln_gdppc2010&lt;/code>.&lt;/p>
&lt;pre>&lt;code class="language-python">slope, intercept, r_value, p_value, std_err = stats.linregress(
gdf[&amp;quot;ln_gdppc2010&amp;quot;], gdf[&amp;quot;g&amp;quot;]
)
print(f&amp;quot;Slope (convergence coefficient): {slope:.4f}&amp;quot;)
print(f&amp;quot;R-squared: {r_value**2:.4f}&amp;quot;)
print(f&amp;quot;p-value: {p_value:.6f}&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Slope (convergence coefficient): -0.1948
R-squared: 0.2135
p-value: 0.000000
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-python">fig, ax = plt.subplots(figsize=(10, 7))
ax.scatter(gdf[&amp;quot;ln_gdppc2010&amp;quot;], gdf[&amp;quot;g&amp;quot;],
color=STEEL_BLUE, edgecolors=GRID_LINE, s=35, alpha=0.6, zorder=3)
x_range = np.linspace(gdf[&amp;quot;ln_gdppc2010&amp;quot;].min(), gdf[&amp;quot;ln_gdppc2010&amp;quot;].max(), 100)
ax.plot(x_range, intercept + slope * x_range, color=WARM_ORANGE,
linewidth=2, zorder=2)
ax.set_xlabel(&amp;quot;Log GDP per capita (2010)&amp;quot;)
ax.set_ylabel(&amp;quot;GDP growth rate (2010–2018)&amp;quot;)
ax.set_title(&amp;quot;Global convergence regression&amp;quot;)
plt.savefig(&amp;quot;mgwr_scatter_global.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;)
plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="mgwr_scatter_global.png" alt="Scatter plot of log GDP per capita 2010 vs growth rate with OLS regression line.">&lt;/p>
&lt;p>The global regression confirms that convergence exists &lt;strong>on average&lt;/strong>: the slope is $-0.195$ (p &amp;lt; 0.001), meaning a 1-unit increase in log initial income is associated with a 0.195 percentage-point lower growth rate. However, the R² of only 0.214 means this single line explains just 21% of the variation in growth rates. The scatter plot shows enormous dispersion around the regression line &amp;mdash; many districts with similar initial income experienced vastly different growth trajectories. This low explanatory power is the motivation for MGWR: perhaps the relationship is not weak everywhere, but rather strong in some regions and absent in others, and a single coefficient is simply averaging over this heterogeneity.&lt;/p>
&lt;h2 id="7-from-global-to-local-why-mgwr">7. From global to local: why MGWR?&lt;/h2>
&lt;h3 id="71-the-limitation-of-a-single-coefficient">7.1 The limitation of a single coefficient&lt;/h3>
&lt;p>The global regression tells us that $\beta = -0.195$ on average across Indonesia. But consider two districts with the same initial income &amp;mdash; one in Java, where infrastructure and market access are strong, and one in Papua, where remoteness and institutional challenges dominate. There is no reason to expect the same convergence dynamic in both places. A single coefficient forces them onto the same line.&lt;/p>
&lt;p>&lt;strong>Geographically Weighted Regression (GWR)&lt;/strong> addresses this by estimating a separate regression at each location, using a kernel function &amp;mdash; a distance-decay weighting scheme (typically Gaussian or bisquare) that gives more weight to nearby observations and less to distant ones. The result is a set of &lt;strong>location-specific coefficients&lt;/strong> &amp;mdash; each district gets its own slope and intercept:&lt;/p>
&lt;p>$$g_i = \alpha(u_i, v_i) + \beta(u_i, v_i) \cdot \ln(y_{i,2010}) + \varepsilon_i$$&lt;/p>
&lt;p>where $(u_i, v_i)$ are the geographic coordinates of district $i$, and both $\alpha$ and $\beta$ are now functions of location rather than fixed constants. In the code, $(u_i, v_i)$ correspond to &lt;code>COORD_X&lt;/code> and &lt;code>COORD_Y&lt;/code>. The &lt;strong>bandwidth&lt;/strong> parameter $h$ controls how many neighbors contribute to each local regression &amp;mdash; a small bandwidth means only very close districts matter (highly local), while a large bandwidth approaches the global model.&lt;/p>
&lt;p>However, standard GWR uses a single bandwidth for all variables, which means the intercept and the convergence coefficient are forced to vary at the same spatial scale.&lt;/p>
&lt;p>&lt;strong>MGWR&lt;/strong> removes this constraint. It allows each variable to find its own optimal bandwidth through an iterative back-fitting procedure &amp;mdash; a process that cycles through each variable, optimizing its bandwidth while holding the others fixed, until all bandwidths converge. If baseline growth conditions vary smoothly across large regions (large bandwidth), while the convergence speed varies sharply between neighboring districts (small bandwidth), MGWR will discover this from the data. This makes MGWR a more flexible and realistic model for processes that operate at multiple spatial scales. The key assumption is that spatial relationships are &lt;strong>locally stationary&lt;/strong> within each kernel window &amp;mdash; the relationship between income and growth is approximately constant among the nearest $h$ districts, even if it differs across the full map.&lt;/p>
&lt;h3 id="72-mgwr-estimation">7.2 MGWR estimation&lt;/h3>
&lt;p>The &lt;code>mgwr&lt;/code> package requires variables to be &lt;strong>standardized&lt;/strong> (zero mean, unit variance) before multiscale bandwidth selection. This ensures that the bandwidths are comparable across variables measured in different units. The &lt;code>spherical=True&lt;/code> flag tells the algorithm to compute great-circle distances rather than Euclidean distances, which is essential when working with geographic coordinates spanning a large area like Indonesia.&lt;/p>
&lt;pre>&lt;code class="language-python"># Prepare variables
y = gdf[&amp;quot;g&amp;quot;].values.reshape((-1, 1))
X = gdf[[&amp;quot;ln_gdppc2010&amp;quot;]].values
coords = list(zip(gdf[&amp;quot;COORD_X&amp;quot;], gdf[&amp;quot;COORD_Y&amp;quot;]))
# Standardize (required for MGWR)
Zy = (y - y.mean(axis=0)) / y.std(axis=0)
ZX = (X - X.mean(axis=0)) / X.std(axis=0)
# Bandwidth selection and model fitting
mgwr_selector = Sel_BW(coords, Zy, ZX, multi=True, spherical=True)
mgwr_bw = mgwr_selector.search()
mgwr_results = MGWR(coords, Zy, ZX, mgwr_selector, spherical=True).fit()
mgwr_results.summary()
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">===========================================================================
Model type Gaussian
Number of observations: 514
Number of covariates: 2
Global Regression Results
---------------------------------------------------------------------------
R2: 0.214
Adj. R2: 0.212
Multi-Scale Geographically Weighted Regression (MGWR) Results
---------------------------------------------------------------------------
Spatial kernel: Adaptive bisquare
MGWR bandwidths
---------------------------------------------------------------------------
Variable Bandwidth ENP_j Adj t-val(95%) Adj alpha(95%)
X0 44.000 26.805 3.127 0.002
X1 44.000 25.271 3.109 0.002
Diagnostic information
---------------------------------------------------------------------------
Residual sum of squares: 122.081
Effective number of parameters (trace(S)): 52.076
Sigma estimate: 0.514
R2 0.762
Adjusted R2 0.736
AICc: 838.405
===========================================================================
&lt;/code>&lt;/pre>
&lt;p>The MGWR results are striking. &lt;strong>R² jumps from 0.214 (global) to 0.762 (MGWR)&lt;/strong> &amp;mdash; the spatially varying model explains more than three times as much variation as the global regression. Both the intercept and the convergence coefficient receive a bandwidth of 44, meaning each local regression draws on the 44 nearest districts. This is a relatively local scale (44 out of 514 districts, or about 8.6% of the sample), confirming that the convergence relationship varies substantially across the archipelago. The effective number of parameters is 52.1, reflecting the cost of estimating location-specific coefficients instead of two global ones.&lt;/p>
&lt;h3 id="73-mapping-mgwr-coefficients">7.3 Mapping MGWR coefficients&lt;/h3>
&lt;p>The power of MGWR lies in the coefficient maps. Instead of a single number for the whole country, we can now visualize how the convergence relationship changes from district to district. Because MGWR is estimated on standardized variables, the mapped coefficients are in &lt;strong>standard-deviation units&lt;/strong>: a coefficient of $-1.0$ means that a one-standard-deviation increase in log initial income is associated with a one-standard-deviation decrease in growth at that location.&lt;/p>
&lt;pre>&lt;code class="language-python">gdf[&amp;quot;mgwr_intercept&amp;quot;] = mgwr_results.params[:, 0]
gdf[&amp;quot;mgwr_slope&amp;quot;] = mgwr_results.params[:, 1]
&lt;/code>&lt;/pre>
&lt;p>&lt;strong>Intercept map&lt;/strong> &amp;mdash; the intercept captures baseline growth conditions after accounting for initial income. Positive values indicate districts that grew faster than expected given their income level; negative values indicate underperformance.&lt;/p>
&lt;pre>&lt;code class="language-python">fig, ax = plt.subplots(figsize=(14, 8))
# Fisher-Jenks classification with Patch legend (see script.py for details)
gdf.plot(ax=ax, column=&amp;quot;mgwr_intercept&amp;quot;, scheme=&amp;quot;FisherJenks&amp;quot;, k=5,
cmap=&amp;quot;coolwarm&amp;quot;, edgecolor=GRID_LINE, linewidth=0.2, legend=True)
ax.set_title(f&amp;quot;MGWR intercept (bandwidth = {int(mgwr_bw[0])})&amp;quot;)
ax.set_axis_off()
plt.savefig(&amp;quot;mgwr_mgwr_intercept.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;)
plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="mgwr_mgwr_intercept.png" alt="MGWR intercept map across Indonesia&amp;rsquo;s 514 districts.">&lt;/p>
&lt;p>The intercept map reveals a clear east&amp;ndash;west gradient. Districts in &lt;strong>western Indonesia&lt;/strong> (Sumatra and Java) tend to have negative intercepts &amp;mdash; they grew &lt;strong>less&lt;/strong> than the convergence model would predict based on their initial income alone. Districts in &lt;strong>eastern Indonesia&lt;/strong> (Papua, Maluku, Nusa Tenggara) show positive intercepts, indicating growth that &lt;strong>exceeded&lt;/strong> what initial income would predict. This pattern may reflect the role of resource extraction, infrastructure investment, and fiscal transfers that disproportionately boosted growth in less-developed eastern regions during the 2010&amp;ndash;2018 period.&lt;/p>
&lt;p>&lt;strong>Convergence coefficient map&lt;/strong> &amp;mdash; the slope captures how strongly initial income predicts subsequent growth at each location. Large negative values indicate rapid catching-up; values near zero or positive indicate no convergence or divergence.&lt;/p>
&lt;pre>&lt;code class="language-python">fig, ax = plt.subplots(figsize=(14, 8))
gdf.plot(ax=ax, column=&amp;quot;mgwr_slope&amp;quot;, scheme=&amp;quot;FisherJenks&amp;quot;, k=5,
cmap=&amp;quot;coolwarm&amp;quot;, edgecolor=GRID_LINE, linewidth=0.2, legend=True)
ax.set_title(f&amp;quot;MGWR convergence coefficient (bandwidth = {int(mgwr_bw[1])})&amp;quot;)
ax.set_axis_off()
plt.savefig(&amp;quot;mgwr_mgwr_slope.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;)
plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="mgwr_mgwr_slope.png" alt="MGWR convergence coefficient map across Indonesia.">&lt;/p>
&lt;p>The convergence coefficient map is the central finding of this analysis. The global regression reported a single $\beta = -0.195$, but MGWR reveals that this average hides enormous spatial variation. The &lt;strong>strongest catching-up&lt;/strong> (deepest blue, coefficients as negative as $-1.74$) concentrates in &lt;strong>western Sumatra and parts of Kalimantan&lt;/strong> &amp;mdash; districts where poorer areas grew much faster than richer neighbors. In contrast, most of &lt;strong>Java, eastern Indonesia, and the Maluku islands&lt;/strong> show coefficients near zero (light pink), indicating that the convergence relationship is essentially absent in these areas. A handful of districts show weakly positive coefficients (up to 0.42), suggesting localized divergence where richer districts pulled further ahead. The coefficient ranges from $-1.74$ to $+0.42$, with a median of $-0.085$ and a standard deviation of 0.553 &amp;mdash; far from the single value of $-0.195$ reported by the global model.&lt;/p>
&lt;h3 id="74-statistical-significance">7.4 Statistical significance&lt;/h3>
&lt;p>Not all local coefficients are statistically distinguishable from zero. MGWR provides t-values corrected for multiple testing, which we use to classify each district&amp;rsquo;s convergence coefficient as significantly negative (catching-up), not significant, or significantly positive (diverging).&lt;/p>
&lt;pre>&lt;code class="language-python">mgwr_filtered_t = mgwr_results.filter_tvals()
t_sig = mgwr_filtered_t[:, 1] # Slope t-values
sig_cats = np.where(t_sig &amp;lt; 0, &amp;quot;Negative (catching-up)&amp;quot;,
np.where(t_sig &amp;gt; 0, &amp;quot;Positive (diverging)&amp;quot;, &amp;quot;Not significant&amp;quot;))
print(f&amp;quot;Negative (catching-up): {(sig_cats == 'Negative (catching-up)').sum()}&amp;quot;)
print(f&amp;quot;Not significant: {(sig_cats == 'Not significant').sum()}&amp;quot;)
print(f&amp;quot;Positive (diverging): {(sig_cats == 'Positive (diverging)').sum()}&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Negative (catching-up): 149
Not significant: 365
Positive (diverging): 0
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-python">fig, ax = plt.subplots(figsize=(14, 8))
cat_colors = {
&amp;quot;Negative (catching-up)&amp;quot;: &amp;quot;#2c7bb6&amp;quot;,
&amp;quot;Not significant&amp;quot;: GRID_LINE,
&amp;quot;Positive (diverging)&amp;quot;: &amp;quot;#d7191c&amp;quot;,
}
colors_sig = [cat_colors[c] for c in sig_cats]
gdf.plot(ax=ax, color=colors_sig, edgecolor=GRID_LINE, linewidth=0.2)
ax.set_title(&amp;quot;MGWR convergence coefficient: statistical significance&amp;quot;)
ax.set_axis_off()
plt.savefig(&amp;quot;mgwr_mgwr_significance.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;)
plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="mgwr_mgwr_significance.png" alt="Significance map showing districts with statistically significant catching-up.">&lt;/p>
&lt;p>Of 514 districts, &lt;strong>149 (29%)&lt;/strong> show statistically significant convergence at the corrected 5% level &amp;mdash; concentrated in &lt;strong>Sumatra, western Kalimantan, and Sulawesi&lt;/strong>. The remaining &lt;strong>365 districts (71%)&lt;/strong> have convergence coefficients that are not distinguishable from zero after correcting for multiple comparisons. &lt;strong>No district&lt;/strong> shows significant divergence. This means that while the global regression detects convergence on average, it is actually driven by a minority of districts &amp;mdash; primarily in western Indonesia &amp;mdash; while the majority of the archipelago shows no significant relationship between initial income and growth.&lt;/p>
&lt;h2 id="8-model-comparison">8. Model comparison&lt;/h2>
&lt;p>The table below summarizes how much explanatory power the spatially varying model adds over the global baseline.&lt;/p>
&lt;pre>&lt;code class="language-python">print(f&amp;quot;{'Metric':&amp;lt;25} {'Global OLS':&amp;gt;12} {'MGWR':&amp;gt;12}&amp;quot;)
print(f&amp;quot;{'R²':&amp;lt;25} {0.2135:&amp;gt;12.4f} {0.7625:&amp;gt;12.4f}&amp;quot;)
print(f&amp;quot;{'Adj. R²':&amp;lt;25} {0.2120:&amp;gt;12.4f} {0.7357:&amp;gt;12.4f}&amp;quot;)
print(f&amp;quot;{'AICc':&amp;lt;25} {1341.25:&amp;gt;12.2f} {838.41:&amp;gt;12.2f}&amp;quot;)
print(f&amp;quot;{'Bandwidth (intercept)':&amp;lt;25} {'all (514)':&amp;gt;12} {'44':&amp;gt;12}&amp;quot;)
print(f&amp;quot;{'Bandwidth (slope)':&amp;lt;25} {'all (514)':&amp;gt;12} {'44':&amp;gt;12}&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Metric Global OLS MGWR
R² 0.2135 0.7625
Adj. R² 0.2120 0.7357
AICc 1341.25 838.41
Bandwidth (intercept) all (514) 44
Bandwidth (slope) all (514) 44
&lt;/code>&lt;/pre>
&lt;p>MGWR more than triples the explained variance ($R^2$: 0.214 to 0.762) and dramatically reduces the AICc from 1341 to 838, confirming that the improvement in fit is not merely due to additional flexibility. The bandwidth of 44 for both variables means each local regression uses the nearest 44 districts (about 8.6% of the sample), confirming that the convergence process is highly localized. The adjusted $R^2$ of 0.736 accounts for the additional complexity (52 effective parameters vs 2 in OLS) and still shows a massive improvement, indicating that the spatial variation in coefficients is genuine and not overfitting.&lt;/p>
&lt;h2 id="9-discussion">9. Discussion&lt;/h2>
&lt;p>&lt;strong>Economic catching-up in Indonesia is not uniform &amp;mdash; it is concentrated in western Sumatra and parts of Kalimantan, while most of the archipelago shows no significant convergence.&lt;/strong> The global regression&amp;rsquo;s $\beta = -0.195$ suggests a moderate convergence tendency, but MGWR reveals that this average is driven by a subset of 149 districts (29%) with strong catching-up dynamics. The remaining 365 districts have convergence coefficients indistinguishable from zero.&lt;/p>
&lt;p>The intercept map adds another dimension: eastern Indonesian districts tend to have positive intercepts (above-expected growth), while western districts have negative intercepts (below-expected growth). This east&amp;ndash;west gradient likely reflects the impact of fiscal transfers, resource booms, and infrastructure programs that targeted less-developed regions during the 2010&amp;ndash;2018 period. Combined with the convergence coefficient map, the picture is nuanced: eastern Indonesia grew faster than expected (high intercept), but not because of convergence dynamics (near-zero slope) &amp;mdash; rather, because of other factors captured by the intercept.&lt;/p>
&lt;p>For policy, these findings challenge the assumption that national-level convergence statistics reflect what is happening locally. A policymaker looking at $\beta = -0.195$ might conclude that Indonesia&amp;rsquo;s development strategy is successfully closing regional gaps. MGWR reveals that catching-up is geographically selective, and the majority of districts are not on a convergence path at all. Spatially targeted interventions &amp;mdash; rather than uniform national programs &amp;mdash; may be needed to address this uneven landscape.&lt;/p>
&lt;h2 id="10-summary-and-next-steps">10. Summary and next steps&lt;/h2>
&lt;p>&lt;strong>Key takeaways:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Method insight:&lt;/strong> MGWR reveals spatial heterogeneity invisible to global regression. R² improves from 0.214 to 0.762 by allowing location-specific coefficients. Both variables operate at a bandwidth of 44 districts (~8.6% of the sample), indicating highly localized economic dynamics. Variable standardization is essential before MGWR estimation.&lt;/li>
&lt;li>&lt;strong>Data insight:&lt;/strong> Only 149 of 514 Indonesian districts (29%) show statistically significant convergence, concentrated in Sumatra and Kalimantan. The convergence coefficient ranges from $-1.74$ to $+0.42$, far from the global average of $-0.195$. Eastern Indonesia grows faster than expected (positive intercepts) but not through convergence &amp;mdash; the catching-up mechanism is absent there.&lt;/li>
&lt;li>&lt;strong>Limitation:&lt;/strong> The bivariate model (one independent variable) is intentionally simple for pedagogical purposes. Real convergence analysis would include controls for human capital, infrastructure, institutional quality, and sectoral composition. The bandwidth of 44 applies to both variables in this case, but with additional covariates, MGWR&amp;rsquo;s ability to assign different bandwidths per variable would be more visible.&lt;/li>
&lt;li>&lt;strong>Next step:&lt;/strong> Extend the model with additional covariates (education, investment, fiscal transfers) to disentangle the sources of spatial heterogeneity. Apply MGWR to panel data with multiple time periods. Compare MGWR results with the spatial clusters identified in the &lt;a href="https://carlos-mendez.org/post/python_esda2/">ESDA tutorial&lt;/a> to see whether convergence hotspots align with LISA clusters.&lt;/li>
&lt;/ul>
&lt;h2 id="11-exercises">11. Exercises&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Add a second variable.&lt;/strong> Include an education indicator (e.g., years of schooling) as a second independent variable and re-run MGWR. Do the two covariates receive different bandwidths? What does that tell you about the spatial scale at which education affects growth?&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Map the t-values.&lt;/strong> Instead of mapping the raw coefficients, map the local t-statistics from &lt;code>mgwr_results.tvalues[:, 1]&lt;/code>. How does this map compare to the significance map based on corrected t-values?&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Compare with ESDA.&lt;/strong> Run a Moran&amp;rsquo;s I test on the MGWR residuals. Is there remaining spatial autocorrelation? If not, MGWR has successfully captured the spatial structure. If yes, what might be missing?&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="12-references">12. References&lt;/h2>
&lt;ol>
&lt;li>&lt;a href="https://doi.org/10.1080/24694452.2017.1352480" target="_blank" rel="noopener">Fotheringham, A. S., Yang, W., and Kang, W. (2017). Multiscale Geographically Weighted Regression (MGWR). &lt;em>Annals of the American Association of Geographers&lt;/em>, 107(6), 1247&amp;ndash;1265.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://doi.org/10.21105/joss.01750" target="_blank" rel="noopener">Oshan, T. M., Li, Z., Kang, W., Wolf, L. J., and Fotheringham, A. S. (2019). mgwr: A Python Implementation of Multiscale Geographically Weighted Regression. &lt;em>JOSS&lt;/em>, 4(42), 1750.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://doi.org/10.1111/j.1538-4632.1996.tb00936.x" target="_blank" rel="noopener">Brunsdon, C., Fotheringham, A. S., and Charlton, M. E. (1996). Geographically Weighted Regression: A Method for Exploring Spatial Nonstationarity. &lt;em>Geographical Analysis&lt;/em>, 28(4), 281&amp;ndash;298.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.wiley.com/en-us/Geographically&amp;#43;Weighted&amp;#43;Regression-p-9780471496168" target="_blank" rel="noopener">Fotheringham, A. S., Brunsdon, C., and Charlton, M. (2002). &lt;em>Geographically Weighted Regression: The Analysis of Spatially Varying Relationships&lt;/em>. Wiley.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://carlos-mendez.org/publication/20241219-ae/" target="_blank" rel="noopener">Mendez, C. and Jiang, Q. (2024). Spatial Heterogeneity Modeling for Regional Economic Analysis: A Computational Approach Using Python and Cloud Computing. Working Paper, Nagoya University.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://mgwr.readthedocs.io/" target="_blank" rel="noopener">mgwr documentation&lt;/a>&lt;/li>
&lt;/ol>
&lt;h4 id="acknowledgements">Acknowledgements&lt;/h4>
&lt;p>AI tools (Claude Code, Gemini, NotebookLM) were used to make the contents of this post more accessible to students. Nevertheless, the content in this post may still have errors. Caution is needed when applying the contents of this post to true research projects.&lt;/p></description></item><item><title>Exploratory Spatial Data Analysis (ESDA)</title><link>https://carlos-mendez.org/post/python_esda/</link><pubDate>Fri, 01 Mar 2024 00:00:00 +0000</pubDate><guid>https://carlos-mendez.org/post/python_esda/</guid><description>&lt;h1 id="exploratory-spatial-data-analysis-esda-of-regional-development">Exploratory Spatial Data Analysis (ESDA) of Regional Development&lt;/h1>
&lt;p>This &lt;a href="https://esda101-bolivia339.streamlit.app/" target="_blank" rel="noopener">interactive application&lt;/a> enables users to explore municipal development indicators across Bolivia. In particular, it offers:&lt;/p>
&lt;ul>
&lt;li>🗺️ Geographical data visualizations&lt;/li>
&lt;li>📈 Distribution and comparative analysis tools&lt;/li>
&lt;li>💾 Downloadable datasets&lt;/li>
&lt;li>🧮 Access to a cloud-based computational notebook on &lt;a href="https://colab.research.google.com/drive/1JHf8wPxSxBdKKhXaKQZUzhEpVznKGiep?usp=sharing" target="_blank" rel="noopener">Google Colab&lt;/a>&lt;/li>
&lt;/ul>
&lt;iframe
src="https://cmg777.github.io/open-results/files/mapBolivia339imds.html"
width="100%"
height="576"
frameborder="0"
loading="lazy"
style="border:none;">
&lt;/iframe>
&lt;blockquote>
&lt;p>⚠️ This application is open source and still work in progress. Source code is available at: &lt;a href="https://github.com/cmg777/streamlit_esda101" target="_blank" rel="noopener">github.com/cmg777/streamlit_esda101&lt;/a>&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h2 id="-data-sources-and-credits">📚 Data Sources and Credits&lt;/h2>
&lt;ul>
&lt;li>Primary data source: &lt;a href="https://sdsnbolivia.org/Atlas/" target="_blank" rel="noopener">Municipal Atlas of the SDGs in Bolivia 2020.&lt;/a>&lt;/li>
&lt;li>Additional indicators for multiple years were sourced from the &lt;a href="https://www.aiddata.org/geoquery" target="_blank" rel="noopener">GeoQuery project.&lt;/a>&lt;/li>
&lt;li>Administrative boundaries from the &lt;a href="https://www.geoboundaries.org/" target="_blank" rel="noopener">GeoBoundaries database&lt;/a>&lt;/li>
&lt;li>Streamlit web app and computational notebook by &lt;a href="https://carlos-mendez.org" target="_blank" rel="noopener">Carlos Mendez.&lt;/a>&lt;/li>
&lt;li>Erick Gonzales and Pedro Leoni also colaborated in the organization of the data and the creation of the initial geospatial database&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Citation&lt;/strong>:&lt;br>
Mendez, C. (2025, March 24). &lt;em>Regional Development Indicators of Bolivia: A Dashboard for Exploratory Analysis&lt;/em> (Version 0.0.2) [Computer software]. Zenodo. &lt;a href="https://doi.org/10.5281/zenodo.15074864" target="_blank" rel="noopener">https://doi.org/10.5281/zenodo.15074864&lt;/a>&lt;/p>
&lt;hr>
&lt;h2 id="-context-and-motivation">🌐 Context and Motivation&lt;/h2>
&lt;p>Adopted in 2015, the &lt;strong>2030 Agenda for Sustainable Development&lt;/strong> established 17 Sustainable Development Goals. While global metrics offer useful benchmarks, they often overlook subnational disparities—particularly in heterogeneous countries such as Bolivia.&lt;/p>
&lt;ul>
&lt;li>🇧🇴 Bolivia ranks &lt;strong>79/166&lt;/strong> on the 2020 SDG Index (score: 69.3)&lt;/li>
&lt;li>🏘️ The &lt;em>&lt;a href="http://atlas.sdsnbolivia.org" target="_blank" rel="noopener">Municipal Atlas of the SDGs in Bolivia 2020&lt;/a>&lt;/em> reveals &lt;strong>intra-national disparities&lt;/strong> comparable to &lt;strong>global inter-country variation&lt;/strong>&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="-development-index-índice-municipal-de-desarrollo-sostenible-imds">📊 Development Index: Índice Municipal de Desarrollo Sostenible (IMDS)&lt;/h2>
&lt;p>The &lt;strong>Municipal Sustainable Development Index (IMDS)&lt;/strong> summarizes municipal performance using 62 indicators across 15 Sustainable Development Goals. However, systematic and reliable information on goals 12 and 14 were not available at the municipal level.&lt;/p>
&lt;h3 id="-methodological-criteria">🎯 Methodological Criteria&lt;/h3>
&lt;ul>
&lt;li>✅ Relevance to local Sustainable Development Goal targets&lt;/li>
&lt;li>📥 Data availability from official or trusted sources&lt;/li>
&lt;li>🌐 Full municipal coverage (339 municipalities)&lt;/li>
&lt;li>🕒 Data mostly from 2012–2019&lt;/li>
&lt;li>🧮 Low redundancy between indicators&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="-indicators-by-sustainable-development-goal">🗃️ Indicators by Sustainable Development Goal&lt;/h2>
&lt;h3 id="-goal-1-no-poverty">🧱 Goal 1: No Poverty&lt;/h3>
&lt;ul>
&lt;li>Energy poverty rate (2012, INE)&lt;/li>
&lt;li>Multidimensional Poverty Index (2013, UDAPE)&lt;/li>
&lt;li>Unmet Basic Needs (2012, INE)&lt;/li>
&lt;li>Access to basic services: water, sanitation, electricity (2012, INE)&lt;/li>
&lt;/ul>
&lt;h3 id="-goal-2-zero-hunger">🌾 Goal 2: Zero Hunger&lt;/h3>
&lt;ul>
&lt;li>Chronic malnutrition in children under five (2016, Ministry of Health)&lt;/li>
&lt;li>Obesity prevalence in women (2016, Ministry of Health)&lt;/li>
&lt;li>Average agricultural unit size (2013, Agricultural Census)&lt;/li>
&lt;li>Tractor density per 1,000 farms (2013, Agricultural Census)&lt;/li>
&lt;/ul>
&lt;h3 id="-goal-3-good-health-and-well-being">🏥 Goal 3: Good Health and Well-being&lt;/h3>
&lt;ul>
&lt;li>Infant and under-five mortality rates (2016, Ministry of Health)&lt;/li>
&lt;li>Institutional birth coverage (2016, Ministry of Health)&lt;/li>
&lt;li>Incidence of Chagas, HIV, malaria, tuberculosis, dengue (2016, Ministry of Health)&lt;/li>
&lt;li>Adolescent fertility rate (2016, Ministry of Health)&lt;/li>
&lt;/ul>
&lt;h3 id="-goal-4-quality-education">📚 Goal 4: Quality Education&lt;/h3>
&lt;ul>
&lt;li>Secondary school dropout rates, by gender (2016, Ministry of Education)&lt;/li>
&lt;li>Adult literacy rate (2012, INE)&lt;/li>
&lt;li>Share of population with higher education (2012, INE)&lt;/li>
&lt;li>Share of qualified teachers, initial and secondary levels (2016, Ministry of Education)&lt;/li>
&lt;/ul>
&lt;h3 id="-goal-5-gender-equality">⚖️ Goal 5: Gender Equality&lt;/h3>
&lt;ul>
&lt;li>Gender parity in education, labor participation, and poverty (2012–2016, INE and UDAPE)&lt;/li>
&lt;li>&lt;em>Note: Data on gender-based violence not available at municipal level&lt;/em>&lt;/li>
&lt;/ul>
&lt;h3 id="-goal-6-clean-water-and-sanitation">💧 Goal 6: Clean Water and Sanitation&lt;/h3>
&lt;ul>
&lt;li>Access to potable water (2012, INE)&lt;/li>
&lt;li>Access to sanitation services (2012, INE)&lt;/li>
&lt;li>Proportion of treated wastewater (2015, Ministry of Environment)&lt;/li>
&lt;/ul>
&lt;h3 id="-goal-7-affordable-and-clean-energy">⚡ Goal 7: Affordable and Clean Energy&lt;/h3>
&lt;ul>
&lt;li>Electricity coverage (2012, INE)&lt;/li>
&lt;li>Per capita electricity consumption (2015, Ministry of Energy)&lt;/li>
&lt;li>Use of clean cooking energy (2015, Ministry of Hydrocarbons)&lt;/li>
&lt;li>CO₂ emissions per capita, energy-related (2015, international satellite data)&lt;/li>
&lt;/ul>
&lt;h3 id="-goal-8-decent-work-and-economic-growth">💼 Goal 8: Decent Work and Economic Growth&lt;/h3>
&lt;ul>
&lt;li>Share of non-functioning electricity meters (proxy for informality/unemployment) (2015, Ministry of Energy)&lt;/li>
&lt;li>Labor force participation rate (2012, INE)&lt;/li>
&lt;li>Youth not in education, employment, or training (NEET rate) (2015, Ministry of Labor)&lt;/li>
&lt;/ul>
&lt;h3 id="-goal-9-industry-innovation-and-infrastructure">🏗️ Goal 9: Industry, Innovation, and Infrastructure&lt;/h3>
&lt;ul>
&lt;li>Internet access in households (2012, INE)&lt;/li>
&lt;li>Mobile signal coverage (2015, telecommunications data)&lt;/li>
&lt;li>Availability of urban infrastructure (2015, Ministry of Public Works)&lt;/li>
&lt;/ul>
&lt;h3 id="-goal-10-reduced-inequality">⚖️ Goal 10: Reduced Inequality&lt;/h3>
&lt;ul>
&lt;li>Proxy measures: municipal differences in poverty and participation rates (2012–2016, INE and UDAPE)&lt;/li>
&lt;/ul>
&lt;h3 id="-goal-11-sustainable-cities-and-communities">🏘️ Goal 11: Sustainable Cities and Communities&lt;/h3>
&lt;ul>
&lt;li>Urban housing adequacy (2012, INE)&lt;/li>
&lt;li>Access to collective transportation (2015, Ministry of Transport)&lt;/li>
&lt;/ul>
&lt;h3 id="-goal-13-climate-action">🌍 Goal 13: Climate Action&lt;/h3>
&lt;ul>
&lt;li>Natural disaster resilience index (2015, Ministry of Environment)&lt;/li>
&lt;li>CO₂ emissions and forest degradation (2015, satellite data)&lt;/li>
&lt;/ul>
&lt;h3 id="-goal-15-life-on-land">🌳 Goal 15: Life on Land&lt;/h3>
&lt;ul>
&lt;li>Deforestation rates (2015, satellite data)&lt;/li>
&lt;li>Biodiversity loss indicators (2015, Ministry of Environment)&lt;/li>
&lt;/ul>
&lt;h3 id="-goal-16-peace-justice-and-strong-institutions">🕊️ Goal 16: Peace, Justice, and Strong Institutions&lt;/h3>
&lt;ul>
&lt;li>Birth registration coverage (2012, INE)&lt;/li>
&lt;li>Crime and homicide rates (2015, Ministry of Government)&lt;/li>
&lt;li>Corruption perceptions (2015, civil society organizations)&lt;/li>
&lt;/ul>
&lt;h3 id="-goal-17-partnerships-for-the-goals">🤝 Goal 17: Partnerships for the Goals&lt;/h3>
&lt;ul>
&lt;li>Municipal fiscal capacity (2015, Ministry of Economy)&lt;/li>
&lt;li>Public investment per capita (2015, Ministry of Economy)&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="-limitations-and-future-work">⚠️ Limitations and Future Work&lt;/h2>
&lt;ul>
&lt;li>No disaggregated data for Indigenous Territories (TIOC)&lt;/li>
&lt;li>Many indicators based on 2012 Census; updates pending&lt;/li>
&lt;li>Limited information for Goals 12 and 14 at municipal level&lt;/li>
&lt;li>No indicators for educational quality (due to lack of standardized testing)&lt;/li>
&lt;li>Gender violence data unavailable at municipal scale&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="-access">🔗 Access&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Original website&lt;/strong>: &lt;a href="http://atlas.sdsnbolivia.org" target="_blank" rel="noopener">atlas.sdsnbolivia.org&lt;/a>&lt;/li>
&lt;li>&lt;strong>Original Publication&lt;/strong>: &lt;a href="http://www.sdsnbolivia.org/Atlas" target="_blank" rel="noopener">sdsnbolivia.org/Atlas&lt;/a>&lt;/li>
&lt;li>&lt;strong>Source Code of the Web App&lt;/strong>: &lt;a href="https://github.com/cmg777/streamlit_esda101" target="_blank" rel="noopener">github.com/cmg777/streamlit_esda101&lt;/a>&lt;/li>
&lt;li>&lt;strong>Computational Notebook&lt;/strong>: &lt;a href="https://colab.research.google.com/drive/1JHf8wPxSxBdKKhXaKQZUzhEpVznKGiep?usp=sharing" target="_blank" rel="noopener">Google Colab&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Monitoring subnational human development</title><link>https://carlos-mendez.org/post/python_monitor_subnational_hdi/</link><pubDate>Sun, 24 Sep 2023 00:00:00 +0000</pubDate><guid>https://carlos-mendez.org/post/python_monitor_subnational_hdi/</guid><description>&lt;h1 id="a-geocomputational-notebook-to-monitor-subnational-human-development">&lt;strong>A geocomputational notebook to monitor subnational human development&lt;/strong>&lt;/h1>
&lt;ul>
&lt;li>Exploratory data analysis&lt;/li>
&lt;li>Exploratory spatial data analysis
&lt;ul>
&lt;li>Spatial mapping&lt;/li>
&lt;li>Spatial dependence&lt;/li>
&lt;li>Spatial inequality&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul></description></item><item><title>Convergence clubs</title><link>https://carlos-mendez.org/post/r_convergence_clubs/</link><pubDate>Sun, 03 Sep 2023 00:00:00 +0000</pubDate><guid>https://carlos-mendez.org/post/r_convergence_clubs/</guid><description>&lt;p>&lt;a href="https://zenodo.org/badge/latestdoi/268529303" target="_blank" rel="noopener">&lt;img src="https://zenodo.org/badge/268529303.svg" alt="DOI">&lt;/a>&lt;/p>
&lt;h2 id="about-the-book">About the book&lt;/h2>
&lt;p>Testing for economic convergence across countries has been a central issue in the literature of economic growth and development. This book introduces a modern framework to study the cross-country convergence dynamics of labor productivity and its proximate sources: capital accumulation and aggregate efficiency. In particular, recent convergence dynamics of developed as well as developing countries are evaluated through the lens of a non-linear dynamic factor model and a clustering algorithm for panel data. This framework allows us to examine key economic phenomena such as technological heterogeneity and multiple equilibria. Overall, the book provides a succinct review of the recent club convergence literature, a comparative view of developed and developing countries, and a tutorial on how to implement the club convergence framework in the statistical software Stata. These three features will help graduate students and researchers catch up with the latest developments and methodological implementations of the club convergence literature.&lt;/p>
&lt;ul>
&lt;li>
&lt;p>About the author: &lt;a href="https://carlos-mendez.org" target="_blank" rel="noopener">https://carlos-mendez.org&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Read the book online: &lt;a href="https://ebookcentral.proquest.com/lib/nagoyauniv/detail.action?docID=6386038" target="_blank" rel="noopener">Only for Nagoya University students&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://www.springer.com/gp/book/9789811586286" target="_blank" rel="noopener">Buy the ebook&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://www.amazon.co.jp/Convergence-Clubs-Productivity-Proximate-Sources/dp/9811586284/ref=sr_1_1?dchild=1&amp;amp;keywords=%22Convergence&amp;#43;Clubs&amp;#43;in&amp;#43;Labor&amp;#43;Productivity&amp;#43;and&amp;#43;its&amp;#43;Proximate&amp;#43;Sources%22&amp;amp;qid=1599180007&amp;amp;sr=8-1" target="_blank" rel="noopener">Buy the book&lt;/a>&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="table-of-contents">Table of contents&lt;/h2>
&lt;ol>
&lt;li>Introduction and overview&lt;/li>
&lt;li>Measuring labor productivity and its proximate sources&lt;/li>
&lt;li>A modern framework to study convergence&lt;/li>
&lt;li>Convergence clubs in labor productivity&lt;/li>
&lt;li>Convergence clubs in capital accumulation&lt;/li>
&lt;li>Convergence clubs in aggregate efficiency&lt;/li>
&lt;li>Concluding remarks and new research directions&lt;/li>
&lt;/ol>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tutorials&lt;/th>
&lt;th>Download datasets&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;a href="https://youtu.be/FO8Ngl57HRQ" target="_blank" rel="noopener">Video Tutorial&lt;/a>&lt;/td>
&lt;td>&lt;a href="assets/dat.csv.zip?raw=true">Download full dataset&lt;/a>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://github.com/quarcs-lab/mendez2020-convergence-clubs-code-data/raw/master/assets/tutorial-hiYes_log_lp.zip" target="_blank" rel="noopener">Convergence clubs analysis using Stata&lt;/a>&lt;/td>
&lt;td>&lt;a href="assets/dat-definitions.csv.zip?raw=true">Download dataset definitions&lt;/a>; &lt;a href="https://carlos-mendez.org/dat-definitions.csv">See dataset definitions&lt;/a>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://colab.research.google.com/drive/1GjO43UJIhtqX39qja5yUl4j9suwKIpMl?usp=sharing" target="_blank" rel="noopener">Convergence clubs analysis using R&lt;/a>&lt;/td>
&lt;td>&lt;a href="assets/dat_hiNo.zip?raw=true">Download R dataset of developed countries&lt;/a>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://deepnote.com/@Dev-macro/Explore-labor-productivity-data-TvVTPkcdQPiAYlLIfPZG7g" target="_blank" rel="noopener">Explore the data using Python in Deepnote&lt;/a>&lt;/td>
&lt;td>&lt;a href="assets/dat_hiYes.zip?raw=true">Download R dataset of developing countries&lt;/a>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://colab.research.google.com/github/quarcs-lab/mendez2020-convergence-clubs-code-data/blob/master/assets/dat.ipynb" target="_blank" rel="noopener">Explore the data using Python in Google Colab&lt;/a>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://rstudio.cloud/project/2047179" target="_blank" rel="noopener">Explore the data using R in R Studio Cloud&lt;/a>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="tutorial-convergence-test-and-identification-of-clubs-using-stata">Tutorial: Convergence test and identification of clubs using Stata&lt;/h2>
&lt;p>&lt;a href="https://www.stata-journal.com/article.html?article=st0503" target="_blank" rel="noopener">Du (2017)&lt;/a> introduced a Stata package to perform the econometric convergence analysis and club clustering algorithm of &lt;a href="https://onlinelibrary.wiley.com/doi/abs/10.1111/j.1468-0262.2007.00811.x" target="_blank" rel="noopener">Phillips and Sul (2007)&lt;/a>.
Although the package is well documented and easy to use, it does not include commands to create figures or export tables of results.
In what follows, the basic use of the package is described with some additional pieces of code to automate the creation of figures and export of results.&lt;/p>
&lt;p>The code below installs the convergence clubs package and its dependencies. It is important to note that Stata 12.1 or higher is needed to run the convergence clubs package. In addition, to export the results to excel, Stata 14.2 or higher is needed to use the &lt;code>putexcel&lt;/code> command. Finally, note that this installation should only be done once.&lt;/p>
&lt;pre>&lt;code>*-------------------------------------------------------
***************** Install packages*********************
*-------------------------------------------------------
* Install the convergence clubs package
findit st0503_1
net install st0503_1, from(http://www.stata-journal.com/software/sj19-1)
* Install package dependencies
ssc install moremata
*-------------------------------------------------------
&lt;/code>&lt;/pre>
&lt;p>After installing the package, we need to define some global (macro) parameters such as the name of the dataset (for example, &lt;code>hiYes_log_lp&lt;/code>), the main variable to be studied (for example, &lt;code>log_lp&lt;/code>), the label of that variable (for example, &lt;code>Labor Productivity&lt;/code>), the type of cross-sectional unit (for example, &lt;code>country&lt;/code>), and the type of temporal unit (for example,&lt;code>year&lt;/code>). Users of this code should carefully check these five parameters as the next steps crucially depend on them to work correctly.&lt;/p>
&lt;pre>&lt;code>*-------------------------------------------------------
clear all
macro drop _all
set more off
*-------------------------------------------------------
***************** Define five global parameters*********
*-------------------------------------------------------
* (1) Indicate name of the dataset (Example: hiYes_log_lp.dta)
global dataSet hiYes_log_lp
* (2) Indicate name of the variable to be studied (Example: log_lp)
global xVar log_lp
* (3) Write label of the variable (Example: Labor Productivity)
global xVarLabel Labor Productivity
* (4) Indicate cross-sectional unit ID (Example: country)
global csUnitName country
* (5) Indicate temporal unit ID (Example: year)
global timeUnit year
*-------------------------------------------------------
&lt;/code>&lt;/pre>
&lt;p>To have a record of the written commands and results (excluding the display of figures), let us start a log file. The name of this file is automatically captured from the previously defined parameters.&lt;/p>
&lt;pre>&lt;code>*-------------------------------------------------------
***************** Start log file************************
*-------------------------------------------------------
log using &amp;quot;${dataSet}_clubs.txt&amp;quot;, text replace
*-------------------------------------------------------
&lt;/code>&lt;/pre>
&lt;p>Next, from the current working directory, we load the dataset, which is in a .dta format, and set the structure of the data. Again, we do not have to modify anything from this code as long as the global parameters are correctly defined.&lt;/p>
&lt;pre>&lt;code>*-------------------------------------------------------
***************** Load and set panel data ***********
*-------------------------------------------------------
** Load data
use &amp;quot;${dataSet}.dta&amp;quot;
* Keep necessary variables
keep id ${csUnitName} ${timeUnit} ${xVar}
* Set panel data
xtset id ${timeUnit}
*-------------------------------------------------------
&lt;/code>&lt;/pre>
&lt;p>The next piece of code is the most important one of the entire package. It runs the log-t convergence test, the clustering and merge algorithms, and lists the final results in a table. If we are using a log file, all code and results are recorded in the &lt;code>dataSet_clubs.txt&lt;/code> file. In addition, by using the &lt;code>putexcel&lt;/code> we can export the results in a table form to excel.&lt;/p>
&lt;pre>&lt;code>*-------------------------------------------------------
***************** Apply PS convergence test ***********
*-------------------------------------------------------
* (1) Run log-t regression
putexcel set &amp;quot;${dataSet}_test.xlsx&amp;quot;, sheet(logtTest) replace
logtreg ${xVar}, kq(0.333)
ereturn list
matrix result0 = e(res)
putexcel A1 = matrix(result0), names nformat(&amp;quot;#.##&amp;quot;) overwritefmt
* (2) Run clustering algorithm
putexcel set &amp;quot;${dataSet}_test.xlsx&amp;quot;, sheet(initialClusters) modify
psecta ${xVar}, name(${csUnitName}) kq(0.333) gen(club_${xVar})
matrix b=e(bm)
matrix t=e(tm)
matrix result1=(b \ t)
matlist result1, border(rows) rowtitle(&amp;quot;log(t)&amp;quot;) format(%9.3f) left(4)
putexcel A1 = matrix(result1), names nformat(&amp;quot;#.##&amp;quot;) overwritefmt
* (3) Run merge algorithm
putexcel set &amp;quot;${dataSet}_test.xlsx&amp;quot;, sheet(mergingClusters) modify
scheckmerge ${xVar}, kq(0.333) club(club_${xVar})
matrix b=e(bm)
matrix t=e(tm)
matrix result2=(b \ t)
matlist result2, border(rows) rowtitle(&amp;quot;log(t)&amp;quot;) format(%9.3f) left(4)
putexcel A1 = matrix(result2), names nformat(&amp;quot;#.##&amp;quot;) overwritefmt
* (4) List final clusters
putexcel set &amp;quot;${dataSet}_test.xlsx&amp;quot;, sheet(finalClusters) modify
imergeclub ${xVar}, name(${csUnitName}) kq(0.333) club(club_${xVar}) gen(finalclub_${xVar})
matrix b=e(bm)
matrix t=e(tm)
matrix result3=(b \ t)
matlist result3, border(rows) rowtitle(&amp;quot;log(t)&amp;quot;) format(%9.3f) left(4)
putexcel A1 = matrix(result3), names nformat(&amp;quot;#.##&amp;quot;) overwritefmt
*-------------------------------------------------------
&lt;/code>&lt;/pre>
&lt;p>To plot the dynamics of the cross-sectional units and their respective convergence clubs, we first need to re-scale the data based on the cross-sectional average of each year. The code below performs that task. The result of this code is an extended panel dataset (in both &lt;code>.dta&lt;/code> and &lt;code>.csv&lt;/code> formats) that includes the list of countries, club membership, and the absolute and relative values of the variable under study.&lt;/p>
&lt;pre>&lt;code>*-------------------------------------------------------
***************** Generate relative variables**********
*-------------------------------------------------------
** Generate relative variable (useful for ploting)
save &amp;quot;temporary1.dta&amp;quot;,replace
use &amp;quot;temporary1.dta&amp;quot;
collapse ${xVar}, by(${timeUnit})
gen id=999999
append using &amp;quot;temporary1.dta&amp;quot;
sort id ${timeUnit}
gen ${xVar}_av = ${xVar} if id==999999
bysort ${timeUnit} (${xVar}_av): replace ${xVar}_av = ${xVar}_av[1]
gen re_${xVar} = 1*(${xVar}/${xVar}_av)
label var re_${xVar} &amp;quot;Relative ${xVar} (Average=1)&amp;quot;
drop ${xVar}_av
sort id ${timeUnit}
drop if id == 999999
rm &amp;quot;temporary1.dta&amp;quot;
* order variables
order ${csUnitName}, before(${timeUnit})
order id, before(${csUnitName})
* Export data to csv
export delimited using &amp;quot;${dataSet}_clubs.csv&amp;quot;, replace
save &amp;quot;${dataSet}_clubs.dta&amp;quot;, replace
*-------------------------------------------------------
&lt;/code>&lt;/pre>
&lt;p>Given the extended dataset, the code below plots multiple figures and export them as &lt;code>.pdf&lt;/code> and &lt;code>.gph&lt;/code> formats. There are three types of plots. First, the relative transition paths of all countries are plotted. This plot is useful as it provides a first graphical overview of dataset. Second, relative transition paths are plotted based on the club classification. Not only a plot for each club is created, but there is also a plot that compares all clubs using a common y-axis. Third, a plot based on within-club averages is also created. It is important to note that the colors and design of figures are based on the &lt;code>plotplainblind&lt;/code> scheme. See @Bischof2017 for further information about the graphical scheme. This scheme can be installed by typing the following in the Stata console: &lt;code>net install gr0070, from(http://www.stata-journal.com/software/sj17-3)&lt;/code>. Activate the scheme by typing: &lt;code>set scheme plotplainblind&lt;/code>.&lt;/p>
&lt;pre>&lt;code>*-------------------------------------------------------
***************** Plot the clubs *********************
*-------------------------------------------------------
** All lines
xtline re_${xVar}, overlay legend(off) scale(1.6) ytitle(&amp;quot;${xVarLabel}&amp;quot;, size(small)) yscale(lstyle(none)) ylabel(, noticks labcolor(gs10)) xscale(lstyle(none)) xlabel(, noticks labcolor(gs10)) xtitle(&amp;quot;&amp;quot;) name(allLines, replace)
graph save &amp;quot;${dataSet}_allLines.gph&amp;quot;, replace
graph export &amp;quot;${dataSet}_allLines.pdf&amp;quot;, replace
** Indentified Clubs
summarize finalclub_${xVar}
return list
scalar nunberOfClubs = r(max)
forval i=1/`=nunberOfClubs' {
xtline re_${xVar} if finalclub_${xVar} == `i', overlay title(&amp;quot;Club `i'&amp;quot;, size(small)) legend(off) scale(1.5) yscale(lstyle(none)) ytitle(&amp;quot;${xVarLabel}&amp;quot;, size(small)) ylabel(, noticks labcolor(gs10)) xtitle(&amp;quot;&amp;quot;) xscale(lstyle(none)) xlabel(, noticks labcolor(gs10)) name(club`i', replace)
local graphs `graphs' club`i'
}
graph combine `graphs', ycommon
graph save &amp;quot;${dataSet}_clubsLines.gph&amp;quot;, replace
graph export &amp;quot;${dataSet}_clubsLines.pdf&amp;quot;, replace
** Within-club averages
collapse (mean) re_${xVar}, by(finalclub_${xVar} ${timeUnit})
xtset finalclub_${xVar} ${timeUnit}
rename finalclub_${xVar} Club
xtline re_${xVar}, overlay scale(1.6) ytitle(&amp;quot;${xVarLabel}&amp;quot;, size(small)) yscale(lstyle(none)) ylabel(, noticks labcolor(gs10)) xscale(lstyle(none)) xlabel(, noticks labcolor(gs10)) xtitle(&amp;quot;&amp;quot;) name(clubsAverages, replace)
graph save &amp;quot;${dataSet}_clubsAverages.gph&amp;quot;, replace
graph export &amp;quot;${dataSet}_clubsAverages.pdf&amp;quot;, replace
clear
use &amp;quot;${dataSet}_clubs.dta&amp;quot;
*-------------------------------------------------------
&lt;/code>&lt;/pre>
&lt;p>The code below exports the list of countries and their club membership to a &lt;code>.csv&lt;/code> file. This list can be used as a handy reference in the appendix section of a publication.&lt;/p>
&lt;pre>&lt;code>*-------------------------------------------------------
***************** Export list of clubs ****************
*-------------------------------------------------------
summarize ${timeUnit}
scalar finalYear = r(max)
keep if ${timeUnit} == `=finalYear'
keep id ${csUnitName} finalclub_${xVar}
sort finalclub_${xVar} ${csUnitName}
export delimited using &amp;quot;${dataSet}_clubsList.csv&amp;quot;, replace
*-------------------------------------------------------
&lt;/code>&lt;/pre>
&lt;p>Finally, the code below closes the log file.&lt;/p>
&lt;pre>&lt;code>*-------------------------------------------------------
***************** Close log file*************
*-------------------------------------------------------
log close
*-------------------------------------------------------
&lt;/code>&lt;/pre></description></item><item><title>Spatial inequality dynamics</title><link>https://carlos-mendez.org/post/python_gds_spatial_inequality/</link><pubDate>Sun, 27 Aug 2023 00:00:00 +0000</pubDate><guid>https://carlos-mendez.org/post/python_gds_spatial_inequality/</guid><description/></item><item><title>Monitoring regional sustainable development</title><link>https://carlos-mendez.org/post/python_monitor_regional_development/</link><pubDate>Sat, 26 Aug 2023 00:00:00 +0000</pubDate><guid>https://carlos-mendez.org/post/python_monitor_regional_development/</guid><description>&lt;h1 id="a-geocomputational-notebook-to-monitor-regional-development-in-bolivia">&lt;strong>A geocomputational notebook to monitor regional development in Bolivia&lt;/strong>&lt;/h1>
&lt;p>Carlos Mendez (Nagoya Univerisity), Erick Gonzales (United Nations), Lykke Andersen (SDSN Bolivia)&lt;/p>
&lt;ul>
&lt;li>Exploratory data analysis&lt;/li>
&lt;li>Exploratory spatial data analysis
&lt;ul>
&lt;li>Spatial dependence&lt;/li>
&lt;li>Spatial inequality&lt;/li>
&lt;li>Spatial heterogeneity&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;pre>&lt;code>https://shorturl.at/evEFS
&lt;/code>&lt;/pre>
&lt;p>&lt;a href="https://colab.research.google.com/github/quarcs-lab/project2021o-notebook/blob/main/notebookColab.ipynb" target="_blank" rel="noopener">&lt;img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab">&lt;/a>&lt;/p>
&lt;pre>&lt;code>Suggested citation:
Mendez, C., Gonzales, E., &amp;amp; Andersen, L. (2023). A geocomputational notebook to monitor regional development in Bolivia. Zenodo. https://doi.org/10.5281/zenodo.828685
&lt;/code>&lt;/pre>
&lt;p>&lt;a href="https://zenodo.org/badge/latestdoi/683583423" target="_blank" rel="noopener">&lt;img src="https://zenodo.org/badge/683583423.svg" alt="DOI">&lt;/a>&lt;/p>
&lt;p>Github repository: &lt;a href="https://github.com/quarcs-lab/project2021o-notebook" target="_blank" rel="noopener">https://github.com/quarcs-lab/project2021o-notebook&lt;/a>&lt;/p></description></item><item><title>The Solow growth model and its convergence prediction</title><link>https://carlos-mendez.org/post/rpy_solow_model/</link><pubDate>Sat, 29 Jul 2023 00:00:00 +0000</pubDate><guid>https://carlos-mendez.org/post/rpy_solow_model/</guid><description>&lt;h2 id="-the-augmented-solow-model-an-overview-with-python-r-and-stata">📊 The Augmented Solow Model: An Overview with Python, R, and Stata&lt;/h2>
&lt;p>&lt;strong>How do countries grow richer, and why do some grow faster than others?&lt;/strong> Today, we&amp;rsquo;re diving into a computational exploration of economic growth using the &lt;strong>augmented Solow model&lt;/strong>, an enhanced version of Solow&amp;rsquo;s foundational 1956 model that includes insights from Mankiw, Romer, and Weil (1992). This model helps explain &lt;strong>why some countries grow richer than others&lt;/strong> and whether poor countries are indeed catching up to the wealthier ones. Let&amp;rsquo;s unpack the model, the equations, and what the data says.&lt;/p>
&lt;h3 id="-the-classic-solow-model-a-quick-recap">🔍 The Classic Solow Model: A Quick Recap&lt;/h3>
&lt;p>The &lt;strong>Solow model&lt;/strong> is one of the cornerstones of economic growth theory. It explains how countries grow by focusing on three main ingredients:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Physical Capital (★)&lt;/strong>: Think of it as the machines, factories, and tools that help us produce more.&lt;/li>
&lt;li>&lt;strong>Labor (👨‍🌾)&lt;/strong>: The workforce that puts the capital to use.&lt;/li>
&lt;li>&lt;strong>Technology (or Productivity)&lt;/strong>: The magic that makes capital and labor more effective.&lt;/li>
&lt;/ul>
&lt;p>The original Solow model tells us that growth can occur through accumulating &lt;strong>physical capital&lt;/strong>, increasing the &lt;strong>workforce&lt;/strong>, and through &lt;strong>technological progress&lt;/strong>. However, over time, capital experiences diminishing returns — the more you invest, the less extra output you get, unless technology improves.&lt;/p>
&lt;h3 id="-why-augment-the-model">🧠 Why Augment the Model?&lt;/h3>
&lt;p>In 1992, &lt;strong>Mankiw, Romer, and Weil&lt;/strong> suggested adding &lt;strong>human capital&lt;/strong> to the mix. Human capital, like education and health, can significantly enhance productivity. By adding this to the model, we get a richer understanding of growth disparities between nations.&lt;/p>
&lt;p>This shows that growth is not just about physical investments and labor but also about how well the workforce is trained and educated. Human capital plays a pivotal role in enhancing productivity, which can accelerate growth, particularly in poorer countries.&lt;/p>
&lt;h3 id="-convergence-are-poorer-countries-catching-up">📈 Convergence: Are Poorer Countries Catching Up?&lt;/h3>
&lt;p>A critical prediction of the Solow model is &lt;strong>convergence&lt;/strong> — the idea that poorer countries should grow faster than richer countries, eventually catching up in terms of per capita income.&lt;/p>
&lt;p>However, data shows &lt;strong>conditional convergence&lt;/strong> rather than unconditional convergence. This means countries tend to converge to their own steady-state levels of income, which are defined by their individual characteristics like &lt;strong>savings rate&lt;/strong>, &lt;strong>population growth&lt;/strong>, and &lt;strong>human capital&lt;/strong> levels.&lt;/p>
&lt;h3 id="-data-analysis--key-insights">🗃️ Data Analysis &amp;amp; Key Insights&lt;/h3>
&lt;p>The dataset used in this analysis includes cross-country data on economic indicators like GDP, investment rates, and education levels.&lt;/p>
&lt;p>&lt;strong>Data Samples&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Non-oil Sample (98 countries)&lt;/strong>: Countries not heavily reliant on oil production.&lt;/li>
&lt;li>&lt;strong>Intermediate Sample (75 countries)&lt;/strong>: Excludes very small countries and those with data issues.&lt;/li>
&lt;li>&lt;strong>OECD Sample (22 countries)&lt;/strong>: Focuses on countries with higher data quality.&lt;/li>
&lt;/ul>
&lt;p>The Python notebook processes these datasets to estimate the parameters for &lt;strong>savings&lt;/strong>, &lt;strong>population growth&lt;/strong>, and &lt;strong>human capital&lt;/strong>, helping us understand the role of these factors in determining income levels and growth rates across countries.&lt;/p>
&lt;h3 id="-further-resources">🔗 Further Resources&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Video review&lt;/strong>: For a foundational overview of the Solow growth model, check out &lt;a href="https://youtu.be/md0cjl51JTk?si=P4OEEYJqMoBYl3Ir" target="_blank" rel="noopener">this introductory video&lt;/a>&lt;/li>
&lt;li>&lt;strong>Stata Replication Code&lt;/strong>: To replicate the key tables and figures from Mankiw, Romer, and Weil, access the &lt;a href="https://gist.github.com/cmg777/a1181c89de80e5eb5e8c8b" target="_blank" rel="noopener">GitHub Gist here&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Primer on the Solow Model&lt;/strong>: For those new to the basics, &lt;a href="https://wke.lt/w/s/NOD3t3" target="_blank" rel="noopener">this primer&lt;/a> is a great place to start.&lt;/li>
&lt;/ul>
&lt;h3 id="-python-notebook-insights">🖥️ Python Notebook Insights&lt;/h3>
&lt;p>The computational notebook provides step-by-step Python-based analysis, from loading the dataset to estimating parameters and visualizing growth trends. By transforming variables like &lt;strong>GDP&lt;/strong>, &lt;strong>savings&lt;/strong>, and &lt;strong>education&lt;/strong> into their logarithmic forms, the model reveals the underlying dynamics of growth and the relative importance of each factor.&lt;/p>
&lt;h3 id="-summary">📝 Summary&lt;/h3>
&lt;p>The &lt;strong>augmented Solow model&lt;/strong> enriches our understanding of economic growth by adding human capital into the equation. This addition helps explain why some countries grow faster than others and supports the concept of &lt;strong>conditional convergence&lt;/strong> — the idea that countries grow towards their own unique steady states based on their &lt;strong>savings rates&lt;/strong>, &lt;strong>population growth&lt;/strong>, and &lt;strong>education&lt;/strong>.&lt;/p>
&lt;center>
&lt;div class="alert alert-note">
&lt;div>
Learn by R coding using this &lt;a href="https://colab.research.google.com/drive/1MbagABPt4e38e6LhgLuaoBCheuA7ZJ85?usp=sharing">Google Colab notebook&lt;/a>.
&lt;/div>
&lt;/div>
&lt;/center>
&lt;center>
&lt;div class="alert alert-note">
&lt;div>
Learn by Python coding using this &lt;a href="https://colab.research.google.com/drive/1mTgF08Jbf6oNxONbGHyWJZrkygiX0E9N?usp=sharing">Google Colab notebook&lt;/a>.
&lt;/div>
&lt;/div>
&lt;/center>
&lt;center>
&lt;div class="alert alert-note">
&lt;div>
Learn by Stata coding using this &lt;a href="https://gist.github.com/cmg777/a1181c89de80e5eb5e8c8be2383342d1">Stata script&lt;/a>.
&lt;/div>
&lt;/div>
&lt;/center></description></item><item><title>Introduction to spatial data science</title><link>https://carlos-mendez.org/post/python_intro_spatial_data_science/</link><pubDate>Mon, 01 Apr 2019 00:00:00 +0000</pubDate><guid>https://carlos-mendez.org/post/python_intro_spatial_data_science/</guid><description>&lt;p>Introduction to spatial data science with Python&lt;/p></description></item></channel></rss>