Data Preparation / Data Preprocessing Slides 📂 Introduction · 6 of 13 45 min read

Fixing Inconsistent Data & Outlier Detection in Python

A practical, visual guide to the quieter half of data cleaning. Standardise messy strings, dates, units and encodings with pandas, then detect outliers three ways — IQR, Z-score and Isolation Forest — and treat them with capping, log transforms and RobustScaler. Every method comes with a real-world cautionary story.

Fixing Inconsistent Data & Detecting Outliers

The quieter, more dangerous half of data cleaning — phantom categories, mixed formats and extreme values that slip past every check and corrupt every result.
String & Date Fixes Units & Encodings Outlier Detection Treatment

Press Next → or use ← → arrow keys

Section 01

The Hidden Cost of Inconsistent Data

The city that existed 847 times
A logistics company found 847 unique city values where only 28 exist. "Mumbai" alone appeared as mumbai, MUMBAI, Mum., mum, Mumbay, Bombay… Routing optimised against these phantom cities burned ₹12 crore over three months — and the fix was 11 lines of pandas.
847→28Phantom cities collapsed
₹12 crCost of the silence
11Lines of code to fix
100%Passed every null check
🕵️
More dangerous than missing values

A null announces itself — isnull() finds it. An inconsistent value looks perfectly valid, passes every check, and quietly splinters one real category into dozens.

Section 01 · Types

Six Faces of Inconsistency

🔤 Case
Mumbai · mumbai · MUMBAI
␣ Whitespace
"Delhi " · " Delhi" · "new delhi"
✂️ Abbreviations
Dr. · Doctor · dr  |  Ltd · Limited
📅 Date formats
2024-03-12 · 12/03/2024 · Mar 12
⚖️ Units
500 g vs 0.5 kg · USD 50 vs ₹4150
🔣 Encodings
True · "True" · 1 · "yes"  |  NaN · "N/A"
🧭
One diagnostic to rule them all

Run df['col'].value_counts() on every categorical column. If the unique count doesn't match your domain knowledge, an inconsistency is hiding in plain sight.

Section 02

Fixing String Inconsistencies

Collapsing 8 variants into one canonical value "Mumbai" "MUMBAI" "Mum." "Bombay" "mumbay" "mumbai" 1 canonical form · 8,702 rows
df['city'] = (df['city']
    .str.lower()                          # Mumbai → mumbai
    .str.strip()                          # " mumbai " → mumbai
    .str.replace(r'\s+', ' ', regex=True))  # collapse inner spaces
city_map = {'mum':'mumbai', 'bombay':'mumbai', 'blr':'bengaluru', 'madras':'chennai'}
df['city'] = df['city'].replace(city_map)
df['city'].nunique()   # 847 → 28 ✅
Section 02 · Order

Fuzzy Matching & Why Order Matters

1 · lowercase.str.lower() 2 · strip.str.strip() 3 · de-space\s+ → ' ' 4 · map.replace(map) 5 · fuzzyrapidfuzz ≥ 80
from rapidfuzz import process, fuzz
canonical = ['mumbai', 'delhi', 'bengaluru', 'hyderabad', 'chennai']

def fuzzy_fix(val, choices, threshold=80):
    hit = process.extractOne(val, choices, scorer=fuzz.ratio)
    return hit[0] if hit and hit[1] >= threshold else val

df['city'] = df['city'].apply(lambda x: fuzzy_fix(x, canonical))
📋
The order is not optional

Each step exposes matches for the next. Fuzzy-match before lowercasing and "Mumbai" and "mumbai" look different; map before stripping and " mum " never hits the dictionary.

Section 03

Fixing Date & Time Inconsistencies

A retail team merged POS (DD/MM/YYYY), web (YYYY-MM-DD) and ERP (DD-Mon-YY). Stored as strings, they sorted alphabetically — scrambling six months of history. One to_datetime() fixed it.

Before · mixed strings
rawsource
12/03/2024POS
2024-03-15Web
20-Mar-24ERP
March 22, 2024Manual
20240328API
After · datetime64
cleandtype
2024-03-12datetime64
2024-03-15datetime64
2024-03-20datetime64
2024-03-22datetime64
2024-03-28datetime64
df['order_date'] = pd.to_datetime(
    df['order_date'],
    dayfirst=True,       # DD/MM first
    errors='coerce')    # bad → NaT

failed = df['order_date'].isnull().sum()

# mine features from clean dates
df['month']   = df['order_date'].dt.month
df['dow']     = df['order_date'].dt.day_name()
df['quarter'] = df['order_date'].dt.quarter
df['wknd']    = df['order_date'].dt.dayofweek >= 5
⏱️
Always errors='coerce'

Unparseable dates become NaT instead of crashing — audit those rows, don't lose the run.

Section 04

Units, Encodings & Type Fixes

# boolean / yes-no chaos → real booleans
yes_no = {'yes':True,'y':True,'1':True,'true':True,'no':False,'n':False,'0':False}
df['is_returned'] = df['is_returned'].astype('str').str.lower().str.strip().map(yes_no)

# fake NA strings pandas misses
df = df.replace(['na','n/a','none','null','-','','unknown'], np.nan)

# unify currency (USD → INR) and weight (kg → g)
df.loc[df['currency']=='USD', 'amount'] *= 83.5;  df['currency'] = 'INR'
df.loc[df['unit'].str.lower()=='kg', 'weight'] *= 1000;  df['unit'] = 'g'
📥
Catch fake NAs at load time

Stop them before they enter the frame: pd.read_csv('f.csv', na_values=['NA','N/A','none','-','unknown','']). A fake NA that survives as a string corrupts every null count and fill downstream.

Section 05

Outliers — Investigate Before You Delete

The data scientist who deleted their best customer
An auto-IQR filter removed a ₹4.2 crore enterprise client — 18% of revenue. The churn model, never having seen them, "learned" big customers don't churn and scored the real VIP as low risk while a competitor courted them. The contract was lost.
Is this an outlier? Data error→ fix or remove Legit extreme→ keep & document Uncertain→ domain expert Keeping? use robust models (median, IQR, RobustScaler). Treating? cap, transform, or model extremes separately.
Section 06

Three Ways to Spot an Outlier

📦
IQR Method
Q1−1.5·IQR … Q3+1.5·IQR
Non-parametric, assumes no distribution, handles skew. The default for business data.
📐
Z-Score
z = (x−μ)/σ , |z| > 3
Parametric — assumes normality. Fast and interpretable, but distorted by the very outliers it hunts.
🌲
Isolation Forest
ML · multivariate
Finds points anomalous across many columns at once — the only one that catches joint anomalies.
🎯
Same data, different nets

IQR casts the widest single-column net, Z-score flags only extreme univariate values, and Isolation Forest sees combinations. Pick by distribution and dimensionality, not habit.

Section 06 · Method 1

IQR Method — Fences & Flags

Distribution with IQR fences — 3.7% flagged Q1−1.5·IQR Q3+1.5·IQR normal 96.3% outliers
Q1, Q3 = df['purchase_amount'].quantile([0.25, 0.75])
IQR = Q3 - Q1
lo, hi = Q1 - 1.5*IQR, Q3 + 1.5*IQR
df['is_outlier'] = (df['purchase_amount'] < lo) | (df['purchase_amount'] > hi)
# INSPECT before treating — never blind-delete
df[df['is_outlier']].sort_values('purchase_amount', ascending=False).head(20)
Section 06 · Method 2

Z-Score & the Robust Modified Z

from scipy import stats
# standard z — assumes normality
z = np.abs(stats.zscore(
      df['purchase_amount'].dropna()))
df['is_outlier_z'] = z > 3

# modified z — uses median + MAD
med = df['purchase_amount'].median()
mad = (df['purchase_amount']-med).abs().median()
mz  = 0.6745*(df['purchase_amount']-med)/mad
df['is_outlier_modz'] = mz.abs() > 3.5
🔁
The circular trap of standard Z

Standard Z uses the mean and σ — both inflated by the very outliers you're trying to find, so extreme points can mask themselves. The modified Z swaps in the median and MAD (median absolute deviation), which outliers can't distort. Prefer it for skewed data.

📊
Only after a normality check

Reach for Z-score only once a histogram or Q–Q plot confirms the column is roughly normal. Otherwise the IQR method is the safer default.

Section 06 · Method 3

Isolation Forest — Catching Joint Anomalies

Normal on each axis, odd together x = normal y = normal
from sklearn.ensemble import \
    IsolationForest
X = df[['age','purchase_amount',
        'rating','delivery_days']].dropna()
iso = IsolationForest(
    contamination=0.05,
    n_estimators=200, random_state=42)
df['is_outlier_iso'] = \
    iso.fit_predict(X) == -1
df['anomaly_score'] = \
    iso.score_samples(X)
🌲
Why IQR misses it

The red point sits in the normal range on every single axis, so IQR and Z-score wave it through. Only a multivariate method sees that the combination is impossible.

Section 07

Four Ways to Treat an Outlier

StrategyHowWhenRisk
Removedf[~mask]Confirmed data-entry errors onlyhigh
Cap / Winsorise.clip(lo, hi)Legit extremes; linear modelsmedium
Transformnp.log1p(x)Right-skewed long tailslow
Keep + scaleRobustScalerLegit extremes; tree modelslow
🗑️
Removal is the last resort, not the first

Deletion throws away information forever and is only safe for values that cannot be real (age 300, negative price). For everything else, cap, transform, or keep — and always .head(20) the flagged rows first.

Section 07 · Transform

Log Transform — Taming the Long Tail

Before — right-skewed long tail → log1p After — near-normal
df['purchase_log']  = np.log1p(df['purchase_amount'])   # log(1+x), safe at 0
df['purchase_sqrt'] = np.sqrt(df['purchase_amount'])    # moderate skew
from scipy.stats import boxcox
df['purchase_bc'], lam = boxcox(df['purchase_amount'] + 1)  # best λ automatically
Section 07 · Scale

RobustScaler vs StandardScaler

Original + outlier outlier RobustScaler ✓ normal spread preserved (median + IQR) StandardScaler ✗ normals crushed together (mean + σ dragged)
from sklearn.preprocessing import RobustScaler, StandardScaler
df['robust'] = RobustScaler().fit_transform(df[['purchase_amount']])  # median + IQR ✓
df['std']    = StandardScaler().fit_transform(df[['purchase_amount']]) # mean + σ ✗
Section 08

A Reusable Fix-It Template

def fix_inconsistencies(df, string_maps=None):
    df = df.copy()
    for col in df.select_dtypes(['object','category']).columns:
        df[col] = df[col].str.lower().str.strip()
        if string_maps and col in string_maps:
            df[col] = df[col].replace(string_maps[col])
    return df

def detect_and_treat_outliers(df, cols, method='iqr', treatment='cap'):
    df, report = df.copy(), {}
    for col in cols:
        s = df[col].dropna()
        if method == 'iqr':
            Q1, Q3 = s.quantile([0.25, 0.75]); IQR = Q3 - Q1
            lo, hi = Q1 - 1.5*IQR, Q3 + 1.5*IQR
        else:
            lo, hi = s.mean() - 3*s.std(), s.mean() + 3*s.std()
        mask = (df[col] < lo) | (df[col] > hi)
        report[col] = {'n': int(mask.sum()), 'lo': lo, 'hi': hi}
        df[col+'_outlier'] = mask
        if treatment == 'cap':  df[col] = df[col].clip(lo, hi)
        elif treatment == 'log': df[col] = np.log1p(df[col].clip(lower=0))
    return df, report
Pro · Debug

Common Pitfalls & Their Fixes

SymptomThe trapThe fix
Hundreds of phantom categoriesMapping before lower/striplower → strip → de-space → map → fuzzy
Dates sort wrongKept as stringsto_datetime(errors='coerce')
Nulls miscountedFake "N/A" stringsna_values=[…] at load
VIP silently deletedAuto-remove IQR outliersInspect .head(20) first
Extreme point hides itselfStandard Z on skewed dataModified Z (median + MAD) or IQR
Scaled features crushed flatStandardScaler with outliersRobustScaler
Joint anomaly missedOnly per-column checksIsolation Forest on all features
Section 09 · Part 1

The Golden Rules — 1 to 4

🔧 INCONSISTENCY & OUTLIERS · RULES 1–4
1
Diagnose first. Run value_counts() on every categorical column — 847 cities when you expect 28 is a signal, not a detail.
2
Order matters. lowercase → strip → de-space → map → fuzzy. Each step feeds the next.
3
Always errors='coerce' on dates — unparseable values become NaT to audit, not an exception that halts the run.
4
Kill fake NAs at load with na_values=[…] — string "N/A" corrupts every null count downstream.
Section 09 · Part 2

The Golden Rules — 5 to 8 & Takeaway

🔧 INCONSISTENCY & OUTLIERS · RULES 5–8
5
Inspect before removing. df[df['is_outlier']].sort_values(...).head(20) — your most important rows are often your most extreme.
6
IQR by default. Only switch to Z-score after a histogram or Q–Q plot confirms normality.
7
Prefer RobustScaler whenever legitimate extremes exist — mean and σ are distorted by outliers, median and IQR are not.
8
Document everything. What changed, why, and the assumption behind it. Undocumented cleaning is a liability.
🔎
Curiosity is the most powerful cleaning tool

Inconsistencies and outliers are different problems with one cure: systematic investigation before treatment. Run value_counts() on every column, inspect every flagged outlier, and your models come out more accurate, more robust and more trustworthy.

🔧 End of tutorial · Press to review, or click Restart