Exploratory Data Analysis in Python: A Repeatable Workflow from CSV to Insights
A 7-step EDA workflow in pandas for any CSV: quality checks, single columns, group comparisons, trends, correlations and clear findings.
Exploratory data analysis often begins with a few familiar commands: head(), describe(), maybe a histogram. Those commands are useful, but they're not a workflow. Run them in a random order and you'll miss the duplicate rows, or spend an hour charting a column full of errors.
Here's a repeatable seven-step routine. Use it on any CSV and you'll go from "I have a file" to "here's what matters" without skipping the boring checks that save you later.
The dataset
Six months of online orders: channel, category, quantity, price, discount and revenue. Download orders.csv to follow along, or use your own file with similar columns. The steps don't change.
Think of EDA like a new coach watching a team for the first time. You don't start by changing tactics. You watch: who's fit, who's injured, who plays where, what happens in the second half. Then you decide.
Step 1: First look
import pandas as pd
df = pd.read_csv("orders.csv", parse_dates=["order_date"])
print(df.shape) # (600, 8)
df.info() # Column Non-Null Count Dtype
--- ------ -------------- -----
0 order_id 600 non-null int64
1 order_date 600 non-null datetime64[us]
2 channel 600 non-null str
3 category 600 non-null str
4 quantity 600 non-null int64
5 unit_price 588 non-null float64
6 discount_pct 600 non-null int64
7 revenue 588 non-null float64info() answers three questions at once: how many rows, what type each column is, and how many values are present. Already we can see unit_price and revenue have 588 values out of 600.
parse_dates matters. Without it, dates load as text and you can't group by month later.
Step 2: Data quality checks
missing = df.isna().sum()
print(missing[missing > 0])
print(df.duplicated("order_id").sum()) # 0unit_price 12
revenue 12Twelve orders have no price, and therefore no revenue. Write that down: every revenue number you report is based on 588 orders, not 600. No duplicate order IDs, good.
If this step finds problems, stop and fix them first. Here's a full guide to cleaning missing values, duplicates and wrong types.
Step 3: One column at a time
Numbers first:
print(df[["quantity", "unit_price", "discount_pct", "revenue"]].describe().round(1)) quantity unit_price discount_pct revenue
mean 2.4 1190.6 9.8 2633.0
std 1.1 928.2 6.8 2619.2
min 1.0 212.0 0.0 189.4
50% 2.0 856.0 10.0 1656.9
max 4.0 3486.0 30.0 13359.4Compare the mean and the median (50%). Revenue's mean (2,633) is far above its median (1,657). That means a few large orders pull the average up. In a report, the median is the more honest "typical order".
Then categories:
print(df["channel"].value_counts(normalize=True).round(2))Website 0.54
App 0.28
Marketplace 0.18Over half of orders come through the website. normalize=True gives shares instead of counts, which are easier to talk about.
Step 4: Compare groups
This is where insights usually appear. Which categories actually make the money?
summary = (df.groupby("category")
.agg(orders=("order_id", "count"),
revenue=("revenue", "sum"),
avg_order=("revenue", "mean"))
.sort_values("revenue", ascending=False)
.round(0))
summary["share_%"] = (summary["revenue"] / summary["revenue"].sum() * 100).round(1)
print(summary) orders revenue avg_order share_%
category
Electronics 173 933463.0 5590.0 60.3
Home 255 489722.0 1936.0 31.6
Books 172 125000.0 744.0 8.1Home has the most orders, but Electronics brings 60% of the revenue with fewer orders. That's a finding a manager can use: a marketing push on Home increases order volume, while Electronics moves the revenue line.
Now a second comparison:
print(df.groupby("channel")["discount_pct"].mean().round(1))App 7.3
Marketplace 20.0
Website 7.8Marketplace orders carry nearly three times the discount. Another note for the findings.
Step 5: Trends over time
monthly = (df.set_index("order_date")
.resample("MS")
.agg(orders=("order_id", "count"), revenue=("revenue", "sum"))
.round(0))
print(monthly) orders revenue
order_date
2026-01-01 103 285032.0
2026-02-01 90 233555.0
2026-03-01 104 304937.0
2026-04-01 110 262243.0
2026-05-01 95 182780.0
2026-06-01 98 279638.0May looks bad: revenue is about 30% below April. Before raising an alarm, look at the order count. It barely moved (95 orders, close to normal). So May didn't lose customers; it lost order value. The next question writes itself: did May sell fewer Electronics? That's a group comparison (Step 4) inside a time slice.
"MS" means "month start", so each row is labelled with the first day of the month.
Reporting a dip as a trend. With a few hundred orders a month, some months will be low by chance. Check the order count and the mix before you explain it, and look for a pattern across several months before calling it a trend.
Step 6: Relationships between numbers
print(df[["quantity", "unit_price", "discount_pct", "revenue"]].corr().round(2)) quantity unit_price discount_pct revenue
quantity 1.00 0.02 0.03 0.47
unit_price 0.02 1.00 -0.03 0.81
discount_pct 0.03 -0.03 1.00 -0.07
revenue 0.47 0.81 -0.07 1.00Revenue follows unit price (0.81) far more than quantity (0.47). Bigger discounts don't come with bigger orders (−0.07), which raises a fair question about whether the marketplace discounts are buying any extra sales.
Correlation is a hint, not a verdict. Read correlation vs causation before you turn any of these into a recommendation.
Step 7: Write the findings
EDA isn't finished until someone else can act on it. Write short findings, each with its evidence:
- Electronics drives revenue: 60% of revenue from 29% of orders. Protect stock and visibility.
- Marketplace orders get 20% average discount versus about 8% elsewhere, with no sign of larger orders. Review marketplace pricing.
- May revenue fell about 30% from April while orders stayed flat. Investigate the category mix that month.
- Data note: 12 of 600 orders (2%) have no price and are excluded from revenue.
Four lines, each with a number and a suggested next step. That's what a manager reads.
Make it repeatable
Put steps 1 to 6 in a notebook or a function and run it on every new extract. Add charts where they help (see Matplotlib vs Seaborn vs Plotly), and when the same questions come up every week, turn them into a Streamlit dashboard.
In one line: look, check quality, study single columns, compare groups, follow time, test relationships, and write findings with evidence, in that order, every time.
Keep reading
New games, tools and tutorials
About one short email a week. No spam, one-click unsubscribe.