Where we start
Session 1 ended with a table: one row per customer, R, F and M, and a knife at the mean of each column. The mean is where most tutorials put it, and it is one of at least five defensible places. Each place produces a different set of people called "high", and downstream every action table, every win-back list and every dashboard tile inherits that choice without saying so. Tonight is four short ideas, where the knife can go, what a column full of ties does to it, what the popular taxonomies feed on, and what happens when the pile is 300 people instead of 93,000, and then 22 minutes of running all five cuts on the same table and reading the differences.
★ What you walk out with tonight. Five cuts you can run on any RFM table in twenty lines, a one-function report that prints who each cut calls best and how many of them bought once, the reason pd.qcut refuses your frequency column and what the usual fix silently does, and the question to ask of any marketing tool before trusting its Champions tile: "where is the knife, and what does it do with ties?"
Five honest places for one knife
6 min liveTake the spend column. 93,358 numbers, mean R$ 165.20, median R$ 107.78, and the mean sits above the median because a long tail of big baskets drags it right. Every cut below is a legitimate answer to "who spends a lot?", and each one lands on a different share of the same people.
Livepd.cut fixes the edges, pd.qcut fixes the counts4 min▶
There are two families of knife and pandas has a function for each. pd.cut takes edges you choose: the mean, the median, a round R$ 300, a 180-day line. The shares that land in each bin are whatever the data makes them, and on a skewed column they are lopsided. pd.qcut takes counts you choose: thirds, fifths, tenths. It then puts the edges wherever it has to so the bins come out equal, which on a skewed column means the edges crowd into the dense part and one bin at the top spans everything from "a bit above typical" to the largest order in the file.
Two mechanics to hold onto. Both functions build left-open, right-closed intervals by default, so a customer sitting exactly on an edge falls into the lower bin; if your threshold is "R$ 300 or more counts as big", pass right=False or shift the edge. And recency is scored in reverse: fewer days is better, so the labels for R run labels=[5, 4, 3, 2, 1] while F and M run [1, 2, 3, 4, 5]. Forgetting the reversal is the most common RFM bug in the wild, and it produces a Champions segment made of people who have not bought in a year.
The five placements tonight, on the spend column: the mean (R$ 165.20, 29.3 percent above it), the median (R$ 107.78, 50 percent above it by construction), rank thirds (the top 33 percent by construction), rank fifths (the top 20 percent by construction), and a merchant threshold, a round number the owner would actually write on a whiteboard. On Olist a R$ 300 line takes roughly the top tenth of customers. Five knives, five shares, one column.
Self-studyWhy mean and median disagree on spend: skew2 min▶
If spend were symmetric the mean and the median would sit on the same number and "above the mean" would be half the customers. On Olist the mean is R$ 165 and the median R$ 108. The gap is the tail: a minority of big baskets pulls the arithmetic average up while leaving the middle customer where she was. The top 10 percent of customers by spend hold 38.3 percent of revenue, and every one of those large numbers pushes the mean right.
The consequence for the knife: "above the mean" on a skewed column is not "the top half", it is a minority, 29.3 percent here, and the more skewed the column the smaller that minority gets. The median is immune to the tail by construction. Neither is wrong. The mean answers "who spends more than the average customer's share of the money"; the median answers "who is in the upper half". Write down which question you are asking before choosing the knife, and expect the two to disagree more on spend than on recency, because money has a longer tail than time does.
Two analysts, one table. One cuts spend at the mean, the other at the median, both by habit. In the first report "high spend" is 29 percent of customers; in the second it is 50 percent. The dashboard shows both numbers in the same week and the marketing lead asks which one is broken. Neither is. Both analysts made a defensible choice, and the argument that followed was the cost of not writing the choice into the code where a diff would have ended it in one line.
Identical customers, different scores
5 min liveFrequency on Olist is 97.0 percent ones. Ask pandas for fifths and it refuses, honestly, because there is no place to put four edges inside a column that is almost entirely the same value. The usual fix makes the error go away. It does not make the problem go away; it hides it inside the row order of your database.
LiveRow order decides: identical customers, different scores3 min▶
Run pd.qcut(rfm["F"], 5) on Olist and pandas raises ValueError: Bin edges must be unique: [1, 1, 1, 1, 1, 15]. Read the edges: the 20th, 40th, 60th and 80th percentiles of frequency are all 1, because 97 percent of the column is 1. There is no honest place to put four knives. The error is pandas telling you the truth about your data.
The fix every tutorial reaches for is to rank first: pd.qcut(rfm["F"].rank(method="first"), 5). method="first" breaks ties by order of appearance, so the 90,557 identical one-time buyers get 90,557 different ranks, the edges become unique, and the error disappears. What has actually happened is that a column with almost no information has been given five equal bins by row order. On Olist that scores 18,672 identical one-time buyers a 1 and 15,871 identical one-time buyers a 5. The 2,801 repeat buyers all land in the top fifth too, which is correct, but they share it with fifteen thousand people who are no different from the ones in the bottom fifth.
Every downstream Champion built on that score is decided by that row order. It is the whole reason the vendor maps in Part 3 crown one-time buyers.
Self-studyWhat the fix is actually for2 min▶
The rank trick is not wrong in itself. It exists for spiky columns with real variety: a frequency column where most values are distinct but a few hundred customers share a value that happens to sit on a quintile edge. There, ranking breaks a tie among a few people and moves an edge by a few rows, which is a fair price for equal bins. The trick becomes arbitrary only when the tie is most of the column, as it is on any one-time-buyer base, which is most retail data.
The test before applying it: rfm["F"].value_counts(normalize=True).head(). If one value holds more than a fifth of the rows, fifths on that column are not fifths, whatever pandas returns, and the honest move is a cut with chosen edges (once, twice, three or more) or a different dimension altogether. The full quintile build, the personas it feeds, and the persona-migration crosstab between two snapshots live in the retention course, Retention a4; the mechanics are taught there and not repeated here. Tonight is about what the mechanics do to a base like this one.
The platform default. A marketing platform ships quintile RFM scoring out of the box and a shop with a one-time-buyer base turns it on. The scoring runs without complaint, because the platform's implementation breaks ties the same way the tutorials do. Its "top fifth by frequency" tile is then filled by whichever one-time buyers were stored last, a segment that changes every time the database is re-indexed and that nobody can explain to the owner. The tile is not lying about the arithmetic. It is lying about what a fifth means on this base.
Eight groups, six groups, eleven groups
5 min liveA cut produces scores; a taxonomy turns scores into names. Two cuts per axis give 8 codes and 8 groups. Klaviyo's published rule, three levels per axis, gives 27 codes collapsed to six groups. Fifths per axis give 125 codes, which Putler's much-copied table collapses to eleven. On the same 93,358 people, read what each one calls "Champions".
| Cut and map | Cells | Called "best" | Share of customers | Of whom bought exactly once |
|---|---|---|---|---|
| Mean split, eight groups | 8 | High value (111) | 1.28% | 0% |
| Klaviyo's rule, six groups | 27, collapsed to 6 | Champions (333, 332, 323) | 13.9% | 91.0% |
| Fifths into Putler's eleven | 125, collapsed to 11 | Champions (R 4-5, F+M 4-5) | 16.1% | 92.1% |
LiveThe tutorial route: fifths first, then collapse to eight3 min▶
Most RFM tutorials, including the one many analysts learn from, do not put the knife at the mean. They rank each column into fifths with pd.qcut, then collapse: a score of 4 or 5 becomes a 1, a score of 1 to 3 becomes a 0, and the three bits are read as the eight groups of session 1. It looks like the same taxonomy with a tidier scoring step in front of it.
It is a different knife. Collapsing at 3 is a rank split at the top two fifths of each axis, by construction, so 40 percent of customers are "high" on every column regardless of the column's shape. On frequency that is impossible to do honestly: 97 percent of the column is the value 1, so the fifths come from row order (Part 2), and the collapse then promotes about two fifths of identical one-time buyers to "frequent". On Olist the route calls 6.92 percent of customers High value, and 84.8 percent of them bought exactly once; its Key win-back group is 9.8 percent of customers and 86.3 percent of those bought once. The mean split on the same people: 1.28 percent High value with nobody who bought once, 0.79 percent Key win-back and every one a repeat buyer.
Neither is the wrong taxonomy. The eight names are identical. What differs is where the knife sits and whether the column can bear it, and that is a decision to write down, not inherit. Session 3 runs both on the same 4,000 people side by side.
LiveMore cells is not more truth3 min▶
The mean split's High value corner is small because "frequent" means above the mean of 1.033 orders, which is two or more, and only 3.0 percent of Olist customers ever got there. Zero percent of High value bought once, by construction. It is a tiny group, and it is a true one.
Klaviyo's rule scores recency by calendar (3 if the last order was within 180 days, 2 within 365, else 1) and frequency and spend by rank thirds. Rank thirds on a 97-percent-ones column is the Part 2 row-order problem with three bins instead of five: two thirds of the one-time buyers are handed an F of 2 or 3 by row order. Champions are the codes 333, 332 and 323, which lands on 13.9 percent of customers, and 91.0 percent of them bought once. Putler's table takes fifths on all three axes, averages F and M, and calls R 4-5 with F+M 4-5 Champions: 16.1 percent, 92.1 percent of them one-time buyers. On this base the two vendor maps agree with each other and disagree with the data.
Replication note. Neither Klaviyo nor Putler documents how its live product breaks ties on a column that is 97 percent the same value. The figures above use the pandas rank(method="first") convention, which is what every tutorial that "fixes" the duplicate-edge error does. They are a property of that convention on this base, not a claim about either product's output on your account. Ask your vendor the tie question directly; the honest answer is worth more than the segment tile.
Self-studyKlaviyo's and Putler's published rules, quoted4 min▶
Klaviyo (help centre article 17797937793179). Recency: 3 if the customer bought within the last 180 days, 2 if within 365 days, 1 otherwise. Frequency and monetary: each ranked into thirds, 3 for the top third. The 27 three-digit codes collapse into six groups:
| Group | Codes |
|---|---|
| Champions | 333, 332, 323 |
| Loyal | 321, 322, 331, 232, 233 |
| Recent | 312, 313, 311, 222, 223 |
| Needs attention | 213, 221, 123, 132, 133 |
| At risk | 231, 212, 122, 131, 211 |
| Inactive | 111, 112, 113, 121 |
On Olist that produces Recent 21.6 percent, Loyal 21.5 percent, At risk 17.3 percent, Needs attention 15.5 percent, Champions 13.9 percent. Needs attention, the natural win-back audience, is 96.7 percent one-time buyers.
Putler (putler.com/rfm-analysis). Each of R, F and M ranked into fifths, 5 best; F and M averaged into one score; eleven segments by ranges of R and F+M: Champions R 4-5 and F+M 4-5; Loyal R 2-5 and F+M 3-5; Potential loyalist R 3-5 and F+M 1-3; New R 4-5 and F+M 0-1; Promising R 3-4 and F+M 0-1; Needs attention R 2-3 and F+M 2-3; About to sleep R 2-3 and F+M 0-2; At risk R 0-2 and F+M 2-5; Can't lose R 0-1 and F+M 4-5; Hibernating R 1-2 and F+M 1-2; Lost R 0-2 and F+M 0-2. The published ranges overlap, so a customer can match more than one; the replication takes the first match reading top to bottom, and the build-along says so in the code.
On Olist that produces Loyal 31.6 percent, Potential loyalist 24.3 percent, At risk 19.2 percent, Champions 16.1 percent, and four of the eleven segments empty: New, Promising, Can't lose and Lost never fill because averaging F and M fifths never reaches the corner values when 97 percent of F is tied. At risk plus Can't lose, the win-back audience, is 97.6 percent one-time buyers.
The Champions tile. A shop switches on an RFM dashboard with Klaviyo-style defaults and the tile says 14 percent of customers are Champions. The team plans a loyalty tier around them. Someone counts how many of those Champions have a second order in the system and the answer is roughly one in ten. The tile was computed correctly from its own rule; the rule was written for a base where thirds mean something. The question that would have saved the quarter is not "is the maths right" but "what does the top third of a column that is 97 percent ones contain".
When the pile is 300 people
3 min liveEverything so far was on 93,358 rows, where a 1.28 percent group is still 1,195 people. Now a constructed case: a WeChat community shop, a 私域 (private-domain community) group of 300 customers, one of whom is a household that buys for a whole residential compound. The shop is invented. The shape is not, and it breaks every rank-based cut on the page.
LiveSixty people a bin, and one of them is a building3 min▶
Say the shop has 300 customers in its community group. Fifths put 60 in each bin, thirds put 100. A "Champions" segment of 5 percent is 15 people. Percentages below that are not segments; they are a short list of names the owner could recite. Any rank-based cut on 300 rows manufactures a precision the pile does not have, and the tie problem from Part 2 is worse, not better, because a community shop's frequency column is often a handful of values shared by dozens.
Then the household. One member buys for an entire compound, so her spend is many times anyone else's. The mean of the spend column is dragged toward her, and "above the mean" is suddenly a group of a few dozen people who happen to be on the near side of one buyer's total. The median does not move; a threshold the owner would write down ("regulars are anyone over 500 yuan a quarter") does not move either. The honest cut at this size is a median or a merchant threshold, plus owner knowledge: the owner already knows which twenty customers matter, and the model's job is to confirm and organise that, not to overrule it with fifths.
Your numbers will differ, and no output is printed for this shop anywhere on the page, because it is invented. What transfers is the check: divide your N by the number of bins before you choose the knife, and if the answer is a room of people rather than a population, cut with edges you can defend out loud.
This shop is invented, and the pattern is everywhere in 私域 (private-domain community). Community selling on WeChat runs at exactly this scale: a few hundred contacts in a group, one seller, orders taken in chat. One member routinely buys for neighbours, a dormitory or a family compound, because that is how group buying works, and her total is a business line, not a data error. Every tutorial cut assumes a population; this is a room. Vendor claims that private-traffic repeat rates run several times higher than public-platform rates circulate widely and are not verified by any primary source read for this page, so treat them as marketing until you have measured your own room.
Five cuts on the same 93,358 customers
★ 22 min · everyone buildsStart from the table session 1 saved, rfm_2018-09-01.csv, one row per customer with R, F and M. Nine steps: one report function, then six knives through it (mean, median, fixed edges, Klaviyo thirds, fifths into Putler, fifths then collapse), then a table that puts the four "Champions" shares side by side. Every number in a comment is what the code prints on Olist; if yours differs, the difference is the knife.
Load the saved table and write the report once. Every cut below produces a Series of group labels aligned to the table; the report takes that Series plus the labels that mean "best" and "win back", and prints the same four things every time.
The mean split, as a function of the centre. Same as last week, but the centre is a parameter so step 3 can swap it in one word. Recency is reversed.
The median split. One word changes. The frequency median is 1, so "frequent" still means two or more; the spend knife drops from R$ 165 to R$ 108 and every group that has a 1 in the third digit grows.
Fifths on frequency: the error, then the fix, then what the fix did. Let the error print. Then rank first, and cross-tabulate the real order count against the score it was given.
Fixed edges with pd.cut, and the row it quietly drops. The tutorial edges for recency are 0, 50, 100, 200, 300 and 365 days. On a file whose recency runs to 715 days, every customer past the last edge becomes NaN, and nothing warns you.
Klaviyo's rule. Recency by calendar, 180 and 365 days; frequency and spend by rank thirds, with the same rank trick; then the published dictionary from 27 codes to six groups.
Putler's eleven. Fifths on all three, F and M averaged, half scores rounded up, then the published ranges. The ranges overlap, so the function reads top to bottom and the first match wins; that is the convention, and it is written where the next reader can see it.
The tutorial route: fifths, then collapse to eight. Score every column 1 to 5 by rank, call a score above 3 "high", read the bits as the same eight groups. It is a rank split at the top two fifths of each axis, so on frequency it inherits step 4's row-order scoring before the collapse.
One table, four "Champions". The same people, four shares, and a column that says how many of the best never came back.
Keep the cut in the filename too. segments_2018-09-01_mean.csv, segments_2018-09-01_klaviyo.csv. A segment table without its knife written on it is the Part 1 argument waiting to happen again next quarter, and session 6 will need both the date and the cut to compare two runs honestly.
Before session 3
30 min- Run all five cuts on your own table, or on Olist if you do not have one. Use the step 1 report unchanged so the five printouts line up, and keep the printouts.
- Find out which cut your marketing tool uses. Open its documentation, not its dashboard, and write down the knife: fixed days or ranks for recency, thirds or fifths or thresholds for the rest, and whether it says anything at all about ties. If it does not, write "ties: undocumented".
- Count how many of your "Champions" bought once under each of the five cuts. On Olist the answers are 0 percent, 0 percent, 91 percent, 92 percent and whatever your own thresholds give. Yours will differ; the spread between them is the point.
- Bring the numbers to session 3. A 4,000-customer sample of this table goes on a bench where six cuts are computed live, and the win-back audience each one draws gets costed at R$ 0.30 a message.
Three questions
Clear all three and session 2 gets stamped in your passport.
1 · pd.qcut(rfm["F"], 5) raises "Bin edges must be unique: [1, 1, 1, 1, 1, 15]" on Olist. What is going on, and what does the usual rank(method="first") fix do?
The error is pandas being honest about a column with almost no information in it. Ranking first makes the error go away by turning row order into a score, and every rank-based segment downstream inherits that arbitrariness. Check value_counts before reaching for fifths; if one value holds more than a fifth of the rows, fifths are not fifths.
2 · On a base where 97 percent of customers bought once, Klaviyo's rule calls 13.9 percent Champions and Putler's map calls 16.1 percent Champions, and about nine in ten of each are one-time buyers. Why?
More cells is not more truth. The top third or top fifth of a column that is 97 percent the same value contains row order, not loyalty, and both maps then read that score as if it meant something. The replication note matters here: neither vendor documents its tie handling, so this is a property of the standard pandas convention on this base, not a claim about either product's live output.
3 · Key win-back (011) is 0.79 percent of Olist customers with the knife at the mean and 1.18 percent with it at the median. Which statement is right?
Same people, same date, one knife moved on one column. The mean sits right of the median on a skewed spend column, so "above the mean" is a stricter test than "above the median" and every group with a 1 in the third digit is smaller under it. Session 3 puts a price on the difference: a bigger win-back audience costs more messages and reaches a lower share of true repeat buyers.
What this session teaches, and where it came from
Every fact on this page, and its verification tier, is recorded in the course's source map.
Session 2 cheat sheet · pin this
You have five cuts and a report that makes them comparable. Session 3 puts 4,000 of these customers on a bench where six cuts, the five from tonight plus one anti-lever, are computed live in the page, every share is counted as you watch, and each cut's win-back audience is costed at R$ 0.30 a message against a stated response model. Two of the audiences differ by a factor of 77 in size. Bring your homework numbers; the bench will tell you which knife your tool is holding.