{"spec_id":"lift-curve","library":"letsplot","language":"python","code":"# ruff: noqa: F405\n\"\"\"anyplot.ai\nlift-curve: Model Lift Chart\nLibrary: lets-plot | Python 3.13\nQuality: pending | Created: 2025-12-27\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import *\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\"\nBRAND = \"#009E73\"  # Okabe-Ito position 1\nREFERENCE = \"#AE3030\"  # Okabe-Ito position 5 for reference line\n\n# Data: Fraud detection model\n# Using exponential distribution for scores to reflect realistic fraud detection patterns\nnp.random.seed(42)\nn_samples = 1200\n\n# Fraud cases (10% base rate) with skewed distribution\nn_fraud = int(n_samples * 0.10)\nfraud_scores = np.random.exponential(scale=0.3, size=n_fraud)\nfraud_scores = np.clip(fraud_scores, 0, 1)  # Normalize to [0, 1]\n\n# Legitimate cases with lower exponential scores\nlegit_scores = np.random.exponential(scale=0.15, size=n_samples - n_fraud)\nlegit_scores = np.clip(legit_scores, 0, 1)\n\ny_true = np.concatenate([np.ones(n_fraud), np.zeros(n_samples - n_fraud)])\ny_score = np.concatenate([fraud_scores, legit_scores])\n\n# Shuffle data\nshuffle_idx = np.random.permutation(n_samples)\ny_true = y_true[shuffle_idx]\ny_score = y_score[shuffle_idx]\n\n# Calculate lift curve\nsorted_idx = np.argsort(y_score)[::-1]\ny_true_sorted = y_true[sorted_idx]\n\nn_fraud_total = np.sum(y_true)\nbaseline_rate = n_fraud_total / n_samples\ncumsum_fraud = np.cumsum(y_true_sorted)\nn_evaluated = np.arange(1, n_samples + 1)\npopulation_pct = n_evaluated / n_samples * 100\nresponse_rate = cumsum_fraud / n_evaluated\nlift = response_rate / baseline_rate\n\n# Sample points for smooth curve\nsample_idx = np.arange(0, n_samples, max(1, n_samples // 100))\ndf = pd.DataFrame({\"population_pct\": population_pct[sample_idx], \"lift\": lift[sample_idx]})\n\n# Reference line\nref_df = pd.DataFrame({\"population_pct\": [0, 100], \"lift\": [1, 1]})\n\n# Create plot with theme-adaptive styling\nplot = (\n    ggplot()\n    + geom_line(aes(x=\"population_pct\", y=\"lift\"), data=ref_df, color=INK_SOFT, size=1.5, linetype=\"dashed\", alpha=0.6)\n    + geom_line(aes(x=\"population_pct\", y=\"lift\"), data=df, color=BRAND, size=2.5)\n    + geom_point(aes(x=\"population_pct\", y=\"lift\"), data=df[::5], color=BRAND, size=4, alpha=0.8)\n    + labs(x=\"Population Targeted (%)\", y=\"Cumulative Lift\", title=\"lift-curve · letsplot · anyplot.ai\")\n    + scale_x_continuous(breaks=list(range(0, 101, 10)), limits=[0, 100])\n    + scale_y_continuous(breaks=[1, 2, 3, 4, 5, 6, 7])\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=INK_SOFT, size=0.2),\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, size=0.5),\n        plot_title=element_text(size=24, color=INK),\n        legend_background=element_rect(fill=ELEVATED_BG),\n        legend_text=element_text(color=INK_SOFT),\n    )\n    + ggsize(1600, 900)\n)\n\n# Add reference line label\nplot = plot + geom_text(\n    aes(x=\"x\", y=\"y\", label=\"label\"),\n    data=pd.DataFrame({\"x\": [85], \"y\": [1.15], \"label\": [\"Baseline (Lift = 1)\"]}),\n    size=14,\n    color=INK_SOFT,\n)\n\n# Save with theme suffix\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=3)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}