Links: CS224W L14 (advanced topics: conformal prediction, the L14 proof workshop on split conformal),
L21 (trust), notebooks/project/retweet_thread.py §L14 (the same detector with 95% prediction sets). Atlas: uncertainty
quantification; trustworthy ML on social data.
Question
Split conformal prediction promises that a prediction set contains the true label with probability at least 1 − α, for any model, as long as calibration and test data are exchangeable. A moderation team that publishes "95% coverage" makes that promise about next month's accounts, but next month's coordinated campaign may use different tactics. Does the guarantee survive, for whom does it break, and how cheaply can it be repaired?
Hypotheses (written before running)
-
In distribution. On new worlds of the training type, both marginal and class-conditional (Mondrian) conformal cover at least 0.93 of accounts, and Mondrian covers at least 0.90 of bots.
-
Marginal coverage hides the failure. Under every shift, marginal coverage stays at least 0.93, because humans are 97% of accounts.
-
A tactic change breaks the bot guarantee. When rings switch from reciprocal to co-amplification, Mondrian bot coverage falls below 0.50.
-
Cheap repair. Recalibrating Mondrian on 20 labelled bots (and 200 humans) of the new type restores bot coverage to at least 0.85.
Setup
- Detector: the label-free model (
gl.exp.label_free_features, logistic regression), trained on one reciprocal world. - Calibration: three other reciprocal worlds (90 bots). Score: 1 − p̂(true label). α = 0.05.
-
Test worlds (new seeds) of six types: same (reciprocal), co-amplification, naive, heavy camouflage (30 camouflage retweets per bot), large rings (3 × 20), and early (reciprocal observed for 10 of 30 days).
-
Reported: overall, bot and human coverage; share of bots flagged as {bot} alone; share of uninformative sets {human, bot}.
Run: uv run marimo edit experiments/conformal-shift/conformal_shift.py and press the run button.
Results (summary; full table in results.md)
| test type | marginal: coverage | marginal: bot coverage | Mondrian: bot coverage | recalibrated: bot coverage | recalibrated: {human, bot} sets |
|---|---|---|---|---|---|
| same | 0.953 | 0.978 | 0.967 | 0.967 | 0.000 |
| co-amplify | 0.939 | 0.233 | 0.144 | 0.967 | 0.554 |
| naive | 0.954 | 1.000 | 1.000 | 0.978 | 0.000 |
| heavy camouflage | 0.947 | 1.000 | 1.000 | 0.900 | 0.000 |
| large rings | 0.943 | 0.989 | 0.978 | 0.978 | 0.000 |
| early (day 10) | 0.955 | 0.156 | 0.133 | 1.000 | 0.751 |
-
H1–H4 all supported. The guarantee holds in distribution; under the two damaging shifts (a tactic change to co-amplification, and catching a campaign after 10 days) marginal coverage still reads 0.94–0.96 while bot coverage is 0.13–0.23. Mondrian makes no difference without exchangeability.
-
Not every shift hurts: heavier camouflage, bigger rings and naive bots are all still covered, because the detector still separates them. Conformal fails exactly when the model fails, and says nothing about it.
-
Recalibrating on 20 labelled bots of the new type restores bot coverage (0.90–1.00) by making sets honest: 55% (co-amplify) and 75% (early) of accounts get {human, bot}.
-
Side effect: recalibrated human coverage falls to 0.84–0.92 in several types. A reading, not yet tested: one sibling world is one draw of world-level randomness, so its accounts are not exchangeable with another world's.
Real-data extension: Twitch countries (added after the first run; hypotheses written before running it)
The synthetic shifts were designed. A real platform has shifts nobody designed: Twitch's six country graphs
(gl.datasets.twitch) share one label (does the streamer broadcast mature content?) and the same account fields, but the
communities, the share of mature streamers (0.25 in RU to 0.61 in DE) and the follow patterns differ. Detector: logistic
regression on account fields that mean the same everywhere (log views, log account age, partner, log degree, clustering)
and their neighbour means, trained on 40% of England (ENGB), calibrated on another 30%, tested on the last 30% ("same")
and on DE, ES, FR, PTBR and RU. α = 0.05; "positive" = mature.
- In distribution. On held-out ENGB accounts, marginal and Mondrian coverage are both at least 0.93.
-
A real shift breaks class-conditional coverage. In at least 2 of the 5 other countries, Mondrian coverage of one class falls below 0.90.
-
Marginal coverage hides it. In those countries marginal coverage stays at least 0.90.
- Cheap repair. Recalibrating Mondrian on 200 labelled accounts of the target country gives both classes at least 0.90 coverage in every country.
Run: the second button in the notebook (under a minute; downloads 3 MB). Results go to twitch_results.json and a
separate section of results.md.
Twitch results (summary; full table in results.md)
| country | mature share | detector AUC | marginal: coverage | Mondrian: non-mature coverage | recalibrated: worst class | recalibrated: both-label sets |
|---|---|---|---|---|---|---|
| ENGB (same) | 0.55 | 0.608 | 0.947 | 0.948 | 0.947 | 0.848 |
| DE | 0.60 | 0.658 | 0.941 | 0.870 | 0.944 | 0.819 |
| ES | 0.29 | 0.655 | 0.918 | 0.893 | 0.949 | 0.845 |
| FR | 0.37 | 0.620 | 0.863 | 0.793 | 0.951 | 0.863 |
| PTBR | 0.35 | 0.627 | 0.930 | 0.909 | 0.941 | 0.833 |
| RU | 0.25 | 0.545 | 0.925 | 0.914 | 0.947 | 0.879 |
- H8 supported. Held-out England is covered at 0.947 (marginal and Mondrian).
-
H9 supported. Under the country shift the non-mature class loses coverage under Mondrian: 0.87 (DE), 0.89 (ES), 0.79 (FR). Mature streamers stay over-covered (0.97–0.99); why the shift hits one class and not the other is not tested here.
-
H10 refuted. Unlike the synthetic shift, marginal coverage does not hide this one: France falls to 0.863 and Spain to 0.918. A real shift moved the whole score distribution, not one small class, so the aggregate shows it. The synthetic finding (marginal coverage 0.94–0.96 while bots drop to 0.13–0.23) depended on bots being 3% of accounts.
-
H11 supported. Recalibrating on 200 labelled accounts of the country restores both classes to 0.94–0.96 everywhere.
- Caveat. The detector is weak (AUC 0.55–0.66 from account fields and neighbour means), so 72–88% of sets are {mature, non-mature}: the guarantee is honest because it is mostly uninformative. The synthetic study's lesson holds: conformal repairs honesty, not accuracy.
Next steps
- Recalibrate from several small samples of different worlds (test the side-effect reading).
-
Weighted conformal under covariate shift (Tibshirani et al. 2019) with density-ratio weights from unlabelled new-world accounts, which needs no new labels.
-
Shift detection: a conformal martingale over the stream of new accounts' p-values (Vovk), to raise an alarm when exchangeability breaks.
Results
Generated by conformal_shift.py from config.yaml (α = 0.05, train world 0,
calibration worlds [1, 2, 3], test worlds [10, 11, 12]). Means over test worlds.
Thresholds: marginal 0.470; Mondrian human 0.472, bot 0.326.
| test type | method | coverage | bot coverage | human coverage | bots flagged {bot} alone | {human, bot} sets |
|---|---|---|---|---|---|---|
| same | marginal | 0.953 | 0.978 | 0.952 | 0.978 | 0.000 |
| same | Mondrian | 0.953 | 0.967 | 0.952 | 0.967 | 0.000 |
| same | Mondrian, recalibrated | 0.913 | 0.967 | 0.911 | 0.967 | 0.000 |
| co-amplify | marginal | 0.939 | 0.233 | 0.957 | 0.233 | 0.000 |
| co-amplify | Mondrian | 0.937 | 0.144 | 0.957 | 0.144 | 0.000 |
| co-amplify | Mondrian, recalibrated | 0.839 | 0.967 | 0.836 | 0.489 | 0.554 |
| naive | marginal | 0.954 | 1.000 | 0.953 | 1.000 | 0.000 |
| naive | Mondrian | 0.954 | 1.000 | 0.953 | 1.000 | 0.000 |
| naive | Mondrian, recalibrated | 0.880 | 0.978 | 0.878 | 0.978 | 0.000 |
| heavy camouflage | marginal | 0.947 | 1.000 | 0.946 | 1.000 | 0.000 |
| heavy camouflage | Mondrian | 0.947 | 1.000 | 0.946 | 1.000 | 0.000 |
| heavy camouflage | Mondrian, recalibrated | 0.922 | 0.900 | 0.923 | 0.900 | 0.000 |
| large rings | marginal | 0.943 | 0.989 | 0.941 | 0.989 | 0.000 |
| large rings | Mondrian | 0.943 | 0.978 | 0.941 | 0.978 | 0.000 |
| large rings | Mondrian, recalibrated | 0.908 | 0.978 | 0.904 | 0.978 | 0.000 |
| early (day 10) | marginal | 0.955 | 0.156 | 0.975 | 0.156 | 0.000 |
| early (day 10) | Mondrian | 0.954 | 0.133 | 0.975 | 0.133 | 0.000 |
| early (day 10) | Mondrian, recalibrated | 0.974 | 1.000 | 0.974 | 0.200 | 0.751 |
Verdicts: H1 supported, H2 supported, H3 supported, H4 supported.
Real-data extension: Twitch countries
Generated by conformal_shift.py (Twitch extension) from config.yaml: train and calibrate on
ENGB, test on held-out ENGB ("same") and DE, ES, FR, PTBR, RU; α = 0.05; seeds
[0, 1, 2]; recalibration on 200 labelled accounts of the target country. Means over seeds.
| country | method | mature share | detector AUC | coverage | mature coverage | non-mature coverage | both-label sets |
|---|---|---|---|---|---|---|---|
| same | marginal | 0.55 | 0.608 | 0.947 | 0.949 | 0.945 | 0.848 |
| same | Mondrian | 0.55 | 0.608 | 0.947 | 0.947 | 0.948 | 0.848 |
| same | Mondrian, recalibrated | 0.55 | 0.608 | 0.947 | 0.947 | 0.948 | 0.848 |
| DE | marginal | 0.60 | 0.658 | 0.941 | 0.991 | 0.865 | 0.747 |
| DE | Mondrian | 0.60 | 0.658 | 0.943 | 0.990 | 0.870 | 0.753 |
| DE | Mondrian, recalibrated | 0.60 | 0.658 | 0.956 | 0.964 | 0.944 | 0.819 |
| ES | marginal | 0.29 | 0.655 | 0.918 | 0.987 | 0.890 | 0.817 |
| ES | Mondrian | 0.29 | 0.655 | 0.920 | 0.986 | 0.893 | 0.821 |
| ES | Mondrian, recalibrated | 0.29 | 0.655 | 0.953 | 0.962 | 0.949 | 0.845 |
| FR | marginal | 0.37 | 0.620 | 0.863 | 0.995 | 0.786 | 0.723 |
| FR | Mondrian | 0.37 | 0.620 | 0.867 | 0.995 | 0.793 | 0.731 |
| FR | Mondrian, recalibrated | 0.37 | 0.620 | 0.954 | 0.960 | 0.951 | 0.863 |
| PTBR | marginal | 0.35 | 0.627 | 0.930 | 0.978 | 0.905 | 0.818 |
| PTBR | Mondrian | 0.35 | 0.627 | 0.932 | 0.977 | 0.909 | 0.822 |
| PTBR | Mondrian, recalibrated | 0.35 | 0.627 | 0.947 | 0.957 | 0.941 | 0.833 |
| RU | marginal | 0.25 | 0.545 | 0.925 | 0.973 | 0.909 | 0.869 |
| RU | Mondrian | 0.25 | 0.545 | 0.928 | 0.972 | 0.914 | 0.872 |
| RU | Mondrian, recalibrated | 0.25 | 0.545 | 0.949 | 0.947 | 0.949 | 0.879 |
Verdicts: H8 supported, H9 supported, H10 refuted, H11 supported.
In the repository: experiments/conformal-shift/ · README.md config.yaml conformal_shift.py results.json results.md twitch_results.json