{"spec_id":"curve-bias-variance-tradeoff","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\ncurve-bias-variance-tradeoff: Bias-Variance Tradeoff Curve\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 83/100 | Updated: 2026-05-28\n\"\"\"\n\nimport os\n\nimport cairosvg\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Theme-adaptive zone colors: pastel tints in light mode, vivid mid-tones in dark mode\nZONE_LEFT = \"#C8EDF2\" if THEME == \"light\" else \"#1E7D8A\"  # cyan underfitting zone\nZONE_RIGHT = \"#EDD4E7\" if THEME == \"light\" else \"#6B2A56\"  # rose overfitting zone\n\n# Data\nnp.random.seed(42)\ncomplexity = np.linspace(0.5, 10, 100)\nbias_squared = 4.0 / (1 + 0.8 * complexity)\nvariance = 0.1 * complexity**1.5\nirreducible_error = np.full_like(complexity, 0.5)\ntotal_error = bias_squared + variance + irreducible_error\n\noptimal_idx = int(np.argmin(total_error))\noptimal_complexity = float(complexity[optimal_idx])\noptimal_error = float(total_error[optimal_idx])\n\ntitle = \"curve-bias-variance-tradeoff · python · pygal · anyplot.ai\"\n\n# Series order: zones (0,1) → Bias² (2=green) → Variance (3) → Total (4) → Irred (5) → vert line (6=red) → point (7=red)\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=(ZONE_LEFT, ZONE_RIGHT, \"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#AE3030\"),\n    title_font_size=66,\n    label_font_size=28,  # Reduced from 44 to prevent y_title overflow at current canvas size\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    stroke_width=3,\n)\n\nchart = pygal.XY(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=title,\n    x_title=\"Model Complexity\",\n    y_title=\"Prediction Error\",\n    show_dots=False,\n    show_x_guides=True,\n    show_y_guides=True,\n    legend_at_bottom=True,\n    legend_box_size=30,\n    truncate_legend=-1,\n    range=(0, 5.0),\n    xrange=(0, 11),\n    print_values=False,\n)\n\n# Zone fills — flat lines at y=5 with fill=True create shaded background rectangles\nunderfitting_fill = [(0.0, 5.0)] + [(float(x), 5.0) for x in complexity[: optimal_idx + 1]]\noverfitting_fill = [(float(x), 5.0) for x in complexity[optimal_idx:]] + [(11.0, 5.0)]\nchart.add(\"← Underfitting Zone\", underfitting_fill, fill=True, stroke=False, show_dots=False)\nchart.add(\"Overfitting Zone →\", overfitting_fill, fill=True, stroke=False, show_dots=False)\n\n# Main curves (palette positions 2–5: green, lavender, blue, ochre)\nbias_data = [(float(x), float(y)) for x, y in zip(complexity, bias_squared, strict=True)]\nvariance_data = [(float(x), float(y)) for x, y in zip(complexity, variance, strict=True)]\ntotal_data = [(float(x), float(y)) for x, y in zip(complexity, total_error, strict=True)]\nirreducible_data = [(float(x), float(y)) for x, y in zip(complexity, irreducible_error, strict=True)]\n\nchart.add(\"Bias² (decreasing ↘)\", bias_data, stroke_style={\"width\": 7, \"dasharray\": \"14, 8\"}, show_dots=False)\nchart.add(\"Variance (increasing ↗)\", variance_data, stroke_style={\"width\": 7, \"dasharray\": \"8, 5\"}, show_dots=False)\nchart.add(\"Total Error (U-shaped)\", total_data, stroke_style={\"width\": 9}, show_dots=False)\nchart.add(\n    \"Irreducible Error (constant)\", irreducible_data, stroke_style={\"width\": 6, \"dasharray\": \"4, 6\"}, show_dots=False\n)\n\n# Vertical dashed line from y=0 to optimal error at the optimal complexity point\nvertical_line = [(optimal_complexity, 0.0), (optimal_complexity, optimal_error)]\nchart.add(\n    f\"│ Optimal x={optimal_complexity:.1f}\",\n    vertical_line,\n    stroke_style={\"width\": 4, \"dasharray\": \"10, 8\"},\n    show_dots=False,\n)\n\n# Optimal point marker (palette position 7: #AE3030 semantic red)\noptimal_point = [\n    {\n        \"value\": (optimal_complexity, optimal_error),\n        \"label\": f\"Optimal: x={optimal_complexity:.1f}, err={optimal_error:.2f}\",\n    }\n]\nchart.add(f\"★ Optimal Point (x={optimal_complexity:.1f})\", optimal_point, show_dots=True, dots_size=20, stroke=False)\n\n# Get SVG output for annotation post-processing\nsvg_bytes = chart.render()\n\n# Add direct curve annotations via SVG text elements.\n# Coordinate mapping for pygal 3200×1800 with legend_at_bottom:\n#   chart left ≈230px (y-title + tick labels), right ≈3155px, top ≈175px, bottom ≈1490px\nCHART_LEFT, CHART_RIGHT = 230, 3155\nCHART_TOP, CHART_BOTTOM = 175, 1490\nCHART_W = CHART_RIGHT - CHART_LEFT\nCHART_H = CHART_BOTTOM - CHART_TOP\nX_MIN, X_MAX = 0.0, 11.0\nY_MIN, Y_MAX = 0.0, 5.0\n\n\ndef to_px(x_d: float, y_d: float) -> tuple[int, int]:\n    xp = int(CHART_LEFT + (x_d - X_MIN) / (X_MAX - X_MIN) * CHART_W)\n    yp = int(CHART_BOTTOM - (y_d - Y_MIN) / (Y_MAX - Y_MIN) * CHART_H)\n    return xp, yp\n\n\n# Compute annotation positions at x=9.0 (near right end of curves)\nx_ann = 9.0\nb_y = 4.0 / (1 + 0.8 * x_ann)  # ~0.465\nv_y = 0.1 * x_ann**1.5  # ~2.927\nt_y = b_y + v_y + 0.5  # ~3.892\n\nann_font = 38\nann_style = f\"font-family: Arial, sans-serif; font-size: {ann_font}px; font-weight: bold;\"\n\nsvg_str = svg_bytes.decode(\"utf-8\")\ntext_elements = []\n\n# Formula annotation at top of chart area (spec requirement: show Total = Bias² + Variance + ε)\nformula_style = f\"font-family: Arial, sans-serif; font-size: 34px; fill: {INK_MUTED};\"\nfx, fy = to_px(5.5, 4.70)\ntext_elements.append(\n    f'<text x=\"{fx}\" y=\"{fy}\" text-anchor=\"middle\" style=\"{formula_style}\">Total = Bias² + Variance + ε</text>'\n)\n\n# Total Error — above the highest curve at x=9\ntx, ty = to_px(x_ann, t_y + 0.10)\ntext_elements.append(f'<text x=\"{tx}\" y=\"{ty}\" style=\"{ann_style} fill: #4467A3;\">Total Error</text>')\n# Variance label — above the variance curve\ntx, ty = to_px(x_ann, v_y + 0.10)\ntext_elements.append(f'<text x=\"{tx}\" y=\"{ty}\" style=\"{ann_style} fill: #C475FD;\">Variance</text>')\n# Bias² label — below the bias curve at x=9 (near bottom right)\ntx, ty = to_px(x_ann, b_y - 0.15)\ntext_elements.append(f'<text x=\"{tx}\" y=\"{ty}\" style=\"{ann_style} fill: #009E73;\">Bias²</text>')\n# Irreducible Error label — placed at x=4.5 to avoid overlap with bias annotation at x=9\ntx, ty = to_px(4.5, 0.5 + 0.14)\ntext_elements.append(f'<text x=\"{tx}\" y=\"{ty}\" style=\"{ann_style} fill: #BD8233;\">Irreducible ε</text>')\n\nsvg_str = svg_str.replace(\"</svg>\", \"\\n\".join(text_elements) + \"\\n</svg>\")\n\n# Render modified SVG to PNG with curve annotation text visible in both themes\ncairosvg.svg2png(bytestring=svg_str.encode(\"utf-8\"), write_to=f\"plot-{THEME}.png\")\n\n# Save interactive HTML (unmodified SVG — tooltips serve as annotations in browser)\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(svg_bytes)\n"}