/ proof-of-concept / audio-spike-sorting
Sign in
proof-of-concept / audio-spike-sorting
audio-spike-sorting / src / bench / score.ts
91 lines · 3.2 KBCodeBlameHistory
2 * Comparison with ground truth. Matching ignores labels: candidate pairs
3 * within the tolerance are taken greedily by |dt|, so each truth event and
4 * each detection is used at most once and the closest pairs win. A matched
5 * pair with the right label is correct, with the wrong label a
6 * misclassification; unmatched truth is a miss, an unmatched detection a
7 * false positive. The confusion matrix carries all four kinds: an extra
8 * "missed" column and a false-positive row.
9 */
10import type { Detection, GroundTruthEvent, Score, ScoredEvent } from './types.ts';
12export function score(
13 truth: GroundTruthEvent[],
14 detections: Detection[],
15 nTypes: number,
16 toleranceS: number,
17): Score {
18 const pairs: { ti: number; di: number; dt: number }[] = [];
19 // truth is sorted by t; walk a window rather than the full cross product.
20 const dets = [...detections].sort((a, b) => a.t - b.t);
21 let lo = 0;
22 for (let di = 0; di < dets.length; di++) {
23 while (lo < truth.length && truth[lo].t < dets[di].t - toleranceS) lo++;
24 for (let ti = lo; ti < truth.length && truth[ti].t <= dets[di].t + toleranceS; ti++) {
25 pairs.push({ ti, di, dt: Math.abs(truth[ti].t - dets[di].t) });
26 }
27 }
28 pairs.sort((a, b) => a.dt - b.dt);
30 const truthMatch = new Array<number>(truth.length).fill(-1);
31 const detMatch = new Array<number>(dets.length).fill(-1);
32 for (const p of pairs) {
33 if (truthMatch[p.ti] >= 0 || detMatch[p.di] >= 0) continue;
34 truthMatch[p.ti] = p.di;
35 detMatch[p.di] = p.ti;
36 }
38 // confusion: (nTypes + 1) x (nTypes + 1); row nTypes = false positives,
39 // column nTypes = missed.
40 const confusion = Array.from({ length: nTypes + 1 }, () => new Array<number>(nTypes + 1).fill(0));
41 const perType = Array.from({ length: nTypes }, () => ({
42 correct: 0, misclassified: 0, missed: 0, falsePositives: 0, nTruth: 0,
43 }));
44 const events: ScoredEvent[] = [];
45 let correct = 0, misclassified = 0, missed = 0, falsePositives = 0;
47 for (let ti = 0; ti < truth.length; ti++) {
48 const tr = truth[ti];
49 perType[tr.typeIndex].nTruth++;
50 const di = truthMatch[ti];
51 if (di < 0) {
52 missed++;
53 perType[tr.typeIndex].missed++;
54 confusion[tr.typeIndex][nTypes]++;
55 events.push({ kind: 'missed', truth: tr });
56 } else {
57 const d = dets[di];
58 confusion[tr.typeIndex][d.typeIndex]++;
59 if (d.typeIndex === tr.typeIndex) {
60 correct++;
61 perType[tr.typeIndex].correct++;
62 events.push({ kind: 'correct', truth: tr, det: d });
63 } else {
64 misclassified++;
65 perType[tr.typeIndex].misclassified++;
66 events.push({ kind: 'misclassified', truth: tr, det: d });
67 }
68 }
69 }
70 for (let di = 0; di < dets.length; di++) {
71 if (detMatch[di] >= 0) continue;
72 falsePositives++;
73 const d = dets[di];
74 if (d.typeIndex >= 0 && d.typeIndex < nTypes) {
75 perType[d.typeIndex].falsePositives++;
76 confusion[nTypes][d.typeIndex]++;
77 }
78 events.push({ kind: 'falsePositive', det: d });
79 }
81 const denom = correct + misclassified + missed + falsePositives;
82 return {
83 nTruth: truth.length,
84 nDetected: dets.length,
85 correct, misclassified, missed, falsePositives,
86 accuracy: denom > 0 ? correct / denom : 1,
87 confusion,
88 events,
89 perType,
90 };
↑↓ move↵openescclose