How to extend the benchmark¶
Every part of the benchmark is registered by name in a registry (marathonts/bench/registry.py) and selected in the
configuration by that name. Your own components are written in a separate Python file — a plugin — and are
switched on without editing the benchmark code or the notebooks: a class or function in the plugin and a line in
the configuration (CONFIGURATION.md).
Each recipe below follows the same steps: what the component is, the minimal plugin code, how to switch it on, which stages to run, and where the result appears.
| Component | Registry / function | Interface | Switched on by |
|---|---|---|---|
| Dataset | register_parquet_dataset, registry.datasets |
load(cfg) -> pd.Series (hourly) |
datasets |
| Task | register_task(Task(...)) |
fit(train) -> params, labels(values, params) -> y |
registered = on (tasks=None) |
| Pool generator | registry.pool_generators |
fit(ctx), sample(n, ctx), state(), load_state(arrays, ctx) |
pool_suites |
| Long-series generator | registry.long_generators |
fit(ctx), generate(ctx) -> (values, how, seams) |
long_methods |
| Assembler | registry.assemblers |
assemble(pool, ctx) -> (path, info) |
extra_assemblers |
| Seam processing | registry.joiners |
windows_needed(K, T, cfg), join(W, L, cfg) -> (X, seams, src, info) |
extra_joiners, smooth_paths |
| Single method | bench.methods.register_method |
pool + assembler (+ seam processing), or long-series generator | methods |
| ML model | registry.models |
fit, predict, state, load_state, kinds |
models |
| Fidelity metric | bench.fidelity.register_fidelity_group |
fn(ctx) -> {metric: value} + orientation |
registered = on (fidelity_groups=None) |
| Utility metric | registry.utility_metrics |
fn(y, pred, ctx) -> dict |
registered = on; orientation in metric_orient |
| Statistical test | hypotheses.stats.register_test |
fn(z) -> dict or fn(d, margin) -> dict |
registered = on |
| Hypothesis | hypotheses.catalog.register_hypothesis |
fn(tables, cfg) -> {"global", "per_dataset", "extra"} |
registered = on |
Plugin files¶
A plugin is any Python file that imports the benchmark and registers components; one file can hold several. Plugins are loaded in every process that needs them:
Config.plugins— a list of files. They are imported when the configuration is created (Config.__post_init__), so they reach every stage, the stage subprocesses of the notebooks (run_in_envpasses the whole configuration) and the analysis (collectrecords the list intables/config.json, andhypotheses.runreads it from there). A relative path is looked up in the project folder, then in the working directory; each file is imported once per process.--plugins FILE …— onpython -m marathonts.bench.runthe files are loaded before the configuration and added toConfig.plugins; onpython -m marathonts.hypotheses.runthey add tests and hypotheses to the analysis.- Notebooks —
plugins=["my_plugins.py"]in the scope cell of notebooks 01–06 (the file lies in the project folder on Drive). Notebook 07 loads the plugins recorded with the tables; a file with only tests or hypotheses is added asmain([..., "--plugins", "my_tests.py"]). Re-running the setup cell re-imports an edited plugin. - Docker — the images contain only the package
marathonts/; the file is mounted into/app:
docker compose run --rm -v "$PWD/my_plugins.py:/app/my_plugins.py" bench all --plugins my_plugins.py
docker compose run --rm -v "$PWD/my_plugins.py:/app/my_plugins.py" hypotheses
A name that is already registered raises KeyError unless the component is registered with overwrite=True.
Parameters of your components are set in extra={"<component name>": {...}} and arrive as ctx.params
(generators, assemblers, fidelity groups), cfg.params("<name>") (seam processing) or self.params (models).
The configuration lines of all recipes together, in the notebook form:
cfg = dataclasses.replace(
cfg,
methods=None, baselines=None, # methods and baselines are rebuilt from the lines below
plugins=["my_plugins.py"], # path from the project folder
datasets=[*cfg.datasets, "my_sales"], # dataset
pool_suites={"vae": "tsgm_vae", "gan": "my_gan"}, # pool generator: the assembly suite on it
extra_assemblers=["my_greedy"], # assembler on every pool: vae_my_greedy, gan_my_greedy
extra_joiners=["my_blend"], # seam processing, run like the built-in ones
smooth_paths=["greedy", "my_greedy"], # assemblers that get seam processing
long_methods=[*cfg.long_methods, "seasonal_boot"], # long-series generator: method and baseline
models=[*cfg.models, "my_trees"], # ML model
extra={**cfg.extra, "my_greedy": {"start": 0}}, # parameters of your components
)
Dataset¶
What it is. Any series with a time and a value, brought to an hourly step with an "hour of week" calendar.
The prepare stage splits it into train / val / OOT, scales it and derives the window count K and the pool size N
from its length; the window length T is common to all datasets.
Plugin. From a Parquet or CSV file with a time column and a value column (agg: mean, sum or last
per hour; the path is resolved against the working directory):
from marathonts.bench.datasets import register_parquet_dataset
register_parquet_dataset("my_sales", "data/raw/my_sales/sales.parquet",
time_col="ts", value_col="units", agg="sum")
Any other source through DatasetSpec and a load(cfg) that returns an hourly pd.Series with a
DatetimeIndex (NaN where unknown):
import numpy as np
import pandas as pd
from marathonts.bench import registry as R
from marathonts.bench.datasets import DatasetSpec, hourly
def load(cfg):
df = pd.read_csv("data/raw/my_prices/prices.csv", parse_dates=["time"])
return np.log(hourly(df.set_index("time")["price"], "last"))
R.datasets.register("my_prices", DatasetSpec(load, "log price, hourly"))
Switch on. datasets=[*cfg.datasets, "my_sales"] in the configuration, or --datasets ett my_sales on the
command line.
Stages. prepare, then pools … collect and the analysis. The raw stage skips a dataset without
raw_files. The first prepare proposes the dataset's task thresholds by the tasks' rules on its train split and
writes them to Config.target_params_file (the log says so); review and commit the file so that later runs
read the same thresholds. The thresholds of an added dataset enter only its own fingerprints: nothing is redone on
the other datasets.
Check. tables/datasets.parquet lists n, K, N and the period of the dataset; its rows appear in every table
and report. A series too short for the window length (train under 4·T points, val or OOT under 2·T + embargo)
is rejected with a message; gaps longer than max_gap_interp stay missing.
Task¶
What it is. A utility task turns a series in real units into labels: fit(train) -> params (thresholds,
computed on train only) and labels(values, params) -> y (NaN where the future is unknown). kind determines the
models and metrics: clf (binary), mc (multiclass, labels 0..n_classes−1), reg, qreg (quantiles
0.1 / 0.5 / 0.9), ms (vector of h values), vol.
Plugin.
import numpy as np
from marathonts.bench.tasks import Task, register_task
def fit(v): # train only
d = v[12:] - v[:-12]
return {"thr": float(np.nanquantile(np.abs(d), 0.95))}
def labels(v, p):
y = np.full(len(v), np.nan)
d = v[12:] - v[:-12]
y[:-12] = (np.abs(d) > p["thr"]).astype(float)
y[:-12][np.isnan(d)] = np.nan
return y
register_task(Task("spike_h12", "clf", h=12, fit=fit, labels=labels))
Switch on. With tasks=None (default) every registered task runs; tasks=[...] selects a subset. A task
becomes the task of the case-study cells (CS7, CS10, CS12, CS13) and of F8 with stitch_task="spike_h12", or for
the analysis only with --case-task spike_h12.
Stages. The parameters returned by fit are exogenous: they are read from marathonts/bench/target_params.json
({dataset: {task: {parameter: value}}}), not computed during a run. prepare proposes the missing ones by the
fit rule on train and writes them; python -m marathonts.bench.run targets --plugins my_plugins.py does the same on its
own. Review and commit the file; tasks without parameters (such as regression) need no entry. Then utility
computes only the added task's cells in the existing units; fidelity recomputes the group F11 (class balance over
the classification tasks of the run); then collect and the analysis.
Check. Rows with task == "spike_h12" in tables/utility.parquet, the task in METRIC_VALUES.md and in the
utility criteria of the rankings.
A task of a kind other than the built-in ones also needs, for the analysis, a primary metric
(primary_metrics) and a utility criterion (task_criteria, else it forms its own, which the four rankings do not
include) (CONFIGURATION.md); for the hyperparameter search (hpo) a
selection metric Task(..., select_by=("metric", higher_is_better)) (else the search is skipped and the fixed
parameters are used); for the case study's row-level tests Task(..., row_loss=fn) with
fn(y, pred, scale) -> (n,) (else its rows are left out of them). Its utility metrics come from a
utility metric for that kind, its models from those that list the kind in kinds.
Pool generator¶
What it is. A generator of short windows. It is trained on the train windows and samples a pool of N windows
(N, T, 3), float32 in the benchmark channels: the value (min-max over train, ctx.lo / ctx.hi) and
(sin + 1) / 2, (cos + 1) / 2 of the hour of week. Training windows are ctx.X_core, early-stopping windows
ctx.X_hold, the whole train ctx.X_train; ctx.seed, ctx.device, ctx.N, ctx.K, ctx.T and
ctx.params are available. Every assembler of the suite then takes the same pool.
Plugin.
import numpy as np
from marathonts.bench import registry as R
from marathonts.bench.generators.base import PoolGenerator
from marathonts.bench.storage import torch_state
@R.pool_generators.register("my_gan")
class MyGAN(PoolGenerator):
env = "mygan" # envs/mygan.txt; "core" (default) = the main environment
requires = ("my_gan_lib",) # modules checked before training
def fit(self, ctx):
from my_gan_lib import train_gan
self.net = train_gan(ctx.X_core, ctx.X_hold, epochs=ctx.params.get("epochs", 100),
device=ctx.device, seed=ctx.seed)
self.info["epochs_run"] = self.net.epochs_run # goes into the pool record
def sample(self, n, ctx):
return self.net.sample(n) # (n, T, 3), values in [0, 1]
def state(self): # weights → artifacts/<profile>/generators/…parquet
return torch_state(self.net)
def load_state(self, arrays, ctx): # back, without training
from my_gan_lib import build_gan
self.net = build_gan(ctx).load_from(arrays)
return self
state() returns a dict of numpy arrays; anything picklable can be stored with bench.storage.blob(obj) and
restored with unblob. Without load_state a stored generator cannot be reused and is trained again. Adapters to
use as examples are in marathonts/bench/generators/builtin.py: tsgm_vae (tsgm), sdv_par (SDV PARSynthesizer),
real_windows.
Switch on. pool_suites={"vae": "tsgm_vae", "gan": "my_gan"} with methods=None. The whole assembly suite
runs on the new pool: gan_rand, gan_greedy, gan_atsp, gan_pctsp, gan_mbb, gan_knn, gan_quilting,
gan_herding, plus gan_rand_b with noise_twin=True, every custom assembler, and seam processing over the
assemblers of smooth_paths. The assembler zseries has published results only and is not run on any pool, so
smooth_paths names the assemblers that get seam processing on the new pool (e.g. ["greedy"]).
Stages. pools --pool-generators my_gan in the generator's environment
(python -m marathonts.bench.run pools --pool-generators my_gan --plugins my_plugins.py; Colab:
run_in_env("mygan", "pools", "--pool-generators", "my_gan", config=cfg) in notebook 02). Without its modules,
the other stages skip it and name envs/mygan.txt. A generator of the main environment is also trained by
generate when its pool is missing. Then generate … collect and the analysis.
Check. tables/pools.parquet (rows with pool_generator == "my_gan": training and sampling time,
self.info), the methods gan_* in every table and ranking (RQ1 includes the methods of all suites). The case
study of a method on the new pool: python -m marathonts.hypotheses.run --case-method gan_greedy --noise-pair gan_rand gan_rand_b.
Long-series generator¶
What it is. A generator that produces the whole series of length ctx.L = K·T itself: values in real units,
hour of week per point, and seam positions (indices where a newly generated piece starts; empty if none). It runs
as a method under its own name and is a baseline of the case study.
Plugin. A runnable example: a seasonal block bootstrap of the train series (weeks drawn at random).
import numpy as np
from marathonts.bench import registry as R
from marathonts.bench.generators.base import LongGenerator
@R.long_generators.register("seasonal_boot")
class SeasonalBoot(LongGenerator):
# env = "myenv"; requires = ("my_lib",) # own environment, as for a pool generator
# uses_windows = True # if it trains on ctx.X_core / ctx.X_hold
def fit(self, ctx): # optional; timed as fit
v, how = ctx.train_series() # real train: values, hour of week
self.v = v[~np.isnan(v)]
def generate(self, ctx):
rng = np.random.default_rng(ctx.seed)
n_weeks = len(self.v) // 168
starts = rng.integers(0, n_weeks, size=ctx.L // 168 + 1) * 168
values = np.concatenate([self.v[s:s + 168] for s in starts])[:ctx.L]
how = (ctx.train_series()[1][-1] + 1 + np.arange(ctx.L)) % 168
seams = np.arange(168, ctx.L, 168) # a new block starts every week
return values, how, seams
state() / load_state(arrays, ctx) store and restore trained weights as for a pool generator. Diffusion-TS,
Chronos, TimesFM and Timestral in marathonts/bench/generators/builtin.py are examples of trained and pretrained
generators.
Switch on. long_methods=[*cfg.long_methods, "seasonal_boot"] with methods=None, baselines=None in the
notebook cell (with Config.profile(...) the lists are rebuilt automatically). It becomes the method
seasonal_boot and a baseline.
Stages. generate (in its environment if it has one: generate --methods seasonal_boot), fidelity,
utility, collect, the analysis. Other methods are not redone.
Check. Rows with method == "seasonal_boot" in tables/timing.parquet, fidelity.parquet,
utility.parquet; the method in every ranking, in RQ2 and in the case-study comparisons CS9 / CS10 (the
pre-specified case-study cell compares against Diffusion-TS and Chronos only).
Assembler¶
What it is. An assembler: it receives the pool (N, T, 3) and returns a path — the indices of ctx.K
windows in order — and a dict of diagnostics. With seam processing ctx.K is larger than K (as many windows as
the joiner requests). Built-in assemblers are in marathonts/bench/assemblers.py; seam_jump(pool) gives the N × N matrix
of jumps at the seams (the cost of the built-in greedy and TSP assemblers).
Plugin.
import numpy as np
from marathonts.bench import registry as R
from marathonts.bench.assemblers import Assembler, seam_jump
@R.assemblers.register("my_greedy")
class MyGreedy(Assembler):
# requires = ("my_solver",) # modules; without them the unit fails with their names
# uses_targets = True # if it reads the task thresholds (they enter its fingerprint)
def assemble(self, pool, ctx):
E = seam_jump(pool) # jump at the seam i → j, (N, N); diagonal +inf
path = [int(ctx.params.get("start", 0))]
used = set(path)
for _ in range(ctx.K - 1):
row = E[path[-1]].copy()
row[list(used)] = np.inf
nxt = int(np.argmin(row))
path.append(nxt)
used.add(nxt)
return np.array(path), {"edge_sum": float(sum(E[a, b] for a, b in zip(path, path[1:])))}
Switch on. extra_assemblers=["my_greedy"] (with methods=None in the notebook cell): the method
<prefix>_my_greedy on every pool of pool_suites (vae_my_greedy). Parameters: extra={"my_greedy": {...}}.
With smooth_paths containing "my_greedy" it also gets seam processing (vae_my_greedy_xfade, …).
Stages. generate (the pools are reused), fidelity, utility, collect, the analysis.
Check. Rows with method == "vae_my_greedy" in the tables; the diagnostics of info in
tables/timing.parquet; the method in the rankings, RQ0–RQ9 and, on the pool of the case-study method, in
CS9 / CS10.
Seam processing¶
What it is. A joiner: it receives the windows of a path in order (n, T, F) and joins them into a series of
length L = K·T. Overlaps shorten the series, so windows_needed(K, T, cfg) says how many windows to request
from the assembler. It returns the series channels X (L, F), the seam positions, the index of the path window of
each point and diagnostics. Built-ins are crossfade, wsola, quilt (marathonts/bench/joiners.py).
Plugin. A linear crossfade of the value channel over overlap points:
import numpy as np
from marathonts.bench import registry as R
from marathonts.bench.joiners import Joiner
@R.joiners.register("my_blend")
class MyBlend(Joiner):
def _o(self, cfg):
return int(cfg.params("my_blend").get("overlap", 6))
def windows_needed(self, K, T, cfg):
return 1 + int(np.ceil((K * T - T) / (T - self._o(cfg))))
def join(self, W, L, cfg):
o = self._o(cfg)
n, T, F = W.shape
X = np.zeros((T + (n - 1) * (T - o), F), np.float32)
src = np.zeros(len(X), np.int64)
X[:T], pos, seams = W[0], T, []
r = np.linspace(0, 1, o, endpoint=False)[:, None] # weight of the next window
for k in range(1, n):
X[pos - o:pos, :1] = (1 - r) * X[pos - o:pos, :1] + r * W[k, :o, :1]
X[pos:pos + T - o] = W[k, o:]
src[pos - o // 2:pos + T - o] = k
seams.append(pos - o // 2)
pos += T - o
seams = np.array([s for s in seams if 0 < s < L], int)
return X[:L], seams, src[:L], {"overlap": o}
Switch on. extra_joiners=["my_blend"] and the assemblers it runs over in smooth_paths (e.g.
["greedy"]; "all" — every assembler except the random path and vae_quilting), with methods=None. joiners
selects among the seam processings (None — the built-in ones and extra_joiners). Methods:
<prefix>_<assembler>_my_blend (vae_greedy_my_blend).
Stages. generate, fidelity, utility, collect, the analysis.
Check. Rows of vae_greedy_my_blend in the tables. The rankings leave out the seam-processing variants
unless every assembler has them (rank_exclude=None and smooth_paths="all"); --rank-exclude without names
ranks every method. The variants enter RQ3 (b, c) and C4.
Single method¶
What it is. A method is what is compared: pool + assembler (+ seam processing) or a long-series generator. The
fields above register methods in bulk; register_method registers one combination.
Plugin.
from marathonts.bench.methods import register_method
register_method("vae_greedy_blend", pool="tsgm_vae", assembler="greedy", joiner="my_blend")
register_method("gan_greedy", pool="my_gan", assembler="greedy")
register_method("my_long", long="seasonal_boot")
register_stitch_suite("gan", "my_gan") (bench.methods) registers the whole suite on one pool, as
pool_suites does. The names real and anything with @ are reserved.
Switch on. A method registered this way is not part of the method list built from the fields; name it in
methods (methods=[*cfg.methods, "vae_greedy_blend"], or --methods with the full list).
Stages and check. As for an assembler or a long-series generator.
ML model¶
What it is. A downstream model of the utility stage, trained per task × regime (TRTR on real train, TSTR on
the synthetic series) and tested on the real val / OOT. kinds declares the task kinds it supports. The format
of predict by kind: clf → (n,) probability of class 1; mc → (n, C); reg, vol → (n,); qreg →
(n, 3); ms → (n, h). Hyperparameters come from marathonts/bench/model_configs.json ([<name>]), updated by
extra[<name>], as self.params; self.task, self.seed and self.device are set.
Plugin.
from marathonts.bench import registry as R
from marathonts.bench.models import BaseModel
@R.models.register("my_trees")
class MyTrees(BaseModel):
kinds = {"clf", "reg"} # task kinds the model supports
def fit(self, X, y):
from sklearn.ensemble import ExtraTreesClassifier, ExtraTreesRegressor
cls = ExtraTreesClassifier if self.task.kind == "clf" else ExtraTreesRegressor
self.m = cls(n_estimators=self.params.get("n_estimators", 200), random_state=self.seed, n_jobs=-1)
self.m.fit(X, y)
return self
def predict(self, X): # clf → P(y = 1)
return self.m.predict_proba(X)[:, 1] if self.task.kind == "clf" else self.m.predict(X)
def state(self):
from marathonts.bench.storage import blob
return {"model": blob(self.m)}
def load_state(self, arrays):
from marathonts.bench.storage import unblob
self.m = unblob(arrays["model"])
return self
Switch on. models=[*cfg.models, "my_trees"] or --models linear gbdt mlp my_trees; model_tasks={"my_trees":
[...]} restricts it to some tasks. Its fixed hyperparameters and search grid can be added to
marathonts/bench/model_configs.json ("my_trees": {...}, "grids": {"my_trees": [...]}) or set in extra.
Stages. utility computes only the added model's cells (task × model) in the existing units; then collect and
the analysis. A model without kinds inherits every kind and the stage warns; it also warns about a task that no
model of the run can fit.
Check. Rows with model == "my_trees" in tables/utility.parquet, METRIC_VALUES.md (per model), the model
leaves of LEADERBOARD.md. The aggregated utility criteria use the models fitted on every task of the criterion;
a model that covers only some of them (here: no mc) stays in the per-model leaves.
Fidelity metric¶
What it is. A metric group: a function that receives a shared context and returns {metric: value}; the
orientation of each metric is given at registration (−1: lower is better, +1: higher is better). The context
(bench.fidelity.FidelityContext) holds the synthetic series ctx.x and its calendar ctx.how, the real
reference ctx.ref / ctx.ref_how, the real train ctx.train / ctx.train_how, ctx.T, the seams ctx.sv,
z-scored series ctx.zx / ctx.zr, random windows ctx.windows, window features ctx.features, ctx.rng,
ctx.target_params and ctx.params.
Plugin.
import numpy as np
from marathonts.bench.fidelity import register_fidelity_group
@register_fidelity_group("M_spikes", {"M1_spike_rate_err": -1})
def spikes(ctx):
thr = 3 * np.nanstd(np.diff(ctx.ref))
rate = lambda v: np.nanmean(np.abs(np.diff(v)) > thr)
return {"M1_spike_rate_err": abs(rate(ctx.x) - rate(ctx.ref))}
Switch on. With fidelity_groups=None (default) every registered group runs. The metric is computed against
every reference (M1_spike_rate_err, M1_spike_rate_err@train, M1_spike_rate_err@oot); primary_only=True
in the decorator computes it once, against the primary reference. Parameters: extra={"M_spikes": {...}} →
ctx.params.
Stages. fidelity computes only the added group on the existing series (per-group fingerprints), then
collect and the analysis. A group that raises does not lose the unit: its metrics are NaN, the error goes to
errors.log and the group is retried on the next run; a returned metric that is not in the spec (no orientation)
is logged there too and ignored by the analysis.
Check. The columns in tables/fidelity.parquet and metric_values/fidelity.parquet; the metric in the
rankings (its group is its own criterion unless metric_criteria={"M_spikes": "distribution"} assigns one), RQ0,
RQ2–RQ4 and CS9. The overall leaderboard and the tables of METRIC_VALUES.md use the core metrics only
(core_metrics). metric_criteria and core_metrics are analysis-only settings; they reach the analysis through
hypotheses.run --config (CONFIGURATION.md). Which case-study
hypotheses a metric enters is set by its name prefix (CS2 — F1.–F3., CS4 — F7., …) or by your own
hypothesis.
Utility metric¶
What it is. A function of the labels and predictions of one test split, called for every task;
ctx = {"kind", "scale", "train_share", "task"}. It returns a dict of numbers (other values are dropped with a
warning; a key already returned by another metric function keeps the first value).
Plugin.
import numpy as np
from marathonts.bench import registry as R
@R.utility_metrics.register("top_decile_precision")
def top_decile_precision(y, pred, ctx):
if ctx["kind"] != "clf":
return {}
top = pred >= np.quantile(pred, 0.9)
return {"precision_top10": float(y[top].mean())}
Switch on. Every registered metric function runs. The analysis needs the orientation of each returned
metric: metric_orient={"precision_top10": 1}, passed to the analysis with hypotheses.run --config
(CONFIGURATION.md); without it the column is not analysed and the
analysis warns.
Stages. utility: the metric is computed when a cell is computed. The list of metric functions is not part
of the cell fingerprints, so cells already done keep their metrics; to add the metric to an existing run, its
utility units are recomputed (a unit without cells.json has no done cells, e.g.
rm artifacts/<profile>/utility/*/*/seed*/cells.json). Then collect and the analysis.
Check. The column precision_top10 in tables/utility.parquet and metric_values/utility.parquet.
Statistical test¶
What it is. A test that adds columns to every comparison table of the analysis. The global level receives
the dataset effects in noise units (z); the dataset level receives the per-seed paired differences of one
dataset and the noise margin (d, margin). A test that raises adds a column <name>_error and does not stop the
analysis.
Plugin.
from marathonts.hypotheses.stats import register_test
@register_test("bayes_sign", level="global")
def bayes_sign(z):
from scipy import stats
k, n = int((z > 0).sum()), len(z)
return {"p_bayes_better": float(stats.beta(1 + k, 1 + n - k).sf(0.5))}
Switch on and run. Load the file in the analysis: python -m marathonts.hypotheses.run --plugins my_tests.py
(notebook 07: main([..., "--plugins", "my_tests.py"])). Nothing in stages 0–6 is redone.
Check. The column p_bayes_better in artifacts/<profile>/hypotheses/*_global.parquet.
Hypothesis¶
What it is. A question about a specific method, reported with the case study: a function of the benchmark
tables (timing, fidelity, utility, cross_seed, datasets, pools) and the configuration that returns
{"global": DataFrame, "per_dataset": DataFrame, "extra": dict}. hypotheses.catalog provides the helpers
of the built-in ones (fidelity_long, _run: paired comparisons of the case-study method against comparators,
noise margins, corrections, verdicts).
Plugin.
from marathonts.bench.fidelity import ORIENT
from marathonts.hypotheses.catalog import _run, fidelity_long, register_hypothesis
@register_hypothesis("CS16", "Spike rate of the assembled series against the random path")
def cs16(tables, cfg):
L = fidelity_long(tables["fidelity"])
return _run(L, ORIENT, ["vae_rand"], units=["M1_spike_rate_err"], adjust="bh",
alpha=cfg.alpha, q=cfg.equivalence_margin_q, n_boot=cfg.n_boot)
Switch on and run. python -m marathonts.hypotheses.run --plugins my_plugins.py my_tests.py — the plugin of the metric
that the hypothesis tests is loaded too (otherwise its orientation is unknown and its rows are dropped; plugins
recorded in tables/config.json are loaded automatically). Hypotheses run with the case study, i.e. when
--case-method (default Config.case_method) has results in the tables and --no-case-study is not given;
--only CS16 runs it alone.
Check. The section CS16 of REPORT.md and artifacts/<profile>/hypotheses/CS16_global.parquet,
CS16_per_dataset.parquet.
What is saved and how to load it back¶
Everything goes to artifacts/<profile>/ in Parquet (BENCHMARK.md, "Artifacts"). The analysis
writes its Markdown reports to artifacts/<profile>/reports/ — REPORT.md, OVERALL.md, LEADERBOARD.md and
METRIC_VALUES.md — and their tables to hypotheses/, overall/, leaderboard/ and metric_values/, read back
with pandas.read_parquet. Trained generators and models are loaded back without training (marathonts/bench/load.py):
from marathonts.bench import load
gen = load.load_generator(cfg, "ett", "my_gan", seed=0)
m = load.load_model(cfg, "ett", "gan_greedy", 0, task="spike_h12", model="my_trees", regime="S")
X, y, _ = load.task_xy(cfg, "ett", "spike_h12", split="oot")
pred = m.predict(X)
For this the component must have a load_state that is the inverse of state.