///|
/// Frozen out-of-fold absolute residuals and their held-out model identifiers.
pub struct CvPlus {
priv residuals : Array[Double]
priv fold : Array[Int32]
priv folds : Int32
}
///|
/// Every row's prediction must come from a model trained without that row.
pub fn cvplus_calibrate(
truth : Array[Double],
out_of_fold : Array[Double],
fold : Array[Int32],
folds : Int32,
) -> CvPlus raise ConformalError {
let scores = absolute_scores(truth, out_of_fold)
same_length(truth.length(), fold.length())
if folds < 2 || folds > truth.length() || folds > 1000 {
raise Invalid("CV+ folds must be in 2..min(n,1000)")
}
let counts = Array::make(folds, 0)
let residuals = []
for i in 0..= folds {
raise Invalid("fold index out of range")
}
counts[fold[i]] = counts[fold[i]] + 1
residuals.push((truth[i] - out_of_fold[i]).abs())
}
// Validation above also checks overflow; retain original row order, not sorted scores.
if scores.count() != residuals.length() {
raise Invalid("residual count mismatch")
}
for count in counts {
if count == 0 {
raise Invalid("every fold must have held-out rows")
}
}
{ residuals, fold: fold.copy(), folds, }
}
///|
pub fn CvPlus::count(self : CvPlus) -> Int32 {
self.residuals.length()
}
///|
pub fn CvPlus::fold_count(self : CvPlus) -> Int32 {
self.folds
}
///| test_by_fold[j][f] is model f's prediction for test row j.
///| Endpoints use lower floor((n+1)*alpha), upper ceil((n+1)*(1-alpha)).
///|
/// This implements the endpoint construction; it does not promise exact 1-alpha coverage.
pub fn CvPlus::predict(
self : CvPlus,
test_by_fold : Array[Array[Double]],
alpha : Double,
) -> Array[Interval] raise ConformalError {
validate_alpha(alpha)
if alpha >= 0.5 {
raise Invalid("CV+ endpoint alpha must be below 0.5")
}
if test_by_fold.is_empty() || test_by_fold.length() > 100000 {
raise Invalid("empty or oversized CV+ batch")
}
if test_by_fold.length() > 2000000 / self.count() {
raise ResourceLimit("CV+ batch exceeds two million candidate pairs")
}
let n = self.count()
let high_rank = conformal_rank(n, alpha)
let low_rank = n + 1 - high_rank
let result = []
for row in test_by_fold {
validate_values(row, "fold predictions")
same_length(row.length(), self.folds)
let low = []
let high = []
for i in 0.. n {
@double.infinity
} else {
high[high_rank - 1]
},
})
}
result
}