Cleaning data
Cleaning removes duplicates, impossible values and inconsistent spellings, and the honest sign that it worked is often a lower score than before.
- 12 min read
- 3 reading levels
- Updated
Read these first
On this page 8
One lesson, three depths. Pick the one that fits you today — you can switch any time.
Beginner — No maths. Plain English.
Cleaning data means finding the rows that are wrong, repeated or impossible, and dealing with them before a model ever sees them.
Think about a wedding guest list assembled from four different WhatsApp groups. The same aunt appears three times, spelled three ways. One entry says the guest is minus two years old.
You would not print invitation cards from that list. You would sit down and merge the duplicates first.
Cleaning is that sitting-down step, for data.
Why it exists
Data arrives from forms, apps, sensors and other people's spreadsheets. Every one of those sources has its own idea of what a value should look like.
One system writes "Mumbai". Another writes "mumbai " with a trailing space. A third writes "MUMBAI". To a computer these are three different cities.
Nobody planned this. It is what happens when data comes from more than one place, which is always.
The four faults you will meet constantly
Repeats. The same row written twice, because an upload failed halfway and was retried.
Impossible values. An age of 999. A negative price. A delivery that finished before it started.
Spelling drift. The same thing written many ways: extra spaces, capital letters, short forms.
Silent placeholders. A zero that means "we never measured this", sitting in a column of real zeros.
What cleaning looks like
raw rows -> checks -> clean rows -> a written record
1,040 800 "240 repeats removed,
(240 repeated) 3 impossible ages dropped"That last box is not decoration. Six months later somebody will ask why the row count changed, and the record is the only answer you will have.
The part that feels wrong
Here is the thing nobody warns you about. After a good clean, your accuracy score often goes down.
That is usually the clean working. Repeated rows leak between your practice set and your exam set, so the model is tested on rows it already memorised. Remove them and the score falls to the truth.
A lower honest number beats a higher fake one. The fake one is the one you would have promised to a customer.
Somewhere you have seen this
Your phone's contacts app offering to merge duplicates. That is deduplication, with a friendly face.
Or a shopping site where the same product appears four times under slightly different names. Somebody has not finished cleaning that catalogue.
What is honestly hard here
Deciding whether two rows are the same thing is genuinely difficult. "Ram Kumar, Pune" and "R. Kumar, Pune" may be one person or two.
There is no rule that gets this right every time. You will pick a rule, get some wrong, and pick a better rule later. Anyone who tells you otherwise has not cleaned a real customer table.
Remember this
- Cleaning deals with repeats, impossible values, spelling drift and fake placeholders.
- A score that falls after cleaning is usually a score becoming honest.
- Write down what you removed, and why. Future you will need it.
What to learn next
- Handling missing data — the blanks left behind after cleaning.
- Train, test and validation splits — why the split has to come after the dedupe.
- Building a data pipeline — running these checks automatically, forever.
Developer — Code and libraries.
Two demonstrations. The first shows why duplicates are dangerous rather than untidy. The second is the boring string work that makes up most of the job.
Setup
pip install numpy pandas scikit-learnDuplicates are leakage, not clutter
A retried ingest job wrote three rows in ten a second time. Both copies then get shuffled and split into train and test, so the model is tested on rows it memorised during training.
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split
rng = np.random.default_rng(4)
n = 800
x1, x2 = rng.normal(0, 1, n), rng.normal(0, 1, n)
y = rng.binomial(1, 1 / (1 + np.exp(-(0.9 * x1 + 0.7 * x2))))
base = pd.DataFrame({"x1": x1, "x2": x2, "y": y})
# The ingest job timed out and retried, so three rows in ten were written twice.
retried = base.sample(frac=0.30, random_state=1)
raw = pd.concat([base, retried], ignore_index=True).sample(frac=1, random_state=2).reset_index(drop=True)
def measured_auc(frame):
tr, te = train_test_split(frame, test_size=0.3, random_state=42, stratify=frame["y"])
model = RandomForestClassifier(n_estimators=300, random_state=0).fit(tr[["x1", "x2"]], tr["y"])
return round(roc_auc_score(te["y"], model.predict_proba(te[["x1", "x2"]])[:, 1]), 3)
deduped = raw.drop_duplicates()
print("rows as delivered :", len(raw))
print("rows after dedupe :", len(deduped))
print("duplicate rows :", len(raw) - len(deduped))
print()
print("test AUC, duplicates left in:", measured_auc(raw))
print("test AUC, duplicates removed:", measured_auc(deduped))rows as delivered : 1040 rows after dedupe : 800 duplicate rows : 240 test AUC, duplicates left in: 0.831 test AUC, duplicates removed: 0.696
The 0.831 was a lie
That gap is 0.135 of pure fiction. A random forest memorises training rows readily, so any row appearing in both halves of the split is answered from memory rather than from a learned pattern.
0.696 is what this model will actually do on a customer it has never seen. 0.831 is what you would have put in the launch email.
This is among the most expensive bugs in applied machine learning, because it fails in the flattering direction offline and the costly direction in production. Kapoor and Narayanan (2023) surveyed 17 scientific fields and found leakage of this kind in hundreds of published papers.
Deduplicate before you split. Every time. And note the ordering: drop_duplicates() then train_test_split, never the reverse.
The unglamorous half: strings and impossible values
import pandas as pd
raw = pd.DataFrame({
"city": [" Mumbai", "mumbai", "MUMBAI", "Mumbai ", "Pune", "pune", "PUNE ", "Nagpur", "nagpur"],
"age": [34, 28, 0, 41, 999, 37, 25, -3, 52],
})
print("distinct cities before:", raw["city"].nunique())
clean = raw.assign(city=raw["city"].str.strip().str.lower())
print("distinct cities after :", clean["city"].nunique())
print(sorted(clean["city"].unique()))
print()
impossible = ~clean["age"].between(18, 100)
print("rows with an impossible age:", int(impossible.sum()))
print(clean.loc[impossible])distinct cities before: 9
distinct cities after : 3
['mumbai', 'nagpur', 'pune']
rows with an impossible age: 3
city age
2 mumbai 0
4 pune 999
7 nagpur -3Nine categories became three. If you had one-hot encoded that column, you would have handed the model nine columns holding three cities' worth of information, each with a third of the rows it should have had.
The three bad ages are three different bugs. 0 is a placeholder for "not asked". 999 is a sentinel someone chose decades ago. -3 is a data entry slip. They need three different fixes, and lumping them together loses that.
Line by line, the parts worth knowing
.str.strip().str.lower() handles the two commonest string faults in one pass. Add .str.replace(r"\s+", " ", regex=True) when internal double spaces appear.
drop_duplicates() with no arguments compares every column. That misses the more common real case: the same customer with one field slightly different. Use subset=["customer_id"] to deduplicate on a business key instead, and keep="last" when later rows are corrections.
.between(18, 100) returns False for NaN as well, so ~clean["age"].between(...) flags missing values too. That is often what you want, but know that it is happening.
Common mistakes
Splitting before deduplicating. The bug demonstrated above. It survives code review because both lines look correct on their own.
Deduplicating on all columns when the key is one column. Two rows for one customer with a different timestamp are not identical, so drop_duplicates() keeps both. Deduplicate on the key that identifies the real-world thing.
Deleting impossible rows silently. Print counts before and after every filter. A cleaning step that quietly removed 40% of your data is a bug you want to hear about immediately.
Cleaning in a notebook and never in the pipeline. The same messy values arrive again tomorrow. If the fix is not in the code path that runs in production, you have fixed nothing.
Filling before understanding. Replacing 999 with the mean, before working out that 999 meant "not measured", destroys the fact that it was missing. See handling missing data.
Try it yourself
In the first script, change frac=0.30 to frac=0.05 and re-measure. The inflation shrinks but does not vanish, which is why "only a few duplicates" is not a defence.
Then swap the random forest for LogisticRegression and run it again. The inflation is much smaller, because a linear model cannot memorise individual rows. Different models leak by different amounts through the same dirty data.
What to learn next
- Handling missing data — the blanks left behind after cleaning.
- Train, test and validation splits — why the split has to come after the dedupe.
- Building a data pipeline — running these checks automatically, forever.
Researcher — Mathematics and papers.
Deduplication as a decision problem
Exact duplicates are a GROUP BY. The real problem is entity resolution: deciding whether two records refer to the same real-world entity when no shared key exists.
The classical formulation is Fellegi and Sunter (1969), A Theory for Record Linkage, JASA. For a record pair $(a,b)$, compute a comparison vector $\gamma$ over fields, then the likelihood ratio:
$$ R(\gamma) \;=\; \frac{p(\gamma \mid (a,b) \in M)}{p(\gamma \mid (a,b) \in U)} $$
Where $M$ is the set of matched pairs and $U$ the set of unmatched pairs. Two thresholds $t_\mu > t_\lambda$ partition the decision into link, non-link and a clerical review region. Fellegi and Sunter prove this rule is optimal in the sense of minimising the review region for fixed error rates $\mu$ (false link) and $\lambda$ (false non-link).
The $m$- and $u$-probabilities per field are typically estimated by EM under a conditional independence assumption across fields, which is known to be violated in practice (surname and locality correlate) and is the main source of miscalibration.
Complexity and blocking
Naive pairwise comparison is $O(n^2)$. At $n = 10^7$ that is $5 \times 10^{13}$ pairs, which is not feasible.
Blocking partitions records by a cheap key so that only within-block pairs are compared. Cost drops to $O!\left(\sum_b n_b^2\right)$, which for $B$ balanced blocks is $O(n^2/B)$. The trade-off is recall: any true pair split across blocks is lost permanently. Multi-pass blocking with several keys, unioned, mitigates this.
Sorted neighbourhood (Hernández and Stolfo, 1995) sorts on a key and compares within a sliding window of size $w$, giving $O(n \log n + nw)$.
MinHash with LSH (Broder, 1997) estimates Jaccard similarity in sublinear expected time. For a band of $r$ rows repeated $b$ times, the probability that a pair with Jaccard similarity $s$ becomes a candidate is:
$$ P(\text{candidate}) \;=\; 1 - \left(1 - s^{\,r}\right)^{b} $$
Choosing $r$ and $b$ sets the S-curve threshold at approximately $(1/b)^{1/r}$. This is the standard tool for near-duplicate text at scale, and the same machinery used to deduplicate LLM pretraining corpora.
Duplicate leakage in pretraining corpora
Lee et al. (2022), Deduplicating Training Data Makes Language Models Better, ACL, found that C4 contains sequences repeated tens of thousands of times, and that over 1% of tokens emitted by models trained on undeduplicated data were memorised continuations. Deduplication with suffix arrays and MinHash reduced memorised output by roughly a factor of ten, cut training steps needed for equal perplexity, and — critically — removed train-test overlap that had inflated reported perplexity.
Carlini et al. (2023), Quantifying Memorization Across Neural Language Models, showed memorisation scales log-linearly with duplicate count. Deduplication is a privacy control, not only a quality control.
Constraint-based cleaning
The database literature treats cleaning as repair under integrity constraints. Functional dependencies $X \to Y$ generalise to conditional functional dependencies (Bohannon et al., 2007), which hold on a subset of tuples selected by a pattern — for example, [country = "IN", pin] -> city.
Given violated constraints, the minimal repair problem is to find a database at minimum edit distance from the original satisfying all constraints. It is NP-hard for most useful constraint classes, so systems (HoloClean, Rekatsinas et al., 2017, VLDB) relax it to probabilistic inference over a factor graph combining constraints, quantitative statistics and external dictionaries.
Validation as an executable contract
Rather than cleaning ad hoc, express expectations as assertions that run on every batch:
- Deequ (Schelter et al., 2018, Automating Large-Scale Data Quality Verification, VLDB) — declarative constraints over Spark, with incremental computation on growing datasets and anomaly detection over metric history.
- TensorFlow Data Validation (Breck et al., 2019, Data Validation for Machine Learning, SysML) — infers a schema from a baseline, then detects drift and skew against it in production.
- Great Expectations — the same idea, Python-native, with generated documentation.
Breck et al. (2017), The ML Test Score, IEEE Big Data, formalises this as a rubric. The data tests are the section most teams score zero on.
What to measure
- Pairwise precision and recall on a hand-adjudicated sample of candidate pairs, not cluster-level accuracy, which is dominated by singletons.
- Reduction ratio for blocking: $1 - (\text{candidate pairs}) / \binom{n}{2}$, reported alongside pair completeness, the fraction of true matches surviving blocking. Optimising one alone is meaningless.
- Row counts at every stage, persisted. A cleaning step that changes its own removal rate week on week is an incident.
Reading
- Fellegi and Sunter, A Theory for Record Linkage, JASA 1969.
- Christen, Data Matching, Springer 2012 — the standard book-length treatment.
- Lee et al., Deduplicating Training Data Makes Language Models Better, ACL 2022 — arxiv.org/abs/2107.06499
- Rekatsinas et al., HoloClean: Holistic Data Repairs with Probabilistic Inference, VLDB 2017 — arxiv.org/abs/1702.00820
- Schelter et al., Automating Large-Scale Data Quality Verification, VLDB 2018.
What to learn next
- Handling missing data — the blanks left behind after cleaning.
- Train, test and validation splits — why the split has to come after the dedupe.
- Building a data pipeline — running these checks automatically, forever.