recall¶
Goal: measure what fraction of known duplicate pairs the blocking plan actually reaches — per source, and as a union.
This is the falsifiability check for automatic blocking. With hand-written rules you can reason about what they miss; with a rare-value cap or an LSH band count you cannot. If recall is not measured, a source that silently misses a third of the matches looks identical to one that works. That is the most likely way for this pipeline to produce confidently wrong output.
Synopsis¶
cpplink recall --schema <schema.json> --truth <truth.csv> [--why]
[--show-misses N] [--count] [--json] [--mode MODE]
<file.parquet>...
| Option | Meaning |
|---|---|
--schema <file> |
required; must declare blocking, and the schema needs a unique_id |
--truth <file.csv> |
required; known matching pairs |
--why |
additionally report why each source missed the pairs it missed |
--show-misses N |
print N missed pairs in full; implies --why |
--count |
price the union by enumerating it, instead of by the sum over sources |
--json |
write the same numbers as JSON, for a harness that sweeps a knob |
| (positional) | required; the parquet file |
--mode dedup\|link\|link-and-dedup |
which pairs to enumerate: link when more than one file is given, otherwise dedup; see linking |
--all-pairs |
measure the plan that does no blocking instead of the schema's, which reaches every known pair by construction. See no blocking at all |
The truth file¶
Two columns of unique_id values, with an optional header line beginning id_a:
Ids that do not resolve to a loaded record are counted and reported as unresolved rather
than failing the run. gen-sample --truth writes this format; in production
it comes from a high-precision deterministic rule, or from labels where they exist.
Example¶
cpplink recall --schema examples/sample_schema.json \
--truth examples/sample.truth.csv examples/sample.parquet
Known pairs 143728 resolved
Pairs unblocked 1,619,999,100,000
Source Found PC Candidates PQ First to Marg PQ
----------------------------------------------------------------------------------------------
email exact_value 90,681 63.09% 104,702 86.6% 90,681 86.6%
phone exact_value 105,715 73.55% 138,575 76.3% 38,719 27.9%
dob exact_value 119,118 82.88% 77,957,842 0.153% 11,643 0.0149%
last_name rare_value 81,589 56.77% 16,659,085 0.49% 1,414 0.00849%
last_name sorted_neighbo. 88,361 61.48% 35,999,790 0.245% 200 0.000556%
----------------------------------------------------------------------------------------------
Union 142,657 99.25% 130,859,994 0.109%
Reduction ratio 0.999919
...
1071 known pairs are reachable by no source. No amount of scoring recovers
them: they are never generated as candidates.
Reading the output¶
| Column | Meaning |
|---|---|
Found |
known pairs this source produces on its own, ignoring the plan order |
PC |
pair completeness: Found as a share of resolved known pairs |
Candidates |
pairs the source generates — exact, from the term frequencies |
PQ |
pair quality: Found / Candidates |
First to |
known pairs this source is the first in plan order to produce — its marginal contribution |
Marg PQ |
First to / Candidates: what the source alone reaches, per pair it costs |
PC, PQ and the reduction ratio under the union line are the three numbers the
blocking literature has scored indexing schemes with since Christen's 2012 survey, so a
plan here can be compared against a published one. They are reported together because
none of them means anything alone: every source can buy pair completeness with
candidates, so a source quoted at its recall and not its cost has not been evaluated.
First to is the column that decides¶
Found measures a source in isolation and is almost always flattering, because the sources
overlap heavily on easy pairs. First to measures what the source adds to the plan it is
actually in.
Read the table above that way:
email exact_valueis the plan's foundation. It is first, soFirst toequalsFound.dob exact_valueearns its place. 11,643 pairs no earlier source reached, for 78M candidates — the strongest single contributor after the near-unique identifiers, and it is a low-cardinality column that a rare-value cap would have excluded entirely.last_name sorted_neighbourhooddoes not. It finds 61% of known pairs on its own, but only 200 that no earlier source reached, at 36M candidate pairs — 28% of the whole plan's union for 0.14% of its recall.
Marg PQ is that argument as one number, and the spread across this plan is five orders of
magnitude: email reaches one known pair for every 1.2 candidates it generates, last_name
sorted_neighbourhood one for every 180,000. That ratio, not recall, is what a source is
dropped on. Both denominators are the source's whole candidate count rather than its
marginal one, so Marg PQ understates a source that sits late in the plan — it is a lower
bound, and a source it condemns is condemned.
Do not add a blocking source without running this to see its marginal contribution. The same measurement is what showed MinHash to be strictly dominated on this data: ten bands, 63.2 billion candidates, and between 533 and 867 marginal pairs each. See Blocking.
Comparing methods rather than sources¶
The table ranks the sources of one plan. To rank the methods themselves — is a rare-value
cap or a sorted-neighbourhood window the better way to spend a candidate budget — each has to
be swept over its own knob, because a single operating point per method compares nothing.
bench/sweep_blocking.py drives --json over those grids and reports the frontier: at each
candidate budget, the method that reaches the most known pairs. See
bench/README.md.
The union line, and the ceiling¶
Union 142,657 99.25% 130,859,994 0.109%
Reduction ratio 0.999919
1071 known pairs are reachable by no source.
This is a hard ceiling on the entire pipeline. A pair that is never generated as a candidate is never compared, never scored, and never clustered — no threshold, no model improvement and no amount of CPU recovers it.
Measured end to end on this sample, cluster --truth reports 0.9949 recall at 20 bits against
this blocking recall of 0.9925. Scoring is recovering essentially everything blocking reaches,
so recall is a blocking problem, not a model problem. If you want better recall, the lever
is here, not in the model.
Why end-to-end recall can sit slightly above the blocking ceiling
The ceiling binds pair by pair, and clustering does not work pair by pair. recall scores
the truth file as listed, while cluster scores it closed transitively; a pair blocking
never produced is still asserted if the partition connects its two rows through a third
record. So union–find recovers a little of what blocking missed, and the ceiling is a
ceiling on the edges, not on the partition built from them.
How it is computed¶
recall does not enumerate the candidate set. For each known pair it asks
BlockingPlan::ProducedByAny(a, b), which costs O(sources) key comparisons per pair rather
than a walk over billions of candidates — which is why this runs in seconds even when the plan
it is measuring would take an hour to enumerate.