( name: string, classifyFn: (msg: string) => ContextType, nRuns: number = 1, // >1 for stochastic classifiers )
| 293 | } |
| 294 | |
| 295 | function evaluateClassifier( |
| 296 | name: string, |
| 297 | classifyFn: (msg: string) => ContextType, |
| 298 | nRuns: number = 1, // >1 for stochastic classifiers |
| 299 | ): ClassifierResult { |
| 300 | // For stochastic classifiers, run multiple times and average |
| 301 | const allCorrect: boolean[] = [] |
| 302 | |
| 303 | // Accumulate per-class counts across all runs |
| 304 | const tpAccum: Record<string, number> = {} |
| 305 | const fpAccum: Record<string, number> = {} |
| 306 | const fnAccum: Record<string, number> = {} |
| 307 | for (const ctx of CONTEXT_TYPES) { |
| 308 | tpAccum[ctx] = 0; fpAccum[ctx] = 0; fnAccum[ctx] = 0 |
| 309 | } |
| 310 | |
| 311 | for (let run = 0; run < nRuns; run++) { |
| 312 | for (const test of TEST_CASES) { |
| 313 | const predicted = classifyFn(test.message) |
| 314 | const correct = predicted === test.expectedContext |
| 315 | allCorrect.push(correct) |
| 316 | |
| 317 | if (correct) { |
| 318 | tpAccum[test.expectedContext]++ |
| 319 | } else { |
| 320 | fpAccum[predicted]++ |
| 321 | fnAccum[test.expectedContext]++ |
| 322 | } |
| 323 | } |
| 324 | } |
| 325 | |
| 326 | // Bootstrap CI on accuracy |
| 327 | const ci = bootstrapCI(allCorrect) |
| 328 | |
| 329 | // Per-class metrics (averaged over runs) |
| 330 | const perClass: Record<string, { precision: number; recall: number; f1: number }> = {} |
| 331 | for (const ctx of CONTEXT_TYPES) { |
| 332 | const tp = tpAccum[ctx] / nRuns |
| 333 | const fp = fpAccum[ctx] / nRuns |
| 334 | const fn = fnAccum[ctx] / nRuns |
| 335 | const precision = tp + fp > 0 ? tp / (tp + fp) : 0 |
| 336 | const recall = tp + fn > 0 ? tp / (tp + fn) : 0 |
| 337 | const f1 = precision + recall > 0 ? 2 * precision * recall / (precision + recall) : 0 |
| 338 | perClass[ctx] = { precision, recall, f1 } |
| 339 | } |
| 340 | |
| 341 | const macroF1 = CONTEXT_TYPES.reduce((sum, ctx) => sum + perClass[ctx].f1, 0) / CONTEXT_TYPES.length |
| 342 | |
| 343 | return { |
| 344 | name, |
| 345 | accuracy: ci.mean, |
| 346 | ci_lower: ci.lower, |
| 347 | ci_upper: ci.upper, |
| 348 | perClass, |
| 349 | macroF1, |
| 350 | } |
| 351 | } |
| 352 |
no test coverage detected