Core machine learning types: the seedable Random generator, Matrix2D linear algebra and activations, matrix reference holders, the NeuralNetwork and CSV-backed MatrixReader. Compile with -lib ml.
Metrics
Model evaluation metrics for classification tasks
Operations
- Accuracy
- AtThreshold
- AucRoc
- AveragePrecision
- Bootstrap
- ConfusionMatrix
- F1Score
- MCC
- OperatingPoints
- Percentile
- PrCurve
- Precision
- PrecisionAtBaseRate
- PrintConfusionMatrix
- Recall
- RecallAtFpr
- RecallByGroup
- RocCurve
- ShuffleLabels
- ThresholdAtFpr
- UnseenGroups
Accuracy # native
Calculates accuracy: (TP + TN) / Total
function : native : Accuracy(predictions:Bool[], actuals:Bool[]) ~ FloatParameters
| Name | Type | Description |
|---|---|---|
| predictions | Bool | predicted boolean values |
| actuals | Bool | actual boolean values |
Return
| Type | Description |
|---|---|
| Float | accuracy score between 0.0 and 1.0 |
AtThreshold # function
Recall, false-positive rate and precision at ONE given threshold. The counterpart to RecallAtFpr. That one searches for the operating point fitting a false-alarm budget; this one is handed a threshold and reports what it actually does. Both are needed to answer whether a threshold chosen on one set still honours its budget on another: the budget is intended, the rate reported here is what happened. An instance counts as predicted positive when its score is >= threshold. That matches OperatingPoints, which accumulates from the highest score down and lets instances sharing a score share an operating point, so a threshold from ThresholdAtFpr can be passed straight here. Every rate is guarded. A float division by zero raises in Objeck and would take the program down, and the degenerate cases are ordinary: a threshold above every score predicts nothing positive, leaving precision with no denominator.
function : AtThreshold(scores:Float[], actuals:Bool[], threshold:Float) ~ ThresholdResultParameters
| Name | Type | Description |
|---|---|---|
| scores | Float | one score per instance; higher means more positive |
| actuals | Bool | true where the instance really is positive |
| threshold | Float | the cutoff to apply |
Return
| Type | Description |
|---|---|
| ThresholdResult | the counts and rates, or Nil when the inputs do not line up |
AucRoc # function
Area under the ROC curve, by trapezoid over the operating points.
function : AucRoc(scores:Float[], actuals:Bool[]) ~ FloatParameters
| Name | Type | Description |
|---|---|---|
| scores | Float | one score per instance; higher means more positive |
| actuals | Bool | true where the instance really is positive |
Return
| Type | Description |
|---|---|
| Float | AUC in 0.0 to 1.0, or -1.0 on bad input |
AveragePrecision # function
Average precision: the precision-recall curve summarised as the sum of precision at each point weighted by the recall it gained. Preferred over AUC-ROC when positives are rare, because it does not credit a model for the vast number of true negatives.
function : AveragePrecision(scores:Float[], actuals:Bool[]) ~ FloatParameters
| Name | Type | Description |
|---|---|---|
| scores | Float | one score per instance; higher means more positive |
| actuals | Bool | true where the instance really is positive |
Return
| Type | Description |
|---|---|
| Float | average precision in 0.0 to 1.0, or -1.0 on bad input |
Bootstrap # function
A bootstrap percentile confidence interval for a metric. Resamples the scored instances WITH REPLACEMENT reps times, recomputes the metric on each resample, and takes percentiles of that distribution. The point estimate is the metric on the original sample, not the mean of the resamples. Resampling pairs, not scores: a resample draws (score, label) together, so the class balance moves between resamples the way it would between samples. That is the variability being estimated, and it matters most where it is largest -- a false-alarm budget of 0.001 on a 10,000-row test set is about ten false positives, and ten is a count whose uncertainty is not negligible. A resample can contain only one class, which leaves AUC undefined. The metrics here report that as a NEGATIVE value rather than a real score, so those resamples are discarded rather than averaged in -- folding a sentinel into the distribution would drag the interval toward it. The number that survived is reported, because an interval built from a small fraction of the requested resamples is not the interval that was asked for.
function : Bootstrap(metric:ScoreMetric, scores:Float[], actuals:Bool[], reps:Int, seed:Int, level:Float) ~ ConfIntervalParameters
| Name | Type | Description |
|---|---|---|
| metric | ScoreMetric | the statistic to resample |
| scores | Float | one score per instance; higher means more positive |
| actuals | Bool | true where the instance really is positive |
| reps | Int | how many resamples to draw |
| seed | Int | seeds the resampling, so a run can be repeated |
| level | Float | the interval width, e.g. 0.95 |
Return
| Type | Description |
|---|---|
| ConfInterval | the interval, or Nil on bad input or fewer than two usable resamples |
Bootstrap # function
A bootstrap interval at the conventional 95% level.
function : Bootstrap(metric:ScoreMetric, scores:Float[], actuals:Bool[], reps:Int, seed:Int) ~ ConfIntervalParameters
| Name | Type | Description |
|---|---|---|
| metric | ScoreMetric | the statistic to resample |
| scores | Float | one score per instance; higher means more positive |
| actuals | Bool | true where the instance really is positive |
| reps | Int | how many resamples to draw |
| seed | Int | seeds the resampling |
Return
| Type | Description |
|---|---|
| ConfInterval | the interval, or Nil on bad input |
ConfusionMatrix # native
Creates a confusion matrix
function : native : ConfusionMatrix(predictions:Bool[], actuals:Bool[]) ~ Int[,]Parameters
| Name | Type | Description |
|---|---|---|
| predictions | Bool | predicted boolean values |
| actuals | Bool | actual boolean values |
Return
| Type | Description |
|---|---|
| Int | 2x2 confusion matrix [[TN, FP], [FN, TP]] |
F1Score # native
Calculates F1 score: 2 * (Precision * Recall) / (Precision + Recall)
function : native : F1Score(predictions:Bool[], actuals:Bool[]) ~ FloatParameters
| Name | Type | Description |
|---|---|---|
| predictions | Bool | predicted boolean values |
| actuals | Bool | actual boolean values |
Return
| Type | Description |
|---|---|
| Float | F1 score between 0.0 and 1.0 |
MCC # native
Calculates Matthews Correlation Coefficient (MCC)
function : native : MCC(predictions:Bool[], actuals:Bool[]) ~ FloatParameters
| Name | Type | Description |
|---|---|---|
| predictions | Bool | predicted boolean values |
| actuals | Bool | actual boolean values |
Return
| Type | Description |
|---|---|
| Float | MCC score between -1.0 and 1.0 |
OperatingPoints # function
Operating points of a scored classifier, one row per distinct score, from the highest score down: [threshold, false positives, true positives]. Every metric below is a walk over this. It exists because the functions above take already-thresholded Bool predictions, which throw away exactly what these questions need: a single Bool array fixes one operating point, and cannot answer what the recall would be at a different false-alarm rate. Instances that share a score share an operating point. Splitting them would invent an ordering the scores do not express and let a metric read better than the model earned.
function : OperatingPoints(scores:Float[], actuals:Bool[]) ~ Float[,]Parameters
| Name | Type | Description |
|---|---|---|
| scores | Float | one score per instance; higher means more positive |
| actuals | Bool | true where the instance really is positive |
Return
| Type | Description |
|---|---|
| Float | points x 3 matrix, or Nil when the inputs do not line up |
Percentile # function
A percentile of a set of values, by linear interpolation. Used for bootstrap intervals, and exposed because a caller resampling a statistic this bundle does not implement still needs the interval. p is clamped rather than rejected: 0.0 gives the minimum and 1.0 the maximum, which is what a caller asking for them wants.
function : Percentile(values:Float[], p:Float) ~ FloatParameters
| Name | Type | Description |
|---|---|---|
| values | Float | the sample; not modified |
| p | Float | the percentile in [0,1] |
Return
| Type | Description |
|---|---|
| Float | the percentile, or 0.0 for an empty sample |
PrCurve # function
The precision-recall curve as [threshold, recall, precision] rows.
function : PrCurve(scores:Float[], actuals:Bool[]) ~ Float[,]Parameters
| Name | Type | Description |
|---|---|---|
| scores | Float | one score per instance; higher means more positive |
| actuals | Bool | true where the instance really is positive |
Return
| Type | Description |
|---|---|
| Float | points x 3 matrix, or Nil on bad input |
Precision # native
Calculates precision: TP / (TP + FP)
function : native : Precision(predictions:Bool[], actuals:Bool[]) ~ FloatParameters
| Name | Type | Description |
|---|---|---|
| predictions | Bool | predicted boolean values |
| actuals | Bool | actual boolean values |
Return
| Type | Description |
|---|---|
| Float | precision score between 0.0 and 1.0 |
PrecisionAtBaseRate # function
Precision an operating point would show at an assumed base rate. A detection rate and a false-alarm rate are properties of the detector. The precision an operator sees is not: it depends on how common the thing being detected actually is. The same detector that looks precise on a benchmark where half the records are positive can raise mostly false alarms in a deployment where one in a thousand is. precision = tpr*pi / (tpr*pi + fpr*(1-pi)) Worth being blunt about what this does NOT do. It reprojects the measured rates onto a different class balance, assuming both rates stay the same. They often do not: a deployment whose base rate differs usually differs in other ways too, and the detector does not have to behave identically. The number answers "if these rates held, what precision would this base rate imply" -- which is a bound worth knowing, not a prediction.
function : PrecisionAtBaseRate(tpr:Float, fpr:Float, base_rate:Float) ~ FloatParameters
| Name | Type | Description |
|---|---|---|
| tpr | Float | the detection rate, in [0,1] |
| fpr | Float | the false-alarm rate, in [0,1] |
| base_rate | Float | the assumed proportion of positives, in [0,1] |
Return
| Type | Description |
|---|---|
| Float | the implied precision, 0.0 when no alerts would be raised, or -1.0 when an argument is out of range |
PrecisionAtBaseRate # function
Precision a measured operating point would show at an assumed base rate. Takes the rates from a ThresholdResult, so a threshold that has actually been measured on held-out data can be reprojected without the caller restating its numbers. The same caveat applies: this assumes both rates carry over.
function : PrecisionAtBaseRate(result:ThresholdResult, base_rate:Float) ~ FloatParameters
| Name | Type | Description |
|---|---|---|
| result | ThresholdResult | a measured operating point |
| base_rate | Float | the assumed proportion of positives, in [0,1] |
Return
| Type | Description |
|---|---|
| Float | the implied precision, or -1.0 on bad input |
PrintConfusionMatrix # native
Prints a formatted confusion matrix
function : native : PrintConfusionMatrix(matrix:Int[,]) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| matrix | Int | 2x2 confusion matrix |
Recall # native
Calculates recall (sensitivity): TP / (TP + FN)
function : native : Recall(predictions:Bool[], actuals:Bool[]) ~ FloatParameters
| Name | Type | Description |
|---|---|---|
| predictions | Bool | predicted boolean values |
| actuals | Bool | actual boolean values |
Return
| Type | Description |
|---|---|
| Float | recall score between 0.0 and 1.0 |
RecallAtFpr # function
The highest recall reachable without exceeding a false-positive rate. This is the number an analyst actually cares about: alerts arrive against a fixed budget, so "how many attacks do we catch while raising no more than one false alarm in a thousand" is the question, not accuracy.
function : RecallAtFpr(scores:Float[], actuals:Bool[], max_fpr:Float) ~ FloatParameters
| Name | Type | Description |
|---|---|---|
| scores | Float | one score per instance; higher means more positive |
| actuals | Bool | true where the instance really is positive |
| max_fpr | Float | the false-positive rate that must not be exceeded, 0.0 to 1.0 |
Return
| Type | Description |
|---|---|
| Float | recall at the best qualifying operating point, or -1.0 on bad input |
Example
r := Metrics->RecallAtFpr(scores, actuals, 0.001);
"recall at 0.1% FPR: {$r}"->PrintLine();RecallByGroup # function
Recall within each group, at one threshold. An overall recall is an average over whatever mix of sub-populations the sample happens to contain, and a rare one contributes almost nothing to it. A detector can report strong recall while missing a whole category that makes up a fraction of a percent of the data -- which is often the category that matters most, because rarity in a benchmark is not unimportance in a deployment. Recall is computed over the POSITIVES in each group. Rows of a group that are not positive are counted nowhere: there is no recall to measure on them. A group with no positives is still returned, with a count of zero, so a caller iterating the result sees that it was empty rather than finding it missing.
function : RecallByGroup(scores:Float[], actuals:Bool[], groups:Float[], threshold:Float) ~ GroupRecall[]Parameters
| Name | Type | Description |
|---|---|---|
| scores | Float | one score per instance; higher means more positive |
| actuals | Bool | true where the instance really is positive |
| groups | Float | one group code per instance |
| threshold | Float | the cutoff to apply, as in AtThreshold |
Return
| Type | Description |
|---|---|
| GroupRecall | one entry per distinct group, or Nil when the inputs do not line up |
RocCurve # function
The ROC curve as [threshold, false-positive rate, true-positive rate] rows, with the origin included so it can be plotted directly.
function : RocCurve(scores:Float[], actuals:Bool[]) ~ Float[,]Parameters
| Name | Type | Description |
|---|---|---|
| scores | Float | one score per instance; higher means more positive |
| actuals | Bool | true where the instance really is positive |
Return
| Type | Description |
|---|---|
| Float | (points + 1) x 3 matrix, or Nil on bad input |
ShuffleLabels # function
A seeded permutation of a label array. For a shuffled-label control: a model trained on permuted labels should score near chance, and anything better means information is reaching the model that should not be -- a leak, or a bug in the pipeline rather than in the model. It is the cheapest check there is against a result that is too good. Returns a new array; the input is not modified, because the real labels are almost always still needed afterwards to score the control against something. Seeded so that a control which does find something can be reproduced. An unseeded shuffle that reported an AUC of 0.68 once cannot be investigated.
function : ShuffleLabels(labels:Bool[], seed:Int) ~ Bool[]Parameters
| Name | Type | Description |
|---|---|---|
| labels | Bool | the labels to permute |
| seed | Int | seeds the permutation |
Return
| Type | Description |
|---|---|
| Bool | a permuted copy, or Nil when labels is Nil |
ThresholdAtFpr # function
The score threshold of the operating point RecallAtFpr selects.
function : ThresholdAtFpr(scores:Float[], actuals:Bool[], max_fpr:Float) ~ FloatParameters
| Name | Type | Description |
|---|---|---|
| scores | Float | one score per instance; higher means more positive |
| actuals | Bool | true where the instance really is positive |
| max_fpr | Float | the false-positive rate that must not be exceeded |
Return
| Type | Description |
|---|---|
| Float | the threshold to classify at, or 0.0 on bad input |
UnseenGroups # function
Group codes present in one set and absent from another. For reporting on sub-populations a model was never trained on. Recognising patterns resembling the training data is a different task from detecting something genuinely new, and a single overall number cannot tell them apart, so the groups that were never seen have to be identified before they can be reported separately. Order follows first appearance in later, so the result lines up with a RecallByGroup over the same array.
function : UnseenGroups(earlier:Float[], later:Float[]) ~ Float[]Parameters
| Name | Type | Description |
|---|---|---|
| earlier | Float | the codes already seen, e.g. in training |
| later | Float | the codes to check, e.g. in a test set |
Return
| Type | Description |
|---|---|
| Float | codes in later but not in earlier, or Nil when either is Nil |