conformalforecast’s orchestration or its view of the
world.
That is what the array surface is for: method.fit on numpy arrays
returns a fitted object, whose interval (and, for online methods,
update) you call from then on. It takes no forecaster, no adapter and
no DataFrame.
The invariant
An array-surface signature may only accept numpy-coercible arrays and plain-Python config — never adapters, DataFrames, or library types.This holds across
methods/, nonconformity/ and panel.py, and it is
the entire reason those modules are embeddable. A method has no opinion
about how you produce forecasts, where your data lives, or what your
cross-validation looks like. It takes arrays and gives back arrays.
fit never modifies method, so the
method is a reusable, serializable description of the calibration
(method.model_dump()) and fitted is the per-model state your library
stores next to the model it calibrates. For an online method,
fitted.update(prediction, truth) advances that state in place;
fitted.model_copy(deep=True) takes a snapshot.
Construction is keyword-only (SplitConformal(alpha=0.1)) because every
method is a pydantic model, and an out-of-range alpha is a
pydantic.ValidationError rather than a bare ValueError — worth
knowing if you re-raise it as your own error type. The nonconformity
score is config too:
SplitConformal(alpha=0.1, score=AbsoluteResidual()), rejected at
construction if the method cannot invert it. fit works on a copy of
it, retained on the fitted object as score_fn_ for interval to
invert with.
Nothing here needs an adapter. An object fitted through the array
surface alone has no forecaster_, so its predict raises
CalibrationError pointing you at interval — the two surfaces do not
have to be mixed, and for an embedding they should not be.
The dependency you take on is narwhals, numpy, pydantic and scipy —
nothing else. Importing conformalforecast never imports statsforecast,
mlforecast, neuralforecast or torch.
What you get that a hand-rolled quantile does not
A library that already computesnp.quantile on its residuals is most
of the way there. What the array surface adds is the parts that are easy
to get subtly wrong:
- The exact conformal quantile. Split and CQR take
k = ceil((1 - alpha)(n + 1))as an order statistic, not an interpolated quantile. Then + 1is what makes the coverage guarantee finite-sample. The adaptive methods need a time-varying level, so ACI and AgACI use a linear quantile and NexCP and WeightedSplit a weighted one. - Version-stable arithmetic.
_quantiles.pycomputes its interpolation constant infloat64regardless of input dtype. numpy changed that behaviour in 2.5; following numpy would have shifted everyfloat32interval bound on an upgrade. Output is identical across numpy versions by contract, with tests pinning it. - NaN-aware ragged handling. Per-series effective counts, masked
order statistics, and
InsufficientCalibrationSamplesnaming the series that are too short. See ragged panels. float32preservation. No silent upcast of neural network output.- Batched math. Nothing loops over the
(n_series × horizon)grid in Python. The per-cell quantile is one gather, and the online methods keep an incrementally-sorted history rather than re-quantiling from scratch each step. SPCI, which fits one regressor per cell, is the sanctioned exception. - Six more methods. Once the array contract is satisfied,
ACI, AgACI,
NexCP, CQR,
SPCI and
WeightedSplit are the same three calls
—
fit,interval, andupdatefor the online four. CQR’sfitandintervaltake a quantile cube rather than point forecasts, with a trailing(q_lo, q_hi)axis.
The reference migration
conformalforecast.compat.mlforecast_shim is a worked example of
embedding a method behind an existing library’s interval hook —
mlforecast’s _INTERVAL_METHODS registry.
register_interval_method takes the method class plus any extra
config keywords (gamma=, rho=, seed=) and constructs one method
per requested level, since alpha comes from the level. It is
deliberately a shim, not a supported API: the expectation is that the
upstream PR copies the pattern in-repo and this module is deleted. Read
its module docstring before writing your own — it documents the three
things that turned out to be fiddly, all of which will apply to any
library:
Array layout. mlforecast’s conformity scores arrive window-major,
(cs_n_windows, n_series, cs_h), and the array surface wants
(n_series, n_windows, horizon). One transpose. Note that the shim does
not use panel.long_to_panel for this — the scores are already a
contiguous cube, and pivoting would re-sort the series axis away from
mlforecast’s own order. Getting the series axis to line up with the
caller’s is the single most common integration bug.
Score reconstruction. mlforecast drops the target column before the
hook runs, so its cs_df holds abs(prediction - truth) and the signed
residual is unrecoverable. The shim reconstructs the method’s input as
predictions = 0, truths = scores — exact under AbsoluteResidual,
and wrong under any signed or quantile score. It therefore rejects
methods whose default score is anything else, rather than producing a
plausible wrong number. If your library can preserve the sign, do; it
unlocks the signed-score methods.
A stricter calibration floor. fit requires ceil(1/alpha)
effective windows per series and raises otherwise. mlforecast’s native
methods interpolate a quantile from as few as two. This will look like a
regression to your users and is not one — it is the sample size the
requested quantile actually needs — so surface the error rather than
papering over it.
Two upstream changes the shim wants
Both are small, and both are documented in the module docstring:PredictionIntervals.__init__validatesmethodagainst a hard-coded list rather than_INTERVAL_METHODS.keys(), so a newly registered name is reachable throughget_conformal_methodbut not throughPredictionIntervals(method=...). One line.compute_conformity_scoresalready takessigned=, butMLForecast.fitnever passessigned=Trueon the ordinary path. Threading it through would make the signed-score methods usable.
Not supported by the shim
weights / target_weights (density-ratio reweighting) and
is_transfer=True (pooled cross-domain calibration) raise
NotImplementedError. NexCP’s rho decay is a different mechanism
and must not be silently substituted for density-ratio weights — they
answer different questions and would give different intervals under the
same name.
Next
- Architecture — the two surfaces and the shape conventions.
- Methods — what each method does, with the papers.
- Adapters — the forecaster-surface route, if you want the orchestration after all.

