{"spec_id":"ice-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nice-basic: Individual Conditional Expectation (ICE) Plot\nLibrary: altair 6.2.2 | Python 3.13.15\nQuality: 87/100 | Updated: 2026-08-17\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent this file from shadowing the altair package\n_script_dir = os.path.dirname(os.path.abspath(__file__)) if \"__file__\" in dir() else os.getcwd()\nif _script_dir in sys.path:\n    sys.path.remove(_script_dir)\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nfrom sklearn.ensemble import GradientBoostingRegressor\n\n\n# Theme tokens (see prompts/default-style-guide.md \"Theme-adaptive Chrome\")\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\"\n\n# Imprint palette — first series ALWAYS #009E73\nBRAND = \"#009E73\"\nPDP_COLOR = \"#C475FD\"\n\n# Canvas — landscape inner view, see prompts/library/altair.md \"Canvas — hard rule\"\nVIEW_W, VIEW_H = 620, 320\nTARGET_W, TARGET_H = 3200, 1800\n\n# Data\nnp.random.seed(42)\nn_obs = 100\nsqft = np.random.uniform(800, 3500, n_obs)\nbedrooms = np.random.randint(1, 6, n_obs)\nhouse_age = np.random.uniform(0, 50, n_obs)\n\nprice = 120 * sqft + 25000 * bedrooms - 600 * house_age + 0.008 * sqft**2 + np.random.normal(0, 25000, n_obs)\n\nX = np.column_stack([sqft, bedrooms, house_age])\nmodel = GradientBoostingRegressor(n_estimators=200, max_depth=4, random_state=42)\nmodel.fit(X, price)\n\n# Build ICE curves — vary square footage across its range for each observation\ngrid_size = 60\nsqft_grid = np.linspace(sqft.min(), sqft.max(), grid_size)\n\nrecords = []\nfor obs_id in range(n_obs):\n    X_ice = np.column_stack([sqft_grid, np.full(grid_size, bedrooms[obs_id]), np.full(grid_size, house_age[obs_id])])\n    preds = model.predict(X_ice)\n    for sq, pred in zip(sqft_grid, preds, strict=False):\n        records.append({\"obs_id\": obs_id, \"sqft\": sq, \"price_k\": pred / 1000, \"series\": \"ICE Curves\"})\n\nice_df = pd.DataFrame(records)\n\n# PDP: mean prediction at each sqft grid point\npdp_df = ice_df.groupby(\"sqft\", as_index=False)[\"price_k\"].mean()\npdp_df[\"series\"] = \"Partial Dependence\"\n\n# Shared color scale for legend\ncolor_scale = alt.Scale(domain=[\"ICE Curves\", \"Partial Dependence\"], range=[BRAND, PDP_COLOR])\ncolor_legend = alt.Legend(title=\"\", labelFontSize=10, symbolSize=120, symbolStrokeWidth=2, orient=\"top-right\")\n\n# Real interactivity in the HTML export: hovering a curve highlights it. The\n# unselected (default) branch matches the static PNG state, so the PNG render\n# is unaffected — only the interactive HTML gains the hover behavior.\nhover = alt.selection_point(fields=[\"obs_id\"], on=\"pointerover\", empty=False)\n\n# ICE individual curves — semi-transparent to show density\nice_layer = (\n    alt.Chart(ice_df)\n    .mark_line(strokeWidth=1.5)\n    .encode(\n        x=alt.X(\"sqft:Q\", title=\"Square Footage (sq ft)\"),\n        y=alt.Y(\"price_k:Q\", title=\"Predicted Price ($K)\"),\n        detail=\"obs_id:N\",\n        color=alt.Color(\"series:N\", scale=color_scale, legend=color_legend),\n        opacity=alt.condition(hover, alt.value(0.9), alt.value(0.10)),\n    )\n    .add_params(hover)\n)\n\n# PDP overlay — bold opaque curve showing average marginal effect\npdp_layer = (\n    alt.Chart(pdp_df)\n    .mark_line(strokeWidth=5)\n    .encode(x=\"sqft:Q\", y=\"price_k:Q\", color=alt.Color(\"series:N\", scale=color_scale, legend=color_legend))\n)\n\n# Divergent-observation callout — instead of repeating the legend's \"Partial\n# Dependence\" label, find the single point where an individual curve deviates\n# furthest from the average and name it, surfacing the heterogeneity ICE\n# plots exist to reveal.\nmerged = ice_df.merge(pdp_df[[\"sqft\", \"price_k\"]], on=\"sqft\", suffixes=(\"\", \"_pdp\"))\nmerged[\"deviation\"] = merged[\"price_k\"] - merged[\"price_k_pdp\"]\ndivergent_row = merged.loc[merged[\"deviation\"].abs().idxmax()]\ndivergent_id = int(divergent_row[\"obs_id\"])\ndivergent_deviation = float(divergent_row[\"deviation\"])\ndivergent_direction = \"above\" if divergent_deviation > 0 else \"below\"\n\ndivergent_df = ice_df[ice_df[\"obs_id\"] == divergent_id]\ndivergent_layer = (\n    alt.Chart(divergent_df)\n    .mark_line(strokeWidth=2.5, opacity=1.0)\n    .encode(x=\"sqft:Q\", y=\"price_k:Q\", color=alt.Color(\"series:N\", scale=color_scale, legend=color_legend))\n)\n\ndivergent_point_df = merged.loc[[merged[\"deviation\"].abs().idxmax()], [\"sqft\", \"price_k\"]]\n# Keep the callout clear of the y-axis title (left edge) and the top-right\n# legend by flipping horizontal/vertical anchoring based on where the\n# divergent point actually falls on the canvas.\nsqft_mid = (sqft_grid.min() + sqft_grid.max()) / 2\ntext_align = \"left\" if divergent_row[\"sqft\"] < sqft_mid else \"right\"\ntext_dx = 10 if text_align == \"left\" else -10\ntext_dy = -8 if divergent_row[\"price_k\"] < pdp_df[\"price_k\"].max() * 0.85 else 16\ndivergent_annotation = (\n    alt.Chart(divergent_point_df)\n    .mark_text(align=text_align, dx=text_dx, dy=text_dy, fontSize=10, color=BRAND, fontWeight=\"bold\")\n    .encode(\n        x=\"sqft:Q\",\n        y=\"price_k:Q\",\n        text=alt.value(f\"Obs #{divergent_id}: ${abs(divergent_deviation):.0f}K {divergent_direction} average\"),\n    )\n)\n\n# Rug plot — actual observed sqft values along the x-axis\nrug_df = pd.DataFrame({\"sqft\": sqft})\nrug_layer = (\n    alt.Chart(rug_df)\n    .mark_tick(thickness=1.5, size=8, opacity=0.45, color=INK_SOFT)\n    .encode(x=\"sqft:Q\", y=alt.value(VIEW_H - 8))\n)\n\nchart = (\n    alt.layer(ice_layer, divergent_layer, pdp_layer, divergent_annotation, rug_layer)\n    .properties(\n        width=VIEW_W, height=VIEW_H, background=PAGE_BG, title=alt.Title(\"ice-basic · altair · anyplot.ai\", fontSize=16)\n    )\n    .configure_view(fill=PAGE_BG, strokeWidth=0)\n    .configure_axis(\n        domain=False,\n        tickColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.10,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=10,\n        titleFontSize=12,\n    )\n    .configure_title(color=INK, fontSize=16)\n    .configure_legend(\n        fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK, labelFontSize=10\n    )\n)\n\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\nchart.save(f\"plot-{THEME}.html\")\n\n# PAD-only to exact target canvas — vl-convert pads title/axis/legend outside\n# width/height, so the saved PNG needs a final centered pad, never a crop.\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TARGET_W or _h > TARGET_H:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}x{_h}, exceeds target {TARGET_W}x{TARGET_H}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TARGET_W or _h < TARGET_H:\n    _canvas = Image.new(\"RGB\", (TARGET_W, TARGET_H), PAGE_BG)\n    _canvas.paste(_img, ((TARGET_W - _w) // 2, (TARGET_H - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n"}