QuantileGAM

Non-crossing quantile GAM.

Usage

Source

QuantileGAM(
    formula,
    quantiles=None,
    *,
    sigma=0.1,
    non_crossing=True,
)

Fits a separate additive quantile regression model for each requested quantile level tau, and enforces the natural ordering constraint that quantile curves must not cross: for tau_1 < tau_2, the fitted curve q_{tau_1}(x) must lie at or below q_{tau_2}(x) at every observed covariate combination. Ordinary quantile GAMs, fit independently for each tau, provide no such guarantee and can produce curves that cross, especially in regions with sparse data or heavy smoothing.

Use QuantileGAM whenever you need multiple quantiles of a conditional distribution (e.g. to build a prediction interval or characterize skewness/heteroscedasticity) and want the estimated quantiles to respect the required monotone ordering in tau.

Parameters

formula: str | Formula

Model formula (e.g. "y ~ s(x)"), shared across all quantile levels; only the loss function differs between them.

quantiles: list[float] | None = None

Quantile levels to fit. Must be in (0, 1) and will be sorted. Defaults to [0.1, 0.25, 0.5, 0.75, 0.9].

sigma: float = 0.1

Bandwidth of the smoothed pinball (“extended log-F”, ELF) loss used to approximate the non-differentiable quantile check loss. Smaller sigma more closely approximates the true quantile loss but can slow IRLS convergence; use calibrate_sigma() to select it via cross-validation.

non_crossing: bool = True
If True (default), enforce the non-crossing constraint via iterative isotonic projection. If False, quantiles are fit completely independently and may cross.

Notes

Each quantile is fit by minimizing a smoothed pinball loss (the ELF loss of Fasiolo et al. 2021), which approximates the quantile check function

\rho_\tau(u) = u \, (\tau - \mathbb{1}[u < 0])

with a twice-differentiable surrogate suitable for IRLS. After each round of fitting, the vector of fitted quantiles at every observation, [q_{\tau_1}(x_i), \dots, q_{\tau_k}(x_i)], is checked for monotonicity; if it is violated anywhere, the vector is projected onto the monotone non-decreasing cone via the pool-adjacent-violators algorithm (PAVA), following the “stepwise projection” strategy of Bondell, Reich, & Wang (2010). The projected fitted values are then used to re-derive coefficients (via a least-squares refit against the corrected working response), and the cycle repeats for up to max_iter rounds or until no crossings remain.

Examples

import numpy as np
from whittaker.quantile_gam import QuantileGAM

rng = np.random.default_rng(0)
n = 500
x = rng.uniform(0, 1, n)
y = np.sin(2 * np.pi * x) + rng.normal(scale=0.2 + 0.3 * x, size=n)

model = QuantileGAM("y ~ s(x)", quantiles=[0.1, 0.25, 0.5, 0.75, 0.9])
model.fit({"x": x, "y": y})
preds = model.predict({"x": x[:5]})  # dict of tau -> PredictionResult
print(model.crossing_fraction())
0.0

Attributes

Name Description
formula The parsed model formula shared by every quantile level.
is_fitted Whether fit() has been called successfully.
non_crossing Whether the non-crossing constraint is enforced.
quantiles Quantile levels fitted by this model, in ascending order.
sigma Bandwidth of the smoothed pinball (ELF) loss used for every quantile.

formula

The parsed model formula shared by every quantile level.

formula: Formula


is_fitted

Whether fit() has been called successfully.

is_fitted: bool


non_crossing

Whether the non-crossing constraint is enforced.

non_crossing: bool


quantiles

Quantile levels fitted by this model, in ascending order.

quantiles: list[float]


sigma

Bandwidth of the smoothed pinball (ELF) loss used for every quantile.

sigma: float

Methods

Name Description
coverage() Compute empirical coverage of the outermost quantile interval.
crossing_fraction() Fraction of observations where quantile curves cross.
fit() Fit the quantile GAM.
predict() Predict quantiles on new data.
predict_interval() Return prediction interval from the lowest and highest quantiles.
summary() Return a text summary of the fitted quantile GAM.

coverage()

Compute empirical coverage of the outermost quantile interval.

Usage

Source

coverage(data=None)

Parameters

data: InputData | None = None
Data to evaluate on. Defaults to the training data.

Returns

float
Fraction of observations within [q_low, q_high].

crossing_fraction()

Fraction of observations where quantile curves cross.

Usage

Source

crossing_fraction(data=None)

Parameters

data: InputData | None = None
Data to evaluate on. Defaults to the training data.

Returns

float
Fraction of observations with at least one crossing (0.0 if non-crossing constraint is satisfied everywhere).

fit()

Fit the quantile GAM.

Usage

Source

fit(
    data,
    *,
    method="REML",
    max_iter=5,
    select=False,
)

Fits a separate ELF-based GAM for every quantile level in self.quantiles. When non_crossing=True, this is repeated for up to max_iter rounds: after each round, the fitted quantile curves are checked for crossing violations and, if found, corrected via isotonic projection (see class Notes); the corrected values are used to re-derive each quantile model’s coefficients before the next round of refitting.

Parameters

data: InputData

Column-oriented data containing the response and all covariates in formula.

method: str = "REML"

Smoothing parameter selection method applied to every quantile’s GAM fit: "GCV", "REML" (default), or "ML".

max_iter: int = 5

Number of non-crossing projection iterations. Each iteration fits all quantiles and projects to enforce ordering. Ignored when non_crossing=False (in which case a single independent fit per quantile is performed).

select: bool = False
If True, enable double-penalty variable selection for each quantile’s GAM.

Returns

QuantileGAM
Returns self for method chaining.

predict()

Predict quantiles on new data.

Usage

Source

predict(
    new_data,
    *,
    se=False,
)

Predicts each quantile level’s GAM independently on new_data, then, if non_crossing=True, re-applies the isotonic projection across quantile levels at each new observation so the returned predictions never cross, even if the fitted models happen to disagree slightly on out-of-sample covariate combinations.

Parameters

new_data: InputData

New covariate data.

se: bool = False
If True, include standard errors for each quantile’s linear predictor.

Returns

dict[float, PredictionResult]
Dict mapping quantile level to predictions.

predict_interval()

Return prediction interval from the lowest and highest quantiles.

Usage

Source

predict_interval(
    new_data,
    lower_tau=None,
    upper_tau=None,
)

Parameters

new_data: InputData

New covariate data.

lower_tau: float | None = None

Lower quantile level. Defaults to the smallest fitted quantile.

upper_tau: float | None = None
Upper quantile level. Defaults to the largest fitted quantile.

Returns

tuple[NDArray, NDArray]
(lower, upper) prediction bounds.

summary()

Return a text summary of the fitted quantile GAM.

Usage

Source

summary()

Reports the formula, fitted quantile levels, non-crossing setting, and ELF sigma, followed by one line per quantile giving its GAM’s total effective degrees of freedom and deviance.

Returns

str
Multi-line, human-readable summary suitable for printing.