///|
/// Robust ensemble prediction with validation-aware model weights.
pub struct EnsembleMember {
name : String
prediction : Array[Double]
score : Double
weight : Double
}
///|
pub struct EnsembleResult {
prediction : Array[Double]
members : Array[EnsembleMember]
diversity : Double
confidence : Double
}
///|
pub fn ensemble_member(
name : String,
prediction : Array[Double],
score : Double,
) -> EnsembleMember {
{ name, prediction, score, weight: 0.0 }
}
///|
pub fn ensemble_members(
predictions : Array[Array[Double]],
scores : Array[Double],
) -> Array[EnsembleMember] {
let result = []
for index = 0; index < predictions.length(); index = index + 1 {
let score = if index < scores.length() { scores[index] } else { 0.0 }
result.push(
ensemble_member("model-" + index.to_string(), predictions[index], score),
)
}
let mut total = 0.0
for entry in result {
total += if entry.score < 0.0 { 0.0 } else { entry.score }
}
for index = 0; index < result.length(); index = index + 1 {
let score = if result[index].score < 0.0 {
0.0
} else {
result[index].score
}
result[index] = {
..result[index],
weight: if total == 0.0 {
1.0 / result.length().to_double()
} else {
score / total
},
}
}
result
}
///|
pub fn ensemble_predict(members : Array[EnsembleMember]) -> Array[Double] {
let result = []
if members.length() == 0 {
return result
}
let count = members[0].prediction.length()
for index = 0; index < count; index = index + 1 {
let mut value = 0.0
for entry in members {
if index < entry.prediction.length() {
value += entry.weight * entry.prediction[index]
}
}
result.push(value)
}
result
}
///|
pub fn ensemble_diversity(members : Array[EnsembleMember]) -> Double {
if members.length() < 2 {
return 0.0
}
let mut total = 0.0
let mut pairs = 0
for left = 0; left < members.length(); left = left + 1 {
for right = left + 1; right < members.length(); right = right + 1 {
let count = if members[left].prediction.length() <
members[right].prediction.length() {
members[left].prediction.length()
} else {
members[right].prediction.length()
}
let mut distance = 0.0
for index = 0; index < count; index = index + 1 {
distance += abs_double(
members[left].prediction[index] - members[right].prediction[index],
)
}
if count > 0 {
total += distance / count.to_double()
pairs += 1
}
}
}
if pairs == 0 {
0.0
} else {
total / pairs.to_double()
}
}
///|
pub fn ensemble_confidence(members : Array[EnsembleMember]) -> Double {
if members.length() == 0 {
0.0
} else {
transform_clip(1.0 - ensemble_diversity(members), 0.0, 1.0)
}
}
///|
pub fn ensemble_fit(
actual : Array[Double],
predictions : Array[Array[Double]],
) -> EnsembleResult {
let scores = []
for prediction in predictions {
scores.push(1.0 / (1.0 + mean_absolute_error(actual, prediction)))
}
let members = ensemble_members(predictions, scores)
{
prediction: ensemble_predict(members),
members,
diversity: ensemble_diversity(members),
confidence: ensemble_confidence(members),
}
}
///|
pub fn ensemble_fit_forecasts(
data : Array[Double],
horizon : Int,
) -> EnsembleResult {
let predictions = [
forecast_naive(data, horizon),
forecast_mean_level(data, horizon),
forecast_median_level(data, horizon),
forecast_drift(data, horizon),
forecast_huber_level(data, horizon),
]
let actual = if data.length() < horizon {
data.copy()
} else {
forecast_prefix(data, data.length() - horizon)
}
ensemble_fit(actual, predictions)
}
///|
pub fn ensemble_weights(members : Array[EnsembleMember]) -> Array[Double] {
let result = []
for entry in members {
result.push(entry.weight)
}
result
}
///|
pub fn ensemble_names(members : Array[EnsembleMember]) -> Array[String] {
let result = []
for entry in members {
result.push(entry.name)
}
result
}
///|
pub fn ensemble_score(members : Array[EnsembleMember]) -> Double {
let mut total = 0.0
for entry in members {
total += entry.score * entry.weight
}
total
}
///|
pub fn ensemble_result_vector(result : EnsembleResult) -> Array[Double] {
[
result.prediction.length().to_double(),
result.members.length().to_double(),
result.diversity,
result.confidence,
ensemble_score(result.members),
]
}
///|
pub fn ensemble_result_lines(result : EnsembleResult) -> Array[String] {
let lines = [
"prediction_count=" + result.prediction.length().to_string(),
"members=" + result.members.length().to_string(),
"diversity=" + result.diversity.to_string(),
"confidence=" + result.confidence.to_string(),
]
for entry in result.members {
lines.push(
entry.name +
"|score=" +
entry.score.to_string() +
"|weight=" +
entry.weight.to_string(),
)
}
lines
}
///|
pub fn ensemble_result_string(result : EnsembleResult) -> String {
ensemble_result_lines(result).join("\n")
}
///|
pub fn ensemble_stable(
left : EnsembleResult,
right : EnsembleResult,
tolerance : Double,
) -> Bool {
if left.prediction.length() != right.prediction.length() {
return false
}
for index = 0; index < left.prediction.length(); index = index + 1 {
if abs_double(left.prediction[index] - right.prediction[index]) > tolerance {
return false
}
}
true
}
///|
pub fn ensemble_residual_quality(
actual : Array[Double],
result : EnsembleResult,
) -> Double {
1.0 / (1.0 + mean_absolute_error(actual, result.prediction))
}
///|
pub fn ensemble_compare(
left : EnsembleResult,
right : EnsembleResult,
) -> Array[Double] {
[
left.diversity,
right.diversity,
left.confidence,
right.confidence,
left.prediction.length().to_double(),
right.prediction.length().to_double(),
]
}
///|
pub fn ensemble_summary(
actual : Array[Double],
predictions : Array[Array[Double]],
) -> Array[Double] {
let result = ensemble_fit(actual, predictions)
[
ensemble_residual_quality(actual, result),
result.diversity,
result.confidence,
ensemble_score(result.members),
]
}