Source code for bayesflow.diagnostics.plots.bayes_factor_recovery

from collections.abc import Sequence

import matplotlib.pyplot as plt
import numpy as np

from bayesflow.utils.plot_utils import make_figure, make_quadratic, add_metric, prettify_subplots, add_titles_and_labels


[docs] def bayes_factor_recovery( pred_log_bayes_factors: np.ndarray, true_log_bayes_factors: np.ndarray, true_models: np.ndarray, model_names: Sequence[str] = None, reference_model: int = 0, add_corr: bool = True, figsize: tuple = None, label_fontsize: int = 16, title_fontsize: int = 18, metric_fontsize: int = 14, tick_fontsize: int = 12, alpha: float = 0.5, markersize: float = None, num_col: int = None, num_row: int = None, ) -> plt.Figure: """Scatter plot of true vs. predicted log Bayes factors for M-model comparison. Produces one subplot per competing model, each showing the predicted log Bayes factor against the analytically or numerically exact value. Points are coloured by the true generating model. Quadrant shading distinguishes correct decisions (true and predicted Bayes factors favour the same model) from incorrect ones. Parameters ---------- pred_log_bayes_factors : np.ndarray of shape (num_datasets, num_models - 1) Predicted log Bayes factors ``log BF_{k, reference}`` for ``k != reference``, as output by a Bayes factor scoring rule network. true_log_bayes_factors : np.ndarray of shape (num_datasets, num_models - 1) Ground-truth log Bayes factors with the same column ordering as ``pred_log_bayes_factors``. true_models : np.ndarray of shape (num_datasets, num_models) or (num_datasets,) Either one-hot encoded model labels or integer class indices. model_names : Sequence[str] or None, optional Human-readable names for all ``num_models`` models, used for axis labels and the legend. Defaults to ``M_1, M_2, ...``. reference_model : int, optional Index (0-based) of the reference model against which all log Bayes factors are computed (default: 0). add_corr : bool, optional Annotate each panel with the Pearson correlation between true and predicted log Bayes factors (default: True). figsize : tuple or None, optional Passed to ``matplotlib``. Inferred from the number of panels if None. label_fontsize : int, optional Font size for axis labels (default: 16). title_fontsize : int, optional Font size for subplot titles (default: 18). metric_fontsize : int, optional Font size for the correlation annotation (default: 14). tick_fontsize : int, optional Font size for tick labels (default: 12). alpha : float, optional Scatter point opacity (default: 0.5). markersize : float or None, optional Scatter point size in points (default: None → matplotlib default). num_col : int or None, optional Number of subplot columns. Inferred if None. num_row : int or None, optional Number of subplot rows. Inferred if None. Returns ------- fig : plt.Figure """ pred_log_bayes_factors = np.asarray(pred_log_bayes_factors) true_log_bayes_factors = np.asarray(true_log_bayes_factors) true_models = np.asarray(true_models) if pred_log_bayes_factors.shape != true_log_bayes_factors.shape: raise ValueError( f"pred_log_bayes_factors and true_log_bayes_factors must have the same shape, " f"got {pred_log_bayes_factors.shape} and {true_log_bayes_factors.shape}." ) num_panels = pred_log_bayes_factors.shape[1] num_models = num_panels + 1 # Accept one-hot or integer labels if true_models.ndim == 2: true_model_idx = np.argmax(true_models, axis=-1) else: true_model_idx = true_models.astype(int) if model_names is None: model_names = [rf"$M_{{{m}}}$" for m in range(1, num_models + 1)] # Build subplot titles, e.g. "M_2 vs M_1" ref_name = model_names[reference_model] competing = [model_names[k] for k in range(num_models) if k != reference_model] panel_titles = [f"{name} vs. {ref_name}" for name in competing] # Categorical colors — one per model cmap = plt.colormaps["tab10"].resampled(num_models) model_colors = [cmap(m) for m in range(num_models)] # Layout if num_col is None and num_row is None: num_col = min(num_panels, 3) num_row = int(np.ceil(num_panels / num_col)) elif num_col is None: num_col = int(np.ceil(num_panels / num_row)) elif num_row is None: num_row = int(np.ceil(num_panels / num_col)) fig, axes = make_figure(num_row, num_col, figsize=figsize) axes_flat = np.asarray(axes).flat scatter_kw = dict(alpha=alpha) if markersize is not None: scatter_kw["s"] = markersize**2 for panel_idx, ax in enumerate(axes_flat): if panel_idx >= num_panels: break x = true_log_bayes_factors[:, panel_idx] y = pred_log_bayes_factors[:, panel_idx] # Per-model scatter for m in range(num_models): mask = true_model_idx == m if not mask.any(): continue ax.scatter( x[mask], y[mask], color=model_colors[m], label=model_names[m], **scatter_kw, ) # Make axes square and add identity line make_quadratic(ax, x, y) xlim, ylim = ax.get_xlim(), ax.get_ylim() # Quadrant shading: correct = same sign (Q1 and Q3), incorrect = opposite. # Bayes factor accent blue (#6969ff), distinct from the navy used elsewhere. ax.fill_between( [0, xlim[1]], 0, ylim[1], color="#6969ff", alpha=0.10, zorder=0, ) ax.fill_between( [xlim[0], 0], ylim[0], 0, color="#6969ff", alpha=0.10, zorder=0, ) ax.fill_between( [xlim[0], 0], 0, ylim[1], color="gray", alpha=0.15, zorder=0, ) ax.fill_between( [0, xlim[1]], ylim[0], 0, color="gray", alpha=0.15, zorder=0, ) ax.axhline(0, color="gray", linewidth=0.8, linestyle=":", zorder=1) ax.axvline(0, color="gray", linewidth=0.8, linestyle=":", zorder=1) if add_corr: corr = np.corrcoef(x, y)[0, 1] add_metric(ax, metric_text="$r$", metric_value=corr, metric_fontsize=metric_fontsize) ax.set_title(panel_titles[panel_idx], fontsize=title_fontsize) prettify_subplots(np.asarray(axes), num_subplots=num_panels, tick_fontsize=tick_fontsize) add_titles_and_labels( axes=np.asarray(axes), num_row=num_row, num_col=num_col, xlabel=f"True $\\log \\mathrm{{BF}}_{{k,{reference_model}}}$", ylabel=f"Predicted $\\log \\mathrm{{BF}}_{{k,{reference_model}}}$", label_fontsize=label_fontsize, ) # Single shared legend below the figure handles = [ plt.Line2D([0], [0], marker="o", color="w", markerfacecolor=model_colors[m], markersize=8, label=model_names[m]) for m in range(num_models) ] fig.legend( handles=handles, loc="lower center", bbox_to_anchor=(0.5, -0.05), ncol=num_models, fontsize=tick_fontsize, frameon=False, ) fig.tight_layout() return fig