{"spec_id":"precision-recall","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nprecision-recall: Precision-Recall Curve\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-05-10\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, Legend\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\nfrom sklearn.datasets import make_classification\nfrom sklearn.linear_model import LogisticRegression\nfrom sklearn.metrics import average_precision_score, precision_recall_curve\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.naive_bayes import GaussianNB\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\nBRAND = \"#009E73\"  # Okabe-Ito position 1\nACCENT = \"#C475FD\"  # Okabe-Ito position 2\n\n# Data - Generate imbalanced classification dataset\nnp.random.seed(42)\nX, y_true = make_classification(\n    n_samples=2000, n_features=20, n_informative=10, n_redundant=5, n_classes=2, weights=[0.7, 0.3], random_state=42\n)\n\n# Split into train and test for realistic evaluation\nX_train, X_test, y_train, y_test = train_test_split(X, y_true, test_size=0.5, random_state=42, stratify=y_true)\n\n# Train two classifiers for comparison\nlr_model = LogisticRegression(random_state=42, max_iter=1000)\nnb_model = GaussianNB()\n\nlr_model.fit(X_train, y_train)\nnb_model.fit(X_train, y_train)\n\n# Get prediction probabilities on test set\nlr_scores = lr_model.predict_proba(X_test)[:, 1]\nnb_scores = nb_model.predict_proba(X_test)[:, 1]\n\n# Calculate precision-recall curves\nlr_precision, lr_recall, _ = precision_recall_curve(y_test, lr_scores)\nnb_precision, nb_recall, _ = precision_recall_curve(y_test, nb_scores)\n\n# Calculate Average Precision scores\nlr_ap = average_precision_score(y_test, lr_scores)\nnb_ap = average_precision_score(y_test, nb_scores)\n\n# Baseline (random classifier) - positive class ratio\nbaseline = np.mean(y_test)\n\n# Plot\np = figure(\n    width=4800,\n    height=2700,\n    title=\"precision-recall · bokeh · anyplot.ai\",\n    x_axis_label=\"Recall\",\n    y_axis_label=\"Precision\",\n    x_range=(-0.02, 1.05),\n    y_range=(0, 1.08),\n)\n\n# Create data sources for stepped lines\nlr_source = ColumnDataSource(data={\"recall\": lr_recall, \"precision\": lr_precision})\nnb_source = ColumnDataSource(data={\"recall\": nb_recall, \"precision\": nb_precision})\n\n# Plot Precision-Recall curves with step style\nlr_line = p.step(x=\"recall\", y=\"precision\", source=lr_source, line_width=5, color=BRAND, alpha=0.9, mode=\"after\")\n\nnb_line = p.step(x=\"recall\", y=\"precision\", source=nb_source, line_width=5, color=ACCENT, alpha=0.9, mode=\"after\")\n\n# Baseline reference line (random classifier)\nbaseline_source = ColumnDataSource(data={\"x\": [0, 1], \"y\": [baseline, baseline]})\nbaseline_line = p.line(\n    x=\"x\", y=\"y\", source=baseline_source, line_width=4, line_dash=\"dashed\", color=INK_SOFT, alpha=0.6\n)\n\n# Create legend with AP scores\nlegend = Legend(\n    items=[\n        (f\"Logistic Regression (AP = {lr_ap:.3f})\", [lr_line]),\n        (f\"Naive Bayes (AP = {nb_ap:.3f})\", [nb_line]),\n        (f\"Random Classifier (baseline = {baseline:.2f})\", [baseline_line]),\n    ],\n    location=\"top_right\",\n    label_text_font_size=\"18pt\",\n    glyph_width=50,\n    glyph_height=30,\n    spacing=20,\n    padding=25,\n    background_fill_alpha=0.95,\n    background_fill_color=ELEVATED_BG,\n    border_line_color=INK_SOFT,\n    border_line_width=2,\n)\n\np.add_layout(legend)\n\n# Style the plot\np.title.text_font_size = \"28pt\"\np.title.text_color = INK\np.xaxis.axis_label_text_font_size = \"22pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_font_size = \"22pt\"\np.yaxis.axis_label_text_color = INK\np.xaxis.major_label_text_font_size = \"18pt\"\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_font_size = \"18pt\"\np.yaxis.major_label_text_color = INK_SOFT\n\n# Grid styling\np.xgrid.grid_line_alpha = 0.10\np.ygrid.grid_line_alpha = 0.10\np.xgrid.grid_line_color = INK\np.ygrid.grid_line_color = INK\n\n# Axis styling\np.xaxis.axis_line_color = INK_SOFT\np.yaxis.axis_line_color = INK_SOFT\np.xaxis.axis_line_width = 3\np.yaxis.axis_line_width = 3\np.xaxis.major_tick_line_color = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\np.xaxis.major_tick_line_width = 3\np.yaxis.major_tick_line_width = 3\n\n# Background\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = INK_SOFT\np.outline_line_width = 2\n\n# Min border for padding\np.min_border_left = 100\np.min_border_right = 100\np.min_border_top = 80\np.min_border_bottom = 100\n\n# Save\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome\nW, H = 4800, 2700\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()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}