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:
objectImmutable time series decomposition result.
- Parameters:
original (TimeSeries)
trend (TimeSeries)
seasonal (TimeSeries)
residual (TimeSeries)
period (int)
model (str)
method (str)
strength_trend (float)
strength_seasonal (float)
n_obs_used (int)
- original
The input series.
- Type:
- trend
Smooth trend component. May contain NaN at the edges (classical decomposition only; STL fills the edges with LOESS extrapolation).
- Type:
- seasonal
Periodic seasonal component with the same length as original.
- Type:
- residual
Remainder after removing trend and seasonal. NaN wherever trend is NaN.
- Type:
- period
Number of observations per seasonal cycle (e.g., 12 for monthly data with an annual pattern).
- Type:
- strength_trend
Wang et al. (2006) trend strength:
max(0, 1 − Var(R) / Var(T + R)). In [0, 1].- Type:
- strength_seasonal
Wang et al. (2006) seasonality strength:
max(0, 1 − Var(R) / Var(S + R)). In [0, 1].- Type:
- original: TimeSeries
- trend: TimeSeries
- seasonal: TimeSeries
- residual: TimeSeries
- to_dataframe()[source]
Return all four components as a
pandas.DataFrame.- Returns:
Columns:
observed,trend,seasonal,residual.- Return type:
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']
- __init__(original, trend, seasonal, residual, period, model, method, strength_trend, strength_seasonal, n_obs_used)
- Parameters:
original (TimeSeries)
trend (TimeSeries)
seasonal (TimeSeries)
residual (TimeSeries)
period (int)
model (str)
method (str)
strength_trend (float)
strength_seasonal (float)
n_obs_used (int)
- Return type:
None
- class tseda.decomposition.ClassicalDecomposer[source]
Bases:
objectDecompose a
TimeSeriesusing the centered moving-average (classical) method.The decomposer is stateless — one instance may be reused.
- decompose(ts, period, model)[source]
Return a
DecompositionResult.- Parameters:
ts (TimeSeries)
period (int | None)
model (str)
- Return type:
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:
- 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:
objectDecompose a
TimeSeriesusing 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:
ts (TimeSeries)
period (int | None)
robust (bool)
seasonal_deg (int)
trend_deg (int)
- Return type:
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.freqwhen 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:
methodis"stl"when statsmodels is used, or"stl-fallback"otherwise.- Return type:
- Raises:
TypeError – If ts is not a
TimeSeries.ValueError – If period cannot be inferred, is < 2, or the series is too short.
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:
Additive —
y = T + S + R(default)Multiplicative —
y = 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:
objectImmutable time series decomposition result.
- Parameters:
original (TimeSeries)
trend (TimeSeries)
seasonal (TimeSeries)
residual (TimeSeries)
period (int)
model (str)
method (str)
strength_trend (float)
strength_seasonal (float)
n_obs_used (int)
- original
The input series.
- Type:
- trend
Smooth trend component. May contain NaN at the edges (classical decomposition only; STL fills the edges with LOESS extrapolation).
- Type:
- seasonal
Periodic seasonal component with the same length as original.
- Type:
- residual
Remainder after removing trend and seasonal. NaN wherever trend is NaN.
- Type:
- period
Number of observations per seasonal cycle (e.g., 12 for monthly data with an annual pattern).
- Type:
- strength_trend
Wang et al. (2006) trend strength:
max(0, 1 − Var(R) / Var(T + R)). In [0, 1].- Type:
- strength_seasonal
Wang et al. (2006) seasonality strength:
max(0, 1 − Var(R) / Var(S + R)). In [0, 1].- Type:
- original: TimeSeries
- trend: TimeSeries
- seasonal: TimeSeries
- residual: TimeSeries
- to_dataframe()[source]
Return all four components as a
pandas.DataFrame.- Returns:
Columns:
observed,trend,seasonal,residual.- Return type:
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']
- __init__(original, trend, seasonal, residual, period, model, method, strength_trend, strength_seasonal, n_obs_used)
- Parameters:
original (TimeSeries)
trend (TimeSeries)
seasonal (TimeSeries)
residual (TimeSeries)
period (int)
model (str)
method (str)
strength_trend (float)
strength_seasonal (float)
n_obs_used (int)
- Return type:
None
- class tseda.decomposition.classical.ClassicalDecomposer[source]
Bases:
objectDecompose a
TimeSeriesusing the centered moving-average (classical) method.The decomposer is stateless — one instance may be reused.
- decompose(ts, period, model)[source]
Return a
DecompositionResult.- Parameters:
ts (TimeSeries)
period (int | None)
model (str)
- Return type:
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:
- 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:
Trend via Savitzky-Golay smoothing (
scipy.signal.savgol_filter()).Seasonal component via per-phase averaging (same as classical).
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:
objectDecompose a
TimeSeriesusing 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:
ts (TimeSeries)
period (int | None)
robust (bool)
seasonal_deg (int)
trend_deg (int)
- Return type:
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.freqwhen 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:
methodis"stl"when statsmodels is used, or"stl-fallback"otherwise.- Return type:
- Raises:
TypeError – If ts is not a
TimeSeries.ValueError – If period cannot be inferred, is < 2, or the series is too short.
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