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.

By CrunchCubeUpdated 21 October 20265 min read

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    float64

info() 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())  # 0
unit_price    12
revenue       12

Twelve 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.4

Compare 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.18

Over 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.1

Home 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.8

Marketplace orders carry nearly three times the discount. Another note for the findings.

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.0

May 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.

Common mistake

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.00

Revenue 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:

  1. Electronics drives revenue: 60% of revenue from 29% of orders. Protect stock and visibility.
  2. Marketplace orders get 20% average discount versus about 8% elsewhere, with no sign of larger orders. Review marketplace pricing.
  3. May revenue fell about 30% from April while orders stayed flat. Investigate the category mix that month.
  4. 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.