///|
pub fn RepeatedValidationReport::repetition_count(
self : RepeatedValidationReport,
) -> Int {
self.round_reports.length()
}
///|
pub fn RepeatedValidationReport::fold_count(
self : RepeatedValidationReport,
) -> Int {
self.folds_per_round
}
///|
pub fn RepeatedValidationReport::reports(
self : RepeatedValidationReport,
) -> Array[ValidationReport] {
self.round_reports.copy()
}
///|
pub fn RepeatedValidationReport::mean_accuracy(
self : RepeatedValidationReport,
) -> Double {
self.accuracy_mean
}
///|
pub fn RepeatedValidationReport::minimum_accuracy(
self : RepeatedValidationReport,
) -> Double {
self.accuracy_minimum
}
///|
pub fn RepeatedValidationReport::maximum_accuracy(
self : RepeatedValidationReport,
) -> Double {
self.accuracy_maximum
}
///|
pub fn RepeatedValidationReport::accuracy_standard_deviation(
self : RepeatedValidationReport,
) -> Double {
self.accuracy_deviation
}
///|
pub fn RepeatedValidationReport::mean_macro_f1(
self : RepeatedValidationReport,
) -> Double {
self.macro_f1_mean
}
///|
pub fn RepeatedValidationReport::minimum_macro_f1(
self : RepeatedValidationReport,
) -> Double {
self.macro_f1_minimum
}
///|
pub fn RepeatedValidationReport::maximum_macro_f1(
self : RepeatedValidationReport,
) -> Double {
self.macro_f1_maximum
}
///|
pub fn RepeatedValidationReport::macro_f1_standard_deviation(
self : RepeatedValidationReport,
) -> Double {
self.macro_f1_deviation
}
///|
fn stratified_round_folds(
data : Dataset,
fold_count : Int,
round : Int,
) -> Result[Array[StratifiedFold], SvmError] {
if fold_count < 2 || fold_count > data.row_count() {
return Err(InvalidFoldCount(fold_count))
}
let classes = data.classes()
for label in classes {
let count = data.class_count(label)
if count < fold_count {
return Err(InsufficientClassSamples(label, count, fold_count))
}
}
let tests : Array[Array[Int]] = Array::makei(fold_count, fn(_) { [] })
let labels = data.labels()
for class_index, label in classes {
let mut occurrence = 0
let offset = round * (class_index + 1) % fold_count
for row, actual in labels {
if actual == label {
tests[(occurrence + offset) % fold_count].push(row)
occurrence = occurrence + 1
}
}
}
let folds : Array[StratifiedFold] = []
for fold_index = 0; fold_index < fold_count; fold_index = fold_index + 1 {
let train : Array[Int] = []
for row = 0; row < data.row_count(); row = row + 1 {
if !contains_index(tests[fold_index], row) {
train.push(row)
}
}
folds.push({
fold_index,
training_indices: train,
testing_indices: tests[fold_index].copy(),
})
}
Ok(folds)
}
///|
fn evaluate_partition_set(
data : Dataset,
config : BinaryConfig,
partitions : Array[StratifiedFold],
scaling : ScalingPlan,
) -> Result[ValidationReport, SvmError] {
let evaluations : Array[FoldEvaluation] = []
let aggregate_actual : Array[Int] = []
let aggregate_predicted : Array[Int] = []
for partition in partitions {
let training_data = match
data.subset(partition.train_indices(), "repeated training fold") {
Err(error) => return Err(error)
Ok(value) => value
}
let testing_data = match
data.subset(partition.test_indices(), "repeated testing fold") {
Err(error) => return Err(error)
Ok(value) => value
}
let (fit_data, predict_data, scaler) = match
scaled_fold_data(training_data, testing_data, scaling) {
Err(error) => return Err(error)
Ok(value) => value
}
let model = match train_multiclass(fit_data, config) {
Err(error) => return Err(error)
Ok(value) => value
}
let actual = predict_data.labels()
let predicted = match model.predict_batch(predict_data.features()) {
Err(error) => return Err(error)
Ok(value) => value
}
let metrics = match classification_metrics(actual, predicted) {
Err(error) => return Err(error)
Ok(value) => value
}
for label in actual {
aggregate_actual.push(label)
}
for label in predicted {
aggregate_predicted.push(label)
}
evaluations.push({
fold_number: partition.index(),
training_rows: partition.train_indices(),
testing_rows: partition.test_indices(),
actual_labels: actual,
predicted_labels: predicted,
fold_metrics: metrics,
fitted_scaler: scaler,
})
}
let aggregate = match
classification_metrics(aggregate_actual, aggregate_predicted) {
Err(error) => return Err(error)
Ok(value) => value
}
Ok({
fold_evaluations: evaluations,
aggregate_metrics: aggregate,
evaluated_observations: data.row_count(),
scaling_plan: scaling,
})
}
///|
fn numeric_summary(values : Array[Double]) -> (Double, Double, Double, Double) {
let mut minimum = values[0]
let mut maximum = values[0]
let mut total = 0.0
for value in values {
if value < minimum {
minimum = value
}
if value > maximum {
maximum = value
}
total = total + value
}
let mean = total / values.length().to_double()
let mut squared_total = 0.0
for value in values {
let difference = value - mean
squared_total = squared_total + difference * difference
}
(mean, minimum, maximum, (squared_total / values.length().to_double()).sqrt())
}
///|
/// Repeats deterministic stratified CV with class-specific fold rotations.
pub fn repeated_cross_validate(
data : Dataset,
config : BinaryConfig,
fold_count : Int,
repetitions : Int,
scaling : ScalingPlan,
) -> Result[RepeatedValidationReport, SvmError] {
if repetitions <= 0 {
return Err(InvalidIterationCount(repetitions))
}
let reports : Array[ValidationReport] = []
let accuracies : Array[Double] = []
let macro_f1_values : Array[Double] = []
for round = 0; round < repetitions; round = round + 1 {
let partitions = match stratified_round_folds(data, fold_count, round) {
Err(error) => return Err(error)
Ok(value) => value
}
let report = match
evaluate_partition_set(data, config, partitions, scaling) {
Err(error) => return Err(error)
Ok(value) => value
}
accuracies.push(report.metrics().accuracy())
macro_f1_values.push(report.metrics().macro_f1())
reports.push(report)
}
let (accuracy_mean, accuracy_minimum, accuracy_maximum, accuracy_deviation) = numeric_summary(
accuracies,
)
let (macro_f1_mean, macro_f1_minimum, macro_f1_maximum, macro_f1_deviation) = numeric_summary(
macro_f1_values,
)
Ok({
round_reports: reports,
folds_per_round: fold_count,
accuracy_mean,
accuracy_minimum,
accuracy_maximum,
accuracy_deviation,
macro_f1_mean,
macro_f1_minimum,
macro_f1_maximum,
macro_f1_deviation,
})
}