<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://amanpawar9.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://amanpawar9.github.io/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-07-24T10:19:45+02:00</updated><id>https://amanpawar9.github.io/feed.xml</id><title type="html">Aman Pawar</title><subtitle>Aman Pawar is a PhD researcher in the MP-AIX programme at the Max Planck Institute (with Prof. Peter Benner and Prof. Philipp Hennig), working on scientific and probabilistic machine learning, Bayesian inference, causal discovery, operator learning, and uncertainty quantification for complex dynamical systems.</subtitle><author><name>Aman Pawar</name><email>pawar@mpi-magdeburg.mpg.de</email></author><entry><title type="html">Learning Instruments You Don’t Have: Synthesizing Instrumental Variables with Graph Attention</title><link href="https://amanpawar9.github.io/blog/synthetic-instrumental-variables/" rel="alternate" type="text/html" title="Learning Instruments You Don’t Have: Synthesizing Instrumental Variables with Graph Attention" /><published>2026-07-20T00:00:00+02:00</published><updated>2026-07-20T00:00:00+02:00</updated><id>https://amanpawar9.github.io/blog/synthetic-instrumental-variables</id><content type="html" xml:base="https://amanpawar9.github.io/blog/synthetic-instrumental-variables/"><![CDATA[<p>Ask a causal question, <em>does this drug lower risk? does this policy raise wages?</em>,
and the first enemy you meet is <strong>confounding</strong>. Something you didn’t measure nudges
both the treatment and the outcome, and a naïve regression happily reports that
nudge as if it were the effect of the treatment itself.</p>

<p>The textbook cure is the <strong>instrumental variable (IV)</strong>: a source of variation that
moves the treatment but touches the outcome <em>only</em> through the treatment. The
trouble is practical, good instruments are rare, and the ones people reach for are
often only <em>barely</em> valid. This post is about a question I find genuinely fun:
<strong>if you can’t find an instrument, can you learn one?</strong> It’s the idea behind my
<a href="https://github.com/AmanPawar9/synthetic-instrumental-variables"><code class="language-plaintext highlighter-rouge">synthetic-instrumental-variables</code></a>
project.</p>

<h2 id="the-setup-briefly">The setup, briefly</h2>

<p>Write the treatment as \(T\), the outcome as \(Y\), the covariates we observe as
\(\mathbf{X}\), and the confounder we <em>don’t</em> as \(\mathbf{U}\). A simple structural
model:</p>

\[Y = \tau\,T + \phi(\mathbf{U}) + \varepsilon,
\qquad
T = \psi(\mathbf{U}, \mathbf{X}) + \eta .\]

<p>We want \(\tau\), the causal effect of \(T\) on \(Y\). Because \(\mathbf{U}\) sits in
both equations, regressing \(Y\) on \(T\) gives an estimate that is biased, the
regression can’t tell \(\tau\) apart from the shared influence of \(\mathbf{U}\).</p>

<p>An instrument \(Z\) rescues us if it satisfies two conditions:</p>

<ul>
  <li><strong>Relevance</strong>, it actually moves the treatment: \(\operatorname{Cov}(Z, T) \neq 0\).</li>
  <li><strong>Exogeneity (the exclusion restriction)</strong>, it affects the outcome <em>only</em> through the treatment: \(\operatorname{Cov}(Z, \varepsilon) = 0\).</li>
</ul>

<p>Given such a \(Z\), two-stage least squares (2SLS) recovers the effect. In the
single-instrument case it’s the wonderfully compact ratio</p>

\[\hat\tau_{\text{2SLS}} = \frac{\operatorname{Cov}(Z, Y)}{\operatorname{Cov}(Z, T)} .\]

<figure class="reveal">
<svg viewBox="0 0 760 300" role="img" aria-labelledby="dag-t dag-d" style="width:100%;height:auto;background:var(--surface-2);border:1px solid var(--border);border-radius:12px;padding:14px 10px">
  <title id="dag-t">Causal diagram for synthetic instrumental variables</title>
  <desc id="dag-d">Observed covariates X feed a graph-attention network that produces a synthetic instrument Z-hat. Z-hat is relevant to the treatment T. An unobserved confounder U affects both T and the outcome Y. The causal effect of T on Y is tau. Crucially, the synthetic instrument has no direct path to Y.</desc>
  <defs>
    <marker id="ai" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#c08a1e" /></marker>
    <marker id="ar" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#ef4444" /></marker>
    <marker id="ag" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#94a3b8" /></marker>
  </defs>
  <!-- causal path -->
  <line x1="92" y1="150" x2="222" y2="150" stroke="#c08a1e" stroke-width="2" marker-end="url(#ai)" />
  <line x1="278" y1="150" x2="442" y2="150" stroke="#c08a1e" stroke-width="2" marker-end="url(#ai)" />
  <line x1="498" y1="150" x2="662" y2="150" stroke="#c08a1e" stroke-width="3" marker-end="url(#ai)" />
  <!-- confounding -->
  <line x1="470" y1="76" x2="470" y2="122" stroke="#ef4444" stroke-width="2" stroke-dasharray="5 4" marker-end="url(#ar)" />
  <path d="M492,58 Q610,80 690,124" fill="none" stroke="#ef4444" stroke-width="2" stroke-dasharray="5 4" marker-end="url(#ar)" />
  <!-- excluded path -->
  <path d="M250,178 Q470,278 662,180" fill="none" stroke="#94a3b8" stroke-width="1.6" stroke-dasharray="3 5" marker-end="url(#ag)" />
  <text x="456" y="262" fill="#94a3b8" font-size="12" font-family="monospace">excluded, no direct path</text>
  <!-- nodes -->
  <g fill="var(--surface)" stroke="currentColor" stroke-width="1.6">
    <circle cx="66" cy="150" r="26" /><circle cx="250" cy="150" r="26" />
    <circle cx="470" cy="150" r="26" /><circle cx="690" cy="150" r="26" />
    <circle cx="470" cy="50" r="24" />
  </g>
  <g fill="currentColor" font-family="monospace" font-size="17" text-anchor="middle" stroke="none">
    <text x="66" y="156">X</text><text x="250" y="156">Ẑ</text>
    <text x="470" y="156">T</text><text x="690" y="156">Y</text><text x="470" y="56">U</text>
  </g>
  <g fill="currentColor" font-size="11.5" text-anchor="middle" opacity="0.72" stroke="none">
    <text x="66" y="196">covariates</text><text x="250" y="196">synthetic IV</text>
    <text x="470" y="196">treatment</text><text x="690" y="196">outcome</text><text x="470" y="20">confounder (U)</text>
  </g>
  <g font-family="monospace" font-size="11.5" text-anchor="middle" stroke="none">
    <text x="157" y="140" fill="#c08a1e">GAT</text>
    <text x="360" y="140" fill="#c08a1e">relevance</text>
    <text x="580" y="140" fill="#c08a1e">τ (effect)</text>
  </g>
</svg>
<figcaption>The synthetic instrument Ẑ is built from covariates X and must be relevant to T, while having no direct path to Y. The unobserved confounder U (red) is what makes naïve regression biased.</figcaption>
</figure>

<h2 id="the-idea-build-the-instrument-you-wish-you-had">The idea: build the instrument you wish you had</h2>

<p>Instead of hunting for a pre-existing instrument, we ask a model to <strong>construct</strong>
one from the covariates we already have. Not every covariate is a good ingredient,
some are themselves confounded, some are irrelevant, so the model has to be
selective, and it has to learn <em>interactions</em>, not just weights.</p>

<p>That’s a natural fit for a <strong>graph-attention network (GAT)</strong>. Represent the
covariates as nodes on a fully-connected graph and let attention decide which
covariates (and which combinations) form a strong, well-behaved instrument. The
network outputs a single scalar per unit, our synthetic instrument
\(Z = f_\theta(\mathbf{X})\).</p>

<p>The whole trick lives in the <strong>loss</strong>. We want \(Z\) to be strongly relevant to the
treatment, while staying uncorrelated with the part of the outcome the treatment
doesn’t explain, the second-stage residual \(\hat\varepsilon\), which we use as a
tractable proxy for exogeneity:</p>

\[\mathcal{L}(\theta) =
\underbrace{-\,\big|\widehat{\operatorname{Corr}}(Z, T)\big|}_{\text{be relevant}}
\;+\;
\lambda \underbrace{\big|\widehat{\operatorname{Corr}}(Z, \hat\varepsilon)\big|}_{\text{stay exogenous}} .\]

<p>The two terms pull against each other, and \(\lambda\) sets the trade-off. Minimising
this is, in spirit, asking the network for the <em>most useful</em> instrument that still
behaves itself.</p>

<h2 id="in-code">In code</h2>

<p>The core is small, a two-layer GAT that emits one number per unit, and a loss that
trades relevance against exogeneity:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">torch</span>
<span class="kn">import</span> <span class="nn">torch.nn</span> <span class="k">as</span> <span class="n">nn</span>
<span class="kn">from</span> <span class="nn">torch_geometric.nn</span> <span class="kn">import</span> <span class="n">GATConv</span>


<span class="k">class</span> <span class="nc">InstrumentGenerator</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="s">"""Turn covariates X into a single synthetic instrument Z = f(X)."""</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">d_in</span><span class="p">,</span> <span class="n">hidden</span><span class="o">=</span><span class="mi">64</span><span class="p">,</span> <span class="n">heads</span><span class="o">=</span><span class="mi">4</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">()</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">g1</span> <span class="o">=</span> <span class="n">GATConv</span><span class="p">(</span><span class="n">d_in</span><span class="p">,</span> <span class="n">hidden</span><span class="p">,</span> <span class="n">heads</span><span class="o">=</span><span class="n">heads</span><span class="p">,</span> <span class="n">concat</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">g2</span> <span class="o">=</span> <span class="n">GATConv</span><span class="p">(</span><span class="n">hidden</span> <span class="o">*</span> <span class="n">heads</span><span class="p">,</span> <span class="n">hidden</span><span class="p">,</span> <span class="n">heads</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">concat</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">head</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">hidden</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">edge_index</span><span class="p">):</span>
        <span class="n">h</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">relu</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">g1</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">edge_index</span><span class="p">))</span>
        <span class="n">h</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">relu</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">g2</span><span class="p">(</span><span class="n">h</span><span class="p">,</span> <span class="n">edge_index</span><span class="p">))</span>
        <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">head</span><span class="p">(</span><span class="n">h</span><span class="p">).</span><span class="n">squeeze</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">)</span>          <span class="c1"># one instrument value per unit
</span>

<span class="k">def</span> <span class="nf">pearson</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">eps</span><span class="o">=</span><span class="mf">1e-8</span><span class="p">):</span>
    <span class="n">a</span><span class="p">,</span> <span class="n">b</span> <span class="o">=</span> <span class="n">a</span> <span class="o">-</span> <span class="n">a</span><span class="p">.</span><span class="n">mean</span><span class="p">(),</span> <span class="n">b</span> <span class="o">-</span> <span class="n">b</span><span class="p">.</span><span class="n">mean</span><span class="p">()</span>
    <span class="k">return</span> <span class="p">(</span><span class="n">a</span> <span class="o">*</span> <span class="n">b</span><span class="p">).</span><span class="n">mean</span><span class="p">()</span> <span class="o">/</span> <span class="p">(</span><span class="n">a</span><span class="p">.</span><span class="n">std</span><span class="p">()</span> <span class="o">*</span> <span class="n">b</span><span class="p">.</span><span class="n">std</span><span class="p">()</span> <span class="o">+</span> <span class="n">eps</span><span class="p">)</span>


<span class="k">def</span> <span class="nf">iv_loss</span><span class="p">(</span><span class="n">z</span><span class="p">,</span> <span class="n">t</span><span class="p">,</span> <span class="n">resid</span><span class="p">,</span> <span class="n">lam</span><span class="o">=</span><span class="mf">1.0</span><span class="p">):</span>
    <span class="s">"""Maximise relevance to T; penalise correlation with the 2SLS residual."""</span>
    <span class="n">relevance</span>  <span class="o">=</span> <span class="n">pearson</span><span class="p">(</span><span class="n">z</span><span class="p">,</span> <span class="n">t</span><span class="p">).</span><span class="nb">abs</span><span class="p">()</span>             <span class="c1"># want this large
</span>    <span class="n">exogeneity</span> <span class="o">=</span> <span class="n">pearson</span><span class="p">(</span><span class="n">z</span><span class="p">,</span> <span class="n">resid</span><span class="p">).</span><span class="nb">abs</span><span class="p">()</span>         <span class="c1"># want this near zero
</span>    <span class="k">return</span> <span class="o">-</span><span class="n">relevance</span> <span class="o">+</span> <span class="n">lam</span> <span class="o">*</span> <span class="n">exogeneity</span>
</code></pre></div></div>

<p>Training alternates between fitting 2SLS with the current instrument (to get the
residual \(\hat\varepsilon\)) and nudging the generator to lower <code class="language-plaintext highlighter-rouge">iv_loss</code>. The
<code class="language-plaintext highlighter-rouge">edge_index</code> wires up the covariate graph; in the simplest version it’s
fully-connected and attention does the pruning.</p>

<h2 id="does-it-help">Does it help?</h2>

<p>I tested it on the <strong>LaLonde (NSW)</strong> benchmark, the classic stress-test where a
naïve comparison is badly confounded and the experimental ground truth is known.
Pairing the learned instrument with 2SLS pulled the treatment-effect estimate
substantially back toward the experimental benchmark, cutting bias relative to
ordinary least squares (about <strong>18%</strong> on my synthetic-confounding experiments,
\(n \approx 10\text{k}\)).</p>

<p>It is emphatically <strong>not</strong> magic, and it’s worth being honest about why:</p>

<ul>
  <li><strong>Exogeneity can’t be conjured from nothing.</strong> If a covariate is itself confounded, any instrument built from it inherits that flaw. The penalty term is a <em>proxy</em>, it discourages the symptom, not the disease.</li>
  <li><strong>It needs overlap and signal.</strong> With weak covariates or thin support, the “instrument’’ is weak too, and weak instruments have their own well-known pathologies.</li>
  <li><strong>Validity remains an assumption.</strong> No loss function can <em>prove</em> the exclusion restriction. I treat synthetic IVs as a tool for <strong>exploration and sensitivity analysis</strong>, one lens among several, rather than a black box that stamps a causal number.</li>
</ul>

<h2 id="why-i-like-this-direction">Why I like this direction</h2>

<p>It sits exactly where my research lives: taking a clean idea from causal inference
and asking what a modern, differentiable, representation-learning view adds, without
letting the flexibility of deep learning quietly launder away the assumptions that
make causal claims meaningful. Learned instruments won’t replace careful design, but
as a way to <em>search</em> the space of candidate instruments and to reason about
robustness, I think they’re a genuinely useful addition to the toolbox.</p>

<p>The code, including the GAT generator, the custom loss, and the 2SLS evaluation
against biased OLS, is on
<a href="https://github.com/AmanPawar9/synthetic-instrumental-variables">GitHub</a>, feedback and
issues are very welcome.</p>]]></content><author><name>Aman Pawar</name><email>pawar@mpi-magdeburg.mpg.de</email></author><category term="Causal Inference" /><category term="Graph Neural Networks" /><category term="Instrumental Variables" /><summary type="html"><![CDATA[When a valid instrument is nowhere to be found, can we learn one? A look at using graph-attention networks to synthesize instrumental variables for less-biased treatment-effect estimation under hidden confounding.]]></summary></entry></feed>