Find repeated records before they distort a collection
documents
deduplication
workforce research
Learn how exact, normalized, and near-duplicate checks behave on Riverton workforce documents.
Before training and evaluation, one repeated record can make a collection look larger than it is. The coordinator sees the same training sentence more than once and has to decide whether it represents a repeated record or a separate notice.
That choice affects every count that follows. A duplicate document can inflate a common phrase, make a rare label look common, and leak between a training split and an evaluation split.
This lesson works at the document level. A duplicate is a record that should not be counted as a separate document for the question being asked.
Lesson 12 builds augmented examples with source_family_id; those rows need the same split-protection idea. This page uses a smaller constructed set so the distance mechanism is easy to inspect.
Note
Riverton is a teaching case; the documents and duplicate families below are fictional.
find formatting-only duplicates with a normalized hash;
compare near duplicates with character trigram distance; and
choose a threshold by naming the error it makes cheaper.
Build a small review set
The review table starts with the sentence file, then adds table shaping, repeated calculations, text normalization, fingerprints, distance measures, pairs, and a threshold plot.
A hash is a fixed-length fingerprint made from text. The same input gives the same hash. A changed input should give a different hash, so a hash checks exact identity rather than similarity.
This six-row set was written to expose a known failure mode. Four rows are hand-marked as the same duplicate family: the source sentence, an exact copy, a spacing-and-case copy, and a one-word paraphrase. The exam sentence changes the claim while leaving much of the surface text intact. The spreadsheet sentence is unrelated.
Find exact duplicates
An exact duplicate has the same text byte for byte. The code hashes each text value with SHA-256. In the code, \(value) is R shorthand for “for each value, run the small calculation that follows.”
Only the source and the exact copy match, giving one exact duplicate pair. The formatting copy receives a different fingerprint because capital letters and extra spaces are real characters.
Normalize before hashing
Normalization means applying a recorded rule before comparing text. This rule does two things: lowercase the text and replace repeated whitespace with one space. It catches formatting-only differences and destroys information about capitalization and layout.
Normalized hashes after lowercasing and whitespace squishing
Document
Normalized text
Normalized SHA-256 start
D01
the employer pays for certification training.
69adc10ac2fe
D02
the employer pays for certification training.
69adc10ac2fe
D03
the employer pays for certification training.
69adc10ac2fe
D04
the employer covers certification training.
cfdd886532f0
D05
the employer pays for certification exam.
4dbec4bc5178
D06
applicants need basic spreadsheet skills.
c2c27a31f3b6
The normalized hash joins three rows, which make three duplicate pairs. That is useful here, but risky for a flyer where line breaks or all-capital headings carry meaning.
Measure near duplicates
A near duplicate is close enough to deserve review, even when the text is not identical. Cut each normalized string into every overlapping three-character piece, discard order and repeats, and the result is a set of character trigrams. Jaccard distance is the share of pieces the two sets do not have in common: 0 when the sets match, 1 when they share nothing. Because order is discarded, the measure counts how much surface text differs, not which change matters.
Pairwise distances across the six review documents
Pair
Hand-marked duplicate
Jaccard distance
D01 / D02
TRUE
0.0000
D01 / D03
TRUE
0.0000
D01 / D04
TRUE
0.3200
D01 / D05
FALSE
0.2917
D01 / D06
FALSE
0.9877
D02 / D03
TRUE
0.0000
D02 / D04
TRUE
0.3200
D02 / D05
FALSE
0.2917
D02 / D06
FALSE
0.9877
D03 / D04
TRUE
0.3200
D03 / D05
FALSE
0.2917
D03 / D06
FALSE
0.9877
D04 / D05
FALSE
0.5455
D04 / D06
FALSE
0.9873
D05 / D06
FALSE
0.9870
knitr::kable( calibration_pair |>mutate(jaccard_distance =round(jaccard_distance, 4)),col.names =c("Left text", "Right text", "Jaccard distance"),caption ="Calibration pair for reading the Jaccard scale",row.names =FALSE)
Calibration pair for reading the Jaccard scale
Left text
Right text
Jaccard distance
Evening shifts require a valid forklift certification.
Evening shifts require a valid forklift certificate.
0.1132
The calibration strings are not part of the review set; they show how to read the scale. Two sentences that differ only in certification versus certificate score 0.1132. In this constructed review set, the exam distractor scores 0.2917 against the source, while the meaning-preserving paraphrase scores 0.32. The distractor changes one short word and the duplicate changes one longer phrase, so surface overlap and meaning point in opposite directions. Real collections can contain pairs like this; the small set is designed so the mechanism is visible.
Sweep the threshold
A threshold is the distance at or below which the pipeline joins two documents. The threshold is not learned here. The team tries several values and counts both kinds of outcome.
Figure 1: Duplicate pairs caught and distinct pairs wrongly joined at each tested threshold.
threshold_sweep |> tidyr::pivot_longer(cols =c(caught_true, wrongly_joined),names_to ="result",values_to ="pairs" ) |>mutate(result =case_when( result =="caught_true"~"True duplicate pairs caught", result =="wrongly_joined"~"Distinct pairs wrongly joined",TRUE~ result ) ) |>ggplot(aes(x = threshold, y = pairs, color = result)) +geom_step(linewidth =0.8, direction ="hv") +geom_point(size =2) +scale_x_continuous(breaks = threshold_sweep$threshold) +labs(x ="Jaccard distance threshold",y ="Pairs",color =NULL ) +theme_minimal()
Figure 2: Duplicate pairs caught and distinct pairs wrongly joined at each tested threshold.
For this constructed set, 0.2917 is smaller than 0.32. The distractor is closer to the source than the paraphrase is. Because those two distances are ordered that way, no threshold on these pairs can catch the paraphrase without also joining the distractor. The sweep confirms that threshold choice trades one error against another here; it is not a general law of deduplication.
Deduplication at scale
The expand_grid() step used here builds an all-pairs combination, which mathematically scales at \(O(N^2)\). Pairwise comparisons work for a tiny teaching set, but mathematically collapse in production. For real-world datasets, an all-pairs Jaccard distance calculation is too slow and requires too much memory.
Production systems bypass this \(O(N^2)\) scaling boundary by using Locality Sensitive Hashing (LSH) and MinHash over shingled text. R packages like textreuse (or Python’s datasketch) implement these algorithms to group near-duplicates across millions of documents without comparing every pair.
What to remember
Duplicate documents can inflate counts and leak between training and evaluation.
Exact hashes catch only byte-for-byte repeats.
Normalized hashes catch some formatting differences and erase formatting facts.
Character-trigram Jaccard distance measures surface overlap, not preserved meaning.
Near-duplicate thresholds trade missed duplicates against wrongly joined records in this set.
The right threshold depends on which error costs more for the project.
Exact repeats can be removed automatically; near matches belong in review before any training or evaluation split. The threshold sweep is a warning label on this constructed set, not a universal setting for Riverton documents.