Skip to content

Commit 8721eb7

Browse files
feat(altair): implement line-loss-training (#2909)
## Implementation: `line-loss-training` - altair Implements the **altair** version of `line-loss-training`. **File:** `plots/line-loss-training/implementations/altair.py` **Parent Issue:** #2860 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20608675031)* --------- 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 de72986 commit 8721eb7

2 files changed

Lines changed: 130 additions & 0 deletions

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
""" pyplots.ai
2+
line-loss-training: Training Loss Curve
3+
Library: altair 6.0.0 | Python 3.13.11
4+
Quality: 91/100 | Created: 2025-12-31
5+
"""
6+
7+
import altair as alt
8+
import numpy as np
9+
import pandas as pd
10+
11+
12+
# Data - Simulating neural network training loss curves
13+
np.random.seed(42)
14+
epochs = np.arange(1, 51)
15+
16+
# Training loss: exponential decay with noise (continues decreasing)
17+
train_loss = 2.5 * np.exp(-0.08 * epochs) + 0.15 + np.random.normal(0, 0.02, len(epochs))
18+
19+
# Validation loss: decay then overfitting (U-shape after minimum)
20+
val_base = 2.5 * np.exp(-0.07 * epochs) + 0.35
21+
val_loss = val_base + np.random.normal(0, 0.025, len(epochs))
22+
# Add overfitting: loss increases after epoch 25
23+
val_loss[25:] = val_loss[25:] + np.linspace(0, 0.35, 25)
24+
25+
# Find minimum validation loss epoch for annotation
26+
min_val_epoch = epochs[np.argmin(val_loss)]
27+
min_val_loss = np.min(val_loss)
28+
29+
# Create DataFrame in long format for Altair
30+
df = pd.DataFrame(
31+
{
32+
"Epoch": np.tile(epochs, 2),
33+
"Loss": np.concatenate([train_loss, val_loss]),
34+
"Type": ["Training Loss"] * len(epochs) + ["Validation Loss"] * len(epochs),
35+
}
36+
)
37+
38+
# Point for minimum validation loss annotation
39+
min_point_df = pd.DataFrame({"Epoch": [min_val_epoch], "Loss": [min_val_loss], "Type": ["Optimal Stopping Point"]})
40+
41+
# Base line chart
42+
lines = (
43+
alt.Chart(df)
44+
.mark_line(strokeWidth=3)
45+
.encode(
46+
x=alt.X("Epoch:Q", title="Epoch", axis=alt.Axis(labelFontSize=18, titleFontSize=22)),
47+
y=alt.Y("Loss:Q", title="Cross-Entropy Loss", axis=alt.Axis(labelFontSize=18, titleFontSize=22)),
48+
color=alt.Color(
49+
"Type:N",
50+
scale=alt.Scale(domain=["Training Loss", "Validation Loss"], range=["#306998", "#FFD43B"]),
51+
legend=alt.Legend(title="Curve Type", labelFontSize=16, titleFontSize=18),
52+
),
53+
)
54+
)
55+
56+
# Add points on lines for visibility
57+
points = (
58+
alt.Chart(df)
59+
.mark_point(size=60, filled=True)
60+
.encode(
61+
x="Epoch:Q",
62+
y="Loss:Q",
63+
color=alt.Color(
64+
"Type:N",
65+
scale=alt.Scale(domain=["Training Loss", "Validation Loss"], range=["#306998", "#FFD43B"]),
66+
legend=None,
67+
),
68+
)
69+
)
70+
71+
# Annotation for minimum validation loss
72+
min_marker = (
73+
alt.Chart(min_point_df)
74+
.mark_point(size=300, shape="diamond", filled=True, color="#E63946")
75+
.encode(x="Epoch:Q", y="Loss:Q")
76+
)
77+
78+
# Text annotation for optimal stopping point
79+
min_text = (
80+
alt.Chart(min_point_df)
81+
.mark_text(align="left", dx=12, dy=-10, fontSize=16, fontWeight="bold", color="#E63946")
82+
.encode(x="Epoch:Q", y="Loss:Q", text=alt.value(f"Min Val Loss (Epoch {min_val_epoch})"))
83+
)
84+
85+
# Combine all layers
86+
chart = (
87+
(lines + points + min_marker + min_text)
88+
.properties(
89+
width=1600,
90+
height=900,
91+
title=alt.Title("line-loss-training · altair · pyplots.ai", fontSize=28, anchor="middle"),
92+
)
93+
.configure_axis(labelFontSize=18, titleFontSize=22, gridOpacity=0.3)
94+
.configure_legend(labelFontSize=16, titleFontSize=18)
95+
.configure_view(strokeWidth=0)
96+
)
97+
98+
# Save as PNG (4800 x 2700 with scale_factor=3)
99+
chart.save("plot.png", scale_factor=3.0)
100+
101+
# Save as HTML for interactivity
102+
chart.save("plot.html")
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
library: altair
2+
specification_id: line-loss-training
3+
created: '2025-12-31T00:11:50Z'
4+
updated: '2025-12-31T00:16:02Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20608675031
7+
issue: 2860
8+
python_version: 3.13.11
9+
library_version: 6.0.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/line-loss-training/altair/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/line-loss-training/altair/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/line-loss-training/altair/plot.html
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent implementation of overfitting visualization with validation loss diverging
17+
from training loss after epoch 25
18+
- Clean use of Altair's layered composition with separate chart objects for lines,
19+
points, and annotations
20+
- Red diamond marker with annotation clearly indicates the optimal stopping point
21+
at minimum validation loss
22+
- Colorblind-safe color scheme with strong contrast between blue training and yellow
23+
validation curves
24+
- Proper title format and axis labeling including loss function type as specified
25+
weaknesses:
26+
- Legend title "Curve Type" is generic; could be more descriptive
27+
- Points on every epoch (50 points per curve) add visual noise; could use opacity
28+
or fewer markers

0 commit comments

Comments
 (0)