From 1392d619bf05f97af040ab61063288f9f25b6d5b Mon Sep 17 00:00:00 2001 From: tommoral Date: Thu, 9 Jul 2026 16:09:26 +0000 Subject: [PATCH] FIX CD plot: drop param-collapsing labels (rely on benchopt-shortened solver_name), declare scikit-posthocs requirement --- plots/plot_cd_grid.py | 14 ++++---- tests/plots/test_plot_cd_grid.py | 59 ++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 tests/plots/test_plot_cd_grid.py diff --git a/plots/plot_cd_grid.py b/plots/plot_cd_grid.py index 8c822d8..f9d9473 100644 --- a/plots/plot_cd_grid.py +++ b/plots/plot_cd_grid.py @@ -24,7 +24,6 @@ import argparse import io import math -import re import sys from pathlib import Path @@ -42,10 +41,6 @@ from benchmark_utils.metrics import is_higher_better # noqa: E402 -def short_solver(name: str) -> str: - return re.sub(r"\[.*?\]$", "", str(name)).strip() - - def discover_metrics(df: pd.DataFrame) -> list[str]: """Return all numeric objective_* columns that have any non-NaN values.""" cols = [] @@ -209,7 +204,6 @@ def _cd_global(df: pd.DataFrame, ax: plt.Axes, alpha: float = 0.05) -> str: k, n_blocks = stacked.shape mean_ranks = stacked.mean(axis=1) - mean_ranks.index = [short_solver(s) for s in mean_ranks.index] # Friedman on the stacked ranks. friedmanchisquare ranks within each # block; since we already supplied per-block ranks, the re-ranking is a @@ -319,8 +313,7 @@ def cd_for_metric( [rankdata(rank_mat[c].values, method="average") for c in rank_mat.columns] ).T ranks_df = pd.DataFrame(ranks_arr, index=mat.index, columns=mat.columns) - mean_ranks = ranks_df.mean(axis=1).rename(short_solver) - mean_ranks.index = [short_solver(s) for s in mean_ranks.index] + mean_ranks = ranks_df.mean(axis=1) # Friedman test chi2, pval = friedmanchisquare(*[mat.iloc[i].values for i in range(k)]) @@ -498,6 +491,7 @@ class Plot(BasePlot): name = "Critical Difference Diagram" type = "image" + requirements = ["pip::scikit-posthocs"] options = { "objective_column": ..., } @@ -522,6 +516,10 @@ def _get_all_plots(self, df): return plots, options def plot(self, df, objective_column): + # benchopt pre-shortens ``solver_name`` before ``plot`` is called + # (keeping only the parameters that vary), so several parametrizations + # of one solver arrive as distinct labels — the CD diagram groups on + # that column and gets unique short labels for free. fig, ax = plt.subplots(figsize=(10, 5)) cd_for_metric(df, objective_column, ax) return [{"image": _fig_to_array(fig), "label": objective_column}] diff --git a/tests/plots/test_plot_cd_grid.py b/tests/plots/test_plot_cd_grid.py new file mode 100644 index 0000000..7145165 --- /dev/null +++ b/tests/plots/test_plot_cd_grid.py @@ -0,0 +1,59 @@ +"""Regression test for the CD-diagram plot. + +benchopt pre-shortens ``solver_name`` (keeping only the parameters that vary), +so the plot receives unique labels and no longer collapses several +parametrizations of one solver into a duplicate label — which used to make +``scikit_posthocs`` raise "truth value of a Series is ambiguous". +""" + +import sys +from pathlib import Path + +import matplotlib +import numpy as np +import pandas as pd + +matplotlib.use("Agg") +import matplotlib.pyplot as plt # noqa: E402 + +BENCHMARK_DIR = Path(__file__).parents[2] +sys.path.insert(0, str(BENCHMARK_DIR / "plots")) + +import plot_cd_grid as cd # noqa: E402 + + +def _synthetic_results(): + # Distinct solver names, as benchopt delivers them after short-labeling. + rng = np.random.default_rng(0) + solvers = [ + "SeasonalNaive[season_length=1]", + "SeasonalNaive[season_length=7]", + "Chronos2", + ] + datasets = [f"d{i}" for i in range(6)] + return pd.DataFrame([ + { + "solver_name": s, + "dataset_name": d, + "objective_mae": rng.random(), + "objective_mse": rng.random(), + } + for s in solvers + for d in datasets + ]) + + +def test_cd_for_metric_runs(): + df = _synthetic_results() + fig, ax = plt.subplots() + status = cd.cd_for_metric(df, "objective_mae", ax) + plt.close(fig) + assert "objective_mae" in status + + +def test_cd_global_runs(): + df = _synthetic_results() + fig, ax = plt.subplots() + status = cd.cd_for_metric(df, cd.GLOBAL_KEY, ax) + plt.close(fig) + assert isinstance(status, str)