Censorship-vs-natural-outage attribution meta-classifier
Many recorded shutdowns are ambiguous — a country goes dark for eight hours, was it state-ordered censorship or a fiber cut, BGP misconfiguration, DDoS, weather, planned maintenance? Confirmed-censorship attribution carries journalistic weight; misattributing a natural outage as censorship is reputation-damaging. This finding ships a meta-classifier at POST /v1/atlas/attribute-outage that takes a country / time window and returns probabilities across censorship | natural_outage (with ddos / infrastructure / weather / maintenance sub-cause hints) | mixed_unknown, plus a verdict, a band (high / medium / low / very_low), and the top-5 contributing features with their current values. Model: GradientBoostingClassifier (200 trees, depth 4, lr 0.05) trained on 2,696 incidents — 399 censorship+mixed positive, 2,297 IODA disruption negative — using 52 features across declared-incident fields, temporal shape (duration, time-of-day, weekday, very-long-flag), and evidence-window shape (signal-type mix, ASN diversity, source diversity, NEWS/ANON/GRP domain category fractions, blocking-method count, critical-signal fraction). Stratified 5-fold CV AUC 0.9974. PASSED both promote floors: censorship recall at P>=0.6 is 99.2% OOF / 100% in-sample (floor 80%), natural-outage correct at P<0.4 is 99.8% OOF / 100% in-sample (floor 60%). Per-incident-type recall on the 343 confirmed censorship rows: 100% in-sample, >99% OOF. Honest caveats inline in every response: (1) sub-causes are HEURISTIC feature-shape hints not supervised classes — we have no ground truth for ddos / fiber / weather; (2) the negative class is a PRIOR not labeled ground truth — some IODA disruption rows are probably mislabeled state shutdowns; (3) the headline AUC is partly recovering the labeling rule (top feature is has_ioda_source at 87.6% importance) so we also report honest_auc_no_source_features=0.9978 by retraining with the eight source-identity features dropped — the genuine shape-signal AUC; (4) the [0.40, 0.60) range is reserved as mixed_unknown for ambiguous windows. Implementation: scripts/build-outage-attribution-features.py + train-outage-attribution.py + patch-outage-attribution-endpoint.py.