|
1 | 1 | """ anyplot.ai |
2 | 2 | scatter-regression-linear: Scatter Plot with Linear Regression |
3 | | -Library: matplotlib 3.10.9 | Python 3.13.13 |
4 | | -Quality: 93/100 | Updated: 2026-05-06 |
| 3 | +Library: matplotlib 3.11.1 | Python 3.13.14 |
| 4 | +Quality: 92/100 | Updated: 2026-08-05 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import os |
8 | 8 |
|
9 | 9 | import matplotlib.pyplot as plt |
10 | 10 | import numpy as np |
| 11 | +from matplotlib.transforms import blended_transform_factory |
11 | 12 |
|
12 | 13 |
|
13 | | -# Theme tokens |
| 14 | +# Theme tokens (Imprint) |
14 | 15 | THEME = os.getenv("ANYPLOT_THEME", "light") |
15 | 16 | PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
16 | 17 | ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420" |
17 | 18 | INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
18 | 19 | INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
19 | | -BRAND = "#009E73" # Okabe-Ito position 1 |
20 | | -SECONDARY = "#C475FD" # Okabe-Ito position 2 for regression line |
| 20 | +BRAND = "#009E73" # Imprint palette position 1 |
| 21 | +SECONDARY = "#C475FD" # Imprint palette position 2, for regression line |
21 | 22 |
|
22 | | -# Data: Study hours vs exam scores (realistic educational context) |
| 23 | +# Data: study hours vs exam scores (realistic educational context) |
23 | 24 | np.random.seed(42) |
24 | 25 | n_points = 80 |
25 | 26 | x = np.random.uniform(1, 10, n_points) # Study hours |
|
31 | 32 | coefficients = np.polyfit(x, y, 1) |
32 | 33 | slope, intercept = coefficients[0], coefficients[1] |
33 | 34 |
|
34 | | -# Calculate R-squared |
| 35 | +# Coefficient of determination |
35 | 36 | y_pred = np.polyval(coefficients, x) |
36 | 37 | ss_res = np.sum((y - y_pred) ** 2) |
37 | 38 | ss_tot = np.sum((y - np.mean(y)) ** 2) |
38 | 39 | r_squared = 1 - (ss_res / ss_tot) |
39 | 40 |
|
| 41 | +# Largest residual: notable outlier worth calling out |
| 42 | +outlier_idx = np.argmax(np.abs(y - y_pred)) |
| 43 | + |
40 | 44 | # Regression line and 95% confidence interval |
41 | 45 | x_line = np.linspace(x.min() - 0.5, x.max() + 0.5, 100) |
42 | 46 | y_line = np.polyval(coefficients, x_line) |
|
45 | 49 | ss_xx = np.sum((x - x_mean) ** 2) |
46 | 50 | se_y = np.sqrt(ss_res / (n_points - 2)) |
47 | 51 | se_line = se_y * np.sqrt(1 / n_points + (x_line - x_mean) ** 2 / ss_xx) |
48 | | -t_val = 1.99 # 95% CI |
| 52 | +t_val = 1.99 # 95% CI, df ~ 78 |
49 | 53 | ci_upper = y_line + t_val * se_line |
50 | 54 | ci_lower = y_line - t_val * se_line |
51 | 55 |
|
52 | 56 | # Plot |
53 | | -fig, ax = plt.subplots(figsize=(16, 9), facecolor=PAGE_BG) |
| 57 | +fig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG) |
54 | 58 | ax.set_facecolor(PAGE_BG) |
55 | 59 |
|
56 | | -# Confidence interval band (using SECONDARY color with reduced alpha) |
57 | | -ax.fill_between(x_line, ci_lower, ci_upper, alpha=0.2, color=SECONDARY, label="95% CI") |
| 60 | +# Confidence interval band (SECONDARY color, low alpha) |
| 61 | +ax.fill_between(x_line, ci_lower, ci_upper, alpha=0.2, color=SECONDARY, label="95% CI", zorder=1) |
58 | 62 |
|
59 | | -# Scatter points (BRAND green as first series) |
60 | | -ax.scatter(x, y, s=200, alpha=0.7, color=BRAND, edgecolors=PAGE_BG, linewidth=0.5, zorder=3) |
| 63 | +# Rug plot: marginal x-distribution along the bottom, drawn in a blended |
| 64 | +# transform (data x, axes y) so it hugs the axis regardless of y-range. |
| 65 | +trans_x = blended_transform_factory(ax.transData, ax.transAxes) |
| 66 | +ax.plot(x, np.full_like(x, 0.015), "|", transform=trans_x, color=BRAND, alpha=0.5, markersize=7, zorder=1) |
61 | 67 |
|
62 | | -# Regression line (using SECONDARY color) |
63 | | -ax.plot(x_line, y_line, color=SECONDARY, linewidth=3, label="Regression Line", zorder=2) |
| 68 | +# Scatter points (BRAND green as first series) |
| 69 | +ax.scatter(x, y, s=130, alpha=0.7, color=BRAND, edgecolors=PAGE_BG, linewidth=0.5, zorder=3) |
| 70 | + |
| 71 | +# Regression line (SECONDARY color) |
| 72 | +ax.plot(x_line, y_line, color=SECONDARY, linewidth=2.5, label="Regression Line", zorder=2) |
| 73 | + |
| 74 | +# Callout for the largest residual, showing how far the point strays from the fit |
| 75 | +ax.annotate( |
| 76 | + "Largest residual", |
| 77 | + xy=(x[outlier_idx], y[outlier_idx]), |
| 78 | + xytext=(x[outlier_idx] + 1.4, y[outlier_idx] + 10), |
| 79 | + fontsize=8, |
| 80 | + color=INK_SOFT, |
| 81 | + arrowprops={"arrowstyle": "->", "color": INK_SOFT, "linewidth": 1}, |
| 82 | + zorder=4, |
| 83 | +) |
64 | 84 |
|
65 | | -# Annotations with theme-adaptive styling |
| 85 | +# Annotation: equation and R-squared |
66 | 86 | equation = f"y = {slope:.2f}x + {intercept:.2f}" |
67 | 87 | r_text = f"R² = {r_squared:.3f}" |
68 | 88 | ax.text( |
69 | | - 0.05, |
70 | | - 0.95, |
| 89 | + 0.04, |
| 90 | + 0.94, |
71 | 91 | f"{equation}\n{r_text}", |
72 | 92 | transform=ax.transAxes, |
73 | | - fontsize=18, |
| 93 | + fontsize=9, |
74 | 94 | verticalalignment="top", |
75 | 95 | color=INK, |
76 | 96 | bbox={"boxstyle": "round,pad=0.4", "facecolor": ELEVATED_BG, "edgecolor": INK_SOFT, "alpha": 0.95}, |
77 | 97 | ) |
78 | 98 |
|
79 | | -# Styling with theme-adaptive chrome |
80 | | -ax.set_xlabel("Study Hours (hrs)", fontsize=20, color=INK) |
81 | | -ax.set_ylabel("Exam Score (points)", fontsize=20, color=INK) |
82 | | -ax.set_title("scatter-regression-linear · matplotlib · anyplot.ai", fontsize=24, fontweight="medium", color=INK) |
83 | | -ax.tick_params(axis="both", labelsize=16, colors=INK_SOFT) |
| 99 | +# Title and axis labels (title short enough to use the default 12pt) |
| 100 | +title = "scatter-regression-linear · python · matplotlib · anyplot.ai" |
| 101 | +ax.set_title(title, fontsize=12, fontweight="medium", color=INK) |
| 102 | +ax.set_xlabel("Study Hours (hrs)", fontsize=10, color=INK) |
| 103 | +ax.set_ylabel("Exam Score (points)", fontsize=10, color=INK) |
| 104 | +ax.tick_params(axis="both", labelsize=8, colors=INK_SOFT) |
84 | 105 |
|
85 | | -# Grid styling (subtle, y-axis preferred) |
86 | | -ax.yaxis.grid(True, alpha=0.15, linewidth=0.8, color=INK) |
| 106 | +# Grid: both axes, subtle (scatter convention) |
| 107 | +ax.grid(True, alpha=0.15, linewidth=0.8, color=INK) |
| 108 | +ax.set_axisbelow(True) |
87 | 109 |
|
88 | 110 | # Spine styling |
89 | 111 | ax.spines["top"].set_visible(False) |
|
92 | 114 | ax.spines[spine].set_color(INK_SOFT) |
93 | 115 |
|
94 | 116 | # Legend styling |
95 | | -leg = ax.legend(fontsize=16, loc="lower right") |
| 117 | +leg = ax.legend(fontsize=8, loc="lower right") |
96 | 118 | if leg: |
97 | 119 | leg.get_frame().set_facecolor(ELEVATED_BG) |
98 | 120 | leg.get_frame().set_edgecolor(INK_SOFT) |
|
101 | 123 | text.set_color(INK_SOFT) |
102 | 124 |
|
103 | 125 | plt.tight_layout() |
104 | | -plt.savefig(f"plot-{THEME}.png", dpi=300, bbox_inches="tight", facecolor=PAGE_BG) |
| 126 | +plt.savefig(f"plot-{THEME}.png", dpi=400, facecolor=PAGE_BG) |
0 commit comments