Source code for tseda.visualization.distribution_plots

"""
Distribution and rolling-statistics plots.

Functions
---------
plot_distribution — histogram + KDE + normal overlay
plot_qq           — quantile-quantile plot vs normal
plot_rolling_stats — rolling mean / std panel
"""
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 scipy import stats as sp_stats

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

__all__ = ["plot_distribution", "plot_qq", "plot_rolling_stats"]


[docs] def plot_distribution( ts: TimeSeries, *, bins: int = 30, ax: Optional[Axes] = None, title: Optional[str] = None, figsize: Optional[Tuple[float, float]] = None, ) -> Figure: """Histogram + KDE + fitted-normal overlay. Parameters ---------- ts : TimeSeries bins : int Number of histogram bins. ax, title, figsize : optional Returns ------- matplotlib.figure.Figure """ from scipy.stats import gaussian_kde fig, ax = _make_fig_ax(ax, figsize, (8, 5)) x = ts.values[~np.isnan(ts.values)] if len(x) == 0: ax.text(0.5, 0.5, "No data", transform=ax.transAxes, ha="center") _set_title(ax, title, ts.name) return fig ax.hist(x, bins=bins, density=True, color=PALETTE["accent"], alpha=0.5, edgecolor="white", label="histogram") x_grid = np.linspace(x.min(), x.max(), 300) if len(x) >= 3: kde = gaussian_kde(x) ax.plot(x_grid, kde(x_grid), color=PALETTE["dark"], linewidth=2, label="KDE") mu, sigma = float(np.mean(x)), float(np.std(x)) if sigma > 0: ax.plot(x_grid, sp_stats.norm.pdf(x_grid, mu, sigma), color=PALETTE["anomaly"], linewidth=1.5, linestyle="--", label=f"Normal(μ={mu:.2f}, σ={sigma:.2f})") ax.set_xlabel(ts.unit or "Value") ax.set_ylabel("Density") ax.legend(fontsize=9) _set_title(ax, title, f"Distribution — {ts.name}") fig.tight_layout() return fig
[docs] def plot_qq( ts: TimeSeries, *, ax: Optional[Axes] = None, title: Optional[str] = None, figsize: Optional[Tuple[float, float]] = None, ) -> Figure: """Quantile-quantile plot of *ts* values against a standard normal. Parameters ---------- ts : TimeSeries ax, title, figsize : optional Returns ------- matplotlib.figure.Figure """ fig, ax = _make_fig_ax(ax, figsize, (6, 6)) x = ts.values[~np.isnan(ts.values)] if len(x) < 4: ax.text(0.5, 0.5, "Too few observations", transform=ax.transAxes, ha="center") _set_title(ax, title, ts.name) return fig (osm, osr), (slope, intercept, _) = sp_stats.probplot(x, dist="norm") ax.scatter(osm, osr, s=12, color=PALETTE["accent"], alpha=0.7, label="data") lo, hi = float(osm[0]), float(osm[-1]) ax.plot([lo, hi], [slope * lo + intercept, slope * hi + intercept], color=PALETTE["anomaly"], linewidth=1.5, label="Normal reference") ax.set_xlabel("Theoretical quantiles") ax.set_ylabel("Sample quantiles") ax.legend(fontsize=9) _set_title(ax, title, f"Q-Q plot — {ts.name}") fig.tight_layout() return fig
[docs] def plot_rolling_stats( ts: TimeSeries, window: int, *, ax: Optional[Axes] = None, title: Optional[str] = None, figsize: Optional[Tuple[float, float]] = None, ) -> Figure: """Two-panel plot: rolling mean (top) and rolling std (bottom). Parameters ---------- ts : TimeSeries window : int Rolling window width in observations. ax, title, figsize : optional *ax* is ignored for this multi-panel plot; a new figure is always created unless passed explicitly. Returns ------- matplotlib.figure.Figure """ import pandas as pd window = max(2, int(window)) fig, (ax1, ax2) = plt.subplots(2, 1, figsize=figsize or (12, 6), sharex=True) s = pd.Series(ts.values, index=ts.index) roll = s.rolling(window, center=True) ax1.plot(ts.index, ts.values, color=PALETTE["neutral"], linewidth=0.8, alpha=0.7, label="original") ax1.plot(ts.index, roll.mean().values, color=PALETTE["accent"], linewidth=1.5, label=f"Rolling mean ({window})") ax1.set_ylabel(ts.unit or "Value") ax1.legend(fontsize=9) ax2.plot(ts.index, roll.std().values, color=PALETTE["anomaly"], linewidth=1.5, label=f"Rolling std ({window})") ax2.set_xlabel("Time") ax2.set_ylabel("Std") ax2.legend(fontsize=9) fig.suptitle(title or f"Rolling statistics (window={window}) — {ts.name}") fig.tight_layout() return fig