{"spec_id":"gain-curve","library":"altair","language":"python","code":"\"\"\" anyplot.ai\ngain-curve: Cumulative Gains Chart\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-05-11\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\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\nBRAND = \"#009E73\"  # Model curve\nSECONDARY = \"#C475FD\"  # Baseline reference\n\n# Data - Simulated model predictions for fraud detection\nnp.random.seed(42)\nn_samples = 1500\n\n# Generate model scores with strong discrimination\nbase_score = np.random.beta(2, 5, n_samples)\nnoise = np.random.normal(0, 0.1, n_samples)\ny_score = np.clip(base_score + noise, 0, 1)\n\n# Generate actual fraud outcomes - strong correlation with scores\nfraud_prob = 0.5 * y_score + 0.02\ny_true = (np.random.random(n_samples) < fraud_prob).astype(int)\n\n# Calculate cumulative gains curve\nsorted_indices = np.argsort(y_score)[::-1]\ny_true_sorted = y_true[sorted_indices]\n\ncumulative_positives = np.cumsum(y_true_sorted)\ntotal_positives = y_true_sorted.sum()\npct_population = np.arange(1, n_samples + 1) / n_samples * 100\npct_gain = cumulative_positives / total_positives * 100\n\n# Add origin point\npct_population = np.insert(pct_population, 0, 0)\npct_gain = np.insert(pct_gain, 0, 0)\n\n# Subsample for smooth visual\nsample_idx = np.concatenate([[0], np.arange(15, len(pct_population) - 1, 15), [len(pct_population) - 1]])\npct_population_smooth = pct_population[sample_idx]\npct_gain_smooth = pct_gain[sample_idx]\n\n# Create DataFrame\ndf_gains = pd.DataFrame({\"population\": pct_population_smooth, \"gain\": pct_gain_smooth, \"Type\": \"Model\"})\ndf_baseline = pd.DataFrame({\"population\": [0, 100], \"gain\": [0, 100], \"Type\": \"Baseline\"})\ndf_combined = pd.concat([df_gains, df_baseline], ignore_index=True)\n\n# Shaded area under model curve\narea_model = (\n    alt.Chart(df_combined[df_combined[\"Type\"] == \"Model\"])\n    .mark_area(opacity=0.15, color=BRAND, interpolate=\"monotone\")\n    .encode(x=\"population:Q\", y=\"gain:Q\")\n)\n\n# Model curve line\nmodel_curve = (\n    alt.Chart(df_combined[df_combined[\"Type\"] == \"Model\"])\n    .mark_line(strokeWidth=4, color=BRAND, interpolate=\"monotone\")\n    .encode(\n        x=alt.X(\n            \"population:Q\",\n            title=\"Population Targeted (%)\",\n            scale=alt.Scale(domain=[0, 100]),\n            axis=alt.Axis(titleFontSize=22, labelFontSize=18, tickCount=10, labelColor=INK_SOFT, titleColor=INK),\n        ),\n        y=alt.Y(\n            \"gain:Q\",\n            title=\"Positive Cases Captured (%)\",\n            scale=alt.Scale(domain=[0, 100]),\n            axis=alt.Axis(titleFontSize=22, labelFontSize=18, tickCount=10, labelColor=INK_SOFT, titleColor=INK),\n        ),\n    )\n)\n\n# Baseline diagonal line\nbaseline_line = (\n    alt.Chart(df_combined[df_combined[\"Type\"] == \"Baseline\"])\n    .mark_line(strokeWidth=3, strokeDash=[8, 4], color=INK_SOFT)\n    .encode(x=\"population:Q\", y=\"gain:Q\")\n)\n\n# Legend in lower-right area (out of data overlap)\nlegend_model_line = (\n    alt.Chart(pd.DataFrame({\"x\": [68, 68], \"y\": [15, 20]}))\n    .mark_line(strokeWidth=4, color=BRAND)\n    .encode(x=alt.X(\"x:Q\", scale=alt.Scale(domain=[0, 100])), y=alt.Y(\"y:Q\", scale=alt.Scale(domain=[0, 100])))\n)\n\nlegend_model_text = (\n    alt.Chart(pd.DataFrame({\"x\": [72], \"y\": [17.5], \"text\": [\"Model\"]}))\n    .mark_text(align=\"left\", fontSize=18, color=INK)\n    .encode(x=\"x:Q\", y=\"y:Q\", text=\"text:N\")\n)\n\nlegend_baseline_line = (\n    alt.Chart(pd.DataFrame({\"x\": [68, 68], \"y\": [8, 13]}))\n    .mark_line(strokeWidth=3, strokeDash=[8, 4], color=INK_SOFT)\n    .encode(x=alt.X(\"x:Q\", scale=alt.Scale(domain=[0, 100])), y=alt.Y(\"y:Q\", scale=alt.Scale(domain=[0, 100])))\n)\n\nlegend_baseline_text = (\n    alt.Chart(pd.DataFrame({\"x\": [72], \"y\": [10.5], \"text\": [\"Baseline\"]}))\n    .mark_text(align=\"left\", fontSize=18, color=INK)\n    .encode(x=\"x:Q\", y=\"y:Q\", text=\"text:N\")\n)\n\n# Combine all layers\nchart = (\n    alt.layer(\n        area_model,\n        baseline_line,\n        model_curve,\n        legend_model_line,\n        legend_model_text,\n        legend_baseline_line,\n        legend_baseline_text,\n    )\n    .properties(\n        width=1600,\n        height=900,\n        background=PAGE_BG,\n        title=alt.Title(\"gain-curve · altair · anyplot.ai\", fontSize=28, anchor=\"middle\", color=INK),\n    )\n    .configure_axis(gridColor=INK_SOFT, gridOpacity=0.10, gridDash=[2, 2], domainColor=INK_SOFT, tickColor=INK_SOFT)\n    .configure_view(strokeWidth=0, fill=PAGE_BG)\n    .configure_title(color=INK)\n)\n\n# Save output\nchart.save(f\"plot-{THEME}.png\", scale_factor=3.0)\nchart.save(f\"plot-{THEME}.html\")\n"}