Pandas Data Cleaning and Analysis

From beginner DataFrames to medium feature aggregation and advanced leak-free production preprocessing pipelines for Machine Learning.

28 minIntermediateCode Examples

The Core Thesis: Real-world datasets never arrive as clean numerical matrices ready for a GPU. They arrive as messy CSVs, JSON logs, and SQL dumps filled with missing values, corrupted strings, duplicate rows, and extreme outliers. Pandas is the industry-standard tabular engine that transforms raw, chaotic data into clean, statistically sound feature matrices—without leaking test-set information into training.

  1. Beginner Foundations: Series, DataFrames, and the Index

While a NumPy array is a raw homogeneous grid of numbers accessed strictly by integer positions, Pandas wraps columnar arrays (backed by NumPy or Apache Arrow) with labeled row indices and named heterogeneous columns:

1. Pandas Series (1D Column)1D Labeled Vector

A single typed column of data paired with an Index. Think of it as a hybrid between a 1D NumPy vector (supports fast vectorized math) and a Python Dictionary (supports O(1)\mathcal{O}(1) label lookup).

df["price"] # Extracts a 1D Series

2. Pandas DataFrame (2D Table)2D Feature Matrix

A 2D table of rows (observations NN) and columns (features DD) that all share the same row Index. Different columns can hold different data types (float64, int64, string, category, datetime64).

df[["sqft", "beds"]] # Extracts a 2D DataFrame

The 60-Second EDA (Exploratory Data Analysis) Checklist: Whenever you load a dataset with df = pd.read_csv("data.csv"), run these five commands before writing a single line of model code:

1. df.shape → (Rows, Columns)
2. df.info() → Column dtypes & RAM usage
3. df.head(5) → Inspect first 5 rows
4. df.isna().sum() → Count missing values
5. df.describe() → Mean, Std, Min, Quartiles
6. df.nunique() → Cardinality per column

  1. Precision Selection: .loc vs. .iloc and Boolean Masking

Beginners often chain brackets like df[0:5]["age"], which causes unpredictable behavior and triggers Pandas' notorious SettingWithCopyWarning. Instead, always use explicit accessors:

1. df.loc[row_label, col_name]
Label-Based Indexing

Selects by explicit Index labels and column names. Warning: Slicing with .loc[0:5] is inclusive of both start and stop labels!

2. df.iloc[row_pos, col_pos]
Integer Position Indexing

Selects strictly by 00-based integer position (just like Python lists and NumPy). Slicing .iloc[0:5] is exclusive of the stop index 55.

3. Vectorized Boolean Masks
df.loc[(cond1) & (cond2)]

Filters millions of rows in C speed using bitwise operators & (AND), | (OR), and ~ (NOT) wrapped in parentheses.

⚡ Knowledge Check

Suppose a DataFrame has default integer index labels 0,1,2,3,4,50, 1, 2, 3, 4, 5. How many rows does df.loc[1:3] return compared to df.iloc[1:3]?

A) df.loc[1:3] returns 3 rows (1, 2, 3); df.iloc[1:3] returns 2 rows (1, 2)▼
✓ Correct!.loc slices by label and includes the stop label (returning rows 1,2,31, 2, 3), whereas .iloc slices by 00-based integer position and excludes the stop position (returning rows 11 and 22).
B) Both return 2 rows because slicing is always exclusive in Python▼
✕ Incorrect.Because label slices like df.loc["Jan":"Mar"] would be awkward if "Mar" were excluded, Pandas makes .loc inclusive of both endpoints.

  1. Intermediate Data Cleaning: Missing Values, Outliers & Strings

Most machine learning algorithms (Linear Regression, SVMs, Neural Networks) will immediately crash or output NaN if fed even a single missing value. Proper data cleaning requires diagnosing why data is missing and choosing the right statistical treatment:

Cleaning TaskPandas MethodWhen to Use It in Machine Learning
Drop Missing Rowsdf.dropna(subset=["target"])Always drop rows where the ground-truth label yy is missing (never invent fake labels!).
Median Imputationdf["inc"].fillna(train_median)Best for skewed numerical features (income, house prices) because median ignores extreme outliers.
Forward/Backward Filldf["sensor"].ffill()For ordered Time-Series data (carries the most recent known past measurement forward).
Deduplicationdf.drop_duplicates(subset=["id"])Prevents identical records from appearing in both Train and Test sets (Data Leakage).
Vectorized String Opsdf["price"].str.replace("$", "")Cleans currency symbols, whitespace (.str.strip()), and regex patterns before casting to float.

Statistical Outlier Detection and Clipping (ZZ-Score vs. IQR)

A single corrupted sensor reading (e.g., an age entered as 999999 instead of 2929) can wildly distort the slope of Linear Regression or explode neural network gradients. We detect and cap outliers using two mathematical bounds:

1. Z-Score Standardization (Gaussian Data)

Measures how many standard deviations σ\sigma a point lies from the mean μ\mu. Points with ∣z∣>3|z| \gt 3 (99.7%99.7\% threshold) are flagged as outliers:

zi=xi−μσz_i = \dfrac{x_i - \mu}{\sigma}
2. Interquartile Range / IQR (Skewed Data)

Uses the 25th25\text{th} percentile (Q1Q_1) and 75th75\text{th} percentile (Q3Q_3) where IQR=Q3−Q1\text{IQR} = Q_3 - Q_1. Cap values using df["x"].clip(lower, upper):

[Q1−1.5⋅IQR,Q3+1.5⋅IQR][Q_1 - 1.5 \cdot \text{IQR}, \quad Q_3 + 1.5 \cdot \text{IQR}]

  1. Intermediate Analysis: GroupBy (Split-Apply-Combine), Merging & Time-Series

To engineer high-signal features (like a customer's average spend compared to their city's average spend), Pandas uses the Split-Apply-Combine paradigm via df.groupby():

1. .agg() — Collapse Groups into Summary Rows

Reduces NN rows down to KK unique group rows. Ideal for building summary tables and reporting metrics:

df.groupby("city")["price"].agg(["count", "mean", "median"])

2. .transform() — Broadcast Group Stats to Original Rows

Returns a Series with the exact same length NN as the original DataFrame! Essential for ML feature engineering:

df["city_avg"] = df.groupby("city")["price"].transform("mean")

When combining multiple tables (e.g., joining user profiles with transaction logs) or engineering temporal features, rely on three workhorse operations:

Relational Joins (pd.merge)

SQL-style joins (how="left", "inner"). Always verify row counts before and after merging so many-to-many key duplicates do not silently multiply your dataset!

Rolling Windows (.rolling)

Computes moving averages (df["sales"].rolling(7).mean()) and exponentially weighted moving averages (.ewm()) for time-series forecasting.

Datetime Accessor (.dt)

After parsing with pd.to_datetime(), extract cyclical ML features instantly via df["ts"].dt.dayofweek, .dt.hour, and .dt.month.

⚡ Knowledge Check

Your DataFrame has 10,00010{,}000 house listings across 5050 zip codes. You want to add a new column zip_median_price directly onto the 10,00010{,}000-row DataFrame so each house can be compared against its own zip code's median. Which GroupBy method should you use?

A) df.groupby("zip")["price"].transform("median")▼
✓ Correct!.transform("median") computes the median for each of the 5050 zip codes and broadcasts the result back to all 10,00010{,}000 rows, aligning with the original DataFrame index!
B) df.groupby("zip")["price"].agg("median")▼
✕ Incorrect..agg("median") collapses the result into a 5050-row Series indexed by zip code, which cannot be assigned directly as a 10,00010{,}000-row column without a separate merge.

  1. Advanced Production Pandas: Preventing Data Leakage, Method Chaining & Memory Optimization

At the Advanced / Production AI level, three skills separate senior ML engineers from beginners: Preventing Train-Test Data Leakage, Vectorization & RAM Optimization, and Functional Method Chaining:

1. The #1 Production ML Bug: Train-Test Data Leakage in Pandas

If you compute df["age"].fillna(df["age"].median()) or normalize features across the entire DataFrame BEFORE splitting into Train and Test sets, information from the unseen future Test set leaks into your Training set!

Rule: Split Train/Test FIRST → Compute statistics (mean, median, IQR, target encoding) ONLY on Train → Apply those frozen Train statistics to both Train and Test!

2. Why .apply(axis=1) is a Performance Trap

Calling df.apply(fn, axis=1) runs a slow Python-level loop over every single row, instantiating a new Series object per row (300×300\times slower than C vectorization).

Speed Hierarchy (Fastest to Slowest):
1. NumPy / Pandas Vectorized Ops (C/PyArrow)
2. np.where(cond, a, b) / np.select()
3. df.apply(..., axis=1) / iterrows() (Avoid!)
3. Slashing RAM Usage by 80% (Dtypes & PyArrow)

By default, Pandas loads numbers as 64-bit (float64, int64) and strings as Python pointers (object). On a 20M-row dataset, you can cut RAM by 70–85%70\text{--}85\%:

• Low-cardinality strings → .astype("category")
• 64-bit floats → .astype("float32") (matches GPU!)
• Pandas 2.0+ → dtype_backend="pyarrow"
• Disk I/O → Save as .parquet instead of .csv
⚡ Knowledge Check

You are imputing missing values in df["income"] before training an XGBoost model. Why is calling df["income"] = df["income"].fillna(df["income"].median()) on the full dataset BEFORE calling train_test_split(df) a critical error?

A) It leaks Test-set distribution statistics into the Training set▼
✓ Correct!Computing the median across the entire dataset uses unseen test-set values to fill training rows. Always compute train_median = train_df["income"].median() on the training split only, and use that exact number to fill both train_df and test_df.
B) Because .fillna() only works after sorting the DataFrame▼
✕ Incorrect..fillna() works regardless of sort order; the issue is strictly statistical data leakage between the training and evaluation splits.

  1. Visual Explanation: Leak-Free Pandas ML Preprocessing Pipeline

Look at how a production machine learning pipeline cleans structural errors first, splits into Train and Test sets, fits statistical imputers and group aggregations strictly on the Training split, and exports clean float32 tensors:

  1. Python Implementation: Beginner-to-Advanced Pandas Pipeline

Here is a complete, end-to-end Pandas script demonstrating Beginner string/type cleaning, Intermediate GroupBy .transform() and IQR outlier clipping, and Advanced leak-free train/test statistical imputation with method chaining:

pandas_ml_pipeline.pyPython 3.11+ · Pandas 2.x & NumPy
import numpy as npimport pandas as pd # 1. Raw Messy Dataset (Strings, Missing Values, Duplicates, Outlier)rawData = {    "id": [101, 102, 102, 103, 104, 105],    "city": ["  austin ", "AUSTIN", "AUSTIN", "seattle", "Seattle ", "austin"],    "sqft": [1500.0, np.nan, np.nan, 2100.0, 1850.0, 99999.0],  # 99999 is an outlier!    "price": ["450,000", "520,000", "520,000", "780,000", "690,000", "490,000"]}df = pd.DataFrame(rawData) # 2. BEGINNER + INTERMEDIATE: Clean Strings, Deduplicate & Optimize DtypescleanDf = (    df.drop_duplicates(subset=["id"])    .assign(        city=lambda x: x["city"].str.strip().str.title().astype("category"),        price=lambda x: x["price"].str.replace("$", "", regex=False)                                  .str.replace(",", "", regex=False)                                  .astype("float32")    )    .reset_index(drop=True)) # 3. ADVANCED: Leak-Free Train / Test Statistical Imputation & IQR ClippingtrainDf = cleanDf.iloc[:4].copy()           # First 4 rows = Train SplittestDf = cleanDf.iloc[4:].copy()            # Remaining rows = Unseen Test Split # Compute statistics STRICTLY on Train Split!q1, q3 = trainDf["sqft"].quantile([0.25, 0.75])iqr = q3 - q1upperBound = q3 + 1.5 * iqrtrainMedianSqft = trainDf["sqft"].median()cityMeanMap = trainDf.groupby("city", observed=True)["price"].mean() # Apply frozen Train statistics to both Train and Testfor split in (trainDf, testDf):    split["sqft"] = split["sqft"].fillna(trainMedianSqft).clip(upper=upperBound)    split["cityAvgPrice"] = split["city"].map(cityMeanMap).astype("float32")    split["isLargeHome"] = np.where(split["sqft"] >= 1800, 1, 0) print("Cleaned Train DataFrame:\n", trainDf[["id", "city", "sqft", "price", "cityAvgPrice"]])print("Cleaned Test DataFrame (Outlier 99999 clipped!):\n", testDf[["id", "city", "sqft", "price"]])

Pro Tip (Why We Used .map(cityMeanMap) Across Splits): On a single training table, trainDf.groupby("city")["price"].transform("mean") is super convenient. Across Train and Test splits, however, saving the training group means into a dictionary/Series (cityMeanMap) and applying testDf["city"].map(cityMeanMap) guarantees that Test target prices never leak into your features!

Key Points

✓A Pandas Series is a 1D labeled column vector, and a DataFrame is a 2D table of heterogeneous columns aligned along a shared row Index.
✓Always use df.loc[row_label, col_name] for label-based indexing (inclusive slicing) and df.iloc[row_pos, col_pos] for 00-based integer indexing (exclusive slicing).
✓Handle missing values based on distribution shape: drop rows missing the target label yy, impute skewed numerical features with the median, and forward-fill (.ffill()) ordered time-series.
✓In groupby(), use .agg() to reduce groups into a smaller summary table, and use .transform() to broadcast group-level statistics back to the original row length.
✓To prevent Data Leakage, split Train and Test sets before computing any dataset-wide statistics (medians, means, quantiles, or encodings), fitting strictly on the Training split.
✓Replace slow Python row loops (df.apply(axis=1), iterrows()) with vectorized column expressions, np.where(), category dtypes, and float32 downcasting.

Common Mistakes

✕ Chained indexing assignment like df[df["age"] > 30]["status"] = "senior".

Chaining two bracket operations often modifies a temporary copy in memory that is immediately discarded, leaving df unchanged and triggering SettingWithCopyWarning. Always assign in one step using df.loc[df["age"] > 30, "status"] = "senior".

✕ Using Python keywords (and / or / not) instead of bitwise operators (& / | / ~) in masks.

In Pandas, df[(df["a"] > 1) and (df["b"] < 5)] throws a ValueError: The truth value of a Series is ambiguous. Always use &, |, and ~ with each condition wrapped in parentheses.

✕ Imputing missing values or target-encoding categories before splitting Train and Test.

Calculating means, medians, or group target averages across the full DataFrame leaks test-set information into training features, producing artificially high validation scores that fail in production.

✕ Iterating over DataFrame rows with for idx, row in df.iterrows():.

iterrows() boxes every single row into a new Pandas Series object in Python, turning a 55-millisecond vectorized C operation into a 3030-second bottleneck.

The Big Picture

Manual Notebook Scripting (Fragile & Leaky)

Raw CSV → Global fillna() Leakage → Slow apply(axis=1) Loops → Unreliable Model

Production Pandas Engineering (Fast, Vectorized & Leak-Free)

Typed DataFrame → Structural Clean → Train/Test Split → Fit Train Stats Only → GPU-Ready Tensors

The important conceptual shift is viewing Pandas not as an interactive spreadsheet for manual edits, but as a deterministic, vectorized feature-engineering pipeline. When you combine fast C/Arrow column operations with strict train-test statistical isolation, your models train faster and generalize reliably to real-world production data.

Remember: In Machine Learning, better data beats a bigger model every time[cite: 13]. Pandas is the tool that turns noisy, real-world records into the high-signal features that make learning possible.