Conformalized quantile regression.

Classes

ConformalizedQuantileRegressor

class uncertainty.conformal.cqr.ConformalizedQuantileRegressor(ConformalPredictor)

Conformalized quantile regression — valid and heteroscedastic intervals.

SplitConformalRegressor adds a constant radius everywhere, which over-covers where the noise is small and under-covers where it is large. CQR starts from a quantile regressor, which already models how the spread varies with x, and conformalises it — keeping the shape while restoring the exact coverage guarantee the quantile fit lacks on its own.
Constructor
__init__(
    self,
    lower_estimator: Any,
    upper_estimator: Any,
    alpha: float = 0.1,
    calibration_size: float = 0.25,
    random_state: Optional[int] = None) -> None,
)

Overview

  1. Fit a lower-quantile model at \alpha/2 and an upper-quantile
model at 1 - \alpha/2 on the proper training set.
  1. On the calibration set, score each sample by how far outside the
predicted band it falls — negative when comfortably inside.
  1. Take the corrected quantile of those scores.
  2. Widen (or, when the quantile fit was conservative, narrow) the band
by that amount.

Theory

With \hat{q}_{lo} and \hat{q}_{hi} the fitted quantile functions, the conformity score is the signed distance outside the band:

E_i = \max\left( \hat{q}_{lo}(x_i) - y_i,\ \ y_i - \hat{q}_{hi}(x_i) \right)

and with \hat{q} its corrected quantile the interval is

C(x) = \left[ \hat{q}_{lo}(x) - \hat{q},\ \ \hat{q}_{hi}(x) + \hat{q} \right]

A negative \hat{q} is not an error: it means the quantile models over-covered, and CQR correctly shrinks the band. Coverage remains at least 1 - \alpha either way.

Parameters

lower_estimator
Regressor
Model fitted to predict the :math:`\alpha/2` quantile. Any regressor trained with a pinball loss at that level — for example tuiml.sklearn.linear.QuantileRegressor(quantile=0.05) or tuiml.sklearn.ensemble.GradientBoostingRegressor(loss='quantile', alpha=0.05).
upper_estimator
Regressor
Model fitted to predict the :math:`1 - \alpha/2` quantile.
alpha
float = 0.1
Miscoverage level. Must match the quantile levels the two estimators were configured for, otherwise the band is valid but needlessly wide.
calibration_size
float = 0.25
Fraction of the training data held out for calibration.
random_state
int
Seed for the train/calibration split.

Attributes

scores_
np.ndarray of shape (n_calibration,)
Signed distances outside the predicted band.
quantile_
float
The additive correction applied to both edges; may be negative.
fitted_
bool
Whether fit has been called.

Notes

Complexity. Two estimator fits plus O(n \log n).

When to use. Use CQR whenever the noise level plainly depends on the input — sensor readings that degrade with range, demand that fluctuates more at high volume, any funnel-shaped residual plot. On homoscedastic data it costs a second model fit for intervals that are no tighter than split conformal's.

Because TuiML has no native quantile regressor yet, this class takes two externally-fitted quantile models rather than cloning one estimator. Pass the tuiml.sklearn wrappers named above, or any object with fit and predict.

References

Romano2019
Romano, Y., Patterson, E., & Candès, E. J. (2019). Conformalized Quantile Regression. NeurIPS, 3543-3553. :arxiv:`1905.03222`

Requires pip install tuiml[sklearn] for a quantile regressor. Passing

two mean regressors instead silently degenerates this class into

SplitConformalRegressor: both models predict

the same thing, the band has zero width, and only the constant correction

survives.

python
>>> import numpy as np
>>> from tuiml.uncertainty import ConformalizedQuantileRegressor
>>> from tuiml.sklearn.ensemble import GradientBoostingRegressor
>>> rng = np.random.default_rng(0)
>>> X = rng.uniform(0, 4, size=(400, 1))
>>> y = X[:, 0] + rng.normal(0, 0.2 + 0.5 * X[:, 0], 400)
>>> cqr = ConformalizedQuantileRegressor(
...     GradientBoostingRegressor(loss='quantile', alpha=0.05, n_estimators=50),
...     GradientBoostingRegressor(loss='quantile', alpha=0.95, n_estimators=50),
...     alpha=0.1, random_state=0)
>>> cqr.fit(X, y)
ConformalizedQuantileRegressor(alpha=0.1)
>>> widths = np.diff(cqr.predict_interval(X), axis=1).ravel()
>>> bool(widths.std() > 0.0)  # the band adapts to the local noise
True

Methods

fit (self, X: np.ndarray, y: np.ndarray) -> 'ConformalizedQuantileRegressor'

Fit both quantile models and calibrate the band correction.

Parameters
X
np.ndarray of shape (n_samples, n_features)
Training features.
y
np.ndarray of shape (n_samples,)
Training targets.
Returns
self
ConformalizedQuantileRegressor
The fitted predictor.
predict (self, X: np.ndarray) -> np.ndarray

Return the midpoint of the calibrated band as a point prediction.

Parameters
X
np.ndarray of shape (n_samples, n_features)
Test features.
Returns
y_pred
np.ndarray of shape (n_samples,)
Midpoint of the lower and upper quantile predictions.
predict_interval (self, X: np.ndarray) -> np.ndarray

Predict calibrated, input-dependent lower and upper bounds.

Parameters
X
np.ndarray of shape (n_samples, n_features)
Test features.
Returns
intervals
np.ndarray of shape (n_samples, 2)
Column 0 holds the lower bound, column 1 the upper bound. Widths vary with X wherever the quantile models say they should.
get_parameter_schema (cls) -> Dict[str, Any]

Return JSON Schema for constructor parameters.

__repr__ (self) -> str

Return a readable representation of the predictor.