# conformal_fit()


Fit a GAM with conformal calibration.


Usage

``` python
conformal_fit(
    formula,
    data,
    *,
    method="split",
    family=None,
    level=0.95,
    cal_fraction=0.25,
    n_folds=10,
    fit_method="REML",
    select=False,
    seed=None,
)
```


Fits a [GAM](GAM.md#whittaker.GAM) and calibrates it so that [ConformalPredictor.predict()](ConformalPredictor.md#whittaker.ConformalPredictor.predict) returns prediction intervals with a distribution-free, finite-sample marginal coverage guarantee, using one of three conformal methods:

- **Split conformal**: the data is randomly split into a training set (fit the GAM) and a calibration set (compute absolute residuals). The interval half-width is the `ceil((n_cal+1) * level) / n_cal` empirical quantile of the calibration residuals, giving intervals of constant width `values +/- quantile`. Simple and fast, but "wastes" data on the calibration split and can be less efficient than the alternatives.
- **CV+**: the data is split into `n_folds` folds; each fold is used to compute out-of-fold residuals from a model trained on the rest. At prediction time, all fold models' predictions are combined with all fold residuals via a min/max construction (Barber et al. 2021), giving tighter, per-observation intervals without a dedicated calibration split.
- **Jackknife+**: the leave-one-out analogue of CV+, using `n` individual leave-one-out refits. Provides the tightest intervals of the three but is the most computationally expensive since it requires `n` refits.


## Parameters


`formula: str`  
GAM formula string.

`data: InputData`  
Column-oriented data dict.

`method: str = ``"split"`  
Conformal method: `"split"` (default), `"cv+"`, or `"jackknife+"`.

`family: Family | None = None`  
Response distribution family. Defaults to [Gaussian()](Gaussian.md#whittaker.Gaussian).

`level: float = ``0.95`  
Nominal coverage probability (default `0.95`).

`cal_fraction: float = ``0.25`  
Fraction of data held out for calibration in the split method (default `0.25`). Ignored for `"cv+"` and `"jackknife+"`.

`n_folds: int = ``10`  
Number of folds for the `"cv+"` method (default `10`). Ignored for `"split"` and `"jackknife+"`.

`fit_method: str = ``"REML"`  
Smoothing parameter selection method for the GAM (default `"REML"`).

`select: bool = ``False`  
If `True`, enable double-penalty variable selection.

`seed: int | None = None`  
Random seed for data splitting.


## Notes

Split conformal computes the calibration quantile as

\hat q = \left\lceil (n\_{\text{cal}} + 1) \cdot \text{level} \right\rceil \big/ n\_{\text{cal}} \quad \text{quantile of} \quad \\\|y_i - \hat\mu(x_i)\| : i \in \text{calibration set}\\,

which, under exchangeability of calibration and test points, guarantees `P(y \in [\hat\mu(x) - \hat q, \hat\mu(x) + \hat q]) \ge \text{level}` marginally over new draws. CV+ and jackknife+ replace this single quantile with, for each test point, the appropriate quantile of the `n` (or `n_folds`) values `{fold/LOO prediction +/- that fold's residual}`, trading extra computation for tighter, locally-adapted intervals while retaining the same finite-sample coverage guarantee.


## Returns


`ConformalPredictor`  
A calibrated predictor that can produce intervals on new data.


## Examples


``` python
import numpy as np
from whittaker.conformal import conformal_fit, conformal_coverage

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.3, size=n)

predictor = conformal_fit("y ~ s(x)", {"x": x, "y": y}, method="split", level=0.9, seed=0)
result = predictor.predict({"x": x[:5]})
print(result.lower, result.upper)
print(conformal_coverage(predictor, {"x": x, "y": y}, response="y"))
```


    [-1.33066482  0.41215593 -0.28890212 -0.39394914 -1.5030175 ] [-0.25196998  1.49085076  0.78979271  0.6847457  -0.42432267]
    0.948
