{"spec_id":"ice-basic","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nice-basic: Individual Conditional Expectation (ICE) Plot\nLibrary: matplotlib 3.11.1 | Python 3.13.15\nQuality: 93/100 | Updated: 2026-08-17\n\"\"\"\n\nimport os\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom matplotlib.lines import Line2D\nfrom sklearn.ensemble import GradientBoostingRegressor\n\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\nBRAND = \"#009E73\"  # Imprint palette position 1 — ICE individual lines\nACCENT = \"#C475FD\"  # Imprint palette position 2 — PDP average line\n\n# Data: synthetic housing dataset\nnp.random.seed(42)\nn_obs = 120\n\nsqft = np.random.uniform(800, 3500, n_obs)\nbedrooms = np.random.choice([2, 3, 4, 5], n_obs, p=[0.15, 0.45, 0.30, 0.10])\nage_years = np.random.uniform(1, 40, n_obs)\nlot_size = np.random.uniform(3000, 15000, n_obs)\n\nprice = sqft * 180 + bedrooms * 12000 - age_years * 1500 + lot_size * 2.5 + np.random.normal(0, 25000, n_obs)\n\nX = np.column_stack([sqft, bedrooms, age_years, lot_size])\ny = price\n\n# Train gradient boosting model\nmodel = GradientBoostingRegressor(n_estimators=150, max_depth=4, learning_rate=0.05, random_state=42)\nmodel.fit(X, y)\n\n# ICE curves: vary sqft (index 0) across a grid, holding other features at observed values\nsqft_grid = np.linspace(sqft.min(), sqft.max(), 70)\nice_matrix = np.zeros((n_obs, len(sqft_grid)))\n\nfor j, val in enumerate(sqft_grid):\n    X_temp = X.copy()\n    X_temp[:, 0] = val\n    ice_matrix[:, j] = model.predict(X_temp)\n\npdp_line = ice_matrix.mean(axis=0)\nice_min = ice_matrix.min(axis=0)\nice_max = ice_matrix.max(axis=0)\n\n# Locate where individual curves diverge most from the average (heterogeneity peak)\nspread = ice_matrix.std(axis=0)\ndivergence_idx = np.argmax(spread)\ndivergence_sqft = sqft_grid[divergence_idx]\ndivergence_y = ice_max[divergence_idx]\n\n# Plot\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\n# Min-max envelope of individual curves — shows the full spread at a glance\nax.fill_between(sqft_grid, ice_min, ice_max, color=BRAND, alpha=0.06, linewidth=0, zorder=1)\n\n# ICE individual lines\nfor i in range(n_obs):\n    ax.plot(sqft_grid, ice_matrix[i], color=BRAND, alpha=0.14, linewidth=0.9, zorder=2)\n\n# PDP average line\nax.plot(sqft_grid, pdp_line, color=ACCENT, linewidth=3, zorder=5)\n\n# Explicit y-limits with headroom for the rug plot so its position is fixed\n# regardless of layout adjustments (rug sits inside the bottom margin, never\n# overlapping the lowest ICE curve).\ny_span = ice_max.max() - ice_min.min()\ny_bottom = ice_min.min() - 0.10 * y_span\ny_top = ice_max.max() + 0.04 * y_span\nax.set_ylim(y_bottom, y_top)\nrug_y = ice_min.min() - 0.055 * y_span\nax.plot(sqft, np.full(n_obs, rug_y), \"|\", color=INK_MUTED, alpha=0.5, markersize=6, markeredgewidth=1.0, zorder=3)\n\n# Annotation: call out where individual effects diverge most from the average\nax.annotate(\n    f\"Effects diverge most\\naround {divergence_sqft:,.0f} sqft\",\n    xy=(divergence_sqft, divergence_y),\n    xytext=(0.62, 0.90),\n    textcoords=\"axes fraction\",\n    fontsize=8,\n    color=INK_SOFT,\n    ha=\"left\",\n    va=\"top\",\n    arrowprops={\"arrowstyle\": \"->\", \"color\": INK_SOFT, \"linewidth\": 1.0},\n    bbox={\"facecolor\": ELEVATED_BG, \"edgecolor\": INK_SOFT, \"alpha\": 0.9, \"boxstyle\": \"round,pad=0.4\", \"linewidth\": 0.8},\n)\n\n# Style\nax.set_xlabel(\"Square Footage (sqft)\", fontsize=10, color=INK)\nax.set_ylabel(\"Predicted House Price ($)\", fontsize=10, color=INK)\nax.set_title(\n    \"House Price by Square Footage · ice-basic · matplotlib · anyplot.ai\", fontsize=12, fontweight=\"medium\", color=INK\n)\nax.tick_params(axis=\"both\", labelsize=8, colors=INK_SOFT, length=0)\nax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f\"${x:,.0f}\"))\n\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\nax.spines[\"left\"].set_color(INK_SOFT)\nax.spines[\"bottom\"].set_color(INK_SOFT)\nax.yaxis.grid(True, alpha=0.10, linewidth=0.8, color=INK)\nax.set_axisbelow(True)\n\n# Legend\nlegend_handles = [\n    Line2D([0], [0], color=BRAND, alpha=0.6, linewidth=2, label=f\"Individual ICE lines (n={n_obs})\"),\n    Line2D([0], [0], color=BRAND, alpha=0.2, linewidth=8, label=\"Prediction range (min–max)\"),\n    Line2D([0], [0], color=ACCENT, linewidth=3, label=\"Partial dependence (average)\"),\n]\nleg = ax.legend(handles=legend_handles, fontsize=8, loc=\"upper left\")\nleg.get_frame().set_facecolor(ELEVATED_BG)\nleg.get_frame().set_edgecolor(INK_SOFT)\nplt.setp(leg.get_texts(), color=INK_SOFT)\n\nplt.tight_layout()\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}