EDA: Exploratory Data Analysis Complete Workflow

Exploratory data analysis is the part everyone wants to skip and nobody should. Before you train a single model, you sit with the data and get to know it: load it, look at its shape, find the missing values, spot the outliers, and check which columns actually move together. This exploratory data analysis Python tutorial walks the full workflow on a realistic employee dataset, every number printed is real output from the code you see.

“Fools ignore complexity. Pragmatists suffer it. Some can avoid it. Geniuses remove it.”

Alan Perlis, Epigrams on Programming

Last Updated: July 2026 | Tested on: Python 3.14.6, Pandas 2.3.3, Seaborn 0.13.2 | Difficulty: Advanced | Reading Time: 17 minutes

Think of EDA like checking into a hotel room before you unpack. You flick the light switch, look in the bathroom, test the Wi-Fi, and check the view. You are not living there yet, you are just making sure nothing is broken before you settle in. Exploratory data analysis (EDA) is that same walk-through for a dataset. You poke at it from every angle until you trust it enough to build on top of it.

Here is the trap. A junior data scientist named Niranjan skipped this walk-through and jumped straight into model building. His random forest hit 96% accuracy on customer churn prediction. Impressive, right? Not really. Ten minutes of EDA would have shown that the days_since_last_login column was basically the answer in disguise: customers who had already churned all had huge values. The model was reading the answer key, not learning anything. EDA is how you catch that before it costs you a week.

EDA is not a straight line, it loops. You notice something odd, dig in, find a data quality issue, fix it, look again, spot a new pattern, and go around once more. The goal is not a final answer. The goal is understanding, so that feature engineering, model choice, and every claim you make later rests on something solid. To see this workflow feed into modeling from start to finish, the end-to-end data science project runs EDA and then keeps going all the way to a trained model.

IterateEngineer, Deliver7. Feature EngDerived columnsEncode categories8. DocumentKey findingsRecommendationsExplore Patterns4. UnivariateHistogramsvalue_counts5. BivariateScatter, corr()groupby stats6. MultivariateHeatmap, pairplotVar interactionsPrepare Data1. Load Datapd.read_csvdf.head, df.info2. Inspectdf.describedf.isnull.sum3. Cleanfillna, dropnadrop_duplicatesPython Exploratory Data Analysis: The 8-Step Workflow Loop from Load to Document

Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.

The diagram lays out the whole EDA workflow top to bottom: load the data, inspect its shape and types, clean missing values and duplicates, study each variable on its own, then study variables in pairs, then in groups, build a few useful features, and write down what you found. Notice the dotted arrow looping back up. EDA is a circle, not a ladder. Every chart you draw can raise a new question that sends you back to cleaning or analysis. Follow this path and you will understand the dataset before you ever build a model or hand a number to your boss.

Prerequisites

This post assumes you have worked through the earlier Part 4 posts, especially the Pandas data cleaning tutorial, the descriptive statistics tutorial, and the Seaborn tutorial. You will need Pandas, NumPy, Matplotlib, and Seaborn installed. Everything below was run on Python 3.14.6 (latest stable at the time of writing) with Pandas 2.3.3 and Seaborn 0.13.2.

Step 1: Load and Inspect

Normally you start with pd.read_csv("yourfile.csv"). Here we build a fake but believable employee dataset in code so you can run every line yourself without downloading anything. The seed 42 makes the random numbers come out the same on your machine as on mine, so your output should match the numbers below exactly. The first thing you ever do with a new DataFrame is look at it from a few angles: shape, a few rows, the column types, and where the gaps are.

📄 eda_step1.py: first contact with your data

import pandas as pd
import numpy as np

# Build a realistic employee dataset (normally you would pd.read_csv instead)
rng = np.random.default_rng(42)
n = 500

experience = rng.integers(0, 12, n)

df = pd.DataFrame({
    "emp_id": range(1, n + 1),
    "name": [f"Employee_{i}" for i in range(1, n + 1)],
    "age": rng.integers(22, 34, n),
    "department": rng.choice(["Engineering", "Data Science", "Marketing", "Sales", "HR"], n, p=[0.35, 0.2, 0.15, 0.2, 0.1]),
    "experience": experience,
    # Salary genuinely grows with experience, plus base pay and random noise
    "salary": (52000 + experience * 2600 + rng.normal(0, 9000, n)).astype(int),
    "performance_score": rng.choice([1, 2, 3, 4, 5], n, p=[0.05, 0.15, 0.35, 0.30, 0.15]),
    "remote": rng.choice([True, False], n, p=[0.6, 0.4]),
})

# Inject some messiness: a few missing values and a handful of high earners
df.loc[rng.choice(n, 15, replace=False), "salary"] = np.nan
df.loc[rng.choice(n, 8, replace=False), "age"] = np.nan
df.loc[[10, 120, 250, 480], "salary"] = [142000, 138000, 155000, 134000]

# First contact: always run these four
print(f"Shape: {df.shape}")
print(f"\nHead:\n{df.head().to_string()}\n")
print("Info:")
df.info()
print(f"\nMissing values:\n{df.isnull().sum()}")

▶ Output

Shape: (500, 8)

Head:
   emp_id        name   age    department  experience   salary  performance_score  remote
0       1  Employee_1  23.0         Sales           1  47920.0                  3    True
1       2  Employee_2  29.0         Sales           9  83719.0                  3    True
2       3  Employee_3  33.0            HR           7  70511.0                  4   False
3       4  Employee_4  32.0  Data Science           5  62454.0                  2    True
4       5  Employee_5  25.0  Data Science           5  64044.0                  2    True

Info:
<class 'pandas.DataFrame'>
RangeIndex: 500 entries, 0 to 499
Data columns (total 8 columns):
 #   Column             Non-Null Count  Dtype
---  ------             --------------  -----
 0   emp_id             500 non-null    int64
 1   name               500 non-null    str
 2   age                492 non-null    float64
 3   department         500 non-null    str
 4   experience         500 non-null    int64
 5   salary             485 non-null    float64
 6   performance_score  500 non-null    int64
 7   remote             500 non-null    bool
dtypes: bool(1), float64(2), int64(3), str(2)
memory usage: 38.0 KB

Missing values:
emp_id                0
name                  0
age                   8
department            0
experience            0
salary               15
performance_score     0
remote                0
dtype: int64

What happened here: Four quick commands and you already know a lot. df.shape says 500 rows and 8 columns. df.head() shows the first five rows so you can eyeball whether the data looks sane (we chain .to_string() so every column prints in full instead of getting squeezed into a ... when the frame is wide). df.info() lists each column with its type and how many non-null values it has, and df.isnull().sum() counts the gaps: 8 missing ages and 15 missing salaries.

Spot the surprise in the age column: it reads 23.0, not 23. The moment a single NaN lands in an integer column, Pandas quietly promotes the whole column to float64, because NaN is technically a float. That tiny detail trips up beginners constantly. One more thing worth noticing: text columns show up as str rather than the old object, because Pandas 2.3.3 gives real strings their own dtype.

Step 2: Clean

Missing values are like blank spots on a paper form. You cannot just ignore them, because most analysis and every model will choke on a gap. The simplest honest fix for a numeric column is to fill the blanks with the median, which is the middle value and does not get dragged around by extreme numbers the way the average does. Then we hunt for outliers, the salaries that sit far away from everyone else.

📄 eda_step2.py: handle missing values and find outliers

# Fill numeric missing values with the median (robust to extreme values)
df["age"] = df["age"].fillna(df["age"].median())
df["salary"] = df["salary"].fillna(df["salary"].median())

print(f"After cleaning: {df.isnull().sum().sum()} missing values")
print(f"Salary range: ${df['salary'].min():,.0f} to ${df['salary'].max():,.0f}")

# Find outliers with the IQR (interquartile range) rule
Q1 = df["salary"].quantile(0.25)
Q3 = df["salary"].quantile(0.75)
IQR = Q3 - Q1
outliers = df[(df["salary"] < Q1 - 1.5 * IQR) | (df["salary"] > Q3 + 1.5 * IQR)]
print(f"Salary outliers: {len(outliers)} employees")

▶ Output

After cleaning: 0 missing values
Salary range: $26,196 to $155,000
Salary outliers: 6 employees

What happened here: After filling the gaps, df.isnull().sum().sum() is 0, so the whole frame is now complete. The IQR (interquartile range) rule is the standard trick for catching outliers. Q1 is the value below which a quarter of salaries fall, Q3 is the value below which three quarters fall, and the gap between them (the IQR) is the “normal middle” spread. Anything more than 1.5 times that gap below Q1 or above Q3 gets flagged.

We get 6 outliers, which makes sense: we deliberately planted a few very high earners (think senior leadership on 142k and up). In real work you do not delete outliers on sight. You ask why they are there. A 155k salary might be a genuine VP, or it might be a typo where someone added a zero. EDA is where you find out.

Step 3: Univariate Analysis

Think of this like meeting each member of a new team one at a time before you watch how they work together. “Univariate” just means “one variable at a time”. Before you look at how columns relate to each other, you look at each column on its own. What shape is the salary spread? How old is everyone? Which department is biggest? For numbers you reach for a histogram (a bar chart of how often each value range shows up), and for categories you reach for a count plot (how many rows fall in each bucket).

📄 eda_step3.py: study each variable on its own

import seaborn as sns
import matplotlib.pyplot as plt

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# Numeric columns: distributions
sns.histplot(data=df, x="salary", kde=True, ax=axes[0, 0])
axes[0, 0].set_title("Salary Distribution")

sns.histplot(data=df, x="age", kde=True, ax=axes[0, 1], color="orange")
axes[0, 1].set_title("Age Distribution")

# Categorical columns: counts
sns.countplot(data=df, x="department", ax=axes[1, 0], order=df["department"].value_counts().index)
axes[1, 0].set_title("Department Distribution")
axes[1, 0].tick_params(axis="x", rotation=30)

# Assign hue to color the bars (Seaborn 0.13 way), legend off since it would just repeat the x labels
sns.countplot(data=df, x="performance_score", hue="performance_score", legend=False, ax=axes[1, 1], palette="viridis")
axes[1, 1].set_title("Performance Score Distribution")

plt.tight_layout()
plt.savefig("eda_univariate.png", dpi=150)
plt.show()

What happened here: This builds a 2 by 2 grid of charts in one figure and saves it as eda_univariate.png. The top row holds two histograms (salary and age), where kde=True overlays a smooth curve so the shape is easier to read. The bottom row holds two count plots for the categorical columns. One small but important detail for Seaborn 0.13.2: if you pass a palette to color the bars, you must also set hue, otherwise Seaborn prints a FutureWarning.

We set hue="performance_score" and turn the legend off, because the legend would just repeat the labels already on the x axis. The salary histogram leans right with a long tail (those high earners), age is fairly flat between 22 and 33, Engineering is the tallest department bar, and performance scores pile up in the middle around 3.

Step 4: Bivariate Analysis

Now the fun part: how do columns relate to each other? This is like noticing that every time it rains, umbrella sales climb. You are checking whether two things tend to move together. “Bivariate” means two variables at a time. A scatter plot shows whether two numbers rise and fall together. A box plot compares one number across categories. And a correlation heatmap puts a number on every numeric pairing at once, from -1 (perfect opposite) through 0 (no link) to +1 (perfect match).

📄 eda_step4.py: relationships between pairs of variables

import seaborn as sns
import matplotlib.pyplot as plt

fig, axes = plt.subplots(1, 3, figsize=(18, 5))

# Scatter: does salary rise with experience?
sns.scatterplot(data=df, x="experience", y="salary", hue="department", ax=axes[0], alpha=0.6)
axes[0].set_title("Experience vs Salary")

# Box: how does salary spread across departments?
sns.boxplot(data=df, x="department", y="salary", ax=axes[1])
axes[1].set_title("Salary by Department")
axes[1].tick_params(axis="x", rotation=30)

# Heatmap: correlation between every numeric pair
numeric_cols = df.select_dtypes(include=[np.number])
sns.heatmap(numeric_cols.corr(), annot=True, cmap="coolwarm", center=0, ax=axes[2], fmt=".2f")
axes[2].set_title("Correlation Matrix")

plt.tight_layout()
plt.savefig("eda_bivariate.png", dpi=150)
plt.show()

# Print the correlation numbers so we can read them exactly
print("Correlation matrix:")
print(numeric_cols.corr().round(2))

▶ Output

Correlation matrix:
                   emp_id   age  experience  salary  performance_score
emp_id               1.00 -0.04       -0.02   -0.02               0.01
age                 -0.04  1.00       -0.00    0.02               0.08
experience          -0.02 -0.00        1.00    0.59              -0.03
salary              -0.02  0.02        0.59    1.00              -0.01
performance_score    0.01  0.08       -0.03   -0.01               1.00

What happened here: The heatmap printout is the headline. Read the diagonal first: every column correlates perfectly with itself, so it is always 1.00. The number that jumps out is experience versus salary at 0.59, a moderate positive link. People with more years on the job tend to earn more, exactly what the scatter plot shows as an upward drift. Everything else hovers near 0, which means those columns barely move together. That is genuinely useful: it tells you age and performance_score are not driving salary in this data, so if you later build a salary model, experience is the feature to lean on. select_dtypes(include=[np.number]) grabs only the numeric columns, since you cannot correlate text.

Step 5: Key Findings

The last step of any EDA is writing down what you learned in plain language. Think of the summary line at the top of a medical report: the reader wants the takeaways, not every raw measurement. Charts are great while you are exploring, but the people who read your report want a short list of takeaways. Printing the findings straight from the data (rather than typing numbers by hand) keeps your summary honest and saves you from copy paste mistakes.

📄 eda_findings.py: summarize the insights

print("=" * 50)
print("EDA KEY FINDINGS")
print("=" * 50)
print(f"1. Dataset: {len(df)} employees, {df.shape[1]} features")
print(f"2. Salary range: ${df['salary'].min():,.0f} - ${df['salary'].max():,.0f}")
print(f"3. Mean salary: ${df['salary'].mean():,.0f} (median: ${df['salary'].median():,.0f})")
print(f"4. Department distribution: Engineering dominates ({(df['department']=='Engineering').mean():.0%})")
print(f"5. Experience correlates with salary: r = {df['experience'].corr(df['salary']):.2f}")
print(f"6. {(df['remote']==True).mean():.0%} of employees work remotely")
print(f"7. Performance scores cluster around the middle (roughly bell shaped)")
print(f"8. {len(outliers)} salary outliers detected (IQR method)")

▶ Output

==================================================
EDA KEY FINDINGS
==================================================
1. Dataset: 500 employees, 8 features
2. Salary range: $26,196 - $155,000
3. Mean salary: $67,239 (median: $67,430)
4. Department distribution: Engineering dominates (37%)
5. Experience correlates with salary: r = 0.59
6. 64% of employees work remotely
7. Performance scores cluster around the middle (roughly bell shaped)
8. 6 salary outliers detected (IQR method)

What happened here: Eight plain sentences, every number pulled live from the DataFrame. Notice how close the mean ($67,239) and median ($67,430) salaries are. When those two sit near each other, the distribution is fairly balanced. When the mean races far ahead of the median, you have a long right tail, often from a few very high earners. The standout finding is line 5: experience and salary move together at r = 0.59. That single sentence is worth more to whoever models this data than any chart, because it tells them where the signal lives.

The Catch: Correlation Is Not Causation

Here is the one mistake that sinks more data projects than any bug. You find a strong correlation and you announce a cause. Our data shows experience and salary correlate at 0.59, and in this case we know experience really does drive salary, because we built the data that way. But out in the wild, a correlation only tells you two numbers move together. It never tells you which one pushes the other, or whether some third thing is pushing both.

The classic example: ice cream sales and drowning deaths rise together almost every summer. Strong correlation. Does ice cream cause drowning? Of course not. Hot weather is the hidden third factor that drives both, more ice cream and more swimming on the same days. A correlation heatmap is brilliant for spotting which columns to investigate, but treat every strong number as a question (“why are these linked?”), never as an answer.

So when an analyst named Viraj reports “experience drives salary, r = 0.59” to the team, the honest version is “experience and salary are strongly linked in our data, and we should test whether experience explains pay once we account for department and performance.” EDA hands you the leads. Proving cause needs a controlled comparison or an experiment, which comes much later.

Practice Exercises

  1. Exercise 1: Add a salary_per_year_experience column (salary / (experience + 1) to avoid dividing by zero) and check which department gives the best pay for the least experience.
  2. Exercise 2: Download a real CSV from Kaggle, run the exact same five steps on it, and write your own eight line findings summary.
  3. Exercise 3: Replace the median fill in Step 2 with a group median (fill each missing salary using the median of that person’s department) and see how the correlation changes.

Conclusion

You just walked the full exploratory data analysis workflow on a realistic dataset: load and inspect, clean the missing values and outliers, study each column on its own, then in pairs, write down the findings in plain language, and stay honest about correlation versus causation. That loop is the whole difference between trusting your data and getting fooled by it, like Niranjan’s 96% model that was really just reading the answer key. The habit to keep is simple: never build on a dataset you have not walked through first.

Next up in Part 5, you take findings exactly like these into a full end to end data science project, turning raw data into insights and a working model. To see how this post fits the bigger picture and to keep learning for free, head to the Python + AI/ML tutorial series home.

Frequently Asked Questions

What is exploratory data analysis in Python?

Exploratory data analysis (EDA) in Python is the workflow of getting to know a dataset before modeling it. You load the data, inspect its shape and types, clean missing values and outliers, study each column on its own and in pairs, and write down what you found. The usual tools are Pandas for the data, plus Matplotlib and Seaborn for the charts.

How long should EDA take?

For a tidy Kaggle dataset, 30 to 60 minutes. For a messy real business dataset with quality issues, several hours to a full day. The time pays for itself. Skipping EDA and jumping straight to modeling almost always costs more time later, debugging models that learned the wrong thing.

Should I automate EDA?

For a first pass, automated tools like ydata-profiling, sweetviz, or D-Tale generate a full EDA report in one line. Use them as a starting point, then dig into anything interesting by hand. Automated EDA catches the obvious stuff. Manual EDA finds the subtle insights that actually matter.

Does a high correlation mean one variable causes the other?

No. A correlation only says two columns move together. It never proves one causes the other, and a hidden third factor can drive both (the classic case is ice cream sales and drowning deaths, both driven by hot weather). Use a correlation heatmap to decide what to investigate, then test causation with a controlled comparison or experiment.

What is the final output of EDA?

A short, documented list of findings: data quality issues, the key distributions, the relationships between variables, candidate features for modeling, and open questions for domain experts. That list feeds directly into feature engineering and model selection in Part 5.

Interview Questions on Exploratory Data Analysis

The same ideas as they show up in real interviews, framed as scenarios you can practice out loud.

Q: Walk me through the steps of an EDA workflow.

Load the data and inspect its shape, types, and missing values with df.shape, df.head(), df.info(), and df.isnull().sum(). Clean the gaps and check for outliers, usually with a median fill and the IQR rule. Then run univariate analysis (each column alone), bivariate analysis (pairs, including a correlation heatmap), and optionally multivariate analysis, before writing a short documented list of findings. EDA is a loop, not a straight line: any chart can send you back to cleaning.

Q: Why did an integer column turn into float64 after you loaded the data?

Because it contains at least one missing value. In Pandas, NaN is technically a float, so the moment a gap lands in an integer column the whole column is promoted to float64 and the values print as 23.0 instead of 23. This trips up beginners constantly. If you need to keep whole numbers, you can use the nullable Int64 dtype, which supports missing values without switching to float.

Q: How does the IQR rule flag outliers, and why 1.5?

Q1 is the 25th percentile and Q3 is the 75th percentile, so the IQR (Q3 minus Q1) is the spread of the middle half of the data. Anything below Q1 minus 1.5 times the IQR or above Q3 plus 1.5 times the IQR is flagged. The 1.5 multiplier is a widely used convention that catches genuinely unusual points without flagging normal variation. Flagging is not deleting: you investigate each outlier to decide whether it is a real value or a data error.

Q: What is the difference between univariate, bivariate, and multivariate analysis?

Univariate looks at one variable at a time, for example a histogram of salary or a count plot of departments. Bivariate looks at two variables together, such as a scatter of experience versus salary or a correlation heatmap. Multivariate looks at three or more at once, for example a heatmap combined with a hue dimension or a pairplot, to spot interactions that only show up when several columns are considered together.

Q: Your correlation heatmap shows one feature at 0.98 with the target and your model hits 99% accuracy on the first try. What do you check first?

Suspect data leakage before celebrating. A near perfect correlation and suspiciously high accuracy usually mean the feature is the answer in disguise, like a days_since_last_login column that is only large because the customer already churned. Check when the feature is recorded relative to the target: if it is only known after the outcome, it cannot be used for prediction. Remove or reconstruct it so the model learns real signal instead of reading the answer key.

Q: A salary column has missing values and a handful of very high earners. How do you fill the gaps and why?

Fill with the median rather than the mean. The median is the middle value and is robust to extreme numbers, so a few 150k salaries will not drag the fill value upward the way they would drag the mean. An even better version fills by group, for example the median salary within each department, so the imputed value matches the person’s context. Whatever you choose, record it as a data quality note in your findings.

Q: You report a strong correlation to a stakeholder. How do you phrase it responsibly?

State it as a link, not a cause. Instead of “experience drives salary”, say “experience and salary are strongly linked in our data, r = 0.59, and we should test whether it holds once we account for department and performance.” A correlation never proves direction, and a hidden third factor can drive both variables. EDA hands you leads to investigate; proving cause needs a controlled comparison or an experiment.

Go deeper: when you outgrow this post, the official Python documentation is the next stop.

Previous: Python: Matplotlib vs Seaborn vs Plotly, Visualization Libraries Compared

Next: Data Science: End-to-End Project, Raw Data to Insights

Series Home: Python + AI/ML Tutorial Series

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *