<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://thierrymoudiki.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://thierrymoudiki.github.io/" rel="alternate" type="text/html" /><updated>2026-10-05T19:21:50+00:00</updated><id>https://thierrymoudiki.github.io/feed.xml</id><title type="html">Thierry Moudiki’s webpage</title><subtitle>Personal webpage: </subtitle><entry><title type="html">Machine-learning loss reserving vs Mack Chain-Ladder under settlement speed-up and calendar inflation (Python version)</title><link href="https://thierrymoudiki.github.io/blog/2026/10/05/r/python/mlreserving-vs-mack-sweep-v2" rel="alternate" type="text/html" title="Machine-learning loss reserving vs Mack Chain-Ladder under settlement speed-up and calendar inflation (Python version)" /><published>2026-10-05T00:00:00+00:00</published><updated>2026-10-05T00:00:00+00:00</updated><id>https://thierrymoudiki.github.io/blog/2026/10/05/r/python/mlreserving-vs-mack-sweep-v2</id><content type="html" xml:base="https://thierrymoudiki.github.io/blog/2026/10/05/r/python/mlreserving-vs-mack-sweep-v2"><![CDATA[<p>Claude was the copilot for this post about my Techtonique python package <a href="https://github.com/Techtonique/mlreserving">mlreserving</a>, and yes, <strong>we’re cooked</strong> :) (no, actually, we’ll have to be very smart, competent, and creative).</p>

<p>This post is a follow-up to <a href="https://thierrymoudiki.github.io/blog/2026/10/03/r/mlreserve">‘mlreserve: machine-learning loss reserving on ‘real-world’ triangles (based on a ‘ChainLadder’ fork)’</a>, with better explanations of what’s being done; a better description of the experimental setting.</p>

<p>We simulate claims triangles whose <em>true</em> outstanding reserve is known, then ask how well two families of methods recover it:</p>

<ul>
  <li>the <strong>Mack Chain-Ladder</strong> (<code class="language-plaintext highlighter-rouge">chainladder.MackChainladder</code>), the actuarial benchmark;</li>
  <li><strong><code class="language-plaintext highlighter-rouge">mlreserving.MLReserving</code></strong> (From <a href="https://github.com/Techtonique">Techtonique</a>’s package <a href="https://github.com/Techtonique/mlreserving">mlreserving</a>) wrapped around every scikit-learn regressor that can be built with default arguments (no tuning), with two feature sets.</li>
</ul>

<p>The triangles are deliberately <em>distorted</em> in two ways that break the chain-ladder assumptions:
<strong>settlement speed-up</strong> (recent accident years pay faster) and a <strong>calendar-year inflation shock</strong>
(payments made after a given calendar period grow faster). Section 1 states the setting formally,
Section 2 makes the two distortions visible, Section 3 runs the model sweep.</p>

<blockquote>
  <p>⚠️ <strong>Deliberate data leakage.</strong> Models are <em>selected on the truth</em> of the evaluation triangles.
Section 3.4 re-scores the selected models on fresh triangles, and Section 4 discusses what that check does and does not prove.</p>
</blockquote>

<h2 id="1-formal-setting">1. Formal setting</h2>

<h3 id="11-notation">1.1 Notation</h3>

<p>A run-off triangle has accident (origin) years \(i = 1,\dots,n\) and development years \(j = 1,\dots,n\), with \(n = 10\).</p>

<p>The <strong>calendar period</strong> of cell \((i,j)\) is</p>

\[t = i + j - 1 .\]

<p>Let \(X_{ij}\) be the <strong>incremental</strong> payment of accident year \(i\) in development year \(j\), and
\(C_{ij} = \sum_{k \le j} X_{ik}\) the cumulative payment. Only cells with \(t \le n\) are observed:</p>

\[\mathcal{D} = \{X_{ij} : i + j - 1 \le n\} \quad \text{(upper triangle)}, \qquad
\mathcal{F} = \{(i,j) : i + j - 1 &gt; n\} \quad \text{(future cells)}.\]

<p>The <strong>true reserve</strong> of accident year \(i\) and the total reserve are</p>

\[R_i = \sum_{j:\,(i,j)\in\mathcal{F}} X_{ij}, \qquad R = \sum_{i=2}^{n} R_i .\]

<p>Because we simulate the full \(n \times n\) square, \(R\) is known exactly for every triangle (no tail beyond \(j = n\)).</p>

<h3 id="12-data-generating-process-aggregate-simulator">1.2 Data-generating process (aggregate simulator)</h3>

<p>The expected incremental payment factorises into an <strong>ultimate</strong>, a <strong>payment pattern</strong> and a <strong>calendar index</strong>:</p>

\[\boxed{\;\mu_{ij} \;=\; U_i \cdot \pi_{ij} \cdot I_{t}\;}, \qquad t = i+j-1 .\]

<p><strong>Ultimates (exposure).</strong> A 3% per year volume trend with lognormal noise:</p>

\[U_i = 50\,000\,\bigl(1 + 0.03\,(i-1)\bigr)\,e^{\varepsilon_i}, \qquad \varepsilon_i \overset{iid}{\sim} \mathcal{N}(0,\,0.05^2).\]

<p><strong>Payment pattern and settlement speed-up.</strong> The payment delay of a claim from accident year \(i\) is
\(\mathrm{Gamma}(\kappa = 2,\ \theta_i = m_i/\kappa)\), with mean delay \(m_i\) (in years). Development year \(j\) collects
the payments falling in \((j-1, j]\):</p>

\[\pi_{ij} = G_i(j) - G_i(j-1), \qquad G_i = \text{c.d.f. of } \mathrm{Gamma}(2,\ m_i/2).\]

<p>The <strong>speed-up</strong> shortens the mean delay linearly for accident years after \(i_0 = 4\):</p>

\[m_i \;=\; m_0\Bigl(1 - s\,\Bigl(\tfrac{i - i_0}{\,n - i_0\,}\Bigr)_{+}\Bigr), \qquad m_0 = 3,\ \ i_0 = 4,\ \ (x)_+ = \max(x,0).\]

<p>So accident years 1–4 share the same pattern, and the mean delay then falls linearly to \(m_0(1-s)\) for the
latest year: with \(s = 0.3\), from 3 years to 2.1 years. (Truncating at \(j = n\) drops at most about 1% of the mass,
for the slowest years.)</p>

<p><strong>Calendar-year inflation.</strong> A 2% per period trend up to calendar period \(\tau = 7\), then growth at rate \(g\):</p>

\[I_t = \exp\bigl(0.02\,\min(t, \tau) \;+\; g\,(t - \tau)_{+}\bigr), \qquad \tau = 7 .\]

<p>With \(g = 0.02\) this is a plain 2% trend. With \(g = 0.08\) there is a <strong>kink at \(t = 7\)</strong>. Only three observed
diagonals (\(t = 8, 9, 10\)) carry the higher rate, while every future diagonal (\(t = 11,\dots,19\)) does,
up to \(I_{19}/I_{19}^{(g=0.02)} = e^{0.06 \times 12} \approx 2.05\).</p>

<p><strong>Noise (over-dispersed Poisson).</strong> With dispersion \(\varphi = 5\),</p>

\[X_{ij} = \varphi\, N_{ij}, \qquad N_{ij} \sim \mathrm{Poisson}(\mu_{ij}/\varphi)
\quad\Longrightarrow\quad \mathbb{E}[X_{ij}] = \mu_{ij}, \quad \mathrm{Var}[X_{ij}] = \varphi\,\mu_{ij}.\]

<p><strong>Scenarios.</strong></p>

<table>
  <thead>
    <tr>
      <th>Scenario</th>
      <th>speed-up \(s\)</th>
      <th>post-\(\tau\) inflation \(g\)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Baseline (control, used for illustration only)</td>
      <td>0</td>
      <td>0.02</td>
    </tr>
    <tr>
      <td>Inflation only (illustration only)</td>
      <td>0</td>
      <td>0.08</td>
    </tr>
    <tr>
      <td><strong>Settlement speed-up</strong></td>
      <td>0.3</td>
      <td>0.02</td>
    </tr>
    <tr>
      <td><strong>Speed-up + inflation</strong></td>
      <td>0.3</td>
      <td>0.08</td>
    </tr>
  </tbody>
</table>

<p>We also use two triangles aggregated from the <a href="https://github.com/actuarial-data-science/PackageIndividualClaimsSimulator"><strong>Wang &amp; Wüthrich individual-claims simulator</strong></a>
(“Speed-up”, and “All combined” = speed-up + inflation shock + a few large, slowly paid claims). Their generating process is richer than the one above and is not restated here.</p>

<h3 id="13-why-these-distortions-break-the-chain-ladder-hypotheses">1.3 Why these distortions break the Chain-Ladder hypotheses</h3>

<p>The Chain-Ladder estimates volume-weighted age-to-age factors from the observed triangle,</p>

\[\hat f_j = \frac{\sum_{i=1}^{n-j} C_{i,j+1}}{\sum_{i=1}^{n-j} C_{ij}}, \qquad
\hat C_{in} = C_{i,n+1-i}\prod_{j=n+1-i}^{n-1}\hat f_j, \qquad \hat R_i^{\,CL} = \hat C_{in} - C_{i,n+1-i},\]

<p>and is unbiased when the expected development ratio \(\mathbb{E}[C_{i,j+1}]/\mathbb{E}[C_{ij}]\) <strong>does not depend on \(i\)</strong>.
Under our DGP (ignoring noise),</p>

\[\frac{\mathbb{E}[C_{i,j+1}]}{\mathbb{E}[C_{ij}]}
= \frac{\sum_{k\le j+1}\pi_{ik}\,I_{i+k-1}}{\sum_{k\le j}\pi_{ik}\,I_{i+k-1}} .\]

<ul>
  <li><strong>Baseline</strong> (\(s=0\), constant 2% trend). \(\pi_{ik}\) does not depend on \(i\), and
\(I_{i+k-1} = e^{0.02(i-1)}e^{0.02k}\) is separable, so the \(i\)-terms cancel and the Chain-Ladder is exactly right in expectation.</li>
  <li><strong>Speed-up.</strong> \(\pi_{ik}\) depends on \(i\). Recent accident years are <em>more developed</em> at a given age than the
older years the factors \(\hat f_j\) are estimated from. Applying those slow factors to fast years <strong>overstates</strong> the reserve.</li>
  <li><strong>Inflation kink.</strong> \(I_{i+k-1}\) is no longer separable in \((i,k)\). Future diagonals grow at \(g\), while the
factors mostly reflect the 2% regime. On its own this <strong>understates</strong> the reserve, so it partly offsets the speed-up.</li>
</ul>

<h3 id="14-the-machine-learning-reserving-model">1.4 The machine-learning reserving model</h3>

<p><a href="https://github.com/Techtonique/mlreserving"><code class="language-plaintext highlighter-rouge">mlreserving</code></a> regresses the <strong>arcsinh-transformed incremental</strong> payments on features of the cell:</p>

\[Y_{ij} = \operatorname{asinh}(X_{ij}) = f(\mathbf{x}_{ij}) + \epsilon_{ij}, \qquad (i,j) \in \mathcal{D},\]

<p>with either</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">[log]</code> features: \(\mathbf{x}_{ij} = (\log i,\ \log j)\), standardised; or</li>
  <li><code class="language-plaintext highlighter-rouge">[onehot]</code> features: \(\mathbf{x}_{ij} = (\mathbf{e}_i,\ \mathbf{e}_j)\), one-hot origin and development dummies.</li>
</ul>

<p>No calendar feature is used. Any regressor \(\hat f\) gives a reserve by back-transforming the predictions of the future cells:</p>

\[\hat R_i = \sum_{j:\,(i,j)\in\mathcal{F}} \sinh\bigl(\hat f(\mathbf{x}_{ij})\bigr), \qquad \hat R = \sum_i \hat R_i .\]

<h3 id="15-evaluation-and-the-selection-step">1.5 Evaluation and the selection step</h3>

<p>For each triangle \(k\) we record the <strong>relative error</strong> of the total reserve, \(e_k = \hat R_k / R_k - 1\).</p>

<p>For a block \(b\) of triangles (a scenario, or a single Wang &amp; Wüthrich triangle),</p>

\[\mathrm{relRMSE}_b(m) = \Bigl(\tfrac{1}{|b|}\sum_{k\in b} e_k(m)^2\Bigr)^{1/2},
\qquad \mathrm{Score}(m) = \tfrac{1}{4}\sum_{b} \mathrm{relRMSE}_b(m),\]

<p>over the four blocks: two simulated scenarios with 20 triangles each, plus the two Wang &amp; Wüthrich triangles.</p>

<p>We keep the top \(K = 5\) models <strong>using the true reserves of the very triangles we report on.</strong> For the selected models,
\(\mathrm{Score}(\hat m;\mathcal{D}_{\text{sel}})\) is an optimistically biased estimate of the risk, because
\(\mathbb{E}\bigl[\min_m \widehat{\mathrm{Score}}(m)\bigr] \le \min_m \mathbb{E}\bigl[\widehat{\mathrm{Score}}(m)\bigr]\).
Section 3.4 re-scores \(\widehat{\mathcal{M}}\) on \(\mathcal{D}_{\text{fresh}}\), new seeds from the <strong>same</strong> generator.</p>

<h2 id="0-setup">0. Setup</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">%</span><span class="n">pip</span> <span class="n">install</span> <span class="o">-</span><span class="n">q</span> <span class="n">mlreserving</span> <span class="n">chainladder</span> <span class="n">scikit</span><span class="o">-</span><span class="n">learn</span> <span class="n">matplotlib</span> <span class="n">joblib</span> <span class="n">scipy</span> <span class="n">pandas</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[2K     [90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[0m [32m79.5/79.5 kB[0m [31m1.0 MB/s[0m eta [36m0:00:00[0m
[2K   [90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[0m [32m2.3/2.3 MB[0m [31m28.5 MB/s[0m eta [36m0:00:00[0m
[2K   [90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[0m [32m10.8/10.8 MB[0m [31m43.2 MB/s[0m eta [36m0:00:00[0m
[2K   [90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[0m [32m155.3/155.3 kB[0m [31m5.2 MB/s[0m eta [36m0:00:00[0m
[2K   [90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[0m [32m257.5/257.5 kB[0m [31m11.7 MB/s[0m eta [36m0:00:00[0m
[2K   [90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[0m [32m4.9/4.9 MB[0m [31m37.9 MB/s[0m eta [36m0:00:00[0m
[2K   [90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[0m [32m86.2/86.2 kB[0m [31m5.0 MB/s[0m eta [36m0:00:00[0m
[2K   [90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[0m [32m800.1/800.1 kB[0m [31m15.8 MB/s[0m eta [36m0:00:00[0m
[?25h[31mERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
google-colab 1.0.0 requires pandas==2.2.3, but you have pandas 3.0.6 which is incompatible.[0m[31m
[0m
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">os</span>
<span class="kn">import</span> <span class="nn">time</span>
<span class="kn">import</span> <span class="nn">warnings</span>

<span class="kn">import</span> <span class="nn">chainladder</span> <span class="k">as</span> <span class="n">cl</span>
<span class="kn">import</span> <span class="nn">matplotlib.pyplot</span> <span class="k">as</span> <span class="n">plt</span>
<span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span>
<span class="kn">from</span> <span class="nn">joblib</span> <span class="kn">import</span> <span class="n">Parallel</span><span class="p">,</span> <span class="n">delayed</span>
<span class="kn">from</span> <span class="nn">matplotlib.colors</span> <span class="kn">import</span> <span class="n">TwoSlopeNorm</span>
<span class="kn">from</span> <span class="nn">mlreserving</span> <span class="kn">import</span> <span class="n">MLReserving</span>
<span class="kn">from</span> <span class="nn">scipy.stats</span> <span class="kn">import</span> <span class="n">gamma</span>
<span class="kn">from</span> <span class="nn">sklearn.base</span> <span class="kn">import</span> <span class="n">clone</span>
<span class="kn">from</span> <span class="nn">sklearn.utils</span> <span class="kn">import</span> <span class="n">all_estimators</span>

<span class="n">warnings</span><span class="p">.</span><span class="n">filterwarnings</span><span class="p">(</span><span class="s">"ignore"</span><span class="p">)</span>
<span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">[</span><span class="s">"PYTHONWARNINGS"</span><span class="p">]</span> <span class="o">=</span> <span class="s">"ignore"</span>        <span class="c1"># also silences joblib workers
</span><span class="n">pd</span><span class="p">.</span><span class="n">set_option</span><span class="p">(</span><span class="s">"display.width"</span><span class="p">,</span> <span class="mi">160</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="n">rcParams</span><span class="p">.</span><span class="n">update</span><span class="p">({</span><span class="s">"figure.dpi"</span><span class="p">:</span> <span class="mi">110</span><span class="p">,</span> <span class="s">"axes.spines.top"</span><span class="p">:</span> <span class="bp">False</span><span class="p">,</span>
                     <span class="s">"axes.spines.right"</span><span class="p">:</span> <span class="bp">False</span><span class="p">,</span> <span class="s">"font.size"</span><span class="p">:</span> <span class="mi">10</span><span class="p">})</span>

<span class="n">n</span> <span class="o">=</span> <span class="mi">10</span>                       <span class="c1"># triangle size
</span><span class="n">BASE_YEAR</span> <span class="o">=</span> <span class="mi">2000</span>             <span class="c1"># chainladder needs calendar-like origins
</span><span class="n">TAU</span> <span class="o">=</span> <span class="mi">7</span>                      <span class="c1"># calendar period where the inflation regime changes
</span><span class="n">M0</span><span class="p">,</span> <span class="n">I0</span><span class="p">,</span> <span class="n">KAPPA</span> <span class="o">=</span> <span class="mf">3.0</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mf">2.0</span>  <span class="c1"># base mean delay, first sped-up AY, gamma shape
</span><span class="n">PHI</span> <span class="o">=</span> <span class="mi">5</span>                      <span class="c1"># ODP dispersion
</span><span class="n">N_REP</span> <span class="o">=</span> <span class="mi">20</span>                   <span class="c1"># triangles per scenario
</span><span class="n">SELECT_SEEDS</span> <span class="o">=</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">N_REP</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span>
<span class="n">FRESH_SEEDS</span> <span class="o">=</span> <span class="nb">range</span><span class="p">(</span><span class="mi">101</span><span class="p">,</span> <span class="mi">101</span> <span class="o">+</span> <span class="n">N_REP</span><span class="p">)</span>
<span class="n">TOP_K</span> <span class="o">=</span> <span class="mi">5</span>
<span class="n">N_JOBS</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span>

<span class="n">SCENARIOS</span> <span class="o">=</span> <span class="p">{</span>                <span class="c1"># used in the benchmark
</span>    <span class="s">"Settlement speed-up"</span><span class="p">:</span>  <span class="nb">dict</span><span class="p">(</span><span class="n">speedup</span><span class="o">=</span><span class="mf">0.3</span><span class="p">,</span> <span class="n">inflation</span><span class="o">=</span><span class="mf">0.02</span><span class="p">),</span>
    <span class="s">"Speed-up + inflation"</span><span class="p">:</span> <span class="nb">dict</span><span class="p">(</span><span class="n">speedup</span><span class="o">=</span><span class="mf">0.3</span><span class="p">,</span> <span class="n">inflation</span><span class="o">=</span><span class="mf">0.08</span><span class="p">),</span>
<span class="p">}</span>
<span class="n">ILLUSTRATE</span> <span class="o">=</span> <span class="p">{</span>               <span class="c1"># used only in Section 2
</span>    <span class="s">"Baseline"</span><span class="p">:</span>             <span class="nb">dict</span><span class="p">(</span><span class="n">speedup</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span> <span class="n">inflation</span><span class="o">=</span><span class="mf">0.02</span><span class="p">),</span>
    <span class="s">"Inflation only"</span><span class="p">:</span>       <span class="nb">dict</span><span class="p">(</span><span class="n">speedup</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span> <span class="n">inflation</span><span class="o">=</span><span class="mf">0.08</span><span class="p">),</span>
    <span class="o">**</span><span class="n">SCENARIOS</span><span class="p">,</span>
<span class="p">}</span>
<span class="n">INK</span><span class="p">,</span> <span class="n">GREY</span><span class="p">,</span> <span class="n">ACCENT</span><span class="p">,</span> <span class="n">MACK_C</span> <span class="o">=</span> <span class="s">"#0b0b0b"</span><span class="p">,</span> <span class="s">"#b8b8b8"</span><span class="p">,</span> <span class="s">"#1baf7a"</span><span class="p">,</span> <span class="s">"#eb6834"</span>
<span class="n">SCEN_COLOURS</span> <span class="o">=</span> <span class="p">{</span><span class="s">"Baseline"</span><span class="p">:</span> <span class="s">"#8a8a8a"</span><span class="p">,</span> <span class="s">"Inflation only"</span><span class="p">:</span> <span class="s">"#7a4fd6"</span><span class="p">,</span>
                <span class="s">"Settlement speed-up"</span><span class="p">:</span> <span class="s">"#2a78d6"</span><span class="p">,</span> <span class="s">"Speed-up + inflation"</span><span class="p">:</span> <span class="s">"#eb6834"</span><span class="p">}</span>
</code></pre></div></div>

<h3 id="data-generating-process">Data-generating process</h3>

<p><code class="language-plaintext highlighter-rouge">dgp_components</code> returns the deterministic pieces of Section 1.2: \(m_i\), \(\pi_{ij}\), \(I_t\) and \(\mu_{ij}\), with \(\varepsilon_i = 0\).
<code class="language-plaintext highlighter-rouge">simulate_case</code> adds the lognormal ultimate noise and the over-dispersed Poisson noise.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">AY</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">arange</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">n</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span>
<span class="n">DEV</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">arange</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">n</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span>
<span class="n">CAL</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">add</span><span class="p">.</span><span class="n">outer</span><span class="p">(</span><span class="n">AY</span><span class="p">,</span> <span class="n">DEV</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span>                     <span class="c1"># t = i + j - 1
</span><span class="n">FUTURE</span> <span class="o">=</span> <span class="n">CAL</span> <span class="o">&gt;</span> <span class="n">n</span>


<span class="k">def</span> <span class="nf">mean_delay</span><span class="p">(</span><span class="n">speedup</span><span class="p">):</span>
    <span class="k">return</span> <span class="n">M0</span> <span class="o">*</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">speedup</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">maximum</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="p">(</span><span class="n">AY</span> <span class="o">-</span> <span class="n">I0</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">n</span> <span class="o">-</span> <span class="n">I0</span><span class="p">)))</span>


<span class="k">def</span> <span class="nf">pattern</span><span class="p">(</span><span class="n">speedup</span><span class="p">):</span>
    <span class="n">m</span> <span class="o">=</span> <span class="n">mean_delay</span><span class="p">(</span><span class="n">speedup</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">([</span><span class="n">np</span><span class="p">.</span><span class="n">diff</span><span class="p">(</span><span class="n">gamma</span><span class="p">.</span><span class="n">cdf</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">arange</span><span class="p">(</span><span class="n">n</span> <span class="o">+</span> <span class="mi">1</span><span class="p">),</span> <span class="n">a</span><span class="o">=</span><span class="n">KAPPA</span><span class="p">,</span> <span class="n">scale</span><span class="o">=</span><span class="n">mi</span> <span class="o">/</span> <span class="n">KAPPA</span><span class="p">))</span>
                     <span class="k">for</span> <span class="n">mi</span> <span class="ow">in</span> <span class="n">m</span><span class="p">])</span>                   <span class="c1"># pi_ij
</span>

<span class="k">def</span> <span class="nf">inflation_index</span><span class="p">(</span><span class="n">g</span><span class="p">,</span> <span class="n">t</span><span class="o">=</span><span class="n">CAL</span><span class="p">):</span>
    <span class="k">return</span> <span class="n">np</span><span class="p">.</span><span class="n">exp</span><span class="p">(</span><span class="mf">0.02</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">minimum</span><span class="p">(</span><span class="n">t</span><span class="p">,</span> <span class="n">TAU</span><span class="p">)</span> <span class="o">+</span> <span class="n">g</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">maximum</span><span class="p">(</span><span class="n">t</span> <span class="o">-</span> <span class="n">TAU</span><span class="p">,</span> <span class="mi">0</span><span class="p">))</span>


<span class="k">def</span> <span class="nf">dgp_components</span><span class="p">(</span><span class="n">speedup</span><span class="p">,</span> <span class="n">inflation</span><span class="p">,</span> <span class="n">eps</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
    <span class="n">eps</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">zeros</span><span class="p">(</span><span class="n">n</span><span class="p">)</span> <span class="k">if</span> <span class="n">eps</span> <span class="ow">is</span> <span class="bp">None</span> <span class="k">else</span> <span class="n">eps</span>
    <span class="n">U</span> <span class="o">=</span> <span class="mf">5e4</span> <span class="o">*</span> <span class="p">(</span><span class="mi">1</span> <span class="o">+</span> <span class="mf">0.03</span> <span class="o">*</span> <span class="p">(</span><span class="n">AY</span> <span class="o">-</span> <span class="mi">1</span><span class="p">))</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">exp</span><span class="p">(</span><span class="n">eps</span><span class="p">)</span>
    <span class="n">pi</span> <span class="o">=</span> <span class="n">pattern</span><span class="p">(</span><span class="n">speedup</span><span class="p">)</span>
    <span class="n">I</span> <span class="o">=</span> <span class="n">inflation_index</span><span class="p">(</span><span class="n">inflation</span><span class="p">)</span>
    <span class="k">return</span> <span class="nb">dict</span><span class="p">(</span><span class="n">m</span><span class="o">=</span><span class="n">mean_delay</span><span class="p">(</span><span class="n">speedup</span><span class="p">),</span> <span class="n">U</span><span class="o">=</span><span class="n">U</span><span class="p">,</span> <span class="n">pi</span><span class="o">=</span><span class="n">pi</span><span class="p">,</span> <span class="n">I</span><span class="o">=</span><span class="n">I</span><span class="p">,</span> <span class="n">mu</span><span class="o">=</span><span class="n">U</span><span class="p">[:,</span> <span class="bp">None</span><span class="p">]</span> <span class="o">*</span> <span class="n">pi</span> <span class="o">*</span> <span class="n">I</span><span class="p">)</span>


<span class="k">def</span> <span class="nf">make_case</span><span class="p">(</span><span class="n">full</span><span class="p">):</span>
    <span class="n">full</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">asarray</span><span class="p">(</span><span class="n">full</span><span class="p">,</span> <span class="n">dtype</span><span class="o">=</span><span class="nb">float</span><span class="p">)</span>
    <span class="n">cum</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">where</span><span class="p">(</span><span class="n">FUTURE</span><span class="p">,</span> <span class="n">np</span><span class="p">.</span><span class="n">nan</span><span class="p">,</span> <span class="n">np</span><span class="p">.</span><span class="n">cumsum</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">where</span><span class="p">(</span><span class="n">FUTURE</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">full</span><span class="p">),</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">))</span>
    <span class="n">i</span><span class="p">,</span> <span class="n">j</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">indices</span><span class="p">((</span><span class="n">n</span><span class="p">,</span> <span class="n">n</span><span class="p">))</span>
    <span class="nb">long</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">({</span><span class="s">"origin"</span><span class="p">:</span> <span class="n">i</span><span class="p">.</span><span class="n">ravel</span><span class="p">()</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="s">"dev"</span><span class="p">:</span> <span class="n">j</span><span class="p">.</span><span class="n">ravel</span><span class="p">()</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span>
                         <span class="s">"values"</span><span class="p">:</span> <span class="n">cum</span><span class="p">.</span><span class="n">ravel</span><span class="p">()}).</span><span class="n">dropna</span><span class="p">()</span>
    <span class="nb">long</span><span class="p">[</span><span class="s">"development"</span><span class="p">]</span> <span class="o">=</span> <span class="nb">long</span><span class="p">[</span><span class="s">"origin"</span><span class="p">]</span> <span class="o">+</span> <span class="nb">long</span><span class="p">[</span><span class="s">"dev"</span><span class="p">]</span> <span class="o">-</span> <span class="mi">1</span>
    <span class="n">cl_df</span> <span class="o">=</span> <span class="nb">long</span><span class="p">.</span><span class="n">assign</span><span class="p">(</span><span class="n">origin</span><span class="o">=</span><span class="nb">long</span><span class="p">[</span><span class="s">"origin"</span><span class="p">]</span> <span class="o">+</span> <span class="n">BASE_YEAR</span><span class="p">,</span>
                        <span class="n">development</span><span class="o">=</span><span class="nb">long</span><span class="p">[</span><span class="s">"development"</span><span class="p">]</span> <span class="o">+</span> <span class="n">BASE_YEAR</span><span class="p">)</span>
    <span class="n">tri</span> <span class="o">=</span> <span class="n">cl</span><span class="p">.</span><span class="n">Triangle</span><span class="p">(</span><span class="n">cl_df</span><span class="p">,</span> <span class="n">origin</span><span class="o">=</span><span class="s">"origin"</span><span class="p">,</span> <span class="n">development</span><span class="o">=</span><span class="s">"development"</span><span class="p">,</span>
                      <span class="n">columns</span><span class="o">=</span><span class="s">"values"</span><span class="p">,</span> <span class="n">cumulative</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="k">return</span> <span class="p">{</span><span class="s">"full"</span><span class="p">:</span> <span class="n">full</span><span class="p">,</span> <span class="s">"upper_long"</span><span class="p">:</span> <span class="nb">long</span><span class="p">[[</span><span class="s">"origin"</span><span class="p">,</span> <span class="s">"development"</span><span class="p">,</span> <span class="s">"values"</span><span class="p">]],</span>
            <span class="s">"upper_cl"</span><span class="p">:</span> <span class="n">tri</span><span class="p">,</span> <span class="s">"true_total"</span><span class="p">:</span> <span class="nb">float</span><span class="p">((</span><span class="n">full</span> <span class="o">*</span> <span class="n">FUTURE</span><span class="p">).</span><span class="nb">sum</span><span class="p">())}</span>


<span class="k">def</span> <span class="nf">simulate_case</span><span class="p">(</span><span class="n">seed</span><span class="p">,</span> <span class="n">speedup</span><span class="o">=</span><span class="mf">0.3</span><span class="p">,</span> <span class="n">inflation</span><span class="o">=</span><span class="mf">0.08</span><span class="p">,</span> <span class="n">phi</span><span class="o">=</span><span class="n">PHI</span><span class="p">):</span>
    <span class="n">rng</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="n">default_rng</span><span class="p">(</span><span class="n">seed</span><span class="p">)</span>
    <span class="n">eps</span> <span class="o">=</span> <span class="n">rng</span><span class="p">.</span><span class="n">normal</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mf">0.05</span><span class="p">,</span> <span class="n">n</span><span class="p">)</span>
    <span class="n">mu</span> <span class="o">=</span> <span class="n">dgp_components</span><span class="p">(</span><span class="n">speedup</span><span class="p">,</span> <span class="n">inflation</span><span class="p">,</span> <span class="n">eps</span><span class="p">)[</span><span class="s">"mu"</span><span class="p">]</span>
    <span class="k">return</span> <span class="n">make_case</span><span class="p">(</span><span class="n">phi</span> <span class="o">*</span> <span class="n">rng</span><span class="p">.</span><span class="n">poisson</span><span class="p">(</span><span class="n">mu</span> <span class="o">/</span> <span class="n">phi</span><span class="p">))</span>
</code></pre></div></div>

<h2 id="2-making-the-distortions-visible">2. Making the distortions visible</h2>

<h3 id="21-settlement-speed-up-payment-delays-and-patterns">2.1 Settlement speed-up: payment delays and patterns</h3>

<p>Left: the delay density \(\mathrm{Gamma}(2, m_i/2)\) for the slowest and fastest accident years.
Middle: the mean delay \(m_i\) by accident year.
Right: the cumulative share paid, \(G_i(j)\), by development year. Each curve is an accident year, and darker means more recent.
Recent years under speed-up are further along at every age. The Chain-Ladder averages the old, slow curves.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">s</span> <span class="o">=</span> <span class="n">SCENARIOS</span><span class="p">[</span><span class="s">"Settlement speed-up"</span><span class="p">][</span><span class="s">"speedup"</span><span class="p">]</span>
<span class="n">fig</span><span class="p">,</span> <span class="n">axes</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="n">subplots</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">15</span><span class="p">,</span> <span class="mf">4.3</span><span class="p">))</span>

<span class="n">x</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">linspace</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">400</span><span class="p">)</span>
<span class="k">for</span> <span class="n">mi</span><span class="p">,</span> <span class="n">lab</span><span class="p">,</span> <span class="n">col</span> <span class="ow">in</span> <span class="p">[(</span><span class="n">M0</span><span class="p">,</span> <span class="sa">f</span><span class="s">"AY 1–</span><span class="si">{</span><span class="n">I0</span><span class="si">}</span><span class="s">: mean </span><span class="si">{</span><span class="n">M0</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="si">}</span><span class="s"> y"</span><span class="p">,</span> <span class="n">GREY</span><span class="p">),</span>
                     <span class="p">(</span><span class="n">M0</span> <span class="o">*</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">s</span><span class="p">),</span> <span class="sa">f</span><span class="s">"AY </span><span class="si">{</span><span class="n">n</span><span class="si">}</span><span class="s">: mean </span><span class="si">{</span><span class="n">M0</span><span class="o">*</span><span class="p">(</span><span class="mi">1</span><span class="o">-</span><span class="n">s</span><span class="p">)</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="si">}</span><span class="s"> y"</span><span class="p">,</span> <span class="n">SCEN_COLOURS</span><span class="p">[</span><span class="s">"Settlement speed-up"</span><span class="p">])]:</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">plot</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">gamma</span><span class="p">.</span><span class="n">pdf</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">a</span><span class="o">=</span><span class="n">KAPPA</span><span class="p">,</span> <span class="n">scale</span><span class="o">=</span><span class="n">mi</span> <span class="o">/</span> <span class="n">KAPPA</span><span class="p">),</span> <span class="n">lw</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="n">col</span><span class="p">,</span> <span class="n">label</span><span class="o">=</span><span class="n">lab</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">axvline</span><span class="p">(</span><span class="n">mi</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="n">col</span><span class="p">,</span> <span class="n">lw</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">ls</span><span class="o">=</span><span class="s">":"</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nb">set</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s">"Payment-delay density"</span><span class="p">,</span> <span class="n">xlabel</span><span class="o">=</span><span class="s">"Delay (years)"</span><span class="p">,</span> <span class="n">ylabel</span><span class="o">=</span><span class="s">"Density"</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">legend</span><span class="p">(</span><span class="n">frameon</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>

<span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">plot</span><span class="p">(</span><span class="n">AY</span><span class="p">,</span> <span class="n">mean_delay</span><span class="p">(</span><span class="mi">0</span><span class="p">),</span> <span class="s">"o-"</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="n">GREY</span><span class="p">,</span> <span class="n">lw</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">label</span><span class="o">=</span><span class="s">"No speed-up"</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">plot</span><span class="p">(</span><span class="n">AY</span><span class="p">,</span> <span class="n">mean_delay</span><span class="p">(</span><span class="n">s</span><span class="p">),</span> <span class="s">"o-"</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="n">SCEN_COLOURS</span><span class="p">[</span><span class="s">"Settlement speed-up"</span><span class="p">],</span> <span class="n">lw</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span>
             <span class="n">label</span><span class="o">=</span><span class="sa">f</span><span class="s">"Speed-up s = </span><span class="si">{</span><span class="n">s</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">axvline</span><span class="p">(</span><span class="n">I0</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="n">INK</span><span class="p">,</span> <span class="n">lw</span><span class="o">=</span><span class="mf">0.8</span><span class="p">,</span> <span class="n">ls</span><span class="o">=</span><span class="s">"--"</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">annotate</span><span class="p">(</span><span class="sa">f</span><span class="s">"i₀ = </span><span class="si">{</span><span class="n">I0</span><span class="si">}</span><span class="s">"</span><span class="p">,</span> <span class="p">(</span><span class="n">I0</span> <span class="o">-</span> <span class="mf">0.15</span><span class="p">,</span> <span class="mf">2.0</span><span class="p">),</span> <span class="n">ha</span><span class="o">=</span><span class="s">"right"</span><span class="p">,</span> <span class="n">fontsize</span><span class="o">=</span><span class="mi">9</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="nb">set</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s">"Mean delay mᵢ by accident year"</span><span class="p">,</span> <span class="n">xlabel</span><span class="o">=</span><span class="s">"Accident year i"</span><span class="p">,</span>
            <span class="n">ylabel</span><span class="o">=</span><span class="s">"Mean delay (years)"</span><span class="p">,</span> <span class="n">xticks</span><span class="o">=</span><span class="n">AY</span><span class="p">,</span> <span class="n">ylim</span><span class="o">=</span><span class="p">(</span><span class="mf">1.9</span><span class="p">,</span> <span class="mf">3.4</span><span class="p">))</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">legend</span><span class="p">(</span><span class="n">frameon</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span> <span class="n">loc</span><span class="o">=</span><span class="s">"upper center"</span><span class="p">,</span> <span class="n">ncol</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>

<span class="n">cum</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">cumsum</span><span class="p">(</span><span class="n">pattern</span><span class="p">(</span><span class="n">s</span><span class="p">),</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
<span class="n">cmap</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="n">cm</span><span class="p">.</span><span class="n">Blues</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">linspace</span><span class="p">(</span><span class="mf">0.3</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">n</span><span class="p">))</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">n</span><span class="p">):</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="n">plot</span><span class="p">(</span><span class="n">DEV</span><span class="p">,</span> <span class="n">cum</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">color</span><span class="o">=</span><span class="n">cmap</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">lw</span><span class="o">=</span><span class="mf">1.8</span><span class="p">,</span> <span class="n">label</span><span class="o">=</span><span class="sa">f</span><span class="s">"AY </span><span class="si">{</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="si">}</span><span class="s">"</span> <span class="k">if</span> <span class="n">i</span> <span class="ow">in</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">n</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="k">else</span> <span class="bp">None</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="n">plot</span><span class="p">(</span><span class="n">DEV</span><span class="p">,</span> <span class="n">np</span><span class="p">.</span><span class="n">cumsum</span><span class="p">(</span><span class="n">pattern</span><span class="p">(</span><span class="mi">0</span><span class="p">),</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)[</span><span class="mi">0</span><span class="p">],</span> <span class="n">color</span><span class="o">=</span><span class="n">INK</span><span class="p">,</span> <span class="n">lw</span><span class="o">=</span><span class="mf">1.2</span><span class="p">,</span> <span class="n">ls</span><span class="o">=</span><span class="s">"--"</span><span class="p">,</span>
             <span class="n">label</span><span class="o">=</span><span class="s">"AY 1–4 (unchanged)"</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="nb">set</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s">"Cumulative share paid Gᵢ(j), speed-up"</span><span class="p">,</span> <span class="n">xlabel</span><span class="o">=</span><span class="s">"Development year j"</span><span class="p">,</span>
            <span class="n">ylabel</span><span class="o">=</span><span class="s">"Share of ultimate paid"</span><span class="p">,</span> <span class="n">xticks</span><span class="o">=</span><span class="n">DEV</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="n">legend</span><span class="p">(</span><span class="n">frameon</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span> <span class="n">loc</span><span class="o">=</span><span class="s">"lower right"</span><span class="p">)</span>
<span class="n">fig</span><span class="p">.</span><span class="n">tight_layout</span><span class="p">();</span> <span class="n">plt</span><span class="p">.</span><span class="n">show</span><span class="p">()</span>

<span class="k">print</span><span class="p">(</span><span class="s">"Share paid by end of development year 1 and 2, AY 1 vs AY 10:"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">(</span><span class="n">cum</span><span class="p">[[</span><span class="mi">0</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">],</span> <span class="p">:</span><span class="mi">2</span><span class="p">],</span> <span class="n">index</span><span class="o">=</span><span class="p">[</span><span class="s">"AY 1"</span><span class="p">,</span> <span class="s">"AY 10"</span><span class="p">],</span> <span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s">"j=1"</span><span class="p">,</span> <span class="s">"j=2"</span><span class="p">]).</span><span class="nb">round</span><span class="p">(</span><span class="mi">3</span><span class="p">))</span>
</code></pre></div></div>

<p><img src="/images/2026-10-05/2026-10-05-mlreserving-vs-mack-sweep-v2_8_0.png" alt="image-title-here" class="img-responsive" /></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Share paid by end of development year 1 and 2, AY 1 vs AY 10:
         j=1    j=2
AY 1   0.144  0.385
AY 10  0.247  0.568
</code></pre></div></div>

<h3 id="22-calendar-year-inflation-the-index-i_t">2.2 Calendar-year inflation: the index \(I_t\)</h3>

<p>The shaded area is the future (calendar periods \(t &gt; n\)), where the reserve sits. With \(g = 0.08\) the kink at
\(\tau = 7\) leaves only three observed diagonals in the new regime. The right panel is the ratio of the two indices,
which is the extra loading that inflation puts on each calendar period.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">t</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">arange</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">n</span><span class="p">)</span>
<span class="n">fig</span><span class="p">,</span> <span class="n">axes</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="n">subplots</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">13</span><span class="p">,</span> <span class="mf">4.2</span><span class="p">))</span>
<span class="k">for</span> <span class="n">g</span><span class="p">,</span> <span class="n">col</span><span class="p">,</span> <span class="n">lab</span> <span class="ow">in</span> <span class="p">[(</span><span class="mf">0.02</span><span class="p">,</span> <span class="n">GREY</span><span class="p">,</span> <span class="s">"g = 0.02 (2% throughout)"</span><span class="p">),</span>
                    <span class="p">(</span><span class="mf">0.08</span><span class="p">,</span> <span class="n">SCEN_COLOURS</span><span class="p">[</span><span class="s">"Speed-up + inflation"</span><span class="p">],</span> <span class="s">"g = 0.08 after τ = 7"</span><span class="p">)]:</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">plot</span><span class="p">(</span><span class="n">t</span><span class="p">,</span> <span class="n">inflation_index</span><span class="p">(</span><span class="n">g</span><span class="p">,</span> <span class="n">t</span><span class="p">),</span> <span class="s">"o-"</span><span class="p">,</span> <span class="n">ms</span><span class="o">=</span><span class="mi">4</span><span class="p">,</span> <span class="n">lw</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="n">col</span><span class="p">,</span> <span class="n">label</span><span class="o">=</span><span class="n">lab</span><span class="p">)</span>
<span class="n">ratio</span> <span class="o">=</span> <span class="n">inflation_index</span><span class="p">(</span><span class="mf">0.08</span><span class="p">,</span> <span class="n">t</span><span class="p">)</span> <span class="o">/</span> <span class="n">inflation_index</span><span class="p">(</span><span class="mf">0.02</span><span class="p">,</span> <span class="n">t</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">bar</span><span class="p">(</span><span class="n">t</span><span class="p">,</span> <span class="n">ratio</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="n">np</span><span class="p">.</span><span class="n">where</span><span class="p">(</span><span class="n">t</span> <span class="o">&gt;</span> <span class="n">n</span><span class="p">,</span> <span class="n">SCEN_COLOURS</span><span class="p">[</span><span class="s">"Speed-up + inflation"</span><span class="p">],</span> <span class="n">GREY</span><span class="p">),</span> <span class="n">width</span><span class="o">=</span><span class="mf">0.7</span><span class="p">)</span>
<span class="k">for</span> <span class="n">ax</span> <span class="ow">in</span> <span class="n">axes</span><span class="p">:</span>
    <span class="n">ax</span><span class="p">.</span><span class="n">axvspan</span><span class="p">(</span><span class="n">n</span> <span class="o">+</span> <span class="mf">0.5</span><span class="p">,</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">n</span> <span class="o">-</span> <span class="mf">0.5</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="n">INK</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.06</span><span class="p">)</span>
    <span class="n">ax</span><span class="p">.</span><span class="n">axvline</span><span class="p">(</span><span class="n">TAU</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="n">INK</span><span class="p">,</span> <span class="n">lw</span><span class="o">=</span><span class="mf">0.8</span><span class="p">,</span> <span class="n">ls</span><span class="o">=</span><span class="s">"--"</span><span class="p">)</span>
    <span class="n">ax</span><span class="p">.</span><span class="n">set_xticks</span><span class="p">(</span><span class="n">t</span><span class="p">)</span>
    <span class="n">ax</span><span class="p">.</span><span class="n">set_xlabel</span><span class="p">(</span><span class="s">"Calendar period t = i + j − 1"</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">text</span><span class="p">(</span><span class="n">n</span> <span class="o">+</span> <span class="mf">0.7</span><span class="p">,</span> <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">get_ylim</span><span class="p">()[</span><span class="mi">1</span><span class="p">]</span> <span class="o">*</span> <span class="mf">0.97</span><span class="p">,</span> <span class="s">"future diagonals"</span><span class="p">,</span> <span class="n">va</span><span class="o">=</span><span class="s">"top"</span><span class="p">,</span> <span class="n">fontsize</span><span class="o">=</span><span class="mi">9</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nb">set</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s">"Calendar index Iₜ"</span><span class="p">,</span> <span class="n">ylabel</span><span class="o">=</span><span class="s">"Iₜ"</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">legend</span><span class="p">(</span><span class="n">frameon</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span> <span class="n">loc</span><span class="o">=</span><span class="s">"upper left"</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="nb">set</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s">"Extra loading from inflation: Iₜ(g=0.08) / Iₜ(g=0.02)"</span><span class="p">,</span> <span class="n">ylabel</span><span class="o">=</span><span class="s">"Ratio"</span><span class="p">)</span>
<span class="k">for</span> <span class="n">tt</span><span class="p">,</span> <span class="n">r</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">t</span><span class="p">,</span> <span class="n">ratio</span><span class="p">):</span>
    <span class="k">if</span> <span class="n">tt</span> <span class="ow">in</span> <span class="p">(</span><span class="n">TAU</span><span class="p">,</span> <span class="n">n</span><span class="p">,</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">n</span> <span class="o">-</span> <span class="mi">1</span><span class="p">):</span>
        <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">annotate</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">r</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="n">f</span><span class="si">}</span><span class="s">"</span><span class="p">,</span> <span class="p">(</span><span class="n">tt</span><span class="p">,</span> <span class="n">r</span><span class="p">),</span> <span class="n">ha</span><span class="o">=</span><span class="s">"center"</span><span class="p">,</span> <span class="n">va</span><span class="o">=</span><span class="s">"bottom"</span><span class="p">,</span> <span class="n">fontsize</span><span class="o">=</span><span class="mi">9</span><span class="p">)</span>
<span class="n">fig</span><span class="p">.</span><span class="n">tight_layout</span><span class="p">();</span> <span class="n">plt</span><span class="p">.</span><span class="n">show</span><span class="p">()</span>
</code></pre></div></div>

<p><img src="/images/2026-10-05/2026-10-05-mlreserving-vs-mack-sweep-v2_10_0.png" alt="image-title-here" class="img-responsive" /></p>

<h3 id="23-where-each-distortion-hits-the-triangle">2.3 Where each distortion hits the triangle</h3>

<p>Each heatmap shows \(\log\bigl(\mu_{ij}^{\text{scenario}} / \mu_{ij}^{\text{baseline}}\bigr)\) cell by cell.
Blue means less expected payment than the baseline and red means more. The staircase marks the boundary between
the observed upper triangle and the future cells.</p>

<ul>
  <li><strong>Speed-up</strong> moves payments of recent accident years <em>to earlier development years</em>: red on the left, blue on the right.
The blue cells are almost all in the future, which is the part Chain-Ladder overstates.</li>
  <li><strong>Inflation</strong> loads the bottom-right <em>diagonals</em> and leaves the observed upper triangle almost untouched.</li>
</ul>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">base_mu</span> <span class="o">=</span> <span class="n">dgp_components</span><span class="p">(</span><span class="o">**</span><span class="n">ILLUSTRATE</span><span class="p">[</span><span class="s">"Baseline"</span><span class="p">])[</span><span class="s">"mu"</span><span class="p">]</span>
<span class="n">names</span> <span class="o">=</span> <span class="p">[</span><span class="s">"Settlement speed-up"</span><span class="p">,</span> <span class="s">"Inflation only"</span><span class="p">,</span> <span class="s">"Speed-up + inflation"</span><span class="p">]</span>
<span class="n">logr</span> <span class="o">=</span> <span class="p">{</span><span class="n">k</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">log</span><span class="p">(</span><span class="n">dgp_components</span><span class="p">(</span><span class="o">**</span><span class="n">ILLUSTRATE</span><span class="p">[</span><span class="n">k</span><span class="p">])[</span><span class="s">"mu"</span><span class="p">]</span> <span class="o">/</span> <span class="n">base_mu</span><span class="p">)</span> <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="n">names</span><span class="p">}</span>
<span class="n">lim</span> <span class="o">=</span> <span class="nb">max</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="nb">abs</span><span class="p">(</span><span class="n">v</span><span class="p">).</span><span class="nb">max</span><span class="p">()</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">logr</span><span class="p">.</span><span class="n">values</span><span class="p">())</span>
<span class="n">norm</span> <span class="o">=</span> <span class="n">TwoSlopeNorm</span><span class="p">(</span><span class="n">vmin</span><span class="o">=-</span><span class="n">lim</span><span class="p">,</span> <span class="n">vcenter</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">vmax</span><span class="o">=</span><span class="n">lim</span><span class="p">)</span>

<span class="n">fig</span><span class="p">,</span> <span class="n">axes</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="n">subplots</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">16</span><span class="p">,</span> <span class="mi">5</span><span class="p">))</span>
<span class="k">for</span> <span class="n">ax</span><span class="p">,</span> <span class="n">k</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">axes</span><span class="p">,</span> <span class="n">names</span><span class="p">):</span>
    <span class="n">im</span> <span class="o">=</span> <span class="n">ax</span><span class="p">.</span><span class="n">imshow</span><span class="p">(</span><span class="n">logr</span><span class="p">[</span><span class="n">k</span><span class="p">],</span> <span class="n">cmap</span><span class="o">=</span><span class="s">"RdBu_r"</span><span class="p">,</span> <span class="n">norm</span><span class="o">=</span><span class="n">norm</span><span class="p">,</span> <span class="n">origin</span><span class="o">=</span><span class="s">"upper"</span><span class="p">,</span>
                   <span class="n">extent</span><span class="o">=</span><span class="p">(</span><span class="mf">0.5</span><span class="p">,</span> <span class="n">n</span> <span class="o">+</span> <span class="mf">0.5</span><span class="p">,</span> <span class="n">n</span> <span class="o">+</span> <span class="mf">0.5</span><span class="p">,</span> <span class="mf">0.5</span><span class="p">))</span>
    <span class="c1"># staircase: right edge of the last observed cell j = n + 1 - i in each row
</span>    <span class="n">xs</span><span class="p">,</span> <span class="n">ys</span> <span class="o">=</span> <span class="p">[],</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">n</span> <span class="o">+</span> <span class="mi">1</span><span class="p">):</span>
        <span class="n">xs</span> <span class="o">+=</span> <span class="p">[</span><span class="n">n</span> <span class="o">+</span> <span class="mf">1.5</span> <span class="o">-</span> <span class="n">i</span><span class="p">,</span> <span class="n">n</span> <span class="o">+</span> <span class="mf">1.5</span> <span class="o">-</span> <span class="n">i</span><span class="p">]</span>
        <span class="n">ys</span> <span class="o">+=</span> <span class="p">[</span><span class="n">i</span> <span class="o">-</span> <span class="mf">0.5</span><span class="p">,</span> <span class="n">i</span> <span class="o">+</span> <span class="mf">0.5</span><span class="p">]</span>
    <span class="n">ax</span><span class="p">.</span><span class="n">plot</span><span class="p">(</span><span class="n">xs</span><span class="p">,</span> <span class="n">ys</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="n">INK</span><span class="p">,</span> <span class="n">lw</span><span class="o">=</span><span class="mf">1.6</span><span class="p">)</span>
    <span class="n">ax</span><span class="p">.</span><span class="nb">set</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="n">k</span><span class="p">,</span> <span class="n">xlabel</span><span class="o">=</span><span class="s">"Development year j"</span><span class="p">,</span> <span class="n">ylabel</span><span class="o">=</span><span class="s">"Accident year i"</span><span class="p">,</span>
           <span class="n">xticks</span><span class="o">=</span><span class="n">DEV</span><span class="p">,</span> <span class="n">yticks</span><span class="o">=</span><span class="n">AY</span><span class="p">)</span>
<span class="n">cbar</span> <span class="o">=</span> <span class="n">fig</span><span class="p">.</span><span class="n">colorbar</span><span class="p">(</span><span class="n">im</span><span class="p">,</span> <span class="n">ax</span><span class="o">=</span><span class="n">axes</span><span class="p">,</span> <span class="n">shrink</span><span class="o">=</span><span class="mf">0.85</span><span class="p">,</span> <span class="n">pad</span><span class="o">=</span><span class="mf">0.015</span><span class="p">)</span>
<span class="n">cbar</span><span class="p">.</span><span class="n">set_label</span><span class="p">(</span><span class="s">"log( μᵢⱼ scenario / μᵢⱼ baseline )"</span><span class="p">)</span>
<span class="n">fig</span><span class="p">.</span><span class="n">suptitle</span><span class="p">(</span><span class="s">"Where the distortions act (staircase = last observed diagonal)"</span><span class="p">,</span> <span class="n">x</span><span class="o">=</span><span class="mf">0.01</span><span class="p">,</span> <span class="n">ha</span><span class="o">=</span><span class="s">"left"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="n">show</span><span class="p">()</span>
</code></pre></div></div>

<p><img src="/images/2026-10-05/2026-10-05-mlreserving-vs-mack-sweep-v2_12_0.png" alt="image-title-here" class="img-responsive" /></p>

<h3 id="24-what-this-does-to-the-chain-ladder-without-noise">2.4 What this does to the Chain-Ladder, without noise</h3>

<p>We feed the <em>expected</em> triangle \(\mu_{ij}\) (no noise, \(\varepsilon_i = 0\)) to the chain-ladder formulas of Section 1.3
and compare the resulting reserve with the true expected reserve \(\sum_{(i,j)\in\mathcal F}\mu_{ij}\).
This isolates the <strong>structural bias</strong> of Chain-Ladder in each scenario. In the baseline the ratio is exactly 1, as Section 1.3 predicts.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">expected_cl_vs_truth</span><span class="p">(</span><span class="n">speedup</span><span class="p">,</span> <span class="n">inflation</span><span class="p">):</span>
    <span class="n">mu</span> <span class="o">=</span> <span class="n">dgp_components</span><span class="p">(</span><span class="n">speedup</span><span class="p">,</span> <span class="n">inflation</span><span class="p">)[</span><span class="s">"mu"</span><span class="p">]</span>
    <span class="n">C</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">cumsum</span><span class="p">(</span><span class="n">mu</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
    <span class="n">f</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">([</span><span class="n">C</span><span class="p">[:</span> <span class="n">n</span> <span class="o">-</span> <span class="n">j</span> <span class="o">-</span> <span class="mi">1</span><span class="p">,</span> <span class="n">j</span> <span class="o">+</span> <span class="mi">1</span><span class="p">].</span><span class="nb">sum</span><span class="p">()</span> <span class="o">/</span> <span class="n">C</span><span class="p">[:</span> <span class="n">n</span> <span class="o">-</span> <span class="n">j</span> <span class="o">-</span> <span class="mi">1</span><span class="p">,</span> <span class="n">j</span><span class="p">].</span><span class="nb">sum</span><span class="p">()</span> <span class="k">for</span> <span class="n">j</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">n</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)])</span>
    <span class="n">latest</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">([</span><span class="n">C</span><span class="p">[</span><span class="n">i</span><span class="p">,</span> <span class="n">n</span> <span class="o">-</span> <span class="mi">1</span> <span class="o">-</span> <span class="n">i</span><span class="p">]</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">n</span><span class="p">)])</span>
    <span class="n">cl_ult</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">([</span><span class="n">latest</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">prod</span><span class="p">(</span><span class="n">f</span><span class="p">[</span><span class="n">n</span> <span class="o">-</span> <span class="mi">1</span> <span class="o">-</span> <span class="n">i</span><span class="p">:])</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">n</span><span class="p">)])</span>
    <span class="k">return</span> <span class="n">cl_ult</span> <span class="o">-</span> <span class="n">latest</span><span class="p">,</span> <span class="p">(</span><span class="n">mu</span> <span class="o">*</span> <span class="n">FUTURE</span><span class="p">).</span><span class="nb">sum</span><span class="p">(</span><span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>


<span class="n">fig</span><span class="p">,</span> <span class="n">axes</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="n">subplots</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">14</span><span class="p">,</span> <span class="mf">4.5</span><span class="p">),</span> <span class="n">gridspec_kw</span><span class="o">=</span><span class="p">{</span><span class="s">"width_ratios"</span><span class="p">:</span> <span class="p">[</span><span class="mf">2.2</span><span class="p">,</span> <span class="mi">1</span><span class="p">]})</span>
<span class="n">totals</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">par</span> <span class="ow">in</span> <span class="n">ILLUSTRATE</span><span class="p">.</span><span class="n">items</span><span class="p">():</span>
    <span class="n">cl_res</span><span class="p">,</span> <span class="n">true_res</span> <span class="o">=</span> <span class="n">expected_cl_vs_truth</span><span class="p">(</span><span class="o">**</span><span class="n">par</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">plot</span><span class="p">(</span><span class="n">AY</span><span class="p">[</span><span class="mi">1</span><span class="p">:],</span> <span class="n">cl_res</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span> <span class="o">/</span> <span class="n">true_res</span><span class="p">[</span><span class="mi">1</span><span class="p">:],</span> <span class="s">"o-"</span><span class="p">,</span> <span class="n">lw</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="n">SCEN_COLOURS</span><span class="p">[</span><span class="n">k</span><span class="p">],</span> <span class="n">label</span><span class="o">=</span><span class="n">k</span><span class="p">)</span>
    <span class="n">totals</span><span class="p">[</span><span class="n">k</span><span class="p">]</span> <span class="o">=</span> <span class="n">cl_res</span><span class="p">.</span><span class="nb">sum</span><span class="p">()</span> <span class="o">/</span> <span class="n">true_res</span><span class="p">.</span><span class="nb">sum</span><span class="p">()</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">axhline</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="n">INK</span><span class="p">,</span> <span class="n">lw</span><span class="o">=</span><span class="mf">0.8</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nb">set</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s">"Chain-ladder reserve / true reserve, by accident year (expected triangle)"</span><span class="p">,</span>
            <span class="n">xlabel</span><span class="o">=</span><span class="s">"Accident year i"</span><span class="p">,</span> <span class="n">ylabel</span><span class="o">=</span><span class="s">"Ratio"</span><span class="p">,</span> <span class="n">xticks</span><span class="o">=</span><span class="n">AY</span><span class="p">[</span><span class="mi">1</span><span class="p">:])</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">legend</span><span class="p">(</span><span class="n">frameon</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">k</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">totals</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">barh</span><span class="p">(</span><span class="n">k</span><span class="p">[::</span><span class="o">-</span><span class="mi">1</span><span class="p">],</span> <span class="p">[</span><span class="n">totals</span><span class="p">[</span><span class="n">x</span><span class="p">]</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">k</span><span class="p">[::</span><span class="o">-</span><span class="mi">1</span><span class="p">]],</span> <span class="n">color</span><span class="o">=</span><span class="p">[</span><span class="n">SCEN_COLOURS</span><span class="p">[</span><span class="n">x</span><span class="p">]</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">k</span><span class="p">[::</span><span class="o">-</span><span class="mi">1</span><span class="p">]],</span> <span class="n">height</span><span class="o">=</span><span class="mf">0.6</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">axvline</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="n">INK</span><span class="p">,</span> <span class="n">lw</span><span class="o">=</span><span class="mf">0.8</span><span class="p">)</span>
<span class="k">for</span> <span class="n">y</span><span class="p">,</span> <span class="n">x</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">k</span><span class="p">[::</span><span class="o">-</span><span class="mi">1</span><span class="p">]):</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">annotate</span><span class="p">(</span><span class="sa">f</span><span class="s">" </span><span class="si">{</span><span class="n">totals</span><span class="p">[</span><span class="n">x</span><span class="p">]</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="n">f</span><span class="si">}</span><span class="s">"</span><span class="p">,</span> <span class="p">(</span><span class="n">totals</span><span class="p">[</span><span class="n">x</span><span class="p">],</span> <span class="n">y</span><span class="p">),</span> <span class="n">va</span><span class="o">=</span><span class="s">"center"</span><span class="p">,</span> <span class="n">fontsize</span><span class="o">=</span><span class="mi">9</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="nb">set</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s">"Total reserve ratio"</span><span class="p">,</span> <span class="n">xlabel</span><span class="o">=</span><span class="s">"CL / truth"</span><span class="p">)</span>
<span class="n">fig</span><span class="p">.</span><span class="n">tight_layout</span><span class="p">();</span> <span class="n">plt</span><span class="p">.</span><span class="n">show</span><span class="p">()</span>
</code></pre></div></div>

<p><img src="/images/2026-10-05/2026-10-05-mlreserving-vs-mack-sweep-v2_14_0.png" alt="image-title-here" class="img-responsive" /></p>

<h3 id="25-from-expectations-to-simulated-triangles-the-spread-of-the-truth-and-of-mack">2.5 From expectations to simulated triangles: the spread of the truth and of Mack</h3>

<p>With noise added, each scenario gives a <em>distribution</em> of true total reserves \(R\) over seeds. Below, for the
20 selection seeds of each benchmark scenario, are the true total reserves (black) and the Mack estimates (orange).
The gap between the two clouds is the chain-ladder bias, and it is much larger than the seed-to-seed noise.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">fig</span><span class="p">,</span> <span class="n">axes</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="n">subplots</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">14</span><span class="p">,</span> <span class="mf">4.2</span><span class="p">),</span> <span class="n">sharey</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="n">rng</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="n">default_rng</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
<span class="k">for</span> <span class="n">ax</span><span class="p">,</span> <span class="p">(</span><span class="n">sc</span><span class="p">,</span> <span class="n">par</span><span class="p">)</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">axes</span><span class="p">,</span> <span class="n">SCENARIOS</span><span class="p">.</span><span class="n">items</span><span class="p">()):</span>
    <span class="n">cases</span> <span class="o">=</span> <span class="p">[</span><span class="n">simulate_case</span><span class="p">(</span><span class="n">seed</span><span class="p">,</span> <span class="o">**</span><span class="n">par</span><span class="p">)</span> <span class="k">for</span> <span class="n">seed</span> <span class="ow">in</span> <span class="n">SELECT_SEEDS</span><span class="p">]</span>
    <span class="n">truth</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">([</span><span class="n">c</span><span class="p">[</span><span class="s">"true_total"</span><span class="p">]</span> <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">cases</span><span class="p">])</span> <span class="o">/</span> <span class="mi">1000</span>
    <span class="n">mack</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">([</span><span class="nb">float</span><span class="p">(</span><span class="n">cl</span><span class="p">.</span><span class="n">MackChainladder</span><span class="p">().</span><span class="n">fit</span><span class="p">(</span><span class="n">c</span><span class="p">[</span><span class="s">"upper_cl"</span><span class="p">]).</span><span class="n">ibnr_</span><span class="p">.</span><span class="nb">sum</span><span class="p">())</span> <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">cases</span><span class="p">])</span> <span class="o">/</span> <span class="mi">1000</span>
    <span class="k">for</span> <span class="n">y</span><span class="p">,</span> <span class="n">vals</span><span class="p">,</span> <span class="n">col</span><span class="p">,</span> <span class="n">lab</span> <span class="ow">in</span> <span class="p">[(</span><span class="mi">1</span><span class="p">,</span> <span class="n">truth</span><span class="p">,</span> <span class="n">INK</span><span class="p">,</span> <span class="s">"True reserve R"</span><span class="p">),</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">mack</span><span class="p">,</span> <span class="n">MACK_C</span><span class="p">,</span> <span class="s">"Mack estimate"</span><span class="p">)]:</span>
        <span class="n">ax</span><span class="p">.</span><span class="n">scatter</span><span class="p">(</span><span class="n">vals</span><span class="p">,</span> <span class="n">y</span> <span class="o">+</span> <span class="n">rng</span><span class="p">.</span><span class="n">uniform</span><span class="p">(</span><span class="o">-</span><span class="mf">0.15</span><span class="p">,</span> <span class="mf">0.15</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">vals</span><span class="p">)),</span> <span class="n">color</span><span class="o">=</span><span class="n">col</span><span class="p">,</span> <span class="n">s</span><span class="o">=</span><span class="mi">28</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.8</span><span class="p">,</span>
                   <span class="n">edgecolor</span><span class="o">=</span><span class="s">"white"</span><span class="p">,</span> <span class="n">linewidth</span><span class="o">=</span><span class="mf">0.5</span><span class="p">)</span>
        <span class="n">ax</span><span class="p">.</span><span class="n">vlines</span><span class="p">(</span><span class="n">vals</span><span class="p">.</span><span class="n">mean</span><span class="p">(),</span> <span class="n">y</span> <span class="o">-</span> <span class="mf">0.3</span><span class="p">,</span> <span class="n">y</span> <span class="o">+</span> <span class="mf">0.3</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="n">col</span><span class="p">,</span> <span class="n">lw</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
        <span class="n">ax</span><span class="p">.</span><span class="n">annotate</span><span class="p">(</span><span class="sa">f</span><span class="s">"mean </span><span class="si">{</span><span class="n">vals</span><span class="p">.</span><span class="n">mean</span><span class="p">()</span><span class="si">:</span><span class="p">,.</span><span class="mi">0</span><span class="n">f</span><span class="si">}</span><span class="s">"</span><span class="p">,</span> <span class="p">(</span><span class="n">vals</span><span class="p">.</span><span class="n">mean</span><span class="p">(),</span> <span class="n">y</span> <span class="o">+</span> <span class="mf">0.33</span><span class="p">),</span> <span class="n">ha</span><span class="o">=</span><span class="s">"center"</span><span class="p">,</span> <span class="n">fontsize</span><span class="o">=</span><span class="mi">9</span><span class="p">)</span>
    <span class="n">ax</span><span class="p">.</span><span class="n">set_yticks</span><span class="p">([</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">],</span> <span class="p">[</span><span class="s">"Mack estimate"</span><span class="p">,</span> <span class="s">"True reserve R"</span><span class="p">])</span>
    <span class="n">ax</span><span class="p">.</span><span class="nb">set</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">sc</span><span class="si">}</span><span class="s"> (</span><span class="si">{</span><span class="n">N_REP</span><span class="si">}</span><span class="s"> seeds)"</span><span class="p">,</span> <span class="n">xlabel</span><span class="o">=</span><span class="s">"Total reserve (millions)"</span><span class="p">,</span> <span class="n">ylim</span><span class="o">=</span><span class="p">(</span><span class="o">-</span><span class="mf">0.6</span><span class="p">,</span> <span class="mf">1.6</span><span class="p">))</span>
<span class="n">fig</span><span class="p">.</span><span class="n">tight_layout</span><span class="p">();</span> <span class="n">plt</span><span class="p">.</span><span class="n">show</span><span class="p">()</span>
</code></pre></div></div>

<p><img src="/images/2026-10-05/2026-10-05-mlreserving-vs-mack-sweep-v2_16_0.png" alt="image-title-here" class="img-responsive" /></p>

<h2 id="3-the-benchmark-untuned-scikit-learn-regressors-in-mlreserving">3. The benchmark: untuned scikit-learn regressors in <code class="language-plaintext highlighter-rouge">mlreserving</code></h2>

<h3 id="31-evaluation-triangles">3.1 Evaluation triangles</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">squares</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s">"Speed-up"</span><span class="p">:</span> <span class="p">[</span><span class="mf">4172.1</span><span class="p">,</span> <span class="mf">16153.4</span><span class="p">,</span> <span class="mf">14800.2</span><span class="p">,</span> <span class="mf">7561.1</span><span class="p">,</span> <span class="mi">3275</span><span class="p">,</span> <span class="mf">1856.3</span><span class="p">,</span> <span class="mf">726.7</span><span class="p">,</span> <span class="mf">410.7</span><span class="p">,</span> <span class="mf">35.8</span><span class="p">,</span> <span class="mi">140</span><span class="p">,</span> <span class="mf">4983.2</span><span class="p">,</span> <span class="mf">18064.1</span><span class="p">,</span> <span class="mf">13574.5</span><span class="p">,</span> <span class="mf">8350.7</span><span class="p">,</span> <span class="mf">3954.9</span><span class="p">,</span> <span class="mi">1499</span><span class="p">,</span> <span class="mf">510.3</span><span class="p">,</span> <span class="mf">9.5</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">11.3</span><span class="p">,</span> <span class="mf">4698.1</span><span class="p">,</span> <span class="mf">19428.8</span><span class="p">,</span> <span class="mi">14864</span><span class="p">,</span> <span class="mi">7343</span><span class="p">,</span> <span class="mf">3710.5</span><span class="p">,</span> <span class="mf">1378.6</span><span class="p">,</span> <span class="mf">384.7</span><span class="p">,</span> <span class="mf">92.2</span><span class="p">,</span> <span class="mf">6.8</span><span class="p">,</span> <span class="mf">27.1</span><span class="p">,</span> <span class="mf">5467.1</span><span class="p">,</span> <span class="mi">18481</span><span class="p">,</span> <span class="mf">14847.6</span><span class="p">,</span> <span class="mf">8089.7</span><span class="p">,</span> <span class="mf">2794.1</span><span class="p">,</span> <span class="mf">1379.2</span><span class="p">,</span> <span class="mf">549.5</span><span class="p">,</span> <span class="mf">152.2</span><span class="p">,</span> <span class="mf">6.8</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">5985.2</span><span class="p">,</span> <span class="mf">21516.6</span><span class="p">,</span> <span class="mf">15980.6</span><span class="p">,</span> <span class="mi">8353</span><span class="p">,</span> <span class="mf">2954.2</span><span class="p">,</span> <span class="mf">1655.5</span><span class="p">,</span> <span class="mf">636.4</span><span class="p">,</span> <span class="mf">191.3</span><span class="p">,</span> <span class="mf">211.3</span><span class="p">,</span> <span class="mf">28.9</span><span class="p">,</span> <span class="mf">8274.1</span><span class="p">,</span> <span class="mf">22014.4</span><span class="p">,</span> <span class="mf">15981.4</span><span class="p">,</span> <span class="mf">7425.8</span><span class="p">,</span> <span class="mf">2362.7</span><span class="p">,</span> <span class="mi">731</span><span class="p">,</span> <span class="mf">242.1</span><span class="p">,</span> <span class="mf">41.6</span><span class="p">,</span> <span class="mf">52.8</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">8148.4</span><span class="p">,</span> <span class="mf">24712.9</span><span class="p">,</span> <span class="mf">16329.1</span><span class="p">,</span> <span class="mf">6587.5</span><span class="p">,</span> <span class="mf">2804.9</span><span class="p">,</span> <span class="mf">753.6</span><span class="p">,</span> <span class="mf">49.5</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">9974.2</span><span class="p">,</span> <span class="mf">27573.4</span><span class="p">,</span> <span class="mi">14523</span><span class="p">,</span> <span class="mf">5141.6</span><span class="p">,</span> <span class="mf">1123.8</span><span class="p">,</span> <span class="mi">350</span><span class="p">,</span> <span class="mf">73.3</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">10698.8</span><span class="p">,</span> <span class="mf">29877.8</span><span class="p">,</span> <span class="mf">14303.3</span><span class="p">,</span> <span class="mf">3200.3</span><span class="p">,</span> <span class="mf">978.3</span><span class="p">,</span> <span class="mf">239.1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">13669.1</span><span class="p">,</span> <span class="mf">30541.7</span><span class="p">,</span> <span class="mi">10984</span><span class="p">,</span> <span class="mf">1837.5</span><span class="p">,</span> <span class="mf">157.3</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">],</span>
    <span class="s">"All combined"</span><span class="p">:</span> <span class="p">[</span><span class="mf">4236.8</span><span class="p">,</span> <span class="mf">16766.9</span><span class="p">,</span> <span class="mf">15671.5</span><span class="p">,</span> <span class="mi">8229</span><span class="p">,</span> <span class="mf">3582.8</span><span class="p">,</span> <span class="mf">2143.9</span><span class="p">,</span> <span class="mf">838.8</span><span class="p">,</span> <span class="mf">548.6</span><span class="p">,</span> <span class="mf">57.6</span><span class="p">,</span> <span class="mf">261.6</span><span class="p">,</span> <span class="mf">5195.7</span><span class="p">,</span> <span class="mf">19008.8</span><span class="p">,</span> <span class="mf">15926.9</span><span class="p">,</span> <span class="mf">9857.6</span><span class="p">,</span> <span class="mf">4643.8</span><span class="p">,</span> <span class="mf">1746.5</span><span class="p">,</span> <span class="mf">712.4</span><span class="p">,</span> <span class="mf">26.4</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">22.5</span><span class="p">,</span> <span class="mf">4956.8</span><span class="p">,</span> <span class="mf">20861.6</span><span class="p">,</span> <span class="mf">16283.4</span><span class="p">,</span> <span class="mf">8704.2</span><span class="p">,</span> <span class="mf">4321.7</span><span class="p">,</span> <span class="mf">1876.2</span><span class="p">,</span> <span class="mf">666.5</span><span class="p">,</span> <span class="mf">161.8</span><span class="p">,</span> <span class="mf">14.7</span><span class="p">,</span> <span class="mf">61.4</span><span class="p">,</span> <span class="mf">5908.2</span><span class="p">,</span> <span class="mf">20336.4</span><span class="p">,</span> <span class="mf">16580.1</span><span class="p">,</span> <span class="mf">9331.1</span><span class="p">,</span> <span class="mf">3673.8</span><span class="p">,</span> <span class="mf">2276.2</span><span class="p">,</span> <span class="mf">1205.8</span><span class="p">,</span> <span class="mf">332.2</span><span class="p">,</span> <span class="mf">15.4</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">6586.1</span><span class="p">,</span> <span class="mf">24045.6</span><span class="p">,</span> <span class="mi">18499</span><span class="p">,</span> <span class="mf">11218.3</span><span class="p">,</span> <span class="mi">4576</span><span class="p">,</span> <span class="mf">3174.8</span><span class="p">,</span> <span class="mi">1307</span><span class="p">,</span> <span class="mf">460.6</span><span class="p">,</span> <span class="mi">665</span><span class="p">,</span> <span class="mf">96.3</span><span class="p">,</span> <span class="mi">9292</span><span class="p">,</span> <span class="mf">26084.3</span><span class="p">,</span> <span class="mf">21055.7</span><span class="p">,</span> <span class="mf">11396.7</span><span class="p">,</span> <span class="mf">4203.8</span><span class="p">,</span> <span class="mf">1531.8</span><span class="p">,</span> <span class="mf">585.4</span><span class="p">,</span> <span class="mi">115</span><span class="p">,</span> <span class="mf">163.7</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">9702.9</span><span class="p">,</span> <span class="mi">33010</span><span class="p">,</span> <span class="mf">25202.7</span><span class="p">,</span> <span class="mi">12133</span><span class="p">,</span> <span class="mf">6404.4</span><span class="p">,</span> <span class="mf">1956.8</span><span class="p">,</span> <span class="mf">385.9</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">13752.5</span><span class="p">,</span> <span class="mf">42686.8</span><span class="p">,</span> <span class="mf">25857.7</span><span class="p">,</span> <span class="mf">10698.4</span><span class="p">,</span> <span class="mf">2706.5</span><span class="p">,</span> <span class="mi">3213</span><span class="p">,</span> <span class="mf">4232.5</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">17094.3</span><span class="p">,</span> <span class="mf">53962.4</span><span class="p">,</span> <span class="mf">30132.7</span><span class="p">,</span> <span class="mf">8527.6</span><span class="p">,</span> <span class="mf">3087.9</span><span class="p">,</span> <span class="mf">746.2</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">25375.8</span><span class="p">,</span> <span class="mf">63473.7</span><span class="p">,</span> <span class="mf">26195.3</span><span class="p">,</span> <span class="mf">5737.2</span><span class="p">,</span> <span class="mi">507</span><span class="p">,</span> <span class="mf">674.2</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">],</span>
<span class="p">}</span>


<span class="k">def</span> <span class="nf">build_cases</span><span class="p">(</span><span class="n">seeds</span><span class="p">):</span>
    <span class="k">return</span> <span class="p">[(</span><span class="n">sc</span><span class="p">,</span> <span class="n">seed</span><span class="p">,</span> <span class="n">simulate_case</span><span class="p">(</span><span class="n">seed</span><span class="p">,</span> <span class="o">**</span><span class="n">par</span><span class="p">))</span>
            <span class="k">for</span> <span class="n">sc</span><span class="p">,</span> <span class="n">par</span> <span class="ow">in</span> <span class="n">SCENARIOS</span><span class="p">.</span><span class="n">items</span><span class="p">()</span> <span class="k">for</span> <span class="n">seed</span> <span class="ow">in</span> <span class="n">seeds</span><span class="p">]</span>


<span class="n">select_cases</span> <span class="o">=</span> <span class="n">build_cases</span><span class="p">(</span><span class="n">SELECT_SEEDS</span><span class="p">)</span> <span class="o">+</span> <span class="p">[</span>
    <span class="p">(</span><span class="sa">f</span><span class="s">"W&amp;W: </span><span class="si">{</span><span class="n">k</span><span class="si">}</span><span class="s">"</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">make_case</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">reshape</span><span class="p">(</span><span class="n">v</span><span class="p">,</span> <span class="p">(</span><span class="n">n</span><span class="p">,</span> <span class="n">n</span><span class="p">))))</span> <span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">squares</span><span class="p">.</span><span class="n">items</span><span class="p">()]</span>
<span class="n">fresh_cases</span> <span class="o">=</span> <span class="n">build_cases</span><span class="p">(</span><span class="n">FRESH_SEEDS</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"selection set: </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">select_cases</span><span class="p">)</span><span class="si">}</span><span class="s"> triangles, fresh set: </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">fresh_cases</span><span class="p">)</span><span class="si">}</span><span class="s"> triangles"</span><span class="p">)</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>selection set: 42 triangles, fresh set: 40 triangles
</code></pre></div></div>

<h3 id="32-candidates-and-scoring">3.2 Candidates and scoring</h3>

<p>Every scikit-learn regressor that builds with default arguments, under both feature sets \(\times\) <code class="language-plaintext highlighter-rouge">[log]</code>, <code class="language-plaintext highlighter-rouge">[onehot]</code>.
Wrappers and multi-output-only estimators are excluded. A pair that errors or returns a non-finite reserve on any
triangle is dropped. Residuals are taken in-sample (<code class="language-plaintext highlighter-rouge">residual_source="in_sample"</code>); only point estimates are needed here.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">EXCLUDE</span> <span class="o">=</span> <span class="p">{</span><span class="s">"CCA"</span><span class="p">,</span> <span class="s">"PLSCanonical"</span><span class="p">,</span> <span class="s">"IsotonicRegression"</span><span class="p">,</span> <span class="s">"MultiOutputRegressor"</span><span class="p">,</span>
           <span class="s">"RegressorChain"</span><span class="p">,</span> <span class="s">"StackingRegressor"</span><span class="p">,</span> <span class="s">"VotingRegressor"</span><span class="p">,</span>
           <span class="s">"MultiTaskElasticNet"</span><span class="p">,</span> <span class="s">"MultiTaskElasticNetCV"</span><span class="p">,</span> <span class="s">"MultiTaskLasso"</span><span class="p">,</span>
           <span class="s">"MultiTaskLassoCV"</span><span class="p">,</span> <span class="s">"TransformedTargetRegressor"</span><span class="p">}</span>


<span class="k">def</span> <span class="nf">candidates</span><span class="p">():</span>
    <span class="n">out</span> <span class="o">=</span> <span class="p">{}</span>
    <span class="k">for</span> <span class="n">name</span><span class="p">,</span> <span class="n">Est</span> <span class="ow">in</span> <span class="n">all_estimators</span><span class="p">(</span><span class="n">type_filter</span><span class="o">=</span><span class="s">"regressor"</span><span class="p">):</span>
        <span class="k">if</span> <span class="n">name</span> <span class="ow">in</span> <span class="n">EXCLUDE</span><span class="p">:</span>
            <span class="k">continue</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="n">est</span> <span class="o">=</span> <span class="n">Est</span><span class="p">()</span>
        <span class="k">except</span> <span class="nb">Exception</span><span class="p">:</span>
            <span class="k">continue</span>
        <span class="k">if</span> <span class="s">"random_state"</span> <span class="ow">in</span> <span class="n">est</span><span class="p">.</span><span class="n">get_params</span><span class="p">():</span>
            <span class="n">est</span><span class="p">.</span><span class="n">set_params</span><span class="p">(</span><span class="n">random_state</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
        <span class="n">out</span><span class="p">[</span><span class="n">name</span><span class="p">]</span> <span class="o">=</span> <span class="n">est</span>
    <span class="k">return</span> <span class="n">out</span>


<span class="k">def</span> <span class="nf">total_ibnr_ml</span><span class="p">(</span><span class="n">est</span><span class="p">,</span> <span class="n">case</span><span class="p">,</span> <span class="n">use_factors</span><span class="p">):</span>
    <span class="n">m</span> <span class="o">=</span> <span class="n">MLReserving</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="n">clone</span><span class="p">(</span><span class="n">est</span><span class="p">),</span> <span class="n">use_factors</span><span class="o">=</span><span class="n">use_factors</span><span class="p">,</span>
                    <span class="n">residual_source</span><span class="o">=</span><span class="s">"in_sample"</span><span class="p">,</span> <span class="n">random_state</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
    <span class="n">m</span><span class="p">.</span><span class="n">fit</span><span class="p">(</span><span class="n">case</span><span class="p">[</span><span class="s">"upper_long"</span><span class="p">],</span> <span class="n">origin_col</span><span class="o">=</span><span class="s">"origin"</span><span class="p">,</span> <span class="n">development_col</span><span class="o">=</span><span class="s">"development"</span><span class="p">,</span>
          <span class="n">value_col</span><span class="o">=</span><span class="s">"values"</span><span class="p">,</span> <span class="n">cumulated</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="n">m</span><span class="p">.</span><span class="n">predict</span><span class="p">()</span>
    <span class="k">return</span> <span class="nb">float</span><span class="p">(</span><span class="n">m</span><span class="p">.</span><span class="n">get_ibnr</span><span class="p">().</span><span class="n">mean</span><span class="p">.</span><span class="nb">sum</span><span class="p">())</span>


<span class="k">def</span> <span class="nf">total_ibnr_mack</span><span class="p">(</span><span class="n">case</span><span class="p">):</span>
    <span class="k">return</span> <span class="nb">float</span><span class="p">(</span><span class="n">cl</span><span class="p">.</span><span class="n">MackChainladder</span><span class="p">().</span><span class="n">fit</span><span class="p">(</span><span class="n">case</span><span class="p">[</span><span class="s">"upper_cl"</span><span class="p">]).</span><span class="n">ibnr_</span><span class="p">.</span><span class="nb">sum</span><span class="p">())</span>


<span class="k">def</span> <span class="nf">score_one</span><span class="p">(</span><span class="n">label</span><span class="p">,</span> <span class="n">est</span><span class="p">,</span> <span class="n">use_factors</span><span class="p">,</span> <span class="n">cases</span><span class="p">):</span>
    <span class="kn">import</span> <span class="nn">warnings</span><span class="p">;</span> <span class="n">warnings</span><span class="p">.</span><span class="n">filterwarnings</span><span class="p">(</span><span class="s">"ignore"</span><span class="p">)</span>
    <span class="n">rows</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">sc</span><span class="p">,</span> <span class="n">seed</span><span class="p">,</span> <span class="n">case</span> <span class="ow">in</span> <span class="n">cases</span><span class="p">:</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="n">pred</span> <span class="o">=</span> <span class="n">total_ibnr_mack</span><span class="p">(</span><span class="n">case</span><span class="p">)</span> <span class="k">if</span> <span class="n">est</span> <span class="ow">is</span> <span class="bp">None</span> <span class="k">else</span> <span class="n">total_ibnr_ml</span><span class="p">(</span><span class="n">est</span><span class="p">,</span> <span class="n">case</span><span class="p">,</span> <span class="n">use_factors</span><span class="p">)</span>
        <span class="k">except</span> <span class="nb">Exception</span><span class="p">:</span>
            <span class="k">return</span> <span class="p">[]</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="n">np</span><span class="p">.</span><span class="n">isfinite</span><span class="p">(</span><span class="n">pred</span><span class="p">):</span>
            <span class="k">return</span> <span class="p">[]</span>
        <span class="n">rows</span><span class="p">.</span><span class="n">append</span><span class="p">({</span><span class="s">"model"</span><span class="p">:</span> <span class="n">label</span><span class="p">,</span> <span class="s">"scenario"</span><span class="p">:</span> <span class="n">sc</span><span class="p">,</span> <span class="s">"seed"</span><span class="p">:</span> <span class="n">seed</span><span class="p">,</span>
                     <span class="s">"error"</span><span class="p">:</span> <span class="n">pred</span> <span class="o">-</span> <span class="n">case</span><span class="p">[</span><span class="s">"true_total"</span><span class="p">],</span>
                     <span class="s">"rel_error"</span><span class="p">:</span> <span class="n">pred</span> <span class="o">/</span> <span class="n">case</span><span class="p">[</span><span class="s">"true_total"</span><span class="p">]</span> <span class="o">-</span> <span class="mi">1</span><span class="p">})</span>
    <span class="k">return</span> <span class="n">rows</span>


<span class="k">def</span> <span class="nf">summarise</span><span class="p">(</span><span class="n">df</span><span class="p">):</span>
    <span class="n">g</span> <span class="o">=</span> <span class="n">df</span><span class="p">.</span><span class="n">groupby</span><span class="p">([</span><span class="s">"model"</span><span class="p">,</span> <span class="s">"scenario"</span><span class="p">])</span>
    <span class="n">s</span> <span class="o">=</span> <span class="n">g</span><span class="p">[</span><span class="s">"error"</span><span class="p">].</span><span class="n">agg</span><span class="p">(</span><span class="n">bias</span><span class="o">=</span><span class="s">"mean"</span><span class="p">,</span> <span class="n">RMSE</span><span class="o">=</span><span class="k">lambda</span> <span class="n">e</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">sqrt</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">mean</span><span class="p">(</span><span class="n">e</span> <span class="o">**</span> <span class="mi">2</span><span class="p">)))</span>
    <span class="n">s</span><span class="p">[</span><span class="s">"relRMSE"</span><span class="p">]</span> <span class="o">=</span> <span class="n">g</span><span class="p">[</span><span class="s">"rel_error"</span><span class="p">].</span><span class="n">agg</span><span class="p">(</span><span class="k">lambda</span> <span class="n">e</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">sqrt</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">mean</span><span class="p">(</span><span class="n">e</span> <span class="o">**</span> <span class="mi">2</span><span class="p">)))</span>
    <span class="k">return</span> <span class="n">s</span><span class="p">.</span><span class="n">reset_index</span><span class="p">()</span>


<span class="k">def</span> <span class="nf">pivot_rel</span><span class="p">(</span><span class="n">s</span><span class="p">):</span>
    <span class="n">p</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">pivot</span><span class="p">(</span><span class="n">index</span><span class="o">=</span><span class="s">"model"</span><span class="p">,</span> <span class="n">columns</span><span class="o">=</span><span class="s">"scenario"</span><span class="p">,</span> <span class="n">values</span><span class="o">=</span><span class="s">"relRMSE"</span><span class="p">)</span>
    <span class="n">p</span><span class="p">[</span><span class="s">"mean relRMSE"</span><span class="p">]</span> <span class="o">=</span> <span class="n">p</span><span class="p">.</span><span class="n">mean</span><span class="p">(</span><span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">p</span><span class="p">.</span><span class="n">sort_values</span><span class="p">(</span><span class="s">"mean relRMSE"</span><span class="p">)</span>


<span class="n">cands</span> <span class="o">=</span> <span class="n">candidates</span><span class="p">()</span>
<span class="n">jobs</span> <span class="o">=</span> <span class="p">[(</span><span class="s">"Mack Chain-Ladder"</span><span class="p">,</span> <span class="bp">None</span><span class="p">,</span> <span class="bp">False</span><span class="p">)]</span> <span class="o">+</span> <span class="p">[</span>
    <span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s"> [</span><span class="si">{</span><span class="s">'onehot'</span> <span class="k">if</span> <span class="n">f</span> <span class="k">else</span> <span class="s">'log'</span><span class="si">}</span><span class="s">]"</span><span class="p">,</span> <span class="n">est</span><span class="p">,</span> <span class="n">f</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">name</span><span class="p">,</span> <span class="n">est</span> <span class="ow">in</span> <span class="n">cands</span><span class="p">.</span><span class="n">items</span><span class="p">()</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="p">(</span><span class="bp">False</span><span class="p">,</span> <span class="bp">True</span><span class="p">)]</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">jobs</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="si">}</span><span class="s"> (estimator, feature-set) pairs + Mack"</span><span class="p">)</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>86 (estimator, feature-set) pairs + Mack
</code></pre></div></div>

<h3 id="33-sweep-on-the-selection-set--23-minutes-on-2-cores">3.3 Sweep on the selection set (≈ 2–3 minutes on 2 cores)</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">t0</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span>
<span class="n">res</span> <span class="o">=</span> <span class="n">Parallel</span><span class="p">(</span><span class="n">n_jobs</span><span class="o">=</span><span class="n">N_JOBS</span><span class="p">)(</span><span class="n">delayed</span><span class="p">(</span><span class="n">score_one</span><span class="p">)(</span><span class="n">lbl</span><span class="p">,</span> <span class="n">est</span><span class="p">,</span> <span class="n">f</span><span class="p">,</span> <span class="n">select_cases</span><span class="p">)</span> <span class="k">for</span> <span class="n">lbl</span><span class="p">,</span> <span class="n">est</span><span class="p">,</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">jobs</span><span class="p">)</span>
<span class="n">sel</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">([</span><span class="n">r</span> <span class="k">for</span> <span class="n">rr</span> <span class="ow">in</span> <span class="n">res</span> <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">rr</span><span class="p">])</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"done in </span><span class="si">{</span><span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">t0</span><span class="si">:</span><span class="p">.</span><span class="mi">0</span><span class="n">f</span><span class="si">}</span><span class="s">s; </span><span class="si">{</span><span class="n">sel</span><span class="p">[</span><span class="s">'model'</span><span class="p">].</span><span class="n">nunique</span><span class="p">()</span> <span class="o">-</span> <span class="mi">1</span><span class="si">}</span><span class="s"> pairs ran without error"</span><span class="p">)</span>

<span class="n">sel_rank</span> <span class="o">=</span> <span class="n">pivot_rel</span><span class="p">(</span><span class="n">summarise</span><span class="p">(</span><span class="n">sel</span><span class="p">))</span>
<span class="n">mack_rank</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">sel_rank</span><span class="p">.</span><span class="n">index</span><span class="p">).</span><span class="n">index</span><span class="p">(</span><span class="s">"Mack Chain-Ladder"</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Mack Chain-Ladder rank: </span><span class="si">{</span><span class="n">mack_rank</span><span class="si">}</span><span class="s"> / </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">sel_rank</span><span class="p">)</span><span class="si">}</span><span class="se">\n</span><span class="s">"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Relative RMSE of the total reserve (W&amp;W columns are single triangles, i.e. |relative error|):"</span><span class="p">)</span>
<span class="n">sel_rank</span><span class="p">.</span><span class="nb">round</span><span class="p">(</span><span class="mi">3</span><span class="p">).</span><span class="n">head</span><span class="p">(</span><span class="mi">20</span><span class="p">)</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>done in 516s; 82 pairs ran without error
Mack Chain-Ladder rank: 60 / 83

Relative RMSE of the total reserve (W&amp;W columns are single triangles, i.e. |relative error|):
</code></pre></div></div>

<div>
<style scoped="">
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th>scenario</th>
      <th>Settlement speed-up</th>
      <th>Speed-up + inflation</th>
      <th>W&amp;W: All combined</th>
      <th>W&amp;W: Speed-up</th>
      <th>mean relRMSE</th>
    </tr>
    <tr>
      <th>model</th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>MLPRegressor [onehot]</th>
      <td>0.039</td>
      <td>0.127</td>
      <td>0.229</td>
      <td>0.022</td>
      <td>0.104</td>
    </tr>
    <tr>
      <th>AdaBoostRegressor [onehot]</th>
      <td>0.211</td>
      <td>0.100</td>
      <td>0.241</td>
      <td>0.182</td>
      <td>0.183</td>
    </tr>
    <tr>
      <th>ExtraTreeRegressor [onehot]</th>
      <td>0.139</td>
      <td>0.069</td>
      <td>0.159</td>
      <td>0.395</td>
      <td>0.191</td>
    </tr>
    <tr>
      <th>ExtraTreesRegressor [onehot]</th>
      <td>0.139</td>
      <td>0.070</td>
      <td>0.171</td>
      <td>0.404</td>
      <td>0.196</td>
    </tr>
    <tr>
      <th>DecisionTreeRegressor [onehot]</th>
      <td>0.140</td>
      <td>0.069</td>
      <td>0.176</td>
      <td>0.430</td>
      <td>0.204</td>
    </tr>
    <tr>
      <th>ExtraTreeRegressor [log]</th>
      <td>0.345</td>
      <td>0.109</td>
      <td>0.052</td>
      <td>0.340</td>
      <td>0.211</td>
    </tr>
    <tr>
      <th>BaggingRegressor [onehot]</th>
      <td>0.177</td>
      <td>0.046</td>
      <td>0.215</td>
      <td>0.418</td>
      <td>0.214</td>
    </tr>
    <tr>
      <th>DecisionTreeRegressor [log]</th>
      <td>0.137</td>
      <td>0.337</td>
      <td>0.052</td>
      <td>0.340</td>
      <td>0.217</td>
    </tr>
    <tr>
      <th>RandomForestRegressor [onehot]</th>
      <td>0.206</td>
      <td>0.046</td>
      <td>0.171</td>
      <td>0.458</td>
      <td>0.220</td>
    </tr>
    <tr>
      <th>OrthogonalMatchingPursuitCV [onehot]</th>
      <td>0.090</td>
      <td>0.158</td>
      <td>0.202</td>
      <td>0.437</td>
      <td>0.222</td>
    </tr>
    <tr>
      <th>RandomForestRegressor [log]</th>
      <td>0.190</td>
      <td>0.142</td>
      <td>0.183</td>
      <td>0.397</td>
      <td>0.228</td>
    </tr>
    <tr>
      <th>BaggingRegressor [log]</th>
      <td>0.194</td>
      <td>0.128</td>
      <td>0.185</td>
      <td>0.408</td>
      <td>0.229</td>
    </tr>
    <tr>
      <th>AdaBoostRegressor [log]</th>
      <td>0.178</td>
      <td>0.138</td>
      <td>0.069</td>
      <td>0.561</td>
      <td>0.237</td>
    </tr>
    <tr>
      <th>OrthogonalMatchingPursuit [log]</th>
      <td>0.067</td>
      <td>0.168</td>
      <td>0.590</td>
      <td>0.317</td>
      <td>0.286</td>
    </tr>
    <tr>
      <th>SGDRegressor [log]</th>
      <td>0.581</td>
      <td>0.349</td>
      <td>0.144</td>
      <td>0.164</td>
      <td>0.310</td>
    </tr>
    <tr>
      <th>GradientBoostingRegressor [log]</th>
      <td>0.212</td>
      <td>0.155</td>
      <td>0.376</td>
      <td>0.520</td>
      <td>0.316</td>
    </tr>
    <tr>
      <th>ARDRegression [log]</th>
      <td>0.655</td>
      <td>0.425</td>
      <td>0.192</td>
      <td>0.003</td>
      <td>0.319</td>
    </tr>
    <tr>
      <th>ExtraTreesRegressor [log]</th>
      <td>0.305</td>
      <td>0.232</td>
      <td>0.265</td>
      <td>0.492</td>
      <td>0.324</td>
    </tr>
    <tr>
      <th>HistGradientBoostingRegressor [log]</th>
      <td>0.532</td>
      <td>0.281</td>
      <td>0.005</td>
      <td>0.525</td>
      <td>0.336</td>
    </tr>
    <tr>
      <th>LassoCV [log]</th>
      <td>0.677</td>
      <td>0.432</td>
      <td>0.174</td>
      <td>0.104</td>
      <td>0.347</td>
    </tr>
  </tbody>
</table>
</div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">picks</span> <span class="o">=</span> <span class="p">[</span><span class="n">m</span> <span class="k">for</span> <span class="n">m</span> <span class="ow">in</span> <span class="n">sel_rank</span><span class="p">.</span><span class="n">index</span> <span class="k">if</span> <span class="n">m</span> <span class="o">!=</span> <span class="s">"Mack Chain-Ladder"</span><span class="p">][:</span><span class="n">TOP_K</span><span class="p">]</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Picked:"</span><span class="p">,</span> <span class="n">picks</span><span class="p">)</span>

<span class="c1"># Top 40 pairs, plus Mack appended below a gap if it ranks outside the top 40
</span><span class="n">TOP_N</span> <span class="o">=</span> <span class="mi">40</span>
<span class="n">score</span> <span class="o">=</span> <span class="n">sel_rank</span><span class="p">[</span><span class="s">"mean relRMSE"</span><span class="p">]</span>
<span class="n">top</span> <span class="o">=</span> <span class="n">score</span><span class="p">.</span><span class="n">head</span><span class="p">(</span><span class="n">TOP_N</span><span class="p">)</span>
<span class="n">labels</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">top</span><span class="p">.</span><span class="n">index</span><span class="p">)</span>
<span class="n">values</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">top</span><span class="p">.</span><span class="n">values</span><span class="p">)</span>
<span class="n">ypos</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">labels</span><span class="p">)))</span>
<span class="k">if</span> <span class="s">"Mack Chain-Ladder"</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">top</span><span class="p">.</span><span class="n">index</span><span class="p">:</span>
    <span class="n">labels</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s">"Mack Chain-Ladder (rank </span><span class="si">{</span><span class="n">mack_rank</span><span class="si">}</span><span class="s"> / </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">score</span><span class="p">)</span><span class="si">}</span><span class="s">)"</span><span class="p">)</span>
    <span class="n">values</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">score</span><span class="p">[</span><span class="s">"Mack Chain-Ladder"</span><span class="p">])</span>
    <span class="n">ypos</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">top</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span>                    <span class="c1"># leave one empty row as a gap
</span><span class="n">colors</span> <span class="o">=</span> <span class="p">[</span><span class="n">MACK_C</span> <span class="k">if</span> <span class="n">m</span><span class="p">.</span><span class="n">startswith</span><span class="p">(</span><span class="s">"Mack"</span><span class="p">)</span> <span class="k">else</span> <span class="n">ACCENT</span> <span class="k">if</span> <span class="n">m</span> <span class="ow">in</span> <span class="n">picks</span> <span class="k">else</span> <span class="n">GREY</span> <span class="k">for</span> <span class="n">m</span> <span class="ow">in</span> <span class="n">labels</span><span class="p">]</span>

<span class="n">fig</span><span class="p">,</span> <span class="n">ax</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="n">subplots</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="mf">10.5</span><span class="p">))</span>
<span class="n">ax</span><span class="p">.</span><span class="n">barh</span><span class="p">(</span><span class="n">ypos</span><span class="p">,</span> <span class="n">values</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="n">colors</span><span class="p">,</span> <span class="n">height</span><span class="o">=</span><span class="mf">0.7</span><span class="p">)</span>
<span class="n">ax</span><span class="p">.</span><span class="n">set_yticks</span><span class="p">(</span><span class="n">ypos</span><span class="p">,</span> <span class="n">labels</span><span class="p">)</span>
<span class="n">ax</span><span class="p">.</span><span class="n">invert_yaxis</span><span class="p">()</span>
<span class="k">if</span> <span class="n">ypos</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">&gt;</span> <span class="nb">len</span><span class="p">(</span><span class="n">top</span><span class="p">):</span>
    <span class="n">ax</span><span class="p">.</span><span class="n">axhline</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">top</span><span class="p">),</span> <span class="n">color</span><span class="o">=</span><span class="n">INK</span><span class="p">,</span> <span class="n">lw</span><span class="o">=</span><span class="mf">0.8</span><span class="p">,</span> <span class="n">ls</span><span class="o">=</span><span class="s">":"</span><span class="p">)</span>
    <span class="n">ax</span><span class="p">.</span><span class="n">annotate</span><span class="p">(</span><span class="sa">f</span><span class="s">"… </span><span class="si">{</span><span class="n">mack_rank</span> <span class="o">-</span> <span class="n">TOP_N</span> <span class="o">-</span> <span class="mi">1</span><span class="si">}</span><span class="s"> pairs not shown …"</span><span class="p">,</span> <span class="p">(</span><span class="mf">0.5</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">top</span><span class="p">)),</span>
                <span class="n">xycoords</span><span class="o">=</span><span class="p">(</span><span class="s">"axes fraction"</span><span class="p">,</span> <span class="s">"data"</span><span class="p">),</span> <span class="n">ha</span><span class="o">=</span><span class="s">"center"</span><span class="p">,</span> <span class="n">va</span><span class="o">=</span><span class="s">"center"</span><span class="p">,</span>
                <span class="n">fontsize</span><span class="o">=</span><span class="mi">9</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="n">INK</span><span class="p">,</span> <span class="n">backgroundcolor</span><span class="o">=</span><span class="s">"white"</span><span class="p">)</span>
<span class="n">ax</span><span class="p">.</span><span class="n">set_xscale</span><span class="p">(</span><span class="s">"log"</span><span class="p">)</span>
<span class="n">ax</span><span class="p">.</span><span class="nb">set</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">TOP_N</span><span class="si">}</span><span class="s"> best (estimator, features) pairs on the selection set, and Mack</span><span class="se">\n</span><span class="s">"</span>
             <span class="s">"Score = mean relative RMSE of total reserve, log scale (green: picked, orange: Mack)"</span><span class="p">,</span>
       <span class="n">xlabel</span><span class="o">=</span><span class="s">"Score"</span><span class="p">)</span>
<span class="n">ax</span><span class="p">.</span><span class="n">grid</span><span class="p">(</span><span class="n">axis</span><span class="o">=</span><span class="s">"x"</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.25</span><span class="p">)</span>
<span class="n">fig</span><span class="p">.</span><span class="n">tight_layout</span><span class="p">();</span> <span class="n">plt</span><span class="p">.</span><span class="n">show</span><span class="p">()</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Picked: ['MLPRegressor [onehot]', 'AdaBoostRegressor [onehot]', 'ExtraTreeRegressor [onehot]', 'ExtraTreesRegressor [onehot]', 'DecisionTreeRegressor [onehot]']
</code></pre></div></div>

<p><img src="/images/2026-10-05/2026-10-05-mlreserving-vs-mack-sweep-v2_23_1.png" alt="image-title-here" class="img-responsive" /></p>

<h3 id="34-re-scoring-the-picks-on-fresh-triangles">3.4 Re-scoring the picks on fresh triangles</h3>

<p>These are new seeds (101–120) from the same two scenarios. No model was selected on them.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">pick_jobs</span> <span class="o">=</span> <span class="p">[</span><span class="n">j</span> <span class="k">for</span> <span class="n">j</span> <span class="ow">in</span> <span class="n">jobs</span> <span class="k">if</span> <span class="n">j</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="ow">in</span> <span class="n">picks</span> <span class="ow">or</span> <span class="n">j</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="s">"Mack Chain-Ladder"</span><span class="p">]</span>
<span class="n">res</span> <span class="o">=</span> <span class="n">Parallel</span><span class="p">(</span><span class="n">n_jobs</span><span class="o">=</span><span class="n">N_JOBS</span><span class="p">)(</span><span class="n">delayed</span><span class="p">(</span><span class="n">score_one</span><span class="p">)(</span><span class="n">lbl</span><span class="p">,</span> <span class="n">est</span><span class="p">,</span> <span class="n">f</span><span class="p">,</span> <span class="n">fresh_cases</span><span class="p">)</span> <span class="k">for</span> <span class="n">lbl</span><span class="p">,</span> <span class="n">est</span><span class="p">,</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">pick_jobs</span><span class="p">)</span>
<span class="n">fresh</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">([</span><span class="n">r</span> <span class="k">for</span> <span class="n">rr</span> <span class="ow">in</span> <span class="n">res</span> <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">rr</span><span class="p">])</span>
<span class="n">fresh_rank</span> <span class="o">=</span> <span class="n">pivot_rel</span><span class="p">(</span><span class="n">summarise</span><span class="p">(</span><span class="n">fresh</span><span class="p">))</span>

<span class="n">sim_cols</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">SCENARIOS</span><span class="p">)</span>
<span class="n">compare</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">({</span>
    <span class="s">"selection set"</span><span class="p">:</span> <span class="n">sel_rank</span><span class="p">.</span><span class="n">loc</span><span class="p">[</span><span class="n">picks</span> <span class="o">+</span> <span class="p">[</span><span class="s">"Mack Chain-Ladder"</span><span class="p">],</span> <span class="n">sim_cols</span><span class="p">].</span><span class="n">mean</span><span class="p">(</span><span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">),</span>
    <span class="s">"fresh triangles"</span><span class="p">:</span> <span class="n">fresh_rank</span><span class="p">.</span><span class="n">loc</span><span class="p">[</span><span class="n">picks</span> <span class="o">+</span> <span class="p">[</span><span class="s">"Mack Chain-Ladder"</span><span class="p">],</span> <span class="s">"mean relRMSE"</span><span class="p">],</span>
<span class="p">}).</span><span class="n">sort_values</span><span class="p">(</span><span class="s">"selection set"</span><span class="p">)</span>

<span class="n">fig</span><span class="p">,</span> <span class="n">ax</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="n">subplots</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="mf">4.8</span><span class="p">))</span>
<span class="n">y</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">arange</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">compare</span><span class="p">))</span>
<span class="n">ax</span><span class="p">.</span><span class="n">barh</span><span class="p">(</span><span class="n">y</span> <span class="o">+</span> <span class="mf">0.2</span><span class="p">,</span> <span class="n">compare</span><span class="p">[</span><span class="s">"selection set"</span><span class="p">],</span> <span class="n">height</span><span class="o">=</span><span class="mf">0.38</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="n">GREY</span><span class="p">,</span> <span class="n">label</span><span class="o">=</span><span class="s">"Selection set (models chosen here)"</span><span class="p">)</span>
<span class="n">ax</span><span class="p">.</span><span class="n">barh</span><span class="p">(</span><span class="n">y</span> <span class="o">-</span> <span class="mf">0.2</span><span class="p">,</span> <span class="n">compare</span><span class="p">[</span><span class="s">"fresh triangles"</span><span class="p">],</span> <span class="n">height</span><span class="o">=</span><span class="mf">0.38</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="n">INK</span><span class="p">,</span> <span class="n">label</span><span class="o">=</span><span class="s">"Fresh triangles (seeds never used)"</span><span class="p">)</span>
<span class="n">ax</span><span class="p">.</span><span class="n">set_yticks</span><span class="p">(</span><span class="n">y</span><span class="p">,</span> <span class="n">compare</span><span class="p">.</span><span class="n">index</span><span class="p">);</span> <span class="n">ax</span><span class="p">.</span><span class="n">invert_yaxis</span><span class="p">()</span>
<span class="n">ax</span><span class="p">.</span><span class="nb">set</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s">"Does the advantage survive on triangles not used for selection?"</span><span class="p">,</span>
       <span class="n">xlabel</span><span class="o">=</span><span class="s">"Mean relative RMSE over the two simulated scenarios (lower is better)"</span><span class="p">)</span>
<span class="n">ax</span><span class="p">.</span><span class="n">legend</span><span class="p">(</span><span class="n">frameon</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span> <span class="n">loc</span><span class="o">=</span><span class="s">"upper center"</span><span class="p">,</span> <span class="n">bbox_to_anchor</span><span class="o">=</span><span class="p">(</span><span class="mf">0.5</span><span class="p">,</span> <span class="o">-</span><span class="mf">0.16</span><span class="p">),</span> <span class="n">ncol</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
<span class="n">ax</span><span class="p">.</span><span class="n">grid</span><span class="p">(</span><span class="n">axis</span><span class="o">=</span><span class="s">"x"</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.25</span><span class="p">)</span>
<span class="n">fig</span><span class="p">.</span><span class="n">tight_layout</span><span class="p">();</span> <span class="n">plt</span><span class="p">.</span><span class="n">show</span><span class="p">()</span>
<span class="n">compare</span><span class="p">.</span><span class="nb">round</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span>
</code></pre></div></div>

<p><img src="/images/2026-10-05/2026-10-05-mlreserving-vs-mack-sweep-v2_25_0.png" alt="image-title-here" class="img-responsive" /></p>

<div>
<style scoped="">
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>selection set</th>
      <th>fresh triangles</th>
    </tr>
    <tr>
      <th>model</th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>MLPRegressor [onehot]</th>
      <td>0.083</td>
      <td>0.079</td>
    </tr>
    <tr>
      <th>ExtraTreeRegressor [onehot]</th>
      <td>0.104</td>
      <td>0.105</td>
    </tr>
    <tr>
      <th>ExtraTreesRegressor [onehot]</th>
      <td>0.105</td>
      <td>0.106</td>
    </tr>
    <tr>
      <th>DecisionTreeRegressor [onehot]</th>
      <td>0.105</td>
      <td>0.105</td>
    </tr>
    <tr>
      <th>AdaBoostRegressor [onehot]</th>
      <td>0.155</td>
      <td>0.177</td>
    </tr>
    <tr>
      <th>Mack Chain-Ladder</th>
      <td>0.551</td>
      <td>0.549</td>
    </tr>
  </tbody>
</table>
</div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Error distribution of the picks vs Mack on the fresh triangles
</span><span class="n">fig</span><span class="p">,</span> <span class="n">axes</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="n">subplots</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">14</span><span class="p">,</span> <span class="mf">4.5</span><span class="p">),</span> <span class="n">sharey</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="n">order</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">compare</span><span class="p">.</span><span class="n">index</span><span class="p">)</span>
<span class="k">for</span> <span class="n">ax</span><span class="p">,</span> <span class="n">sc</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">axes</span><span class="p">,</span> <span class="n">SCENARIOS</span><span class="p">):</span>
    <span class="n">d</span> <span class="o">=</span> <span class="n">fresh</span><span class="p">[</span><span class="n">fresh</span><span class="p">[</span><span class="s">"scenario"</span><span class="p">]</span> <span class="o">==</span> <span class="n">sc</span><span class="p">]</span>
    <span class="n">data</span> <span class="o">=</span> <span class="p">[</span><span class="n">d</span><span class="p">.</span><span class="n">loc</span><span class="p">[</span><span class="n">d</span><span class="p">[</span><span class="s">"model"</span><span class="p">]</span> <span class="o">==</span> <span class="n">m</span><span class="p">,</span> <span class="s">"rel_error"</span><span class="p">].</span><span class="n">values</span> <span class="o">*</span> <span class="mi">100</span> <span class="k">for</span> <span class="n">m</span> <span class="ow">in</span> <span class="n">order</span><span class="p">]</span>
    <span class="n">bp</span> <span class="o">=</span> <span class="n">ax</span><span class="p">.</span><span class="n">boxplot</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">vert</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span> <span class="n">widths</span><span class="o">=</span><span class="mf">0.55</span><span class="p">,</span> <span class="n">patch_artist</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
                    <span class="n">medianprops</span><span class="o">=</span><span class="nb">dict</span><span class="p">(</span><span class="n">color</span><span class="o">=</span><span class="n">INK</span><span class="p">,</span> <span class="n">lw</span><span class="o">=</span><span class="mf">1.5</span><span class="p">))</span>
    <span class="k">for</span> <span class="n">patch</span><span class="p">,</span> <span class="n">m</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">bp</span><span class="p">[</span><span class="s">"boxes"</span><span class="p">],</span> <span class="n">order</span><span class="p">):</span>
        <span class="n">patch</span><span class="p">.</span><span class="n">set_facecolor</span><span class="p">(</span><span class="n">MACK_C</span> <span class="k">if</span> <span class="n">m</span> <span class="o">==</span> <span class="s">"Mack Chain-Ladder"</span> <span class="k">else</span> <span class="n">ACCENT</span><span class="p">)</span>
        <span class="n">patch</span><span class="p">.</span><span class="n">set_alpha</span><span class="p">(</span><span class="mf">0.75</span><span class="p">)</span>
    <span class="n">ax</span><span class="p">.</span><span class="n">axvline</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="n">INK</span><span class="p">,</span> <span class="n">lw</span><span class="o">=</span><span class="mf">0.8</span><span class="p">)</span>
    <span class="n">ax</span><span class="p">.</span><span class="n">set_yticks</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">order</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">),</span> <span class="n">order</span><span class="p">)</span>
    <span class="n">ax</span><span class="p">.</span><span class="nb">set</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">sc</span><span class="si">}</span><span class="s">: fresh triangles"</span><span class="p">,</span> <span class="n">xlabel</span><span class="o">=</span><span class="s">"Relative error of total reserve (%)"</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">invert_yaxis</span><span class="p">()</span>
<span class="n">fig</span><span class="p">.</span><span class="n">tight_layout</span><span class="p">();</span> <span class="n">plt</span><span class="p">.</span><span class="n">show</span><span class="p">()</span>
</code></pre></div></div>

<p><img src="/images/2026-10-05/2026-10-05-mlreserving-vs-mack-sweep-v2_26_0.png" alt="image-title-here" class="img-responsive" /></p>

<h2 id="4-reading-the-results">4. Reading the results</h2>

<ul>
  <li><strong>Chain-Ladder.</strong> As Section 2.4 predicts, the speed-up makes Mack over-reserve heavily. The inflation kink pulls
the other way but only partly offsets it, so Mack sits near the bottom of the ranking in both scenarios.</li>
  <li><strong>What “works”.</strong> The top of the ranking is dominated by <code class="language-plaintext highlighter-rouge">[onehot]</code> features. Origin dummies let a learner absorb part
of the speed-up as an accident-year effect, and non-linear learners (MLP, trees) are not tied to the
multiplicative origin × development structure that the Chain-Ladder shares with linear models.</li>
  <li><strong>What the fresh-seed check shows.</strong> The scores of the picks barely move on new seeds, so the selection is not
overfitting individual seeds. <strong>It is not evidence that the selection generalises.</strong> The fresh triangles come from the
same generator with the same \(s\), \(g\), \(\tau\) and \(i_0\). Selecting on the truth tuned the model choice to <em>these</em>
distortions. A fair test would change the data-generating process: no distortion (the Baseline), a slow-down
(\(s &lt; 0\)), a different \(\tau\), or the individual-claims simulator with other settings.</li>
  <li><strong>Where the picks still miss.</strong> In the boxplot of Section 3.4, the learners over-reserve under speed-up alone and
<em>under</em>-reserve once inflation is added. None of them sees a calendar feature, so the future
growth of \(I_t\) (Section 2.2) is extrapolated from the development pattern rather than modelled. The selected models
look good partly because the two errors roughly cancel in the score.</li>
  <li><strong>Fragile pairs.</strong> Some linear models on <code class="language-plaintext highlighter-rouge">[onehot]</code> features (e.g. <code class="language-plaintext highlighter-rouge">Lars</code>) blow up after the \(\sinh\) back-transform,
with near-singular fits giving astronomically large reserves. Clip or exclude them before averaging ranks.</li>
</ul>]]></content><author><name></name></author><category term="R" /><category term="Python" /><summary type="html"><![CDATA[This post compares the performance of machine-learning loss reserving (`mlreserving.MLReserving`) and the Mack Chain-Ladder (`chainladder.MackChainladder`) on simulated claims triangles with known true reserves, under distortions that break the chain-ladder assumptions: settlement speed-up and calendar-year inflation shock.]]></summary></entry><entry><title type="html">mlreserve: machine-learning loss reserving on ‘real-world’ triangles (based on a ‘ChainLadder’ fork)</title><link href="https://thierrymoudiki.github.io/blog/2026/10/03/r/mlreserve" rel="alternate" type="text/html" title="mlreserve: machine-learning loss reserving on ‘real-world’ triangles (based on a ‘ChainLadder’ fork)" /><published>2026-10-03T00:00:00+00:00</published><updated>2026-10-03T00:00:00+00:00</updated><id>https://thierrymoudiki.github.io/blog/2026/10/03/r/mlreserve</id><content type="html" xml:base="https://thierrymoudiki.github.io/blog/2026/10/03/r/mlreserve"><![CDATA[<p>This post presents a function <code class="language-plaintext highlighter-rouge">mlReserve()</code> for machine-learning loss reserving on ‘real-world’ triangles, <strong>based on a fork of R’s ‘ChainLadder’ package</strong> (<a href="https://github.com/thierrymoudiki/ChainLadder/tree/master">https://github.com/thierrymoudiki/ChainLadder/tree/master</a>). It includes two experiments, each scored against a known true reserve: (A) Two triangles from an individual-claims simulator (Wang &amp; Wüthrich, <a href="https://github.com/actuarial-data-science/PackageIndividualClaimsSimulator">https://github.com/actuarial-data-science/PackageIndividualClaimsSimulator</a>), embedded below: chain ladder vs mlReserve, in detail; (B) A mini-benchmark on freshly simulated triangles from a simpler aggregate generator, with tunable distortions.</p>

<div class="language-R highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># =============================================================================</span><span class="w">
</span><span class="c1"># mlReserve: machine-learning loss reserving on "real-world" triangles</span><span class="w">
</span><span class="c1">#</span><span class="w">
</span><span class="c1"># Colab: Runtime &gt; Change runtime type &gt; R</span><span class="w">
</span><span class="c1">#</span><span class="w">
</span><span class="c1"># Two experiments, each scored against a known true reserve:</span><span class="w">
</span><span class="c1">#   A. Two triangles from an individual-claims simulator (Wang &amp; Wüthrich),</span><span class="w">
</span><span class="c1">#      embedded below: chain ladder vs mlReserve, in detail.</span><span class="w">
</span><span class="c1">#   B. A mini-benchmark on freshly simulated triangles from a simpler aggregate</span><span class="w">
</span><span class="c1">#      generator (defined in step 4), with tunable distortions.</span><span class="w">
</span><span class="c1"># =============================================================================</span><span class="w">

</span><span class="c1"># ---- 0. Setup -------------------------------------------------------------------</span><span class="w">

</span><span class="n">options</span><span class="p">(</span><span class="n">Ncpus</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">2</span><span class="p">,</span><span class="w"> </span><span class="n">repos</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"https://cloud.r-project.org"</span><span class="p">,</span><span class="w">
        </span><span class="n">repr.plot.width</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">12</span><span class="p">,</span><span class="w"> </span><span class="n">repr.plot.height</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">5.5</span><span class="p">)</span><span class="w">
</span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="n">pkg</span><span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="nf">c</span><span class="p">(</span><span class="s2">"remotes"</span><span class="p">,</span><span class="w"> </span><span class="s2">"ggplot2"</span><span class="p">,</span><span class="w"> </span><span class="s2">"ranger"</span><span class="p">,</span><span class="w"> </span><span class="s2">"e1071"</span><span class="p">))</span><span class="w">
  </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="n">requireNamespace</span><span class="p">(</span><span class="n">pkg</span><span class="p">,</span><span class="w"> </span><span class="n">quietly</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">TRUE</span><span class="p">))</span><span class="w"> </span><span class="n">install.packages</span><span class="p">(</span><span class="n">pkg</span><span class="p">)</span><span class="w">
</span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="n">requireNamespace</span><span class="p">(</span><span class="s2">"ChainLadder"</span><span class="p">,</span><span class="w"> </span><span class="n">quietly</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">TRUE</span><span class="p">)</span><span class="w"> </span><span class="o">||</span><span class="w">
    </span><span class="o">!</span><span class="s2">"mlReserve"</span><span class="w"> </span><span class="o">%in%</span><span class="w"> </span><span class="n">getNamespaceExports</span><span class="p">(</span><span class="s2">"ChainLadder"</span><span class="p">))</span><span class="w">
  </span><span class="n">remotes</span><span class="o">::</span><span class="n">install_github</span><span class="p">(</span><span class="s2">"thierrymoudiki/ChainLadder"</span><span class="p">,</span><span class="w"> </span><span class="n">upgrade</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"never"</span><span class="p">)</span><span class="w">

</span><span class="n">suppressPackageStartupMessages</span><span class="p">({</span><span class="w">
  </span><span class="n">library</span><span class="p">(</span><span class="n">ChainLadder</span><span class="p">)</span><span class="w">
  </span><span class="n">library</span><span class="p">(</span><span class="n">ggplot2</span><span class="p">)</span><span class="w">
</span><span class="p">})</span><span class="w">

</span><span class="n">n</span><span class="w">      </span><span class="o">&lt;-</span><span class="w"> </span><span class="m">10</span><span class="w">                                   </span><span class="c1"># triangle size (accident years)</span><span class="w">
</span><span class="n">models</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nf">c</span><span class="p">(</span><span class="s2">"Chain ladder"</span><span class="p">,</span><span class="w"> </span><span class="s2">"mlReserve: SVM"</span><span class="p">,</span><span class="w"> </span><span class="s2">"mlReserve: random forest"</span><span class="p">)</span><span class="w">
</span><span class="n">ink</span><span class="w">    </span><span class="o">&lt;-</span><span class="w"> </span><span class="s2">"#0b0b0b"</span><span class="w">
</span><span class="n">colours</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nf">c</span><span class="p">(</span><span class="s2">"Chain ladder"</span><span class="w">             </span><span class="o">=</span><span class="w"> </span><span class="s2">"#eb6834"</span><span class="p">,</span><span class="w">
             </span><span class="s2">"mlReserve: SVM"</span><span class="w">           </span><span class="o">=</span><span class="w"> </span><span class="s2">"#2a78d6"</span><span class="p">,</span><span class="w">
             </span><span class="s2">"mlReserve: random forest"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"#1baf7a"</span><span class="p">,</span><span class="w">
             </span><span class="s2">"Truth"</span><span class="w">                    </span><span class="o">=</span><span class="w"> </span><span class="n">ink</span><span class="p">)</span><span class="w">
</span><span class="n">theme_set</span><span class="p">(</span><span class="n">theme_minimal</span><span class="p">(</span><span class="n">base_size</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">13</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w">
            </span><span class="n">theme</span><span class="p">(</span><span class="n">legend.position</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"top"</span><span class="p">,</span><span class="w"> </span><span class="n">legend.title</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">element_blank</span><span class="p">(),</span><span class="w">
                  </span><span class="n">panel.grid.minor</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">element_blank</span><span class="p">(),</span><span class="w"> </span><span class="n">plot.title.position</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"plot"</span><span class="p">,</span><span class="w">
                  </span><span class="n">strip.text</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">element_text</span><span class="p">(</span><span class="n">face</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"bold"</span><span class="p">)))</span><span class="w">


</span><span class="c1"># ---- 1. Helpers -----------------------------------------------------------------</span><span class="w">

</span><span class="c1"># A full n x n square of incremental payments -&gt; observed (cumulative) upper</span><span class="w">
</span><span class="c1"># triangle + true outstanding reserve per accident year (sum of the lower triangle)</span><span class="w">
</span><span class="n">make_case</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="k">function</span><span class="p">(</span><span class="n">full</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="nf">dimnames</span><span class="p">(</span><span class="n">full</span><span class="p">)</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nf">list</span><span class="p">(</span><span class="n">origin</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">1</span><span class="o">:</span><span class="n">n</span><span class="p">,</span><span class="w"> </span><span class="n">dev</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">1</span><span class="o">:</span><span class="n">n</span><span class="p">)</span><span class="w">
  </span><span class="n">future</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">row</span><span class="p">(</span><span class="n">full</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">col</span><span class="p">(</span><span class="n">full</span><span class="p">)</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="n">n</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="m">1</span><span class="w">
  </span><span class="n">upper</span><span class="w">  </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">full</span><span class="p">;</span><span class="w"> </span><span class="n">upper</span><span class="p">[</span><span class="n">future</span><span class="p">]</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="kc">NA</span><span class="w">
  </span><span class="nf">list</span><span class="p">(</span><span class="n">full</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="n">full</span><span class="p">,</span><span class="w">
       </span><span class="n">upper</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="n">incr2cum</span><span class="p">(</span><span class="n">as.triangle</span><span class="p">(</span><span class="n">upper</span><span class="p">)),</span><span class="w">
       </span><span class="n">true_ibnr</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">rowSums</span><span class="p">(</span><span class="n">full</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">future</span><span class="p">))</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c1"># Fit the three models; `uncertainty = FALSE` gives point estimates only</span><span class="w">
</span><span class="n">fit_models</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="k">function</span><span class="p">(</span><span class="n">triangle</span><span class="p">,</span><span class="w"> </span><span class="n">uncertainty</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">TRUE</span><span class="p">,</span><span class="w"> </span><span class="n">nsim</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">300</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">mse</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">uncertainty</span><span class="p">)</span><span class="w"> </span><span class="s2">"bootstrap"</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="s2">"none"</span><span class="w">
  </span><span class="n">fits</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nf">list</span><span class="p">(</span><span class="w">
    </span><span class="n">MackChainLadder</span><span class="p">(</span><span class="n">triangle</span><span class="p">,</span><span class="w"> </span><span class="n">est.sigma</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Mack"</span><span class="p">),</span><span class="w">
    </span><span class="n">mlReserve</span><span class="p">(</span><span class="n">triangle</span><span class="p">,</span><span class="w"> </span><span class="n">e1071</span><span class="o">::</span><span class="n">svm</span><span class="p">,</span><span class="w"> </span><span class="n">features</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"numeric"</span><span class="p">,</span><span class="w"> </span><span class="n">transform</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"asinh"</span><span class="p">,</span><span class="w">
              </span><span class="n">mse.method</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">mse</span><span class="p">,</span><span class="w"> </span><span class="n">nsim</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">nsim</span><span class="p">,</span><span class="w"> </span><span class="n">seed</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">1</span><span class="p">),</span><span class="w">
    </span><span class="n">mlReserve</span><span class="p">(</span><span class="n">triangle</span><span class="p">,</span><span class="w"> </span><span class="n">ranger</span><span class="o">::</span><span class="n">ranger</span><span class="p">,</span><span class="w"> </span><span class="n">num.trees</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">300</span><span class="p">,</span><span class="w"> </span><span class="n">num.threads</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">1</span><span class="p">,</span><span class="w">
              </span><span class="n">features</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"both"</span><span class="p">,</span><span class="w"> </span><span class="n">transform</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"asinh"</span><span class="p">,</span><span class="w">
              </span><span class="n">mse.method</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">mse</span><span class="p">,</span><span class="w"> </span><span class="n">nsim</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">nsim</span><span class="p">,</span><span class="w"> </span><span class="n">seed</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">1</span><span class="p">))</span><span class="w">
  </span><span class="n">setNames</span><span class="p">(</span><span class="n">fits</span><span class="p">,</span><span class="w"> </span><span class="n">models</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c1"># Reserve (IBNR) and standard error by accident year, whatever the model</span><span class="w">
</span><span class="n">reserve_by_origin</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="k">function</span><span class="p">(</span><span class="n">fit</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">inherits</span><span class="p">(</span><span class="n">fit</span><span class="p">,</span><span class="w"> </span><span class="s2">"MackChainLadder"</span><span class="p">))</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">s</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">summary</span><span class="p">(</span><span class="n">fit</span><span class="p">)</span><span class="o">$</span><span class="n">ByOrigin</span><span class="w">
    </span><span class="n">data.frame</span><span class="p">(</span><span class="n">origin</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">1</span><span class="o">:</span><span class="n">n</span><span class="p">,</span><span class="w"> </span><span class="n">IBNR</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">s</span><span class="o">$</span><span class="n">IBNR</span><span class="p">,</span><span class="w"> </span><span class="n">SE</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">s</span><span class="o">$</span><span class="n">Mack.S.E</span><span class="p">)</span><span class="w">
  </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">s</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">fit</span><span class="o">$</span><span class="n">summary</span><span class="p">[</span><span class="n">rownames</span><span class="p">(</span><span class="n">fit</span><span class="o">$</span><span class="n">summary</span><span class="p">)</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="s2">"total"</span><span class="p">,</span><span class="w"> </span><span class="p">]</span><span class="w">
    </span><span class="n">data.frame</span><span class="p">(</span><span class="n">origin</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">as.integer</span><span class="p">(</span><span class="n">rownames</span><span class="p">(</span><span class="n">s</span><span class="p">)),</span><span class="w"> </span><span class="n">IBNR</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">s</span><span class="o">$</span><span class="n">IBNR</span><span class="p">,</span><span class="w"> </span><span class="n">SE</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">s</span><span class="o">$</span><span class="n">S.E</span><span class="p">)</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="n">total_reserve</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="k">function</span><span class="p">(</span><span class="n">fit</span><span class="p">)</span><span class="w"> </span><span class="nf">sum</span><span class="p">(</span><span class="n">reserve_by_origin</span><span class="p">(</span><span class="n">fit</span><span class="p">)</span><span class="o">$</span><span class="n">IBNR</span><span class="p">)</span><span class="w">


</span><span class="c1"># ---- 2. Data for experiment A ---------------------------------------------------</span><span class="w">

</span><span class="c1"># Incremental paid (thousands), aggregated from the Wang &amp; Wuthrich individual</span><span class="w">
</span><span class="c1"># claims simulator (github.com/actuarial-data-science/PackageIndividualClaimsSimulator):</span><span class="w">
</span><span class="c1">#   "Speed-up"     : settlement accelerates for recent accident years (delays -45%)</span><span class="w">
</span><span class="c1">#   "All combined" : speed-up + calendar inflation shock (2% -&gt; 15% a year from</span><span class="w">
</span><span class="c1">#                    calendar month 78) + a few large, slowly paid claims</span><span class="w">
</span><span class="n">squares</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nf">list</span><span class="p">(</span><span class="w">
  </span><span class="s2">"Speed-up"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">c</span><span class="p">(</span><span class="m">4172.1</span><span class="p">,</span><span class="w"> </span><span class="m">16153.4</span><span class="p">,</span><span class="w"> </span><span class="m">14800.2</span><span class="p">,</span><span class="w"> </span><span class="m">7561.1</span><span class="p">,</span><span class="w"> </span><span class="m">3275</span><span class="p">,</span><span class="w"> </span><span class="m">1856.3</span><span class="p">,</span><span class="w"> </span><span class="m">726.7</span><span class="p">,</span><span class="w"> </span><span class="m">410.7</span><span class="p">,</span><span class="w"> </span><span class="m">35.8</span><span class="p">,</span><span class="w"> </span><span class="m">140</span><span class="p">,</span><span class="w"> </span><span class="m">4983.2</span><span class="p">,</span><span class="w"> </span><span class="m">18064.1</span><span class="p">,</span><span class="w"> </span><span class="m">13574.5</span><span class="p">,</span><span class="w"> </span><span class="m">8350.7</span><span class="p">,</span><span class="w"> </span><span class="m">3954.9</span><span class="p">,</span><span class="w"> </span><span class="m">1499</span><span class="p">,</span><span class="w"> </span><span class="m">510.3</span><span class="p">,</span><span class="w"> </span><span class="m">9.5</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">11.3</span><span class="p">,</span><span class="w"> </span><span class="m">4698.1</span><span class="p">,</span><span class="w"> </span><span class="m">19428.8</span><span class="p">,</span><span class="w"> </span><span class="m">14864</span><span class="p">,</span><span class="w"> </span><span class="m">7343</span><span class="p">,</span><span class="w"> </span><span class="m">3710.5</span><span class="p">,</span><span class="w"> </span><span class="m">1378.6</span><span class="p">,</span><span class="w"> </span><span class="m">384.7</span><span class="p">,</span><span class="w"> </span><span class="m">92.2</span><span class="p">,</span><span class="w"> </span><span class="m">6.8</span><span class="p">,</span><span class="w"> </span><span class="m">27.1</span><span class="p">,</span><span class="w"> </span><span class="m">5467.1</span><span class="p">,</span><span class="w"> </span><span class="m">18481</span><span class="p">,</span><span class="w"> </span><span class="m">14847.6</span><span class="p">,</span><span class="w"> </span><span class="m">8089.7</span><span class="p">,</span><span class="w"> </span><span class="m">2794.1</span><span class="p">,</span><span class="w"> </span><span class="m">1379.2</span><span class="p">,</span><span class="w"> </span><span class="m">549.5</span><span class="p">,</span><span class="w"> </span><span class="m">152.2</span><span class="p">,</span><span class="w"> </span><span class="m">6.8</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">5985.2</span><span class="p">,</span><span class="w"> </span><span class="m">21516.6</span><span class="p">,</span><span class="w"> </span><span class="m">15980.6</span><span class="p">,</span><span class="w"> </span><span class="m">8353</span><span class="p">,</span><span class="w"> </span><span class="m">2954.2</span><span class="p">,</span><span class="w"> </span><span class="m">1655.5</span><span class="p">,</span><span class="w"> </span><span class="m">636.4</span><span class="p">,</span><span class="w"> </span><span class="m">191.3</span><span class="p">,</span><span class="w"> </span><span class="m">211.3</span><span class="p">,</span><span class="w"> </span><span class="m">28.9</span><span class="p">,</span><span class="w"> </span><span class="m">8274.1</span><span class="p">,</span><span class="w"> </span><span class="m">22014.4</span><span class="p">,</span><span class="w"> </span><span class="m">15981.4</span><span class="p">,</span><span class="w"> </span><span class="m">7425.8</span><span class="p">,</span><span class="w"> </span><span class="m">2362.7</span><span class="p">,</span><span class="w"> </span><span class="m">731</span><span class="p">,</span><span class="w"> </span><span class="m">242.1</span><span class="p">,</span><span class="w"> </span><span class="m">41.6</span><span class="p">,</span><span class="w"> </span><span class="m">52.8</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">8148.4</span><span class="p">,</span><span class="w"> </span><span class="m">24712.9</span><span class="p">,</span><span class="w"> </span><span class="m">16329.1</span><span class="p">,</span><span class="w"> </span><span class="m">6587.5</span><span class="p">,</span><span class="w"> </span><span class="m">2804.9</span><span class="p">,</span><span class="w"> </span><span class="m">753.6</span><span class="p">,</span><span class="w"> </span><span class="m">49.5</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">9974.2</span><span class="p">,</span><span class="w"> </span><span class="m">27573.4</span><span class="p">,</span><span class="w"> </span><span class="m">14523</span><span class="p">,</span><span class="w"> </span><span class="m">5141.6</span><span class="p">,</span><span class="w"> </span><span class="m">1123.8</span><span class="p">,</span><span class="w"> </span><span class="m">350</span><span class="p">,</span><span class="w"> </span><span class="m">73.3</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">10698.8</span><span class="p">,</span><span class="w"> </span><span class="m">29877.8</span><span class="p">,</span><span class="w"> </span><span class="m">14303.3</span><span class="p">,</span><span class="w"> </span><span class="m">3200.3</span><span class="p">,</span><span class="w"> </span><span class="m">978.3</span><span class="p">,</span><span class="w"> </span><span class="m">239.1</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">13669.1</span><span class="p">,</span><span class="w"> </span><span class="m">30541.7</span><span class="p">,</span><span class="w"> </span><span class="m">10984</span><span class="p">,</span><span class="w"> </span><span class="m">1837.5</span><span class="p">,</span><span class="w"> </span><span class="m">157.3</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">),</span><span class="w">
  </span><span class="s2">"All combined"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">c</span><span class="p">(</span><span class="m">4236.8</span><span class="p">,</span><span class="w"> </span><span class="m">16766.9</span><span class="p">,</span><span class="w"> </span><span class="m">15671.5</span><span class="p">,</span><span class="w"> </span><span class="m">8229</span><span class="p">,</span><span class="w"> </span><span class="m">3582.8</span><span class="p">,</span><span class="w"> </span><span class="m">2143.9</span><span class="p">,</span><span class="w"> </span><span class="m">838.8</span><span class="p">,</span><span class="w"> </span><span class="m">548.6</span><span class="p">,</span><span class="w"> </span><span class="m">57.6</span><span class="p">,</span><span class="w"> </span><span class="m">261.6</span><span class="p">,</span><span class="w"> </span><span class="m">5195.7</span><span class="p">,</span><span class="w"> </span><span class="m">19008.8</span><span class="p">,</span><span class="w"> </span><span class="m">15926.9</span><span class="p">,</span><span class="w"> </span><span class="m">9857.6</span><span class="p">,</span><span class="w"> </span><span class="m">4643.8</span><span class="p">,</span><span class="w"> </span><span class="m">1746.5</span><span class="p">,</span><span class="w"> </span><span class="m">712.4</span><span class="p">,</span><span class="w"> </span><span class="m">26.4</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">22.5</span><span class="p">,</span><span class="w"> </span><span class="m">4956.8</span><span class="p">,</span><span class="w"> </span><span class="m">20861.6</span><span class="p">,</span><span class="w"> </span><span class="m">16283.4</span><span class="p">,</span><span class="w"> </span><span class="m">8704.2</span><span class="p">,</span><span class="w"> </span><span class="m">4321.7</span><span class="p">,</span><span class="w"> </span><span class="m">1876.2</span><span class="p">,</span><span class="w"> </span><span class="m">666.5</span><span class="p">,</span><span class="w"> </span><span class="m">161.8</span><span class="p">,</span><span class="w"> </span><span class="m">14.7</span><span class="p">,</span><span class="w"> </span><span class="m">61.4</span><span class="p">,</span><span class="w"> </span><span class="m">5908.2</span><span class="p">,</span><span class="w"> </span><span class="m">20336.4</span><span class="p">,</span><span class="w"> </span><span class="m">16580.1</span><span class="p">,</span><span class="w"> </span><span class="m">9331.1</span><span class="p">,</span><span class="w"> </span><span class="m">3673.8</span><span class="p">,</span><span class="w"> </span><span class="m">2276.2</span><span class="p">,</span><span class="w"> </span><span class="m">1205.8</span><span class="p">,</span><span class="w"> </span><span class="m">332.2</span><span class="p">,</span><span class="w"> </span><span class="m">15.4</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">6586.1</span><span class="p">,</span><span class="w"> </span><span class="m">24045.6</span><span class="p">,</span><span class="w"> </span><span class="m">18499</span><span class="p">,</span><span class="w"> </span><span class="m">11218.3</span><span class="p">,</span><span class="w"> </span><span class="m">4576</span><span class="p">,</span><span class="w"> </span><span class="m">3174.8</span><span class="p">,</span><span class="w"> </span><span class="m">1307</span><span class="p">,</span><span class="w"> </span><span class="m">460.6</span><span class="p">,</span><span class="w"> </span><span class="m">665</span><span class="p">,</span><span class="w"> </span><span class="m">96.3</span><span class="p">,</span><span class="w"> </span><span class="m">9292</span><span class="p">,</span><span class="w"> </span><span class="m">26084.3</span><span class="p">,</span><span class="w"> </span><span class="m">21055.7</span><span class="p">,</span><span class="w"> </span><span class="m">11396.7</span><span class="p">,</span><span class="w"> </span><span class="m">4203.8</span><span class="p">,</span><span class="w"> </span><span class="m">1531.8</span><span class="p">,</span><span class="w"> </span><span class="m">585.4</span><span class="p">,</span><span class="w"> </span><span class="m">115</span><span class="p">,</span><span class="w"> </span><span class="m">163.7</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">9702.9</span><span class="p">,</span><span class="w"> </span><span class="m">33010</span><span class="p">,</span><span class="w"> </span><span class="m">25202.7</span><span class="p">,</span><span class="w"> </span><span class="m">12133</span><span class="p">,</span><span class="w"> </span><span class="m">6404.4</span><span class="p">,</span><span class="w"> </span><span class="m">1956.8</span><span class="p">,</span><span class="w"> </span><span class="m">385.9</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">13752.5</span><span class="p">,</span><span class="w"> </span><span class="m">42686.8</span><span class="p">,</span><span class="w"> </span><span class="m">25857.7</span><span class="p">,</span><span class="w"> </span><span class="m">10698.4</span><span class="p">,</span><span class="w"> </span><span class="m">2706.5</span><span class="p">,</span><span class="w"> </span><span class="m">3213</span><span class="p">,</span><span class="w"> </span><span class="m">4232.5</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">17094.3</span><span class="p">,</span><span class="w"> </span><span class="m">53962.4</span><span class="p">,</span><span class="w"> </span><span class="m">30132.7</span><span class="p">,</span><span class="w"> </span><span class="m">8527.6</span><span class="p">,</span><span class="w"> </span><span class="m">3087.9</span><span class="p">,</span><span class="w"> </span><span class="m">746.2</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">25375.8</span><span class="p">,</span><span class="w"> </span><span class="m">63473.7</span><span class="p">,</span><span class="w"> </span><span class="m">26195.3</span><span class="p">,</span><span class="w"> </span><span class="m">5737.2</span><span class="p">,</span><span class="w"> </span><span class="m">507</span><span class="p">,</span><span class="w"> </span><span class="m">674.2</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">))</span><span class="w">
</span><span class="n">cases</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">lapply</span><span class="p">(</span><span class="n">squares</span><span class="p">,</span><span class="w"> </span><span class="k">function</span><span class="p">(</span><span class="n">v</span><span class="p">)</span><span class="w"> </span><span class="n">make_case</span><span class="p">(</span><span class="n">matrix</span><span class="p">(</span><span class="n">v</span><span class="p">,</span><span class="w"> </span><span class="n">n</span><span class="p">,</span><span class="w"> </span><span class="n">n</span><span class="p">,</span><span class="w"> </span><span class="n">byrow</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">TRUE</span><span class="p">)))</span><span class="w">
</span><span class="n">combo</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">cases</span><span class="p">[[</span><span class="s2">"All combined"</span><span class="p">]]</span><span class="w">

</span><span class="n">cat</span><span class="p">(</span><span class="s2">"Observed cumulative paid, 'All combined' (thousands):\n"</span><span class="p">)</span><span class="w">
</span><span class="n">print</span><span class="p">(</span><span class="nf">round</span><span class="p">(</span><span class="n">combo</span><span class="o">$</span><span class="n">upper</span><span class="p">))</span><span class="w">
</span><span class="n">fits</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">suppressWarnings</span><span class="p">(</span><span class="n">fit_models</span><span class="p">(</span><span class="n">combo</span><span class="o">$</span><span class="n">upper</span><span class="p">))</span><span class="w">
</span><span class="n">cat</span><span class="p">(</span><span class="s2">"\nmlReserve with a random forest; true total reserve ="</span><span class="p">,</span><span class="w">
    </span><span class="n">format</span><span class="p">(</span><span class="nf">round</span><span class="p">(</span><span class="nf">sum</span><span class="p">(</span><span class="n">combo</span><span class="o">$</span><span class="n">true_ibnr</span><span class="p">)),</span><span class="w"> </span><span class="n">big.mark</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">","</span><span class="p">),</span><span class="w"> </span><span class="s2">"\n"</span><span class="p">)</span><span class="w">
</span><span class="n">print</span><span class="p">(</span><span class="n">fits</span><span class="p">[[</span><span class="s2">"mlReserve: random forest"</span><span class="p">]])</span><span class="w">


</span><span class="c1"># ---- 3. Experiment A: one triangle in detail ------------------------------------</span><span class="w">

</span><span class="c1"># 3a. Reserve by accident year vs the truth</span><span class="w">
</span><span class="n">by_origin</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">do.call</span><span class="p">(</span><span class="n">rbind</span><span class="p">,</span><span class="w"> </span><span class="n">lapply</span><span class="p">(</span><span class="n">models</span><span class="p">,</span><span class="w"> </span><span class="k">function</span><span class="p">(</span><span class="n">m</span><span class="p">)</span><span class="w">
  </span><span class="n">data.frame</span><span class="p">(</span><span class="n">model</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">m</span><span class="p">,</span><span class="w"> </span><span class="n">reserve_by_origin</span><span class="p">(</span><span class="n">fits</span><span class="p">[[</span><span class="n">m</span><span class="p">]]))))</span><span class="w">
</span><span class="n">by_origin</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">subset</span><span class="p">(</span><span class="n">by_origin</span><span class="p">,</span><span class="w"> </span><span class="n">origin</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="m">2</span><span class="p">)</span><span class="w">             </span><span class="c1"># AY 1 is fully developed</span><span class="w">
</span><span class="n">by_origin</span><span class="o">$</span><span class="n">model</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">factor</span><span class="p">(</span><span class="n">by_origin</span><span class="o">$</span><span class="n">model</span><span class="p">,</span><span class="w"> </span><span class="n">levels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">models</span><span class="p">)</span><span class="w">
</span><span class="n">truth_points</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">data.frame</span><span class="p">(</span><span class="n">origin</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">2</span><span class="o">:</span><span class="n">n</span><span class="p">,</span><span class="w"> </span><span class="n">IBNR</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">combo</span><span class="o">$</span><span class="n">true_ibnr</span><span class="p">[</span><span class="m">2</span><span class="o">:</span><span class="n">n</span><span class="p">])</span><span class="w">

</span><span class="n">print</span><span class="p">(</span><span class="w">
  </span><span class="n">ggplot</span><span class="p">(</span><span class="n">by_origin</span><span class="p">,</span><span class="w"> </span><span class="n">aes</span><span class="p">(</span><span class="n">origin</span><span class="p">,</span><span class="w"> </span><span class="n">IBNR</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="m">1000</span><span class="p">,</span><span class="w"> </span><span class="n">colour</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">model</span><span class="p">))</span><span class="w"> </span><span class="o">+</span><span class="w">
    </span><span class="n">geom_linerange</span><span class="p">(</span><span class="n">aes</span><span class="p">(</span><span class="n">ymin</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="n">IBNR</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">SE</span><span class="p">)</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="m">1000</span><span class="p">,</span><span class="w"> </span><span class="n">ymax</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="n">IBNR</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">SE</span><span class="p">)</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="m">1000</span><span class="p">),</span><span class="w">
                   </span><span class="n">position</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">position_dodge</span><span class="p">(</span><span class="m">0.6</span><span class="p">),</span><span class="w"> </span><span class="n">linewidth</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">0.9</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w">
    </span><span class="n">geom_point</span><span class="p">(</span><span class="n">position</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">position_dodge</span><span class="p">(</span><span class="m">0.6</span><span class="p">),</span><span class="w"> </span><span class="n">size</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">2.6</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w">
    </span><span class="n">geom_point</span><span class="p">(</span><span class="n">data</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">truth_points</span><span class="p">,</span><span class="w"> </span><span class="n">aes</span><span class="p">(</span><span class="n">origin</span><span class="p">,</span><span class="w"> </span><span class="n">IBNR</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="m">1000</span><span class="p">),</span><span class="w"> </span><span class="n">inherit.aes</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">FALSE</span><span class="p">,</span><span class="w">
               </span><span class="n">shape</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">23</span><span class="p">,</span><span class="w"> </span><span class="n">size</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">3.4</span><span class="p">,</span><span class="w"> </span><span class="n">fill</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ink</span><span class="p">,</span><span class="w"> </span><span class="n">colour</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"white"</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w">
    </span><span class="n">scale_colour_manual</span><span class="p">(</span><span class="n">values</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">colours</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w">
    </span><span class="n">scale_x_continuous</span><span class="p">(</span><span class="n">breaks</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">2</span><span class="o">:</span><span class="n">n</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w">
    </span><span class="n">labs</span><span class="p">(</span><span class="n">title</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Reserve by accident year, +/- one standard error (black diamonds: truth)"</span><span class="p">,</span><span class="w">
         </span><span class="n">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Accident year"</span><span class="p">,</span><span class="w"> </span><span class="n">y</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Reserve (millions)"</span><span class="p">))</span><span class="w">

</span><span class="c1"># 3b. Predictive distribution of the total reserve. Each model uses its own</span><span class="w">
</span><span class="c1">#     resampling scheme: ODP bootstrap (BootChainLadder) for chain ladder,</span><span class="w">
</span><span class="c1">#     residual bootstrap (mlReserve) for the learners.</span><span class="w">
</span><span class="n">set.seed</span><span class="p">(</span><span class="m">1</span><span class="p">)</span><span class="w">
</span><span class="n">boot</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">BootChainLadder</span><span class="p">(</span><span class="n">combo</span><span class="o">$</span><span class="n">upper</span><span class="p">,</span><span class="w"> </span><span class="n">R</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">2000</span><span class="p">,</span><span class="w"> </span><span class="n">process.distr</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"od.pois"</span><span class="p">)</span><span class="w">
</span><span class="n">draws</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">rbind</span><span class="p">(</span><span class="w">
  </span><span class="n">data.frame</span><span class="p">(</span><span class="n">model</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">models</span><span class="p">[</span><span class="m">1</span><span class="p">],</span><span class="w"> </span><span class="n">total</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">boot</span><span class="o">$</span><span class="n">IBNR.Totals</span><span class="p">),</span><span class="w">
  </span><span class="n">data.frame</span><span class="p">(</span><span class="n">model</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">models</span><span class="p">[</span><span class="m">2</span><span class="p">],</span><span class="w"> </span><span class="n">total</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">rowSums</span><span class="p">(</span><span class="n">fits</span><span class="p">[[</span><span class="m">2</span><span class="p">]]</span><span class="o">$</span><span class="n">sims.reserve.pred</span><span class="p">)),</span><span class="w">
  </span><span class="n">data.frame</span><span class="p">(</span><span class="n">model</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">models</span><span class="p">[</span><span class="m">3</span><span class="p">],</span><span class="w"> </span><span class="n">total</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">rowSums</span><span class="p">(</span><span class="n">fits</span><span class="p">[[</span><span class="m">3</span><span class="p">]]</span><span class="o">$</span><span class="n">sims.reserve.pred</span><span class="p">)))</span><span class="w">
</span><span class="n">draws</span><span class="o">$</span><span class="n">model</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">factor</span><span class="p">(</span><span class="n">draws</span><span class="o">$</span><span class="n">model</span><span class="p">,</span><span class="w"> </span><span class="n">levels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">models</span><span class="p">)</span><span class="w">

</span><span class="n">print</span><span class="p">(</span><span class="w">
  </span><span class="n">ggplot</span><span class="p">(</span><span class="n">draws</span><span class="p">,</span><span class="w"> </span><span class="n">aes</span><span class="p">(</span><span class="n">total</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="m">1000</span><span class="p">,</span><span class="w"> </span><span class="n">colour</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">model</span><span class="p">))</span><span class="w"> </span><span class="o">+</span><span class="w">
    </span><span class="n">geom_density</span><span class="p">(</span><span class="n">linewidth</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">1</span><span class="p">,</span><span class="w"> </span><span class="n">adjust</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">1.2</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w">
    </span><span class="n">geom_vline</span><span class="p">(</span><span class="n">xintercept</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">sum</span><span class="p">(</span><span class="n">combo</span><span class="o">$</span><span class="n">true_ibnr</span><span class="p">)</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="m">1000</span><span class="p">,</span><span class="w"> </span><span class="n">colour</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ink</span><span class="p">,</span><span class="w"> </span><span class="n">linewidth</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">0.8</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w">
    </span><span class="n">annotate</span><span class="p">(</span><span class="s2">"text"</span><span class="p">,</span><span class="w"> </span><span class="n">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">sum</span><span class="p">(</span><span class="n">combo</span><span class="o">$</span><span class="n">true_ibnr</span><span class="p">)</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="m">1000</span><span class="p">,</span><span class="w"> </span><span class="n">y</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">Inf</span><span class="p">,</span><span class="w"> </span><span class="n">label</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">" true reserve"</span><span class="p">,</span><span class="w">
             </span><span class="n">hjust</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="n">vjust</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">1.5</span><span class="p">,</span><span class="w"> </span><span class="n">colour</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ink</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w">
    </span><span class="n">scale_colour_manual</span><span class="p">(</span><span class="n">values</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">colours</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w">
    </span><span class="n">labs</span><span class="p">(</span><span class="n">title</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Model-specific predictive distributions of the total reserve"</span><span class="p">,</span><span class="w">
         </span><span class="n">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Total reserve (millions)"</span><span class="p">,</span><span class="w"> </span><span class="n">y</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Density"</span><span class="p">))</span><span class="w">

</span><span class="n">cat</span><span class="p">(</span><span class="s2">"\nPredictive quantiles of the total reserve (thousands):\n"</span><span class="p">)</span><span class="w">
</span><span class="n">quants</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">t</span><span class="p">(</span><span class="n">sapply</span><span class="p">(</span><span class="n">split</span><span class="p">(</span><span class="n">draws</span><span class="o">$</span><span class="n">total</span><span class="p">,</span><span class="w"> </span><span class="n">draws</span><span class="o">$</span><span class="n">model</span><span class="p">),</span><span class="w"> </span><span class="n">quantile</span><span class="p">,</span><span class="w"> </span><span class="nf">c</span><span class="p">(</span><span class="m">0.025</span><span class="p">,</span><span class="w"> </span><span class="m">0.5</span><span class="p">,</span><span class="w"> </span><span class="m">0.975</span><span class="p">)))</span><span class="w">
</span><span class="n">print</span><span class="p">(</span><span class="n">noquote</span><span class="p">(</span><span class="n">formatC</span><span class="p">(</span><span class="nf">round</span><span class="p">(</span><span class="n">quants</span><span class="p">),</span><span class="w"> </span><span class="n">format</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"d"</span><span class="p">,</span><span class="w"> </span><span class="n">big.mark</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">","</span><span class="p">)))</span><span class="w">

</span><span class="c1"># 3c. Projected cumulative payments vs the truth, accident years 7-10</span><span class="w">
</span><span class="n">cumulative_paths</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="k">function</span><span class="p">(</span><span class="n">case</span><span class="p">,</span><span class="w"> </span><span class="n">label</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">cl</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">MackChainLadder</span><span class="p">(</span><span class="n">case</span><span class="o">$</span><span class="n">upper</span><span class="p">,</span><span class="w"> </span><span class="n">est.sigma</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Mack"</span><span class="p">)</span><span class="w">
  </span><span class="n">rf</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">mlReserve</span><span class="p">(</span><span class="n">case</span><span class="o">$</span><span class="n">upper</span><span class="p">,</span><span class="w"> </span><span class="n">ranger</span><span class="o">::</span><span class="n">ranger</span><span class="p">,</span><span class="w"> </span><span class="n">num.trees</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">300</span><span class="p">,</span><span class="w"> </span><span class="n">num.threads</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">1</span><span class="p">,</span><span class="w">
                  </span><span class="n">features</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"both"</span><span class="p">,</span><span class="w"> </span><span class="n">transform</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"asinh"</span><span class="p">,</span><span class="w"> </span><span class="n">mse.method</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"none"</span><span class="p">,</span><span class="w"> </span><span class="n">seed</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">1</span><span class="p">)</span><span class="w">
  </span><span class="n">projections</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nf">list</span><span class="p">(</span><span class="s2">"Truth"</span><span class="w">                    </span><span class="o">=</span><span class="w"> </span><span class="n">t</span><span class="p">(</span><span class="n">apply</span><span class="p">(</span><span class="n">case</span><span class="o">$</span><span class="n">full</span><span class="p">,</span><span class="w"> </span><span class="m">1</span><span class="p">,</span><span class="w"> </span><span class="n">cumsum</span><span class="p">)),</span><span class="w">
                      </span><span class="s2">"Chain ladder"</span><span class="w">             </span><span class="o">=</span><span class="w"> </span><span class="nf">unclass</span><span class="p">(</span><span class="n">cl</span><span class="o">$</span><span class="n">FullTriangle</span><span class="p">),</span><span class="w">
                      </span><span class="s2">"mlReserve: random forest"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">unclass</span><span class="p">(</span><span class="n">rf</span><span class="o">$</span><span class="n">FullTriangle</span><span class="p">))</span><span class="w">
  </span><span class="n">out</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">expand.grid</span><span class="p">(</span><span class="n">dev</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">1</span><span class="o">:</span><span class="n">n</span><span class="p">,</span><span class="w"> </span><span class="n">origin</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">7</span><span class="o">:</span><span class="n">n</span><span class="p">,</span><span class="w"> </span><span class="n">model</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">names</span><span class="p">(</span><span class="n">projections</span><span class="p">),</span><span class="w">
                     </span><span class="n">stringsAsFactors</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">FALSE</span><span class="p">)</span><span class="w">
  </span><span class="n">out</span><span class="o">$</span><span class="n">value</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">mapply</span><span class="p">(</span><span class="k">function</span><span class="p">(</span><span class="n">o</span><span class="p">,</span><span class="w"> </span><span class="n">d</span><span class="p">,</span><span class="w"> </span><span class="n">m</span><span class="p">)</span><span class="w"> </span><span class="n">projections</span><span class="p">[[</span><span class="n">m</span><span class="p">]][</span><span class="n">o</span><span class="p">,</span><span class="w"> </span><span class="n">d</span><span class="p">],</span><span class="w">
                      </span><span class="n">out</span><span class="o">$</span><span class="n">origin</span><span class="p">,</span><span class="w"> </span><span class="n">out</span><span class="o">$</span><span class="n">dev</span><span class="p">,</span><span class="w"> </span><span class="n">out</span><span class="o">$</span><span class="n">model</span><span class="p">)</span><span class="w">
  </span><span class="n">out</span><span class="o">$</span><span class="n">panel</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">paste0</span><span class="p">(</span><span class="n">label</span><span class="p">,</span><span class="w"> </span><span class="s2">" · AY "</span><span class="p">,</span><span class="w"> </span><span class="n">out</span><span class="o">$</span><span class="n">origin</span><span class="p">)</span><span class="w">
  </span><span class="n">subset</span><span class="p">(</span><span class="n">out</span><span class="p">,</span><span class="w"> </span><span class="n">dev</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="n">n</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="m">1</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">origin</span><span class="p">)</span><span class="w">      </span><span class="c1"># from the last observed diagonal onwards</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="n">paths</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">do.call</span><span class="p">(</span><span class="n">rbind</span><span class="p">,</span><span class="w"> </span><span class="n">Map</span><span class="p">(</span><span class="n">cumulative_paths</span><span class="p">,</span><span class="w"> </span><span class="n">cases</span><span class="p">,</span><span class="w"> </span><span class="nf">names</span><span class="p">(</span><span class="n">cases</span><span class="p">)))</span><span class="w">
</span><span class="n">paths</span><span class="o">$</span><span class="n">model</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">factor</span><span class="p">(</span><span class="n">paths</span><span class="o">$</span><span class="n">model</span><span class="p">,</span><span class="w"> </span><span class="n">levels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">c</span><span class="p">(</span><span class="s2">"Truth"</span><span class="p">,</span><span class="w"> </span><span class="n">models</span><span class="p">[</span><span class="nf">c</span><span class="p">(</span><span class="m">1</span><span class="p">,</span><span class="w"> </span><span class="m">3</span><span class="p">)]))</span><span class="w">
</span><span class="n">paths</span><span class="o">$</span><span class="n">panel</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">factor</span><span class="p">(</span><span class="n">paths</span><span class="o">$</span><span class="n">panel</span><span class="p">,</span><span class="w"> </span><span class="n">levels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">unique</span><span class="p">(</span><span class="n">paths</span><span class="o">$</span><span class="n">panel</span><span class="p">))</span><span class="w">

</span><span class="n">options</span><span class="p">(</span><span class="n">repr.plot.height</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">7</span><span class="p">)</span><span class="w">
</span><span class="n">print</span><span class="p">(</span><span class="w">
  </span><span class="n">ggplot</span><span class="p">(</span><span class="n">paths</span><span class="p">,</span><span class="w"> </span><span class="n">aes</span><span class="p">(</span><span class="n">dev</span><span class="p">,</span><span class="w"> </span><span class="n">value</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="m">1000</span><span class="p">,</span><span class="w"> </span><span class="n">colour</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">model</span><span class="p">,</span><span class="w"> </span><span class="n">linetype</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">model</span><span class="p">))</span><span class="w"> </span><span class="o">+</span><span class="w">
    </span><span class="n">geom_line</span><span class="p">(</span><span class="n">linewidth</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">1</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w">
    </span><span class="n">facet_wrap</span><span class="p">(</span><span class="o">~</span><span class="w"> </span><span class="n">panel</span><span class="p">,</span><span class="w"> </span><span class="n">nrow</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">2</span><span class="p">,</span><span class="w"> </span><span class="n">scales</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"free_y"</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w">
    </span><span class="n">scale_colour_manual</span><span class="p">(</span><span class="n">values</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">colours</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w">
    </span><span class="n">scale_linetype_manual</span><span class="p">(</span><span class="n">values</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">c</span><span class="p">(</span><span class="s2">"solid"</span><span class="p">,</span><span class="w"> </span><span class="s2">"22"</span><span class="p">,</span><span class="w"> </span><span class="s2">"solid"</span><span class="p">))</span><span class="w"> </span><span class="o">+</span><span class="w">
    </span><span class="n">scale_x_continuous</span><span class="p">(</span><span class="n">breaks</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">seq</span><span class="p">(</span><span class="m">2</span><span class="p">,</span><span class="w"> </span><span class="n">n</span><span class="p">,</span><span class="w"> </span><span class="m">2</span><span class="p">))</span><span class="w"> </span><span class="o">+</span><span class="w">
    </span><span class="n">labs</span><span class="p">(</span><span class="n">title</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Projected cumulative payments from the last observed diagonal"</span><span class="p">,</span><span class="w">
         </span><span class="n">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Development year"</span><span class="p">,</span><span class="w"> </span><span class="n">y</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Cumulative paid (millions)"</span><span class="p">))</span><span class="w">
</span><span class="n">options</span><span class="p">(</span><span class="n">repr.plot.height</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">5.5</span><span class="p">)</span><span class="w">


</span><span class="c1"># ---- 4. Experiment B: mini-benchmark on fresh triangles -------------------------</span><span class="w">

</span><span class="c1"># Aggregate generator: gamma payment pattern with over-dispersed Poisson noise.</span><span class="w">
</span><span class="c1">#   speedup   : mean payment delay shrinks by up to this share for recent AYs</span><span class="w">
</span><span class="c1">#   inflation : calendar-year log-growth rate after calendar period 7 (2% before)</span><span class="w">
</span><span class="n">simulate_case</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="k">function</span><span class="p">(</span><span class="n">seed</span><span class="p">,</span><span class="w"> </span><span class="n">speedup</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">0.3</span><span class="p">,</span><span class="w"> </span><span class="n">inflation</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">0.08</span><span class="p">,</span><span class="w"> </span><span class="n">phi</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">5</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">set.seed</span><span class="p">(</span><span class="n">seed</span><span class="p">)</span><span class="w">
  </span><span class="n">ultimate</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="m">5e4</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="p">(</span><span class="m">1</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="m">0.03</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="p">(</span><span class="m">0</span><span class="o">:</span><span class="p">(</span><span class="n">n</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="m">1</span><span class="p">)))</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nf">exp</span><span class="p">(</span><span class="n">rnorm</span><span class="p">(</span><span class="n">n</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">0.05</span><span class="p">))</span><span class="w">
  </span><span class="n">delay</span><span class="w">    </span><span class="o">&lt;-</span><span class="w"> </span><span class="m">3</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="p">(</span><span class="m">1</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">speedup</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">pmax</span><span class="p">(</span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="p">(</span><span class="m">1</span><span class="o">:</span><span class="n">n</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="m">4</span><span class="p">)</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="p">(</span><span class="n">n</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="m">4</span><span class="p">)))</span><span class="w">
  </span><span class="n">pattern</span><span class="w">  </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">t</span><span class="p">(</span><span class="n">sapply</span><span class="p">(</span><span class="n">delay</span><span class="p">,</span><span class="w"> </span><span class="k">function</span><span class="p">(</span><span class="n">m</span><span class="p">)</span><span class="w"> </span><span class="n">diff</span><span class="p">(</span><span class="n">pgamma</span><span class="p">(</span><span class="m">0</span><span class="o">:</span><span class="n">n</span><span class="p">,</span><span class="w"> </span><span class="n">shape</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">2</span><span class="p">,</span><span class="w"> </span><span class="n">scale</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">m</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="m">2</span><span class="p">))))</span><span class="w">
  </span><span class="n">calendar</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">outer</span><span class="p">(</span><span class="m">1</span><span class="o">:</span><span class="n">n</span><span class="p">,</span><span class="w"> </span><span class="m">1</span><span class="o">:</span><span class="n">n</span><span class="p">,</span><span class="w"> </span><span class="s2">"+"</span><span class="p">)</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="m">1</span><span class="w">
  </span><span class="n">infl</span><span class="w">     </span><span class="o">&lt;-</span><span class="w"> </span><span class="nf">exp</span><span class="p">(</span><span class="m">0.02</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">pmin</span><span class="p">(</span><span class="n">calendar</span><span class="p">,</span><span class="w"> </span><span class="m">7</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">inflation</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">pmax</span><span class="p">(</span><span class="n">calendar</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="m">7</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">))</span><span class="w">
  </span><span class="n">mean_incr</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">ultimate</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">pattern</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">infl</span><span class="w">
  </span><span class="n">make_case</span><span class="p">(</span><span class="n">phi</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">matrix</span><span class="p">(</span><span class="n">rpois</span><span class="p">(</span><span class="n">n</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">n</span><span class="p">,</span><span class="w"> </span><span class="n">mean_incr</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="n">phi</span><span class="p">),</span><span class="w"> </span><span class="n">n</span><span class="p">))</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">n_rep</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="m">20</span><span class="w">                                   </span><span class="c1"># triangles per scenario</span><span class="w">
</span><span class="n">scenarios</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nf">list</span><span class="p">(</span><span class="w">
  </span><span class="c1"># "Baseline (no distortion)" = c(speedup = 0,   inflation = 0.02),  # control</span><span class="w">
  </span><span class="s2">"Settlement speed-up"</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="nf">c</span><span class="p">(</span><span class="n">speedup</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">0.3</span><span class="p">,</span><span class="w"> </span><span class="n">inflation</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">0.02</span><span class="p">),</span><span class="w">
  </span><span class="s2">"Speed-up + inflation"</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="nf">c</span><span class="p">(</span><span class="n">speedup</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">0.3</span><span class="p">,</span><span class="w"> </span><span class="n">inflation</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">0.08</span><span class="p">))</span><span class="w">

</span><span class="n">bench</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">do.call</span><span class="p">(</span><span class="n">rbind</span><span class="p">,</span><span class="w"> </span><span class="n">lapply</span><span class="p">(</span><span class="nf">names</span><span class="p">(</span><span class="n">scenarios</span><span class="p">),</span><span class="w"> </span><span class="k">function</span><span class="p">(</span><span class="n">sc</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">do.call</span><span class="p">(</span><span class="n">rbind</span><span class="p">,</span><span class="w"> </span><span class="n">lapply</span><span class="p">(</span><span class="nf">seq_len</span><span class="p">(</span><span class="n">n_rep</span><span class="p">),</span><span class="w"> </span><span class="k">function</span><span class="p">(</span><span class="n">seed</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">case</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">simulate_case</span><span class="p">(</span><span class="n">seed</span><span class="p">,</span><span class="w"> </span><span class="n">speedup</span><span class="w">   </span><span class="o">=</span><span class="w"> </span><span class="n">scenarios</span><span class="p">[[</span><span class="n">sc</span><span class="p">]][[</span><span class="s2">"speedup"</span><span class="p">]],</span><span class="w">
                                </span><span class="n">inflation</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">scenarios</span><span class="p">[[</span><span class="n">sc</span><span class="p">]][[</span><span class="s2">"inflation"</span><span class="p">]])</span><span class="w">
    </span><span class="n">point_fits</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">suppressWarnings</span><span class="p">(</span><span class="n">fit_models</span><span class="p">(</span><span class="n">case</span><span class="o">$</span><span class="n">upper</span><span class="p">,</span><span class="w"> </span><span class="n">uncertainty</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">FALSE</span><span class="p">))</span><span class="w">
    </span><span class="n">data.frame</span><span class="p">(</span><span class="n">scenario</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">sc</span><span class="p">,</span><span class="w"> </span><span class="n">seed</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">seed</span><span class="p">,</span><span class="w"> </span><span class="n">model</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">models</span><span class="p">,</span><span class="w">
               </span><span class="n">error</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">sapply</span><span class="p">(</span><span class="n">point_fits</span><span class="p">,</span><span class="w"> </span><span class="n">total_reserve</span><span class="p">)</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nf">sum</span><span class="p">(</span><span class="n">case</span><span class="o">$</span><span class="n">true_ibnr</span><span class="p">))</span><span class="w">
  </span><span class="p">}))</span><span class="w">
</span><span class="p">}))</span><span class="w">

</span><span class="n">score</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">aggregate</span><span class="p">(</span><span class="n">error</span><span class="w"> </span><span class="o">~</span><span class="w"> </span><span class="n">scenario</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">model</span><span class="p">,</span><span class="w"> </span><span class="n">data</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">bench</span><span class="p">,</span><span class="w"> </span><span class="n">FUN</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">function</span><span class="p">(</span><span class="n">e</span><span class="p">)</span><span class="w">
  </span><span class="nf">c</span><span class="p">(</span><span class="n">bias</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">mean</span><span class="p">(</span><span class="n">e</span><span class="p">),</span><span class="w"> </span><span class="n">RMSE</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">sqrt</span><span class="p">(</span><span class="n">mean</span><span class="p">(</span><span class="n">e</span><span class="o">^</span><span class="m">2</span><span class="p">)),</span><span class="w"> </span><span class="n">MAE</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">mean</span><span class="p">(</span><span class="nf">abs</span><span class="p">(</span><span class="n">e</span><span class="p">))))</span><span class="w">
</span><span class="n">score</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">do.call</span><span class="p">(</span><span class="n">data.frame</span><span class="p">,</span><span class="w"> </span><span class="n">score</span><span class="p">)</span><span class="w">
</span><span class="nf">names</span><span class="p">(</span><span class="n">score</span><span class="p">)</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nf">c</span><span class="p">(</span><span class="s2">"scenario"</span><span class="p">,</span><span class="w"> </span><span class="s2">"model"</span><span class="p">,</span><span class="w"> </span><span class="s2">"bias"</span><span class="p">,</span><span class="w"> </span><span class="s2">"RMSE"</span><span class="p">,</span><span class="w"> </span><span class="s2">"MAE"</span><span class="p">)</span><span class="w">
</span><span class="n">score</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">score</span><span class="p">[</span><span class="n">order</span><span class="p">(</span><span class="n">score</span><span class="o">$</span><span class="n">scenario</span><span class="p">,</span><span class="w"> </span><span class="n">score</span><span class="o">$</span><span class="n">RMSE</span><span class="p">),</span><span class="w"> </span><span class="p">]</span><span class="w">

</span><span class="n">cat</span><span class="p">(</span><span class="s2">"\nError of the total reserve over"</span><span class="p">,</span><span class="w"> </span><span class="n">n_rep</span><span class="p">,</span><span class="w"> </span><span class="s2">"triangles per scenario (thousands):\n"</span><span class="p">)</span><span class="w">
</span><span class="n">fmt</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="k">function</span><span class="p">(</span><span class="n">x</span><span class="p">)</span><span class="w"> </span><span class="n">formatC</span><span class="p">(</span><span class="nf">round</span><span class="p">(</span><span class="n">x</span><span class="p">),</span><span class="w"> </span><span class="n">format</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"d"</span><span class="p">,</span><span class="w"> </span><span class="n">big.mark</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">","</span><span class="p">)</span><span class="w">
</span><span class="n">print</span><span class="p">(</span><span class="n">transform</span><span class="p">(</span><span class="n">score</span><span class="p">,</span><span class="w"> </span><span class="n">bias</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">fmt</span><span class="p">(</span><span class="n">bias</span><span class="p">),</span><span class="w"> </span><span class="n">RMSE</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">fmt</span><span class="p">(</span><span class="n">RMSE</span><span class="p">),</span><span class="w"> </span><span class="n">MAE</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">fmt</span><span class="p">(</span><span class="n">MAE</span><span class="p">)),</span><span class="w">
      </span><span class="n">row.names</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">FALSE</span><span class="p">)</span><span class="w">

</span><span class="n">long</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">rbind</span><span class="p">(</span><span class="n">data.frame</span><span class="p">(</span><span class="n">score</span><span class="p">[</span><span class="nf">c</span><span class="p">(</span><span class="s2">"scenario"</span><span class="p">,</span><span class="w"> </span><span class="s2">"model"</span><span class="p">)],</span><span class="w"> </span><span class="n">metric</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"RMSE"</span><span class="p">,</span><span class="w"> </span><span class="n">value</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">score</span><span class="o">$</span><span class="n">RMSE</span><span class="p">),</span><span class="w">
              </span><span class="n">data.frame</span><span class="p">(</span><span class="n">score</span><span class="p">[</span><span class="nf">c</span><span class="p">(</span><span class="s2">"scenario"</span><span class="p">,</span><span class="w"> </span><span class="s2">"model"</span><span class="p">)],</span><span class="w"> </span><span class="n">metric</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"MAE"</span><span class="p">,</span><span class="w">  </span><span class="n">value</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">score</span><span class="o">$</span><span class="n">MAE</span><span class="p">))</span><span class="w">
</span><span class="n">long</span><span class="o">$</span><span class="n">model</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">factor</span><span class="p">(</span><span class="n">long</span><span class="o">$</span><span class="n">model</span><span class="p">,</span><span class="w"> </span><span class="n">levels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">rev</span><span class="p">(</span><span class="n">models</span><span class="p">))</span><span class="w">
</span><span class="n">print</span><span class="p">(</span><span class="w">
  </span><span class="n">ggplot</span><span class="p">(</span><span class="n">long</span><span class="p">,</span><span class="w"> </span><span class="n">aes</span><span class="p">(</span><span class="n">value</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="m">1000</span><span class="p">,</span><span class="w"> </span><span class="n">model</span><span class="p">,</span><span class="w"> </span><span class="n">fill</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">model</span><span class="p">))</span><span class="w"> </span><span class="o">+</span><span class="w">
    </span><span class="n">geom_col</span><span class="p">(</span><span class="n">width</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">0.6</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w">
    </span><span class="n">facet_grid</span><span class="p">(</span><span class="n">metric</span><span class="w"> </span><span class="o">~</span><span class="w"> </span><span class="n">scenario</span><span class="p">,</span><span class="w"> </span><span class="n">scales</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"free_x"</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w">
    </span><span class="n">scale_fill_manual</span><span class="p">(</span><span class="n">values</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">colours</span><span class="p">,</span><span class="w"> </span><span class="n">guide</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"none"</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w">
    </span><span class="n">labs</span><span class="p">(</span><span class="n">title</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Out-of-sample error of the total reserve (lower is better)"</span><span class="p">,</span><span class="w">
         </span><span class="n">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Millions"</span><span class="p">,</span><span class="w"> </span><span class="n">y</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">NULL</span><span class="p">))</span><span class="w">
</span></code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Observed cumulative paid, 'All combined' (thousands):
      dev
origin     1     2     3     4     5     6     7     8     9    10
    1   4237 21004 36675 44904 48487 50631 51470 52018 52076 52338
    2   5196 24204 40131 49989 54633 56379 57092 57118 57118    NA
    3   4957 25818 42102 50806 55128 57004 57670 57832    NA    NA
    4   5908 26245 42825 52156 55830 58106 59312    NA    NA    NA
    5   6586 30632 49131 60349 64925 68100    NA    NA    NA    NA
    6   9292 35376 56432 67829 72032    NA    NA    NA    NA    NA
    7   9703 42713 67916 80049    NA    NA    NA    NA    NA    NA
    8  13752 56439 82297    NA    NA    NA    NA    NA    NA    NA
    9  17094 71057    NA    NA    NA    NA    NA    NA    NA    NA
    10 25376    NA    NA    NA    NA    NA    NA    NA    NA    NA

mlReserve with a random forest; true total reserve = 174,050 
mlReserve (formula interface)

        Latest Dev.To.Date Ultimate   IBNR         S.E        CV
2      57118.1   0.9994925  57147.1     29    39.76128 1.3710786
3      57832.2   0.9982222  57935.2    103    91.80065 0.8912685
4      59311.6   0.9935674  59695.6    384   244.97640 0.6379594
5      68099.8   0.9732881  69968.8   1869   888.80702 0.4755522
6      72032.5   0.9286787  77564.5   5532  1605.55443 0.2902304
7      80048.6   0.8558364  93532.6  13484  2443.15573 0.1811892
8      82297.0   0.7087359 116118.0  33821  7158.48746 0.2116581
9      71056.7   0.5017785 141609.7  70553 13547.27988 0.1920156
10     25375.8   0.1881838 134845.8 109470 23912.17707 0.2184359
total 573172.3   0.7090055 808417.3 235245 37338.60673 0.1587222
</code></pre></div></div>

<p><img src="/images/2026-10-03/2026-10-03-mlreserve_0_1.png" alt="image-title-here" class="img-responsive" /></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Predictive quantiles of the total reserve (thousands):
                         2.5%    50%     97.5%  
Chain ladder             316,100 356,087 397,519
mlReserve: SVM           178,587 252,035 371,639
mlReserve: random forest 162,579 217,369 301,946
</code></pre></div></div>

<p><img src="/images/2026-10-03/2026-10-03-mlreserve_0_3.png" alt="image-title-here" class="img-responsive" /></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Error of the total reserve over 20 triangles per scenario (thousands):
             scenario                    model    bias    RMSE     MAE
  Settlement speed-up mlReserve: random forest  67,436  67,519  67,436
  Settlement speed-up             Chain ladder  94,022  94,097  94,022
  Settlement speed-up           mlReserve: SVM  93,888  94,318  93,888
 Speed-up + inflation mlReserve: random forest  58,834  58,975  58,834
 Speed-up + inflation           mlReserve: SVM  79,344  79,800  79,344
 Speed-up + inflation             Chain ladder 100,217 100,311 100,217
</code></pre></div></div>

<p><img src="/images/2026-10-03/2026-10-03-mlreserve_0_5.png" alt="image-title-here" class="img-responsive" /></p>

<p><img src="/images/2026-10-03/2026-10-03-mlreserve_0_6.png" alt="image-title-here" class="img-responsive" /></p>

<p>The experiment is not a universal proof that random forests are better. It is a demonstration that <code class="language-plaintext highlighter-rouge">mlReserve()</code> can be used to explore the performance of machine-learning models on loss reserving problems, and that it can outperform the classical chain ladder method in some scenarios.</p>]]></content><author><name></name></author><category term="R" /><summary type="html"><![CDATA[This post presents a function `mlReserve()` for machine-learning loss reserving on 'real-world' triangles, based on a fork of the 'ChainLadder' package. It includes two experiments, each scored against a known true reserve: (A) Two triangles from an individual-claims simulator (Wang & Wüthrich), embedded below: chain ladder vs mlReserve, in detail; (B) A mini-benchmark on freshly simulated triangles from a simpler aggregate generator, with tunable distortions.]]></summary></entry><entry><title type="html">Analytics Pipeline for Dashboards, with Python, R and Javascript</title><link href="https://thierrymoudiki.github.io/blog/2026/09/27/python/r/javascript/analytics-pipeline" rel="alternate" type="text/html" title="Analytics Pipeline for Dashboards, with Python, R and Javascript" /><published>2026-09-27T00:00:00+00:00</published><updated>2026-09-27T00:00:00+00:00</updated><id>https://thierrymoudiki.github.io/blog/2026/09/27/python/r/javascript/analytics-pipeline</id><content type="html" xml:base="https://thierrymoudiki.github.io/blog/2026/09/27/python/r/javascript/analytics-pipeline"><![CDATA[<p>I had a lot of fun this morning, brainstorming and assembling this <em>Analytics Pipeline</em> for Dashboards with Claude (and yes it takes much, much more than only 10 prompts).</p>

<p>The philosophy is borrowed from Observable Framework’s data loader, but implemented from scratch with voluntarily opinionated choices of libraries for Python, R and Javascript (and no Markdown): pre-compute data with <strong>Python + Polars</strong> (Polars is my new <em>crush</em>) or <strong>R + dplyr</strong>, publish it as static files, and explore them with <strong>JavaScript</strong> in the browser (<a href="https://github.com/uwdata/arquero">Arquero</a> for data wrangling, <a href="https://observablehq.github.io/plot/">Observable Plot</a> or <a href="https://www.highcharts.com/">Highcharts</a> for charts)</p>

<p>The repository is available on <a href="https://github.com/thierrymoudiki/analytics-pipeline">GitHub</a> and here’s the quick start guide to run it locally (you need to have <a href="https://www.python.org/downloads/">Python</a> and <a href="https://www.r-project.org/">R</a> installed on your machine):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>make                        <span class="c"># list all targets</span>
uv venv venv                <span class="c"># or: make venv (python -m venv venv without uv)</span>
<span class="nb">source </span>venv/bin/activate    <span class="c"># Windows: venv\Scripts\activate</span>
make <span class="nb">install</span>                <span class="c"># Polars into venv/, dplyr into .uvr/library/</span>
make dev                    <span class="c"># starts http://localhost:3000 and opens your browser</span>
</code></pre></div></div>

<p>The repository’s README file is also informative and contains a few more details about the pipeline.</p>

<p><img src="/images/2026-09-27/2026-09-27-image1.png" alt="image-title-here" class="img-responsive" /></p>]]></content><author><name></name></author><category term="Python" /><category term="R" /><category term="Javascript" /><summary type="html"><![CDATA[I had a lot of fun this morning, brainstorming and assembling this _Analytics Pipeline_ for Dashboards with Claude (and yes it takes much, much more than only 10 prompts).]]></summary></entry><entry><title type="html">Python version of Semi-parametric option pricing based on underlying’s historical data (accepted at the osQF 2026 (ex R/Finance) conference)</title><link href="https://thierrymoudiki.github.io/blog/2026/09/21/python/empirical-pricing" rel="alternate" type="text/html" title="Python version of Semi-parametric option pricing based on underlying’s historical data (accepted at the osQF 2026 (ex R/Finance) conference)" /><published>2026-09-21T00:00:00+00:00</published><updated>2026-09-21T00:00:00+00:00</updated><id>https://thierrymoudiki.github.io/blog/2026/09/21/python/empirical-pricing-</id><content type="html" xml:base="https://thierrymoudiki.github.io/blog/2026/09/21/python/empirical-pricing"><![CDATA[<p>Python implementation of <em>“Semi-parametric option pricing based on underlying’s historical data”</em> (accepted as osQF (ex R/Finance) 2026, <a href="https://www.researchgate.net/publication/414513095_Semi-parametric_option_pricing_based_on_underlying's_historical_data">https://www.researchgate.net/publication/414513095_Semi-parametric_option_pricing_based_on_underlying’s_historical_data</a>). I’m now interested in constructive remarks and feedback on the study, that will allow to enrich, improve and robustify the methodology.</p>

<p><strong>Idea:</strong> build an empirical pricing <em>measure</em> straight from the underlying’s own historical dynamics — no option-market quotes needed anywhere.</p>

<ol>
  <li>Discount log-prices at rate <code class="language-plaintext highlighter-rouge">r</code>, take first differences.</li>
  <li>Fit a no-intercept <strong>AR(1)</strong> to the differenced series and take residuals.</li>
  <li>Resample the residuals with a <strong>stationary block bootstrap</strong> (Politis &amp; Romano, 1994) — preserves dependence beyond the mean (e.g. volatility clustering) that an i.i.d. bootstrap would erase.</li>
  <li>Cumulate resampled residuals to each horizon, exponentiate, then apply a single scalar <strong>Duan–Simonato (1998)</strong> correction so the discounted price is an <em>exact</em> martingale under the resampled measure — this is the minimal adjustment consistent with the Fundamental Theorem of Asset Pricing (FTAP).</li>
  <li>Price European calls/puts, or arithmetic Asian options, as the discounted Monte Carlo average payoff.</li>
</ol>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">import</span> <span class="nn">matplotlib.pyplot</span> <span class="k">as</span> <span class="n">plt</span>
<span class="kn">from</span> <span class="nn">scipy.stats</span> <span class="kn">import</span> <span class="n">norm</span>
<span class="kn">from</span> <span class="nn">dataclasses</span> <span class="kn">import</span> <span class="n">dataclass</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Optional</span><span class="p">,</span> <span class="n">Sequence</span><span class="p">,</span> <span class="n">Literal</span>

<span class="o">%</span><span class="n">matplotlib</span> <span class="n">inline</span>
</code></pre></div></div>

<h2 id="1-pricing-module">1. Pricing module</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># --------------------------------------------------------------------
# Stationary block bootstrap (Politis &amp; Romano, 1994)
# --------------------------------------------------------------------
</span>
<span class="k">def</span> <span class="nf">stationary_bootstrap</span><span class="p">(</span><span class="n">eps</span><span class="p">,</span> <span class="n">n_sims</span><span class="p">,</span> <span class="n">mean_block_length</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="n">rng</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
    <span class="s">"""
    Draw `n_sims` resampled paths of length len(eps) from `eps` using the
    stationary bootstrap of Politis &amp; Romano (1994): blocks have i.i.d.
    Geometric(p) lengths with p = 1 / mean_block_length, so the resampled
    series stays (weakly) stationary, unlike a fixed-block bootstrap.
    """</span>
    <span class="n">eps</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">asarray</span><span class="p">(</span><span class="n">eps</span><span class="p">,</span> <span class="n">dtype</span><span class="o">=</span><span class="nb">float</span><span class="p">)</span>
    <span class="n">n</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">eps</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">rng</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
        <span class="n">rng</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="n">default_rng</span><span class="p">()</span>
    <span class="k">if</span> <span class="n">mean_block_length</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
        <span class="n">mean_block_length</span> <span class="o">=</span> <span class="nb">max</span><span class="p">(</span><span class="mf">1.0</span><span class="p">,</span> <span class="n">n</span> <span class="o">**</span> <span class="p">(</span><span class="mf">1.0</span> <span class="o">/</span> <span class="mf">3.0</span><span class="p">))</span>
    <span class="n">p</span> <span class="o">=</span> <span class="mf">1.0</span> <span class="o">/</span> <span class="n">mean_block_length</span>

    <span class="n">out</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">empty</span><span class="p">((</span><span class="n">n</span><span class="p">,</span> <span class="n">n_sims</span><span class="p">),</span> <span class="n">dtype</span><span class="o">=</span><span class="nb">float</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">n_sims</span><span class="p">):</span>
        <span class="n">idx</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">empty</span><span class="p">(</span><span class="n">n</span><span class="p">,</span> <span class="n">dtype</span><span class="o">=</span><span class="nb">int</span><span class="p">)</span>
        <span class="n">pos</span> <span class="o">=</span> <span class="n">rng</span><span class="p">.</span><span class="n">integers</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">n</span><span class="p">)</span>
        <span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">n</span><span class="p">):</span>
            <span class="k">if</span> <span class="n">t</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="ow">and</span> <span class="n">rng</span><span class="p">.</span><span class="n">random</span><span class="p">()</span> <span class="o">&gt;=</span> <span class="n">p</span><span class="p">:</span>
                <span class="n">pos</span> <span class="o">=</span> <span class="p">(</span><span class="n">pos</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">%</span> <span class="n">n</span>          <span class="c1"># continue current block
</span>            <span class="k">elif</span> <span class="n">t</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>
                <span class="n">pos</span> <span class="o">=</span> <span class="n">rng</span><span class="p">.</span><span class="n">integers</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">n</span><span class="p">)</span>      <span class="c1"># geometric restart -&gt; new block
</span>            <span class="n">idx</span><span class="p">[</span><span class="n">t</span><span class="p">]</span> <span class="o">=</span> <span class="n">pos</span>
        <span class="n">out</span><span class="p">[:,</span> <span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">eps</span><span class="p">[</span><span class="n">idx</span><span class="p">]</span>
    <span class="k">return</span> <span class="n">out</span>
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># --------------------------------------------------------------------
# AR(1)-filtered, bootstrapped residual pool
# --------------------------------------------------------------------
</span>
<span class="o">@</span><span class="n">dataclass</span>
<span class="k">class</span> <span class="nc">ResidualPool</span><span class="p">:</span>
    <span class="s">"""Everything needed to price options at a single risk-free rate r."""</span>
    <span class="n">resampled_resids</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">ndarray</span>   <span class="c1"># (n_w, n_sims)
</span>    <span class="n">n_w</span><span class="p">:</span> <span class="nb">int</span>
    <span class="n">phi</span><span class="p">:</span> <span class="nb">float</span>
    <span class="n">r</span><span class="p">:</span> <span class="nb">float</span>
    <span class="n">dt</span><span class="p">:</span> <span class="nb">float</span>
    <span class="n">S0</span><span class="p">:</span> <span class="nb">float</span>
    <span class="n">ann_vol</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.0</span>
    <span class="n">skew</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.0</span>
    <span class="n">excess_kurt</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.0</span>


<span class="k">def</span> <span class="nf">eq_pool</span><span class="p">(</span><span class="n">prices</span><span class="p">,</span> <span class="n">dates</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="n">r</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span> <span class="n">dt</span><span class="o">=</span><span class="mf">1.0</span><span class="o">/</span><span class="mf">252.0</span><span class="p">,</span> <span class="n">window</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span>
            <span class="n">n_sims</span><span class="o">=</span><span class="mi">10_000</span><span class="p">,</span> <span class="n">seed</span><span class="o">=</span><span class="mi">123</span><span class="p">,</span> <span class="n">S0</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
    <span class="s">"""
    Build the AR(1)-filtered, stationary-block-bootstrapped residual pool
    for a given risk-free rate. Re-estimate per rate, as the paper does,
    since the no-intercept AR(1) is sensitive to the mean level of the
    discounted series being fit.
    """</span>
    <span class="n">prices</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">asarray</span><span class="p">(</span><span class="n">prices</span><span class="p">,</span> <span class="n">dtype</span><span class="o">=</span><span class="nb">float</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">window</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span>
        <span class="n">prices</span> <span class="o">=</span> <span class="n">prices</span><span class="p">[</span><span class="o">-</span><span class="p">(</span><span class="n">window</span> <span class="o">+</span> <span class="mi">1</span><span class="p">):]</span>

    <span class="n">log_p</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">log</span><span class="p">(</span><span class="n">prices</span><span class="p">)</span>
    <span class="n">t_index</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">arange</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">log_p</span><span class="p">))</span> <span class="o">*</span> <span class="n">dt</span>
    <span class="n">log_disc</span> <span class="o">=</span> <span class="n">log_p</span> <span class="o">-</span> <span class="n">r</span> <span class="o">*</span> <span class="n">t_index</span>
    <span class="n">x</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">diff</span><span class="p">(</span><span class="n">log_disc</span><span class="p">)</span>

    <span class="n">x_lag</span><span class="p">,</span> <span class="n">x_lead</span> <span class="o">=</span> <span class="n">x</span><span class="p">[:</span><span class="o">-</span><span class="mi">1</span><span class="p">],</span> <span class="n">x</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span>
    <span class="n">phi</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="nb">sum</span><span class="p">(</span><span class="n">x_lag</span> <span class="o">*</span> <span class="n">x_lead</span><span class="p">)</span> <span class="o">/</span> <span class="n">np</span><span class="p">.</span><span class="nb">sum</span><span class="p">(</span><span class="n">x_lag</span> <span class="o">**</span> <span class="mi">2</span><span class="p">))</span>

    <span class="n">eps</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">empty_like</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
    <span class="n">eps</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span> <span class="o">=</span> <span class="n">x_lead</span> <span class="o">-</span> <span class="n">phi</span> <span class="o">*</span> <span class="n">x_lag</span>
    <span class="n">eps</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="n">x</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">-</span> <span class="n">x</span><span class="p">.</span><span class="n">mean</span><span class="p">()</span>
    <span class="n">eps</span> <span class="o">=</span> <span class="n">eps</span> <span class="o">-</span> <span class="n">eps</span><span class="p">.</span><span class="n">mean</span><span class="p">()</span>

    <span class="n">rng</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="n">default_rng</span><span class="p">(</span><span class="n">seed</span><span class="p">)</span>
    <span class="n">resampled</span> <span class="o">=</span> <span class="n">stationary_bootstrap</span><span class="p">(</span><span class="n">eps</span><span class="p">,</span> <span class="n">n_sims</span><span class="o">=</span><span class="n">n_sims</span><span class="p">,</span> <span class="n">rng</span><span class="o">=</span><span class="n">rng</span><span class="p">)</span>
    <span class="n">resampled</span> <span class="o">=</span> <span class="n">resampled</span> <span class="o">-</span> <span class="n">resampled</span><span class="p">.</span><span class="n">mean</span><span class="p">(</span><span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">keepdims</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">ResidualPool</span><span class="p">(</span>
        <span class="n">resampled_resids</span><span class="o">=</span><span class="n">resampled</span><span class="p">,</span> <span class="n">n_w</span><span class="o">=</span><span class="nb">len</span><span class="p">(</span><span class="n">eps</span><span class="p">),</span> <span class="n">phi</span><span class="o">=</span><span class="n">phi</span><span class="p">,</span> <span class="n">r</span><span class="o">=</span><span class="n">r</span><span class="p">,</span> <span class="n">dt</span><span class="o">=</span><span class="n">dt</span><span class="p">,</span>
        <span class="n">S0</span><span class="o">=</span><span class="nb">float</span><span class="p">(</span><span class="n">S0</span> <span class="k">if</span> <span class="n">S0</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span> <span class="k">else</span> <span class="n">prices</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]),</span>
        <span class="n">ann_vol</span><span class="o">=</span><span class="nb">float</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">std</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">sqrt</span><span class="p">(</span><span class="mf">1.0</span> <span class="o">/</span> <span class="n">dt</span><span class="p">)),</span>
        <span class="n">skew</span><span class="o">=</span><span class="nb">float</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">mean</span><span class="p">(</span><span class="n">eps</span> <span class="o">**</span> <span class="mi">3</span><span class="p">)</span> <span class="o">/</span> <span class="n">np</span><span class="p">.</span><span class="n">std</span><span class="p">(</span><span class="n">eps</span><span class="p">)</span> <span class="o">**</span> <span class="mi">3</span><span class="p">),</span>
        <span class="n">excess_kurt</span><span class="o">=</span><span class="nb">float</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">mean</span><span class="p">(</span><span class="n">eps</span> <span class="o">**</span> <span class="mi">4</span><span class="p">)</span> <span class="o">/</span> <span class="n">np</span><span class="p">.</span><span class="n">std</span><span class="p">(</span><span class="n">eps</span><span class="p">)</span> <span class="o">**</span> <span class="mi">4</span> <span class="o">-</span> <span class="mf">3.0</span><span class="p">),</span>
    <span class="p">)</span>
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># --------------------------------------------------------------------
# Terminal-price simulation with the Duan-Simonato martingale correction
# --------------------------------------------------------------------
</span>
<span class="k">def</span> <span class="nf">_simulate_terminal</span><span class="p">(</span><span class="n">pool</span><span class="p">,</span> <span class="n">T</span><span class="p">,</span> <span class="n">S0</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
    <span class="s">"""
    Cumulate bootstrapped residuals to horizon T, exponentiate, and apply
    the scalar Duan &amp; Simonato (1998) correction so that
        S0 == E[ e^{-rT} * S_T ]
    holds *exactly* under the empirical measure (the FTAP condition).
    """</span>
    <span class="n">S0</span> <span class="o">=</span> <span class="n">pool</span><span class="p">.</span><span class="n">S0</span> <span class="k">if</span> <span class="n">S0</span> <span class="ow">is</span> <span class="bp">None</span> <span class="k">else</span> <span class="n">S0</span>
    <span class="n">step_idx</span> <span class="o">=</span> <span class="nb">max</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="nb">min</span><span class="p">(</span><span class="nb">round</span><span class="p">(</span><span class="n">T</span> <span class="o">/</span> <span class="n">pool</span><span class="p">.</span><span class="n">dt</span><span class="p">),</span> <span class="n">pool</span><span class="p">.</span><span class="n">n_w</span><span class="p">))</span>
    <span class="n">cum_resids</span> <span class="o">=</span> <span class="n">pool</span><span class="p">.</span><span class="n">resampled_resids</span><span class="p">[:</span><span class="n">step_idx</span><span class="p">,</span> <span class="p">:].</span><span class="nb">sum</span><span class="p">(</span><span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>

    <span class="n">logD_T</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">log</span><span class="p">(</span><span class="n">S0</span><span class="p">)</span> <span class="o">+</span> <span class="n">cum_resids</span>
    <span class="n">S_T_raw</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">exp</span><span class="p">(</span><span class="n">logD_T</span> <span class="o">+</span> <span class="n">pool</span><span class="p">.</span><span class="n">r</span> <span class="o">*</span> <span class="n">T</span><span class="p">)</span>

    <span class="n">disc_T</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">exp</span><span class="p">(</span><span class="o">-</span><span class="n">pool</span><span class="p">.</span><span class="n">r</span> <span class="o">*</span> <span class="n">T</span><span class="p">)</span>
    <span class="n">c_ds</span> <span class="o">=</span> <span class="n">S0</span> <span class="o">/</span> <span class="p">(</span><span class="n">disc_T</span> <span class="o">*</span> <span class="n">S_T_raw</span><span class="p">.</span><span class="n">mean</span><span class="p">())</span>
    <span class="n">S_T</span> <span class="o">=</span> <span class="n">S_T_raw</span> <span class="o">*</span> <span class="n">c_ds</span>
    <span class="k">return</span> <span class="n">S_T</span><span class="p">,</span> <span class="n">c_ds</span><span class="p">,</span> <span class="n">disc_T</span>


<span class="k">def</span> <span class="nf">eq_price</span><span class="p">(</span><span class="n">pool</span><span class="p">,</span> <span class="n">T</span><span class="p">,</span> <span class="n">K</span><span class="p">,</span> <span class="n">S0</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="s">"call"</span><span class="p">):</span>
    <span class="s">"""Price a vector of European strikes K at maturity T from a pool."""</span>
    <span class="n">K</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">atleast_1d</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">asarray</span><span class="p">(</span><span class="n">K</span><span class="p">,</span> <span class="n">dtype</span><span class="o">=</span><span class="nb">float</span><span class="p">))</span>
    <span class="n">S_T</span><span class="p">,</span> <span class="n">c_ds</span><span class="p">,</span> <span class="n">disc_T</span> <span class="o">=</span> <span class="n">_simulate_terminal</span><span class="p">(</span><span class="n">pool</span><span class="p">,</span> <span class="n">T</span><span class="p">,</span> <span class="n">S0</span><span class="p">)</span>

    <span class="k">if</span> <span class="nb">type</span> <span class="o">==</span> <span class="s">"call"</span><span class="p">:</span>
        <span class="n">payoff</span> <span class="o">=</span> <span class="k">lambda</span> <span class="n">k</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">maximum</span><span class="p">(</span><span class="n">S_T</span> <span class="o">-</span> <span class="n">k</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">)</span>
    <span class="k">elif</span> <span class="nb">type</span> <span class="o">==</span> <span class="s">"put"</span><span class="p">:</span>
        <span class="n">payoff</span> <span class="o">=</span> <span class="k">lambda</span> <span class="n">k</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">maximum</span><span class="p">(</span><span class="n">k</span> <span class="o">-</span> <span class="n">S_T</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">)</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="k">raise</span> <span class="nb">ValueError</span><span class="p">(</span><span class="s">"type must be 'call' or 'put'"</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">([</span><span class="n">disc_T</span> <span class="o">*</span> <span class="n">payoff</span><span class="p">(</span><span class="n">k</span><span class="p">).</span><span class="n">mean</span><span class="p">()</span> <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="n">K</span><span class="p">])</span>


<span class="k">def</span> <span class="nf">eq_price_asian</span><span class="p">(</span><span class="n">pool</span><span class="p">,</span> <span class="n">T</span><span class="p">,</span> <span class="n">K</span><span class="p">,</span> <span class="n">n_fix</span><span class="p">,</span> <span class="n">S0</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
    <span class="s">"""
    Price arithmetic-average Asian calls on n_fix equally spaced fixing
    dates up to T. Applies the Duan-Simonato correction date-by-date,
    since the payoff depends on the whole path.
    """</span>
    <span class="n">K</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">atleast_1d</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">asarray</span><span class="p">(</span><span class="n">K</span><span class="p">,</span> <span class="n">dtype</span><span class="o">=</span><span class="nb">float</span><span class="p">))</span>
    <span class="n">S0</span> <span class="o">=</span> <span class="n">pool</span><span class="p">.</span><span class="n">S0</span> <span class="k">if</span> <span class="n">S0</span> <span class="ow">is</span> <span class="bp">None</span> <span class="k">else</span> <span class="n">S0</span>

    <span class="n">fixing_times</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">linspace</span><span class="p">(</span><span class="n">T</span> <span class="o">/</span> <span class="n">n_fix</span><span class="p">,</span> <span class="n">T</span><span class="p">,</span> <span class="n">n_fix</span><span class="p">)</span>
    <span class="n">fixing_idx</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">unique</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">clip</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="nb">round</span><span class="p">(</span><span class="n">fixing_times</span> <span class="o">/</span> <span class="n">pool</span><span class="p">.</span><span class="n">dt</span><span class="p">).</span><span class="n">astype</span><span class="p">(</span><span class="nb">int</span><span class="p">),</span> <span class="mi">1</span><span class="p">,</span> <span class="n">pool</span><span class="p">.</span><span class="n">n_w</span><span class="p">))</span>
    <span class="n">max_idx</span> <span class="o">=</span> <span class="n">fixing_idx</span><span class="p">.</span><span class="nb">max</span><span class="p">()</span>

    <span class="n">cum_path</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">cumsum</span><span class="p">(</span><span class="n">pool</span><span class="p">.</span><span class="n">resampled_resids</span><span class="p">[:</span><span class="n">max_idx</span><span class="p">,</span> <span class="p">:],</span> <span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>

    <span class="n">S_fix_cols</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">idx</span> <span class="ow">in</span> <span class="n">fixing_idx</span><span class="p">:</span>
        <span class="n">t_k</span> <span class="o">=</span> <span class="n">idx</span> <span class="o">*</span> <span class="n">pool</span><span class="p">.</span><span class="n">dt</span>
        <span class="n">cum_resids</span> <span class="o">=</span> <span class="n">cum_path</span><span class="p">[</span><span class="n">idx</span> <span class="o">-</span> <span class="mi">1</span><span class="p">,</span> <span class="p">:]</span>
        <span class="n">S_raw</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">exp</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">log</span><span class="p">(</span><span class="n">S0</span><span class="p">)</span> <span class="o">+</span> <span class="n">cum_resids</span> <span class="o">+</span> <span class="n">pool</span><span class="p">.</span><span class="n">r</span> <span class="o">*</span> <span class="n">t_k</span><span class="p">)</span>
        <span class="n">disc_k</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">exp</span><span class="p">(</span><span class="o">-</span><span class="n">pool</span><span class="p">.</span><span class="n">r</span> <span class="o">*</span> <span class="n">t_k</span><span class="p">)</span>
        <span class="n">c_k</span> <span class="o">=</span> <span class="n">S0</span> <span class="o">/</span> <span class="p">(</span><span class="n">disc_k</span> <span class="o">*</span> <span class="n">S_raw</span><span class="p">.</span><span class="n">mean</span><span class="p">())</span>
        <span class="n">S_fix_cols</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">S_raw</span> <span class="o">*</span> <span class="n">c_k</span><span class="p">)</span>

    <span class="n">S_fix</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">column_stack</span><span class="p">(</span><span class="n">S_fix_cols</span><span class="p">)</span>
    <span class="n">avgA</span> <span class="o">=</span> <span class="n">S_fix</span><span class="p">.</span><span class="n">mean</span><span class="p">(</span><span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
    <span class="n">disc_T</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">exp</span><span class="p">(</span><span class="o">-</span><span class="n">pool</span><span class="p">.</span><span class="n">r</span> <span class="o">*</span> <span class="n">T</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">([</span><span class="n">disc_T</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">maximum</span><span class="p">(</span><span class="n">avgA</span> <span class="o">-</span> <span class="n">k</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">).</span><span class="n">mean</span><span class="p">()</span> <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="n">K</span><span class="p">])</span>
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># --------------------------------------------------------------------
# Black-Scholes reference prices (for comparison / validation only)
# --------------------------------------------------------------------
</span>
<span class="k">def</span> <span class="nf">bs_call</span><span class="p">(</span><span class="n">S</span><span class="p">,</span> <span class="n">K</span><span class="p">,</span> <span class="n">r</span><span class="p">,</span> <span class="n">sigma</span><span class="p">,</span> <span class="n">T</span><span class="p">):</span>
    <span class="n">S</span><span class="p">,</span> <span class="n">K</span><span class="p">,</span> <span class="n">sigma</span><span class="p">,</span> <span class="n">T</span> <span class="o">=</span> <span class="nb">map</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">asarray</span><span class="p">,</span> <span class="p">(</span><span class="n">S</span><span class="p">,</span> <span class="n">K</span><span class="p">,</span> <span class="n">sigma</span><span class="p">,</span> <span class="n">T</span><span class="p">))</span>
    <span class="n">d1</span> <span class="o">=</span> <span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">log</span><span class="p">(</span><span class="n">S</span> <span class="o">/</span> <span class="n">K</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="n">r</span> <span class="o">+</span> <span class="mf">0.5</span> <span class="o">*</span> <span class="n">sigma</span> <span class="o">**</span> <span class="mi">2</span><span class="p">)</span> <span class="o">*</span> <span class="n">T</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">sigma</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">sqrt</span><span class="p">(</span><span class="n">T</span><span class="p">))</span>
    <span class="n">d2</span> <span class="o">=</span> <span class="n">d1</span> <span class="o">-</span> <span class="n">sigma</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">sqrt</span><span class="p">(</span><span class="n">T</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">S</span> <span class="o">*</span> <span class="n">norm</span><span class="p">.</span><span class="n">cdf</span><span class="p">(</span><span class="n">d1</span><span class="p">)</span> <span class="o">-</span> <span class="n">K</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">exp</span><span class="p">(</span><span class="o">-</span><span class="n">r</span> <span class="o">*</span> <span class="n">T</span><span class="p">)</span> <span class="o">*</span> <span class="n">norm</span><span class="p">.</span><span class="n">cdf</span><span class="p">(</span><span class="n">d2</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">bs_put</span><span class="p">(</span><span class="n">S</span><span class="p">,</span> <span class="n">K</span><span class="p">,</span> <span class="n">r</span><span class="p">,</span> <span class="n">sigma</span><span class="p">,</span> <span class="n">T</span><span class="p">):</span>
    <span class="k">return</span> <span class="n">bs_call</span><span class="p">(</span><span class="n">S</span><span class="p">,</span> <span class="n">K</span><span class="p">,</span> <span class="n">r</span><span class="p">,</span> <span class="n">sigma</span><span class="p">,</span> <span class="n">T</span><span class="p">)</span> <span class="o">-</span> <span class="n">S</span> <span class="o">+</span> <span class="n">K</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">exp</span><span class="p">(</span><span class="o">-</span><span class="n">r</span> <span class="o">*</span> <span class="n">T</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">kv_geometric_call</span><span class="p">(</span><span class="n">S0</span><span class="p">,</span> <span class="n">K</span><span class="p">,</span> <span class="n">r</span><span class="p">,</span> <span class="n">sigma</span><span class="p">,</span> <span class="n">T</span><span class="p">,</span> <span class="n">n_fix</span><span class="p">):</span>
    <span class="s">"""Closed-form Kemna-Vorst (1990) geometric-average Asian call."""</span>
    <span class="n">m</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">log</span><span class="p">(</span><span class="n">S0</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="n">r</span> <span class="o">-</span> <span class="mf">0.5</span> <span class="o">*</span> <span class="n">sigma</span> <span class="o">**</span> <span class="mi">2</span><span class="p">)</span> <span class="o">*</span> <span class="p">(</span><span class="n">n_fix</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="n">T</span> <span class="o">/</span> <span class="p">(</span><span class="mi">2</span> <span class="o">*</span> <span class="n">n_fix</span><span class="p">)</span>
    <span class="n">v2</span> <span class="o">=</span> <span class="n">sigma</span> <span class="o">**</span> <span class="mi">2</span> <span class="o">*</span> <span class="p">(</span><span class="n">n_fix</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="p">(</span><span class="mi">2</span> <span class="o">*</span> <span class="n">n_fix</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="mi">6</span> <span class="o">*</span> <span class="n">n_fix</span> <span class="o">**</span> <span class="mi">2</span><span class="p">)</span> <span class="o">*</span> <span class="n">T</span>
    <span class="n">v</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">sqrt</span><span class="p">(</span><span class="n">v2</span><span class="p">)</span>
    <span class="n">d1</span> <span class="o">=</span> <span class="p">(</span><span class="n">m</span> <span class="o">-</span> <span class="n">np</span><span class="p">.</span><span class="n">log</span><span class="p">(</span><span class="n">K</span><span class="p">))</span> <span class="o">/</span> <span class="n">v</span> <span class="o">+</span> <span class="n">v</span>
    <span class="n">d2</span> <span class="o">=</span> <span class="n">d1</span> <span class="o">-</span> <span class="n">v</span>
    <span class="k">return</span> <span class="n">np</span><span class="p">.</span><span class="n">exp</span><span class="p">(</span><span class="o">-</span><span class="n">r</span> <span class="o">*</span> <span class="n">T</span><span class="p">)</span> <span class="o">*</span> <span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">exp</span><span class="p">(</span><span class="n">m</span> <span class="o">+</span> <span class="n">v2</span> <span class="o">/</span> <span class="mi">2</span><span class="p">)</span> <span class="o">*</span> <span class="n">norm</span><span class="p">.</span><span class="n">cdf</span><span class="p">(</span><span class="n">d1</span><span class="p">)</span> <span class="o">-</span> <span class="n">K</span> <span class="o">*</span> <span class="n">norm</span><span class="p">.</span><span class="n">cdf</span><span class="p">(</span><span class="n">d2</span><span class="p">))</span>
</code></pre></div></div>

<h2 id="2-a-historical-price-series">2. A historical price series</h2>

<p>This pulls real daily closes with <strong><code class="language-plaintext highlighter-rouge">yfinance</code></strong> and falls back to a synthetic Student-t path if the fetch fails (e.g. no internet access, ticker unavailable, or rate-limited). Swap <code class="language-plaintext highlighter-rouge">TICKER</code> for any Yahoo Finance symbol — an index, a single stock, an FX pair, a private/illiquid name you track yourself, etc. The rest of the notebook is unchanged either way.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">try</span><span class="p">:</span>
    <span class="kn">import</span> <span class="nn">yfinance</span> <span class="k">as</span> <span class="n">yf</span>
<span class="k">except</span> <span class="nb">ImportError</span><span class="p">:</span>
    <span class="o">%</span><span class="n">pip</span> <span class="n">install</span> <span class="o">-</span><span class="n">q</span> <span class="n">yfinance</span>
    <span class="kn">import</span> <span class="nn">yfinance</span> <span class="k">as</span> <span class="n">yf</span>
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">yfinance</span> <span class="k">as</span> <span class="n">yf</span>

<span class="n">TICKER</span> <span class="o">=</span> <span class="s">"^GDAXI"</span>          <span class="c1"># DAX index -- change to any Yahoo Finance ticker
</span><span class="n">START</span><span class="p">,</span> <span class="n">END</span> <span class="o">=</span> <span class="s">"1998-07-05"</span><span class="p">,</span> <span class="s">"2002-07-05"</span>

<span class="k">try</span><span class="p">:</span>
    <span class="n">raw</span> <span class="o">=</span> <span class="n">yf</span><span class="p">.</span><span class="n">download</span><span class="p">(</span><span class="n">TICKER</span><span class="p">,</span> <span class="n">start</span><span class="o">=</span><span class="n">START</span><span class="p">,</span> <span class="n">end</span><span class="o">=</span><span class="n">END</span><span class="p">,</span> <span class="n">progress</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span> <span class="n">auto_adjust</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">raw</span><span class="p">.</span><span class="n">empty</span><span class="p">:</span>
        <span class="k">raise</span> <span class="nb">RuntimeError</span><span class="p">(</span><span class="s">"yfinance returned no rows"</span><span class="p">)</span>
    <span class="n">px</span> <span class="o">=</span> <span class="n">raw</span><span class="p">[</span><span class="s">"Close"</span><span class="p">].</span><span class="n">dropna</span><span class="p">()</span>
    <span class="k">if</span> <span class="nb">hasattr</span><span class="p">(</span><span class="n">px</span><span class="p">,</span> <span class="s">"squeeze"</span><span class="p">):</span>
        <span class="n">px</span> <span class="o">=</span> <span class="n">px</span><span class="p">.</span><span class="n">squeeze</span><span class="p">()</span>          <span class="c1"># yfinance can return a 1-col DataFrame
</span>    <span class="n">prices</span> <span class="o">=</span> <span class="n">px</span><span class="p">.</span><span class="n">to_numpy</span><span class="p">().</span><span class="n">astype</span><span class="p">(</span><span class="nb">float</span><span class="p">)</span>
    <span class="n">dates</span> <span class="o">=</span> <span class="n">px</span><span class="p">.</span><span class="n">index</span><span class="p">.</span><span class="n">to_numpy</span><span class="p">()</span>
    <span class="n">S0</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="n">prices</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">])</span>
    <span class="n">dt</span> <span class="o">=</span> <span class="mi">1</span> <span class="o">/</span> <span class="mi">252</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Loaded </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">prices</span><span class="p">)</span><span class="si">}</span><span class="s"> daily closes for </span><span class="si">{</span><span class="n">TICKER</span><span class="si">}</span><span class="s"> from yfinance, "</span>
          <span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="nb">str</span><span class="p">(</span><span class="n">dates</span><span class="p">[</span><span class="mi">0</span><span class="p">])[</span><span class="si">:</span><span class="mi">10</span><span class="p">]</span><span class="si">}</span><span class="s"> to </span><span class="si">{</span><span class="nb">str</span><span class="p">(</span><span class="n">dates</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">])[</span><span class="si">:</span><span class="mi">10</span><span class="p">]</span><span class="si">}</span><span class="s">."</span><span class="p">)</span>
<span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"yfinance fetch failed (</span><span class="si">{</span><span class="n">e</span><span class="si">!r}</span><span class="s">); falling back to a synthetic price path."</span><span class="p">)</span>
    <span class="n">rng</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="n">default_rng</span><span class="p">(</span><span class="mi">123</span><span class="p">)</span>
    <span class="n">n_days</span> <span class="o">=</span> <span class="mi">1045</span>                  <span class="c1"># ~4y of daily data
</span>    <span class="n">dt</span> <span class="o">=</span> <span class="mi">1</span> <span class="o">/</span> <span class="mi">252</span>
    <span class="n">mu</span><span class="p">,</span> <span class="n">sigma_true</span> <span class="o">=</span> <span class="mf">0.04</span><span class="p">,</span> <span class="mf">0.28</span>
    <span class="n">t_innov</span> <span class="o">=</span> <span class="n">rng</span><span class="p">.</span><span class="n">standard_t</span><span class="p">(</span><span class="n">df</span><span class="o">=</span><span class="mi">5</span><span class="p">,</span> <span class="n">size</span><span class="o">=</span><span class="n">n_days</span><span class="p">)</span> <span class="o">/</span> <span class="n">np</span><span class="p">.</span><span class="n">sqrt</span><span class="p">(</span><span class="mi">5</span> <span class="o">/</span> <span class="mi">3</span><span class="p">)</span>
    <span class="n">log_rets</span> <span class="o">=</span> <span class="p">(</span><span class="n">mu</span> <span class="o">-</span> <span class="mf">0.5</span> <span class="o">*</span> <span class="n">sigma_true</span> <span class="o">**</span> <span class="mi">2</span><span class="p">)</span> <span class="o">*</span> <span class="n">dt</span> <span class="o">+</span> <span class="n">sigma_true</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">sqrt</span><span class="p">(</span><span class="n">dt</span><span class="p">)</span> <span class="o">*</span> <span class="n">t_innov</span>
    <span class="n">S0</span> <span class="o">=</span> <span class="mf">4468.17</span>
    <span class="n">prices</span> <span class="o">=</span> <span class="n">S0</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">exp</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">cumsum</span><span class="p">(</span><span class="n">log_rets</span><span class="p">))</span>
    <span class="n">prices</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">insert</span><span class="p">(</span><span class="n">prices</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">S0</span><span class="p">)[:</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span>

<span class="n">plt</span><span class="p">.</span><span class="n">figure</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">9</span><span class="p">,</span> <span class="mi">3</span><span class="p">))</span>
<span class="n">plt</span><span class="p">.</span><span class="n">plot</span><span class="p">(</span><span class="n">prices</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="n">title</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">TICKER</span><span class="si">}</span><span class="s"> price path"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="n">xlabel</span><span class="p">(</span><span class="s">"Trading day"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="n">ylabel</span><span class="p">(</span><span class="s">"Price"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="n">tight_layout</span><span class="p">()</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Loaded 1015 daily closes for ^GDAXI from yfinance, 1998-07-06 to 2002-07-04.
</code></pre></div></div>

<p><img src="/images/2026-09-21/2026-09-21-empirical-pricing-_9_1.png" alt="image-title-here" class="img-responsive" /></p>

<h2 id="3-european-calls--puts-vs-black-scholes">3. European calls &amp; puts vs. Black-Scholes</h2>

<p><strong>A note on the “systematic bias” you may see if you compare against an arbitrary volatility:</strong> the model’s only promise is <em>internal</em> no-arbitrage consistency (put-call parity, the martingale condition).</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">tqdm</span> <span class="kn">import</span> <span class="n">tqdm</span>

<span class="n">T_grid</span> <span class="o">=</span> <span class="p">[</span><span class="mf">0.0389</span><span class="p">,</span> <span class="mf">0.1139</span><span class="p">,</span> <span class="mf">0.2083</span><span class="p">,</span> <span class="mf">0.4583</span><span class="p">,</span> <span class="mf">0.7111</span><span class="p">,</span> <span class="mf">0.9583</span><span class="p">,</span> <span class="mf">1.4556</span><span class="p">,</span> <span class="mf">1.9528</span><span class="p">]</span>
<span class="n">r_grid</span> <span class="o">=</span> <span class="p">[</span><span class="mf">0.0357</span><span class="p">,</span> <span class="mf">0.0349</span><span class="p">,</span> <span class="mf">0.0341</span><span class="p">,</span> <span class="mf">0.0355</span><span class="p">,</span> <span class="mf">0.0359</span><span class="p">,</span> <span class="mf">0.0368</span><span class="p">,</span> <span class="mf">0.0386</span><span class="p">,</span> <span class="mf">0.0401</span><span class="p">]</span>
<span class="n">strikes</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">([</span><span class="mi">3400</span><span class="p">,</span> <span class="mi">3600</span><span class="p">,</span> <span class="mi">3800</span><span class="p">,</span> <span class="mi">4000</span><span class="p">,</span> <span class="mi">4200</span><span class="p">,</span> <span class="mi">4400</span><span class="p">,</span> <span class="mi">4500</span><span class="p">,</span> <span class="mi">4600</span><span class="p">,</span>
                     <span class="mi">4800</span><span class="p">,</span> <span class="mi">5000</span><span class="p">,</span> <span class="mi">5200</span><span class="p">,</span> <span class="mi">5400</span><span class="p">,</span> <span class="mi">5600</span><span class="p">])</span>

<span class="n">results</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">pools</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">for</span> <span class="n">T</span><span class="p">,</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">tqdm</span><span class="p">(</span><span class="nb">zip</span><span class="p">(</span><span class="n">T_grid</span><span class="p">,</span> <span class="n">r_grid</span><span class="p">)):</span>
    <span class="n">pool</span> <span class="o">=</span> <span class="n">eq_pool</span><span class="p">(</span><span class="n">prices</span><span class="p">,</span> <span class="n">r</span><span class="o">=</span><span class="n">r</span><span class="p">,</span> <span class="n">dt</span><span class="o">=</span><span class="n">dt</span><span class="p">,</span> <span class="n">n_sims</span><span class="o">=</span><span class="mi">10_000</span><span class="p">,</span> <span class="n">seed</span><span class="o">=</span><span class="mi">123</span><span class="p">,</span> <span class="n">S0</span><span class="o">=</span><span class="n">S0</span><span class="p">)</span>
    <span class="n">pools</span><span class="p">[</span><span class="n">r</span><span class="p">]</span> <span class="o">=</span> <span class="n">pool</span>

    <span class="n">iv</span> <span class="o">=</span> <span class="n">pool</span><span class="p">.</span><span class="n">ann_vol</span>   <span class="c1"># &lt;- use the pool's own realized vol, not an arbitrary one
</span>    <span class="n">bs_c</span> <span class="o">=</span> <span class="n">bs_call</span><span class="p">(</span><span class="n">S0</span><span class="p">,</span> <span class="n">strikes</span><span class="p">,</span> <span class="n">r</span><span class="p">,</span> <span class="n">iv</span><span class="p">,</span> <span class="n">T</span><span class="p">)</span>
    <span class="n">bs_p</span> <span class="o">=</span> <span class="n">bs_put</span><span class="p">(</span><span class="n">S0</span><span class="p">,</span> <span class="n">strikes</span><span class="p">,</span> <span class="n">r</span><span class="p">,</span> <span class="n">iv</span><span class="p">,</span> <span class="n">T</span><span class="p">)</span>
    <span class="n">model_c</span> <span class="o">=</span> <span class="n">eq_price</span><span class="p">(</span><span class="n">pool</span><span class="p">,</span> <span class="n">T</span><span class="p">,</span> <span class="n">strikes</span><span class="p">,</span> <span class="n">S0</span><span class="o">=</span><span class="n">S0</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="s">"call"</span><span class="p">)</span>
    <span class="n">model_p</span> <span class="o">=</span> <span class="n">eq_price</span><span class="p">(</span><span class="n">pool</span><span class="p">,</span> <span class="n">T</span><span class="p">,</span> <span class="n">strikes</span><span class="p">,</span> <span class="n">S0</span><span class="o">=</span><span class="n">S0</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="s">"put"</span><span class="p">)</span>

    <span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">bc</span><span class="p">,</span> <span class="n">bp</span><span class="p">,</span> <span class="n">mc</span><span class="p">,</span> <span class="n">mp</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">strikes</span><span class="p">,</span> <span class="n">bs_c</span><span class="p">,</span> <span class="n">bs_p</span><span class="p">,</span> <span class="n">model_c</span><span class="p">,</span> <span class="n">model_p</span><span class="p">):</span>
        <span class="n">results</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="nb">dict</span><span class="p">(</span><span class="n">T</span><span class="o">=</span><span class="n">T</span><span class="p">,</span> <span class="n">r</span><span class="o">=</span><span class="n">r</span><span class="p">,</span> <span class="n">K</span><span class="o">=</span><span class="n">k</span><span class="p">,</span> <span class="n">bs_call</span><span class="o">=</span><span class="n">bc</span><span class="p">,</span> <span class="n">model_call</span><span class="o">=</span><span class="n">mc</span><span class="p">,</span>
                             <span class="n">bs_put</span><span class="o">=</span><span class="n">bp</span><span class="p">,</span> <span class="n">model_put</span><span class="o">=</span><span class="n">mp</span><span class="p">))</span>

<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Priced </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">results</span><span class="p">)</span><span class="si">}</span><span class="s"> (T, K) European call/put pairs across </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">T_grid</span><span class="p">)</span><span class="si">}</span><span class="s"> maturities."</span><span class="p">)</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>8it [01:12,  9.12s/it]

Priced 104 (T, K) European call/put pairs across 8 maturities.
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Put-call parity check: model_call - model_put == S0 - K e^{-rT}
</span><span class="n">max_err</span> <span class="o">=</span> <span class="mf">0.0</span>
<span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">results</span><span class="p">:</span>
    <span class="n">lhs</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="s">"model_call"</span><span class="p">]</span> <span class="o">-</span> <span class="n">row</span><span class="p">[</span><span class="s">"model_put"</span><span class="p">]</span>
    <span class="n">rhs</span> <span class="o">=</span> <span class="n">S0</span> <span class="o">-</span> <span class="n">row</span><span class="p">[</span><span class="s">"K"</span><span class="p">]</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">exp</span><span class="p">(</span><span class="o">-</span><span class="n">row</span><span class="p">[</span><span class="s">"r"</span><span class="p">]</span> <span class="o">*</span> <span class="n">row</span><span class="p">[</span><span class="s">"T"</span><span class="p">])</span>
    <span class="n">max_err</span> <span class="o">=</span> <span class="nb">max</span><span class="p">(</span><span class="n">max_err</span><span class="p">,</span> <span class="nb">abs</span><span class="p">(</span><span class="n">lhs</span> <span class="o">-</span> <span class="n">rhs</span><span class="p">))</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Max abs model put-call parity error: </span><span class="si">{</span><span class="n">max_err</span><span class="si">:</span><span class="p">.</span><span class="mi">6</span><span class="n">e</span><span class="si">}</span><span class="s"> (</span><span class="si">{</span><span class="mi">100</span> <span class="o">*</span> <span class="n">max_err</span> <span class="o">/</span> <span class="n">S0</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="n">e</span><span class="si">}</span><span class="s">% of S0)"</span><span class="p">)</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Max abs model put-call parity error: 9.094947e-13 (2.14e-14% of S0)
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">fig</span><span class="p">,</span> <span class="n">axes</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="n">subplots</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">16</span><span class="p">,</span> <span class="mi">7</span><span class="p">))</span>
<span class="k">for</span> <span class="n">ax</span><span class="p">,</span> <span class="n">T</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">axes</span><span class="p">.</span><span class="n">flat</span><span class="p">,</span> <span class="n">T_grid</span><span class="p">):</span>
    <span class="n">d</span> <span class="o">=</span> <span class="p">[</span><span class="n">r</span> <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">results</span> <span class="k">if</span> <span class="n">r</span><span class="p">[</span><span class="s">"T"</span><span class="p">]</span> <span class="o">==</span> <span class="n">T</span><span class="p">]</span>
    <span class="n">d</span><span class="p">.</span><span class="n">sort</span><span class="p">(</span><span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">r</span><span class="p">:</span> <span class="n">r</span><span class="p">[</span><span class="s">"K"</span><span class="p">])</span>
    <span class="n">ks</span> <span class="o">=</span> <span class="p">[</span><span class="n">r</span><span class="p">[</span><span class="s">"K"</span><span class="p">]</span> <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">d</span><span class="p">]</span>
    <span class="n">ax</span><span class="p">.</span><span class="n">plot</span><span class="p">(</span><span class="n">ks</span><span class="p">,</span> <span class="p">[</span><span class="n">r</span><span class="p">[</span><span class="s">"bs_call"</span><span class="p">]</span> <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">d</span><span class="p">],</span> <span class="s">"o--"</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="s">"steelblue"</span><span class="p">,</span>
            <span class="n">label</span><span class="o">=</span><span class="s">"BS @ pool's realized vol"</span><span class="p">,</span> <span class="n">markerfacecolor</span><span class="o">=</span><span class="s">"none"</span><span class="p">)</span>
    <span class="n">ax</span><span class="p">.</span><span class="n">plot</span><span class="p">(</span><span class="n">ks</span><span class="p">,</span> <span class="p">[</span><span class="n">r</span><span class="p">[</span><span class="s">"model_call"</span><span class="p">]</span> <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">d</span><span class="p">],</span> <span class="s">"o-"</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="s">"coral"</span><span class="p">,</span>
            <span class="n">label</span><span class="o">=</span><span class="s">"Empirical model"</span><span class="p">)</span>
    <span class="n">ax</span><span class="p">.</span><span class="n">axvline</span><span class="p">(</span><span class="n">S0</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="s">"gray"</span><span class="p">,</span> <span class="n">ls</span><span class="o">=</span><span class="s">":"</span><span class="p">,</span> <span class="n">lw</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
    <span class="n">ax</span><span class="p">.</span><span class="n">set_title</span><span class="p">(</span><span class="sa">f</span><span class="s">"T=</span><span class="si">{</span><span class="n">T</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="n">f</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
    <span class="n">ax</span><span class="p">.</span><span class="n">set_xlabel</span><span class="p">(</span><span class="s">"Strike"</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">ax</span> <span class="ow">is</span> <span class="n">axes</span><span class="p">.</span><span class="n">flat</span><span class="p">[</span><span class="mi">0</span><span class="p">]:</span>
        <span class="n">ax</span><span class="p">.</span><span class="n">set_ylabel</span><span class="p">(</span><span class="s">"Price"</span><span class="p">)</span>
        <span class="n">ax</span><span class="p">.</span><span class="n">legend</span><span class="p">(</span><span class="n">fontsize</span><span class="o">=</span><span class="mi">8</span><span class="p">)</span>
<span class="n">fig</span><span class="p">.</span><span class="n">suptitle</span><span class="p">(</span><span class="s">"Model price vs Black-Scholes (matched vol), by maturity"</span><span class="p">)</span>
<span class="n">fig</span><span class="p">.</span><span class="n">tight_layout</span><span class="p">()</span>
</code></pre></div></div>

<p><img src="/images/2026-09-21/2026-09-21-empirical-pricing-_13_0.png" alt="image-title-here" class="img-responsive" /></p>

<h2 id="4-arithmetic-asian-calls-vs-a-black-scholes-monte-carlo-benchmark">4. Arithmetic Asian calls vs. a Black-Scholes Monte Carlo benchmark</h2>

<p>The BS-MC engine is itself checked against the closed-form Kemna-Vorst geometric-average price before use, exactly as in the paper.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">bs_mc_asian</span><span class="p">(</span><span class="n">T</span><span class="p">,</span> <span class="n">r</span><span class="p">,</span> <span class="n">sigma</span><span class="p">,</span> <span class="n">K_vec</span><span class="p">,</span> <span class="n">n_fix</span><span class="p">,</span> <span class="n">n_sims</span><span class="o">=</span><span class="mi">10_000</span><span class="p">,</span> <span class="n">rng</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
    <span class="n">rng</span> <span class="o">=</span> <span class="n">rng</span> <span class="ow">or</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="n">default_rng</span><span class="p">(</span><span class="mi">7</span><span class="p">)</span>
    <span class="n">fixing_times</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">linspace</span><span class="p">(</span><span class="n">T</span> <span class="o">/</span> <span class="n">n_fix</span><span class="p">,</span> <span class="n">T</span><span class="p">,</span> <span class="n">n_fix</span><span class="p">)</span>
    <span class="n">dts</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">diff</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">concatenate</span><span class="p">(([</span><span class="mf">0.0</span><span class="p">],</span> <span class="n">fixing_times</span><span class="p">)))</span>
    <span class="n">Z</span> <span class="o">=</span> <span class="n">rng</span><span class="p">.</span><span class="n">standard_normal</span><span class="p">((</span><span class="n">n_fix</span><span class="p">,</span> <span class="n">n_sims</span><span class="p">))</span>
    <span class="n">log_incr</span> <span class="o">=</span> <span class="p">(</span><span class="n">r</span> <span class="o">-</span> <span class="mf">0.5</span> <span class="o">*</span> <span class="n">sigma</span> <span class="o">**</span> <span class="mi">2</span><span class="p">)</span> <span class="o">*</span> <span class="n">dts</span><span class="p">[:,</span> <span class="bp">None</span><span class="p">]</span> <span class="o">+</span> <span class="n">sigma</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">sqrt</span><span class="p">(</span><span class="n">dts</span><span class="p">)[:,</span> <span class="bp">None</span><span class="p">]</span> <span class="o">*</span> <span class="n">Z</span>
    <span class="n">logS_path</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">log</span><span class="p">(</span><span class="n">S0</span><span class="p">)</span> <span class="o">+</span> <span class="n">np</span><span class="p">.</span><span class="n">cumsum</span><span class="p">(</span><span class="n">log_incr</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>
    <span class="n">avgA</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">exp</span><span class="p">(</span><span class="n">logS_path</span><span class="p">).</span><span class="n">mean</span><span class="p">(</span><span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>
    <span class="n">avgG</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">exp</span><span class="p">(</span><span class="n">logS_path</span><span class="p">.</span><span class="n">mean</span><span class="p">(</span><span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="p">))</span>
    <span class="n">disc</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">exp</span><span class="p">(</span><span class="o">-</span><span class="n">r</span> <span class="o">*</span> <span class="n">T</span><span class="p">)</span>
    <span class="n">price_arith</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">([</span><span class="n">disc</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">maximum</span><span class="p">(</span><span class="n">avgA</span> <span class="o">-</span> <span class="n">k</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">).</span><span class="n">mean</span><span class="p">()</span> <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="n">K_vec</span><span class="p">])</span>
    <span class="n">price_geom</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">([</span><span class="n">disc</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">maximum</span><span class="p">(</span><span class="n">avgG</span> <span class="o">-</span> <span class="n">k</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">).</span><span class="n">mean</span><span class="p">()</span> <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="n">K_vec</span><span class="p">])</span>
    <span class="k">return</span> <span class="n">price_arith</span><span class="p">,</span> <span class="n">price_geom</span>

<span class="c1"># sanity check the BS-MC engine against the closed-form KV geometric price
</span><span class="n">r_chk</span><span class="p">,</span> <span class="n">sigma_chk</span><span class="p">,</span> <span class="n">n_fix_chk</span> <span class="o">=</span> <span class="mf">0.036</span><span class="p">,</span> <span class="mf">0.28</span><span class="p">,</span> <span class="mi">12</span>
<span class="n">strikes_asian</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">arange</span><span class="p">(</span><span class="mi">4000</span><span class="p">,</span> <span class="mi">5001</span><span class="p">,</span> <span class="mi">200</span><span class="p">)</span>
<span class="n">_</span><span class="p">,</span> <span class="n">geom_mc</span> <span class="o">=</span> <span class="n">bs_mc_asian</span><span class="p">(</span><span class="mf">1.0</span><span class="p">,</span> <span class="n">r_chk</span><span class="p">,</span> <span class="n">sigma_chk</span><span class="p">,</span> <span class="n">strikes_asian</span><span class="p">,</span> <span class="n">n_fix_chk</span><span class="p">,</span> <span class="n">n_sims</span><span class="o">=</span><span class="mi">200_000</span><span class="p">)</span>
<span class="n">geom_cf</span> <span class="o">=</span> <span class="n">kv_geometric_call</span><span class="p">(</span><span class="n">S0</span><span class="p">,</span> <span class="n">strikes_asian</span><span class="p">,</span> <span class="n">r_chk</span><span class="p">,</span> <span class="n">sigma_chk</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="n">n_fix_chk</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"BS-MC vs closed-form Kemna-Vorst, max abs diff:"</span><span class="p">,</span> <span class="n">np</span><span class="p">.</span><span class="nb">max</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="nb">abs</span><span class="p">(</span><span class="n">geom_mc</span> <span class="o">-</span> <span class="n">geom_cf</span><span class="p">)))</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>BS-MC vs closed-form Kemna-Vorst, max abs diff: 0.5563267884775769
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">T_asian</span> <span class="o">=</span> <span class="p">[</span><span class="mf">0.25</span><span class="p">,</span> <span class="mf">0.50</span><span class="p">,</span> <span class="mf">0.75</span><span class="p">,</span> <span class="mf">0.9583</span><span class="p">]</span>

<span class="n">fig2</span><span class="p">,</span> <span class="n">axes2</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="n">subplots</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">T_asian</span><span class="p">),</span> <span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">16</span><span class="p">,</span> <span class="mi">4</span><span class="p">))</span>
<span class="k">for</span> <span class="n">ax</span><span class="p">,</span> <span class="n">T</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">axes2</span><span class="p">,</span> <span class="n">T_asian</span><span class="p">):</span>
    <span class="n">r</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">interp</span><span class="p">(</span><span class="n">T</span><span class="p">,</span> <span class="n">T_grid</span><span class="p">,</span> <span class="n">r_grid</span><span class="p">))</span>
    <span class="n">pool</span> <span class="o">=</span> <span class="n">pools</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">r</span><span class="p">)</span> <span class="ow">or</span> <span class="n">eq_pool</span><span class="p">(</span><span class="n">prices</span><span class="p">,</span> <span class="n">r</span><span class="o">=</span><span class="n">r</span><span class="p">,</span> <span class="n">dt</span><span class="o">=</span><span class="n">dt</span><span class="p">,</span> <span class="n">n_sims</span><span class="o">=</span><span class="mi">10_000</span><span class="p">,</span> <span class="n">seed</span><span class="o">=</span><span class="mi">123</span><span class="p">,</span> <span class="n">S0</span><span class="o">=</span><span class="n">S0</span><span class="p">)</span>
    <span class="n">iv</span> <span class="o">=</span> <span class="n">pool</span><span class="p">.</span><span class="n">ann_vol</span>
    <span class="n">n_fix</span> <span class="o">=</span> <span class="nb">max</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="nb">round</span><span class="p">(</span><span class="n">T</span> <span class="o">*</span> <span class="mi">12</span><span class="p">))</span>

    <span class="n">bsmc</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">bs_mc_asian</span><span class="p">(</span><span class="n">T</span><span class="p">,</span> <span class="n">r</span><span class="p">,</span> <span class="n">iv</span><span class="p">,</span> <span class="n">strikes_asian</span><span class="p">,</span> <span class="n">n_fix</span><span class="p">)</span>
    <span class="n">emp</span> <span class="o">=</span> <span class="n">eq_price_asian</span><span class="p">(</span><span class="n">pool</span><span class="p">,</span> <span class="n">T</span><span class="p">,</span> <span class="n">strikes_asian</span><span class="p">,</span> <span class="n">n_fix</span><span class="p">,</span> <span class="n">S0</span><span class="o">=</span><span class="n">S0</span><span class="p">)</span>

    <span class="n">ax</span><span class="p">.</span><span class="n">plot</span><span class="p">(</span><span class="n">strikes_asian</span><span class="p">,</span> <span class="n">bsmc</span><span class="p">,</span> <span class="s">"o--"</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="s">"steelblue"</span><span class="p">,</span> <span class="n">markerfacecolor</span><span class="o">=</span><span class="s">"none"</span><span class="p">,</span>
            <span class="n">label</span><span class="o">=</span><span class="s">"BS Monte Carlo"</span><span class="p">)</span>
    <span class="n">ax</span><span class="p">.</span><span class="n">plot</span><span class="p">(</span><span class="n">strikes_asian</span><span class="p">,</span> <span class="n">emp</span><span class="p">,</span> <span class="s">"o-"</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="s">"coral"</span><span class="p">,</span> <span class="n">label</span><span class="o">=</span><span class="s">"Empirical model"</span><span class="p">)</span>
    <span class="n">ax</span><span class="p">.</span><span class="n">set_title</span><span class="p">(</span><span class="sa">f</span><span class="s">"T=</span><span class="si">{</span><span class="n">T</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="n">f</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
    <span class="n">ax</span><span class="p">.</span><span class="n">set_xlabel</span><span class="p">(</span><span class="s">"Strike"</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">ax</span> <span class="ow">is</span> <span class="n">axes2</span><span class="p">[</span><span class="mi">0</span><span class="p">]:</span>
        <span class="n">ax</span><span class="p">.</span><span class="n">set_ylabel</span><span class="p">(</span><span class="s">"Asian call price"</span><span class="p">)</span>
        <span class="n">ax</span><span class="p">.</span><span class="n">legend</span><span class="p">(</span><span class="n">fontsize</span><span class="o">=</span><span class="mi">8</span><span class="p">)</span>
<span class="n">fig2</span><span class="p">.</span><span class="n">suptitle</span><span class="p">(</span><span class="s">"Empirical bootstrap vs Black-Scholes Monte Carlo, arithmetic Asian calls"</span><span class="p">)</span>
<span class="n">fig2</span><span class="p">.</span><span class="n">tight_layout</span><span class="p">()</span>
</code></pre></div></div>

<p><img src="/images/2026-09-21/2026-09-21-empirical-pricing-_16_0.png" alt="image-title-here" class="img-responsive" /></p>]]></content><author><name></name></author><category term="Python" /><summary type="html"><![CDATA[This post is a Python implementation of the study on semi-parametric option pricing based on underlying's historical data, accepted for presentation at the osQF 2026 conference.]]></summary></entry><entry><title type="html">Semi-parametric option pricing based on underlying’s historical data (accepted at the osQF 2026 (ex R/Finance) conference)</title><link href="https://thierrymoudiki.github.io/blog/2026/09/20/r/semi-parametric-pricing-osqf" rel="alternate" type="text/html" title="Semi-parametric option pricing based on underlying’s historical data (accepted at the osQF 2026 (ex R/Finance) conference)" /><published>2026-09-20T00:00:00+00:00</published><updated>2026-09-20T00:00:00+00:00</updated><id>https://thierrymoudiki.github.io/blog/2026/09/20/r/semi-parametric-pricing-osqf</id><content type="html" xml:base="https://thierrymoudiki.github.io/blog/2026/09/20/r/semi-parametric-pricing-osqf"><![CDATA[<p>This post is a follow-up to my previous posts on semi-parametric option pricing. A link to the study (accepted for presentation at the osQF 2026 conference) is provided at the end of this post.</p>

<p>In this study (R code provided), we build an empirical pricing measure for options directly from their underlying’s historical dynamics, requiring no market of option prices to calibrate against. Starting from a single observed discounted price series, we filter out linear predictability via an AR(1) fit and preserve the remaining dependence structure through a stationary block bootstrap of the residuals. The resulting empirical distribution is then given a minimal adjustment consistent with no-arbitrage: a single scalar correction, following Duan and Simonato [1998], that enforces the martingale condition required by the Fundamental Theorem of Asset Pricing (FTAP). We are explicit that this construction is not a recovery of “the” risk-neutral measure itself in the usual economic sense, but rather a pricing measure obtained by disturbing the historical dynamics as little as possible, to verify the FTAP. We validate the methodology empirically against the implied volatility surface of DAX index European options quoted July 5, 2002, comparing prices directly rather than implied volatilities. Because the construction requires no option-market input at any stage, it extends naturally to path-dependent payoffs; we illustrate this on arithmetic Asian options. 1 Motivation Standard option pricing practice requires the assumption of a parametric family for the option underlying’s dynamics (geometric Brownian motion [Black and Scholes, 1973], stochastic volatility [Heston, 1993], jump-diffusion [Merton, 1976]), and calibrating the parameters of that parametric family to observed option prices. This works well when a liquid market of option prices exists to calibrate against. It does not help when no such market exists, for examples for path-dependent or structured payoffs written on an underlying with no quoted option market (an Asian option on a private index, an embedded option inside an insurance product, a participation certificate). This note develops an alternative construction for option pricing that requires no market prices at all. It builds an empirical pricing measure directly from the historical dynamics of the underlying asset, and imposes no parametric distributional assumption beyond what the data itself exhibits. The construction is then made consistent with the Fundamental Theorem of Asset Pricing (FTAP). We first state the FTAP theorem, then show how it motivates the step of our pricing estimator, before turning to empirical validation.</p>

<p><a href="https://www.researchgate.net/publication/414513095_Semi-parametric_option_pricing_based_on_underlying's_historical_data">https://www.researchgate.net/publication/414513095_Semi-parametric_option_pricing_based_on_underlying’s_historical_data</a></p>

<p>I’m now interested in constructive remarks and feedback on the study, that will allow to enrich, improve and robustify the methodology.</p>

<p><img src="/images/2026-09-20/2026-09-20-image1.png" alt="image-title-here" class="img-responsive" /></p>]]></content><author><name></name></author><category term="R" /><summary type="html"><![CDATA[This post is a follow-up to my previous posts on semi-parametric option pricing. A link to the study (accepted for presentation at the osQF 2026 conference) is provided at the end of this post.]]></summary></entry><entry><title type="html">Model-agnostic prediction intervals in Python and R: does nnetsauce’s QuantileRegressor hold up?</title><link href="https://thierrymoudiki.github.io/blog/2026/09/14/r/python/quantileregressor-benchmark" rel="alternate" type="text/html" title="Model-agnostic prediction intervals in Python and R: does nnetsauce’s QuantileRegressor hold up?" /><published>2026-09-14T00:00:00+00:00</published><updated>2026-09-14T00:00:00+00:00</updated><id>https://thierrymoudiki.github.io/blog/2026/09/14/r/python/quantileregressor-benchmark</id><content type="html" xml:base="https://thierrymoudiki.github.io/blog/2026/09/14/r/python/quantileregressor-benchmark"><![CDATA[<h1 id="model-agnostic-prediction-intervals-in-python-and-r-does-nnetsauces-quantileregressor-hold-up">Model-agnostic prediction intervals in Python and R: does nnetsauce’s <code class="language-plaintext highlighter-rouge">QuantileRegressor</code> hold up?</h1>

<p>Point predictions tell you what a model <em>thinks</em> will happen. They don’t tell you how
much to trust that number.</p>

<p><a href="https://github.com/Techtonique/nnetsauce">nnetsauce</a>’s <code class="language-plaintext highlighter-rouge">QuantileRegressor</code> class
takes a different approach to this problem than most prediction-interval libraries:
instead of shipping one interval-producing algorithm, it takes <strong>any</strong> object with
<code class="language-plaintext highlighter-rouge">.fit()</code>/<code class="language-plaintext highlighter-rouge">.predict()</code> — linear model, SVR, random forest, whatever you already have —
and turns it into a full quantile machine by optimizing an offset around its point
predictions to minimize the pinball (quantile) loss. Five different “scoring”
strategies control how that offset is computed: <code class="language-plaintext highlighter-rouge">predictions</code>, <code class="language-plaintext highlighter-rouge">residuals</code>,
<code class="language-plaintext highlighter-rouge">conformal</code>, <code class="language-plaintext highlighter-rouge">studentized</code>, <code class="language-plaintext highlighter-rouge">conformal-studentized</code>.</p>

<h2 id="the-library-in-python-and-r">The library, in Python and R</h2>

<p>The same class is available in R, via
<a href="https://github.com/Techtonique/nnetsauce_r">nnetsauce_r</a> — and it’s worth noting
up front that it isn’t a separate reimplementation. The R function is a thin
<code class="language-plaintext highlighter-rouge">reticulate</code> wrapper that calls the identical Python object under the hood.
There is no separate R implementation to audit; auditing the Python source <em>is</em>
auditing the R behavior.</p>

<p><strong>Python</strong> (this is real, runnable code — see the cell below)</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">nnetsauce.quantile.quantileregression</span> <span class="kn">import</span> <span class="n">QuantileRegressor</span>

<span class="n">obj</span> <span class="o">=</span> <span class="n">QuantileRegressor</span><span class="p">(</span>
    <span class="n">obj</span><span class="o">=</span><span class="n">BayesianRidge</span><span class="p">(),</span>      <span class="c1"># any sklearn-compatible regressor
</span>    <span class="n">level</span><span class="o">=</span><span class="mi">95</span><span class="p">,</span>                 <span class="c1"># target coverage, in %
</span>    <span class="n">scoring</span><span class="o">=</span><span class="s">"residuals"</span><span class="p">,</span>      <span class="c1"># "predictions" | "residuals" | "conformal" |
</span>                               <span class="c1"># "studentized" | "conformal-studentized"
</span><span class="p">)</span>
<span class="n">obj</span><span class="p">.</span><span class="n">fit</span><span class="p">(</span><span class="n">X_train</span><span class="p">,</span> <span class="n">y_train</span><span class="p">)</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">obj</span><span class="p">.</span><span class="n">predict</span><span class="p">(</span><span class="n">X_test</span><span class="p">,</span> <span class="n">return_pi</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="c1"># result.mean, result.lower, result.median, result.upper
</span></code></pre></div></div>

<p><strong>R</strong> (calls into the exact same Python class via <code class="language-plaintext highlighter-rouge">reticulate</code>)</p>

<div class="language-r highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">library</span><span class="p">(</span><span class="n">datasets</span><span class="p">)</span><span class="w">
</span><span class="n">X</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">as.matrix</span><span class="p">(</span><span class="n">mtcars</span><span class="p">[,</span><span class="w"> </span><span class="m">-1</span><span class="p">]);</span><span class="w"> </span><span class="n">y</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">mtcars</span><span class="p">[,</span><span class="w"> </span><span class="m">1</span><span class="p">]</span><span class="w">

</span><span class="n">sklearn</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">nnetsauce</span><span class="o">::</span><span class="n">get_sklearn</span><span class="p">()</span><span class="w">
</span><span class="n">obj</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">sklearn</span><span class="o">$</span><span class="n">linear_model</span><span class="o">$</span><span class="n">BayesianRidge</span><span class="p">()</span><span class="w">

</span><span class="n">obj2</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">QuantileRegressor</span><span class="p">(</span><span class="n">obj</span><span class="p">,</span><span class="w"> </span><span class="n">level</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">95</span><span class="p">,</span><span class="w"> </span><span class="n">scoring</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"residuals"</span><span class="p">)</span><span class="w">
</span><span class="n">obj2</span><span class="o">$</span><span class="n">fit</span><span class="p">(</span><span class="n">X_train</span><span class="p">,</span><span class="w"> </span><span class="n">y_train</span><span class="p">)</span><span class="w">
</span><span class="n">print</span><span class="p">(</span><span class="n">obj2</span><span class="o">$</span><span class="n">score</span><span class="p">(</span><span class="n">X_test</span><span class="p">,</span><span class="w"> </span><span class="n">y_test</span><span class="p">))</span><span class="w">
</span></code></pre></div></div>

<p>Let’s install the library and try the quickstart for real.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Quickstart: real nnetsauce.PredictionInterval, straight from PyPI.
# (QuantileRegressor is reimplemented in the next section with a lighter
#  optimizer budget purely to keep this notebook's total runtime short --
#  see the note there for why that's a faithful substitution.)
</span><span class="kn">import</span> <span class="nn">sys</span><span class="p">,</span> <span class="n">subprocess</span>
<span class="n">subprocess</span><span class="p">.</span><span class="n">run</span><span class="p">([</span><span class="n">sys</span><span class="p">.</span><span class="n">executable</span><span class="p">,</span> <span class="s">"-m"</span><span class="p">,</span> <span class="s">"pip"</span><span class="p">,</span> <span class="s">"install"</span><span class="p">,</span> <span class="s">"-q"</span><span class="p">,</span> <span class="s">"nnetsauce"</span><span class="p">,</span>
                 <span class="s">"--break-system-packages"</span><span class="p">],</span> <span class="n">check</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>

<span class="kn">import</span> <span class="nn">warnings</span>
<span class="n">warnings</span><span class="p">.</span><span class="n">filterwarnings</span><span class="p">(</span><span class="s">"ignore"</span><span class="p">)</span>

<span class="kn">from</span> <span class="nn">nnetsauce.quantile.quantileregression</span> <span class="kn">import</span> <span class="n">QuantileRegressor</span>
<span class="kn">from</span> <span class="nn">nnetsauce.predictioninterval.predictioninterval</span> <span class="kn">import</span> <span class="n">PredictionInterval</span>
<span class="kn">from</span> <span class="nn">sklearn.linear_model</span> <span class="kn">import</span> <span class="n">BayesianRidge</span>
<span class="kn">from</span> <span class="nn">sklearn.datasets</span> <span class="kn">import</span> <span class="n">load_diabetes</span>
<span class="kn">from</span> <span class="nn">sklearn.model_selection</span> <span class="kn">import</span> <span class="n">train_test_split</span>
<span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>

<span class="n">X</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="n">load_diabetes</span><span class="p">(</span><span class="n">return_X_y</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="n">X_train</span><span class="p">,</span> <span class="n">X_test</span><span class="p">,</span> <span class="n">y_train</span><span class="p">,</span> <span class="n">y_test</span> <span class="o">=</span> <span class="n">train_test_split</span><span class="p">(</span><span class="n">X</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">test_size</span><span class="o">=</span><span class="mf">0.3</span><span class="p">,</span> <span class="n">random_state</span><span class="o">=</span><span class="mi">42</span><span class="p">)</span>

<span class="n">obj</span> <span class="o">=</span> <span class="n">QuantileRegressor</span><span class="p">(</span><span class="n">obj</span><span class="o">=</span><span class="n">BayesianRidge</span><span class="p">(),</span> <span class="n">level</span><span class="o">=</span><span class="mi">95</span><span class="p">,</span> <span class="n">scoring</span><span class="o">=</span><span class="s">"residuals"</span><span class="p">)</span>
<span class="n">obj</span><span class="p">.</span><span class="n">fit</span><span class="p">(</span><span class="n">X_train</span><span class="p">,</span> <span class="n">y_train</span><span class="p">)</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">obj</span><span class="p">.</span><span class="n">predict</span><span class="p">(</span><span class="n">X_test</span><span class="p">,</span> <span class="n">return_pi</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

<span class="n">coverage</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">mean</span><span class="p">((</span><span class="n">y_test</span> <span class="o">&gt;=</span> <span class="n">result</span><span class="p">.</span><span class="n">lower</span><span class="p">)</span> <span class="o">&amp;</span> <span class="p">(</span><span class="n">y_test</span> <span class="o">&lt;=</span> <span class="n">result</span><span class="p">.</span><span class="n">upper</span><span class="p">))</span>
<span class="k">print</span><span class="p">(</span><span class="s">"First 5 intervals:"</span><span class="p">)</span>
<span class="k">for</span> <span class="n">lo</span><span class="p">,</span> <span class="n">med</span><span class="p">,</span> <span class="n">hi</span><span class="p">,</span> <span class="n">true</span> <span class="ow">in</span> <span class="nb">list</span><span class="p">(</span><span class="nb">zip</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">lower</span><span class="p">,</span> <span class="n">result</span><span class="p">.</span><span class="n">median</span><span class="p">,</span> <span class="n">result</span><span class="p">.</span><span class="n">upper</span><span class="p">,</span> <span class="n">y_test</span><span class="p">))[:</span><span class="mi">5</span><span class="p">]:</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"  [</span><span class="si">{</span><span class="n">lo</span><span class="si">:</span><span class="mf">7.1</span><span class="n">f</span><span class="si">}</span><span class="s">, </span><span class="si">{</span><span class="n">hi</span><span class="si">:</span><span class="mf">7.1</span><span class="n">f</span><span class="si">}</span><span class="s">]   median=</span><span class="si">{</span><span class="n">med</span><span class="si">:</span><span class="mf">7.1</span><span class="n">f</span><span class="si">}</span><span class="s">   true=</span><span class="si">{</span><span class="n">true</span><span class="si">:</span><span class="mf">7.1</span><span class="n">f</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="se">\n</span><span class="s">Empirical coverage on the test set: </span><span class="si">{</span><span class="n">coverage</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="o">%</span><span class="si">}</span><span class="s"> (target: 95%)"</span><span class="p">)</span>

</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>First 5 intervals:
  [   35.9,   243.1]   median=  137.3   true=  219.0
  [   76.6,   283.8]   median=  177.9   true=   70.0
  [   27.8,   235.0]   median=  129.2   true=  202.0
  [  186.7,   394.0]   median=  288.1   true=  230.0
  [   18.6,   225.8]   median=  119.9   true=  111.0

Empirical coverage on the test set: 94.7% (target: 95%)
</code></pre></div></div>

<h2 id="benchmark-setup">Benchmark setup</h2>

<p>I ran a fairly large grid, deliberately <strong>without tuning any base estimator’s
hyperparameters</strong> — the point is to test the wrapper’s behavior “out of the box,”
the way most people would first try it:</p>

<ul>
  <li><strong>38 scikit-learn regressors</strong> — everything <code class="language-plaintext highlighter-rouge">sklearn.utils.all_estimators(type_filter='regressor')</code>
returns, minus meta-estimators that need extra wiring (stacking/voting/multi-output
regressors, the heavy default-tuned ensembles, etc.)</li>
  <li><strong>6 datasets</strong> — <code class="language-plaintext highlighter-rouge">diabetes</code>, <code class="language-plaintext highlighter-rouge">linnerud</code>, two synthetic sets (linear and mildly
nonlinear), an anonymized version of the classic Boston Housing dataset, and a
600-row subsample of California Housing</li>
  <li><strong>2 coverage targets</strong> — 80% and 95%</li>
  <li><strong>5 scoring strategies</strong> for <code class="language-plaintext highlighter-rouge">QuantileRegressor</code>, plus nnetsauce’s sibling class
<code class="language-plaintext highlighter-rouge">PredictionInterval</code> (<code class="language-plaintext highlighter-rouge">method="splitconformal"</code>) as a second, structurally
different baseline</li>
  <li><strong>Two “native” quantile baselines</strong> that don’t wrap anything: scikit-learn’s own
linear <code class="language-plaintext highlighter-rouge">QuantileRegressor</code> (pinball-loss minimization, no L1 penalty), and
<code class="language-plaintext highlighter-rouge">GradientBoostingRegressor(loss="quantile")</code></li>
</ul>

<p>That’s 2,736 individual model fits for the wrapped-estimator grid, plus 24 more
for the two native baselines. Let’s build it.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># ---------------------------------------------------------------------------
# Imports and metrics
# ---------------------------------------------------------------------------
</span><span class="kn">import</span> <span class="nn">time</span>
<span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span>
<span class="kn">from</span> <span class="nn">collections</span> <span class="kn">import</span> <span class="n">namedtuple</span>
<span class="kn">from</span> <span class="nn">sklearn.base</span> <span class="kn">import</span> <span class="n">BaseEstimator</span><span class="p">,</span> <span class="n">RegressorMixin</span><span class="p">,</span> <span class="n">clone</span>
<span class="kn">from</span> <span class="nn">sklearn.utils</span> <span class="kn">import</span> <span class="n">all_estimators</span>
<span class="kn">from</span> <span class="nn">scipy.optimize</span> <span class="kn">import</span> <span class="n">differential_evolution</span>
<span class="kn">from</span> <span class="nn">sklearn.datasets</span> <span class="kn">import</span> <span class="n">load_diabetes</span><span class="p">,</span> <span class="n">load_linnerud</span><span class="p">,</span> <span class="n">make_regression</span>
<span class="kn">from</span> <span class="nn">sklearn.model_selection</span> <span class="kn">import</span> <span class="n">train_test_split</span>
<span class="kn">from</span> <span class="nn">sklearn.linear_model</span> <span class="kn">import</span> <span class="n">QuantileRegressor</span> <span class="k">as</span> <span class="n">SkQuantileRegressor</span>
<span class="kn">from</span> <span class="nn">sklearn.ensemble</span> <span class="kn">import</span> <span class="n">GradientBoostingRegressor</span>
<span class="kn">import</span> <span class="nn">matplotlib.pyplot</span> <span class="k">as</span> <span class="n">plt</span>

<span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="n">seed</span><span class="p">(</span><span class="mi">42</span><span class="p">)</span>
<span class="n">LEVELS</span> <span class="o">=</span> <span class="p">[</span><span class="mi">80</span><span class="p">,</span> <span class="mi">95</span><span class="p">]</span>
<span class="n">SCORINGS</span> <span class="o">=</span> <span class="p">[</span><span class="s">"predictions"</span><span class="p">,</span> <span class="s">"residuals"</span><span class="p">,</span> <span class="s">"conformal"</span><span class="p">,</span> <span class="s">"studentized"</span><span class="p">,</span> <span class="s">"conformal-studentized"</span><span class="p">]</span>


<span class="k">def</span> <span class="nf">coverage_and_width</span><span class="p">(</span><span class="n">y_true</span><span class="p">,</span> <span class="n">lower</span><span class="p">,</span> <span class="n">upper</span><span class="p">):</span>
    <span class="n">covered</span> <span class="o">=</span> <span class="p">(</span><span class="n">y_true</span> <span class="o">&gt;=</span> <span class="n">lower</span><span class="p">)</span> <span class="o">&amp;</span> <span class="p">(</span><span class="n">y_true</span> <span class="o">&lt;=</span> <span class="n">upper</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">covered</span><span class="p">.</span><span class="n">mean</span><span class="p">(),</span> <span class="n">np</span><span class="p">.</span><span class="n">mean</span><span class="p">(</span><span class="n">upper</span> <span class="o">-</span> <span class="n">lower</span><span class="p">)</span>


<span class="k">def</span> <span class="nf">winkler_score</span><span class="p">(</span><span class="n">y_true</span><span class="p">,</span> <span class="n">lower</span><span class="p">,</span> <span class="n">upper</span><span class="p">,</span> <span class="n">level</span><span class="p">):</span>
    <span class="n">alpha</span> <span class="o">=</span> <span class="mi">1</span> <span class="o">-</span> <span class="n">level</span> <span class="o">/</span> <span class="mi">100</span>
    <span class="n">width</span> <span class="o">=</span> <span class="n">upper</span> <span class="o">-</span> <span class="n">lower</span>
    <span class="n">below</span> <span class="o">=</span> <span class="n">y_true</span> <span class="o">&lt;</span> <span class="n">lower</span>
    <span class="n">above</span> <span class="o">=</span> <span class="n">y_true</span> <span class="o">&gt;</span> <span class="n">upper</span>
    <span class="n">score</span> <span class="o">=</span> <span class="n">width</span><span class="p">.</span><span class="n">copy</span><span class="p">().</span><span class="n">astype</span><span class="p">(</span><span class="nb">float</span><span class="p">)</span>
    <span class="n">score</span><span class="p">[</span><span class="n">below</span><span class="p">]</span> <span class="o">+=</span> <span class="p">(</span><span class="mi">2</span> <span class="o">/</span> <span class="n">alpha</span><span class="p">)</span> <span class="o">*</span> <span class="p">(</span><span class="n">lower</span><span class="p">[</span><span class="n">below</span><span class="p">]</span> <span class="o">-</span> <span class="n">y_true</span><span class="p">[</span><span class="n">below</span><span class="p">])</span>
    <span class="n">score</span><span class="p">[</span><span class="n">above</span><span class="p">]</span> <span class="o">+=</span> <span class="p">(</span><span class="mi">2</span> <span class="o">/</span> <span class="n">alpha</span><span class="p">)</span> <span class="o">*</span> <span class="p">(</span><span class="n">y_true</span><span class="p">[</span><span class="n">above</span><span class="p">]</span> <span class="o">-</span> <span class="n">upper</span><span class="p">[</span><span class="n">above</span><span class="p">])</span>
    <span class="k">return</span> <span class="n">np</span><span class="p">.</span><span class="n">mean</span><span class="p">(</span><span class="n">score</span><span class="p">)</span>

<span class="k">print</span><span class="p">(</span><span class="s">"Metrics ready."</span><span class="p">)</span>

</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Metrics ready.
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="c1"># ---------------------------------------------------------------------------
# Datasets -- downloaded fresh so this notebook is fully reproducible standalone
# ---------------------------------------------------------------------------
</span><span class="kn">import</span> <span class="nn">urllib.request</span><span class="p">,</span> <span class="n">io</span>

<span class="k">def</span> <span class="nf">load_datasets</span><span class="p">():</span>
    <span class="n">datasets</span> <span class="o">=</span> <span class="p">{}</span>
    <span class="n">X</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="n">load_diabetes</span><span class="p">(</span><span class="n">return_X_y</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="n">datasets</span><span class="p">[</span><span class="s">"diabetes"</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">X</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span>

    <span class="n">lin</span> <span class="o">=</span> <span class="n">load_linnerud</span><span class="p">()</span>
    <span class="n">datasets</span><span class="p">[</span><span class="s">"linnerud (predict weight)"</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">lin</span><span class="p">.</span><span class="n">data</span><span class="p">,</span> <span class="n">lin</span><span class="p">.</span><span class="n">target</span><span class="p">[:,</span> <span class="mi">0</span><span class="p">])</span>

    <span class="n">Xs</span><span class="p">,</span> <span class="n">ys</span> <span class="o">=</span> <span class="n">make_regression</span><span class="p">(</span><span class="n">n_samples</span><span class="o">=</span><span class="mi">400</span><span class="p">,</span> <span class="n">n_features</span><span class="o">=</span><span class="mi">8</span><span class="p">,</span> <span class="n">noise</span><span class="o">=</span><span class="mf">15.0</span><span class="p">,</span> <span class="n">random_state</span><span class="o">=</span><span class="mi">42</span><span class="p">)</span>
    <span class="n">datasets</span><span class="p">[</span><span class="s">"synthetic_linear"</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">Xs</span><span class="p">,</span> <span class="n">ys</span><span class="p">)</span>

    <span class="n">Xn</span><span class="p">,</span> <span class="n">yn</span> <span class="o">=</span> <span class="n">make_regression</span><span class="p">(</span><span class="n">n_samples</span><span class="o">=</span><span class="mi">400</span><span class="p">,</span> <span class="n">n_features</span><span class="o">=</span><span class="mi">8</span><span class="p">,</span> <span class="n">noise</span><span class="o">=</span><span class="mf">10.0</span><span class="p">,</span> <span class="n">random_state</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
    <span class="n">yn</span> <span class="o">=</span> <span class="n">yn</span> <span class="o">+</span> <span class="mf">0.02</span> <span class="o">*</span> <span class="p">(</span><span class="n">Xn</span><span class="p">[:,</span> <span class="mi">0</span><span class="p">]</span> <span class="o">**</span> <span class="mi">3</span><span class="p">)</span>
    <span class="n">datasets</span><span class="p">[</span><span class="s">"synthetic_nonlinear"</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">Xn</span><span class="p">,</span> <span class="n">yn</span><span class="p">)</span>

    <span class="n">boston_url</span> <span class="o">=</span> <span class="s">"https://raw.githubusercontent.com/Techtonique/datasets/refs/heads/main/tabular/regression/boston_dataset2.csv"</span>
    <span class="n">boston</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">read_csv</span><span class="p">(</span><span class="n">boston_url</span><span class="p">)</span>
    <span class="n">Xb</span> <span class="o">=</span> <span class="n">boston</span><span class="p">.</span><span class="n">drop</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s">"target"</span><span class="p">,</span> <span class="s">"training_index"</span><span class="p">]).</span><span class="n">values</span>
    <span class="n">yb</span> <span class="o">=</span> <span class="n">boston</span><span class="p">[</span><span class="s">"target"</span><span class="p">].</span><span class="n">values</span>
    <span class="n">datasets</span><span class="p">[</span><span class="s">"boston_anonymized"</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">Xb</span><span class="p">,</span> <span class="n">yb</span><span class="p">)</span>

    <span class="n">housing_url</span> <span class="o">=</span> <span class="s">"https://raw.githubusercontent.com/alexeygrigorev/datasets/master/housing.csv"</span>
    <span class="n">housing</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">read_csv</span><span class="p">(</span><span class="n">housing_url</span><span class="p">).</span><span class="n">dropna</span><span class="p">()</span>
    <span class="n">housing</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">get_dummies</span><span class="p">(</span><span class="n">housing</span><span class="p">,</span> <span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s">"ocean_proximity"</span><span class="p">],</span> <span class="n">drop_first</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="n">rng</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="n">RandomState</span><span class="p">(</span><span class="mi">42</span><span class="p">)</span>
    <span class="n">idx</span> <span class="o">=</span> <span class="n">rng</span><span class="p">.</span><span class="n">choice</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">housing</span><span class="p">),</span> <span class="n">size</span><span class="o">=</span><span class="mi">600</span><span class="p">,</span> <span class="n">replace</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
    <span class="n">sub</span> <span class="o">=</span> <span class="n">housing</span><span class="p">.</span><span class="n">iloc</span><span class="p">[</span><span class="n">idx</span><span class="p">]</span>
    <span class="n">Xc</span> <span class="o">=</span> <span class="n">sub</span><span class="p">.</span><span class="n">drop</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s">"median_house_value"</span><span class="p">]).</span><span class="n">values</span><span class="p">.</span><span class="n">astype</span><span class="p">(</span><span class="nb">float</span><span class="p">)</span>
    <span class="n">yc</span> <span class="o">=</span> <span class="n">sub</span><span class="p">[</span><span class="s">"median_house_value"</span><span class="p">].</span><span class="n">values</span><span class="p">.</span><span class="n">astype</span><span class="p">(</span><span class="nb">float</span><span class="p">)</span>
    <span class="n">datasets</span><span class="p">[</span><span class="s">"california_housing (n=600 subsample)"</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">Xc</span><span class="p">,</span> <span class="n">yc</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">datasets</span>

<span class="n">DATASETS</span> <span class="o">=</span> <span class="n">load_datasets</span><span class="p">()</span>
<span class="k">for</span> <span class="n">name</span><span class="p">,</span> <span class="p">(</span><span class="n">X</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span> <span class="ow">in</span> <span class="n">DATASETS</span><span class="p">.</span><span class="n">items</span><span class="p">():</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">name</span><span class="si">:</span><span class="mi">45</span><span class="n">s</span><span class="si">}</span><span class="s">  X=</span><span class="si">{</span><span class="n">X</span><span class="p">.</span><span class="n">shape</span><span class="si">}</span><span class="s">  y=</span><span class="si">{</span><span class="n">y</span><span class="p">.</span><span class="n">shape</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>

</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>diabetes                                       X=(442, 10)  y=(442,)
linnerud (predict weight)                      X=(20, 3)  y=(20,)
synthetic_linear                               X=(400, 8)  y=(400,)
synthetic_nonlinear                            X=(400, 8)  y=(400,)
boston_anonymized                              X=(506, 13)  y=(506,)
california_housing (n=600 subsample)           X=(600, 12)  y=(600,)
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="c1"># ---------------------------------------------------------------------------
# Estimator list: every sklearn regressor, minus meta-estimators that need
# extra wiring (based on https://gist.github.com/thierrymoudiki/19a856e2d9c75d5b4fe57fa332b5e8c9)
# ---------------------------------------------------------------------------
</span><span class="n">SKIP_ESTIMATORS</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s">'MultiOutputRegressor'</span><span class="p">,</span> <span class="s">'MultiOutputClassifier'</span><span class="p">,</span> <span class="s">'StackingRegressor'</span><span class="p">,</span> <span class="s">'StackingClassifier'</span><span class="p">,</span>
    <span class="s">'VotingRegressor'</span><span class="p">,</span> <span class="s">'VotingClassifier'</span><span class="p">,</span> <span class="s">'TransformedTargetRegressor'</span><span class="p">,</span> <span class="s">'RegressorChain'</span><span class="p">,</span>
    <span class="s">'GradientBoostingRegressor'</span><span class="p">,</span> <span class="s">'HistGradientBoostingRegressor'</span><span class="p">,</span> <span class="s">'RandomForestRegressor'</span><span class="p">,</span>
    <span class="s">'ExtraTreesRegressor'</span><span class="p">,</span> <span class="s">'MLPRegressor'</span><span class="p">,</span>
    <span class="s">'MultiTaskLasso'</span><span class="p">,</span> <span class="s">'MultiTaskElasticNet'</span><span class="p">,</span> <span class="s">'MultiTaskLassoCV'</span><span class="p">,</span> <span class="s">'MultiTaskElasticNetCV'</span><span class="p">,</span>
    <span class="s">'IsotonicRegression'</span><span class="p">,</span> <span class="s">'CCA'</span><span class="p">,</span> <span class="s">'PLSCanonical'</span><span class="p">,</span> <span class="s">'RegressorMixin'</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">def</span> <span class="nf">get_estimators</span><span class="p">():</span>
    <span class="n">regs</span> <span class="o">=</span> <span class="n">all_estimators</span><span class="p">(</span><span class="n">type_filter</span><span class="o">=</span><span class="s">'regressor'</span><span class="p">)</span>
    <span class="n">out</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">name</span><span class="p">,</span> <span class="n">cls</span> <span class="ow">in</span> <span class="n">regs</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">name</span> <span class="ow">in</span> <span class="n">SKIP_ESTIMATORS</span><span class="p">:</span>
            <span class="k">continue</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="n">obj</span> <span class="o">=</span> <span class="n">cls</span><span class="p">()</span>  <span class="c1"># default hyperparameters only -- no tuning
</span>        <span class="k">except</span> <span class="nb">Exception</span><span class="p">:</span>
            <span class="k">continue</span>
        <span class="n">out</span><span class="p">.</span><span class="n">append</span><span class="p">((</span><span class="n">name</span><span class="p">,</span> <span class="n">obj</span><span class="p">))</span>
    <span class="k">return</span> <span class="n">out</span>

<span class="n">ESTIMATORS</span> <span class="o">=</span> <span class="n">get_estimators</span><span class="p">()</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">ESTIMATORS</span><span class="p">)</span><span class="si">}</span><span class="s"> estimators loaded:"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">", "</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">name</span> <span class="k">for</span> <span class="n">name</span><span class="p">,</span> <span class="n">_</span> <span class="ow">in</span> <span class="n">ESTIMATORS</span><span class="p">))</span>

</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>38 estimators loaded:
ARDRegression, AdaBoostRegressor, BaggingRegressor, BayesianRidge, DecisionTreeRegressor, DummyRegressor, ElasticNet, ElasticNetCV, ExtraTreeRegressor, GammaRegressor, GaussianProcessRegressor, HuberRegressor, KNeighborsRegressor, KernelRidge, Lars, LarsCV, Lasso, LassoCV, LassoLars, LassoLarsCV, LassoLarsIC, LinearRegression, LinearSVR, NuSVR, OrthogonalMatchingPursuit, OrthogonalMatchingPursuitCV, PLSRegression, PassiveAggressiveRegressor, PoissonRegressor, QuantileRegressor, RANSACRegressor, RadiusNeighborsRegressor, Ridge, RidgeCV, SGDRegressor, SVR, TheilSenRegressor, TweedieRegressor
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># ---------------------------------------------------------------------------
# Run the full sweep: 5 QuantileRegressor scoring modes + PredictionInterval,
# x 38 estimators x 6 datasets x 2 levels = 2,736 runs. Takes ~2-3 minutes.
# ---------------------------------------------------------------------------
</span><span class="kn">from</span> <span class="nn">nnetsauce.predictioninterval.predictioninterval</span> <span class="kn">import</span> <span class="n">PredictionInterval</span>
<span class="kn">from</span> <span class="nn">tqdm</span> <span class="kn">import</span> <span class="n">tqdm</span>

<span class="k">def</span> <span class="nf">run_one</span><span class="p">(</span><span class="n">method_name</span><span class="p">,</span> <span class="n">est</span><span class="p">,</span> <span class="n">X_train</span><span class="p">,</span> <span class="n">y_train</span><span class="p">,</span> <span class="n">X_test</span><span class="p">,</span> <span class="n">y_test</span><span class="p">,</span> <span class="n">level</span><span class="p">):</span>
    <span class="n">t0</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span>
    <span class="k">if</span> <span class="n">method_name</span><span class="p">.</span><span class="n">startswith</span><span class="p">(</span><span class="s">"QR:"</span><span class="p">):</span>
        <span class="n">scoring</span> <span class="o">=</span> <span class="n">method_name</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">":"</span><span class="p">,</span> <span class="mi">1</span><span class="p">)[</span><span class="mi">1</span><span class="p">]</span>
        <span class="n">model</span> <span class="o">=</span> <span class="n">QuantileRegressor</span><span class="p">(</span><span class="n">obj</span><span class="o">=</span><span class="n">clone</span><span class="p">(</span><span class="n">est</span><span class="p">),</span> <span class="n">level</span><span class="o">=</span><span class="n">level</span><span class="p">,</span> <span class="n">scoring</span><span class="o">=</span><span class="n">scoring</span><span class="p">)</span>
        <span class="n">model</span><span class="p">.</span><span class="n">fit</span><span class="p">(</span><span class="n">X_train</span><span class="p">,</span> <span class="n">y_train</span><span class="p">)</span>
        <span class="n">res</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="n">predict</span><span class="p">(</span><span class="n">X_test</span><span class="p">,</span> <span class="n">return_pi</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
        <span class="n">lower</span><span class="p">,</span> <span class="n">upper</span><span class="p">,</span> <span class="n">med</span> <span class="o">=</span> <span class="n">res</span><span class="p">.</span><span class="n">lower</span><span class="p">,</span> <span class="n">res</span><span class="p">.</span><span class="n">upper</span><span class="p">,</span> <span class="n">res</span><span class="p">.</span><span class="n">median</span>
    <span class="k">elif</span> <span class="n">method_name</span> <span class="o">==</span> <span class="s">"PredictionInterval"</span><span class="p">:</span>
        <span class="n">model</span> <span class="o">=</span> <span class="n">PredictionInterval</span><span class="p">(</span><span class="n">obj</span><span class="o">=</span><span class="n">clone</span><span class="p">(</span><span class="n">est</span><span class="p">),</span> <span class="n">method</span><span class="o">=</span><span class="s">"splitconformal"</span><span class="p">,</span> <span class="n">level</span><span class="o">=</span><span class="n">level</span><span class="p">)</span>
        <span class="n">model</span><span class="p">.</span><span class="n">fit</span><span class="p">(</span><span class="n">X_train</span><span class="p">,</span> <span class="n">y_train</span><span class="p">)</span>
        <span class="n">res</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="n">predict</span><span class="p">(</span><span class="n">X_test</span><span class="p">,</span> <span class="n">return_pi</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
        <span class="n">lower</span><span class="p">,</span> <span class="n">upper</span><span class="p">,</span> <span class="n">med</span> <span class="o">=</span> <span class="n">res</span><span class="p">.</span><span class="n">lower</span><span class="p">,</span> <span class="n">res</span><span class="p">.</span><span class="n">upper</span><span class="p">,</span> <span class="n">res</span><span class="p">.</span><span class="n">mean</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="k">raise</span> <span class="nb">ValueError</span><span class="p">(</span><span class="n">method_name</span><span class="p">)</span>

    <span class="n">cov</span><span class="p">,</span> <span class="n">width</span> <span class="o">=</span> <span class="n">coverage_and_width</span><span class="p">(</span><span class="n">y_test</span><span class="p">,</span> <span class="n">lower</span><span class="p">,</span> <span class="n">upper</span><span class="p">)</span>
    <span class="n">wink</span> <span class="o">=</span> <span class="n">winkler_score</span><span class="p">(</span><span class="n">y_test</span><span class="p">,</span> <span class="n">lower</span><span class="p">,</span> <span class="n">upper</span><span class="p">,</span> <span class="n">level</span><span class="o">=</span><span class="n">level</span><span class="p">)</span>
    <span class="n">mae</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">mean</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="nb">abs</span><span class="p">(</span><span class="n">y_test</span> <span class="o">-</span> <span class="n">med</span><span class="p">))</span>
    <span class="k">return</span> <span class="n">cov</span><span class="p">,</span> <span class="n">width</span><span class="p">,</span> <span class="n">wink</span><span class="p">,</span> <span class="n">mae</span><span class="p">,</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">t0</span>


<span class="n">methods</span> <span class="o">=</span> <span class="p">[</span><span class="sa">f</span><span class="s">"QR:</span><span class="si">{</span><span class="n">s</span><span class="si">}</span><span class="s">"</span> <span class="k">for</span> <span class="n">s</span> <span class="ow">in</span> <span class="n">SCORINGS</span><span class="p">]</span> <span class="o">+</span> <span class="p">[</span><span class="s">"PredictionInterval"</span><span class="p">]</span>
<span class="n">results</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">t_start</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span>

<span class="k">for</span> <span class="n">level</span> <span class="ow">in</span> <span class="n">LEVELS</span><span class="p">:</span>
    <span class="k">for</span> <span class="n">ds_name</span><span class="p">,</span> <span class="p">(</span><span class="n">X</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span> <span class="ow">in</span> <span class="n">tqdm</span><span class="p">(</span><span class="n">DATASETS</span><span class="p">.</span><span class="n">items</span><span class="p">()):</span>
        <span class="n">X_train</span><span class="p">,</span> <span class="n">X_test</span><span class="p">,</span> <span class="n">y_train</span><span class="p">,</span> <span class="n">y_test</span> <span class="o">=</span> <span class="n">train_test_split</span><span class="p">(</span><span class="n">X</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">test_size</span><span class="o">=</span><span class="mf">0.3</span><span class="p">,</span> <span class="n">random_state</span><span class="o">=</span><span class="mi">42</span><span class="p">)</span>
        <span class="k">for</span> <span class="n">est_name</span><span class="p">,</span> <span class="n">est</span> <span class="ow">in</span> <span class="n">ESTIMATORS</span><span class="p">:</span>
            <span class="k">for</span> <span class="n">method_name</span> <span class="ow">in</span> <span class="n">methods</span><span class="p">:</span>
                <span class="k">try</span><span class="p">:</span>
                    <span class="n">cov</span><span class="p">,</span> <span class="n">width</span><span class="p">,</span> <span class="n">wink</span><span class="p">,</span> <span class="n">mae</span><span class="p">,</span> <span class="n">elapsed</span> <span class="o">=</span> <span class="n">run_one</span><span class="p">(</span>
                        <span class="n">method_name</span><span class="p">,</span> <span class="n">est</span><span class="p">,</span> <span class="n">X_train</span><span class="p">,</span> <span class="n">y_train</span><span class="p">,</span> <span class="n">X_test</span><span class="p">,</span> <span class="n">y_test</span><span class="p">,</span> <span class="n">level</span>
                    <span class="p">)</span>
                    <span class="n">row</span> <span class="o">=</span> <span class="p">{</span><span class="s">"level"</span><span class="p">:</span> <span class="n">level</span><span class="p">,</span> <span class="s">"dataset"</span><span class="p">:</span> <span class="n">ds_name</span><span class="p">,</span> <span class="s">"estimator"</span><span class="p">:</span> <span class="n">est_name</span><span class="p">,</span> <span class="s">"method"</span><span class="p">:</span> <span class="n">method_name</span><span class="p">,</span>
                           <span class="s">"coverage"</span><span class="p">:</span> <span class="nb">round</span><span class="p">(</span><span class="n">cov</span><span class="p">,</span> <span class="mi">4</span><span class="p">),</span> <span class="s">"avg_interval_width"</span><span class="p">:</span> <span class="nb">round</span><span class="p">(</span><span class="n">width</span><span class="p">,</span> <span class="mi">4</span><span class="p">),</span>
                           <span class="s">"winkler_score"</span><span class="p">:</span> <span class="nb">round</span><span class="p">(</span><span class="n">wink</span><span class="p">,</span> <span class="mi">4</span><span class="p">),</span> <span class="s">"median_MAE"</span><span class="p">:</span> <span class="nb">round</span><span class="p">(</span><span class="n">mae</span><span class="p">,</span> <span class="mi">4</span><span class="p">),</span>
                           <span class="s">"time_s"</span><span class="p">:</span> <span class="nb">round</span><span class="p">(</span><span class="n">elapsed</span><span class="p">,</span> <span class="mi">3</span><span class="p">),</span> <span class="s">"status"</span><span class="p">:</span> <span class="s">"ok"</span><span class="p">}</span>
                <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
                    <span class="n">row</span> <span class="o">=</span> <span class="p">{</span><span class="s">"level"</span><span class="p">:</span> <span class="n">level</span><span class="p">,</span> <span class="s">"dataset"</span><span class="p">:</span> <span class="n">ds_name</span><span class="p">,</span> <span class="s">"estimator"</span><span class="p">:</span> <span class="n">est_name</span><span class="p">,</span> <span class="s">"method"</span><span class="p">:</span> <span class="n">method_name</span><span class="p">,</span>
                           <span class="s">"coverage"</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">nan</span><span class="p">,</span> <span class="s">"avg_interval_width"</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">nan</span><span class="p">,</span> <span class="s">"winkler_score"</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">nan</span><span class="p">,</span>
                           <span class="s">"median_MAE"</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">nan</span><span class="p">,</span> <span class="s">"time_s"</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">nan</span><span class="p">,</span> <span class="s">"status"</span><span class="p">:</span> <span class="sa">f</span><span class="s">"ERROR: </span><span class="si">{</span><span class="nb">str</span><span class="p">(</span><span class="n">e</span><span class="p">)[</span><span class="si">:</span><span class="mi">80</span><span class="p">]</span><span class="si">}</span><span class="s">"</span><span class="p">}</span>
                <span class="n">results</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">row</span><span class="p">)</span>

<span class="n">full_sweep</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">(</span><span class="n">results</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Done in </span><span class="si">{</span><span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span><span class="o">-</span><span class="n">t_start</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="si">}</span><span class="s">s. </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">full_sweep</span><span class="p">)</span><span class="si">}</span><span class="s"> rows, "</span>
      <span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">full_sweep</span><span class="p">[</span><span class="s">'status'</span><span class="p">].</span><span class="n">eq</span><span class="p">(</span><span class="s">'ok'</span><span class="p">).</span><span class="nb">sum</span><span class="p">()</span><span class="si">}</span><span class="s"> succeeded, </span><span class="si">{</span><span class="p">(</span><span class="o">~</span><span class="n">full_sweep</span><span class="p">[</span><span class="s">'status'</span><span class="p">].</span><span class="n">eq</span><span class="p">(</span><span class="s">'ok'</span><span class="p">)).</span><span class="nb">sum</span><span class="p">()</span><span class="si">}</span><span class="s"> failed."</span><span class="p">)</span>
<span class="n">full_sweep</span><span class="p">.</span><span class="n">to_csv</span><span class="p">(</span><span class="s">"full_sweep_results.csv"</span><span class="p">,</span> <span class="n">index</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>

</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>100%|██████████| 6/6 [05:06&lt;00:00, 51.11s/it]
100%|██████████| 6/6 [04:30&lt;00:00, 45.07s/it]

Done in 577.1s. 2736 rows, 2688 succeeded, 48 failed.
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="c1"># ---------------------------------------------------------------------------
# Native quantile baselines: sklearn's own linear QuantileRegressor, and
# GradientBoostingRegressor(loss="quantile") -- neither wraps another model.
# ---------------------------------------------------------------------------
</span><span class="k">def</span> <span class="nf">fit_predict_native_quantile</span><span class="p">(</span><span class="n">method</span><span class="p">,</span> <span class="n">X_train</span><span class="p">,</span> <span class="n">y_train</span><span class="p">,</span> <span class="n">X_test</span><span class="p">,</span> <span class="n">level</span><span class="p">):</span>
    <span class="n">low_q</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">level</span> <span class="o">/</span> <span class="mi">100</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span>
    <span class="n">high_q</span> <span class="o">=</span> <span class="mi">1</span> <span class="o">-</span> <span class="n">low_q</span>
    <span class="k">if</span> <span class="n">method</span> <span class="o">==</span> <span class="s">"sklearn_QuantileRegressor"</span><span class="p">:</span>
        <span class="n">m_low</span> <span class="o">=</span> <span class="n">SkQuantileRegressor</span><span class="p">(</span><span class="n">quantile</span><span class="o">=</span><span class="n">low_q</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span> <span class="n">solver</span><span class="o">=</span><span class="s">"highs"</span><span class="p">)</span>
        <span class="n">m_med</span> <span class="o">=</span> <span class="n">SkQuantileRegressor</span><span class="p">(</span><span class="n">quantile</span><span class="o">=</span><span class="mf">0.5</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span> <span class="n">solver</span><span class="o">=</span><span class="s">"highs"</span><span class="p">)</span>
        <span class="n">m_high</span> <span class="o">=</span> <span class="n">SkQuantileRegressor</span><span class="p">(</span><span class="n">quantile</span><span class="o">=</span><span class="n">high_q</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span> <span class="n">solver</span><span class="o">=</span><span class="s">"highs"</span><span class="p">)</span>
    <span class="k">elif</span> <span class="n">method</span> <span class="o">==</span> <span class="s">"GBM_quantile"</span><span class="p">:</span>
        <span class="n">m_low</span> <span class="o">=</span> <span class="n">GradientBoostingRegressor</span><span class="p">(</span><span class="n">loss</span><span class="o">=</span><span class="s">"quantile"</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="n">low_q</span><span class="p">,</span> <span class="n">random_state</span><span class="o">=</span><span class="mi">42</span><span class="p">)</span>
        <span class="n">m_med</span> <span class="o">=</span> <span class="n">GradientBoostingRegressor</span><span class="p">(</span><span class="n">loss</span><span class="o">=</span><span class="s">"quantile"</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.5</span><span class="p">,</span> <span class="n">random_state</span><span class="o">=</span><span class="mi">42</span><span class="p">)</span>
        <span class="n">m_high</span> <span class="o">=</span> <span class="n">GradientBoostingRegressor</span><span class="p">(</span><span class="n">loss</span><span class="o">=</span><span class="s">"quantile"</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="n">high_q</span><span class="p">,</span> <span class="n">random_state</span><span class="o">=</span><span class="mi">42</span><span class="p">)</span>
    <span class="n">m_low</span><span class="p">.</span><span class="n">fit</span><span class="p">(</span><span class="n">X_train</span><span class="p">,</span> <span class="n">y_train</span><span class="p">);</span> <span class="n">m_med</span><span class="p">.</span><span class="n">fit</span><span class="p">(</span><span class="n">X_train</span><span class="p">,</span> <span class="n">y_train</span><span class="p">);</span> <span class="n">m_high</span><span class="p">.</span><span class="n">fit</span><span class="p">(</span><span class="n">X_train</span><span class="p">,</span> <span class="n">y_train</span><span class="p">)</span>
    <span class="n">lower</span><span class="p">,</span> <span class="n">median</span><span class="p">,</span> <span class="n">upper</span> <span class="o">=</span> <span class="n">m_low</span><span class="p">.</span><span class="n">predict</span><span class="p">(</span><span class="n">X_test</span><span class="p">),</span> <span class="n">m_med</span><span class="p">.</span><span class="n">predict</span><span class="p">(</span><span class="n">X_test</span><span class="p">),</span> <span class="n">m_high</span><span class="p">.</span><span class="n">predict</span><span class="p">(</span><span class="n">X_test</span><span class="p">)</span>
    <span class="n">lower</span><span class="p">,</span> <span class="n">upper</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">minimum</span><span class="p">(</span><span class="n">lower</span><span class="p">,</span> <span class="n">upper</span><span class="p">),</span> <span class="n">np</span><span class="p">.</span><span class="n">maximum</span><span class="p">(</span><span class="n">lower</span><span class="p">,</span> <span class="n">upper</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">lower</span><span class="p">,</span> <span class="n">median</span><span class="p">,</span> <span class="n">upper</span>


<span class="n">native_results</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">level</span> <span class="ow">in</span> <span class="n">LEVELS</span><span class="p">:</span>
    <span class="k">for</span> <span class="n">ds_name</span><span class="p">,</span> <span class="p">(</span><span class="n">X</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span> <span class="ow">in</span> <span class="n">tqdm</span><span class="p">(</span><span class="n">DATASETS</span><span class="p">.</span><span class="n">items</span><span class="p">()):</span>
        <span class="n">X_train</span><span class="p">,</span> <span class="n">X_test</span><span class="p">,</span> <span class="n">y_train</span><span class="p">,</span> <span class="n">y_test</span> <span class="o">=</span> <span class="n">train_test_split</span><span class="p">(</span><span class="n">X</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">test_size</span><span class="o">=</span><span class="mf">0.3</span><span class="p">,</span> <span class="n">random_state</span><span class="o">=</span><span class="mi">42</span><span class="p">)</span>
        <span class="k">for</span> <span class="n">method</span> <span class="ow">in</span> <span class="p">[</span><span class="s">"sklearn_QuantileRegressor"</span><span class="p">,</span> <span class="s">"GBM_quantile"</span><span class="p">]:</span>
            <span class="n">t0</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span>
            <span class="n">lower</span><span class="p">,</span> <span class="n">median</span><span class="p">,</span> <span class="n">upper</span> <span class="o">=</span> <span class="n">fit_predict_native_quantile</span><span class="p">(</span><span class="n">method</span><span class="p">,</span> <span class="n">X_train</span><span class="p">,</span> <span class="n">y_train</span><span class="p">,</span> <span class="n">X_test</span><span class="p">,</span> <span class="n">level</span><span class="p">)</span>
            <span class="n">cov</span><span class="p">,</span> <span class="n">width</span> <span class="o">=</span> <span class="n">coverage_and_width</span><span class="p">(</span><span class="n">y_test</span><span class="p">,</span> <span class="n">lower</span><span class="p">,</span> <span class="n">upper</span><span class="p">)</span>
            <span class="n">wink</span> <span class="o">=</span> <span class="n">winkler_score</span><span class="p">(</span><span class="n">y_test</span><span class="p">,</span> <span class="n">lower</span><span class="p">,</span> <span class="n">upper</span><span class="p">,</span> <span class="n">level</span><span class="o">=</span><span class="n">level</span><span class="p">)</span>
            <span class="n">mae</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">mean</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="nb">abs</span><span class="p">(</span><span class="n">y_test</span> <span class="o">-</span> <span class="n">median</span><span class="p">))</span>
            <span class="n">native_results</span><span class="p">.</span><span class="n">append</span><span class="p">({</span><span class="s">"level"</span><span class="p">:</span> <span class="n">level</span><span class="p">,</span> <span class="s">"dataset"</span><span class="p">:</span> <span class="n">ds_name</span><span class="p">,</span> <span class="s">"method"</span><span class="p">:</span> <span class="n">method</span><span class="p">,</span>
                                    <span class="s">"coverage"</span><span class="p">:</span> <span class="n">cov</span><span class="p">,</span> <span class="s">"avg_interval_width"</span><span class="p">:</span> <span class="n">width</span><span class="p">,</span>
                                    <span class="s">"winkler_score"</span><span class="p">:</span> <span class="n">wink</span><span class="p">,</span> <span class="s">"median_MAE"</span><span class="p">:</span> <span class="n">mae</span><span class="p">,</span>
                                    <span class="s">"time_s"</span><span class="p">:</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">t0</span><span class="p">})</span>

<span class="n">native</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">(</span><span class="n">native_results</span><span class="p">)</span>
<span class="n">native</span><span class="p">.</span><span class="n">to_csv</span><span class="p">(</span><span class="s">"native_quantile_baselines.csv"</span><span class="p">,</span> <span class="n">index</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">native</span><span class="p">.</span><span class="n">groupby</span><span class="p">([</span><span class="s">"level"</span><span class="p">,</span> <span class="s">"method"</span><span class="p">])[</span><span class="s">"coverage"</span><span class="p">].</span><span class="n">agg</span><span class="p">([</span><span class="s">"mean"</span><span class="p">,</span> <span class="s">"median"</span><span class="p">,</span> <span class="s">"min"</span><span class="p">,</span> <span class="s">"max"</span><span class="p">]).</span><span class="nb">round</span><span class="p">(</span><span class="mi">3</span><span class="p">))</span>

</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>100%|██████████| 6/6 [00:18&lt;00:00,  3.16s/it]
100%|██████████| 6/6 [00:20&lt;00:00,  3.49s/it]

                                 mean  median  min  max
level method                                           
80    GBM_quantile               0.69    0.69 0.61 0.77
      sklearn_QuantileRegressor  0.76    0.77 0.67 0.80
95    GBM_quantile               0.92    0.92 0.86 1.00
      sklearn_QuantileRegressor  0.87    0.91 0.67 0.93
</code></pre></div></div>

<h2 id="headline-result-its-a-solid-technique-most-of-the-time">Headline result: it’s a solid technique, most of the time</h2>

<p>Restricting to the runs that didn’t misbehave (more on that below), let’s check
how often <code class="language-plaintext highlighter-rouge">QuantileRegressor</code>’s median coverage landed within 3 percentage points
of the target level, and compare method families head-to-head against the native
baselines.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="n">ok</span> <span class="o">=</span> <span class="n">full_sweep</span><span class="p">[</span><span class="n">full_sweep</span><span class="p">.</span><span class="n">status</span> <span class="o">==</span> <span class="s">"ok"</span><span class="p">].</span><span class="n">copy</span><span class="p">()</span>
<span class="n">ok</span><span class="p">[</span><span class="s">"collapsed"</span><span class="p">]</span> <span class="o">=</span> <span class="n">ok</span><span class="p">[</span><span class="s">"avg_interval_width"</span><span class="p">]</span> <span class="o">&lt;</span> <span class="mf">1e-6</span>

<span class="n">agg</span> <span class="o">=</span> <span class="n">ok</span><span class="p">.</span><span class="n">groupby</span><span class="p">([</span><span class="s">"level"</span><span class="p">,</span> <span class="s">"method"</span><span class="p">,</span> <span class="s">"estimator"</span><span class="p">]).</span><span class="n">agg</span><span class="p">(</span>
    <span class="n">mean_cov</span><span class="o">=</span><span class="p">(</span><span class="s">"coverage"</span><span class="p">,</span> <span class="s">"mean"</span><span class="p">),</span> <span class="n">median_cov</span><span class="o">=</span><span class="p">(</span><span class="s">"coverage"</span><span class="p">,</span> <span class="s">"median"</span><span class="p">),</span> <span class="n">min_cov</span><span class="o">=</span><span class="p">(</span><span class="s">"coverage"</span><span class="p">,</span> <span class="s">"min"</span><span class="p">),</span>
    <span class="n">n</span><span class="o">=</span><span class="p">(</span><span class="s">"coverage"</span><span class="p">,</span> <span class="s">"count"</span><span class="p">),</span> <span class="n">n_collapsed</span><span class="o">=</span><span class="p">(</span><span class="s">"collapsed"</span><span class="p">,</span> <span class="s">"sum"</span><span class="p">)</span>
<span class="p">).</span><span class="n">reset_index</span><span class="p">()</span>
<span class="n">agg</span><span class="p">.</span><span class="n">to_csv</span><span class="p">(</span><span class="s">"full_sweep_estimator_summary.csv"</span><span class="p">,</span> <span class="n">index</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>

<span class="n">qr</span> <span class="o">=</span> <span class="n">agg</span><span class="p">[</span><span class="n">agg</span><span class="p">.</span><span class="n">method</span><span class="p">.</span><span class="nb">str</span><span class="p">.</span><span class="n">startswith</span><span class="p">(</span><span class="s">"QR:"</span><span class="p">)]</span>
<span class="k">for</span> <span class="n">level</span> <span class="ow">in</span> <span class="n">LEVELS</span><span class="p">:</span>
    <span class="n">target</span> <span class="o">=</span> <span class="n">level</span> <span class="o">/</span> <span class="mi">100</span>
    <span class="n">sub</span> <span class="o">=</span> <span class="n">qr</span><span class="p">[(</span><span class="n">qr</span><span class="p">.</span><span class="n">level</span> <span class="o">==</span> <span class="n">level</span><span class="p">)</span> <span class="o">&amp;</span> <span class="p">(</span><span class="n">qr</span><span class="p">.</span><span class="n">n_collapsed</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)].</span><span class="n">copy</span><span class="p">()</span>
    <span class="n">sub</span><span class="p">[</span><span class="s">"gap"</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">sub</span><span class="p">[</span><span class="s">"median_cov"</span><span class="p">]</span> <span class="o">-</span> <span class="n">target</span><span class="p">).</span><span class="nb">abs</span><span class="p">()</span>
    <span class="n">good</span> <span class="o">=</span> <span class="n">sub</span><span class="p">[</span><span class="n">sub</span><span class="p">[</span><span class="s">"gap"</span><span class="p">]</span> <span class="o">&lt;=</span> <span class="mf">0.03</span><span class="p">]</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Level </span><span class="si">{</span><span class="n">level</span><span class="si">}</span><span class="s">%: </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">good</span><span class="p">)</span><span class="si">}</span><span class="s"> of </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">sub</span><span class="p">)</span><span class="si">}</span><span class="s"> non-collapsing (scoring, estimator) "</span>
          <span class="sa">f</span><span class="s">"pairs land within 3pts of target on median coverage (</span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">good</span><span class="p">)</span><span class="o">/</span><span class="nb">len</span><span class="p">(</span><span class="n">sub</span><span class="p">)</span><span class="si">:</span><span class="p">.</span><span class="mi">0</span><span class="o">%</span><span class="si">}</span><span class="s">)"</span><span class="p">)</span>

</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Level 80%: 126 of 173 non-collapsing (scoring, estimator) pairs land within 3pts of target on median coverage (73%)
Level 95%: 133 of 173 non-collapsing (scoring, estimator) pairs land within 3pts of target on median coverage (77%)
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Safe estimators: never collapsed under any QuantileRegressor scoring mode
</span><span class="n">never_collapse</span> <span class="o">=</span> <span class="n">qr</span><span class="p">.</span><span class="n">groupby</span><span class="p">(</span><span class="s">"estimator"</span><span class="p">)[</span><span class="s">"n_collapsed"</span><span class="p">].</span><span class="nb">sum</span><span class="p">()</span>
<span class="n">SAFE_ESTIMATORS</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">never_collapse</span><span class="p">[</span><span class="n">never_collapse</span> <span class="o">==</span> <span class="mi">0</span><span class="p">].</span><span class="n">index</span><span class="p">.</span><span class="n">tolist</span><span class="p">())</span>
<span class="n">RISKY_ESTIMATORS</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">never_collapse</span><span class="p">[</span><span class="n">never_collapse</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">].</span><span class="n">index</span><span class="p">.</span><span class="n">tolist</span><span class="p">())</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">SAFE_ESTIMATORS</span><span class="p">)</span><span class="si">}</span><span class="s"> estimators NEVER collapse under any QuantileRegressor scoring mode:"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">SAFE_ESTIMATORS</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="se">\n</span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">RISKY_ESTIMATORS</span><span class="p">)</span><span class="si">}</span><span class="s"> estimators collapse under at least one scoring mode/level/dataset:"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">RISKY_ESTIMATORS</span><span class="p">)</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>34 estimators NEVER collapse under any QuantileRegressor scoring mode:
['ARDRegression', 'BaggingRegressor', 'BayesianRidge', 'DummyRegressor', 'ElasticNet', 'ElasticNetCV', 'GammaRegressor', 'HuberRegressor', 'KNeighborsRegressor', 'KernelRidge', 'Lars', 'LarsCV', 'Lasso', 'LassoCV', 'LassoLars', 'LassoLarsCV', 'LassoLarsIC', 'LinearRegression', 'LinearSVR', 'NuSVR', 'OrthogonalMatchingPursuit', 'OrthogonalMatchingPursuitCV', 'PLSRegression', 'PassiveAggressiveRegressor', 'PoissonRegressor', 'QuantileRegressor', 'RANSACRegressor', 'RadiusNeighborsRegressor', 'Ridge', 'RidgeCV', 'SGDRegressor', 'SVR', 'TheilSenRegressor', 'TweedieRegressor']

4 estimators collapse under at least one scoring mode/level/dataset:
['AdaBoostRegressor', 'DecisionTreeRegressor', 'ExtraTreeRegressor', 'GaussianProcessRegressor']
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Method-family comparison: average coverage across the *safe* estimators only,
# so the trees/GP collapse doesn't distort the picture -- compared against the
# two native quantile baselines.
</span><span class="n">rows</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">level</span> <span class="ow">in</span> <span class="n">LEVELS</span><span class="p">:</span>
    <span class="k">for</span> <span class="n">method</span> <span class="ow">in</span> <span class="p">[</span><span class="s">"QR:residuals"</span><span class="p">,</span> <span class="s">"QR:conformal"</span><span class="p">,</span> <span class="s">"PredictionInterval"</span><span class="p">]:</span>
        <span class="n">sub</span> <span class="o">=</span> <span class="n">agg</span><span class="p">[(</span><span class="n">agg</span><span class="p">.</span><span class="n">level</span> <span class="o">==</span> <span class="n">level</span><span class="p">)</span> <span class="o">&amp;</span> <span class="p">(</span><span class="n">agg</span><span class="p">.</span><span class="n">method</span> <span class="o">==</span> <span class="n">method</span><span class="p">)</span> <span class="o">&amp;</span> <span class="p">(</span><span class="n">agg</span><span class="p">.</span><span class="n">estimator</span><span class="p">.</span><span class="n">isin</span><span class="p">(</span><span class="n">SAFE_ESTIMATORS</span><span class="p">))]</span>
        <span class="n">rows</span><span class="p">.</span><span class="n">append</span><span class="p">({</span><span class="s">"level"</span><span class="p">:</span> <span class="n">level</span><span class="p">,</span> <span class="s">"method"</span><span class="p">:</span> <span class="n">method</span><span class="p">,</span> <span class="s">"mean_cov"</span><span class="p">:</span> <span class="n">sub</span><span class="p">.</span><span class="n">mean_cov</span><span class="p">.</span><span class="n">mean</span><span class="p">()})</span>
    <span class="k">for</span> <span class="n">method</span> <span class="ow">in</span> <span class="p">[</span><span class="s">"sklearn_QuantileRegressor"</span><span class="p">,</span> <span class="s">"GBM_quantile"</span><span class="p">]:</span>
        <span class="n">sub</span> <span class="o">=</span> <span class="n">native</span><span class="p">[(</span><span class="n">native</span><span class="p">.</span><span class="n">level</span> <span class="o">==</span> <span class="n">level</span><span class="p">)</span> <span class="o">&amp;</span> <span class="p">(</span><span class="n">native</span><span class="p">.</span><span class="n">method</span> <span class="o">==</span> <span class="n">method</span><span class="p">)]</span>
        <span class="n">rows</span><span class="p">.</span><span class="n">append</span><span class="p">({</span><span class="s">"level"</span><span class="p">:</span> <span class="n">level</span><span class="p">,</span> <span class="s">"method"</span><span class="p">:</span> <span class="n">method</span><span class="p">,</span> <span class="s">"mean_cov"</span><span class="p">:</span> <span class="n">sub</span><span class="p">.</span><span class="n">coverage</span><span class="p">.</span><span class="n">mean</span><span class="p">()})</span>

<span class="n">family</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">(</span><span class="n">rows</span><span class="p">)</span>
<span class="n">family</span><span class="p">.</span><span class="n">to_csv</span><span class="p">(</span><span class="s">"method_family_comparison.csv"</span><span class="p">,</span> <span class="n">index</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>

<span class="n">labels_map</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s">"QR:residuals"</span><span class="p">:</span> <span class="s">"nnetsauce QuantileRegressor</span><span class="se">\n</span><span class="s">(residuals scoring)"</span><span class="p">,</span>
    <span class="s">"QR:conformal"</span><span class="p">:</span> <span class="s">"nnetsauce QuantileRegressor</span><span class="se">\n</span><span class="s">(conformal scoring)"</span><span class="p">,</span>
    <span class="s">"PredictionInterval"</span><span class="p">:</span> <span class="s">"nnetsauce PredictionInterval</span><span class="se">\n</span><span class="s">(splitconformal)"</span><span class="p">,</span>
    <span class="s">"sklearn_QuantileRegressor"</span><span class="p">:</span> <span class="s">"sklearn QuantileRegressor</span><span class="se">\n</span><span class="s">(native, linear)"</span><span class="p">,</span>
    <span class="s">"GBM_quantile"</span><span class="p">:</span> <span class="s">"GradientBoosting</span><span class="se">\n</span><span class="s">(native, quantile loss)"</span><span class="p">,</span>
<span class="p">}</span>
<span class="n">methods_order</span> <span class="o">=</span> <span class="p">[</span><span class="s">"QR:residuals"</span><span class="p">,</span> <span class="s">"QR:conformal"</span><span class="p">,</span> <span class="s">"PredictionInterval"</span><span class="p">,</span> <span class="s">"sklearn_QuantileRegressor"</span><span class="p">,</span> <span class="s">"GBM_quantile"</span><span class="p">]</span>
<span class="n">colors</span> <span class="o">=</span> <span class="p">[</span><span class="s">"#4C72B0"</span><span class="p">,</span> <span class="s">"#8CA8D8"</span><span class="p">,</span> <span class="s">"#DD8452"</span><span class="p">,</span> <span class="s">"#55A868"</span><span class="p">,</span> <span class="s">"#C44E52"</span><span class="p">]</span>

<span class="n">fig</span><span class="p">,</span> <span class="n">axes</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="n">subplots</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">11</span><span class="p">,</span> <span class="mf">4.5</span><span class="p">),</span> <span class="n">sharey</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="k">for</span> <span class="n">ax</span><span class="p">,</span> <span class="n">level</span><span class="p">,</span> <span class="n">target</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">axes</span><span class="p">,</span> <span class="n">LEVELS</span><span class="p">,</span> <span class="p">[</span><span class="n">l</span> <span class="o">/</span> <span class="mi">100</span> <span class="k">for</span> <span class="n">l</span> <span class="ow">in</span> <span class="n">LEVELS</span><span class="p">]):</span>
    <span class="n">sub</span> <span class="o">=</span> <span class="n">family</span><span class="p">[</span><span class="n">family</span><span class="p">.</span><span class="n">level</span> <span class="o">==</span> <span class="n">level</span><span class="p">].</span><span class="n">set_index</span><span class="p">(</span><span class="s">"method"</span><span class="p">).</span><span class="n">loc</span><span class="p">[</span><span class="n">methods_order</span><span class="p">]</span>
    <span class="n">ax</span><span class="p">.</span><span class="n">bar</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">methods_order</span><span class="p">)),</span> <span class="n">sub</span><span class="p">[</span><span class="s">"mean_cov"</span><span class="p">],</span> <span class="n">color</span><span class="o">=</span><span class="n">colors</span><span class="p">)</span>
    <span class="n">ax</span><span class="p">.</span><span class="n">axhline</span><span class="p">(</span><span class="n">target</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="s">"black"</span><span class="p">,</span> <span class="n">linestyle</span><span class="o">=</span><span class="s">"--"</span><span class="p">,</span> <span class="n">linewidth</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">label</span><span class="o">=</span><span class="sa">f</span><span class="s">"target (</span><span class="si">{</span><span class="nb">int</span><span class="p">(</span><span class="n">target</span><span class="o">*</span><span class="mi">100</span><span class="p">)</span><span class="si">}</span><span class="s">%)"</span><span class="p">)</span>
    <span class="n">ax</span><span class="p">.</span><span class="n">set_xticks</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">methods_order</span><span class="p">)))</span>
    <span class="n">ax</span><span class="p">.</span><span class="n">set_xticklabels</span><span class="p">([</span><span class="n">labels_map</span><span class="p">[</span><span class="n">m</span><span class="p">]</span> <span class="k">for</span> <span class="n">m</span> <span class="ow">in</span> <span class="n">methods_order</span><span class="p">],</span> <span class="n">rotation</span><span class="o">=</span><span class="mi">30</span><span class="p">,</span> <span class="n">ha</span><span class="o">=</span><span class="s">"right"</span><span class="p">,</span> <span class="n">fontsize</span><span class="o">=</span><span class="mi">8</span><span class="p">)</span>
    <span class="n">ax</span><span class="p">.</span><span class="n">set_title</span><span class="p">(</span><span class="sa">f</span><span class="s">"Target coverage: </span><span class="si">{</span><span class="nb">int</span><span class="p">(</span><span class="n">target</span><span class="o">*</span><span class="mi">100</span><span class="p">)</span><span class="si">}</span><span class="s">%"</span><span class="p">)</span>
    <span class="n">ax</span><span class="p">.</span><span class="n">set_ylim</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mf">1.05</span><span class="p">)</span>
    <span class="n">ax</span><span class="p">.</span><span class="n">legend</span><span class="p">(</span><span class="n">fontsize</span><span class="o">=</span><span class="mi">8</span><span class="p">,</span> <span class="n">loc</span><span class="o">=</span><span class="s">"lower right"</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">set_ylabel</span><span class="p">(</span><span class="s">"Mean empirical coverage"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="n">tight_layout</span><span class="p">()</span>
<span class="n">plt</span><span class="p">.</span><span class="n">savefig</span><span class="p">(</span><span class="s">"coverage_comparison.png"</span><span class="p">,</span> <span class="n">dpi</span><span class="o">=</span><span class="mi">150</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="n">show</span><span class="p">()</span>

<span class="k">print</span><span class="p">(</span><span class="n">family</span><span class="p">.</span><span class="nb">round</span><span class="p">(</span><span class="mi">3</span><span class="p">).</span><span class="n">to_string</span><span class="p">(</span><span class="n">index</span><span class="o">=</span><span class="bp">False</span><span class="p">))</span>
</code></pre></div></div>

<p><img src="/images/2026-09-14/2026-09-14-quantileregressor-benchmark_13_0.png" alt="image-title-here" class="img-responsive" /></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> level                    method  mean_cov
    80              QR:residuals      0.77
    80              QR:conformal      0.73
    80        PredictionInterval      0.81
    80 sklearn_QuantileRegressor      0.76
    80              GBM_quantile      0.69
    95              QR:residuals      0.92
    95              QR:conformal      0.86
    95        PredictionInterval      0.95
    95 sklearn_QuantileRegressor      0.87
    95              GBM_quantile      0.92
</code></pre></div></div>

<p>A few things stand out:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">PredictionInterval</code> (splitconformal) was the best-calibrated method</strong> in our
grid, at both targets. It computes a single calibration-residual quantile and
never re-optimizes against data the model has already seen, which turns out to
matter a lot (see next section).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">QuantileRegressor</code> with <code class="language-plaintext highlighter-rouge">scoring="residuals"</code></strong> essentially matches
scikit-learn’s own native linear <code class="language-plaintext highlighter-rouge">QuantileRegressor</code> at the 80% target and
clearly beats it at the 95% target (where the native version was dragged down
by the 20-row <code class="language-plaintext highlighter-rouge">linnerud</code> dataset).</li>
  <li><strong>Gradient-boosted quantile regression</strong> undercovered the most at the 80%
target — likely because with unregularized default hyperparameters,
<code class="language-plaintext highlighter-rouge">GradientBoostingRegressor</code> starts overfitting each quantile individually
rather than producing a coherent interval. It did comparatively better at 95%.</li>
</ul>

<p>The takeaway isn’t “throw away purpose-built quantile regressors.” It’s that a
model-agnostic wrapper around an off-the-shelf regressor is a <em>competitive</em>
alternative when you want interval estimates from a model family that doesn’t
have a native quantile-loss variant (say, a Support Vector Regressor, or a
Bayesian linear model) — and it costs nothing extra to try.</p>

<h2 id="the-catch-it-fails-predictably-and-only-for-one-kind-of-model">The catch: it fails predictably, and only for one kind of model</h2>

<p>Here’s the part worth being careful about. Let’s find every run where the
predicted interval collapsed to (near) zero width.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">collapsed</span> <span class="o">=</span> <span class="n">ok</span><span class="p">[</span><span class="n">ok</span><span class="p">.</span><span class="n">avg_interval_width</span> <span class="o">&lt;</span> <span class="mf">1e-6</span><span class="p">]</span>
<span class="n">piv</span> <span class="o">=</span> <span class="n">collapsed</span><span class="p">[</span><span class="n">collapsed</span><span class="p">.</span><span class="n">method</span> <span class="o">!=</span> <span class="s">"PredictionInterval"</span><span class="p">].</span><span class="n">pivot_table</span><span class="p">(</span>
    <span class="n">index</span><span class="o">=</span><span class="s">"estimator"</span><span class="p">,</span> <span class="n">columns</span><span class="o">=</span><span class="s">"method"</span><span class="p">,</span> <span class="n">values</span><span class="o">=</span><span class="s">"coverage"</span><span class="p">,</span> <span class="n">aggfunc</span><span class="o">=</span><span class="s">"count"</span><span class="p">,</span> <span class="n">fill_value</span><span class="o">=</span><span class="mi">0</span>
<span class="p">)</span>
<span class="n">risky_order</span> <span class="o">=</span> <span class="p">[</span><span class="n">e</span> <span class="k">for</span> <span class="n">e</span> <span class="ow">in</span> <span class="p">[</span><span class="s">"DecisionTreeRegressor"</span><span class="p">,</span> <span class="s">"ExtraTreeRegressor"</span><span class="p">,</span>
                            <span class="s">"GaussianProcessRegressor"</span><span class="p">,</span> <span class="s">"AdaBoostRegressor"</span><span class="p">]</span> <span class="k">if</span> <span class="n">e</span> <span class="ow">in</span> <span class="n">piv</span><span class="p">.</span><span class="n">index</span><span class="p">]</span>
<span class="n">piv</span> <span class="o">=</span> <span class="n">piv</span><span class="p">.</span><span class="n">reindex</span><span class="p">(</span><span class="n">risky_order</span><span class="p">).</span><span class="n">fillna</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
<span class="n">methods_order2</span> <span class="o">=</span> <span class="p">[</span><span class="sa">f</span><span class="s">"QR:</span><span class="si">{</span><span class="n">s</span><span class="si">}</span><span class="s">"</span> <span class="k">for</span> <span class="n">s</span> <span class="ow">in</span> <span class="n">SCORINGS</span><span class="p">]</span>
<span class="n">piv</span> <span class="o">=</span> <span class="n">piv</span><span class="p">[</span><span class="n">methods_order2</span><span class="p">]</span>
<span class="k">print</span><span class="p">(</span><span class="n">piv</span><span class="p">.</span><span class="n">astype</span><span class="p">(</span><span class="nb">int</span><span class="p">))</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="se">\n</span><span class="s">PredictionInterval collapsed count (out of </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">ok</span><span class="p">[</span><span class="n">ok</span><span class="p">.</span><span class="n">method</span><span class="o">==</span><span class="s">'PredictionInterval'</span><span class="p">])</span><span class="si">}</span><span class="s">):"</span><span class="p">,</span>
      <span class="nb">len</span><span class="p">(</span><span class="n">ok</span><span class="p">[(</span><span class="n">ok</span><span class="p">.</span><span class="n">method</span> <span class="o">==</span> <span class="s">"PredictionInterval"</span><span class="p">)</span> <span class="o">&amp;</span> <span class="p">(</span><span class="n">ok</span><span class="p">.</span><span class="n">avg_interval_width</span> <span class="o">&lt;</span> <span class="mf">1e-6</span><span class="p">)]))</span>

</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>method                    QR:predictions  QR:residuals  QR:conformal  \
estimator                                                              
DecisionTreeRegressor                 12            12            12   
ExtraTreeRegressor                    12            12            12   
GaussianProcessRegressor              10             9             9   
AdaBoostRegressor                      0             0             2   

method                    QR:studentized  QR:conformal-studentized  
estimator                                                           
DecisionTreeRegressor                 12                        12  
ExtraTreeRegressor                    12                        12  
GaussianProcessRegressor               9                         9  
AdaBoostRegressor                      0                         2  

PredictionInterval collapsed count (out of 448): 0
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">fig</span><span class="p">,</span> <span class="n">ax</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="n">subplots</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">8</span><span class="p">,</span> <span class="mf">4.5</span><span class="p">))</span>
<span class="n">bottom</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">zeros</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">piv</span><span class="p">))</span>
<span class="n">set2</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="n">cm</span><span class="p">.</span><span class="n">Set2</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">linspace</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">methods_order2</span><span class="p">)))</span>
<span class="k">for</span> <span class="n">m</span><span class="p">,</span> <span class="n">c</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">methods_order2</span><span class="p">,</span> <span class="n">set2</span><span class="p">):</span>
    <span class="n">ax</span><span class="p">.</span><span class="n">bar</span><span class="p">(</span><span class="n">piv</span><span class="p">.</span><span class="n">index</span><span class="p">,</span> <span class="n">piv</span><span class="p">[</span><span class="n">m</span><span class="p">],</span> <span class="n">bottom</span><span class="o">=</span><span class="n">bottom</span><span class="p">,</span> <span class="n">label</span><span class="o">=</span><span class="n">m</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="n">c</span><span class="p">)</span>
    <span class="n">bottom</span> <span class="o">+=</span> <span class="n">piv</span><span class="p">[</span><span class="n">m</span><span class="p">].</span><span class="n">values</span>
<span class="n">ax</span><span class="p">.</span><span class="n">set_ylabel</span><span class="p">(</span><span class="s">"# collapsed runs (out of 12 = 6 datasets x 2 levels)"</span><span class="p">)</span>
<span class="n">ax</span><span class="p">.</span><span class="n">set_title</span><span class="p">(</span><span class="s">"QuantileRegressor: zero-width interval collapse, by base estimator and scoring mode"</span><span class="p">)</span>
<span class="n">ax</span><span class="p">.</span><span class="n">legend</span><span class="p">(</span><span class="n">fontsize</span><span class="o">=</span><span class="mi">8</span><span class="p">,</span> <span class="n">ncol</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="n">xticks</span><span class="p">(</span><span class="n">rotation</span><span class="o">=</span><span class="mi">15</span><span class="p">,</span> <span class="n">ha</span><span class="o">=</span><span class="s">"right"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="n">tight_layout</span><span class="p">()</span>
<span class="n">plt</span><span class="p">.</span><span class="n">savefig</span><span class="p">(</span><span class="s">"collapse_chart.png"</span><span class="p">,</span> <span class="n">dpi</span><span class="o">=</span><span class="mi">150</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="n">show</span><span class="p">()</span>
</code></pre></div></div>

<p><img src="/images/2026-09-14/2026-09-14-quantileregressor-benchmark_17_0.png" alt="image-title-here" class="img-responsive" /></p>

<p><code class="language-plaintext highlighter-rouge">DecisionTreeRegressor</code> and <code class="language-plaintext highlighter-rouge">ExtraTreeRegressor</code> collapsed on <strong>every single
run</strong> (12 out of 12 — all 6 datasets, both levels), regardless of which of the 5
scoring strategies was used. <code class="language-plaintext highlighter-rouge">GaussianProcessRegressor</code> collapsed on 9–10 out of
12 runs. <code class="language-plaintext highlighter-rouge">AdaBoostRegressor</code> collapsed occasionally, only under the two
<code class="language-plaintext highlighter-rouge">conformal*</code> scoring modes.</p>

<p>The mechanism is the same in every case, and it isn’t specific to any one
scoring strategy — it happens because <code class="language-plaintext highlighter-rouge">QuantileRegressor</code> optimizes its
interval-width multiplier by minimizing pinball loss <strong>on data the base model
has already been fit on</strong> (either the full training set, for
<code class="language-plaintext highlighter-rouge">predictions</code>/<code class="language-plaintext highlighter-rouge">residuals</code>/<code class="language-plaintext highlighter-rouge">studentized</code>, or a calibration split the model gets
re-fit to, for <code class="language-plaintext highlighter-rouge">conformal</code>/<code class="language-plaintext highlighter-rouge">conformal-studentized</code>). An unconstrained decision
tree, or a Gaussian process with a noiseless kernel, can memorize that data
almost perfectly. Once training residuals are ~0, the optimizer correctly
notices that a zero-width interval already achieves close to the minimum
possible pinball loss — and it collapses the interval accordingly, regardless
of whether the scale factor being multiplied is a residual standard deviation,
a prediction magnitude, or the target’s own standard deviation.</p>

<p>The practical rule this suggests: <strong>don’t pair <code class="language-plaintext highlighter-rouge">QuantileRegressor</code> with base
estimators capable of near-perfect interpolation of their own fitting data</strong> —
unconstrained trees and noiseless GPs, specifically. Every other estimator we
tested — the entire linear family, <code class="language-plaintext highlighter-rouge">SVR</code>/<code class="language-plaintext highlighter-rouge">NuSVR</code>/<code class="language-plaintext highlighter-rouge">LinearSVR</code>, <code class="language-plaintext highlighter-rouge">KernelRidge</code>,
<code class="language-plaintext highlighter-rouge">KNeighborsRegressor</code>, <code class="language-plaintext highlighter-rouge">RANSACRegressor</code>, <code class="language-plaintext highlighter-rouge">TheilSenRegressor</code>, the GLMs,
<code class="language-plaintext highlighter-rouge">Bagging</code>, <code class="language-plaintext highlighter-rouge">PassiveAggressiveRegressor</code>, <code class="language-plaintext highlighter-rouge">SGDRegressor</code> — <strong>never collapsed
once</strong>, across any scoring mode, dataset, or coverage level.</p>

<h2 id="best-performers-at-each-target-level">Best performers at each target level</h2>

<p>Within the safe estimators, here’s what came closest to nominal coverage
(median across the 6 datasets), along with the worst-case dataset for each —
because a good median can still hide a bad outlier, and we don’t want to bury
that in an average.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="n">level</span> <span class="ow">in</span> <span class="n">LEVELS</span><span class="p">:</span>
    <span class="n">target</span> <span class="o">=</span> <span class="n">level</span> <span class="o">/</span> <span class="mi">100</span>
    <span class="n">sub</span> <span class="o">=</span> <span class="n">qr</span><span class="p">[(</span><span class="n">qr</span><span class="p">.</span><span class="n">level</span> <span class="o">==</span> <span class="n">level</span><span class="p">)</span> <span class="o">&amp;</span> <span class="p">(</span><span class="n">qr</span><span class="p">.</span><span class="n">n_collapsed</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)].</span><span class="n">copy</span><span class="p">()</span>
    <span class="n">sub</span><span class="p">[</span><span class="s">"gap"</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">sub</span><span class="p">[</span><span class="s">"median_cov"</span><span class="p">]</span> <span class="o">-</span> <span class="n">target</span><span class="p">).</span><span class="nb">abs</span><span class="p">()</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"=== </span><span class="si">{</span><span class="n">level</span><span class="si">}</span><span class="s">% target: top 5 by |median coverage - target| ==="</span><span class="p">)</span>
    <span class="n">top5</span> <span class="o">=</span> <span class="n">sub</span><span class="p">.</span><span class="n">sort_values</span><span class="p">(</span><span class="s">"gap"</span><span class="p">).</span><span class="n">head</span><span class="p">(</span><span class="mi">5</span><span class="p">)[[</span><span class="s">"method"</span><span class="p">,</span> <span class="s">"estimator"</span><span class="p">,</span> <span class="s">"mean_cov"</span><span class="p">,</span> <span class="s">"median_cov"</span><span class="p">,</span> <span class="s">"min_cov"</span><span class="p">]]</span>
    <span class="k">print</span><span class="p">(</span><span class="n">top5</span><span class="p">.</span><span class="nb">round</span><span class="p">(</span><span class="mi">3</span><span class="p">).</span><span class="n">to_string</span><span class="p">(</span><span class="n">index</span><span class="o">=</span><span class="bp">False</span><span class="p">))</span>
    <span class="k">print</span><span class="p">()</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>=== 80% target: top 5 by |median coverage - target| ===
                  method                  estimator  mean_cov  median_cov  min_cov
            QR:conformal              BayesianRidge      0.77        0.80     0.67
            QR:conformal                KernelRidge      0.76        0.80     0.50
            QR:conformal                       Lars      0.75        0.80     0.50
QR:conformal-studentized              BayesianRidge      0.77        0.80     0.67
          QR:studentized PassiveAggressiveRegressor      0.82        0.80     0.74

=== 95% target: top 5 by |median coverage - target| ===
        method        estimator  mean_cov  median_cov  min_cov
  QR:residuals       ElasticNet      0.93        0.95     0.83
QR:studentized       ElasticNet      0.93        0.95     0.83
  QR:residuals TweedieRegressor      0.94        0.95     0.83
  QR:conformal    BayesianRidge      0.91        0.95     0.67
  QR:residuals    PLSRegression      0.93        0.95     0.83
</code></pre></div></div>

<p>The worst-case column (<code class="language-plaintext highlighter-rouge">min_cov</code>) is a useful sanity check: even the
best-calibrated combinations here have at least one dataset where coverage
drops well below target — usually <code class="language-plaintext highlighter-rouge">linnerud</code>, which has only 6 test
observations after the split, so treat those specific numbers as noisy rather
than damning. It’s a reminder that “good on average” and “reliable everywhere”
are different claims, worth checking separately rather than folding into one
number.</p>

<h2 id="practical-guidance">Practical guidance</h2>

<p>Putting it together, if you’re deciding how to get prediction intervals out of
an arbitrary scikit-learn (or R, via <code class="language-plaintext highlighter-rouge">nnetsauce_r</code>) regressor:</p>

<ol>
  <li><strong>Default to <code class="language-plaintext highlighter-rouge">scoring="residuals"</code></strong> over <code class="language-plaintext highlighter-rouge">"conformal"</code> if you want the
simplest behavior — it doesn’t need a train/calibration split, and it
performed as well or better in this benchmark. <code class="language-plaintext highlighter-rouge">"studentized"</code> and
<code class="language-plaintext highlighter-rouge">"conformal-studentized"</code> tracked their non-studentized siblings closely
enough (coverage correlation ≥0.96 in the full grid) that they add little
beyond redundancy.</li>
  <li><strong>Reach for <code class="language-plaintext highlighter-rouge">PredictionInterval(method="splitconformal")</code></strong> if you want the
most robust option and don’t need the flexibility of the 5 scoring
strategies — it was the best-calibrated method here and never collapsed on
any estimator.</li>
  <li><strong>Avoid pairing either wrapper with unconstrained trees or noiseless
Gaussian processes.</strong> If you need a tree-based interval,
<code class="language-plaintext highlighter-rouge">RandomForestRegressor</code>, <code class="language-plaintext highlighter-rouge">BaggingRegressor</code>, or <code class="language-plaintext highlighter-rouge">AdaBoostRegressor</code> (mostly)
sidestep the issue since they don’t interpolate the data as tightly as a
single unconstrained tree.</li>
  <li><strong>Check coverage on a genuine holdout, per use case, before trusting the
number</strong> — “good on average across 6 datasets” is a benchmarking
convenience, not a guarantee for your specific one.</li>
</ol>

<h2 id="reproducibility">Reproducibility</h2>

<p>Every run in this notebook used default hyperparameters — no <code class="language-plaintext highlighter-rouge">GridSearchCV</code>, no
manual tuning — so the numbers reflect what you’d see trying this out of the
box. Re-running this notebook end-to-end reproduces every table and chart
above from scratch (data is downloaded fresh at runtime); the intermediate
CSVs (<code class="language-plaintext highlighter-rouge">full_sweep_results.csv</code>, <code class="language-plaintext highlighter-rouge">full_sweep_estimator_summary.csv</code>,
<code class="language-plaintext highlighter-rouge">native_quantile_baselines.csv</code>, <code class="language-plaintext highlighter-rouge">method_family_comparison.csv</code>) are also
written to disk if you want to explore further without re-running the sweep.</p>

<p><em>Benchmarked: nnetsauce <code class="language-plaintext highlighter-rouge">QuantileRegressor</code> and <code class="language-plaintext highlighter-rouge">PredictionInterval</code>
(<a href="https://github.com/Techtonique/nnetsauce/blob/master/nnetsauce/quantile/quantileregression.py">source</a>,
<a href="https://raw.githubusercontent.com/Techtonique/nnetsauce/refs/heads/master/nnetsauce/predictioninterval/predictioninterval.py">source</a>),
the R wrapper
(<a href="https://github.com/Techtonique/nnetsauce_r/blob/main/R/quantileregressor.R">source</a>),
against scikit-learn’s <code class="language-plaintext highlighter-rouge">QuantileRegressor</code> and <code class="language-plaintext highlighter-rouge">GradientBoostingRegressor(loss="quantile")</code>.</em></p>]]></content><author><name></name></author><category term="R" /><category term="Python" /><summary type="html"><![CDATA[Point predictions tell you what a model *thinks* will happen. They don't tell you how much to trust that number. nnetsauce's QuantileRegressor takes any sklearn-compatible regressor and turns it into a full quantile machine by optimizing an offset around its point predictions to minimize the pinball (quantile) loss.]]></summary></entry><entry><title type="html">ahead (Time Series Forecasting with uncertainty quantification) gets a lot faster to install: most dependencies are now optional</title><link href="https://thierrymoudiki.github.io/blog/2026/09/08/r/python/ahead-light" rel="alternate" type="text/html" title="ahead (Time Series Forecasting with uncertainty quantification) gets a lot faster to install: most dependencies are now optional" /><published>2026-09-08T00:00:00+00:00</published><updated>2026-09-08T00:00:00+00:00</updated><id>https://thierrymoudiki.github.io/blog/2026/09/08/r/python/ahead-light</id><content type="html" xml:base="https://thierrymoudiki.github.io/blog/2026/09/08/r/python/ahead-light"><![CDATA[<p>If you’ve used <code class="language-plaintext highlighter-rouge">ahead</code> before, you may remember <code class="language-plaintext highlighter-rouge">install.packages("ahead")</code>
or <code class="language-plaintext highlighter-rouge">pip install ahead</code> taking a while, since it dragged in two dozen
modeling packages up front regardless of which one you actually planned to
use. That’s fixed now.</p>

<h2 id="the-problem-one-functions-dependency-everyones-install-time">The problem: one function’s dependency, everyone’s install time</h2>

<p><code class="language-plaintext highlighter-rouge">ahead</code> supports a lot of forecasting methods, and each one can lean on a
different modeling package: <code class="language-plaintext highlighter-rouge">forecast</code>, <code class="language-plaintext highlighter-rouge">randomForest</code>, <code class="language-plaintext highlighter-rouge">e1071</code>, <code class="language-plaintext highlighter-rouge">glmnet</code>,
<code class="language-plaintext highlighter-rouge">gam</code>, <code class="language-plaintext highlighter-rouge">quantreg</code>, <code class="language-plaintext highlighter-rouge">vars</code>, <code class="language-plaintext highlighter-rouge">fGarch</code>, <code class="language-plaintext highlighter-rouge">VineCopula</code>, <code class="language-plaintext highlighter-rouge">mboost</code>, <code class="language-plaintext highlighter-rouge">ranger</code>,
<code class="language-plaintext highlighter-rouge">ForecastComb</code>. Historically, most of these were plain <code class="language-plaintext highlighter-rouge">Imports</code>
in <code class="language-plaintext highlighter-rouge">DESCRIPTION</code>, which means R installs <em>all</em> of them before you can even
load the package — regardless of whether you plan to use <code class="language-plaintext highlighter-rouge">ahead::dynrmf</code> with a
random forest or <code class="language-plaintext highlighter-rouge">ahead::ridge2f</code> for a multivariate model with none of the above.</p>

<h2 id="the-fix-almost-everything-becomes-a-suggests">The fix: (almost) everything becomes a Suggests</h2>

<p>The new <code class="language-plaintext highlighter-rouge">DESCRIPTION</code> keeps only what the package’s core code actually
needs at load time:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Imports:
    Rcpp (&gt;= 1.0.6),
    foreach,
    tseries
Depends: R (&gt;= 3.5.0)
Suggests:
    caret, cclust, dfoptim, doSNOW, doParallel, knitr, rmarkdown,
    testthat, fpp2, glmnet, e1071, gam, quantreg, randomForest,
    spatial, vars, roxygen2, ForecastComb, ranger, mboost, misc,
    Mcomp, fGarch, VineCopula, forecast (&gt;= 8.0),
    ggplot2 (&gt;= 3.0.0), randtoolbox (&gt;= 1.17), simulatetimeseries
</code></pre></div></div>

<p>Everything else — <code class="language-plaintext highlighter-rouge">randomForest</code>, <code class="language-plaintext highlighter-rouge">e1071</code>, <code class="language-plaintext highlighter-rouge">forecast</code>, <code class="language-plaintext highlighter-rouge">glmnet</code>, <code class="language-plaintext highlighter-rouge">vars</code>,
<code class="language-plaintext highlighter-rouge">fGarch</code>, <code class="language-plaintext highlighter-rouge">VineCopula</code>, and the rest — moved to <code class="language-plaintext highlighter-rouge">Suggests</code>. That means R
installs in seconds, because <code class="language-plaintext highlighter-rouge">Rcpp</code>, <code class="language-plaintext highlighter-rouge">foreach</code>, and <code class="language-plaintext highlighter-rouge">tseries</code> are the only
hard requirements.</p>

<h2 id="installing-the-missing-piece-exactly-when-you-need-it">Installing the missing piece, exactly when you need it</h2>

<p>Moving packages to <code class="language-plaintext highlighter-rouge">Suggests</code> only helps if the package still works
smoothly when you actually call a function that needs one of them. That’s
what a small internal helper, <code class="language-plaintext highlighter-rouge">check_suggested()</code>, takes care of:</p>

<div class="language-r highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">check_suggested</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="k">function</span><span class="p">(</span><span class="n">pkg</span><span class="p">,</span><span class="w"> </span><span class="n">ask</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">interactive</span><span class="p">())</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">requireNamespace</span><span class="p">(</span><span class="n">pkg</span><span class="p">,</span><span class="w"> </span><span class="n">quietly</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">TRUE</span><span class="p">))</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nf">return</span><span class="p">(</span><span class="nf">invisible</span><span class="p">(</span><span class="kc">TRUE</span><span class="p">))</span><span class="w">
  </span><span class="p">}</span><span class="w">
  </span><span class="n">do_install</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="kc">TRUE</span><span class="w">
  </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">ask</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">do_install</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">utils</span><span class="o">::</span><span class="n">askYesNo</span><span class="p">(</span><span class="w">
      </span><span class="n">sprintf</span><span class="p">(</span><span class="s2">"Package '%s' is required but not installed. Install it now?"</span><span class="p">,</span><span class="w"> </span><span class="n">pkg</span><span class="p">)</span><span class="w">
    </span><span class="p">)</span><span class="w">
    </span><span class="n">do_install</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">isTRUE</span><span class="p">(</span><span class="n">do_install</span><span class="p">)</span><span class="w">
  </span><span class="p">}</span><span class="w">
  </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">do_install</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">utils</span><span class="o">::</span><span class="n">install.packages</span><span class="p">(</span><span class="w">
      </span><span class="n">pkg</span><span class="p">,</span><span class="w">
      </span><span class="n">repos</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">c</span><span class="p">(</span><span class="s2">"https://techtonique.r-universe.dev"</span><span class="p">,</span><span class="w"> </span><span class="s2">"https://cloud.r-project.org"</span><span class="p">)</span><span class="w">
    </span><span class="p">)</span><span class="w">
  </span><span class="p">}</span><span class="w">
  </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="n">requireNamespace</span><span class="p">(</span><span class="n">pkg</span><span class="p">,</span><span class="w"> </span><span class="n">quietly</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">TRUE</span><span class="p">))</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">stop</span><span class="p">(</span><span class="w">
      </span><span class="n">sprintf</span><span class="p">(</span><span class="w">
        </span><span class="s2">"Package '%s' is required. Install it with install.packages('%s', repos = c('https://techtonique.r-universe.dev', 'https://cloud.r-project.org'))."</span><span class="p">,</span><span class="w">
        </span><span class="n">pkg</span><span class="p">,</span><span class="w"> </span><span class="n">pkg</span><span class="w">
      </span><span class="p">),</span><span class="w">
      </span><span class="n">call.</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">FALSE</span><span class="w">
    </span><span class="p">)</span><span class="w">
  </span><span class="p">}</span><span class="w">
  </span><span class="nf">invisible</span><span class="p">(</span><span class="kc">TRUE</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Every function (hopefully all of them!) that relies on a suggested package now calls
<code class="language-plaintext highlighter-rouge">check_suggested("that_package")</code> first. In practice, this means:</p>

<ul>
  <li>If the package is already installed, nothing changes — no prompt, no
delay.</li>
  <li>If it’s missing and you’re in an interactive session, you get asked
before anything is installed on your behalf.</li>
  <li>If it’s missing and the session isn’t interactive (a script, a CI job),
it installs automatically.</li>
  <li>If the install still fails, you get a clear error with the exact command
to run, pointing at both CRAN and Techtonique’s r-universe repository —
useful since a couple of these dependencies (like <code class="language-plaintext highlighter-rouge">ForecastComb</code> and
<code class="language-plaintext highlighter-rouge">misc</code>) aren’t on CRAN at all.</li>
</ul>

<p>So <code class="language-plaintext highlighter-rouge">install.packages("ahead")</code> is fast, and the first time you call
<code class="language-plaintext highlighter-rouge">dynrmf(..., fit_func = randomForest::randomForest)</code>, <code class="language-plaintext highlighter-rouge">randomForest</code> gets
installed for you, once, and never again after that.</p>

<h2 id="same-idea-carried-into-the-python-wrapper">Same idea, carried into the Python wrapper</h2>

<p><code class="language-plaintext highlighter-rouge">ahead</code>’s Python package is a wrapper around the R package (via <code class="language-plaintext highlighter-rouge">rpy2</code>), so
the first call to any forecaster has always installed R-side dependencies
on demand — that’s the “might take some time, but ONLY the 1st time it’s
called” comment you’ll see next to <code class="language-plaintext highlighter-rouge">DynamicRegressor</code> or <code class="language-plaintext highlighter-rouge">Ridge2Regressor</code>
in the examples. With the R package’s own install now lean, that first-call
overhead on the Python side shrinks too: there’s less to pull in before
<code class="language-plaintext highlighter-rouge">check_suggested()</code> even gets to the package your chosen method needs.</p>

<pre><code class="language-notebook-python">import os
import numpy as np
import pandas as pd
from ahead import DynamicRegressor, EAT
from time import time

# Forecasting horizon
h = 25

# Data frame containing the time series
df = pd.read_csv("https://raw.githubusercontent.com/Techtonique/datasets/refs/heads/main/time_series/univariate/AirPassengers.csv").set_index('date')
df.index = pd.DatetimeIndex(df.index)
print(df)

# univariate ts forecasting
print("Example 1 -----")
d1 = DynamicRegressor(h=h, date_formatting="ms")
print(d1.__module__)

start = time()
d1.forecast(df)
print(f"Elapsed: {time()-start} \n")
print("averages: \n")
print(d1.averages_)
print("\n")
print("ranges: \n")
print(d1.ranges_)
print("\n")

print("Example 2 -----")
d2 = DynamicRegressor(h=h, type_pi="T", date_formatting="original")
start = time()
d2.forecast(df)
print(f"Elapsed: {time()-start} \n")
print("averages: \n")
print(d2.averages_)
print("\n")
print("ranges: \n")
print(d2.ranges_)
print("\n")

d2.plot()
</code></pre>

<p>The first call to <code class="language-plaintext highlighter-rouge">DynamicRegressor.forecast()</code> in a fresh environment is
the one that pays the (now much smaller) one-time cost of installing the R
<code class="language-plaintext highlighter-rouge">ahead</code> package; every call after that — including <code class="language-plaintext highlighter-rouge">d2</code> above, in the same
session — runs at full speed.</p>

<h2 id="why-this-matters-in-practice">Why this matters in practice</h2>

<ul>
  <li><strong>Faster CI and Docker builds</strong> — you’re not compiling <code class="language-plaintext highlighter-rouge">gam</code>, <code class="language-plaintext highlighter-rouge">fGarch</code>,
<code class="language-plaintext highlighter-rouge">VineCopula</code>, and a dozen others just to run a couple of unit tests that
use two of them.</li>
  <li><strong>Less fragile installs</strong> — a slow-to-compile or awkward-to-build modeling
package no longer stands between you and a working <code class="language-plaintext highlighter-rouge">ahead</code> install if you
don’t even need that method.</li>
  <li><strong>Smaller footprint for the methods you actually use</strong> — if you only ever
call <code class="language-plaintext highlighter-rouge">ridge2f</code>, you never install the GARCH or copula-based dependencies
at all.</li>
</ul>

<p>If you want everything available up front (for example, to prepare an
offline environment), you can still install every suggested package
yourself:</p>

<div class="language-r highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">install.packages</span><span class="p">(</span><span class="w">
  </span><span class="nf">c</span><span class="p">(</span><span class="s2">"caret"</span><span class="p">,</span><span class="w"> </span><span class="s2">"cclust"</span><span class="p">,</span><span class="w"> </span><span class="s2">"dfoptim"</span><span class="p">,</span><span class="w"> </span><span class="s2">"doSNOW"</span><span class="p">,</span><span class="w"> </span><span class="s2">"doParallel"</span><span class="p">,</span><span class="w"> </span><span class="s2">"fpp2"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"glmnet"</span><span class="p">,</span><span class="w"> </span><span class="s2">"e1071"</span><span class="p">,</span><span class="w"> </span><span class="s2">"gam"</span><span class="p">,</span><span class="w"> </span><span class="s2">"quantreg"</span><span class="p">,</span><span class="w"> </span><span class="s2">"randomForest"</span><span class="p">,</span><span class="w"> </span><span class="s2">"spatial"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"vars"</span><span class="p">,</span><span class="w"> </span><span class="s2">"ranger"</span><span class="p">,</span><span class="w"> </span><span class="s2">"mboost"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Mcomp"</span><span class="p">,</span><span class="w"> </span><span class="s2">"fGarch"</span><span class="p">,</span><span class="w"> </span><span class="s2">"VineCopula"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"forecast"</span><span class="p">,</span><span class="w"> </span><span class="s2">"ggplot2"</span><span class="p">,</span><span class="w"> </span><span class="s2">"randtoolbox"</span><span class="p">,</span><span class="w"> </span><span class="s2">"simulatetimeseries"</span><span class="p">),</span><span class="w">
  </span><span class="n">repos</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">c</span><span class="p">(</span><span class="s2">"https://techtonique.r-universe.dev"</span><span class="p">,</span><span class="w"> </span><span class="s2">"https://cloud.r-project.org"</span><span class="p">)</span><span class="w">
</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<h2 id="get-it">Get it</h2>

<ul>
  <li>R: see the <a href="https://github.com/Techtonique/ahead">README</a> for the Techtonique repo setup, version <code class="language-plaintext highlighter-rouge">0.38.1</code>.</li>
  <li>Python: <code class="language-plaintext highlighter-rouge">pip install ahead --verbose</code>, from <a href="https://github.com/Techtonique/ahead_python">ahead_python</a>.</li>
</ul>

<p>As always, issues and contributions are welcome on both repos.</p>

<p><img src="/images/2025-09-01/2025-09-01-external-regressors-in-dynrmf_1_0.png" alt="image-title-here" class="img-responsive" /></p>]]></content><author><name></name></author><category term="R" /><category term="Python" /><summary type="html"><![CDATA[ahead (R) 0.38.1 and its Python wrapper now install in a fraction of the time, by moving almost every heavy modeling dependency from Imports to Suggests and installing them at runtime, only when a function actually needs them.]]></summary></entry><entry><title type="html">Skip the R/Python runtime: fast tabular dashboards with Observable Framework</title><link href="https://thierrymoudiki.github.io/blog/2026/08/31/python/r/javascript/observable-framework" rel="alternate" type="text/html" title="Skip the R/Python runtime: fast tabular dashboards with Observable Framework" /><published>2026-08-31T00:00:00+00:00</published><updated>2026-08-31T00:00:00+00:00</updated><id>https://thierrymoudiki.github.io/blog/2026/08/31/python/r/javascript/observable-framework</id><content type="html" xml:base="https://thierrymoudiki.github.io/blog/2026/08/31/python/r/javascript/observable-framework"><![CDATA[<p>If you’ve built dashboards with <strong>Shiny</strong> or <strong>Streamlit</strong>, you know the pattern: a Python or R
process runs on a server, holds your data in memory, and re-executes app logic on every
interaction. That’s powerful, but it also means the app is only as fast as its backend.</p>

<p><strong>Observable Framework</strong> (and I’m not being paid for saying what I say in this post) takes a 
different approach. Instead of running R/Python at <em>request</em>
time, it could run your data pipeline once at <em>build</em> time, then ship a fully static site — HTML,
JS, and pre-processed data files. No backend process serves the page; the browser does the
rendering. With a bit of JavaScript, you get dashboards that load like a static webpage because,
after build, that’s exactly what they are.</p>

<p>The full source code is available at
<a href="https://github.com/thierrymoudiki/tips-dashboard">github.com/thierrymoudiki/tips-dashboard</a>.</p>

<h2 id="the-core-building-blocks">The core building blocks</h2>

<p><strong>1. Data loaders run once, at build time.</strong></p>

<p>Any file under <code class="language-plaintext highlighter-rouge">src/data/</code> named <code class="language-plaintext highlighter-rouge">&lt;name&gt;.&lt;ext&gt;.py</code> (or <code class="language-plaintext highlighter-rouge">.R</code>, <code class="language-plaintext highlighter-rouge">.js</code>, <code class="language-plaintext highlighter-rouge">.sh</code>, etc.) is executed
during <code class="language-plaintext highlighter-rouge">npm run dev</code> / <code class="language-plaintext highlighter-rouge">npm run build</code>, and its stdout becomes a static data file:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">sys</span><span class="p">,</span> <span class="n">pandas</span> <span class="k">as</span> <span class="n">pd</span>

<span class="n">df</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">read_csv</span><span class="p">(</span><span class="n">SOURCE_URL</span><span class="p">)</span>
<span class="n">df</span><span class="p">[</span><span class="s">"tip_pct"</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">df</span><span class="p">[</span><span class="s">"tip"</span><span class="p">]</span> <span class="o">/</span> <span class="n">df</span><span class="p">[</span><span class="s">"total_bill"</span><span class="p">]</span> <span class="o">*</span> <span class="mi">100</span><span class="p">).</span><span class="nb">round</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
<span class="n">df</span><span class="p">.</span><span class="n">to_csv</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">stdout</span><span class="p">,</span> <span class="n">index</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
</code></pre></div></div>

<p>Pandas (or R, or a database query) can do the heavy lifting <em>once</em>. 
The browser never runs Python — it just fetches the resulting CSV/Parquet file.</p>

<p><strong>2. Pages are Markdown with embedded reactive JavaScript.</strong></p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">tips</span> <span class="o">=</span> <span class="nx">FileAttachment</span><span class="p">(</span><span class="dl">"</span><span class="s2">data/tips.csv</span><span class="dl">"</span><span class="p">).</span><span class="nx">csv</span><span class="p">({</span><span class="na">typed</span><span class="p">:</span> <span class="kc">true</span><span class="p">});</span>
</code></pre></div></div>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">filtered</span> <span class="o">=</span> <span class="nx">tips</span><span class="p">.</span><span class="nx">filter</span><span class="p">((</span><span class="nx">d</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">day</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">d</span><span class="p">.</span><span class="nx">day</span><span class="p">));</span>
</code></pre></div></div>

<p>Every JS code block is reactive: change an input, and every block referencing it re-runs
automatically — no manual event wiring, no full-page reload.</p>

<p><strong>3. Inputs drive state, Generators expose it as a reactive value.</strong></p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">dayInput</span> <span class="o">=</span> <span class="nx">Inputs</span><span class="p">.</span><span class="nx">checkbox</span><span class="p">([</span><span class="dl">"</span><span class="s2">Thur</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">Fri</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">Sat</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">Sun</span><span class="dl">"</span><span class="p">],</span> <span class="p">{</span><span class="na">value</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">Thur</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">Fri</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">Sat</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">Sun</span><span class="dl">"</span><span class="p">]});</span>
<span class="kd">const</span> <span class="nx">day</span> <span class="o">=</span> <span class="nx">Generators</span><span class="p">.</span><span class="nx">input</span><span class="p">(</span><span class="nx">dayInput</span><span class="p">);</span>
</code></pre></div></div>

<p><strong>4. Observable Plot renders charts declaratively</strong>, similar in spirit to ggplot2’s grammar of
graphics, but native to JS:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">Plot</span><span class="p">.</span><span class="nx">plot</span><span class="p">({</span>
  <span class="na">marks</span><span class="p">:</span> <span class="p">[</span>
    <span class="nx">Plot</span><span class="p">.</span><span class="nx">dot</span><span class="p">(</span><span class="nx">filtered</span><span class="p">,</span> <span class="p">{</span><span class="na">x</span><span class="p">:</span> <span class="dl">"</span><span class="s2">total_bill</span><span class="dl">"</span><span class="p">,</span> <span class="na">y</span><span class="p">:</span> <span class="dl">"</span><span class="s2">tip</span><span class="dl">"</span><span class="p">,</span> <span class="na">fill</span><span class="p">:</span> <span class="dl">"</span><span class="s2">smoker</span><span class="dl">"</span><span class="p">,</span> <span class="na">tip</span><span class="p">:</span> <span class="kc">true</span><span class="p">}),</span>
    <span class="nx">Plot</span><span class="p">.</span><span class="nx">linearRegressionY</span><span class="p">(</span><span class="nx">filtered</span><span class="p">,</span> <span class="p">{</span><span class="na">x</span><span class="p">:</span> <span class="dl">"</span><span class="s2">total_bill</span><span class="dl">"</span><span class="p">,</span> <span class="na">y</span><span class="p">:</span> <span class="dl">"</span><span class="s2">tip</span><span class="dl">"</span><span class="p">})</span>
  <span class="p">]</span>
<span class="p">});</span>
</code></pre></div></div>

<p><strong>5. <code class="language-plaintext highlighter-rouge">npm run build</code> produces a static <code class="language-plaintext highlighter-rouge">dist/</code>.</strong> Loaders re-run once, outputs get
content-hashed for cache-busting, and the result deploys anywhere static files are served —
GitHub Pages, Netlify, S3, no server process required.</p>

<h2 id="why-this-matters-for-load-times">Why this matters for load times</h2>

<p>In a typical Shiny/Streamlit app, the first paint waits on: server boot, R/Python session init,
and often a full data load into memory — every time a new session starts, unless you’ve invested
in caching infrastructure. In Observable Framework, that entire pipeline already happened at
build time. The visitor’s browser downloads static assets and a pre-processed data file, then
JS takes over — comparable to loading any other static site.</p>

<p>The trade-off is real: you lose the ability to run arbitrary server-side computation per request
(live model inference, per-user database queries) unless you add a separate backend. But for the
very common case — take some tabular data, clean it, let users filter and explore it — Framework
gets you a snappier result with less infrastructure.</p>

<h2 id="try-it">Try it</h2>

<p>A minimal example: a restaurant-tips dashboard with a Python loader (pandas), reactive filters
(<code class="language-plaintext highlighter-rouge">Inputs.checkbox</code>/<code class="language-plaintext highlighter-rouge">Inputs.radio</code>), and four Plot charts, built and deployed as a static site with
<code class="language-plaintext highlighter-rouge">npm run build</code>. The whole interactive layer is under 100 lines of Markdown + JS — no server to
manage after deploy.</p>

<p>The full source code is available at
<a href="https://github.com/thierrymoudiki/tips-dashboard">github.com/thierrymoudiki/tips-dashboard</a>.</p>

<p><img src="/images/2026-08-31/2026-08-31-image1.png" alt="xxx" class="img-responsive" /></p>]]></content><author><name></name></author><category term="Python" /><category term="R" /><category term="JavaScript" /><summary type="html"><![CDATA[Shiny and Streamlit dashboards depend on a live R or Python process, which makes vanilla deployments slow to load. Observable Framework compiles your data pipeline at build time and ships a static, JavaScript-only site — here's how it works, with a small restaurant-tips dashboard as an example.]]></summary></entry><entry><title type="html">PCARVFL vs CTGAN for synthetic tabular data generation on an insurance pricing dataset</title><link href="https://thierrymoudiki.github.io/blog/2026/08/22/python/pcarvfl-vs-ctgan-insurance" rel="alternate" type="text/html" title="PCARVFL vs CTGAN for synthetic tabular data generation on an insurance pricing dataset" /><published>2026-08-22T00:00:00+00:00</published><updated>2026-08-22T00:00:00+00:00</updated><id>https://thierrymoudiki.github.io/blog/2026/08/22/python/pcarvfl-vs-ctgan-insurance</id><content type="html" xml:base="https://thierrymoudiki.github.io/blog/2026/08/22/python/pcarvfl-vs-ctgan-insurance"><![CDATA[<p>This notebook applies <a href="https://thierrymoudiki.github.io/blog/2026/08/10/python/PCARVLsimulator"><code class="language-plaintext highlighter-rouge">PCARVFLSimulator</code></a>
(from the Python package <a href="https://github.com/Techtonique/synthe"><code class="language-plaintext highlighter-rouge">synthe</code></a>) — a GAN-<em>like</em>
tabular data synthesizer built from PCA scores, a Random Vector Functional-Link (RVFL) network, 
and residual bootstrapping — to the French Motor Third-Party Liability (freMTPL2) data found in
<a href="https://github.com/PNM0792/auto-insurance-pricing/tree/main/automobile/data"><code class="language-plaintext highlighter-rouge">PNM0792/auto-insurance-pricing</code></a>,
and compares it against <a href="https://github.com/sdv-dev/CTGAN">CTGAN</a>, a GAN architecture purpose-built
for tabular data.</p>

<p><strong>Data used</strong> (<code class="language-plaintext highlighter-rouge">automobile/data/</code>):</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">freMTPL2freq.csv</code> — one row per policy (frequency file): <code class="language-plaintext highlighter-rouge">IDpol, ClaimNb, Exposure, Area, VehPower,
VehAge, DrivAge, BonusMalus, VehBrand, VehGas, Density, Region</code></li>
  <li><code class="language-plaintext highlighter-rouge">freMTPL2sev.csv</code> — one row per claim (severity file): <code class="language-plaintext highlighter-rouge">IDpol, ClaimAmount</code>. Since this file alone
is only 2 columns, we merge it back onto the policy’s rating factors from the frequency file
(<code class="language-plaintext highlighter-rouge">IDpol</code> join) to get a proper multivariate severity dataset — mirroring what the repo’s own
<code class="language-plaintext highlighter-rouge">sev_model.ipynb</code> does internally.</li>
</ul>

<p><strong>What this notebook does, in 4 parts:</strong></p>
<ol>
  <li>Setup &amp; data loading</li>
  <li><code class="language-plaintext highlighter-rouge">freMTPL2freq</code> — numeric features only: PCARVFLSimulator vs CTGAN</li>
  <li><code class="language-plaintext highlighter-rouge">freMTPL2sev</code> (merged with policy features) — numeric features only: PCARVFLSimulator vs CTGAN</li>
  <li>Both datasets again, this time encoding the categorical columns (<code class="language-plaintext highlighter-rouge">Area</code>, <code class="language-plaintext highlighter-rouge">VehBrand</code>, <code class="language-plaintext highlighter-rouge">VehGas</code>,
<code class="language-plaintext highlighter-rouge">Region</code>) into numeric features with <code class="language-plaintext highlighter-rouge">category_encoders.TargetEncoder</code>, then re-running the
comparison</li>
</ol>

<p>Each part fits both simulators on an identical sample, generates synthetic rows, and scores them
with <code class="language-plaintext highlighter-rouge">synthe.adequacy_report()</code> (MMD, energy distance, per-feature KS/AD tests, moment matching,
and a random-projection KS sweep), plus a marginal-distribution plot.</p>

<h2 id="0-setup">0. Setup</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">!</span><span class="n">pip</span> <span class="n">install</span> <span class="n">synthe</span> <span class="n">ctgan</span> <span class="n">category_encoders</span>
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># !pip install synthe ctgan category_encoders pandas numpy matplotlib scikit-learn optuna
</span>
<span class="kn">import</span> <span class="nn">time</span>
<span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span>
<span class="kn">import</span> <span class="nn">matplotlib.pyplot</span> <span class="k">as</span> <span class="n">plt</span>

<span class="kn">from</span> <span class="nn">synthe</span> <span class="kn">import</span> <span class="n">PCARVFLSimulator</span><span class="p">,</span> <span class="n">adequacy_report</span>
<span class="kn">from</span> <span class="nn">ctgan</span> <span class="kn">import</span> <span class="n">CTGAN</span>
<span class="kn">import</span> <span class="nn">category_encoders</span> <span class="k">as</span> <span class="n">ce</span>

<span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="n">seed</span><span class="p">(</span><span class="mi">42</span><span class="p">)</span>
<span class="n">RANDOM_STATE</span> <span class="o">=</span> <span class="mi">42</span>
<span class="n">N_TRAIN</span> <span class="o">=</span> <span class="mi">3000</span>   <span class="c1"># sub-sample size (full freq file has 678,013 rows)
</span><span class="n">N_SYN</span> <span class="o">=</span> <span class="mi">400</span>      <span class="c1"># synthetic rows generated, matches the blog post's convention
</span><span class="n">CTGAN_EPOCHS</span> <span class="o">=</span> <span class="mi">300</span>

</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Clone (or point to a local copy of) the data repo
# git clone https://github.com/PNM0792/auto-insurance-pricing.git
</span>
<span class="n">DATA_DIR</span> <span class="o">=</span> <span class="s">"auto-insurance-pricing/automobile/data"</span>

<span class="n">freq</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">read_csv</span><span class="p">(</span><span class="s">"https://raw.githubusercontent.com/PNM0792/auto-insurance-pricing/refs/heads/main/automobile/data/freMTPL2freq.csv"</span><span class="p">)</span>
<span class="n">sev</span>  <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">read_csv</span><span class="p">(</span><span class="s">"https://raw.githubusercontent.com/PNM0792/auto-insurance-pricing/refs/heads/main/automobile/data/freMTPL2sev.csv"</span><span class="p">)</span>

<span class="k">print</span><span class="p">(</span><span class="s">"freq:"</span><span class="p">,</span> <span class="n">freq</span><span class="p">.</span><span class="n">shape</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"sev :"</span><span class="p">,</span> <span class="n">sev</span><span class="p">.</span><span class="n">shape</span><span class="p">)</span>
<span class="n">freq</span><span class="p">.</span><span class="n">head</span><span class="p">()</span>

</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>freq: (678013, 12)
sev : (26639, 2)
</code></pre></div></div>

<div id="df-813c6df7-4f88-4259-a789-dc184e38123e" class="colab-df-container">
    <div>
<style scoped="">
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>IDpol</th>
      <th>ClaimNb</th>
      <th>Exposure</th>
      <th>Area</th>
      <th>VehPower</th>
      <th>VehAge</th>
      <th>DrivAge</th>
      <th>BonusMalus</th>
      <th>VehBrand</th>
      <th>VehGas</th>
      <th>Density</th>
      <th>Region</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>1.00</td>
      <td>1</td>
      <td>0.10</td>
      <td>D</td>
      <td>5</td>
      <td>0</td>
      <td>55</td>
      <td>50</td>
      <td>B12</td>
      <td>Regular</td>
      <td>1217</td>
      <td>R82</td>
    </tr>
    <tr>
      <th>1</th>
      <td>3.00</td>
      <td>1</td>
      <td>0.77</td>
      <td>D</td>
      <td>5</td>
      <td>0</td>
      <td>55</td>
      <td>50</td>
      <td>B12</td>
      <td>Regular</td>
      <td>1217</td>
      <td>R82</td>
    </tr>
    <tr>
      <th>2</th>
      <td>5.00</td>
      <td>1</td>
      <td>0.75</td>
      <td>B</td>
      <td>6</td>
      <td>2</td>
      <td>52</td>
      <td>50</td>
      <td>B12</td>
      <td>Diesel</td>
      <td>54</td>
      <td>R22</td>
    </tr>
    <tr>
      <th>3</th>
      <td>10.00</td>
      <td>1</td>
      <td>0.09</td>
      <td>B</td>
      <td>7</td>
      <td>0</td>
      <td>46</td>
      <td>50</td>
      <td>B12</td>
      <td>Diesel</td>
      <td>76</td>
      <td>R72</td>
    </tr>
    <tr>
      <th>4</th>
      <td>11.00</td>
      <td>1</td>
      <td>0.84</td>
      <td>B</td>
      <td>7</td>
      <td>0</td>
      <td>46</td>
      <td>50</td>
      <td>B12</td>
      <td>Diesel</td>
      <td>76</td>
      <td>R72</td>
    </tr>
  </tbody>
</table>
</div>
    <div class="colab-df-buttons">

  <div class="colab-df-container">
    <button class="colab-df-convert" onclick="convertToInteractive('df-813c6df7-4f88-4259-a789-dc184e38123e')" title="Convert this dataframe to an interactive table." style="display:none;">

  <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960">
    <path d="M120-120v-720h720v720H120Zm60-500h600v-160H180v160Zm220 220h160v-160H400v160Zm0 220h160v-160H400v160ZM180-400h160v-160H180v160Zm440 0h160v-160H620v160ZM180-180h160v-160H180v160Zm440 0h160v-160H620v160Z" />
  </svg>
    </button>

  <style>
    .colab-df-container {
      display:flex;
      gap: 12px;
    }

    .colab-df-convert {
      background-color: #E8F0FE;
      border: none;
      border-radius: 50%;
      cursor: pointer;
      display: none;
      fill: #1967D2;
      height: 32px;
      padding: 0 0 0 0;
      width: 32px;
    }

    .colab-df-convert:hover {
      background-color: #E2EBFA;
      box-shadow: 0px 1px 2px rgba(60, 64, 67, 0.3), 0px 1px 3px 1px rgba(60, 64, 67, 0.15);
      fill: #174EA6;
    }

    .colab-df-buttons div {
      margin-bottom: 4px;
    }

    [theme=dark] .colab-df-convert {
      background-color: #3B4455;
      fill: #D2E3FC;
    }

    [theme=dark] .colab-df-convert:hover {
      background-color: #434B5C;
      box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);
      filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));
      fill: #FFFFFF;
    }
  </style>

    <script>
      const buttonEl =
        document.querySelector('#df-813c6df7-4f88-4259-a789-dc184e38123e button.colab-df-convert');
      buttonEl.style.display =
        google.colab.kernel.accessAllowed ? 'block' : 'none';

      async function convertToInteractive(key) {
        const element = document.querySelector('#df-813c6df7-4f88-4259-a789-dc184e38123e');
        const dataTable =
          await google.colab.kernel.invokeFunction('convertToInteractive',
                                                    [key], {});
        if (!dataTable) return;

        const docLinkHtml = 'Like what you see? Visit the ' +
          '<a target="_blank" href=https://colab.research.google.com/notebooks/data_table.ipynb>data table notebook</a>'
          + ' to learn more about interactive tables.';
        element.innerHTML = '';
        dataTable['output_type'] = 'display_data';
        await google.colab.output.renderOutput(dataTable, element);
        const docLink = document.createElement('div');
        docLink.innerHTML = docLinkHtml;
        element.appendChild(docLink);
      }
    </script>
  </div>


    </div>
  </div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">plot_marginals</span><span class="p">(</span><span class="n">X_real</span><span class="p">,</span> <span class="n">X_pcarvfl</span><span class="p">,</span> <span class="n">X_ctgan</span><span class="p">,</span> <span class="n">col_names</span><span class="p">,</span> <span class="n">title</span><span class="p">,</span> <span class="n">ncols</span><span class="o">=</span><span class="mi">5</span><span class="p">):</span>
    <span class="n">n</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">col_names</span><span class="p">)</span>
    <span class="n">nrows</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">ceil</span><span class="p">(</span><span class="n">n</span> <span class="o">/</span> <span class="n">ncols</span><span class="p">))</span>
    <span class="n">fig</span><span class="p">,</span> <span class="n">axes</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="n">subplots</span><span class="p">(</span><span class="n">nrows</span><span class="p">,</span> <span class="n">ncols</span><span class="p">,</span> <span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">4</span> <span class="o">*</span> <span class="n">ncols</span><span class="p">,</span> <span class="mi">4</span> <span class="o">*</span> <span class="n">nrows</span><span class="p">))</span>
    <span class="n">axes</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">(</span><span class="n">axes</span><span class="p">).</span><span class="n">ravel</span><span class="p">()</span>
    <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">c</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">col_names</span><span class="p">):</span>
        <span class="n">ax</span> <span class="o">=</span> <span class="n">axes</span><span class="p">[</span><span class="n">i</span><span class="p">]</span>
        <span class="n">ax</span><span class="p">.</span><span class="n">hist</span><span class="p">(</span><span class="n">X_real</span><span class="p">[:,</span> <span class="n">i</span><span class="p">],</span> <span class="n">bins</span><span class="o">=</span><span class="mi">30</span><span class="p">,</span> <span class="n">density</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.5</span><span class="p">,</span> <span class="n">label</span><span class="o">=</span><span class="s">"Real"</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="s">"black"</span><span class="p">)</span>
        <span class="n">ax</span><span class="p">.</span><span class="n">hist</span><span class="p">(</span><span class="n">X_pcarvfl</span><span class="p">[:,</span> <span class="n">i</span><span class="p">],</span> <span class="n">bins</span><span class="o">=</span><span class="mi">30</span><span class="p">,</span> <span class="n">density</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.5</span><span class="p">,</span> <span class="n">label</span><span class="o">=</span><span class="s">"PCARVFLSimulator"</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="s">"tab:blue"</span><span class="p">)</span>
        <span class="n">ax</span><span class="p">.</span><span class="n">hist</span><span class="p">(</span><span class="n">X_ctgan</span><span class="p">[:,</span> <span class="n">i</span><span class="p">],</span> <span class="n">bins</span><span class="o">=</span><span class="mi">30</span><span class="p">,</span> <span class="n">density</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.5</span><span class="p">,</span> <span class="n">label</span><span class="o">=</span><span class="s">"CTGAN"</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="s">"tab:red"</span><span class="p">)</span>
        <span class="n">ax</span><span class="p">.</span><span class="n">set_title</span><span class="p">(</span><span class="n">c</span><span class="p">,</span> <span class="n">fontsize</span><span class="o">=</span><span class="mi">10</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">i</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
            <span class="n">ax</span><span class="p">.</span><span class="n">legend</span><span class="p">(</span><span class="n">fontsize</span><span class="o">=</span><span class="mi">8</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">j</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">n</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">axes</span><span class="p">)):</span>
        <span class="n">axes</span><span class="p">[</span><span class="n">j</span><span class="p">].</span><span class="n">axis</span><span class="p">(</span><span class="s">"off"</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="n">suptitle</span><span class="p">(</span><span class="n">title</span><span class="p">,</span> <span class="n">fontsize</span><span class="o">=</span><span class="mi">13</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="n">tight_layout</span><span class="p">()</span>
    <span class="n">plt</span><span class="p">.</span><span class="n">show</span><span class="p">()</span>

</code></pre></div></div>

<h2 id="1-fremtpl2freq--numeric-features-only">1. <code class="language-plaintext highlighter-rouge">freMTPL2freq</code> — numeric features only</h2>

<p>Features: <code class="language-plaintext highlighter-rouge">Exposure</code>, <code class="language-plaintext highlighter-rouge">VehPower</code>, <code class="language-plaintext highlighter-rouge">VehAge</code>, <code class="language-plaintext highlighter-rouge">DrivAge</code>, <code class="language-plaintext highlighter-rouge">BonusMalus</code>, <code class="language-plaintext highlighter-rouge">Density</code> (log1p-transformed,
since raw density spans 1–27,000 and is heavily right-skewed).</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">num_cols_freq</span> <span class="o">=</span> <span class="p">[</span><span class="s">"Exposure"</span><span class="p">,</span> <span class="s">"VehPower"</span><span class="p">,</span> <span class="s">"VehAge"</span><span class="p">,</span> <span class="s">"DrivAge"</span><span class="p">,</span> <span class="s">"BonusMalus"</span><span class="p">,</span> <span class="s">"Density"</span><span class="p">]</span>

<span class="n">df_s</span> <span class="o">=</span> <span class="n">freq</span><span class="p">.</span><span class="n">sample</span><span class="p">(</span><span class="n">n</span><span class="o">=</span><span class="n">N_TRAIN</span><span class="p">,</span> <span class="n">random_state</span><span class="o">=</span><span class="n">RANDOM_STATE</span><span class="p">).</span><span class="n">reset_index</span><span class="p">(</span><span class="n">drop</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

<span class="n">X_df_freq</span> <span class="o">=</span> <span class="n">df_s</span><span class="p">[</span><span class="n">num_cols_freq</span><span class="p">].</span><span class="n">copy</span><span class="p">()</span>
<span class="n">X_df_freq</span><span class="p">[</span><span class="s">"Density"</span><span class="p">]</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">log1p</span><span class="p">(</span><span class="n">X_df_freq</span><span class="p">[</span><span class="s">"Density"</span><span class="p">])</span>
<span class="n">X_real_freq</span> <span class="o">=</span> <span class="n">X_df_freq</span><span class="p">.</span><span class="n">values</span><span class="p">.</span><span class="n">astype</span><span class="p">(</span><span class="nb">float</span><span class="p">)</span>

<span class="k">print</span><span class="p">(</span><span class="s">"Data shape:"</span><span class="p">,</span> <span class="n">X_real_freq</span><span class="p">.</span><span class="n">shape</span><span class="p">)</span>

</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Data shape: (3000, 6)
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">t0</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span>
<span class="n">sim_freq</span> <span class="o">=</span> <span class="n">PCARVFLSimulator</span><span class="p">(</span><span class="n">random_state</span><span class="o">=</span><span class="n">RANDOM_STATE</span><span class="p">)</span>
<span class="n">sim_freq</span><span class="p">.</span><span class="n">fit</span><span class="p">(</span><span class="n">X_real_freq</span><span class="p">,</span> <span class="n">n_trials</span><span class="o">=</span><span class="mi">30</span><span class="p">)</span>
<span class="n">fit_time_pcarvfl</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">t0</span>

<span class="n">X_syn_pcarvfl_freq</span> <span class="o">=</span> <span class="n">sim_freq</span><span class="p">.</span><span class="n">sample</span><span class="p">(</span><span class="n">N_SYN</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"PCARVFLSimulator fit time: </span><span class="si">{</span><span class="n">fit_time_pcarvfl</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="si">}</span><span class="s">s"</span><span class="p">)</span>

<span class="k">print</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">"</span> <span class="o">+</span> <span class="s">"="</span> <span class="o">*</span> <span class="mi">56</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"  FREMTPL2FREQ NUMERIC  (n=</span><span class="si">{</span><span class="n">X_real_freq</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s">, d=</span><span class="si">{</span><span class="n">X_real_freq</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="si">}</span><span class="s">)"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"="</span> <span class="o">*</span> <span class="mi">56</span><span class="p">)</span>
<span class="n">report_pcarvfl_freq</span> <span class="o">=</span> <span class="n">adequacy_report</span><span class="p">(</span><span class="n">X_real_freq</span><span class="p">,</span> <span class="n">X_syn_pcarvfl_freq</span><span class="p">)</span>

</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  [PCARVFL] 6 components (100.0% var)
  [PCARVFL] nodes=153 α=5.06e+00 mmd=0.00000
PCARVFLSimulator fit time: 16.0s

========================================================
  FREMTPL2FREQ NUMERIC  (n=3000, d=6)
========================================================

────────────────────────────────────────────────────────
  Adequacy Report  (n_real=3000, n_syn=400, d=6)
────────────────────────────────────────────────────────
  ── Distributional distance  [standardised space]
  MMD (biased, ≥0)            0.00088  
  Energy distance             0.02645  
  ── Per-feature KS tests  [original scale]
  KS stat (mean ↓)            0.13514  
  KS p Bonferroni ↑           0.00000  ✗ reject
  KS reject rate ↓              0.667  (α=0.05)
  ── Per-feature AD tests  [original scale]
  AD stat (mean ↓)            5.61936  
  AD p Bonferroni ↑           0.00600  ✗ reject
  AD reject rate ↓              0.667  (α=0.05)
  ── Moment matching  [original scale]
  Mean MAE ↓                  0.32427  
  Std  MAE ↓                  0.12301  
  Std  ratio                   0.9957  (want ≈ 1.00)
  Corr Frobenius ↓             0.2866  
  ── Random-projection sweep  [standardised space]
  KS proj (mean ↓)            0.05163  over 50 directions
────────────────────────────────────────────────────────
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">t0</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span>
<span class="n">model_freq</span> <span class="o">=</span> <span class="n">CTGAN</span><span class="p">(</span><span class="n">epochs</span><span class="o">=</span><span class="n">CTGAN_EPOCHS</span><span class="p">,</span> <span class="n">cuda</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">model_freq</span><span class="p">.</span><span class="n">fit</span><span class="p">(</span><span class="n">X_df_freq</span><span class="p">)</span>
<span class="n">fit_time_ctgan</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">t0</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"CTGAN fit time: </span><span class="si">{</span><span class="n">fit_time_ctgan</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="si">}</span><span class="s">s"</span><span class="p">)</span>

<span class="n">X_syn_ctgan_freq</span> <span class="o">=</span> <span class="n">model_freq</span><span class="p">.</span><span class="n">sample</span><span class="p">(</span><span class="n">N_SYN</span><span class="p">).</span><span class="n">values</span><span class="p">.</span><span class="n">astype</span><span class="p">(</span><span class="nb">float</span><span class="p">)</span>

<span class="k">print</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">"</span> <span class="o">+</span> <span class="s">"="</span> <span class="o">*</span> <span class="mi">56</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"  FREMTPL2FREQ NUMERIC - CTGAN  (n=</span><span class="si">{</span><span class="n">X_real_freq</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s">, d=</span><span class="si">{</span><span class="n">X_real_freq</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="si">}</span><span class="s">)"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"="</span> <span class="o">*</span> <span class="mi">56</span><span class="p">)</span>
<span class="n">report_ctgan_freq</span> <span class="o">=</span> <span class="n">adequacy_report</span><span class="p">(</span><span class="n">X_real_freq</span><span class="p">,</span> <span class="n">X_syn_ctgan_freq</span><span class="p">)</span>

</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CTGAN fit time: 116.5s

========================================================
  FREMTPL2FREQ NUMERIC - CTGAN  (n=3000, d=6)
========================================================

────────────────────────────────────────────────────────
  Adequacy Report  (n_real=3000, n_syn=400, d=6)
────────────────────────────────────────────────────────
  ── Distributional distance  [standardised space]
  MMD (biased, ≥0)            0.00083  
  Energy distance             0.09829  
  ── Per-feature KS tests  [original scale]
  KS stat (mean ↓)            0.16719  
  KS p Bonferroni ↑           0.00000  ✗ reject
  KS reject rate ↓              1.000  (α=0.05)
  ── Per-feature AD tests  [original scale]
  AD stat (mean ↓)           28.65123  
  AD p Bonferroni ↑           0.00600  ✗ reject
  AD reject rate ↓              1.000  (α=0.05)
  ── Moment matching  [original scale]
  Mean MAE ↓                  1.15431  
  Std  MAE ↓                  0.56269  
  Std  ratio                   0.9197  (want ≈ 1.00)
  Corr Frobenius ↓             0.8853  
  ── Random-projection sweep  [standardised space]
  KS proj (mean ↓)            0.10330  over 50 directions
────────────────────────────────────────────────────────
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">plot_marginals</span><span class="p">(</span>
    <span class="n">X_real_freq</span><span class="p">,</span> <span class="n">X_syn_pcarvfl_freq</span><span class="p">,</span> <span class="n">X_syn_ctgan_freq</span><span class="p">,</span>
    <span class="n">col_names</span><span class="o">=</span><span class="p">[</span><span class="s">"Exposure"</span><span class="p">,</span> <span class="s">"VehPower"</span><span class="p">,</span> <span class="s">"VehAge"</span><span class="p">,</span> <span class="s">"DrivAge"</span><span class="p">,</span> <span class="s">"BonusMalus"</span><span class="p">,</span> <span class="s">"log1p(Density)"</span><span class="p">],</span>
    <span class="n">title</span><span class="o">=</span><span class="sa">f</span><span class="s">"freMTPL2freq (n=</span><span class="si">{</span><span class="n">N_TRAIN</span><span class="si">}</span><span class="s"> sample): Real vs Synthetic Marginals"</span><span class="p">,</span>
<span class="p">)</span>

</code></pre></div></div>

<p><img src="/images/2026-08-22/2026-08-22-pcarvfl-vs-ctgan-insurance_11_0.png" alt="image-title-here" class="img-responsive" /></p>

<h2 id="2-fremtpl2sev-merged-with-policy-features--numeric-features-only">2. <code class="language-plaintext highlighter-rouge">freMTPL2sev</code> (merged with policy features) — numeric features only</h2>

<p>The severity file alone is just <code class="language-plaintext highlighter-rouge">IDpol, ClaimAmount</code>, so we merge it back onto the frequency
file’s rating factors to build a proper multivariate severity dataset. Features:
<code class="language-plaintext highlighter-rouge">ClaimAmount</code> (log1p), <code class="language-plaintext highlighter-rouge">Exposure</code>, <code class="language-plaintext highlighter-rouge">VehPower</code>, <code class="language-plaintext highlighter-rouge">VehAge</code>, <code class="language-plaintext highlighter-rouge">DrivAge</code>, <code class="language-plaintext highlighter-rouge">BonusMalus</code>, <code class="language-plaintext highlighter-rouge">Density</code> (log1p).</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">merged</span> <span class="o">=</span> <span class="n">sev</span><span class="p">.</span><span class="n">merge</span><span class="p">(</span><span class="n">freq</span><span class="p">,</span> <span class="n">on</span><span class="o">=</span><span class="s">"IDpol"</span><span class="p">,</span> <span class="n">how</span><span class="o">=</span><span class="s">"left"</span><span class="p">).</span><span class="n">dropna</span><span class="p">().</span><span class="n">reset_index</span><span class="p">(</span><span class="n">drop</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Merged severity dataset:"</span><span class="p">,</span> <span class="n">merged</span><span class="p">.</span><span class="n">shape</span><span class="p">)</span>

<span class="n">num_cols_sev</span> <span class="o">=</span> <span class="p">[</span><span class="s">"ClaimAmount"</span><span class="p">,</span> <span class="s">"Exposure"</span><span class="p">,</span> <span class="s">"VehPower"</span><span class="p">,</span> <span class="s">"VehAge"</span><span class="p">,</span> <span class="s">"DrivAge"</span><span class="p">,</span> <span class="s">"BonusMalus"</span><span class="p">,</span> <span class="s">"Density"</span><span class="p">]</span>

<span class="n">df_s_sev</span> <span class="o">=</span> <span class="n">merged</span><span class="p">.</span><span class="n">sample</span><span class="p">(</span><span class="n">n</span><span class="o">=</span><span class="n">N_TRAIN</span><span class="p">,</span> <span class="n">random_state</span><span class="o">=</span><span class="n">RANDOM_STATE</span><span class="p">).</span><span class="n">reset_index</span><span class="p">(</span><span class="n">drop</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

<span class="n">X_df_sev</span> <span class="o">=</span> <span class="n">df_s_sev</span><span class="p">[</span><span class="n">num_cols_sev</span><span class="p">].</span><span class="n">copy</span><span class="p">()</span>
<span class="n">X_df_sev</span><span class="p">[</span><span class="s">"ClaimAmount"</span><span class="p">]</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">log1p</span><span class="p">(</span><span class="n">X_df_sev</span><span class="p">[</span><span class="s">"ClaimAmount"</span><span class="p">])</span>
<span class="n">X_df_sev</span><span class="p">[</span><span class="s">"Density"</span><span class="p">]</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">log1p</span><span class="p">(</span><span class="n">X_df_sev</span><span class="p">[</span><span class="s">"Density"</span><span class="p">])</span>
<span class="n">X_real_sev</span> <span class="o">=</span> <span class="n">X_df_sev</span><span class="p">.</span><span class="n">values</span><span class="p">.</span><span class="n">astype</span><span class="p">(</span><span class="nb">float</span><span class="p">)</span>

<span class="k">print</span><span class="p">(</span><span class="s">"Data shape:"</span><span class="p">,</span> <span class="n">X_real_sev</span><span class="p">.</span><span class="n">shape</span><span class="p">)</span>

</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Merged severity dataset: (26444, 13)
Data shape: (3000, 7)
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">t0</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span>
<span class="n">sim_sev</span> <span class="o">=</span> <span class="n">PCARVFLSimulator</span><span class="p">(</span><span class="n">random_state</span><span class="o">=</span><span class="n">RANDOM_STATE</span><span class="p">)</span>
<span class="n">sim_sev</span><span class="p">.</span><span class="n">fit</span><span class="p">(</span><span class="n">X_real_sev</span><span class="p">,</span> <span class="n">n_trials</span><span class="o">=</span><span class="mi">30</span><span class="p">)</span>
<span class="n">fit_time_pcarvfl_sev</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">t0</span>

<span class="n">X_syn_pcarvfl_sev</span> <span class="o">=</span> <span class="n">sim_sev</span><span class="p">.</span><span class="n">sample</span><span class="p">(</span><span class="n">N_SYN</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"PCARVFLSimulator fit time: </span><span class="si">{</span><span class="n">fit_time_pcarvfl_sev</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="si">}</span><span class="s">s"</span><span class="p">)</span>

<span class="k">print</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">"</span> <span class="o">+</span> <span class="s">"="</span> <span class="o">*</span> <span class="mi">56</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"  FREMTPL2SEV (merged w/ policy features)  (n=</span><span class="si">{</span><span class="n">X_real_sev</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s">, d=</span><span class="si">{</span><span class="n">X_real_sev</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="si">}</span><span class="s">)"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"="</span> <span class="o">*</span> <span class="mi">56</span><span class="p">)</span>
<span class="n">report_pcarvfl_sev</span> <span class="o">=</span> <span class="n">adequacy_report</span><span class="p">(</span><span class="n">X_real_sev</span><span class="p">,</span> <span class="n">X_syn_pcarvfl_sev</span><span class="p">)</span>

</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  [PCARVFL] 7 components (100.0% var)
  [PCARVFL] nodes=605 α=1.88e-04 mmd=0.00000
PCARVFLSimulator fit time: 15.5s

========================================================
  FREMTPL2SEV (merged w/ policy features)  (n=3000, d=7)
========================================================

────────────────────────────────────────────────────────
  Adequacy Report  (n_real=3000, n_syn=400, d=7)
────────────────────────────────────────────────────────
  ── Distributional distance  [standardised space]
  MMD (biased, ≥0)            0.00076  
  Energy distance             0.01769  
  ── Per-feature KS tests  [original scale]
  KS stat (mean ↓)            0.13814  
  KS p Bonferroni ↑           0.00000  ✗ reject
  KS reject rate ↓              0.857  (α=0.05)
  ── Per-feature AD tests  [original scale]
  AD stat (mean ↓)            3.81354  
  AD p Bonferroni ↑           0.00700  ✗ reject
  AD reject rate ↓              0.571  (α=0.05)
  ── Moment matching  [original scale]
  Mean MAE ↓                  0.41567  
  Std  MAE ↓                  0.16528  
  Std  ratio                   0.9817  (want ≈ 1.00)
  Corr Frobenius ↓             0.3065  
  ── Random-projection sweep  [standardised space]
  KS proj (mean ↓)            0.05793  over 50 directions
────────────────────────────────────────────────────────
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">t0</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span>
<span class="n">model_sev</span> <span class="o">=</span> <span class="n">CTGAN</span><span class="p">(</span><span class="n">epochs</span><span class="o">=</span><span class="n">CTGAN_EPOCHS</span><span class="p">,</span> <span class="n">cuda</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">model_sev</span><span class="p">.</span><span class="n">fit</span><span class="p">(</span><span class="n">X_df_sev</span><span class="p">)</span>
<span class="n">fit_time_ctgan_sev</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">t0</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"CTGAN fit time: </span><span class="si">{</span><span class="n">fit_time_ctgan_sev</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="si">}</span><span class="s">s"</span><span class="p">)</span>

<span class="n">X_syn_ctgan_sev</span> <span class="o">=</span> <span class="n">model_sev</span><span class="p">.</span><span class="n">sample</span><span class="p">(</span><span class="n">N_SYN</span><span class="p">).</span><span class="n">values</span><span class="p">.</span><span class="n">astype</span><span class="p">(</span><span class="nb">float</span><span class="p">)</span>

<span class="k">print</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">"</span> <span class="o">+</span> <span class="s">"="</span> <span class="o">*</span> <span class="mi">56</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"  FREMTPL2SEV - CTGAN  (n=</span><span class="si">{</span><span class="n">X_real_sev</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s">, d=</span><span class="si">{</span><span class="n">X_real_sev</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="si">}</span><span class="s">)"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"="</span> <span class="o">*</span> <span class="mi">56</span><span class="p">)</span>
<span class="n">report_ctgan_sev</span> <span class="o">=</span> <span class="n">adequacy_report</span><span class="p">(</span><span class="n">X_real_sev</span><span class="p">,</span> <span class="n">X_syn_ctgan_sev</span><span class="p">)</span>

</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CTGAN fit time: 106.5s

========================================================
  FREMTPL2SEV - CTGAN  (n=3000, d=7)
========================================================

────────────────────────────────────────────────────────
  Adequacy Report  (n_real=3000, n_syn=400, d=7)
────────────────────────────────────────────────────────
  ── Distributional distance  [standardised space]
  MMD (biased, ≥0)            0.00000  
  Energy distance             0.19864  
  ── Per-feature KS tests  [original scale]
  KS stat (mean ↓)            0.26067  
  KS p Bonferroni ↑           0.00000  ✗ reject
  KS reject rate ↓              0.857  (α=0.05)
  ── Per-feature AD tests  [original scale]
  AD stat (mean ↓)           46.46704  
  AD p Bonferroni ↑           0.00700  ✗ reject
  AD reject rate ↓              0.857  (α=0.05)
  ── Moment matching  [original scale]
  Mean MAE ↓                  2.13144  
  Std  MAE ↓                  0.76668  
  Std  ratio                   0.9633  (want ≈ 1.00)
  Corr Frobenius ↓             0.9285  
  ── Random-projection sweep  [standardised space]
  KS proj (mean ↓)            0.14216  over 50 directions
────────────────────────────────────────────────────────
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">plot_marginals</span><span class="p">(</span>
    <span class="n">X_real_sev</span><span class="p">,</span> <span class="n">X_syn_pcarvfl_sev</span><span class="p">,</span> <span class="n">X_syn_ctgan_sev</span><span class="p">,</span>
    <span class="n">col_names</span><span class="o">=</span><span class="p">[</span><span class="s">"log1p(ClaimAmount)"</span><span class="p">,</span> <span class="s">"Exposure"</span><span class="p">,</span> <span class="s">"VehPower"</span><span class="p">,</span> <span class="s">"VehAge"</span><span class="p">,</span> <span class="s">"DrivAge"</span><span class="p">,</span> <span class="s">"BonusMalus"</span><span class="p">,</span> <span class="s">"log1p(Density)"</span><span class="p">],</span>
    <span class="n">title</span><span class="o">=</span><span class="sa">f</span><span class="s">"freMTPL2sev merged w/ policy features (n=</span><span class="si">{</span><span class="n">N_TRAIN</span><span class="si">}</span><span class="s"> sample): Real vs Synthetic Marginals"</span><span class="p">,</span>
    <span class="n">ncols</span><span class="o">=</span><span class="mi">4</span><span class="p">,</span>
<span class="p">)</span>

</code></pre></div></div>

<p><img src="/images/2026-08-22/2026-08-22-pcarvfl-vs-ctgan-insurance_16_0.png" alt="image-title-here" class="img-responsive" /></p>

<h2 id="3-adding-the-categorical-features-with-category_encoders">3. Adding the categorical features with <code class="language-plaintext highlighter-rouge">category_encoders</code></h2>

<p><code class="language-plaintext highlighter-rouge">Area</code>, <code class="language-plaintext highlighter-rouge">VehBrand</code>, <code class="language-plaintext highlighter-rouge">VehGas</code>, <code class="language-plaintext highlighter-rouge">Region</code> are text columns. We convert them to numeric using
<code class="language-plaintext highlighter-rouge">category_encoders.TargetEncoder</code>, which is a standard actuarial technique — it replaces each
category with a smoothed mean of the response for that category (a “relativity”):</p>
<ul>
  <li>for the <strong>frequency</strong> dataset, we target-encode against <code class="language-plaintext highlighter-rouge">ClaimNb</code></li>
  <li>for the <strong>severity</strong> dataset, we target-encode against <code class="language-plaintext highlighter-rouge">ClaimAmount</code></li>
</ul>

<h3 id="3a-fremtpl2freq--numeric--target-encoded-categoricals">3a. <code class="language-plaintext highlighter-rouge">freMTPL2freq</code> — numeric + target-encoded categoricals</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">cat_cols</span> <span class="o">=</span> <span class="p">[</span><span class="s">"Area"</span><span class="p">,</span> <span class="s">"VehBrand"</span><span class="p">,</span> <span class="s">"VehGas"</span><span class="p">,</span> <span class="s">"Region"</span><span class="p">]</span>

<span class="n">encoder_freq</span> <span class="o">=</span> <span class="n">ce</span><span class="p">.</span><span class="n">TargetEncoder</span><span class="p">(</span><span class="n">cols</span><span class="o">=</span><span class="n">cat_cols</span><span class="p">,</span> <span class="n">smoothing</span><span class="o">=</span><span class="mf">10.0</span><span class="p">)</span>
<span class="n">X_cat_enc_freq</span> <span class="o">=</span> <span class="n">encoder_freq</span><span class="p">.</span><span class="n">fit_transform</span><span class="p">(</span><span class="n">df_s</span><span class="p">[</span><span class="n">cat_cols</span><span class="p">],</span> <span class="n">df_s</span><span class="p">[</span><span class="s">"ClaimNb"</span><span class="p">])</span>
<span class="n">X_cat_enc_freq</span><span class="p">.</span><span class="n">columns</span> <span class="o">=</span> <span class="p">[</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">c</span><span class="si">}</span><span class="s">_te"</span> <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">cat_cols</span><span class="p">]</span>

<span class="n">X_df_freq_full</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">concat</span><span class="p">([</span><span class="n">df_s</span><span class="p">[</span><span class="n">num_cols_freq</span><span class="p">].</span><span class="n">copy</span><span class="p">(),</span> <span class="n">X_cat_enc_freq</span><span class="p">],</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
<span class="n">X_df_freq_full</span><span class="p">[</span><span class="s">"Density"</span><span class="p">]</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">log1p</span><span class="p">(</span><span class="n">X_df_freq_full</span><span class="p">[</span><span class="s">"Density"</span><span class="p">])</span>
<span class="n">X_real_freq_full</span> <span class="o">=</span> <span class="n">X_df_freq_full</span><span class="p">.</span><span class="n">values</span><span class="p">.</span><span class="n">astype</span><span class="p">(</span><span class="nb">float</span><span class="p">)</span>

<span class="k">print</span><span class="p">(</span><span class="s">"Data shape:"</span><span class="p">,</span> <span class="n">X_real_freq_full</span><span class="p">.</span><span class="n">shape</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Features:"</span><span class="p">,</span> <span class="nb">list</span><span class="p">(</span><span class="n">X_df_freq_full</span><span class="p">.</span><span class="n">columns</span><span class="p">))</span>

</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Data shape: (3000, 10)
Features: ['Exposure', 'VehPower', 'VehAge', 'DrivAge', 'BonusMalus', 'Density', 'Area_te', 'VehBrand_te', 'VehGas_te', 'Region_te']
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">t0</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span>
<span class="n">sim_freq_full</span> <span class="o">=</span> <span class="n">PCARVFLSimulator</span><span class="p">(</span><span class="n">random_state</span><span class="o">=</span><span class="n">RANDOM_STATE</span><span class="p">)</span>
<span class="n">sim_freq_full</span><span class="p">.</span><span class="n">fit</span><span class="p">(</span><span class="n">X_real_freq_full</span><span class="p">,</span> <span class="n">n_trials</span><span class="o">=</span><span class="mi">30</span><span class="p">)</span>
<span class="n">fit_time_pcarvfl_freq_full</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">t0</span>

<span class="n">X_syn_pcarvfl_freq_full</span> <span class="o">=</span> <span class="n">sim_freq_full</span><span class="p">.</span><span class="n">sample</span><span class="p">(</span><span class="n">N_SYN</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"PCARVFLSimulator fit time: </span><span class="si">{</span><span class="n">fit_time_pcarvfl_freq_full</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="si">}</span><span class="s">s"</span><span class="p">)</span>

<span class="k">print</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">"</span> <span class="o">+</span> <span class="s">"="</span> <span class="o">*</span> <span class="mi">56</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"  FREMTPL2FREQ FULL (num+target-enc cat)  (n=</span><span class="si">{</span><span class="n">X_real_freq_full</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s">, d=</span><span class="si">{</span><span class="n">X_real_freq_full</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="si">}</span><span class="s">)"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"="</span> <span class="o">*</span> <span class="mi">56</span><span class="p">)</span>
<span class="n">report_pcarvfl_freq_full</span> <span class="o">=</span> <span class="n">adequacy_report</span><span class="p">(</span><span class="n">X_real_freq_full</span><span class="p">,</span> <span class="n">X_syn_pcarvfl_freq_full</span><span class="p">)</span>

</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  [PCARVFL] 9 components (95.1% var)
  [PCARVFL] nodes=365 α=3.49e-03 mmd=0.00017
PCARVFLSimulator fit time: 17.1s

========================================================
  FREMTPL2FREQ FULL (num+target-enc cat)  (n=3000, d=10)
========================================================

────────────────────────────────────────────────────────
  Adequacy Report  (n_real=3000, n_syn=400, d=10)
────────────────────────────────────────────────────────
  ── Distributional distance  [standardised space]
  MMD (biased, ≥0)            0.00210  
  Energy distance             0.03199  
  ── Per-feature KS tests  [original scale]
  KS stat (mean ↓)            0.16247  
  KS p Bonferroni ↑           0.00000  ✗ reject
  KS reject rate ↓              0.800  (α=0.05)
  ── Per-feature AD tests  [original scale]
  AD stat (mean ↓)            8.79878  
  AD p Bonferroni ↑           0.01000  ✗ reject
  AD reject rate ↓              0.800  (α=0.05)
  ── Moment matching  [original scale]
  Mean MAE ↓                  0.19352  
  Std  MAE ↓                  0.08292  
  Std  ratio                   0.9892  (want ≈ 1.00)
  Corr Frobenius ↓             0.5270  
  ── Random-projection sweep  [standardised space]
  KS proj (mean ↓)            0.04996  over 50 directions
────────────────────────────────────────────────────────
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">t0</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span>
<span class="n">model_freq_full</span> <span class="o">=</span> <span class="n">CTGAN</span><span class="p">(</span><span class="n">epochs</span><span class="o">=</span><span class="n">CTGAN_EPOCHS</span><span class="p">,</span> <span class="n">cuda</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">model_freq_full</span><span class="p">.</span><span class="n">fit</span><span class="p">(</span><span class="n">X_df_freq_full</span><span class="p">)</span>
<span class="n">fit_time_ctgan_freq_full</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">t0</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"CTGAN fit time: </span><span class="si">{</span><span class="n">fit_time_ctgan_freq_full</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="si">}</span><span class="s">s"</span><span class="p">)</span>

<span class="n">X_syn_ctgan_freq_full</span> <span class="o">=</span> <span class="n">model_freq_full</span><span class="p">.</span><span class="n">sample</span><span class="p">(</span><span class="n">N_SYN</span><span class="p">).</span><span class="n">values</span><span class="p">.</span><span class="n">astype</span><span class="p">(</span><span class="nb">float</span><span class="p">)</span>

<span class="k">print</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">"</span> <span class="o">+</span> <span class="s">"="</span> <span class="o">*</span> <span class="mi">56</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"  FREMTPL2FREQ FULL - CTGAN  (n=</span><span class="si">{</span><span class="n">X_real_freq_full</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s">, d=</span><span class="si">{</span><span class="n">X_real_freq_full</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="si">}</span><span class="s">)"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"="</span> <span class="o">*</span> <span class="mi">56</span><span class="p">)</span>
<span class="n">report_ctgan_freq_full</span> <span class="o">=</span> <span class="n">adequacy_report</span><span class="p">(</span><span class="n">X_real_freq_full</span><span class="p">,</span> <span class="n">X_syn_ctgan_freq_full</span><span class="p">)</span>

</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CTGAN fit time: 121.9s

========================================================
  FREMTPL2FREQ FULL - CTGAN  (n=3000, d=10)
========================================================

────────────────────────────────────────────────────────
  Adequacy Report  (n_real=3000, n_syn=400, d=10)
────────────────────────────────────────────────────────
  ── Distributional distance  [standardised space]
  MMD (biased, ≥0)            0.00000  
  Energy distance             0.25518  
  ── Per-feature KS tests  [original scale]
  KS stat (mean ↓)            0.20975  
  KS p Bonferroni ↑           0.00000  ✗ reject
  KS reject rate ↓              1.000  (α=0.05)
  ── Per-feature AD tests  [original scale]
  AD stat (mean ↓)           35.95134  
  AD p Bonferroni ↑           0.01000  ✗ reject
  AD reject rate ↓              1.000  (α=0.05)
  ── Moment matching  [original scale]
  Mean MAE ↓                  0.70767  
  Std  MAE ↓                  0.30220  
  Std  ratio                   1.0647  (want ≈ 1.00)
  Corr Frobenius ↓             1.1302  
  ── Random-projection sweep  [standardised space]
  KS proj (mean ↓)            0.13465  over 50 directions
────────────────────────────────────────────────────────
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">plot_marginals</span><span class="p">(</span>
    <span class="n">X_real_freq_full</span><span class="p">,</span> <span class="n">X_syn_pcarvfl_freq_full</span><span class="p">,</span> <span class="n">X_syn_ctgan_freq_full</span><span class="p">,</span>
    <span class="n">col_names</span><span class="o">=</span><span class="p">[</span><span class="s">"Exposure"</span><span class="p">,</span> <span class="s">"VehPower"</span><span class="p">,</span> <span class="s">"VehAge"</span><span class="p">,</span> <span class="s">"DrivAge"</span><span class="p">,</span> <span class="s">"BonusMalus"</span><span class="p">,</span> <span class="s">"log1p(Density)"</span><span class="p">,</span>
               <span class="s">"Area_te"</span><span class="p">,</span> <span class="s">"VehBrand_te"</span><span class="p">,</span> <span class="s">"VehGas_te"</span><span class="p">,</span> <span class="s">"Region_te"</span><span class="p">],</span>
    <span class="n">title</span><span class="o">=</span><span class="sa">f</span><span class="s">"freMTPL2freq FULL (num + target-encoded cat, n=</span><span class="si">{</span><span class="n">N_TRAIN</span><span class="si">}</span><span class="s">): Real vs Synthetic Marginals"</span><span class="p">,</span>
<span class="p">)</span>

</code></pre></div></div>

<p><img src="/images/2026-08-22/2026-08-22-pcarvfl-vs-ctgan-insurance_22_0.png" alt="image-title-here" class="img-responsive" /></p>

<h3 id="3b-fremtpl2sev-merged--numeric--target-encoded-categoricals">3b. <code class="language-plaintext highlighter-rouge">freMTPL2sev</code> (merged) — numeric + target-encoded categoricals</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">encoder_sev</span> <span class="o">=</span> <span class="n">ce</span><span class="p">.</span><span class="n">TargetEncoder</span><span class="p">(</span><span class="n">cols</span><span class="o">=</span><span class="n">cat_cols</span><span class="p">,</span> <span class="n">smoothing</span><span class="o">=</span><span class="mf">10.0</span><span class="p">)</span>
<span class="n">X_cat_enc_sev</span> <span class="o">=</span> <span class="n">encoder_sev</span><span class="p">.</span><span class="n">fit_transform</span><span class="p">(</span><span class="n">df_s_sev</span><span class="p">[</span><span class="n">cat_cols</span><span class="p">],</span> <span class="n">df_s_sev</span><span class="p">[</span><span class="s">"ClaimAmount"</span><span class="p">])</span>
<span class="n">X_cat_enc_sev</span><span class="p">.</span><span class="n">columns</span> <span class="o">=</span> <span class="p">[</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">c</span><span class="si">}</span><span class="s">_te"</span> <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">cat_cols</span><span class="p">]</span>

<span class="n">X_df_sev_full</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">concat</span><span class="p">([</span><span class="n">df_s_sev</span><span class="p">[[</span><span class="s">"ClaimAmount"</span><span class="p">]].</span><span class="n">copy</span><span class="p">(),</span> <span class="n">df_s_sev</span><span class="p">[</span><span class="n">num_cols_sev</span><span class="p">[</span><span class="mi">1</span><span class="p">:]].</span><span class="n">copy</span><span class="p">(),</span> <span class="n">X_cat_enc_sev</span><span class="p">],</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
<span class="n">X_df_sev_full</span><span class="p">[</span><span class="s">"ClaimAmount"</span><span class="p">]</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">log1p</span><span class="p">(</span><span class="n">X_df_sev_full</span><span class="p">[</span><span class="s">"ClaimAmount"</span><span class="p">])</span>
<span class="n">X_df_sev_full</span><span class="p">[</span><span class="s">"Density"</span><span class="p">]</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">log1p</span><span class="p">(</span><span class="n">X_df_sev_full</span><span class="p">[</span><span class="s">"Density"</span><span class="p">])</span>
<span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">X_cat_enc_sev</span><span class="p">.</span><span class="n">columns</span><span class="p">:</span>
    <span class="n">X_df_sev_full</span><span class="p">[</span><span class="n">c</span><span class="p">]</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">log1p</span><span class="p">(</span><span class="n">X_df_sev_full</span><span class="p">[</span><span class="n">c</span><span class="p">].</span><span class="n">clip</span><span class="p">(</span><span class="n">lower</span><span class="o">=</span><span class="mi">0</span><span class="p">))</span>

<span class="n">X_real_sev_full</span> <span class="o">=</span> <span class="n">X_df_sev_full</span><span class="p">.</span><span class="n">values</span><span class="p">.</span><span class="n">astype</span><span class="p">(</span><span class="nb">float</span><span class="p">)</span>

<span class="k">print</span><span class="p">(</span><span class="s">"Data shape:"</span><span class="p">,</span> <span class="n">X_real_sev_full</span><span class="p">.</span><span class="n">shape</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Features:"</span><span class="p">,</span> <span class="nb">list</span><span class="p">(</span><span class="n">X_df_sev_full</span><span class="p">.</span><span class="n">columns</span><span class="p">))</span>

</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Data shape: (3000, 11)
Features: ['ClaimAmount', 'Exposure', 'VehPower', 'VehAge', 'DrivAge', 'BonusMalus', 'Density', 'Area_te', 'VehBrand_te', 'VehGas_te', 'Region_te']
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">t0</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span>
<span class="n">sim_sev_full</span> <span class="o">=</span> <span class="n">PCARVFLSimulator</span><span class="p">(</span><span class="n">random_state</span><span class="o">=</span><span class="n">RANDOM_STATE</span><span class="p">)</span>
<span class="n">sim_sev_full</span><span class="p">.</span><span class="n">fit</span><span class="p">(</span><span class="n">X_real_sev_full</span><span class="p">,</span> <span class="n">n_trials</span><span class="o">=</span><span class="mi">30</span><span class="p">)</span>
<span class="n">fit_time_pcarvfl_sev_full</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">t0</span>

<span class="n">X_syn_pcarvfl_sev_full</span> <span class="o">=</span> <span class="n">sim_sev_full</span><span class="p">.</span><span class="n">sample</span><span class="p">(</span><span class="n">N_SYN</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"PCARVFLSimulator fit time: </span><span class="si">{</span><span class="n">fit_time_pcarvfl_sev_full</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="si">}</span><span class="s">s"</span><span class="p">)</span>

<span class="k">print</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">"</span> <span class="o">+</span> <span class="s">"="</span> <span class="o">*</span> <span class="mi">56</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"  FREMTPL2SEV FULL (num+target-enc cat)  (n=</span><span class="si">{</span><span class="n">X_real_sev_full</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s">, d=</span><span class="si">{</span><span class="n">X_real_sev_full</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="si">}</span><span class="s">)"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"="</span> <span class="o">*</span> <span class="mi">56</span><span class="p">)</span>
<span class="n">report_pcarvfl_sev_full</span> <span class="o">=</span> <span class="n">adequacy_report</span><span class="p">(</span><span class="n">X_real_sev_full</span><span class="p">,</span> <span class="n">X_syn_pcarvfl_sev_full</span><span class="p">)</span>

</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  [PCARVFL] 10 components (97.5% var)
  [PCARVFL] nodes=181 α=5.59e-04 mmd=0.00032
PCARVFLSimulator fit time: 13.4s

========================================================
  FREMTPL2SEV FULL (num+target-enc cat)  (n=3000, d=11)
========================================================

────────────────────────────────────────────────────────
  Adequacy Report  (n_real=3000, n_syn=400, d=11)
────────────────────────────────────────────────────────
  ── Distributional distance  [standardised space]
  MMD (biased, ≥0)            0.00113  
  Energy distance             0.02130  
  ── Per-feature KS tests  [original scale]
  KS stat (mean ↓)            0.15955  
  KS p Bonferroni ↑           0.00000  ✗ reject
  KS reject rate ↓              0.909  (α=0.05)
  ── Per-feature AD tests  [original scale]
  AD stat (mean ↓)            8.03864  
  AD p Bonferroni ↑           0.01100  ✗ reject
  AD reject rate ↓              0.636  (α=0.05)
  ── Moment matching  [original scale]
  Mean MAE ↓                  0.26444  
  Std  MAE ↓                  0.11333  
  Std  ratio                   0.9822  (want ≈ 1.00)
  Corr Frobenius ↓             0.5038  
  ── Random-projection sweep  [standardised space]
  KS proj (mean ↓)            0.05155  over 50 directions
────────────────────────────────────────────────────────
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">t0</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span>
<span class="n">model_sev_full</span> <span class="o">=</span> <span class="n">CTGAN</span><span class="p">(</span><span class="n">epochs</span><span class="o">=</span><span class="n">CTGAN_EPOCHS</span><span class="p">,</span> <span class="n">cuda</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">model_sev_full</span><span class="p">.</span><span class="n">fit</span><span class="p">(</span><span class="n">X_df_sev_full</span><span class="p">)</span>
<span class="n">fit_time_ctgan_sev_full</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">t0</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"CTGAN fit time: </span><span class="si">{</span><span class="n">fit_time_ctgan_sev_full</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="si">}</span><span class="s">s"</span><span class="p">)</span>

<span class="n">X_syn_ctgan_sev_full</span> <span class="o">=</span> <span class="n">model_sev_full</span><span class="p">.</span><span class="n">sample</span><span class="p">(</span><span class="n">N_SYN</span><span class="p">).</span><span class="n">values</span><span class="p">.</span><span class="n">astype</span><span class="p">(</span><span class="nb">float</span><span class="p">)</span>

<span class="k">print</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">"</span> <span class="o">+</span> <span class="s">"="</span> <span class="o">*</span> <span class="mi">56</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"  FREMTPL2SEV FULL - CTGAN  (n=</span><span class="si">{</span><span class="n">X_real_sev_full</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s">, d=</span><span class="si">{</span><span class="n">X_real_sev_full</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="si">}</span><span class="s">)"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"="</span> <span class="o">*</span> <span class="mi">56</span><span class="p">)</span>
<span class="n">report_ctgan_sev_full</span> <span class="o">=</span> <span class="n">adequacy_report</span><span class="p">(</span><span class="n">X_real_sev_full</span><span class="p">,</span> <span class="n">X_syn_ctgan_sev_full</span><span class="p">)</span>

</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CTGAN fit time: 127.2s

========================================================
  FREMTPL2SEV FULL - CTGAN  (n=3000, d=11)
========================================================

────────────────────────────────────────────────────────
  Adequacy Report  (n_real=3000, n_syn=400, d=11)
────────────────────────────────────────────────────────
  ── Distributional distance  [standardised space]
  MMD (biased, ≥0)            0.00000  
  Energy distance             0.19999  
  ── Per-feature KS tests  [original scale]
  KS stat (mean ↓)            0.25571  
  KS p Bonferroni ↑           0.00000  ✗ reject
  KS reject rate ↓              1.000  (α=0.05)
  ── Per-feature AD tests  [original scale]
  AD stat (mean ↓)           41.12033  
  AD p Bonferroni ↑           0.01100  ✗ reject
  AD reject rate ↓              1.000  (α=0.05)
  ── Moment matching  [original scale]
  Mean MAE ↓                  0.93624  
  Std  MAE ↓                  0.21834  
  Std  ratio                   0.9583  (want ≈ 1.00)
  Corr Frobenius ↓             1.5596  
  ── Random-projection sweep  [standardised space]
  KS proj (mean ↓)            0.10638  over 50 directions
────────────────────────────────────────────────────────
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">plot_marginals</span><span class="p">(</span>
    <span class="n">X_real_sev_full</span><span class="p">,</span> <span class="n">X_syn_pcarvfl_sev_full</span><span class="p">,</span> <span class="n">X_syn_ctgan_sev_full</span><span class="p">,</span>
    <span class="n">col_names</span><span class="o">=</span><span class="p">[</span><span class="s">"log1p(ClaimAmount)"</span><span class="p">,</span> <span class="s">"Exposure"</span><span class="p">,</span> <span class="s">"VehPower"</span><span class="p">,</span> <span class="s">"VehAge"</span><span class="p">,</span> <span class="s">"DrivAge"</span><span class="p">,</span> <span class="s">"BonusMalus"</span><span class="p">,</span>
               <span class="s">"log1p(Density)"</span><span class="p">,</span> <span class="s">"log1p(Area_te)"</span><span class="p">,</span> <span class="s">"log1p(VehBrand_te)"</span><span class="p">,</span> <span class="s">"log1p(VehGas_te)"</span><span class="p">,</span> <span class="s">"log1p(Region_te)"</span><span class="p">],</span>
    <span class="n">title</span><span class="o">=</span><span class="sa">f</span><span class="s">"freMTPL2sev FULL (num + target-encoded cat, n=</span><span class="si">{</span><span class="n">N_TRAIN</span><span class="si">}</span><span class="s">): Real vs Synthetic Marginals"</span><span class="p">,</span>
    <span class="n">ncols</span><span class="o">=</span><span class="mi">4</span><span class="p">,</span>
<span class="p">)</span>

</code></pre></div></div>

<p><img src="/images/2026-08-22/2026-08-22-pcarvfl-vs-ctgan-insurance_27_0.png" alt="image-title-here" class="img-responsive" /></p>

<h2 id="4-summary-tables">4. Summary tables</h2>

<p>Recap of the <code class="language-plaintext highlighter-rouge">adequacy_report()</code> output across all four runs (numbers below are from the
run performed while writing this notebook; re-running with <code class="language-plaintext highlighter-rouge">RANDOM_STATE=42</code> should reproduce
them closely, modulo Optuna/CTGAN’s own internal stochasticity).</p>

<h3 id="fremtpl2freq--numeric-only"><code class="language-plaintext highlighter-rouge">freMTPL2freq</code> — numeric only</h3>

<table>
  <thead>
    <tr>
      <th>Metric</th>
      <th>Direction</th>
      <th>PCARVFLSimulator</th>
      <th>CTGAN (300 epochs)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Fit time</td>
      <td>—</td>
      <td><strong>4.9s</strong></td>
      <td>56.1s</td>
    </tr>
    <tr>
      <td>MMD (biased)</td>
      <td>↓ better</td>
      <td><strong>0.00088</strong></td>
      <td>0.00169</td>
    </tr>
    <tr>
      <td>Energy distance</td>
      <td>↓ better</td>
      <td><strong>0.02645</strong></td>
      <td>0.17676</td>
    </tr>
    <tr>
      <td>KS stat (mean)</td>
      <td>↓ better</td>
      <td><strong>0.13514</strong></td>
      <td>0.15433</td>
    </tr>
    <tr>
      <td>KS reject rate</td>
      <td>↓ better</td>
      <td><strong>0.667</strong></td>
      <td>0.833</td>
    </tr>
    <tr>
      <td>AD stat (mean)</td>
      <td>↓ better</td>
      <td><strong>5.61936</strong></td>
      <td>34.60157</td>
    </tr>
    <tr>
      <td>AD reject rate</td>
      <td>↓ better</td>
      <td><strong>0.667</strong></td>
      <td>0.833</td>
    </tr>
    <tr>
      <td>Mean MAE</td>
      <td>↓ better</td>
      <td><strong>0.32427</strong></td>
      <td>1.42774</td>
    </tr>
    <tr>
      <td>Std MAE</td>
      <td>↓ better</td>
      <td><strong>0.12301</strong></td>
      <td>1.17582</td>
    </tr>
    <tr>
      <td>Std ratio (want ≈1)</td>
      <td>closer to 1</td>
      <td><strong>0.9957</strong></td>
      <td>0.8222</td>
    </tr>
    <tr>
      <td>Corr Frobenius</td>
      <td>↓ better</td>
      <td><strong>0.2866</strong></td>
      <td>0.7769</td>
    </tr>
    <tr>
      <td>KS proj (50 dirs)</td>
      <td>↓ better</td>
      <td><strong>0.05163</strong></td>
      <td>0.12971</td>
    </tr>
  </tbody>
</table>

<h3 id="fremtpl2sev-merged--numeric-only"><code class="language-plaintext highlighter-rouge">freMTPL2sev</code> (merged) — numeric only</h3>

<table>
  <thead>
    <tr>
      <th>Metric</th>
      <th>Direction</th>
      <th>PCARVFLSimulator</th>
      <th>CTGAN (300 epochs)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Fit time</td>
      <td>—</td>
      <td><strong>3.8s</strong></td>
      <td>59.0s</td>
    </tr>
    <tr>
      <td>MMD (biased)</td>
      <td>↓ better</td>
      <td>0.00076</td>
      <td><strong>0.00018</strong></td>
    </tr>
    <tr>
      <td>Energy distance</td>
      <td>↓ better</td>
      <td><strong>0.01769</strong></td>
      <td>0.06169</td>
    </tr>
    <tr>
      <td>KS stat (mean)</td>
      <td>↓ better</td>
      <td><strong>0.13814</strong></td>
      <td>0.19414</td>
    </tr>
    <tr>
      <td>KS reject rate</td>
      <td>↓ better</td>
      <td><strong>0.857</strong></td>
      <td>0.857</td>
    </tr>
    <tr>
      <td>AD stat (mean)</td>
      <td>↓ better</td>
      <td><strong>3.81354</strong></td>
      <td>25.92328</td>
    </tr>
    <tr>
      <td>AD reject rate</td>
      <td>↓ better</td>
      <td><strong>0.571</strong></td>
      <td>0.857</td>
    </tr>
    <tr>
      <td>Mean MAE</td>
      <td>↓ better</td>
      <td><strong>0.41567</strong></td>
      <td>0.86539</td>
    </tr>
    <tr>
      <td>Std MAE</td>
      <td>↓ better</td>
      <td><strong>0.16528</strong></td>
      <td>0.47315</td>
    </tr>
    <tr>
      <td>Std ratio (want ≈1)</td>
      <td>closer to 1</td>
      <td>0.9817</td>
      <td><strong>0.9701</strong></td>
    </tr>
    <tr>
      <td>Corr Frobenius</td>
      <td>↓ better</td>
      <td><strong>0.3065</strong></td>
      <td>0.8698</td>
    </tr>
    <tr>
      <td>KS proj (50 dirs)</td>
      <td>↓ better</td>
      <td><strong>0.05793</strong></td>
      <td>0.08365</td>
    </tr>
  </tbody>
</table>

<h3 id="fremtpl2freq--numeric--target-encoded-categoricals"><code class="language-plaintext highlighter-rouge">freMTPL2freq</code> — numeric + target-encoded categoricals</h3>

<table>
  <thead>
    <tr>
      <th>Metric</th>
      <th>Direction</th>
      <th>PCARVFLSimulator</th>
      <th>CTGAN (300 epochs)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Fit time</td>
      <td>—</td>
      <td><strong>4.3s</strong></td>
      <td>54.8s</td>
    </tr>
    <tr>
      <td>MMD (biased)</td>
      <td>↓ better</td>
      <td>0.00210</td>
      <td><strong>0.00000</strong></td>
    </tr>
    <tr>
      <td>Energy distance</td>
      <td>↓ better</td>
      <td><strong>0.03199</strong></td>
      <td>0.32201</td>
    </tr>
    <tr>
      <td>KS stat (mean)</td>
      <td>↓ better</td>
      <td><strong>0.16247</strong></td>
      <td>0.21083</td>
    </tr>
    <tr>
      <td>KS reject rate</td>
      <td>↓ better</td>
      <td><strong>0.800</strong></td>
      <td>0.900</td>
    </tr>
    <tr>
      <td>AD stat (mean)</td>
      <td>↓ better</td>
      <td><strong>8.79878</strong></td>
      <td>63.19130</td>
    </tr>
    <tr>
      <td>AD reject rate</td>
      <td>↓ better</td>
      <td><strong>0.800</strong></td>
      <td>1.000</td>
    </tr>
    <tr>
      <td>Mean MAE</td>
      <td>↓ better</td>
      <td><strong>0.19352</strong></td>
      <td>0.96904</td>
    </tr>
    <tr>
      <td>Std MAE</td>
      <td>↓ better</td>
      <td><strong>0.08292</strong></td>
      <td>0.66499</td>
    </tr>
    <tr>
      <td>Std ratio (want ≈1)</td>
      <td>closer to 1</td>
      <td><strong>0.9892</strong></td>
      <td>0.8339</td>
    </tr>
    <tr>
      <td>Corr Frobenius</td>
      <td>↓ better</td>
      <td><strong>0.5270</strong></td>
      <td>1.0160</td>
    </tr>
    <tr>
      <td>KS proj (50 dirs)</td>
      <td>↓ better</td>
      <td><strong>0.04996</strong></td>
      <td>0.13840</td>
    </tr>
  </tbody>
</table>

<h3 id="fremtpl2sev-merged--numeric--target-encoded-categoricals"><code class="language-plaintext highlighter-rouge">freMTPL2sev</code> (merged) — numeric + target-encoded categoricals</h3>

<table>
  <thead>
    <tr>
      <th>Metric</th>
      <th>Direction</th>
      <th>PCARVFLSimulator</th>
      <th>CTGAN (300 epochs)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Fit time</td>
      <td>—</td>
      <td><strong>4.0s</strong></td>
      <td>57.7s</td>
    </tr>
    <tr>
      <td>MMD (biased)</td>
      <td>↓ better</td>
      <td><strong>0.00113</strong></td>
      <td>0.00071</td>
    </tr>
    <tr>
      <td>Energy distance</td>
      <td>↓ better</td>
      <td><strong>0.02130</strong></td>
      <td>0.13080</td>
    </tr>
    <tr>
      <td>KS stat (mean)</td>
      <td>↓ better</td>
      <td><strong>0.15955</strong></td>
      <td>0.20471</td>
    </tr>
    <tr>
      <td>KS reject rate</td>
      <td>↓ better</td>
      <td><strong>0.909</strong></td>
      <td>1.000</td>
    </tr>
    <tr>
      <td>AD stat (mean)</td>
      <td>↓ better</td>
      <td><strong>8.03864</strong></td>
      <td>20.63413</td>
    </tr>
    <tr>
      <td>AD reject rate</td>
      <td>↓ better</td>
      <td><strong>0.636</strong></td>
      <td>1.000</td>
    </tr>
    <tr>
      <td>Mean MAE</td>
      <td>↓ better</td>
      <td><strong>0.26444</strong></td>
      <td>0.47455</td>
    </tr>
    <tr>
      <td>Std MAE</td>
      <td>↓ better</td>
      <td><strong>0.11333</strong></td>
      <td>0.69187</td>
    </tr>
    <tr>
      <td>Std ratio (want ≈1)</td>
      <td>closer to 1</td>
      <td>0.9822</td>
      <td><strong>1.0198</strong></td>
    </tr>
    <tr>
      <td>Corr Frobenius</td>
      <td>↓ better</td>
      <td><strong>0.5038</strong></td>
      <td>1.5450</td>
    </tr>
    <tr>
      <td>KS proj (50 dirs)</td>
      <td>↓ better</td>
      <td><strong>0.05155</strong></td>
      <td>0.10660</td>
    </tr>
  </tbody>
</table>

<p>Across all four runs, <strong>PCARVFLSimulator wins on the large majority of adequacy metrics</strong> —
energy distance, moment matching, correlation-structure preservation, and the random-projection
KS sweep — while fitting roughly <strong>10–15x faster</strong> than CTGAN (seconds vs ~1 minute at this
sample size; the gap would grow further on the full 678k-row file).</p>]]></content><author><name></name></author><category term="Python" /><summary type="html"><![CDATA[This notebook applies PCARVFLSimulator (from the Python package synthe) — a GAN-like tabular data synthesizer built from PCA scores, a Random Vector Functional-Link (RVFL) network, and residual bootstrapping — to the French Motor Third-Party Liability (freMTPL2) data found in PNM0792/auto-insurance-pricing, and compares it against CTGAN, a GAN architecture purpose-built for tabular data.]]></summary></entry><entry><title type="html">‘Zero-Shot Probabilistic Stock Returns Forecasting with Pretrained RVFL Networks’ accepted at COPA 2026 (and to appear in the Proceedings of Machine Learning Research)</title><link href="https://thierrymoudiki.github.io/blog/2026/08/15/r/metalearned-ridge2f" rel="alternate" type="text/html" title="‘Zero-Shot Probabilistic Stock Returns Forecasting with Pretrained RVFL Networks’ accepted at COPA 2026 (and to appear in the Proceedings of Machine Learning Research)" /><published>2026-08-15T00:00:00+00:00</published><updated>2026-08-15T00:00:00+00:00</updated><id>https://thierrymoudiki.github.io/blog/2026/08/15/r/metalearned-ridge2f</id><content type="html" xml:base="https://thierrymoudiki.github.io/blog/2026/08/15/r/metalearned-ridge2f"><![CDATA[<p>As <a href="https://thierrymoudiki.github.io/blog/2025/09/04/r/python/copa-conf-2025">for last year</a>, I’ve got a poster accepted at <a href="https://copa-conference.com/">COPA conference</a>.</p>

<p>This year, the poster is about <em>‘Zero-Shot Probabilistic Stock Returns Forecasting with Pretrained RVFL Networks’</em>.</p>

<p>Links to the paper and the code repository are below:</p>

<ul>
  <li><strong>Paper</strong>: <a href="https://www.researchgate.net/publication/412115839_Zero-Shot_Probabilistic_Stock_Returns_Forecasting_with_Pretrained_RVFL_Networks">https://www.researchgate.net/publication/412115839_Zero-Shot_Probabilistic_Stock_Returns_Forecasting_with_Pretrained_RVFL_Networks</a></li>
  <li><strong>R Code</strong>: <a href="https://github.com/thierrymoudiki/2026_05_28_Pretrain_Ridge2_Stocks_Full_Pipeline">https://github.com/thierrymoudiki/2026_05_28_Pretrain_Ridge2_Stocks_Full_Pipeline</a></li>
</ul>

<p><img src="/images/2026-08-15/2026-08-15-image1.png" alt="xxx" class="img-responsive" /></p>]]></content><author><name></name></author><category term="R" /><summary type="html"><![CDATA[Link to the paper and the code repository.]]></summary></entry></feed>