Source code for pysatl_cpd.analysis.visualization.benchmarking.metrics.arl_based_metric_visualizer

# -*- coding: ascii -*-
"""
ARL-based metric visualizer.
"""

from __future__ import annotations

__author__ = "Danil Totmyanin"
__copyright__ = "Copyright (c) 2026 PySATL project"
__license__ = "SPDX-License-Identifier: MIT"

import pandas as pd
import plotly.graph_objects as go

from pysatl_cpd.analysis.visualization.abstracts import IMetricVisualizer
from pysatl_cpd.analysis.visualization.abstracts.imetric_visualizer import METRIC_AXIS_NAME
from pysatl_cpd.analysis.visualization.specs import LineSpec
from pysatl_cpd.analysis.visualization.typedefs import DrawBackend, GoAxMapping, GoFigure, PltAxMapping, PltFigure
from pysatl_cpd.analysis.visualization.utils import line_spec_to_plotly_trace_kwargs

PLOTLY_GRID_WIDTH = 1
PLOTLY_GRID_COLOR = "lightgray"
MPL_GRID_ALPHA = 0.3


[docs] class ARLBasedMetricVisualizer(IMetricVisualizer): """ Draw metrics as functions of ARL. Rows that share the same ``arl`` value (e.g. different thresholds) are collapsed by taking the minimum of each plotted metric, yielding the lower envelope along the ARL axis. """
[docs] def __init__( self, *, backend: DrawBackend | str = DrawBackend.MATPLOTLIB, y_metrics: list[str], title: str = "ARL Curve", ylabel: str = "Values", style_map: dict[str, LineSpec] | None = None, ) -> None: super().__init__(backend=backend) self._y_metrics = y_metrics self._title = title self._ylabel = ylabel self._style_map = style_map or {}
@property def requirements(self) -> list[str]: """Return required columns: ARL plus all configured Y metrics.""" columns = ["arl", *self._y_metrics] return list(dict.fromkeys(columns)) def _arl_metric_envelope(self, table: pd.DataFrame) -> pd.DataFrame: """One row per distinct ARL with each metric replaced by its group-wise minimum.""" metric_cols = list(dict.fromkeys(self._y_metrics)) return table.groupby("arl", sort=False)[metric_cols].min().reset_index().sort_values("arl", ascending=True) def _draw_matplotlib(self, figure: PltFigure, axes: PltAxMapping) -> PltFigure: """Draw ARL-based curves on a Matplotlib axes. Parameters ---------- figure Matplotlib figure. axes Matplotlib axes mapping containing the metric axes. Returns ------- pltFigure The figure with curves drawn. """ ax = axes[METRIC_AXIS_NAME] for entry_key, benchmark_table in self._iter_algorithm_tables(): table = self._arl_metric_envelope(benchmark_table) for metric in self._y_metrics: style = self._resolve_line_style(metric_style_map=self._style_map, entry_key=entry_key, metric=metric) ax.plot( table["arl"], table[metric], label=f"{entry_key}: {metric}", linestyle=style.get("linestyle", "-"), color=style.get("color"), linewidth=float(style.get("linewidth", 1)), ) ax.set_title(self._title) ax.set_xlabel("ARL") ax.set_ylabel(self._ylabel) ax.grid(True, alpha=MPL_GRID_ALPHA) ax.legend(loc="best") return figure def _draw_plotly(self, figure: GoFigure, axes: GoAxMapping) -> GoFigure: """Draw ARL-based curves on a Plotly subplot. Parameters ---------- figure Plotly figure. axes Plotly axes mapping containing the metric subplot position. Returns ------- GoFigure The figure with curves drawn. """ row, col = axes[METRIC_AXIS_NAME] for entry_key, benchmark_table in self._iter_algorithm_tables(): table = self._arl_metric_envelope(benchmark_table) for metric in self._y_metrics: style = self._resolve_line_style(metric_style_map=self._style_map, entry_key=entry_key, metric=metric) figure.add_trace( go.Scatter( x=table["arl"], y=table[metric], mode="lines", name=f"{entry_key}: {metric}", **line_spec_to_plotly_trace_kwargs(style), ), row=row, col=col, ) figure.update_xaxes( title_text="ARL", showgrid=True, gridwidth=PLOTLY_GRID_WIDTH, gridcolor=PLOTLY_GRID_COLOR, row=row, col=col, ) figure.update_yaxes( title_text=self._ylabel, showgrid=True, gridwidth=PLOTLY_GRID_WIDTH, gridcolor=PLOTLY_GRID_COLOR, row=row, col=col, ) return figure