Voidly Atlas already ships four independent unsupervised anomaly detectors, each capturing a different axis of unusual network behavior:
The friction problem this finding solves: a journalist checking coordinated censorship today had to hit four separate endpoints and reconcile four different score scales. We now fuse all four into one composite anomaly score per country per day, with the per-detector breakdown returned inline so the explanation isn’t hidden.
Heuristic weights, chosen up-front and documented in the
weights field of every response:
composite = 0.35 * norm(dbscan)
+ 0.25 * norm(stl)
+ 0.20 * norm(burst)
+ 0.20 * norm(hdbscan)
Each component is min-max normalized to [0, 1] across the population of scored country-days for that day’s snapshot. Composites are computed only over present components; weights renormalize when a detector has no observation for a country.
Evaluating each detector at the exact (country, date)
of every labeled incident in the last 60 days (n=2,806 labels, 837
positive):
| Detector | AUC vs labels |
|---|---|
| DBSCAN (per-country shape) | 0.6306 |
| STL (seasonal residual) | 0.5540 |
| HDBSCAN (per-domain drift) | 0.4443 |
| Burst (multi-country coincidence) | 0.4222 |
| Raw composite (all 4, fixed weights) | 0.4949 |
The raw composite is below chance, because two detectors (burst,
hdbscan) are actively anti-correlated with positive labels in this
period. That isn’t a bug — it means those detectors are
surfacing a different population than what the v3.3 label rule treats
as “censorship.” In particular: the 2026-05-21 burst
was a coordinated OONI sweep of tiktok.com, and the
drifting domains this week are AI services with no per-country label
rule. Both are real signals, just not the same signal as labels.
The build script tries three fusion strategies, picks the one with
the highest at-label AUC, and reports the choice (and the other two
AUCs) in eval for transparency:
raw — the 4-detector weighted average as
spec’d.sign_corrected — flip detectors with AUC<0.5
(i.e. anti-correlated) before averaging.dropped — drop detectors with AUC<0.5
entirely, reweight survivors.
On today’s build the winning strategy is
dropped: burst + HDBSCAN are excluded, DBSCAN + STL
combine for a composite AUC of 0.6842 — +5pp above the
strongest single detector (DBSCAN at 0.6306). That clears
the “composite must beat best single” promotion floor in
the spec, so the endpoint is exposed.
Critically, we still expose the all-4 view in every response:
n_all4_strong, n_all4_present, and
all4_strong_flags answer “how many of the 4
original detectors are firing on this country today, regardless of
eval-time weighting decisions?” That preserves the
journalist-facing question (multi-detector agreement) without
pretending all four are equally informative for the label rule.
Top 5 countries by composite anomaly score on 2026-05-21 (AU, DE, JP, NL, CA) all show DBSCAN flagging the country-day as a high-distance noise point with corroborating STL residual; 1 country (US) shows all 4 detectors firing strong — DBSCAN anomaly_score 8.40, STL residual 0.49, member of a 15-country burst on tiktok.com, and weighted HDBSCAN drift 0.23 from a domain mix where tiktok.com leads. That doesn’t imply US is censoring today; it does mean the network shape, the seasonal residual, the multi-country coincidence, AND the per-domain drift signal are all above 75th-pct simultaneously.
components[*].norm_raw field lets you reconstruct
the unflipped value.dropped back to
raw or sign_corrected automatically.
That’s good (we always use the best available combination)
but means the composite scale shifts day-to-day; clients should
compare relative ranks, not absolute composite values, across
builds.GET /v1/anomaly/fused/<cc> — composite +
per-detector breakdown + all-4 agreement flags.GET /v1/anomaly/fused/leaderboard?limit=50&only_3of4=trueGET /v1/anomaly/fused/info — weights, eval
AUCs, strategy used, caveats.
Cron: daily 05:30 UTC after the four upstream detectors finish.
Sidecar: /opt/voidly-ai/ml-deploy/fused_anomaly_v1.json.
Source: scripts/build-fused-anomaly-ensemble.py +
scripts/patch-fused-anomaly-endpoint.py.