# QuantileGAM


Non-crossing quantile GAM.


Usage

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


Fits a separate additive quantile regression model for each requested quantile level [tau](QuantileFamily.md#whittaker.QuantileFamily.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](QuantileFamily.md#whittaker.QuantileFamily.tau), provide no such guarantee and can produce curves that cross, especially in regions with sparse data or heavy smoothing.

Use [QuantileGAM](QuantileGAM.md#whittaker.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](QuantileFamily.md#whittaker.QuantileFamily.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()](calibrate_sigma.md#whittaker.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


``` python
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](#formula) | The parsed model formula shared by every quantile level. |
| [is_fitted](#is_fitted) | Whether `fit()` has been called successfully. |
| [non_crossing](#non_crossing) | Whether the non-crossing constraint is enforced. |
| [quantiles](#quantiles) | Quantile levels fitted by this model, in ascending order. |
| [sigma](#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()](#coverage) | Compute empirical coverage of the outermost quantile interval. |
| [crossing_fraction()](#crossing_fraction) | Fraction of observations where quantile curves cross. |
| [fit()](#fit) | Fit the quantile GAM. |
| [predict()](#predict) | Predict quantiles on new data. |
| [predict_interval()](#predict_interval) | Return prediction interval from the lowest and highest quantiles. |
| [summary()](#summary) | Return a text summary of the fitted quantile GAM. |

------------------------------------------------------------------------


### coverage()


Compute empirical coverage of the outermost quantile interval.


Usage

``` python
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

``` python
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

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


Fits a separate ELF-based [GAM](GAM.md#whittaker.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](GAM.md#whittaker.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](GAM.md#whittaker.GAM).


#### Returns


`QuantileGAM`  
Returns `self` for method chaining.


------------------------------------------------------------------------


### predict()


Predict quantiles on new data.


Usage

``` python
predict(
    new_data,
    *,
    se=False,
)
```


Predicts each quantile level's [GAM](GAM.md#whittaker.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

``` python
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

``` python
summary()
```


Reports the formula, fitted quantile levels, non-crossing setting, and ELF `sigma`, followed by one line per quantile giving its [GAM](GAM.md#whittaker.GAM)'s total effective degrees of freedom and deviance.


#### Returns


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