{"spec_id":"line-impurity-comparison","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nline-impurity-comparison: Gini Impurity vs Entropy Comparison\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 87/100 | Updated: 2026-05-29\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\n\n# Theme tokens (Imprint palette — see prompts/default-style-guide.md)\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\"\n\n# Imprint categorical palette positions 1 and 2\nGINI_COLOR = \"#009E73\"  # position 1 — always first series\nENTROPY_COLOR = \"#C475FD\"  # position 2\n\n# Data: probability range [0, 1], 200 points for smooth curves\np = np.linspace(0, 1, 200)\ngini = 2 * p * (1 - p)\n\n# Entropy with safe log (0 at boundaries as required by spec)\nwith np.errstate(divide=\"ignore\", invalid=\"ignore\"):\n    entropy_raw = -p * np.log2(p) - (1 - p) * np.log2(1 - p)\nentropy_raw = np.nan_to_num(entropy_raw, nan=0.0)\nentropy = entropy_raw / np.max(entropy_raw)\n\nGINI_LABEL = \"Gini: 2p(1−p)\"\nENTROPY_LABEL = \"Entropy: −p·log₂(p) (scaled)\"\n\ndf = pd.DataFrame(\n    {\n        \"p\": np.tile(p, 2),\n        \"Impurity\": np.concatenate([gini, entropy]),\n        \"Measure\": [GINI_LABEL] * len(p) + [ENTROPY_LABEL] * len(p),\n    }\n)\n\n# Wide-format for shaded band between the two curves\ndf_area = pd.DataFrame({\"p\": p, \"gini\": gini, \"entropy\": entropy})\n\nannotation_df = pd.DataFrame(\n    {\"p\": [0.5, 0.5], \"Impurity\": [0.5, 1.0], \"label\": [\"Gini max = 0.5\", \"Entropy max = 1.0\"]}\n)\n\n# Title with scaled font size (67-char baseline)\ntitle_text = \"line-impurity-comparison · python · altair · anyplot.ai\"\nn = len(title_text)\ntitle_fontsize = max(11, round(16 * (67 / n if n > 67 else 1.0)))\n\n# Color + dash scales for distinguishable lines (solid Gini, dashed Entropy)\ncolor_scale = alt.Scale(domain=[GINI_LABEL, ENTROPY_LABEL], range=[GINI_COLOR, ENTROPY_COLOR])\ndash_scale = alt.Scale(domain=[GINI_LABEL, ENTROPY_LABEL], range=[[1, 0], [8, 4]])\n\n# Shaded band between Gini and Entropy — entropy > Gini across all of (0,1)\n# This is the key educational insight: entropy is uniformly higher than Gini impurity\narea_fill = alt.Chart(df_area).mark_area(opacity=0.12, color=ENTROPY_COLOR).encode(x=\"p:Q\", y=\"gini:Q\", y2=\"entropy:Q\")\n\n# Lines with color and dash differentiation\nlines = (\n    alt.Chart(df)\n    .mark_line(strokeWidth=4)\n    .encode(\n        x=alt.X(\n            \"p:Q\",\n            title=\"Probability p\",\n            scale=alt.Scale(domain=[0, 1]),\n            axis=alt.Axis(\n                labelFontSize=10, titleFontSize=12, values=[0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]\n            ),\n        ),\n        y=alt.Y(\n            \"Impurity:Q\",\n            title=\"Impurity Measure (normalized)\",\n            scale=alt.Scale(domain=[0, 1.1]),\n            axis=alt.Axis(labelFontSize=10, titleFontSize=12),\n        ),\n        color=alt.Color(\n            \"Measure:N\",\n            scale=color_scale,\n            legend=alt.Legend(\n                title=None, labelFontSize=10, orient=\"bottom-right\", offset=10, symbolStrokeWidth=4, symbolSize=300\n            ),\n        ),\n        strokeDash=alt.StrokeDash(\"Measure:N\", scale=dash_scale, legend=None),\n    )\n)\n\n# Dots at maxima (p=0.5 for both curves)\nannotation_point = (\n    alt.Chart(annotation_df).mark_point(size=150, filled=True, color=INK, opacity=0.75).encode(x=\"p:Q\", y=\"Impurity:Q\")\n)\n\n# Text labels at maxima — dx=40 keeps annotations clear of the legend\nannotation_text = (\n    alt.Chart(annotation_df)\n    .mark_text(fontSize=10, dx=40, fontWeight=\"bold\", align=\"left\", color=INK)\n    .encode(x=\"p:Q\", y=\"Impurity:Q\", text=\"label:N\")\n)\n\n# Vertical rule at p=0.5 where both measures peak\nrule_df = pd.DataFrame({\"p\": [0.5]})\nvertical_rule = alt.Chart(rule_df).mark_rule(strokeDash=[6, 4], strokeWidth=1.5, color=INK_MUTED).encode(x=\"p:Q\")\n\n# Compose — area_fill rendered first (behind lines)\nchart = (\n    (area_fill + lines + vertical_rule + annotation_point + annotation_text)\n    .properties(\n        width=620, height=320, background=PAGE_BG, title=alt.Title(title_text, fontSize=title_fontsize, color=INK)\n    )\n    .configure_view(fill=PAGE_BG, strokeWidth=0)\n    .configure_axis(\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.13,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        domain=False,\n    )\n    .configure_legend(\n        fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK, labelFontSize=10\n    )\n    .configure_title(color=INK, fontSize=title_fontsize)\n)\n\n# Save PNG + HTML\nTW, TH = 3200, 1800\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\nchart.save(f\"plot-{THEME}.html\")\n\n# Pad PNG to exact target dimensions (PAD only — never crop)\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n"}