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

Deploy resolved perturbation benchmark branch

Browse files

Source branch: julienduquesne/perturbation-tasks at d9477beab1597762fb3b4f3b695e4ed5dba289cf

Files changed (7) hide show
  1. README.md +8 -6
  2. leaderboard.py +28 -11
  3. pages/about.md +6 -3
  4. pages/submit.md +3 -3
  5. render.py +3 -3
  6. results.py +8 -9
  7. test_leaderboard.py +18 -10
README.md CHANGED
@@ -175,11 +175,13 @@ new datasets until their owners submit embeddings for `d011`–`d016`.
175
  ## Baselines
176
 
177
  `task_results.csv` carries an `is_baseline` flag. Reference submissions we
178
- produce ourselves (a random embedding, PCA / HVG recipes over log-CPM) are
179
- published with it set, rendered as `name (baseline)`, and **ranked in place**.
180
- A foundation model losing to a PCA is exactly the result worth publishing, so we
181
- keep it in the table rather than tucked underneath. They are generated and
182
- pushed by `benchmark/public_benchmark/baselines.py --score --publish`.
 
 
183
 
184
  ## Space configuration
185
 
@@ -209,7 +211,7 @@ pushed by `benchmark/public_benchmark/baselines.py --score --publish`.
209
  The three dataset repos are derived from one constant, `ORG` in `evaluator.py`.
210
  The rest of the org name is spelled out and has to be changed by hand:
211
 
212
- - `SPACE_REPO` in `benchmark/public_benchmark/deploy_space.py`
213
  - `PUBLIC_REPO` in `quickstart.py`
214
  - the links in this file and in `pages/*.md`
215
 
 
175
  ## Baselines
176
 
177
  `task_results.csv` carries an `is_baseline` flag. Reference submissions we
178
+ produce ourselves are published with it set, rendered as `name (baseline)`, and
179
+ **ranked in place**. A foundation model losing to raw log-CPM expression is
180
+ exactly the result worth publishing, so we keep it in the table rather than
181
+ tucked underneath. Two are published today, both log2(CPM+1) with no gene
182
+ scaling: `HVG-1200-genes` (the 1,200 highest-variance genes) and `Raw data`
183
+ (every gene). The HVG selection is label-blind but fit over every sample of a
184
+ dataset, so read both as a floor set by the raw features.
185
 
186
  ## Space configuration
187
 
 
211
  The three dataset repos are derived from one constant, `ORG` in `evaluator.py`.
212
  The rest of the org name is spelled out and has to be changed by hand:
213
 
214
+ - `SPACE_REPO` in `deploy_space.py`
215
  - `PUBLIC_REPO` in `quickstart.py`
216
  - the links in this file and in `pages/*.md`
217
 
leaderboard.py CHANGED
@@ -15,7 +15,7 @@ Two tables per board, deliberately different:
15
 
16
  Our own baselines are entries like any other: they are labelled, and they rank
17
  where their score puts them. A baseline pinned to the bottom would hide the one
18
- result worth publishing -- a foundation model losing to a PCA.
19
 
20
  Task metadata is read defensively: a registry written before diseases were
21
  recorded yields blank cells rather than breaking the page.
@@ -90,8 +90,8 @@ def latest_only(df: pd.DataFrame) -> pd.DataFrame:
90
  """Keep each entry's most recent submission, so nobody can shop for a lucky run.
91
 
92
  An entry is ``(name, is_baseline)``, not just the name. Sharing one namespace
93
- would let somebody who submits a model called ``pca-50`` bury the published
94
- ``pca-50`` baseline simply by submitting after it.
95
  """
96
  flagged = with_submission_id(
97
  with_paper_link(
@@ -163,24 +163,41 @@ def _repeat_scores(row: pd.Series) -> tuple[float, ...]:
163
 
164
 
165
  def _entries(df: pd.DataFrame, by_id: dict[str, dict], board: Board) -> list[dict]:
166
- """Per-category means for every model that covered the whole board."""
 
 
 
 
 
167
  scoped = _board_registry(by_id, board)
 
 
 
 
 
 
 
 
 
 
168
  entries = []
169
- for (model, is_baseline, submitted, submission_id), rows in latest_only(df).groupby(
170
  [MODEL_NAME, IS_BASELINE, "submitted_at", SUBMISSION_ID]
171
  ):
172
- if not board.task_ids.issubset({_norm_id(t) for t in rows["task_id"]}):
 
 
 
 
173
  continue
174
- categories = category_means(scores_from_rows(rows, scoped))
175
  task_scores = {
176
  _norm_id(row["task_id"]): float(row["score"])
177
- for _, row in rows.iterrows()
178
- if _norm_id(row["task_id"]) in board.task_ids
179
  }
180
  game_scores = {
181
  f"{_norm_id(row['task_id'])}:{repeat}": value
182
- for _, row in rows.iterrows()
183
- if _norm_id(row["task_id"]) in board.task_ids
184
  for repeat, value in enumerate(_repeat_scores(row))
185
  }
186
  entries.append(
 
15
 
16
  Our own baselines are entries like any other: they are labelled, and they rank
17
  where their score puts them. A baseline pinned to the bottom would hide the one
18
+ result worth publishing: a foundation model losing to the raw expression.
19
 
20
  Task metadata is read defensively: a registry written before diseases were
21
  recorded yields blank cells rather than breaking the page.
 
90
  """Keep each entry's most recent submission, so nobody can shop for a lucky run.
91
 
92
  An entry is ``(name, is_baseline)``, not just the name. Sharing one namespace
93
+ would let somebody who submits a model called ``Raw data`` bury the published
94
+ ``Raw data`` baseline simply by submitting after it.
95
  """
96
  flagged = with_submission_id(
97
  with_paper_link(
 
163
 
164
 
165
  def _entries(df: pd.DataFrame, by_id: dict[str, dict], board: Board) -> list[dict]:
166
+ """Per-category means for every model covering the board's scored tasks.
167
+
168
+ A task with no finite score from any latest submission is not a leaderboard
169
+ task yet. Keeping it out of the coverage check lets the rest of the board be
170
+ ranked while still leaving the task visible in the per-task table.
171
+ """
172
  scoped = _board_registry(by_id, board)
173
+ latest = latest_only(df)
174
+ scored_tasks = {
175
+ _norm_id(row["task_id"])
176
+ for _, row in latest.iterrows()
177
+ if _norm_id(row["task_id"]) in board.task_ids
178
+ and math.isfinite(float(row["score"]))
179
+ }
180
+ if not scored_tasks:
181
+ return []
182
+
183
  entries = []
184
+ for (model, is_baseline, submitted, submission_id), rows in latest.groupby(
185
  [MODEL_NAME, IS_BASELINE, "submitted_at", SUBMISSION_ID]
186
  ):
187
+ task_rows = rows[
188
+ rows["task_id"].map(_norm_id).isin(list(scored_tasks))
189
+ & rows["score"].map(lambda score: math.isfinite(float(score)))
190
+ ]
191
+ if len(task_rows["task_id"].map(_norm_id).unique()) != len(scored_tasks):
192
  continue
193
+ categories = category_means(scores_from_rows(task_rows, scoped))
194
  task_scores = {
195
  _norm_id(row["task_id"]): float(row["score"])
196
+ for _, row in task_rows.iterrows()
 
197
  }
198
  game_scores = {
199
  f"{_norm_id(row['task_id'])}:{repeat}": value
200
+ for _, row in task_rows.iterrows()
 
201
  for repeat, value in enumerate(_repeat_scores(row))
202
  }
203
  entries.append(
pages/about.md CHANGED
@@ -27,9 +27,12 @@ representation is any use in the clinic, and that is the gap PRIMO tries to fill
27
  Spearman never share a category column.
28
 
29
  The probe is identical for everyone, which is what makes the numbers comparable:
30
- what changes between two rows is the representation behind them. Perturbation
31
- residual Spearman is normalized to `[0, 1]`, with 0.5 as the uninformative
32
- mean-response anchor.
 
 
 
33
 
34
  A board covering more than one family also shows a **Mean** of the family
35
  columns. It is there to give the table an order, but it does average AUROC,
 
27
  Spearman never share a category column.
28
 
29
  The probe is identical for everyone, which is what makes the numbers comparable:
30
+ what changes between two rows is the representation behind them. The rows marked
31
+ `(baseline)` are our own submissions, scored by that same probe on the log-CPM
32
+ expression itself, whole or cut to its most variable genes. That cut never reads
33
+ a label, but it is made over every sample, so read those rows as a floor set by
34
+ the raw features rather than as a competing model. Perturbation residual Spearman
35
+ is normalized to `[0, 1]`, with 0.5 as the uninformative mean-response anchor.
36
 
37
  A board covering more than one family also shows a **Mean** of the family
38
  columns. It is there to give the table an order, but it does average AUROC,
pages/submit.md CHANGED
@@ -32,6 +32,6 @@ scored: you get ranked on every **board** whose tasks you covered in full, and
32
  your numbers still show up in each board's **per-task** table, so nothing you
33
  send is thrown away.
34
 
35
- **Your first target is the baselines.** We run our own reference submissions,
36
- linear probes on the most variable genes, and they sit on the boards labelled
37
- `(baseline)`. Beating them is the bar to clear.
 
32
  your numbers still show up in each board's **per-task** table, so nothing you
33
  send is thrown away.
34
 
35
+ **Your first target is the baselines.** We run our own reference submissions on
36
+ the log-CPM expression itself, whole or cut down to its most variable genes, and
37
+ they sit on the boards labelled `(baseline)`. Beating them is the bar to clear.
render.py CHANGED
@@ -129,8 +129,8 @@ def _leading(top) -> str:
129
  """The card's mini-ranking, with the same empty/baseline states as before.
130
 
131
  An empty board says "be the first"; a board held only by our baselines says
132
- "beat the baseline" instead, because "be the first" misleads once a PCA
133
- already holds a score.
134
  """
135
  parts = ['<p class="pm-over" style="margin-top:14px">Leading (Elo)</p>']
136
  if not top:
@@ -353,7 +353,7 @@ def render_board(board: Board | None, df: pd.DataFrame, by_id: dict[str, dict])
353
  ranked = _df_to_table(
354
  ranked_table(df, by_id, board),
355
  bold_axis=0,
356
- empty="No model has covered every task of this board yet. Be the first to submit.",
357
  )
358
  per_task = _df_to_table(
359
  per_task_table(df, by_id, board),
 
129
  """The card's mini-ranking, with the same empty/baseline states as before.
130
 
131
  An empty board says "be the first"; a board held only by our baselines says
132
+ "beat the baseline" instead, because "be the first" misleads once the raw
133
+ expression already holds a score.
134
  """
135
  parts = ['<p class="pm-over" style="margin-top:14px">Leading (Elo)</p>']
136
  if not top:
 
353
  ranked = _df_to_table(
354
  ranked_table(df, by_id, board),
355
  bold_axis=0,
356
+ empty="No model has covered every scored task of this board yet. Be the first to submit.",
357
  )
358
  per_task = _df_to_table(
359
  per_task_table(df, by_id, board),
results.py CHANGED
@@ -1,15 +1,14 @@
1
  """Persisted leaderboard rows: the results-CSV schema and its Hugging Face IO.
2
 
3
  Kept out of ``app.py`` so the schema has one owner and so the baseline publisher
4
- (``benchmark/public_benchmark/baselines.py``) can append rows without importing
5
- Gradio. ``huggingface_hub`` is imported lazily, so the unit tests touch no
6
- network.
7
 
8
- ``is_baseline`` marks a reference submission we produced ourselves (a random
9
- embedding, a PCA of log-CPM) rather than a model somebody sent us. Baselines are
10
- ranked in place, never pinned: the point of showing them is that a foundation
11
- model can lose to a PCA, and a row pushed to the bottom of the table would hide
12
- exactly that.
13
 
14
  ``hf_username`` privately records who submitted a name, because the board keeps
15
  each name's LATEST rows: without an owner, anyone could supersede another team's
@@ -129,7 +128,7 @@ def owner_of(df: pd.DataFrame, model: str) -> str:
129
  """Who claimed this submitted model name, or ``""`` if it is free.
130
 
131
  Baselines are ignored: they live in their own namespace (``is_baseline``), so
132
- publishing ``pca-50`` never stops somebody submitting a model of that name.
133
  """
134
  if df.empty:
135
  return ""
 
1
  """Persisted leaderboard rows: the results-CSV schema and its Hugging Face IO.
2
 
3
  Kept out of ``app.py`` so the schema has one owner and so the baseline publisher
4
+ can append rows without importing Gradio. ``huggingface_hub`` is imported lazily,
5
+ so the unit tests touch no network.
 
6
 
7
+ ``is_baseline`` marks a reference submission we produced ourselves (log-CPM
8
+ expression, whole or cut to its most variable genes) rather than a model somebody
9
+ sent us. Baselines are ranked in place, never pinned: the point of showing them is
10
+ that a foundation model can lose to the raw features, and a row pushed to the
11
+ bottom of the table would hide exactly that.
12
 
13
  ``hf_username`` privately records who submitted a name, because the board keeps
14
  each name's LATEST rows: without an owner, anyone could supersede another team's
 
128
  """Who claimed this submitted model name, or ``""`` if it is free.
129
 
130
  Baselines are ignored: they live in their own namespace (``is_baseline``), so
131
+ publishing ``Raw data`` never stops somebody submitting a model of that name.
132
  """
133
  if df.empty:
134
  return ""
test_leaderboard.py CHANGED
@@ -33,6 +33,15 @@ def test_full_board_coverage_is_ranked_and_partial_is_not(registry, named, resul
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
  ):
@@ -119,21 +128,20 @@ def test_nullable_credits_render_as_missing(registry, named, results):
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):
@@ -190,11 +198,11 @@ def test_top_models_are_ordered_and_limited(registry, named, results):
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):
 
33
  assert list(board["Model"]) == ["full"]
34
 
35
 
36
+ def test_tasks_with_no_finite_score_do_not_block_rankings(registry, named, results):
37
+ df = results(
38
+ *_full("better", "2026-01-01", scores=(0.9, 0.9, 0.9, 0.9, float("nan"))),
39
+ *_full("worse", "2026-01-01", scores=(0.2, 0.2, 0.2, 0.2, float("nan"))),
40
+ )
41
+ board = ranked_table(df, registry, named("bulk RNA"))
42
+ assert list(board["Model"]) == ["better", "worse"]
43
+
44
+
45
  def test_a_partial_model_is_ranked_on_the_board_it_fully_covered(
46
  registry, named, results
47
  ):
 
128
  assert board.loc[0, "Model"].is_author_submission is False
129
 
130
 
131
+ def test_a_model_scoring_nothing_finite_is_not_ranked(registry, named, results):
132
+ """A submission with no finite score has no scored task coverage."""
133
  df = results(
134
  *_full("nan-everywhere", "2026-01-01", scores=(float("nan"),) * 5),
135
  *_full("bad", "2026-01-01", scores=(-0.2,) * 5),
136
  )
137
  board = ranked_table(df, registry, named("bulk RNA"))
138
+ assert list(board["Model"]) == ["bad"]
 
139
 
140
 
141
+ def test_a_card_is_empty_when_no_model_has_a_finite_score(registry, named, results):
142
  df = results(*_full("nan-everywhere", "2026-01-01", scores=(float("nan"),) * 5))
143
  top = top_models(df, registry, named("bulk RNA"), 3)
144
+ assert top == []
145
 
146
 
147
  def test_ranked_is_empty_when_nobody_covered_the_board(registry, named, results):
 
198
  assert not any(entry.is_baseline for entry in top)
199
 
200
 
201
+ def test_top_models_uses_the_remaining_scored_tasks(registry, named, results):
 
 
202
  df = results(("partial", "t001", 0.99, "2026-01-01"))
203
+ assert [entry.name for entry in top_models(df, registry, named("bulk RNA"), 3)] == [
204
+ "partial"
205
+ ]
206
 
207
 
208
  def test_a_baseline_is_ranked_in_place_and_labelled(registry, named, flagged_results):