Where tonight sits on the track
Session 1 marked the buyer domain as the least obtainable row on the estate map. Session 2 conformed three marketplace exports into one schema, and each export kept a different slice of the buyer. Tonight measures what those slices are worth as join keys, at three grains: the customer, the store and the SKU. The rows are real Olist orders and the people who placed them are real (anonymised by Olist); the three-export split, the synthetic names and phones, and every export's masking are constructed, modelled on what integrators publicly report. Session 8's bench runs the same ladder in your browser; tonight you rebuild it in about sixty lines.
★ What you walk out with tonight. A pair scorer in plain Python that counts predicted, true and correct pairs without ever building a pair, the true match rate that punishes both missed and invented links, a measured answer to "can we build a single customer view" (no: the best of 127 key combinations reaches 0.2329), and the habit of scoring a join before anyone builds on it.
A join key is a claim about pairs
6 min liveWhen you join on a key you are claiming that every two rows sharing its value belong to the same thing. So the unit of scoring is not the row, it is the pair. Count the pairs the key claims, the pairs that are really true, and the pairs on both lists, and three numbers tell you everything a key is worth.
LiveThree counts: P, T and TP3 min▶
Hide the truth in a column the key never sees: for customer grain, which real person placed each order. Then three counts:
| Count | What it is | In the toy |
|---|---|---|
| P, predicted | Pairs of rows that share every chosen key value | 4 |
| T, true | Pairs of rows that share the hidden truth | 4 |
| TP, correct | Pairs on both lists | 1 |
From them, recall = TP / T (how many real links the key finds), precision = TP / P (how many of its links are real), and true match rate = TP / (P + T - TP). A row whose export left a chosen field empty links to nothing: it adds no predicted pairs, and its true pairs count as missed. That rule is what makes a masked field visible in the score.
LiveWhy the union in the denominator2 min▶
P + T - TP is the number of distinct pairs on either list: the union of the two pair sets. TP over that union is the Jaccard index of the claimed pairs and the true pairs. It is 1 only when the key draws exactly the true lines, no more and no fewer.
That is why one number is enough. A key that links everything finds every true pair, recall 100 percent, and drowns them: on the bench, the masked name alone claims 2,371,086 pairs to find 339 true ones, a rate of 0.0001. A key that links nothing has TP = 0 and scores 0. Recall rewards the first, precision rewards the second, and the union punishes both. If someone quotes you a match rate as a recall, ask for P.
Self-studyHow to count pairs without making them4 min▶
8,614 orders make 37,096,191 possible pairs. You never need to list them. Group the rows by key value: a group of n rows holds n choose 2 = n(n-1)/2 pairs, so P is the sum of n choose 2 over key groups. The same over truth groups gives T.
For TP, group by the key value and the truth together, the cells of a contingency table of key against truth. Two rows are a correct pair exactly when they share both, so TP is the sum of n choose 2 over those cells. Three Counters and three sums, linear in the rows, exact to the pair. The build-along writes it; the entity resolution course's first session shows why comparing every pair stops being an option long before Serai's scale.
Most pairs are between strangers. In the public Olist dataset, 93,358 customers placed 96,478 delivered orders, and only 2,801 of them bought twice or more: a 3.0 percent repeat rate. At that base rate almost every pair of orders belongs to two different people, so a key that groups loosely manufactures false pairs far faster than it finds true ones. The bench's sample keeps that real ratio on purpose (250 repeat customers against 8,083 one-time customers), because the base rate is what decides how bad a weak key looks.
Masking turns identity into fragments
6 min liveThe marketplaces do not hand an enabler the buyer. They hand it fragments: a first and last letter, a couple of digits, sometimes an address, sometimes not, and a different mix on each platform. Each fragment either collides, because thousands of buyers share it, or goes silent, because the export dropped it. Stacking fragments trades one failure for the other, and the ladder stops well below any gate.
LiveCollide or go silent3 min▶
The bench's masking is constructed, modelled on the reported patterns: every export shows the name as its first letter, three stars and its last letter, and the last two digits of the phone. Platform A keeps zip and city; Platform B keeps neither; Platform C keeps the phone and the address only on seller-shipped orders. Scored at customer grain on 8,614 orders with 339 true pairs:
| Key | Rate | Recall | Predicted pairs | What happened |
|---|---|---|---|---|
| masked name | 0.0001 | 100% | 2,371,086 | 63 masks for 8,333 people: collides |
| masked phone | 0.0009 | 100% | 371,023 | 100 possible values: collides |
| name + phone | 0.0142 | 100% | 23,928 | still colliding, about 70 claimed pairs per true one |
| name + phone + city + zip | 0.1917 | 19.2% | 65 | never wrong, silent wherever the address was dropped |
| name + phone + store id | 0.2329 | 29.2% | 185 | best of 127; precision 53.5% |
Every rung trades one failure for the other. Fragments that appear on every export collide; the fragment that disambiguates, the address, exists on too few rows to find most repeat buyers. There is no combination in between that does both, and the search over all 127 subsets of the seven keys proves it rather than asserting it.
LiveA hash needs the raw value2 min▶
The standard suggestion is to hash the phone and join on the hash. On the bench, the hashed phone links nothing: 0 predicted pairs. Only the 265 seller-shipped Platform C orders carried a raw phone to hash, and no two of those 265 orders were the same person.
A hash is a way of keeping a raw value private while still comparing it, and it can only exist where the raw value arrived. Hashing the masked fragment gives you the fragment again, with its collisions. So the question for any hashed-identity proposal is not the algorithm, it is: on how many rows, from which exports, do we receive the raw value today? That is an obtainability question, and session 1's scale already answered it: closed on platform-shipped orders.
Self-studyThe sample flatters fragments: treat 23.3 percent as a ceiling4 min▶
The bench scores 8,614 orders. Serai's real estate would hold many times that. True pairs grow roughly with the number of rows (each new repeat buyer adds a few), but false pairs from a colliding key grow with the square of the rows: double the orders and every masked-name group doubles, so its pairs roughly quadruple. P grows faster than TP, and every fragment rung sinks as the data grows.
So read 0.2329 as a ceiling measured on a small, friendly sample, not an estimate of what a production join would reach. The direction is certain; the size at Serai's scale is not measured here, and nothing on this page claims it.
Three marketplaces, three different slices of the buyer. Integrators' public notes report that all three marketplaces mask the buyer's name and phone on orders they fulfil, and that each keeps a different amount of the address; seller-delivered orders are the exception. Shopee is reported to remove buyer names and phone numbers while leaving the address for order matching (Easy Shopee & Lazada help centre, 2022). Lazada is reported to mask name, address and phone in its seller centre and exports from 12 January 2026 and in its Open Platform API from 1 July 2026, with full details kept on delivered-by-seller orders (BigSeller; Zetpy, June 2026). TikTok Shop is reported to desensitise recipient name and phone when its own logistics ship the order (openlinker-project, GitHub issue 2882). All three are reported, not read at source, and the rules have changed twice in 2026 alone: re-check before you build on any of them.
Grain decides what can be joined
4 min liveThe buyer is not Serai's to key, but the store and the SKU are. Run the same scorer with the truth set to the store or the SKU behind each order line, and the picture flips: the keys are Serai's own ids, and they clear a 90 percent gate with room to spare. The gate is not about matching skill. It is about who owns the key.
LiveStore 1.0000, SKU 0.9748, and where the 2.5 percent went2 min▶
At store grain (every order line, truth = the store behind it) the store id scores 1.0000: 186,387 predicted pairs, all of them true, none missed. At SKU grain the SKU scores 0.9748: it finds 17,033 of 17,474 true pairs with no false ones. The gap is 219 lines on listings missing from Serai's SKU mapping table, so their SKU is empty and they link to nothing.
That gap is the lesson in miniature. Owning the key means maintaining it: a listing created on a marketplace last week and not yet mapped is invisible to every SKU-grain use case. The fix is a job and an owner (a daily check for unmapped listings, a count on the ops dashboard), not a better algorithm.
LiveThe break button: shuffle the truth1 min▶
A scorer that returns high numbers for the wrong reason is worse than none. So break it on purpose: shuffle the hidden ids (seed 7 on the bench) and score again. The best customer key drops to 0.0000, the store id to 0.0037, the SKU to 0.0004. If the store id had kept a high score on shuffled truth, the rate would be measuring how big the groups are, not whether they are right. It falls to near zero, so it measures matching.
The quieter sanity check sits in the key list: the order id is unique per order, so at customer grain it claims 0 pairs and scores 0. A key that is unique per row can never join anything above its own grain, which is exactly what it should print.
Self-studyWhat this does not teach: probabilistic linkage2 min▶
Everything above asks one question: does a deterministic key exist at this grain? It does not try to beat the fragments with weighted agreement scores, blocking or a clerical review band. That is record linkage proper, and it has its own session: Fellegi-Sunter in the entity resolution course. Read it with tonight's numbers in hand: a probabilistic model can weigh a matching address above a matching initial, but it still cannot recover a value the export never contained.
Olist keys its sellers and products itself. Olist describes itself as connecting small businesses from all over Brazil to marketplace channels with a single contract, an enabler model. In its public dataset every order line carries Olist's own seller_id and product_id, which is why store and SKU grain hold on its data: the ids belong to the business doing the joining, not to the channel. Serai's version of that is its store and SKU mapping tables from session 2. The 219 unmapped lines on the bench are constructed (the full Olist file has no gaps), placed there to show what an unmaintained mapping table costs.
Score keys against the hidden truth
★ 22 min · everyone buildsPlain Python, standard library only, on the bench's own sample: 8,614 real Olist orders with constructed exports and masking. Every output below is what this code printed when it was run for this page. The unshuffled numbers must match the bench to the digit; if yours do not, the bug is in your row rules, and step 2 is where to look.
Load the sample and expand the lines. The bench's sample is a JavaScript file: a comment header, then window.ENABLER_SAMPLE = , then plain JSON, then a semicolon. Split off everything up to the prefix, strip the trailing semicolon, and json.loads the rest. Lines ship as a count per order; expand them into a line-to-order index.
Build the customer-grain rows, one per order. Each field follows the constructed masking exactly. The masked name and the two phone digits reach every export. The hashed phone exists only where the raw phone arrived, on seller-shipped orders; since a hash is one value per person, the stand-in stores the customer index there. City and zip exist on Platform A (export 0) and on seller-shipped orders. Store and SKU come from the order's first line, and the SKU is empty when that listing is missing from the mapping table. The truth, the real customer, lives in its own list and is never a key.
Write the scorer with three Counters. Skip any row missing a chosen key for P and TP, count every row for T, and sum n choose 2 over each. Then check it against the bench's first rung before you trust it: masked name must print P 2371086, T 339, TP 339.
Run the rungs. Single keys first, then the stacks. Watch P fall and recall hold, until the address arrives and recall drops to the 19.2 percent of true pairs where both orders carry it.
Search every combination, not the ones you like. Seven keys give 127 non-empty subsets. A hand-picked ladder can always be accused of missing the clever one; the exhaustive search cannot.
Change the grain, then break the truth. At store and SKU grain there is one row per order line and the truth is the line's store or SKU. Then shuffle each truth list and search again. This uses Python's random.Random(7).shuffle, which is not the bench's mulberry32 shuffle, so your shuffled numbers differ slightly from the bench's (0.0036 here against its 0.0037 at store grain); what matters is that every grain collapses to near zero.
Put the scorer next to the registry. Every use case on the roadmap names the grain it needs; the gate is a one-line check of that grain's best rate against 0.90, run every time an export changes. Session 8's roadmap sequencer does exactly that and blocks the single customer view. Serai, its exports and its masking are constructed; the orders, and who placed them, are real Olist rows, and no figure here is a claim about any real enabler.
Before session 4
30 min- Change the masking rule in step 2 so Platform B also keeps city and zip, rerun steps 4 and 5, and write down how far the best customer-grain rate moves. Is it anywhere near 0.90?
- Double the data by stacking the rows with a second copy whose customer ids are offset (so no new true pairs cross copies), and rescore the masked phone. Check that P roughly quadruples while T only doubles.
- Pick one join your team relies on today and score it: write down P, T, TP and the rate, or write down why you cannot, because nobody holds the truth to score it against.
- List every use case on your roadmap with the grain it needs. Mark each grain with its best measured rate, and circle the ones that sit below the gate.
Three questions
Clear all three and Analyst session 3 gets stamped in your passport.
1 · The masked phone finds all 339 true pairs. Why does it score only 0.0009?
Recall is 100 percent and precision is almost zero. The true match rate is TP over the union of claimed and true pairs, 339 over roughly 371,023, so a key that links everything scores almost like one that links nothing.
2 · Someone proposes hashing the buyer's phone to build a single customer view. What does the bench say?
Hashing protects a value you already hold; it cannot create one. Where the export masked the phone there is nothing to hash, so the question is how many rows arrive with the raw value, and on platform-shipped orders the answer is none.
3 · The store id scores 1.0000 at store grain. After shuffling the hidden store ids it scores 0.0037. What does the second number tell you?
The break button is a test of the scorer, not of the key. A metric that stayed high on shuffled truth would be rewarding group size; one that falls to near zero at every grain is measuring matching.
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.
Analyst session 3 cheat sheet · pin this
The buyer stays out of reach, so the value moves to what Serai can observe itself. Session 4 writes the per-minute event spec for Pasar Live, a constructed live channel: room entry, product card impression, click, add to cart and order, keyed by stream, minute and SKU on one clock, with an owning team named for every event. It is a contract before it is a table.