///|
pub fn PredictionObservation::row_index(self : PredictionObservation) -> Int {
self.source_row_index
}
///|
pub fn PredictionObservation::actual_class(self : PredictionObservation) -> Int {
self.observed_class
}
///|
pub fn PredictionObservation::predicted_class(
self : PredictionObservation,
) -> Int {
self.selected_class
}
///|
pub fn PredictionObservation::winning_score(
self : PredictionObservation,
) -> Double {
self.selected_score
}
///|
pub fn PredictionObservation::second_class(self : PredictionObservation) -> Int {
self.runner_up_class
}
///|
pub fn PredictionObservation::second_score(
self : PredictionObservation,
) -> Double {
self.runner_up_score
}
///|
pub fn PredictionObservation::winning_margin(
self : PredictionObservation,
) -> Double {
self.selected_margin
}
///|
pub fn PredictionObservation::scores(
self : PredictionObservation,
) -> Array[Double] {
self.ordered_scores.copy()
}
///|
pub fn PredictionObservation::is_correct(self : PredictionObservation) -> Bool {
self.prediction_correct
}
///|
pub fn ClassScoreProfile::class_label(self : ClassScoreProfile) -> Int {
self.profiled_class
}
///|
pub fn ClassScoreProfile::positive_count(self : ClassScoreProfile) -> Int {
self.positive_observations
}
///|
pub fn ClassScoreProfile::negative_count(self : ClassScoreProfile) -> Int {
self.negative_observations
}
///|
pub fn ClassScoreProfile::minimum_positive_score(
self : ClassScoreProfile,
) -> Double {
self.positive_score_minimum
}
///|
pub fn ClassScoreProfile::maximum_positive_score(
self : ClassScoreProfile,
) -> Double {
self.positive_score_maximum
}
///|
pub fn ClassScoreProfile::positive_mean_score(
self : ClassScoreProfile,
) -> Double {
self.positive_score_mean
}
///|
pub fn ClassScoreProfile::minimum_negative_score(
self : ClassScoreProfile,
) -> Double {
self.negative_score_minimum
}
///|
pub fn ClassScoreProfile::maximum_negative_score(
self : ClassScoreProfile,
) -> Double {
self.negative_score_maximum
}
///|
pub fn ClassScoreProfile::negative_mean_score(
self : ClassScoreProfile,
) -> Double {
self.negative_score_mean
}
///|
pub fn ClassScoreProfile::score_separation(self : ClassScoreProfile) -> Double {
self.mean_score_separation
}
///|
pub fn PredictionAudit::observation_count(self : PredictionAudit) -> Int {
self.audited_observations.length()
}
///|
pub fn PredictionAudit::correct_count(self : PredictionAudit) -> Int {
self.correct_prediction_count
}
///|
pub fn PredictionAudit::incorrect_count(self : PredictionAudit) -> Int {
self.incorrect_prediction_count
}
///|
pub fn PredictionAudit::accuracy(self : PredictionAudit) -> Double {
self.audit_metrics.accuracy()
}
///|
pub fn PredictionAudit::metrics(
self : PredictionAudit,
) -> ClassificationMetrics {
self.audit_metrics
}
///|
pub fn PredictionAudit::observations(
self : PredictionAudit,
) -> Array[PredictionObservation] {
self.audited_observations.copy()
}
///|
pub fn PredictionAudit::misclassified_observations(
self : PredictionAudit,
) -> Array[PredictionObservation] {
let result : Array[PredictionObservation] = []
for observation in self.audited_observations {
if !observation.is_correct() {
result.push(observation)
}
}
result
}
///|
pub fn PredictionAudit::class_score_profiles(
self : PredictionAudit,
) -> Array[ClassScoreProfile] {
self.audited_class_profiles.copy()
}
///|
pub fn PredictionAudit::minimum_winning_margin(
self : PredictionAudit,
) -> Double {
self.smallest_winning_margin
}
///|
pub fn PredictionAudit::maximum_winning_margin(
self : PredictionAudit,
) -> Double {
self.largest_winning_margin
}
///|
pub fn PredictionAudit::mean_winning_margin(self : PredictionAudit) -> Double {
self.average_winning_margin
}
///|
fn audit_class_index(classes : Array[Int], label : Int) -> Int? {
for index, candidate in classes {
if candidate == label {
return Some(index)
}
}
None
}
///|
fn winning_score_indices(scores : Array[Double]) -> (Int, Int) {
let mut best = 0
let mut second = 1
if scores[second] > scores[best] {
let temporary = best
best = second
second = temporary
}
for index = 2; index < scores.length(); index = index + 1 {
if scores[index] > scores[best] {
second = best
best = index
} else if scores[index] > scores[second] {
second = index
}
}
(best, second)
}
///|
fn class_score_profile(
class_index : Int,
classes : Array[Int],
labels : Array[Int],
observations : Array[PredictionObservation],
) -> Result[ClassScoreProfile, SvmError] {
let label = classes[class_index]
let mut positive_count = 0
let mut negative_count = 0
let mut positive_minimum = 0.0
let mut positive_maximum = 0.0
let mut negative_minimum = 0.0
let mut negative_maximum = 0.0
let mut positive_total = 0.0
let mut negative_total = 0.0
for row, actual in labels {
let score = observations[row].ordered_scores[class_index]
if actual == label {
if positive_count == 0 || score < positive_minimum {
positive_minimum = score
}
if positive_count == 0 || score > positive_maximum {
positive_maximum = score
}
positive_total = positive_total + score
positive_count = positive_count + 1
} else {
if negative_count == 0 || score < negative_minimum {
negative_minimum = score
}
if negative_count == 0 || score > negative_maximum {
negative_maximum = score
}
negative_total = negative_total + score
negative_count = negative_count + 1
}
}
if positive_count == 0 {
return Err(InsufficientClassSamples(label, 0, 1))
}
if negative_count == 0 {
return Err(SingleClassDataset)
}
let positive_mean = positive_total / positive_count.to_double()
let negative_mean = negative_total / negative_count.to_double()
Ok({
profiled_class: label,
positive_observations: positive_count,
negative_observations: negative_count,
positive_score_minimum: positive_minimum,
positive_score_maximum: positive_maximum,
positive_score_mean: positive_mean,
negative_score_minimum: negative_minimum,
negative_score_maximum: negative_maximum,
negative_score_mean: negative_mean,
mean_score_separation: positive_mean - negative_mean,
})
}
///|
/// Audits every OvR prediction and class score on a compatible dataset.
pub fn multiclass_prediction_audit(
model : MulticlassModel,
data : Dataset,
) -> Result[PredictionAudit, SvmError] {
if data.feature_count() != model.feature_count() {
return Err(
PredictionDimensionMismatch(model.feature_count(), data.feature_count()),
)
}
let classes = model.classes()
let labels = data.labels()
for label in labels {
if audit_class_index(classes, label) is None {
return Err(UnknownClassLabel(label))
}
}
let observations : Array[PredictionObservation] = []
let predictions : Array[Int] = []
let mut correct = 0
let mut incorrect = 0
let mut minimum_margin = 0.0
let mut maximum_margin = 0.0
let mut margin_total = 0.0
for row_index, row in data.features() {
let scores = match model.decision_values(row) {
Err(error) => return Err(error)
Ok(value) => value
}
let (best, second) = winning_score_indices(scores)
let predicted = classes[best]
let margin = scores[best] - scores[second]
let is_correct = predicted == labels[row_index]
if is_correct {
correct = correct + 1
} else {
incorrect = incorrect + 1
}
if row_index == 0 || margin < minimum_margin {
minimum_margin = margin
}
if row_index == 0 || margin > maximum_margin {
maximum_margin = margin
}
margin_total = margin_total + margin
predictions.push(predicted)
observations.push({
source_row_index: row_index,
observed_class: labels[row_index],
selected_class: predicted,
selected_score: scores[best],
runner_up_class: classes[second],
runner_up_score: scores[second],
selected_margin: margin,
ordered_scores: scores,
prediction_correct: is_correct,
})
}
let metrics = match classification_metrics(labels, predictions) {
Err(error) => return Err(error)
Ok(value) => value
}
let profiles : Array[ClassScoreProfile] = []
for class_index = 0
class_index < classes.length()
class_index = class_index + 1 {
match class_score_profile(class_index, classes, labels, observations) {
Err(error) => return Err(error)
Ok(value) => profiles.push(value)
}
}
Ok({
audited_observations: observations,
audited_class_profiles: profiles,
audit_metrics: metrics,
correct_prediction_count: correct,
incorrect_prediction_count: incorrect,
smallest_winning_margin: minimum_margin,
largest_winning_margin: maximum_margin,
average_winning_margin: margin_total / data.row_count().to_double(),
})
}