ml_meta_perf.figures

Generate the study's figure set from a finished report.

Figures carry no titles: each is written for a LaTeX figure environment where the caption supplies the description. CAPTIONS holds a suggested caption per file, including the disclosures that would otherwise have to be drawn into the image.

  1"""Generate the study's figure set from a finished report.
  2
  3Figures carry no titles: each is written for a LaTeX ``figure`` environment where the
  4caption supplies the description. ``CAPTIONS`` holds a suggested caption per file,
  5including the disclosures that would otherwise have to be drawn into the image.
  6"""
  7
  8from __future__ import annotations
  9
 10from pathlib import Path
 11
 12import polars as pl
 13
 14from ml_meta_perf.data import DATASET_FEATURES, MODEL_FEATURES, columns_as_arrays, load, target
 15from ml_meta_perf.experiment import Report
 16from ml_meta_perf.plots import (
 17    count_below_floor,
 18    decision_quality,
 19    equation_comparison,
 20    practice_effects,
 21    predicted_versus_actual,
 22    ranking_quality,
 23    term_count_curve,
 24    term_effects,
 25)
 26
 27ADDITIVE_REFERENCE_ROW = "reference: additive mean-based reference"
 28
 29#: The figure set in the order the study presents it. **The order is the identity**: files
 30#: are written as ``01_equation_comparison.png`` and so on, so a figure can be named by its
 31#: number in a review comment or a caption without anyone having to agree on which
 32#: "comparison" plot is meant. Position in this tuple is the only place a number is written
 33#: down -- `figure_name` derives it -- so re-ordering the set renumbers the files and the
 34#: captions together and cannot leave the two disagreeing.
 35FIGURE_ORDER: tuple[str, ...] = (
 36    "equation_comparison",
 37    "term_count_curve",
 38    "predicted_vs_actual",
 39    "term_effects",
 40    "practice_effects",
 41    "ranking_quality",
 42    "decision_quality",
 43)
 44
 45
 46def figure_name(stem: str) -> str:
 47    """The published filename for a figure, ``NN_stem.png``, numbered from `FIGURE_ORDER`."""
 48    return f"{FIGURE_ORDER.index(stem) + 1:02d}_{stem}.png"
 49
 50
 51def _additive_reference(report: Report) -> float | None:
 52    """The additive mean-based reference as this run computed it.
 53
 54    Read from the report rather than hardcoded: the reference is a property of the data,
 55    so a literal here would silently drift out of step with the comparison table the
 56    moment the meta-dataset changed.
 57    """
 58    matched = report.comparison.filter(pl.col("equation") == ADDITIVE_REFERENCE_ROW)
 59    return float(matched["r2"][0]) if matched.height else None
 60
 61
 62def _published_length(report: Report) -> int | None:
 63    """How many terms the reported E3-Valid equation actually has."""
 64    return len(report.e3.equation.terms) or None
 65
 66
 67def generate(report: Report, destination: str | Path, data: str | Path | None = None) -> list[Path]:
 68    """Write every figure and return the paths, in the order they appear in the README.
 69
 70    ``data`` must be the meta-dataset the report was fitted on. Defaulting it to the
 71    packaged one is only safe because that is the usual case; a caller who fitted on
 72    another file and does not pass it here would get figures drawn from the wrong rows.
 73    """
 74    folder = Path(destination)
 75    folder.mkdir(parents=True, exist_ok=True)
 76    frame = load(data)
 77    columns = columns_as_arrays(frame, DATASET_FEATURES + MODEL_FEATURES)
 78    truth = target(frame)
 79    predicted = report.e3.equation.predict(columns)
 80
 81    written = [
 82        equation_comparison(report.comparison, folder / figure_name("equation_comparison")),
 83        term_count_curve(
 84            report.e3.curve,
 85            folder / figure_name("term_count_curve"),
 86            reference=_additive_reference(report),
 87            marker=_published_length(report),
 88            marker_label="selected term count",
 89        ),
 90        predicted_versus_actual(truth, predicted, folder / figure_name("predicted_vs_actual")),
 91        term_effects(report.effects, folder / figure_name("term_effects")),
 92        practice_effects(report.practices, folder / figure_name("practice_effects")),
 93        ranking_quality(report.selection, folder / figure_name("ranking_quality")),
 94        decision_quality(report.decision_baselines, folder / figure_name("decision_quality")),
 95    ]
 96    # The list is what the README and the chapters index against, so it has to come back in
 97    # `FIGURE_ORDER`. Asserting it here means a call added out of order fails the run rather
 98    # than shipping a figure numbered one thing and referenced as another.
 99    expected = [folder / figure_name(stem) for stem in FIGURE_ORDER]
100    if written != expected:
101        raise AssertionError(f"figures written out of FIGURE_ORDER: {[path.name for path in written]}")
102    return written
103
104
105def captions(report: Report, data: str | Path | None = None) -> dict[str, str]:
106    """Suggested LaTeX captions, carrying the disclosures kept out of the images."""
107    frame = load(data)
108    columns = columns_as_arrays(frame, DATASET_FEATURES + MODEL_FEATURES)
109    truth = target(frame)
110    hidden = count_below_floor(truth, report.e3.equation.predict(columns))
111
112    return {
113        figure_name("equation_comparison"): (
114            "Each fitted equation against the level it is read against, all scored on the "
115            f"same {frame.height} rows. Dataset-only and model-only equations are bounded by what "
116            "their group identity can explain. The additive mean-based reference bounds only an equation "
117            "additive in dataset and model effects, which E3 is not: its mixed terms can "
118            "carry interactions beyond that reference, and the current E3 passes it. "
119            "All bars use in-sample (IS) estimates."
120        ),
121        figure_name("term_count_curve"): (
122            "Accuracy against equation length for E3 under in-sample (IS), "
123            "leave-one-dataset-out (LODO), and leave-one-model-out (LOMO) evaluation. "
124            "The vertical line is the published length, chosen as the first knee of the "
125            "smoothed worst-protocol curve followed by a sustained plateau. "
126            "The additive mean-based reference is the best score reachable by an equation additive in "
127            "dataset and model effects; an equation passes it only by representing the "
128            "dataset-by-model interaction the additive reference cannot."
129        ),
130        figure_name("predicted_vs_actual"): (
131            "Predicted against actual MCC for E3. Axes start at 0; "
132            f"{hidden} point below that is not shown. The equation compresses toward the "
133            "middle of the range, as a shrunk linear fit will."
134        ),
135        figure_name("term_effects"): (
136            "Per-term effect on predicted MCC, measured as the swing between the term's "
137            "10th and 90th percentile. Sign follows the fitted weight."
138        ),
139        figure_name("practice_effects"): (
140            "Per-feature effect on predicted MCC between the feature's lowest and highest "
141            "decile, shaded by the confidence its practice was rated at. Only the confidence "
142            "levels present in the table appear in the legend."
143        ),
144        figure_name("decision_quality"): (
145            "F1 of the above-or-below-threshold decision against the threshold, one line per "
146            "evaluation protocol: in-sample (IS), leave-one-dataset-out (LODO), "
147            "leave-one-model-out (LOMO), and doubly held-out (DHO). The four differ only in "
148            "what the equation was allowed to see, so the "
149            "spread between them is the cost of generalisation on this task. F1 rather than "
150            "accuracy because the classes are unbalanced at the outer thresholds, where "
151            "always answering with the larger class reaches 0.51 accuracy at an F1 of zero."
152        ),
153        figure_name("ranking_quality"): (
154            "Head-of-list ranking quality per dataset under doubly held-out (DHO) evaluation, "
155            "beside what a bad ranking "
156            "costs. Left: average precision and reciprocal rank, with a star where the "
157            "top-ranked choice is within 0.01 MCC of the dataset's best. Right: the MCC given "
158            "up by taking the top-ranked model. Both are "
159            "scored with the dataset and the model of every cell held out of the fit."
160        ),
161    }
ADDITIVE_REFERENCE_ROW = 'reference: additive mean-based reference'
FIGURE_ORDER: tuple[str, ...] = ('equation_comparison', 'term_count_curve', 'predicted_vs_actual', 'term_effects', 'practice_effects', 'ranking_quality', 'decision_quality')
def figure_name(stem: str) -> str:
47def figure_name(stem: str) -> str:
48    """The published filename for a figure, ``NN_stem.png``, numbered from `FIGURE_ORDER`."""
49    return f"{FIGURE_ORDER.index(stem) + 1:02d}_{stem}.png"

The published filename for a figure, NN_stem.png, numbered from FIGURE_ORDER.

def generate( report: ml_meta_perf.experiment.Report, destination: str | pathlib.Path, data: str | pathlib.Path | None = None) -> list[pathlib.Path]:
 68def generate(report: Report, destination: str | Path, data: str | Path | None = None) -> list[Path]:
 69    """Write every figure and return the paths, in the order they appear in the README.
 70
 71    ``data`` must be the meta-dataset the report was fitted on. Defaulting it to the
 72    packaged one is only safe because that is the usual case; a caller who fitted on
 73    another file and does not pass it here would get figures drawn from the wrong rows.
 74    """
 75    folder = Path(destination)
 76    folder.mkdir(parents=True, exist_ok=True)
 77    frame = load(data)
 78    columns = columns_as_arrays(frame, DATASET_FEATURES + MODEL_FEATURES)
 79    truth = target(frame)
 80    predicted = report.e3.equation.predict(columns)
 81
 82    written = [
 83        equation_comparison(report.comparison, folder / figure_name("equation_comparison")),
 84        term_count_curve(
 85            report.e3.curve,
 86            folder / figure_name("term_count_curve"),
 87            reference=_additive_reference(report),
 88            marker=_published_length(report),
 89            marker_label="selected term count",
 90        ),
 91        predicted_versus_actual(truth, predicted, folder / figure_name("predicted_vs_actual")),
 92        term_effects(report.effects, folder / figure_name("term_effects")),
 93        practice_effects(report.practices, folder / figure_name("practice_effects")),
 94        ranking_quality(report.selection, folder / figure_name("ranking_quality")),
 95        decision_quality(report.decision_baselines, folder / figure_name("decision_quality")),
 96    ]
 97    # The list is what the README and the chapters index against, so it has to come back in
 98    # `FIGURE_ORDER`. Asserting it here means a call added out of order fails the run rather
 99    # than shipping a figure numbered one thing and referenced as another.
100    expected = [folder / figure_name(stem) for stem in FIGURE_ORDER]
101    if written != expected:
102        raise AssertionError(f"figures written out of FIGURE_ORDER: {[path.name for path in written]}")
103    return written

Write every figure and return the paths, in the order they appear in the README.

data must be the meta-dataset the report was fitted on. Defaulting it to the packaged one is only safe because that is the usual case; a caller who fitted on another file and does not pass it here would get figures drawn from the wrong rows.

def captions( report: ml_meta_perf.experiment.Report, data: str | pathlib.Path | None = None) -> dict[str, str]:
106def captions(report: Report, data: str | Path | None = None) -> dict[str, str]:
107    """Suggested LaTeX captions, carrying the disclosures kept out of the images."""
108    frame = load(data)
109    columns = columns_as_arrays(frame, DATASET_FEATURES + MODEL_FEATURES)
110    truth = target(frame)
111    hidden = count_below_floor(truth, report.e3.equation.predict(columns))
112
113    return {
114        figure_name("equation_comparison"): (
115            "Each fitted equation against the level it is read against, all scored on the "
116            f"same {frame.height} rows. Dataset-only and model-only equations are bounded by what "
117            "their group identity can explain. The additive mean-based reference bounds only an equation "
118            "additive in dataset and model effects, which E3 is not: its mixed terms can "
119            "carry interactions beyond that reference, and the current E3 passes it. "
120            "All bars use in-sample (IS) estimates."
121        ),
122        figure_name("term_count_curve"): (
123            "Accuracy against equation length for E3 under in-sample (IS), "
124            "leave-one-dataset-out (LODO), and leave-one-model-out (LOMO) evaluation. "
125            "The vertical line is the published length, chosen as the first knee of the "
126            "smoothed worst-protocol curve followed by a sustained plateau. "
127            "The additive mean-based reference is the best score reachable by an equation additive in "
128            "dataset and model effects; an equation passes it only by representing the "
129            "dataset-by-model interaction the additive reference cannot."
130        ),
131        figure_name("predicted_vs_actual"): (
132            "Predicted against actual MCC for E3. Axes start at 0; "
133            f"{hidden} point below that is not shown. The equation compresses toward the "
134            "middle of the range, as a shrunk linear fit will."
135        ),
136        figure_name("term_effects"): (
137            "Per-term effect on predicted MCC, measured as the swing between the term's "
138            "10th and 90th percentile. Sign follows the fitted weight."
139        ),
140        figure_name("practice_effects"): (
141            "Per-feature effect on predicted MCC between the feature's lowest and highest "
142            "decile, shaded by the confidence its practice was rated at. Only the confidence "
143            "levels present in the table appear in the legend."
144        ),
145        figure_name("decision_quality"): (
146            "F1 of the above-or-below-threshold decision against the threshold, one line per "
147            "evaluation protocol: in-sample (IS), leave-one-dataset-out (LODO), "
148            "leave-one-model-out (LOMO), and doubly held-out (DHO). The four differ only in "
149            "what the equation was allowed to see, so the "
150            "spread between them is the cost of generalisation on this task. F1 rather than "
151            "accuracy because the classes are unbalanced at the outer thresholds, where "
152            "always answering with the larger class reaches 0.51 accuracy at an F1 of zero."
153        ),
154        figure_name("ranking_quality"): (
155            "Head-of-list ranking quality per dataset under doubly held-out (DHO) evaluation, "
156            "beside what a bad ranking "
157            "costs. Left: average precision and reciprocal rank, with a star where the "
158            "top-ranked choice is within 0.01 MCC of the dataset's best. Right: the MCC given "
159            "up by taking the top-ranked model. Both are "
160            "scored with the dataset and the model of every cell held out of the fit."
161        ),
162    }

Suggested LaTeX captions, carrying the disclosures kept out of the images.