K-Nearest-Neighbours Classifier
K-nearest-neighbours makes no assumption about the shape of the boundary at all: to score a new row it finds the k stored training rows nearest to it on standardised predictors and votes the share belonging to the event of interest.
R equivalent
library(class)
set.seed(1)
n <- 200
amount <- c(rnorm(n / 2, mean = 40, sd = 15), rnorm(n / 2, mean = 220, sd = 60))
velocity <- c(rpois(n / 2, 2), rpois(n / 2, 9))
fraud <- factor(c(rep(0, n / 2), rep(1, n / 2)))
train_x <- scale(data.frame(amount, velocity))
new_cases <- data.frame(amount = c(35, 260), velocity = c(1, 10))
new_x <- scale(new_cases, center = attr(train_x, "scaled:center"),
scale = attr(train_x, "scaled:scale"))
result <- knn(train_x, new_x, cl = fraud, k = 15, prob = TRUE)
print(result)
print(attr(result, "prob"))
Console output:
[1] 0 1
attr(,"prob")
[1] 1 1
Levels: 0 1
[1] 1 1
Tool equivalent:
Jube.Service.Repository.AnalyticsTrainingRun.AdvancedAnalyticsKNearestNeighboursClassifierTrainingService / tool
SubmitKNearestNeighboursClassifierTraining — Kind = Classifier, recall class KNearestNeighboursClassifierRecall.
NeighbourCount (default 15) is k; Standardise (default true) is the scale() step above. This family is unlike
every other classifier family in this section: “training” really is just remembering the rows, so the background worker
stores every standardised, imputed training row inside the model version and recall means searching those stored rows,
rather than a small fitted parameter set. The raw vote fraction is still Platt-calibrated (Calibrate,
DecisionThreshold) exactly as the other classifiers are.
Code audit
AdvancedAnalyticsKNearestNeighboursClassifierTrainingService.SubmitTrainingAsync
(Jube.Service/Repository/AnalyticsTrainingRun/AdvancedAnalyticsKNearestNeighboursClassifierTrainingService.cs:48-50,85-135)
only queues the run; the actual fit happens in KNearestNeighboursClassifierTrainer.Train
(Jube.Engine/AdvancedAnalytics/Classification/KNearestNeighboursClassifierTrainer.cs:36-58). That trainer calls no
Accord.MachineLearning.* type at all — there is no Accord.MachineLearning.KNearestNeighbors in this codepath.
Training and recall are both a hand-written implementation in
Jube.Engine.AdvancedAnalytics.Classification.
- Distance metric: squared Euclidean distance, accumulated per variable in a plain double loop
(
KNearestNeighboursClassifierPipeline.Explain,Jube.Engine/AdvancedAnalytics/Classification/KNearestNeighboursClassifierPipeline.cs:87-101). The square root is taken only when reporting the neighbour distances for explanation (line 117), never for ranking, so ranking is equivalent to plain Euclidean distance. - Neighbour search structure: brute force. Every call to
Explainscans every stored training row (for (var i = 0; i < _rows.Length; i++), line 89) — an O (rows) linear scan with no spatial index; there is no k-d tree, no ball tree and no approximate search of any kind. - Tie-breaking: neighbours are selected with
Enumerable.Range(0, _rows.Length).OrderBy(i => distancesSquared[i]).Take(NeighbourCount)(lines 103-106). LINQ’sOrderByis a stable sort, so rows tied on distance keep their original storage order — the lowest-index (earliest-inserted) tied row always wins. This is an implicit rule; nothing in the DTO or its validator documents it. - k validated against dataset size: yes, in two places.
KNearestNeighboursClassifierTrainer.TrainWithSplitthrows before any fitting iftrainX.Length < options.NeighbourCount(lines 91-94), andKNearestNeighboursClassifierPipeline’s constructor repeats the same check defensively (lines 39-42). There is no equivalent check against the validation or test split sizes, only against the training split. - Vote fraction to probability: the raw vote fraction (share of the k neighbours labelled 1, computed at
KNearestNeighboursClassifierPipeline.Explainlines 134-135) is not returned as-is. WhenCalibrateis true and the validation split contains both classes it is passed throughJube.Analytics.PlattCalibration.Fit(Jube.Analytics/PlattCalibration.cs:18-165) — a from-scratch re-implementation of Platt scaling with the Lin/Weng/Keerthi (2007) target smoothing (high/lowat lines 88-89) and a damped-Newton line search (lines 110-162). The vendoredAccord.MachineLearning.VectorMachines.Learning.Probabilistic.ProbabilisticOutputCalibrationclass (Accord.MachineLearning/VectorMachines/Learning/Probabilistic/ProbabilisticOutputCalibration.cs) is never referenced by this family. - Snapshot cost: the persisted model (
AdvancedAnalyticsKNearestNeighboursClassifierPipelineSnapshot, populated atKNearestNeighboursClassifierTrainer.cs:243-262) stores every standardised, imputed training row together with its label. Recall re-runs the same brute-force scan against those stored rows, so recall cost, not just snapshot size, scales linearly with the training split’s row count — the trainer’s ownLargeSnapshotWarningThreshold(200,000 rows, lines 34, 237-241) exists specifically because there is no sparsification of the stored rows at any complexity or neighbour count.
