Skip to content
Open
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
14 changes: 6 additions & 8 deletions plots/plot_cd_grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import argparse
import io
import math
import re
import sys
from pathlib import Path

Expand All @@ -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 = []
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)])
Expand Down Expand Up @@ -498,6 +491,7 @@ class Plot(BasePlot):

name = "Critical Difference Diagram"
type = "image"
requirements = ["pip::scikit-posthocs"]
options = {
"objective_column": ...,
}
Expand All @@ -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}]
Expand Down
59 changes: 59 additions & 0 deletions tests/plots/test_plot_cd_grid.py
Original file line number Diff line number Diff line change
@@ -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)
Loading