Decomposition

tseda.decomposition

Time series decomposition into trend, seasonal, and residual components.

Public API

DecompositionResult

Frozen dataclass holding all four components and quality metrics.

ClassicalDecomposer

Centered moving-average decomposition (additive or multiplicative). Pure numpy — no extra dependencies.

STLDecomposer

LOESS-based STL decomposition (statsmodels primary, scipy fallback).

class tseda.decomposition.DecompositionResult(original, trend, seasonal, residual, period, model, method, strength_trend, strength_seasonal, n_obs_used)[source]

Bases: object

Immutable time series decomposition result.

Parameters:
original

The input series.

Type:

TimeSeries

trend

Smooth trend component. May contain NaN at the edges (classical decomposition only; STL fills the edges with LOESS extrapolation).

Type:

TimeSeries

seasonal

Periodic seasonal component with the same length as original.

Type:

TimeSeries

residual

Remainder after removing trend and seasonal. NaN wherever trend is NaN.

Type:

TimeSeries

period

Number of observations per seasonal cycle (e.g., 12 for monthly data with an annual pattern).

Type:

int

model

"additive" or "multiplicative".

Type:

str

method

"classical" or "stl".

Type:

str

strength_trend

Wang et al. (2006) trend strength: max(0, 1 Var(R) / Var(T + R)). In [0, 1].

Type:

float

strength_seasonal

Wang et al. (2006) seasonality strength: max(0, 1 Var(R) / Var(S + R)). In [0, 1].

Type:

float

n_obs_used

Number of non-NaN residual observations used for strength metrics.

Type:

int

original: TimeSeries
trend: TimeSeries
seasonal: TimeSeries
residual: TimeSeries
period: int
model: str
method: str
strength_trend: float
strength_seasonal: float
n_obs_used: int
to_dataframe()[source]

Return all four components as a pandas.DataFrame.

Returns:

Columns: observed, trend, seasonal, residual.

Return type:

pandas.DataFrame

Examples

>>> import numpy as np, pandas as pd
>>> from tseda import TimeSeries
>>> from tseda.decomposition.classical import ClassicalDecomposer
>>> idx = pd.date_range("2020", periods=36, freq="MS")
>>> ts  = TimeSeries(np.ones(36) + np.tile(np.arange(12), 3), index=idx)
>>> df  = ClassicalDecomposer().decompose(ts, period=12).to_dataframe()
>>> list(df.columns)
['observed', 'trend', 'seasonal', 'residual']
summary()[source]

Return a plain-text summary of the decomposition.

Return type:

str

__repr__()[source]

Return repr(self).

Return type:

str

__init__(original, trend, seasonal, residual, period, model, method, strength_trend, strength_seasonal, n_obs_used)
Parameters:
Return type:

None

class tseda.decomposition.ClassicalDecomposer[source]

Bases: object

Decompose a TimeSeries using the centered moving-average (classical) method.

The decomposer is stateless — one instance may be reused.

decompose(ts, period, model)[source]

Return a DecompositionResult.

Parameters:
Return type:

DecompositionResult

Notes

The classical method has well-known limitations:

  • The trend component has NaN at both ends (half the period width).

  • It assumes a fixed seasonal pattern throughout the series.

  • It can be sensitive to outliers.

For more robust results, use STLDecomposer.

Examples

Additive decomposition:

>>> import numpy as np, pandas as pd
>>> from tseda import TimeSeries
>>> from tseda.decomposition.classical import ClassicalDecomposer
>>> rng  = np.random.default_rng(1)
>>> n    = 48
>>> seas = np.tile(np.sin(2 * np.pi * np.arange(12) / 12) * 5, 4)
>>> y    = np.arange(n, dtype=float) * 0.3 + seas + rng.standard_normal(n)
>>> idx  = pd.date_range("2020-01", periods=n, freq="MS")
>>> ts   = TimeSeries(y, index=idx)
>>> r    = ClassicalDecomposer().decompose(ts, period=12)
>>> r.model
'additive'
>>> r.strength_seasonal > 0.5
True
decompose(ts, period=None, *, model='additive')[source]

Decompose ts into trend, seasonal, and residual components.

Parameters:
  • ts (TimeSeries) – Input series. Should be regularly spaced and have length >= 2 × period.

  • period (int, optional) – Seasonal period (number of observations per cycle). When omitted the period is inferred from ts.freq: daily → 7, monthly → 12, quarterly → 4, etc.

  • model (str, optional) – "additive" (default) or "multiplicative".

Return type:

DecompositionResult

Raises:
  • TypeError – If ts is not a TimeSeries.

  • ValueError – If period cannot be inferred, is < 2, or the series is too short; also if model is not recognised.

Examples

>>> import numpy as np, pandas as pd
>>> from tseda import TimeSeries
>>> from tseda.decomposition.classical import ClassicalDecomposer
>>> idx = pd.date_range("2020", periods=36, freq="MS")
>>> y   = np.tile(np.arange(12, dtype=float), 3) + np.linspace(0, 6, 36)
>>> ts  = TimeSeries(y, index=idx)
>>> r   = ClassicalDecomposer().decompose(ts, period=12)
>>> r.seasonal.n
36
>>> r.trend.n
36
class tseda.decomposition.STLDecomposer[source]

Bases: object

Decompose a TimeSeries using the STL algorithm.

The decomposer is stateless — one instance may be reused.

decompose(ts, period, robust, seasonal_deg, trend_deg)[source]

Return a DecompositionResult.

Parameters:
Return type:

DecompositionResult

Examples

>>> import numpy as np, pandas as pd
>>> from tseda import TimeSeries
>>> from tseda.decomposition.stl import STLDecomposer
>>> rng  = np.random.default_rng(2)
>>> n    = 48
>>> seas = np.tile(np.sin(2 * np.pi * np.arange(12) / 12) * 8, 4)
>>> y    = np.linspace(0, 10, n) + seas + rng.standard_normal(n) * 0.3
>>> idx  = pd.date_range("2020-01", periods=n, freq="MS")
>>> ts   = TimeSeries(y, index=idx)
>>> r    = STLDecomposer().decompose(ts, period=12)
>>> r.strength_seasonal > 0.7
True
decompose(ts, period=None, *, robust=True, seasonal_deg=1, trend_deg=1)[source]

Decompose ts using STL.

Parameters:
  • ts (TimeSeries) – Input series. Must be regularly spaced and length >= 2 × period.

  • period (int, optional) – Seasonal period. Inferred from ts.freq when omitted.

  • robust (bool, optional) – When True (default), use robust LOESS fitting to down-weight outliers. Has no effect on the fallback path.

  • seasonal_deg (int, optional) – Polynomial degree for seasonal LOESS smoother (0 or 1). Passed to statsmodels.tsa.seasonal.STL. Default 1.

  • trend_deg (int, optional) – Polynomial degree for trend LOESS smoother (0 or 1). Default 1.

Returns:

method is "stl" when statsmodels is used, or "stl-fallback" otherwise.

Return type:

DecompositionResult

Raises:

Examples

>>> import numpy as np, pandas as pd
>>> from tseda import TimeSeries
>>> from tseda.decomposition.stl import STLDecomposer
>>> rng = np.random.default_rng(3)
>>> idx = pd.date_range("2020-01", periods=36, freq="MS")
>>> seas = np.tile(np.arange(12, dtype=float), 3)
>>> ts  = TimeSeries(seas + rng.standard_normal(36) * 0.2, index=idx)
>>> r   = STLDecomposer().decompose(ts, period=12)
>>> r.residual.n
36

Classical time series decomposition.

Decomposes a TimeSeries into trend, seasonal, and residual components using the centered moving-average method (classical / X-11 style).

Two models are supported:

  • Additivey = T + S + R (default)

  • Multiplicativey = T × S × R (use when variance scales with level)

The trend is estimated by a centered moving average whose window width equals the seasonal period. For even periods (e.g., 12) a 2×MA is applied to obtain a truly centered estimate.

All arithmetic is pure numpy / pandas — no extra dependencies.

Classes

DecompositionResult

Frozen dataclass holding all four components and quality metrics. Shared by tseda.decomposition.stl.

ClassicalDecomposer

Stateless decomposer.

Examples

>>> import numpy as np, pandas as pd
>>> from tseda import TimeSeries
>>> from tseda.decomposition.classical import ClassicalDecomposer

Monthly sales with trend + seasonality:

>>> rng  = np.random.default_rng(0)
>>> n    = 60
>>> t    = np.arange(n, dtype=float)
>>> seas = np.tile([-2, -1, 0, 1, 2, 3, 3, 2, 1, 0, -1, -2], 5)
>>> y    = 100 + 0.5 * t + seas + rng.standard_normal(n) * 0.5
>>> idx  = pd.date_range("2020-01", periods=n, freq="MS")
>>> ts   = TimeSeries(y, index=idx, name="sales", unit="units")
>>> dec  = ClassicalDecomposer().decompose(ts, period=12)
>>> dec.method
'classical'
>>> dec.model
'additive'
>>> round(dec.strength_seasonal, 2) > 0.5
True
class tseda.decomposition.classical.DecompositionResult(original, trend, seasonal, residual, period, model, method, strength_trend, strength_seasonal, n_obs_used)[source]

Bases: object

Immutable time series decomposition result.

Parameters:
original

The input series.

Type:

TimeSeries

trend

Smooth trend component. May contain NaN at the edges (classical decomposition only; STL fills the edges with LOESS extrapolation).

Type:

TimeSeries

seasonal

Periodic seasonal component with the same length as original.

Type:

TimeSeries

residual

Remainder after removing trend and seasonal. NaN wherever trend is NaN.

Type:

TimeSeries

period

Number of observations per seasonal cycle (e.g., 12 for monthly data with an annual pattern).

Type:

int

model

"additive" or "multiplicative".

Type:

str

method

"classical" or "stl".

Type:

str

strength_trend

Wang et al. (2006) trend strength: max(0, 1 Var(R) / Var(T + R)). In [0, 1].

Type:

float

strength_seasonal

Wang et al. (2006) seasonality strength: max(0, 1 Var(R) / Var(S + R)). In [0, 1].

Type:

float

n_obs_used

Number of non-NaN residual observations used for strength metrics.

Type:

int

original: TimeSeries
trend: TimeSeries
seasonal: TimeSeries
residual: TimeSeries
period: int
model: str
method: str
strength_trend: float
strength_seasonal: float
n_obs_used: int
to_dataframe()[source]

Return all four components as a pandas.DataFrame.

Returns:

Columns: observed, trend, seasonal, residual.

Return type:

pandas.DataFrame

Examples

>>> import numpy as np, pandas as pd
>>> from tseda import TimeSeries
>>> from tseda.decomposition.classical import ClassicalDecomposer
>>> idx = pd.date_range("2020", periods=36, freq="MS")
>>> ts  = TimeSeries(np.ones(36) + np.tile(np.arange(12), 3), index=idx)
>>> df  = ClassicalDecomposer().decompose(ts, period=12).to_dataframe()
>>> list(df.columns)
['observed', 'trend', 'seasonal', 'residual']
summary()[source]

Return a plain-text summary of the decomposition.

Return type:

str

__repr__()[source]

Return repr(self).

Return type:

str

__init__(original, trend, seasonal, residual, period, model, method, strength_trend, strength_seasonal, n_obs_used)
Parameters:
Return type:

None

class tseda.decomposition.classical.ClassicalDecomposer[source]

Bases: object

Decompose a TimeSeries using the centered moving-average (classical) method.

The decomposer is stateless — one instance may be reused.

decompose(ts, period, model)[source]

Return a DecompositionResult.

Parameters:
Return type:

DecompositionResult

Notes

The classical method has well-known limitations:

  • The trend component has NaN at both ends (half the period width).

  • It assumes a fixed seasonal pattern throughout the series.

  • It can be sensitive to outliers.

For more robust results, use STLDecomposer.

Examples

Additive decomposition:

>>> import numpy as np, pandas as pd
>>> from tseda import TimeSeries
>>> from tseda.decomposition.classical import ClassicalDecomposer
>>> rng  = np.random.default_rng(1)
>>> n    = 48
>>> seas = np.tile(np.sin(2 * np.pi * np.arange(12) / 12) * 5, 4)
>>> y    = np.arange(n, dtype=float) * 0.3 + seas + rng.standard_normal(n)
>>> idx  = pd.date_range("2020-01", periods=n, freq="MS")
>>> ts   = TimeSeries(y, index=idx)
>>> r    = ClassicalDecomposer().decompose(ts, period=12)
>>> r.model
'additive'
>>> r.strength_seasonal > 0.5
True
decompose(ts, period=None, *, model='additive')[source]

Decompose ts into trend, seasonal, and residual components.

Parameters:
  • ts (TimeSeries) – Input series. Should be regularly spaced and have length >= 2 × period.

  • period (int, optional) – Seasonal period (number of observations per cycle). When omitted the period is inferred from ts.freq: daily → 7, monthly → 12, quarterly → 4, etc.

  • model (str, optional) – "additive" (default) or "multiplicative".

Return type:

DecompositionResult

Raises:
  • TypeError – If ts is not a TimeSeries.

  • ValueError – If period cannot be inferred, is < 2, or the series is too short; also if model is not recognised.

Examples

>>> import numpy as np, pandas as pd
>>> from tseda import TimeSeries
>>> from tseda.decomposition.classical import ClassicalDecomposer
>>> idx = pd.date_range("2020", periods=36, freq="MS")
>>> y   = np.tile(np.arange(12, dtype=float), 3) + np.linspace(0, 6, 36)
>>> ts  = TimeSeries(y, index=idx)
>>> r   = ClassicalDecomposer().decompose(ts, period=12)
>>> r.seasonal.n
36
>>> r.trend.n
36

STL (Seasonal-Trend decomposition using LOESS) for time series.

STL (Cleveland et al., 1990) is preferred over the classical moving-average approach because it:

  • Fills the trend at the edges (no NaN border effects).

  • Allows the seasonal component to evolve slowly over time.

  • Provides a robust fitting option that down-weights outliers.

  • Handles any period length, including non-integer values.

Primary path — delegates to statsmodels.tsa.seasonal.STL when statsmodels is installed (pip install timeseries-eda[stats]).

Fallback path — when statsmodels is absent, a simplified iterative decomposition is used:

  1. Trend via Savitzky-Golay smoothing (scipy.signal.savgol_filter()).

  2. Seasonal component via per-phase averaging (same as classical).

  3. Residual = y trend seasonal.

The fallback is clearly labelled "stl-fallback" in method.

Classes

STLDecomposer

Stateless decomposer.

Notes

STL always produces an additive decomposition. For multiplicative behaviour apply log() before decomposing and exponentiate the components afterwards.

Examples

>>> import numpy as np, pandas as pd
>>> from tseda import TimeSeries
>>> from tseda.decomposition.stl import STLDecomposer

Monthly temperature with annual seasonality:

>>> rng  = np.random.default_rng(0)
>>> n    = 60
>>> seas = np.tile(np.array([3, 5, 8, 12, 16, 19, 21, 20, 16, 11, 6, 3], dtype=float), 5)
>>> y    = seas + np.linspace(0, 3, n) + rng.standard_normal(n) * 0.3
>>> idx  = pd.date_range("2020-01", periods=n, freq="MS")
>>> ts   = TimeSeries(y, index=idx, name="temp", unit="°C")
>>> r    = STLDecomposer().decompose(ts, period=12)
>>> r.method in ("stl", "stl-fallback")
True
>>> r.trend.n == ts.n
True
class tseda.decomposition.stl.STLDecomposer[source]

Bases: object

Decompose a TimeSeries using the STL algorithm.

The decomposer is stateless — one instance may be reused.

decompose(ts, period, robust, seasonal_deg, trend_deg)[source]

Return a DecompositionResult.

Parameters:
Return type:

DecompositionResult

Examples

>>> import numpy as np, pandas as pd
>>> from tseda import TimeSeries
>>> from tseda.decomposition.stl import STLDecomposer
>>> rng  = np.random.default_rng(2)
>>> n    = 48
>>> seas = np.tile(np.sin(2 * np.pi * np.arange(12) / 12) * 8, 4)
>>> y    = np.linspace(0, 10, n) + seas + rng.standard_normal(n) * 0.3
>>> idx  = pd.date_range("2020-01", periods=n, freq="MS")
>>> ts   = TimeSeries(y, index=idx)
>>> r    = STLDecomposer().decompose(ts, period=12)
>>> r.strength_seasonal > 0.7
True
decompose(ts, period=None, *, robust=True, seasonal_deg=1, trend_deg=1)[source]

Decompose ts using STL.

Parameters:
  • ts (TimeSeries) – Input series. Must be regularly spaced and length >= 2 × period.

  • period (int, optional) – Seasonal period. Inferred from ts.freq when omitted.

  • robust (bool, optional) – When True (default), use robust LOESS fitting to down-weight outliers. Has no effect on the fallback path.

  • seasonal_deg (int, optional) – Polynomial degree for seasonal LOESS smoother (0 or 1). Passed to statsmodels.tsa.seasonal.STL. Default 1.

  • trend_deg (int, optional) – Polynomial degree for trend LOESS smoother (0 or 1). Default 1.

Returns:

method is "stl" when statsmodels is used, or "stl-fallback" otherwise.

Return type:

DecompositionResult

Raises:

Examples

>>> import numpy as np, pandas as pd
>>> from tseda import TimeSeries
>>> from tseda.decomposition.stl import STLDecomposer
>>> rng = np.random.default_rng(3)
>>> idx = pd.date_range("2020-01", periods=36, freq="MS")
>>> seas = np.tile(np.arange(12, dtype=float), 3)
>>> ts  = TimeSeries(seas + rng.standard_normal(36) * 0.2, index=idx)
>>> r   = STLDecomposer().decompose(ts, period=12)
>>> r.residual.n
36