Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "Lighthouse"
uuid = "ac2c24cd-07f0-4848-96b2-1b82c3ea0e59"
authors = ["Beacon Biosignals, Inc."]
version = "0.13.3"
version = "0.13.4"

[deps]
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
Expand Down
2 changes: 1 addition & 1 deletion src/metrics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ Return `(κ, p₀)` where `κ` is Cohen's kappa and `p₀` percent agreement giv
their equivalents in [`confusion_matrix`](@ref)).
"""
function cohens_kappa(class_count, hard_label_pairs)
@assert all(issubset(pair, 1:class_count) for pair in hard_label_pairs)
all(issubset(pair, 1:class_count) for pair in hard_label_pairs) || throw(ArgumentError("Unexpected class in `hard_label_pairs`."))
p₀ = accuracy(confusion_matrix(class_count, hard_label_pairs))
pₑ = _probability_of_chance_agreement(class_count, hard_label_pairs)
return _cohens_kappa(p₀, pₑ), p₀
Expand Down
11 changes: 6 additions & 5 deletions src/utilities.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,18 @@ end
area_under_curve(x, y)

Calculates the area under the curve specified by the `x` vector and `y` vector
using the trapezoidal rule.
using the trapezoidal rule. If inputs are empty, return `missing`.
"""
function area_under_curve(x, y)
@assert length(x) == length(y)
length(x) == length(y) || throw(ArgumentError("Length of inputs must match."))
length(x) == 0 && return missing
auc = zero(middle(one(eltype(x)), one(eltype(y))))
perms = sortperm(x)
sorted_x = view(x, perms)
sorted_y = view(y, perms)
# calculate the trapazoidal method https://en.wikipedia.org/wiki/Trapezoidal_rule
for i in 2:length(x)
auc += middle(sorted_y[i], sorted_y[i - 1]) * (sorted_x[i] - sorted_x[i - 1])
auc += middle(sorted_y[i], sorted_y[i-1]) * (sorted_x[i] - sorted_x[i-1])
end
return auc
end
Expand All @@ -32,10 +33,10 @@ end
area_under_curve_unit_square(x, y)

Calculates the area under the curve specified by the `x` vector and `y` vector
for a unit square, using the trapezoidal rule.
for a unit square, using the trapezoidal rule. If inputs are empty, return `missing`.
"""
function area_under_curve_unit_square(x, y)
@assert length(x) == length(y)
length(x) == length(y) || throw(ArgumentError("Length of inputs must match."))
kept = [(i, j)
for (i, j) in zip(x, y)
if !(ismissing(i) || ismissing(j)) && (0 <= i <= 1 && 0 <= j <= 1)]
Expand Down
62 changes: 32 additions & 30 deletions test/metrics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -49,22 +49,22 @@
c = confusion_matrix(k, hard_label_pairs)
kappa, percent_agreement = cohens_kappa(3, hard_label_pairs)
@test percent_agreement == accuracy(c)
@test isapprox(percent_agreement, 0.5; atol=0.02)
@test isapprox(kappa, 0.0; atol=0.02)
@test isapprox(percent_agreement, 0.5; atol = 0.02)
@test isapprox(kappa, 0.0; atol = 0.02)
stats = binary_statistics(c, 1)
@test isapprox(stats.predicted_positives, 500_000; atol=2000)
@test isapprox(stats.predicted_negatives, 500_000; atol=2000)
@test isapprox(stats.actual_positives, 500_000; atol=2000)
@test isapprox(stats.actual_negatives, 500_000; atol=2000)
@test isapprox(stats.true_positives, 250_000; atol=2000)
@test isapprox(stats.true_negatives, 250_000; atol=2000)
@test isapprox(stats.false_positives, 250_000; atol=2000)
@test isapprox(stats.false_negatives, 250_000; atol=2000)
@test isapprox(stats.true_positive_rate, 0.5; atol=0.02)
@test isapprox(stats.true_negative_rate, 0.5; atol=0.02)
@test isapprox(stats.false_positive_rate, 0.5; atol=0.02)
@test isapprox(stats.false_negative_rate, 0.5; atol=0.02)
@test isapprox(stats.precision, 0.5; atol=0.02)
@test isapprox(stats.predicted_positives, 500_000; atol = 2000)
@test isapprox(stats.predicted_negatives, 500_000; atol = 2000)
@test isapprox(stats.actual_positives, 500_000; atol = 2000)
@test isapprox(stats.actual_negatives, 500_000; atol = 2000)
@test isapprox(stats.true_positives, 250_000; atol = 2000)
@test isapprox(stats.true_negatives, 250_000; atol = 2000)
@test isapprox(stats.false_positives, 250_000; atol = 2000)
@test isapprox(stats.false_negatives, 250_000; atol = 2000)
@test isapprox(stats.true_positive_rate, 0.5; atol = 0.02)
@test isapprox(stats.true_negative_rate, 0.5; atol = 0.02)
@test isapprox(stats.false_positive_rate, 0.5; atol = 0.02)
@test isapprox(stats.false_negative_rate, 0.5; atol = 0.02)
@test isapprox(stats.precision, 0.5; atol = 0.02)

@test confusion_matrix(10, ()) == zeros(10, 10)
@test all(ismissing, cohens_kappa(10, ()))
Expand All @@ -91,6 +91,8 @@
@test Lighthouse._cohens_kappa(p, p / 2) > 0
end
end

@test_throws ArgumentError cohens_kappa(3, [(4,5),(8,2)])
end

@testset "`calibration_curve`" begin
Expand All @@ -99,42 +101,42 @@ end
bitmask = rand(rng, Bool, 1_000_000)
bin_count = 12
bins, fractions, totals, mean_squared_error = calibration_curve(probs, bitmask;
bin_count=bin_count)
bin_count = bin_count)
@test bin_count == length(bins)
@test first(first(bins)) == 0.0 && last(last(bins)) == 1.0
@test all(!ismissing, fractions)
@test all(!iszero, totals)
@test all(isapprox.(fractions, 0.5; atol=0.02))
@test all(isapprox.(totals, length(probs) / bin_count; atol=1000))
@test all(isapprox.(fractions, 0.5; atol = 0.02))
@test all(isapprox.(totals, length(probs) / bin_count; atol = 1000))
@test sum(totals) == length(probs)
@test isapprox(ceil(mean(fractions) * length(bitmask)), count(bitmask); atol=1)
@test isapprox(mean_squared_error, inv(bin_count); atol=0.002)
@test isapprox(ceil(mean(fractions) * length(bitmask)), count(bitmask); atol = 1)
@test isapprox(mean_squared_error, inv(bin_count); atol = 0.002)

rng = StableRNG(42)
probs = range(0.0, 1.0; length=1_000_000)
probs = range(0.0, 1.0; length = 1_000_000)
bitmask = [rand(rng) <= p for p in probs]
bin_count = 10
bins, fractions, totals, mean_squared_error = calibration_curve(probs, bitmask;
bin_count=bin_count)
ideal = range(mean(first(bins)), mean(last(bins)); length=bin_count)
bin_count = bin_count)
ideal = range(mean(first(bins)), mean(last(bins)); length = bin_count)
@test bin_count == length(bins)
@test first(first(bins)) == 0.0 && last(last(bins)) == 1.0
@test all(!ismissing, fractions)
@test all(!iszero, totals)
@test all(isapprox.(fractions, ideal; atol=0.01))
@test all(isapprox.(fractions, ideal; atol = 0.01))
@test all(totals .== 1_000_000 / bin_count)
@test isapprox(ceil(mean(fractions) * length(bitmask)), count(bitmask); atol=1)
@test isapprox(mean_squared_error, 0.0; atol=0.00001)
@test isapprox(ceil(mean(fractions) * length(bitmask)), count(bitmask); atol = 1)
@test isapprox(mean_squared_error, 0.0; atol = 0.00001)

bitmask = reverse(bitmask)
bins, fractions, totals, mean_squared_error = calibration_curve(probs, bitmask;
bin_count=bin_count)
bin_count = bin_count)
@test bin_count == length(bins)
@test first(first(bins)) == 0.0 && last(last(bins)) == 1.0
@test all(!ismissing, fractions)
@test all(!iszero, totals)
@test all(isapprox.(fractions, reverse(ideal); atol=0.01))
@test all(isapprox.(fractions, reverse(ideal); atol = 0.01))
@test all(totals .== 1_000_000 / bin_count)
@test isapprox(ceil(mean(fractions) * length(bitmask)), count(bitmask); atol=1)
@test isapprox(mean_squared_error, 1 / 3; atol=0.01)
@test isapprox(ceil(mean(fractions) * length(bitmask)), count(bitmask); atol = 1)
@test isapprox(mean_squared_error, 1 / 3; atol = 0.01)
end
20 changes: 11 additions & 9 deletions test/utilities.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,25 @@
@test majority([1, 2, 1, 3, 2, 2, 3, 4], 3:4) == 3
rng = StableRNG(42)
picked = [majority(rng, 1:2, 1:2) for _ in 1:1_000_000]
@test isapprox(count(==(1), picked), 500_000; atol=1000)
@test isapprox(count(==(1), picked), 500_000; atol = 1000)
end

@testset "`Lighthouse.area_under_curve`" begin
@test_throws AssertionError Lighthouse.area_under_curve([0, 1, 2], [0, 1])
@test_throws ArgumentError Lighthouse.area_under_curve([0, 1, 2], [0, 1])
@test ismissing(Lighthouse.area_under_curve([], []))
@test isapprox(Lighthouse.area_under_curve(collect(0:0.01:1), collect(0:0.01:1)), 0.5;
atol=0.01)
atol = 0.01)
@test isapprox(Lighthouse.area_under_curve(collect(0:0.01:(2π)), sin.(0:0.01:(2π))),
0.0; atol=0.01)
0.0; atol = 0.01)
end

@testset "`Lighthouse.area_under_curve_unit_square`" begin
@test_throws AssertionError Lighthouse.area_under_curve_unit_square([0, 1, 2], [0, 1])
@test_throws ArgumentError Lighthouse.area_under_curve_unit_square([0, 1, 2], [0, 1])
@test ismissing(Lighthouse.area_under_curve_unit_square([], []))
@test isapprox(Lighthouse.area_under_curve_unit_square(collect(0:0.01:1),
collect(0:0.01:1)), 0.5;
atol=0.01)
collect(0:0.01:1)), 0.5;
atol = 0.01)
@test isapprox(Lighthouse.area_under_curve_unit_square(collect(0:0.01:(2π)),
sin.(0:0.01:(2π))), 0.459;
atol=0.01)
sin.(0:0.01:(2π))), 0.459;
atol = 0.01)
end