Skip to content
RgramIntervals

Pointwise bootstrap intervals

What this example reviews

This example reviews sampling uncertainty around a fitted regression curve. Independent synthetic feature values are sampled uniformly, then explicitly sorted before independent response noise is generated. A Gaussian local-constant fit uses bandwidth 0.4. Paired IID bootstrap resampling refits the model and evaluates bounds at 100 locations in the original observed feature range.

Fitted local mean with pointwise bootstrap confidence bounds and the known synthetic generating mean.

Choices and their impact

Item What is being reviewed Impact and interpretation
Gray responses Variation of individual observations Their vertical spread is response noise, which is not the target of the shaded curve intervals.
Blue fitted curve The local response mean estimated from the original data Remains the reference estimate while the bootstrap describes variation across resampled fits.
Shaded 95% bounds Pointwise percentile bootstrap intervals At each query, bounds come from the distribution of bootstrap predictions. Connecting them is a display choice, not a simultaneous band.
Dashed generating mean Known synthetic relationship Makes possible smoothing bias visible; the bootstrap does not correct that bias.
100 resamples and fixed seed Monte Carlo calculation and reproducibility The seed makes this run reproducible. More draws improve numerical stability of bootstrap quantiles, not the validity of sampling assumptions.
100 query locations Where uncertainty is evaluated More locations increase plotting detail and computation without adding training data.

Interpreting the plot

Read the shaded region as uncertainty about a fitted curve at each separate location. It is not the expected spread of a future response and does not imply 95% coverage of the entire curve at once. Compare the original fit with the dashed mean as well as with its bounds: systematic smoothing bias may remain even when bounds look narrow.

Inspect n_valid and bootstrap_coverage before interpreting a bound. They count finite bootstrap predictions, rather than estimating nominal confidence coverage. The default requires every draw to support a query; otherwise bounds remain NaN. Resampled feature ranges may differ from the original range, so extrapolation advisories can occur during this example even though the original queries are in range.

When this is useful

Use this workflow to explore how sensitive a curve statistic is to sampling and to communicate its pointwise bootstrap variability. It is more informative than drawing a response-spread envelope and calling it uncertainty about the mean.

Paired IID resampling assumes independent random-design rows. Grouped or ordered dependent observations need appropriate resampling choices. The selected bandwidth is held fixed: these bounds omit bandwidth-selection uncertainty and smoothing-bias correction. They do not remove confounding or certify extrapolation.

Explore further

Increase n_resamples, retaining the seed and data, to examine Monte Carlo stability. Change confidence_level to compare nominal interval levels. For a suitable dependent dataset, investigate group/block resampling rather than interpreting IID bounds as universally valid.

Run this example

From the checkout, with rgram[plot] installed:

python examples/plot_relationships.py --example intervals
# Save just this figure without opening a desktop window:
python examples/plot_relationships.py --example intervals --output-dir /tmp/rgram-figures

Download this standalone example. The complete code below includes imports, data, configuration, and plotting. Run the downloaded file with python intervals.py.

from __future__ import annotations

import argparse
from pathlib import Path
from typing import TYPE_CHECKING
import warnings

import numpy as np

from rgram import KernelSmoother, Regressogram, SupportWarning, quantile

if TYPE_CHECKING:
    from matplotlib.axes import Axes
    from matplotlib.figure import Figure

# A consistent palette makes comparisons readable across the gallery.
COLORS = ["#1764ab", "#de682b", "#21856b", "#8855a2"]


def _create_axes(
    title: str, xlabel: str = "Feature", ylabel: str = "Response"
) -> tuple[Figure, Axes]:
    """Create a readable, single-panel figure with consistent gallery styling."""
    import matplotlib.pyplot as plt

    with plt.rc_context({"font.size": 14, "axes.labelsize": 16, "axes.titlesize": 20}):
        figure, ax = plt.subplots(figsize=(10, 6.6), layout="constrained")
        ax.set(title=title, xlabel=xlabel, ylabel=ylabel)
    ax.set_facecolor("#f8fafc")
    ax.grid(color="#dce3ec", linewidth=0.8)
    ax.set_axisbelow(True)
    ax.spines[["top", "right"]].set_visible(False)
    ax.tick_params(labelsize=13)
    return figure, ax


def _plot_observations(ax: Axes, X: np.ndarray, y: np.ndarray) -> None:
    """Show original rows without connecting them or hiding response spread."""
    ax.scatter(
        X,
        y,
        color="#64748b",
        alpha=0.4,
        s=28,
        edgecolors="none",
        label="Observed responses",
    )


def _add_legend(ax: Axes) -> None:
    """Identify every plotted comparison using the same readable legend style."""
    ax.legend(fontsize=12, framealpha=0.96, edgecolor="#dce3ec", loc="best")


def _gaussian_smoother(bandwidth: float, **kwargs) -> KernelSmoother:
    """Keep kernel and bandwidth units explicit; kwargs select other model options."""
    return KernelSmoother(
        kernel="gaussian", bandwidth="manual", bandwidth_value=bandwidth, **kwargs
    )


def intervals() -> Figure:
    """Display supported pointwise bootstrap bounds for a regression curve."""
    rng = np.random.default_rng(17)
    # Sample a random design with independent noise, appropriate to paired IID draws.
    # Sorting is explicit here for the display; the estimator never sorts rows.
    X = np.sort(rng.uniform(0, 6, 100))
    y = np.sin(X) + rng.normal(0, 0.3, len(X))
    # Keep original queries in the observed range; resampled ranges can differ.
    query = np.linspace(X.min(), X.max(), 100)
    model = _gaussian_smoother(0.4).fit(X, y)
    # Fix the seed for reproducibility. More draws reduce Monte Carlo variation,
    # but do not correct smoothing bias or include bandwidth-selection uncertainty.
    interval = model.predict_interval(query, n_resamples=100, random_state=17)
    figure, ax = _create_axes("Pointwise bootstrap uncertainty for the curve")
    _plot_observations(ax, X, y)
    # Connecting pointwise bounds is a display choice, not a simultaneous band.
    # NaN bounds would remain gaps rather than being silently filled.
    ax.fill_between(
        query,
        interval["lower"].to_numpy(),
        interval["upper"].to_numpy(),
        color=COLORS[0],
        alpha=0.2,
        label="95% pointwise bootstrap bounds",
    )
    ax.plot(
        query,
        interval["prediction"].to_numpy(),
        color=COLORS[0],
        lw=3,
        label="Fitted local mean",
    )
    ax.plot(query, np.sin(query), "--", color="#334155", lw=2, label="Generating mean")
    _add_legend(ax)
    return figure

if __name__ == "__main__":
    import matplotlib.pyplot as plt

    figure = intervals()
    plt.show()
    plt.close(figure)