From Numbers to Tiers: Turning Continuous Spend into Business Buckets
Your stakeholder never says "Show me customers who spent $437.20 vs $441.80" โ they say "How many VIPs do we have?" or "Who is in the top 20%?". To answer, you must turn a continuous number like total_amount into a handful of labeled tiers. Pandas gives you two knives for that โ cut and qcut โ and they slice the same data in completely different ways. This article shows both on the same 10 customers so the difference is obvious.
1. The Data: 10 Customers Sorted by Total Spend
Same customers as the RFM article โ one row per customer, sorted low โ high. This ordering makes both binning methods visible:
| customer_id | total_amount ($) | Same value, what you will do with it |
|---|---|---|
| C007 | 18 | Small spenders |
| C003 | 90 | |
| C009 | 210 | Middle |
| C004 | 680 | |
| C005 | 890 | Big โ but gaps vary wildly (890โ2740 in 3 steps) |
| C001 | 1,274 | |
| C002 | 1,380 | |
| C010 | 2,740 | |
| C008 | 8,500 | Single huge outlier โ 3ร the next |
| C006 | 12,000 | Extreme outlier โ would dominate any fixed-width bin |
The spread is 18 โ 12,000 with one monster at 12K. Watch how each method handles it.
2. Method A โ pd.cut: Same Dollar Width Per Bucket (Business Rules)
cut chops the value range into equal-width intervals. You pick the dollar boundaries โ like pricing tiers on a website.
bins = [0, 500, 1500, 5000, float('inf')] # dollar cutoffs you choose
labels = ['Low', 'Medium', 'High', 'VIP'] # one label per interval
df['spend_tier'] = pd.cut(df['total_amount'], bins=bins, labels=labels)
print(df[['customer_id','total_amount','spend_tier']])
# customer_id total_amount spend_tier
# 0 C007 18 Low
# 1 C003 90 Low
# 2 C009 210 Low
# 3 C004 680 Medium
# 4 C005 890 Medium
# 5 C001 1274 Medium
# 6 C002 1380 Medium
# 7 C010 2740 High
# 8 C008 8500 VIP
# 9 C006 12000 VIP
# Counts: Low=3, Medium=4, High=1, VIP=2 โ buckets have VERY different sizes!
| Bucket | Dollar range | Who lands here | Count |
|---|---|---|---|
| Low | 0 โ 500 | 18, 90, 210 | 3 |
| Medium | 500 โ 1,500 | 680, 890, 1,274, 1,380 | 4 |
| High | 1,500 โ 5,000 | 2,740 | 1 |
| VIP | 5,000 โ โ | 8,500, 12,000 | 2 |
When to use cut: The buckets mean the same dollars for everyone โ stakeholders set them ("VIP starts at $5,000"). Good for pricing tiers, age bands, score thresholds. Weakness: skewed data โ lopsided bucket sizes โ High has 1 customer while Low has 3, and the 12K outlier sits comfortably in VIP.
3. Method B โ pd.qcut: Same Headcount Per Bucket (Fair Ranking)
qcut ignores dollar widths and forces equal headcount per bucket โ each bucket gets exactly 20% of customers when q=5. The dollar boundaries move to make that happen.
# 5 quintiles โ exactly 2 customers per bucket (10 customers / 5 = 2)
df['quintile'] = pd.qcut(df['total_amount'], q=5, labels=[1, 2, 3, 4, 5])
print(df[['customer_id','total_amount','quintile']])
# customer_id total_amount quintile
# 0 C007 18 1
# 1 C003 90 1
# 2 C009 210 2
# 3 C004 680 2
# 4 C005 890 3
# 5 C001 1274 3
# 6 C002 1380 4
# 7 C010 2740 4
# 8 C008 8500 5
# 9 C006 12000 5
# Counts: 1โ2, 2โ2, 3โ2, 4โ2, 5โ2 โ perfectly balanced!
# Boundaries were ~[18-210], [210-890], [890-1380], [1380-8500], [8500-12000]
| Quintile | Who lands here (by rank, not dollars) | Count | Dollar span |
|---|---|---|---|
| 1 (bottom 20%) | 18, 90 | 2 | $72 wide |
| 2 | 210, 680 | 2 | $470 wide |
| 3 | 890, 1,274 | 2 | $384 wide |
| 4 | 1,380, 2,740 | 2 | $1,360 wide |
| 5 (top 20%) | 8,500, 12,000 | 2 | $3,500 wide |
When to use qcut: You need fair, equal-sized tiers โ exactly what RFM scoring needs (top 20% โ score 5, next 20% โ 4, โฆ). Good for ranking, leaderboards, percentile bands. Weakness: bucket dollar widths are uneven and shift when data changes โ don't use it when stakeholders expect fixed $500 / $1,500 cutoffs.
4. Side-by-Side: Same Data, Different Story
| Question | pd.cut (equal-width) | pd.qcut (equal-frequency) |
|---|---|---|
| What stays equal? | Dollar width of each bucket | Headcount in each bucket |
| What varies? | Headcount (1โ4 per bucket) | Dollar width ($72 โ $3,500) |
| Pick whenโฆ | Business defines fixed thresholds (VIP = $5K+) | You need fair 20% tiers (RFM quintile scores) |
| Outlier effect | Outlier sits in the top bucket, rest of buckets still meaningful | Outlier compresses top bucket but counts stay fair |
How this feeds the next article: RFM scoring uses qcut โ not cut โ because "top 20% of spenders get score 5" must be fair regardless of the dollar spread. Read the next article's ยง5 to see why cut would mis-rank your best customers.
Quiz cue (Q3 Q04โ05): "Equal-width โ cut / Equal-frequency (20% each) โ qcut." "5 equal-sized tiers" is qcut(..., q=5).