{"spec_id":"ice-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nice-basic: Individual Conditional Expectation (ICE) Plot\nLibrary: bokeh 3.9.2 | Python 3.13.15\nQuality: 92/100 | Created: 2026-08-17\n\"\"\"\n\nimport os\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, HoverTool, Range1d\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\nfrom sklearn.ensemble import GradientBoostingRegressor\n\n\n# Theme tokens (see prompts/default-style-guide.md \"Background\" + \"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 (see prompts/default-style-guide.md \"Categorical Palette\")\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nBRAND = IMPRINT_PALETTE[0]  # ALWAYS first series\n\n# Data — gradient boosting house-price model with a square-footage x age interaction,\n# so predicted price grows faster with square footage for newer houses than older ones.\nnp.random.seed(42)\nn_obs = 80\ngrid_size = 60\n\nsquare_footage = np.random.uniform(600, 4000, n_obs)\nbedrooms = np.random.randint(1, 6, n_obs).astype(float)\nhouse_age = np.random.uniform(0, 60, n_obs)\nlot_size = np.random.uniform(2000, 20000, n_obs)\nnoise = np.random.normal(0, 15000, n_obs)\n\nprice = (\n    80 * square_footage\n    + 9000 * bedrooms\n    - 900 * house_age\n    + 1.2 * lot_size\n    + 0.04 * square_footage * np.clip(60 - house_age, 0, None)\n    + noise\n)\n\nfeatures = np.column_stack([square_footage, bedrooms, house_age, lot_size])\nmodel = GradientBoostingRegressor(n_estimators=300, max_depth=3, learning_rate=0.05, random_state=42)\nmodel.fit(features, price)\n\nfeature_grid = np.linspace(600, 4000, grid_size)\nice_predictions = np.zeros((n_obs, grid_size))\nfor i in range(n_obs):\n    grid_features = np.tile(features[i], (grid_size, 1))\n    grid_features[:, 0] = feature_grid\n    ice_predictions[i] = model.predict(grid_features)\n\nice_price_k = ice_predictions / 1000\npdp_price_k = ice_price_k.mean(axis=0)\n\ny_min, y_max = float(ice_price_k.min()), float(ice_price_k.max())\ny_span = y_max - y_min\nrug_y0 = y_min - 0.09 * y_span\nrug_y1 = y_min - 0.03 * y_span\n\nsource = ColumnDataSource(\n    data={\n        \"xs\": [feature_grid.tolist()] * n_obs,\n        \"ys\": [row.tolist() for row in ice_price_k],\n        \"obs_id\": list(range(n_obs)),\n    }\n)\n\n# Plot\ntitle = \"ice-basic · python · bokeh · anyplot.ai\"\np = figure(\n    width=3200,\n    height=1800,\n    title=title,\n    x_axis_label=\"Square Footage\",\n    y_axis_label=\"Predicted Price ($k)\",\n    toolbar_location=None,\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=50,\n)\np.y_range = Range1d(rug_y0 - 0.02 * y_span, y_max + 0.08 * y_span)\n\nice_renderer = p.multi_line(\n    xs=\"xs\",\n    ys=\"ys\",\n    source=source,\n    line_color=BRAND,\n    line_alpha=0.25,\n    line_width=1.5,\n    legend_label=\"Individual houses (ICE)\",\n)\np.line(feature_grid, pdp_price_k, line_color=INK, line_width=6, legend_label=\"Average effect (PDP)\")\np.segment(\n    x0=square_footage, y0=rug_y0, x1=square_footage, y1=rug_y1, line_color=INK_SOFT, line_alpha=0.4, line_width=1.5\n)\n\n# HoverTool showcases bokeh's distinctive HTML interactivity (toolbar_location=None\n# only hides the button row — hover still fires on mouse move over a line).\np.add_tools(\n    HoverTool(\n        renderers=[ice_renderer],\n        tooltips=[(\"Observation\", \"@obs_id\"), (\"Sq Ft\", \"$x{0,0}\"), (\"Price\", \"$y{0.0}k\")],\n        mode=\"mouse\",\n    )\n)\n\n# Style\np.title.text_font_size = \"50pt\"\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\n\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = INK_SOFT\n\np.title.text_color = INK\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\np.xaxis.axis_line_color = INK_SOFT\np.yaxis.axis_line_color = INK_SOFT\np.xaxis.major_tick_line_color = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\n\np.xgrid.grid_line_color = None\np.ygrid.grid_line_color = INK\np.ygrid.grid_line_alpha = 0.15\n\np.legend.location = \"top_left\"\np.legend.label_text_font_size = \"34pt\"\np.legend.glyph_width = 60\np.legend.glyph_height = 40\np.legend.background_fill_color = ELEVATED_BG\np.legend.border_line_color = INK_SOFT\np.legend.label_text_color = INK_SOFT\n\n# Save — write HTML, then screenshot it with headless Chrome (export_png is unreliable in CI)\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\nW, H = 3200, 1800\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}