|
1 | 1 | """ pyplots.ai |
2 | 2 | density-basic: Basic Density Plot |
3 | | -Library: plotly 6.5.0 | Python 3.13.11 |
4 | | -Quality: 92/100 | Created: 2025-12-23 |
| 3 | +Library: plotly 6.5.2 | Python 3.14.3 |
| 4 | +Quality: 93/100 | Updated: 2026-02-23 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import numpy as np |
8 | 8 | import plotly.graph_objects as go |
| 9 | +from scipy.stats import gaussian_kde |
9 | 10 |
|
10 | 11 |
|
11 | | -# Data - Test scores with realistic bimodal distribution |
| 12 | +# Data - SAT Math scores with bimodal distribution |
12 | 13 | np.random.seed(42) |
13 | | -scores = np.concatenate( |
| 14 | +sat_scores = np.concatenate( |
14 | 15 | [ |
15 | | - np.random.normal(72, 10, 300), # Main group around 72 |
16 | | - np.random.normal(88, 5, 100), # High achievers around 88 |
| 16 | + np.random.normal(540, 60, 350), # Main group around 540 |
| 17 | + np.random.normal(680, 35, 150), # High achievers around 680 |
17 | 18 | ] |
18 | 19 | ) |
| 20 | +sat_scores = np.clip(sat_scores, 200, 800) # SAT range |
19 | 21 |
|
20 | | -# Compute KDE using Silverman's rule of thumb for bandwidth |
21 | | -n = len(scores) |
22 | | -std = np.std(scores, ddof=1) |
23 | | -iqr = np.percentile(scores, 75) - np.percentile(scores, 25) |
24 | | -bandwidth = 0.9 * min(std, iqr / 1.34) * n ** (-0.2) |
| 22 | +# KDE using scipy |
| 23 | +kde = gaussian_kde(sat_scores) |
| 24 | +x_grid = np.linspace(350, 800, 500) |
| 25 | +density = kde(x_grid) |
25 | 26 |
|
26 | | -# Evaluate density at each point on a grid |
27 | | -x_range = np.linspace(scores.min() - 10, scores.max() + 10, 500) |
28 | | -density = np.zeros_like(x_range) |
29 | | -for xi in scores: |
30 | | - density += np.exp(-0.5 * ((x_range - xi) / bandwidth) ** 2) |
31 | | -density /= n * bandwidth * np.sqrt(2 * np.pi) |
| 27 | +# Identify peaks for annotations (split at valley ~620 pts) |
| 28 | +split = int(500 * (620 - 350) / (800 - 350)) |
| 29 | +peak1_idx = np.argmax(density[:split]) |
| 30 | +peak2_idx = split + np.argmax(density[split:]) |
| 31 | +peak1_x, peak1_y = x_grid[peak1_idx], density[peak1_idx] |
| 32 | +peak2_x, peak2_y = x_grid[peak2_idx], density[peak2_idx] |
32 | 33 |
|
33 | | -# Create figure |
| 34 | +# Plot |
34 | 35 | fig = go.Figure() |
35 | 36 |
|
36 | | -# Density curve with fill |
37 | 37 | fig.add_trace( |
38 | 38 | go.Scatter( |
39 | | - x=x_range, |
| 39 | + x=x_grid, |
40 | 40 | y=density, |
41 | 41 | mode="lines", |
42 | 42 | fill="tozeroy", |
43 | | - fillcolor="rgba(48, 105, 152, 0.3)", |
44 | | - line={"color": "#306998", "width": 4}, |
| 43 | + fillcolor="rgba(48, 105, 152, 0.25)", |
| 44 | + line={"color": "#306998", "width": 3.5}, |
45 | 45 | name="Density", |
46 | | - hovertemplate="Score: %{x:.1f}<br>Density: %{y:.4f}<extra></extra>", |
| 46 | + hovertemplate="Score: %{x:.0f}<br>Density: %{y:.4f}<extra></extra>", |
47 | 47 | ) |
48 | 48 | ) |
49 | 49 |
|
50 | | -# Rug plot showing individual observations |
| 50 | +# Rug plot |
51 | 51 | fig.add_trace( |
52 | 52 | go.Scatter( |
53 | | - x=scores, |
54 | | - y=[-0.001] * len(scores), |
| 53 | + x=sat_scores, |
| 54 | + y=np.zeros(len(sat_scores)), |
55 | 55 | mode="markers", |
56 | | - marker={"symbol": "line-ns", "size": 12, "color": "#306998", "line": {"width": 1.5}}, |
| 56 | + marker={"symbol": "line-ns", "size": 14, "color": "#306998", "opacity": 0.5, "line": {"width": 1.5}}, |
57 | 57 | name="Observations", |
58 | | - hovertemplate="Score: %{x:.1f}<extra></extra>", |
| 58 | + hovertemplate="Score: %{x:.0f}<extra></extra>", |
59 | 59 | ) |
60 | 60 | ) |
61 | 61 |
|
| 62 | +# Peak annotations to highlight bimodal structure |
| 63 | +fig.add_annotation( |
| 64 | + x=peak1_x, |
| 65 | + y=peak1_y, |
| 66 | + text=f"<b>Primary Peak</b><br>~{peak1_x:.0f} pts", |
| 67 | + showarrow=True, |
| 68 | + arrowhead=2, |
| 69 | + arrowsize=1.2, |
| 70 | + arrowwidth=2, |
| 71 | + arrowcolor="#306998", |
| 72 | + font={"size": 18, "color": "#306998"}, |
| 73 | + ax=-80, |
| 74 | + ay=-50, |
| 75 | + bgcolor="rgba(255, 255, 255, 0.9)", |
| 76 | + borderpad=6, |
| 77 | +) |
| 78 | +fig.add_annotation( |
| 79 | + x=peak2_x, |
| 80 | + y=peak2_y, |
| 81 | + text=f"<b>High Achievers</b><br>~{peak2_x:.0f} pts", |
| 82 | + showarrow=True, |
| 83 | + arrowhead=2, |
| 84 | + arrowsize=1.2, |
| 85 | + arrowwidth=2, |
| 86 | + arrowcolor="#306998", |
| 87 | + font={"size": 18, "color": "#306998"}, |
| 88 | + ax=80, |
| 89 | + ay=-40, |
| 90 | + bgcolor="rgba(255, 255, 255, 0.9)", |
| 91 | + borderpad=6, |
| 92 | +) |
| 93 | + |
62 | 94 | # Layout |
63 | 95 | fig.update_layout( |
64 | 96 | title={"text": "density-basic · plotly · pyplots.ai", "font": {"size": 36}, "x": 0.5, "xanchor": "center"}, |
65 | 97 | xaxis={ |
66 | | - "title": {"text": "Test Score", "font": {"size": 28}}, |
| 98 | + "title": {"text": "SAT Math Score (points)", "font": {"size": 28}}, |
67 | 99 | "tickfont": {"size": 22}, |
68 | | - "showgrid": True, |
69 | | - "gridwidth": 1, |
70 | | - "gridcolor": "rgba(128, 128, 128, 0.2)", |
| 100 | + "showgrid": False, |
71 | 101 | "zeroline": False, |
| 102 | + "showspikes": True, |
| 103 | + "spikemode": "across", |
| 104 | + "spikethickness": 1, |
| 105 | + "spikecolor": "rgba(48, 105, 152, 0.3)", |
| 106 | + "spikedash": "dot", |
72 | 107 | }, |
73 | 108 | yaxis={ |
74 | 109 | "title": {"text": "Density", "font": {"size": 28}}, |
75 | 110 | "tickfont": {"size": 22}, |
76 | | - "showgrid": True, |
| 111 | + "gridcolor": "rgba(128, 128, 128, 0.15)", |
77 | 112 | "gridwidth": 1, |
78 | | - "gridcolor": "rgba(128, 128, 128, 0.2)", |
79 | 113 | "zeroline": False, |
80 | 114 | "rangemode": "tozero", |
81 | 115 | }, |
82 | 116 | template="plotly_white", |
83 | 117 | showlegend=True, |
84 | 118 | legend={ |
85 | 119 | "font": {"size": 20}, |
86 | | - "x": 0.98, |
87 | | - "y": 0.98, |
| 120 | + "x": 0.97, |
| 121 | + "y": 0.95, |
88 | 122 | "xanchor": "right", |
89 | 123 | "yanchor": "top", |
90 | 124 | "bgcolor": "rgba(255, 255, 255, 0.8)", |
| 125 | + "borderwidth": 0, |
91 | 126 | }, |
92 | | - margin={"l": 100, "r": 60, "t": 100, "b": 100}, |
| 127 | + hovermode="x", |
| 128 | + margin={"l": 90, "r": 40, "t": 90, "b": 90}, |
| 129 | + plot_bgcolor="white", |
93 | 130 | ) |
94 | 131 |
|
95 | | -# Save as PNG and HTML |
| 132 | +# Save |
96 | 133 | fig.write_image("plot.png", width=1600, height=900, scale=3) |
97 | | -fig.write_html("plot.html", include_plotlyjs="cdn") |
| 134 | +fig.write_html("plot.html", include_plotlyjs="cdn", config={"displayModeBar": True, "scrollZoom": True}) |
0 commit comments