Study interactive :: Progress tools open in the Study Hub reader.

Exploratory Data Analysis (EDA)

This guide covers performing systematic exploratory data analysis to understand your data before modeling.

ML for beginners curriculum map (this guide)

EDA and data cleaning sit between loading data and training models. Use this map with the ML intro beginner track.

Table of Contents


Introduction

What is EDA?

Exploratory Data Analysis (EDA) is the process of analyzing datasets to summarize their main characteristics, often with visual methods. It helps you:

Why EDA is Critical

EDA Philosophy

"The purpose of computing is insight, not numbers.": Richard Hamming

EDA is about asking questions and finding answers in your data.


EDA Workflow

Systematic Approach

  1. Load and Inspect - First look at your data
  2. Understand Structure - Shape, types, missing values
  3. Univariate Analysis - Individual variables
  4. Bivariate Analysis - Relationships between pairs
  5. Multivariate Analysis - Complex relationships
  6. Data Quality - Check for issues
  7. Summary and Insights - Document findings

Understanding Your Data

Initial Inspection

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# Load data
df = pd.read_csv('data.csv')

# First look
print("Dataset Shape:", df.shape)
print("\nColumn Names:", df.columns.tolist())
print("\nData Types:\n", df.dtypes)
print("\nFirst 5 rows:")
print(df.head())
print("\nLast 5 rows:")
print(df.tail())
print("\nRandom 5 rows:")
print(df.sample(5))

Basic Information

# Comprehensive info
df.info()

# Summary statistics
print("\nSummary Statistics:")
print(df.describe())

# For categorical columns
print("\nCategorical Summary:")
print(df.describe(include=['object']))

# Memory usage
print("\nMemory Usage:")
print(df.memory_usage(deep=True))

Missing Values Analysis

# Missing values count
missing = df.isnull().sum()
missing_percent = 100 * missing / len(df)
missing_df = pd.DataFrame({
    'Missing Count': missing,
    'Percentage': missing_percent
})
missing_df = missing_df[missing_df['Missing Count'] > 0].sort_values(
    'Missing Count', ascending=False
)
print("\nMissing Values:")
print(missing_df)

# Visualize missing values
plt.figure(figsize=(10, 6))
sns.heatmap(df.isnull(), yticklabels=False, cbar=True, cmap='viridis')
plt.title('Missing Values Heatmap')
plt.tight_layout()
plt.show()

# Missing values pattern
import missingno as msno
msno.matrix(df)
plt.show()
msno.bar(df)
plt.show()

Duplicate Analysis

# Check for duplicates
duplicates = df.duplicated().sum()
print(f"\nNumber of duplicate rows: {duplicates}")

if duplicates > 0:
    print("\nDuplicate rows:")
    print(df[df.duplicated(keep=False)])
    
    # Remove duplicates
    df_clean = df.drop_duplicates()
    print(f"\nAfter removing duplicates: {df_clean.shape}")

Univariate Analysis

Numerical Variables

def analyze_numerical(df, column):
    """
    Comprehensive analysis of a numerical column
    """
    print(f"\n{'='*50}")
    print(f"Analysis of: {column}")
    print(f"{'='*50}")
    
    # Basic statistics
    print("\nBasic Statistics:")
    print(df[column].describe())
    
    # Additional statistics
    print(f"\nMean: {df[column].mean():.2f}")
    print(f"Median: {df[column].median():.2f}")
    print(f"Mode: {df[column].mode().values}")
    print(f"Std Dev: {df[column].std():.2f}")
    print(f"Variance: {df[column].var():.2f}")
    print(f"Skewness: {df[column].skew():.2f}")
    print(f"Kurtosis: {df[column].kurtosis():.2f}")
    
    # Outliers (IQR method)
    Q1 = df[column].quantile(0.25)
    Q3 = df[column].quantile(0.75)
    IQR = Q3 - Q1
    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR
    outliers = df[(df[column] < lower_bound) | (df[column] > upper_bound)]
    print(f"\nOutliers (IQR method): {len(outliers)}")
    
    # Visualizations
    fig, axes = plt.subplots(2, 2, figsize=(15, 10))
    
    # Histogram
    axes[0, 0].hist(df[column].dropna(), bins=30, edgecolor='black')
    axes[0, 0].set_title(f'Histogram of {column}')
    axes[0, 0].set_xlabel(column)
    axes[0, 0].set_ylabel('Frequency')
    
    # Box plot
    axes[0, 1].boxplot(df[column].dropna())
    axes[0, 1].set_title(f'Box Plot of {column}')
    axes[0, 1].set_ylabel(column)
    
    # KDE plot
    axes[1, 0].hist(df[column].dropna(), bins=30, density=True, alpha=0.7, label='Histogram')
    df[column].dropna().plot(kind='kde', ax=axes[1, 0], label='KDE')
    axes[1, 0].set_title(f'Distribution of {column}')
    axes[1, 0].set_xlabel(column)
    axes[1, 0].legend()
    
    # Q-Q plot (check normality)
    from scipy import stats
    stats.probplot(df[column].dropna(), dist="norm", plot=axes[1, 1])
    axes[1, 1].set_title(f'Q-Q Plot of {column}')
    
    plt.tight_layout()
    plt.show()

# Example
analyze_numerical(df, 'age')

Categorical Variables

def analyze_categorical(df, column):
    """
    Comprehensive analysis of a categorical column
    """
    print(f"\n{'='*50}")
    print(f"Analysis of: {column}")
    print(f"{'='*50}")
    
    # Value counts
    value_counts = df[column].value_counts()
    print("\nValue Counts:")
    print(value_counts)
    
    # Percentages
    print("\nPercentages:")
    print(df[column].value_counts(normalize=True) * 100)
    
    # Unique values
    print(f"\nNumber of unique values: {df[column].nunique()}")
    print(f"Unique values: {df[column].unique()}")
    
    # Visualizations
    fig, axes = plt.subplots(1, 2, figsize=(15, 5))
    
    # Bar plot
    value_counts.plot(kind='bar', ax=axes[0])
    axes[0].set_title(f'Bar Plot of {column}')
    axes[0].set_xlabel(column)
    axes[0].set_ylabel('Count')
    axes[0].tick_params(axis='x', rotation=45)
    
    # Pie chart
    value_counts.plot(kind='pie', ax=axes[1], autopct='%1.1f%%')
    axes[1].set_title(f'Pie Chart of {column}')
    axes[1].set_ylabel('')
    
    plt.tight_layout()
    plt.show()

# Example
analyze_categorical(df, 'category')

Bivariate Analysis

Numerical vs Numerical

def analyze_numerical_pair(df, col1, col2):
    """
    Analyze relationship between two numerical variables
    """
    # Correlation
    correlation = df[col1].corr(df[col2])
    print(f"\nCorrelation between {col1} and {col2}: {correlation:.3f}")
    
    # Visualizations
    fig, axes = plt.subplots(2, 2, figsize=(15, 10))
    
    # Scatter plot
    axes[0, 0].scatter(df[col1], df[col2], alpha=0.5)
    axes[0, 0].set_xlabel(col1)
    axes[0, 0].set_ylabel(col2)
    axes[0, 0].set_title(f'Scatter Plot: {col1} vs {col2}')
    
    # Add regression line
    z = np.polyfit(df[col1].dropna(), df[col2].dropna(), 1)
    p = np.poly1d(z)
    axes[0, 0].plot(df[col1], p(df[col1]), "r--", alpha=0.5)
    
    # Hexbin plot (for large datasets)
    axes[0, 1].hexbin(df[col1], df[col2], gridsize=20, cmap='Blues')
    axes[0, 1].set_xlabel(col1)
    axes[0, 1].set_ylabel(col2)
    axes[0, 1].set_title(f'Hexbin Plot: {col1} vs {col2}')
    
    # Joint plot (using seaborn)
    sns.jointplot(data=df, x=col1, y=col2, kind='scatter', ax=axes[1, 0])
    
    # 2D Histogram
    axes[1, 1].hist2d(df[col1], df[col2], bins=20, cmap='Blues')
    axes[1, 1].set_xlabel(col1)
    axes[1, 1].set_ylabel(col2)
    axes[1, 1].set_title(f'2D Histogram: {col1} vs {col2}')
    
    plt.tight_layout()
    plt.show()

# Example
analyze_numerical_pair(df, 'age', 'income')

Numerical vs Categorical

def analyze_numerical_categorical(df, num_col, cat_col):
    """
    Analyze relationship between numerical and categorical variables
    """
    # Group statistics
    print(f"\nStatistics of {num_col} by {cat_col}:")
    print(df.groupby(cat_col)[num_col].describe())
    
    # Visualizations
    fig, axes = plt.subplots(2, 2, figsize=(15, 10))
    
    # Box plot
    df.boxplot(column=num_col, by=cat_col, ax=axes[0, 0])
    axes[0, 0].set_title(f'{num_col} by {cat_col}')
    axes[0, 0].set_xlabel(cat_col)
    axes[0, 0].set_ylabel(num_col)
    
    # Violin plot
    sns.violinplot(data=df, x=cat_col, y=num_col, ax=axes[0, 1])
    axes[0, 1].set_title(f'Violin Plot: {num_col} by {cat_col}')
    
    # Histogram by category
    for category in df[cat_col].unique():
        axes[1, 0].hist(df[df[cat_col]==category][num_col], 
                       alpha=0.5, label=category, bins=20)
    axes[1, 0].set_xlabel(num_col)
    axes[1, 0].set_ylabel('Frequency')
    axes[1, 0].set_title(f'Distribution of {num_col} by {cat_col}')
    axes[1, 0].legend()
    
    # Bar plot of means
    means = df.groupby(cat_col)[num_col].mean()
    means.plot(kind='bar', ax=axes[1, 1])
    axes[1, 1].set_title(f'Mean {num_col} by {cat_col}')
    axes[1, 1].set_xlabel(cat_col)
    axes[1, 1].set_ylabel(f'Mean {num_col}')
    axes[1, 1].tick_params(axis='x', rotation=45)
    
    plt.tight_layout()
    plt.show()

# Example
analyze_numerical_categorical(df, 'income', 'category')

Categorical vs Categorical

def analyze_categorical_pair(df, col1, col2):
    """
    Analyze relationship between two categorical variables
    """
    # Contingency table
    c df[col2])
    print(f"\nContingency Table: {col1} vs {col2}")
    print(contingency)
    
    # Chi-square test
    from scipy.stats import chi2_contingency
    chi2, p_value, dof, expected = chi2_contingency(contingency)
    print(f"\nChi-square test:")
    print(f"Chi-square statistic: {chi2:.3f}")
    print(f"P-value: {p_value:.3f}")
    print(f"Degrees of freedom: {dof}")
    
    # Visualizations
    fig, axes = plt.subplots(1, 2, figsize=(15, 5))
    
    # Stacked bar chart
    contingency.plot(kind='bar', stacked=True, ax=axes[0])
    axes[0].set_title(f'Stacked Bar Chart: {col1} vs {col2}')
    axes[0].set_xlabel(col1)
    axes[0].set_ylabel('Count')
    axes[0].legend(title=col2)
    axes[0].tick_params(axis='x', rotation=45)
    
    # Heatmap
    sns.heatmap(contingency, annot=True, fmt='d', cmap='Blues', ax=axes[1])
    axes[1].set_title(f'Heatmap: {col1} vs {col2}')
    
    plt.tight_layout()
    plt.show()

# Example
analyze_categorical_pair(df, 'category', 'status')

Multivariate Analysis

Correlation Matrix

# Calculate correlation matrix
numeric_df = df.select_dtypes(include=[np.number])
correlati>

# Visualize
plt.figure(figsize=(12, 10))
sns.heatmap(correlation_matrix, annot=True, fmt='.2f', 
            cmap='coolwarm', center=0, square=True, linewidths=1)
plt.title('Correlation Matrix')
plt.tight_layout()
plt.show()

# Find highly correlated pairs
high_corr = []
for i in range(len(correlation_matrix.columns)):
    for j in range(i+1, len(correlation_matrix.columns)):
        if abs(correlation_matrix.iloc[i, j]) > 0.7:
            high_corr.append((
                correlation_matrix.columns[i],
                correlation_matrix.columns[j],
                correlation_matrix.iloc[i, j]
            ))

print("\nHighly Correlated Pairs (|r| > 0.7):")
for col1, col2, corr in high_corr:
    print(f"{col1} - {col2}: {corr:.3f}")

Pair Plot

# Pair plot for key numerical variables
key_vars = ['var1', 'var2', 'var3', 'var4']  # Select key variables
sns.pairplot(df[key_vars], diag_kind='kde')
plt.show()

Principal Component Analysis (PCA) Visualization

from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler

# Select numerical columns
numeric_df = df.select_dtypes(include=[np.number]).dropna()

# Standardize
scaler = StandardScaler()
scaled_data = scaler.fit_transform(numeric_df)

# Apply PCA
pca = PCA(n_components=2)
pca_result = pca.fit_transform(scaled_data)

# Visualize
plt.figure(figsize=(10, 6))
plt.scatter(pca_result[:, 0], pca_result[:, 1], alpha=0.5)
plt.xlabel(f'PC1 ({pca.explained_variance_ratio_[0]:.2%} variance)')
plt.ylabel(f'PC2 ({pca.explained_variance_ratio_[1]:.2%} variance)')
plt.title('PCA Visualization')
plt.show()

Data Quality Checks

Outlier Detection

def detect_outliers_iqr(df, column):
    """
    Detect outliers using IQR method
    """
    Q1 = df[column].quantile(0.25)
    Q3 = df[column].quantile(0.75)
    IQR = Q3 - Q1
    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR
    
    outliers = df[(df[column] < lower_bound) | (df[column] > upper_bound)]
    return outliers

def detect_outliers_zscore(df, column, threshold=3):
    """
    Detect outliers using Z-score method
    """
    z_scores = np.abs((df[column] - df[column].mean()) / df[column].std())
    outliers = df[z_scores > threshold]
    return outliers

# Example
outliers_iqr = detect_outliers_iqr(df, 'age')
outliers_zscore = detect_outliers_zscore(df, 'age')
print(f"IQR outliers: {len(outliers_iqr)}")
print(f"Z-score outliers: {len(outliers_zscore)}")

Data Consistency Checks

# Check for logical inconsistencies
def check_consistency(df):
    """
    Check for logical inconsistencies in data
    """
    issues = []
    
    # Example: Age should be positive
    if 'age' in df.columns:
        negative_age = df[df['age'] < 0]
        if len(negative_age) > 0:
            issues.append(f"Found {len(negative_age)} rows with negative age")
    
    # Example: Start date should be before end date
    if 'start_date' in df.columns and 'end_date' in df.columns:
        invalid_dates = df[df['start_date'] > df['end_date']]
        if len(invalid_dates) > 0:
            issues.append(f"Found {len(invalid_dates)} rows with start_date > end_date")
    
    return issues

issues = check_consistency(df)
if issues:
    print("Data Consistency Issues:")
    for issue in issues:
        print(f"  - {issue}")
else:
    print("No consistency issues found")

EDA Best Practices

1. Start with Questions

Before analyzing, ask:

2. Document Everything

# Create EDA report
def create_eda_report(df, output_file='eda_report.txt'):
    """
    Generate comprehensive EDA report
    """
    with open(output_file, 'w') as f:
        f.write("="*50 + "\n")
        f.write("EXPLORATORY DATA ANALYSIS REPORT\n")
        f.write("="*50 + "\n\n")
        
        f.write("1. DATASET OVERVIEW\n")
        f.write(f"   Shape: {df.shape}\n")
        f.write(f"   Columns: {df.columns.tolist()}\n\n")
        
        f.write("2. MISSING VALUES\n")
        missing = df.isnull().sum()
        for col, count in missing[missing > 0].items():
            f.write(f"   {col}: {count} ({100*count/len(df):.2f}%)\n")
        f.write("\n")
        
        f.write("3. DATA TYPES\n")
        for col, dtype in df.dtypes.items():
            f.write(f"   {col}: {dtype}\n")
        f.write("\n")
        
        f.write("4. SUMMARY STATISTICS\n")
        f.write(str(df.describe()))
        f.write("\n\n")
        
        # Add more sections as needed
        
    print(f"EDA report saved to {output_file}")

create_eda_report(df)

3. Use Visualizations

4. Iterate

5. Validate Assumptions


Practice Exercises

Exercise 1: Complete EDA

Perform complete EDA on a dataset:

  1. Load and inspect
  2. Check missing values
  3. Analyze each variable
  4. Find relationships
  5. Detect outliers
  6. Create summary report

Exercise 2: Hypothesis Testing

Form hypotheses and test them:

Exercise 3: Feature Engineering Ideas

Based on EDA, suggest:


Resources

Libraries

Books

Tools

# Install: pip install pandas-profiling
from pandas_profiling import ProfileReport

profile = ProfileReport(df, title="EDA Report")
profile.to_file("eda_report.html")

Data Storytelling

What is Data Storytelling?

Data storytelling is the art of communicating insights from data analysis in a clear, compelling, and actionable way. It combines data analysis, visualization, and narrative to help stakeholders understand and act on findings.

Why Data Storytelling Matters

Elements of Effective Data Stories

1. Clear Narrative Structure

Three-Act Structure:

Example Structure:

Problem: Customer churn is increasing
Analysis: Found 3 key factors driving churn
Solution: Implement retention strategies targeting high-risk segments
Impact: Expected 15% reduction in churn rate

2. Know Your Audience

Executive Audience:

Analytical Audience:

General Business Audience:

3. Effective Visualizations

Choose the Right Chart:

Visualization Best Practices:

import matplotlib.pyplot as plt
import seaborn as sns

# Clear, professional styling
sns.set_style("whitegrid")
plt.figure(figsize=(10, 6))

# Example: Sales trend
plt.plot(df['date'], df['sales'], linewidth=2, color='#2E86AB')
plt.title('Monthly Sales Trend (2023)', fontsize=16, fontweight='bold')
plt.xlabel('Month', fontsize=12)
plt.ylabel('Sales ($)', fontsize=12)
plt.grid(True, alpha=0.3)

# Add annotations for key points
plt.annotate('Peak Sales', 
             xy=('2023-11', df.loc[df['date']=='2023-11', 'sales'].values[0]),
             xytext=(10, 10), textcoords='offset points',
             bbox=dict(boxstyle='round,pad=0.5', fc='yellow', alpha=0.7),
             arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0'))

plt.tight_layout()
plt.show()

4. Key Principles

Simplicity:

Consistency:

Emphasis:

5. Narrative Techniques

Start with a Hook:

"Last quarter, we lost $2M in revenue due to customer churn. 
Today, I'll show you why and what we can do about it."

Use Analogies:

"Think of our customer segments like neighborhoods. 
Some neighborhoods have higher crime rates (churn), 
and we need to understand why."

Tell a Story:

"Let's follow a typical customer journey. 
They start here [point to chart], 
encounter this problem [show data], 
and here's where we lose them [highlight]."

6. Actionable Recommendations

Structure Recommendations:

  1. What: Specific action
  2. Why: Data-driven rationale
  3. Impact: Expected outcome
  4. How: Implementation steps

Example:

Recommendation: Implement targeted email campaigns for 
high-risk customers (churn probability > 0.7)

Why: Our analysis shows these customers have 3x higher 
churn rate and respond well to personalized communication

Impact: Expected 20% reduction in churn for this segment, 
saving $400K annually

How: 
1. Identify high-risk customers using our model
2. Create personalized email templates
3. Send weekly engagement emails
4. Track response rates and adjust

Creating a Data Story

Step 1: Define Your Message

Step 2: Structure Your Story

Step 3: Choose Visualizations

Step 4: Write the Narrative

Step 5: Add Context

Example: Complete Data Story

Title: Understanding Customer Churn

Slide 1: The Problem

Customer churn increased 25% last quarter
Lost revenue: $2M
Need to understand why and take action

Slide 2: Key Finding

Visualization: Bar chart showing churn by customer segment
Insight: Segment A has 3x higher churn rate

Slide 3: Root Cause Analysis

Visualization: Correlation heatmap
Insight: Low engagement score correlates with churn

Slide 4: Recommendation

Action: Implement engagement program for Segment A
Expected Impact: 20% reduction in churn = $400K saved

Slide 5: Next Steps

1. Launch pilot program (next month)
2. Measure engagement metrics
3. Expand if successful

Tools for Data Storytelling

Python:

Other Tools:

Best Practices

  1. Start with the End in Mind: Know your message before creating visuals
  2. Less is More: Fewer, clearer visualizations beat many cluttered ones
  3. Test Your Story: Practice with colleagues before presenting
  4. Be Honest: Acknowledge limitations and uncertainties
  5. Focus on Action: Every insight should lead to a recommendation
  6. Use Data Ethically: Present data accurately, avoid manipulation

Common Mistakes to Avoid

  1. Data Dump: Showing all data without narrative
  2. Overwhelming Visuals: Too many charts, too much information
  3. No Clear Message: Audience doesn't know what to take away
  4. Ignoring Audience: Too technical or too simple for audience
  5. No Call to Action: Insights without recommendations
  6. Misleading Visuals: Incorrect scales, cherry-picked data

Key Takeaways

  1. EDA is Essential: Never skip this step
  2. Ask Questions: Start with hypotheses
  3. Visualize: Use plots to understand data
  4. Document: Keep notes of findings
  5. Iterate: EDA is an ongoing process
  6. Validate: Check assumptions and data quality
  7. Tell Stories: Communicate insights effectively

Try next: Write five findings from EDA before you train. Keep at least one that surprised you.

Recall ::

Why do EDA before training a model?

To find quality issues, distributions, and surprises that would silently break training.