Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*

.external
.external

performance_staging/
146 changes: 146 additions & 0 deletions docs/contributing-results.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
---
title: Contributing Leaderboard Results
sidebar_position: 5
---

## Contributing Leaderboard Results

The [leaderboard](/leaderboard) is built from per-dataset JSON files under
[`static/data/performance/`](https://github.com/Project-AgML/project-agml.github.io/tree/main/static/data/performance)
— one file per dataset, named after the dataset (e.g. `ash_gourd_disease_classification.json`), each holding a
JSON array of raw benchmark run records. `index.json` and `global.json` in that same directory are generated
manifests, not something you edit by hand — see [Regenerating the manifests](#regenerating-the-manifests) below.

### The Run Record Format

Each element of a dataset's JSON array describes a single model evaluated on that dataset. For example:

```json
{
"benchmark_id": 1,
"timestamp": "2026-07-18T23:27:54.133075+00:00",
"dataset": "Project-AgML/ash_gourd_disease_classification",
"dataset_config": null,
"split": "train",
"model": "openai/clip-vit-base-patch32",
"task": "classification",
"result_type": "zero-shot",
"optimized": "no",
"train_pct": 0.0,
"test_pct": 100.0,
"val_pct": 0.0,
"num_samples": 2676,
"device": "cuda",
"inference_time_seconds": 70.37,
"inference_time_seconds_per_image": 0.026298,
"train_time_seconds": 0.0,
"train_time_seconds_per_image": 0.0,
"metrics": {
"f1": 0.152805,
"precision": 0.139378,
"recall": 0.199623
},
"notes": ""
}
```

| Field | Required | Description |
| :--- | :--- | :--- |
| `benchmark_id` | Recommended | Numeric ID grouping runs from the same benchmark sweep. |
| `timestamp` | Recommended | ISO-8601 timestamp of when the run was produced. Its first 10 characters (`YYYY-MM-DD`) are shown as the result's date. |
| `dataset` | Recommended | The Hugging Face dataset path (e.g. `Project-AgML/<dataset_name>`). |
| `dataset_config` | Optional | The dataset config the model was evaluated on (e.g. `"augmented"`). Omit or `null` for the raw config. |
| `split` | Optional | The data split evaluated, e.g. `"train"` or `"test"`. |
| `model` | **Yes** | The model identifier, e.g. `"openai/clip-vit-base-patch32"`. Rows are grouped by this value. |
| `task` | **Yes** | One of `classification`, `detection`, `segmentation` — determines which `metrics` key is used as the row's primary score (see below). |
| `result_type` | Optional | Free-text label such as `"zero-shot"`; informational only — whether a run is treated as zero-shot or fine-tuned is actually determined by the presence of a `finetune` object (see [Fine-tuned runs](#fine-tuned-runs)). |
| `optimized` | Optional | `"yes"` or `"no"` (string, not boolean) — whether the model was run with inference optimizations. |
| `train_pct` / `test_pct` / `val_pct` | Optional | Informational split percentages; not used to compute `splitBreakdown` (that's derived from sample counts instead — see `num_samples` and `finetune.train_samples`/`val_samples`). |
| `num_samples` | Recommended | Number of samples the model was evaluated on. Shown in the row's notes and used as the "test" share of the split breakdown. |
| `device` | Recommended | The hardware platform the run used, e.g. `"cuda"`. Shown as the row's platform. |
| `inference_time_seconds` / `inference_time_seconds_per_image` | Optional | Timing info. |
| `train_time_seconds` / `train_time_seconds_per_image` | Optional | Timing info for fine-tuning runs. |
| `metrics` | **Yes** | Object of metric name → numeric value (see below). |
| `notes` | Optional | Free-text notes, e.g. the prompt used for a zero-shot classification run. |
| `finetune` | Only for fine-tuned runs | See [Fine-tuned runs](#fine-tuned-runs). |

#### `task` and `metrics`

`task` determines which key in `metrics` is picked as the row's primary sortable score, in priority order:

| `task` | Metric keys tried, in order |
| :--- | :--- |
| `classification` | `f1`, `accuracy`, `top1_accuracy` |
| `detection` | `map`, `map_50`, `map50`, `mAP`, `mAP@0.5`, `f1_at_iou50` |
| `segmentation` | `miou`, `iou`, `mean_iou` |

If none of those keys are present, the first metric with a finite numeric value is used instead.

Separately from the primary score, the leaderboard's four sortable global columns (F1, mAP, Precision, Recall) are
computed from `metrics` directly by matching key name patterns — a `metrics.f1` key becomes the row's F1 score, an
`mAP@0.5`-style key becomes its mAP score, and so on. You don't need to add extra keys for this; reporting whichever
metrics you actually computed (`f1`, `precision`, `recall`, `map`, ...) is enough to populate the columns that apply.
Metrics you didn't compute are left blank rather than penalized.

#### Fine-tuned runs

A run is treated as fine-tuned (rather than zero-shot) when it includes a `finetune` object:

```json
"finetune": {
"train_samples": 2000,
"val_samples": 300,
"epochs": 10,
"lr": 0.0001,
"weight_decay": 0.01,
"split_seed": 42,
"train_ratio": 0.8,
"training_time_seconds": 812.4,
"optimized": "no"
}
```

All fields are optional and used only to render notes and the split breakdown. If a model has both a zero-shot and a
fine-tuned run in the same file, the leaderboard shows the best result of each side by side rather than picking one.

A model can appear multiple times per dataset (e.g. repeated runs). Only the single best zero-shot run and the
single best fine-tuned run per model are kept — you don't need to prune older/worse runs from the file yourself,
though doing so keeps the file smaller.

### Adding Results

1. Find (or create) the dataset's file at `static/data/performance/<dataset_name>.json` — `<dataset_name>` must
match the dataset's `name` in the catalog (`static/data/datasets.json` or `static/data/hf_datasets.json`).
2. Append your run record(s) to the JSON array, following the format above.
3. If you're adding results for a brand-new dataset (no existing file), just create the file with a JSON array
containing your record(s) — it will be picked up automatically the next time the manifests are regenerated.

If you have a batch of new runs and don't want to hand-merge them into existing files, drop per-dataset JSON arrays
into a `performance_staging/` directory at the project root (same filenames as their target dataset files) and run:

```bash
python3 scripts/apply_performance_staging.py
```

This appends each staging file's records onto the matching file in `static/data/performance/` (staging files are
left in place afterward). Pass `--dry-run` to preview the merge without writing anything.

### Regenerating the Manifests

`index.json` (the list of datasets with performance data) and `global.json` (the aggregated, percentile-ranked
leaderboard used by the site-wide leaderboard view) are both generated from the per-dataset files — never edit them
directly. Regenerate them with:

```bash
npm run generate-datasets
```

This also runs automatically before `npm start` and `npm run build`. After regenerating, build or run the site
locally and check the [leaderboard page](/leaderboard) and the affected dataset's leaderboard tab to confirm your
results appear with the expected score, platform, and split breakdown.

### Opening the Pull Request

Once your run records are added and the manifests are regenerated, open a pull request against the
`project-agml.github.io` repository with the JSON changes (per-dataset files plus the regenerated `index.json` and
`global.json`). Include where the results came from (benchmark script, paper, etc.) in the PR description.
3 changes: 2 additions & 1 deletion docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ into a single training pipeline. Datasets on the Hugging Face Hub encode annotat
We welcome contributions! If you would like to contribute a new feature, fix an issue that you've noticed, or even just mention
a bug or feature that you would like to see implemented, please don't hesitate to use the *Issues* tab to bring it to our attention.

See the [contributing guidelines](/docs/development) for more information.
See the [contributing guidelines](/docs/development) for more information, or the
[guide to contributing leaderboard results](/docs/contributing-results) if you have benchmark results to add.

## Funding
This project is partly funded by the [National AI Institute for Food Systems](https://aifs.ucdavis.edu).
69 changes: 69 additions & 0 deletions scripts/apply_performance_staging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Appends run records from performance_staging/*.json into the matching
static/data/performance/<dataset>.json files. Staging files are left in
place (not cleared) after merging. Run the start command afterward to
regenerate the index.json/global.json leaderboard manifests.

Usage: python3 scripts/apply_performance_staging.py [--dry-run]
"""

import json
import sys
from pathlib import Path

project_root = Path(__file__).resolve().parent.parent
staging_dir = project_root / 'performance_staging'
performance_dir = project_root / 'static' / 'data' / 'performance'
dry_run = '--dry-run' in sys.argv[1:]


def read_json_array(file_path):
if not file_path.exists():
return []
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
if not isinstance(data, list):
raise ValueError(f'Expected a JSON array in {file_path}')
return data


def write_json_array(file_path, records):
file_path.parent.mkdir(parents=True, exist_ok=True)
with open(file_path, 'w', encoding='utf-8') as f:
f.write(json.dumps(records, indent=2) + '\n')


def main():
if not staging_dir.exists():
print('No performance_staging directory found, nothing to do.')
return

staging_files = sorted(f.name for f in staging_dir.iterdir() if f.suffix == '.json')
if not staging_files:
print('performance_staging is empty, nothing to do.')
return

for file in staging_files:
staging_path = staging_dir / file
target_path = performance_dir / file

new_records = read_json_array(staging_path)
existing_records = read_json_array(target_path)
merged = existing_records + new_records

print(
f'{file}: {len(existing_records)} existing + {len(new_records)} staged = {len(merged)} total'
)

if not dry_run:
write_json_array(target_path, merged)

if dry_run:
print('\nDry run - no files were changed.')
return

print('\nDone.')


if __name__ == '__main__':
main()
36 changes: 31 additions & 5 deletions scripts/generate-datasets.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ function buildDatasetMetadataLookup(...manifests) {
// rendering of the same files — keep the two in sync if the run schema changes.
const TASK_METRIC_KEYS = {
classification: ['f1', 'accuracy', 'top1_accuracy'],
detection: ['map', 'map_50', 'map50', 'mAP', 'mAP@0.5'],
detection: ['map', 'map_50', 'map50', 'mAP', 'mAP@0.5', 'f1_at_iou50'],
segmentation: ['miou', 'iou', 'mean_iou'],
};

Expand All @@ -82,16 +82,40 @@ function isMapMetricKey(key) {
return /^m?ap([_@-]|$)/i.test(key);
}

// Detection runs that can't produce a confidence-ranked mAP instead report f1/precision/recall
// "at IoU" (e.g. f1_at_iou50) — same metric families as classification, just suffixed. Matches
// the bare key too (e.g. "f1") so this subsumes the plain-key case.
function baseMetricKind(key) {
const normalized = key.toLowerCase();
if (/^f1([_@-]|$)/.test(normalized)) return 'f1';
if (/^(precision|prec)([_@-]|$)/.test(normalized)) return 'precision';
if (/^(recall|rec)([_@-]|$)/.test(normalized)) return 'recall';
return null;
}

function computeCategoryScores(metrics) {
const f1 = isFiniteNumber(metrics.f1) ? metrics.f1 : null;
let f1 = null;
let map = null;
let precision = null;
let recall = null;
for (const [key, value] of Object.entries(metrics)) {
if (isMapMetricKey(key) && isFiniteNumber(value)) {
if (!isFiniteNumber(value)) continue;
if (isMapMetricKey(key)) {
map = map == null ? value : Math.max(map, value);
continue;
}
switch (baseMetricKind(key)) {
case 'f1':
f1 = f1 == null ? value : Math.max(f1, value);
break;
case 'precision':
precision = precision == null ? value : Math.max(precision, value);
break;
case 'recall':
recall = recall == null ? value : Math.max(recall, value);
break;
}
}
const precision = isFiniteNumber(metrics.precision) ? metrics.precision : null;
const recall = isFiniteNumber(metrics.recall) ? metrics.recall : null;
return { f1, map, precision, recall };
}

Expand Down Expand Up @@ -135,6 +159,7 @@ function makeLeaderboardRow(entry, metricKey, variant) {
model: entry.model.trim(),
score: entry.metrics[metricKey],
categoryScores: computeCategoryScores(entry.metrics),
benchmarkId: isFiniteNumber(entry.benchmark_id) ? entry.benchmark_id : null,
variant,
date: typeof entry.timestamp === 'string' ? entry.timestamp.slice(0, 10) : null,
submitted_by: null,
Expand Down Expand Up @@ -252,6 +277,7 @@ function buildGlobalPerformanceRecords(performanceDatasets, metadataLookup) {
scores: entry.categoryScores,
crop_types: meta.crop_types,
machine_learning_task: meta.machine_learning_task,
benchmarkId: entry.benchmarkId ?? null,
variant: entry.variant === 'zero-shot' || entry.variant === 'fine-tuned' ? entry.variant : null,
optimized: Boolean(entry.optimized),
platform: typeof entry.platform === 'string' && entry.platform.trim() ? entry.platform.trim() : null,
Expand Down
2 changes: 1 addition & 1 deletion sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const sidebars: SidebarsConfig = {
label: 'Guide',
collapsible: false,
className: 'sidebarCategoryLabel',
items: ['index', 'development'],
items: ['index', 'development', 'contributing-results'],
},
{
type: 'category',
Expand Down
3 changes: 3 additions & 0 deletions src/components/DatasetMetadataModal.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,9 @@
color: var(--agml-text);
line-height: 1.5;
overflow-wrap: anywhere;
white-space: pre-wrap;
max-height: 220px;
overflow-y: auto;
}

.leaderboardTable a {
Expand Down
19 changes: 18 additions & 1 deletion src/components/LeaderboardDetailModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,25 @@ function toLabel(value: string) {
return value.replace(/_/g, ' ');
}

// 1st, 2nd, 3rd, 4th, ..., 11th-13th stay "th" (the exception the mod-10 rule alone gets wrong).
function ordinal(value: number): string {
const rounded = Math.round(value);
const mod100 = rounded % 100;
if (mod100 >= 11 && mod100 <= 13) return `${rounded}th`;
switch (rounded % 10) {
case 1:
return `${rounded}st`;
case 2:
return `${rounded}nd`;
case 3:
return `${rounded}rd`;
default:
return `${rounded}th`;
}
}

function formatPercentile(value: number | null) {
return value == null ? null : `${value.toFixed(0)}th pctl`;
return value == null ? null : `${ordinal(value)} pctl`;
}

function formatScore(value: number) {
Expand Down
Loading
Loading