Non-crossing quantile GAM.
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())
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.
|
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.
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.
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.
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(
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.
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.
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.
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.