Source code for timesmith.utils.plotting

"""Plotting utilities using plotsmith for time series visualization."""

import logging
from typing import Optional, Tuple, Union

import numpy as np
import pandas as pd

logger = logging.getLogger(__name__)

# Try to import plotsmith
try:
    import plotsmith as ps

    HAS_PLOTSMITH = True
except ImportError:
    HAS_PLOTSMITH = False
    ps = None
    logger.warning(
        "plotsmith not installed. Plotting functions will not be available. "
        "Install with: pip install plotsmith"
    )

# Export HAS_PLOTSMITH and all plotting functions
__all__ = [
    "HAS_PLOTSMITH",
    "plot_timeseries",
    "plot_forecast",
    "plot_residuals",
    "plot_multiple_series",
    "plot_autocorrelation",
    "plot_monte_carlo_paths",
]


[docs] def plot_timeseries( data: Union[pd.Series, pd.DataFrame], title: Optional[str] = None, xlabel: Optional[str] = None, ylabel: Optional[str] = None, figsize: Optional[Tuple[int, int]] = None, **kwargs, ): """Plot time series data using plotsmith. Args: data: Time series data (Series or DataFrame). title: Plot title. xlabel: X-axis label. ylabel: Y-axis label. figsize: Figure size (width, height). **kwargs: Additional arguments passed to plotsmith. Returns: Figure and axes objects. """ if not HAS_PLOTSMITH: raise ImportError( "plotsmith is required for plotting. Install with: pip install plotsmith" ) import matplotlib.pyplot as plt # Try to use plotsmith's plot_timeseries if available try: if ps is not None and hasattr(ps, "plot_timeseries"): if isinstance(data, pd.Series): data_for_plot = data.to_frame() else: data_for_plot = data fig, ax = ps.plot_timeseries( data_for_plot, title=title, xlabel=xlabel, ylabel=ylabel, figsize=figsize, **kwargs, ) return fig, ax except (AttributeError, TypeError, Exception) as e: logger.debug(f"Plotsmith plot_timeseries not available, using matplotlib: {e}") # Fallback to matplotlib fig, ax = plt.subplots(figsize=figsize or (12, 6)) if isinstance(data, pd.Series): ax.plot(data.index, data.values, **kwargs) else: for col in data.columns: ax.plot(data.index, data[col].values, label=col, **kwargs) if title: ax.set_title(title) if xlabel: ax.set_xlabel(xlabel) if ylabel: ax.set_ylabel(ylabel) if isinstance(data, pd.DataFrame) and len(data.columns) > 1: ax.legend() ax.grid(True, alpha=0.3) return fig, ax
[docs] def plot_forecast( historical: pd.Series, forecast: pd.Series, intervals: Optional[pd.DataFrame] = None, title: Optional[str] = "Forecast", **kwargs, ): """Plot forecast with historical data and optional confidence intervals. Args: historical: Historical time series data. forecast: Forecasted values. intervals: Optional DataFrame with 'lower' and 'upper' columns for confidence intervals. title: Plot title. **kwargs: Additional arguments passed to plotsmith. Returns: Figure and axes objects. """ if not HAS_PLOTSMITH: raise ImportError( "plotsmith is required for plotting. Install with: pip install plotsmith" ) import matplotlib.pyplot as plt # Try to use plotsmith if available try: if ps is not None and hasattr(ps, "plot_timeseries"): combined = pd.concat([historical, forecast]) fig, ax = ps.plot_timeseries(combined, title=title, **kwargs) else: raise AttributeError("Plotsmith plot_timeseries not available") except (AttributeError, TypeError, Exception) as e: logger.debug(f"Plotsmith not available, using matplotlib: {e}") # Fallback to matplotlib fig, ax = plt.subplots(figsize=(12, 6)) ax.plot(historical.index, historical.values, label="Historical", linewidth=2) ax.plot( forecast.index, forecast.values, label="Forecast", linewidth=2, linestyle="--", ) if title: ax.set_title(title) ax.legend() ax.grid(True, alpha=0.3) # Add forecast start line ax.axvline( historical.index[-1], color="red", linestyle=":", alpha=0.7, label="Forecast Start", ) # Add confidence intervals if provided if intervals is not None: ax.fill_between( forecast.index, intervals["lower"].values, intervals["upper"].values, alpha=0.3, label="Confidence Interval", ) ax.legend() return fig, ax
[docs] def plot_residuals( actual: np.ndarray, predicted: np.ndarray, plot_type: str = "scatter", title: Optional[str] = "Residuals", **kwargs, ): """Plot residuals using plotsmith. Args: actual: Actual values. predicted: Predicted values. plot_type: Type of plot ('scatter', 'line', 'histogram'). title: Plot title. **kwargs: Additional arguments passed to plotsmith. Returns: Figure and axes objects. """ if not HAS_PLOTSMITH: raise ImportError( "plotsmith is required for plotting. Install with: pip install plotsmith" ) residuals = actual - predicted import matplotlib.pyplot as plt # Try to use plotsmith's plot_residuals if available try: if ps is not None and hasattr(ps, "plot_residuals"): fig, ax = ps.plot_residuals( actual, predicted, plot_type=plot_type, title=title, **kwargs ) return fig, ax except (AttributeError, TypeError, Exception) as e: logger.debug(f"Plotsmith plot_residuals not available, using matplotlib: {e}") # Fallback to matplotlib fig, ax = plt.subplots(figsize=(10, 6)) if plot_type == "scatter": ax.scatter(actual, residuals, **kwargs) elif plot_type == "line": ax.plot(actual, residuals, **kwargs) elif plot_type == "histogram": ax.hist(residuals, bins=30, **kwargs) ax.set_title(title) ax.set_xlabel("Actual") ax.set_ylabel("Residuals") ax.grid(True, alpha=0.3) return fig, ax
[docs] def plot_multiple_series( series_dict: dict, title: Optional[str] = None, figsize: Optional[Tuple[int, int]] = None, **kwargs, ): """Plot multiple time series on the same plot. Args: series_dict: Dictionary mapping labels to Series/DataFrame. title: Plot title. figsize: Figure size (width, height). **kwargs: Additional arguments passed to plotsmith. Returns: Figure and axes objects. """ if not HAS_PLOTSMITH: raise ImportError( "plotsmith is required for plotting. Install with: pip install plotsmith" ) import matplotlib.pyplot as plt # Try to use plotsmith if available try: if ps is not None and hasattr(ps, "plot_timeseries"): # Combine all series into a DataFrame combined = pd.DataFrame(series_dict) fig, ax = ps.plot_timeseries( combined, title=title, figsize=figsize, **kwargs ) return fig, ax except (AttributeError, TypeError, Exception) as e: logger.debug(f"Plotsmith plot_timeseries not available, using matplotlib: {e}") # Fallback to matplotlib fig, ax = plt.subplots(figsize=figsize or (12, 6)) for label, series in series_dict.items(): ax.plot(series.index, series.values, label=label, **kwargs) if title: ax.set_title(title) ax.legend() ax.grid(True, alpha=0.3) return fig, ax
[docs] def plot_autocorrelation( acf_values: np.ndarray, pacf_values: Optional[np.ndarray] = None, max_lag: Optional[int] = None, title: Optional[str] = "Autocorrelation", **kwargs, ): """Plot autocorrelation and partial autocorrelation functions. Args: acf_values: ACF values. pacf_values: Optional PACF values. max_lag: Maximum lag to display. title: Plot title. **kwargs: Additional arguments passed to plotsmith. Returns: Figure and axes objects. """ if not HAS_PLOTSMITH: raise ImportError( "plotsmith is required for plotting. Install with: pip install plotsmith" ) lags = np.arange(len(acf_values)) if max_lag is not None: lags = lags[:max_lag] acf_values = acf_values[:max_lag] if pacf_values is not None: pacf_values = pacf_values[:max_lag] # Use matplotlib for ACF/PACF plots (bar charts) import matplotlib.pyplot as plt if pacf_values is not None: fig, axes = plt.subplots(2, 1, figsize=(12, 8), sharex=True) axes[0].bar(lags, acf_values) axes[0].set_title(f"{title} - ACF") axes[0].set_ylabel("ACF") axes[0].grid(True, alpha=0.3) axes[1].bar(lags, pacf_values) axes[1].set_title(f"{title} - PACF") axes[1].set_xlabel("Lag") axes[1].set_ylabel("PACF") axes[1].grid(True, alpha=0.3) else: fig, ax = plt.subplots(figsize=(12, 6)) ax.bar(lags, acf_values) ax.set_title(title) ax.set_xlabel("Lag") ax.set_ylabel("ACF") ax.grid(True, alpha=0.3) axes = [ax] plt.tight_layout() return fig, axes
[docs] def plot_monte_carlo_paths( paths: np.ndarray, title: Optional[str] = "Monte Carlo Simulation", show_mean: bool = True, show_percentiles: bool = True, **kwargs, ): """Plot Monte Carlo simulation paths using plotsmith. Args: paths: Array of shape (n_steps, n_simulations) with simulation paths. title: Plot title. show_mean: Whether to show mean path. show_percentiles: Whether to show percentile bands. **kwargs: Additional arguments passed to plotsmith. Returns: Figure and axes objects. """ if not HAS_PLOTSMITH: raise ImportError( "plotsmith is required for plotting. Install with: pip install plotsmith" ) import matplotlib.pyplot as plt # Ensure paths is 2D: (n_steps, n_simulations) if paths.ndim == 1: paths = paths.reshape(-1, 1) n_steps, n_simulations = paths.shape fig, ax = plt.subplots(figsize=(12, 6)) # Plot individual paths alpha = 0.3 if n_simulations > 10 else 0.7 for i in range(min(n_simulations, 100)): # Limit to 100 paths for performance ax.plot(paths[:, i], color="gray", alpha=alpha, linewidth=0.5) if show_mean: mean_path = paths.mean(axis=1) ax.plot(mean_path, color="black", linewidth=2, label="Mean Path") if show_percentiles: lower = np.percentile(paths, 2.5, axis=1) upper = np.percentile(paths, 97.5, axis=1) ax.fill_between( np.arange(n_steps), lower, upper, alpha=0.2, label="95% Confidence Interval" ) ax.set_title(title) ax.set_xlabel("Steps") ax.set_ylabel("Simulated Value") ax.legend() ax.grid(True, alpha=0.3) return fig, ax