<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>LASSO | Carlos Mendez</title><link>https://carlos-mendez.org/category/lasso/</link><atom:link href="https://carlos-mendez.org/category/lasso/index.xml" rel="self" type="application/rss+xml"/><description>LASSO</description><generator>Wowchemy (https://wowchemy.com)</generator><language>en-us</language><copyright>Carlos Mendez</copyright><lastBuildDate>Sat, 04 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>LASSO</title><link>https://carlos-mendez.org/category/lasso/</link></image><item><title>Identifying Latent Group Structures in Panel Data: The classifylasso Command in Stata</title><link>https://carlos-mendez.org/post/stata_panel_lasso_cluster/</link><pubDate>Sat, 04 Apr 2026 00:00:00 +0000</pubDate><guid>https://carlos-mendez.org/post/stata_panel_lasso_cluster/</guid><description>&lt;h2 id="1-overview">1. Overview&lt;/h2>
&lt;p>Do all countries respond the same way to inflation? To interest rates? To democratic transitions? Most panel data models assume yes. They force every country to share the same slope coefficients. That is a strong assumption &amp;mdash; and often a wrong one.&lt;/p>
&lt;p>Here is a preview of what we will discover. When we estimate the effect of inflation on savings across 56 countries, the pooled model says: &amp;ldquo;no significant effect.&amp;rdquo; But that average is a lie. One group of countries saves &lt;em>less&lt;/em> when inflation rises. Another group saves &lt;em>more&lt;/em>. The pooled estimate averages a negative and a positive effect, producing a misleading zero.&lt;/p>
&lt;p>The &lt;strong>Classifier-LASSO&lt;/strong> (C-LASSO) method solves this problem. Developed by Su, Shi, and Phillips (2016), it discovers &lt;strong>latent groups&lt;/strong> in your panel data. Countries within each group share the same coefficients. Countries across groups can differ. Think of it like a sorting hat: rather than treating all countries as identical or all as unique, C-LASSO sorts them into a small number of groups with shared behavioral patterns.&lt;/p>
&lt;p>This tutorial demonstrates the &lt;code>classifylasso&lt;/code> Stata command (Huang, Wang, and Zhou 2024) with two applications:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Savings behavior&lt;/strong> across 56 countries (1995&amp;ndash;2010) &amp;mdash; where inflation affects savings in &lt;em>opposite directions&lt;/em> depending on the country group&lt;/li>
&lt;li>&lt;strong>Democracy and economic growth&lt;/strong> across 98 countries (1970&amp;ndash;2010) &amp;mdash; where the pooled estimate of +1.05 masks a split of +2.15 in one group and -0.94 in another&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Learning objectives:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Understand why assuming homogeneous slopes can be misleading in panel data&lt;/li>
&lt;li>Learn the Classifier-LASSO method for identifying latent group structures&lt;/li>
&lt;li>Implement &lt;code>classifylasso&lt;/code> in Stata with both static and dynamic specifications&lt;/li>
&lt;li>Use postestimation commands (&lt;code>classogroup&lt;/code>, &lt;code>classocoef&lt;/code>, &lt;code>predict gid&lt;/code>) to visualize and interpret results&lt;/li>
&lt;li>Compare pooled fixed-effects estimates with group-specific C-LASSO estimates&lt;/li>
&lt;/ul>
&lt;p>The diagram below maps the tutorial&amp;rsquo;s progression. We start simple and build complexity step by step.&lt;/p>
&lt;pre>&lt;code class="language-mermaid">graph LR
A[&amp;quot;&amp;lt;b&amp;gt;EDA&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Savings data&amp;quot;] --&amp;gt; B[&amp;quot;&amp;lt;b&amp;gt;Baseline FE&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Pooled &amp;amp;&amp;lt;br/&amp;gt;fixed effects&amp;quot;]
B --&amp;gt; C[&amp;quot;&amp;lt;b&amp;gt;C-LASSO&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Static model&amp;lt;br/&amp;gt;(no lagged DV)&amp;quot;]
C --&amp;gt; D[&amp;quot;&amp;lt;b&amp;gt;C-LASSO&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Dynamic model&amp;lt;br/&amp;gt;(jackknife)&amp;quot;]
D --&amp;gt; E[&amp;quot;&amp;lt;b&amp;gt;Democracy&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Application&amp;lt;br/&amp;gt;(two-way FE)&amp;quot;]
E --&amp;gt; F[&amp;quot;&amp;lt;b&amp;gt;Comparison&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Pooled vs&amp;lt;br/&amp;gt;group-specific&amp;quot;]
style A fill:#141413,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:#141413
style F fill:#1a3a8a,stroke:#141413,color:#fff
&lt;/code>&lt;/pre>
&lt;hr>
&lt;h2 id="2-the-problem-homogeneous-vs-heterogeneous-slopes">2. The Problem: Homogeneous vs Heterogeneous Slopes&lt;/h2>
&lt;h3 id="21-three-approaches-to-slope-heterogeneity">2.1 Three approaches to slope heterogeneity&lt;/h3>
&lt;p>Imagine 56 students taking the same exam. &lt;strong>Approach 1&lt;/strong> assumes they all studied the same way &amp;mdash; one average study strategy explains everyone&amp;rsquo;s score. &lt;strong>Approach 2&lt;/strong> gives each student a unique strategy &amp;mdash; but with only a few data points per student, the estimates are noisy. &lt;strong>Approach 3&lt;/strong> (C-LASSO) discovers that students naturally fall into 2&amp;ndash;3 study groups. Students within a group share the same strategy. Students across groups differ.&lt;/p>
&lt;p>The same logic applies to panel data. The standard fixed-effects model is:&lt;/p>
&lt;p>$$y_{it} = \mu_i + \boldsymbol{\beta}' \mathbf{x}_{it} + u_{it}$$&lt;/p>
&lt;p>Here, $y_{it}$ is the outcome for country $i$ at time $t$. The term $\mu_i$ captures country-specific intercepts (fixed effects). The slope vector $\boldsymbol{\beta}$ links the regressors $\mathbf{x}_{it}$ to the outcome. The critical assumption: $\boldsymbol{\beta}$ is the &lt;strong>same for all countries&lt;/strong>. Japan and Nigeria get the same coefficient on inflation. That may be wrong.&lt;/p>
&lt;p>At the other extreme, we could run separate regressions for each country. But with only $T = 15$ time periods per country, individual estimates are noisy. We lose statistical power.&lt;/p>
&lt;p>C-LASSO introduces a middle ground. It assumes countries belong to $K$ latent groups:&lt;/p>
&lt;p>$$\boldsymbol{\beta}_i = \boldsymbol{\alpha}_k \quad \text{if} \quad i \in G_k, \quad k = 1, \ldots, K$$&lt;/p>
&lt;p>In words, country $i$ gets the slope coefficients of its group $G_k$. The method estimates three things simultaneously: the number of groups $K$, which countries belong to which group, and each group&amp;rsquo;s coefficients $\boldsymbol{\alpha}_k$. You do not need to specify the groups in advance. The data reveals them.&lt;/p>
&lt;h3 id="22-why-not-just-use-k-means">2.2 Why not just use K-means?&lt;/h3>
&lt;p>A natural question: why not run individual regressions first and then cluster the coefficients with K-means? C-LASSO has two advantages. First, it estimates group membership and coefficients &lt;strong>jointly&lt;/strong>. A two-step approach (estimate, then cluster) propagates first-stage errors into the grouping. Second, C-LASSO&amp;rsquo;s penalty structure naturally pulls similar countries toward the same group. It is a statistically principled sorting mechanism, not an ad-hoc post-processing step.&lt;/p>
&lt;hr>
&lt;h2 id="3-the-classifier-lasso-method">3. The Classifier-LASSO Method&lt;/h2>
&lt;h3 id="31-the-c-lasso-objective-function">3.1 The C-LASSO objective function&lt;/h3>
&lt;p>C-LASSO minimizes a penalized least-squares objective:&lt;/p>
&lt;p>$$Q_{NT,\lambda}^{(K)} = \frac{1}{NT} \sum_{i=1}^{N} \sum_{t=1}^{T} (y_{it} - \boldsymbol{\beta}_i' \mathbf{x}_{it})^2 + \frac{\lambda_{NT}}{N} \sum_{i=1}^{N} \prod_{k=1}^{K} |\boldsymbol{\beta}_i - \boldsymbol{\alpha}_k|$$&lt;/p>
&lt;p>The first term is the standard sum of squared residuals. It measures how well the model fits the data. The second term is the &lt;strong>penalty&lt;/strong>. It encourages each country&amp;rsquo;s coefficients $\boldsymbol{\beta}_i$ to be close to one of the group centers $\boldsymbol{\alpha}_k$.&lt;/p>
&lt;p>Think of each group center as a &lt;strong>planet with gravitational pull&lt;/strong>. If a country&amp;rsquo;s coefficients are close to &lt;em>any&lt;/em> planet, the product $\prod_k |\boldsymbol{\beta}_i - \boldsymbol{\alpha}_k|$ shrinks toward zero. The penalty becomes small. The country gets pulled into that group. If the coefficients are far from all planets, the penalty stays large. The tuning parameter $\lambda_{NT} = c_\lambda T^{-1/3}$ controls how strong this gravitational pull is.&lt;/p>
&lt;h3 id="32-three-step-estimation-procedure">3.2 Three-step estimation procedure&lt;/h3>
&lt;p>The &lt;code>classifylasso&lt;/code> command works in three steps:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Sort countries into groups.&lt;/strong> For each candidate number of groups $K$, the algorithm iteratively updates group centers and reassigns countries until convergence. Starting values come from unit-by-unit regressions.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Re-estimate within groups (postlasso).&lt;/strong> The LASSO penalty biases the coefficient estimates. So after sorting, we discard the penalized estimates and re-run plain OLS within each group. Think of it like a talent show: LASSO is the audition that selects who is in which group, but the final performance (the coefficient estimates) is unpenalized. This postlasso step gives us valid standard errors and confidence intervals.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Pick the best $K$ (information criterion).&lt;/strong> How many groups are there? The command tests $K = 1, 2, \ldots, K_{\max}$ and picks the $K$ that minimizes an information criterion. The IC acts like a &lt;strong>referee&lt;/strong> balancing two concerns: fit (more groups fit better) and complexity (more groups risk overfitting). It works like AIC or BIC. The tuning parameter $\rho_{NT} = c_\rho (NT)^{-1/2}$ controls how harshly the referee penalizes extra groups.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h3 id="33-dynamic-panels-and-nickell-bias">3.3 Dynamic panels and Nickell bias&lt;/h3>
&lt;p>What if your model includes a lagged dependent variable, like $y_{i,t-1}$? This creates a problem called &lt;strong>Nickell bias&lt;/strong>. When you demean the data to remove fixed effects, the demeaned lagged outcome becomes correlated with the demeaned error. The result: biased coefficients.&lt;/p>
&lt;p>The &lt;code>classifylasso&lt;/code> command offers a &lt;code>dynamic&lt;/code> option to fix this. It uses the &lt;strong>half-panel jackknife&lt;/strong> (Dhaene and Jochmans 2015). The idea is simple: split the time series in half. Estimate the model on each half. Combine the two estimates in a way that cancels the bias. Problem solved.&lt;/p>
&lt;p>Now that we understand the method, let&amp;rsquo;s apply it to real data.&lt;/p>
&lt;hr>
&lt;h2 id="4-data-exploration-savings">4. Data Exploration: Savings&lt;/h2>
&lt;h3 id="41-load-and-describe-the-data">4.1 Load and describe the data&lt;/h3>
&lt;p>Our first application uses a panel of 56 countries over 15 years, from Su, Shi, and Phillips (2016). The outcome is the savings-to-GDP ratio. The regressors are lagged savings, CPI inflation, real interest rates, and GDP growth.&lt;/p>
&lt;pre>&lt;code class="language-stata">use &amp;quot;https://github.com/cmg777/starter-academic-v501/raw/master/content/post/stata_panel_lasso_cluster/refMaterials/saving.dta&amp;quot;, clear
xtset code year
summarize savings lagsavings cpi interest gdp
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> Variable | Obs Mean Std. dev. Min Max
-------------+---------------------------------------------------------
savings | 840 -2.87e-08 1.000596 -2.495871 2.893858
lagsavings | 840 5.81e-08 1.000596 -2.832278 2.91508
cpi | 840 3.56e-09 1.000596 -2.773791 3.548945
interest | 840 -7.17e-09 1.000596 -3.600348 3.277582
gdp | 840 1.06e-08 1.000596 -3.554419 2.461317
&lt;/code>&lt;/pre>
&lt;p>The panel is strongly balanced: 56 countries $\times$ 15 years = 840 observations. All variables are standardized to mean zero and standard deviation one. This means coefficients are in standard-deviation units. A coefficient of 0.18 means &amp;ldquo;a one-SD increase in CPI is associated with a 0.18-SD change in savings.&amp;rdquo; The balanced structure matters: C-LASSO requires all countries to be observed in all time periods.&lt;/p>
&lt;h3 id="42-visualize-cross-country-heterogeneity">4.2 Visualize cross-country heterogeneity&lt;/h3>
&lt;p>Before running any regressions, it helps to visualize how savings trajectories differ across countries. The &lt;code>xtline&lt;/code> command overlays all 56 country lines on a single plot:&lt;/p>
&lt;pre>&lt;code class="language-stata">xtline savings, overlay ///
title(&amp;quot;Savings-to-GDP Ratio Across 56 Countries&amp;quot;, size(medium)) ///
subtitle(&amp;quot;Each line represents one country&amp;quot;, size(small)) ///
ytitle(&amp;quot;Savings / GDP&amp;quot;) xtitle(&amp;quot;Year&amp;quot;) legend(off)
graph export &amp;quot;stata_panel_lasso_cluster_fig1_savings_scatter.png&amp;quot;, replace width(2400)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_panel_lasso_cluster_fig1_savings_scatter.png" alt="Spaghetti plot of savings-to-GDP ratio across 56 countries, showing wide dispersion in trajectories.">
&lt;em>Figure 1: Savings-to-GDP ratio across 56 countries (1995&amp;ndash;2010). Each line represents one country, revealing substantial heterogeneity in savings dynamics.&lt;/em>&lt;/p>
&lt;p>The spaghetti plot tells a clear story: countries do not move in lockstep. Some maintain positive savings ratios throughout. Others swing below zero. The lines diverge, cross, and cluster &amp;mdash; suggesting that different countries follow fundamentally different savings dynamics. This is exactly the kind of heterogeneity that C-LASSO is designed to detect. Perhaps subsets of countries share similar responses, even if the full panel does not.&lt;/p>
&lt;p>But first, let&amp;rsquo;s see what the standard models say.&lt;/p>
&lt;hr>
&lt;h2 id="5-baseline-pooled-and-fixed-effects-regressions">5. Baseline: Pooled and Fixed Effects Regressions&lt;/h2>
&lt;p>Before applying C-LASSO, we establish a benchmark by estimating the standard pooled OLS and fixed-effects models. These models assume that all 56 countries share the same slope coefficients.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Pooled OLS
regress savings lagsavings cpi interest gdp
* Standard Fixed Effects
xtreg savings lagsavings cpi interest gdp, fe
* Robust Fixed Effects (reghdfe)
reghdfe savings lagsavings cpi interest gdp, absorb(code) vce(robust)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> Pooled OLS FE (robust)
lagsavings 0.6051 0.6051
cpi 0.0301 0.0301
interest 0.0059 0.0059
gdp 0.1882 0.1882
&lt;/code>&lt;/pre>
&lt;p>The pooled OLS and fixed-effects estimates are virtually identical. R-squared is 0.438. Lagged savings dominates (coefficient 0.605, $p &amp;lt; 0.001$). GDP growth matters too (0.188, $p &amp;lt; 0.001$).&lt;/p>
&lt;p>Now look at the two remaining variables. CPI: 0.030. Interest rate: 0.006. Both statistically insignificant. A textbook conclusion would be: &amp;ldquo;Inflation and interest rates do not affect savings.&amp;rdquo;&lt;/p>
&lt;p>But what if the average is lying? Imagine a city where half the neighborhoods warm up by 5 degrees and the other half cool down by 5 degrees. The citywide average temperature change is zero. A meteorologist reporting &amp;ldquo;no change&amp;rdquo; would be wrong &amp;mdash; there &lt;em>are&lt;/em> changes, just in opposite directions. This is exactly what we will discover with C-LASSO.&lt;/p>
&lt;hr>
&lt;h2 id="6-classifier-lasso-savings-static-model">6. Classifier-LASSO: Savings, Static Model&lt;/h2>
&lt;h3 id="61-estimation">6.1 Estimation&lt;/h3>
&lt;p>We start with the simplest C-LASSO specification: a static model without the lagged dependent variable. This lets us focus on the core mechanics before adding complexity.&lt;/p>
&lt;pre>&lt;code class="language-stata">classifylasso savings cpi interest gdp, grouplist(1/5) tolerance(1e-4)
&lt;/code>&lt;/pre>
&lt;p>The command searches over $K = 1$ to $K = 5$ groups and reports the information criterion (IC) for each:&lt;/p>
&lt;pre>&lt;code class="language-text">Estimation 1: Group Number = 1; IC = 0.054
Estimation 2: Group Number = 2; IC = -0.028 ← minimum
Estimation 3: Group Number = 3; IC = 0.059
Estimation 4: Group Number = 4; IC = 0.131
Estimation 5: Group Number = 5; IC = 0.213
* Selected Group Number: 2
&lt;/code>&lt;/pre>
&lt;p>The IC is minimized at $K = 2$, with values rising monotonically from $K = 3$ onward. This clear U-shape provides strong evidence for exactly two latent groups in the data.&lt;/p>
&lt;h3 id="62-group-specific-coefficients">6.2 Group-specific coefficients&lt;/h3>
&lt;pre>&lt;code class="language-stata">classoselect, postselection
predict gid_static, gid
tabulate gid_static
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Group 1 (34 countries, 510 obs): Within R-sq. = 0.2019
cpi | -0.1813 (z = -4.29, p &amp;lt; 0.001)
interest | -0.1966 (z = -4.64, p &amp;lt; 0.001)
gdp | 0.3346 (z = 7.98, p &amp;lt; 0.001)
Group 2 (22 countries, 330 obs): Within R-sq. = 0.2369
cpi | 0.4781 (z = 9.10, p &amp;lt; 0.001)
interest | 0.2631 (z = 5.01, p &amp;lt; 0.001)
gdp | 0.1117 (z = 2.23, p = 0.026)
&lt;/code>&lt;/pre>
&lt;p>The results are striking. Look at CPI.&lt;/p>
&lt;p>In &lt;strong>Group 1&lt;/strong> (34 countries), higher inflation &lt;em>reduces&lt;/em> savings: coefficient $-0.181$ ($p &amp;lt; 0.001$). In &lt;strong>Group 2&lt;/strong> (22 countries), higher inflation &lt;em>increases&lt;/em> savings: coefficient $+0.478$ ($p &amp;lt; 0.001$). The sign flips completely.&lt;/p>
&lt;p>The same reversal appears for the interest rate: $-0.197$ in Group 1 versus $+0.263$ in Group 2.&lt;/p>
&lt;p>Now the pooled CPI coefficient of $+0.030$ makes sense. It was averaging $-0.181$ and $+0.478$ &amp;mdash; a negative and a positive effect canceling each other out. The &amp;ldquo;insignificant&amp;rdquo; result was not evidence of no effect. It was evidence of &lt;strong>two opposing effects&lt;/strong> hidden inside the average.&lt;/p>
&lt;p>Why the reversal? In Group 1, higher inflation erodes the real value of savings, discouraging people from saving. In Group 2, higher inflation may trigger &lt;strong>precautionary savings&lt;/strong> &amp;mdash; households save &lt;em>more&lt;/em> precisely because the economic environment feels uncertain. Same macroeconomic shock, opposite behavioral response.&lt;/p>
&lt;h3 id="63-group-selection-plot">6.3 Group selection plot&lt;/h3>
&lt;pre>&lt;code class="language-stata">classogroup
graph export &amp;quot;stata_panel_lasso_cluster_fig2_group_selection_static.png&amp;quot;, replace width(2400)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_panel_lasso_cluster_fig2_group_selection_static.png" alt="Information criterion and iteration count by number of groups for the static savings model. IC is minimized at K=2.">
&lt;em>Figure 2: Group selection for the static savings model. The information criterion (left axis) is minimized at K=2, with a clear U-shape from K=3 onward.&lt;/em>&lt;/p>
&lt;p>The triangle marks the IC minimum at $K = 2$. The left axis shows IC values; the right axis shows iterations to convergence. Notice: $K = 2$ converged quickly (about 3 iterations). Models with $K \geq 3$ hit the maximum 20 iterations. When the algorithm struggles to converge, it is a sign of overparameterization &amp;mdash; too many groups for the data to support.&lt;/p>
&lt;p>So far, we have found two groups with a static model. But we omitted lagged savings. Let&amp;rsquo;s add it back.&lt;/p>
&lt;hr>
&lt;h2 id="7-classifier-lasso-savings-dynamic-model">7. Classifier-LASSO: Savings, Dynamic Model&lt;/h2>
&lt;h3 id="71-adding-the-lagged-dependent-variable">7.1 Adding the lagged dependent variable&lt;/h3>
&lt;p>Savings are highly persistent. The pooled coefficient on &lt;code>lagsavings&lt;/code> was 0.605 &amp;mdash; a country&amp;rsquo;s savings this year strongly predicts its savings next year. Omitting this variable may bias everything else. We now add it back and replicate Su, Shi, and Phillips (2016). The &lt;code>dynamic&lt;/code> option activates the half-panel jackknife to correct Nickell bias.&lt;/p>
&lt;pre>&lt;code class="language-stata">use &amp;quot;https://github.com/cmg777/starter-academic-v501/raw/master/content/post/stata_panel_lasso_cluster/refMaterials/saving.dta&amp;quot;, clear
xtset code year
classifylasso savings lagsavings cpi interest gdp, ///
grouplist(1/5) lambda(1.5485) tolerance(1e-4) dynamic
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">* Selected Group Number: 2
The algorithm takes 9min57s.
Group 1 (31 countries, 465 obs): Within R-sq. = 0.4988
lagsavings | 0.6952 (z = 18.15, p &amp;lt; 0.001)
cpi | -0.1602 (z = -4.09, p &amp;lt; 0.001)
interest | -0.1490 (z = -4.04, p &amp;lt; 0.001)
gdp | 0.2892 (z = 7.62, p &amp;lt; 0.001)
Group 2 (25 countries, 375 obs): Within R-sq. = 0.4372
lagsavings | 0.6939 (z = 19.45, p &amp;lt; 0.001)
cpi | 0.1967 (z = 4.93, p &amp;lt; 0.001)
interest | 0.1225 (z = 2.98, p = 0.003)
gdp | 0.1127 (z = 2.38, p = 0.018)
&lt;/code>&lt;/pre>
&lt;p>Again, C-LASSO selects $K = 2$ groups. The sign reversal on CPI survives: $-0.160$ in Group 1 versus $+0.197$ in Group 2. Same for the interest rate: $-0.149$ versus $+0.123$.&lt;/p>
&lt;p>Here is what is interesting about the &lt;code>lagsavings&lt;/code> coefficient. Both groups show nearly identical persistence: 0.695 in Group 1 and 0.694 in Group 2. Think of it like a speedometer. Both groups of countries cruise at the same speed (savings persistence). But they swerve in opposite directions when they hit a pothole (an inflation or interest rate shock). The heterogeneity is about &lt;em>reactions to shocks&lt;/em>, not about baseline behavior.&lt;/p>
&lt;p>Adding lagged savings also improved the fit. Within R-squared jumped from 0.20&amp;ndash;0.24 (static) to 0.44&amp;ndash;0.50 (dynamic). The lagged variable clearly matters.&lt;/p>
&lt;h3 id="72-coefficient-plots">7.2 Coefficient plots&lt;/h3>
&lt;p>The &lt;code>classocoef&lt;/code> postestimation command visualizes group-specific coefficients with 95% confidence bands:&lt;/p>
&lt;pre>&lt;code class="language-stata">classocoef cpi
graph export &amp;quot;stata_panel_lasso_cluster_fig3_coef_cpi.png&amp;quot;, replace width(2400)
classocoef interest
graph export &amp;quot;stata_panel_lasso_cluster_fig4_coef_interest.png&amp;quot;, replace width(2400)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_panel_lasso_cluster_fig3_coef_cpi.png" alt="CPI coefficient estimates and 95% confidence bands by group, showing a clear sign reversal with non-overlapping confidence intervals.">
&lt;em>Figure 3: Heterogeneous effects of CPI on savings. Group 1 (31 countries) shows a negative effect; Group 2 (25 countries) shows a positive effect. Confidence bands do not overlap.&lt;/em>&lt;/p>
&lt;p>This is the &amp;ldquo;smoking gun&amp;rdquo; figure. The two horizontal lines are the group-specific coefficients. The dashed lines show 95% confidence bands. The bands do not overlap. This is not a marginal difference. It is a robust sign reversal.&lt;/p>
&lt;p>For 31 countries (Group 1), higher inflation reduces savings ($-0.160$, $p &amp;lt; 0.001$). For 25 countries (Group 2), higher inflation increases savings ($+0.197$, $p &amp;lt; 0.001$). A pooled model averages these opposing forces and finds CPI &amp;ldquo;insignificant.&amp;rdquo; That is aggregation bias at work.&lt;/p>
&lt;p>&lt;img src="stata_panel_lasso_cluster_fig4_coef_interest.png" alt="Interest rate coefficient estimates and 95% confidence bands by group, showing the same sign reversal pattern as CPI.">
&lt;em>Figure 4: Heterogeneous effects of the interest rate on savings. The same sign reversal pattern as CPI: negative in Group 1, positive in Group 2.&lt;/em>&lt;/p>
&lt;p>The interest rate tells the same story. Group 1 countries save &lt;em>less&lt;/em> when rates rise ($-0.149$). Group 2 countries save &lt;em>more&lt;/em> ($+0.123$).&lt;/p>
&lt;p>Why? One interpretation: in Group 1 (more developed financial markets), higher returns make consumption more attractive &amp;mdash; the &lt;strong>substitution effect&lt;/strong> dominates. In Group 2 (limited financial access), higher returns make saving more rewarding &amp;mdash; the &lt;strong>income effect&lt;/strong> dominates.&lt;/p>
&lt;p>We have now established that latent groups exist in savings data. The next question: does the same pattern appear in a completely different economic context?&lt;/p>
&lt;hr>
&lt;h2 id="8-democracy-application-does-democracy-cause-growth">8. Democracy Application: Does Democracy Cause Growth?&lt;/h2>
&lt;h3 id="81-the-acemoglu-et-al-2019-question">8.1 The Acemoglu et al. (2019) question&lt;/h3>
&lt;p>&amp;ldquo;Democracy does cause growth.&amp;rdquo; That is the title of a famous 2019 paper by Acemoglu, Naidu, Restrepo, and Robinson in the &lt;em>Journal of Political Economy&lt;/em>. Their evidence: a pooled two-way fixed-effects model with lagged GDP finds a positive, significant effect.&lt;/p>
&lt;p>But we have learned to be skeptical of pooled estimates. Does this average apply to all 98 countries? Or does it mask the same kind of sign reversal we found in savings?&lt;/p>
&lt;h3 id="82-data-exploration">8.2 Data exploration&lt;/h3>
&lt;pre>&lt;code class="language-stata">use &amp;quot;https://github.com/cmg777/starter-academic-v501/raw/master/content/post/stata_panel_lasso_cluster/refMaterials/democracy.dta&amp;quot;, clear
xtset country year
summarize lnPGDP Democracy ly1
tabulate Democracy
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> Variable | Obs Mean Std. dev. Min Max
-------------+---------------------------------------------------------
lnPGDP | 4,018 758.5558 162.9137 405.6728 1094.003
Democracy | 4,018 .5450473 .4980286 0 1
ly1 | 3,920 757.7754 162.6702 405.6728 1094.003
Democracy | Freq. Percent
------------+-----------------------------------
0 | 1,828 45.50
1 | 2,190 54.50
&lt;/code>&lt;/pre>
&lt;p>The panel covers 98 countries from 1970 to 2010 &amp;mdash; 4,018 observations. The binary &lt;code>Democracy&lt;/code> indicator is 1 for democratic country-years and 0 otherwise. About 55% of observations are democratic, reflecting the global wave of democratization. The dependent variable &lt;code>lnPGDP&lt;/code> (log per-capita GDP, scaled) ranges from 406 to 1,094 &amp;mdash; the full spectrum from low-income to high-income countries.&lt;/p>
&lt;h3 id="83-pooled-fixed-effects-benchmark">8.3 Pooled fixed-effects benchmark&lt;/h3>
&lt;pre>&lt;code class="language-stata">reghdfe lnPGDP Democracy ly1, absorb(country year) cluster(country)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">HDFE Linear regression Number of obs = 3,920
R-squared = 0.9991
Within R-sq. = 0.9607
(Std. err. adjusted for 98 clusters in country)
lnPGDP | Coefficient Robust std. err. t P&amp;gt;|t|
Democracy | 1.054992 .369806 2.85 0.005
ly1 | .970495 .0059964 161.85 0.000
&lt;/code>&lt;/pre>
&lt;p>Democracy is associated with a 1.055-unit increase in log per-capita GDP ($p = 0.005$, clustered SE = 0.370). Lagged GDP has a coefficient of 0.970 &amp;mdash; strong persistence. This replicates Acemoglu et al. (2019): on average, democracy promotes growth.&lt;/p>
&lt;p>On average. But we already know what &amp;ldquo;on average&amp;rdquo; can hide. Let&amp;rsquo;s run C-LASSO.&lt;/p>
&lt;h3 id="84-c-lasso-revealing-the-heterogeneity">8.4 C-LASSO: revealing the heterogeneity&lt;/h3>
&lt;pre>&lt;code class="language-stata">classifylasso lnPGDP Democracy ly1, ///
grouplist(1/5) rho(0.2) absorb(country year) ///
cluster(country) dynamic optmaxiter(300)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">* Selected Group Number: 2
The algorithm takes 2h33min41s.
Group 1 (57 countries, 2,280 obs): Within R-sq. = 0.9609
Democracy | 2.151397 (z = 3.94, p &amp;lt; 0.001)
ly1 | 1.032752 (z = 149.97, p &amp;lt; 0.001)
Group 2 (41 countries, 1,640 obs): Within R-sq. = 0.9538
Democracy | -0.935589 (z = -2.69, p = 0.007)
ly1 | 0.979327 (z = 95.73, p &amp;lt; 0.001)
&lt;/code>&lt;/pre>
&lt;p>This is the tutorial&amp;rsquo;s most striking finding.&lt;/p>
&lt;p>The pooled coefficient of $+1.055$ is &lt;strong>not representative of any actual country group&lt;/strong>. It is a weighted average of two fundamentally different effects:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Group 1&lt;/strong> (57 countries): democracy effect = $+2.151$ ($p &amp;lt; 0.001$). More than twice the pooled estimate.&lt;/li>
&lt;li>&lt;strong>Group 2&lt;/strong> (41 countries): democracy effect = $-0.936$ ($p = 0.007$). Negative and significant.&lt;/li>
&lt;/ul>
&lt;p>The coefficient literally changes sign. For 58% of countries, democratic transitions are associated with GDP gains. For the remaining 42%, they are associated with GDP declines. The pooled model sees one number. C-LASSO sees two stories.&lt;/p>
&lt;p>Note: these are conditional associations within the panel model. A causal interpretation requires the same identifying assumptions as Acemoglu et al. (2019).&lt;/p>
&lt;h3 id="85-visualizing-the-democracy-growth-split">8.5 Visualizing the democracy-growth split&lt;/h3>
&lt;pre>&lt;code class="language-stata">classogroup
graph export &amp;quot;stata_panel_lasso_cluster_fig5_democracy_selection.png&amp;quot;, replace width(2400)
classocoef Democracy
graph export &amp;quot;stata_panel_lasso_cluster_fig6_democracy_coef.png&amp;quot;, replace width(2400)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_panel_lasso_cluster_fig5_democracy_selection.png" alt="Information criterion and iteration count for the democracy model. IC is minimized at K=2, though values are close across specifications.">
&lt;em>Figure 5: Group selection for the democracy-growth model. IC is minimized at K=2, though values are close across all K (range 3.267&amp;ndash;3.280).&lt;/em>&lt;/p>
&lt;p>The IC selects $K = 2$. But look closely: the IC values range from 3.267 to 3.280 &amp;mdash; a span of just 0.013. The 2-group structure is optimal but not overwhelmingly so. This is a useful reminder: always check sensitivity to the tuning parameter $\rho$.&lt;/p>
&lt;p>&lt;img src="stata_panel_lasso_cluster_fig6_democracy_coef.png" alt="Democracy coefficient polarization across two groups: Group 1 (57 countries) shows a positive effect around +2.2, Group 2 (41 countries) shows a negative effect around -1.0.">
&lt;em>Figure 6: Heterogeneous effects of democracy on economic growth. Group 1 (57 countries) shows a positive effect (+2.15); Group 2 (41 countries) shows a negative effect (-0.94). The pooled estimate of +1.05 describes neither group.&lt;/em>&lt;/p>
&lt;p>This is the key figure of the tutorial. Each dot is one country&amp;rsquo;s individual coefficient estimate. The horizontal lines show group-specific postlasso estimates with 95% confidence bands.&lt;/p>
&lt;p>The polarization is unmistakable. Group 1 (left cluster): strongly positive. Group 2 (right cluster): negative. Neither group&amp;rsquo;s confidence band crosses zero. Both effects are statistically significant.&lt;/p>
&lt;p>This is not &amp;ldquo;some countries benefit, others see no effect.&amp;rdquo; It is a genuine sign reversal. Democracy is associated with growth in one group and with decline in another.&lt;/p>
&lt;hr>
&lt;h2 id="9-comparison-what-the-pooled-model-misses">9. Comparison: What the Pooled Model Misses&lt;/h2>
&lt;h3 id="91-summary-table">9.1 Summary table&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>Pooled FE&lt;/th>
&lt;th>C-LASSO Group 1&lt;/th>
&lt;th>C-LASSO Group 2&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Democracy coefficient&lt;/strong>&lt;/td>
&lt;td>+1.055&lt;/td>
&lt;td>+2.151&lt;/td>
&lt;td>-0.936&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Standard error&lt;/strong>&lt;/td>
&lt;td>0.370&lt;/td>
&lt;td>0.546&lt;/td>
&lt;td>0.348&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>p-value&lt;/strong>&lt;/td>
&lt;td>0.005&lt;/td>
&lt;td>&amp;lt; 0.001&lt;/td>
&lt;td>0.007&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Lagged GDP&lt;/strong>&lt;/td>
&lt;td>0.970&lt;/td>
&lt;td>1.033&lt;/td>
&lt;td>0.979&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Countries&lt;/strong>&lt;/td>
&lt;td>98&lt;/td>
&lt;td>57&lt;/td>
&lt;td>41&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Observations&lt;/strong>&lt;/td>
&lt;td>3,920&lt;/td>
&lt;td>2,280&lt;/td>
&lt;td>1,640&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="92-simpsons-paradox-in-panel-data">9.2 Simpson&amp;rsquo;s paradox in panel data&lt;/h3>
&lt;p>This is &lt;strong>Simpson&amp;rsquo;s paradox&lt;/strong> &amp;mdash; the phenomenon where a trend that appears in aggregated data reverses when you look at subgroups.&lt;/p>
&lt;p>Here is a concrete analogy. A hospital treats two types of patients: mild cases and severe cases. For mild cases, Treatment A has a higher survival rate. For severe cases, Treatment A also has a higher survival rate. But when you pool all patients together, Treatment B appears better &amp;mdash; because it treats a disproportionate number of mild (easy) cases. The aggregate reverses the subgroup trend.&lt;/p>
&lt;p>The same thing happened here. The pooled democracy estimate of $+1.055$ sits between $+2.151$ and $-0.936$. It describes neither group accurately. A policymaker relying on the pooled result would conclude that democracy universally promotes growth. They would miss that for 41 countries (42% of the sample), the relationship runs in the opposite direction.&lt;/p>
&lt;p>The savings model showed the same pattern. The insignificant pooled CPI coefficient ($+0.030$) masked significant effects of $-0.160$ and $+0.197$. When effects have opposite signs, pooling does not just underestimate the magnitude. It produces a qualitatively wrong conclusion.&lt;/p>
&lt;h3 id="93-robustness-of-the-group-structure">9.3 Robustness of the group structure&lt;/h3>
&lt;p>Across all three C-LASSO specifications &amp;mdash; static savings, dynamic savings, and democracy &amp;mdash; the IC consistently selected $K = 2$ groups. The CPI sign reversal survived the switch from static to dynamic, despite a shift in group composition (34/22 to 31/25). This consistency suggests the latent groups are real structural features of the data, not artifacts of a particular specification.&lt;/p>
&lt;hr>
&lt;h2 id="10-summary-and-takeaways">10. Summary and Takeaways&lt;/h2>
&lt;h3 id="101-what-we-learned">10.1 What we learned&lt;/h3>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>Pooled estimates can be misleading.&lt;/strong> The insignificant pooled CPI coefficient ($+0.030$) in the savings model masked opposing effects of $-0.160$ and $+0.197$ in two latent groups. The pooled democracy coefficient ($+1.055$) masked a split of $+2.151$ versus $-0.936$.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>C-LASSO finds latent groups.&lt;/strong> In all three specifications, the information criterion selected $K = 2$ groups, revealing binary latent structures in both datasets. The &lt;code>classifylasso&lt;/code> command handles the full workflow: estimation, group selection, and postestimation.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>The &lt;code>dynamic&lt;/code> option corrects Nickell bias.&lt;/strong> When lagged dependent variables are included, the half-panel jackknife bias correction preserves the group structure while improving within-group R-squared (from 0.20&amp;ndash;0.24 in the static model to 0.44&amp;ndash;0.50 in the dynamic model).&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Postestimation tools aid interpretation.&lt;/strong> The &lt;code>classogroup&lt;/code> command visualizes the information criterion, &lt;code>classocoef&lt;/code> plots group-specific coefficients with confidence bands, and &lt;code>predict gid&lt;/code> assigns countries to groups.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h3 id="102-limitations">10.2 Limitations&lt;/h3>
&lt;p>Three caveats. First, the IC values in the democracy model were very close across $K = 1$ through $K = 5$ (range 3.267&amp;ndash;3.280). The 2-group structure is optimal but not dominant. Second, the datasets use numeric country codes, not names. We cannot easily identify which countries are in which group. Third, C-LASSO is computationally intensive. The democracy model took over 2.5 hours. Plan accordingly.&lt;/p>
&lt;h3 id="103-exercises">10.3 Exercises&lt;/h3>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Sensitivity analysis.&lt;/strong> Re-run the democracy model with &lt;code>rho(0.5)&lt;/code> and &lt;code>rho(1.0)&lt;/code> instead of &lt;code>rho(0.2)&lt;/code>. Does the selected number of groups change? How sensitive are the group assignments to this tuning parameter?&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Extended lag structure.&lt;/strong> Following the reference &lt;code>empirical.do&lt;/code>, estimate the democracy model with 2, 3, and 4 lags of GDP (&lt;code>ly1-ly2&lt;/code>, &lt;code>ly1-ly3&lt;/code>, &lt;code>ly1-ly4&lt;/code>). Do the group-specific democracy coefficients remain stable?&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Static vs dynamic comparison.&lt;/strong> Run &lt;code>classifylasso savings cpi interest gdp&lt;/code> (without &lt;code>dynamic&lt;/code>) on the savings data and compare group assignments with the dynamic model using &lt;code>tabulate gid_static gid_dynamic&lt;/code>. How many countries switch groups?&lt;/p>
&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="references">References&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>Su, L., Shi, Z., and Phillips, P. C. B. (2016). &lt;a href="https://doi.org/10.3982/ECTA12560" target="_blank" rel="noopener">Identifying latent structures in panel data&lt;/a>. &lt;em>Econometrica&lt;/em>, 84(6), 2215&amp;ndash;2264.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Huang, W., Wang, Y., and Zhou, L. (2024). &lt;a href="https://doi.org/10.1177/1536867X241233664" target="_blank" rel="noopener">Identify latent group structures in panel data: The classifylasso command&lt;/a>. &lt;em>Stata Journal&lt;/em>, 24(1), 173&amp;ndash;203.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Acemoglu, D., Naidu, S., Restrepo, P., and Robinson, J. A. (2019). &lt;a href="https://doi.org/10.1086/700936" target="_blank" rel="noopener">Democracy does cause growth&lt;/a>. &lt;em>Journal of Political Economy&lt;/em>, 127(1), 47&amp;ndash;100.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Dhaene, G. and Jochmans, K. (2015). &lt;a href="https://doi.org/10.1093/restud/rdv007" target="_blank" rel="noopener">Split-panel jackknife estimation of fixed-effect models&lt;/a>. &lt;em>Review of Economic Studies&lt;/em>, 82(3), 991&amp;ndash;1030.&lt;/p>
&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>Taming Model Uncertainty in the Environmental Kuznets Curve: BMA and Double-Selection LASSO with Panel Data</title><link>https://carlos-mendez.org/post/stata_bma_dsl/</link><pubDate>Sun, 29 Mar 2026 00:00:00 +0000</pubDate><guid>https://carlos-mendez.org/post/stata_bma_dsl/</guid><description>&lt;h2 id="1-overview">1. Overview&lt;/h2>
&lt;p>Can countries grow their way out of pollution? The &lt;strong>Environmental Kuznets Curve (EKC)&lt;/strong> hypothesis says yes &amp;mdash; up to a point. As economies develop, pollution first rises with industrialization and then falls as countries grow wealthy enough to afford cleaner technology. But recent research suggests a more complex &lt;strong>inverted-N&lt;/strong> shape: pollution falls at very low incomes, rises through industrialization, and then falls again at high incomes.&lt;/p>
&lt;p>Testing for this shape requires a cubic polynomial in GDP per capita &amp;mdash; and beyond GDP, many other factors might affect CO&lt;sub>2&lt;/sub> emissions. With 12 candidate control variables, there are $2^{12} = 4{,}096$ possible regression models. &lt;strong>Which model should we estimate?&lt;/strong> This is the &lt;strong>model uncertainty problem&lt;/strong>.&lt;/p>
&lt;p>This tutorial introduces two principled solutions:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Bayesian Model Averaging (BMA)&lt;/strong> estimates thousands of models and averages the results, weighting each by how well it fits the data. Each variable gets a &lt;strong>Posterior Inclusion Probability (PIP)&lt;/strong> &amp;mdash; the fraction of high-quality models that include it.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Post-Double-Selection LASSO (DSL)&lt;/strong> uses LASSO to automatically select which controls matter &amp;mdash; once for the outcome, once for each variable of interest &amp;mdash; then runs OLS with the union of all selected controls. This &amp;ldquo;select, then regress&amp;rdquo; approach protects against omitted variable bias.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>We use &lt;strong>synthetic panel data&lt;/strong> with a known &amp;ldquo;answer key&amp;rdquo; &amp;mdash; we designed the data so that 5 controls truly affect CO&lt;sub>2&lt;/sub> and 7 are pure noise. This lets us grade each method: does it correctly identify the true predictors? The data is inspired by the panel dataset of Gravina and Lanzafame (2025) but is fully synthetic and not identical to the original.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Companion tutorial.&lt;/strong> For a cross-sectional perspective using R with BMA, LASSO, and WALS, see the &lt;a href="https://carlos-mendez.org/post/r_bma_lasso_wals/">R tutorial on variable selection&lt;/a>.&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>Learning objectives:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Understand the EKC hypothesis and why a cubic polynomial tests for an inverted-N shape&lt;/li>
&lt;li>Recognize model uncertainty as a practical challenge when many controls are available&lt;/li>
&lt;li>Implement BMA with &lt;code>bmaregress&lt;/code> and interpret PIPs and coefficient densities&lt;/li>
&lt;li>Implement post-double-selection LASSO with &lt;code>dsregress&lt;/code> and understand its four-step algorithm: LASSO on outcome, LASSO on each variable of interest, union, then OLS&lt;/li>
&lt;li>Evaluate both methods against a known ground truth to assess their accuracy&lt;/li>
&lt;/ul>
&lt;p>The following diagram summarizes the methodological sequence of this tutorial. We begin with exploratory data analysis to visualize the raw income&amp;ndash;pollution relationship, then estimate baseline fixed effects regressions to expose the model uncertainty problem. Next, we apply BMA and DSL as two alternative solutions, and finally compare both methods against the known answer key.&lt;/p>
&lt;pre>&lt;code class="language-mermaid">graph LR
A[&amp;quot;&amp;lt;b&amp;gt;EDA&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Scatter plot&amp;quot;] --&amp;gt; B[&amp;quot;&amp;lt;b&amp;gt;Baseline FE&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Standard panel&amp;lt;br/&amp;gt;regressions&amp;quot;]
B --&amp;gt; C[&amp;quot;&amp;lt;b&amp;gt;BMA&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Bayesian Model&amp;lt;br/&amp;gt;Averaging&amp;quot;]
C --&amp;gt; D[&amp;quot;&amp;lt;b&amp;gt;DSL&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Double-Selection&amp;lt;br/&amp;gt;LASSO&amp;quot;]
D --&amp;gt; E[&amp;quot;&amp;lt;b&amp;gt;Comparison&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Check against&amp;lt;br/&amp;gt;answer key&amp;quot;]
style A fill:#141413,stroke:#141413,color:#fff
style B fill:#6a9bcc,stroke:#141413,color:#fff
style C fill:#d97757,stroke:#141413,color:#fff
style D fill:#00d4c8,stroke:#141413,color:#141413
style E fill:#1a3a8a,stroke:#141413,color:#fff
&lt;/code>&lt;/pre>
&lt;h2 id="2-setup-and-synthetic-data">2. Setup and Synthetic Data&lt;/h2>
&lt;h3 id="21-why-synthetic-data">2.1 Why synthetic data?&lt;/h3>
&lt;p>Real-world datasets rarely come with an answer key. We never know which control variables &lt;em>truly&lt;/em> belong in the model. By generating synthetic data with a known data-generating process (DGP), we can verify whether BMA and DSL correctly recover the truth. This is the same &amp;ldquo;answer key&amp;rdquo; approach used in the &lt;a href="https://carlos-mendez.org/post/r_bma_lasso_wals/">companion R tutorial&lt;/a>, applied here to panel data.&lt;/p>
&lt;h3 id="22-the-data-generating-process">2.2 The data-generating process&lt;/h3>
&lt;p>The outcome &amp;mdash; log CO&lt;sub>2&lt;/sub> per capita &amp;mdash; follows a cubic EKC with country and year fixed effects:&lt;/p>
&lt;p>$$\ln(\text{CO2})_{it} = \beta_1 \ln(\text{GDP})_{it} + \beta_2 [\ln(\text{GDP})_{it}]^2 + \beta_3 [\ln(\text{GDP})_{it}]^3 + \mathbf{X}_{it}^{\text{true}} \boldsymbol{\gamma} + \alpha_i + \delta_t + \varepsilon_{it}$$&lt;/p>
&lt;p>In words, log CO&lt;sub>2&lt;/sub> depends on a cubic function of log GDP (producing the inverted-N shape), five true control variables $\mathbf{X}^{\text{true}}$, country fixed effects $\alpha_i$, year fixed effects $\delta_t$, and random noise $\varepsilon_{it}$.&lt;/p>
&lt;p>The &lt;strong>answer key&lt;/strong> &amp;mdash; which variables are true predictors and which are noise:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Variable&lt;/th>
&lt;th>Group&lt;/th>
&lt;th>In DGP?&lt;/th>
&lt;th>True coef.&lt;/th>
&lt;th>GDP corr.&lt;/th>
&lt;th>Role&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>fossil_fuel&lt;/code>&lt;/td>
&lt;td>Energy&lt;/td>
&lt;td>&lt;strong>Yes&lt;/strong>&lt;/td>
&lt;td>+0.015&lt;/td>
&lt;td>moderate&lt;/td>
&lt;td>More fossil fuels → more CO&lt;sub>2&lt;/sub>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>renewable&lt;/code>&lt;/td>
&lt;td>Energy&lt;/td>
&lt;td>&lt;strong>Yes&lt;/strong>&lt;/td>
&lt;td>&amp;ndash;0.010&lt;/td>
&lt;td>moderate&lt;/td>
&lt;td>More renewables → less CO&lt;sub>2&lt;/sub>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>urban&lt;/code>&lt;/td>
&lt;td>Socio&lt;/td>
&lt;td>&lt;strong>Yes&lt;/strong>&lt;/td>
&lt;td>+0.007&lt;/td>
&lt;td>moderate&lt;/td>
&lt;td>More urbanization → more CO&lt;sub>2&lt;/sub>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>democracy&lt;/code>&lt;/td>
&lt;td>Institutional&lt;/td>
&lt;td>&lt;strong>Yes&lt;/strong>&lt;/td>
&lt;td>&amp;ndash;0.005&lt;/td>
&lt;td>low&lt;/td>
&lt;td>More democracy → less CO&lt;sub>2&lt;/sub>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>industry&lt;/code>&lt;/td>
&lt;td>Economic&lt;/td>
&lt;td>&lt;strong>Yes&lt;/strong>&lt;/td>
&lt;td>+0.010&lt;/td>
&lt;td>moderate&lt;/td>
&lt;td>More industry → more CO&lt;sub>2&lt;/sub>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>globalization&lt;/code>&lt;/td>
&lt;td>Socio&lt;/td>
&lt;td>No&lt;/td>
&lt;td>0&lt;/td>
&lt;td>&lt;strong>high&lt;/strong>&lt;/td>
&lt;td>Noise &amp;mdash; tricky (correlated with GDP)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>pop_density&lt;/code>&lt;/td>
&lt;td>Socio&lt;/td>
&lt;td>No&lt;/td>
&lt;td>0&lt;/td>
&lt;td>low&lt;/td>
&lt;td>Noise&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>corruption&lt;/code>&lt;/td>
&lt;td>Institutional&lt;/td>
&lt;td>No&lt;/td>
&lt;td>0&lt;/td>
&lt;td>low&lt;/td>
&lt;td>Noise&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>services&lt;/code>&lt;/td>
&lt;td>Economic&lt;/td>
&lt;td>No&lt;/td>
&lt;td>0&lt;/td>
&lt;td>&lt;strong>high&lt;/strong>&lt;/td>
&lt;td>Noise &amp;mdash; tricky (correlated with GDP)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>trade&lt;/code>&lt;/td>
&lt;td>Economic&lt;/td>
&lt;td>No&lt;/td>
&lt;td>0&lt;/td>
&lt;td>moderate&lt;/td>
&lt;td>Noise &amp;mdash; tricky (correlated with GDP)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>fdi&lt;/code>&lt;/td>
&lt;td>Economic&lt;/td>
&lt;td>No&lt;/td>
&lt;td>0&lt;/td>
&lt;td>low&lt;/td>
&lt;td>Noise&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>credit&lt;/code>&lt;/td>
&lt;td>Economic&lt;/td>
&lt;td>No&lt;/td>
&lt;td>0&lt;/td>
&lt;td>moderate&lt;/td>
&lt;td>Noise &amp;mdash; tricky (correlated with GDP)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The &amp;ldquo;GDP corr.&amp;rdquo; column is key to understanding why this problem is non-trivial. Four noise variables (&lt;code>globalization&lt;/code>, &lt;code>services&lt;/code>, &lt;code>trade&lt;/code>, &lt;code>credit&lt;/code>) are deliberately correlated with GDP. A naive regression would find them &amp;ldquo;significant&amp;rdquo; because they piggyback on GDP&amp;rsquo;s true effect. The challenge for BMA and DSL is to see through this correlation and correctly identify that only the 5 true controls belong in the model.&lt;/p>
&lt;p>With the DGP and answer key defined, we now load the synthetic data and set up the Stata environment.&lt;/p>
&lt;h3 id="23-load-the-data">2.3 Load the data&lt;/h3>
&lt;p>The synthetic data is hosted on GitHub for reproducibility. It was generated by &lt;code>generate_data.do&lt;/code> (see the link above).&lt;/p>
&lt;pre>&lt;code class="language-stata">* Load synthetic data from GitHub
import delimited &amp;quot;https://github.com/cmg777/starter-academic-v501/raw/master/content/post/stata_bma_dsl/synthetic_ekc_panel.csv&amp;quot;, clear
xtset country_id year, yearly
&lt;/code>&lt;/pre>
&lt;h3 id="24-define-macros">2.4 Define macros&lt;/h3>
&lt;p>We define all variable groups as global macros &amp;mdash; used in every command throughout the tutorial:&lt;/p>
&lt;pre>&lt;code class="language-stata">global outcome &amp;quot;ln_co2&amp;quot;
global gdp_vars &amp;quot;ln_gdp ln_gdp_sq ln_gdp_cb&amp;quot;
global energy &amp;quot;fossil_fuel renewable&amp;quot;
global socio &amp;quot;urban globalization pop_density&amp;quot;
global inst &amp;quot;democracy corruption&amp;quot;
global econ &amp;quot;industry services trade fdi credit&amp;quot;
global controls &amp;quot;$energy $socio $inst $econ&amp;quot;
global fe &amp;quot;i.country_id i.year&amp;quot;
* Ground truth (for evaluation)
global true_vars &amp;quot;fossil_fuel renewable urban democracy industry&amp;quot;
global noise_vars &amp;quot;globalization pop_density corruption services trade fdi credit&amp;quot;
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-stata">summarize $outcome $gdp_vars $controls
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> Variable | Obs Mean Std. dev. Min Max
-------------+---------------------------------------------------------
ln_co2 | 1,600 -19.0385 .7863276 -21.03685 -16.8315
ln_gdp | 1,600 9.58387 1.329675 6.974263 11.9704
ln_gdp_sq | 1,600 93.6174 25.55106 48.64035 143.2904
ln_gdp_cb | 1,600 931.105 373.829 339.2306 1715.243
fossil_fuel | 1,600 54.7724 19.14168 6.36807 95
renewable | 1,600 29.5413 11.96568 1 64.2207
urban | 1,600 53.6742 14.778 15.95174 91.63234
globalizat~n | 1,600 57.6498 12.71537 26.75758 95
pop_density | 1,600 121.344 210.2646 1 1571.771
democracy | 1,600 2.33346 4.179503 -6.12244 10
corruption | 1,600 52.3523 28.52792 0 100
industry | 1,600 24.6433 6.180478 5.843938 45.32926
services | 1,600 43.5598 9.366089 17.82623 64.07455
trade | 1,600 67.4355 19.36148 10.04306 128.0595
fdi | 1,600 2.98237 4.373857 -11.50437 16.19903
credit | 1,600 53.4402 18.20204 11.32991 123.2399
&lt;/code>&lt;/pre>
&lt;p>The dataset contains 1,600 observations from 80 countries over 20 years (1995&amp;ndash;2014). Log GDP per capita ranges from 6.97 to 11.97, spanning the full income spectrum from about \$1,065 to \$158,000 in synthetic international dollars. Log CO&lt;sub>2&lt;/sub> has a mean of &amp;ndash;19.04 with substantial variation (standard deviation 0.79), reflecting the wide range of development levels in our synthetic panel. With the data loaded, we next visualize the raw income&amp;ndash;pollution relationship.&lt;/p>
&lt;h2 id="3-exploratory-data-analysis">3. Exploratory Data Analysis&lt;/h2>
&lt;p>Before modeling, let us look at the raw relationship between income and emissions.&lt;/p>
&lt;pre>&lt;code class="language-stata">twoway (scatter $outcome ln_gdp, ///
msize(vsmall) mcolor(&amp;quot;106 155 204&amp;quot;%40) msymbol(circle)), ///
ytitle(&amp;quot;Log CO2 per capita&amp;quot;) ///
xtitle(&amp;quot;Log GDP per capita&amp;quot;) ///
title(&amp;quot;Synthetic Data: CO2 vs. Income&amp;quot;, size(medium)) ///
subtitle(&amp;quot;80 countries, 1995-2014 (N = 1,600)&amp;quot;, size(small)) ///
scheme(s2color)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_bma_dsl_fig1_scatter.png" alt="Scatter plot of log CO2 per capita versus log GDP per capita for 80 synthetic countries. The cloud of points shows a clear nonlinear pattern consistent with the inverted-N EKC shape.">&lt;/p>
&lt;p>The scatter reveals a distinctly nonlinear pattern. At low income levels, CO&lt;sub>2&lt;/sub> emissions increase steeply with GDP. At higher income levels, the relationship flattens and bends. This curvature motivates the cubic EKC specification. The diagram below shows the two competing EKC shapes &amp;mdash; the classic inverted-U (quadratic) and the more complex inverted-N (cubic) with its three distinct phases:&lt;/p>
&lt;pre>&lt;code class="language-mermaid">graph TD
EKC[&amp;quot;&amp;lt;b&amp;gt;Environmental Kuznets Curve&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;How does pollution change&amp;lt;br/&amp;gt;as income grows?&amp;quot;]
EKC --&amp;gt; IU[&amp;quot;&amp;lt;b&amp;gt;Inverted-U&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Quadratic: β₁ &amp;gt; 0, β₂ &amp;lt; 0&amp;lt;br/&amp;gt;One turning point&amp;quot;]
EKC --&amp;gt; IN[&amp;quot;&amp;lt;b&amp;gt;Inverted-N&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Cubic: β₁ &amp;lt; 0, β₂ &amp;gt; 0, β₃ &amp;lt; 0&amp;lt;br/&amp;gt;Two turning points&amp;quot;]
IN --&amp;gt; P1[&amp;quot;&amp;lt;b&amp;gt;Phase 1: Declining&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Very poor countries&amp;quot;]
IN --&amp;gt; P2[&amp;quot;&amp;lt;b&amp;gt;Phase 2: Rising&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Industrializing countries&amp;quot;]
IN --&amp;gt; P3[&amp;quot;&amp;lt;b&amp;gt;Phase 3: Declining&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Wealthy countries&amp;quot;]
style EKC fill:#141413,stroke:#141413,color:#fff
style IU fill:#6a9bcc,stroke:#141413,color:#fff
style IN fill:#d97757,stroke:#141413,color:#fff
style P1 fill:#00d4c8,stroke:#141413,color:#141413
style P2 fill:#d97757,stroke:#141413,color:#fff
style P3 fill:#00d4c8,stroke:#141413,color:#141413
&lt;/code>&lt;/pre>
&lt;p>For an inverted-N, we need $\beta_1 &amp;lt; 0$, $\beta_2 &amp;gt; 0$, $\beta_3 &amp;lt; 0$. Our synthetic DGP was designed with exactly this sign pattern ($\beta_1 = -7.1$, $\beta_2 = 0.81$, $\beta_3 = -0.03$), so BMA and DSL should recover it &amp;mdash; but can they also correctly identify which of the 12 controls truly matter? Let us start with standard panel regressions to see how sensitive the GDP coefficients are to the choice of controls.&lt;/p>
&lt;h2 id="4-baseline-----standard-fixed-effects">4. Baseline &amp;mdash; Standard Fixed Effects&lt;/h2>
&lt;p>Before reaching for sophisticated methods, let us see what standard panel regressions say. We run two specifications using macros:&lt;/p>
&lt;h3 id="41-sparse-specification">4.1 Sparse specification&lt;/h3>
&lt;pre>&lt;code class="language-stata">reghdfe $outcome $gdp_vars, absorb(country_id year) vce(cluster country_id)
estimates store fe_sparse
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">HDFE Linear regression Number of obs = 1,600
R-squared = 0.9620
Within R-sq. = 0.0354
Number of clusters (country_id) = 80
(Std. err. adjusted for 80 clusters in country_id)
------------------------------------------------------------------------------
| Robust
ln_co2 | Coefficient std. err. t P&amp;gt;|t| [95% conf. interval]
-------------+----------------------------------------------------------------
ln_gdp | -7.498046 1.623988 -4.62 0.000 -10.73051 -4.26558
ln_gdp_sq | .848967 .1704533 4.98 0.000 .5096881 1.188246
ln_gdp_cb | -.0314993 .005931 -5.31 0.000 -.0433047 -.019694
------------------------------------------------------------------------------
&lt;/code>&lt;/pre>
&lt;p>The sparse model finds the inverted-N sign pattern ($\beta_1 &amp;lt; 0$, $\beta_2 &amp;gt; 0$, $\beta_3 &amp;lt; 0$), all significant at the 0.1% level with cluster-robust standard errors (clustered at the country level). The within R² is just 0.035 &amp;mdash; the GDP polynomial alone explains only about 3.5% of within-country CO&lt;sub>2&lt;/sub> variation after absorbing country and year fixed effects. The overall R² of 0.96 is high because the country fixed effects capture most of the variation.&lt;/p>
&lt;h3 id="42-kitchen-sink-specification">4.2 Kitchen-sink specification&lt;/h3>
&lt;pre>&lt;code class="language-stata">reghdfe $outcome $gdp_vars $controls, absorb(country_id year) vce(cluster country_id)
estimates store fe_kitchen
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">HDFE Linear regression Number of obs = 1,600
R-squared = 0.9655
Within R-sq. = 0.1249
Number of clusters (country_id) = 80
(Std. err. adjusted for 80 clusters in country_id)
------------------------------------------------------------------------------
| Robust
ln_co2 | Coefficient std. err. t P&amp;gt;|t| [95% conf. interval]
-------------+----------------------------------------------------------------
ln_gdp | -7.130693 1.562581 -4.56 0.000 -10.24093 -4.020453
ln_gdp_sq | .8059928 .1647973 4.89 0.000 .477972 1.134014
ln_gdp_cb | -.0298133 .0057365 -5.20 0.000 -.0412314 -.0183951
fossil_fuel | .0138444 .0014853 9.32 0.000 .010888 .0168008
renewable | -.006795 .0019322 -3.52 0.001 -.0106409 -.0029491
urban | .0057534 .0021432 2.68 0.009 .0014875 .0100192
globalizat~n | .0015186 .0012832 1.18 0.240 -.0010357 .0040728
pop_density | .0000794 .0002303 0.34 0.731 -.000379 .0005378
democracy | -.0002971 .007735 -0.04 0.969 -.0156933 .0150991
corruption | .0009812 .0008415 1.17 0.247 -.0006936 .0026561
industry | .0086336 .0017848 4.84 0.000 .0050811 .0121861
services | -.0005642 .0017205 -0.33 0.744 -.0039889 .0028604
trade | -.0002458 .0007695 -0.32 0.750 -.0017774 .0012858
fdi | -.0017599 .0019509 -0.90 0.370 -.005643 .0021232
credit | -.00139 .0007516 -1.85 0.068 -.002886 .0001061
------------------------------------------------------------------------------
&lt;/code>&lt;/pre>
&lt;p>Adding all 12 controls raises the within R² from 0.035 to 0.125 &amp;mdash; a meaningful improvement, though the country and year FE still dominate the overall explanatory power (R² = 0.966). The three strongest true predictors (fossil fuel, industry, urban) are clearly significant, while most noise variables are statistically insignificant. Democracy&amp;rsquo;s estimate (&amp;ndash;0.0003, p = 0.97) is far from its true value (&amp;ndash;0.005) and indistinguishable from zero &amp;mdash; illustrating why weak signals are hard to detect even with the correct model.&lt;/p>
&lt;p>The critical question is: which specification should we trust? The next subsection shows that the GDP coefficients &amp;mdash; and hence the EKC shape &amp;mdash; shift depending on which controls we include.&lt;/p>
&lt;h3 id="43-the-model-uncertainty-problem">4.3 The model uncertainty problem&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Coefficient&lt;/th>
&lt;th>Sparse FE&lt;/th>
&lt;th>Kitchen-Sink FE&lt;/th>
&lt;th>True DGP&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>$\beta_1$ (GDP)&lt;/td>
&lt;td>&amp;ndash;7.498&lt;/td>
&lt;td>&amp;ndash;7.131&lt;/td>
&lt;td>&amp;ndash;7.100&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>$\beta_2$ (GDP²)&lt;/td>
&lt;td>0.849&lt;/td>
&lt;td>0.806&lt;/td>
&lt;td>0.810&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>$\beta_3$ (GDP³)&lt;/td>
&lt;td>&amp;ndash;0.031&lt;/td>
&lt;td>&amp;ndash;0.030&lt;/td>
&lt;td>&amp;ndash;0.030&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Both specifications recover the correct sign pattern, but the magnitudes shift. The kitchen-sink FE estimates (&amp;ndash;7.131, 0.806, &amp;ndash;0.030) are closer to the true DGP values (&amp;ndash;7.100, 0.810, &amp;ndash;0.030) than the sparse FE (&amp;ndash;7.498, 0.849, &amp;ndash;0.031), because the omitted true controls create bias in the sparse model. But which of the 12 controls actually belongs?&lt;/p>
&lt;pre>&lt;code class="language-stata">* Compare coefficients side by side (simplified from analysis.do)
graph twoway ///
(bar value order if spec == &amp;quot;Sparse FE&amp;quot;, ///
barwidth(0.35) color(&amp;quot;106 155 204&amp;quot;)) ///
(bar value order if spec == &amp;quot;Kitchen-Sink FE&amp;quot;, ///
barwidth(0.35) color(&amp;quot;217 119 87&amp;quot;)), ///
xlabel(1 `&amp;quot;&amp;quot;b1&amp;quot; &amp;quot;(GDP)&amp;quot;&amp;quot;' 2 `&amp;quot;&amp;quot;b2&amp;quot; &amp;quot;(GDP sq)&amp;quot;&amp;quot;' 3 `&amp;quot;&amp;quot;b3&amp;quot; &amp;quot;(GDP cb)&amp;quot;&amp;quot;' ///
4 `&amp;quot;&amp;quot;b1&amp;quot; &amp;quot;(GDP)&amp;quot;&amp;quot;' 5 `&amp;quot;&amp;quot;b2&amp;quot; &amp;quot;(GDP sq)&amp;quot;&amp;quot;' 6 `&amp;quot;&amp;quot;b3&amp;quot; &amp;quot;(GDP cb)&amp;quot;&amp;quot;') ///
xline(3.5, lcolor(gs10) lpattern(dash)) ///
ytitle(&amp;quot;Coefficient value&amp;quot;) ///
title(&amp;quot;Coefficient Instability Across Specifications&amp;quot;) ///
legend(order(1 &amp;quot;Sparse FE (no controls)&amp;quot; 2 &amp;quot;Kitchen-Sink FE (all 12 controls)&amp;quot;) ///
rows(1) position(6)) ///
scheme(s2color)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_bma_dsl_fig2_instability.png" alt="Bar chart comparing GDP polynomial coefficients between sparse and kitchen-sink fixed effects specifications. The coefficients shift between the two models, demonstrating model uncertainty.">&lt;/p>
&lt;p>To understand the practical implications of these coefficient shifts, we compute the income thresholds where emissions change direction. The &lt;strong>turning points&lt;/strong> are found by setting the first derivative of the cubic to zero:&lt;/p>
&lt;p>$$x^* = \frac{-\hat{\beta}_2 \pm \sqrt{\hat{\beta}_2^2 - 3\hat{\beta}_1\hat{\beta}_3}}{3\hat{\beta}_3}, \quad \text{GDP}^* = \exp(x^*)$$&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Turning point&lt;/th>
&lt;th>Sparse FE&lt;/th>
&lt;th>Kitchen-Sink FE&lt;/th>
&lt;th>True DGP&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Minimum (CO&lt;sub>2&lt;/sub> starts rising)&lt;/td>
&lt;td>\$2,478&lt;/td>
&lt;td>\$2,426&lt;/td>
&lt;td>\$1,895&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Maximum (CO&lt;sub>2&lt;/sub> starts falling)&lt;/td>
&lt;td>\$25,656&lt;/td>
&lt;td>\$27,694&lt;/td>
&lt;td>\$34,647&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The turning points shift modestly between specifications &amp;mdash; the minimum stays near \$2,400&amp;ndash;\$2,500 while the maximum moves from \$25,656 to \$27,694 depending on controls. Neither matches the true DGP values perfectly, motivating BMA and DSL as principled alternatives to ad hoc control selection.&lt;/p>
&lt;h2 id="5-bayesian-model-averaging">5. Bayesian Model Averaging&lt;/h2>
&lt;h3 id="51-the-idea">5.1 The idea&lt;/h3>
&lt;p>Think of BMA as betting on a horse race. Instead of putting all your money on one model, BMA spreads bets across the field, wagering more on models with better track records.&lt;/p>
&lt;pre>&lt;code class="language-mermaid">graph TD
Start[&amp;quot;&amp;lt;b&amp;gt;12 Candidate Controls&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;2¹² = 4,096&amp;lt;br/&amp;gt;possible models&amp;quot;] --&amp;gt; MCMC[&amp;quot;&amp;lt;b&amp;gt;MCMC Sampling&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Draw 50,000 models&amp;quot;]
MCMC --&amp;gt; Post[&amp;quot;&amp;lt;b&amp;gt;Posterior Probability&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Weight by fit × parsimony&amp;quot;]
Post --&amp;gt; Avg[&amp;quot;&amp;lt;b&amp;gt;Weighted Average&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Coefficients averaged&amp;lt;br/&amp;gt;across models&amp;quot;]
Post --&amp;gt; PIP[&amp;quot;&amp;lt;b&amp;gt;PIPs&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Inclusion probability&amp;lt;br/&amp;gt;for each variable&amp;quot;]
style Start fill:#141413,stroke:#141413,color:#fff
style MCMC fill:#6a9bcc,stroke:#141413,color:#fff
style Post fill:#d97757,stroke:#141413,color:#fff
style Avg fill:#00d4c8,stroke:#141413,color:#141413
style PIP fill:#00d4c8,stroke:#141413,color:#141413
&lt;/code>&lt;/pre>
&lt;p>Formally, this betting process follows Bayes' rule, which tells us how to weight models by their fit and complexity.&lt;/p>
&lt;p>&lt;strong>Step 1: Model posterior probabilities.&lt;/strong> The posterior probability of model $M_k$ is:&lt;/p>
&lt;p>$$P(M_k | \text{data}) = \frac{P(\text{data} | M_k) \cdot P(M_k)}{\sum_{l=1}^{K} P(\text{data} | M_l) \cdot P(M_l)}$$&lt;/p>
&lt;p>In words, the probability of model $k$ being correct equals how well it fits the data (the &lt;em>marginal likelihood&lt;/em> $P(\text{data} | M_k)$) times our prior belief ($P(M_k)$), divided by the total across all models. Models that fit the data well &lt;em>and&lt;/em> are parsimonious receive higher posterior weight &amp;mdash; this is BMA&amp;rsquo;s built-in Occam&amp;rsquo;s razor.&lt;/p>
&lt;p>The marginal likelihood $P(\text{data} | M_k)$ is not the same as the ordinary likelihood. It integrates over all possible coefficient values, penalizing models with many parameters that &amp;ldquo;waste&amp;rdquo; probability mass on parameter regions the data does not support:&lt;/p>
&lt;p>$$P(\text{data} | M_k) = \int P(\text{data} | \boldsymbol{\beta}_k, M_k) \, P(\boldsymbol{\beta}_k | M_k) \, d\boldsymbol{\beta}_k$$&lt;/p>
&lt;p>In words, the marginal likelihood asks: &amp;ldquo;If we averaged this model&amp;rsquo;s fit across all plausible coefficient values (weighted by the prior $P(\boldsymbol{\beta}_k | M_k)$), how well does it explain the data?&amp;rdquo; This integral is what makes BMA automatically penalize overly complex models &amp;mdash; a model with many parameters spreads its prior probability thinly across a high-dimensional space, and only recovers that probability if the data strongly supports those extra dimensions.&lt;/p>
&lt;p>&lt;strong>Step 2: Posterior Inclusion Probabilities.&lt;/strong> The &lt;strong>PIP&lt;/strong> for variable $j$ sums the posterior probabilities across all models that include it:&lt;/p>
&lt;p>$$\text{PIP}_j = \sum_{k:\, x_j \in M_k} P(M_k | \text{data})$$&lt;/p>
&lt;p>In words, PIP answers: &amp;ldquo;Across all the models BMA considered, what fraction of the total posterior weight belongs to models that include variable $j$?&amp;rdquo; If fossil fuel appears in every high-probability model, its PIP approaches 1.0. If democracy only appears in low-probability models, its PIP stays near 0.&lt;/p>
&lt;p>&lt;strong>Step 3: BMA posterior mean.&lt;/strong> BMA does not just select variables &amp;mdash; it also produces model-averaged coefficient estimates. The posterior mean of coefficient $\beta_j$ averages across all models, weighted by their posterior probabilities:&lt;/p>
&lt;p>$$\hat{\beta}_j^{\text{BMA}} = \sum_{k=1}^{K} P(M_k | \text{data}) \cdot \hat{\beta}_{j,k}$$&lt;/p>
&lt;p>where $\hat{\beta}_{j,k}$ is the coefficient estimate of variable $j$ in model $M_k$ (set to zero if $j$ is not in $M_k$). In words, the BMA estimate is a weighted average of the coefficient across all models, including models where the variable is absent (contributing zero). This shrinks the coefficient toward zero in proportion to the evidence against inclusion &amp;mdash; a variable with PIP = 0.5 has its BMA coefficient shrunk by roughly half compared to its conditional estimate.&lt;/p>
&lt;p>Think of PIP as a &lt;strong>democratic vote&lt;/strong> across all candidate models. Each model casts a weighted vote for which variables matter, with better-fitting models getting louder voices. &lt;a href="https://doi.org/10.2307/271063" target="_blank" rel="noopener">Raftery (1995)&lt;/a> proposed standard interpretation thresholds based on the strength of evidence:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>PIP range&lt;/th>
&lt;th>Evidence&lt;/th>
&lt;th>Analogy&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>$\geq 0.99$&lt;/td>
&lt;td>Decisive&lt;/td>
&lt;td>Beyond reasonable doubt&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>$0.95 - 0.99$&lt;/td>
&lt;td>Very strong&lt;/td>
&lt;td>Strong consensus&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>$0.80 - 0.95$&lt;/td>
&lt;td>Strong (robust)&lt;/td>
&lt;td>Clear majority&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>$0.50 - 0.80$&lt;/td>
&lt;td>Borderline&lt;/td>
&lt;td>Split vote&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>$&amp;lt; 0.50$&lt;/td>
&lt;td>Weak/none (fragile)&lt;/td>
&lt;td>Minority opinion&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>We use &lt;strong>PIP $\geq$ 0.80&lt;/strong> as our robustness threshold throughout this tutorial &amp;mdash; a variable with PIP above 0.80 appears in the vast majority of the probability-weighted model space, providing &amp;ldquo;strong evidence&amp;rdquo; by Raftery&amp;rsquo;s classification. This is the most widely used cutoff in applied BMA studies.&lt;/p>
&lt;p>A key assumption underlying BMA is that the true data-generating process is well-approximated by a weighted combination of the candidate models (the &amp;ldquo;M-closed&amp;rdquo; assumption). When the candidate set omits important functional forms or interactions, BMA&amp;rsquo;s posterior probabilities may be unreliable.&lt;/p>
&lt;h3 id="52-key-options">5.2 Key options&lt;/h3>
&lt;p>With the conceptual framework in place, we now turn to implementation. Stata 18&amp;rsquo;s &lt;a href="https://www.stata.com/manuals/bmabmaregress.pdf" target="_blank" rel="noopener">&lt;code>bmaregress&lt;/code>&lt;/a> command has three families of options: &lt;strong>priors&lt;/strong> (what you believe before seeing the data), &lt;strong>MCMC controls&lt;/strong> (how the algorithm explores the model space), and &lt;strong>output formatting&lt;/strong> (what gets displayed). The full option list is in the &lt;a href="https://www.stata.com/manuals/bmabmaregress.pdf" target="_blank" rel="noopener">Stata manual&lt;/a>; here we explain the ones used in this tutorial:&lt;/p>
&lt;p>&lt;strong>Prior specifications&lt;/strong> (see &lt;a href="https://www.stata.com/manuals/bmabmaregresspostestimation.pdf" target="_blank" rel="noopener">&lt;code>bmaregress&lt;/code> priors&lt;/a> for alternatives):&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://www.stata.com/manuals/bmabmaregress.pdf" target="_blank" rel="noopener">&lt;code>gprior(uip)&lt;/code>&lt;/a>&lt;/strong> &amp;mdash; Unit Information Prior: sets the prior precision on coefficients equal to the information in one observation ($g = N$). This is a standard, relatively uninformative choice that lets the data dominate. Alternatives include &lt;code>gprior(bric)&lt;/code> (benchmark risk inflation criterion, $g = \max(N, p^2)$), &lt;code>gprior(zs)&lt;/code> (Zellner-Siow), and &lt;code>gprior(hyper)&lt;/code> (hyper-g prior with data-driven $g$)&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://www.stata.com/manuals/bmabmaregress.pdf" target="_blank" rel="noopener">&lt;code>mprior(uniform)&lt;/code>&lt;/a>&lt;/strong> &amp;mdash; all $2^{12} = 4{,}096$ models are equally likely a priori; no model is privileged before seeing the data. The alternative &lt;code>mprior(binomial)&lt;/code> applies a beta-binomial prior that penalizes very large or very small models, often producing more conservative PIPs&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>MCMC controls:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>mcmcsize(50000)&lt;/code>&lt;/strong> &amp;mdash; draws 50,000 models from the model space using MC$^3$ (Markov chain Monte Carlo model composition) sampling. Larger values improve posterior estimates but increase computation time&lt;/li>
&lt;li>&lt;strong>&lt;code>burnin(5000)&lt;/code>&lt;/strong> &amp;mdash; discards the first 5,000 draws to allow the chain to reach its stationary distribution before collecting samples&lt;/li>
&lt;li>&lt;strong>&lt;code>rseed(9988)&lt;/code>&lt;/strong> &amp;mdash; fixes the random number seed for exact reproducibility. Students running the same command will get identical results&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://www.stata.com/manuals/bmabmaregress.pdf" target="_blank" rel="noopener">&lt;code>groupfv&lt;/code>&lt;/a>&lt;/strong> &amp;mdash; treats all dummies from a single factor variable as one group that enters or exits models together. Without &lt;code>groupfv&lt;/code>, writing &lt;code>i.country_id&lt;/code> would create 80 individual dummy variables, and BMA would consider including or excluding each one independently &amp;mdash; producing an astronomical model space ($2^{80}$ combinations of country dummies alone) that is both computationally infeasible and conceptually meaningless. With &lt;code>groupfv&lt;/code>, the 80 country dummies move as a &lt;em>package&lt;/em>: either all 80 are in the model or none are. Think of it like hiring a sports team &amp;mdash; you recruit the whole roster, not individual players one by one. In the output, this is why you see &amp;ldquo;Groups = 15&amp;rdquo; instead of 113: BMA treats the 80 country dummies as 1 group, the 19 year dummies as 1 group, and each of the 12 candidate controls + 3 GDP terms as their own groups ($1 + 1 + 15 = 17$, minus 2 that are &amp;ldquo;always&amp;rdquo; included = 15 groups subject to selection)&lt;/li>
&lt;li>&lt;strong>&lt;code>($fe, always)&lt;/code>&lt;/strong> &amp;mdash; country and year fixed effects are always included in every model; they are not subject to model selection. This is standard practice in panel data BMA: we want to control for unobserved country and time heterogeneity in &lt;em>every&lt;/em> model, and only let BMA decide about the candidate controls&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Output formatting:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>pipcutoff(0.8)&lt;/code>&lt;/strong> &amp;mdash; display only variables with PIP above 0.80 in the output table. This is a &lt;em>display&lt;/em> threshold only &amp;mdash; it does not affect the underlying estimation&lt;/li>
&lt;li>&lt;strong>&lt;code>inputorder&lt;/code>&lt;/strong> &amp;mdash; display variables in the order they were specified in the command, rather than sorted by PIP&lt;/li>
&lt;/ul>
&lt;h3 id="53-estimation">5.3 Estimation&lt;/h3>
&lt;pre>&lt;code class="language-stata">bmaregress $outcome $gdp_vars $controls ///
($fe, always), ///
mprior(uniform) groupfv gprior(uip) ///
mcmcsize(50000) rseed(9988) inputorder pipcutoff(0.8)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Bayesian model averaging No. of obs = 1,600
Linear regression No. of predictors = 113
MC3 sampling Groups = 15
Always = 98
No. of models = 163
Priors: Mean model size = 104.578
Models: Uniform MCMC sample size = 50,000
Coef.: Zellner's g Acceptance rate = 0.0904
g: Unit-information, g = 1,600 Shrinkage, g/(1+g) = 0.9994
Sampling correlation = 0.9997
------------------------------------------------------------------------------
ln_co2 | Mean Std. dev. Group PIP
-------------+----------------------------------------------------------------
ln_gdp | -7.13901 1.811093 1 .99401
ln_gdp_sq | .8078437 .1892418 2 .99991
ln_gdp_cb | -.0299182 .0065105 3 .99976
fossil_fuel | .0138139 .001283 4 1
renewable | -.0068332 .0023506 5 .95945
industry | .0085503 .0019766 11 .99867
------------------------------------------------------------------------------
Note: 9 predictors with PIP less than .8 not shown.
&lt;/code>&lt;/pre>
&lt;blockquote>
&lt;p>The Stata output says &amp;ldquo;PIP less than .8&amp;rdquo; because we set &lt;code>pipcutoff(0.8)&lt;/code> as the display threshold &amp;mdash; only variables exceeding this stricter robustness criterion appear in the table. The 9 hidden variables are the two weak true controls (urban, democracy) and all 7 noise variables (services, trade, FDI, credit, population density, corruption, globalization). Figure 3 below shows PIP values for all 15 variables.&lt;/p>
&lt;/blockquote>
&lt;p>The output shows 113 predictors in 15 groups: the 80 country dummies (grouped as 1 by &lt;code>groupfv&lt;/code>) + 19 year dummies (grouped as 1) + 12 candidate controls (each its own group) + the 3 GDP terms (each its own group) = 15 selection groups total, with 98 variables &amp;ldquo;always&amp;rdquo; included (the country and year FE). BMA sampled 163 distinct models out of 4,096 possible. This might seem low, but the MC$^3$ algorithm does not need to visit every model &amp;mdash; it concentrates on the high-posterior-probability region. The sampling correlation of 0.9997 (very close to 1.0) confirms that the MC$^3$ chain adequately explored the model space &amp;mdash; the posterior probability is concentrated on a relatively small number of high-quality models. The acceptance rate of 0.09 is below the typical 20&amp;ndash;40% range, but the high sampling correlation provides reassurance that the results are reliable. Six variables have PIP above the 0.80 robustness threshold: the three GDP terms (PIP = 0.994&amp;ndash;1.000) and three of the five true controls &amp;mdash; fossil fuel (PIP = 1.000), industry (PIP = 0.999), and renewable energy (PIP = 0.959). The BMA posterior means (&amp;ndash;7.139, 0.808, &amp;ndash;0.030) are remarkably close to the true DGP values (&amp;ndash;7.100, 0.810, &amp;ndash;0.030), substantially closer than the sparse FE estimates.&lt;/p>
&lt;p>Two true controls &amp;mdash; urban (coefficient 0.007) and democracy (coefficient &amp;ndash;0.005) &amp;mdash; have PIPs well below 0.80. Their true effects are small, making them hard to distinguish from noise. This is a realistic limitation: even a powerful method like BMA struggles with weak signals.&lt;/p>
&lt;h3 id="54-turning-points">5.4 Turning points&lt;/h3>
&lt;p>Using the BMA posterior means, the turning points are:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Minimum:&lt;/strong> \$2,411 GDP per capita (true: \$1,895)&lt;/li>
&lt;li>&lt;strong>Maximum:&lt;/strong> \$27,269 GDP per capita (true: \$34,647)&lt;/li>
&lt;/ul>
&lt;p>Both turning points are in the right ballpark but not exact. The turning point formula amplifies small differences across all three coefficients &amp;mdash; even though each BMA posterior mean is within 1% of the true DGP value, the compound effect shifts the maximum turning point from \$34,647 (true) to \$27,269 (BMA). The inverted-N shape is clearly recovered.&lt;/p>
&lt;h3 id="55-posterior-inclusion-probabilities">5.5 Posterior Inclusion Probabilities&lt;/h3>
&lt;p>The PIP chart is BMA&amp;rsquo;s signature output. We extract PIPs from the estimation results, label each variable, and color-code bars by ground truth: steel blue for true predictors, gray for noise.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Extract PIPs and create a horizontal bar chart
matrix pip_mat = e(pip)
* ... (create dataset of variable names and PIPs, add readable labels) ...
* Mark true vs noise predictors
gen is_true = inlist(varname, &amp;quot;fossil_fuel&amp;quot;, &amp;quot;renewable&amp;quot;, &amp;quot;urban&amp;quot;, ///
&amp;quot;democracy&amp;quot;, &amp;quot;industry&amp;quot;, &amp;quot;ln_gdp&amp;quot;, &amp;quot;ln_gdp_sq&amp;quot;, &amp;quot;ln_gdp_cb&amp;quot;)
gsort -pip
graph twoway ///
(bar pip order if is_true == 1, horizontal barwidth(0.6) ///
color(&amp;quot;106 155 204&amp;quot;)) ///
(bar pip order if is_true == 0, horizontal barwidth(0.6) ///
color(gs11)), ///
xline(0.8, lcolor(&amp;quot;217 119 87&amp;quot;) lpattern(dash) lwidth(medium)) ///
ylabel(1(1)15, valuelabel angle(0) labsize(small)) ///
xlabel(0(0.2)1, format(%3.1f)) ///
xtitle(&amp;quot;Posterior Inclusion Probability (PIP)&amp;quot;) ///
title(&amp;quot;BMA: Which Variables Matter?&amp;quot;) ///
legend(order(1 &amp;quot;True predictor (in DGP)&amp;quot; 2 &amp;quot;Noise variable (not in DGP)&amp;quot;) ///
rows(1) position(6)) ///
scheme(s2color)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_bma_dsl_fig3_pip.png" alt="Horizontal bar chart showing Posterior Inclusion Probabilities for all 15 variables. True predictors are colored in steel blue, noise variables in gray. A dashed orange line marks the 0.80 robustness threshold.">&lt;/p>
&lt;p>The PIP chart cleanly separates the variables into two groups. At the top (PIP near 1.0): fossil fuel share, GDP terms, industry, and renewable energy &amp;mdash; all true predictors correctly identified. At the bottom (PIP near 0.0): the seven noise variables (globalization, corruption, services, trade, FDI, credit, population density) plus urban population and democracy. BMA correctly assigns zero-like PIPs to all noise variables, and correctly flags 3 of 5 true predictors as robust. The two misses (urban, democracy) have small true coefficients (0.007 and &amp;ndash;0.005), making them genuinely hard to detect.&lt;/p>
&lt;h3 id="56-coefficient-density-plots">5.6 Coefficient density plots&lt;/h3>
&lt;p>The &lt;a href="https://www.stata.com/manuals/bmabmagraphcoefdensity.pdf" target="_blank" rel="noopener">&lt;code>bmagraph coefdensity&lt;/code>&lt;/a> command shows the posterior distribution of each coefficient across all sampled models. We plot all six variables with PIP above 0.80 in a 3x2 grid &amp;mdash; the three GDP polynomial terms (top row) and the three robust controls (bottom row). In each panel, the blue curve shows the density conditional on the variable being included in the model, and the red horizontal line shows the probability of noninclusion (1 &amp;ndash; PIP). When the red line is flat near zero and the blue curve is far from zero, the variable is strongly supported.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Consistent formatting for all panels
local panel_opts `&amp;quot; xtitle(&amp;quot;Coefficient value&amp;quot;, size(vsmall)) &amp;quot;'
local panel_opts `&amp;quot; `panel_opts' ytitle(&amp;quot;Density&amp;quot;, size(vsmall)) &amp;quot;'
local panel_opts `&amp;quot; `panel_opts' ylabel(, labsize(vsmall) angle(0)) &amp;quot;'
local panel_opts `&amp;quot; `panel_opts' xlabel(, labsize(vsmall)) &amp;quot;'
local panel_opts `&amp;quot; `panel_opts' legend(off) scheme(s2color) &amp;quot;'
* Generate density for all 6 robust variables (PIP &amp;gt; 0.80)
bmagraph coefdensity ln_gdp, title(&amp;quot;GDP per capita (log)&amp;quot;, size(small)) `panel_opts' name(dens_gdp, replace)
bmagraph coefdensity ln_gdp_sq, title(&amp;quot;GDP squared (log)&amp;quot;, size(small)) `panel_opts' name(dens_gdp_sq, replace)
bmagraph coefdensity ln_gdp_cb, title(&amp;quot;GDP cubed (log)&amp;quot;, size(small)) `panel_opts' name(dens_gdp_cb, replace)
bmagraph coefdensity fossil_fuel, title(&amp;quot;Fossil fuel share (%)&amp;quot;, size(small)) `panel_opts' name(dens_fossil, replace)
bmagraph coefdensity renewable, title(&amp;quot;Renewable energy (%)&amp;quot;, size(small)) `panel_opts' name(dens_renewable, replace)
bmagraph coefdensity industry, title(&amp;quot;Industry VA (% GDP)&amp;quot;, size(small)) `panel_opts' name(dens_industry, replace)
graph combine dens_gdp dens_gdp_sq dens_gdp_cb ///
dens_fossil dens_renewable dens_industry, ///
cols(3) rows(2) imargin(small) ///
title(&amp;quot;BMA: Posterior Coefficient Densities&amp;quot;, size(medsmall)) ///
subtitle(&amp;quot;All 6 robust variables (PIP &amp;gt; 0.80)&amp;quot;, size(small)) ///
note(&amp;quot;Blue curve = posterior density conditional on inclusion.&amp;quot; ///
&amp;quot;Red line = probability of noninclusion (1 - PIP).&amp;quot; ///
&amp;quot;Near-zero red line + blue curve far from zero = strong evidence.&amp;quot;, size(vsmall)) ///
scheme(s2color) xsize(12) ysize(7)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_bma_dsl_fig4_coefdensity.png" alt="Posterior coefficient density plots for all six robust variables in a 3x2 grid. Top row: GDP linear, squared, and cubic terms. Bottom row: fossil fuel, renewable energy, and industry. All densities are concentrated well away from zero.">&lt;/p>
&lt;p>All six densities are concentrated well away from zero, confirming that every variable with PIP above 0.80 has a genuinely non-zero effect. The three GDP terms (top row) form the inverted-N polynomial: the linear term is centered near &amp;ndash;7.1 (true: &amp;ndash;7.1), the squared term near +0.81 (true: +0.81), and the cubic term near &amp;ndash;0.030 (true: &amp;ndash;0.030). The three controls (bottom row) show tight, unimodal densities: fossil fuel near +0.014 (true: +0.015), renewable energy near &amp;ndash;0.007 (true: &amp;ndash;0.010), and industry near +0.009 (true: +0.010). Renewable energy&amp;rsquo;s posterior mean (&amp;ndash;0.007) is slightly attenuated compared to the true value (&amp;ndash;0.010), reflecting the BMA shrinkage that occurs when a variable&amp;rsquo;s PIP is below 1.0 &amp;mdash; models that exclude it pull the average toward zero.&lt;/p>
&lt;h3 id="57-pooled-bma-without-fixed-effects">5.7 Pooled BMA (without fixed effects)&lt;/h3>
&lt;p>To parallel the pooled DSL comparison in Section 6.6, we also run BMA without country or year fixed effects &amp;mdash; treating the panel as a pooled cross-section. This removes the &lt;code>($fe, always)&lt;/code> and &lt;code>groupfv&lt;/code> options, leaving only the 12 candidate controls and 3 GDP terms as predictors (15 total, vs 113 with FE).&lt;/p>
&lt;pre>&lt;code class="language-stata">* BMA without FE -- pooled cross-section
bmaregress ln_co2 ln_gdp ln_gdp_sq ln_gdp_cb ///
fossil_fuel renewable urban industry democracy ///
services trade fdi credit pop_density ///
corruption globalization, ///
mprior(uniform) gprior(uip) ///
mcmcsize(50000) rseed(9988) pipcutoff(0.5) burnin(5000)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Bayesian model averaging No. of obs = 1,600
Linear regression No. of predictors = 15
MC3 sampling Groups = 15
Always = 0
No. of models = 34
Priors: Mean model size = 11.978
Models: Uniform MCMC sample size = 50,000
Coef.: Zellner's g Acceptance rate = 0.0733
g: Unit-information, g = 1,600 Shrinkage, g/(1+g) = 0.9994
Sampling correlation = 0.9996
------------------------------------------------------------------------------
ln_co2 | Mean Std. dev. Group PIP
-------------+----------------------------------------------------------------
ln_gdp | -21.25807 1.641676 1 1
ln_gdp_sq | 2.284729 .1748838 2 1
ln_gdp_cb | -.0813937 .0061308 3 1
fossil_fuel | .0188853 .0010554 4 1
renewable | -.0192089 .0013911 5 1
urban | .0103139 .0012072 6 1
industry | .0138361 .0023478 7 1
services | .0164633 .0016573 9 1
pop_density | -.0004314 .0000567 13 1
credit | .0041017 .0008414 12 .99984
trade | -.0020939 .001084 10 .86009
democracy | .007879 .0042984 8 .84142
------------------------------------------------------------------------------
Note: 3 predictors with PIP less than .5 not shown.
&lt;/code>&lt;/pre>
&lt;p>The pooled BMA results are striking in two ways. First, the GDP coefficients are severely biased &amp;mdash; the same pattern as pooled DSL: $\beta_1 = -21.26$ (true: &amp;ndash;7.10), $\beta_2 = 2.28$ (true: 0.81), $\beta_3 = -0.081$ (true: &amp;ndash;0.03). Without country fixed effects, the GDP terms absorb persistent cross-country differences in emissions levels, inflating the coefficients by a factor of 2&amp;ndash;3x.&lt;/p>
&lt;p>Second, the PIPs tell a completely different story than with FE. Without fixed effects, &lt;strong>12 of 15 variables have PIP above 0.80&lt;/strong> &amp;mdash; including noise variables like services (PIP = 1.000), population density (PIP = 1.000), credit (PIP = 1.000), and trade (PIP = 0.860). With FE, only 6 variables cleared the 0.80 threshold and all 7 noise variables had PIPs near zero. The pooled BMA commits &lt;strong>5 false positives&lt;/strong> (services, pop_density, credit, trade, and democracy incorrectly flagged as robust noise variables or given inflated PIPs) compared to &lt;strong>zero&lt;/strong> false positives with FE. This happens because the noise variables are correlated with omitted country effects &amp;mdash; without FE to absorb those effects, the correlations create spurious associations that BMA interprets as genuine predictive power.&lt;/p>
&lt;p>The turning points (\$5,752 minimum, \$23,298 maximum) are far from the truth, and the 95% credible intervals fail to cover the true values for all three GDP terms &amp;mdash; the same coverage failure seen in pooled DSL. The lesson is clear: &lt;strong>fixed effects are not optional in panel BMA&lt;/strong>. They are essential for correct variable selection, not just coefficient estimation.&lt;/p>
&lt;h2 id="6-post-double-selection-lasso">6. Post-Double-Selection LASSO&lt;/h2>
&lt;h3 id="61-the-idea">6.1 The idea&lt;/h3>
&lt;p>Stata&amp;rsquo;s &lt;a href="https://www.stata.com/manuals/lassodsregress.pdf" target="_blank" rel="noopener">&lt;code>dsregress&lt;/code>&lt;/a> implements the &lt;strong>post-double-selection&lt;/strong> method of Belloni, Chernozhukov, and Hansen (2014). Think of it as a smart research assistant who reads the data twice &amp;mdash; once to find controls that predict the outcome (CO&lt;sub>2&lt;/sub>), and again to find controls that predict the variables of interest (GDP terms) &amp;mdash; then runs a clean OLS regression using only the controls that survived at least one selection.&lt;/p>
&lt;p>The &amp;ldquo;double&amp;rdquo; in double-selection refers to the &lt;strong>union&lt;/strong> of two separate LASSO selections. Why is this union necessary? If a control variable predicts both CO&lt;sub>2&lt;/sub> &lt;em>and&lt;/em> GDP but a single LASSO run on CO&lt;sub>2&lt;/sub> happens to miss it, omitting it from the final regression would bias the GDP coefficient. The second LASSO step (on GDP) catches variables that the first step might miss, and vice versa.&lt;/p>
&lt;p>The algorithm has four steps:&lt;/p>
&lt;pre>&lt;code class="language-mermaid">graph TD
Controls[&amp;quot;&amp;lt;b&amp;gt;12 Candidate Controls&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;+ country &amp;amp; year FE&amp;quot;]
Controls --&amp;gt; Step1[&amp;quot;&amp;lt;b&amp;gt;Step 1: LASSO on Outcome&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;CO2 ~ all controls&amp;lt;br/&amp;gt;→ Selected set X̃y&amp;quot;]
Controls --&amp;gt; Step2[&amp;quot;&amp;lt;b&amp;gt;Step 2: LASSO on Each Variable of Interest&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;GDP ~ all controls → X̃₁&amp;lt;br/&amp;gt;GDP² ~ all controls → X̃₂&amp;lt;br/&amp;gt;GDP³ ~ all controls → X̃₃&amp;quot;]
Step1 --&amp;gt; Union[&amp;quot;&amp;lt;b&amp;gt;Step 3: Take the Union&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;X̂ = X̃y ∪ X̃₁ ∪ X̃₂ ∪ X̃₃&amp;lt;br/&amp;gt;Only controls surviving&amp;lt;br/&amp;gt;at least one selection&amp;quot;]
Step2 --&amp;gt; Union
Union --&amp;gt; OLS[&amp;quot;&amp;lt;b&amp;gt;Step 4: Final OLS&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;CO2 ~ GDP + GDP² + GDP³ + X̂&amp;lt;br/&amp;gt;Standard OLS with valid&amp;lt;br/&amp;gt;inference on GDP terms&amp;quot;]
style Controls fill:#141413,stroke:#141413,color:#fff
style Step1 fill:#6a9bcc,stroke:#141413,color:#fff
style Step2 fill:#d97757,stroke:#141413,color:#fff
style Union fill:#1a3a8a,stroke:#141413,color:#fff
style OLS fill:#00d4c8,stroke:#141413,color:#141413
&lt;/code>&lt;/pre>
&lt;p>At the heart of each LASSO step is a penalized regression that shrinks irrelevant coefficients to exactly zero:&lt;/p>
&lt;p>$$\hat{\boldsymbol{\beta}}^{\text{LASSO}} = \arg\min_{\boldsymbol{\beta}} \left\{ \frac{1}{2N} \sum_{i=1}^{N}(y_i - \mathbf{x}_i'\boldsymbol{\beta})^2 + \lambda \sum_{j=1}^{p} |\beta_j| \right\}$$&lt;/p>
&lt;p>In words, LASSO minimizes the sum of squared residuals (the usual OLS objective) plus a penalty term $\lambda \sum |\beta_j|$ that charges a cost proportional to the &lt;em>absolute value&lt;/em> of each coefficient. The tuning parameter $\lambda$ controls how harsh this penalty is &amp;mdash; think of it as a &amp;ldquo;strictness dial.&amp;rdquo; When $\lambda = 0$, LASSO is just OLS. As $\lambda$ increases, more coefficients are forced to exactly zero. The L1 (absolute value) penalty is what makes LASSO a variable selector: unlike the L2 (squared) penalty used in Ridge regression, the L1 penalty has sharp corners at zero that drive weak coefficients to exactly zero rather than merely shrinking them.&lt;/p>
&lt;p>&lt;strong>Why &amp;ldquo;double&amp;rdquo; selection?&lt;/strong> The key insight of Belloni, Chernozhukov, and Hansen (2014) is that a single LASSO selection can miss important confounders. Consider our panel setting. We want to estimate the effect of GDP terms ($\mathbf{D}$) on CO&lt;sub>2&lt;/sub> ($Y$), controlling for other variables ($\mathbf{W}$). The model is:&lt;/p>
&lt;p>$$Y_i = \mathbf{D}_i' \boldsymbol{\alpha} + \mathbf{W}_i' \boldsymbol{\beta} + \varepsilon_i$$&lt;/p>
&lt;p>A confounder $W_j$ that affects both $Y$ and $\mathbf{D}$ must be included to avoid omitted variable bias. But if $W_j$ has a weak effect on $Y$, the LASSO on $Y$ might miss it. The double-selection strategy solves this by running LASSO twice:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Step 1&lt;/strong> selects controls that predict $Y$: $\quad \hat{S}_Y = \{j : \hat{\beta}_j^{\text{LASSO}(Y)} \neq 0\}$&lt;/li>
&lt;li>&lt;strong>Step 2&lt;/strong> selects controls that predict each $D_k$: $\quad \hat{S}_{D_k} = \{j : \hat{\gamma}_{j,k}^{\text{LASSO}(D_k)} \neq 0\}$&lt;/li>
&lt;li>&lt;strong>Step 3&lt;/strong> takes the union: $\quad \hat{S} = \hat{S}_Y \cup \hat{S}_{D_1} \cup \hat{S}_{D_2} \cup \hat{S}_{D_3}$&lt;/li>
&lt;li>&lt;strong>Step 4&lt;/strong> runs OLS of $Y$ on $\mathbf{D}$ and $\mathbf{W}_{\hat{S}}$ with standard inference&lt;/li>
&lt;/ul>
&lt;p>The union in Step 3 ensures that a confounder missed by the $Y$-LASSO but caught by the $D$-LASSO is still included. This &amp;ldquo;safety net&amp;rdquo; property is what gives post-double-selection its valid inference guarantees &amp;mdash; the final OLS produces consistent estimates of $\boldsymbol{\alpha}$ even if each individual LASSO makes some selection mistakes.&lt;/p>
&lt;p>The &lt;code>dsregress&lt;/code> command uses a &amp;ldquo;plugin&amp;rdquo; method to choose $\lambda$ &amp;mdash; an analytical formula that sets the penalty based on the sample size and noise level, without requiring cross-validation. A key assumption underlying DSL is &lt;em>approximate sparsity&lt;/em>: only a small number of controls truly matter, so LASSO can safely set the rest to zero. When the true model is dense (many small effects rather than a few large ones), LASSO may struggle to select the right variables.&lt;/p>
&lt;p>Before implementing DSL, it helps to see the two methods side by side:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Feature&lt;/th>
&lt;th>BMA&lt;/th>
&lt;th>Post-Double-Selection&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Philosophy&lt;/td>
&lt;td>Bayesian (posteriors)&lt;/td>
&lt;td>Frequentist (p-values)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Strategy&lt;/td>
&lt;td>Average across models&lt;/td>
&lt;td>Select controls, then OLS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Output&lt;/td>
&lt;td>PIPs for every variable&lt;/td>
&lt;td>Set of selected controls&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Speed&lt;/td>
&lt;td>Minutes (MCMC)&lt;/td>
&lt;td>Seconds (optimization)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Reference&lt;/td>
&lt;td>Raftery et al. (1997)&lt;/td>
&lt;td>Belloni, Chernozhukov, Hansen (2014)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="62-key-options">6.2 Key options&lt;/h3>
&lt;p>With the algorithm clear, let us examine the Stata implementation. The &lt;a href="https://www.stata.com/manuals/lassodsregress.pdf" target="_blank" rel="noopener">&lt;code>dsregress&lt;/code>&lt;/a> command has a concise syntax, but each element plays a specific role. The full option list is in the &lt;a href="https://www.stata.com/manuals/lasso.pdf" target="_blank" rel="noopener">Stata LASSO manual&lt;/a>; here we explain the ones used in this tutorial:&lt;/p>
&lt;p>&lt;strong>Syntax structure:&lt;/strong> &lt;code>dsregress depvar varsofinterest, controls(controlvars) [options]&lt;/code>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>$outcome&lt;/code>&lt;/strong> (&lt;code>ln_co2&lt;/code>) &amp;mdash; the dependent variable. DSL will run LASSO on this variable against all controls (Step 1)&lt;/li>
&lt;li>&lt;strong>&lt;code>$gdp_vars&lt;/code>&lt;/strong> (&lt;code>ln_gdp ln_gdp_sq ln_gdp_cb&lt;/code>) &amp;mdash; the &lt;em>variables of interest&lt;/em>. These are never penalized by LASSO; they always appear in the final OLS. DSL runs a separate LASSO for each one against all controls (Steps 2a&amp;ndash;2c)&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://www.stata.com/manuals/lassodsregress.pdf" target="_blank" rel="noopener">&lt;code>controls(($fe) $controls)&lt;/code>&lt;/a>&lt;/strong> &amp;mdash; the candidate controls subject to LASSO selection. Parentheses around &lt;code>$fe&lt;/code> tell Stata to treat factor variables (country and year dummies) as always-included in the LASSO penalty but available for selection. The 12 candidate controls are subject to the standard LASSO penalty&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://www.stata.com/manuals/lassodsregress.pdf" target="_blank" rel="noopener">&lt;code>vce(cluster country_id)&lt;/code>&lt;/a>&lt;/strong> &amp;mdash; compute cluster-robust standard errors at the country level in the final OLS (Step 4). This also affects the LASSO penalty through the &lt;a href="https://www.stata.com/manuals/lassolasso.pdf" target="_blank" rel="noopener">&lt;code>selection(plugin)&lt;/code>&lt;/a> method, which adjusts $\lambda$ for cluster dependence&lt;/li>
&lt;li>&lt;strong>&lt;code>selection(plugin)&lt;/code>&lt;/strong> (default) &amp;mdash; choose $\lambda$ using a data-driven analytical formula rather than cross-validation. The alternative &lt;a href="https://www.stata.com/manuals/lassolasso.pdf" target="_blank" rel="noopener">&lt;code>selection(cv)&lt;/code>&lt;/a> uses cross-validation but is slower&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://www.stata.com/manuals/lassolassoinfo.pdf" target="_blank" rel="noopener">&lt;code>lassoinfo&lt;/code>&lt;/a>&lt;/strong> (post-estimation) &amp;mdash; reports the number of selected controls and the $\lambda$ value for each LASSO step&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://www.stata.com/manuals/lassolassocoef.pdf" target="_blank" rel="noopener">&lt;code>lassocoef&lt;/code>&lt;/a>&lt;/strong> (post-estimation) &amp;mdash; displays which specific variables were selected or dropped by LASSO&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>&lt;strong>Related commands.&lt;/strong> Stata also offers &lt;a href="https://www.stata.com/manuals/lassoporegress.pdf" target="_blank" rel="noopener">&lt;code>poregress&lt;/code>&lt;/a> (partialing-out regression), which &lt;em>residualizes&lt;/em> both the outcome and the treatment against all controls instead of selecting then regressing. Both methods provide valid inference. &lt;a href="https://www.stata.com/manuals/lassoxporegress.pdf" target="_blank" rel="noopener">&lt;code>xporegress&lt;/code>&lt;/a> extends this to cross-fit partialing-out for even more robust inference. This tutorial uses &lt;code>dsregress&lt;/code> because its select-then-regress logic is more intuitive for beginners.&lt;/p>
&lt;/blockquote>
&lt;h3 id="63-estimation">6.3 Estimation&lt;/h3>
&lt;pre>&lt;code class="language-stata">dsregress $outcome $gdp_vars, ///
controls(($fe) $controls) ///
vce(cluster country_id)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Double-selection linear model Number of obs = 1,600
Number of controls = 112
Number of selected controls = 102
Wald chi2(3) = 53.15
Prob &amp;gt; chi2 = 0.0000
(Std. err. adjusted for 80 clusters in country_id)
------------------------------------------------------------------------------
| Robust
ln_co2 | Coefficient std. err. z P&amp;gt;|z| [95% conf. interval]
-------------+----------------------------------------------------------------
ln_gdp | -7.433319 1.628321 -4.57 0.000 -10.62477 -4.241868
ln_gdp_sq | .8401567 .1713522 4.90 0.000 .5043126 1.176001
ln_gdp_cb | -.0310764 .005952 -5.22 0.000 -.0427421 -.0194107
------------------------------------------------------------------------------
&lt;/code>&lt;/pre>
&lt;p>Post-double-selection completed in seconds with cluster-robust standard errors at the country level. Internally, &lt;code>dsregress&lt;/code> ran four separate LASSO regressions (Step 1 on CO&lt;sub>2&lt;/sub>, Steps 2a&amp;ndash;2c on each GDP term), took the union of all selected controls, and then ran a final OLS of CO&lt;sub>2&lt;/sub> on the GDP terms plus that union. All three GDP terms are significant at the 0.1% level. The Wald test strongly rejects the null that GDP terms are jointly zero ($\chi^2 = 53.15$, p &amp;lt; 0.001).&lt;/p>
&lt;h3 id="64-turning-points">6.4 Turning points&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Minimum:&lt;/strong> \$2,429 GDP per capita (true: \$1,895)&lt;/li>
&lt;li>&lt;strong>Maximum:&lt;/strong> \$27,672 GDP per capita (true: \$34,647)&lt;/li>
&lt;/ul>
&lt;p>The post-double-selection turning points (\$2,429 and \$27,672) fall between the sparse FE and kitchen-sink estimates, closer to the BMA values. With cluster-robust standard errors, the LASSO selection retained 102 of 112 controls for the outcome equation and 100 for each GDP term. The union of selected controls in Step 3 includes a few more candidate variables than without clustering, producing coefficients (&amp;ndash;7.433, 0.840, &amp;ndash;0.031) that lie between the sparse and kitchen-sink specifications.&lt;/p>
&lt;h3 id="65-lasso-selection">6.5 LASSO selection&lt;/h3>
&lt;p>To understand which controls LASSO kept and which it dropped, we inspect the selection details:&lt;/p>
&lt;pre>&lt;code class="language-stata">lassoinfo
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> Estimate: active
Command: dsregress
------------------------------------------------------
| No. of
| Selection selected
Variable | Model method lambda variables
------------+-----------------------------------------
ln_co2 | linear plugin .3818852 102
ln_gdp | linear plugin .3818852 100
ln_gdp_sq | linear plugin .3818852 100
ln_gdp_cb | linear plugin .3818852 100
------------------------------------------------------
&lt;/code>&lt;/pre>
&lt;p>The &lt;code>lassoinfo&lt;/code> output shows each of the four LASSO steps. The outcome equation selected 102 of 112 controls, while each GDP equation selected 100. The 112 candidates include 80 country dummies + 19 year dummies = 99 FE dummies, plus the 12 candidate variables and the constant. LASSO retains nearly all informative FE dummies and drops about 10&amp;ndash;12 of the weakest candidates at each step. The union across all four steps (Step 3) yields the final control set for Step 4&amp;rsquo;s OLS. With cluster-robust standard errors, the lambda is larger (0.382 vs 0.090 without clustering), leading to slightly different selection and producing DSL coefficients (&amp;ndash;7.433, 0.840, &amp;ndash;0.031) that fall between the sparse and kitchen-sink FE.&lt;/p>
&lt;p>Why does DSL not match BMA&amp;rsquo;s accuracy here? In panel data settings where FE dummies dominate the control set (99 of 112 variables), LASSO retains nearly all FE dummies and has limited room to discriminate among the 12 candidate controls of interest &amp;mdash; it dropped only 10&amp;ndash;12 variables at each step, most of them weak FE dummies rather than noise controls. This &amp;ldquo;almost everything selected&amp;rdquo; outcome means DSL&amp;rsquo;s final OLS is close to the kitchen-sink specification, which explains why its coefficients (&amp;ndash;7.433, 0.840, &amp;ndash;0.031) fall between sparse and kitchen-sink FE rather than converging to the true DGP. To see LASSO&amp;rsquo;s selection power unleashed, we next run DSL &lt;em>without&lt;/em> fixed effects.&lt;/p>
&lt;h3 id="66-pooled-dsl-without-fixed-effects">6.6 Pooled DSL (without fixed effects)&lt;/h3>
&lt;p>What happens when LASSO has only 12 candidate controls instead of 112? To answer this, we run DSL on the pooled data &amp;mdash; treating the panel as a cross-sectional dataset without country or year fixed effects. This gives LASSO full room to discriminate among the candidate controls, but at the cost of omitting the unobserved country heterogeneity that fixed effects would absorb.&lt;/p>
&lt;pre>&lt;code class="language-stata">* DSL without FE -- pooled cross-section with cluster-robust SEs
dsregress $outcome $gdp_vars, ///
controls($controls) ///
vce(cluster country_id)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Double-selection linear model Number of obs = 1,600
Number of controls = 12
Number of selected controls = 7
Wald chi2(3) = 25.05
Prob &amp;gt; chi2 = 0.0000
(Std. err. adjusted for 80 clusters in country_id)
------------------------------------------------------------------------------
| Robust
ln_co2 | Coefficient std. err. z P&amp;gt;|z| [95% conf. interval]
-------------+----------------------------------------------------------------
ln_gdp | -22.03297 5.277295 -4.18 0.000 -32.37628 -11.68966
ln_gdp_sq | 2.366878 .5652276 4.19 0.000 1.259052 3.474703
ln_gdp_cb | -.084224 .0199055 -4.23 0.000 -.1232381 -.04521
------------------------------------------------------------------------------
&lt;/code>&lt;/pre>
&lt;p>The pooled DSL still finds the correct inverted-N sign pattern ($\beta_1 &amp;lt; 0$, $\beta_2 &amp;gt; 0$, $\beta_3 &amp;lt; 0$), but the magnitudes are dramatically different from the true DGP. The linear coefficient (&amp;ndash;22.03) is more than &lt;em>three times&lt;/em> the true value (&amp;ndash;7.10), and the other terms are similarly inflated. This is &lt;strong>omitted variable bias&lt;/strong>: without country fixed effects, the GDP terms absorb not only their own effect on CO&lt;sub>2&lt;/sub> but also the persistent cross-country differences in emissions levels that fixed effects would have captured.&lt;/p>
&lt;pre>&lt;code class="language-stata">lassoinfo
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> Estimate: active
Command: dsregress
------------------------------------------------------
| No. of
| Selection selected
Variable | Model method lambda variables
------------+-----------------------------------------
ln_co2 | linear plugin .3818852 5
ln_gdp | linear plugin .3818852 7
ln_gdp_sq | linear plugin .3818852 7
ln_gdp_cb | linear plugin .3818852 7
------------------------------------------------------
&lt;/code>&lt;/pre>
&lt;p>Now the contrast with the FE-based DSL is stark. The outcome LASSO selected only &lt;strong>5 of 12&lt;/strong> controls (vs 102 of 112 with FE), and the GDP LASSOes selected &lt;strong>7 of 12&lt;/strong> (vs 100 of 112). Without FE dummies flooding the candidate set, LASSO can genuinely discriminate &amp;mdash; it zeroed out 5&amp;ndash;7 controls as irrelevant. The turning points are \$5,581 (minimum) and \$24,532 (maximum), far from the true values.&lt;/p>
&lt;p>This comparison illustrates a fundamental tradeoff in panel data econometrics: &lt;strong>fixed effects remove bias but limit LASSO&amp;rsquo;s selection power&lt;/strong>. With FE, the estimates are unbiased but LASSO selects almost everything. Without FE, LASSO selects sharply but the estimates are biased by unobserved heterogeneity. The FE-based DSL from Section 6.3 is the correct specification for this data, even though LASSO&amp;rsquo;s selection looks less impressive.&lt;/p>
&lt;h2 id="7-head-to-head-comparison">7. Head-to-Head Comparison&lt;/h2>
&lt;h3 id="71-coefficient-comparison">7.1 Coefficient comparison&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>Sparse FE&lt;/th>
&lt;th>Kitchen-Sink FE&lt;/th>
&lt;th>BMA (FE)&lt;/th>
&lt;th>DSL (FE)&lt;/th>
&lt;th>BMA (pooled)&lt;/th>
&lt;th>DSL (pooled)&lt;/th>
&lt;th>True DGP&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>$\beta_1$ (GDP)&lt;/td>
&lt;td>&amp;ndash;7.498&lt;/td>
&lt;td>&amp;ndash;7.131&lt;/td>
&lt;td>&amp;ndash;7.139&lt;/td>
&lt;td>&amp;ndash;7.433&lt;/td>
&lt;td>&amp;ndash;21.258&lt;/td>
&lt;td>&amp;ndash;22.033&lt;/td>
&lt;td>&amp;ndash;7.100&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>$\beta_2$ (GDP²)&lt;/td>
&lt;td>0.849&lt;/td>
&lt;td>0.806&lt;/td>
&lt;td>0.808&lt;/td>
&lt;td>0.840&lt;/td>
&lt;td>2.285&lt;/td>
&lt;td>2.367&lt;/td>
&lt;td>0.810&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>$\beta_3$ (GDP³)&lt;/td>
&lt;td>&amp;ndash;0.031&lt;/td>
&lt;td>&amp;ndash;0.030&lt;/td>
&lt;td>&amp;ndash;0.030&lt;/td>
&lt;td>&amp;ndash;0.031&lt;/td>
&lt;td>&amp;ndash;0.081&lt;/td>
&lt;td>&amp;ndash;0.084&lt;/td>
&lt;td>&amp;ndash;0.030&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Min TP&lt;/strong>&lt;/td>
&lt;td>\$2,478&lt;/td>
&lt;td>\$2,426&lt;/td>
&lt;td>\$2,411&lt;/td>
&lt;td>\$2,429&lt;/td>
&lt;td>\$5,752&lt;/td>
&lt;td>\$5,581&lt;/td>
&lt;td>\$1,895&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Max TP&lt;/strong>&lt;/td>
&lt;td>\$25,656&lt;/td>
&lt;td>\$27,694&lt;/td>
&lt;td>\$27,269&lt;/td>
&lt;td>\$27,672&lt;/td>
&lt;td>\$23,298&lt;/td>
&lt;td>\$24,532&lt;/td>
&lt;td>\$34,647&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The table reveals a sharp divide between FE-based and pooled specifications. The four FE-based methods (columns 2&amp;ndash;5) all produce GDP coefficients within a narrow range of the true values &amp;mdash; BMA (FE) and Kitchen-Sink FE are closest, with estimates within 1% of the truth. The two pooled methods (columns 6&amp;ndash;7) are dramatically biased, with coefficients inflated 2&amp;ndash;3x. Strikingly, BMA (pooled) and DSL (pooled) agree closely with &lt;em>each other&lt;/em> (&amp;ndash;21.26 vs &amp;ndash;22.03 for $\beta_1$), confirming that the bias comes from omitting fixed effects, not from the choice of variable selection method. Both pooled methods produce turning points displaced from the truth (\$5,600&amp;ndash;5,800 vs true \$1,895 for the minimum).&lt;/p>
&lt;h3 id="72-uncertainty-confidence-and-credible-intervals">7.2 Uncertainty: confidence and credible intervals&lt;/h3>
&lt;p>Point estimates tell only half the story. How &lt;em>uncertain&lt;/em> is each method, and does the interval actually contain the truth? The table below shows 95% confidence intervals (for the frequentist methods) and approximate 95% credible intervals (for BMA, computed as posterior mean $\pm$ 2 posterior SD). The last column checks whether the true DGP value falls inside the interval.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>$\beta_1$ (GDP) interval&lt;/th>
&lt;th>Covers true?&lt;/th>
&lt;th>$\beta_2$ (GDP²) interval&lt;/th>
&lt;th>Covers true?&lt;/th>
&lt;th>$\beta_3$ (GDP³) interval&lt;/th>
&lt;th>Covers true?&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Sparse FE&lt;/strong>&lt;/td>
&lt;td>[&amp;ndash;10.731, &amp;ndash;4.266]&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>[0.510, 1.188]&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>[&amp;ndash;0.043, &amp;ndash;0.020]&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Kitchen-Sink FE&lt;/strong>&lt;/td>
&lt;td>[&amp;ndash;10.241, &amp;ndash;4.021]&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>[0.478, 1.134]&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>[&amp;ndash;0.041, &amp;ndash;0.018]&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>BMA (FE)&lt;/strong> (credible)&lt;/td>
&lt;td>[&amp;ndash;10.761, &amp;ndash;3.517]&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>[0.429, 1.186]&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>[&amp;ndash;0.043, &amp;ndash;0.017]&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>DSL (FE)&lt;/strong>&lt;/td>
&lt;td>[&amp;ndash;10.625, &amp;ndash;4.242]&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>[0.504, 1.176]&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>[&amp;ndash;0.043, &amp;ndash;0.019]&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>BMA (pooled)&lt;/strong> (credible)&lt;/td>
&lt;td>[&amp;ndash;24.541, &amp;ndash;17.975]&lt;/td>
&lt;td>&lt;strong>No&lt;/strong>&lt;/td>
&lt;td>[1.935, 2.635]&lt;/td>
&lt;td>&lt;strong>No&lt;/strong>&lt;/td>
&lt;td>[&amp;ndash;0.094, &amp;ndash;0.069]&lt;/td>
&lt;td>&lt;strong>No&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>DSL (pooled)&lt;/strong>&lt;/td>
&lt;td>[&amp;ndash;32.376, &amp;ndash;11.690]&lt;/td>
&lt;td>&lt;strong>No&lt;/strong>&lt;/td>
&lt;td>[1.259, 3.475]&lt;/td>
&lt;td>&lt;strong>No&lt;/strong>&lt;/td>
&lt;td>[&amp;ndash;0.123, &amp;ndash;0.045]&lt;/td>
&lt;td>&lt;strong>No&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>True DGP&lt;/strong>&lt;/td>
&lt;td>&amp;ndash;7.100&lt;/td>
&lt;td>&lt;/td>
&lt;td>0.810&lt;/td>
&lt;td>&lt;/td>
&lt;td>&amp;ndash;0.030&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The four FE-based methods all produce intervals that contain the true parameter values &amp;mdash; a reassuring result. Both pooled methods, however, &lt;strong>fail to cover the truth for any of the three coefficients&lt;/strong>. The pooled DSL intervals are wide (the $\beta_1$ interval spans 20.7 units) but centered so far from the truth that even this width cannot compensate. The pooled BMA credible intervals are actually &lt;em>narrower&lt;/em> (spanning 6.6 units for $\beta_1$) but even more precisely wrong &amp;mdash; they are tightly concentrated around the biased estimate. This is the worst-case scenario: &lt;strong>false precision from a misspecified model&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>Width reflects uncertainty.&lt;/strong> Among the FE-based methods, BMA produces the widest interval for $\beta_1$ (width = 7.24), followed by Sparse FE (6.47), DSL with FE (6.38), and Kitchen-Sink FE (6.22). BMA&amp;rsquo;s wider intervals reflect its honest accounting of model uncertainty &amp;mdash; it averages across thousands of models, each contributing slightly different coefficient estimates, which inflates the posterior standard deviation. The frequentist methods condition on a single model and therefore understate the total uncertainty.&lt;/p>
&lt;p>&lt;strong>Centering reflects bias.&lt;/strong> Kitchen-Sink FE and BMA center their intervals closest to the true value (&amp;ndash;7.131 and &amp;ndash;7.139 vs. true &amp;ndash;7.100), while Sparse FE (&amp;ndash;7.498) and DSL with FE (&amp;ndash;7.433) are slightly further away. The pooled DSL (&amp;ndash;22.033) is dramatically off-center, illustrating that omitted variable bias overwhelms any precision gained from better variable selection.&lt;/p>
&lt;p>&lt;strong>Coverage requires correct specification.&lt;/strong> The pooled DSL result drives home a critical lesson: a confidence interval is only as good as the model behind it. The 95% label promises that, in repeated sampling, 95% of intervals would contain the truth &amp;mdash; but this guarantee holds only if the model is correctly specified. When country fixed effects are omitted, the model is misspecified, and the intervals fail despite being statistically &amp;ldquo;valid&amp;rdquo; within the pooled framework.&lt;/p>
&lt;p>&lt;strong>Bayesian vs frequentist interpretation.&lt;/strong> BMA&amp;rsquo;s credible intervals have a different interpretation: a 95% BMA credible interval says &amp;ldquo;given the data and priors, there is a 95% posterior probability the true coefficient lies in this range,&amp;rdquo; while a 95% confidence interval says &amp;ldquo;if we repeated this procedure many times, 95% of the intervals would contain the truth.&amp;rdquo; In practice, both require correct model specification to be reliable.&lt;/p>
&lt;h3 id="73-predicted-ekc-curves">7.3 Predicted EKC curves&lt;/h3>
&lt;p>The curves are normalized to zero at the sample-mean GDP so both methods are directly comparable:&lt;/p>
&lt;pre>&lt;code class="language-stata">* Generate predicted EKC curves for BMA and DSL, normalized at mean GDP
summarize ln_gdp
local xmin = r(min)
local xmax = r(max)
local xmean = r(mean)
clear
set obs 500
gen lngdp = `xmin' + (_n - 1) * (`xmax' - `xmin') / 499
* Cubic component for each method (using stored coefficients)
gen fit_bma = `b1_bma' * lngdp + `b2_bma' * lngdp^2 + `b3_bma' * lngdp^3
gen fit_dsl = `b1_dsl' * lngdp + `b2_dsl' * lngdp^2 + `b3_dsl' * lngdp^3
* Normalize: subtract value at sample-mean GDP
local norm_bma = `b1_bma' * `xmean' + `b2_bma' * `xmean'^2 + `b3_bma' * `xmean'^3
local norm_dsl = `b1_dsl' * `xmean' + `b2_dsl' * `xmean'^2 + `b3_dsl' * `xmean'^3
replace fit_bma = fit_bma - `norm_bma'
replace fit_dsl = fit_dsl - `norm_dsl'
twoway ///
(line fit_bma lngdp, lcolor(&amp;quot;106 155 204&amp;quot;) lwidth(medthick)) ///
(line fit_dsl lngdp, lcolor(&amp;quot;217 119 87&amp;quot;) lwidth(medthick) lpattern(dash)), ///
xline(`lnmin_bma', lcolor(&amp;quot;106 155 204&amp;quot;%50) lpattern(shortdash)) ///
xline(`lnmax_bma', lcolor(&amp;quot;106 155 204&amp;quot;%50) lpattern(shortdash)) ///
ytitle(&amp;quot;Predicted log CO2 (normalized at mean GDP)&amp;quot;) ///
xtitle(&amp;quot;Log GDP per capita&amp;quot;) ///
title(&amp;quot;Predicted EKC Shape: BMA vs. DSL&amp;quot;) ///
legend(order(1 &amp;quot;BMA&amp;quot; 2 &amp;quot;DSL&amp;quot;) rows(1) position(6)) ///
scheme(s2color)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_bma_dsl_fig5_ekc_curves.png" alt="Predicted EKC curves from BMA and DSL, normalized at the sample mean. Both methods trace a clear inverted-N shape with closely aligned turning points.">&lt;/p>
&lt;p>Both curves trace a clear inverted-N: CO&lt;sub>2&lt;/sub> falls at low incomes, rises through industrialization, and falls again at high incomes. The BMA curve (solid blue) and DSL curve (dashed orange) are nearly indistinguishable, with turning points closely aligned. The normalization at mean GDP makes the shape immediately visible &amp;mdash; a major improvement over plotting raw cubic components that would sit at different y-levels.&lt;/p>
&lt;h3 id="74-answer-key-grading-the-methods">7.4 Answer key: grading the methods&lt;/h3>
&lt;p>The ultimate test: do BMA and DSL correctly identify the 5 true predictors and reject the 7 noise variables?&lt;/p>
&lt;pre>&lt;code class="language-stata">* Dot plot: BMA PIPs color-coded by ground truth
* (extract PIPs, label variables, mark true vs noise --- see analysis.do)
graph twoway ///
(scatter order pip if is_true == 1, ///
mcolor(&amp;quot;106 155 204&amp;quot;) msymbol(circle) msize(large)) ///
(scatter order pip if is_true == 0, ///
mcolor(gs9) msymbol(diamond) msize(large)), ///
xline(0.8, lcolor(&amp;quot;217 119 87&amp;quot;) lpattern(dash) lwidth(medium)) ///
ylabel(1(1)15, valuelabel angle(0) labsize(small)) ///
xlabel(0(0.2)1, format(%3.1f)) ///
xtitle(&amp;quot;BMA Posterior Inclusion Probability&amp;quot;) ///
title(&amp;quot;Answer Key: Do BMA and DSL Recover the Truth?&amp;quot;) ///
legend(order(1 &amp;quot;True predictor&amp;quot; 2 &amp;quot;Noise variable&amp;quot;) ///
rows(1) position(6)) ///
scheme(s2color)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_bma_dsl_fig6_answer_key.png" alt="Dot plot showing BMA Posterior Inclusion Probabilities for each variable, color-coded by ground truth. True predictors (circles, blue) cluster above the 0.80 threshold; noise variables (diamonds, gray) cluster below it.">&lt;/p>
&lt;p>&lt;strong>BMA&amp;rsquo;s report card:&lt;/strong> Of the 8 true predictors (3 GDP terms + 5 controls), BMA correctly assigns PIP &amp;gt; 0.80 to 6 &amp;mdash; the three GDP terms, fossil fuel, industry, and renewable energy. It misses urban (PIP ~ 0.27) and democracy (PIP ~ 0.02), whose true coefficients are small (0.007 and &amp;ndash;0.005). All 7 noise variables receive PIPs well below 0.80. BMA makes &lt;strong>zero false positives&lt;/strong> (no noise variable incorrectly flagged as robust) and &lt;strong>two false negatives&lt;/strong> (two weak true predictors missed).&lt;/p>
&lt;p>&lt;strong>Post-double-selection&amp;rsquo;s report card:&lt;/strong> With cluster-robust SEs, the union of all four LASSO steps selected 102 of 112 total controls (including FE dummies). The resulting DSL coefficients (&amp;ndash;7.433, 0.840, &amp;ndash;0.031) fall between the sparse and kitchen-sink FE, closer to the true DGP than the sparse specification. The entire procedure runs in seconds rather than minutes.&lt;/p>
&lt;p>&lt;strong>Bottom line:&lt;/strong> Both methods recover the inverted-N EKC shape. BMA provides more granular variable-level inference (PIPs), while DSL provides fast, valid coefficient estimates. The synthetic data &amp;ldquo;answer key&amp;rdquo; confirms that both are doing their job &amp;mdash; with the expected limitation that weak signals are hard to detect.&lt;/p>
&lt;h2 id="8-discussion">8. Discussion&lt;/h2>
&lt;h3 id="81-what-the-results-mean-for-the-ekc">8.1 What the results mean for the EKC&lt;/h3>
&lt;p>Both BMA and DSL identify the &lt;strong>inverted-N&lt;/strong> EKC shape with turning points close to the true DGP values. BMA correctly identifies 6 of 8 true predictors (3 GDP terms + fossil fuel, industry, renewable) with zero false positives among noise variables. The inverted-N shape implies three phases of the income&amp;ndash;pollution relationship:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Declining phase&lt;/strong> (below ~\$2,400): Very poor countries where CO&lt;sub>2&lt;/sub> may fall as subsistence agriculture shifts toward slightly cleaner energy.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Rising phase&lt;/strong> (~\$2,400 to ~\$27,000): Industrializing countries where emissions rise sharply. Most of the world&amp;rsquo;s population lives here.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Declining phase&lt;/strong> (above ~\$27,000): Wealthy countries where clean technology and regulation reduce emissions.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>The policy implication is important: the inverted-N suggests that the &amp;ldquo;environmental improvement&amp;rdquo; phase is not automatic. Unlike the simpler inverted-U hypothesis, which predicts a single turning point after which pollution monotonically declines, the inverted-N warns that countries at very low income levels may &lt;em>already&lt;/em> be on a declining emissions path that reverses once industrialization begins. This makes the middle-income range &amp;mdash; where emissions rise steeply &amp;mdash; the critical window for environmental policy intervention.&lt;/p>
&lt;p>The three robust control variables identified by BMA reinforce this narrative:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Fossil fuel dependence&lt;/strong> (PIP = 1.000) is the single strongest predictor of CO&lt;sub>2&lt;/sub> emissions, with a coefficient close to the true DGP value.&lt;/li>
&lt;li>&lt;strong>Renewable energy share&lt;/strong> (PIP = 0.959) enters with a negative sign, confirming that energy mix transitions reduce emissions.&lt;/li>
&lt;li>&lt;strong>Industry value-added&lt;/strong> (PIP = 0.999) captures the composition effect &amp;mdash; economies dominated by manufacturing produce more CO&lt;sub>2&lt;/sub> per unit of GDP than service-based economies.&lt;/li>
&lt;/ul>
&lt;h3 id="82-when-to-use-bma-vs-post-double-selection">8.2 When to use BMA vs post-double-selection&lt;/h3>
&lt;p>The two methods answer fundamentally different research questions:&lt;/p>
&lt;p>&lt;strong>Use BMA&lt;/strong> when the question is &lt;em>&amp;ldquo;which variables robustly predict the outcome?&amp;quot;&lt;/em> BMA provides PIPs, coefficient densities, and a complete picture of the model space. It excels in exploratory settings where variable importance is the goal. In our simulation, BMA produced the most accurate coefficient estimates (&amp;ndash;7.139 vs true &amp;ndash;7.100) and provided rich diagnostics (PIP chart, density plots) that make the evidence for each variable transparent. The cost is computational: BMA requires MCMC sampling (minutes to hours depending on the model space).&lt;/p>
&lt;p>&lt;strong>Use post-double-selection&lt;/strong> when the question is &lt;em>&amp;ldquo;what is the causal effect of a specific variable of interest, controlling for high-dimensional confounders?&amp;quot;&lt;/em> DSL provides fast, valid inference on the coefficients of interest with standard errors and confidence intervals. It is designed for settings where you have a clear treatment variable and many potential controls. In our simulation, DSL completed in seconds and produced valid standard errors, but its coefficient estimates (&amp;ndash;7.433) were less accurate than BMA&amp;rsquo;s because LASSO had limited room to discriminate among controls in the FE-heavy panel setting.&lt;/p>
&lt;p>&lt;strong>Use both together&lt;/strong> (as in this tutorial) when you want the strongest possible evidence. If a Bayesian and a frequentist method agree on the sign, magnitude, and significance of an effect, the finding is unlikely to be an artifact of any single modeling choice. Disagreements between the methods are also informative &amp;mdash; they signal areas where the evidence is sensitive to assumptions.&lt;/p>
&lt;h3 id="83-pooled-vs-fixed-effects-a-cautionary-comparison">8.3 Pooled vs fixed effects: a cautionary comparison&lt;/h3>
&lt;p>The pooled specifications (Sections 5.7 and 6.6) provide a powerful pedagogical contrast. When we strip away fixed effects and run both BMA and DSL on pooled data, three things happen simultaneously:&lt;/p>
&lt;p>&lt;strong>LASSO selection improves but estimates worsen.&lt;/strong> Without 99 FE dummies diluting the candidate set, LASSO in pooled DSL selected only 5&amp;ndash;7 of 12 controls (vs 102 of 112 with FE). This is closer to the &amp;ldquo;textbook&amp;rdquo; LASSO scenario where the method has genuine discriminating power. Yet the resulting coefficient estimates are 2&amp;ndash;3x the true values because omitted country heterogeneity biases everything.&lt;/p>
&lt;p>&lt;strong>BMA PIPs become unreliable.&lt;/strong> With fixed effects, BMA assigned PIP near zero to all 7 noise variables &amp;mdash; zero false positives. Without FE, 5 noise variables (services, pop_density, credit, trade, and inflated democracy) received PIPs above 0.80. The noise variables are correlated with omitted country effects, and BMA interprets these spurious correlations as genuine predictive power. This demonstrates that &lt;strong>PIP thresholds are only meaningful when the model set is correctly specified&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>Both methods agree on the bias.&lt;/strong> Pooled BMA and pooled DSL produce remarkably similar biased coefficients ($\beta_1 = -21.26$ vs $-22.03$), confirming that the problem is not the variable selection method but the omitted fixed effects. The agreement between a Bayesian and a frequentist method on the &lt;em>wrong&lt;/em> answer reinforces the lesson: &lt;strong>method agreement is not a substitute for correct model specification&lt;/strong>.&lt;/p>
&lt;p>The practical takeaway for applied researchers: in panel data settings, always include entity fixed effects (or equivalent controls for unobserved heterogeneity) before applying BMA or DSL. Running these methods on pooled data without FE will produce misleading results &amp;mdash; not because the methods fail, but because the models they average over or select from are all misspecified.&lt;/p>
&lt;h3 id="84-limitations-and-caveats">8.4 Limitations and caveats&lt;/h3>
&lt;p>&lt;strong>Synthetic vs real data.&lt;/strong> This is synthetic data &amp;mdash; the patterns are sharper than real-world data, and we can verify ground truth only because we designed the DGP. With real data, model uncertainty is genuinely unresolvable, and there is no answer key to check against. The separation between true predictors and noise variables is cleaner here than in most applications.&lt;/p>
&lt;p>&lt;strong>Weak signals are hard to detect.&lt;/strong> Both methods missed urban population (PIP = 0.27) and democracy (PIP = 0.02), whose true coefficients are small (0.007 and &amp;ndash;0.005). This is not a failure of the methods &amp;mdash; it is a fundamental statistical limitation. Detecting a coefficient of 0.005 in the presence of panel-level noise requires either a much larger sample or a stronger signal.&lt;/p>
&lt;p>&lt;strong>Panel FE and LASSO.&lt;/strong> In our panel setting, 99 of 112 candidate controls are FE dummies that LASSO retains almost entirely. This limits DSL&amp;rsquo;s ability to discriminate among the 12 candidate controls. In cross-sectional settings or settings with many genuinely irrelevant variables, DSL would have more room to operate and potentially match BMA&amp;rsquo;s accuracy.&lt;/p>
&lt;p>&lt;strong>Extensions.&lt;/strong> Researchers working with real EKC data should also consider endogeneity (via 2SLS-BMA, as in Gravina and Lanzafame, 2025), alternative pollutants (SO&lt;sub>2&lt;/sub>, PM2.5), spatial dependence across countries, and structural breaks in the income&amp;ndash;pollution relationship.&lt;/p>
&lt;h2 id="9-summary-and-next-steps">9. Summary and Next Steps&lt;/h2>
&lt;h3 id="takeaways">Takeaways&lt;/h3>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>Both methods confirm the inverted-N shape.&lt;/strong> BMA (Bayesian, averaging across models) and post-double-selection (frequentist, LASSO-based) both recover the inverted-N EKC. BMA produces coefficients closest to the true DGP (&amp;ndash;7.139 vs &amp;ndash;7.100 for $\beta_1$). DSL with cluster-robust SEs gives &amp;ndash;7.433, falling between the sparse and kitchen-sink FE. Both methods outperform the naive sparse specification.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Both methods recover the ground truth.&lt;/strong> BMA correctly identifies 6 of 8 true predictors with zero false positives. The three strongest true controls (fossil fuel, industry, renewable energy) all receive PIPs above 0.95. The two misses (urban, democracy) have small true coefficients, illustrating that even good methods have limits with weak signals.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Model uncertainty is real.&lt;/strong> The GDP linear coefficient shifts from &amp;ndash;7.498 (sparse) to &amp;ndash;7.131 (kitchen-sink) depending on which controls are included. The maximum turning point moves by \$2,000. BMA and DSL provide principled solutions.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>BMA and post-double-selection serve different purposes.&lt;/strong> BMA excels at variable selection (PIPs, coefficient densities) and produced the most accurate coefficient estimates in this setting. Post-double-selection is fastest and provides standard frequentist inference with cluster-robust SEs. In panel settings dominated by FE dummies, LASSO has limited room to discriminate among candidate controls; DSL would be more powerful in cross-sectional settings with many irrelevant variables.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Fixed effects are essential, not optional.&lt;/strong> Running either method on pooled data without FE produces coefficients inflated 2&amp;ndash;3x (BMA pooled: &amp;ndash;21.26, DSL pooled: &amp;ndash;22.03, vs true &amp;ndash;7.10 for $\beta_1$). Worse, pooled BMA assigns high PIPs to 5 noise variables that the FE-based BMA correctly rejects. Confidence and credible intervals from pooled models fail to cover the true values for all three coefficients. The lesson: always include fixed effects in panel data before applying variable selection methods.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h3 id="exercises">Exercises&lt;/h3>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Sensitivity to the g-prior.&lt;/strong> Re-run &lt;code>bmaregress&lt;/code> with &lt;code>gprior(bric)&lt;/code> instead of &lt;code>gprior(uip)&lt;/code>. The BIC prior penalizes model complexity more heavily. Do the PIPs change? Does it still identify fossil fuel, industry, and renewable as robust? (&lt;em>Hint:&lt;/em> BIC priors tend to be more conservative, so borderline variables may drop below the threshold.)&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Test for inverted-U.&lt;/strong> Drop &lt;code>ln_gdp_cb&lt;/code> and re-run with only linear and squared GDP terms. What do BMA and DSL say about the simpler quadratic specification? (&lt;em>Hint:&lt;/em> since the DGP includes a cubic term, the quadratic model is misspecified &amp;mdash; check whether the coefficients absorb the cubic effect or produce a visibly different EKC shape.)&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Increase noise.&lt;/strong> Re-generate the synthetic data with &lt;code>sigma_eps = 0.30&lt;/code> (double the noise) in &lt;code>generate_data.do&lt;/code> and re-run the full analysis. How does this affect BMA&amp;rsquo;s ability to distinguish true predictors from noise? (&lt;em>Hint:&lt;/em> expect more variables with PIPs in the ambiguous 0.3&amp;ndash;0.7 range, and possibly some noise variables crossing the 0.80 threshold &amp;mdash; false positives become more likely with noisier data.)&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="appendix-a-first-differences-analysis">Appendix A: First-Differences Analysis&lt;/h2>
&lt;h3 id="a1-motivation">A.1 Motivation&lt;/h3>
&lt;p>The fixed effects estimator removes time-invariant country heterogeneity by demeaning each variable within country. An alternative approach is &lt;strong>first differencing&lt;/strong>: computing the change between the last and first year for each country ($\Delta x_i = x_{i,2014} - x_{i,1995}$). This also removes time-invariant effects and produces a pure &lt;strong>cross-sectional&lt;/strong> dataset of 80 observations &amp;mdash; one per country. The cross-sectional setting is where LASSO-based methods are most powerful, because there are no FE dummies diluting the candidate set.&lt;/p>
&lt;p>The tradeoff is statistical power: first differencing uses only two data points per country (discarding 18 intermediate years), while the within-estimator uses all 20. We expect noisier estimates but cleaner variable selection.&lt;/p>
&lt;h3 id="a2-constructing-the-first-difference-dataset">A.2 Constructing the first-difference dataset&lt;/h3>
&lt;pre>&lt;code class="language-stata">* Keep only first (1995) and last (2014) years, reshape, compute differences
keep if year == 1995 | year == 2014
reshape wide $outcome $gdp_vars $controls, i(country_id) j(year)
foreach v in $outcome $gdp_vars $controls {
gen d_`v' = `v'2014 - `v'1995
}
&lt;/code>&lt;/pre>
&lt;p>This produces 80 observations, each representing how much a country&amp;rsquo;s variables changed over the 20-year period. For example, &lt;code>d_ln_gdp&lt;/code> measures the log growth in GDP per capita from 1995 to 2014.&lt;/p>
&lt;h3 id="a3-baseline-ols-on-first-differences">A.3 Baseline OLS on first differences&lt;/h3>
&lt;pre>&lt;code class="language-stata">* Sparse: GDP terms only
regress d_ln_co2 d_ln_gdp d_ln_gdp_sq d_ln_gdp_cb, robust
* Kitchen-sink: all 12 controls
regress d_ln_co2 d_ln_gdp d_ln_gdp_sq d_ln_gdp_cb ///
d_fossil_fuel d_renewable d_urban d_industry d_democracy ///
d_services d_trade d_fdi d_credit d_pop_density ///
d_corruption d_globalization, robust
&lt;/code>&lt;/pre>
&lt;p>&lt;strong>FD Sparse OLS:&lt;/strong>&lt;/p>
&lt;pre>&lt;code class="language-text">Linear regression Number of obs = 80
Prob &amp;gt; F = 0.0009
R-squared = 0.1433
------------------------------------------------------------------------------
| Robust
d_ln_co2 | Coefficient std. err. t P&amp;gt;|t| [95% conf. interval]
-------------+----------------------------------------------------------------
d_ln_gdp | -10.36189 4.092422 -2.53 0.013 -18.51265 -2.211121
d_ln_gdp_sq | 1.155962 .4223643 2.74 0.008 .3147506 1.997173
d_ln_gdp_cb | -.0414947 .0143721 -2.89 0.005 -.0701192 -.0128702
_cons | -.3036562 .0724366 -4.19 0.000 -.4479262 -.1593861
------------------------------------------------------------------------------
&lt;/code>&lt;/pre>
&lt;p>&lt;strong>FD Kitchen-sink OLS:&lt;/strong>&lt;/p>
&lt;pre>&lt;code class="language-text">Linear regression Number of obs = 80
Prob &amp;gt; F = 0.0029
R-squared = 0.3707
------------------------------------------------------------------------------
| Robust
d_ln_co2 | Coefficient std. err. t P&amp;gt;|t| [95% conf. interval]
-------------+----------------------------------------------------------------
d_ln_gdp | -8.109709 5.031758 -1.61 0.112 -18.1618 1.942382
d_ln_gdp_sq | .9238864 .5213262 1.77 0.081 -.1175823 1.965355
d_ln_gdp_cb | -.0336221 .0179583 -1.87 0.066 -.0694979 .0022536
d_fossil_f~l | .0147108 .0067313 2.19 0.033 .0012635 .0281582
d_renewable | -.0237808 .0110384 -2.15 0.035 -.0458327 -.001729
d_urban | .0002501 .014913 0.02 0.987 -.0295421 .0300424
d_industry | .0309085 .0105974 2.92 0.005 .0097377 .0520793
d_democracy | .019337 .0290345 0.67 0.508 -.038666 .07734
d_services | -.0047239 .0098816 -0.48 0.634 -.0244647 .0150169
d_trade | .006726 .0044062 1.53 0.132 -.0020764 .0155284
d_fdi | .0000124 .0091898 0.00 0.999 -.0183463 .0183712
d_credit | .0028644 .0043456 0.66 0.512 -.0058169 .0115457
d_pop_dens~y | .0006396 .0004991 1.28 0.205 -.0003575 .0016366
d_corruption | -.0036115 .0033497 -1.08 0.285 -.0103033 .0030803
d_globaliz~n | -.0004567 .0082494 -0.06 0.956 -.0169368 .0160235
_cons | -.0085823 .1746184 -0.05 0.961 -.3574226 .340258
------------------------------------------------------------------------------
&lt;/code>&lt;/pre>
&lt;p>The FD sparse OLS finds the inverted-N sign pattern with all three terms significant at the 5% level &amp;mdash; but the coefficients are noisier than the FE estimates (e.g., $\beta_1 = -10.36$ vs &amp;ndash;7.50 for sparse FE). The R² of 0.14 is low, reflecting the loss of within-country time-series variation when collapsing 20 years into a single difference.&lt;/p>
&lt;p>Adding controls in the kitchen-sink raises R² to 0.37 but makes the GDP terms individually insignificant (p = 0.07&amp;ndash;0.11) &amp;mdash; a consequence of having only 80 observations and 15 regressors. Among the controls, fossil fuel (p = 0.033), renewable energy (p = 0.035), and industry (p = 0.005) are significant &amp;mdash; the same three strong predictors identified by BMA with fixed effects.&lt;/p>
&lt;h3 id="a4-bma-on-first-differences">A.4 BMA on first differences&lt;/h3>
&lt;pre>&lt;code class="language-stata">bmaregress d_ln_co2 d_ln_gdp d_ln_gdp_sq d_ln_gdp_cb ///
d_fossil_fuel d_renewable d_urban d_industry d_democracy ///
d_services d_trade d_fdi d_credit d_pop_density ///
d_corruption d_globalization, ///
mprior(uniform) gprior(uip) ///
mcmcsize(50000) rseed(9988) pipcutoff(0.5) burnin(5000)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Bayesian model averaging No. of obs = 80
Linear regression No. of predictors = 15
MC3 sampling Groups = 15
Always = 0
No. of models = 2,317
For CPMP &amp;gt;= .9 = 581
Priors: Mean model size = 3.304
Models: Uniform Burn-in = 5,000
Cons.: Noninformative MCMC sample size = 50,000
Coef.: Zellner's g Acceptance rate = 0.3080
g: Unit-information, g = 80 Shrinkage, g/(1+g) = 0.9877
sigma2: Noninformative Mean sigma2 = 0.051
Sampling correlation = 0.9958
------------------------------------------------------------------------------
d_ln_co2 | Mean Std. dev. Group PIP
-------------+----------------------------------------------------------------
d_industry | .0364834 .0090778 7 .99823
------------------------------------------------------------------------------
Note: 14 predictors with PIP less than .5 not shown.
&lt;/code>&lt;/pre>
&lt;p>The FD-BMA result is dramatically different from the FE-based BMA. Only &lt;strong>one variable&lt;/strong> passes the 0.50 PIP display threshold: the change in industry share (PIP = 0.998). The three GDP polynomial terms all have PIPs below 0.30:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Variable&lt;/th>
&lt;th>PIP (FD-BMA)&lt;/th>
&lt;th>PIP (FE-BMA)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>d_ln_gdp&lt;/td>
&lt;td>0.298&lt;/td>
&lt;td>0.994&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>d_ln_gdp_sq&lt;/td>
&lt;td>0.267&lt;/td>
&lt;td>1.000&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>d_ln_gdp_cb&lt;/td>
&lt;td>0.271&lt;/td>
&lt;td>1.000&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>d_fossil_fuel&lt;/td>
&lt;td>0.183&lt;/td>
&lt;td>1.000&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>d_renewable&lt;/td>
&lt;td>0.350&lt;/td>
&lt;td>0.959&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>d_urban&lt;/td>
&lt;td>0.096&lt;/td>
&lt;td>0.268&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>d_industry&lt;/td>
&lt;td>&lt;strong>0.998&lt;/strong>&lt;/td>
&lt;td>0.999&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>d_democracy&lt;/td>
&lt;td>0.094&lt;/td>
&lt;td>0.023&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>With only 80 cross-sectional observations, BMA&amp;rsquo;s evidence threshold is much harder to clear. The GDP terms &amp;mdash; which are &lt;em>the core of the EKC&lt;/em> &amp;mdash; do not survive because the 20-year differences are noisy and the cubic polynomial requires precise estimation of three correlated terms simultaneously.&lt;/p>
&lt;p>The change in industry share is the only variable with a strong enough signal-to-noise ratio to clear BMA&amp;rsquo;s bar. The FE-based BMA (N = 1,600) has 20x more observations to work with, which is why it identifies 6 robust variables.&lt;/p>
&lt;h3 id="a5-dsl-on-first-differences">A.5 DSL on first differences&lt;/h3>
&lt;pre>&lt;code class="language-stata">dsregress d_ln_co2 d_ln_gdp d_ln_gdp_sq d_ln_gdp_cb, ///
controls(d_fossil_fuel d_renewable d_urban d_industry d_democracy ///
d_services d_trade d_fdi d_credit d_pop_density ///
d_corruption d_globalization) ///
rseed(9988)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Double-selection linear model Number of obs = 80
Number of controls = 12
Number of selected controls = 1
Wald chi2(3) = 10.65
Prob &amp;gt; chi2 = 0.0138
------------------------------------------------------------------------------
| Robust
d_ln_co2 | Coefficient std. err. z P&amp;gt;|z| [95% conf. interval]
-------------+----------------------------------------------------------------
d_ln_gdp | -5.047196 4.558593 -1.11 0.268 -13.98187 3.887483
d_ln_gdp_sq | .5943786 .4700569 1.26 0.206 -.326916 1.515673
d_ln_gdp_cb | -.0220809 .0160386 -1.38 0.169 -.0535159 .0093541
------------------------------------------------------------------------------
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-stata">lassoinfo
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> Estimate: active
Command: dsregress
------------------------------------------------------
| No. of
| Selection selected
Variable | Model method lambda variables
------------+-----------------------------------------
d_ln_co2 | linear plugin .3818852 1
d_ln_gdp | linear plugin .3818852 0
d_ln_gdp_sq | linear plugin .3818852 0
d_ln_gdp_cb | linear plugin .3818852 0
------------------------------------------------------
&lt;/code>&lt;/pre>
&lt;p>FD-DSL selected only &lt;strong>1 control&lt;/strong> for the outcome equation (likely d_industry, consistent with BMA) and &lt;strong>zero controls&lt;/strong> for each of the three GDP equations. With such sparse selection, the final OLS is essentially a regression of d_ln_co2 on the three GDP terms plus one control &amp;mdash; and none of the three GDP terms are individually significant (p = 0.17&amp;ndash;0.27). The Wald test for joint significance is borderline (p = 0.014), suggesting the GDP terms collectively have some explanatory power, but the individual estimates are too noisy for inference.&lt;/p>
&lt;h3 id="a6-comparison-first-differences-vs-fixed-effects">A.6 Comparison: first differences vs fixed effects&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>FD Sparse&lt;/th>
&lt;th>FD Kitchen&lt;/th>
&lt;th>FD BMA&lt;/th>
&lt;th>FD DSL&lt;/th>
&lt;th>FE BMA&lt;/th>
&lt;th>FE DSL&lt;/th>
&lt;th>True DGP&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>$\beta_1$ (GDP)&lt;/td>
&lt;td>&amp;ndash;10.362&lt;/td>
&lt;td>&amp;ndash;8.110&lt;/td>
&lt;td>n/a&lt;/td>
&lt;td>&amp;ndash;5.047&lt;/td>
&lt;td>&amp;ndash;7.139&lt;/td>
&lt;td>&amp;ndash;7.433&lt;/td>
&lt;td>&amp;ndash;7.100&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>$\beta_2$ (GDP²)&lt;/td>
&lt;td>1.156&lt;/td>
&lt;td>0.924&lt;/td>
&lt;td>n/a&lt;/td>
&lt;td>0.594&lt;/td>
&lt;td>0.808&lt;/td>
&lt;td>0.840&lt;/td>
&lt;td>0.810&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>$\beta_3$ (GDP³)&lt;/td>
&lt;td>&amp;ndash;0.041&lt;/td>
&lt;td>&amp;ndash;0.034&lt;/td>
&lt;td>n/a&lt;/td>
&lt;td>&amp;ndash;0.022&lt;/td>
&lt;td>&amp;ndash;0.030&lt;/td>
&lt;td>&amp;ndash;0.031&lt;/td>
&lt;td>&amp;ndash;0.030&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>GDP terms robust?&lt;/strong>&lt;/td>
&lt;td>Yes (p &amp;lt; 0.05)&lt;/td>
&lt;td>No (p &amp;gt; 0.05)&lt;/td>
&lt;td>&lt;strong>No&lt;/strong> (PIP &amp;lt; 0.30)&lt;/td>
&lt;td>No (p &amp;gt; 0.05)&lt;/td>
&lt;td>&lt;strong>Yes&lt;/strong> (PIP &amp;gt; 0.99)&lt;/td>
&lt;td>Yes (p &amp;lt; 0.001)&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Controls selected&lt;/strong>&lt;/td>
&lt;td>n/a&lt;/td>
&lt;td>n/a&lt;/td>
&lt;td>1 of 12&lt;/td>
&lt;td>1 of 12&lt;/td>
&lt;td>6 of 12&lt;/td>
&lt;td>102 of 112&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Min TP&lt;/strong>&lt;/td>
&lt;td>\$1,913&lt;/td>
&lt;td>\$1,465&lt;/td>
&lt;td>n/a&lt;/td>
&lt;td>\$987&lt;/td>
&lt;td>\$2,411&lt;/td>
&lt;td>\$2,429&lt;/td>
&lt;td>\$1,895&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Max TP&lt;/strong>&lt;/td>
&lt;td>\$60,817&lt;/td>
&lt;td>\$61,655&lt;/td>
&lt;td>n/a&lt;/td>
&lt;td>\$62,983&lt;/td>
&lt;td>\$27,269&lt;/td>
&lt;td>\$27,672&lt;/td>
&lt;td>\$34,647&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;blockquote>
&lt;p>&lt;strong>Note.&lt;/strong> FD-BMA posterior means for the GDP terms are heavily shrunk toward zero (because their PIPs are ~0.27&amp;ndash;0.30), so we report &amp;ldquo;n/a&amp;rdquo; rather than misleading point estimates.&lt;/p>
&lt;/blockquote>
&lt;p>The comparison reveals a stark trade-off between the two identification strategies:&lt;/p>
&lt;p>&lt;strong>Fixed effects win on accuracy.&lt;/strong> The FE-based estimates are close to the true DGP values, with BMA (FE) achieving the best accuracy ($\beta_1 = -7.139$ vs true &amp;ndash;7.100). The FD estimates are noisier: FD-sparse overshoots ($\beta_1 = -10.36$), while FD-DSL undershoots (&amp;ndash;5.05). The FD turning points are wildly inaccurate &amp;mdash; the maximum turning point is \$61,000&amp;ndash;63,000 in first differences vs \$27,000 with FE (true: \$34,647).&lt;/p>
&lt;p>&lt;strong>First differences struggle with the cubic polynomial.&lt;/strong> Estimating a cubic EKC requires precise measurement of three highly correlated terms ($\ln GDP$, $(\ln GDP)^2$, $(\ln GDP)^3$). With only 80 observations (one 20-year change per country), the multicollinearity among differenced GDP terms is severe. Both BMA and DSL respond rationally: BMA gives all three terms PIPs below 0.30, and DSL selects zero controls for the GDP equations. Neither method &amp;ldquo;trusts&amp;rdquo; the cubic specification in this small sample.&lt;/p>
&lt;p>&lt;strong>Industry is the strongest cross-sectional signal.&lt;/strong> Both FD-BMA (PIP = 0.998) and FD-DSL (selected as the sole control) identify the change in industry share as the most important cross-sectional predictor of CO&lt;sub>2&lt;/sub> change. This makes economic sense: countries that industrialized the most over 1995&amp;ndash;2014 also increased their emissions the most, regardless of their income trajectory.&lt;/p>
&lt;p>&lt;strong>Practical implication.&lt;/strong> First differences are appropriate when the research question is about &lt;em>long-run changes&lt;/em> rather than &lt;em>levels&lt;/em>. But for testing the EKC cubic shape, the panel FE approach is far more powerful because it uses all 1,600 observations rather than collapsing to 80. The FD analysis confirms that the inverted-N result in the main body is robust to the identification strategy in spirit (the signs are correct in FD-sparse OLS), but the magnitudes and statistical power are substantially weaker.&lt;/p>
&lt;h2 id="references">References&lt;/h2>
&lt;ol>
&lt;li>&lt;a href="https://doi.org/10.1016/j.eneco.2025.108649" target="_blank" rel="noopener">Gravina, A. F. &amp;amp; Lanzafame, M. (2025). What&amp;rsquo;s your shape? Bayesian model averaging and double machine learning for the Environmental Kuznets Curve. &lt;em>Energy Economics&lt;/em>, 108649.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://doi.org/10.1002/jae.623" target="_blank" rel="noopener">Fernandez, C., Ley, E., &amp;amp; Steel, M. F. J. (2001). Model uncertainty in cross-country growth regressions. &lt;em>Journal of Applied Econometrics&lt;/em>, 16(5), 563&amp;ndash;576.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://doi.org/10.1093/restud/rdt044" target="_blank" rel="noopener">Belloni, A., Chernozhukov, V., &amp;amp; Hansen, C. (2014). Inference on treatment effects after selection among high-dimensional controls. &lt;em>Review of Economic Studies&lt;/em>, 81(2), 608&amp;ndash;650.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://doi.org/10.1080/01621459.1997.10473615" target="_blank" rel="noopener">Raftery, A. E., Madigan, D., &amp;amp; Hoeting, J. A. (1997). Bayesian model averaging for linear regression models. &lt;em>Journal of the American Statistical Association&lt;/em>, 92(437), 179&amp;ndash;191.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://doi.org/10.2307/271063" target="_blank" rel="noopener">Raftery, A. E. (1995). Bayesian model selection in social research. &lt;em>Sociological Methodology&lt;/em>, 25, 111&amp;ndash;163.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.stata.com/manuals/bmabmaregress.pdf" target="_blank" rel="noopener">Stata 18 Manual: &lt;code>bmaregress&lt;/code> &amp;mdash; Bayesian Model Averaging regression&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.stata.com/manuals/lassodsregress.pdf" target="_blank" rel="noopener">Stata 18 Manual: &lt;code>dsregress&lt;/code> &amp;mdash; Double-Selection LASSO linear regression&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>Three Methods for Robust Variable Selection: BMA, LASSO, and WALS</title><link>https://carlos-mendez.org/post/r_bma_lasso_wals/</link><pubDate>Mon, 23 Mar 2026 00:00:00 +0000</pubDate><guid>https://carlos-mendez.org/post/r_bma_lasso_wals/</guid><description>&lt;h2 id="1-overview">1. Overview&lt;/h2>
&lt;p>Imagine you are an economist advising a government on climate policy. Your team has collected cross-country data on a dozen potential drivers of CO&lt;sub>2&lt;/sub> emissions: GDP per capita, fossil fuel dependence, urbanization, industrial output, democratic governance, trade networks, agricultural activity, trade openness, foreign direct investment, corruption, tourism, and domestic credit. The government has a limited budget and wants to know: &lt;strong>which of these factors truly drive CO&lt;sub>2&lt;/sub> emissions, and which are red herrings?&lt;/strong>&lt;/p>
&lt;p>This is the &lt;strong>variable selection&lt;/strong> problem, and it is harder than it sounds. With 12 candidate variables, each either included or excluded from a regression, there are $2^{12} = 4,096$ possible models you could estimate. Run one model and report it as &amp;ldquo;the answer,&amp;rdquo; and you have implicitly assumed the other 4,095 models are wrong. That is a very strong assumption &amp;mdash; and almost certainly unjustified.&lt;/p>
&lt;p>In practice, researchers handle this by &lt;em>specification searching&lt;/em>: they try many models, drop insignificant variables, and report whichever specification &amp;ldquo;works best.&amp;rdquo; This process inflates false discoveries. A noise variable that happens to look significant in one specification gets reported, while the many failed specifications are hidden in the researcher&amp;rsquo;s desk drawer. This is sometimes called the &lt;strong>file drawer problem&lt;/strong> or &lt;strong>pretesting bias&lt;/strong>.&lt;/p>
&lt;p>This tutorial introduces three principled approaches to the variable selection problem:&lt;/p>
&lt;pre>&lt;code class="language-mermaid">graph TD
Q[&amp;quot;&amp;lt;b&amp;gt;Variable Selection&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Which of 12 variables&amp;lt;br/&amp;gt;truly matter?&amp;quot;] --&amp;gt; BMA
Q --&amp;gt; LASSO
Q --&amp;gt; WALS
BMA[&amp;quot;&amp;lt;b&amp;gt;BMA&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Bayesian Model Averaging&amp;lt;br/&amp;gt;PIPs from 4,096 models&amp;quot;] --&amp;gt; R[&amp;quot;&amp;lt;b&amp;gt;Convergence&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Variables identified&amp;lt;br/&amp;gt;by all 3 methods&amp;quot;]
LASSO[&amp;quot;&amp;lt;b&amp;gt;LASSO&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;L1 penalized regression&amp;lt;br/&amp;gt;Automatic selection&amp;quot;] --&amp;gt; R
WALS[&amp;quot;&amp;lt;b&amp;gt;WALS&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Frequentist averaging&amp;lt;br/&amp;gt;t-statistics&amp;quot;] --&amp;gt; R
style Q fill:#141413,stroke:#141413,color:#fff
style BMA fill:#6a9bcc,stroke:#141413,color:#fff
style LASSO fill:#d97757,stroke:#141413,color:#fff
style WALS fill:#00d4c8,stroke:#141413,color:#fff
style R fill:#1a3a8a,stroke:#141413,color:#fff
&lt;/code>&lt;/pre>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Bayesian Model Averaging (BMA)&lt;/strong>: Average across all 4,096 models, weighting each by how well it fits the data. Variables that appear important across many models earn a high &amp;ldquo;inclusion probability.&amp;rdquo;&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>LASSO (Least Absolute Shrinkage and Selection Operator)&lt;/strong>: Add a penalty to the regression that forces the coefficients of irrelevant variables to be &lt;em>exactly zero&lt;/em>, performing automatic selection.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Weighted Average Least Squares (WALS)&lt;/strong>: A fast frequentist model-averaging method that transforms the problem so each variable can be evaluated independently.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>We use &lt;strong>synthetic data&lt;/strong> throughout this tutorial. This means we &lt;em>know the true data-generating process&lt;/em> &amp;mdash; which variables truly matter and which do not. This &amp;ldquo;answer key&amp;rdquo; lets us verify whether each method correctly recovers the truth. By the end, you will understand not just &lt;em>how&lt;/em> to run each method, but &lt;em>why&lt;/em> it works and &lt;em>when&lt;/em> to prefer one over the others.&lt;/p>
&lt;p>&lt;strong>Learning objectives:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Understand the variable selection problem and why running a single model is insufficient when model uncertainty is large&lt;/li>
&lt;li>Implement Bayesian Model Averaging in R and interpret Posterior Inclusion Probabilities (PIPs)&lt;/li>
&lt;li>Apply LASSO with cross-validation to perform automatic variable selection and use Post-LASSO for unbiased estimation&lt;/li>
&lt;li>Run WALS as a fast frequentist model-averaging alternative and interpret its t-statistics&lt;/li>
&lt;li>Compare results across all three methods to identify truly robust determinants via methodological triangulation&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Content outline.&lt;/strong> Section 2 sets up the R environment. Section 3 introduces the synthetic dataset and its built-in &amp;ldquo;answer key&amp;rdquo; &amp;mdash; 7 true predictors and 5 noise variables with realistic multicollinearity. Section 4 runs naive OLS to illustrate the spurious significance problem. Sections 5&amp;ndash;8 cover BMA: Bayes' rule foundations, the PIP framework, a toy example, and full implementation. Sections 9&amp;ndash;12 cover LASSO: the bias-variance tradeoff, L1/L2 geometry, cross-validated implementation, and Post-LASSO. Sections 13&amp;ndash;16 cover WALS: frequentist model averaging, the semi-orthogonal transformation, the Laplace prior, and implementation. Section 17 brings all three methods together for a grand comparison. Section 18 summarizes key takeaways and provides further reading.&lt;/p>
&lt;h2 id="2-setup">2. Setup&lt;/h2>
&lt;p>Before running the analysis, install the required packages if needed. The following code checks for missing packages and installs them automatically.&lt;/p>
&lt;pre>&lt;code class="language-r"># List all packages needed for this tutorial
required_packages &amp;lt;- c(
&amp;quot;tidyverse&amp;quot;, # data manipulation and ggplot2 visualization
&amp;quot;BMS&amp;quot;, # Bayesian Model Averaging via the bms() function
&amp;quot;glmnet&amp;quot;, # LASSO and Ridge regression via coordinate descent
&amp;quot;WALS&amp;quot;, # Weighted Average Least Squares estimation
&amp;quot;scales&amp;quot;, # nice axis formatting in plots
&amp;quot;patchwork&amp;quot;, # combine multiple ggplot panels
&amp;quot;ggrepel&amp;quot;, # non-overlapping text labels on plots
&amp;quot;corrplot&amp;quot;, # correlation matrix heatmaps
&amp;quot;broom&amp;quot; # tidy model summaries
)
# Install any packages not yet available
missing &amp;lt;- required_packages[!sapply(required_packages, requireNamespace, quietly = TRUE)]
if (length(missing) &amp;gt; 0) {
install.packages(missing, repos = &amp;quot;https://cloud.r-project.org&amp;quot;)
}
# Load libraries
library(tidyverse)
library(BMS)
library(glmnet)
library(WALS)
library(scales)
library(patchwork)
library(ggrepel)
library(corrplot)
library(broom)
&lt;/code>&lt;/pre>
&lt;h2 id="3-the-synthetic-dataset">3. The Synthetic Dataset&lt;/h2>
&lt;h3 id="31-the-data-generating-process-our-answer-key">3.1 The data-generating process (our &amp;ldquo;answer key&amp;rdquo;)&lt;/h3>
&lt;p>We use a cross-sectional dataset of 120 fictional countries. The key design choices:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>7 variables have true nonzero effects&lt;/strong> on CO&lt;sub>2&lt;/sub> emissions&lt;/li>
&lt;li>&lt;strong>5 variables are pure noise&lt;/strong> (their true coefficients are exactly zero)&lt;/li>
&lt;li>The noise variables are &lt;strong>correlated with GDP and other true predictors&lt;/strong>, creating realistic multicollinearity. This makes variable selection genuinely challenging &amp;mdash; naive OLS will find spurious &amp;ldquo;significant&amp;rdquo; results for noise variables.&lt;/li>
&lt;/ul>
&lt;p>Think of this as setting up a controlled experiment. We know the answer before we begin, so we can grade each method&amp;rsquo;s performance.&lt;/p>
&lt;p>The data-generating process below shows exactly how the synthetic dataset was built. The CSV file &lt;code>synthetic-co2-cross-section.csv&lt;/code> was generated with &lt;code>set.seed(2017)&lt;/code> and can be loaded directly from GitHub for full reproducibility.&lt;/p>
&lt;pre>&lt;code class="language-r"># --- DATA-GENERATING PROCESS (reference) ---
set.seed(2017)
n &amp;lt;- 120 # number of &amp;quot;countries&amp;quot;
# GDP drives many other variables (realistic: richer countries
# have higher urbanization, more industry, etc.)
log_gdp &amp;lt;- rnorm(n, mean = 8.5, sd = 1.5)
# --- TRUE PREDICTORS (correlated with GDP) ---
fossil_fuel &amp;lt;- 30 + 3 * log_gdp + rnorm(n, 0, 10) # higher in richer countries
urban_pop &amp;lt;- 20 + 5 * log_gdp + rnorm(n, 0, 12) # increases with income
industry &amp;lt;- 15 + 1.5 * log_gdp + rnorm(n, 0, 6) # industry share
democracy &amp;lt;- 5 + 2 * log_gdp + rnorm(n, 0, 8) # democracy index
trade_network &amp;lt;- 0.2 + 0.05 * log_gdp + rnorm(n, 0, 0.15) # trade centrality
agriculture &amp;lt;- 40 - 3 * log_gdp + rnorm(n, 0, 8) # negatively correlated with GDP
# --- NOISE VARIABLES (correlated with GDP but NO true effect) ---
log_trade &amp;lt;- 3.5 + 0.1 * log_gdp + rnorm(n, 0, 0.5)
fdi &amp;lt;- 2 + rnorm(n, 0, 4)
corruption &amp;lt;- 0.8 - 0.05 * log_gdp + rnorm(n, 0, 0.15)
log_tourism &amp;lt;- 12 + 0.3 * log_gdp + rnorm(n, 0, 1.2)
log_credit &amp;lt;- 2.5 + 0.15 * log_gdp + rnorm(n, 0, 0.6)
# --- TRUE DATA-GENERATING PROCESS ---
log_co2 &amp;lt;- 2.0 + # intercept
1.200 * log_gdp + # GDP: strong positive (elasticity)
0.008 * industry + # industry: positive
0.012 * fossil_fuel + # fossil fuel: positive
0.010 * urban_pop + # urbanization: positive
0.004 * democracy + # democracy: small positive
0.500 * trade_network + # trade network: moderate positive
0.005 * agriculture + # agriculture: weak positive
# NOISE VARIABLES HAVE ZERO TRUE EFFECT
rnorm(n, 0, 0.3) # random noise (sigma = 0.3)
&lt;/code>&lt;/pre>
&lt;p>The true coefficients serve as our &amp;ldquo;answer key&amp;rdquo;:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:left">Variable&lt;/th>
&lt;th style="text-align:left">True $\beta$&lt;/th>
&lt;th style="text-align:left">Role&lt;/th>
&lt;th style="text-align:left">Interpretation&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:left">log_gdp&lt;/td>
&lt;td style="text-align:left">1.200&lt;/td>
&lt;td style="text-align:left">True predictor&lt;/td>
&lt;td style="text-align:left">1% more GDP $\to$ 1.2% more CO&lt;sub>2&lt;/sub>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">trade_network&lt;/td>
&lt;td style="text-align:left">0.500&lt;/td>
&lt;td style="text-align:left">True predictor&lt;/td>
&lt;td style="text-align:left">Moderate positive effect&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">fossil_fuel&lt;/td>
&lt;td style="text-align:left">0.012&lt;/td>
&lt;td style="text-align:left">True predictor&lt;/td>
&lt;td style="text-align:left">1 pp more fossil fuel $\to$ 1.2% more CO&lt;sub>2&lt;/sub>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">urban_pop&lt;/td>
&lt;td style="text-align:left">0.010&lt;/td>
&lt;td style="text-align:left">True predictor&lt;/td>
&lt;td style="text-align:left">1 pp more urbanization $\to$ 1.0% more CO&lt;sub>2&lt;/sub>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">industry&lt;/td>
&lt;td style="text-align:left">0.008&lt;/td>
&lt;td style="text-align:left">True predictor&lt;/td>
&lt;td style="text-align:left">Positive composition effect&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">agriculture&lt;/td>
&lt;td style="text-align:left">0.005&lt;/td>
&lt;td style="text-align:left">True predictor&lt;/td>
&lt;td style="text-align:left">Weak positive effect&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">democracy&lt;/td>
&lt;td style="text-align:left">0.004&lt;/td>
&lt;td style="text-align:left">True predictor&lt;/td>
&lt;td style="text-align:left">Small positive effect&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">log_trade&lt;/td>
&lt;td style="text-align:left">0&lt;/td>
&lt;td style="text-align:left">Noise&lt;/td>
&lt;td style="text-align:left">No true effect&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">fdi&lt;/td>
&lt;td style="text-align:left">0&lt;/td>
&lt;td style="text-align:left">Noise&lt;/td>
&lt;td style="text-align:left">No true effect&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">corruption&lt;/td>
&lt;td style="text-align:left">0&lt;/td>
&lt;td style="text-align:left">Noise&lt;/td>
&lt;td style="text-align:left">No true effect&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">log_tourism&lt;/td>
&lt;td style="text-align:left">0&lt;/td>
&lt;td style="text-align:left">Noise&lt;/td>
&lt;td style="text-align:left">No true effect&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">log_credit&lt;/td>
&lt;td style="text-align:left">0&lt;/td>
&lt;td style="text-align:left">Noise&lt;/td>
&lt;td style="text-align:left">No true effect&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Now let us load the pre-generated dataset:&lt;/p>
&lt;pre>&lt;code class="language-r"># Load the synthetic dataset directly from GitHub
DATA_URL &amp;lt;- &amp;quot;https://raw.githubusercontent.com/cmg777/starter-academic-v501/master/content/post/r_bma_lasso_wals/synthetic-co2-cross-section.csv&amp;quot;
synth_data &amp;lt;- read.csv(DATA_URL)
cat(&amp;quot;Dataset:&amp;quot;, nrow(synth_data), &amp;quot;countries,&amp;quot;, ncol(synth_data), &amp;quot;variables\n&amp;quot;)
head(synth_data)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Dataset: 120 countries, 14 variables
country log_co2 log_gdp industry fossil_fuel urban_pop democracy trade_network
1 Country_001 13.27 9.47 29.25 66.94 67.97 25.67 0.77
2 Country_002 12.18 8.44 24.97 51.43 66.14 20.51 0.85
3 Country_003 13.50 10.16 28.19 50.62 73.91 29.08 0.73
...
&lt;/code>&lt;/pre>
&lt;h3 id="32-descriptive-statistics">3.2 Descriptive statistics&lt;/h3>
&lt;p>The following summary statistics give us a first look at the data structure. Note the wide range of scales: GDP is in log units (mean around 8.5), while percentage variables like fossil fuel share and urbanization range from single digits to near 100.&lt;/p>
&lt;pre>&lt;code class="language-r"># Descriptive statistics for all 13 numeric variables
synth_data |&amp;gt;
select(-country) |&amp;gt;
pivot_longer(everything(), names_to = &amp;quot;variable&amp;quot;, values_to = &amp;quot;value&amp;quot;) |&amp;gt;
summarise(
n = n(),
mean = round(mean(value), 2),
sd = round(sd(value), 2),
min = round(min(value), 2),
max = round(max(value), 2),
.by = variable
)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> variable n mean sd min max
log_co2 120 14.22 2.11 8.76 20.36
log_gdp 120 8.53 1.57 4.61 13.21
industry 120 27.87 6.21 8.32 44.98
fossil_fuel 120 55.49 9.62 24.72 81.22
urban_pop 120 62.52 13.25 29.81 97.62
democracy 120 22.94 8.32 3.10 45.00
trade_network 120 0.64 0.17 0.18 1.04
agriculture 120 13.87 8.11 1.00 37.11
log_trade 120 4.43 0.46 3.45 5.84
fdi 120 2.23 4.19 -5.00 13.62
corruption 120 0.37 0.16 0.05 0.71
log_tourism 120 14.61 1.32 11.54 19.63
log_credit 120 3.83 0.65 2.30 5.50
&lt;/code>&lt;/pre>
&lt;p>The dataset has 120 observations and 14 variables (1 dependent, 12 candidate regressors, 1 country identifier). The dependent variable &lt;code>log_co2&lt;/code> has a mean of 14.22 with a standard deviation of 2.11 log points, reflecting substantial cross-country variation in emissions. The candidate regressors span very different scales &amp;mdash; trade_network ranges from 0.18 to 1.04, while urban_pop ranges from 29.8 to 97.6 &amp;mdash; which is why BMA, LASSO, and WALS each handle scaling internally.&lt;/p>
&lt;h3 id="33-correlation-structure">3.3 Correlation structure&lt;/h3>
&lt;p>A key feature of our synthetic data is that the noise variables are correlated with the true predictors &amp;mdash; especially with GDP. This correlation is what makes variable selection difficult: in a standard OLS regression, the noise variables will &amp;ldquo;borrow&amp;rdquo; explanatory power from the true predictors.&lt;/p>
&lt;pre>&lt;code class="language-r"># Compute correlation matrix for all 12 candidate regressors
cor_matrix &amp;lt;- synth_data |&amp;gt;
select(-country, -log_co2) |&amp;gt;
cor()
# Draw the heatmap
corrplot(cor_matrix, method = &amp;quot;color&amp;quot;, type = &amp;quot;lower&amp;quot;,
addCoef.col = &amp;quot;black&amp;quot;, number.cex = 0.7,
col = colorRampPalette(c(&amp;quot;#d97757&amp;quot;, &amp;quot;white&amp;quot;, &amp;quot;#6a9bcc&amp;quot;))(200),
diag = FALSE)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="bma_lasso_wals_01_correlation.png" alt="Correlation matrix heatmap showing that noise variables like trade openness, tourism, and credit are correlated with GDP and other true predictors, creating the multicollinearity that makes variable selection challenging.">&lt;/p>
&lt;p>The correlation heatmap reveals the realistic structure we built into the data. GDP is positively correlated with fossil fuel use, urbanization, industry, and the trade network &amp;mdash; but also with the noise variables like trade openness, tourism, and credit. This multicollinearity is precisely what makes a naive &amp;ldquo;throw everything into OLS&amp;rdquo; approach unreliable. For example, log_tourism has a correlation of approximately 0.3 with log_gdp, which means it can pick up GDP&amp;rsquo;s signal even though its true effect is zero.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Note.&lt;/strong> We created a synthetic dataset where we &lt;em>know&lt;/em> which 7 variables truly affect CO&lt;sub>2&lt;/sub> emissions and which 5 are noise. The noise variables are deliberately correlated with the true predictors, mimicking the multicollinearity found in real cross-country data.&lt;/p>
&lt;/blockquote>
&lt;h2 id="4-the-general-model">4. The General Model&lt;/h2>
&lt;p>Our goal is to estimate the following linear model:&lt;/p>
&lt;p>$$
\log(\text{CO}_{2,i}) = \beta_0 + \sum_{j=1}^{12} \beta_j x_{j,i} + \varepsilon_i
$$&lt;/p>
&lt;p>where:&lt;/p>
&lt;ul>
&lt;li>$\log(\text{CO}_{2,i})$ is the log of CO&lt;sub>2&lt;/sub> emissions for country $i$&lt;/li>
&lt;li>$\beta_0$ is the &lt;strong>intercept&lt;/strong> (the predicted log CO&lt;sub>2&lt;/sub> when all regressors are zero)&lt;/li>
&lt;li>$\beta_j$ is the &lt;strong>coefficient&lt;/strong> on the $j$-th regressor: the change in log CO&lt;sub>2&lt;/sub> associated with a one-unit increase in $x_j$, holding all other variables constant&lt;/li>
&lt;li>$\varepsilon_i$ is the &lt;strong>error term&lt;/strong>: everything that affects CO&lt;sub>2&lt;/sub> emissions but is not captured by the 12 regressors&lt;/li>
&lt;/ul>
&lt;p>Because the dependent variable is in logs, the interpretation of each coefficient depends on whether the regressor is also in logs:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:left">Regressor type&lt;/th>
&lt;th style="text-align:left">Interpretation of $\beta_j$&lt;/th>
&lt;th style="text-align:left">Example&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:left">Log-log (e.g., log GDP)&lt;/td>
&lt;td style="text-align:left">&lt;strong>Elasticity&lt;/strong>: a 1% increase in GDP is associated with a $\beta_j$% change in CO&lt;sub>2&lt;/sub>&lt;/td>
&lt;td style="text-align:left">$\beta = 1.2$ means 1% more GDP $\to$ 1.2% more CO&lt;sub>2&lt;/sub>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">Level-log (e.g., fossil fuel %)&lt;/td>
&lt;td style="text-align:left">&lt;strong>Semi-elasticity&lt;/strong>: a 1-unit increase in the regressor is associated with a $100 \times \beta_j$% change in CO&lt;sub>2&lt;/sub>&lt;/td>
&lt;td style="text-align:left">$\beta = 0.012$ means 1 pp more fossil fuel $\to$ 1.2% more CO&lt;sub>2&lt;/sub>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>We want to determine &lt;strong>which $\beta_j$ are truly nonzero&lt;/strong>. We know the answer (we designed the data), but let us first see what happens if we just run OLS with all 12 variables.&lt;/p>
&lt;pre>&lt;code class="language-r"># Run OLS with all 12 candidate regressors
ols_full &amp;lt;- lm(log_co2 ~ log_gdp + industry + fossil_fuel + urban_pop +
democracy + trade_network + agriculture +
log_trade + fdi + corruption + log_tourism + log_credit,
data = synth_data)
# Display summary
summary(ols_full)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Coefficients:
Estimate Std. Error t value Pr(&amp;gt;|t|)
(Intercept) 2.283773 0.494736 4.616 1.06e-05 ***
log_gdp 1.163669 0.032747 35.537 &amp;lt; 2e-16 ***
industry 0.017577 0.005004 3.513 0.000661 ***
fossil_fuel 0.011988 0.003240 3.698 0.000349 ***
urban_pop 0.008221 0.002689 3.057 0.002794 **
democracy 0.010497 0.003975 2.640 0.009549 **
trade_network 0.912828 0.203681 4.482 1.94e-05 ***
agriculture -0.000629 0.004242 -0.148 0.882568
log_trade -0.055738 0.064829 -0.860 0.391509
fdi 0.000789 0.007045 0.112 0.910964
corruption 0.010767 0.201954 0.053 0.957573
log_tourism -0.028025 0.024415 -1.148 0.253610
log_credit 0.045689 0.049690 0.919 0.360252
---
Multiple R-squared: 0.9801, Adjusted R-squared: 0.9779
&lt;/code>&lt;/pre>
&lt;p>Look carefully at the noise variables. For example, log_trade has a t-statistic of $-0.86$ (p = 0.392) and corruption has a t-statistic of $0.05$ (p = 0.958). None reach conventional significance in this sample. However, their estimated coefficients can be non-negligible in magnitude &amp;mdash; and in a different random sample, some noise variables could easily cross the 5% threshold. This is the risk of &lt;strong>spurious significance&lt;/strong>, caused by the correlation between noise variables and the true predictors. It is precisely this problem that motivates the three methods we study next.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Warning.&lt;/strong> With 12 correlated regressors and only 120 observations, OLS can produce misleading significance levels. A variable with a true coefficient of zero may appear significant simply because it is correlated with a genuinely important predictor. This is why we need principled variable selection methods.&lt;/p>
&lt;/blockquote>
&lt;div style="background: linear-gradient(135deg, #6a9bcc 0%, #00d4c8 100%); padding: 1.5em 2em; border-radius: 8px; margin: 2em 0; color: #fff; font-size: 1.3em; font-weight: 600;">
PART 1: Bayesian Model Averaging
&lt;/div>
&lt;h2 id="5-bayes-rule-----the-foundation">5. Bayes' Rule &amp;mdash; The Foundation&lt;/h2>
&lt;p>Before we can understand Bayesian Model Averaging, we need to understand &lt;strong>Bayes' rule&lt;/strong> &amp;mdash; the mathematical machinery that powers the entire framework.&lt;/p>
&lt;h3 id="51-a-coin-flip-example">5.1 A coin-flip example&lt;/h3>
&lt;p>Suppose a friend gives you a coin. You want to know: &lt;strong>is this coin fair&lt;/strong> (probability of heads = 0.5), or is it &lt;strong>biased&lt;/strong> (probability of heads = 0.7)?&lt;/p>
&lt;p>Before flipping, you have no strong opinion. You assign equal &lt;strong>prior probabilities&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>$P(\text{fair}) = 0.5$ (50% chance the coin is fair)&lt;/li>
&lt;li>$P(\text{biased}) = 0.5$ (50% chance the coin is biased)&lt;/li>
&lt;/ul>
&lt;p>Now you flip the coin 10 times and observe &lt;strong>7 heads&lt;/strong>. How should you update your beliefs?&lt;/p>
&lt;p>The &lt;strong>likelihood&lt;/strong> of seeing 7 heads in 10 flips is:&lt;/p>
&lt;ul>
&lt;li>If the coin is fair ($p = 0.5$): $P(\text{7 heads} | \text{fair}) = \binom{10}{7} (0.5)^{10} = 0.1172$&lt;/li>
&lt;li>If the coin is biased ($p = 0.7$): $P(\text{7 heads} | \text{biased}) = \binom{10}{7} (0.7)^7 (0.3)^3 = 0.2668$&lt;/li>
&lt;/ul>
&lt;p>The biased coin makes the data more likely. Bayes' rule combines the prior and the likelihood:&lt;/p>
&lt;p>$$
P(H|D) = \frac{P(D|H) \cdot P(H)}{P(D)}
$$&lt;/p>
&lt;p>where:&lt;/p>
&lt;ul>
&lt;li>$P(H|D)$ = &lt;strong>posterior probability&lt;/strong> (what we believe &lt;em>after&lt;/em> seeing the data)&lt;/li>
&lt;li>$P(D|H)$ = &lt;strong>likelihood&lt;/strong> (how probable the data is under hypothesis $H$)&lt;/li>
&lt;li>$P(H)$ = &lt;strong>prior probability&lt;/strong> (what we believed &lt;em>before&lt;/em> seeing the data)&lt;/li>
&lt;li>$P(D)$ = &lt;strong>marginal likelihood&lt;/strong> (a normalizing constant that ensures probabilities sum to 1)&lt;/li>
&lt;/ul>
&lt;p>For our coin:&lt;/p>
&lt;p>$$
P(\text{fair}|\text{7H}) = \frac{0.1172 \times 0.5}{0.1172 \times 0.5 + 0.2668 \times 0.5} = \frac{0.0586}{0.1920} = 0.305
$$&lt;/p>
&lt;p>$$
P(\text{biased}|\text{7H}) = \frac{0.2668 \times 0.5}{0.1920} = 0.695
$$&lt;/p>
&lt;p>After seeing 7 heads, we update from 50&amp;ndash;50 to roughly 30&amp;ndash;70 in favor of the biased coin. &lt;strong>The data shifted our beliefs, but did not erase the prior entirely.&lt;/strong>&lt;/p>
&lt;h3 id="52-the-bridge-to-model-averaging">5.2 The bridge to model averaging&lt;/h3>
&lt;p>Now replace &amp;ldquo;fair coin&amp;rdquo; and &amp;ldquo;biased coin&amp;rdquo; with &lt;em>regression models&lt;/em>:&lt;/p>
&lt;ul>
&lt;li>Hypothesis = &amp;ldquo;Which variables belong in the model?&amp;rdquo;&lt;/li>
&lt;li>Prior = &amp;ldquo;Before seeing data, any combination of variables is equally plausible&amp;rdquo;&lt;/li>
&lt;li>Likelihood = &amp;ldquo;How well does each model fit the data?&amp;rdquo;&lt;/li>
&lt;li>Posterior = &amp;ldquo;After seeing data, which models are most credible?&amp;rdquo;&lt;/li>
&lt;/ul>
&lt;p>This is exactly what BMA does. Instead of two coin hypotheses, we have 4,096 model hypotheses &amp;mdash; but the logic of Bayes' rule is identical.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Note.&lt;/strong> Bayes' rule updates prior beliefs using data. The posterior probability of any hypothesis is proportional to its prior probability times its likelihood. BMA applies this same logic to regression models instead of coin flips.&lt;/p>
&lt;/blockquote>
&lt;h2 id="6-the-bma-framework">6. The BMA Framework&lt;/h2>
&lt;h3 id="61-posterior-model-probability">6.1 Posterior model probability&lt;/h3>
&lt;p>With 12 candidate variables, there are $K = 12$ regressors and $2^K = 4,096$ possible models. Denote the $k$-th model as $M_k$. BMA assigns each model a &lt;strong>posterior probability&lt;/strong>:&lt;/p>
&lt;p>$$
P(M_k | y) = \frac{P(y | M_k) \cdot P(M_k)}{\sum_{l=1}^{2^K} P(y | M_l) \cdot P(M_l)}
$$&lt;/p>
&lt;p>This is just Bayes' rule applied to models. Let us unpack each piece:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>$P(y | M_k)$&lt;/strong> is the &lt;strong>marginal likelihood&lt;/strong> of model $M_k$. It measures how well the model fits the data, &lt;em>automatically penalizing complexity&lt;/em>. A model with many parameters can fit the data closely, but the marginal likelihood integrates over all possible parameter values, spreading the probability thin. This acts as a built-in &lt;strong>Occam&amp;rsquo;s razor&lt;/strong>: simpler models that fit the data well receive higher marginal likelihoods than complex models that fit only slightly better.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>$P(M_k)$&lt;/strong> is the &lt;strong>prior model probability&lt;/strong>. With no prior information, we use a &lt;strong>uniform prior&lt;/strong>: every model is equally likely, so $P(M_k) = 1/4,096$ for all $k$. This means the posterior is driven entirely by the data.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>The &lt;strong>denominator&lt;/strong> is a normalizing constant that ensures all posterior model probabilities sum to 1.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h3 id="62-posterior-inclusion-probability-pip">6.2 Posterior Inclusion Probability (PIP)&lt;/h3>
&lt;p>We do not really care about individual models &amp;mdash; we care about individual &lt;em>variables&lt;/em>. The &lt;strong>Posterior Inclusion Probability&lt;/strong> of variable $j$ is the sum of the posterior probabilities of all models that include variable $j$:&lt;/p>
&lt;p>$$
\text{PIP}_j = \sum_{k:\, j \in M_k} P(M_k | y)
$$&lt;/p>
&lt;p>Think of it as a &lt;strong>democratic vote&lt;/strong>. Each of the 4,096 models casts a vote for which variables matter. But the votes are &lt;em>weighted&lt;/em>: models that fit the data well get louder voices. If variable $j$ appears in most of the high-probability models, it earns a high PIP.&lt;/p>
&lt;p>The standard interpretation thresholds (Raftery, 1995):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:left">PIP range&lt;/th>
&lt;th style="text-align:left">Interpretation&lt;/th>
&lt;th style="text-align:left">Analogy&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:left">$\geq 0.99$&lt;/td>
&lt;td style="text-align:left">Decisive evidence&lt;/td>
&lt;td style="text-align:left">Beyond reasonable doubt&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">$0.95 - 0.99$&lt;/td>
&lt;td style="text-align:left">Very strong evidence&lt;/td>
&lt;td style="text-align:left">Strong consensus&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">$0.80 - 0.95$&lt;/td>
&lt;td style="text-align:left">Strong evidence (robust)&lt;/td>
&lt;td style="text-align:left">Clear majority&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">$0.50 - 0.80$&lt;/td>
&lt;td style="text-align:left">Borderline evidence&lt;/td>
&lt;td style="text-align:left">Split vote&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">$&amp;lt; 0.50$&lt;/td>
&lt;td style="text-align:left">Weak/no evidence (fragile)&lt;/td>
&lt;td style="text-align:left">Minority opinion&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>We will use &lt;strong>PIP $\geq$ 0.80&lt;/strong> as our threshold for &amp;ldquo;robust&amp;rdquo; throughout this tutorial.&lt;/p>
&lt;h3 id="63-posterior-mean">6.3 Posterior mean&lt;/h3>
&lt;p>Once we know which variables matter, we want to know &lt;em>how much&lt;/em> they matter. The &lt;strong>posterior mean&lt;/strong> of coefficient $j$ is:&lt;/p>
&lt;p>$$
E[\beta_j | y] = \sum_{k=1}^{2^K} \hat{\beta}_{j,k} \cdot P(M_k | y)
$$&lt;/p>
&lt;p>where $\hat{\beta}_{j,k}$ is the estimated coefficient of variable $j$ in model $k$ (and zero if $j$ is not in model $k$). This is a weighted average of the coefficient across all models. Variables with high PIPs get posterior means close to their &amp;ldquo;full model&amp;rdquo; estimates; variables with low PIPs get posterior means shrunk toward zero.&lt;/p>
&lt;h2 id="7-toy-example-----bma-on-3-variables">7. Toy Example &amp;mdash; BMA on 3 Variables&lt;/h2>
&lt;p>Before running BMA on all 12 variables, let us work through a small example by hand. We pick just 3 variables: &lt;strong>log_gdp&lt;/strong> and &lt;strong>fossil_fuel&lt;/strong> (true predictors) and &lt;strong>log_trade&lt;/strong> (noise). With 3 variables, each can be either IN or OUT of the model, giving us $2^3 = 8$ possible models &amp;mdash; small enough to examine every single one.&lt;/p>
&lt;p>Here are all 8 models written out explicitly:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:left">Model&lt;/th>
&lt;th style="text-align:left">Formula&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:left">$M_1$&lt;/td>
&lt;td style="text-align:left">log_co2 $\sim$ 1 (intercept only)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">$M_2$&lt;/td>
&lt;td style="text-align:left">log_co2 $\sim$ log_gdp&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">$M_3$&lt;/td>
&lt;td style="text-align:left">log_co2 $\sim$ fossil_fuel&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">$M_4$&lt;/td>
&lt;td style="text-align:left">log_co2 $\sim$ log_trade&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">$M_5$&lt;/td>
&lt;td style="text-align:left">log_co2 $\sim$ log_gdp + fossil_fuel&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">$M_6$&lt;/td>
&lt;td style="text-align:left">log_co2 $\sim$ log_gdp + log_trade&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">$M_7$&lt;/td>
&lt;td style="text-align:left">log_co2 $\sim$ fossil_fuel + log_trade&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">$M_8$&lt;/td>
&lt;td style="text-align:left">log_co2 $\sim$ log_gdp + fossil_fuel + log_trade&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="71-step-1-----fit-every-model-and-compute-bic">7.1 Step 1 &amp;mdash; Fit every model and compute BIC&lt;/h3>
&lt;p>We fit each of the 8 models using OLS and compute its BIC score. Remember: &lt;strong>lower BIC = better&lt;/strong> (the model explains the data well without unnecessary complexity).&lt;/p>
&lt;pre>&lt;code class="language-r"># Select our 3 variables
toy_data &amp;lt;- synth_data |&amp;gt;
select(log_co2, log_gdp, fossil_fuel, log_trade)
# Write out all 8 model formulas explicitly
model_formulas &amp;lt;- c(
&amp;quot;log_co2 ~ 1&amp;quot;, # M1: intercept only
&amp;quot;log_co2 ~ log_gdp&amp;quot;, # M2
&amp;quot;log_co2 ~ fossil_fuel&amp;quot;, # M3
&amp;quot;log_co2 ~ log_trade&amp;quot;, # M4
&amp;quot;log_co2 ~ log_gdp + fossil_fuel&amp;quot;, # M5
&amp;quot;log_co2 ~ log_gdp + log_trade&amp;quot;, # M6
&amp;quot;log_co2 ~ fossil_fuel + log_trade&amp;quot;, # M7
&amp;quot;log_co2 ~ log_gdp + fossil_fuel + log_trade&amp;quot; # M8
)
# Fit each model and extract its BIC
bic_values &amp;lt;- sapply(model_formulas, function(f) {
BIC(lm(as.formula(f), data = toy_data))
})
# Organize results in a table
toy_results &amp;lt;- tibble(
model = paste0(&amp;quot;M&amp;quot;, 1:8),
formula = model_formulas,
bic = round(bic_values, 1)
) |&amp;gt;
arrange(bic)
print(toy_results)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> model formula bic
M5 log_co2 ~ log_gdp + fossil_fuel 114.1
M8 log_co2 ~ log_gdp + fossil_fuel + log_trade 118.5
M2 log_co2 ~ log_gdp 120.7
M6 log_co2 ~ log_gdp + log_trade 125.4
M3 log_co2 ~ fossil_fuel 514.4
M7 log_co2 ~ fossil_fuel + log_trade 519.0
M1 log_co2 ~ 1 528.3
M4 log_co2 ~ log_trade 533.0
&lt;/code>&lt;/pre>
&lt;p>The winner is $M_5$ (log_gdp + fossil_fuel) with BIC = 114.1 &amp;mdash; exactly the two true predictors, no noise. The runner-up $M_8$ adds log_trade but its BIC is worse (118.5), meaning the extra variable does not improve the fit enough to justify the added complexity. Models without GDP ($M_1$, $M_3$, $M_4$, $M_7$) have dramatically worse BIC scores, confirming GDP&amp;rsquo;s dominant role.&lt;/p>
&lt;h3 id="72-step-2-----convert-bic-to-posterior-probabilities">7.2 Step 2 &amp;mdash; Convert BIC to posterior probabilities&lt;/h3>
&lt;p>Now we turn each BIC into a posterior model probability. The formula is:&lt;/p>
&lt;p>$$
P(M_k | y) = \frac{\exp(-0.5 \cdot \text{BIC}_k)}{\sum_{l=1}^{8} \exp(-0.5 \cdot \text{BIC}_l)}
$$&lt;/p>
&lt;p>Because the BIC values can be very large, we work with &lt;strong>differences from the best model&lt;/strong> to avoid numerical overflow. Subtracting the minimum BIC from all values does not change the probabilities:&lt;/p>
&lt;p>$$
P(M_k | y) = \frac{\exp\bigl(-0.5 \cdot (\text{BIC}_k - \text{BIC}_{\min})\bigr)}{\sum_{l=1}^{8} \exp\bigl(-0.5 \cdot (\text{BIC}_l - \text{BIC}_{\min})\bigr)}
$$&lt;/p>
&lt;p>Let us plug in the numbers. The best model ($M_5$) has BIC = 114.1, so $\Delta_5 = 0$. The runner-up ($M_8$) has $\Delta_8 = 118.5 - 114.1 = 4.4$:&lt;/p>
&lt;p>$$
w_5 = \exp(-0.5 \times 0) = 1.000, \quad w_8 = \exp(-0.5 \times 4.4) = 0.111
$$&lt;/p>
&lt;p>The remaining models have much larger $\Delta$ values, so their weights are essentially zero. After normalizing by the sum of all weights ($1.000 + 0.111 + 0.037 + \ldots \approx 1.151$):&lt;/p>
&lt;p>$$
P(M_5 | y) = \frac{1.000}{1.151} = 0.869, \quad P(M_8 | y) = \frac{0.111}{1.151} = 0.096
$$&lt;/p>
&lt;pre>&lt;code class="language-r"># Convert BIC to posterior probabilities using the delta-BIC trick
toy_results &amp;lt;- toy_results |&amp;gt;
mutate(
delta_bic = bic - min(bic), # difference from best
weight = exp(-0.5 * delta_bic), # unnormalized weight
post_prob = round(weight / sum(weight), 4) # normalize to sum to 1
)
toy_results |&amp;gt; select(model, bic, delta_bic, weight, post_prob)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> model bic delta_bic weight post_prob
M5 114.1 0.0 1.0000 0.8687
M8 118.5 4.4 0.1108 0.0962
M2 120.7 6.6 0.0369 0.0320
M6 125.4 11.3 0.0035 0.0031
M3 514.4 400.3 0.0000 0.0000
M7 519.0 404.9 0.0000 0.0000
M1 528.3 414.2 0.0000 0.0000
M4 533.0 418.9 0.0000 0.0000
&lt;/code>&lt;/pre>
&lt;p>One model dominates: $M_5$ captures 86.9% of the posterior probability &amp;mdash; exactly the two true predictors. The runner-up $M_8$ (adding log_trade) gets only 9.6%, and $M_2$ (GDP alone) gets 3.2%. The remaining 5 models share less than 0.4% of the total weight. BMA&amp;rsquo;s Occam&amp;rsquo;s razor is at work: adding log_trade to the model ($M_8$) does not improve the fit enough to overcome the complexity penalty, so the simpler model ($M_5$) wins decisively.&lt;/p>
&lt;h3 id="73-step-3-----compute-posterior-inclusion-probabilities">7.3 Step 3 &amp;mdash; Compute Posterior Inclusion Probabilities&lt;/h3>
&lt;p>Finally, we compute the PIP of each variable by summing the posterior probabilities of all models that include it. For example, log_trade appears in models $M_4$, $M_6$, $M_7$, and $M_8$, so:&lt;/p>
&lt;p>$$
\text{PIP}_{\text{log_trade}} = P(M_4 | y) + P(M_6 | y) + P(M_7 | y) + P(M_8 | y) = 0.000 + 0.003 + 0.000 + 0.096 = 0.099
$$&lt;/p>
&lt;p>That is well below the 0.50 threshold &amp;mdash; fragile evidence, exactly what we expect for a noise variable.&lt;/p>
&lt;pre>&lt;code class="language-r"># Compute PIPs: for each variable, sum P(M|y) across models that include it
pip_toy &amp;lt;- tibble(
variable = c(&amp;quot;log_gdp&amp;quot;, &amp;quot;fossil_fuel&amp;quot;, &amp;quot;log_trade&amp;quot;),
true_effect = c(&amp;quot;True&amp;quot;, &amp;quot;True&amp;quot;, &amp;quot;Noise&amp;quot;),
pip = c(
# log_gdp appears in M2, M5, M6, M8
sum(toy_results$post_prob[toy_results$model %in% c(&amp;quot;M2&amp;quot;,&amp;quot;M5&amp;quot;,&amp;quot;M6&amp;quot;,&amp;quot;M8&amp;quot;)]),
# fossil_fuel appears in M3, M5, M7, M8
sum(toy_results$post_prob[toy_results$model %in% c(&amp;quot;M3&amp;quot;,&amp;quot;M5&amp;quot;,&amp;quot;M7&amp;quot;,&amp;quot;M8&amp;quot;)]),
# log_trade appears in M4, M6, M7, M8
sum(toy_results$post_prob[toy_results$model %in% c(&amp;quot;M4&amp;quot;,&amp;quot;M6&amp;quot;,&amp;quot;M7&amp;quot;,&amp;quot;M8&amp;quot;)])
)
)
print(pip_toy)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> variable true_effect pip
log_gdp True 1.000
fossil_fuel True 0.965
log_trade Noise 0.099
&lt;/code>&lt;/pre>
&lt;p>Even with this simple 3-variable example, BMA correctly identifies the two true predictors. GDP has a PIP of 1.000 (decisive evidence) and fossil_fuel has a PIP of 0.965 (robust) &amp;mdash; they appear in every high-probability model. Log_trade has a PIP of only 0.099 (fragile) &amp;mdash; well below the 0.50 threshold. BMA&amp;rsquo;s built-in Occam&amp;rsquo;s razor penalizes models that include noise variables without substantially improving the fit.&lt;/p>
&lt;h2 id="8-bma-on-all-12-variables">8. BMA on All 12 Variables&lt;/h2>
&lt;h3 id="81-running-bma">8.1 Running BMA&lt;/h3>
&lt;p>Now we apply BMA to the full dataset with all 12 candidate regressors using the &lt;code>BMS&lt;/code> package. Because 4,096 models is computationally manageable, the MCMC sampler explores the full model space efficiently.&lt;/p>
&lt;pre>&lt;code class="language-r">set.seed(2021) # reproducibility for MCMC sampling
# Prepare the data matrix: DV in first column, regressors follow
bma_data &amp;lt;- synth_data |&amp;gt;
select(log_co2, log_gdp, industry, fossil_fuel, urban_pop,
democracy, trade_network, agriculture,
log_trade, fdi, corruption, log_tourism, log_credit) |&amp;gt;
as.data.frame()
# Run BMA
bma_fit &amp;lt;- bms(
X.data = bma_data, # data with DV in column 1
burn = 50000, # burn-in iterations
iter = 200000, # post-burn-in iterations
g = &amp;quot;BRIC&amp;quot;, # BRIC g-prior (robust default)
mprior = &amp;quot;uniform&amp;quot;, # uniform model prior
nmodel = 2000, # store top 2000 models
mcmc = &amp;quot;bd&amp;quot;, # birth-death MCMC sampler
user.int = FALSE # suppress interactive output
)
&lt;/code>&lt;/pre>
&lt;p>The key parameters deserve explanation:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>burn = 50,000&lt;/strong>: the first 50,000 MCMC draws are discarded as &amp;ldquo;burn-in&amp;rdquo; to ensure the sampler has converged to the posterior distribution&lt;/li>
&lt;li>&lt;strong>iter = 200,000&lt;/strong>: the next 200,000 draws are used for inference&lt;/li>
&lt;li>&lt;strong>g = &amp;ldquo;BRIC&amp;rdquo;&lt;/strong>: the Benchmark Risk Inflation Criterion prior on the regression coefficients, a robust default choice&lt;/li>
&lt;li>&lt;strong>mprior = &amp;ldquo;uniform&amp;rdquo;&lt;/strong>: every model is equally likely a priori, so the posterior is driven entirely by the data&lt;/li>
&lt;/ul>
&lt;h3 id="82-pip-bar-chart">8.2 PIP bar chart&lt;/h3>
&lt;p>The PIP bar chart classifies each variable as robust (PIP $\geq$ 0.80), borderline (0.50&amp;ndash;0.80), or fragile (PIP $&amp;lt;$ 0.50). This visualization makes it easy to see which variables earn strong support across the model space and which are effectively irrelevant.&lt;/p>
&lt;pre>&lt;code class="language-r"># Extract PIPs and posterior means
bma_coefs &amp;lt;- coef(bma_fit)
bma_df &amp;lt;- as.data.frame(bma_coefs) |&amp;gt;
rownames_to_column(&amp;quot;variable&amp;quot;) |&amp;gt;
as_tibble() |&amp;gt;
rename(pip = PIP, post_mean = `Post Mean`, post_sd = `Post SD`) |&amp;gt;
select(variable, pip, post_mean, post_sd) |&amp;gt;
mutate(
true_beta = true_beta_lookup[variable],
robustness = case_when(
pip &amp;gt;= 0.80 ~ &amp;quot;Robust (PIP &amp;gt;= 0.80)&amp;quot;,
pip &amp;gt;= 0.50 ~ &amp;quot;Borderline&amp;quot;,
TRUE ~ &amp;quot;Fragile (PIP &amp;lt; 0.50)&amp;quot;
),
ci_low = post_mean - 2 * post_sd,
ci_high = post_mean + 2 * post_sd
)
# Plot PIPs
ggplot(bma_df, aes(x = reorder(variable, pip), y = pip, fill = robustness)) +
geom_col(width = 0.65) +
geom_hline(yintercept = 0.80, linetype = &amp;quot;dashed&amp;quot;) +
coord_flip() +
labs(x = NULL, y = &amp;quot;Posterior Inclusion Probability (PIP)&amp;quot;)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="bma_lasso_wals_04_bma_pip.png" alt="BMA Posterior Inclusion Probabilities. Green bars indicate robust variables with PIP greater than or equal to 0.80; teal bars indicate borderline variables; orange bars indicate fragile variables with PIP less than 0.50.">&lt;/p>
&lt;p>The PIP bar chart reveals a clear separation between signal and noise. GDP dominates with a PIP of 1.00, followed by trade_network (0.986), fossil_fuel (0.948), and industry (0.841) &amp;mdash; all with PIPs above the 0.80 robustness threshold. The noise variables (log_trade, fdi, corruption, log_tourism, log_credit) all have PIPs well below 0.15, confirming that BMA correctly classifies them as fragile. Urban_pop ($\beta = 0.010$, PIP = 0.648) and democracy ($\beta = 0.004$, PIP = 0.607) land in the borderline range &amp;mdash; true predictors whose effects are moderate enough that BMA hedges between including and excluding them. Agriculture ($\beta = 0.005$, PIP = 0.087) is classified as fragile, an honest reflection of the sample&amp;rsquo;s limited power to detect its very small effect.&lt;/p>
&lt;h3 id="83-posterior-coefficient-plot">8.3 Posterior coefficient plot&lt;/h3>
&lt;p>Beyond knowing &lt;em>which&lt;/em> variables matter, we want to know &lt;em>how much&lt;/em> they matter and how precisely they are estimated. The posterior coefficient plot displays the BMA-estimated effect size for each variable along with approximate 95% credible intervals (posterior mean $\pm$ 2 posterior standard deviations).&lt;/p>
&lt;pre>&lt;code class="language-r"># Coefficient plot with 95% credible intervals
ggplot(bma_df, aes(x = reorder(variable, pip), y = post_mean, color = robustness)) +
geom_pointrange(aes(ymin = ci_low, ymax = ci_high)) +
geom_hline(yintercept = 0, linetype = &amp;quot;solid&amp;quot;, color = &amp;quot;gray50&amp;quot;) +
coord_flip()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="bma_lasso_wals_05_bma_coefs.png" alt="BMA posterior mean coefficients with approximate 95 percent credible intervals. Variables ordered by PIP. Robust variables have intervals that do not cross zero.">&lt;/p>
&lt;p>The posterior coefficient plot shows the BMA-estimated effect sizes with uncertainty bands. GDP&amp;rsquo;s posterior mean of approximately 1.19 closely recovers the true value of 1.200, and its 95% credible interval is narrow, reflecting high precision. Trade_network has a posterior mean of 0.87, overshooting its true value of 0.500 &amp;mdash; but its wide credible interval honestly reflects substantial estimation uncertainty. The noise variables and low-PIP variables like agriculture have posterior means shrunk very close to zero &amp;mdash; this is BMA&amp;rsquo;s shrinkage at work. Variables with low PIPs appear in few high-probability models, so their posterior means are averaged with many models where the coefficient is zero, pulling the estimate toward zero.&lt;/p>
&lt;h3 id="84-variable-inclusion-map">8.4 Variable-inclusion map&lt;/h3>
&lt;p>The variable-inclusion map shows &lt;em>which&lt;/em> variables appear in the highest-probability models and whether their coefficients are positive or negative. Unlike a simple heatmap, the &lt;strong>width of each column is proportional to the model&amp;rsquo;s posterior probability&lt;/strong> &amp;mdash; so wide columns represent models that the data strongly supports. The x-axis shows cumulative posterior model probability: if the first model has PMP = 0.15, it occupies the region from 0 to 0.15; the second model fills from 0.15 to 0.15 + its PMP, and so on. A solid band of color stretching across most of the x-axis means the variable appears in virtually every high-probability model.&lt;/p>
&lt;pre>&lt;code class="language-r"># Extract top 100 models and their coefficient estimates
top_coefs &amp;lt;- topmodels.bma(bma_fit)
n_top &amp;lt;- min(100, ncol(top_coefs))
top_coefs &amp;lt;- top_coefs[, 1:n_top]
# Extract posterior model probabilities (MCMC-based)
model_pmps &amp;lt;- pmp.bma(bma_fit)[1:n_top, 1]
# Cumulative x positions: each model's width = its PMP
cum_pmp &amp;lt;- c(0, cumsum(model_pmps))
# Order variables by PIP (highest at top)
var_order &amp;lt;- bma_df |&amp;gt; arrange(desc(pip)) |&amp;gt; pull(variable)
# Build rectangle data for every variable × model combination
rect_data &amp;lt;- expand.grid(
var_idx = seq_len(nrow(top_coefs)),
model_idx = seq_len(n_top)
) |&amp;gt;
mutate(
variable = rownames(top_coefs)[var_idx],
coef_value = mapply(function(v, m) top_coefs[v, m], var_idx, model_idx),
sign = case_when(
coef_value &amp;gt; 0 ~ &amp;quot;Positive&amp;quot;,
coef_value &amp;lt; 0 ~ &amp;quot;Negative&amp;quot;,
TRUE ~ &amp;quot;Not included&amp;quot;
),
xmin = cum_pmp[model_idx],
xmax = cum_pmp[model_idx + 1],
variable = factor(variable, levels = rev(var_order))
)
# Plot the variable-inclusion map
ggplot(rect_data, aes(xmin = xmin, xmax = xmax,
ymin = as.numeric(variable) - 0.45,
ymax = as.numeric(variable) + 0.45,
fill = sign)) +
geom_rect() +
scale_fill_manual(
name = &amp;quot;Coefficient&amp;quot;,
values = c(&amp;quot;Positive&amp;quot; = &amp;quot;#6a9bcc&amp;quot;,
&amp;quot;Negative&amp;quot; = &amp;quot;#d97757&amp;quot;,
&amp;quot;Not included&amp;quot; = &amp;quot;#d0cdc8&amp;quot;)
) +
scale_x_continuous(expand = c(0, 0),
labels = scales::label_number(accuracy = 0.1)) +
scale_y_continuous(breaks = seq_along(var_order),
labels = rev(var_order),
expand = c(0, 0)) +
labs(title = &amp;quot;Variable-Inclusion Map&amp;quot;,
subtitle = paste0(&amp;quot;Top &amp;quot;, n_top, &amp;quot; models shown out of &amp;quot;,
nrow(pmp.bma(bma_fit)), &amp;quot; visited&amp;quot;),
x = &amp;quot;Cumulative posterior model probability&amp;quot;,
y = NULL)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="bma_lasso_wals_06_bma_inclusion.png" alt="Variable-inclusion map showing the top 100 BMA models. The x-axis is cumulative posterior model probability, so wider columns represent more probable models. Blue indicates a positive coefficient, orange indicates a negative coefficient, and gray indicates the variable is not included. Variables are ordered by PIP from top to bottom.">&lt;/p>
&lt;p>The variable-inclusion map reveals clear structure. The top variables &amp;mdash; log_gdp, trade_network, fossil_fuel, and industry &amp;mdash; form solid blue bands stretching across nearly the entire x-axis, meaning they appear with positive coefficients in virtually every high-probability model. Urban_pop and democracy also show substantial inclusion, consistent with their borderline PIPs. In contrast, the noise variables (log_trade, fdi, corruption, log_tourism, log_credit) appear as mostly gray with occasional patches of blue or orange, indicating they enter and exit models sporadically and sometimes with the wrong sign. The fact that noise variables occasionally appear with negative coefficients (orange patches) is another sign of fragility &amp;mdash; their coefficient estimates are unstable because they have no true effect.&lt;/p>
&lt;h3 id="85-bma-results-vs-known-truth">8.5 BMA results vs. known truth&lt;/h3>
&lt;pre>&lt;code class="language-r"># Compare BMA results with the true DGP
bma_summary &amp;lt;- bma_df |&amp;gt;
mutate(
bma_robust = pip &amp;gt;= 0.80,
true_nonzero = true_beta != 0,
correct = bma_robust == true_nonzero
) |&amp;gt;
select(variable, true_beta, pip, post_mean, bma_robust, true_nonzero, correct)
print(bma_summary)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> variable true_beta pip post_mean bma_robust true_nonzero correct
log_gdp 1.200 1.000 1.1854 TRUE TRUE TRUE
trade_network 0.500 0.986 0.8727 TRUE TRUE TRUE
fossil_fuel 0.012 0.948 0.0117 TRUE TRUE TRUE
industry 0.008 0.841 0.0142 TRUE TRUE TRUE
urban_pop 0.010 0.648 0.0049 FALSE TRUE FALSE
democracy 0.004 0.607 0.0066 FALSE TRUE FALSE
log_tourism 0.000 0.130 -0.0039 FALSE FALSE TRUE
log_credit 0.000 0.104 0.0051 FALSE FALSE TRUE
agriculture 0.005 0.087 -0.0002 FALSE TRUE FALSE
log_trade 0.000 0.084 -0.0037 FALSE FALSE TRUE
corruption 0.000 0.078 0.0026 FALSE FALSE TRUE
fdi 0.000 0.077 -0.0000 FALSE FALSE TRUE
&lt;/code>&lt;/pre>
&lt;p>BMA correctly classifies 9 of 12 variables. The four strongest true predictors (GDP, trade_network, fossil_fuel, industry) all receive PIPs above 0.80 &amp;mdash; these are the &amp;ldquo;robust&amp;rdquo; determinants. All five noise variables receive PIPs below 0.15 &amp;mdash; correctly identified as fragile. Urban_pop (PIP = 0.648) and democracy (PIP = 0.607) fall in the borderline range &amp;mdash; they are true predictors, but BMA&amp;rsquo;s conservative Occam&amp;rsquo;s razor hedges because their effects are moderate. Agriculture ($\beta = 0.005$, PIP = 0.087) is missed entirely. This reveals an important nuance: BMA prioritizes precision over sensitivity. It would rather miss a small true effect than falsely include a noise variable.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Note.&lt;/strong> BMA on all 12 variables correctly gives high PIPs to the strong true predictors (GDP, trade network, fossil fuel, industry) and low PIPs to the noise variables. Variables with moderate or small true effects may land in the borderline zone. The variable-inclusion map shows that the top models consistently include the core predictors.&lt;/p>
&lt;/blockquote>
&lt;div style="background: linear-gradient(135deg, #d97757 0%, #d97757 100%); padding: 1.5em 2em; border-radius: 8px; margin: 2em 0; color: #fff; font-size: 1.3em; font-weight: 600;">
PART 2: LASSO
&lt;/div>
&lt;h2 id="9-regularization-----adding-a-penalty">9. Regularization &amp;mdash; Adding a Penalty&lt;/h2>
&lt;h3 id="91-the-bias-variance-tradeoff">9.1 The bias-variance tradeoff&lt;/h3>
&lt;p>OLS is an &lt;strong>unbiased&lt;/strong> estimator &amp;mdash; on average, it gets the coefficients right. But with many correlated regressors, OLS coefficients have &lt;strong>high variance&lt;/strong>: they bounce around from sample to sample. Adding or removing a single variable can drastically change the estimates.&lt;/p>
&lt;p>The key insight of regularization is that a &lt;strong>little bias can buy a lot of variance reduction&lt;/strong>, lowering the overall prediction error. The &lt;strong>total error&lt;/strong> of a prediction decomposes as:&lt;/p>
&lt;p>$$
\text{MSE} = \text{Bias}^2 + \text{Variance} + \text{Irreducible noise}
$$&lt;/p>
&lt;p>&lt;img src="bma_lasso_wals_02_bias_variance.png" alt="The bias-variance tradeoff. As model complexity increases (more variables, less regularization), bias decreases but variance increases. The optimal point is a compromise between the two, minimizing total MSE.">&lt;/p>
&lt;p>The figure illustrates the fundamental tradeoff. At low complexity (strong regularization), bias is high but variance is low. At high complexity (weak or no regularization, like OLS), bias is near zero but variance explodes. The optimal point lies in between &amp;mdash; this is exactly where regularized methods like LASSO operate. Think of the penalty as a &amp;ldquo;budget constraint&amp;rdquo; on coefficient sizes: variables that do not contribute enough to prediction are not worth the cost, so their coefficients are set to zero.&lt;/p>
&lt;h2 id="10-l1-vs-l2-geometry">10. L1 vs. L2 Geometry&lt;/h2>
&lt;h3 id="101-the-lasso-l1-penalty">10.1 The LASSO (L1) penalty&lt;/h3>
&lt;p>The LASSO solves the following optimization problem:&lt;/p>
&lt;p>$$
\hat{\beta}_{\text{LASSO}} = \arg\min_\beta \; \frac{1}{2n}\|y - X\beta\|^2 + \lambda \|\beta\|_1
$$&lt;/p>
&lt;p>where:&lt;/p>
&lt;ul>
&lt;li>$\frac{1}{2n}\|y - X\beta\|^2$ is the &lt;strong>sum of squared residuals&lt;/strong> (the usual OLS loss, scaled)&lt;/li>
&lt;li>$\|\beta\|_1 = \sum_{j=1}^{p} |\beta_j|$ is the &lt;strong>L1 norm&lt;/strong> (sum of absolute values)&lt;/li>
&lt;li>$\lambda \geq 0$ is the &lt;strong>regularization parameter&lt;/strong>: it controls how much we penalize large coefficients. When $\lambda = 0$, LASSO reduces to OLS. As $\lambda \to \infty$, all coefficients are shrunk to zero.&lt;/li>
&lt;/ul>
&lt;h3 id="102-the-ridge-l2-penalty">10.2 The Ridge (L2) penalty&lt;/h3>
&lt;p>For comparison, &lt;strong>Ridge regression&lt;/strong> uses the L2 norm instead:&lt;/p>
&lt;p>$$
\hat{\beta}_{\text{Ridge}} = \arg\min_\beta \; \frac{1}{2n}\|y - X\beta\|^2 + \lambda \|\beta\|_2^2
$$&lt;/p>
&lt;p>where $\|\beta\|_2^2 = \sum_{j=1}^{p} \beta_j^2$ is the sum of squared coefficients.&lt;/p>
&lt;h3 id="103-why-lasso-selects-variables-and-ridge-does-not">10.3 Why LASSO selects variables and Ridge does not&lt;/h3>
&lt;p>The geometric explanation is one of the most elegant ideas in modern statistics. The constraint region for LASSO (L1) is a &lt;strong>diamond&lt;/strong>, while the constraint region for Ridge (L2) is a &lt;strong>circle&lt;/strong>. When the elliptical OLS contours meet the diamond, they typically hit a &lt;strong>corner&lt;/strong>, where one or more coefficients are exactly zero. When they meet the circle, they hit a smooth curve &amp;mdash; coefficients are shrunk but never exactly zero.&lt;/p>
&lt;p>&lt;img src="bma_lasso_wals_03_l1_l2_geometry.png" alt="Side-by-side comparison of L1 and L2 constraint geometry. Left panel shows the LASSO diamond where OLS contours hit a corner, setting beta-1 to exactly zero. Right panel shows the Ridge circle where contours hit a smooth boundary, producing no exact zeros.">&lt;/p>
&lt;p>The key insight: &lt;strong>the L1 diamond has corners where coefficients are exactly zero &amp;mdash; this is why LASSO selects variables.&lt;/strong> The L2 circle has no corners, so Ridge shrinks coefficients toward zero but never reaches it. LASSO performs &lt;em>simultaneous estimation and variable selection&lt;/em>; Ridge only estimates.&lt;/p>
&lt;h2 id="11-lasso-on-all-12-variables">11. LASSO on All 12 Variables&lt;/h2>
&lt;h3 id="111-running-lasso-with-cross-validation">11.1 Running LASSO with cross-validation&lt;/h3>
&lt;p>The LASSO has one tuning parameter: $\lambda$, which controls the strength of the penalty. Too small and we include noise; too large and we exclude true predictors. We choose $\lambda$ using &lt;strong>10-fold cross-validation&lt;/strong>: split the data into 10 folds, train on 9, predict the 10th, and repeat. The $\lambda$ that minimizes the average prediction error across folds is called &lt;strong>lambda.min&lt;/strong>.&lt;/p>
&lt;pre>&lt;code class="language-r">set.seed(2021) # reproducibility for cross-validation folds
# Prepare the design matrix X and response vector y
X &amp;lt;- synth_data |&amp;gt;
select(log_gdp, industry, fossil_fuel, urban_pop, democracy,
trade_network, agriculture, log_trade, fdi, corruption,
log_tourism, log_credit) |&amp;gt;
as.matrix()
y &amp;lt;- synth_data$log_co2
# Run LASSO (alpha = 1) with 10-fold cross-validation
lasso_cv &amp;lt;- cv.glmnet(
x = X,
y = y,
alpha = 1, # alpha=1 is LASSO (alpha=0 is Ridge)
nfolds = 10,
standardize = TRUE # standardize predictors internally
)
&lt;/code>&lt;/pre>
&lt;h3 id="112-regularization-path">11.2 Regularization path&lt;/h3>
&lt;pre>&lt;code class="language-r"># Fit the full LASSO path
lasso_full &amp;lt;- glmnet(X, y, alpha = 1, standardize = TRUE)
# Plot coefficient paths
ggplot(path_df, aes(x = log_lambda, y = coefficient, color = variable)) +
geom_line() +
geom_vline(xintercept = log(lasso_cv$lambda.min), linetype = &amp;quot;dashed&amp;quot;) +
geom_vline(xintercept = log(lasso_cv$lambda.1se), linetype = &amp;quot;dotted&amp;quot;)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="bma_lasso_wals_07_lasso_path.png" alt="LASSO regularization path showing how each variable&amp;rsquo;s coefficient changes as the penalty lambda increases from left to right. Steel blue lines represent true predictors, orange lines represent noise variables. GDP (the strongest predictor) is the last to be shrunk to zero.">&lt;/p>
&lt;p>The regularization path reveals the story of LASSO variable selection. Reading from left to right (increasing penalty), the noise variables (orange lines) are the first to be driven to zero &amp;mdash; they provide too little predictive value to justify their &amp;ldquo;cost&amp;rdquo; under the penalty. GDP (the strongest predictor with $\beta = 1.200$) persists the longest, requiring the largest penalty to be eliminated. The vertical lines mark lambda.min (minimum CV error) and lambda.1se (most parsimonious model within 1 SE of the minimum). The gap between them represents the tension between fitting the data well and keeping the model simple.&lt;/p>
&lt;h3 id="113-cross-validation-curve">11.3 Cross-validation curve&lt;/h3>
&lt;pre>&lt;code class="language-r"># Plot the CV curve
ggplot(cv_df, aes(x = log_lambda, y = mse)) +
geom_ribbon(aes(ymin = mse_lo, ymax = mse_hi), fill = &amp;quot;gray85&amp;quot;, alpha = 0.5) +
geom_line(color = &amp;quot;#6a9bcc&amp;quot;) +
geom_vline(xintercept = log(lasso_cv$lambda.min), linetype = &amp;quot;dashed&amp;quot;)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="bma_lasso_wals_08_lasso_cv.png" alt="Ten-fold cross-validation curve for LASSO. The left dashed line marks lambda.min (minimum CV error); the right dotted line marks lambda.1se (most parsimonious model within 1 standard error of the minimum). The shaded band shows plus or minus 1 standard error.">&lt;/p>
&lt;p>The cross-validation curve shows how prediction error varies with the penalty strength. The curve has a characteristic U-shape: too little penalty (left) allows overfitting (high error from variance), while too much penalty (right) underfits (high error from bias). The &amp;ldquo;1 standard error rule&amp;rdquo; is a common default: since CV error estimates are noisy, any model within 1 SE of the best is statistically indistinguishable from the best. We prefer the simpler one (lambda.1se).&lt;/p>
&lt;h3 id="114-selected-variables">11.4 Selected variables&lt;/h3>
&lt;pre>&lt;code class="language-r"># Extract LASSO coefficients at lambda.1se
lasso_coefs_1se &amp;lt;- coef(lasso_cv, s = &amp;quot;lambda.1se&amp;quot;)
lasso_df &amp;lt;- tibble(
variable = rownames(lasso_coefs_1se)[-1],
lasso_coef = as.numeric(lasso_coefs_1se)[-1]
) |&amp;gt;
mutate(
selected = lasso_coef != 0,
true_beta = true_beta_lookup[variable],
is_noise = true_beta == 0,
bar_color = case_when(
!selected ~ &amp;quot;Not selected&amp;quot;,
is_noise ~ &amp;quot;Noise (false positive)&amp;quot;,
TRUE ~ &amp;quot;True predictor (correct)&amp;quot;
)
)
# Plot selected variables
ggplot(lasso_df, aes(x = reorder(variable, abs(lasso_coef)), y = lasso_coef, fill = bar_color)) +
geom_col(width = 0.6) + coord_flip()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="bma_lasso_wals_09_lasso_selected.png" alt="LASSO-selected variables at lambda.1se. Steel blue bars indicate true predictors correctly retained; orange bars indicate noise variables falsely included (if any). Gray bars show variables not selected.">&lt;/p>
&lt;p>At lambda.1se, LASSO selects a sparse subset of the 12 candidate variables. The selected variables are shown with colored bars: steel blue for true predictors correctly retained, orange for any noise variables falsely included. Variables with zero coefficients (gray) have been excluded by the LASSO penalty. The key question is: did LASSO keep the right variables and drop the right ones?&lt;/p>
&lt;h2 id="12-post-lasso">12. Post-LASSO&lt;/h2>
&lt;p>LASSO coefficients are &lt;strong>biased&lt;/strong> because the L1 penalty shrinks them toward zero. The selected variables are correct (we hope), but the coefficient values are too small. This is by design &amp;mdash; the penalty trades bias for variance reduction &amp;mdash; but for &lt;em>interpretation&lt;/em> we want unbiased estimates.&lt;/p>
&lt;p>The fix is simple: &lt;strong>Post-LASSO&lt;/strong> (Belloni and Chernozhukov, 2013). Run OLS using only the variables that LASSO selected. The LASSO does the selection; OLS does the estimation.&lt;/p>
&lt;pre>&lt;code class="language-r"># Identify which variables LASSO selected at lambda.1se
selected_vars &amp;lt;- lasso_df |&amp;gt; filter(selected) |&amp;gt; pull(variable)
# Build the Post-LASSO formula
post_lasso_formula &amp;lt;- as.formula(
paste(&amp;quot;log_co2 ~&amp;quot;, paste(selected_vars, collapse = &amp;quot; + &amp;quot;))
)
# Run OLS on the selected variables only
post_lasso_fit &amp;lt;- lm(post_lasso_formula, data = synth_data)
# Compare: LASSO vs Post-LASSO vs True coefficients
post_lasso_summary &amp;lt;- broom::tidy(post_lasso_fit) |&amp;gt;
filter(term != &amp;quot;(Intercept)&amp;quot;) |&amp;gt;
rename(variable = term, post_lasso_coef = estimate) |&amp;gt;
select(variable, post_lasso_coef) |&amp;gt;
left_join(lasso_df |&amp;gt; select(variable, lasso_coef, true_beta), by = &amp;quot;variable&amp;quot;)
print(post_lasso_summary)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> variable lasso_coef post_lasso_coef true_beta
log_gdp 1.1899 1.1646 1.200
industry 0.0090 0.0176 0.008
fossil_fuel 0.0072 0.0118 0.012
urban_pop 0.0041 0.0078 0.010
democracy 0.0046 0.0113 0.004
trade_network 0.6309 0.8978 0.500
&lt;/code>&lt;/pre>
&lt;p>Notice how the Post-LASSO coefficients are closer to the true values than the raw LASSO coefficients. For example, fossil_fuel&amp;rsquo;s LASSO coefficient is 0.007 (shrunk from the true 0.012), but the Post-LASSO estimate is 0.012 &amp;mdash; recovering the truth almost exactly. Similarly, urban_pop recovers from 0.004 (LASSO) to 0.008 (Post-LASSO), closer to the true value of 0.010. Trade_network&amp;rsquo;s Post-LASSO estimate (0.898) overshoots the true value (0.500), reflecting the difficulty of precisely estimating a coefficient on a low-variance variable. The LASSO selected the right variables; Post-LASSO recovered unbiased magnitudes.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Note.&lt;/strong> LASSO coefficients are shrunk toward zero by design. Post-LASSO runs OLS on only the LASSO-selected variables, producing unbiased coefficient estimates while retaining the variable selection from LASSO.&lt;/p>
&lt;/blockquote>
&lt;div style="background: linear-gradient(135deg, #00d4c8 0%, #00d4c8 100%); padding: 1.5em 2em; border-radius: 8px; margin: 2em 0; color: #141413; font-size: 1.3em; font-weight: 600;">
PART 3: Weighted Average Least Squares (WALS)
&lt;/div>
&lt;h2 id="13-frequentist-model-averaging">13. Frequentist Model Averaging&lt;/h2>
&lt;p>WALS (Weighted Average Least Squares) is a &lt;strong>frequentist&lt;/strong> approach to model averaging. Like BMA, it averages over models instead of selecting just one. But unlike BMA, it does not require MCMC sampling or the specification of a full Bayesian prior.&lt;/p>
&lt;p>The key structural assumption is that regressors are split into two groups:&lt;/p>
&lt;p>$$
y = X_1 \beta_1 + X_2 \beta_2 + \varepsilon
$$&lt;/p>
&lt;p>where:&lt;/p>
&lt;ul>
&lt;li>$X_1$ are &lt;strong>focus regressors&lt;/strong>: variables you are certain belong in the model. In a cross-sectional setting, this is typically just the &lt;strong>intercept&lt;/strong>.&lt;/li>
&lt;li>$X_2$ are &lt;strong>auxiliary regressors&lt;/strong>: the 12 candidate variables whose inclusion is uncertain.&lt;/li>
&lt;li>$\beta_1$ are always estimated; $\beta_2$ are the coefficients we are uncertain about.&lt;/li>
&lt;/ul>
&lt;p>WALS was introduced by Magnus, Powell, and Prufer (2010) and offers a compelling advantage over BMA: &lt;strong>it is extremely fast&lt;/strong>. While BMA explores thousands or millions of models via MCMC, WALS uses a mathematical trick to reduce the problem to $K$ independent averaging problems &amp;mdash; one per auxiliary variable.&lt;/p>
&lt;h2 id="14-the-semi-orthogonal-transformation">14. The Semi-Orthogonal Transformation&lt;/h2>
&lt;h3 id="why-correlated-variables-make-averaging-hard">Why correlated variables make averaging hard&lt;/h3>
&lt;p>In our synthetic data, GDP is correlated with fossil fuel use, urbanization, and even with the noise variables. This means that the decision to include one variable affects the importance of another. If GDP is in the model, fossil fuel&amp;rsquo;s coefficient is partially &amp;ldquo;absorbed&amp;rdquo; by GDP.&lt;/p>
&lt;p>In BMA, this problem is handled by averaging over all model combinations &amp;mdash; but at a high computational cost ($2^{12} = 4,096$ models). WALS uses a different strategy: &lt;strong>transform the auxiliary variables so they become orthogonal&lt;/strong> (uncorrelated with each other). Once orthogonal, each variable can be averaged independently.&lt;/p>
&lt;h3 id="the-mathematical-trick">The mathematical trick&lt;/h3>
&lt;p>The semi-orthogonal transformation works as follows:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Remove the influence of focus regressors&lt;/strong>: project out $X_1$ from both $y$ and $X_2$, obtaining residuals $\tilde{y}$ and $\tilde{X}_2$.&lt;/li>
&lt;li>&lt;strong>Orthogonalize the auxiliaries&lt;/strong>: apply a rotation matrix $P$ (from the eigendecomposition of $\tilde{X}_2'\tilde{X}_2$) to create $Z = \tilde{X}_2 P$, where $Z&amp;rsquo;Z$ is diagonal.&lt;/li>
&lt;li>&lt;strong>Average independently&lt;/strong>: because the columns of $Z$ are orthogonal, the model-averaging problem decomposes into $K$ independent problems. Each transformed variable is averaged separately.&lt;/li>
&lt;/ol>
&lt;p>The computational savings grow dramatically: with 12 variables, we solve &lt;strong>12 independent problems&lt;/strong> instead of enumerating 4,096 models. Think of it as untangling a web of correlated strings until each hangs independently &amp;mdash; once separated, you can measure each string&amp;rsquo;s pull without interference from the others.&lt;/p>
&lt;h2 id="15-the-laplace-prior">15. The Laplace Prior&lt;/h2>
&lt;p>WALS requires a prior distribution for the transformed coefficients. The default and recommended choice is the &lt;strong>Laplace (double-exponential) prior&lt;/strong>:&lt;/p>
&lt;p>$$
p(\gamma_j) \propto \exp(-|\gamma_j| / \tau)
$$&lt;/p>
&lt;p>where $\gamma_j$ is the transformed coefficient and $\tau$ controls the spread. The Laplace prior has two key features:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Peaked at zero&lt;/strong>: it encodes &lt;em>skepticism&lt;/em> &amp;mdash; the prior believes most variables probably have small effects&lt;/li>
&lt;li>&lt;strong>Heavy tails&lt;/strong>: it allows large effects if the data strongly supports them &amp;mdash; variables with strong signal can &amp;ldquo;break through&amp;rdquo; the prior&lt;/li>
&lt;/ol>
&lt;p>&lt;img src="bma_lasso_wals_11_priors.png" alt="Three prior distributions used in model averaging. The Laplace prior (used by WALS) is peaked at zero with heavy tails. The Normal prior (used by BMA g-prior) is also centered at zero but has thinner tails. The Uniform prior assigns equal weight everywhere.">&lt;/p>
&lt;h3 id="the-deep-connection-to-lasso">The deep connection to LASSO&lt;/h3>
&lt;p>Here is a remarkable fact: &lt;strong>the LASSO&amp;rsquo;s L1 penalty is the negative log of a Laplace prior&lt;/strong>. The MAP (maximum a posteriori) estimate under a Laplace prior is:&lt;/p>
&lt;p>$$
\hat{\beta}_{\text{MAP}} = \arg\min_\beta \; \frac{1}{2n}\|y - X\beta\|^2 + \frac{\sigma^2}{\tau} \sum_{j=1}^{p}|\beta_j|
$$&lt;/p>
&lt;p>This is identical to the LASSO objective with $\lambda = \sigma^2 / \tau$. The LASSO penalty and the Laplace prior are two sides of the same coin.&lt;/p>
&lt;p>This means &lt;strong>LASSO and WALS encode the same prior belief&lt;/strong> &amp;mdash; that most coefficients are probably zero or small &amp;mdash; but they use it differently:&lt;/p>
&lt;ul>
&lt;li>LASSO uses the Laplace prior for &lt;strong>selection&lt;/strong>: it finds the single most probable model (the MAP estimate), which sets some coefficients to exactly zero&lt;/li>
&lt;li>WALS uses the Laplace prior for &lt;strong>averaging&lt;/strong>: it averages over all models, weighted by the Laplace prior, producing continuous (nonzero) coefficient estimates with uncertainty measures&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>&lt;strong>Note.&lt;/strong> The Laplace prior is peaked at zero (skeptical) with heavy tails (open-minded). It is the same prior that underlies LASSO&amp;rsquo;s L1 penalty. LASSO uses it for hard selection (zeros vs. nonzeros); WALS uses it for soft averaging (continuous weights).&lt;/p>
&lt;/blockquote>
&lt;h2 id="16-wals-on-all-12-variables">16. WALS on All 12 Variables&lt;/h2>
&lt;h3 id="161-running-wals">16.1 Running WALS&lt;/h3>
&lt;pre>&lt;code class="language-r"># WALS splits regressors into two groups:
# X1 = focus regressors (always included): just the intercept
# X2 = auxiliary regressors (uncertain): our 12 candidate variables
# Prepare the focus regressor matrix (intercept only)
X1_wals &amp;lt;- matrix(1, nrow = nrow(synth_data), ncol = 1)
colnames(X1_wals) &amp;lt;- &amp;quot;(Intercept)&amp;quot;
# Prepare the auxiliary regressor matrix (all 12 candidates)
X2_wals &amp;lt;- synth_data |&amp;gt;
select(log_gdp, industry, fossil_fuel, urban_pop, democracy,
trade_network, agriculture, log_trade, fdi, corruption,
log_tourism, log_credit) |&amp;gt;
as.matrix()
y_wals &amp;lt;- synth_data$log_co2
# Fit WALS with the Laplace prior (the recommended default)
wals_fit &amp;lt;- wals(
x = X1_wals, # focus regressors (intercept)
x2 = X2_wals, # auxiliary regressors (12 candidates)
y = y_wals, # response variable
prior = laplace() # Laplace prior for auxiliaries
)
wals_summary &amp;lt;- summary(wals_fit)
&lt;/code>&lt;/pre>
&lt;p>The WALS function call is remarkably concise. Unlike BMA, there is no MCMC sampling, no burn-in period, and no convergence diagnostics to worry about. The computation is essentially instantaneous.&lt;/p>
&lt;pre>&lt;code class="language-r"># Extract results
aux_coefs &amp;lt;- wals_summary$auxCoefs
wals_df &amp;lt;- tibble(
variable = rownames(aux_coefs),
estimate = aux_coefs[, &amp;quot;Estimate&amp;quot;],
se = aux_coefs[, &amp;quot;Std. Error&amp;quot;],
t_stat = estimate / se
) |&amp;gt;
mutate(
true_beta = true_beta_lookup[variable],
abs_t = abs(t_stat),
wals_robust = abs_t &amp;gt;= 2
)
print(wals_df |&amp;gt; arrange(desc(abs_t)) |&amp;gt; select(variable, estimate, t_stat, true_beta))
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> variable estimate t_stat true_beta
log_gdp 1.1333 34.62 1.200
trade_network 0.8458 4.39 0.500
industry 0.0187 4.01 0.008
fossil_fuel 0.0099 3.26 0.012
urban_pop 0.0082 3.11 0.010
democracy 0.0097 2.58 0.004
log_credit 0.0659 1.43 0.000
agriculture -0.0046 -1.13 0.005
log_tourism -0.0148 -0.64 0.000
log_trade 0.0196 0.31 0.000
fdi -0.0011 -0.17 0.000
corruption -0.0165 -0.09 0.000
&lt;/code>&lt;/pre>
&lt;p>WALS produces familiar t-statistics for each auxiliary variable. Using the $|t| \geq 2$ threshold as our robustness criterion (analogous to BMA&amp;rsquo;s PIP $\geq$ 0.80), we can classify each variable as robust or fragile.&lt;/p>
&lt;h3 id="162-t-statistic-bar-chart">16.2 t-statistic bar chart&lt;/h3>
&lt;p>The t-statistic bar chart provides a visual summary of WALS robustness classification. Variables with $|t| \geq 2$ pass the robustness threshold (analogous to BMA&amp;rsquo;s PIP $\geq$ 0.80), while those below the threshold are considered fragile.&lt;/p>
&lt;pre>&lt;code class="language-r"># Classify each variable for the bar chart
wals_df &amp;lt;- wals_df |&amp;gt;
mutate(
bar_color = case_when(
wals_robust &amp;amp; true_nonzero ~ &amp;quot;True positive&amp;quot;,
wals_robust &amp;amp; !true_nonzero ~ &amp;quot;False positive&amp;quot;,
!wals_robust &amp;amp; true_nonzero ~ &amp;quot;False negative&amp;quot;,
TRUE ~ &amp;quot;True negative&amp;quot;
)
)
ggplot(wals_df, aes(x = reorder(variable, abs_t), y = t_stat, fill = bar_color)) +
geom_col(width = 0.6) +
geom_hline(yintercept = c(-2, 2), linetype = &amp;quot;dashed&amp;quot;) +
coord_flip()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="bma_lasso_wals_10_wals_tstat.png" alt="WALS t-statistics for all 12 variables. The dashed lines mark the t equals 2 robustness threshold. Variables with absolute t-statistic greater than or equal to 2 are considered robust.">&lt;/p>
&lt;p>The t-statistic bar chart shows a clear separation. GDP towers above all others with $|t| = 34.62$, followed by trade_network ($|t| = 4.39$), industry ($|t| = 4.01$), fossil_fuel ($|t| = 3.26$), urban_pop ($|t| = 3.11$), and democracy ($|t| = 2.58$). These six variables pass the $|t| \geq 2$ threshold. The noise variables all have $|t| &amp;lt; 1.5$, confirming they are not robust determinants. Agriculture ($|t| = 1.13$) falls just below the robustness threshold &amp;mdash; its true effect ($\beta = 0.005$) is simply too small to detect reliably with this sample size.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Note.&lt;/strong> WALS produces t-statistics for each auxiliary variable. Using the $|t| \geq 2$ threshold, we can classify variables as robust or fragile. WALS is extremely fast (no MCMC) and provides a frequentist complement to BMA&amp;rsquo;s Bayesian PIPs.&lt;/p>
&lt;/blockquote>
&lt;div style="background: linear-gradient(135deg, #1a3a8a 0%, #141413 100%); padding: 1.5em 2em; border-radius: 8px; margin: 2em 0; color: #fff; font-size: 1.3em; font-weight: 600;">
PART 4: Grand Comparison
&lt;/div>
&lt;h2 id="17-three-methods-same-question-same-data">17. Three Methods, Same Question, Same Data&lt;/h2>
&lt;p>We have now applied all three methods to the same synthetic dataset. Time for the moment of truth: &lt;strong>which variables do all three methods agree on?&lt;/strong>&lt;/p>
&lt;h3 id="171-comprehensive-comparison-table">17.1 Comprehensive comparison table&lt;/h3>
&lt;pre>&lt;code class="language-r"># Merge all results
grand_table &amp;lt;- bma_compare |&amp;gt;
left_join(lasso_compare, by = &amp;quot;variable&amp;quot;) |&amp;gt;
left_join(wals_compare, by = &amp;quot;variable&amp;quot;) |&amp;gt;
mutate(
true_beta = true_beta_lookup[variable],
bma_robust = bma_pip &amp;gt;= 0.80,
n_methods = bma_robust + lasso_selected + wals_robust,
triple_robust = n_methods == 3,
true_nonzero = true_beta != 0
)
print(grand_table |&amp;gt;
select(variable, true_beta, bma_pip, bma_robust, lasso_selected, wals_t, wals_robust, n_methods) |&amp;gt;
arrange(desc(n_methods)))
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> variable true_beta bma_pip bma_robust lasso_selected wals_t wals_robust n_methods
log_gdp 1.200 1.000 TRUE TRUE 34.62 TRUE 3
trade_network 0.500 0.986 TRUE TRUE 4.39 TRUE 3
fossil_fuel 0.012 0.948 TRUE TRUE 3.26 TRUE 3
industry 0.008 0.841 TRUE TRUE 4.01 TRUE 3
urban_pop 0.010 0.648 FALSE TRUE 3.11 TRUE 2
democracy 0.004 0.607 FALSE TRUE 2.58 TRUE 2
log_tourism 0.000 0.130 FALSE FALSE -0.64 FALSE 0
log_credit 0.000 0.104 FALSE FALSE 1.43 FALSE 0
agriculture 0.005 0.087 FALSE FALSE -1.13 FALSE 0
log_trade 0.000 0.084 FALSE FALSE 0.31 FALSE 0
corruption 0.000 0.078 FALSE FALSE -0.09 FALSE 0
fdi 0.000 0.077 FALSE FALSE -0.17 FALSE 0
&lt;/code>&lt;/pre>
&lt;p>The results are striking. Four variables are &lt;strong>triple-robust&lt;/strong> &amp;mdash; identified by all three methods: log_gdp, trade_network, fossil_fuel, and industry. Two more variables &amp;mdash; urban_pop and democracy &amp;mdash; are &lt;strong>double-robust&lt;/strong>, selected by LASSO and WALS but landing in BMA&amp;rsquo;s borderline zone (PIPs of 0.648 and 0.607). All five noise variables are correctly excluded by all three methods. Agriculture ($\beta = 0.005$) is the only true predictor missed by all methods &amp;mdash; its effect is simply too small to detect.&lt;/p>
&lt;h3 id="172-method-agreement-heatmap">17.2 Method agreement heatmap&lt;/h3>
&lt;p>&lt;img src="bma_lasso_wals_12_heatmap.png" alt="Method agreement heatmap showing 12 variables by 3 methods. Steel blue indicates the variable was identified as robust; orange indicates it was not. True predictors are in the top rows, noise variables in the bottom rows.">&lt;/p>
&lt;p>The heatmap provides a visual summary of agreement. The top four rows (GDP, trade_network, fossil_fuel, industry) are solid steel blue across all three columns &amp;mdash; unanimous agreement that these variables matter. Urban_pop and democracy show steel blue for LASSO and WALS but orange for BMA, visualizing BMA&amp;rsquo;s greater conservatism. The bottom five rows (noise) are solid orange &amp;mdash; unanimous agreement that they do not matter. Agriculture is also orange throughout, reflecting all methods' consensus that its tiny effect ($\beta = 0.005$) cannot be reliably distinguished from zero.&lt;/p>
&lt;h3 id="173-bma-pip-vs-wals-t-statistic">17.3 BMA PIP vs. WALS |t-statistic|&lt;/h3>
&lt;p>&lt;img src="bma_lasso_wals_13_pip_vs_t.png" alt="BMA PIP plotted against WALS absolute t-statistic. Point color indicates true status (steel blue for true predictors, orange for noise). Point shape indicates LASSO selection (triangle for selected, cross for not selected). The upper-right quadrant contains variables robust by both BMA and WALS.">&lt;/p>
&lt;p>The scatter plot reveals a strong positive relationship between BMA PIP and WALS $|t|$. Variables in the upper-right quadrant are robust by both methods &amp;mdash; GDP, trade_network, fossil_fuel, and industry. Urban_pop and democracy sit in an interesting middle zone: high WALS $|t|$ (above 2) but moderate BMA PIP (below 0.80), illustrating BMA&amp;rsquo;s more conservative threshold. The noise variables cluster in the lower-left corner (low PIP, low $|t|$). LASSO selection (triangle markers) aligns with the WALS threshold, selecting the same six variables that pass $|t| \geq 2$.&lt;/p>
&lt;h3 id="174-coefficient-comparison">17.4 Coefficient comparison&lt;/h3>
&lt;p>&lt;img src="bma_lasso_wals_14_coef_comparison.png" alt="Coefficient estimates from the three methods compared to the true values in a three-panel faceted scatter plot. Points close to the dashed 45-degree line indicate accurate coefficient recovery.">&lt;/p>
&lt;p>The coefficient comparison plot shows how well each method recovers the true effect sizes. Points on the dashed 45-degree line represent perfect recovery. GDP ($\beta = 1.200$) is recovered almost exactly by all three methods. The smaller coefficients (fossil_fuel at 0.012, urban_pop at 0.010) are also well-estimated. Trade_network&amp;rsquo;s coefficient is overestimated by all methods (true 0.500, estimates around 0.85&amp;ndash;0.90), reflecting the difficulty of precisely estimating an effect on a low-variance variable. BMA&amp;rsquo;s posterior means are slightly attenuated for variables with PIPs below 1.0 (the averaging shrinks them toward zero).&lt;/p>
&lt;h3 id="175-agreement-summary">17.5 Agreement summary&lt;/h3>
&lt;p>&lt;img src="bma_lasso_wals_15_agreement.png" alt="Bar chart showing how many methods (out of 3) identified each variable as robust. Steel blue bars are true predictors, orange bars are noise variables. Four variables achieve triple-robust status and two achieve double-robust status.">&lt;/p>
&lt;p>The agreement bar chart tells a nuanced story: four variables are triple-robust (identified by all three methods), two are double-robust (identified by LASSO and WALS but not BMA), and six are identified by none. The &amp;ldquo;split votes&amp;rdquo; on urban_pop and democracy reveal a genuine methodological difference: LASSO and WALS are more liberal in including moderate-effect variables, while BMA&amp;rsquo;s Bayesian Occam&amp;rsquo;s razor demands stronger evidence. This pattern &amp;mdash; where methods &lt;em>mostly&lt;/em> agree but diverge on borderline cases &amp;mdash; is what makes methodological triangulation valuable.&lt;/p>
&lt;h3 id="176-method-performance">17.6 Method performance&lt;/h3>
&lt;pre>&lt;code class="language-r"># Sensitivity, specificity, and accuracy for each method
results_by_method &amp;lt;- tibble(
method = c(&amp;quot;BMA&amp;quot;, &amp;quot;LASSO&amp;quot;, &amp;quot;WALS&amp;quot;),
true_pos = c(4, 6, 6), # true predictors correctly identified
false_pos = c(0, 0, 0), # noise variables falsely identified
false_neg = c(3, 1, 1), # true predictors missed
true_neg = c(5, 5, 5), # noise variables correctly excluded
sensitivity = true_pos / 7,
specificity = true_neg / 5,
accuracy = (true_pos + true_neg) / 12
)
print(results_by_method)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> method true_pos false_pos false_neg true_neg sensitivity specificity accuracy
BMA 4 0 3 5 0.571 1.000 0.750
LASSO 6 0 1 5 0.857 1.000 0.917
WALS 6 0 1 5 0.857 1.000 0.917
&lt;/code>&lt;/pre>
&lt;p>All three methods achieve &lt;strong>perfect specificity&lt;/strong> (zero false positives) &amp;mdash; none mistakenly identifies a noise variable as robust. The key difference is in &lt;strong>sensitivity&lt;/strong>: LASSO and WALS each detect 6 of 7 true predictors (85.7%), while BMA detects only 4 (57.1%). BMA&amp;rsquo;s lower sensitivity reflects its conservative Bayesian Occam&amp;rsquo;s razor: it places urban_pop and democracy in the &amp;ldquo;borderline&amp;rdquo; zone rather than committing to their inclusion. The one variable missed by all methods &amp;mdash; agriculture ($\beta = 0.005$) &amp;mdash; has an effect so small that it is indistinguishable from noise given our sample size.&lt;/p>
&lt;h3 id="177-when-to-use-which-method">17.7 When to use which method&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:left">Method&lt;/th>
&lt;th style="text-align:left">Best for&lt;/th>
&lt;th style="text-align:left">Strengths&lt;/th>
&lt;th style="text-align:left">Limitations&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:left">BMA&lt;/td>
&lt;td style="text-align:left">Full uncertainty quantification&lt;/td>
&lt;td style="text-align:left">Probabilistic (PIPs), handles model uncertainty formally, coefficient intervals&lt;/td>
&lt;td style="text-align:left">Slower (MCMC), requires prior specification&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">LASSO&lt;/td>
&lt;td style="text-align:left">Prediction, sparse models&lt;/td>
&lt;td style="text-align:left">Fast, automatic selection, works with many variables&lt;/td>
&lt;td style="text-align:left">Binary (in/out), biased coefficients (use Post-LASSO)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">WALS&lt;/td>
&lt;td style="text-align:left">Speed, frequentist inference&lt;/td>
&lt;td style="text-align:left">Very fast, produces t-statistics, no MCMC&lt;/td>
&lt;td style="text-align:left">Less common, limited software support&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The strongest recommendation: &lt;strong>use all three&lt;/strong>. When they converge on the same variables (as with our four triple-robust predictors), you have the strongest possible evidence. When they disagree (as with urban_pop and democracy, where LASSO and WALS say &amp;ldquo;yes&amp;rdquo; but BMA hedges), the disagreement itself is informative &amp;mdash; it tells you the evidence is real but not overwhelming. In real-world data, complications such as nonlinearity, heteroskedasticity, and endogeneity may affect method performance and should be addressed before applying these techniques.&lt;/p>
&lt;h2 id="18-conclusion">18. Conclusion&lt;/h2>
&lt;h3 id="181-summary">18.1 Summary&lt;/h3>
&lt;p>This tutorial introduced three principled approaches to the variable selection problem:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Bayesian Model Averaging (BMA)&lt;/strong> averages over all possible models, weighting each by its posterior probability. It produces Posterior Inclusion Probabilities (PIPs) that quantify how robust each variable is across the entire model space. Variables with PIP $\geq$ 0.80 are considered robust.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>LASSO&lt;/strong> adds an L1 penalty to the OLS objective, forcing irrelevant coefficients to exactly zero. Cross-validation selects the penalty strength. Post-LASSO recovers unbiased coefficient estimates for the selected variables.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>WALS&lt;/strong> uses a semi-orthogonal transformation to decompose the model-averaging problem into independent subproblems &amp;mdash; one per variable. It is extremely fast and produces familiar t-statistics for robustness assessment.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h3 id="182-key-takeaways">18.2 Key takeaways&lt;/h3>
&lt;p>&lt;strong>The methods mostly converge &amp;mdash; and their disagreements are informative.&lt;/strong> Four variables are identified by all three methods (triple-robust), and all methods achieve perfect specificity (zero false positives). LASSO and WALS are more sensitive (detecting 6 of 7 true predictors), while BMA is more conservative (detecting 4). The two variables where they disagree &amp;mdash; urban_pop and democracy &amp;mdash; have moderate effects that BMA&amp;rsquo;s Bayesian Occam&amp;rsquo;s razor treats as borderline. This pattern illustrates the value of methodological triangulation across fundamentally different statistical paradigms.&lt;/p>
&lt;p>&lt;strong>Model uncertainty is real but addressable.&lt;/strong> With 12 candidate variables, there are 4,096 possible models. Rather than pretending one of them is &amp;ldquo;the&amp;rdquo; model, these methods account for the uncertainty explicitly. The result is more honest inference.&lt;/p>
&lt;p>&lt;strong>Synthetic data lets us verify.&lt;/strong> Because we designed the data-generating process, we could check each method&amp;rsquo;s performance against the known truth. In practice, the truth is unknown &amp;mdash; which is precisely why using multiple methods is so valuable.&lt;/p>
&lt;h3 id="183-applying-this-to-your-own-research">18.3 Applying this to your own research&lt;/h3>
&lt;p>The code in this tutorial is designed to be modular. To apply these methods to your own data:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Replace the CSV&lt;/strong>: load your own cross-sectional dataset instead of the synthetic one&lt;/li>
&lt;li>&lt;strong>Define the variable list&lt;/strong>: specify which variables are candidates for selection&lt;/li>
&lt;li>&lt;strong>Run the three methods&lt;/strong>: use the same &lt;code>bms()&lt;/code>, &lt;code>cv.glmnet()&lt;/code>, and &lt;code>wals()&lt;/code> function calls&lt;/li>
&lt;li>&lt;strong>Compare results&lt;/strong>: build the same comparison table and heatmap&lt;/li>
&lt;/ol>
&lt;p>The interpretation framework &amp;mdash; PIPs for BMA, selection for LASSO, t-statistics for WALS &amp;mdash; applies regardless of the specific dataset.&lt;/p>
&lt;h3 id="184-further-reading">18.4 Further reading&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>BMA&lt;/strong>: Hoeting, J.A., Madigan, D., Raftery, A.E., and Volinsky, C.T. (1999). &amp;ldquo;Bayesian Model Averaging: A Tutorial.&amp;rdquo; &lt;em>Statistical Science&lt;/em>, 14(4), 382&amp;ndash;417.&lt;/li>
&lt;li>&lt;strong>LASSO&lt;/strong>: Tibshirani, R. (1996). &amp;ldquo;Regression Shrinkage and Selection via the Lasso.&amp;rdquo; &lt;em>Journal of the Royal Statistical Society, Series B&lt;/em>, 58(1), 267&amp;ndash;288.&lt;/li>
&lt;li>&lt;strong>WALS&lt;/strong>: Magnus, J.R., Powell, O., and Prufer, P. (2010). &amp;ldquo;A Comparison of Two Model Averaging Techniques with an Application to Growth Empirics.&amp;rdquo; &lt;em>Journal of Econometrics&lt;/em>, 154(2), 139&amp;ndash;153.&lt;/li>
&lt;li>&lt;strong>Application&lt;/strong>: Aller, C., Ductor, L., and Grechyna, D. (2021). &amp;ldquo;Robust Determinants of CO&lt;sub>2&lt;/sub> Emissions.&amp;rdquo; &lt;em>Energy Economics&lt;/em>, 96, 105154.&lt;/li>
&lt;li>&lt;strong>Post-LASSO&lt;/strong>: Belloni, A. and Chernozhukov, V. (2013). &amp;ldquo;Least Squares After Model Selection in High-Dimensional Sparse Models.&amp;rdquo; &lt;em>Bernoulli&lt;/em>, 19(2), 521&amp;ndash;547.&lt;/li>
&lt;li>&lt;strong>R Packages&lt;/strong>: &lt;a href="https://cran.r-project.org/web/packages/BMS/vignettes/bms.pdf" target="_blank" rel="noopener">BMS vignette&lt;/a>, &lt;a href="https://glmnet.stanford.edu/articles/glmnet.html" target="_blank" rel="noopener">glmnet vignette&lt;/a>, &lt;a href="https://cran.r-project.org/package=WALS" target="_blank" rel="noopener">WALS package&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ol>
&lt;li>Hoeting, J.A., Madigan, D., Raftery, A.E., and Volinsky, C.T. (1999). Bayesian Model Averaging: A Tutorial. &lt;em>Statistical Science&lt;/em>, 14(4), 382&amp;ndash;417.&lt;/li>
&lt;li>Tibshirani, R. (1996). Regression Shrinkage and Selection via the Lasso. &lt;em>Journal of the Royal Statistical Society, Series B&lt;/em>, 58(1), 267&amp;ndash;288.&lt;/li>
&lt;li>Magnus, J.R., Powell, O., and Prufer, P. (2010). A Comparison of Two Model Averaging Techniques with an Application to Growth Empirics. &lt;em>Journal of Econometrics&lt;/em>, 154(2), 139&amp;ndash;153.&lt;/li>
&lt;li>Raftery, A.E. (1995). Bayesian Model Selection in Social Research. &lt;em>Sociological Methodology&lt;/em>, 25, 111&amp;ndash;163.&lt;/li>
&lt;li>Aller, C., Ductor, L., and Grechyna, D. (2021). Robust Determinants of CO&lt;sub>2&lt;/sub> Emissions. &lt;em>Energy Economics&lt;/em>, 96, 105154.&lt;/li>
&lt;li>Belloni, A. and Chernozhukov, V. (2013). Least Squares After Model Selection in High-Dimensional Sparse Models. &lt;em>Bernoulli&lt;/em>, 19(2), 521&amp;ndash;547.&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>Introduction to Causal Inference: Double Machine Learning</title><link>https://carlos-mendez.org/post/python_doubleml/</link><pubDate>Tue, 10 Mar 2026 00:00:00 +0000</pubDate><guid>https://carlos-mendez.org/post/python_doubleml/</guid><description>&lt;h2 id="overview">Overview&lt;/h2>
&lt;p>Does a cash bonus actually cause unemployed workers to find jobs faster, or do the workers who receive bonuses simply differ from those who do not? This is the core challenge of &lt;strong>causal inference&lt;/strong>: separating a genuine treatment effect from the influence of &lt;em>confounders&lt;/em> — variables that affect both the treatment and the outcome, creating spurious associations. Standard regression can adjust for these confounders, but when their relationship with the outcome is complex and nonlinear, linear models may fail to fully remove bias.&lt;/p>
&lt;p>&lt;strong>Double Machine Learning (DML)&lt;/strong> addresses this problem by using flexible machine learning models to partial out the confounding variation, then estimating the causal effect on the cleaned residuals. In this tutorial we apply DML to the Pennsylvania Bonus Experiment, a real randomized study where some unemployment insurance claimants received a cash bonus for finding employment quickly. We estimate how much the bonus reduced unemployment duration, and we compare DML estimates against naive and covariate-adjusted OLS to see how debiasing changes the results.&lt;/p>
&lt;p>&lt;strong>Learning objectives:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Understand why prediction and causal inference require different approaches&lt;/li>
&lt;li>Learn the Partially Linear Regression (PLR) model and the partialling-out estimator&lt;/li>
&lt;li>Implement Double Machine Learning with cross-fitting using the &lt;code>doubleml&lt;/code> package&lt;/li>
&lt;li>Interpret causal effect estimates, standard errors, and confidence intervals&lt;/li>
&lt;li>Assess robustness by comparing results across different ML learners&lt;/li>
&lt;/ul>
&lt;h2 id="setup-and-imports">Setup and imports&lt;/h2>
&lt;p>Before running the analysis, install the required package if needed:&lt;/p>
&lt;pre>&lt;code class="language-python">pip install doubleml
&lt;/code>&lt;/pre>
&lt;p>The following code imports all necessary libraries and sets the configuration variables for our analysis. We use &lt;code>RANDOM_SEED = 42&lt;/code> throughout to ensure reproducibility, and define the outcome, treatment, and covariate columns that will be used in all subsequent steps.&lt;/p>
&lt;pre>&lt;code class="language-python">import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.base import clone
from sklearn.ensemble import RandomForestRegressor
from sklearn.linear_model import LassoCV, LinearRegression
from doubleml import DoubleMLData, DoubleMLPLR
from doubleml.datasets import fetch_bonus
# Reproducibility
RANDOM_SEED = 42
np.random.seed(RANDOM_SEED)
# Configuration
OUTCOME = &amp;quot;inuidur1&amp;quot;
OUTCOME_LABEL = &amp;quot;Log Unemployment Duration&amp;quot;
TREATMENT = &amp;quot;tg&amp;quot;
COVARIATES = [
&amp;quot;female&amp;quot;, &amp;quot;black&amp;quot;, &amp;quot;othrace&amp;quot;, &amp;quot;dep1&amp;quot;, &amp;quot;dep2&amp;quot;,
&amp;quot;q2&amp;quot;, &amp;quot;q3&amp;quot;, &amp;quot;q4&amp;quot;, &amp;quot;q5&amp;quot;, &amp;quot;q6&amp;quot;,
&amp;quot;agelt35&amp;quot;, &amp;quot;agegt54&amp;quot;, &amp;quot;durable&amp;quot;, &amp;quot;lusd&amp;quot;, &amp;quot;husd&amp;quot;,
]
&lt;/code>&lt;/pre>
&lt;h2 id="data-loading-the-pennsylvania-bonus-experiment">Data loading: The Pennsylvania Bonus Experiment&lt;/h2>
&lt;p>The Pennsylvania Bonus Experiment is a well-known dataset in labor economics. In this study, a random subset of unemployment insurance claimants was offered a cash bonus if they found a new job within a qualifying period. The dataset records whether each claimant received the bonus offer (treatment) and how long they remained unemployed (outcome), along with demographic and labor market covariates.&lt;/p>
&lt;pre>&lt;code class="language-python">df = fetch_bonus(&amp;quot;DataFrame&amp;quot;)
print(f&amp;quot;Dataset shape: {df.shape}&amp;quot;)
print(f&amp;quot;Observations: {len(df)}&amp;quot;)
print(f&amp;quot;\nTreatment groups:&amp;quot;)
print(df[TREATMENT].value_counts().rename({0: &amp;quot;Control&amp;quot;, 1: &amp;quot;Bonus&amp;quot;}))
print(f&amp;quot;\nOutcome ({OUTCOME}) summary:&amp;quot;)
print(df[OUTCOME].describe().round(3))
&lt;/code>&lt;/pre>
&lt;pre>&lt;code>Dataset shape: (5099, 26)
Observations: 5099
Treatment groups:
tg
Control 3354
Bonus 1745
Name: count, dtype: int64
Outcome (inuidur1) summary:
count 5099.000
mean 2.028
std 1.215
min 0.000
25% 1.099
50% 2.398
75% 3.219
max 3.951
Name: inuidur1, dtype: float64
&lt;/code>&lt;/pre>
&lt;p>The dataset contains 5,099 unemployment insurance claimants with 26 variables. The treatment is unevenly split: 1,745 claimants received the bonus offer while 3,354 served as controls. The outcome variable, log unemployment duration (&lt;code>inuidur1&lt;/code>), ranges from 0.0 to 3.95 with a mean of 2.028 and standard deviation of 1.215, indicating substantial variation in how long claimants remained unemployed. The median (2.398) exceeds the mean, suggesting a left-skewed distribution where some claimants found jobs very quickly. The interquartile range spans from 1.099 to 3.219, meaning the middle 50% of claimants had log durations in this band.&lt;/p>
&lt;h2 id="exploratory-data-analysis">Exploratory data analysis&lt;/h2>
&lt;h3 id="outcome-distribution-by-treatment-group">Outcome distribution by treatment group&lt;/h3>
&lt;p>Before modeling, we examine whether the outcome distributions differ visibly between treated and control groups. While a randomized experiment should produce balanced groups on average, visualizing the raw data helps us understand the structure of the outcome and spot any obvious patterns.&lt;/p>
&lt;pre>&lt;code class="language-python">fig, ax = plt.subplots(figsize=(8, 5))
for group, label, color in [(0, &amp;quot;Control&amp;quot;, &amp;quot;#6a9bcc&amp;quot;), (1, &amp;quot;Bonus&amp;quot;, &amp;quot;#d97757&amp;quot;)]:
subset = df[df[TREATMENT] == group][OUTCOME]
ax.hist(subset, bins=30, alpha=0.6, label=f&amp;quot;{label} (mean={subset.mean():.3f})&amp;quot;,
color=color, edgecolor=&amp;quot;white&amp;quot;)
ax.set_xlabel(OUTCOME_LABEL)
ax.set_ylabel(&amp;quot;Count&amp;quot;)
ax.set_title(f&amp;quot;Distribution of {OUTCOME_LABEL} by Treatment Group&amp;quot;)
ax.legend()
plt.savefig(&amp;quot;doubleml_outcome_by_treatment.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;)
plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="doubleml_outcome_by_treatment.png" alt="Distribution of log unemployment duration by treatment group.">&lt;/p>
&lt;p>The histogram reveals that both groups share a similar shape, with a concentration of claimants at higher log durations (around 3.0&amp;ndash;3.5) and a spread of shorter durations below 2.0. The bonus group shows a slightly lower mean (1.971) compared to the control group (2.057), a difference of about 0.09 log points. This raw gap hints at a potential treatment effect, but we cannot yet attribute it to the bonus because confounders may also differ between groups.&lt;/p>
&lt;h3 id="covariate-balance">Covariate balance&lt;/h3>
&lt;p>In a well-designed randomized experiment, the distribution of covariates should be roughly similar across treatment and control groups. We check this balance to verify that randomization worked as expected and to understand which characteristics might confound the treatment-outcome relationship if balance is imperfect.&lt;/p>
&lt;pre>&lt;code class="language-python">covariate_means = df.groupby(TREATMENT)[COVARIATES].mean()
fig, ax = plt.subplots(figsize=(12, 6))
x = np.arange(len(COVARIATES))
width = 0.35
ax.bar(x - width / 2, covariate_means.loc[0], width, label=&amp;quot;Control&amp;quot;,
color=&amp;quot;#6a9bcc&amp;quot;, edgecolor=&amp;quot;white&amp;quot;)
ax.bar(x + width / 2, covariate_means.loc[1], width, label=&amp;quot;Bonus&amp;quot;,
color=&amp;quot;#d97757&amp;quot;, edgecolor=&amp;quot;white&amp;quot;)
ax.set_xticks(x)
ax.set_xticklabels(COVARIATES, rotation=45, ha=&amp;quot;right&amp;quot;)
ax.set_ylabel(&amp;quot;Mean Value&amp;quot;)
ax.set_title(&amp;quot;Covariate Balance: Control vs Bonus Group&amp;quot;)
ax.legend()
plt.savefig(&amp;quot;doubleml_covariate_balance.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;)
plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="doubleml_covariate_balance.png" alt="Covariate balance between control and bonus groups.">&lt;/p>
&lt;p>The covariate means are nearly identical across treatment and control groups for all 15 covariates, confirming that randomization produced well-balanced groups. Demographic variables like &lt;code>female&lt;/code>, &lt;code>black&lt;/code>, and age indicators show negligible differences, as do the economic indicators (&lt;code>durable&lt;/code>, &lt;code>lusd&lt;/code>, &lt;code>husd&lt;/code>). This balance is reassuring: it means that any difference in unemployment duration between groups is unlikely to be driven by observable confounders. Nevertheless, DML provides a principled way to adjust for these covariates and improve precision.&lt;/p>
&lt;h2 id="why-adjust-for-covariates">Why adjust for covariates?&lt;/h2>
&lt;p>Because the Pennsylvania Bonus Experiment is a randomized trial, treatment assignment is independent of covariates by design — there is no confounding bias to remove. However, adjusting for covariates can still improve the &lt;em>precision&lt;/em> of the causal estimate by absorbing residual variation in the outcome. In observational studies, covariate adjustment is essential to avoid confounding bias, but even in an RCT, it sharpens inference. The question is &lt;em>how&lt;/em> to adjust. Standard OLS assumes a linear relationship between covariates and the outcome, which may miss complex nonlinear patterns. The naive OLS model regresses the outcome $Y$ directly on the treatment $D$:&lt;/p>
&lt;p>$$Y_i = \alpha + \beta \, D_i + \epsilon_i \quad \text{(naive, no covariates)}$$&lt;/p>
&lt;p>Adding covariates $X$ linearly gives:&lt;/p>
&lt;p>$$Y_i = \alpha + \beta \, D_i + X_i' \gamma + \epsilon_i \quad \text{(with covariates)}$$&lt;/p>
&lt;p>In our data, $Y_i$ is &lt;code>inuidur1&lt;/code> (log unemployment duration), $D_i$ is &lt;code>tg&lt;/code> (the bonus indicator), and $X_i$ contains the 15 demographic and labor market covariates. In both cases, $\beta$ is the estimated treatment effect. But if the true relationship between $X$ and $Y$ is nonlinear, the linear specification may leave residual confounding in $\hat{\beta}$.&lt;/p>
&lt;h3 id="naive-ols-baseline">Naive OLS baseline&lt;/h3>
&lt;p>We start with two simple OLS regressions to establish baseline estimates: one with no covariates (naive), and one that linearly adjusts for all 15 covariates. These provide a reference point for evaluating how much DML&amp;rsquo;s flexible adjustment changes the estimated treatment effect.&lt;/p>
&lt;pre>&lt;code class="language-python"># Naive OLS: no covariates
ols = LinearRegression()
ols.fit(df[[TREATMENT]], df[OUTCOME])
naive_coef = ols.coef_[0]
# OLS with covariates
ols_full = LinearRegression()
ols_full.fit(df[[TREATMENT] + COVARIATES], df[OUTCOME])
ols_full_coef = ols_full.coef_[0]
print(f&amp;quot;Naive OLS coefficient (no covariates): {naive_coef:.4f}&amp;quot;)
print(f&amp;quot;OLS with covariates coefficient: {ols_full_coef:.4f}&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code>Naive OLS coefficient (no covariates): -0.0855
OLS with covariates coefficient: -0.0717
&lt;/code>&lt;/pre>
&lt;p>The naive OLS estimate is -0.0855, suggesting that the bonus is associated with an 8.6% reduction in log unemployment duration. Adding covariates shifts the estimate to -0.0717 (7.2% reduction). In a randomized experiment, this shift reflects precision improvement from absorbing residual variation — not confounding bias removal. Even so, linear adjustment may not capture complex nonlinear relationships between covariates and the outcome. Double Machine Learning will use flexible ML models to more thoroughly partial out covariate effects and further sharpen the estimate.&lt;/p>
&lt;h2 id="what-is-double-machine-learning">What is Double Machine Learning?&lt;/h2>
&lt;h3 id="the-partially-linear-regression-plr-model">The Partially Linear Regression (PLR) model&lt;/h3>
&lt;p>Double Machine Learning operates within the &lt;strong>Partially Linear Regression&lt;/strong> framework. The key idea is that the outcome $Y$ depends on the treatment $D$ through a linear coefficient (the causal effect we want) plus a potentially complex, nonlinear function of covariates $X$. The PLR model consists of two structural equations:&lt;/p>
&lt;p>$$Y = D \, \theta_0 + g_0(X) + \varepsilon, \quad E[\varepsilon \mid D, X] = 0$$&lt;/p>
&lt;p>$$D = m_0(X) + V, \quad E[V \mid X] = 0$$&lt;/p>
&lt;p>Here, $\theta_0$ is the causal parameter of interest — the &lt;strong>Average Treatment Effect (ATE)&lt;/strong> of the bonus on unemployment duration. The function $g_0(\cdot)$ is a &lt;em>nuisance function&lt;/em>, meaning it is not our target but something we must estimate along the way; it captures how covariates affect the outcome. Similarly, $m_0(\cdot)$ models how covariates predict treatment assignment. Think of nuisance functions as scaffolding: essential during construction but not part of the final result. The error terms $\varepsilon$ and $V$ are orthogonal to the covariates by construction. In our data, $Y$ = &lt;code>inuidur1&lt;/code>, $D$ = &lt;code>tg&lt;/code>, and $X$ includes the 15 covariate columns in &lt;code>COVARIATES&lt;/code>. The challenge is that both $g_0$ and $m_0$ can be arbitrarily complex — DML uses machine learning to estimate them flexibly.&lt;/p>
&lt;h3 id="the-partialling-out-estimator">The partialling-out estimator&lt;/h3>
&lt;p>The DML algorithm works in two stages. First, it uses ML models to predict the outcome from covariates alone (estimating $E[Y \mid X]$) and to predict the treatment from covariates alone (estimating $E[D \mid X]$). Then it computes residuals from both predictions — the part of $Y$ not explained by $X$, and the part of $D$ not explained by $X$:&lt;/p>
&lt;p>$$\tilde{Y} = Y - \hat{g}_0(X) = Y - \hat{E}[Y \mid X]$$&lt;/p>
&lt;p>$$\tilde{D} = D - \hat{m}_0(X) = D - \hat{E}[D \mid X]$$&lt;/p>
&lt;p>Finally, it regresses the outcome residuals on the treatment residuals to obtain the causal estimate:&lt;/p>
&lt;p>$$\hat{\theta}_0 = \left( \frac{1}{N} \sum_{i=1}^{N} \tilde{D}_i^2 \right)^{-1} \frac{1}{N} \sum_{i=1}^{N} \tilde{D}_i \, \tilde{Y}_i$$&lt;/p>
&lt;p>Think of this like noise-canceling headphones: the ML models learn the &amp;ldquo;noise&amp;rdquo; pattern (how covariates influence both $Y$ and $D$), and we subtract it away so that only the &amp;ldquo;signal&amp;rdquo; — the causal relationship between $D$ and $Y$ — remains.&lt;/p>
&lt;h3 id="cross-fitting-why-it-matters">Cross-fitting: why it matters&lt;/h3>
&lt;p>A naive implementation of partialling-out would use the same data to fit the ML models and compute residuals. This introduces &lt;strong>regularization bias&lt;/strong> — a distortion that occurs because the ML model&amp;rsquo;s complexity penalty contaminates the causal estimate. DML solves this with &lt;strong>cross-fitting&lt;/strong>: the data is split into $K$ folds, and each fold&amp;rsquo;s residuals are computed using ML models trained on the other $K-1$ folds. Think of it like grading exams: to avoid bias, we split the class into groups where each group&amp;rsquo;s predictions are made by a model that never saw their data. The cross-fitted estimator is:&lt;/p>
&lt;p>$$\hat{\theta}_0^{CF} = \left( \frac{1}{N} \sum_{k=1}^{K} \sum_{i \in I_k} \left(\tilde{D}_i^{(k)}\right)^2 \right)^{-1} \frac{1}{N} \sum_{k=1}^{K} \sum_{i \in I_k} \tilde{D}_i^{(k)} \, \tilde{Y}_i^{(k)}$$&lt;/p>
&lt;p>where $\tilde{Y}_i^{(k)}$ and $\tilde{D}_i^{(k)}$ are residuals for observation $i$ in fold $k$, computed using models trained on all folds except $k$. In words, we average the treatment effect estimates across all folds, where each fold&amp;rsquo;s estimate uses residuals computed from models that never saw that fold&amp;rsquo;s data. This ensures that the residuals are computed out-of-sample, eliminating overfitting bias and preserving valid statistical inference (standard errors, p-values, confidence intervals).&lt;/p>
&lt;h2 id="setting-up-doubleml">Setting up DoubleML&lt;/h2>
&lt;p>The &lt;code>doubleml&lt;/code> package provides a clean interface for implementing DML. We first wrap our data into a &lt;code>DoubleMLData&lt;/code> object that specifies the outcome, treatment, and covariate columns. Then we configure the ML learners: Random Forest regressors for both the outcome model &lt;code>ml_l&lt;/code> (estimating $\hat{g}_0$) and the treatment model &lt;code>ml_m&lt;/code> (estimating $\hat{m}_0$).&lt;/p>
&lt;pre>&lt;code class="language-python"># Prepare data for DoubleML
dml_data = DoubleMLData(df, y_col=OUTCOME, d_cols=TREATMENT, x_cols=COVARIATES)
print(dml_data)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code>================== DoubleMLData Object ==================
------------------ Data summary ------------------
Outcome variable: inuidur1
Treatment variable(s): ['tg']
Covariates: ['female', 'black', 'othrace', 'dep1', 'dep2', 'q2', 'q3', 'q4', 'q5', 'q6', 'agelt35', 'agegt54', 'durable', 'lusd', 'husd']
Instrument variable(s): None
No. Observations: 5099
&lt;/code>&lt;/pre>
&lt;p>The &lt;code>DoubleMLData&lt;/code> object confirms our setup: &lt;code>inuidur1&lt;/code> as the outcome, &lt;code>tg&lt;/code> as the treatment, and all 15 covariates registered. The object reports 5,099 observations and no instrumental variables, which is correct for the PLR model. Separating the data into these three roles is fundamental to DML: the covariates $X$ will be partialled out from both $Y$ and $D$, while the treatment-outcome relationship $\theta_0$ is the sole target of inference.&lt;/p>
&lt;p>Now we configure the ML learners. We use Random Forest with 500 trees, max depth of 5, and &lt;code>sqrt&lt;/code> feature sampling — a moderate configuration that balances flexibility with regularization.&lt;/p>
&lt;pre>&lt;code class="language-python"># Configure ML learners
learner = RandomForestRegressor(n_estimators=500, max_features=&amp;quot;sqrt&amp;quot;,
max_depth=5, random_state=RANDOM_SEED)
ml_l_rf = clone(learner) # Learner for E[Y|X]
ml_m_rf = clone(learner) # Learner for E[D|X]
print(f&amp;quot;ml_l (outcome model): {type(ml_l_rf).__name__}&amp;quot;)
print(f&amp;quot;ml_m (treatment model): {type(ml_m_rf).__name__}&amp;quot;)
print(f&amp;quot; n_estimators={learner.n_estimators}, max_depth={learner.max_depth}, max_features='{learner.max_features}'&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code>ml_l (outcome model): RandomForestRegressor
ml_m (treatment model): RandomForestRegressor
n_estimators=500, max_depth=5, max_features='sqrt'
&lt;/code>&lt;/pre>
&lt;p>Both the outcome and treatment models use &lt;code>RandomForestRegressor&lt;/code> with 500 estimators and max depth 5. The &lt;code>clone()&lt;/code> function creates independent copies so that each model is trained separately during the DML fitting process. The &lt;code>max_features='sqrt'&lt;/code> setting means each split considers only the square root of 15 covariates (about 4 features), adding randomness that reduces overfitting. Capping tree depth at 5 prevents overfitting to individual observations while still capturing nonlinear interactions among covariates — a balance that matters because overly complex nuisance models can destabilize the cross-fitted residuals.&lt;/p>
&lt;h2 id="fitting-the-plr-model">Fitting the PLR model&lt;/h2>
&lt;p>With data and learners configured, we fit the Partially Linear Regression model using 5-fold cross-fitting. The &lt;code>DoubleMLPLR&lt;/code> class handles the full DML pipeline: splitting data into folds, fitting ML models on training folds, computing out-of-sample residuals, and estimating the causal coefficient with valid standard errors.&lt;/p>
&lt;pre>&lt;code class="language-python">np.random.seed(RANDOM_SEED)
dml_plr_rf = DoubleMLPLR(dml_data, ml_l_rf, ml_m_rf, n_folds=5)
dml_plr_rf.fit()
print(dml_plr_rf.summary)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code> coef std err t P&amp;gt;|t| 2.5 % 97.5 %
tg -0.0736 0.0354 -2.077 0.0378 -0.1430 -0.0041
&lt;/code>&lt;/pre>
&lt;p>The DML estimate with Random Forest learners yields a treatment coefficient of -0.0736 with a standard error of 0.0354. The t-statistic is -2.077, producing a p-value of 0.0378, which is statistically significant at the 5% level. The 95% confidence interval is [-0.1430, -0.0041], meaning we are 95% confident that the true causal effect of the bonus lies between a 14.3% and 0.4% reduction in log unemployment duration.&lt;/p>
&lt;h2 id="interpreting-the-results">Interpreting the results&lt;/h2>
&lt;p>Let us extract and interpret the key quantities from the fitted model to understand both the statistical and practical significance of the estimated treatment effect.&lt;/p>
&lt;pre>&lt;code class="language-python">rf_coef = dml_plr_rf.coef[0]
rf_se = dml_plr_rf.se[0]
rf_pval = dml_plr_rf.pval[0]
rf_ci = dml_plr_rf.confint().values[0]
print(f&amp;quot;Coefficient (theta_0): {rf_coef:.4f}&amp;quot;)
print(f&amp;quot;Standard Error: {rf_se:.4f}&amp;quot;)
print(f&amp;quot;p-value: {rf_pval:.4f}&amp;quot;)
print(f&amp;quot;95% CI: [{rf_ci[0]:.4f}, {rf_ci[1]:.4f}]&amp;quot;)
print(f&amp;quot;\nInterpretation:&amp;quot;)
print(f&amp;quot; The bonus reduces log unemployment duration by {abs(rf_coef):.4f}.&amp;quot;)
print(f&amp;quot; This corresponds to approximately a {abs(rf_coef)*100:.1f}% reduction.&amp;quot;)
print(f&amp;quot; We are 95% confident the true effect lies between&amp;quot;)
print(f&amp;quot; {abs(rf_ci[1])*100:.1f}% and {abs(rf_ci[0])*100:.1f}% reduction.&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code>Coefficient (theta_0): -0.0736
Standard Error: 0.0354
p-value: 0.0378
95% CI: [-0.1430, -0.0041]
Interpretation:
The bonus reduces log unemployment duration by 0.0736.
This corresponds to approximately a 7.4% reduction.
We are 95% confident the true effect lies between
0.4% and 14.3% reduction.
&lt;/code>&lt;/pre>
&lt;p>The estimated causal effect is $\hat{\theta}_0 = -0.0736$, meaning the cash bonus reduces log unemployment duration by approximately 7.4%. Since the outcome is in log scale, this translates to roughly a 7.1% proportional reduction in actual unemployment duration (using $e^{-0.0736} - 1 \approx -0.071$). The effect is statistically significant ($p = 0.0378$), and the 95% confidence interval is constructed as:&lt;/p>
&lt;p>$$\text{CI}_{95\%} = \hat{\theta}_0 \pm 1.96 \times \text{SE}(\hat{\theta}_0) = -0.0736 \pm 1.96 \times 0.0354 = [-0.1430, \; -0.0041]$$&lt;/p>
&lt;p>The interval excludes zero, confirming that the bonus has a genuine causal impact. However, the wide interval — spanning from a 0.4% to 14.3% reduction — reflects meaningful uncertainty about the exact magnitude.&lt;/p>
&lt;h2 id="sensitivity-does-the-choice-of-ml-learner-matter">Sensitivity: does the choice of ML learner matter?&lt;/h2>
&lt;p>A key advantage of DML is that it is &lt;em>agnostic&lt;/em> to the choice of ML learner, as long as the learner is flexible enough to approximate the true confounding function. To verify that our results are not driven by the specific choice of Random Forest, we re-estimate the model using Lasso, a fundamentally different class of learner. Lasso is a linear regression with L1 regularization, meaning it adds a penalty proportional to the absolute size of each coefficient, which drives some coefficients to exactly zero and effectively performs variable selection.&lt;/p>
&lt;pre>&lt;code class="language-python">ml_l_lasso = LassoCV()
ml_m_lasso = LassoCV()
np.random.seed(RANDOM_SEED)
dml_plr_lasso = DoubleMLPLR(dml_data, ml_l_lasso, ml_m_lasso, n_folds=5)
dml_plr_lasso.fit()
print(dml_plr_lasso.summary)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code> coef std err t P&amp;gt;|t| 2.5 % 97.5 %
tg -0.0712 0.0354 -2.009 0.0445 -0.1406 -0.0018
&lt;/code>&lt;/pre>
&lt;p>The Lasso-based DML estimate is -0.0712 with a standard error of 0.0354 and p-value of 0.0445. This is remarkably close to the Random Forest estimate of -0.0736, with a difference of only 0.0024 — less than 7% of the standard error. The 95% confidence interval is [-0.1406, -0.0018], which also excludes zero. The near-identical results across two fundamentally different learners strongly support the robustness of the estimated treatment effect.&lt;/p>
&lt;h2 id="comparing-all-estimates">Comparing all estimates&lt;/h2>
&lt;p>To see how different estimation strategies affect the results, we visualize all four coefficient estimates side by side: naive OLS, OLS with covariates, DML with Random Forest, and DML with Lasso. The DML estimates include confidence intervals derived from valid statistical inference.&lt;/p>
&lt;pre>&lt;code class="language-python">lasso_coef = dml_plr_lasso.coef[0]
lasso_se = dml_plr_lasso.se[0]
lasso_ci = dml_plr_lasso.confint().values[0]
fig, ax = plt.subplots(figsize=(8, 5))
methods = [&amp;quot;Naive OLS&amp;quot;, &amp;quot;OLS + Covariates&amp;quot;, &amp;quot;DoubleML (RF)&amp;quot;, &amp;quot;DoubleML (Lasso)&amp;quot;]
coefs = [naive_coef, ols_full_coef, rf_coef, lasso_coef]
colors = [&amp;quot;#999999&amp;quot;, &amp;quot;#666666&amp;quot;, &amp;quot;#6a9bcc&amp;quot;, &amp;quot;#d97757&amp;quot;]
ax.barh(methods, coefs, color=colors, edgecolor=&amp;quot;white&amp;quot;, height=0.6)
ax.errorbar(rf_coef, 2, xerr=[[rf_coef - rf_ci[0]], [rf_ci[1] - rf_coef]],
fmt=&amp;quot;none&amp;quot;, color=&amp;quot;#141413&amp;quot;, capsize=5, linewidth=2)
ax.errorbar(lasso_coef, 3, xerr=[[lasso_coef - lasso_ci[0]], [lasso_ci[1] - lasso_coef]],
fmt=&amp;quot;none&amp;quot;, color=&amp;quot;#141413&amp;quot;, capsize=5, linewidth=2)
ax.axvline(0, color=&amp;quot;black&amp;quot;, linewidth=0.5, linestyle=&amp;quot;--&amp;quot;)
ax.set_xlabel(&amp;quot;Estimated Coefficient (Effect on Log Unemployment Duration)&amp;quot;)
ax.set_title(&amp;quot;Naive OLS vs Double Machine Learning Estimates&amp;quot;)
plt.savefig(&amp;quot;doubleml_coefficient_comparison.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;)
plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="doubleml_coefficient_comparison.png" alt="Coefficient comparison across all estimation methods.">&lt;/p>
&lt;p>All four methods agree on the direction and approximate magnitude of the treatment effect: the bonus reduces unemployment duration. The naive OLS estimate (-0.0855) is the largest in absolute terms, while covariate adjustment and DML both shrink it toward -0.07. The DML estimates with Random Forest (-0.0736) and Lasso (-0.0712) cluster closely together and fall between the two OLS benchmarks. Crucially, only the DML estimates come with valid confidence intervals, both of which exclude zero, providing statistical evidence that the effect is real.&lt;/p>
&lt;h2 id="confidence-intervals">Confidence intervals&lt;/h2>
&lt;p>To better visualize the uncertainty around the DML estimates, we plot the 95% confidence intervals for both the Random Forest and Lasso specifications. If both intervals are similar and exclude zero, this strengthens our confidence in the causal conclusion.&lt;/p>
&lt;pre>&lt;code class="language-python">fig, ax = plt.subplots(figsize=(8, 4))
y_pos = [0, 1]
labels = [&amp;quot;DoubleML (Random Forest)&amp;quot;, &amp;quot;DoubleML (Lasso)&amp;quot;]
point_estimates = [rf_coef, lasso_coef]
ci_low = [rf_ci[0], lasso_ci[0]]
ci_high = [rf_ci[1], lasso_ci[1]]
for i, (est, lo, hi, label) in enumerate(zip(point_estimates, ci_low, ci_high, labels)):
ax.plot([lo, hi], [i, i], color=&amp;quot;#6a9bcc&amp;quot; if i == 0 else &amp;quot;#d97757&amp;quot;, linewidth=3)
ax.plot(est, i, &amp;quot;o&amp;quot;, color=&amp;quot;#141413&amp;quot;, markersize=8, zorder=5)
ax.text(hi + 0.005, i, f&amp;quot;{est:.4f} [{lo:.4f}, {hi:.4f}]&amp;quot;, va=&amp;quot;center&amp;quot;, fontsize=9)
ax.axvline(0, color=&amp;quot;black&amp;quot;, linewidth=0.5, linestyle=&amp;quot;--&amp;quot;)
ax.set_yticks(y_pos)
ax.set_yticklabels(labels)
ax.set_xlabel(&amp;quot;Treatment Effect Estimate (95% CI)&amp;quot;)
ax.set_title(&amp;quot;Confidence Intervals: DoubleML Estimates&amp;quot;)
plt.savefig(&amp;quot;doubleml_confint.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;)
plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="doubleml_confint.png" alt="95% confidence intervals for DoubleML estimates.">&lt;/p>
&lt;p>Both confidence intervals are nearly identical in width and position, spanning from roughly -0.14 to near zero. The Random Forest interval [-0.1430, -0.0041] and Lasso interval [-0.1406, -0.0018] both exclude zero, but just barely — the upper bounds are very close to zero (0.4% and 0.2% reduction, respectively). This tells us that while the bonus has a statistically significant negative effect on unemployment duration, the effect size is modest and estimated with considerable uncertainty.&lt;/p>
&lt;h2 id="summary-table">Summary table&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Method&lt;/th>
&lt;th>Coefficient&lt;/th>
&lt;th>Std Error&lt;/th>
&lt;th>p-value&lt;/th>
&lt;th>95% CI&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Naive OLS&lt;/td>
&lt;td>-0.0855&lt;/td>
&lt;td>&amp;ndash;&lt;/td>
&lt;td>&amp;ndash;&lt;/td>
&lt;td>&amp;ndash;&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>OLS + Covariates&lt;/td>
&lt;td>-0.0717&lt;/td>
&lt;td>&amp;ndash;&lt;/td>
&lt;td>&amp;ndash;&lt;/td>
&lt;td>&amp;ndash;&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>DoubleML (RF)&lt;/td>
&lt;td>-0.0736&lt;/td>
&lt;td>0.0354&lt;/td>
&lt;td>0.0378&lt;/td>
&lt;td>[-0.1430, -0.0041]&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>DoubleML (Lasso)&lt;/td>
&lt;td>-0.0712&lt;/td>
&lt;td>0.0354&lt;/td>
&lt;td>0.0445&lt;/td>
&lt;td>[-0.1406, -0.0018]&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The summary table confirms a consistent pattern across all four estimation methods. The naive OLS gives the largest estimate at -0.0855; adjusting for covariates improves precision and shifts the estimate toward -0.07. The two DML specifications produce very similar estimates of -0.0736 and -0.0712. Both DML p-values are below 0.05, providing statistically significant evidence of a causal effect. The standard errors are identical (0.0354), which is expected since both use the same sample size and cross-fitting structure.&lt;/p>
&lt;h2 id="discussion">Discussion&lt;/h2>
&lt;p>The Pennsylvania Bonus Experiment provides a clear demonstration of Double Machine Learning in action. Because the experiment was randomized, the DML estimates are close to the OLS estimates — the confounding function $g_0(X)$ is relatively flat when treatment assignment is independent of covariates. This is actually reassuring: in a well-designed experiment, flexible covariate adjustment should not dramatically change the results, and indeed the DML estimates ($\hat{\theta}_0 = -0.0736, -0.0712$) are close to the covariate-adjusted OLS (-0.0717).&lt;/p>
&lt;p>The key finding is that the cash bonus reduces log unemployment duration by approximately 7.4%, and this effect is statistically significant (p &amp;lt; 0.05). In practical terms, this means the bonus incentive helped claimants find new jobs somewhat faster. However, the wide confidence intervals suggest that the true effect could be as small as 0.4% or as large as 14.3%, so policymakers should be cautious about the precise magnitude.&lt;/p>
&lt;p>The robustness across learners (Random Forest vs. Lasso) is a strength of DML. Both learners capture similar confounding structure, and the near-identical estimates provide evidence that the result is not an artifact of a particular modeling choice.&lt;/p>
&lt;h2 id="summary-and-next-steps">Summary and next steps&lt;/h2>
&lt;p>This tutorial demonstrated Double Machine Learning for causal inference using the Pennsylvania Bonus Experiment. The key takeaways are:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Method:&lt;/strong> DML&amp;rsquo;s main advantage over OLS is not the point estimate (both give ~7% here) but the &lt;em>infrastructure&lt;/em> — valid standard errors, confidence intervals, and robustness to nonlinear confounding. On this RCT the estimates are similar; on observational data where $g_0(X)$ is complex, OLS would break down while DML remains valid&lt;/li>
&lt;li>&lt;strong>Data:&lt;/strong> The cash bonus reduces unemployment duration by 7.4% ($p = 0.038$, 95% CI: [-14.3%, -0.4%]). The wide CI means the true effect could be anywhere from negligible to substantial — policymakers should not over-interpret the point estimate&lt;/li>
&lt;li>&lt;strong>Robustness:&lt;/strong> Random Forest and Lasso produce nearly identical estimates (-0.0736 vs -0.0712), differing by less than 7% of the standard error. This learner-agnosticism is a core strength of the DML framework&lt;/li>
&lt;li>&lt;strong>Limitation:&lt;/strong> The PLR model assumes a constant treatment effect ($\theta_0$ is the same for everyone). If the bonus helps some subgroups more than others (e.g., younger vs. older workers), PLR will average over this heterogeneity — use the Interactive Regression Model (IRM) to detect it&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Limitations:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>The Pennsylvania Bonus Experiment is a randomized trial, which is the easiest setting for causal inference. DML&amp;rsquo;s advantages are more pronounced in observational studies where confounding is severe&lt;/li>
&lt;li>We used the PLR model, which assumes a linear treatment effect ($\theta_0$ is constant). More complex treatment heterogeneity could be explored with the Interactive Regression Model (IRM)&lt;/li>
&lt;li>The confidence intervals are wide, reflecting limited sample size and moderate signal strength&lt;/li>
&lt;li>We did not explore heterogeneous treatment effects — situations where the bonus might help some subgroups (e.g., younger workers, women) more than others&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Next steps:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Apply DML to an observational dataset where confounding is more severe&lt;/li>
&lt;li>Explore the Interactive Regression Model for binary treatments&lt;/li>
&lt;li>Investigate treatment effect heterogeneity using DoubleML&amp;rsquo;s &lt;code>cate()&lt;/code> functionality&lt;/li>
&lt;li>Compare additional ML learners (gradient boosting, neural networks)&lt;/li>
&lt;/ul>
&lt;h2 id="exercises">Exercises&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Change the number of folds.&lt;/strong> Re-run the DML analysis with &lt;code>n_folds=3&lt;/code> and &lt;code>n_folds=10&lt;/code>. How do the estimates and standard errors change? What are the tradeoffs of using more vs. fewer folds?&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Try a different ML learner.&lt;/strong> Replace the Random Forest with &lt;code>GradientBoostingRegressor&lt;/code> from scikit-learn. Does the estimated treatment effect change? Compare the result to the RF and Lasso estimates.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Investigate heterogeneous effects.&lt;/strong> Split the sample by gender (&lt;code>female&lt;/code>) and estimate the DML treatment effect separately for men and women. Is the bonus more effective for one group? What might explain any differences?&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="references">References&lt;/h2>
&lt;p>&lt;strong>Academic references:&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>&lt;a href="https://doi.org/10.1111/ectj.12097" target="_blank" rel="noopener">Chernozhukov, V., Chetverikov, D., Demirer, M., Duflo, E., Hansen, C., Newey, W., &amp;amp; Robins, J. (2018). Double/Debiased Machine Learning for Treatment and Structural Parameters. The Econometrics Journal, 21(1), C1&amp;ndash;C68.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.jstor.org/stable/1814176" target="_blank" rel="noopener">Woodbury, S. A., &amp;amp; Spiegelman, R. G. (1987). Bonuses to Workers and Employers to Reduce Unemployment: Randomized Trials in Illinois. American Economic Review, 77(4), 513&amp;ndash;530.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.doubleml.org/stable/api/datasets.html#doubleml.datasets.fetch_bonus" target="_blank" rel="noopener">Pennsylvania Bonus Experiment Dataset &amp;ndash; DoubleML&lt;/a>&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Package and API documentation:&lt;/strong>&lt;/p>
&lt;ol start="4">
&lt;li>&lt;a href="https://docs.doubleml.org/stable/intro/intro.html" target="_blank" rel="noopener">DoubleML &amp;ndash; Python Documentation&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.doubleml.org/stable/api/generated/doubleml.DoubleMLPLR.html" target="_blank" rel="noopener">DoubleMLPLR &amp;ndash; API Reference&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.doubleml.org/stable/api/generated/doubleml.DoubleMLData.html" target="_blank" rel="noopener">DoubleMLData &amp;ndash; API Reference&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestRegressor.html" target="_blank" rel="noopener">scikit-learn &amp;ndash; RandomForestRegressor&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LassoCV.html" target="_blank" rel="noopener">scikit-learn &amp;ndash; LassoCV&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html" target="_blank" rel="noopener">scikit-learn &amp;ndash; LinearRegression&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://numpy.org/doc/stable/" target="_blank" rel="noopener">NumPy &amp;ndash; Documentation&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://pandas.pydata.org/docs/" target="_blank" rel="noopener">pandas &amp;ndash; Documentation&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://matplotlib.org/stable/" target="_blank" rel="noopener">Matplotlib &amp;ndash; 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></channel></rss>