02-decompose
notebooks/02-decompose.py
Decompose — trend, seasonality, residuals
Loads the Parquet artifact from 01-explore.py, splits the series into
three components using classical additive decomposition:
[ y_t = T_t + S_t + R_t ]
where the trend T is a centered 7-day rolling mean, the seasonality S is
the average detrended value by weekday, and the residual R is whatever's
left. All three are persisted as artifacts.
import pandas as pd import jellycell.api as jc df: pd.DataFrame = jc.load("artifacts/daily.parquet") df = df.set_index("date") print(df.head())
value date 2025-01-01 100.914151 2025-01-02 96.934993 2025-01-03 102.361244 2025-01-04 107.986529 2025-01-05 99.366675
loaded
ok
5 ms
trend = df["value"].rolling(7, center=True).mean() print(f"trend span: {trend.first_valid_index().date()} \u2013 {trend.last_valid_index().date()}")
trend span: 2025-01-04 – 2025-12-28
trend
ok
8 ms
detrended = df["value"] - trend by_dow = detrended.groupby(detrended.index.dayofweek).mean() seasonality = pd.Series(by_dow.loc[df.index.dayofweek].values, index=df.index) print("mean seasonal effect by weekday:") print( pd.DataFrame( {"dow": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], "effect": by_dow.round(2).values} ).to_string(index=False) )
mean seasonal effect by weekday: dow effect Mon -3.29 Tue -0.93 Wed -0.93 Thu -0.83 Fri -1.11 Sat 4.28 Sun 2.80
trend seasonality
ok
4 ms
residuals = df["value"] - trend - seasonality print(f"residual std: {residuals.std():.3f}") print(f"residual mean: {residuals.mean():.3f} (should be near 0)")
residual std: 2.618 residual mean: -0.000 (should be near 0)
trend seasonality residuals
ok
8 ms
decomposed = pd.DataFrame( { "observed": df["value"], "trend": trend, "seasonal": seasonality, "residual": residuals, } ).reset_index() jc.save(decomposed, "artifacts/decomposed.parquet")
PosixPath('/Users/blaise/Desktop/blaise-oss/jellycell/examples/timeseries/artifacts/decomposed.parquet')
artifacts
decomposed.parquet
16.5 KB
persist
ok
390 ms
import matplotlib.pyplot as plt fig, axes = plt.subplots(4, 1, figsize=(10, 9), sharex=True) plots = [ ("observed", df["value"], "#1a1a1a"), ("trend", trend, "#4f46e5"), ("seasonal", seasonality, "#0891b2"), ("residual", residuals, "#dc2626"), ] for ax, (label, series, color) in zip(axes, plots, strict=True): ax.plot(series.index, series.values, linewidth=1.1, color=color) ax.set_ylabel(label) ax.grid(alpha=0.3) axes[0].set_title("Additive decomposition") axes[-1].set_xlabel("Date") fig.tight_layout() jc.figure(path="artifacts/decomposition.png", fig=fig)
PosixPath('/Users/blaise/Desktop/blaise-oss/jellycell/examples/timeseries/artifacts/decomposition.png')
<Figure size 1000x900 with 4 Axes>
artifacts
decomposition.png
172.9 KB