{"spec_id":"residual-plot","library":"letsplot","language":"python","code":"# ruff: noqa: F405\n\"\"\"pyplots.ai\nresidual-plot: Residual Plot\nLibrary: lets-plot | Python 3.13\nQuality: pending | Created: 2025-12-26\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import *\n\n\nLetsPlot.setup_html()\n\n# Theme tokens (read from environment)\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\nRULE = \"rgba(26,26,23,0.10)\" if THEME == \"light\" else \"rgba(240,239,232,0.10)\"\n\n# Okabe-Ito palette\nBRAND = \"#009E73\"  # Position 1: bluish green (normal points)\nOUTLIER_COLOR = \"#AE3030\"  # imprint red — outliers (>2σ)\n\n# Data: Generate realistic regression scenario with deliberate pattern in residuals\nnp.random.seed(42)\nn = 150\n\n# Create feature with mild non-linearity to show residual patterns\nX = np.linspace(10, 100, n)\nnoise = np.random.normal(0, 5, n)\n# Add slight heteroscedasticity: variance increases with X\nheteroscedastic_noise = noise * (0.5 + 0.01 * X)\ny_true = 2.5 * X + 0.02 * X**2 + heteroscedastic_noise + 50\n\n# Simple linear regression (manual implementation)\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\n\n# Calculate residual standard deviation for outlier bands\nresidual_std = np.std(residuals)\n\n# Identify outliers (beyond ±2 standard deviations)\noutlier_threshold = 2 * residual_std\nis_outlier = np.abs(residuals) > outlier_threshold\n\n# Create DataFrame for plotting\ndf = pd.DataFrame(\n    {\"Fitted Values\": y_pred, \"Residuals\": residuals, \"Outlier\": np.where(is_outlier, \"Outlier (>2σ)\", \"Normal\")}\n)\n\n# Create residual plot\nplot = (\n    ggplot(df, aes(x=\"Fitted Values\", y=\"Residuals\"))\n    # Reference line at y=0\n    + geom_hline(yintercept=0, color=INK_SOFT, size=1.5, linetype=\"solid\")\n    # Outlier bands at ±2 standard deviations\n    + geom_hline(yintercept=outlier_threshold, color=INK_MUTED, size=1, linetype=\"dashed\", alpha=0.6)\n    + geom_hline(yintercept=-outlier_threshold, color=INK_MUTED, size=1, linetype=\"dashed\", alpha=0.6)\n    # Points colored by outlier status\n    + geom_point(aes(color=\"Outlier\"), size=5, alpha=0.75)\n    # LOWESS smoothing line to detect patterns\n    + geom_smooth(method=\"loess\", color=INK, size=2, se=False, span=0.6)\n    # Color scale for outliers (Normal: brand green, Outlier: vermillion)\n    + scale_color_manual(values=[BRAND, OUTLIER_COLOR], name=\"Point Type\")\n    # Labels\n    + labs(title=\"residual-plot · letsplot · pyplots.ai\", x=\"Fitted Values\", y=\"Residuals (Observed - Predicted)\")\n    # Theme\n    + theme_minimal()\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        panel_grid_major=element_line(color=RULE, size=0.4),\n        panel_grid_minor=element_blank(),\n        plot_title=element_text(size=24, face=\"bold\", color=INK),\n        axis_title=element_text(size=20, color=INK),\n        axis_text=element_text(size=16, color=INK_SOFT),\n        axis_line=element_line(color=INK_SOFT, size=0.5),\n        legend_title=element_text(size=18, color=INK),\n        legend_text=element_text(size=16, color=INK_SOFT),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_position=\"right\",\n    )\n    # Size for export (1600 × 900 base, scaled 3x = 4800 × 2700)\n    + ggsize(1600, 900)\n)\n\n# Save as PNG and HTML with theme-suffixed filenames\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=3)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}