<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[The Learning Gradient]]></title><description><![CDATA[The Learning Gradient]]></description><link>https://ajaykasi.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6adad7600f036ba695b0bd36/bbeb92d9-0f16-41b5-b2ee-8bf9c461cabe.png</url><title>The Learning Gradient</title><link>https://ajaykasi.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 04 Nov 2026 10:50:26 GMT</lastBuildDate><atom:link href="https://ajaykasi.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Your Forecast Says 90% Confidence. But Does It Actually Deliver?]]></title><description><![CDATA[Conformal Prediction Explained with Python, Time-Series Forecasting, and Four Experiments What happens when a model promises 90% coverage, the data…]]></description><link>https://ajaykasi.hashnode.dev/your-forecast-says-90-confidence-but-does-it-actually-deliver</link><guid isPermaLink="true">https://ajaykasi.hashnode.dev/your-forecast-says-90-confidence-but-does-it-actually-deliver</guid><category><![CDATA[AI]]></category><category><![CDATA[ML]]></category><category><![CDATA[forecasting]]></category><dc:creator><![CDATA[Ajay Kasi]]></dc:creator><pubDate>Sat, 24 Oct 2026 05:22:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6adad7600f036ba695b0bd36/2ef79038-9344-4b27-b4a1-8675bfcc4156.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Your Forecast Says 90% Confidence. But Does It Actually Deliver?</h1>
<p><strong>Conformal Prediction Explained with Python, Time-Series Forecasting, and Four Experiments</strong></p>
<p><em>What happens when a model promises 90% coverage, the data changes, and the intervals stop behaving as expected?</em></p>
<hr />
<p>Imagine you've built a machine learning model that predicts tomorrow's electricity consumption.</p>
<p>Your model says:</p>
<p><strong>Tomorrow's predicted average consumption: 1.5 kW.</strong></p>
<p>That sounds useful. But someone asks:</p>
<p><strong>"How sure are you about that prediction?"</strong></p>
<p>You could show the model's accuracy metrics, but those don't directly tell you how far tomorrow's actual consumption might be from 1.5 kW.</p>
<p>Instead, imagine the model produces:</p>
<p><strong>Prediction: 1.5 kW</strong><br /><strong>90% prediction interval: [1.1, 1.9] kW</strong></p>
<p>Now we have more than a single number. But what does that 90% mean? Does it mean 90 out of every 100 consecutive predictions must be covered? What happens when the data changes?</p>
<p>These questions led me to explore <strong>Conformal Prediction</strong>, a framework for constructing prediction intervals from calibration errors.</p>
<p>I compared three approaches:</p>
<ul>
<li><strong>Fixed Conformal Prediction:</strong> Calculate an interval adjustment once and keep it fixed.</li>
<li><strong>Rolling Conformal Prediction:</strong> Recalculate the adjustment using recent errors.</li>
<li><strong>Adaptive Conformal Inference (ACI):</strong> Adjust the miscoverage parameter using feedback from previous predictions.</li>
</ul>
<p>I tested them in four experiments, including synthetic distribution shifts and real household electricity data.</p>
<p>One result stood out: <strong>ACI achieved 90.49% coverage over the full real-world test period, but only 78.33% over the final 60 observations.</strong> Both numbers are correct. Understanding how that can happen is the heart of this article.</p>
<hr />
<h2>1. The Only ML Basics We Need</h2>
<p>In regression, a machine learning model predicts a numerical value. Here is a small <em>illustrative</em> example:</p>
<table>
<thead>
<tr>
<th>Actual electricity usage</th>
<th>Predicted usage</th>
</tr>
</thead>
<tbody><tr>
<td>1.5 kW</td>
<td>1.4 kW</td>
</tr>
<tr>
<td>1.8 kW</td>
<td>1.6 kW</td>
</tr>
<tr>
<td>1.2 kW</td>
<td>1.5 kW</td>
</tr>
<tr>
<td>2.0 kW</td>
<td>1.7 kW</td>
</tr>
<tr>
<td>1.6 kW</td>
<td>1.7 kW</td>
</tr>
</tbody></table>
<h3>Understanding prediction errors</h3>
<p>Every machine learning model makes mistakes. Suppose the model predicts <strong>1.5 kW</strong>, but actual consumption is <strong>1.8 kW</strong>. The model underestimated consumption by <strong>0.3 kW</strong>. That difference is the <strong>prediction error</strong>:</p>
<p><strong>Prediction error = Actual value − Predicted value</strong></p>
<p>Here, the actual value is the actual value, the predicted value is the prediction, and the observation number identifies the observation.</p>
<p>When the model underestimates:</p>
<p><strong>Prediction error = 1.8 − 1.5 = +0.3 kW</strong></p>
<p>When it overestimates:</p>
<p><strong>Prediction error = 1.5 − 1.8 = −0.3 kW</strong></p>
<p>For conformal prediction, we often care about the <strong>size</strong> of the mistake, regardless of direction. That's the <strong>absolute error</strong>:</p>
<p><strong>Absolute error = |Actual value − Predicted value|</strong></p>
<p>For example, |1.5 − 1.8| = 0.3 kW.</p>
<pre><code class="language-python">import numpy as np

actual = np.array([1.5, 1.8, 1.2, 2.0, 1.6])
predicted = np.array([1.4, 1.6, 1.5, 1.7, 1.7])

absolute_errors = np.abs(actual - predicted)
print("Absolute errors:", absolute_errors)
print("MAE:", absolute_errors.mean())
</code></pre>
<p>Output:</p>
<pre><code class="language-text">Absolute errors: [0.1 0.2 0.3 0.3 0.1]
MAE: 0.2
</code></pre>
<p><strong>MAE (Mean Absolute Error)</strong> measures the average size of the model's mistakes. An MAE of 0.2 kW does <strong>not</strong> mean every future error will be 0.2 kW or smaller.</p>
<h3>What makes time-series forecasting different?</h3>
<p>A time series is a sequence of observations ordered by time. To predict tomorrow's electricity usage, we use information available <strong>before tomorrow</strong>. Training on future observations and pretending they were available in the past would create <strong>data leakage</strong>.</p>
<p>That's why our experiments use chronological training, calibration, and testing periods.</p>
<hr />
<h2>2. What Does a 90% Prediction Interval Actually Mean?</h2>
<p>Suppose we produce 100 prediction intervals and then observe the actual outcomes. If 90 intervals contain their corresponding actual values, the <strong>empirical coverage</strong> is:</p>
<p><strong>Coverage = (90 ÷ 100) × 100 = 90%</strong></p>
<p>A 90% <em>target</em> means we want our procedure to cover future outcomes at approximately that rate under the relevant conditions. But <strong>target coverage, observed coverage, and a theoretical guarantee are different things</strong>.</p>
<p>Even if overall coverage is 90%, every block of 100 consecutive predictions need not achieve exactly 90%. A marginal coverage guarantee also doesn't imply 90% conditional coverage for every individual case.</p>
<h3>Coverage isn't everything</h3>
<p>Extremely wide intervals can cover almost everything while providing little useful information. An interval of <strong>[0, 100] kW</strong> might contain tomorrow's value, but it isn't a helpful forecast for most households.</p>
<p>We care about both:</p>
<ul>
<li><strong>Coverage:</strong> How often intervals contain actual outcomes.</li>
<li><strong>Sharpness:</strong> How narrow and informative the intervals are.</li>
</ul>
<hr />
<h2>3. Conformal Prediction: The Intuition</h2>
<p>Suppose we already have a trained regression model. We can examine its mistakes on a separate <strong>calibration dataset</strong>, then use those mistakes to choose an interval width for future predictions.</p>
<h3>Step 1: Split the data chronologically</h3>
<pre><code class="language-text">PAST                                                   FUTURE
|-------------------|-------------------|------------------|
       TRAINING            CALIBRATION           TESTING
   Fit the model        Measure errors       Evaluate intervals
</code></pre>
<p>The calibration observations were not used to fit the forecasting model. Using training errors could make the model appear unrealistically accurate.</p>
<h3>Step 2: Calculate nonconformity scores</h3>
<p>For ordinary symmetric regression intervals, a <strong>nonconformity score</strong> can simply be absolute error:</p>
<p><strong>Score for observation i = |Actual − Predicted|</strong></p>
<p>Suppose five calibration errors are:</p>
<p><strong>[0.1, 0.2, 0.3, 0.4, 0.5]</strong></p>
<h3>Step 3: Choose target coverage</h3>
<p>For this small teaching example, choose <strong>80% coverage</strong>. The desired miscoverage rate is:</p>
<p><strong>alpha = 1 − 0.8 = 0.2</strong></p>
<h3>Step 4: Find the conformal quantile</h3>
<p>The standard split conformal rank is:</p>
<p><strong>k = round up [(n + 1) × (1 − alpha)]</strong></p>
<p>Here n is the number of calibration scores, alpha is target miscoverage, and rounding upward means round up.</p>
<p>With five scores:</p>
<p><strong>k = round up (6 × 0.8) = 5</strong></p>
<p>So we choose the fifth-smallest error: <strong>q = 0.5</strong>.</p>
<h3>Step 5: Construct the interval</h3>
<p>If the model predicts 1.5 kW:</p>
<p><strong>Prediction interval = [prediction − q, prediction + q] = [1.0, 2.0] kW</strong></p>
<p>We didn't need to assume normally distributed errors, but conformal prediction still depends on statistical assumptions for its classical guarantee.</p>
<hr />
<h2>4. Implementing Conformal Prediction in Python</h2>
<pre><code class="language-python">import numpy as np

def conformal_quantile(scores, alpha):
    scores = np.sort(np.asarray(scores, dtype=float))
    if len(scores) == 0:
        raise ValueError("Calibration scores cannot be empty")
    if not 0 &lt; alpha &lt; 1:
        raise ValueError("alpha must be between 0 and 1")

    n = len(scores)
    k = int(np.ceil((n + 1) * (1 - alpha)))
    if k &gt; n:
        return np.inf
    return scores[k - 1]

calibration_errors = [0.1, 0.2, 0.3, 0.4, 0.5]
q = conformal_quantile(calibration_errors, alpha=0.2)
prediction = 1.5

print(f"Prediction: {prediction:.2f} kW")
print(f"Interval: [{prediction-q:.2f}, {prediction+q:.2f}] kW")
</code></pre>
<p>Output:</p>
<pre><code class="language-text">Prediction: 1.50 kW
Interval: [1.00, 2.00] kW
</code></pre>
<h3>Why can the quantile become infinite?</h3>
<p>With only five calibration scores and a <strong>90%</strong> target:</p>
<p><strong>k = round up (6 × 0.9) = 6</strong></p>
<p>But only five scores exist. The conservative finite-sample construction uses an <strong>infinite</strong> quantile rather than pretending a sixth observed score exists.</p>
<pre><code class="language-python">print(conformal_quantile(calibration_errors, alpha=0.1))
# inf
</code></pre>
<p>This gives an unbounded interval. A method can be conservative in theory while producing intervals that are unusable in practice.</p>
<hr />
<h2>5. Does Conformal Prediction Guarantee 90% Coverage?</h2>
<p>Classical split conformal prediction has a finite-sample <strong>marginal coverage</strong> guarantee when its assumptions hold, including <strong>exchangeability</strong> of calibration and future observations and separate fitting of the forecasting model:</p>
<p><strong>Probability that a new outcome is inside its interval ≥ 1 − alpha</strong></p>
<p>Exchangeability means the joint distribution doesn't change when the relevant observations are reordered. Independent, identically distributed observations are a familiar example.</p>
<p>Time series may violate exchangeability because observations depend on earlier values and the distribution can change over time. Therefore, <strong>classical split conformal guarantees do not automatically carry over to dependent, nonstationary time series</strong>.</p>
<p>That's why empirical testing matters.</p>
<hr />
<h2>6. Three Ways to Construct Conformal Intervals</h2>
<h3>Method 1: Fixed Conformal Prediction</h3>
<p>Calculate q on calibration data and leave it unchanged during testing. If q = 0.4, a forecast of 1.5 gives <strong>[1.1, 1.9]</strong>, and a forecast of 2.0 gives <strong>[1.6, 2.4]</strong>. The center moves; the width stays fixed.</p>
<pre><code class="language-python">def fixed_interval(prediction, calibration_scores, alpha):
    q = conformal_quantile(calibration_scores, alpha)
    return prediction - q, prediction + q
</code></pre>
<p><strong>Strength:</strong> Simple and stable. <strong>Weakness:</strong> Doesn't respond to changing error distributions.</p>
<h3>Method 2: Rolling Conformal Prediction</h3>
<p>Keep only a recent window of observed errors, such as the latest 100. Recalculate q as new outcomes arrive.</p>
<pre><code class="language-python">from collections import deque

class RollingConformal:
    def __init__(self, scores, alpha=0.1, window=100):
        self.scores = deque(scores[-window:], maxlen=window)
        self.alpha = alpha

    def predict_interval(self, prediction):
        q = conformal_quantile(list(self.scores), self.alpha)
        return prediction - q, prediction + q

    def update(self, actual, prediction):
        self.scores.append(abs(actual - prediction))
</code></pre>
<p><strong>Crucial:</strong> Construct the interval <em>before</em> using the actual outcome. Update only after the outcome becomes available.</p>
<p><strong>Strength:</strong> Can respond to recent changes. <strong>Weakness:</strong> Window size affects responsiveness and stability.</p>
<h3>Method 3: Adaptive Conformal Inference (ACI)</h3>
<p>ACI adjusts an internal miscoverage parameter based on whether previous intervals covered their outcomes:</p>
<p><strong>next alpha = current alpha + gamma × (target alpha − miss indicator)</strong></p>
<p>Where:</p>
<ul>
<li>alpha is target miscoverage (0.1 for 90% target coverage).</li>
<li>adaptive alpha is the adaptive miscoverage parameter.</li>
<li>gamma controls how quickly it changes.</li>
<li>miss indicator = 1 for an interval miss and 0 for a hit.</li>
</ul>
<p>A miss decreases adaptive alpha, generally leading to a more conservative next interval. A hit increases adaptive alpha, generally allowing a narrower interval.</p>
<pre><code class="language-python">class AdaptiveConformal:
    def __init__(self, target_alpha=0.1, gamma=0.005):
        self.target_alpha = target_alpha
        self.alpha_t = target_alpha
        self.gamma = gamma

    def update(self, missed):
        self.alpha_t += self.gamma * (
            self.target_alpha - int(missed)
        )
</code></pre>
<p>This snippet illustrates <strong>only the update rule</strong>, not a full production ACI implementation. A complete method also uses the adaptive alpha to choose quantiles and must handle out-of-range values and potentially infinite intervals.</p>
<p><strong>Does adaptation actually help? Let's test it.</strong></p>
<hr />
<h1>Experiment 1: What Happens When the Data Suddenly Changes?</h1>
<p>We generated a synthetic autoregressive series:</p>
<p><strong>Current value = 0.65 × previous value + volatility × random noise</strong></p>
<p>The current observation partly depends on the previous one plus random variation. The variation scale changed from <strong>volatility = 1</strong> to <strong>volatility = 3</strong> at time step 2,200. This deliberately makes the series more volatile.</p>
<p>We kept a Ridge Regression forecasting model frozen and compared the three interval methods, targeting 90% coverage.</p>
<table>
<thead>
<tr>
<th>Method</th>
<th align="right">Pre-shift coverage</th>
<th align="right">Post-shift coverage</th>
<th align="right">Pre-shift width</th>
<th align="right">Post-shift width</th>
</tr>
</thead>
<tbody><tr>
<td>Fixed</td>
<td align="right">89.50%</td>
<td align="right">39.50%</td>
<td align="right">3.28</td>
<td align="right">3.28</td>
</tr>
<tr>
<td>Rolling (W=300)</td>
<td align="right">90.63%</td>
<td align="right">83.25%</td>
<td align="right">3.41</td>
<td align="right">9.26</td>
</tr>
<tr>
<td>ACI (W=300, γ=0.005)</td>
<td align="right">90.00%</td>
<td align="right">89.88%</td>
<td align="right">3.33</td>
<td align="right">10.87</td>
</tr>
</tbody></table>
<img src="https://cdn.hashnode.com/uploads/covers/6adad7600f036ba695b0bd36/9bb27f8f-001a-4c9f-b353-5a3653d53b20.png" alt="" style="display:block;margin:0 auto" />

<p><em>Figure 1. Fixed calibration fails to widen after the variance shift; rolling and adaptive methods react with wider intervals.</em></p>
<p>Fixed coverage collapsed to <strong>39.50%</strong> because its width stayed unchanged despite larger errors. Rolling and ACI widened their intervals, with ACI reaching <strong>89.88%</strong> post-shift coverage at the cost of greater width.</p>
<p><strong>Lesson:</strong> A calibration interval that worked yesterday may be too narrow after a distribution shift.</p>
<hr />
<h1>Experiment 2: Does the Result Hold Across Multiple Random Seeds?</h1>
<p>One random run could be misleading, so we repeated the synthetic experiment with <strong>12 seeds (40–51)</strong>, evaluating 800 pre-shift and 800 post-shift observations per seed.</p>
<h3>Average post-shift results</h3>
<table>
<thead>
<tr>
<th>Method</th>
<th align="right">Mean coverage</th>
<th align="right">Coverage SD</th>
<th align="right">Mean width</th>
</tr>
</thead>
<tbody><tr>
<td>Fixed</td>
<td align="right">40.61%</td>
<td align="right">1.63 percentage points</td>
<td align="right">3.22</td>
</tr>
<tr>
<td>Rolling (W=300)</td>
<td align="right">84.04%</td>
<td align="right">0.64 percentage points</td>
<td align="right">8.95</td>
</tr>
<tr>
<td>ACI (W=300, γ=0.005)</td>
<td align="right">89.85%</td>
<td align="right">0.45 percentage points</td>
<td align="right">10.25</td>
</tr>
</tbody></table>
<p>The pattern remained consistent across seeds. But the choice of rolling-window size mattered:</p>
<table>
<thead>
<tr>
<th>Rolling window</th>
<th align="right">Post-shift coverage</th>
<th align="right">Mean width</th>
</tr>
</thead>
<tbody><tr>
<td>100</td>
<td align="right">88.00%</td>
<td align="right">9.63</td>
</tr>
<tr>
<td>300</td>
<td align="right">84.04%</td>
<td align="right">8.95</td>
</tr>
<tr>
<td>500</td>
<td align="right">80.47%</td>
<td align="right">8.28</td>
</tr>
</tbody></table>
<p>A shorter window reacted more quickly to the sudden change in this experiment, though shorter windows can also be more sensitive to noise.</p>
<p>We also varied ACI's learning rate gamma. More aggressive settings could lead to <strong>infinite mean interval widths</strong> when the adaptive quantile rank exceeded the available calibration sample.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6adad7600f036ba695b0bd36/5877db21-ca71-4562-b8d0-9136791fa9c5.png" alt="" style="display:block;margin:0 auto" />

<p><em>Figure 2. The results persist across seeds, but better coverage can come with wider or even unbounded intervals.</em></p>
<p><strong>Lesson:</strong> Coverage alone doesn't tell us whether an interval method is practically useful.</p>
<hr />
<h1>Experiment 3: Conformal Prediction on Real Electricity Consumption</h1>
<p>We next used the <strong>UCI Individual Household Electric Power Consumption dataset</strong>:</p>
<p><a href="https://archive.ics.uci.edu/dataset/235/individual+household+electric+power+consumption">https://archive.ics.uci.edu/dataset/235/individual+household+electric+power+consumption</a></p>
<p>We aggregated <code>Global_active_power</code> into <strong>daily average active power (kW)</strong> and predicted the next day's average using Ridge Regression.</p>
<p>Our features included previous consumption at lags of <strong>1, 2, 7, 14, and 28 days</strong>, plus cyclical day-of-week and month encodings.</p>
<pre><code class="language-python">import numpy as np

day_of_week = 6  # Sunday, when Monday is 0
dow_sin = np.sin(2 * np.pi * day_of_week / 7)
dow_cos = np.cos(2 * np.pi * day_of_week / 7)
</code></pre>
<p>Sine and cosine represent the weekly cycle without treating Sunday and Monday as far apart.</p>
<h3>Chronological data split</h3>
<table>
<thead>
<tr>
<th>Dataset</th>
<th align="right">Observations</th>
<th>Purpose</th>
</tr>
</thead>
<tbody><tr>
<td>Training</td>
<td align="right">789</td>
<td>Fit Ridge Regression</td>
</tr>
<tr>
<td>Calibration</td>
<td align="right">263</td>
<td>Initialize interval calibration</td>
</tr>
<tr>
<td>Testing</td>
<td align="right">263</td>
<td>Evaluate future intervals</td>
</tr>
</tbody></table>
<p>The test period ran from <strong>January 4, 2010 to November 26, 2010</strong>. We used a rolling window of 100 and ACI gamma = 0.005.</p>
<h3>Results</h3>
<table>
<thead>
<tr>
<th>Method</th>
<th align="right">Coverage</th>
<th align="right">Mean width (kW)</th>
<th align="right">Mean interval score</th>
</tr>
</thead>
<tbody><tr>
<td>Fixed</td>
<td align="right">89.73%</td>
<td align="right">0.7103</td>
<td align="right">1.0864</td>
</tr>
<tr>
<td>Rolling</td>
<td align="right">90.49%</td>
<td align="right">0.8106</td>
<td align="right">1.0903</td>
</tr>
<tr>
<td>ACI</td>
<td align="right">90.49%</td>
<td align="right">0.7915</td>
<td align="right">1.0795</td>
</tr>
</tbody></table>
<p><strong>There was no universal winner.</strong> Fixed intervals were narrowest. ACI had the lowest mean interval score, but differences were small, and both ACI and Rolling had identical overall coverage.</p>
<h3>What is interval score?</h3>
<p>Interval score rewards narrow intervals and penalizes observations outside the bounds:</p>
<p><strong>Interval score = interval width + penalty for falling below lower bound + penalty for exceeding upper bound</strong></p>
<p>In plain language: <strong>start with interval width, then add a penalty for each miss</strong>. Lower is better.</p>
<pre><code class="language-python">def interval_score(actual, lower, upper, alpha=0.1):
    width = upper - lower
    below = max(lower - actual, 0)
    above = max(actual - upper, 0)
    return width + (2 / alpha) * (below + above)
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6adad7600f036ba695b0bd36/50c9005d-2598-44c3-855a-afa7edefec5f.png" alt="" style="display:block;margin:0 auto" />

<p><em>Figure 3. The three methods approach 90% overall coverage but differ in sharpness and interval score.</em></p>
<img src="https://cdn.hashnode.com/uploads/covers/6adad7600f036ba695b0bd36/5d695576-2d78-421c-b113-e0ecee7e6dac.png" alt="" style="display:block;margin:0 auto" />

<p><em>Figure 4. The real-world ACI intervals compared with observed daily average power.</em></p>
<p>At first glance, ACI looked promising. But rolling coverage began deteriorating toward the end of the test period.</p>
<p>That led to Experiment 4.</p>
<hr />
<h1>Experiment 4: The Hidden Problem Behind 90% Overall Coverage</h1>
<p>ACI achieved <strong>90.49% overall coverage</strong>, yet the end of the test period looked less reliable.</p>
<p>We performed a <strong>post-hoc diagnostic analysis</strong> of the existing predictions. We did <strong>not</strong> refit or retune the models. We examined rolling coverage, widths, adaptive alpha, and errors.</p>
<h3>Overall vs. local coverage</h3>
<table>
<thead>
<tr>
<th>Method</th>
<th align="right">Overall coverage</th>
<th align="right">First 60 coverage</th>
<th align="right">Last 60 coverage</th>
</tr>
</thead>
<tbody><tr>
<td>Fixed</td>
<td align="right">89.73%</td>
<td align="right">76.67%</td>
<td align="right">88.33%</td>
</tr>
<tr>
<td>Rolling</td>
<td align="right">90.49%</td>
<td align="right">85.00%</td>
<td align="right">80.00%</td>
</tr>
<tr>
<td>ACI</td>
<td align="right">90.49%</td>
<td align="right">86.67%</td>
<td align="right">78.33%</td>
</tr>
</tbody></table>
<p>For ACI, <strong>13 of the last 60 observations fell outside their intervals</strong>. That's <strong>78.33% coverage</strong> for that window despite 90.49% coverage overall.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6adad7600f036ba695b0bd36/b6650661-4fd3-4f54-9688-3fc04fee06f9.png" alt="" style="display:block;margin:0 auto" />

<p><em>Figure 5. Rolling coverage exposes undercoverage that an aggregate metric can hide.</em></p>
<h3>What changed for ACI?</h3>
<table>
<thead>
<tr>
<th>ACI metric</th>
<th align="right">First 60</th>
<th align="right">Last 60</th>
</tr>
</thead>
<tbody><tr>
<td>Coverage</td>
<td align="right">86.67%</td>
<td align="right">78.33%</td>
</tr>
<tr>
<td>Misses</td>
<td align="right">8</td>
<td align="right">13</td>
</tr>
<tr>
<td>Mean interval width (kW)</td>
<td align="right">0.9745</td>
<td align="right">0.5771</td>
</tr>
<tr>
<td>Mean absolute error (kW)</td>
<td align="right">0.2399</td>
<td align="right">0.1979</td>
</tr>
<tr>
<td>Mean adaptive alpha</td>
<td align="right">0.0981</td>
<td align="right">0.1236</td>
</tr>
<tr>
<td>Mean interval score</td>
<td align="right">1.3258</td>
<td align="right">1.3819</td>
</tr>
</tbody></table>
<p>Three things stood out:</p>
<ol>
<li><strong>Average point prediction error improved:</strong> MAE fell from 0.2399 to 0.1979 kW.</li>
<li><strong>Intervals narrowed sharply:</strong> Mean width fell from 0.9745 to 0.5771 kW, roughly <strong>41%</strong>.</li>
<li><strong>Adaptive alpha increased:</strong> Its mean rose from 0.0981 to 0.1236, consistent with less conservative quantiles.</li>
</ol>
<p>These observations are <strong>consistent with</strong> intervals becoming too narrow for some remaining errors. They do not, by themselves, prove a causal mechanism. Lower average error also doesn't rule out occasional large errors.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6adad7600f036ba695b0bd36/e48fb694-e950-479a-9c81-cabf855cfad2.png" alt="" style="display:block;margin:0 auto" />

<p><em>Figure 6. Adaptive parameters and interval widths evolve, but some observations still escape the intervals.</em></p>
<p>This matters operationally. A dashboard displaying only <strong>90.49% overall coverage</strong> could conceal recent undercoverage.</p>
<p>One caution: 60-observation coverage estimates are noisy, and overlapping rolling windows are not independent. Local coverage plots are <strong>diagnostics</strong>, not a guarantee of conditional coverage at every point.</p>
<hr />
<h2>7. What Should We Monitor in Production?</h2>
<p>Our experiments suggest monitoring:</p>
<ul>
<li><strong>Point forecast error (MAE/RMSE):</strong> Is the forecasting model itself deteriorating?</li>
<li><strong>Overall interval coverage:</strong> How often do intervals contain actual observations?</li>
<li><strong>Rolling coverage:</strong> Are recent periods undercovered?</li>
<li><strong>Interval width:</strong> Are intervals becoming too narrow or too broad?</li>
<li><strong>Interval score:</strong> Are we balancing sharpness with misses?</li>
<li><strong>Infinite interval rate:</strong> Are any intervals unbounded or unusable?</li>
<li><strong>Adaptive alpha:</strong> How is ACI changing its calibration level?</li>
<li><strong>Feedback delay:</strong> Are updates applied only after actual outcomes arrive?</li>
</ul>
<p>A one-day-ahead forecast and a 90-day-ahead forecast have very different feedback schedules. A production implementation must respect those delays.</p>
<hr />
<h2>8. Five Things I Learned</h2>
<p><strong>1. Accuracy isn't the same as uncertainty reliability.</strong> Low MAE doesn't automatically mean well-calibrated intervals.</p>
<p><strong>2. Distribution shift can break fixed calibration.</strong> Synthetic post-shift fixed coverage fell to 39.50%.</p>
<p><strong>3. Adaptation has costs.</strong> Rolling and adaptive intervals widened, and some aggressive ACI settings produced infinite intervals.</p>
<p><strong>4. Complexity doesn't guarantee superiority.</strong> On real electricity data, fixed conformal intervals were narrower while still achieving coverage near the target.</p>
<p><strong>5. Aggregate coverage can hide local failures.</strong> ACI's 90.49% overall coverage coexisted with 78.33% coverage in the last 60 observations.</p>
<hr />
<h2>9. Glossary: Conformal Prediction Without the Jargon</h2>
<table>
<thead>
<tr>
<th>Term</th>
<th>Plain-language meaning</th>
</tr>
</thead>
<tbody><tr>
<td>Point prediction</td>
<td>A single predicted value, like 1.5 kW.</td>
</tr>
<tr>
<td>Residual</td>
<td>Actual value minus predicted value.</td>
</tr>
<tr>
<td>Absolute error</td>
<td>The size of a mistake, ignoring its sign.</td>
</tr>
<tr>
<td>MAE</td>
<td>Average absolute prediction error.</td>
</tr>
<tr>
<td>Time series</td>
<td>Data ordered by time.</td>
</tr>
<tr>
<td>Lag feature</td>
<td>An earlier observation used to predict a later one.</td>
</tr>
<tr>
<td>Data leakage</td>
<td>Using information unavailable at prediction time.</td>
</tr>
<tr>
<td>Calibration set</td>
<td>Held-out data used to set interval adjustments.</td>
</tr>
<tr>
<td>Prediction interval</td>
<td>A range intended to contain a future outcome.</td>
</tr>
<tr>
<td>Coverage</td>
<td>Share of actual outcomes contained in intervals.</td>
</tr>
<tr>
<td>Miscoverage</td>
<td>Share of outcomes outside intervals.</td>
</tr>
<tr>
<td>Nonconformity score</td>
<td>A measure of how far an outcome differs from a prediction.</td>
</tr>
<tr>
<td>Quantile</td>
<td>A selected cutoff from ordered values.</td>
</tr>
<tr>
<td>Exchangeability</td>
<td>A statistical property where reordering observations doesn't change their joint distribution.</td>
</tr>
<tr>
<td>Marginal coverage</td>
<td>Coverage averaged over the relevant data-generating process.</td>
</tr>
<tr>
<td>Distribution shift</td>
<td>A change in how data behaves statistically.</td>
</tr>
<tr>
<td>Rolling window</td>
<td>A moving group of recent observations.</td>
</tr>
<tr>
<td>ACI</td>
<td>Adaptive Conformal Inference, which adjusts miscoverage using feedback.</td>
</tr>
<tr>
<td>Learning rate</td>
<td>How strongly an adaptive parameter updates.</td>
</tr>
<tr>
<td>Interval width</td>
<td>Upper bound minus lower bound.</td>
</tr>
<tr>
<td>Interval score</td>
<td>A metric penalizing both wide intervals and misses.</td>
</tr>
<tr>
<td>Local coverage</td>
<td>Observed coverage over a particular window or subgroup.</td>
</tr>
</tbody></table>
<hr />
<h2>10. Reproducing the Experiments</h2>
<p>The real-data experiments used the UCI household electricity dataset:</p>
<p><a href="https://archive.ics.uci.edu/dataset/235/individual+household+electric+power+consumption">https://archive.ics.uci.edu/dataset/235/individual+household+electric+power+consumption</a></p>
<p>From the experiment project directory:</p>
<pre><code class="language-bash">python src/experiment03.py --download
python src/experiment04.py
python -m pytest -q
</code></pre>
<p>The first command runs the real-world electricity experiment, downloading data if needed. The second diagnoses its existing predictions without refitting. The third runs the automated tests. In the completed run, <strong>12 tests passed</strong>.</p>
<p>The results are specific to these datasets, settings, model, and evaluation windows. They are <strong>not</strong> universal rankings of conformal prediction methods.</p>
<hr />
<h2>Final Thoughts: A 90% Promise Is Only as Useful as Its Assumptions</h2>
<p>Conformal prediction gives us a principled way to build prediction intervals using calibration errors. But producing an interval is only the beginning.</p>
<p>We also need to ask whether the assumptions hold, whether intervals respond to changes, whether they remain practically useful, and whether coverage is stable over recent windows.</p>
<p>The synthetic experiments showed how badly fixed intervals can fail under a sudden variance shift. The real-world experiment showed that simpler methods can remain competitive. And our final diagnostic analysis exposed the key lesson:</p>
<p><strong>90% overall coverage doesn't mean 90% coverage everywhere, all the time.</strong></p>
<p>The important question isn't only:</p>
<p><strong>"What will happen tomorrow?"</strong></p>
<p>It's also:</p>
<p><strong>"How much should we trust what our model says about tomorrow — and will that trust still be justified when the world changes?"</strong></p>
<hr />
<p><em>If you're working with forecasting models, try evaluating rolling coverage alongside overall coverage. You may discover patterns that a single aggregate metric never reveals.</em></p>
]]></content:encoded></item></channel></rss>