Skip to content
ExponaBLOG
PRACTICAL AI & MACHINE LEARNING · DAY 005 / 100

Exploratory data analysis checklist: a step-by-step guide

Understand data before modeling it.

Expona Admin7 min read

Why exploratory data analysis matters before you write a single line of model code

You've just received a dataset and the temptation is immediate: split it, pick an algorithm, train. Stop. The exploratory data analysis checklist: a step-by-step guide exists precisely because we've all shipped models that fail silently in production because we skipped the hard work of understanding our data first. Spending a few hours on EDA upfront saves weeks of debugging downstream.

This guide walks through the four critical checks every practitioner should perform before modeling: understanding feature distributions, identifying missing data patterns, assessing class balance, and spotting correlations and data leaks. We'll use real code examples so you can run these checks today.

Distribution of every feature

Your first job is to see what you're working with. Every feature tells a story through its distribution—and that story will shape how your model learns.

Start by computing summary statistics for each numeric column: mean, median, standard deviation, min, and max. Look for outliers. A feature with a median of 100 but a max of 50,000 tells you something is unusual. Then visualize: histograms for univariate distributions, density plots for overlays, box plots to spot outliers at a glance.

python
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

df = pd.read_csv('your_data.csv')

# Summary stats
print(df.describe())

# Check skewness and kurtosis
print(df.skew())
print(df.kurtosis())

# Visualize distributions
fig, axes = plt.subplots(ncols=4, figsize=(15, 4))
for idx, col in enumerate(df.select_dtypes(include=[np.number]).columns[:4]):
    axes[idx].hist(df[col], bins=50, edgecolor='black')
    axes[idx].set_title(col)
    axes[idx].set_xlabel('Value')
    axes[idx].set_ylabel('Frequency')
plt.tight_layout()
plt.show()

Why does this matter? Highly skewed features can dominate tree-based models or cause numerical instability in linear ones. A feature with a Gaussian distribution behaves differently from one with a long tail. If you see a bimodal distribution (two peaks), you might have a hidden grouping in your data—subpopulations that behave differently.

For categorical features, always count occurrences and identify rare classes:

python
for col in df.select_dtypes(include=['object']).columns:
    print(f"\n{col}:")
    print(df[col].value_counts(dropna=False))
    print(f"Unique values: {df[col].nunique()}")

Rare categories often get dropped by models during training. If a category appears only five times in a dataset of one million rows, most algorithms won't learn meaningful patterns. You'll need to decide: group them, drop them, or treat them specially during encoding.

Missing values and their pattern

Missing data isn't random. Understanding how and why it's missing shapes your imputation strategy and reveals potential issues in your data pipeline.

First, quantify the missing data:

python
missing_count = df.isnull().sum()
missing_percent = 100 * df.isnull().sum() / len(df)

missing_df = pd.DataFrame({
    'column': missing_count.index,
    'missing_count': missing_count.values,
    'missing_percent': missing_percent.values
})
missing_df = missing_df[missing_df['missing_count'] > 0].sort_values('missing_percent', ascending=False)
print(missing_df)

Now examine the pattern. Is missingness random (Missing Completely At Random, MCAR)? Or does it correlate with other features (Missing At Random, MAR)? Or with the missing value itself (Missing Not At Random, MNAR)?

A concrete example: in a lending dataset, income might be missing more often for self-employed borrowers—MNAR. If you impute those missing values with the global mean, you're introducing bias. Those borrowers might actually have higher average income, and you've just underestimated them.

Detect missingness patterns:

python
import matplotlib.pyplot as plt

# Heatmap of missing data
fig, ax = plt.subplots(figsize=(12, 6))
missing_matrix = df.isnull()
ax.imshow(missing_matrix.T, cmap='coolwarm', aspect='auto', interpolation='nearest')
plt.xlabel('Row')
plt.ylabel('Column')
plt.title('Missing Data Heatmap')
plt.colorbar(ax.images[0], label='Missing (1) or Present (0)')
plt.show()

# Check if missingness in one column correlates with another
for col in df.columns:
    if df[col].isnull().sum() > 0:
        for other_col in df.select_dtypes(include=[np.number]).columns:
            if other_col != col:
                correlation = df[col].isnull().astype(int).corr(df[other_col])
                if abs(correlation) > 0.1:
                    print(f"Missing in {col} correlates with {other_col}: {correlation:.3f}")

Decisions flow from this analysis. If a column is 95% missing, drop it—there's no signal. If missing values are MCAR, simple imputation (mean, median, forward-fill) is acceptable. If MNAR, you might create a flag indicator to preserve the signal that "this value was missing."

Class balance of the target

If you're working on classification, this check is non-negotiable. An imbalanced target silently ruins model evaluation and real-world performance.

Check your target distribution immediately:

python
target_counts = df['target'].value_counts()
target_percent = 100 * df['target'].value_counts(normalize=True)

balance_df = pd.DataFrame({
    'class': target_counts.index,
    'count': target_counts.values,
    'percent': target_percent.values
})
print(balance_df)

# Visualize
plt.bar(balance_df['class'], balance_df['count'])
plt.ylabel('Count')
plt.xlabel('Class')
plt.title('Class Distribution')
plt.show()

In fraud detection, the positive class (fraud) might be 0.1% of your data. A naive model that predicts "no fraud" for everything achieves 99.9% accuracy—but zero fraud caught. Accuracy is meaningless here; you need precision, recall, F1-score, or AUC-ROC.

Imbalance also affects training. Most algorithms learn better from balanced data. Options: undersample the majority class, oversample the minority class (or use SMOTE), weight classes inversely to their frequency, or adjust decision thresholds post-training. Each has trade-offs. Undersampling loses data. Oversampling can lead to overfitting. Weighting increases computational cost.

Calculate the class weight ratio for your implementation:

python
from sklearn.utils.class_weight import compute_class_weight

classes = np.unique(df['target'])
weights = compute_class_weight('balanced', classes=classes, y=df['target'])
class_weight_dict = dict(zip(classes, weights))
print(f"Class weights: {class_weight_dict}")

Document this. Share it with your team. Your model's performance will depend on how you handled imbalance, and the next person reading your code needs to understand why.

Correlations and obvious leaks

Correlation analysis reveals feature relationships and exposes the most dangerous problem in machine learning: data leakage.

Compute and visualize the correlation matrix:

python
import seaborn as sns

numeric_df = df.select_dtypes(include=[np.number])
corr_matrix = numeric_df.corr()

# Heatmap
plt.figure(figsize=(12, 10))
sns.heatmap(corr_matrix, annot=True, fmt='.2f', cmap='coolwarm', center=0)
plt.title('Feature Correlation Matrix')
plt.tight_layout()
plt.show()

# Find high correlations (excluding diagonal)
threshold = 0.8
high_corr_pairs = []
for i in range(len(corr_matrix.columns)):
    for j in range(i + 1, len(corr_matrix.columns)):
        if abs(corr_matrix.iloc[i, j]) > threshold:
            high_corr_pairs.append((
                corr_matrix.columns[i],
                corr_matrix.columns[j],
                corr_matrix.iloc[i, j]
            ))

for col1, col2, corr in high_corr_pairs:
    print(f"{col1} <-> {col2}: {corr:.3f}")

High correlation between features doesn't disqualify them—it can indicate redundancy, but many models handle it fine. Tree-based models don't care. Linear models might suffer from multicollinearity, which inflates coefficients and hurts interpretability.

What you're really hunting for is leakage: features that shouldn't be available at prediction time or that incorporate the target itself.

Classic leak scenarios:

  • A feature that's calculated after the event you're predicting. Example: predicting loan default using the final payment amount (only known after repayment).
  • A feature perfectly correlated with the target that shouldn't be. Example: in a churn dataset, a "cancelled_date" field that only exists for churned customers.
  • Information from the future. Example: using next month's average spend to predict this month's spending behavior.

Correlation with the target is expected and desired, but correlation that's too perfect (>0.95) warrants inspection:

python
target_corr = numeric_df.corr()['target'].sort_values(ascending=False)
print(target_corr)

# Flag suspiciously high correlations
suspicious = target_corr[(target_corr.abs() > 0.95) & (target_corr.index != 'target')]
if len(suspicious) > 0:
    print("⚠️ Potential leakage detected:")
    print(suspicious)

When you find a leak, your only option is to remove that feature. It inflates your validation metrics and fails catastrophically in production. Better to find it now than after deployment.

Common mistakes

Skipping EDA entirely and going straight to modeling. You'll waste weeks tuning hyperparameters when the real issue is data quality or a fundamental mismatch between your data and the problem.

Treating EDA as a one-time event. Run these checks after every new data pull, schema change, or pipeline update. Data degrades, distributions shift, leaks creep in.

Imputing missing values before understanding them. Understand the pattern first. If 30% of a feature is missing and correlated with your target, that's information. Blindly imputing destroys it.

Ignoring class imbalance because your accuracy looks good. If your target is 1% positive and your model is 98% accurate but finds zero positives, it's worthless. Always check precision, recall, and F1 for classification tasks.

Correlations don't imply causation—but they do imply investigation. A feature with 0.9 correlation to your target might be a leak, might be legitimate signal, or might be a proxy for something else. You need to think about it, not just code through it.

Key takeaways

  • Visualize every numeric feature's distribution and check for skewness, outliers, and bimodality. They inform preprocessing choices.
  • Quantify missing data and examine patterns. Missingness is often informative; understand it before you impute.
  • For classification, always check class balance. Imbalanced targets require special handling in training and evaluation.
  • Compute feature correlations to spot redundancy and hunt for data leaks. Suspiciously high correlations with the target warrant investigation.
  • Document your findings. The next person (or future you) needs to understand what you discovered and why you made preprocessing decisions.
  • Repeat this process regularly. Data changes. Distributions shift. New leaks appear. EDA is not a one-time gate; it's an ongoing practice.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles