Source code for figures.stability
"""The stability figure: how the reference solution holds under refitting.
Built from a ``stability`` run (either mode), the figure shows three things. Panel (a)
contrasts the distribution of the seven-category profile correlation, which clusters high,
with the adjusted Rand index, which is more moderate: the class definitions reproduce, while
individual proband assignments are softer at the boundaries. Panel (b) gives the per-category
correlation, uniformly high. Panel (c) is the mean class-overlap matrix, whose diagonal is
each class's retention.
"""
from __future__ import annotations
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from matplotlib.figure import Figure
from figures import style
def _mode_label(aggregate: dict) -> str:
"""Return a short description of the stability run for the figure title."""
if "n_fits" in aggregate:
return (
f"multi-initialisation ({int(aggregate['n_fits'])} fits, top {int(aggregate['top_k'])})"
)
if "n_reps" in aggregate:
return f"{aggregate['frac']:.0%} subsampling ({int(aggregate['n_reps'])} refits)"
return "refitting"
[docs]
def stability_figure(
comparisons: pd.DataFrame,
aggregate: dict,
overlap_mean: pd.DataFrame,
) -> Figure:
"""Build the stability figure from a ``stability`` run.
Parameters
----------
comparisons : pandas.DataFrame
One row per compared fit, with ``overall_correlation`` and ``adjusted_rand_index``.
aggregate : dict
The aggregate metrics, with ``category_correlation_mean`` (one entry per category) and
a run descriptor (``n_fits``/``top_k`` or ``n_reps``/``frac``).
overlap_mean : pandas.DataFrame
The mean class-overlap matrix (refit class on rows, reference class on columns).
Returns
-------
matplotlib.figure.Figure
A three-panel figure: the correlation and Rand-index distributions, the per-category
correlation, and the mean overlap matrix.
Raises
------
ValueError
When a required column or metric is missing.
"""
for col in ("overall_correlation", "adjusted_rand_index"):
if col not in comparisons.columns:
raise ValueError(f"comparisons is missing column {col!r}")
if "category_correlation_mean" not in aggregate:
raise ValueError("aggregate is missing 'category_correlation_mean'")
profile = comparisons["overall_correlation"].to_numpy(dtype=float)
profile = profile[np.isfinite(profile)]
rand = comparisons["adjusted_rand_index"].to_numpy(dtype=float)
rand = rand[np.isfinite(rand)]
per_category = aggregate["category_correlation_mean"]
categories = list(per_category)
with style.house_style():
fig, (ax_dist, ax_cat, ax_overlap) = plt.subplots(1, 3, figsize=(11.0, 3.8))
# Panel (a): profile correlation (tight, high) against the adjusted Rand index (more
# moderate): stable class definitions, softer proband-level membership.
bodies = ax_dist.violinplot([profile, rand], positions=[1, 2], showextrema=False)
for body, colour in zip(bodies["bodies"], style.PALETTE[:2], strict=True):
body.set_facecolor(colour)
body.set_alpha(0.4)
for position, sample, colour in (
(1, profile, style.PALETTE[0]),
(2, rand, style.PALETTE[1]),
):
ax_dist.scatter(
np.full(sample.shape, position), sample, color=colour, s=6, alpha=0.4, zorder=3
)
ax_dist.scatter(
position, sample.mean(), color=colour, marker="_", s=400, linewidth=2.0, zorder=4
)
ax_dist.set_xticks([1, 2])
ax_dist.set_xticklabels(["profile\ncorrelation", "adjusted\nRand index"])
ax_dist.set_ylim(0.0, 1.05)
ax_dist.set_ylabel("Value across refits")
# Panel (b): per-category profile correlation, uniformly high.
values = [float(per_category[cat]) for cat in categories]
colours = [style.PALETTE[i % len(style.PALETTE)] for i in range(len(categories))]
positions = np.arange(len(categories))
ax_cat.bar(positions, values, color=colours)
ax_cat.set_xticks(positions)
ax_cat.set_xticklabels(
[style.CATEGORY_LABELS.get(cat, cat) for cat in categories], rotation=45, ha="right"
)
ax_cat.set_ylim(0.0, 1.05)
ax_cat.set_ylabel("Mean profile correlation")
# Panel (c): the mean class-overlap matrix; the diagonal is each class's retention.
matrix = overlap_mean.to_numpy(dtype=float)
image = ax_overlap.imshow(matrix, cmap="Blues", vmin=0.0, vmax=1.0, aspect="equal")
size = matrix.shape[0]
for i in range(size):
for j in range(size):
value = matrix[i, j]
if np.isfinite(value):
ax_overlap.text(
j,
i,
f"{value:.2f}",
ha="center",
va="center",
color="white" if value > 0.5 else "black",
fontsize=7,
)
ax_overlap.set_xticks(range(size))
ax_overlap.set_yticks(range(size))
ax_overlap.set_xlabel("reference class")
ax_overlap.set_ylabel("refit class")
ax_overlap.grid(visible=False)
fig.colorbar(image, ax=ax_overlap, fraction=0.046, pad=0.04)
style.panel_title(ax_dist, "A", "Profile correlation and adjusted Rand index")
style.panel_title(ax_cat, "B", "Per-category profile correlation")
style.panel_title(ax_overlap, "C", "Mean class overlap")
fig.suptitle(f"Reference-fit stability under {_mode_label(aggregate)}", y=1.0)
fig.tight_layout(rect=(0.0, 0.0, 1.0, 0.96))
return fig