///|
/// Cross-model validation record with uncertainty and stability diagnostics.
pub struct ModelValidation {
name : String
folds : Int
score : Double
standard_error : Double
coverage : Double
stability : Double
selected : Bool
}
///|
pub struct ValidationLeaderboard {
models : Array[ModelValidation]
winner : String
margin : Double
confidence : Double
}
///|
pub fn model_validation(
name : String,
scores : Array[Double],
coverage : Double,
) -> ModelValidation {
let center = mean(scores)
let error = if scores.length() < 2 {
0.0
} else {
sample_stddev(scores) / scores.length().to_double().sqrt()
}
let stability = if center == 0.0 {
1.0
} else {
1.0 / (1.0 + error / abs_double(center))
}
{
name,
folds: scores.length(),
score: center,
standard_error: error,
coverage: transform_clip(coverage, 0.0, 1.0),
stability,
selected: false,
}
}
///|
pub fn model_validation_from_backtest(
name : String,
report : BacktestReport,
) -> ModelValidation {
let scores = []
for error in backtest_fold_errors(report) {
scores.push(1.0 / (1.0 + abs_double(error)))
}
model_validation(name, scores, backtest_coverage(report))
}
///|
pub fn model_validation_rank(
models : Array[ModelValidation],
) -> Array[ModelValidation] {
let result = models.copy()
result.sort_by((left, right) => {
if left.score > right.score {
-1
} else if left.score < right.score {
1
} else {
0
}
})
for index = 0; index < result.length(); index = index + 1 {
result[index] = { ..result[index], selected: index == 0 }
}
result
}
///|
pub fn model_validation_leaderboard(
models : Array[ModelValidation],
) -> ValidationLeaderboard {
let ranked = model_validation_rank(models)
if ranked.length() == 0 {
return { models: [], winner: "none", margin: 0.0, confidence: 0.0 }
}
let margin = if ranked.length() < 2 {
ranked[0].score
} else {
ranked[0].score - ranked[1].score
}
let confidence = transform_clip(
ranked[0].stability * (0.5 + margin),
0.0,
1.0,
)
{ models: ranked, winner: ranked[0].name, margin, confidence }
}
///|
pub fn model_validation_scores(
leaderboard : ValidationLeaderboard,
) -> Array[Double] {
let result = []
for model in leaderboard.models {
result.push(model.score)
}
result
}
///|
pub fn model_validation_names(
leaderboard : ValidationLeaderboard,
) -> Array[String] {
let result = []
for model in leaderboard.models {
result.push(model.name)
}
result
}
///|
pub fn model_validation_margin(leaderboard : ValidationLeaderboard) -> Double {
leaderboard.margin
}
///|
pub fn model_validation_reliable(
model : ModelValidation,
score_threshold : Double,
coverage_threshold : Double,
) -> Bool {
model.score >= score_threshold &&
model.coverage >= coverage_threshold &&
model.stability >= 0.5
}
///|
pub fn model_validation_compare(
left : ModelValidation,
right : ModelValidation,
) -> Array[Double] {
[
left.score,
right.score,
left.standard_error,
right.standard_error,
left.coverage,
right.coverage,
left.stability,
right.stability,
]
}
///|
pub fn model_validation_lines(model : ModelValidation) -> Array[String] {
[
"name=" + model.name,
"folds=" + model.folds.to_string(),
"score=" + model.score.to_string(),
"standard_error=" + model.standard_error.to_string(),
"coverage=" + model.coverage.to_string(),
"stability=" + model.stability.to_string(),
"selected=" + model.selected.to_string(),
]
}
///|
pub fn model_validation_string(model : ModelValidation) -> String {
model_validation_lines(model).join("\n")
}
///|
pub fn leaderboard_lines(leaderboard : ValidationLeaderboard) -> Array[String] {
let lines = [
"winner=" + leaderboard.winner,
"margin=" + leaderboard.margin.to_string(),
"confidence=" + leaderboard.confidence.to_string(),
]
for index = 0; index < leaderboard.models.length(); index = index + 1 {
let model = leaderboard.models[index]
lines.push(
index.to_string() +
"|" +
model.name +
"|" +
model.score.to_string() +
"|" +
model.coverage.to_string() +
"|" +
model.selected.to_string(),
)
}
lines
}
///|
pub fn leaderboard_string(leaderboard : ValidationLeaderboard) -> String {
leaderboard_lines(leaderboard).join("\n")
}
///|
pub fn model_validation_residual_score(
actual : Array[Double],
predicted : Array[Double],
) -> Double {
if actual.length() != predicted.length() || actual.length() == 0 {
return 0.0
}
let error = mean_absolute_error(actual, predicted)
1.0 / (1.0 + error)
}
///|
pub fn model_validation_rmse_score(
actual : Array[Double],
predicted : Array[Double],
) -> Double {
if actual.length() != predicted.length() || actual.length() == 0 {
return 0.0
}
1.0 / (1.0 + mean_squared_error(actual, predicted).sqrt())
}
///|
pub fn model_validation_bias_score(
actual : Array[Double],
predicted : Array[Double],
) -> Double {
if actual.length() != predicted.length() || actual.length() == 0 {
return 0.0
}
let residuals = []
for index = 0; index < actual.length(); index = index + 1 {
residuals.push(actual[index] - predicted[index])
}
1.0 / (1.0 + abs_double(mean(residuals)))
}
///|
pub fn model_validation_score_vector(
actual : Array[Double],
predicted : Array[Double],
) -> Array[Double] {
[
model_validation_residual_score(actual, predicted),
model_validation_rmse_score(actual, predicted),
model_validation_bias_score(actual, predicted),
coverage_of_interval(actual, [min_value(predicted), max_value(predicted)]),
]
}
///|
pub fn model_validation_ensemble(
actual : Array[Double],
predictions : Array[Array[Double]],
) -> Array[ModelValidation] {
let result = []
for index = 0; index < predictions.length(); index = index + 1 {
let scores = model_validation_score_vector(actual, predictions[index])
result.push(
model_validation("model-" + index.to_string(), scores, scores[0]),
)
}
result
}
///|
pub fn model_validation_select(
actual : Array[Double],
predictions : Array[Array[Double]],
) -> Int {
let models = model_validation_ensemble(actual, predictions)
if models.length() == 0 {
return -1
}
let leaderboard = model_validation_leaderboard(models)
if leaderboard.models.length() == 0 {
return -1
}
for index = 0; index < models.length(); index = index + 1 {
if models[index].name == leaderboard.winner {
return index
}
}
-1
}
///|
pub fn model_validation_learning_curve(
actual : Array[Double],
predictions : Array[Array[Double]],
) -> Array[Double] {
let result = []
for prediction in predictions {
result.push(model_validation_residual_score(actual, prediction))
}
result
}
///|
pub fn model_validation_stable(
scores : Array[Double],
tolerance : Double,
) -> Bool {
if scores.length() == 0 {
return true
}
let center = mean(scores)
for score in scores {
if abs_double(score - center) > tolerance {
return false
}
}
true
}
///|
pub fn model_validation_summary(
leaderboard : ValidationLeaderboard,
) -> Array[Double] {
[
leaderboard.models.length().to_double(),
leaderboard.margin,
leaderboard.confidence,
if leaderboard.winner == "none" {
0.0
} else {
1.0
},
]
}