Source code for tseda.report.console_report

"""
Console EDA report for :class:`~tseda.core.TimeSeries`.

Runs all tseda analysis modules and prints a structured plain-text
summary to stdout (or returns it as a string).

Classes
-------
ConsoleReport
    Stateless report generator.

Examples
--------
>>> import numpy as np, pandas as pd
>>> from tseda import TimeSeries
>>> from tseda.report.console_report import ConsoleReport

>>> rng = np.random.default_rng(0)
>>> idx = pd.date_range("2020-01-01", periods=200, freq="D")
>>> ts  = TimeSeries(rng.standard_normal(200), index=idx, name="demo")
>>> report_str = ConsoleReport().to_string(ts)
>>> "demo" in report_str
True
"""
from __future__ import annotations

from typing import Optional

import numpy as np

from tseda.core.timeseries import TimeSeries

__all__ = ["ConsoleReport"]

_W = 68  # column width


def _hr(char: str = "─") -> str:
    return char * _W


def _header(text: str, char: str = "═") -> str:
    return f"\n{char * _W}\n  {text}\n{char * _W}"


def _section(title: str) -> str:
    return f"\n{_hr('─')}\n  {title}\n{_hr('─')}"


def _kv(key: str, value: str, width: int = 26) -> str:
    return f"  {key:<{width}}: {value}"


[docs] class ConsoleReport: """Generate a plain-text EDA report for a :class:`~tseda.core.TimeSeries`. Methods ------- to_string(ts, period, alpha) Build the report and return it as a string. generate(ts, period, alpha) Print the report to stdout. Examples -------- >>> import numpy as np, pandas as pd >>> from tseda import TimeSeries >>> from tseda.report.console_report import ConsoleReport >>> rng = np.random.default_rng(0) >>> idx = pd.date_range("2020", periods=100, freq="D") >>> ts = TimeSeries(rng.standard_normal(100), index=idx) >>> s = ConsoleReport().to_string(ts) >>> isinstance(s, str) and len(s) > 0 True """
[docs] def to_string( self, ts: TimeSeries, *, period: Optional[int] = None, alpha: float = 0.05, ) -> str: """Return the full EDA report as a string. Parameters ---------- ts : TimeSeries period : int, optional Seasonal period. Auto-detected when ``None``. alpha : float, optional Significance level for statistical tests. Default ``0.05``. Returns ------- str """ if not isinstance(ts, TimeSeries): raise TypeError( f"'ts' must be a TimeSeries, got {type(ts).__name__!r}." ) lines: list[str] = [] # ── Title ────────────────────────────────────────────────── lines.append(_header(f"tseda EDA Report — {ts.name}")) lines.append(_kv("Series name", ts.name)) lines.append(_kv("Observations", str(ts.n))) lines.append(_kv("Frequency", ts.freq or "irregular")) lines.append(_kv("Start", str(ts.start)[:19])) lines.append(_kv("End", str(ts.end)[:19])) lines.append(_kv("Is regular", str(ts.is_regular))) lines.append(_kv("Unit", ts.unit or "—")) # ── 1. Data Quality ──────────────────────────────────────── lines.append(_section("1. DATA QUALITY")) try: from tseda.quality.missing import MissingValueAnalyzer from tseda.quality.outliers import OutlierDetector mq = MissingValueAnalyzer().analyze(ts) lines.append(_kv("Missing values", f"{mq.n_nan} ({mq.pct_nan:.2f} %)")) lines.append(_kv("Longest NaN run", str(mq.longest_nan_run))) lines.append(_kv("Index gaps", str(mq.n_gaps))) oq = OutlierDetector().iqr(ts) lines.append(_kv("IQR outliers", f"{oq.n_outliers} ({oq.n_outliers / max(ts.n, 1) * 100:.2f} %)")) except Exception as exc: lines.append(f" [warning] Quality analysis failed: {exc}") # ── 2. Descriptive Statistics ────────────────────────────── lines.append(_section("2. DESCRIPTIVE STATISTICS")) try: from tseda.statistics.descriptive import DescriptiveAnalyzer ds = DescriptiveAnalyzer().analyze(ts) lines.append(_kv("Mean", f"{ds.mean:.6g}")) lines.append(_kv("Std", f"{ds.std:.6g}")) lines.append(_kv("Min", f"{ds.min:.6g}")) lines.append(_kv("Max", f"{ds.max:.6g}")) lines.append(_kv("Median", f"{ds.median:.6g}")) lines.append(_kv("Skewness", f"{ds.skewness:.4f}")) lines.append(_kv("Kurtosis", f"{ds.kurtosis:.4f}")) lines.append(_kv("CV", f"{ds.cv:.4f}")) except Exception as exc: lines.append(f" [warning] Descriptive stats failed: {exc}") # ── 3. Stationarity ──────────────────────────────────────── lines.append(_section("3. STATIONARITY")) try: from tseda.statistics.stationarity import StationarityTester tester = StationarityTester() adf = tester.adf(ts, alpha=alpha) kpss = tester.kpss(ts, alpha=alpha) lines.append(_kv("ADF p-value", f"{adf.p_value:.4f}{'stationary' if adf.is_stationary else 'non-stationary'}")) lines.append(_kv("KPSS p-value", f"{kpss.p_value:.4f}{'stationary' if kpss.is_stationary else 'non-stationary'}")) # Combined verdict if adf.is_stationary and kpss.is_stationary: verdict = "STATIONARY (both tests agree)" elif adf.is_stationary: verdict = "TREND STATIONARY (consider detrending)" elif kpss.is_stationary: verdict = "DIFFERENCE STATIONARY (consider d=1)" else: verdict = "NON-STATIONARY (strong evidence)" lines.append(_kv("Verdict", verdict)) except Exception as exc: lines.append(f" [warning] Stationarity tests failed: {exc}") # ── 4. Autocorrelation ───────────────────────────────────── lines.append(_section("4. AUTOCORRELATION")) try: from tseda.statistics.autocorrelation import AutocorrelationAnalyzer ac = AutocorrelationAnalyzer().analyze(ts, lags=min(40, ts.n // 2 - 1)) ci = float(ac.conf_upper[1]) n_sig_acf = int(np.sum(np.abs(ac.acf[1:]) > ci)) n_sig_pacf = int(np.sum(np.abs(ac.pacf[1:]) > ci)) lines.append(_kv("Significant ACF lags", f"{n_sig_acf}")) lines.append(_kv("Significant PACF lags", f"{n_sig_pacf}")) lines.append(_kv("Is white noise", str(ac.is_white_noise))) lines.append(_kv("Ljung-Box p (lag 10)", f"{ac.lb_pvalue[min(9, len(ac.lb_pvalue)-1)]:.4f}")) except Exception as exc: lines.append(f" [warning] Autocorrelation failed: {exc}") # ── 5. Seasonality ───────────────────────────────────────── lines.append(_section("5. SEASONALITY")) detected_period = period try: from tseda.seasonality.detector import SeasonalityDetector sr = SeasonalityDetector().detect(ts) lines.append(_kv("Is seasonal", str(sr.is_seasonal))) lines.append(_kv("Dominant period", str(sr.dominant_period))) lines.append(_kv("Candidate periods", str(sr.candidate_periods[:5]))) if period is None and sr.is_seasonal: detected_period = sr.dominant_period except Exception as exc: lines.append(f" [warning] Seasonality detection failed: {exc}") # ── 6. Forecastability ───────────────────────────────────── lines.append(_section("6. FORECASTABILITY")) try: from tseda.forecastability.scorer import ForecastabilityScorer fr = ForecastabilityScorer().score(ts, period=detected_period, alpha=alpha) lines.append(_kv("Overall score", f"{fr.score:.1f} / 100")) lines.append("") sub_w = {"data_quality": 20, "stationarity": 15, "signal_to_noise": 20, "autocorrelation": 15, "sample_size": 15, "regularity": 15} lines.append(f" {'Sub-score':<22} {'Score':>7} {'Weight':>6}") lines.append(f" {'─'*22} {'─'*7} {'─'*6}") for k, v in fr.sub_scores.items(): bar = "█" * int(v / 10) lines.append(f" {k:<22} {v:>7.1f} ({sub_w[k]:>2}%) {bar}") lines.append("") lines.append(_kv("Recommended model", fr.recommended_model)) lines.append(_kv("Recommended diff", f"d={fr.recommended_diff}")) lines.append(_kv("Recommended period", str(fr.recommended_period))) except Exception as exc: lines.append(f" [warning] Forecastability scoring failed: {exc}") lines.append(f"\n{_hr('═')}\n") return "\n".join(lines)
[docs] def generate( self, ts: TimeSeries, *, period: Optional[int] = None, alpha: float = 0.05, ) -> None: """Print the EDA report to stdout. Parameters ---------- ts : TimeSeries period : int, optional alpha : float, optional Examples -------- >>> import numpy as np, pandas as pd >>> from tseda import TimeSeries >>> from tseda.report.console_report import ConsoleReport >>> rng = np.random.default_rng(0) >>> idx = pd.date_range("2020", periods=100, freq="D") >>> ts = TimeSeries(rng.standard_normal(100), index=idx) >>> ConsoleReport().generate(ts) # doctest: +SKIP """ print(self.to_string(ts, period=period, alpha=alpha))