{"spec_id":"precision-recall","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nprecision-recall: Precision-Recall Curve\nLibrary: plotly 6.7.0 | Python 3.13.13\nQuality: 94/100 | Updated: 2026-05-10\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\nfrom sklearn.metrics import average_precision_score, precision_recall_curve\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\"\nGRID = \"rgba(26,26,23,0.10)\" if THEME == \"light\" else \"rgba(240,239,232,0.10)\"\n\n# Okabe-Ito palette\nBRAND = \"#009E73\"  # Position 1, first series\nSECONDARY = \"#BD8233\"  # imprint ochre — secondary reference (red is reserved for semantic bad)\n\n# Data - Simulate a binary classification scenario (fraud detection)\nnp.random.seed(42)\nn_samples = 1000\n\n# Imbalanced dataset: 10% positive class (fraud cases)\ny_true = np.zeros(n_samples, dtype=int)\ny_true[:100] = 1\nnp.random.shuffle(y_true)\n\n# Generate prediction scores - good classifier with some noise\ny_scores = np.where(\n    y_true == 1,\n    np.random.beta(5, 2, n_samples),  # Higher scores for positive class\n    np.random.beta(2, 5, n_samples),  # Lower scores for negative class\n)\n\n# Calculate precision-recall curve\nprecision, recall, thresholds = precision_recall_curve(y_true, y_scores)\naverage_precision = average_precision_score(y_true, y_scores)\n\n# Calculate baseline (random classifier performance)\npositive_class_ratio = np.mean(y_true)\n\n# Create figure\nfig = go.Figure()\n\n# Add precision-recall curve (stepped style for accuracy)\nfig.add_trace(\n    go.Scatter(\n        x=recall,\n        y=precision,\n        mode=\"lines\",\n        name=f\"Classifier (AP = {average_precision:.3f})\",\n        line={\"color\": BRAND, \"width\": 4, \"shape\": \"hv\"},\n        fill=\"tozeroy\",\n        fillcolor=f\"rgba({int(BRAND[1:3], 16)}, {int(BRAND[3:5], 16)}, {int(BRAND[5:7], 16)}, 0.15)\",\n        hovertemplate=\"<b>Classifier</b><br>Recall: %{x:.3f}<br>Precision: %{y:.3f}<extra></extra>\",\n    )\n)\n\n# Add baseline reference line (random classifier)\nfig.add_trace(\n    go.Scatter(\n        x=[0, 1],\n        y=[positive_class_ratio, positive_class_ratio],\n        mode=\"lines\",\n        name=f\"Random Baseline ({positive_class_ratio:.2f})\",\n        line={\"color\": SECONDARY, \"width\": 3, \"dash\": \"dash\"},\n        hovertemplate=\"<b>Random Baseline</b><br>Precision: %{y:.3f}<extra></extra>\",\n    )\n)\n\n# Add iso-F1 curves\nf1_values = [0.2, 0.4, 0.6, 0.8]\nfor f1 in f1_values:\n    # Iso-F1: precision = f1 * recall / (2 * recall - f1) for valid recall range\n    x_iso = np.linspace(f1 / 2 + 0.01, 1, 100)\n    y_iso = f1 * x_iso / (2 * x_iso - f1)\n    # Only keep valid values within [0, 1] range\n    mask = (y_iso > 0) & (y_iso <= 1)\n    fig.add_trace(\n        go.Scatter(\n            x=x_iso[mask],\n            y=y_iso[mask],\n            mode=\"lines\",\n            name=f\"F1 = {f1}\",\n            line={\"color\": INK_SOFT, \"width\": 2, \"dash\": \"dot\"},\n            opacity=0.6,\n            hovertemplate=\"<b>Iso-F1: %{fullData.name}</b><br>Recall: %{x:.3f}<br>Precision: %{y:.3f}<extra></extra>\",\n        )\n    )\n\n# Update layout for 4800x2700 px\nfig.update_layout(\n    title={\n        \"text\": \"precision-recall · plotly · anyplot.ai\",\n        \"font\": {\"size\": 28, \"color\": INK},\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n    },\n    xaxis={\n        \"title\": {\"text\": \"Recall (Sensitivity)\", \"font\": {\"size\": 22, \"color\": INK}},\n        \"tickfont\": {\"size\": 18, \"color\": INK_SOFT},\n        \"range\": [0, 1.02],\n        \"showgrid\": True,\n        \"gridcolor\": GRID,\n        \"gridwidth\": 1,\n        \"zeroline\": False,\n        \"linecolor\": INK_SOFT,\n        \"linewidth\": 1,\n    },\n    yaxis={\n        \"title\": {\"text\": \"Precision (Positive Predictive Value)\", \"font\": {\"size\": 22, \"color\": INK}},\n        \"tickfont\": {\"size\": 18, \"color\": INK_SOFT},\n        \"range\": [0, 1.05],\n        \"showgrid\": True,\n        \"gridcolor\": GRID,\n        \"gridwidth\": 1,\n        \"zeroline\": False,\n        \"linecolor\": INK_SOFT,\n        \"linewidth\": 1,\n    },\n    legend={\n        \"font\": {\"size\": 16, \"color\": INK_SOFT},\n        \"x\": 0.02,\n        \"y\": 0.98,\n        \"xanchor\": \"left\",\n        \"yanchor\": \"top\",\n        \"bgcolor\": ELEVATED_BG,\n        \"bordercolor\": INK_SOFT,\n        \"borderwidth\": 1,\n    },\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    margin={\"l\": 120, \"r\": 60, \"t\": 100, \"b\": 120},\n    hovermode=\"closest\",\n)\n\n# Save outputs\nfig.write_image(f\"plot-{THEME}.png\", width=1600, height=900, scale=3)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}