Skip to content
AI-grafen
DAI developerData handling· about 45 min· fundamentals that rarely change· verified 2026-09-20· EN

Data quality: missing values, duplicates, errors

Be able to find and handle missing values, duplicates and implausible values.

Prerequisites

Intuition

Data work is said to be 80 % of an ML project. That is roughly true, and it is not tedious drudgery — it is where most of the actual improvements come from.

Three problems, in order:

ProblemHow you find it
Missing valuesdf.isna().sum() — but also values coded as -1, 999, "unknown"
Duplicatesdf.duplicated().sum(), and duplicates on the key even when the rows differ
Implausibilitiesdescribe(): ages of 200, negative prices, dates in the future

The hardest question is why the value is missing, because that decides what you are allowed to do about it.

TypeMeansExample
MCARmissing completely at randoma sensor dropped packets
MARmissing depending on something you can seeyounger people answer web surveys more often
MNARmissing depending on the value itselfhigh earners do not state their income

MNAR is the most dangerous: filling in the mean where the highest values are missing shifts the whole distribution downwards, and makes the analysis systematically wrong.

Formal

Strategies for missing values:

StrategyWhenThe risk
Drop the rowfew missing, MCARskew unless MCAR; wasted data
Drop the column> 50 % missing and low valueyou lose the signal
Mean/mediannumeric, MCARartificially reduces the variance
The most common valuecategoricalexaggerates the majority
Model-based (kNN, iterative)there is a pattern in the other columnsexpensive; can leak
An indicator column + imputationnearly always a good complementnone

The last row deserves emphasis: add a column x_was_missing with 0/1 in addition to the imputation. That a value is missing is often informative in itself — a patient with no test result differs from one with a normal test result — and the model can then use that information rather than being fooled by the filled-in value.

The most important rule about imputation: compute the mean on the training data and apply it to validation and test. Compute the mean over the whole dataset and you have leaked information from the test set into the training. The same goes for scaling and encoding.

Duplicates are not always errors. Two identical rows can be two real events. Always check on the key:

df.duplicated().sum()                    # completely identical rows
df.duplicated(subset=["pupil_id"]).sum() # the same key, possibly different data

The second line finds the genuinely problematic case: two records for the same person with different data. Somebody then has to decide which one holds.

Document every decision. One line per action, in the code or in a log file: what was changed, how many rows, and why. In six months somebody — probably you — will wonder why there are 4 812 rows instead of 5 000.

Code

import pandas as pd, numpy as np

df = pd.DataFrame({
    "id":   [1, 2, 3, 3, 5, 6],
    "age":  [15, 16, 999, 999, -1, 17],
    "score": [80.0, np.nan, 70.0, 70.0, 65.0, np.nan],
    "city": ["Malmö", "malmö", "Lund", "Lund", "MALMÖ", "Lund "],
})

log = []

def action(text, before, after):
    log.append(f"{text}: {before} → {after} rows")

# 1. Sentinel values are missing values in disguise
n = len(df)
df["age"] = df["age"].replace({999: np.nan, -1: np.nan})
print(df["age"].isna().sum(), "ages are in fact missing")           # 3

# 2. Duplicates — both identical rows and duplicates on the key
before = len(df)
df = df.drop_duplicates()
action("identical duplicates", before, len(df))
print("duplicates on id:", df.duplicated(subset=["id"]).sum())       # 0

# 3. Normalise the categories before you count the unique values
df["city"] = df["city"].str.strip().str.lower()
print(sorted(df["city"].unique()))                                   # ['lund', 'malmö']

# 4. An indicator plus imputation, with the mean FROM THE TRAINING DATA
train, test = df.iloc[:3].copy(), df.iloc[3:].copy()
for d in (train, test):
    d["score_was_missing"] = d["score"].isna().astype(int)
mean = train["score"].mean()          # ← computed on the training data only
train["score"] = train["score"].fillna(mean)
test["score"] = test["score"].fillna(mean)

print("\n".join(log))
# identical duplicates: 6 → 5 rows

The indicator column score_was_missing costs nothing and often saves the model: without it a filled-in mean looks like a real mean.

Mastery means

  • Finds missing values, duplicates and implausibilities
  • Chooses a handling strategy deliberately
  • Documents every action

Sign in to do the exercises and build your mastery up.

Sources

All the sources and licences