{"spec_id":"ice-basic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nice-basic: Individual Conditional Expectation (ICE) Plot\nLibrary: letsplot 4.11.0 | Python 3.13.15\nQuality: 93/100 | Updated: 2026-08-17\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_line,\n    geom_segment,\n    geom_text,\n    ggplot,\n    ggsize,\n    labs,\n    layer_tooltips,\n    scale_color_manual,\n    theme,\n    theme_minimal,\n)\nfrom lets_plot.export import ggsave\nfrom sklearn.ensemble import GradientBoostingRegressor\n\n\nLetsPlot.setup_html()\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\"\nGRID_COLOR = \"#D0CEC7\" if THEME == \"light\" else \"#2E2E2B\"\n\nBRAND = \"#009E73\"  # Imprint palette position 1 — Suburban houses\nCOLOR2 = \"#C475FD\"  # Imprint palette position 2 — Urban houses\n\n# Data — house price predictions via GradientBoostingRegressor\nnp.random.seed(42)\nn_obs = 100\n\nsqft = np.random.uniform(800, 3500, n_obs)\nbedrooms = np.random.randint(2, 6, n_obs).astype(float)\nage = np.random.uniform(1, 50, n_obs)\nneighborhood = np.random.choice([0, 1], n_obs)\n\nprice = sqft * 150 + bedrooms * 12000 - age * 600 + neighborhood * 75000 + np.random.normal(0, 18000, n_obs)\n\nX = np.column_stack([sqft, bedrooms, age, neighborhood])\nmodel = GradientBoostingRegressor(n_estimators=150, max_depth=3, random_state=42)\nmodel.fit(X, price)\n\nsqft_grid = np.linspace(sqft.min(), sqft.max(), 60)\n\n# ICE curves — one line per observation\nice_rows = []\nfor i in range(n_obs):\n    for sq in sqft_grid:\n        X_mod = np.array([[sq, bedrooms[i], age[i], neighborhood[i]]])\n        pred = model.predict(X_mod)[0] / 1000\n        ice_rows.append(\n            {\n                \"sqft\": sq,\n                \"prediction\": pred,\n                \"obs_id\": str(i),\n                \"location\": \"Urban\" if neighborhood[i] == 1 else \"Suburban\",\n            }\n        )\n\nice_df = pd.DataFrame(ice_rows)\n\n# Partial dependence — average prediction across all observations\npdp_rows = []\nfor sq in sqft_grid:\n    X_mod = X.copy()\n    X_mod[:, 0] = sq\n    pdp_rows.append({\"sqft\": sq, \"prediction\": model.predict(X_mod).mean() / 1000})\n\npdp_df = pd.DataFrame(pdp_rows)\n\n# Annotation — quantify the neighborhood divergence at the top of the sqft range,\n# calling out the interaction effect the color-coding is meant to reveal.\n# The callout text sits in the open space above the mid-range bands; a dashed\n# leader points at the actual gap so it never overlaps the dense ICE lines.\ntop_grid = sqft_grid[-5:]\nmask_top = ice_df[\"sqft\"].isin(top_grid)\nurban_top = ice_df.loc[mask_top & (ice_df[\"location\"] == \"Urban\"), \"prediction\"].mean()\nsuburban_top = ice_df.loc[mask_top & (ice_df[\"location\"] == \"Suburban\"), \"prediction\"].mean()\ngap_x = sqft_grid[-1]\ngap_y = (urban_top + suburban_top) / 2\ncallout_x = sqft_grid[35]\ncallout_y = 600.0\n\ngap_label = pd.DataFrame(\n    {\n        \"sqft\": [callout_x],\n        \"prediction\": [callout_y],\n        \"label\": [f\"Urban premium ≈ ${urban_top - suburban_top:.0f}K at max size\"],\n    }\n)\nleader = pd.DataFrame({\"x\": [callout_x], \"y\": [callout_y - 12], \"xend\": [gap_x - 30], \"yend\": [gap_y]})\n\n# Plot — theme_minimal() drops the panel border entirely (no top/right box);\n# a matched panel_background border color keeps it that way after overriding fill\nanyplot_theme = theme_minimal() + theme(\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_grid_major_y=element_line(color=GRID_COLOR, size=0.5),\n    panel_grid_major_x=element_blank(),\n    panel_grid_minor=element_blank(),\n    axis_title=element_text(color=INK, size=12),\n    axis_text=element_text(color=INK_SOFT, size=10),\n    plot_title=element_text(color=INK, size=16),\n    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    legend_text=element_text(color=INK_SOFT, size=10),\n    legend_title=element_text(color=INK, size=10),\n)\n\npdp_tooltips = layer_tooltips().line(\"Sqft: @sqft\").line(\"Avg. Price: @prediction K\")\n\nplot = (\n    ggplot()\n    + geom_line(\n        aes(x=\"sqft\", y=\"prediction\", group=\"obs_id\", color=\"location\"),\n        data=ice_df,\n        alpha=0.15,\n        size=0.8,\n        tooltips=\"none\",\n    )\n    + scale_color_manual(values={\"Suburban\": BRAND, \"Urban\": COLOR2})\n    + geom_line(aes(x=\"sqft\", y=\"prediction\"), data=pdp_df, color=INK, size=2.5, tooltips=pdp_tooltips)\n    + geom_segment(\n        aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"), data=leader, color=INK_SOFT, size=0.6, alpha=0.6, linetype=\"dashed\"\n    )\n    + geom_text(\n        aes(x=\"sqft\", y=\"prediction\", label=\"label\"), data=gap_label, color=INK_SOFT, size=3.5, hjust=0.5, vjust=0\n    )\n    + labs(\n        x=\"Square Footage (sq ft)\",\n        y=\"Predicted Price ($K)\",\n        title=\"ice-basic · python · letsplot · anyplot.ai\",\n        color=\"Location\",\n    )\n    + anyplot_theme\n    + ggsize(800, 450)\n)\n\n# Save\nggsave(plot, filename=f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, filename=f\"plot-{THEME}.html\", path=\".\")\n"}