Cumulative Gains Chart — Plotly

A cumulative gains chart visualizes the effectiveness of a classification model by showing what percentage of positive cases is captured when targeting increasing percentages of the population, ranked by predicted probability. It answers the question: "If I target the top X% of my predictions, what percentage of all actual positives will I capture?" This plot is essential for evaluating targeting strategies in marketing, risk assessment, and resource allocation scenarios.

Cumulative Gains Chart rendered with Plotly

Python source (Plotly)

""" anyplot.ai
gain-curve: Cumulative Gains Chart
Library: plotly 6.7.0 | Python 3.13.13
Quality: 94/100 | Updated: 2026-05-11
"""

import os

import numpy as np
import plotly.graph_objects as go


# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
GRID = "rgba(26,26,23,0.10)" if THEME == "light" else "rgba(240,239,232,0.10)"

# Okabe-Ito palette
BRAND = "#009E73"  # First series — always
BASELINE = "#888888"  # Neutral for reference
PERFECT = "#4467A3"  # imprint blue — neutral reference for perfect model (red is reserved for semantic bad)

# Data - Customer response model evaluation
np.random.seed(42)
n_samples = 1000

# Simulate a classification model with moderate discrimination
positive_rate = 0.20
y_true = np.random.binomial(1, positive_rate, n_samples)

# Generate predicted scores that correlate with true labels
y_score = np.where(
    y_true == 1,
    np.random.beta(5, 2, n_samples),  # Positives: skewed toward higher scores
    np.random.beta(2, 5, n_samples),  # Negatives: skewed toward lower scores
)
y_score = np.clip(y_score + np.random.normal(0, 0.1, n_samples), 0, 1)

# Calculate cumulative gains
sorted_indices = np.argsort(y_score)[::-1]
y_true_sorted = y_true[sorted_indices]

cumulative_positives = np.cumsum(y_true_sorted)
total_positives = y_true.sum()

pct_population = np.arange(1, n_samples + 1) / n_samples * 100
pct_positives_captured = cumulative_positives / total_positives * 100

# Add origin point
pct_population = np.insert(pct_population, 0, 0)
pct_positives_captured = np.insert(pct_positives_captured, 0, 0)

# Perfect model curve
pct_for_perfect = positive_rate * 100
perfect_x = [0, pct_for_perfect, 100]
perfect_y = [0, 100, 100]

# Plot
fig = go.Figure()

# Random baseline (diagonal)
fig.add_trace(
    go.Scatter(
        x=[0, 100],
        y=[0, 100],
        mode="lines",
        name="Random (Baseline)",
        line=dict(color=BASELINE, width=3, dash="dash"),
        hovertemplate="Baseline<br>Population: %{x:.1f}%<br>Positives: %{y:.1f}%<extra></extra>",
    )
)

# Perfect model
fig.add_trace(
    go.Scatter(
        x=perfect_x,
        y=perfect_y,
        mode="lines",
        name="Perfect Model",
        line=dict(color=PERFECT, width=3, dash="dot"),
        hovertemplate="Perfect<br>Population: %{x:.1f}%<br>Positives: %{y:.1f}%<extra></extra>",
    )
)

# Model gains curve
fig.add_trace(
    go.Scatter(
        x=pct_population,
        y=pct_positives_captured,
        mode="lines",
        name="Model",
        line=dict(color=BRAND, width=4),
        fill="tozeroy",
        fillcolor="rgba(0, 158, 115, 0.15)",
        hovertemplate="Model<br>Population: %{x:.1f}%<br>Positives: %{y:.1f}%<extra></extra>",
    )
)

# Layout
fig.update_layout(
    title=dict(text="gain-curve · plotly · anyplot.ai", font=dict(size=28, color=INK), x=0.5, xanchor="center"),
    xaxis=dict(
        title=dict(text="Percentage of Population Targeted (%)", font=dict(size=22, color=INK)),
        tickfont=dict(size=18, color=INK_SOFT),
        range=[0, 100],
        dtick=20,
        showgrid=True,
        gridcolor=GRID,
        linecolor=INK_SOFT,
    ),
    yaxis=dict(
        title=dict(text="Percentage of Positives Captured (%)", font=dict(size=22, color=INK)),
        tickfont=dict(size=18, color=INK_SOFT),
        range=[0, 100],
        dtick=20,
        showgrid=True,
        gridcolor=GRID,
        linecolor=INK_SOFT,
    ),
    paper_bgcolor=PAGE_BG,
    plot_bgcolor=PAGE_BG,
    legend=dict(
        x=0.98,
        y=0.02,
        xanchor="right",
        yanchor="bottom",
        font=dict(size=18, color=INK_SOFT),
        bgcolor=ELEVATED_BG,
        bordercolor=INK_SOFT,
        borderwidth=1,
    ),
    margin=dict(l=100, r=80, t=100, b=100),
)

# Save
fig.write_image(f"plot-{THEME}.png", width=1600, height=900, scale=3)
fig.write_html(f"plot-{THEME}.html", include_plotlyjs="cdn")

Part of Cumulative Gains Chart on anyplot.ai.

Other implementations