{"spec_id":"contour-decision-boundary","library":"altair","language":"python","code":"\"\"\" anyplot.ai\ncontour-decision-boundary: Decision Boundary Classifier Visualization\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-05-16\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom sklearn.datasets import make_moons\nfrom sklearn.neighbors import KNeighborsClassifier\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\"\n\n# Okabe-Ito palette\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\"]\n\n# Data - generate two-moon classification dataset\nnp.random.seed(42)\nX, y = make_moons(n_samples=150, noise=0.25, random_state=42)\n\n# Train a KNN classifier\nclf = KNeighborsClassifier(n_neighbors=5)\nclf.fit(X, y)\n\n# Create mesh grid for decision boundary\nx_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5\ny_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5\nxx, yy = np.meshgrid(np.linspace(x_min, x_max, 150), np.linspace(y_min, y_max, 150))\n\n# Predict on mesh grid\nZ = clf.predict(np.c_[xx.ravel(), yy.ravel()])\nZ = Z.reshape(xx.shape)\n\n# Create DataFrame for background regions (decision boundary)\nmesh_df = pd.DataFrame(\n    {\"X1\": xx.ravel(), \"X2\": yy.ravel(), \"Class\": [\"Class A\" if z == 0 else \"Class B\" for z in Z.ravel()]}\n)\n\n# Create DataFrame for training points\ntrain_df = pd.DataFrame(\n    {\"X1\": X[:, 0], \"X2\": X[:, 1], \"Class\": [\"Class A\" if label == 0 else \"Class B\" for label in y]}\n)\n\n# Add prediction for training points to show misclassified\ntrain_predictions = clf.predict(X)\ntrain_df[\"Classification\"] = [\"Correct\" if p == t else \"Incorrect\" for p, t in zip(train_predictions, y, strict=True)]\n\n# Decision boundary background using rect marks\nbackground = (\n    alt.Chart(mesh_df)\n    .mark_rect(opacity=0.4)\n    .encode(\n        x=alt.X(\"X1:Q\", bin=alt.Bin(maxbins=150), title=\"Feature X1\"),\n        y=alt.Y(\"X2:Q\", bin=alt.Bin(maxbins=150), title=\"Feature X2\"),\n        color=alt.Color(\n            \"Class:N\",\n            scale=alt.Scale(domain=[\"Class A\", \"Class B\"], range=[IMPRINT[0], IMPRINT[1]]),\n            legend=alt.Legend(title=\"Decision Region\", titleFontSize=18, labelFontSize=16, orient=\"right\"),\n        ),\n    )\n)\n\n# Correctly classified points (circles)\ncorrect_points = (\n    alt.Chart(train_df[train_df[\"Classification\"] == \"Correct\"])\n    .mark_circle(size=250, strokeWidth=2)\n    .encode(\n        x=alt.X(\"X1:Q\"),\n        y=alt.Y(\"X2:Q\"),\n        fill=alt.Color(\n            \"Class:N\", scale=alt.Scale(domain=[\"Class A\", \"Class B\"], range=[IMPRINT[0], IMPRINT[1]]), legend=None\n        ),\n        stroke=alt.value(INK_SOFT),\n        tooltip=[\"X1:Q\", \"X2:Q\", \"Class:N\", \"Classification:N\"],\n    )\n)\n\n# Incorrectly classified points (triangles with orange stroke)\nincorrect_points = (\n    alt.Chart(train_df[train_df[\"Classification\"] == \"Incorrect\"])\n    .mark_point(shape=\"triangle\", size=350, strokeWidth=3, filled=True)\n    .encode(\n        x=alt.X(\"X1:Q\"),\n        y=alt.Y(\"X2:Q\"),\n        fill=alt.Color(\n            \"Class:N\", scale=alt.Scale(domain=[\"Class A\", \"Class B\"], range=[IMPRINT[0], IMPRINT[1]]), legend=None\n        ),\n        stroke=alt.value(IMPRINT[1]),\n        tooltip=[\"X1:Q\", \"X2:Q\", \"Class:N\", \"Classification:N\"],\n    )\n)\n\n# Create a separate legend for shapes (classification status)\nshape_legend_df = pd.DataFrame({\"Classification\": [\"Correct (●)\", \"Incorrect (▲)\"], \"x\": [0, 0], \"y\": [0, 1]})\nshape_legend = (\n    alt.Chart(shape_legend_df)\n    .mark_point(size=0, opacity=0)\n    .encode(\n        x=alt.X(\"x:Q\"),\n        y=alt.Y(\"y:Q\"),\n        shape=alt.Shape(\n            \"Classification:N\",\n            scale=alt.Scale(domain=[\"Correct (●)\", \"Incorrect (▲)\"], range=[\"circle\", \"triangle\"]),\n            legend=alt.Legend(title=\"Classification\", titleFontSize=18, labelFontSize=16, orient=\"right\"),\n        ),\n    )\n)\n\n# Combine layers\nchart = (\n    alt.layer(background, correct_points, incorrect_points, shape_legend)\n    .properties(\n        width=1600,\n        height=900,\n        background=PAGE_BG,\n        title=alt.Title(\"contour-decision-boundary · altair · anyplot.ai\", fontSize=28, anchor=\"middle\", color=INK),\n    )\n    .configure_view(fill=PAGE_BG, stroke=INK_SOFT, strokeWidth=0)\n    .configure_axis(\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.10,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=18,\n        titleFontSize=22,\n    )\n    .configure_legend(\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=16,\n        titleFontSize=18,\n    )\n)\n\n# Save outputs\nchart.save(f\"plot-{THEME}.png\", scale_factor=3.0)\nchart.save(f\"plot-{THEME}.html\")\n"}