Source code for tseda.visualization.quality_plots

"""
Data-quality visualisation plots.

Functions
---------
plot_missing_heatmap — NaN position heatmap  *(innovative)*
plot_outliers        — series with fence lines and flagged points
plot_outlier_score   — outlier score timeline
"""
from __future__ import annotations

from typing import Optional, Tuple

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
from matplotlib.axes import Axes

from tseda.core.timeseries import TimeSeries
from tseda.quality.outliers import OutlierReport
from tseda.visualization.base import PALETTE, _make_fig_ax, _set_title

__all__ = ["plot_missing_heatmap", "plot_outliers", "plot_outlier_score"]


[docs] def plot_missing_heatmap( ts: TimeSeries, *, ax: Optional[Axes] = None, title: Optional[str] = None, figsize: Optional[Tuple[float, float]] = None, ) -> Figure: """NaN positions displayed as a binary heatmap. For long series the observations are binned into columns to keep the plot readable. Rows represent equal-sized chunks of the series; each cell is red if any NaN is present in that chunk, white otherwise. Parameters ---------- ts : TimeSeries ax, title, figsize : optional Returns ------- matplotlib.figure.Figure """ n = ts.n n_cols = min(200, n) chunk = max(1, n // n_cols) n_chunks = (n + chunk - 1) // chunk grid = np.zeros((1, n_chunks), dtype=float) for c in range(n_chunks): seg = ts.values[c * chunk: (c + 1) * chunk] grid[0, c] = float(np.any(np.isnan(seg))) fig, ax = _make_fig_ax(ax, figsize, (12, 1.5)) im = ax.imshow(grid, aspect="auto", cmap="RdYlGn_r", vmin=0, vmax=1, extent=[0, n, -0.5, 0.5]) ax.set_yticks([]) ax.set_xlabel("Observation index") pct = float(np.isnan(ts.values).mean() * 100) _set_title(ax, title, f"Missing value map — {ts.name} ({pct:.1f}% NaN)") fig.colorbar(im, ax=ax, fraction=0.02, pad=0.02, label="Has NaN") fig.tight_layout() return fig
[docs] def plot_outliers( ts: TimeSeries, report: OutlierReport, *, ax: Optional[Axes] = None, title: Optional[str] = None, figsize: Optional[Tuple[float, float]] = None, ) -> Figure: """Series line plot with IQR fence lines and outlier markers. Parameters ---------- ts : TimeSeries report : OutlierReport ax, title, figsize : optional Returns ------- matplotlib.figure.Figure """ fig, ax = _make_fig_ax(ax, figsize, (12, 4)) ax.plot(ts.index, ts.values, color=PALETTE["accent"], linewidth=1.0, label=ts.name, zorder=2) if report.lower_bound is not None: ax.axhline(report.lower_bound, color=PALETTE["warn"], linewidth=1.2, linestyle="--", alpha=0.9, label=f"Lower fence ({report.lower_bound:.2f})") if report.upper_bound is not None: ax.axhline(report.upper_bound, color=PALETTE["warn"], linewidth=1.2, linestyle="--", alpha=0.9, label=f"Upper fence ({report.upper_bound:.2f})") if report.n_outliers > 0: ax.scatter( report.timestamps, report.values, color=PALETTE["anomaly"], s=50, zorder=5, label=f"Outlier ({report.method}, n={report.n_outliers})", ) ax.set_xlabel("Time") ax.set_ylabel(ts.unit or "Value") ax.legend(fontsize=9) _set_title(ax, title, f"Outliers — {ts.name} [{report.method}]") fig.tight_layout() return fig
[docs] def plot_outlier_score( ts: TimeSeries, report: OutlierReport, *, ax: Optional[Axes] = None, title: Optional[str] = None, figsize: Optional[Tuple[float, float]] = None, ) -> Figure: """Outlier score timeline with a threshold reference line at 1.0. Parameters ---------- ts : TimeSeries report : OutlierReport ax, title, figsize : optional Returns ------- matplotlib.figure.Figure """ fig, ax = _make_fig_ax(ax, figsize, (12, 3)) n = min(len(report.indices), ts.n) # Build a score array aligned to ts positions scores = np.zeros(ts.n, dtype=float) for idx_pos in report.indices: if idx_pos < ts.n: scores[idx_pos] = 1.0 ax.bar(range(ts.n), scores, color=PALETTE["anomaly"], alpha=0.7, width=1.0, label="outlier flag") ax.plot(ts.index, ts.values / max(abs(ts.values[~np.isnan(ts.values)]).max(), 1e-15), color=PALETTE["neutral"], linewidth=0.8, alpha=0.5, label="series (normalised)") ax.set_xlabel("Observation index") ax.set_ylabel("Outlier flag") ax.legend(fontsize=9) _set_title(ax, title, f"Outlier positions [{report.method}] — {ts.name}") fig.tight_layout() return fig