{"spec_id":"gain-curve","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\ngain-curve: Cumulative Gains Chart\nLibrary: letsplot 4.9.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-11\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import *\nfrom lets_plot import element_blank, element_line, element_rect, element_text, theme\nfrom scipy.interpolate import make_interp_spline\n\n\nLetsPlot.setup_html()\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\"\nRULE = \"rgba(26,26,23,0.15)\" if THEME == \"light\" else \"rgba(240,239,232,0.15)\"\n\n# Data\nnp.random.seed(42)\nn_samples = 1000\n\n# Generate realistic customer response data\ncustomer_score = np.random.beta(2, 5, n_samples)\ny_true = (np.random.random(n_samples) < customer_score * 0.6 + 0.05).astype(int)\ny_score = customer_score + np.random.normal(0, 0.1, n_samples)\ny_score = np.clip(y_score, 0, 1)\n\n# Sort by predicted score descending\nsorted_indices = np.argsort(y_score)[::-1]\ny_true_sorted = y_true[sorted_indices]\n\n# Calculate cumulative gains\ntotal_positives = y_true.sum()\ncumulative_positives = np.cumsum(y_true_sorted)\ngains = cumulative_positives / total_positives * 100\npopulation_pct = np.arange(1, n_samples + 1) / n_samples * 100\n\n# Add origin point (0, 0)\ngains = np.insert(gains, 0, 0)\npopulation_pct = np.insert(population_pct, 0, 0)\n\n# Smooth the model curve using spline interpolation\nspl = make_interp_spline(population_pct, gains, k=3)\npopulation_smooth = np.linspace(0, 100, 300)\ngains_smooth = spl(population_smooth)\ngains_smooth = np.clip(gains_smooth, 0, 100)\n\n# Perfect model curve\npositive_rate = total_positives / n_samples\nperfect_gains = np.minimum(population_pct / (positive_rate * 100), 1) * 100\n\n# Random baseline\nrandom_gains = population_pct\n\n# Create DataFrames for plotting\ndf_model = pd.DataFrame({\"Population\": population_smooth, \"Gain\": gains_smooth, \"Type\": \"Model\"})\ndf_random = pd.DataFrame({\"Population\": population_pct, \"Gain\": random_gains, \"Type\": \"Random\"})\ndf_perfect = pd.DataFrame({\"Population\": population_pct, \"Gain\": perfect_gains, \"Type\": \"Perfect\"})\ndf_long = pd.concat([df_model, df_random, df_perfect], ignore_index=True)\n\n# Colors: Okabe-Ito palette\ncolors = {\n    \"Model\": \"#009E73\",  # Okabe-Ito position 1 (brand green)\n    \"Random\": \"#888888\",  # Neutral gray for reference line\n    \"Perfect\": \"#4467A3\",  # Okabe-Ito position 3 (blue)\n}\n\n# Plot\nplot = (\n    ggplot(df_long, aes(x=\"Population\", y=\"Gain\", color=\"Type\"))\n    + geom_line(size=1.5)\n    + scale_color_manual(values=colors, name=\"Curve\")\n    + scale_x_continuous(limits=[0, 100])\n    + scale_y_continuous(limits=[0, 100])\n    + labs(x=\"Population Targeted (%)\", y=\"Positive Cases Captured (%)\", title=\"gain-curve · letsplot · anyplot.ai\")\n    + theme_minimal()\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        panel_grid_major=element_line(color=RULE, size=0.3),\n        panel_grid_minor=element_blank(),\n        axis_title=element_text(size=20, color=INK),\n        axis_text=element_text(size=16, color=INK_SOFT),\n        axis_line=element_line(color=INK_SOFT),\n        plot_title=element_text(size=24, color=INK),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_text=element_text(size=16, color=INK_SOFT),\n        legend_title=element_text(size=18, color=INK),\n        legend_position=[0.85, 0.25],\n    )\n    + ggsize(1600, 900)\n)\n\n# Save\noutput_dir = os.getcwd()\nggsave(plot, os.path.join(output_dir, f\"plot-{THEME}.png\"), scale=3)\nggsave(plot, os.path.join(output_dir, f\"plot-{THEME}.html\"))\n"}