Skip to content

Commit 62374b1

Browse files
feat(letsplot): implement timeseries-decomposition (#3037)
## Implementation: `timeseries-decomposition` - letsplot Implements the **letsplot** version of `timeseries-decomposition`. **File:** `plots/timeseries-decomposition/implementations/letsplot.py` **Parent Issue:** #2992 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20617485300)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent f385917 commit 62374b1

2 files changed

Lines changed: 178 additions & 0 deletions

File tree

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
""" pyplots.ai
2+
timeseries-decomposition: Time Series Decomposition Plot
3+
Library: letsplot 4.8.2 | Python 3.13.11
4+
Quality: 91/100 | Created: 2025-12-31
5+
"""
6+
7+
import os
8+
import shutil
9+
10+
import numpy as np
11+
import pandas as pd
12+
from lets_plot import (
13+
LetsPlot,
14+
aes,
15+
element_blank,
16+
element_text,
17+
geom_line,
18+
gggrid,
19+
ggplot,
20+
ggsave,
21+
ggsize,
22+
ggtitle,
23+
labs,
24+
theme,
25+
theme_minimal,
26+
)
27+
from statsmodels.tsa.seasonal import seasonal_decompose
28+
29+
30+
LetsPlot.setup_html()
31+
32+
# Data: Monthly temperature readings over 5 years (60 months)
33+
np.random.seed(42)
34+
n_months = 60
35+
dates = pd.date_range("2019-01-01", periods=n_months, freq="MS")
36+
37+
# Create realistic temperature data with trend, seasonality, and noise
38+
trend = np.linspace(15, 18, n_months) # Gradual warming trend
39+
seasonal = 12 * np.sin(2 * np.pi * np.arange(n_months) / 12) # Annual cycle
40+
noise = np.random.normal(0, 1.5, n_months)
41+
values = trend + seasonal + noise
42+
43+
# Create DataFrame for decomposition
44+
df_ts = pd.DataFrame({"date": dates, "value": values})
45+
df_ts = df_ts.set_index("date")
46+
47+
# Perform seasonal decomposition (additive model)
48+
decomposition = seasonal_decompose(df_ts["value"], model="additive", period=12)
49+
50+
# Extract components and create plotting DataFrames
51+
df_original = pd.DataFrame({"date": dates, "value": values, "component": "Original"})
52+
53+
df_trend = pd.DataFrame({"date": dates, "value": decomposition.trend, "component": "Trend"})
54+
55+
df_seasonal = pd.DataFrame({"date": dates, "value": decomposition.seasonal, "component": "Seasonal"})
56+
57+
df_residual = pd.DataFrame({"date": dates, "value": decomposition.resid, "component": "Residual"})
58+
59+
# Combine all components
60+
df_all = pd.concat([df_original, df_trend, df_seasonal, df_residual])
61+
62+
# Convert date to string for plotting
63+
df_all["date_str"] = df_all["date"].dt.strftime("%Y-%m")
64+
65+
# Create individual plots for each component
66+
colors = {"Original": "#306998", "Trend": "#DC2626", "Seasonal": "#059669", "Residual": "#7C3AED"}
67+
68+
# Plot 1: Original Series
69+
p1 = (
70+
ggplot(df_original, aes(x="date", y="value"))
71+
+ geom_line(color="#306998", size=1.2)
72+
+ labs(x="", y="Temperature (°C)", title="Original Series")
73+
+ theme_minimal()
74+
+ theme(
75+
plot_title=element_text(size=20, face="bold"),
76+
axis_title=element_text(size=16),
77+
axis_text=element_text(size=14),
78+
axis_text_x=element_blank(),
79+
)
80+
+ ggsize(1600, 200)
81+
)
82+
83+
# Plot 2: Trend Component
84+
p2 = (
85+
ggplot(df_trend.dropna(), aes(x="date", y="value"))
86+
+ geom_line(color="#DC2626", size=1.2)
87+
+ labs(x="", y="Temperature (°C)", title="Trend")
88+
+ theme_minimal()
89+
+ theme(
90+
plot_title=element_text(size=20, face="bold"),
91+
axis_title=element_text(size=16),
92+
axis_text=element_text(size=14),
93+
axis_text_x=element_blank(),
94+
)
95+
+ ggsize(1600, 200)
96+
)
97+
98+
# Plot 3: Seasonal Component
99+
p3 = (
100+
ggplot(df_seasonal, aes(x="date", y="value"))
101+
+ geom_line(color="#059669", size=1.2)
102+
+ labs(x="", y="Temperature (°C)", title="Seasonal")
103+
+ theme_minimal()
104+
+ theme(
105+
plot_title=element_text(size=20, face="bold"),
106+
axis_title=element_text(size=16),
107+
axis_text=element_text(size=14),
108+
axis_text_x=element_blank(),
109+
)
110+
+ ggsize(1600, 200)
111+
)
112+
113+
# Plot 4: Residual Component
114+
p4 = (
115+
ggplot(df_residual.dropna(), aes(x="date", y="value"))
116+
+ geom_line(color="#7C3AED", size=1.2)
117+
+ labs(x="Date", y="Temperature (°C)", title="Residual")
118+
+ theme_minimal()
119+
+ theme(
120+
plot_title=element_text(size=20, face="bold"),
121+
axis_title=element_text(size=16),
122+
axis_text=element_text(size=14),
123+
axis_text_x=element_text(angle=45),
124+
)
125+
+ ggsize(1600, 200)
126+
)
127+
128+
# Create combined plot using gggrid
129+
combined = gggrid([p1, p2, p3, p4], ncol=1)
130+
131+
# Add overall title
132+
final_plot = (
133+
combined
134+
+ ggsize(1600, 900)
135+
+ ggtitle("timeseries-decomposition · letsplot · pyplots.ai")
136+
+ theme(plot_title=element_text(size=24, face="bold"))
137+
)
138+
139+
# Save as PNG with scale for 4800x2700 resolution
140+
ggsave(final_plot, "plot.png", scale=3)
141+
142+
# Save HTML for interactive version
143+
ggsave(final_plot, "plot.html")
144+
145+
# Move files from lets-plot subdirectory to current directory
146+
lp_dir = "lets-plot-images"
147+
if os.path.exists(lp_dir):
148+
for f in ["plot.png", "plot.html"]:
149+
src = os.path.join(lp_dir, f)
150+
if os.path.exists(src):
151+
shutil.move(src, f)
152+
os.rmdir(lp_dir)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
library: letsplot
2+
specification_id: timeseries-decomposition
3+
created: '2025-12-31T10:57:55Z'
4+
updated: '2025-12-31T11:08:49Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20617485300
7+
issue: 2992
8+
python_version: 3.13.11
9+
library_version: 4.8.2
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/timeseries-decomposition/letsplot/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/timeseries-decomposition/letsplot/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/timeseries-decomposition/letsplot/plot.html
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent use of gggrid() to create vertically stacked decomposition panels
17+
- Clean, colorblind-safe color palette with distinct colors for each component
18+
- Proper use of statsmodels seasonal_decompose for actual decomposition
19+
- Good realistic temperature scenario with clear seasonal patterns
20+
- Correct title format following pyplots.ai conventions
21+
- Properly handles NaN values from decomposition by using dropna()
22+
weaknesses:
23+
- Grid lines are too subtle and barely visible - could increase alpha for better
24+
readability
25+
- File handling requires moving files from lets-plot-images subdirectory (workaround
26+
needed)

0 commit comments

Comments
 (0)