///|
pub fn binary_labels_from_threshold(
values : Array[Double],
threshold : Double,
) -> Array[Bool] {
let result = []
for value in values {
result.push(value > threshold)
}
result
}
///|
pub fn binary_labels_from_median(values : Array[Double]) -> Array[Bool] {
binary_labels_from_threshold(values, median(values))
}
///|
pub fn accuracy(matrix : ConfusionMatrix) -> Double {
let total = matrix.true_positive +
matrix.false_positive +
matrix.true_negative +
matrix.false_negative
if total == 0 {
0.0
} else {
(matrix.true_positive + matrix.true_negative).to_double() /
total.to_double()
}
}
///|
pub fn false_positive_rate(matrix : ConfusionMatrix) -> Double {
let denominator = matrix.false_positive + matrix.true_negative
if denominator == 0 {
0.0
} else {
matrix.false_positive.to_double() / denominator.to_double()
}
}
///|
pub fn false_negative_rate(matrix : ConfusionMatrix) -> Double {
let denominator = matrix.false_negative + matrix.true_positive
if denominator == 0 {
0.0
} else {
matrix.false_negative.to_double() / denominator.to_double()
}
}
///|
pub fn negative_predictive_value(matrix : ConfusionMatrix) -> Double {
let denominator = matrix.true_negative + matrix.false_negative
if denominator == 0 {
0.0
} else {
matrix.true_negative.to_double() / denominator.to_double()
}
}
///|
pub fn diagnostic_likelihood_positive(matrix : ConfusionMatrix) -> Double {
let fpr = false_positive_rate(matrix)
if fpr == 0.0 {
0.0
} else {
recall(matrix) / fpr
}
}
///|
pub fn diagnostic_likelihood_negative(matrix : ConfusionMatrix) -> Double {
let fpr = false_positive_rate(matrix)
let tnr = specificity(matrix)
if tnr == 0.0 {
0.0
} else {
fpr / tnr
}
}
///|
pub fn roc_points(
scores : Array[Double],
actual : Array[Bool],
thresholds : Array[Double],
) -> Array[Array[Double]] {
let result = []
let sweep = threshold_sweep(scores, actual, thresholds)
for row in sweep {
let threshold = row[0]
let predicted = binary_labels_from_threshold(scores, threshold)
let matrix = confusion_matrix(actual, predicted)
result.push([false_positive_rate(matrix), recall(matrix), threshold])
}
result
}
///|
pub fn auc_from_roc(points : Array[Array[Double]]) -> Double {
if points.length() <= 1 {
return 0.0
}
let sorted = []
for point in points {
sorted.push(point)
}
sorted.sort_by((left, right) => {
if left[0] < right[0] {
-1
} else if left[0] > right[0] {
1
} else {
0
}
})
let mut area = 0.0
for index = 1; index < sorted.length(); index = index + 1 {
let width = sorted[index][0] - sorted[index - 1][0]
let height = (sorted[index][1] + sorted[index - 1][1]) / 2.0
area += width * height
}
area
}
///|
pub fn average_precision(points : Array[Array[Double]]) -> Double {
if points.length() == 0 {
return 0.0
}
let sorted = []
for point in points {
sorted.push(point)
}
sorted.sort_by((left, right) => {
if left[0] > right[0] {
-1
} else if left[0] < right[0] {
1
} else {
0
}
})
let mut area = 0.0
for index = 1; index < sorted.length(); index = index + 1 {
let recall_width = abs_double(sorted[index][0] - sorted[index - 1][0])
area += recall_width * sorted[index][1]
}
area
}
///|
pub fn brier_score(
actual : Array[Bool],
probabilities : Array[Double],
) -> Double {
if actual.length() == 0 || actual.length() != probabilities.length() {
return 0.0
}
let mut total = 0.0
for index = 0; index < actual.length(); index = index + 1 {
let target = if actual[index] { 1.0 } else { 0.0 }
let error = probabilities[index] - target
total += error * error
}
total / actual.length().to_double()
}
///|
pub fn calibration_bins(
actual : Array[Bool],
probabilities : Array[Double],
bins : Int,
) -> Array[Array[Double]] {
if bins <= 0 {
abort("bins must be positive")
}
if actual.length() != probabilities.length() {
return []
}
let result = []
for bin = 0; bin < bins; bin = bin + 1 {
let lower = bin.to_double() / bins.to_double()
let upper = (bin + 1).to_double() / bins.to_double()
let mut count = 0
let mut positive = 0
let mut probability_total = 0.0
for index = 0; index < probabilities.length(); index = index + 1 {
if probabilities[index] >= lower &&
(probabilities[index] < upper || bin == bins - 1) {
count += 1
if actual[index] {
positive += 1
}
probability_total += probabilities[index]
}
}
if count == 0 {
result.push([lower, upper, 0.0, 0.0, 0.0])
} else {
result.push([
lower,
upper,
count.to_double(),
positive.to_double() / count.to_double(),
probability_total / count.to_double(),
])
}
}
result
}
///|
pub fn threshold_metrics(
scores : Array[Double],
actual : Array[Bool],
threshold : Double,
) -> Array[Double] {
let predicted = binary_labels_from_threshold(scores, threshold)
let matrix = confusion_matrix(actual, predicted)
[
accuracy(matrix),
precision(matrix),
recall(matrix),
specificity(matrix),
f1_score(matrix),
matthews_correlation(matrix),
]
}
///|
pub fn robust_classification_report(
scores : Array[Double],
actual : Array[Bool],
thresholds : Array[Double],
) -> Array[Array[Double]] {
let result = []
for threshold in thresholds {
result.push(threshold_metrics(scores, actual, threshold))
}
result
}
///|
pub fn agreement_rate(first : Array[Bool], second : Array[Bool]) -> Double {
if first.length() == 0 || first.length() != second.length() {
return 0.0
}
let mut agreement = 0
for index = 0; index < first.length(); index = index + 1 {
if first[index] == second[index] {
agreement += 1
}
}
agreement.to_double() / first.length().to_double()
}
///|
pub fn robust_rank_agreement(
first : Array[Double],
second : Array[Double],
) -> Double {
spearman_correlation(first, second)
}