{"spec_id":"residual-plot","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nresidual-plot: Residual Plot\nLibrary: plotly 6.7.0 | Python 3.13.13\nQuality: 96/100 | Updated: 2026-05-10\n\"\"\"\n\nimport os\nimport sys\n\nimport numpy as np\n\n# Avoid module shadowing by removing current directory from path during import\ncwd = sys.path[0]\nif cwd in sys.path:\n    sys.path.remove(cwd)\nimport plotly.graph_objects as go\n\nsys.path.insert(0, cwd)\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nGRID = \"rgba(26,26,23,0.10)\" if THEME == \"light\" else \"rgba(240,239,232,0.10)\"\n\n# Okabe-Ito palette\nBRAND = \"#009E73\"\nACCENT = \"#C475FD\"\nNEUTRAL = \"#1A1A1A\" if THEME == \"light\" else \"#E8E8E0\"\n\n# Data - Generate realistic regression scenario with varying residual patterns\nnp.random.seed(42)\nn_samples = 150\n\n# Create features with some non-linearity to show interesting residual patterns\nX = np.linspace(0, 10, n_samples)\n# True relationship with slight curvature (linear model will miss this)\ny_true = 2 * X + 0.3 * X**1.5 + np.random.randn(n_samples) * 2\n\n# Simple linear regression (manual fit)\nX_mean = np.mean(X)\ny_mean = np.mean(y_true)\nslope = np.sum((X - X_mean) * (y_true - y_mean)) / np.sum((X - X_mean) ** 2)\nintercept = y_mean - slope * X_mean\ny_pred = slope * X + intercept\n\n# Calculate residuals\nresiduals = y_true - y_pred\nstd_residuals = np.std(residuals)\n\n# Identify outliers (beyond ±2 standard deviations)\noutlier_mask = np.abs(residuals) > 2 * std_residuals\nnormal_mask = ~outlier_mask\n\n# Create figure\nfig = go.Figure()\n\n# Add ±2 standard deviation bands (dashed lines)\nfig.add_trace(\n    go.Scatter(\n        x=[y_pred.min(), y_pred.max()],\n        y=[2 * std_residuals, 2 * std_residuals],\n        mode=\"lines\",\n        line=dict(color=ACCENT, width=3, dash=\"dash\"),\n        name=\"+2 SD\",\n        showlegend=True,\n    )\n)\n\nfig.add_trace(\n    go.Scatter(\n        x=[y_pred.min(), y_pred.max()],\n        y=[-2 * std_residuals, -2 * std_residuals],\n        mode=\"lines\",\n        line=dict(color=ACCENT, width=3, dash=\"dash\"),\n        name=\"-2 SD\",\n        showlegend=True,\n    )\n)\n\n# Add horizontal reference line at y=0\nfig.add_trace(\n    go.Scatter(\n        x=[y_pred.min(), y_pred.max()],\n        y=[0, 0],\n        mode=\"lines\",\n        line=dict(color=NEUTRAL, width=3),\n        name=\"Zero Line\",\n        showlegend=False,\n    )\n)\n\n# Add normal residuals (using brand green for first series)\nfig.add_trace(\n    go.Scatter(\n        x=y_pred[normal_mask],\n        y=residuals[normal_mask],\n        mode=\"markers\",\n        marker=dict(size=14, color=BRAND, opacity=0.7, line=dict(width=1, color=BRAND)),\n        name=\"Residuals\",\n        hovertemplate=\"Fitted: %{x:.2f}<br>Residual: %{y:.2f}<extra></extra>\",\n    )\n)\n\n# Add outlier residuals\nif np.any(outlier_mask):\n    fig.add_trace(\n        go.Scatter(\n            x=y_pred[outlier_mask],\n            y=residuals[outlier_mask],\n            mode=\"markers\",\n            marker=dict(size=16, color=ACCENT, opacity=0.9, line=dict(width=2, color=ACCENT), symbol=\"diamond\"),\n            name=\"Outliers (>2 SD)\",\n            hovertemplate=\"Fitted: %{x:.2f}<br>Residual: %{y:.2f}<extra></extra>\",\n        )\n    )\n\n# Add smoothing line to detect patterns\nsorted_indices = np.argsort(y_pred)\nwindow_size = 15\nkernel = np.ones(window_size) / window_size\nsmoothed_residuals = np.convolve(residuals[sorted_indices], kernel, mode=\"same\")\n\nfig.add_trace(\n    go.Scatter(\n        x=y_pred[sorted_indices],\n        y=smoothed_residuals,\n        mode=\"lines\",\n        line=dict(color=INK_SOFT, width=4),\n        name=\"Trend Line\",\n        hovertemplate=\"Fitted: %{x:.2f}<br>Smoothed Residual: %{y:.2f}<extra></extra>\",\n    )\n)\n\n# Update layout with theme-adaptive styling\nfig.update_layout(\n    title=dict(text=\"residual-plot · plotly · anyplot.ai\", font=dict(size=28, color=INK), x=0.5, xanchor=\"center\"),\n    xaxis=dict(\n        title=dict(text=\"Fitted Values\", font=dict(size=22, color=INK)),\n        tickfont=dict(size=18, color=INK_SOFT),\n        showgrid=True,\n        gridwidth=1,\n        gridcolor=GRID,\n        linecolor=INK_SOFT,\n        zeroline=False,\n    ),\n    yaxis=dict(\n        title=dict(text=\"Residuals (y_true - y_pred)\", font=dict(size=22, color=INK)),\n        tickfont=dict(size=18, color=INK_SOFT),\n        showgrid=True,\n        gridwidth=1,\n        gridcolor=GRID,\n        linecolor=INK_SOFT,\n        zeroline=False,\n    ),\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    font=dict(color=INK),\n    legend=dict(\n        font=dict(size=18, color=INK_SOFT),\n        x=0.02,\n        y=0.98,\n        xanchor=\"left\",\n        yanchor=\"top\",\n        bgcolor=ELEVATED_BG,\n        bordercolor=INK_SOFT,\n        borderwidth=1,\n    ),\n    margin=dict(l=100, r=50, t=100, b=80),\n)\n\n# Save as PNG and HTML with theme-suffixed filenames\nfig.write_image(f\"plot-{THEME}.png\", width=1600, height=900, scale=3)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}