Skip to content

Commit 7766286

Browse files
feat(plotly): implement residual-plot (#2356)
## Implementation: `residual-plot` - plotly Implements the **plotly** version of `residual-plot`. **File:** `plots/residual-plot/implementations/plotly.py` --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20528203388)* --------- 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 baa1264 commit 7766286

2 files changed

Lines changed: 185 additions & 0 deletions

File tree

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
""" pyplots.ai
2+
residual-plot: Residual Plot
3+
Library: plotly 6.5.0 | Python 3.13.11
4+
Quality: 92/100 | Created: 2025-12-26
5+
"""
6+
7+
import numpy as np
8+
import plotly.graph_objects as go
9+
10+
11+
# Data - Generate realistic regression scenario with varying residual patterns
12+
np.random.seed(42)
13+
n_samples = 150
14+
15+
# Create features with some non-linearity to show interesting residual patterns
16+
X = np.linspace(0, 10, n_samples)
17+
# True relationship with slight curvature (linear model will miss this)
18+
y_true = 2 * X + 0.3 * X**1.5 + np.random.randn(n_samples) * 2
19+
20+
# Simple linear regression (manual fit)
21+
X_mean = np.mean(X)
22+
y_mean = np.mean(y_true)
23+
slope = np.sum((X - X_mean) * (y_true - y_mean)) / np.sum((X - X_mean) ** 2)
24+
intercept = y_mean - slope * X_mean
25+
y_pred = slope * X + intercept
26+
27+
# Calculate residuals
28+
residuals = y_true - y_pred
29+
std_residuals = np.std(residuals)
30+
31+
# Identify outliers (beyond ±2 standard deviations)
32+
outlier_mask = np.abs(residuals) > 2 * std_residuals
33+
normal_mask = ~outlier_mask
34+
35+
# Create figure
36+
fig = go.Figure()
37+
38+
# Add ±2 standard deviation bands (dashed lines)
39+
fig.add_trace(
40+
go.Scatter(
41+
x=[y_pred.min(), y_pred.max()],
42+
y=[2 * std_residuals, 2 * std_residuals],
43+
mode="lines",
44+
line=dict(color="rgba(255, 212, 59, 0.7)", width=3, dash="dash"),
45+
name="+2 SD",
46+
showlegend=True,
47+
)
48+
)
49+
50+
fig.add_trace(
51+
go.Scatter(
52+
x=[y_pred.min(), y_pred.max()],
53+
y=[-2 * std_residuals, -2 * std_residuals],
54+
mode="lines",
55+
line=dict(color="rgba(255, 212, 59, 0.7)", width=3, dash="dash"),
56+
name="-2 SD",
57+
showlegend=True,
58+
)
59+
)
60+
61+
# Add horizontal reference line at y=0
62+
fig.add_trace(
63+
go.Scatter(
64+
x=[y_pred.min(), y_pred.max()],
65+
y=[0, 0],
66+
mode="lines",
67+
line=dict(color="#333333", width=3),
68+
name="Zero Line",
69+
showlegend=False,
70+
)
71+
)
72+
73+
# Add normal residuals
74+
fig.add_trace(
75+
go.Scatter(
76+
x=y_pred[normal_mask],
77+
y=residuals[normal_mask],
78+
mode="markers",
79+
marker=dict(size=14, color="#306998", opacity=0.7, line=dict(width=1, color="#1e4263")),
80+
name="Residuals",
81+
hovertemplate="Fitted: %{x:.2f}<br>Residual: %{y:.2f}<extra></extra>",
82+
)
83+
)
84+
85+
# Add outlier residuals
86+
if np.any(outlier_mask):
87+
fig.add_trace(
88+
go.Scatter(
89+
x=y_pred[outlier_mask],
90+
y=residuals[outlier_mask],
91+
mode="markers",
92+
marker=dict(size=16, color="#FFD43B", opacity=0.9, line=dict(width=2, color="#cc9900"), symbol="diamond"),
93+
name="Outliers (>2 SD)",
94+
hovertemplate="Fitted: %{x:.2f}<br>Residual: %{y:.2f}<extra></extra>",
95+
)
96+
)
97+
98+
# Add smoothing line to detect patterns (moving average with numpy)
99+
sorted_indices = np.argsort(y_pred)
100+
window_size = 15
101+
kernel = np.ones(window_size) / window_size
102+
smoothed_residuals = np.convolve(residuals[sorted_indices], kernel, mode="same")
103+
104+
fig.add_trace(
105+
go.Scatter(
106+
x=y_pred[sorted_indices],
107+
y=smoothed_residuals,
108+
mode="lines",
109+
line=dict(color="#cc4444", width=4),
110+
name="Trend Line",
111+
hovertemplate="Fitted: %{x:.2f}<br>Smoothed Residual: %{y:.2f}<extra></extra>",
112+
)
113+
)
114+
115+
# Update layout
116+
fig.update_layout(
117+
title=dict(
118+
text="residual-plot · plotly · pyplots.ai", font=dict(size=32, color="#333333"), x=0.5, xanchor="center"
119+
),
120+
xaxis=dict(
121+
title=dict(text="Fitted Values", font=dict(size=24)),
122+
tickfont=dict(size=18),
123+
showgrid=True,
124+
gridwidth=1,
125+
gridcolor="rgba(0,0,0,0.1)",
126+
zeroline=False,
127+
),
128+
yaxis=dict(
129+
title=dict(text="Residuals (y_true - y_pred)", font=dict(size=24)),
130+
tickfont=dict(size=18),
131+
showgrid=True,
132+
gridwidth=1,
133+
gridcolor="rgba(0,0,0,0.1)",
134+
zeroline=False,
135+
),
136+
template="plotly_white",
137+
legend=dict(
138+
font=dict(size=18),
139+
x=0.02,
140+
y=0.98,
141+
xanchor="left",
142+
yanchor="top",
143+
bgcolor="rgba(255,255,255,0.8)",
144+
bordercolor="rgba(0,0,0,0.2)",
145+
borderwidth=1,
146+
),
147+
margin=dict(l=100, r=50, t=100, b=80),
148+
plot_bgcolor="white",
149+
)
150+
151+
# Save as PNG (4800 x 2700 px)
152+
fig.write_image("plot.png", width=1600, height=900, scale=3)
153+
154+
# Save as HTML for interactivity
155+
fig.write_html("plot.html", include_plotlyjs="cdn")
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
library: plotly
2+
specification_id: residual-plot
3+
created: '2025-12-26T19:37:08Z'
4+
updated: '2025-12-26T19:43:44Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20528203388
7+
issue: 0
8+
python_version: 3.13.11
9+
library_version: 6.5.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/residual-plot/plotly/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/residual-plot/plotly/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/residual-plot/plotly/plot.html
13+
quality_score: 92
14+
review:
15+
strengths:
16+
- 'Excellent implementation of all key residual plot features: zero reference line,
17+
±2 SD bands, outlier highlighting, and smoothing trend line'
18+
- Good color scheme with distinct visual encoding for normal residuals (blue circles),
19+
outliers (yellow diamonds), and trend (red line)
20+
- Interactive hover templates provide useful diagnostic information (fitted value
21+
and residual)
22+
- Data generation creates interesting non-linear pattern that demonstrates the diagnostic
23+
purpose of residual plots
24+
- Clean code structure following KISS principles
25+
weaknesses:
26+
- Legend shows +2 SD and -2 SD as separate entries; could be combined into single
27+
±2 SD Bounds entry
28+
- Axis labels lack units (though residuals are unitless, y-axis label is verbose)
29+
- Moving average smoothing could benefit from using a more sophisticated LOWESS
30+
approach for smoother trend detection

0 commit comments

Comments
 (0)