One model, four decisions, every number checkable.
What the detector is made of, what it was trained and tested on, how it performs on curves it never saw — recomputed in your browser as you read — and where it should not be trusted.
What the model is.
- Task
- four independent present / absent classifiers (AA, 5-HT, 2E, Mela)
- Input
- raw SWV curve, 200 points, 0.004–0.800 V, no blank subtraction
- Denoising
- sym4 wavelet, 3 levels, soft universal threshold, σ from the MAD of the finest band
- Features
- full rbio1.3 DWT, 6 levels, 228 coefficients (cA6, cD6 … cD1)
- Scaling
- z-score with training statistics
- Classifier
- LightGBM per analyte · learning rate 0.10 · 31 leaves · 300 trees
- Augmentation
- —
- Threshold rule
- MaxSSS (Youden) cut on the real training rows
- Thresholds
- —
- Training data
- 1,114 real curves · seed-0 outer-training partition of ACS-ML-Biomarker
- Evaluation data
- 125 held-out curves from 46 blocks
What it does on 125 curves it never saw.
Your browser loads the model, scores each held-out curve, and compares P ≥ threshold with the label (concentration > 0). Nothing below is typed in.
| Analyte | Present | Absent | Accuracy | Balanced acc. | Sensitivity | Specificity |
|---|
| File | Analyte | P | Label |
|---|
Read this as a sanity check, not a benchmark. One fitted model, one held-out partition; replicates of a mixture share a block, so curves are not independent.
Same answers as Python.
Decisions that differ from the Python reference probabilities stored with each curve.
- max |Pbrowser − Ppython|
- —
- max |Δ denoised|
- 4.4e-16
- max |Δ features|
- 2.7e-15
- max |Δ raw margin|
- 0
- max |Δ probability|
- 8.7e-19
- decision disagreements
- 0
Run with node tools/parity.mjs, every stage compared.
The browser engine reimplements PyWavelets (wavedec / waverec, symmetric mode) and LightGBM tree traversal. Trees, filters, scaler and thresholds are exported from Python into data/model.json; nothing is retrained or approximated in the page.
Where it should not be trusted.
- One model, one partition. A single fitted model and a single split (seed 0). Spread across refits and partitions was not measured.
- Held-out curves come in blocks. Replicates of one mixture sit in the same block, so the 125 curves are not independent samples.
- Only seen these chips. ACS-ML-Biomarker data: P4VP / PVDF coatings, CHI760F, SWV 25 mV, 5 Hz. Its probabilities do not signal when a curve is from elsewhere; a feature z-score check could not separate such curves, because held-out curves already reach max |z| ≈ 950.
- Boundary effects. A 6-level DWT on 200 points has them: 30 of the 228 coefficients depend almost only on the first or last 10 points and carry 9–20% of the model's split gain.
- Saturating probabilities. P pins near 0 and 1, so explanations are read on the log-odds scale.
- Detection only. The model says present or absent; it gives no concentration estimate.
Rebuild it yourself.
- tools/export_model.py
Refits the model through the campaign code; writes model.json and library.json.
- tools/export_xai.py
Writes xai.json from the XAI run, after checking that rows and model match.
- node tools/parity.mjs
Compares the browser engine with Python at every stage on all 125 curves.