graphnetz.benchmark¶
The benchmark layer: a curated task catalogue, a model registry, the runner that executes every compatible (task, model, seed) triple, and the report those runs return.
The report is the return type
run_benchmark does not return an accuracy table. It returns a
BenchmarkReport whose methods are
the statistical layer: per-cell intervals, corrected pairwise tests, the
quantities that say when those tests are uninformative, and rank
aggregation across tasks. See Reading the report.
Running a benchmark¶
benchmark
¶
Statistically robust benchmarks across a category for one or many models.
The dispatcher trains every compatible (model, task) pair across multiple
seeds and returns a BenchmarkReport that exposes mean ± 95 % t-CI,
paired t-tests with Holm-Bonferroni correction, publication-ready LaTeX
tables, and plots.
Custom models are plugged in via the same three paths as before:
-
Decorator / registry::
from graphnetz import register_model
@register_model(task_type="node_cls") class MyGNN(torch.nn.Module): def init(self, in_channels, hidden_channels, out_channels): ...
-
Class attribute::
class MyGNN(torch.nn.Module): task_types = {"node_cls"}
-
Inline tuple
(cls, tasks)or(cls, tasks, factory)in themodelsmapping::run_benchmark("social", {"MyGNN": (MyGNN, "node_cls")})
The default factory calls cls(in_channels, hidden_channels, out_channels);
DGI-task models receive (in_channels, hidden_channels) (the third argument
is dropped).
Functions:
| Name | Description |
|---|---|
run_benchmark |
Run a benchmark across one or more (model, task, seed) combinations. |
SearchSpace
dataclass
¶
SearchSpace(
lr: Sequence[float] = (),
weight_decay: Sequence[float] = (),
hidden_channels: Sequence[int] = (),
n_random: int | None = None,
seed: int = 0,
_axes: tuple[str, ...] = ("lr", "weight_decay", "hidden_channels"),
)
Hyperparameters to search over, and how to enumerate them.
Only knobs the training routines already expose are searchable:
lr and weight_decay (the optimiser) and hidden_channels (the
encoder width). An empty space means "no search", so
SearchSpace() reproduces the unsearched protocol exactly.
n_random switches from an exhaustive grid to that many random draws
(without replacement where the grid is smaller), which is the better use of
a fixed budget once the grid has more than two or three axes
(Bergstra & Bengio, 2012).
Methods:
| Name | Description |
|---|---|
candidates |
Enumerate the candidate configurations, always at least one. |
candidates
¶
Enumerate the candidate configurations, always at least one.
Source code in src/graphnetz/benchmark/_search.py
run_benchmark
¶
run_benchmark(
category: str | None = None,
models: type
| tuple[Any, ...]
| ModelSpec
| dict[str, type | tuple[Any, ...] | ModelSpec]
| None = None,
root: str = "data/benchmark",
hidden_channels: int = 64,
epochs: int | None = None,
only: list[str] | None = None,
verbose: bool = True,
seeds: int | Iterable[int] | None = None,
seed: int | None = None,
task_type: str | None = None,
tasks: Iterable[Task] | None = None,
device: device | str | None = "auto",
search: SearchSpace | None = None,
) -> BenchmarkReport
Run a benchmark across one or more (model, task, seed) combinations.
Two ways to choose tasks:
- By category (default) -- tasks come from
BENCHMARK_TASKSindexed as[category][task_type] -> list[Task]. Passcategory="social"(etc.) and optionally restrict withtask_typeandonly=. - Ad-hoc -- pass
tasks=[Task(...), ...]to bypass the registry entirely. Useful for benchmarking custom datasets without mutating global state.categorythen defaults to"custom"and is used only to namespaceroot/cache directories.
The runner trains every compatible (model, task) pair across each
value in seeds (default (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)) and aggregates the per-seed
histories into a BenchmarkReport.
search optionally adds an inner hyperparameter loop (Algorithm 1,
line 4a): for every (task, model, seed) the runner trains each candidate in
the SearchSpace, scores them on the task's
validation series, and keeps the winner's history. Candidates are never
scored on a held-out metric. The selected configuration and the full search
trace land in report.config["search_selected"], and the cost multiplier
is announced before training starts, because a grid of G candidates
multiplies compute by G.
Source code in src/graphnetz/benchmark/_runner.py
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 | |
The report¶
The plotting methods (plot, plot_forest, plot_pairwise,
plot_critical_difference, plot_learning_curves) are mixed in from a
separate module to keep the statistics and the figure code apart; they are
part of the public surface and are documented here alongside the rest.
benchmark
¶
Statistically robust benchmarks across a category for one or many models.
The dispatcher trains every compatible (model, task) pair across multiple
seeds and returns a BenchmarkReport that exposes mean ± 95 % t-CI,
paired t-tests with Holm-Bonferroni correction, publication-ready LaTeX
tables, and plots.
Custom models are plugged in via the same three paths as before:
-
Decorator / registry::
from graphnetz import register_model
@register_model(task_type="node_cls") class MyGNN(torch.nn.Module): def init(self, in_channels, hidden_channels, out_channels): ...
-
Class attribute::
class MyGNN(torch.nn.Module): task_types = {"node_cls"}
-
Inline tuple
(cls, tasks)or(cls, tasks, factory)in themodelsmapping::run_benchmark("social", {"MyGNN": (MyGNN, "node_cls")})
The default factory calls cls(in_channels, hidden_channels, out_channels);
DGI-task models receive (in_channels, hidden_channels) (the third argument
is dropped).
BenchmarkReport
dataclass
¶
BenchmarkReport(
seeds: tuple[int, ...],
histories: dict[str, dict[str, list[dict[str, list[float]]]]],
config: dict[str, Any] = dict(),
ci_method: str = "t",
bootstrap_n: int = 10000,
bootstrap_seed: int = 0,
pairwise_method: str = "t",
epoch_selection: str = "final",
SCHEMA: int = 1,
)
Bases: _ReportPlotsMixin
Structured outcome of a multi-seed benchmark run.
histories[task][model] is a list with one history dict per seed (in
seed order). The report is also a read-only mapping task -> {model:
history_seed_0} for backward compatibility with single-seed callers.
Methods:
| Name | Description |
|---|---|
plot |
Grouped bar chart of mean ± CI half-width across seeds. |
plot_forest |
Forest plot, one row per task with models jittered within the row. |
plot_pairwise |
Pairwise comparison plot, with two layouts that scale differently. |
plot_critical_difference |
Demšar critical-difference (CD) diagram. |
plot_learning_curves |
Mean ± t-CI learning curves, one panel per task, sharing y-axis. |
final_metrics |
Reduced metric value per (task, model, seed). |
epoch_selection_support |
Per task: whether |
selected_epochs |
The epoch each (task, model, seed) metric was taken from. |
summary |
Per-(task, model) mean, std, sem, CI half-width and bounds. |
pairwise |
Paired pairwise tests between models per task with Holm adjustment. |
power |
Per-comparison minimum detectable effect and observed power. |
equivalence |
Two one-sided tests for equivalence within |
rank_table |
Per-task ranks of the seed means; rows tasks, columns models. |
mean_ranks |
Mean rank per model across tasks, under a choice of task weighting. |
friedman |
Friedman omnibus test on per-task ranks of seed-mean metrics. |
rank_stability |
How much the across-task ranking depends on which tasks were run. |
to_json |
Serialise the report to JSON: seeds, config, and the raw histories. |
from_json |
Rebuild a report from |
to_latex |
Booktabs LaTeX table of mean ± CI half-width with bold-best per task. |
pairwise_to_latex |
LaTeX booktabs table of pairwise Holm-adjusted p-values. |
Attributes:
| Name | Type | Description |
|---|---|---|
epoch_selection |
str
|
How each per-epoch history is reduced to one number. |
SCHEMA |
int
|
Version of the JSON payload written by |
epoch_selection
class-attribute
instance-attribute
¶
epoch_selection: str = 'final'
How each per-epoch history is reduced to one number.
"final" (default) is the fixed-epoch protocol. "best_val" selects
the epoch optimising the paired validation series and reports the held-out
metric there -- proper checkpoint selection. Setting it once switches the
summary, the pairwise tests, the rank aggregation, every plot and the LaTeX
exporters together.
SCHEMA
class-attribute
instance-attribute
¶
SCHEMA: int = 1
Version of the JSON payload written by to_json.
plot
¶
plot(
ax: Axes | None = None,
*,
ci: float = 0.95,
ylabel: str | None = None,
title: str | None = None,
annotate: bool = True,
pretty_tasks: Mapping[str, str] | None = None,
) -> tuple[Figure, Axes]
Grouped bar chart of mean ± CI half-width across seeds.
Source code in src/graphnetz/benchmark/_report_plots.py
plot_forest
¶
plot_forest(
ax: Axes | None = None,
*,
ci: float = 0.95,
pretty_tasks: Mapping[str, str] | None = None,
xlabel: str | None = None,
height_per_task: float = 0.42,
sort_within: bool = False,
band: bool = True,
) -> tuple[Figure, Axes]
Forest plot, one row per task with models jittered within the row.
Height scales with the number of tasks only -- adding more models widens the within-row jitter rather than adding new rows -- so the figure stays compact for many models.
sort_within=True orders the jittered positions per-task so the
best mean lands at the top of the row (helps spot leaders when there
are many models). Each model keeps a stable colour across tasks.
band=True shades alternating task rows (banded reading aid).
Source code in src/graphnetz/benchmark/_report_plots.py
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | |
plot_pairwise
¶
plot_pairwise(
ax: Axes | None = None,
*,
ci: float = 0.95,
alpha: float = 0.05,
pretty_tasks: Mapping[str, str] | None = None,
layout: str = "matrix",
max_cols: int = 3,
method: str | None = None,
) -> tuple[Figure, Any]
Pairwise comparison plot, with two layouts that scale differently.
layout="matrix" (default) -- one significance heatmap per task,
with the lower triangle holding \(-\log_{10}(p_{\text{Holm}})\) and the
upper triangle holding the signed mean difference. Scales to many
models and many tasks (panels arranged in a grid with at most
max_cols columns).
layout="list" -- one row per pairwise comparison with CI whiskers
and a significance marker. Best for small numbers of comparisons.
method overrides self.pairwise_method ("t" or
"wilcoxon") for this call only.
Source code in src/graphnetz/benchmark/_report_plots.py
plot_critical_difference
¶
plot_critical_difference(
*,
alpha: float = 0.05,
title: str | None = None,
aggregation: str = "uniform",
weights: Mapping[str, float] | None = None,
groups: Mapping[str, str] | None = None,
epoch_selection: str | None = None,
strict: bool = True,
) -> tuple[Figure, Axes]
Demšar critical-difference (CD) diagram.
Computes mean ranks of every model across tasks and overlays the
Nemenyi critical difference at level alpha. Models within
CD of each other are joined by a thick horizontal "clique" bar
(i.e., not significantly different). This is the canonical
scalable visualization for multi-method, multi-dataset benchmarks
(Demšar, 2006).
Only models present in every task are included. Requires at least two tasks and at least two such models.
aggregation selects the task weighting (see
mean_ranks). Only the default
"uniform" weighting has a Friedman/Nemenyi null, so any other
choice draws the ranks without the CD reference and clique bars and
labels itself as a diagnostic -- a weighted mean rank must not be
compared against \(CD_\alpha\).
epoch_selection overrides the report-level setting, so the diagram
can be redrawn under checkpoint selection without retraining.
Source code in src/graphnetz/benchmark/_report_plots.py
502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 | |
plot_learning_curves
¶
plot_learning_curves(
*,
ci: float = 0.95,
metric_key: str | None = None,
pretty_tasks: Mapping[str, str] | None = None,
ylabel: str | None = None,
max_cols: int = 4,
) -> tuple[Figure, ndarray]
Mean ± t-CI learning curves, one panel per task, sharing y-axis.
Panels wrap into rows of at most max_cols so individual panels
stay readable on benchmarks with many tasks; a single legend is
shared by all panels below the figure.
Source code in src/graphnetz/benchmark/_report_plots.py
715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 | |
final_metrics
¶
final_metrics(
key: str | None = None, *, epoch_selection: str | None = None, strict: bool = True
) -> dict[str, dict[str, list[float]]]
Reduced metric value per (task, model, seed).
epoch_selection overrides epoch_selection for this call.
Under "best_val" a task whose trainer records no held-out series
cannot be selected honestly; strict=True raises, strict=False
falls back to the final epoch for that task alone (use
epoch_selection_support to see which tasks fell back).
Source code in src/graphnetz/benchmark/_report.py
epoch_selection_support
¶
Per task: whether "best_val" is available, and why not if it is not.
Checkpoint selection needs a validation series and a held-out series in the same history. Node classification and both link-prediction trainers record both; the graph-level trainers record only a validation metric, so selecting on it and reporting it would be optimistically biased. This surfaces that asymmetry instead of hiding it.
Source code in src/graphnetz/benchmark/_report.py
selected_epochs
¶
selected_epochs(
key: str | None = None, *, epoch_selection: str | None = None, strict: bool = True
) -> DataFrame
The epoch each (task, model, seed) metric was taken from.
Auditing surface for "best_val": a selected epoch pinned at the
last epoch for every seed means selection did nothing, and a wildly
varying one means the run had not converged.
Source code in src/graphnetz/benchmark/_report.py
summary
¶
summary(
ci: float = 0.95,
method: str | None = None,
*,
epoch_selection: str | None = None,
strict: bool = True,
) -> DataFrame
Per-(task, model) mean, std, sem, CI half-width and bounds.
method overrides self.ci_method for this call only; choose
"t" for Student's-t intervals (default) or "bootstrap" for
percentile-bootstrap intervals (better for non-Gaussian metrics
such as Hits@K, MRR, or AUC).
Source code in src/graphnetz/benchmark/_report.py
pairwise
¶
pairwise(
alpha: float = 0.05,
method: str | None = None,
*,
epoch_selection: str | None = None,
strict: bool = True,
) -> DataFrame
Paired pairwise tests between models per task with Holm adjustment.
method overrides self.pairwise_method for this call only:
"t"(default) -- paired Student's t-test on per-seed final metrics."wilcoxon"-- non-parametric Wilcoxon signed-rank test on the paired differences. Recommended at small seed counts where the paired t-test's normality assumption is most fragile; see Benavoli et al., JMLR 17(5):1-36, 2016.
epoch_selection overrides epoch_selection, so the same
comparison can be run under a fixed-epoch and a checkpoint-selected
protocol without retraining.
Source code in src/graphnetz/benchmark/_report.py
power
¶
power(
alpha: float = 0.05,
target_power: float = 0.8,
*,
targets: tuple[float, ...] = (0.02, 0.01, 0.005),
epoch_selection: str | None = None,
strict: bool = True,
) -> DataFrame
Per-comparison minimum detectable effect and observed power.
A non-significant pairwise row means one of two very different things:
the models are close, or the design could not tell. This separates
them. mde is the smallest true paired difference detectable at
target_power; detectable flags comparisons whose observed
effect clears their own MDE; seeds_for_* says how many seeds a
given effect size would need.
See graphnetz.benchmark._power for the formulae.
Source code in src/graphnetz/benchmark/_report.py
equivalence
¶
equivalence(
margin: float,
alpha: float = 0.05,
*,
method: str | None = None,
epoch_selection: str | None = None,
strict: bool = True,
) -> DataFrame
Two one-sided tests for equivalence within margin, plus a verdict.
margin is the smallest effect size of interest, in the metric's own
units (e.g. 0.01 accuracy points). Combined with the difference
test, every comparison lands in exactly one of different,
equivalent, trivial or undetermined -- so "we found no
difference" can be stated as a positive claim where the data support it
and withheld where they do not.
Both families are Holm-corrected over the comparisons within a task,
matching pairwise.
Source code in src/graphnetz/benchmark/_report.py
rank_table
¶
Per-task ranks of the seed means; rows tasks, columns models.
Rank 1 is best, ties averaged, and the metric direction is applied per task, so a benchmark mixing accuracy with MAE ranks correctly. Only models present in every task appear -- the same restriction the CD diagram applies, surfaced as data.
Source code in src/graphnetz/benchmark/_report.py
mean_ranks
¶
mean_ranks(
*,
aggregation: str = "uniform",
weights: Mapping[str, float] | None = None,
groups: Mapping[str, str] | None = None,
epoch_selection: str | None = None,
strict: bool = True,
) -> Series
Mean rank per model across tasks, under a choice of task weighting.
The Demšar procedure weights every task equally, which means a small synthetic task counts as much as a large public benchmark. That is a modelling choice, not a law, so it is exposed:
"uniform"
The Demšar default, \(\bar r_i = \frac1N \sum_n r_{n,i}\).
This is the only variant whose null distribution the Friedman and
Nemenyi procedures describe.
"reliability"
Weight each task by the inverse mean 95 % CI half-width of its
cells, so tasks whose seed variance makes their ranking unreliable
contribute less.
"hierarchical"
Average within each group in groups (e.g. research category),
then across groups, so a category with many tasks does not
dominate one with few.
"custom"
Use weights directly, keyed by task name.
Non-uniform variants are diagnostics: they break the exchangeability
the Friedman null assumes, so a weighted mean rank must not be compared
against \(CD_\alpha\). Use rank_stability for uncertainty
on them.
Source code in src/graphnetz/benchmark/_report.py
friedman
¶
friedman(
alpha: float = 0.05, *, epoch_selection: str | None = None, strict: bool = True
) -> dict[str, float | int | bool]
Friedman omnibus test on per-task ranks of seed-mean metrics.
Returns the statistic chi2, the asymptotic \(\chi^2_{k-1}\) p-value,
the rejection flag at alpha, the \((k, N)\) shape, and the Nemenyi
critical_difference implied by that shape. Also returns
n_for_observed_gap: the number of tasks at which \(CD_\alpha\) would
shrink below the largest observed mean-rank gap -- i.e. how much
benchmark breadth the current ordering would need before it could be
called significant. Since \(CD_\alpha \propto 1/\sqrt N\) this is a
design quantity, computed from the observed gap and independent of any
assumption about how new tasks would rank.
The Nemenyi post-hoc surfaced in plot_critical_difference
should only be interpreted when rejected is true (Demšar, 2006).
Only models present in every task are included.
Source code in src/graphnetz/benchmark/_report.py
rank_stability
¶
rank_stability(
*,
n_boot: int = 10000,
alpha: float = 0.05,
seed: int = 0,
epoch_selection: str | None = None,
strict: bool = True,
) -> dict[str, Any]
How much the across-task ranking depends on which tasks were run.
A CD diagram at small N says nothing about whether the ordering
would survive a different draw of benchmark tasks. Two answers:
Bootstrap over tasks. Resample the N tasks with replacement
n_boot times, recompute mean ranks, and report the distribution of
the largest mean-rank gap, the fraction of resamples reproducing the
observed rank order, and for each pair the fraction of resamples in
which the pair separates by more than that resample's own \(CD_\alpha\).
Leave-one-task-out jackknife. Drop each task in turn and record how far every mean rank moves -- exposing single tasks that carry the ordering.
Returns mean_ranks, order, gap_observed, gap_quantiles,
order_stability, separation and jackknife.
Source code in src/graphnetz/benchmark/_report.py
593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 | |
to_json
¶
Serialise the report to JSON: seeds, config, and the raw histories.
The payload is the metric tensor X[task, model, seed] before any
statistic is applied, so a published bundle can be re-analysed under a
different CI method, pairwise test, epoch selection or aggregation
without retraining. JSON rather than pickle so the artefact stays
readable, diffable, and independent of the library version that wrote
it. Returns the JSON text and writes it to path when given.
Source code in src/graphnetz/benchmark/_report.py
from_json
classmethod
¶
from_json(source: str | Path) -> BenchmarkReport
Rebuild a report from to_json output (a path or the text itself).
Source code in src/graphnetz/benchmark/_report.py
to_latex
¶
to_latex(
path: str | Path,
*,
ci: float = 0.95,
bold_best: bool = True,
pretty_tasks: Mapping[str, str] | None = None,
caption: str | None = None,
label: str | None = None,
method: str | None = None,
) -> Path
Booktabs LaTeX table of mean ± CI half-width with bold-best per task.
method overrides self.ci_method ("t" or "bootstrap").
Source code in src/graphnetz/benchmark/_report.py
pairwise_to_latex
¶
pairwise_to_latex(
path: str | Path,
*,
alpha: float = 0.05,
caption: str | None = None,
label: str | None = None,
method: str | None = None,
) -> Path
LaTeX booktabs table of pairwise Holm-adjusted p-values.
method overrides self.pairwise_method ("t" or
"wilcoxon") for this call only.
Source code in src/graphnetz/benchmark/_report.py
Tasks and models¶
benchmark
¶
Statistically robust benchmarks across a category for one or many models.
The dispatcher trains every compatible (model, task) pair across multiple
seeds and returns a BenchmarkReport that exposes mean ± 95 % t-CI,
paired t-tests with Holm-Bonferroni correction, publication-ready LaTeX
tables, and plots.
Custom models are plugged in via the same three paths as before:
-
Decorator / registry::
from graphnetz import register_model
@register_model(task_type="node_cls") class MyGNN(torch.nn.Module): def init(self, in_channels, hidden_channels, out_channels): ...
-
Class attribute::
class MyGNN(torch.nn.Module): task_types = {"node_cls"}
-
Inline tuple
(cls, tasks)or(cls, tasks, factory)in themodelsmapping::run_benchmark("social", {"MyGNN": (MyGNN, "node_cls")})
The default factory calls cls(in_channels, hidden_channels, out_channels);
DGI-task models receive (in_channels, hidden_channels) (the third argument
is dropped).
Functions:
| Name | Description |
|---|---|
iter_benchmark_tasks |
Flatten |
task_from_dataset |
Wrap an already-loaded dataset as a |
register_task |
Register |
unregister_task |
Remove a previously registered task; returns it, or |
register_model |
Register a model with the benchmark dispatcher. |
Attributes:
| Name | Type | Description |
|---|---|---|
BENCHMARK_TASKS |
dict[str, dict[str, list[Task]]]
|
Curated benchmark taxonomy: |
TASK_TYPES |
frozenset[str]
|
|
BENCHMARK_TASKS
module-attribute
¶
BENCHMARK_TASKS: dict[str, dict[str, list[Task]]] = {
"combinatorial": {
"link_pred": [
Task(
"random_tsp",
"link_pred",
lambda root, seed=0: combinatorial.random_tsp(
root, num_graphs=1, num_nodes=200, k=4, seed=seed
),
epochs=80,
),
Task(
"random_coloring",
"link_pred",
lambda root, seed=0: combinatorial.random_coloring(
root, num_graphs=1, num_nodes=200, edge_prob=0.1, seed=seed
),
epochs=80,
),
]
},
"biology": {
"graph_cls": [
Task("mutag", "graph_cls", biology.mutag, epochs=40),
Task("proteins", "graph_cls", biology.proteins, epochs=20),
],
"link_pred": [Task("celegans", "link_pred", biology.celegans, epochs=80)],
},
"social": {
"node_cls": [
Task("cora", "node_cls", social.cora, epochs=100),
Task("citeseer", "node_cls", social.citeseer, epochs=100),
Task("pubmed", "node_cls", social.pubmed, epochs=100),
Task("roman_empire", "node_cls", social.roman_empire, epochs=80),
Task("minesweeper", "node_cls", social.minesweeper, epochs=80),
],
"link_pred": [
Task("cora_link_pred", "link_pred", social.cora, epochs=80),
Task("citeseer_link_pred", "link_pred", social.citeseer, epochs=80),
],
},
"knowledge": {
"link_pred": [
Task("fb15k_237", "link_pred", knowledge.fb15k_237, epochs=20),
Task("wordnet18rr", "link_pred", knowledge.wordnet18rr, epochs=20),
]
},
"infrastructure": {
"link_pred": [
Task("power_grid", "link_pred", infrastructure.power_grid, epochs=80),
Task("euroroad", "link_pred", infrastructure.euroroad, epochs=80),
]
},
"finance": {
"link_pred": [
Task("product_space", "link_pred", finance.product_space, epochs=80),
Task("board_directors", "link_pred", finance.board_directors, epochs=40),
]
},
"computing": {
"link_pred": [
Task(
"internet_as",
"link_pred",
lambda root: computing.internet_as(root),
epochs=40,
),
Task("topology", "link_pred", computing.topology, epochs=10),
]
},
"vision": {
"graph_cls": [
Task(
"mnist_superpixels",
"graph_cls",
lambda root: vision.mnist_superpixels(root)[:1500],
epochs=4,
)
]
},
"physics": {
"graph_reg": [
Task(
"zinc",
"graph_reg",
lambda root: (
physics.zinc(root, subset=True, split="train"),
physics.zinc(root, subset=True, split="val"),
),
epochs=10,
)
],
"link_pred": [
Task(
"ising_lattice",
"link_pred",
lambda root, seed=0: physics.ising_lattice(
root, num_graphs=1, side=20, seed=seed
),
epochs=60,
)
],
},
"security": {
"link_pred": [
Task("terrorists_911", "link_pred", security.terrorists_911, epochs=120)
]
},
}
Curated benchmark taxonomy: category -> task_type -> [Task, ...].
TASK_TYPES
module-attribute
¶
Task
dataclass
¶
A single benchmark task_type: a dataset loader plus its training task.
ModelSpec
dataclass
¶
ModelSpec(
cls: type,
task_type: frozenset[str] = frozenset(),
factory: Callable[..., Module] | None = None,
)
How to instantiate a model and which task tasks it supports.
iter_benchmark_tasks
¶
Flatten BENCHMARK_TASKS to a list, optionally filtered by category/task.
Examples:
>>> [
... t.name
... for t in iter_benchmark_tasks(category="biology", task_type="graph_cls")
... ]
['mutag', 'proteins']
Source code in src/graphnetz/benchmark/_tasks.py
task_from_dataset
¶
Wrap an already-loaded dataset as a Task.
The dataset must satisfy the conventions for task: a PyG dataset or
any object exposing ds[0] plus the relevant attributes (num_features
/ num_classes / num_relations). The benchmark dispatcher caches
the dataset, so the same instance is reused across seeds without
reloading.
Source code in src/graphnetz/benchmark/_tasks.py
register_task
¶
Register task under category in BENCHMARK_TASKS.
The task becomes visible to run_benchmark(category) and to
iter_benchmark_tasks. Use unregister_task to remove it
(e.g. in tearDown of a test).
Source code in src/graphnetz/benchmark/_tasks.py
unregister_task
¶
Remove a previously registered task; returns it, or None if absent.
Source code in src/graphnetz/benchmark/_tasks.py
register_model
¶
register_model(
cls: type | None = None,
*,
task_type: str | Iterable[str],
factory: Callable[..., Module] | None = None,
) -> Callable[[type], type] | type
Register a model with the benchmark dispatcher.
Usable as a decorator (@register_model(task_type="node_cls")) or as a
plain function (register_model(MyGNN, task_type={"graph_cls", "graph_reg"})).
Source code in src/graphnetz/benchmark/_specs.py
Convenience plotting¶
benchmark
¶
Statistically robust benchmarks across a category for one or many models.
The dispatcher trains every compatible (model, task) pair across multiple
seeds and returns a BenchmarkReport that exposes mean ± 95 % t-CI,
paired t-tests with Holm-Bonferroni correction, publication-ready LaTeX
tables, and plots.
Custom models are plugged in via the same three paths as before:
-
Decorator / registry::
from graphnetz import register_model
@register_model(task_type="node_cls") class MyGNN(torch.nn.Module): def init(self, in_channels, hidden_channels, out_channels): ...
-
Class attribute::
class MyGNN(torch.nn.Module): task_types = {"node_cls"}
-
Inline tuple
(cls, tasks)or(cls, tasks, factory)in themodelsmapping::run_benchmark("social", {"MyGNN": (MyGNN, "node_cls")})
The default factory calls cls(in_channels, hidden_channels, out_channels);
DGI-task models receive (in_channels, hidden_channels) (the third argument
is dropped).
Functions:
| Name | Description |
|---|---|
plot_benchmark |
Grouped bar chart with mean ± CI error bars. |
plot_benchmark
¶
plot_benchmark(
results: BenchmarkReport | Mapping[str, Mapping[str, Mapping[str, list[float]]]],
errors: Mapping[str, Mapping[str, float]] | None = None,
ax: Axes | None = None,
title: str | None = None,
annotate: bool = True,
ci: float = 0.95,
) -> tuple[Figure, Axes]
Grouped bar chart with mean ± CI error bars.
Accepts a BenchmarkReport (preferred) or the legacy dict form for
a single seed. errors overrides the default t-CI half-width.