Skip to content

Commit 72cb707

Browse files
feat(altair): implement lift-curve (#2409)
## Implementation: `lift-curve` - altair Implements the **altair** version of `lift-curve`. **File:** `plots/lift-curve/implementations/altair.py` --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20543282471)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 216420a commit 72cb707

2 files changed

Lines changed: 128 additions & 0 deletions

File tree

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
""" pyplots.ai
2+
lift-curve: Model Lift Chart
3+
Library: altair 6.0.0 | Python 3.13.11
4+
Quality: 92/100 | Created: 2025-12-27
5+
"""
6+
7+
import altair as alt
8+
import numpy as np
9+
import pandas as pd
10+
11+
12+
# Data - Simulate customer churn prediction model results
13+
np.random.seed(42)
14+
n_samples = 1000
15+
16+
# Create realistic churn prediction scenario
17+
# True positives have higher scores, some overlap for realism
18+
y_true = np.concatenate([np.ones(200), np.zeros(800)]) # 20% churn rate
19+
y_score = np.where(
20+
y_true == 1,
21+
np.clip(np.random.beta(5, 2, len(y_true)), 0, 1), # Churners: higher scores
22+
np.clip(np.random.beta(2, 5, len(y_true)), 0, 1), # Non-churners: lower scores
23+
)
24+
25+
# Calculate lift curve
26+
sorted_indices = np.argsort(y_score)[::-1] # Sort by score descending
27+
y_true_sorted = y_true[sorted_indices]
28+
29+
# Calculate cumulative lift at each percentage
30+
percentages = np.arange(1, 101)
31+
n_total = len(y_true)
32+
n_positives = y_true.sum()
33+
baseline_rate = n_positives / n_total
34+
35+
lift_values = []
36+
for pct in percentages:
37+
n_selected = int(np.ceil(n_total * pct / 100))
38+
n_captured = y_true_sorted[:n_selected].sum()
39+
model_rate = n_captured / n_selected
40+
lift = model_rate / baseline_rate
41+
lift_values.append(lift)
42+
43+
# Create DataFrame for Altair
44+
df = pd.DataFrame({"Population (%)": percentages, "Cumulative Lift": lift_values})
45+
46+
# Reference line at y=1 (random selection)
47+
df_reference = pd.DataFrame({"Population (%)": [0, 100], "Reference": [1.0, 1.0]})
48+
49+
# Create lift curve chart
50+
lift_line = (
51+
alt.Chart(df)
52+
.mark_line(strokeWidth=4, color="#306998")
53+
.encode(
54+
x=alt.X("Population (%):Q", scale=alt.Scale(domain=[0, 100]), title="Population Targeted (%)"),
55+
y=alt.Y("Cumulative Lift:Q", scale=alt.Scale(domain=[0, 5]), title="Cumulative Lift"),
56+
tooltip=[alt.Tooltip("Population (%):Q", format=".0f"), alt.Tooltip("Cumulative Lift:Q", format=".2f")],
57+
)
58+
)
59+
60+
# Reference line at lift = 1
61+
reference_line = (
62+
alt.Chart(df_reference)
63+
.mark_line(strokeWidth=2, strokeDash=[8, 4], color="#999999")
64+
.encode(x="Population (%):Q", y="Reference:Q")
65+
)
66+
67+
# Add decile markers
68+
decile_df = df[df["Population (%)"].isin([10, 20, 30, 40, 50, 60, 70, 80, 90, 100])]
69+
decile_points = (
70+
alt.Chart(decile_df)
71+
.mark_point(size=200, color="#306998", filled=True)
72+
.encode(
73+
x="Population (%):Q",
74+
y="Cumulative Lift:Q",
75+
tooltip=[
76+
alt.Tooltip("Population (%):Q", format=".0f", title="Decile %"),
77+
alt.Tooltip("Cumulative Lift:Q", format=".2f", title="Lift"),
78+
],
79+
)
80+
)
81+
82+
# Add annotation for reference line
83+
annotation = (
84+
alt.Chart(pd.DataFrame({"x": [75], "y": [1.25], "text": ["Random Selection (Lift = 1)"]}))
85+
.mark_text(fontSize=18, color="#555555", fontWeight="bold", align="center")
86+
.encode(x="x:Q", y="y:Q", text="text:N")
87+
)
88+
89+
# Combine all layers
90+
chart = (
91+
alt.layer(reference_line, lift_line, decile_points, annotation)
92+
.properties(
93+
width=1600, height=900, title=alt.Title(text="lift-curve · altair · pyplots.ai", fontSize=28, anchor="middle")
94+
)
95+
.configure_axis(labelFontSize=18, titleFontSize=22, gridColor="#dddddd", gridOpacity=0.3)
96+
.configure_view(strokeWidth=0)
97+
)
98+
99+
# Save as PNG and HTML
100+
chart.save("plot.png", scale_factor=3.0)
101+
chart.save("plot.html")
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
library: altair
2+
specification_id: lift-curve
3+
created: '2025-12-27T19:22:47Z'
4+
updated: '2025-12-27T19:25:43Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20543282471
7+
issue: 0
8+
python_version: 3.13.11
9+
library_version: 6.0.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/lift-curve/altair/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/lift-curve/altair/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/lift-curve/altair/plot.html
13+
quality_score: 92
14+
review:
15+
strengths:
16+
- Excellent lift curve visualization that clearly demonstrates model performance
17+
vs random selection
18+
- Well-implemented decile markers provide clear reference points for business decisions
19+
- Realistic customer churn prediction scenario with appropriate lift values
20+
- Clean code structure following KISS principles with proper random seed
21+
- Good use of Altair layering to combine lift line, reference line, markers, and
22+
annotation
23+
- Tooltips add interactive value for exploring specific lift values
24+
weaknesses:
25+
- Could add a small legend to label the lift curve line
26+
- Annotation text placement could be slightly adjusted to avoid visual proximity
27+
to the reference line

0 commit comments

Comments
 (0)