Model Lift Chart — Altair

A lift curve visualizes how much better a predictive model performs compared to random selection, showing the cumulative lift ratio as you target increasing percentages of the population. It answers the question: "If I target the top X% of predictions, how many times more responders will I capture than random targeting?" This plot is essential for evaluating and comparing classification models in scenarios where targeting efficiency matters.

Model Lift Chart rendered with Altair

Python source (Altair)

""" anyplot.ai
lift-curve: Model Lift Chart
Library: altair 6.1.0 | Python 3.13.13
Quality: 91/100 | Updated: 2026-05-10
"""

import os
import sys


_script_dir = os.path.dirname(os.path.abspath(__file__))
if _script_dir in sys.path:
    sys.path.remove(_script_dir)
if "" in sys.path:
    sys.path.remove("")

import altair as alt
import numpy as np
import pandas as pd


# Theme tokens (see prompts/default-style-guide.md)
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"

# Okabe-Ito palette
BRAND = "#009E73"  # First series (lift curve)
REFERENCE_COLOR = "#999999"  # Reference line (adaptive neutral)

# Data - Simulate customer churn prediction model results
np.random.seed(42)
n_samples = 1000

# Create realistic churn prediction scenario
# True positives have higher scores, some overlap for realism
y_true = np.concatenate([np.ones(200), np.zeros(800)])  # 20% churn rate
y_score = np.concatenate(
    [
        np.clip(np.random.beta(5, 2, 200), 0, 1),  # Churners: higher scores
        np.clip(np.random.beta(2, 5, 800), 0, 1),  # Non-churners: lower scores
    ]
)

# Calculate lift curve
sorted_indices = np.argsort(y_score)[::-1]  # Sort by score descending
y_true_sorted = y_true[sorted_indices]

# Calculate cumulative lift at each percentage
percentages = np.arange(1, 101)
n_total = len(y_true)
n_positives = y_true.sum()
baseline_rate = n_positives / n_total

lift_values = []
for pct in percentages:
    n_selected = int(np.ceil(n_total * pct / 100))
    n_captured = y_true_sorted[:n_selected].sum()
    model_rate = n_captured / n_selected
    lift = model_rate / baseline_rate
    lift_values.append(lift)

# Create DataFrame for Altair
df = pd.DataFrame({"Population (%)": percentages, "Cumulative Lift": lift_values})

# Reference line at y=1 (random selection)
df_reference = pd.DataFrame({"Population (%)": [0, 100], "Reference": [1.0, 1.0]})

# Create lift curve chart
lift_line = (
    alt.Chart(df)
    .mark_line(strokeWidth=4, color=BRAND)
    .encode(
        x=alt.X("Population (%):Q", scale=alt.Scale(domain=[0, 100]), title="Population Targeted (%)"),
        y=alt.Y("Cumulative Lift:Q", scale=alt.Scale(domain=[0, 5]), title="Cumulative Lift"),
        tooltip=[alt.Tooltip("Population (%):Q", format=".0f"), alt.Tooltip("Cumulative Lift:Q", format=".2f")],
    )
)

# Reference line at lift = 1
reference_line = (
    alt.Chart(df_reference)
    .mark_line(strokeWidth=2, strokeDash=[8, 4], color=REFERENCE_COLOR)
    .encode(x="Population (%):Q", y="Reference:Q")
)

# Add decile markers
decile_df = df[df["Population (%)"].isin([10, 20, 30, 40, 50, 60, 70, 80, 90, 100])]
decile_points = (
    alt.Chart(decile_df)
    .mark_point(size=200, color=BRAND, filled=True)
    .encode(
        x="Population (%):Q",
        y="Cumulative Lift:Q",
        tooltip=[
            alt.Tooltip("Population (%):Q", format=".0f", title="Decile %"),
            alt.Tooltip("Cumulative Lift:Q", format=".2f", title="Lift"),
        ],
    )
)

# Combine all layers
chart = (
    alt.layer(reference_line, lift_line, decile_points)
    .properties(
        width=1600,
        height=900,
        background=PAGE_BG,
        title=alt.Title(text="lift-curve · altair · anyplot.ai", fontSize=28, anchor="middle"),
    )
    .configure_axis(
        labelFontSize=18,
        titleFontSize=22,
        labelColor=INK_SOFT,
        titleColor=INK,
        domainColor=INK_SOFT,
        tickColor=INK_SOFT,
        gridColor=INK,
        gridOpacity=0.10,
    )
    .configure_view(fill=PAGE_BG, stroke=INK_SOFT)
    .configure_title(color=INK)
)

# Save as PNG and HTML
chart.save(f"plot-{THEME}.png", scale_factor=3.0)
chart.save(f"plot-{THEME}.html")

Part of Model Lift Chart on anyplot.ai.

Other implementations