///|
fn window_points(
results : Array[EtaResult],
actuals : Array[Double],
start : Int,
end : Int,
) -> Array[ForecastPoint] {
let points : Array[ForecastPoint] = []
let limit = if end > results.length() { results.length() } else { end }
for index in start..= 0 && index < actuals.length() {
points.push({
predicted: results[index].eta_seconds,
actual: actuals[index],
})
}
}
points
}
///|
/// Evaluate overlapping rolling windows over ETA results.
pub fn rolling_evaluate(
results : Array[EtaResult],
actuals : Array[Double],
window_size~ : Int,
) -> RollingEvaluation {
let windows : Array[EvaluationWindow] = []
let safe_size = if window_size < 1 { 1 } else { window_size }
let limit = if results.length() < actuals.length() {
results.length()
} else {
actuals.length()
}
if limit > 0 {
for start in 0.. limit {
limit
} else {
start + safe_size
}
let points = window_points(results, actuals, start, end)
if points.length() > 0 {
windows.push({ start, end, summary: summarize_errors(points) })
}
}
}
{
windows,
overall: summarize_errors(window_points(results, actuals, 0, limit)),
}
}
///|
/// Return the worst rolling window by MAE.
pub fn worst_window(evaluation : RollingEvaluation) -> EvaluationWindow? {
if evaluation.windows.length() == 0 {
None
} else {
let mut worst = evaluation.windows[0]
for window in evaluation.windows[1:] {
if window.summary.mae > worst.summary.mae {
worst = window
}
}
Some(worst)
}
}
///|
/// Return average MAE across all non-empty rolling windows.
pub fn average_window_mae(evaluation : RollingEvaluation) -> Double {
let values : Array[Double] = []
for window in evaluation.windows {
values.push(window.summary.mae)
}
robust_mean(values)
}
///|
/// Render a compact rolling evaluation table.
pub fn format_rolling_evaluation(evaluation : RollingEvaluation) -> String {
let builder = StringBuilder()
builder.write_string("rolling windows=\{evaluation.windows.length()}\n")
for window in evaluation.windows {
builder.write_string(
"[\{window.start},\{window.end}) mae=\{window.summary.mae}\n",
)
}
builder.write_string("overall ")
builder.write_string(format_error_summary(evaluation.overall))
builder.to_string()
}
///|
/// Compare rolling performance before and after a change.
pub fn rolling_improvement(
before : RollingEvaluation,
after : RollingEvaluation,
) -> Double {
safe_div(before.overall.mae - after.overall.mae, before.overall.mae, 0.0)
}
///|
/// Count windows that satisfy an MAE limit.
pub fn passing_window_count(
evaluation : RollingEvaluation,
limit~ : Double,
) -> Int {
let mut count = 0
for window in evaluation.windows {
if window.summary.mae <= limit {
count += 1
}
}
count
}