Lesson 7 of 30

Chapter 7 — Cleaning the Dataset

Missing data • duplicates • inconsistencies • outliers • auditable decisions

A practical, evidence-based chapter for turning imperfect raw records into a reliable modeling dataset while protecting evaluation integrity.

 

Chapter Overview

Real datasets are rarely clean. Values may be absent, duplicated, inconsistent, mistyped, measured in different units, or statistically unusual. Cleaning is therefore not a cosmetic step. Every correction, deletion, replacement, and transformation changes the information available to the model. The objective is not to make the table look perfect; it is to create a trustworthy representation of the problem while preserving traceability and preventing leakage.

KEY IDEA  Cleaning is part of model design

An imputation value, duplicate rule, category map, range constraint, or outlier treatment is an assumption about the data-generating process. Fit data-dependent choices on the training set and document their consequences.

 

Learning objectives

  • Differentiate explicit missing values, implicit missing values, structural absence, and censored values.
  • Explain MCAR, MAR, and MNAR mechanisms and why they influence the validity of cleaning strategies.
  • Quantify missingness by row, column, group, and pattern.
  • Choose among deletion, simple imputation, missingness indicators, and model-based imputation.
  • Detect exact duplicates, partial duplicates, duplicate entities, repeated measurements, and train-test overlap.
  • Standardize labels, units, data types, ranges, and dates without destroying legitimate distinctions.
  • Detect outliers using statistical and visual methods and decide whether to correct, retain, cap, transform, or model robustly.
  • Build reproducible validation checks and a cleaning decision log.
  • Apply cleaning operations in a pipeline that avoids leakage.
  • Produce a documented, analysis-ready dataset and a defensible lab report.

Running dataset

Variable

Expected form

Injected defects

Role

record_idUnique stringDuplicate records use repeated IDsTraceability only
customer_idStable entity keySome entities occur more than onceGrouping and duplicate checks
age18–100 yearsNegative, child, and impossible valuesNumerical feature
monthly_incomeMonthly MADMissing values, annual values, extreme entriesNumerical feature
income_unitMAD/month or MAD/yearMixed unitsUnit validation
cityCanonical city labelCase, spacing, spelling, and missing valuesCategorical feature
planBasic, Plus, PremiumCase and trailing-space variantsCategorical feature
signup_dateValid past ISO dateImpossible and future datesDate feature
support_callsNon-negative countMissing valuesDiscrete feature
churned0 or 1Complete binary targetTarget

 


 

 

7.1 Missing Data

7.1.1 What counts as missing?

A value is missing when the dataset does not contain the information required by the variable definition. Missingness may be explicit—represented by NaN, NULL, None, or a blank—or implicit, hidden behind a sentinel such as -999, “unknown,” “not provided,” or an impossible date. A field can also be structurally absent: for example, pregnancy-related information may not apply to every patient. These cases should not automatically receive the same treatment.

Form

Example

Interpretation risk

First action

Explicit nullNaN, NULL, blank cellUsually recognized by softwareConfirm source meaning and frequency.
Sentinel value-999, 9999, “N/A”May be treated as a real valueConvert only after verifying the codebook.
Structural absenceEnd date for an active contractNot an error; concept does not applyRepresent explicitly or derive a status flag.
Censored valueIncome recorded as “>100,000”Value is partially knownPreserve censoring information; do not use an arbitrary exact value.
Collection failureSensor offlineMay be informative about conditionsUse process metadata and missingness indicators.
Not yet observedOutcome label arrives after 30 daysLabel delay rather than feature missingnessDefine a label-maturity window.

 

7.1.2 Missing-data mechanisms: MCAR, MAR, and MNAR

Figure 7.1 — Conceptual differences among MCAR, MAR, and MNAR missingness.

The mechanism describes why a value is missing, not merely how much is missing. Under MCAR, complete cases resemble incomplete cases on average. Under MAR, missingness can be explained by observed variables and may be addressed with conditional models. Under MNAR, the missing value or an unobserved factor influences missingness; standard imputation can remain biased. In practice, mechanisms are assumptions supported by collection knowledge and sensitivity analysis, not labels that can usually be proven from the table alone.

Mechanism

Formal intuition

Potential consequence

Example response

MCARP(M=1) independent of observed and missing valuesComplete-case analysis can be unbiased but inefficient if the assumption holds.Investigate random failures; quantify precision loss.
MARP(M=1) depends on observed variablesConditional imputation can reduce bias when relevant predictors are included.Impute within a pipeline using observed features.
MNARP(M=1) depends on the missing value or unobserved causeObserved data alone may not identify the full distribution.Use sensitivity analysis, external data, or explicit missingness models.

 

CAUTION  Do not diagnose mechanisms from percentages alone

A column with 2% missing values may be MNAR, while a column with 40% may be structurally absent or MAR. Collection context matters more than the percentage.

 

7.1.3 Missing-value percentages

Column-level percentages identify features with substantial missingness, but the denominator and scope must be stated. Calculate percentages on the appropriate dataset partition, retain raw counts, and examine whether missingness varies by time, source, target class, or observed subgroup.

PYTHON   •  EXAMPLE 7.1 — COLUMN-LEVEL MISSINGNESS SUMMARY

import pandas as pd

missing_summary = (
    df.isna()
      .agg(["sum""mean"])
      .T
      .rename(columns={"sum""missing_count""mean""missing_rate"})
      .assign(missing_pct=lambda x: 100 * x["missing_rate"])
      .sort_values("missing_pct", ascending=False)
)

print(missing_summary[["missing_count""missing_pct"]])

 

Keep both counts and percentages. A percentage without its denominator can be misleading.

 

Figure 7.2 — Missingness in the deliberately imperfect laboratory dataset.

7.1.4 Rows versus columns with missing data

Column analysis asks whether a feature is usable. Row analysis asks whether individual observations contain enough information for a valid prediction or analysis. Removing a column can discard information from every observation; removing rows can distort the population if incomplete records differ systematically from complete records. The best decision often combines both perspectives.

PYTHON   •  EXAMPLE 7.2 — ROW-LEVEL MISSINGNESS BURDEN

row_missing = df.isna().sum(axis=1)
row_missing_pct = 100 * df.isna().mean(axis=1)

row_report = df.loc[:, ["record_id""customer_id"]].copy()
row_report["missing_fields"= row_missing
row_report["missing_pct"= row_missing_pct

print(row_report.sort_values("missing_fields", ascending=False).head(10))

 

 

Question

Column perspective

Row perspective

How much information is absent?Percentage missing in each featureNumber or percentage missing in each sample
What may be removed?Feature if unusable or unavailable at inferenceObservation if essential fields are absent
Main bias riskDropping a predictive but incomplete variableDropping a systematically different subgroup
Useful visualizationMissingness bar chart or matrixHistogram of missing fields per row
Important extensionMissingness by group or timeCo-occurrence patterns across fields

 

7.1.5 Missingness patterns and associations

Two variables may be missing together because they share a source system, form section, sensor, or process stage. Pattern analysis can reveal a pipeline failure or structural dependency that simple percentages hide. It is also useful to compare missingness with observed variables, while remembering that an association does not prove MAR or MNAR.

PYTHON   •  EXAMPLE 7.3 — MISSINGNESS INDICATORS AND GROUP COMPARISON

df["income_missing"= df["monthly_income"].isna().astype("int8")

by_plan = (
    df.groupby("plan", dropna=False)["income_missing"]
      .agg(["count""mean"])
      .rename(columns={"mean""missing_rate"})
)

print(by_plan.sort_values("missing_rate", ascending=False))

 

 

GOOD PRACTICE  Target-aware inspection must be isolated

During development, it can be useful to inspect whether feature missingness differs by target. Do this only inside the training data. The final test set must remain untouched until final evaluation.

 


 

 

7.2 Missing-Value Strategies

There is no universally best strategy. The choice depends on the variable meaning, missingness mechanism, amount missing, sample size, algorithm, operational use, and cost of bias. A sound strategy begins by asking whether the value should exist, whether it can be recovered, and whether the absence itself contains information.

7.2.1 Strategy decision framework

Strategy

When it may be reasonable

Main risks

Pipeline requirement

Remove rowsFew rows are incomplete; missingness is plausibly MCAR; essential fields are absentLoss of power and subgroup biasApply explicit rule before fitting or inside a documented transformer.
Remove featureFeature is unavailable at prediction time, nearly empty, unreliable, or redundantDiscarding useful signalDecide using training data and operational constraints.
Mean imputationRoughly symmetric numeric feature; simple baselineReduces variance and weakens relationshipsFit mean on training data only.
Median imputationSkewed numeric feature or moderate outliersStill compresses distributionFit median on training data only.
Most-frequent imputationLow-cardinality categorical feature with a dominant categoryOverstates common category and hides uncertaintyFit mode on training data only.
Constant imputationAbsence has a meaningful explicit state such as “Unknown”May create an artificial clusterUse a value distinguishable from real categories.
Missing indicatorAbsence may carry process or behavioral informationCan encode undesirable collection biasGenerate consistently in train and inference.
Model-based imputationSeveral observed variables predict the missing valueComplexity, overfitting, false precisionFit imputer within cross-validation.
No imputationModel handles missing values natively and semantics are acceptableBehavior may be algorithm-specificValidate performance and production compatibility.

 

7.2.2 Removing rows

Complete-case analysis retains only rows without missing values in selected fields. It is easy to explain, but can waste information and alter the population. Define the subset of required variables explicitly; indiscriminately calling dropna() across the entire table can remove rows because of nonessential metadata.

PYTHON   •  EXAMPLE 7.4 — REMOVE ONLY ROWS MISSING ESSENTIAL FIELDS

essential = ["customer_id""churned"]

before = len(df)
clean = df.dropna(subset=essential).copy()
removed = before - len(clean)

print(f"Removed {removed} rows missing essential fields.")

 

 

COMMON MISTAKE  Avoid blanket deletion

`df.dropna()` may remove a record because an optional feature is missing. Name the fields, count the affected rows, and examine who is removed.

 

7.2.3 Removing features

A high missing percentage is a warning, not an automatic deletion rule. A 70% complete laboratory biomarker may still be valuable if available at prediction time and measured for a meaningful subgroup. Conversely, a 99% complete post-event field must be removed because it leaks the target. Feature removal should combine data evidence with operational and domain requirements.

PYTHON   •  EXAMPLE 7.5 — FLAG FEATURES FOR REVIEW, NOT AUTOMATIC DELETION

missing_pct = 100 * df.isna().mean()
review_threshold = 40

review_columns = missing_pct[missing_pct >= review_threshold].index.tolist()
print("Review with domain experts:", review_columns)

# Do not drop automatically without checking meaning and availability.

 

 

7.2.4 Mean and median imputation

Mean imputation preserves the observed mean of a symmetric variable but pulls missing records toward the center and underestimates variance. Median imputation is more robust to skew and extreme values. Both are simple baselines; neither recreates the uncertainty or conditional relationships of the missing observations.

PYTHON   •  EXAMPLE 7.6 — NUMERIC IMPUTATION IN A SCIKIT-LEARN PIPELINE

from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

numeric_pipeline = Pipeline([
    ("imputer", SimpleImputer(strategy="median")),
    ("scaler", StandardScaler()),
])

# The median is learned only when the pipeline is fitted on training data.

 

 

7.2.5 Most-frequent and constant-value imputation

For categorical variables, the mode is convenient but can inflate the majority category. Constant imputation creates an explicit category such as “Unknown” or “Not recorded,” preserving the distinction between observed categories and missingness. Confirm that the chosen token cannot collide with a legitimate value.

PYTHON   •  EXAMPLE 7.7 — CATEGORICAL IMPUTATION

from sklearn.impute import SimpleImputer
from sklearn.preprocessing import OneHotEncoder
from sklearn.pipeline import Pipeline

categorical_pipeline = Pipeline([
    ("imputer", SimpleImputer(strategy="constant", fill_value="Missing")),
    ("encoder", OneHotEncoder(handle_unknown="ignore")),
])

 

 

7.2.6 Missing-value indicators

An indicator allows the model to distinguish an imputed value from a genuinely observed value. It is especially useful when the collection process carries information. However, the indicator may encode access, workflow, or demographic disparities. Its predictive value should be interpreted carefully and monitored over time.

PYTHON   •  EXAMPLE 7.8 — ADD INDICATORS AUTOMATICALLY

from sklearn.impute import SimpleImputer

imputer = SimpleImputer(
    strategy="median",
    add_indicator=True,
)

X_train_imputed = imputer.fit_transform(X_train_numeric)
X_valid_imputed = imputer.transform(X_valid_numeric)

 

The imputer learns medians and which columns need indicators from the training data only.

 

7.2.7 Model-based imputation

Model-based methods estimate a missing feature using other observed variables. K-nearest-neighbor imputation uses similar rows; iterative imputation models each incomplete feature conditionally on other features. These methods can preserve relationships better than a constant statistic, but they add computational cost, may amplify bias, and can create an unjustified impression of precision.

PYTHON   •  EXAMPLE 7.9 — K-NEAREST-NEIGHBOR IMPUTATION

from sklearn.impute import KNNImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

knn_numeric = Pipeline([
    ("scale_before", StandardScaler()),
    ("imputer", KNNImputer(n_neighbors=5, weights="distance")),
])

 

KNN distance is scale-sensitive. Evaluate the complete preprocessing design inside cross-validation.

 

PYTHON   •  EXAMPLE 7.10 — ITERATIVE IMPUTATION

from sklearn.experimental import enable_iterative_imputer  # noqa: F401
from sklearn.impute import IterativeImputer

iterative = IterativeImputer(
    max_iter=15,
    random_state=42,
    initial_strategy="median",
)

X_train_imputed = iterative.fit_transform(X_train_numeric)

 

 

7.2.8 When not to impute

  • The value is structurally inapplicable and should be represented by a separate state or model design.
  • The feature will not be available at prediction time; remove it rather than fabricate availability.
  • The target is missing or immature; do not invent labels for supervised training without a defensible labeling method.
  • The missingness mechanism is likely MNAR and a simple imputation would conceal major uncertainty.
  • The variable is mostly missing, poorly defined, or unreliable and contributes little validated value.
  • A native missing-value model has been validated and preserves a meaningful distinction.
  • The imputation would violate physical constraints or produce an impossible combination.
  • The downstream decision requires an actual measurement, not an estimated substitute.

COMMON MISTAKE  Never impute the target casually

Supervised models learn from labels. Filling missing targets with the majority class, a mean, or the model’s own predictions creates circular evidence and can invalidate evaluation.

 

7.2.9 Compare strategies with cross-validation

Treat imputation as a hyperparameter of the modeling system. Compare plausible strategies using identical folds and a complete pipeline. The comparison should include performance, stability, calibration, subgroup effects, and operational interpretability—not only the mean score.

PYTHON   •  EXAMPLE 7.11 — COMPARE NUMERIC IMPUTATION STRATEGIES

from sklearn.model_selection import cross_validate
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression

for strategy in ["mean""median"]:
    model = Pipeline([
        ("imputer", SimpleImputer(strategy=strategy, add_indicator=True)),
        ("classifier", LogisticRegression(max_iter=1000)),
    ])
    scores = cross_validate(model, X, y, cv=5, scoring="roc_auc")
    print(strategy, scores["test_score"].mean())

 

 


 

 

7.3 Duplicate Data

A duplicate is not defined only by identical rows. The relevant question is whether multiple records represent the same event, entity, measurement, or information. Some repetitions are legitimate longitudinal observations; others are accidental copies that distort frequencies and leak information across data splits.

7.3.1 Exact duplicates

Exact duplicates have identical values across the selected comparison columns. A technical identifier or ingestion timestamp may differ even when the substantive record is the same, so duplicate detection should explicitly exclude fields that are expected to be unique.

PYTHON   •  EXAMPLE 7.12 — DETECT EXACT SUBSTANTIVE DUPLICATES

substantive_columns  = [
    "customer_id""age""monthly_income""income_unit",
    "city""plan""signup_date""support_calls""churned",
]

exact_mask = df.duplicated(subset=substantive_columns, keep=False)
exact_duplicates = df.loc[exact_mask].sort_values(substantive_columns)

print(exact_duplicates)

 

 

7.3.2 Partial duplicates

Partial duplicates match on a meaningful key but disagree on one or more attributes. They may represent corrections, updates, concurrent records, or conflicts between sources. Do not keep an arbitrary first or last row until the ordering, source reliability, and business rule are understood.

Situation

Possible rule

Required evidence

Same transaction ID, identical business fieldsKeep one recordConfirm accidental re-ingestion.
Same customer and timestamp, different amountInvestigate conflictSource system, update sequence, or audit trail.
Same entity, different observation datesRetain as repeated measurementsEnsure time is part of the unit of observation.
Same entity and date, corrected statusKeep latest valid versionReliable version number or processing timestamp.
Near-match names and addressesPotential duplicate entitiesEntity-resolution method and human review for uncertain matches.

 

PYTHON   •  EXAMPLE 7.13 — FIND REPEATED ENTITY-DATE KEYS

key = ["customer_id""signup_date"]

conflicts = (
    df.groupby(key, dropna=False)
      .filter(lambda group: len(group) > 1)
      .sort_values(key)
)

print(conflicts[[*key, "record_id""plan""support_calls"]])

 

 

7.3.3 Duplicate entities

A customer, patient, machine, or person can appear in several rows. Whether this is duplication depends on the unit of observation. If each row is a transaction, repeated customers are expected. If each row should represent one customer at a fixed prediction time, multiple rows require aggregation or a version-selection rule.

PYTHON   •  EXAMPLE 7.14 — COUNT OBSERVATIONS PER ENTITY

entity_counts = df["customer_id"].value_counts(dropna=False)

print(entity_counts.describe())
print("Entities with multiple rows:", (entity_counts > 1).sum())
print(entity_counts[entity_counts > 1].head(20))

 

 

7.3.4 Repeated measurements

Repeated measurements can be valuable temporal information. Removing them as duplicates would destroy the signal. Instead, preserve the measurement timestamp, define the prediction index time, and create history features using only observations available before that time. Splitting should keep correlated measurements from the same entity together unless the evaluation explicitly simulates future observations of known entities.

Unit of observation

Repeated rows are…

Suitable split

One patientPotential duplicates unless versions are intendedGroup split by patient
One patient visitLegitimate repeated measurementsGroup or time-aware split
One machine cycleLegitimate if cycle ID differsGroup by machine and consider time
One daily store recordLegitimate longitudinal observationsTime-based split
One transactionLegitimate if transaction IDs differGroup by customer if entity memorization is a risk

 

7.3.5 Duplicate train-test records

Figure 7.3 — Overlapping records or entities can turn evaluation into memorization.

If the same record appears in training and testing, the score no longer measures generalization. Even nonidentical rows from the same entity can leak stable identifiers, demographics, or behavior. Perform entity-aware or time-aware splitting before data-dependent preprocessing and verify overlap explicitly.

PYTHON   •  EXAMPLE 7.15 — VERIFY ENTITY SEPARATION AFTER SPLITTING

train_entities = set(X_train["customer_id"])
test_entities = set(X_test["customer_id"])

overlap = train_entities.intersection(test_entities)
if overlap:
    raise ValueError(f"Entity leakage: {len(overlap)} IDs occur in both sets")

 

 

7.3.6 Effects of duplication on evaluation

  • Inflated accuracy because copied records are easy to recognize.
  • Narrow confidence intervals because repeated rows are not independent evidence.
  • Distorted class balance and category frequencies.
  • Overweighting of entities with many accidental copies.
  • Biased feature importance toward identifiers or stable entity characteristics.
  • Unrealistic estimates of deployment performance on new entities or future periods.
  • Potentially contradictory labels for the same substantive record.
  • Unstable metrics when duplicate groups are distributed unevenly across folds.

GOOD PRACTICE  Deduplication before or after splitting?

Identify duplication rules using the raw data and domain definition. Remove accidental exact records before splitting, then use group-aware splitting for legitimate repeated entities. Fit learned cleaning parameters only on training folds.

 


 

 

7.4 Inconsistent Data

Inconsistency occurs when values that should share a common meaning use different representations, units, types, or constraints. Standardization should improve semantic consistency without collapsing genuinely different categories. Preserve the raw field or an audit trail whenever transformations are material.

7.4.1 Typographical errors and inconsistent labels

Case, whitespace, punctuation, accents, abbreviations, and spelling can split one category into several levels. Begin with conservative normalization—trim whitespace and standardize case—then map verified variants to canonical values. Fuzzy matching should produce candidates for review rather than silently changing uncertain records.

PYTHON   •  EXAMPLE 7.16 — CONSERVATIVE CATEGORICAL NORMALIZATION

city_map = {
    "casablanca""Casablanca",
    "rabat""Rabat",
    "tanger""Tangier",
    "tangier""Tangier",
    "marrakesh""Marrakesh",
}

normalized = df["city"].astype("string").str.strip().str.lower()
df["city_clean"= normalized.map(city_map)

unmapped = df.loc[df["city"].notna() & df["city_clean"].isna(), "city"].unique()
print("Unmapped labels:", unmapped)

 

 

PYTHON   •  EXAMPLE 7.17 — VALIDATE CATEGORIES AGAINST AN ALLOWED SET

allowed_plans = {"Basic""Plus""Premium"}

df["plan_clean"= df["plan"].astype("string").str.strip().str.title()
invalid_plans = ~df["plan_clean"].isin(allowed_plans)  & df["plan_clean"].notna()

print(df.loc[invalid_plans, ["record_id""plan""plan_clean"]])

 

 

7.4.2 Unit inconsistencies

A single numeric column can combine monthly and annual income, kilograms and pounds, Celsius and Fahrenheit, or seconds and milliseconds. Such values may look like outliers even though the measurements are valid. Convert to a canonical unit using a verified unit field or source-specific metadata, and retain the original value for traceability.

PYTHON   •  EXAMPLE 7.18 — CONVERT INCOME TO A CANONICAL MONTHLY UNIT

def to_monthly_income(row):
    value = row["monthly_income"]
    unit = row["income_unit"]

    if pd.isna(value):
        return pd.NA
    if unit == "MAD/month":
        return value
    if unit == "MAD/year":
        return value / 12
    raise ValueError(f"Unexpected unit: {unit!r}")

df["monthly_income_clean"= df.apply(to_monthly_income, axis=1)

 

 

CAUTION  Convert units before outlier deletion

A value of 96,000 may be a valid annual salary rather than an impossible monthly value. Investigate units and source systems before applying statistical thresholds.

 

7.4.3 Incorrect data types

Numeric values may be stored as text because of currency symbols, decimal separators, or mixed tokens. Dates may be generic strings. Boolean fields may use yes/no, 0/1, and true/false simultaneously. Convert explicitly, count failed conversions, and avoid silently coercing unexpected values to missing without an audit report.

PYTHON   •  EXAMPLE 7.19 — SAFE NUMERIC CONVERSION WITH FAILURE REPORTING

raw_income = df["monthly_income_raw"].astype("string")
normalized = raw_income.str.replace(","".", regex=False).str.strip()
converted = pd.to_numeric(normalized, errors="coerce")

conversion_failed = raw_income.notna() & converted.isna()
print(df.loc[conversion_failed, ["record_id""monthly_income_raw"]])

df["monthly_income"= converted

 

 

7.4.4 Invalid ranges and impossible values

Range validation combines universal constraints and domain rules. Support-call counts cannot be negative. An age may be technically possible but incompatible with the target population. A transaction date may be valid in the calendar but occur before the system existed. Rules should distinguish hard impossibility from a review range.

Rule type

Example

Recommended handling

Hard physical/logical rulesupport_calls < 0Reject, correct from source, or set missing with a logged reason.
Domain eligibility ruleage < 18 for an adult-only serviceInvestigate population definition and source record.
Plausibility review rangemonthly income > 100,000 MADReview unit, source, and customer segment; do not auto-delete.
Cross-field constraintend_date before start_dateResolve temporal inconsistency or exclude the interval.
Conditional constraintpregnancy_status present for inapplicable recordsReview structural missingness and schema design.

 

PYTHON   •  EXAMPLE 7.20 — REUSABLE VALIDATION ASSERTIONS

rules = {
    "age_range": df["age"].between(18100| df["age"].isna(),
    "support_nonnegative": df["support_calls"].ge(0| df["support_calls"].isna(),
    "target_binary": df["churned"].isin([01]),
}

for name, valid_mask in rules.items():
    failures = df.loc[~valid_mask]
    print(name, "failures:"len(failures))

 

 

7.4.5 Date inconsistencies

Date problems include ambiguous day-month order, impossible calendar dates, future dates, swapped fields, mixed time zones, and timestamps outside the observation window. Parse with an explicit expected format when possible, preserve failures, and validate dates relative to a clearly defined reference time.

PYTHON   •  EXAMPLE 7.21 — PARSE AND VALIDATE DATES

reference_date = pd.Timestamp("2026-08-01")

parsed = pd.to_datetime(df["signup_date"], format="%Y-%m-%d", errors="coerce")
parse_failed = df["signup_date"].notna() & parsed.isna()
future = parsed.gt(reference_date)

print("Parse failures:", df.loc[parse_failed, "signup_date"].tolist())
print("Future dates:", df.loc[future, "signup_date"].tolist())

df["signup_date_clean"= parsed.mask(future)

 

 

7.4.6 Cross-field and schema consistency

Individual values can be valid while their combination is impossible. Examples include a closure date before signup, a premium discount greater than the charge, or a newborn with 15 years of employment. Cross-field rules and schema checks should be encoded as executable tests so that new data can be rejected or quarantined automatically.

PYTHON   •  EXAMPLE 7.22 — CROSS-FIELD VALIDATION REPORT

checks = pd.DataFrame(index=df.index)
checks["valid_age"= df["age"].between(18100| df["age"].isna()
checks["valid_unit"= df["income_unit"].isin(["MAD/month""MAD/year"])
checks["valid_date"= df["signup_date_clean"].notna()

failure_count = (~checks).sum(axis=1)
quality_issues = df.loc[failure_count.gt(0), ["record_id""customer_id"]].copy()
quality_issues["failed_rules"= failure_count[failure_count.gt(0)]
print(quality_issues.head())

 

 


 

 

7.5 Outliers

7.5.1 Definition of an outlier

An outlier is an observation that is unusual relative to a reference distribution, rule, model, or domain expectation. The reference must be stated. A value can be globally unusual but normal within a subgroup, or numerically extreme but operationally important. Outlier detection produces candidates for investigation—not an automatic deletion list.

Type

Meaning

Example

Statistical outlierFar from the bulk under a numerical ruleIncome above Q3 + 1.5×IQR
Domain-specific outlierViolates or approaches a domain expectationAge 220 or negative sensor pressure
Measurement errorGenerated by malfunction, entry error, or unit errorTemperature 900°C from a faulty sensor
Valid rare observationUncommon but real member of the populationA legitimate high-value transaction
Contextual outlierUnusual only in a specific contextHigh energy use at night but normal during production
Collective anomalyA sequence or group is unusual togetherA gradual sensor drift across several measurements

 

Figure 7.4 — A domain-informed decision process for unusual observations.

7.5.2 Z-score

The z-score measures distance from the mean in standard-deviation units: z = (x − μ) / σ. A common screening threshold is |z| > 3, but this is not a universal law. The method is sensitive to extreme values and is most interpretable for approximately symmetric distributions. Compute training statistics only, then apply them consistently to validation and test data.

PYTHON   •  EXAMPLE 7.23 — Z-SCORE CANDIDATES

mean_income = X_train["monthly_income_clean"].mean()
std_income = X_train["monthly_income_clean"].std(ddof=0)

= (X_train["monthly_income_clean"- mean_income) / std_income
z_candidates = X_train.loc[z.abs().gt(3)]

print(z_candidates[["customer_id""monthly_income_clean"]])

 

 

7.5.3 Interquartile range

The interquartile range is IQR = Q3 − Q1. Tukey fences commonly flag values below Q1 − 1.5×IQR or above Q3 + 1.5×IQR. The method is robust to extreme values but still depends on the population and distribution. For strongly skewed data, many valid tail observations may be flagged.

PYTHON   •  EXAMPLE 7.24 — IQR CANDIDATES

series = X_train["monthly_income_clean"].dropna()
q1, q3 = series.quantile([0.250.75])
iqr = q3 - q1
lower = q1 - 1.5 * iqr
upper = q3 + 1.5 * iqr

mask = ~X_train["monthly_income_clean"].between(lower, upper)
candidates = X_train.loc[mask & X_train["monthly_income_clean"].notna()]
print(lower, upper, len(candidates))

 

 

7.5.4 Visual detection

Histograms, box plots, scatter plots, and time-series plots reveal context that a threshold cannot. A box plot shows tail candidates but not sample density; a histogram shows shape but can hide relationships; a scatter plot can expose conditional anomalies; a time plot can identify spikes, drift, or sensor resets.

Figure 7.5 — Statistical candidates require contextual investigation before treatment.

PYTHON   •  EXAMPLE 7.25 — COMPLEMENTARY VISUAL CHECKS

import matplotlib.pyplot as plt

fig, axes = plt.subplots(12, figsize=(104))
X_train["monthly_income_clean"].plot(kind="hist", bins=30, ax=axes[0])
X_train.boxplot(column="monthly_income_clean", by="plan_clean", ax=axes[1])

axes[0].set_title("Income distribution")
axes[1].set_title("Income by plan")
fig.suptitle("")
plt.tight_layout()
plt.show()

 

 

7.5.5 Capping and transformation

Capping limits values to selected bounds, often training quantiles. It can reduce the influence of extreme values but changes legitimate observations and creates a pile-up at the boundary. Log or power transformations compress positive skew while retaining order. Both operations must be justified, fitted on training data, and included in the reproducible pipeline.

PYTHON   •  EXAMPLE 7.26 — TRAINING-DERIVED QUANTILE CAPPING

lower, upper = X_train["monthly_income_clean"].quantile([0.010.99])

X_train = X_train.copy()
X_valid = X_valid.copy()
X_train["income_capped"= X_train["monthly_income_clean"].clip(lower, upper)
X_valid["income_capped"= X_valid["monthly_income_clean"].clip(lower, upper)

 

The bounds come only from training data. Record the percentage capped in every partition.

 

PYTHON   •  EXAMPLE 7.27 — LOG TRANSFORMATION FOR POSITIVE SKEW

import numpy as np

if (X_train["monthly_income_clean"].dropna() < 0).any():
    raise ValueError("log1p requires non-negative income values")

X_train["log_income"= np.log1p(X_train["monthly_income_clean"])
X_valid["log_income"= np.log1p(X_valid["monthly_income_clean"])

 

 

7.5.6 Robust models and robust statistics

Sometimes the best response is to keep valid extremes and choose methods that are less sensitive to them. Median and IQR summaries, RobustScaler, Huber regression, quantile regression, and tree-based models can reduce influence without deleting observations. Robustness does not eliminate the need to fix impossible values or unit errors.

PYTHON   •  EXAMPLE 7.28 — ROBUST SCALING AND HUBER REGRESSION

from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import RobustScaler
from sklearn.linear_model import HuberRegressor

robust_model = Pipeline([
    ("imputer", SimpleImputer(strategy="median")),
    ("scaler", RobustScaler()),
    ("regressor", HuberRegressor()),
])

 

 

CAUTION  Rare cases may be the objective

Fraud, equipment failure, severe disease, and cyberattacks are intentionally rare. Removing unusual target-positive cases can destroy the problem the model is meant to solve.

 


 

 

Practical Lab — Clean a Deliberately Imperfect Dataset

PRACTICAL LAB  Lab objective

Create an auditable cleaning workflow that preserves raw data, diagnoses defects, applies justified transformations, validates the result, and protects the test set.

 

Scenario

A subscription company wants to train a churn classifier. The extracted table contains missing values, exact duplicates, repeated customer entities, category variants, mixed income units, invalid ages, malformed dates, and extreme income values. Students must create a cleaning notebook and document every decision. The target is churned; each modeling observation should represent one customer snapshot.

Required outputs

  • An immutable raw-data object and a separate working copy.
  • A data-quality audit with counts, percentages, examples, and affected identifiers.
  • A written cleaning decision log containing evidence, rule, action, and impact.
  • A cleaned feature table with documented types and units.
  • A duplicate and entity-overlap report.
  • A missing-value strategy fitted only on training data.
  • An outlier candidate report distinguishing errors from valid rare cases.
  • Executable validation checks that fail when constraints are violated.
  • A before-and-after summary of row count, entity count, missingness, and ranges.
  • A short reflection on remaining uncertainty and model risks.

Lab Step 1 — Load and preserve the raw data

PYTHON   •  LAB 7.1 — LOAD WITHOUT OVERWRITING THE RAW TABLE

from pathlib import Path
import pandas as pd

DATA_PATH = Path("data/imperfect_customer_data.csv")
raw = pd.read_csv(DATA_PATH)
working = raw.copy(deep=True)

print("Rows:"len(raw))
print("Columns:", raw.shape[1])
print(raw.head())

 

 

Lab Step 2 — Build a compact audit

PYTHON   •  LAB 7.2 — DATA-QUALITY AUDIT FUNCTION

def quality_audit(data, entity_key="customer_id"):
    return {
        "rows"len(data),
        "columns": data.shape[1],
        "entities": data[entity_key].nunique(dropna=False),
        "exact_duplicate_rows"int(data.duplicated().sum()),
        "rows_with_missing"int(data.isna().any(axis=1).sum()),
        "missing_by_column": data.isna().sum().to_dict(),
        "dtypes": data.dtypes.astype(str).to_dict(),
    }

baseline_audit = quality_audit(working)
print(baseline_audit)

 

 

Lab Step 3 — Create a decision log

Issue

Evidence

Decision rule

Action

Impact to record

Missing income22 rows; varies by planRetain rows; median imputation + indicator in training pipelineLeave as NaN in cleaned tableRows affected; validation score comparison
Exact duplicate recordsTwo complete copiesSame substantive fields and no version meaningKeep one; preserve removed IDs in logRows removed; class balance before/after
Mixed income unitsTwo rows marked MAD/yearUnit metadata is trustedDivide annual values by 12Original and converted values
Invalid ages-4, 9, 220Adult service: valid range 18–100Set to missing and flag source errorIDs and original values
Category variantsCase, spaces, Tanger/TangierVerified canonical mapNormalize to approved labelsUnmapped values must be zero
Malformed/future datesImpossible and future stringsDate must parse and be ≤ reference dateSet missing; create issue flagIDs, original text, reason
High incomeSome annual units; two extreme monthly valuesCorrect units first, then review domain evidenceRetain valid high values; compare robust methodsCandidate list and decision rationale

 

Lab Step 4 — Normalize text and units

PYTHON   •  LAB 7.3 — CANONICALIZATION FUNCTIONS

CITY_MAP = {
    "casablanca""Casablanca",
    "rabat""Rabat",
    "tanger""Tangier",
    "tangier""Tangier",
    "marrakesh""Marrakesh",
}

working["city_clean"= (
    working["city"].astype("string").str.strip().str.lower().map(CITY_MAP)
)
working["plan_clean"= working["plan"].astype("string").str.strip().str.title()

annual = working["income_unit"].eq("MAD/year")
working["monthly_income_clean"= working["monthly_income"]
working.loc[annual, "monthly_income_clean"/= 12

 

 

Lab Step 5 — Validate and quarantine impossible values

PYTHON   •  LAB 7.4 — APPLY EXPLICIT HARD CONSTRAINTS

reference_date = pd.Timestamp("2026-08-01")
working["signup_date_clean"= pd.to_datetime(
    working["signup_date"], format="%Y-%m-%d", errors="coerce"
)

invalid_age = ~working["age"].between(18100& working["age"].notna()
future_date = working["signup_date_clean"].gt(reference_date)

working["age_invalid"= invalid_age.astype("int8")
working["date_invalid"= (
    working["signup_date"].notna() & working["signup_date_clean"].isna()
    | future_date
).astype("int8")

working.loc[invalid_age, "age"= pd.NA
working.loc[future_date, "signup_date_clean"= pd.NaT

 

 

Lab Step 6 — Resolve exact duplicates and preserve entity groups

PYTHON   •  LAB 7.5 — REMOVE ACCIDENTAL COPIES

substantive = [
    "customer_id""age""monthly_income""income_unit""city""plan",
    "signup_date""support_calls""churned",
]

duplicate_mask = working.duplicated(subset=substantive, keep="first")
removed_duplicates = working.loc[duplicate_mask, ["record_id""customer_id"]].copy()
working = working.loc[~duplicate_mask].copy()

print("Removed exact copies:"len(removed_duplicates))

 

 

The two remaining repeated customers have different support-call values. They are not automatically deleted. Because the project defines one customer snapshot, the team must identify the correct version using a reliable timestamp or source rule. If no defensible rule exists, quarantine the conflicting entities rather than selecting a row arbitrarily.

Lab Step 7 — Split by entity before fitting imputers

PYTHON   •  LAB 7.6 — GROUP-AWARE TRAIN-TEST SPLIT

from sklearn.model_selection import GroupShuffleSplit

= working.drop(columns="churned")
= working["churned"]
groups = working["customer_id"]

splitter = GroupShuffleSplit(n_splits=1, test_size=0.20, random_state=42)
train_idx, test_idx = next(splitter.split(X, y, groups=groups))

X_train, X_test = X.iloc[train_idx].copy(), X.iloc[test_idx].copy()
y_train, y_test = y.iloc[train_idx].copy(), y.iloc[test_idx].copy()

assert set(X_train["customer_id"]).isdisjoint(X_test["customer_id"])

 

 

Lab Step 8 — Build leakage-safe preprocessing

PYTHON   •  LAB 7.7 — NUMERIC AND CATEGORICAL PIPELINES

from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, RobustScaler

numeric_features = ["age""monthly_income_clean""support_calls"]
categorical_features = ["city_clean""plan_clean"]

numeric = Pipeline([
    ("impute", SimpleImputer(strategy="median", add_indicator=True)),
    ("scale", RobustScaler()),
])
categorical = Pipeline([
    ("impute", SimpleImputer(strategy="constant", fill_value="Missing")),
    ("encode", OneHotEncoder(handle_unknown="ignore")),
])

preprocessor = ColumnTransformer([
    ("numeric", numeric, numeric_features),
    ("categorical", categorical, categorical_features),
])

 

 

Lab Step 9 — Validate the cleaned result

PYTHON   •  LAB 7.8 — ASSERTIONS AND BEFORE-AFTER COMPARISON

assert working["churned"].isin([01]).all()
assert working["income_unit"].isin(["MAD/month""MAD/year"]).all()
assert working["age"].dropna().between(18100).all()
assert working["monthly_income_clean"].dropna().gt(0).all()
assert working["plan_clean"].dropna().isin(["Basic""Plus""Premium"]).all()

before = quality_audit(raw)
after = quality_audit(working)
comparison = pd.DataFrame({"before": before, "after": after})
print(comparison)

 

 

Lab Step 10 — Document five cleaning decisions

For each decision, students write a concise paragraph containing: the evidence, the rule, the action, the number of affected records, the expected modeling consequence, and any remaining uncertainty. The following is a model statement:

GOOD PRACTICE  Example evidence-based decision

Two records contained income values of 72,000 and 96,000 with the unit “MAD/year.” They were converted to 6,000 and 8,000 MAD/month using the trusted unit field. No rows were removed. The original values and units were retained in the audit table. This conversion was performed before outlier screening because mixed units would otherwise create false anomaly candidates.

 

Lab deliverable checklist

Deliverable component

Minimum evidence

Dataset dimensionsRows, columns, unique entities before and after cleaning
Missing-data reportCounts, percentages, row burden, and chosen strategy by feature
Duplicate reportExact copies, repeated keys, entity counts, and split-overlap assertion
Consistency reportCategory map, unit conversions, type failures, range and date rules
Outlier reportCandidate method, IDs, domain investigation, and final action
Decision logEvidence, rule, action, impact, and responsible reviewer
ReproducibilityNotebook runs top-to-bottom; fixed random state; no hidden manual edits
Leakage protectionGroup-aware split; all learned transformations fitted on training data
ValidationAssertions pass; unexpected categories and rule failures are reported
ReflectionRemaining uncertainty, possible bias, and next data-collection improvements

 


 

 

Worked Mini-Cases

Case 1 — Medical measurement missing after device failure

A blood-pressure field is missing for 8% of visits. Missingness is concentrated at one clinic during a device outage. This is related to an observed site and time period, so a MAR assumption may be plausible if clinic and date are included. A complete-case deletion could remove an entire operational episode. A pipeline-based imputation with clinic/time predictors, a missingness indicator, and a clinic-specific sensitivity analysis is more defensible.

Case 2 — Rare fraudulent transactions

Large transactions are flagged by an IQR rule. Investigation shows that several are confirmed fraud and several are legitimate business purchases. Deleting all flagged rows would remove target-positive examples and high-value legitimate cases. The team retains them, corrects one currency conversion error, applies robust scaling, and evaluates precision-recall performance by transaction-value range.

Case 3 — Duplicate patient records

Two hospitals contribute records for the same patient. Names differ slightly and local IDs are different. Fuzzy matching is used only to generate candidate pairs; uncertain matches are reviewed. Confirmed duplicate entities are assigned a stable cross-source patient ID. All visits are retained, but group cross-validation ensures that the same patient never occurs in both training and validation folds.

Knowledge Check

1.  Explain why a high percentage of missing values does not automatically justify dropping a feature.

2.  Differentiate MCAR, MAR, and MNAR using one original example for each mechanism.

3.  Why should imputation statistics be fitted only on the training data?

4.  When can repeated rows be valid rather than duplicates?

5.  Why may exact duplicate records inflate a test score?

6.  Describe one risk of most-frequent categorical imputation.

7.  Why should unit conversion occur before statistical outlier detection?

8.  Compare z-score and IQR screening.

9.  Give two situations in which a statistically extreme observation should be retained.

10.  What information belongs in a cleaning decision log?

11.  Why is fuzzy matching unsuitable as an automatic deletion rule?

12.  What is the difference between an impossible value and a valid rare value?

Suggested Answers

Question

Suggested answer

1Missingness percentage does not reveal feature importance, mechanism, subgroup coverage, operational availability, or whether absence is structural. Review meaning and predictive value.
2MCAR: independent packet loss. MAR: survey income missing more often among an observed age group. MNAR: high earners are more likely to omit income because of the value itself.
3Using validation or test values changes the learned statistic and leaks information about the evaluation distribution, producing optimistic or nonreproducible estimates.
4Repeated rows are valid when the unit is a visit, transaction, cycle, or time point and each row represents a distinct event.
5The model may memorize a record seen during training, so the test result measures recognition rather than generalization.
6It inflates the dominant category, hides uncertainty, and can distort class-conditional relationships.
7Mixed units generate false extremes. Canonical units are required before values can be compared meaningfully.
8Z-scores use mean and standard deviation and are sensitive to extremes; IQR fences use quartiles and are more robust, but both are context-dependent screens.
9Retain a confirmed rare customer or a true fraud/failure case; retain an extreme value that is valid within a particular subgroup.
10Evidence, affected records, decision rule, action, impact, reviewer, date/version, and unresolved uncertainty.
11Similarity scores can merge different people or events. Candidate matches require thresholds, supporting fields, and review for uncertain cases.
12An impossible value violates a hard logical or physical rule; a valid rare value is possible and confirmed, even if statistically unusual.

 

Chapter Summary

  • Missingness must be interpreted through variable meaning, collection process, and mechanism—not percentage alone.
  • Rows and columns provide complementary views of missing data; deletion can introduce bias.
  • Imputation changes distributions and relationships. Fit all data-dependent choices on training data.
  • Exact copies, conflicting keys, duplicate entities, and legitimate repeated measurements require different rules.
  • Train-test overlap invalidates generalization estimates; use entity- or time-aware splitting when necessary.
  • Label, unit, type, range, and date consistency should be enforced with explicit, executable rules.
  • Outlier methods identify candidates. Domain evidence determines whether to correct, retain, transform, cap, or exclude.
  • Robust methods can reduce sensitivity to valid extremes without deleting valuable information.
  • Every material cleaning decision should be reproducible, auditable, and linked to affected records.
  • A clean dataset is not one with no missing values or extreme observations; it is one whose limitations and transformations are understood.

Key Terms

Term

Meaning

MCARMissingness unrelated to observed and unobserved values.
MARMissingness explained by observed variables, under an assumption.
MNARMissingness related to the missing value or an unobserved cause.
ImputationReplacement or estimation of missing feature values.
Missingness indicatorBinary feature showing whether the original value was absent.
Exact duplicateRepeated substantive record with identical compared fields.
Duplicate entityMultiple records referring to the same real-world entity.
Entity leakageThe same entity contributes information to training and evaluation sets.
Canonical valueApproved representation used for a semantic category or unit.
Hard constraintRule that defines physical or logical impossibility.
OutlierObservation unusual relative to a stated reference.
IQRDifference between the third and first quartiles.
CappingReplacing values outside selected bounds with the boundary values.
Robust methodMethod designed to reduce sensitivity to extreme observations.
Decision logAudit record explaining evidence, rule, action, and impact of a cleaning choice.

 

DEFINITION  Expected outcome

Students can transform an imperfect dataset into an analysis-ready training resource while preserving raw evidence, preventing leakage, validating constraints, and documenting every consequential decision.