Binary Classifier Diagnostics

While kstest and roc provide diagnostic measures for comparing model performance, we may want to produce graphs and tables to document its performance, bcdiag allows us to do this easily.

using ROCKS
using Random
using Distributions
using BenchmarkTools
Random.seed!(888)
const x = rand(Uniform(-5, 5), 1_000_000)
const logit = -3.0 .+ 0.5 .* x .+ rand(Normal(0, 0.1), length(x))
const prob = @. 1.0 / (1.0 + exp(-logit))
const target = rand(length(x)) .<= prob

kstest:

kstest(target, prob)
(n = 1000000, n1 = 94316, n0 = 905684, baserate = 0.094316, ks = 0.49102956987524427, ksarg = 0.09243578716933178, ksdep = 0.357299)

roc:

roc(target, prob)
(conc = 69434969897, tied = 392052, disc = 15985130195, auc = 0.8128630985401923, gini = 0.6257261970803847)

These functions are performant:

@benchmark kstest($target, $prob)
BechmarkTools.Trial: 29 samples with 1 evaluations.
 Range (min … max):  165.767 ms … 183.964 ms  ┊ GC (min … max): 0.00% … 0.52%
 Time  (median):     172.593 ms               ┊ GC (median):    0.56%
 Time  (mean ± σ):   173.067 ms ±   5.135 ms  ┊ GC (mean ± σ):  0.43% ± 0.34%

  █ ▁█ ▁   ▁ █ ▁ █    ▁▁▁▁  ▁   ▁   ▁█▁▁ ▁▁    ▁ ▁       ▁    ▁▁ 
  █▁██▁█▁▁▁█▁█▁█▁█▁▁▁▁████▁▁█▁▁▁█▁▁▁████▁██▁▁▁▁█▁█▁▁▁▁▁▁▁█▁▁▁▁█ ▁
  166 ms           Histogram: frequency by time          184 ms <

 Memory estimate: 46.02 MiB, allocs estimate: 19.
@benchmark roc($target, $prob)
BechmarkTools.Trial: 48 samples with 1 evaluations.
 Range (min … max):  105.564 ms … 107.521 ms  ┊ GC (min … max): 0.00% … 1.02%
 Time  (median):     106.274 ms               ┊ GC (median):    0.00%
 Time  (mean ± σ):   106.335 ms ± 418.081 μs  ┊ GC (mean ± σ):  0.13% ± 0.34%

              ▂     ▅   █  ▂                                     
  ▅▁▁▁▁▅▁▅▁▁▁▁███▅███▅▅███████▁▅▅▁▁▅▁▁▁▁▁▁▁▁▁▁▅▁▁▁▅▁▁▁▁▅▅▁▁▁▅▁▅ ▁
  106 ms           Histogram: frequency by time          108 ms <

 Memory estimate: 7.75 MiB, allocs estimate: 8.

bcdiag

In additional to numeric metrics, often we would like to have plots and tables as part of final model documentation. The bcdiag function allows easy generation of plots and tables.

Running bcdiag prints a quick summary:

mdiag = bcdiag(target, prob)
Base rate: 0.0943   n: 1000000   n1: 94316   n0: 905684
ks:        0.491   occurs at value of 0.09243578716933178 depth of 0.357299
roc:       0.8129   concordant pairs: 69434969897   tied pairs: 392052   discordant pairs: 15985130195
Gini:      0.6257

The output structure allows us to create the following plots and tables to understand:

  • the ability of the model to separate the two classes
  • the accuracy of the probability point estimates
  • how to set cutoff for maximum accuracy
  • performance of the model at varying cutoff depth

ksplot

ksplot plots the cumulative distribution of class 1 (true positive rate) and class 0 (false positive rate) versus depth.

ksplot(mdiag)

It shows where the maximum separation of the two distributions occur.

rocplot

rocplot plots the true positive rate vs. false positive rate (depth is implicit).

rocplot(mdiag)

A perfect model has auc of 1, a random model has auc of 0.5.

biasplot

Both ksplot and rocplot rely on the ability of the model to rank order the observations, the score value itself doesn't matter. For example, if you took the score and perform any monotonic transform, ks and auc wouldn't change. There are occasions where the score value does matter, where the probabilities need to be accurate, for example, in expected return calculations. Thus, we need to understand whether the probabilities are accurate, biasplot does this by plotting the observed response rate versus predicted response rate to look for systemic bias. This is also called the calibration graph.

biasplot(mdiag)

An unbiased model would lie on the diagnonal, systemic shift off the diagonal represents over or under estimate of the true probability.

accuracyplot

People often refer to (TP + TN) / N as accuracy of the model, that is, the ability to correctly identify correct cases. It is used to compare model performance as well - model with higher accuracy is a better model. For a probability based classifier, a cutoff is required to turn probability to predicted class. So, what is the cutoff value to use to achieve maximum accuracy?

There are many approaches to setting the best cutoff, one way is to assign utility values to the four outcomes of [TP, FP, FN, TN] and maximize the sum across different cutoff's. Accuracy measure uses the utility values of [1, 0, 0, 1] giving TP + TN. You can assign negative penalty terms for misclassification as well.

Note that this is different from kstest - maximum separation on cumulative distribution (normalized to 100%) does not account for class size difference, e.g., class 1 may be only 2% of the cases.

accuracyplot(mdiag)

liftcurve

liftcurve plots the actual response and predicted response versus depth, with baserate as 1.

liftcurve(mdiag)

We can easily see where the model is performing better than average, approximately the same as average, or below average.

cumliftcurve

cumliftcurve is similar to liftcurve, the difference is it is a plot of cumulative response rate from the top of the model.

cumliftcurve(mdiag)

Tables

bcdiag uses 100 as the default number of groups, this is good for generating plots above.

For tables such as decile reports, we may want to run bcdiag with only 10 groups and then generate the tables:

mdiag10 = bcdiag(target, prob; groups = 10)
Base rate: 0.0943   n: 1000000   n1: 94316   n0: 905684
ks:        0.491   occurs at value of 0.09243578716933178 depth of 0.357299
roc:       0.8129   concordant pairs: 69434969897   tied pairs: 392052   discordant pairs: 15985130195
Gini:      0.6257

liftable

liftable is the table from which liftcurve is plotted.

liftable(mdiag10)

10 rows × 9 columns

grpdepthcountcntObscntPrdrrObsrrPredliftObsliftPrd
Int32Float64Int64Int64Float64Float64Float64Float64Float64
100.11000003222532416.50.322250.3241653.416713.43701
210.21000002236222380.90.223620.2238092.370972.37297
320.31000001499414932.40.149940.1493241.589761.58324
430.410000096279632.060.096270.09632061.020721.02125
540.510000062336071.910.062330.06071910.6608630.643783
650.610000037523772.140.037520.03772140.3978120.399947
760.710000023382325.570.023380.02325570.247890.246572
870.810000013851424.060.013850.01424060.1468470.150989
980.9100000862868.2730.008620.008682730.09139490.09206
1091.0100000538523.7560.005380.005237560.05704230.0555321

cumliftable

cumliftable is the cumulative version of liftable.

cumliftable(mdiag10)

10 rows × 9 columns

grpdepthcountcumObscumPrdcrObscrPrdliftObsliftPrd
Int32Float64Int64Int64Float64Float64Float64Float64Float64
100.11000003222532416.50.322250.3241653.416713.43701
210.22000005458754797.50.2729350.2739872.893842.90499
320.33000006958169729.90.2319370.2324332.459142.46441
430.44000007920879362.00.198020.1984052.099542.10362
540.55000008544185433.90.1708820.1708681.81181.81165
650.66000008919389206.00.1486550.1486771.576141.57637
760.77000009153191531.60.1307590.1307591.386391.3864
870.88000009291692955.70.1161450.1161951.231451.23197
980.99000009377893823.90.1041980.1042491.104771.10531
1091.010000009431694347.70.0943160.09434771.01.00034