HungryTorch commited on
Commit
56eb39d
·
verified ·
1 Parent(s): 1c68f9a

Add cross-species perturbation task coverage

Browse files
Files changed (8) hide show
  1. README.md +1 -1
  2. conftest.py +145 -0
  3. test_boards.py +207 -0
  4. test_evaluator.py +787 -0
  5. test_leaderboard.py +292 -0
  6. test_render.py +248 -0
  7. test_results.py +123 -0
  8. test_scoring.py +133 -0
README.md CHANGED
@@ -170,7 +170,7 @@ Keep the live benchmark unchanged until the local artifact and baseline checks
170
  pass. Then deploy in this order: the backward-compatible scorer, public baseline
171
  inputs, private targets and registry, and finally refreshed baselines. Existing
172
  task-result rows stay in place; their submissions simply lack coverage for the
173
- new datasets until their owners submit embeddings for `d011`–`d014`.
174
 
175
  ## Baselines
176
 
 
170
  pass. Then deploy in this order: the backward-compatible scorer, public baseline
171
  inputs, private targets and registry, and finally refreshed baselines. Existing
172
  task-result rows stay in place; their submissions simply lack coverage for the
173
+ new datasets until their owners submit embeddings for `d011`–`d016`.
174
 
175
  ## Baselines
176
 
conftest.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared fixtures for the Space unit tests -- no network, no Gradio.
2
+
3
+ One small registry stands in for the real ``tasks.yaml``: two tasks share a
4
+ dataset (so per-cohort counting is exercised), three therapeutic areas and
5
+ three categories, all on one modality. Every dataset gets its own ``cohort_id``
6
+ here; the tests that care about shared cohorts rewrite one.
7
+ """
8
+
9
+ import pandas as pd
10
+ import pytest
11
+ from boards import build_boards
12
+ from results import IS_BASELINE
13
+
14
+ RESULT_ROW = ["model_name", "task_id", "score", "submitted_at"]
15
+
16
+ REGISTRY = {
17
+ "t001": {
18
+ "task_id": "t001",
19
+ "dataset_id": "d001",
20
+ "cohort_id": "ibd-antitnf",
21
+ "category": "treatment_outcome",
22
+ "metric": "auroc",
23
+ "modality": "bulk RNA",
24
+ "therapeutic_area": "Gastroenterology",
25
+ "diseases": ["Crohn Disease", "Colitis, Ulcerative"],
26
+ "tissue": "Digestive system",
27
+ "title": "IBD adalimumab remission",
28
+ "description": "Will this patient respond?",
29
+ "n_samples": 90,
30
+ "label": {0: "no", 1: "yes"},
31
+ "sources": ["E-MTAB-7604"],
32
+ "license": "EMBL-EBI Terms of Use",
33
+ },
34
+ "t002": {
35
+ "task_id": "t002",
36
+ "dataset_id": "d002",
37
+ "cohort_id": "cd-mucosa",
38
+ "category": "clinical_scores",
39
+ "metric": "pearson",
40
+ "modality": "bulk RNA",
41
+ "therapeutic_area": "Gastroenterology",
42
+ "diseases": ["Crohn Disease"],
43
+ "tissue": "Digestive system",
44
+ "title": "Crohn SES-CD",
45
+ "description": "How severe?",
46
+ "n_samples": 120,
47
+ "sources": ["GSE193677"],
48
+ "license": "Public (NCBI GEO)",
49
+ },
50
+ "t003": {
51
+ "task_id": "t003",
52
+ "dataset_id": "d002",
53
+ "cohort_id": "cd-mucosa",
54
+ "category": "clinical_scores",
55
+ "metric": "pearson",
56
+ "modality": "bulk RNA",
57
+ "therapeutic_area": "Gastroenterology",
58
+ "diseases": ["Crohn Disease"],
59
+ "tissue": "Digestive system",
60
+ "title": "Crohn HBI",
61
+ "description": "How severe?",
62
+ "n_samples": 110,
63
+ "sources": ["GSE193677"],
64
+ "license": "Public (NCBI GEO)",
65
+ },
66
+ "t004": {
67
+ "task_id": "t004",
68
+ "dataset_id": "d005",
69
+ "cohort_id": "pso-skin",
70
+ "category": "clinical_scores",
71
+ "metric": "pearson",
72
+ "modality": "bulk RNA",
73
+ "therapeutic_area": "Dermatology",
74
+ "diseases": ["Psoriasis"],
75
+ "tissue": "Skin",
76
+ "title": "Psoriasis PASI",
77
+ "description": "How severe?",
78
+ "n_samples": 80,
79
+ "sources": ["GSE54456"],
80
+ "license": "Public (NCBI GEO)",
81
+ },
82
+ "t005": {
83
+ "task_id": "t005",
84
+ "dataset_id": "d007",
85
+ "cohort_id": "ra-cohort",
86
+ "category": "endotype",
87
+ "metric": "auroc",
88
+ "modality": "bulk RNA",
89
+ "therapeutic_area": "Rheumatology",
90
+ "diseases": ["Arthritis, Rheumatoid"],
91
+ "tissue": "Blood",
92
+ "title": "RA blood endotype",
93
+ "description": "Which subtype?",
94
+ "n_samples": 60,
95
+ "label": {0: "fibroid", 1: "lymphoid"},
96
+ "sources": ["E-MTAB-6141"],
97
+ "license": "EMBL-EBI Terms of Use",
98
+ },
99
+ }
100
+
101
+
102
+ @pytest.fixture
103
+ def registry() -> dict[str, dict]:
104
+ return {task_id: dict(task) for task_id, task in REGISTRY.items()}
105
+
106
+
107
+ @pytest.fixture
108
+ def boards(registry):
109
+ return build_boards(registry)
110
+
111
+
112
+ @pytest.fixture
113
+ def named(boards):
114
+ """Look a board up by its display name, e.g. ``named("Rheumatology")``."""
115
+
116
+ def _named(name: str):
117
+ return next(
118
+ board for board in boards if board.name == name or board.modality == name
119
+ )
120
+
121
+ return _named
122
+
123
+
124
+ @pytest.fixture
125
+ def results():
126
+ """A ``task_results.csv`` frame from ``(model, task_id, score, at)`` rows.
127
+
128
+ Deliberately without ``is_baseline``: this is the shape of every row written
129
+ before baselines existed, so the whole suite keeps exercising that path.
130
+ """
131
+
132
+ def _results(*rows) -> pd.DataFrame:
133
+ return pd.DataFrame(list(rows), columns=RESULT_ROW)
134
+
135
+ return _results
136
+
137
+
138
+ @pytest.fixture
139
+ def flagged_results():
140
+ """The same, with the flag: ``(model, task_id, score, at, is_baseline)`` rows."""
141
+
142
+ def _flagged(*rows) -> pd.DataFrame:
143
+ return pd.DataFrame(list(rows), columns=[*RESULT_ROW, IS_BASELINE])
144
+
145
+ return _flagged
test_boards.py ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Unit tests for board construction (no network, no Gradio)."""
2
+
3
+ from boards import (
4
+ AREA_GROUP,
5
+ CATEGORY_GROUP,
6
+ MODALITY_GROUP,
7
+ OPEN_BOARDS,
8
+ build_boards,
9
+ by_slug,
10
+ featured,
11
+ in_group,
12
+ metric_label,
13
+ open_in_group,
14
+ short_code,
15
+ )
16
+
17
+
18
+ def test_modality_board_holds_every_task_of_that_modality(named, registry):
19
+ assert named("bulk RNA").task_ids == set(registry)
20
+
21
+
22
+ def test_area_and_category_boards_each_partition_the_modality(boards, registry):
23
+ for group in (AREA_GROUP, CATEGORY_GROUP):
24
+ covered = [tid for b in in_group(boards, group) for tid in b.task_ids]
25
+ assert sorted(covered) == sorted(registry)
26
+
27
+
28
+ def test_patients_are_counted_per_cohort_not_per_task(named):
29
+ """t002 and t003 read off the same biopsies -- one cohort, not two."""
30
+ gastro = named("Gastroenterology")
31
+ assert gastro.n_tasks == 3
32
+ assert gastro.n_cohorts == 2
33
+ assert gastro.n_patients == 90 + 120
34
+
35
+
36
+ def test_board_counts_roll_up_to_the_modality(named):
37
+ board = named("bulk RNA")
38
+ assert (board.n_cohorts, board.n_patients, board.n_diseases) == (4, 350, 4)
39
+
40
+
41
+ def test_datasets_cut_from_the_same_patients_are_counted_once(registry):
42
+ """Two arms of one trial are two dataset_ids but one group of people."""
43
+ registry["t005"]["cohort_id"] = registry["t001"]["cohort_id"]
44
+ board = next(b for b in build_boards(registry) if b.name == "bulk RNAseq")
45
+ assert board.n_cohorts == 3
46
+ assert board.n_patients == 90 + 120 + 80
47
+
48
+
49
+ def test_a_registry_without_cohorts_still_counts_per_dataset(registry):
50
+ """Falling back to dataset_id is the old behaviour, not a crash."""
51
+ for task in registry.values():
52
+ del task["cohort_id"]
53
+ board = next(b for b in build_boards(registry) if b.name == "bulk RNAseq")
54
+ assert (board.n_cohorts, board.n_patients) == (4, 350)
55
+
56
+
57
+ def test_a_board_never_spans_two_modalities(registry):
58
+ registry["t006"] = {
59
+ **registry["t005"],
60
+ "task_id": "t006",
61
+ "modality": "single-cell",
62
+ }
63
+ for board in build_boards(registry):
64
+ modalities = {registry[t]["modality"] for t in board.task_ids}
65
+ assert modalities == {board.modality}
66
+
67
+
68
+ def test_a_second_modality_adds_cards_instead_of_mixing_them(registry, boards):
69
+ """One single-cell task earns its own modality, area and category card."""
70
+ registry["t006"] = {
71
+ **registry["t005"],
72
+ "task_id": "t006",
73
+ "modality": "single-cell",
74
+ }
75
+ grown = build_boards(registry)
76
+ assert len(in_group(grown, MODALITY_GROUP)) == 2
77
+ assert len(grown) == len(boards) + 3
78
+ rheumatology = [b for b in grown if b.name == "Rheumatology"]
79
+ assert {b.modality for b in rheumatology} == {"bulk RNA", "single-cell"}
80
+
81
+
82
+ def test_featured_leads_with_the_modality_board(boards):
83
+ assert [b.name for b in featured(boards)][0] == "bulk RNAseq"
84
+ assert len(featured(boards)) == 3
85
+
86
+
87
+ def test_slugs_are_unique_and_url_safe(boards):
88
+ slugs = [b.slug for b in boards]
89
+ assert len(set(slugs)) == len(slugs)
90
+ assert all(s.replace("-", "").isalnum() for s in slugs)
91
+
92
+
93
+ def test_unknown_slug_falls_back_to_the_hero_board(boards):
94
+ assert by_slug(boards, "does-not-exist").name == "bulk RNAseq"
95
+ assert by_slug(boards, None).name == "bulk RNAseq"
96
+ assert by_slug([], "anything") is None
97
+
98
+
99
+ def test_slug_round_trips(boards):
100
+ for board in boards:
101
+ assert by_slug(boards, board.slug) is board
102
+
103
+
104
+ def test_registry_without_areas_degrades_to_modality_boards(registry):
105
+ for task in registry.values():
106
+ del task["therapeutic_area"]
107
+ boards = build_boards(registry)
108
+ assert in_group(boards, AREA_GROUP) == []
109
+ assert [b.name for b in in_group(boards, MODALITY_GROUP)] == ["bulk RNAseq"]
110
+
111
+
112
+ def test_empty_registry_yields_no_boards():
113
+ assert build_boards({}) == []
114
+
115
+
116
+ def test_short_codes_come_from_the_name_not_a_lookup_table():
117
+ """A modality nobody planned for still gets a tag instead of a fallback."""
118
+ assert short_code("bulk RNA") == "BUL"
119
+ assert short_code("single-cell RNA") == "SIN"
120
+ assert short_code("Treatment outcome") == "TRE"
121
+ assert short_code("spatial metabolomics") == "SPA"
122
+ assert short_code("") == "n/a"
123
+ assert short_code("42 (!)") == "n/a"
124
+
125
+
126
+ def test_every_board_carries_a_code(boards):
127
+ assert all(board.code.isalpha() and len(board.code) == 3 for board in boards)
128
+
129
+
130
+ def _declared(group: str) -> list[str]:
131
+ return [b.name for b in OPEN_BOARDS if b.group == group]
132
+
133
+
134
+ def test_open_boards_are_the_slices_the_registry_lacks(boards):
135
+ for group in (MODALITY_GROUP, AREA_GROUP):
136
+ names = [b.name for b in open_in_group(boards, group)]
137
+ assert names == _declared(group)
138
+ assert "bulk RNA" not in _declared(MODALITY_GROUP)
139
+ assert "Gastroenterology" not in _declared(AREA_GROUP)
140
+
141
+
142
+ def test_an_open_board_drops_out_when_the_registry_covers_it(registry):
143
+ registry["t006"] = {
144
+ **registry["t005"],
145
+ "task_id": "t006",
146
+ "modality": "single-cell RNA",
147
+ }
148
+ boards = build_boards(registry)
149
+ names = [b.name for b in open_in_group(boards, MODALITY_GROUP)]
150
+ assert "single-cell RNAseq" not in names
151
+ assert len(names) == len(_declared(MODALITY_GROUP)) - 1
152
+
153
+
154
+ def test_an_open_area_drops_out_when_a_cohort_lands_there(registry):
155
+ registry["t006"] = {
156
+ **registry["t005"],
157
+ "task_id": "t006",
158
+ "therapeutic_area": "Oncology",
159
+ }
160
+ names = [b.name for b in open_in_group(build_boards(registry), AREA_GROUP)]
161
+ assert "Oncology" not in names
162
+ assert names == ["Neurology", "Pulmonology"]
163
+
164
+
165
+ def test_task_categories_never_get_an_open_card(boards):
166
+ """A category is pinned to one metric, so an open one would promise a probe."""
167
+ assert open_in_group(boards, CATEGORY_GROUP) == []
168
+ assert _declared(CATEGORY_GROUP) == []
169
+
170
+
171
+ def test_an_empty_registry_still_states_every_open_slice():
172
+ assert len(open_in_group([], MODALITY_GROUP)) == len(_declared(MODALITY_GROUP))
173
+ assert len(open_in_group([], AREA_GROUP)) == len(_declared(AREA_GROUP))
174
+
175
+
176
+ def test_a_truncated_list_drops_the_and(registry):
177
+ """``a, b and c…`` reads as an ellipsis stuck to c; ``a, b, c…`` reads as more."""
178
+ from boards import _join
179
+
180
+ assert _join(["a", "b"]) == "a and b"
181
+ assert _join(["a", "b", "c"]) == "a, b and c"
182
+ assert _join(["a", "b", "c", "d"]) == "a, b, c…"
183
+ assert _join([]) == "n/a"
184
+
185
+
186
+ def test_blurbs_describe_what_the_board_asks(named):
187
+ assert "bulk RNAseq" in named("bulk RNA").blurb
188
+ assert "respond" in named("Treatment outcome").blurb
189
+ assert named("Clinical scores").blurb == "How severe is this patient's disease?"
190
+ assert "Psoriasis" in named("Dermatology").blurb
191
+
192
+
193
+ def test_perturbation_category_is_a_regular_board_with_residual_spearman(registry):
194
+ registry["t018"] = {
195
+ **registry["t005"],
196
+ "task_id": "t018",
197
+ "category": "perturbation_prediction",
198
+ "metric": "residual_spearman",
199
+ }
200
+ board = next(
201
+ board
202
+ for board in build_boards(registry)
203
+ if board.name == "Perturbation response"
204
+ )
205
+ assert board.task_ids == {"t018"}
206
+ assert "treatment" in board.blurb.lower()
207
+ assert metric_label("residual_spearman") == "Residual Spearman score"
test_evaluator.py ADDED
@@ -0,0 +1,787 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Unit tests for the standalone PRIMO probe + score_all (no network)."""
2
+
3
+ import evaluator as ev
4
+ import numpy as np
5
+ import pandas as pd
6
+ import pytest
7
+
8
+
9
+ def test_results_stay_with_scientalab_until_the_history_is_migrated():
10
+ assert ev.RESULTS_REPO == "ScientaLab/primo-results"
11
+
12
+
13
+ def _labels(y, folds, ids=None, repeat_folds=None):
14
+ ids = ids if ids is not None else [f"s{i}" for i in range(len(y))]
15
+ labels = pd.DataFrame({ev.SAMPLE_ID: ids, ev.LABEL: y, ev.FOLD: folds})
16
+ if repeat_folds is not None:
17
+ for repeat, values in enumerate(repeat_folds, start=1):
18
+ labels[f"{ev.FOLD}_{repeat}"] = values
19
+ return labels
20
+
21
+
22
+ def _emb_frame(matrix, ids=None):
23
+ ids = ids if ids is not None else [f"s{i}" for i in range(len(matrix))]
24
+ df = pd.DataFrame(matrix)
25
+ df.index = ids
26
+ return df
27
+
28
+
29
+ def _folds(n, k=5):
30
+ return np.array([i % k for i in range(n)])
31
+
32
+
33
+ def _task(task_id, dataset_id, task_type, metric, therapeutic_area, category=None):
34
+ if category is None:
35
+ category = "treatment_outcome" if metric == "auroc" else "clinical_scores"
36
+ return {
37
+ "task_id": task_id,
38
+ "dataset_id": dataset_id,
39
+ "task_type": task_type,
40
+ "metric": metric,
41
+ "category": category,
42
+ "therapeutic_area": therapeutic_area,
43
+ "disease": "PRIVATE",
44
+ "tissue": "PRIVATE",
45
+ }
46
+
47
+
48
+ def _classification(n=60, seed=42, signal=True):
49
+ rng = np.random.default_rng(seed)
50
+ y = np.array([0, 1] * (n // 2))
51
+ emb = rng.normal(size=(n, 8))
52
+ if signal:
53
+ emb[:, 0] += 3.0 * y
54
+ ids = [f"s{i}" for i in range(n)]
55
+ task = _task("t001", "d001", "classification", "auroc", "Gastro")
56
+ return task, _labels(y, _folds(n), ids), ids, emb
57
+
58
+
59
+ def _regression(n=60, seed=7, signal=True):
60
+ rng = np.random.default_rng(seed)
61
+ y = rng.normal(size=n)
62
+ emb = rng.normal(size=(n, 8))
63
+ if signal:
64
+ emb[:, 0] += 2.0 * y
65
+ ids = [f"r{i}" for i in range(n)]
66
+ task = _task("t002", "d002", "regression", "pearson", "Derm")
67
+ return task, _labels(y, _folds(n), ids), ids, emb
68
+
69
+
70
+ def _write_csv(path, blocks):
71
+ """blocks = {dataset_id: (ids, matrix)} -> a multi-dataset CSV (NaN-padded)."""
72
+ frames = []
73
+ for dataset_id, (ids, mat) in blocks.items():
74
+ df = pd.DataFrame(mat, columns=[f"e{j}" for j in range(mat.shape[1])])
75
+ df.insert(0, ev.SAMPLE_ID, ids)
76
+ df.insert(0, ev.DATASET_ID, dataset_id)
77
+ frames.append(df)
78
+ pd.concat(frames, ignore_index=True).to_csv(path, index=False)
79
+
80
+
81
+ def _no_fetch(task_id, token=None):
82
+ raise AssertionError("fetch_labels should not have been called")
83
+
84
+
85
+ # --- probe: repeated fold-wise scoring ---
86
+
87
+
88
+ def test_repeated_cv_score_averages_external_partitions(monkeypatch):
89
+ labels = _labels(
90
+ np.array([0, 1, 0, 1]),
91
+ np.array([0, 1, 0, 1]),
92
+ repeat_folds=[np.array([1, 0, 1, 0]), np.array([2, 0, 2, 0])],
93
+ )
94
+ seen = []
95
+
96
+ def fold_score(metric, task_type, y, folds, matrix, classes):
97
+ seen.append(folds[0])
98
+ return float(folds[0])
99
+
100
+ monkeypatch.setattr(ev, "_fold_score", fold_score)
101
+ score = ev._repeated_cv_score(
102
+ metric="auroc",
103
+ task_type="classification",
104
+ y=labels[ev.LABEL].to_numpy(),
105
+ labels=labels,
106
+ matrix=np.ones((4, 2)),
107
+ classes=np.array([0, 1]),
108
+ )
109
+ assert seen == [0, 1, 2]
110
+ assert score == 1.0
111
+
112
+
113
+ def test_classification_signal_and_null():
114
+ task, labels, ids, emb = _classification(signal=True)
115
+ res = ev.score_task(task, labels, _emb_frame(emb, ids))
116
+ assert res.score > 0.9
117
+ _, _, ids0, null = _classification(signal=False)
118
+ res0 = ev.score_task(task, labels, _emb_frame(null, ids0))
119
+ assert 0.3 < res0.score < 0.7
120
+
121
+
122
+ def test_regression_signal_and_null():
123
+ task, labels, ids, emb = _regression(signal=True)
124
+ res = ev.score_task(task, labels, _emb_frame(emb, ids))
125
+ assert res.score > 0.8
126
+ _, _, ids0, null = _regression(signal=False)
127
+ res0 = ev.score_task(task, labels, _emb_frame(null, ids0))
128
+ assert abs(res0.score) < 0.4
129
+
130
+
131
+ def test_score_carries_ids_category_and_size():
132
+ task, labels, ids, emb = _classification(n=30)
133
+ res = ev.score_task(task, labels, _emb_frame(emb, ids))
134
+ assert res.task_id == "t001"
135
+ assert res.dataset_id == "d001"
136
+ assert res.n_samples == 30
137
+ assert res.category == "treatment_outcome"
138
+ assert res.metric == "auroc"
139
+ assert np.isfinite(res.score)
140
+
141
+
142
+ def test_missing_sample_raises():
143
+ emb = _emb_frame(np.random.default_rng(1).normal(size=(5, 3)))
144
+ labels = _labels(np.array([0, 1, 0, 1, 0, 1]), _folds(6))
145
+ with pytest.raises(ev.SubmissionError, match="missing"):
146
+ ev._align(labels, emb)
147
+
148
+
149
+ def test_nan_raises():
150
+ matrix = np.ones((4, 3))
151
+ matrix[0, 0] = np.nan
152
+ labels = _labels(np.array([0, 1, 0, 1]), np.array([0, 1, 2, 3]))
153
+ with pytest.raises(ev.SubmissionError, match="NaN"):
154
+ ev._align(labels, _emb_frame(matrix))
155
+
156
+
157
+ def test_id_normalisation_int_vs_str():
158
+ emb = _emb_frame(np.eye(4), ids=["0", "1", "2", "3"])
159
+ labels = _labels(np.array([0, 1, 0, 1]), np.array([0, 1, 2, 3]), ids=[0, 1, 2, 3])
160
+ assert ev._align(labels, emb).shape == (4, 4)
161
+
162
+
163
+ # --- multi-dataset loader ---
164
+
165
+
166
+ def test_multidataset_loader_csv(tmp_path):
167
+ rng = np.random.default_rng(0)
168
+ blocks = {
169
+ "d001": (["a", "b"], rng.normal(size=(2, 4))),
170
+ "d002": (["0", "1", "2"], rng.normal(size=(3, 4))),
171
+ }
172
+ _write_csv(tmp_path / "sub.csv", blocks)
173
+ out = ev.load_submission(tmp_path / "sub.csv")
174
+ assert set(out) == {"d001", "d002"}
175
+ assert ev._to_embedding_frame(out["d001"]).shape == (2, 4)
176
+ assert list(ev._to_embedding_frame(out["d002"]).index) == ["0", "1", "2"]
177
+
178
+
179
+ def test_multidataset_loader_npz(tmp_path):
180
+ rng = np.random.default_rng(1)
181
+ ids = np.array(["a", "b", "c", "d"])
182
+ dsids = np.array(["d001", "d001", "d002", "d002"])
183
+ emb = rng.normal(size=(4, 5))
184
+ np.savez(tmp_path / "sub.npz", dataset_ids=dsids, sample_ids=ids, embeddings=emb)
185
+ out = ev.load_submission(tmp_path / "sub.npz")
186
+ assert set(out) == {"d001", "d002"}
187
+ assert ev._to_embedding_frame(out["d001"]).shape == (2, 5)
188
+
189
+
190
+ def test_missing_dataset_id_column_raises(tmp_path):
191
+ df = pd.DataFrame({ev.SAMPLE_ID: ["a", "b"], "e0": [0.1, 0.2]})
192
+ df.to_csv(tmp_path / "sub.csv", index=False)
193
+ with pytest.raises(ev.SubmissionError, match="dataset_id"):
194
+ ev.load_submission(tmp_path / "sub.csv")
195
+
196
+
197
+ def test_ragged_padding_dims(tmp_path):
198
+ blocks = {
199
+ "d001": (["a", "b"], np.ones((2, 4))),
200
+ "d002": (["a", "b"], np.ones((2, 6))),
201
+ }
202
+ _write_csv(tmp_path / "sub.csv", blocks)
203
+ out = ev.load_submission(tmp_path / "sub.csv")
204
+ assert ev._to_embedding_frame(out["d001"]).shape[1] == 4
205
+ assert ev._to_embedding_frame(out["d002"]).shape[1] == 6
206
+
207
+
208
+ def test_sample_id_reused_across_datasets_ok(tmp_path):
209
+ blocks = {
210
+ "d001": (["x", "y"], np.ones((2, 3))),
211
+ "d002": (["x", "y"], np.ones((2, 3))),
212
+ }
213
+ _write_csv(tmp_path / "sub.csv", blocks)
214
+ out = ev.load_submission(tmp_path / "sub.csv")
215
+ assert ev._to_embedding_frame(out["d001"]).shape == (2, 3)
216
+ assert ev._to_embedding_frame(out["d002"]).shape == (2, 3)
217
+
218
+
219
+ def test_duplicate_within_dataset_raises():
220
+ df = pd.DataFrame({ev.SAMPLE_ID: ["a", "a"], "e0": [1.0, 2.0], "e1": [3.0, 4.0]})
221
+ with pytest.raises(ev.SubmissionError, match="duplicate"):
222
+ ev._to_embedding_frame(df)
223
+
224
+
225
+ # --- registry ---
226
+
227
+
228
+ def test_load_tasks_registry_duplicate_raises(tmp_path):
229
+ path = tmp_path / "tasks.yaml"
230
+ path.write_text(
231
+ "tasks:\n"
232
+ " - {task_id: t1, dataset_id: d001}\n"
233
+ " - {task_id: t1, dataset_id: d002}\n"
234
+ )
235
+ with pytest.raises(ev.EvaluatorError, match="duplicate"):
236
+ ev.load_tasks_registry(path)
237
+
238
+
239
+ def test_scoreable_tasks_skips_orphans():
240
+ tasks = [
241
+ _task("t1", "d001", "classification", "auroc", "G"),
242
+ _task("t9", "d999", "regression", "pearson", "D"),
243
+ ]
244
+ kept = ev.scoreable_tasks(tasks, {"d001"})
245
+ assert [t["task_id"] for t in kept] == ["t1"]
246
+
247
+
248
+ # --- score_all: offline via injected datasets + tasks + fetch_labels ---
249
+
250
+
251
+ def _two_task_submission(tmp_path, signal=True):
252
+ _, cl, cids, cemb = _classification(signal=signal)
253
+ _, rl, rids, remb = _regression(signal=signal)
254
+ _write_csv(tmp_path / "sub.csv", {"d001": (cids, cemb), "d002": (rids, remb)})
255
+ tasks = [
256
+ _task("t1", "d001", "classification", "auroc", "Gastro"),
257
+ _task("t2", "d002", "regression", "pearson", "Derm"),
258
+ ]
259
+ labels_by = {"t1": cl, "t2": rl}
260
+ fetch = lambda tid, token=None: labels_by[tid] # noqa: E731
261
+ datasets = [{"id": "d001"}, {"id": "d002"}]
262
+ return tmp_path / "sub.csv", datasets, tasks, fetch
263
+
264
+
265
+ def test_score_all_full_coverage_signal(tmp_path):
266
+ path, datasets, tasks, fetch = _two_task_submission(tmp_path, signal=True)
267
+ res = ev.score_all(path, datasets=datasets, tasks=tasks, fetch_labels=fetch)
268
+ assert res["full_coverage"] is True
269
+ assert res["n_scored"] == 2 and res["n_total"] == 2
270
+ assert res["coverage"] == 1.0
271
+ assert res["categories"]["treatment_outcome"]["mean"] > 0.8
272
+ assert res["categories"]["clinical_scores"]["mean"] > 0.6
273
+ assert set(res["categories"]) == {"treatment_outcome", "clinical_scores"}
274
+ assert res["missing"] == [] and res["invalid"] == []
275
+ assert res["incomplete"] == []
276
+ assert res["n_datasets_scored"] == 2 and res["n_datasets_total"] == 2
277
+
278
+
279
+ def test_score_all_null_is_chance(tmp_path):
280
+ path, datasets, tasks, fetch = _two_task_submission(tmp_path, signal=False)
281
+ res = ev.score_all(path, datasets=datasets, tasks=tasks, fetch_labels=fetch)
282
+ assert res["categories"]["treatment_outcome"]["mean"] < 0.7
283
+
284
+
285
+ def test_multi_task_per_dataset(tmp_path):
286
+ _, cl, cids, cemb = _classification(signal=True)
287
+ yreg = np.random.default_rng(3).normal(size=len(cids))
288
+ rl = _labels(yreg, _folds(len(cids)), cids)
289
+ _write_csv(tmp_path / "s.csv", {"d001": (cids, cemb)})
290
+ tasks = [
291
+ _task("t1", "d001", "classification", "auroc", "Gastro"),
292
+ _task("t2", "d001", "regression", "pearson", "Gastro"),
293
+ ]
294
+ labels_by = {"t1": cl, "t2": rl}
295
+ fetch = lambda tid, token=None: labels_by[tid] # noqa: E731
296
+ res = ev.score_all(
297
+ path=tmp_path / "s.csv",
298
+ datasets=[{"id": "d001"}],
299
+ tasks=tasks,
300
+ fetch_labels=fetch,
301
+ )
302
+ assert res["n_total"] == 2 and res["n_scored"] == 2
303
+ assert res["full_coverage"] is True
304
+ assert res["n_datasets_total"] == 1 and res["n_datasets_scored"] == 1
305
+ assert set(res["categories"]) == {"treatment_outcome", "clinical_scores"}
306
+ assert res["incomplete"] == []
307
+
308
+
309
+ def test_missing_dataset_makes_all_its_tasks_missing(tmp_path):
310
+ _, cl, cids, cemb = _classification()
311
+ _write_csv(tmp_path / "s.csv", {"d001": (cids, cemb)})
312
+ tasks = [
313
+ _task("t1", "d001", "classification", "auroc", "Gastro"),
314
+ _task("t2", "d002", "regression", "pearson", "Derm"),
315
+ ]
316
+ fetch = lambda tid, token=None: {"t1": cl}[tid] # noqa: E731
317
+ res = ev.score_all(
318
+ path=tmp_path / "s.csv",
319
+ datasets=[{"id": "d001"}, {"id": "d002"}],
320
+ tasks=tasks,
321
+ fetch_labels=fetch,
322
+ )
323
+ assert res["missing"] == ["d002"]
324
+ assert res["full_coverage"] is False
325
+ assert res["n_scored"] == 1 and res["n_total"] == 2
326
+ assert res["n_datasets_scored"] == 1
327
+ assert res["incomplete"] == []
328
+
329
+
330
+ def test_invalid_task_when_dataset_embedded(tmp_path):
331
+ _, cl, cids, cemb = _classification(n=60)
332
+ _write_csv(tmp_path / "s.csv", {"d001": (cids, cemb)})
333
+ tasks = [
334
+ _task("t1", "d001", "classification", "auroc", "Gastro"),
335
+ _task("t2", "d001", "classification", "auroc", "Gastro"),
336
+ ]
337
+ bad = _labels(
338
+ np.array([0, 1, 0, 1]), np.array([0, 1, 2, 3]), ids=["z0", "z1", "z2", "z3"]
339
+ )
340
+ fetch = lambda tid, token=None: {"t1": cl, "t2": bad}[tid] # noqa: E731
341
+ res = ev.score_all(
342
+ path=tmp_path / "s.csv",
343
+ datasets=[{"id": "d001"}],
344
+ tasks=tasks,
345
+ fetch_labels=fetch,
346
+ )
347
+ assert res["n_scored"] == 1 and res["full_coverage"] is False
348
+ invalid = res["invalid"][0]
349
+ assert invalid["task_id"] == "t2" and invalid["dataset_id"] == "d001"
350
+ assert res["incomplete"] == ["d001"]
351
+ assert res["n_datasets_scored"] == 0
352
+
353
+
354
+ def test_block_invalid_fails_all_its_tasks(tmp_path):
355
+ df = pd.DataFrame(
356
+ {
357
+ ev.DATASET_ID: ["d001", "d001"],
358
+ ev.SAMPLE_ID: ["a", "a"],
359
+ "e0": [1.0, 2.0],
360
+ "e1": [3.0, 4.0],
361
+ }
362
+ )
363
+ df.to_csv(tmp_path / "s.csv", index=False)
364
+ tasks = [
365
+ _task("t1", "d001", "classification", "auroc", "Gastro"),
366
+ _task("t2", "d001", "regression", "pearson", "Gastro"),
367
+ ]
368
+ res = ev.score_all(
369
+ path=tmp_path / "s.csv",
370
+ datasets=[{"id": "d001"}],
371
+ tasks=tasks,
372
+ fetch_labels=_no_fetch,
373
+ )
374
+ assert res["n_scored"] == 0
375
+ reasons = {i["task_id"]: i["reason"] for i in res["invalid"]}
376
+ assert set(reasons) == {"t1", "t2"}
377
+ assert "duplicate" in reasons["t1"] and reasons["t1"] == reasons["t2"]
378
+ assert res["incomplete"] == ["d001"]
379
+
380
+
381
+ def test_score_all_unknown_dataset_raises(tmp_path):
382
+ _, cl, cids, cemb = _classification()
383
+ _write_csv(
384
+ tmp_path / "s.csv", {"d001": (cids, cemb), "d999": (["a"], np.ones((1, 8)))}
385
+ )
386
+ tasks = [_task("t1", "d001", "classification", "auroc", "Gastro")]
387
+ with pytest.raises(ev.SubmissionError, match="unknown"):
388
+ ev.score_all(
389
+ path=tmp_path / "s.csv",
390
+ datasets=[{"id": "d001"}],
391
+ tasks=tasks,
392
+ fetch_labels=_no_fetch,
393
+ )
394
+
395
+
396
+ def test_incomplete_dataset_is_invalid(tmp_path):
397
+ _, cl, cids, cemb = _classification(n=60)
398
+ _write_csv(tmp_path / "s.csv", {"d001": (cids[:-5], cemb[:-5])})
399
+ tasks = [_task("t1", "d001", "classification", "auroc", "Gastro")]
400
+ fetch = lambda tid, token=None: cl # noqa: E731
401
+ res = ev.score_all(
402
+ path=tmp_path / "s.csv",
403
+ datasets=[{"id": "d001"}],
404
+ tasks=tasks,
405
+ fetch_labels=fetch,
406
+ )
407
+ assert res["n_scored"] == 0
408
+ assert res["invalid"][0]["task_id"] == "t1"
409
+ assert "missing" in res["invalid"][0]["reason"]
410
+ assert res["incomplete"] == ["d001"]
411
+
412
+
413
+ def test_scoring_error_is_invalid(tmp_path):
414
+ n = 20
415
+ folds = np.repeat(np.arange(5), 4)
416
+ y = np.zeros(n, dtype=int)
417
+ y[:2] = 1
418
+ ids = [f"s{i}" for i in range(n)]
419
+ emb = np.random.default_rng(0).normal(size=(n, 6))
420
+ labels = _labels(y, folds, ids)
421
+ _write_csv(tmp_path / "s.csv", {"d001": (ids, emb)})
422
+ tasks = [_task("t1", "d001", "classification", "auroc", "Gastro")]
423
+ fetch = lambda tid, token=None: labels # noqa: E731
424
+ res = ev.score_all(
425
+ path=tmp_path / "s.csv",
426
+ datasets=[{"id": "d001"}],
427
+ tasks=tasks,
428
+ fetch_labels=fetch,
429
+ )
430
+ assert res["n_scored"] == 0
431
+ assert "single class" in res["invalid"][0]["reason"]
432
+
433
+
434
+ def test_fetch_failure_raises_evaluator_error(tmp_path, monkeypatch):
435
+ monkeypatch.setattr(ev, "FETCH_BACKOFF", 0.0)
436
+ _, cl, cids, cemb = _classification()
437
+ _write_csv(tmp_path / "s.csv", {"d001": (cids, cemb)})
438
+ tasks = [_task("t1", "d001", "classification", "auroc", "Gastro")]
439
+
440
+ def boom(task_id, token=None):
441
+ raise ConnectionError("HF unreachable")
442
+
443
+ with pytest.raises(ev.EvaluatorError, match="could not load"):
444
+ ev.score_all(
445
+ path=tmp_path / "s.csv",
446
+ datasets=[{"id": "d001"}],
447
+ tasks=tasks,
448
+ fetch_labels=boom,
449
+ )
450
+
451
+
452
+ def test_unknown_metric_raises_evaluator_error(tmp_path):
453
+ _, cl, cids, cemb = _classification()
454
+ _write_csv(tmp_path / "s.csv", {"d001": (cids, cemb)})
455
+ tasks = [_task("t1", "d001", "classification", "bogus", "Gastro")]
456
+ fetch = lambda tid, token=None: cl # noqa: E731
457
+ with pytest.raises(ev.EvaluatorError, match="unknown metric"):
458
+ ev.score_all(
459
+ path=tmp_path / "s.csv",
460
+ datasets=[{"id": "d001"}],
461
+ tasks=tasks,
462
+ fetch_labels=fetch,
463
+ )
464
+
465
+
466
+ # --- transfer: fit once on the train cohort, score the test cohort ---
467
+
468
+
469
+ def _transfer_labels(y, split, ids=None):
470
+ ids = ids if ids is not None else [f"s{i}" for i in range(len(y))]
471
+ return pd.DataFrame({ev.SAMPLE_ID: ids, ev.LABEL: y, ev.SPLIT: split})
472
+
473
+
474
+ def _transfer_classification(n_train=40, n_test=20, seed=42, signal=True):
475
+ rng = np.random.default_rng(seed)
476
+ n = n_train + n_test
477
+ y = np.array([0, 1] * (n // 2))
478
+ emb = rng.normal(size=(n, 8))
479
+ if signal:
480
+ emb[:, 0] += 3.0 * y
481
+ split = np.array([ev.SPLIT_TRAIN] * n_train + [ev.SPLIT_TEST] * n_test)
482
+ ids = [f"s{i}" for i in range(n)]
483
+ task = _task("t016", "d010", "classification", "auroc", "Gastro")
484
+ return task, _transfer_labels(y, split, ids), ids, emb
485
+
486
+
487
+ def test_transfer_scoring_signal_and_null():
488
+ task, labels, ids, emb = _transfer_classification(signal=True)
489
+ res = ev.score_task(task, labels, _emb_frame(emb, ids))
490
+ assert res.score > 0.8
491
+ assert res.n_samples == 20
492
+ task0, labels0, ids0, null = _transfer_classification(signal=False)
493
+ res0 = ev.score_task(task0, labels0, _emb_frame(null, ids0))
494
+ assert 0.3 < res0.score < 0.7
495
+
496
+
497
+ def test_transfer_single_class_train_raises_submission_error():
498
+ y = np.array([0, 0, 0, 0, 1, 0])
499
+ split = np.array([ev.SPLIT_TRAIN] * 4 + [ev.SPLIT_TEST] * 2)
500
+ ids = [f"s{i}" for i in range(6)]
501
+ emb = np.random.default_rng(0).normal(size=(6, 5))
502
+ task = _task("t016", "d010", "classification", "auroc", "Gastro")
503
+ with pytest.raises(ev.SubmissionError):
504
+ ev.score_task(task, _transfer_labels(y, split, ids), _emb_frame(emb, ids))
505
+
506
+
507
+ def test_load_labels_accepts_repeated_fold_or_split(tmp_path):
508
+ _labels(
509
+ np.array([0, 1, 0, 1]),
510
+ _folds(4),
511
+ repeat_folds=[np.array([1, 0, 1, 0]), np.array([2, 0, 2, 0])],
512
+ ).to_csv(tmp_path / "f.csv", index=False)
513
+ labels = ev.load_labels(tmp_path / "f.csv")
514
+ assert ev._fold_columns(labels) == ["fold", "fold_1", "fold_2"]
515
+ _transfer_labels(np.array([0, 1]), ["train", "test"]).to_csv(
516
+ tmp_path / "s.csv", index=False
517
+ )
518
+ assert ev.SPLIT in ev.load_labels(tmp_path / "s.csv").columns
519
+
520
+
521
+ def test_load_labels_rejects_both_and_neither(tmp_path):
522
+ both = pd.DataFrame(
523
+ {ev.SAMPLE_ID: ["a"], ev.LABEL: [0], ev.FOLD: [0], ev.SPLIT: ["train"]}
524
+ )
525
+ both.to_csv(tmp_path / "both.csv", index=False)
526
+ with pytest.raises(ev.EvaluatorError, match="exactly one"):
527
+ ev.load_labels(tmp_path / "both.csv")
528
+ neither = pd.DataFrame({ev.SAMPLE_ID: ["a"], ev.LABEL: [0]})
529
+ neither.to_csv(tmp_path / "neither.csv", index=False)
530
+ with pytest.raises(ev.EvaluatorError, match="exactly one"):
531
+ ev.load_labels(tmp_path / "neither.csv")
532
+
533
+
534
+ def test_score_all_transfer_two_tasks(tmp_path):
535
+ _, l16, ids, emb = _transfer_classification(signal=True)
536
+ y = l16[ev.LABEL].to_numpy()
537
+ rev = np.where(
538
+ l16[ev.SPLIT].to_numpy() == ev.SPLIT_TRAIN, ev.SPLIT_TEST, ev.SPLIT_TRAIN
539
+ )
540
+ l17 = _transfer_labels(y, rev, ids)
541
+ _write_csv(tmp_path / "sub.csv", {"d010": (ids, emb)})
542
+ tasks = [
543
+ _task("t016", "d010", "classification", "auroc", "Gastro"),
544
+ _task("t017", "d010", "classification", "auroc", "Gastro"),
545
+ ]
546
+ labels_by = {"t016": l16, "t017": l17}
547
+ fetch = lambda tid, token=None: labels_by[tid] # noqa: E731
548
+ res = ev.score_all(
549
+ path=tmp_path / "sub.csv",
550
+ datasets=[{"id": "d010"}],
551
+ tasks=tasks,
552
+ fetch_labels=fetch,
553
+ )
554
+ assert res["n_total"] == 2 and res["n_scored"] == 2
555
+ assert res["full_coverage"] is True
556
+ assert res["n_datasets_total"] == 1 and res["n_datasets_scored"] == 1
557
+ assert set(res["categories"]) == {"treatment_outcome"}
558
+
559
+
560
+ # --- perturbation response decoding ---
561
+
562
+
563
+ def _perturbation_task(task_id="t018", dataset_id="d011", pairing="paired"):
564
+ return {
565
+ "task_id": task_id,
566
+ "dataset_id": dataset_id,
567
+ "task_type": ev.PERTURBATION,
568
+ "metric": ev.RESIDUAL_SPEARMAN,
569
+ "category": "perturbation_prediction",
570
+ "therapeutic_area": "Dermatology",
571
+ "pairing": pairing,
572
+ "n_top_de_genes": 4,
573
+ }
574
+
575
+
576
+ def _perturbation_labels(ids, folds):
577
+ return pd.DataFrame(
578
+ {
579
+ ev.SAMPLE_ID: ids,
580
+ ev.PAIR_ID: [f"p{i}" for i in range(len(ids))],
581
+ ev.PAIRING: ev.PAIRED,
582
+ ev.FOLD: folds,
583
+ f"{ev.FOLD}_1": np.roll(folds, 1),
584
+ f"{ev.FOLD}_2": np.roll(folds, 2),
585
+ }
586
+ )
587
+
588
+
589
+ def test_perturbation_constant_embedding_anchors_at_chance():
590
+ rng = np.random.default_rng(12)
591
+ n = 30
592
+ ids = [f"s{i}" for i in range(n)]
593
+ labels = _perturbation_labels(ids, _folds(n, 3))
594
+ targets = ev.PerturbationTargets(
595
+ delta=rng.normal(size=(n, 8)), gene_ids=np.array([f"g{i}" for i in range(8)])
596
+ )
597
+ result = ev.score_task(
598
+ _perturbation_task(), labels, _emb_frame(np.ones((n, 3)), ids), targets
599
+ )
600
+ assert result.score == pytest.approx(0.5, abs=1e-12)
601
+ assert len(result.repeat_scores) == 3
602
+ assert result.diagnostics == {}
603
+
604
+
605
+ def test_perturbation_signal_decodes_hidden_response():
606
+ rng = np.random.default_rng(4)
607
+ n = 45
608
+ ids = [f"s{i}" for i in range(n)]
609
+ emb = rng.normal(size=(n, 5))
610
+ weights = rng.normal(size=(5, 8))
611
+ delta = emb @ weights + rng.normal(scale=0.01, size=(n, 8))
612
+ labels = _perturbation_labels(ids, _folds(n, 3))
613
+ targets = ev.PerturbationTargets(delta, np.array([f"g{i}" for i in range(8)]))
614
+ result = ev.score_task(_perturbation_task(), labels, _emb_frame(emb, ids), targets)
615
+ assert result.score > 0.9
616
+ assert result.diagnostics == {}
617
+
618
+
619
+ def test_perturbation_decoder_clips_test_coordinates(monkeypatch):
620
+ seen = {}
621
+
622
+ class Decoder:
623
+ def fit(self, matrix, delta):
624
+ self.mean = delta.mean(axis=0)
625
+ return self
626
+
627
+ def predict(self, matrix):
628
+ seen["maximum"] = float(np.max(np.abs(matrix)))
629
+ return np.tile(self.mean, (len(matrix), 1))
630
+
631
+ monkeypatch.setattr(ev, "RidgeCV", lambda alphas: Decoder())
632
+ matrix = np.array([[0.0], [1.0], [1_000_000.0]])
633
+ delta = np.array([[1.0, 2.0], [2.0, 1.0], [3.0, 4.0]])
634
+ score = ev._perturbation_fold(
635
+ matrix,
636
+ delta,
637
+ np.array([True, True, False]),
638
+ np.array([False, False, True]),
639
+ 2,
640
+ )
641
+
642
+ assert seen["maximum"] == ev.PERTURBATION_TEST_Z_CLIP
643
+ assert score == pytest.approx(0.5)
644
+
645
+
646
+ def test_deg_selection_uses_only_the_supplied_training_rows():
647
+ delta = np.array(
648
+ [
649
+ [8.0, 0.0],
650
+ [8.0, 0.0],
651
+ [0.0, 100.0],
652
+ [0.0, 100.0],
653
+ ]
654
+ )
655
+ source = np.array([True, True, False, False])
656
+ assert ev._select_degs(delta[source], 1).tolist() == [0]
657
+ assert ev._select_degs(delta[~source], 1).tolist() == [1]
658
+
659
+
660
+ def test_directional_transfer_never_fits_on_target_disease(monkeypatch):
661
+ matrix = np.arange(24, dtype=float).reshape(6, 4)
662
+ delta = np.arange(18, dtype=float).reshape(6, 3)
663
+ labels = pd.DataFrame(
664
+ {
665
+ ev.SPLIT: [ev.SPLIT_TRAIN] * 4 + [ev.SPLIT_TEST] * 2,
666
+ }
667
+ )
668
+ seen = {}
669
+
670
+ def fold(matrix_arg, delta_arg, train, test, n_top):
671
+ seen["train"] = np.flatnonzero(train).tolist()
672
+ seen["test"] = np.flatnonzero(test).tolist()
673
+ seen["n_top"] = n_top
674
+ return 0.75
675
+
676
+ monkeypatch.setattr(ev, "_perturbation_fold", fold)
677
+ metrics, n_test = ev._transfer_perturbation_scores(matrix, delta, labels, 2)
678
+ assert seen == {
679
+ "train": [0, 1, 2, 3],
680
+ "test": [4, 5],
681
+ "n_top": 2,
682
+ }
683
+ assert metrics == (0.75,)
684
+ assert n_test == 2
685
+
686
+
687
+ def test_repeated_sample_ids_reuse_one_baseline_embedding():
688
+ labels = pd.DataFrame({ev.SAMPLE_ID: ["s1", "s1", "s2", "s1"]})
689
+ embeddings = _emb_frame(np.array([[1.0, 2.0], [3.0, 4.0]]), ["s1", "s2"])
690
+ aligned = ev._align(labels, embeddings)
691
+ np.testing.assert_array_equal(
692
+ aligned,
693
+ np.array([[1.0, 2.0], [1.0, 2.0], [3.0, 4.0], [1.0, 2.0]]),
694
+ )
695
+
696
+
697
+ def test_mouse_cartesian_partitions_isolate_both_specimens():
698
+ controls = np.repeat(["c1", "c2", "c3"], 3)
699
+ perturbed = np.tile(["p1", "p2", "p3"], 3)
700
+ repeats = ev._cartesian_partitions(controls, perturbed)
701
+ assert len(repeats) == 6
702
+ assert all(len(folds) == 3 for folds in repeats)
703
+ tested_edges = []
704
+ for folds in repeats:
705
+ for train, test in folds:
706
+ held = np.flatnonzero(test)[0]
707
+ tested_edges.append((controls[held], perturbed[held]))
708
+ assert train.sum() == 4 and test.sum() == 1
709
+ assert not np.any(controls[train] == controls[held])
710
+ assert not np.any(perturbed[train] == perturbed[held])
711
+ assert set(tested_edges) == set(zip(controls, perturbed))
712
+ assert len(tested_edges) == 18
713
+
714
+
715
+ def test_mouse_task_has_six_regular_weighted_repeat_scores():
716
+ rng = np.random.default_rng(7)
717
+ controls = np.repeat(["c1", "c2", "c3"], 3)
718
+ perturbed = np.tile(["p1", "p2", "p3"], 3)
719
+ sample_ids = np.repeat(["s1", "s2", "s3"], 3)
720
+ labels = pd.DataFrame(
721
+ {
722
+ ev.SAMPLE_ID: sample_ids,
723
+ ev.PAIR_ID: [f"pair{i}" for i in range(9)],
724
+ ev.CONTROL_ID: controls,
725
+ ev.PERTURBED_ID: perturbed,
726
+ ev.PAIRING: ev.CARTESIAN,
727
+ }
728
+ )
729
+ embeddings = _emb_frame(np.ones((3, 2)), ["s1", "s2", "s3"])
730
+ targets = ev.PerturbationTargets(
731
+ rng.normal(size=(9, 6)), np.array([f"g{i}" for i in range(6)])
732
+ )
733
+ result = ev.score_task(
734
+ _perturbation_task("t020", "d012", ev.CARTESIAN),
735
+ labels,
736
+ embeddings,
737
+ targets,
738
+ )
739
+ assert result.score == pytest.approx(0.5, abs=1e-12)
740
+ assert len(result.repeat_scores) == 6
741
+ assert result.category == "perturbation_prediction"
742
+
743
+
744
+ def test_load_private_perturbation_artifacts(tmp_path):
745
+ labels = pd.DataFrame(
746
+ {
747
+ ev.SAMPLE_ID: ["s1", "s2"],
748
+ ev.PAIR_ID: ["p1", "p2"],
749
+ ev.PAIRING: [ev.PAIRED, ev.PAIRED],
750
+ ev.FOLD: [0, 1],
751
+ }
752
+ )
753
+ labels.to_csv(tmp_path / ev.LABELS_FILENAME, index=False)
754
+ np.savez_compressed(
755
+ tmp_path / ev.TARGETS_FILENAME,
756
+ delta=np.ones((2, 3)),
757
+ gene_ids=np.array(["1", "2", "3"]),
758
+ )
759
+ loaded_labels = ev.load_labels(tmp_path / ev.LABELS_FILENAME)
760
+ loaded_targets = ev.load_targets(tmp_path / ev.TARGETS_FILENAME)
761
+ assert ev.LABEL not in loaded_labels
762
+ assert loaded_targets.delta.shape == (2, 3)
763
+
764
+
765
+ def test_score_all_fetches_hidden_targets_only_for_perturbation(tmp_path):
766
+ rng = np.random.default_rng(31)
767
+ n = 30
768
+ ids = [f"s{i}" for i in range(n)]
769
+ emb = rng.normal(size=(n, 4))
770
+ targets = ev.PerturbationTargets(
771
+ emb @ rng.normal(size=(4, 6)), np.array([f"g{i}" for i in range(6)])
772
+ )
773
+ labels = _perturbation_labels(ids, _folds(n, 3))
774
+ _write_csv(tmp_path / "submission.csv", {"d011": (ids, emb)})
775
+ result = ev.score_all(
776
+ tmp_path / "submission.csv",
777
+ datasets=[{"id": "d011"}],
778
+ tasks=[_perturbation_task()],
779
+ fetch_labels=lambda task_id, token=None: labels,
780
+ fetch_targets=lambda task_id, token=None: targets,
781
+ )
782
+ assert result["full_coverage"] is True
783
+ assert (
784
+ result["categories"]["perturbation_prediction"]["metric"]
785
+ == ev.RESIDUAL_SPEARMAN
786
+ )
787
+ assert result["per_task"][0].diagnostics == {}
test_leaderboard.py ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Unit tests for the board tables and page copy (no network, no Gradio)."""
2
+
3
+ import re
4
+
5
+ import pandas as pd
6
+ import pytest
7
+ from conftest import REGISTRY
8
+ from leaderboard import (
9
+ RESERVED_COLUMNS,
10
+ TASK_COLUMNS,
11
+ latest_only,
12
+ per_task_table,
13
+ ranked_table,
14
+ source_repositories,
15
+ tasks_table,
16
+ top_models,
17
+ )
18
+
19
+ ALL_SCORES = (0.8, 0.5, 0.6, 0.4, 0.7)
20
+
21
+
22
+ def _full(model: str, at: str, scores=ALL_SCORES) -> list[tuple]:
23
+ return [(model, tid, s, at) for tid, s in zip(REGISTRY, scores)]
24
+
25
+
26
+ def _flagged(model: str, at: str, baseline: bool, scores=ALL_SCORES) -> list[tuple]:
27
+ return [(model, tid, s, at, baseline) for tid, s in zip(REGISTRY, scores)]
28
+
29
+
30
+ def test_full_board_coverage_is_ranked_and_partial_is_not(registry, named, results):
31
+ df = results(*_full("full", "2026-01-01"), ("partial", "t001", 0.99, "2026-01-01"))
32
+ board = ranked_table(df, registry, named("bulk RNA"))
33
+ assert list(board["Model"]) == ["full"]
34
+
35
+
36
+ def test_a_partial_model_is_ranked_on_the_board_it_fully_covered(
37
+ registry, named, results
38
+ ):
39
+ """Covering all of Rheumatology ranks you there, even with Dermatology missing."""
40
+ df = results(
41
+ *_full("full", "2026-01-01"),
42
+ ("rheuma_only", "t005", 0.95, "2026-01-01"),
43
+ )
44
+ assert list(ranked_table(df, registry, named("bulk RNA"))["Model"]) == ["full"]
45
+ rheumatology = ranked_table(df, registry, named("Rheumatology"))
46
+ assert list(rheumatology["Model"]) == ["rheuma_only", "full"]
47
+
48
+
49
+ def test_ranks_are_ordered_by_the_mean_of_the_category_means(registry, named, results):
50
+ df = results(
51
+ *_full("better", "2026-01-01", scores=(0.9, 0.9, 0.9, 0.9, 0.9)),
52
+ *_full("worse", "2026-01-01", scores=(0.2, 0.2, 0.2, 0.2, 0.2)),
53
+ )
54
+ board = ranked_table(df, registry, named("bulk RNA"))
55
+ assert list(board["Model"]) == ["better", "worse"]
56
+ assert list(board["Rank"]) == [1, 2]
57
+
58
+
59
+ def test_one_column_per_category_in_its_native_metric(registry, named, results):
60
+ df = results(*_full("full", "2026-01-01"))
61
+ board = ranked_table(df, registry, named("bulk RNA"))
62
+ assert "Treatment outcome (AUROC)" in board.columns
63
+ assert "Clinical scores (Pearson)" in board.columns
64
+ assert "Endotype (AUROC)" in board.columns
65
+
66
+
67
+ def test_comparison_columns_are_present_for_every_board(registry, named, results):
68
+ df = results(*_full("full", "2026-01-01"))
69
+ assert {"Elo", "Mean score", "Mean rank"} <= set(
70
+ ranked_table(df, registry, named("bulk RNA")).columns
71
+ )
72
+ endotype = ranked_table(df, registry, named("Endotype"))
73
+ assert {"Elo", "Mean score", "Mean rank"} <= set(endotype.columns)
74
+ assert "Endotype (AUROC)" in endotype.columns
75
+
76
+
77
+ def test_latest_submission_wins_so_scores_cannot_be_shopped(registry, named, results):
78
+ df = results(
79
+ *_full("m", "2026-01-01", scores=(0.9,) * 5),
80
+ *_full("m", "2026-02-01", scores=(0.1,) * 5),
81
+ )
82
+ assert set(latest_only(df)["submitted_at"]) == {"2026-02-01"}
83
+ board = ranked_table(df, registry, named("bulk RNA"))
84
+ assert board.loc[0, "Treatment outcome (AUROC)"] == pytest.approx(0.1)
85
+
86
+
87
+ def test_equal_timestamp_submissions_do_not_mix_scores_or_credits(
88
+ registry, named, results
89
+ ):
90
+ older = results(*_full("m", "2026-02-01", scores=(0.9,) * 5)).assign(
91
+ submission_id="2026-02-01T00:00:00.000001-a",
92
+ institution="Older University",
93
+ is_author_submission=False,
94
+ paper_link="https://example.org/older",
95
+ )
96
+ newer = results(*_full("m", "2026-02-01", scores=(0.1,) * 5)).assign(
97
+ submission_id="2026-02-01T00:00:00.000002-b",
98
+ institution="Newer University",
99
+ is_author_submission=True,
100
+ paper_link="https://example.org/newer",
101
+ )
102
+ board = ranked_table(pd.concat([older, newer]), registry, named("bulk RNA"))
103
+ assert board.loc[0, "Treatment outcome (AUROC)"] == pytest.approx(0.1)
104
+ assert "Author" not in board
105
+ assert board.loc[0, "Institution"] == "Newer University"
106
+ assert board.loc[0, "Model"].paper_link == "https://example.org/newer"
107
+ assert board.loc[0, "Model"].is_author_submission is True
108
+
109
+
110
+ def test_nullable_credits_render_as_missing(registry, named, results):
111
+ df = results(*_full("m", "2026-01-01")).assign(
112
+ institution=float("nan"),
113
+ paper_link=pd.NA,
114
+ is_author_submission=pd.NA,
115
+ )
116
+ board = ranked_table(df, registry, named("bulk RNA"))
117
+ assert pd.isna(board.loc[0, "Institution"])
118
+ assert board.loc[0, "Model"].paper_link == ""
119
+ assert board.loc[0, "Model"].is_author_submission is False
120
+
121
+
122
+ def test_a_model_scoring_nothing_finite_ranks_below_a_bad_one(registry, named, results):
123
+ """NaN everywhere is "no score", which must not outrank a negative Pearson."""
124
+ df = results(
125
+ *_full("nan-everywhere", "2026-01-01", scores=(float("nan"),) * 5),
126
+ *_full("bad", "2026-01-01", scores=(-0.2,) * 5),
127
+ )
128
+ board = ranked_table(df, registry, named("bulk RNA"))
129
+ assert list(board["Model"]) == ["bad", "nan-everywhere"]
130
+ assert board.loc[1, "Elo"] == 1000
131
+
132
+
133
+ def test_a_card_shows_a_dash_rather_than_minus_infinity(registry, named, results):
134
+ df = results(*_full("nan-everywhere", "2026-01-01", scores=(float("nan"),) * 5))
135
+ top = top_models(df, registry, named("bulk RNA"), 3)
136
+ assert top[0].elo is None
137
+
138
+
139
+ def test_ranked_is_empty_when_nobody_covered_the_board(registry, named, results):
140
+ df = results(("partial", "t001", 0.9, "2026-01-01"))
141
+ assert ranked_table(df, registry, named("Rheumatology")).empty
142
+
143
+
144
+ def test_per_task_keeps_partial_submissions_with_gaps(registry, named, results):
145
+ df = results(
146
+ *_full("full", "2026-01-01"),
147
+ ("partial", "t002", 0.95, "2026-01-01"),
148
+ )
149
+ table = per_task_table(df, registry, named("Gastroenterology")).set_index("Task")
150
+ assert list(table.index) == [
151
+ "IBD adalimumab remission",
152
+ "Crohn SES-CD",
153
+ "Crohn HBI",
154
+ ]
155
+ assert table.loc["Crohn SES-CD", "partial"] == pytest.approx(0.95)
156
+ assert table.loc["Crohn SES-CD", "Best"] == "partial"
157
+ assert pd.isna(table.loc["Crohn HBI", "partial"])
158
+ assert table.loc["Crohn HBI", "Best"] == "full"
159
+
160
+
161
+ def test_reserved_names_are_exactly_the_per_task_metadata_columns(
162
+ registry, named, results
163
+ ):
164
+ """A model named after one of these would overwrite it -- Submit refuses them."""
165
+ df = results(("m", "t004", 0.5, "2026-01-01"))
166
+ table = per_task_table(df, registry, named("Dermatology"))
167
+ assert RESERVED_COLUMNS == set(table.columns) - {"m"}
168
+
169
+
170
+ def test_per_task_only_shows_the_board_it_was_asked_for(registry, named, results):
171
+ df = results(*_full("full", "2026-01-01"))
172
+ tasks = set(per_task_table(df, registry, named("Dermatology"))["Task"])
173
+ assert tasks == {"Psoriasis PASI"}
174
+
175
+
176
+ def test_per_task_is_empty_when_no_result_touches_the_board(registry, named, results):
177
+ df = results(("partial", "t001", 0.9, "2026-01-01"))
178
+ assert per_task_table(df, registry, named("Rheumatology")).empty
179
+
180
+
181
+ def test_top_models_are_ordered_and_limited(registry, named, results):
182
+ df = results(
183
+ *_full("a", "2026-01-01", scores=(0.9,) * 5),
184
+ *_full("b", "2026-01-01", scores=(0.5,) * 5),
185
+ *_full("c", "2026-01-01", scores=(0.1,) * 5),
186
+ )
187
+ top = top_models(df, registry, named("bulk RNA"), 2)
188
+ assert [entry.name for entry in top] == ["a", "b"]
189
+ assert top_models(df, registry, named("bulk RNA"), 3)[0].elo > 1000
190
+ assert not any(entry.is_baseline for entry in top)
191
+
192
+
193
+ def test_top_models_ignores_models_that_did_not_cover_the_board(
194
+ registry, named, results
195
+ ):
196
+ df = results(("partial", "t001", 0.99, "2026-01-01"))
197
+ assert top_models(df, registry, named("bulk RNA"), 3) == []
198
+
199
+
200
+ def test_a_baseline_is_ranked_in_place_and_labelled(registry, named, flagged_results):
201
+ """A PCA that beats a submitted model belongs above it, not in a footnote."""
202
+ df = flagged_results(
203
+ *_flagged("pca-50", "2026-01-01", True, scores=(0.9,) * 5),
204
+ *_flagged("eva", "2026-01-01", False, scores=(0.4,) * 5),
205
+ )
206
+ board = ranked_table(df, registry, named("bulk RNA"))
207
+ assert list(board["Model"]) == ["pca-50 (baseline)", "eva"]
208
+ assert list(board["Rank"]) == [1, 2]
209
+
210
+
211
+ def test_rows_written_before_the_flag_existed_are_not_baselines(
212
+ registry, named, results
213
+ ):
214
+ df = results(*_full("eva", "2026-01-01"))
215
+ assert list(ranked_table(df, registry, named("bulk RNA"))["Model"]) == ["eva"]
216
+
217
+
218
+ def test_a_model_cannot_bury_a_baseline_by_reusing_its_name(
219
+ registry, named, flagged_results
220
+ ):
221
+ """Same name, different kind: two entries, and the later one hides nothing."""
222
+ df = flagged_results(
223
+ *_flagged("pca-50", "2026-01-01", True, scores=(0.9,) * 5),
224
+ *_flagged("pca-50", "2026-06-01", False, scores=(0.2,) * 5),
225
+ )
226
+ board = ranked_table(df, registry, named("bulk RNA"))
227
+ assert list(board["Model"]) == ["pca-50 (baseline)", "pca-50"]
228
+ assert "pca-50 (baseline)" in per_task_table(df, registry, named("Dermatology"))
229
+
230
+
231
+ def test_a_resubmission_still_replaces_the_entry_of_the_same_kind(
232
+ registry, named, flagged_results
233
+ ):
234
+ df = flagged_results(
235
+ *_flagged("eva", "2026-01-01", False, scores=(0.9,) * 5),
236
+ *_flagged("eva", "2026-06-01", False, scores=(0.2,) * 5),
237
+ )
238
+ board = ranked_table(df, registry, named("bulk RNA"))
239
+ assert list(board["Model"]) == ["eva"]
240
+ assert board.loc[0, "Endotype (AUROC)"] == pytest.approx(0.2)
241
+
242
+
243
+ def test_per_task_columns_name_the_baseline(registry, named, flagged_results):
244
+ df = flagged_results(*_flagged("random-32", "2026-01-01", True))
245
+ table = per_task_table(df, registry, named("Dermatology"))
246
+ assert "random-32 (baseline)" in table.columns
247
+ assert table.loc[0, "Best"] == "random-32 (baseline)"
248
+
249
+
250
+ def test_top_models_carry_the_baseline_flag(registry, named, flagged_results):
251
+ df = flagged_results(*_flagged("pca-50", "2026-01-01", True))
252
+ assert top_models(df, registry, named("bulk RNA"), 3)[0].is_baseline
253
+
254
+
255
+ def test_tasks_table_shows_biology_but_never_provenance(registry):
256
+ table = tasks_table(list(registry.values())).set_index("Task")
257
+ row = table.loc["IBD adalimumab remission"]
258
+ assert row["Disease"] == "Crohn Disease, Colitis, Ulcerative"
259
+ assert row["Tissue"] == "Digestive system"
260
+ assert row["Patients"] == 90
261
+ assert row["Metric"] == "AUROC"
262
+ assert row["Family"] == "Treatment outcome"
263
+ assert row["Predicts"] == "Will this patient respond?"
264
+
265
+
266
+ def test_tasks_table_is_ordered_by_task_id(registry):
267
+ assert list(tasks_table(list(registry.values()))["Task"])[0] == (
268
+ "IBD adalimumab remission"
269
+ )
270
+
271
+
272
+ def test_tasks_table_never_leaks_an_accession(registry):
273
+ rendered = tasks_table(list(registry.values())).to_string()
274
+ assert not re.search(r"GSE\d+|E-MTAB-\d+|benchmark_", rendered)
275
+
276
+
277
+ def test_source_repositories_name_the_archive_not_the_study(registry):
278
+ repositories = source_repositories(list(registry.values()))
279
+ assert repositories == ["EMBL-EBI ArrayExpress", "NCBI GEO"]
280
+ assert not re.search(r"GSE\d+|E-MTAB-\d+", " ".join(repositories))
281
+
282
+
283
+ def test_the_unvalidated_licence_field_never_reaches_a_public_page(registry):
284
+ """One hand-written line per dataset must not read as a checked rights position."""
285
+ assert "license" in next(iter(registry.values()))
286
+ assert not any(
287
+ "Terms of Use" in r for r in source_repositories(list(registry.values()))
288
+ )
289
+
290
+
291
+ def test_empty_registry_yields_an_empty_table_not_a_crash():
292
+ assert list(tasks_table([]).columns) == TASK_COLUMNS
test_render.py ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Unit tests for the PRIMO page markup (no network, no Gradio).
2
+
3
+ These guard the guarantees a redesign must not lose: every board is reachable,
4
+ open slices route to Contribute, registry and submission text is escaped, the
5
+ baseline/first-submission states read correctly, the leaderboard bolds the best
6
+ value, provenance never leaks, and public submission credits are safely escaped.
7
+ """
8
+
9
+ import re
10
+
11
+ from boards import build_boards, open_in_group
12
+ from render import SECTIONS, rail_html, render_board, render_boards, render_tasks
13
+
14
+
15
+ def _slugs(html: str) -> list[str]:
16
+ return re.findall(r'href="\?board=([^"]+)"', html)
17
+
18
+
19
+ def _boards_html(boards, registry, results):
20
+ df = results(*[("eva", tid, 0.8, "2026-01-01") for tid in registry])
21
+ return render_boards(boards, df, registry)
22
+
23
+
24
+ # ---------------------------------------------------------------- boards page
25
+ def test_every_board_is_reachable_from_the_boards_page(boards, registry, results):
26
+ html = _boards_html(boards, registry, results)
27
+ assert set(_slugs(html)) == {b.slug for b in boards}
28
+
29
+
30
+ def test_each_facet_section_shows_with_its_note(boards, registry, results):
31
+ html = _boards_html(boards, registry, results)
32
+ assert "Per therapeutic areas" in html
33
+ assert "Task Category" in html
34
+ assert "Every task of one omics layer" in html
35
+ assert "PRIMO evaluates representations of omics samples" in html
36
+
37
+
38
+ def test_modality_labels_are_displayed_as_rnaseq(boards, registry, results):
39
+ html = _boards_html(boards, registry, results)
40
+ assert "bulk RNAseq" in html
41
+ assert "single-cell RNAseq" in html
42
+ assert "Leaderboards" in html
43
+
44
+
45
+ def test_open_cards_state_the_slices_nobody_covers(boards, registry, results):
46
+ html = _boards_html(boards, registry, results)
47
+ assert "proteomics" in html
48
+ assert "Oncology" in html
49
+ assert "OPEN" in html
50
+
51
+
52
+ def test_both_open_axes_get_their_own_counter(boards, registry, results):
53
+ """Modality and Therapeutic Areas each declare three gaps, so each says +3."""
54
+ assert _boards_html(boards, registry, results).count("+3 open") == 2
55
+
56
+
57
+ def test_an_open_card_sends_the_visitor_to_contribute(boards, registry, results):
58
+ html = _boards_html(boards, registry, results)
59
+ assert 'href="?tab=contribute"' in html
60
+ assert "Propose a cohort" in html
61
+
62
+
63
+ def test_an_open_card_disappears_once_the_registry_covers_it(registry, results):
64
+ registry["t006"] = {
65
+ **registry["t005"],
66
+ "task_id": "t006",
67
+ "modality": "single-cell RNA",
68
+ }
69
+ boards = build_boards(registry)
70
+ html = _boards_html(boards, registry, results)
71
+ assert 'href="?board=single-cell-rna"' in html
72
+ assert "+2 open" in html
73
+ assert "proteomics" in html
74
+
75
+
76
+ def test_live_cards_teaser_the_leaders(boards, registry, results):
77
+ html = _boards_html(boards, registry, results)
78
+ assert "Leading" in html
79
+ assert "eva" in html
80
+
81
+
82
+ def test_a_board_nobody_covered_invites_the_first_submission(boards, registry, results):
83
+ """A board no model covered in full asks for the first entry, not a leader."""
84
+ html = render_boards(boards, results(("eva", "t001", 0.8, "2026-01-01")), registry)
85
+ assert "be the first" in html.lower()
86
+
87
+
88
+ def test_a_board_held_only_by_baselines_lists_its_models(
89
+ boards, registry, flagged_results
90
+ ):
91
+ """A board card lists its leading models without special baseline copy."""
92
+ df = flagged_results(
93
+ *[("pca-50", tid, 0.8, "2026-01-01", True) for tid in registry]
94
+ )
95
+ html = render_boards(boards, df, registry)
96
+ assert "be the first" not in html.lower()
97
+ assert "baseline</span>" in html
98
+
99
+
100
+ def test_a_submitted_model_removes_the_challenger_line(
101
+ boards, registry, flagged_results
102
+ ):
103
+ df = flagged_results(
104
+ *[("pca-50", tid, 0.8, "2026-01-01", True) for tid in registry],
105
+ *[("eva", tid, 0.9, "2026-01-01", False) for tid in registry],
106
+ )
107
+ html = render_boards(boards, df, registry)
108
+ assert "baseline</span>" in html
109
+
110
+
111
+ def test_cards_carry_their_counts(boards, registry, results):
112
+ html = _boards_html(boards, registry, results)
113
+ assert "patients" in html and "cohorts" in html and "diseases" in html
114
+
115
+
116
+ def test_registry_text_is_escaped_on_the_boards_page(registry, results):
117
+ registry["t004"]["diseases"] = ['<img src=x onerror="alert(1)">']
118
+ boards = build_boards(registry)
119
+ html = _boards_html(boards, registry, results)
120
+ assert "<img src=x" not in html
121
+ assert "&lt;img src=x" in html
122
+
123
+
124
+ # ------------------------------------------------------------------- board page
125
+ def test_ranked_table_bolds_the_best_and_captions_full_coverage(
126
+ boards, registry, named, flagged_results
127
+ ):
128
+ df = flagged_results(
129
+ *[("eva", tid, 0.9, "2026-01-01", False) for tid in registry],
130
+ *[("foo", tid, 0.5, "2026-01-01", False) for tid in registry],
131
+ )
132
+ html = render_board(named("bulk RNA"), df, registry)
133
+ assert "are ranked" in html
134
+ assert "eva" in html and "foo" in html
135
+ assert "font-weight:700" in html # the best value is bolded
136
+
137
+
138
+ def test_data_tables_expose_sortable_column_headers(boards, registry, named, results):
139
+ df = results(*[("eva", task_id, 0.8, "2026-01-01") for task_id in registry])
140
+ html = render_board(named("bulk RNA"), df, registry)
141
+ assert 'class="pm-sort" data-sort-index="0"' in html
142
+ assert 'aria-sort="none"' in html
143
+
144
+
145
+ def test_ranked_board_shows_only_the_institution_credit(
146
+ boards, registry, flagged_results
147
+ ):
148
+ df = flagged_results(*[("eva", tid, 0.9, "2026-01-01", False) for tid in registry])
149
+ df["institution"] = "Scienta"
150
+ html = render_board(build_boards(registry)[0], df, registry)
151
+ assert "Institution" in html and "Scienta" in html
152
+ assert "Team" not in html
153
+ assert "Author" not in html
154
+
155
+
156
+ def test_board_escapes_submission_credit(registry, named, flagged_results):
157
+ df = flagged_results(*[("eva", tid, 0.9, "2026-01-01", False) for tid in registry])
158
+ df["institution"] = "<script>alert(1)</script>"
159
+ html = render_board(named("bulk RNA"), df, registry)
160
+ assert "<script>" not in html and "&lt;script&gt;" in html
161
+
162
+
163
+ def test_author_submission_gets_a_check_chip_and_paper_link(
164
+ registry, named, flagged_results
165
+ ):
166
+ df = flagged_results(*[("eva", tid, 0.9, "2026-01-01", False) for tid in registry])
167
+ df["is_author_submission"] = True
168
+ df["paper_link"] = "https://example.org/paper"
169
+ html = render_board(named("bulk RNA"), df, registry)
170
+ assert 'href="https://example.org/paper"' in html
171
+ assert ">eva</a>" in html
172
+ assert 'class="pm-author-chip"' in html
173
+ assert "✓ Authors" in html
174
+
175
+
176
+ def test_unsafe_paper_link_is_not_rendered_as_a_link(registry, named, flagged_results):
177
+ df = flagged_results(*[("eva", tid, 0.9, "2026-01-01", False) for tid in registry])
178
+ df["paper_link"] = 'javascript:alert("x")'
179
+ html = render_board(named("bulk RNA"), df, registry)
180
+ assert "javascript:" not in html
181
+ assert ">eva<" in html
182
+
183
+
184
+ def test_board_keeps_legacy_private_owners_hidden(registry, named, flagged_results):
185
+ df = flagged_results(*[("eva", tid, 0.9, "2026-01-01", False) for tid in registry])
186
+ df["hf_username"] = "legacy-private-owner"
187
+ html = render_board(named("bulk RNA"), df, registry)
188
+ assert "legacy-private-owner" not in html
189
+
190
+
191
+ def test_board_escapes_a_hostile_model_name(registry, named, flagged_results):
192
+ hostile = "<img src=x onerror=1>"
193
+ df = flagged_results(
194
+ *[(hostile, tid, 0.9, "2026-01-01", False) for tid in registry]
195
+ )
196
+ html = render_board(named("bulk RNA"), df, registry)
197
+ assert "<img src=x" not in html
198
+ assert "&lt;img src=x" in html
199
+
200
+
201
+ def test_an_empty_board_invites_the_first_submission(registry, results, named):
202
+ html = render_board(named("bulk RNA"), results(), registry)
203
+ assert "be the first" in html.lower()
204
+
205
+
206
+ def test_a_missing_board_renders_a_message_not_a_crash(registry, results):
207
+ html = render_board(None, results(), registry)
208
+ assert "No board available" in html
209
+
210
+
211
+ # ------------------------------------------------------------------- tasks page
212
+ def test_tasks_page_shows_biology_but_never_an_accession(registry):
213
+ html = render_tasks(registry)
214
+ assert "Crohn Disease" in html
215
+ assert "Treatment outcome" in html
216
+ assert not re.search(r"GSE\d+|E-MTAB-\d+", html)
217
+
218
+
219
+ def test_tasks_page_escapes_registry_text(registry):
220
+ registry["t004"]["diseases"] = ['<img src=x onerror="alert(1)">']
221
+ html = render_tasks(registry)
222
+ assert "<img src=x" not in html
223
+ assert "&lt;img src=x" in html
224
+
225
+
226
+ # ------------------------------------------------------------------------ rail
227
+ def test_rail_links_every_board_and_marks_the_active_one(boards):
228
+ active = boards[0].slug
229
+ html = rail_html(boards, active, None)
230
+ assert set(_slugs(html)) == {b.slug for b in boards}
231
+ assert f'class="pm-link pm-active" target="_self" href="?board={active}"' in html
232
+ assert "Submit a model" in html
233
+
234
+
235
+ def test_rail_groups_contain_their_board_links(boards):
236
+ html = rail_html(boards, None, None)
237
+ groups = re.findall(r'<div class="pm-rail-group">(.*?)</div>', html)
238
+ grouped = "".join(groups)
239
+ n_open = sum(len(open_in_group(boards, group)) for group in SECTIONS)
240
+
241
+ assert set(_slugs(grouped)) == {board.slug for board in boards}
242
+ assert grouped.count('href="?tab=contribute"') == n_open
243
+
244
+
245
+ def test_rail_open_slices_route_to_contribute(boards):
246
+ html = rail_html(boards, None, None)
247
+ assert 'href="?tab=contribute"' in html
248
+ assert "proteomics" in html
test_results.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Unit tests for the results schema (no network, no Gradio)."""
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ from results import (
6
+ INSTITUTION,
7
+ IS_AUTHOR_SUBMISSION,
8
+ IS_BASELINE,
9
+ OWNER,
10
+ PAPER_LINK,
11
+ RESULT_COLUMNS,
12
+ SUBMISSION_COLUMNS,
13
+ SUBMISSION_ID,
14
+ display_name,
15
+ owner_of,
16
+ with_author_submission_flag,
17
+ with_baseline_flag,
18
+ with_institution,
19
+ with_owner,
20
+ with_paper_link,
21
+ with_submission_id,
22
+ )
23
+
24
+
25
+ def _frame(**columns) -> pd.DataFrame:
26
+ return pd.DataFrame({"model_name": ["m"], "score": [0.5], **columns})
27
+
28
+
29
+ def _rows(*rows) -> pd.DataFrame:
30
+ return pd.DataFrame(list(rows), columns=["model_name", IS_BASELINE, OWNER])
31
+
32
+
33
+ def test_a_missing_column_means_nobody_is_a_baseline():
34
+ flagged = with_baseline_flag(_frame())
35
+ assert list(flagged[IS_BASELINE]) == [False]
36
+
37
+
38
+ def test_booleans_survive_the_round_trip():
39
+ flagged = with_baseline_flag(_frame(**{IS_BASELINE: [True]}))
40
+ assert list(flagged[IS_BASELINE]) == [True]
41
+
42
+
43
+ def test_the_strings_a_csv_gives_back_are_read_as_booleans():
44
+ """``read_csv`` yields ``"True"``/``"False"`` as soon as one row is blank."""
45
+ flagged = with_baseline_flag(
46
+ pd.DataFrame({IS_BASELINE: ["True", "false", "TRUE ", np.nan, ""]})
47
+ )
48
+ assert list(flagged[IS_BASELINE]) == [True, False, True, False, False]
49
+
50
+
51
+ def test_submission_identity_is_part_of_both_schemas():
52
+ assert {
53
+ IS_BASELINE,
54
+ OWNER,
55
+ INSTITUTION,
56
+ IS_AUTHOR_SUBMISSION,
57
+ PAPER_LINK,
58
+ SUBMISSION_ID,
59
+ } <= set(RESULT_COLUMNS)
60
+ assert {OWNER, INSTITUTION, IS_AUTHOR_SUBMISSION, PAPER_LINK, SUBMISSION_ID} <= set(
61
+ SUBMISSION_COLUMNS
62
+ )
63
+ assert "diagnostics" in RESULT_COLUMNS
64
+
65
+
66
+ def test_only_baselines_are_labelled():
67
+ assert display_name("pca-50", True) == "pca-50 (baseline)"
68
+ assert display_name("eva-rna-v1", False) == "eva-rna-v1"
69
+
70
+
71
+ def test_rows_written_before_the_owner_existed_are_unclaimed():
72
+ assert list(with_owner(_frame())[OWNER]) == [""]
73
+ assert list(with_owner(_frame(**{OWNER: [np.nan]}))[OWNER]) == [""]
74
+
75
+
76
+ def test_rows_written_before_the_institution_existed_have_no_institution():
77
+ assert list(with_institution(_frame())[INSTITUTION]) == [""]
78
+ assert list(with_institution(_frame(**{INSTITUTION: [np.nan]}))[INSTITUTION]) == [
79
+ ""
80
+ ]
81
+
82
+
83
+ def test_legacy_team_values_are_read_as_institutions():
84
+ frame = _frame(team=["Legacy Lab"])
85
+ assert list(with_institution(frame)[INSTITUTION]) == ["Legacy Lab"]
86
+
87
+
88
+ def test_explicit_institution_wins_over_legacy_team():
89
+ frame = _frame(institution=["Current Lab"], team=["Old Lab"])
90
+ assert list(with_institution(frame)[INSTITUTION]) == ["Current Lab"]
91
+
92
+
93
+ def test_legacy_rows_are_not_marked_as_author_submissions():
94
+ assert list(with_author_submission_flag(_frame())[IS_AUTHOR_SUBMISSION]) == [False]
95
+ assert list(
96
+ with_author_submission_flag(_frame(**{IS_AUTHOR_SUBMISSION: [np.nan]}))[
97
+ IS_AUTHOR_SUBMISSION
98
+ ]
99
+ ) == [False]
100
+
101
+
102
+ def test_nullable_public_credits_and_submission_ids_normalize_to_blank():
103
+ frame = _frame(
104
+ **{PAPER_LINK: [pd.NA], INSTITUTION: [pd.NA], SUBMISSION_ID: [pd.NA]}
105
+ )
106
+ assert list(with_paper_link(frame)[PAPER_LINK]) == [""]
107
+ assert list(with_institution(frame)[INSTITUTION]) == [""]
108
+ assert list(with_submission_id(frame)[SUBMISSION_ID]) == [""]
109
+
110
+
111
+ def test_a_name_is_owned_by_whoever_submitted_it():
112
+ assert owner_of(_rows(("eva", False, "scienta")), "eva") == "scienta"
113
+
114
+
115
+ def test_an_unclaimed_or_unknown_name_is_free():
116
+ assert owner_of(_rows(("eva", False, "")), "eva") == ""
117
+ assert owner_of(_rows(("eva", False, "scienta")), "other") == ""
118
+ assert owner_of(pd.DataFrame(), "eva") == ""
119
+
120
+
121
+ def test_publishing_a_baseline_never_locks_the_name_for_submitters():
122
+ """``pca-50`` the baseline and ``pca-50`` the model are separate entries."""
123
+ assert owner_of(_rows(("pca-50", True, "scienta")), "pca-50") == ""
test_scoring.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Unit tests for the pure scoring policy (no network, no probe)."""
2
+
3
+ import numpy as np
4
+ import pytest
5
+ from scoring import (
6
+ TaskScore,
7
+ category_means,
8
+ compute_auroc,
9
+ compute_pearson,
10
+ compute_residual_sample_spearman,
11
+ sort_key,
12
+ )
13
+
14
+
15
+ def _score(task_id, dataset_id, category, metric, score):
16
+ return TaskScore(task_id, dataset_id, category, metric, score, 10)
17
+
18
+
19
+ def test_compute_auroc_binary():
20
+ y = np.array([0, 0, 1, 1])
21
+ proba = np.array([[0.9, 0.1], [0.6, 0.4], [0.3, 0.7], [0.2, 0.8]])
22
+ assert compute_auroc(y, proba) == pytest.approx(1.0)
23
+
24
+
25
+ def test_transfer_auroc_reads_the_column_of_the_class_it_scores():
26
+ """Test cohort holds 2 of 3 classes: scoring class 1's column would be wrong."""
27
+ y_true = np.array([0, 0, 2, 2])
28
+ preds = np.array(
29
+ [[0.7, 0.2, 0.1], [0.6, 0.3, 0.1], [0.1, 0.3, 0.6], [0.2, 0.2, 0.6]]
30
+ )
31
+
32
+ assert compute_auroc(y_true, preds, np.array([0, 1, 2])) == 1.0
33
+ assert compute_auroc(y_true, preds[:, ::-1], np.array([0, 1, 2])) == 0.0
34
+
35
+
36
+ def test_auroc_survives_a_class_the_training_fold_never_saw():
37
+ """Zeroed column + renormalisation, instead of sklearn's sum-to-one refusal."""
38
+ y_true = np.array([0, 0, 1, 1, 2, 2])
39
+ preds = np.zeros((6, 3))
40
+ preds[:, 0] = [0.9, 0.8, 0.2, 0.1, 0.2, 0.1]
41
+ preds[:, 1] = [0.1, 0.2, 0.8, 0.9, 0.3, 0.2]
42
+
43
+ score = compute_auroc(y_true, preds, np.array([0, 1, 2]))
44
+
45
+ assert 0.0 <= score <= 1.0
46
+
47
+
48
+ def test_auroc_refuses_predictions_with_no_mass_on_the_present_classes():
49
+ y_true = np.array([0, 0, 1, 1])
50
+ preds = np.zeros((4, 3))
51
+ preds[:, 2] = 1.0
52
+
53
+ with pytest.raises(ValueError, match="no probability"):
54
+ compute_auroc(y_true, preds, np.array([0, 1, 2]))
55
+
56
+
57
+ def test_compute_pearson_and_degenerate():
58
+ perfect = compute_pearson(np.array([1.0, 2, 3]), np.array([1.0, 2, 3]))
59
+ assert perfect == pytest.approx(1.0)
60
+ assert np.isnan(compute_pearson(np.array([1.0, 2, 3]), np.array([5.0, 5, 5])))
61
+
62
+
63
+ def test_residual_sample_spearman_anchors_mean_response_at_zero():
64
+ training_mean = np.array([10.0, 20.0, 30.0, 40.0])
65
+ truth = training_mean + np.array([[1.0, 3.0, -1.0, 2.0]])
66
+ baseline = training_mean[None, :]
67
+
68
+ assert compute_residual_sample_spearman(
69
+ truth, truth, training_mean
70
+ ) == pytest.approx(1.0)
71
+ assert compute_residual_sample_spearman(
72
+ truth, baseline, training_mean
73
+ ) == pytest.approx(0.0)
74
+
75
+
76
+ def test_category_means_groups_by_category():
77
+ scores = [
78
+ _score("t1", "d1", "treatment_outcome", "auroc", 0.8),
79
+ _score("t2", "d2", "treatment_outcome", "auroc", 0.6),
80
+ _score("t3", "d3", "clinical_scores", "pearson", 0.4),
81
+ ]
82
+ cats = category_means(scores)
83
+ assert cats["treatment_outcome"] == {
84
+ "metric": "auroc",
85
+ "mean": pytest.approx(0.7),
86
+ "n_tasks": 2,
87
+ }
88
+ assert cats["clinical_scores"]["mean"] == pytest.approx(0.4)
89
+ assert cats["clinical_scores"]["n_tasks"] == 1
90
+
91
+
92
+ def test_category_means_drops_non_finite_from_mean():
93
+ scores = [
94
+ _score("t1", "d1", "clinical_scores", "pearson", 0.4),
95
+ _score("t2", "d2", "clinical_scores", "pearson", float("nan")),
96
+ ]
97
+ cats = category_means(scores)
98
+ assert cats["clinical_scores"]["mean"] == pytest.approx(0.4)
99
+ assert cats["clinical_scores"]["n_tasks"] == 2
100
+
101
+
102
+ def test_category_means_all_non_finite_is_nan():
103
+ scores = [_score("t1", "d1", "endotype", "auroc", float("nan"))]
104
+ assert np.isnan(category_means(scores)["endotype"]["mean"])
105
+
106
+
107
+ def test_category_means_empty():
108
+ assert category_means([]) == {}
109
+
110
+
111
+ def test_sort_key_is_mean_of_category_means():
112
+ cats = {
113
+ "treatment_outcome": {"metric": "auroc", "mean": 0.8, "n_tasks": 1},
114
+ "clinical_scores": {"metric": "pearson", "mean": 0.2, "n_tasks": 1},
115
+ }
116
+ assert sort_key(cats) == pytest.approx(0.5)
117
+
118
+
119
+ def test_sort_key_ignores_non_finite_and_empty():
120
+ cats = {
121
+ "treatment_outcome": {"metric": "auroc", "mean": 0.8, "n_tasks": 1},
122
+ "endotype": {"metric": "auroc", "mean": float("nan"), "n_tasks": 1},
123
+ }
124
+ assert sort_key(cats) == pytest.approx(0.8)
125
+
126
+
127
+ def test_nothing_finite_sorts_last_instead_of_at_zero():
128
+ """A constant embedding scores NaN everywhere; that must not beat a bad score."""
129
+ nothing = {"clinical_scores": {"metric": "pearson", "mean": float("nan")}}
130
+ negative = {"clinical_scores": {"metric": "pearson", "mean": -0.2}}
131
+ assert sort_key(nothing) == float("-inf")
132
+ assert sort_key({}) == float("-inf")
133
+ assert sort_key(nothing) < sort_key(negative)