Python Data Visualization, SQL Connectivity & EDA
Back to THE SYSTEMATIC EXPLORATORY DATA ANALYSIS (EDA) WORKFLOW

The 6-Step Systematic EDA Blueprint & Outlier Detection

The complete analyst framework for structural audits, missingness checks, IQR outlier formulas, and bivariate analysis.

9 minutesReading
๐ŸŽฏ Free Guest Mode: You are learning for free. Sign in to save your completion progress and quiz answers.

The 6-Step EDA Blueprint: From Messy Table to Trustworthy Insights

You open the warehouse extract for the first time โ€” 280 rows, 6 columns, 3 months of orders โ€” and nothing looks clean. Dates are strings, one product category has a typo, two rows are exact duplicates, and a $12,000 "Office Supplies" order is clearly a data-entry error. Do you fix it by gut feel? No. You run 6 checks in order, like a pilot's pre-flight checklist. Skip one and the chart you send to leadership is wrong. Run all six and every chart after it is trustworthy. We will run the whole checklist on a tiny 10-row sample you can trace by hand โ€” then the same code scales to 280 rows and to 280,000.

0. The Messy Sample We Will Clean Together

This is what pd.read_sql_query("SELECT * FROM orders", engine) might return before any cleaning โ€” deliberately messy:

idcustomer_idtransaction_dateproduct_categoryquantityunit_pricetotal_amount
1001C0012024-06-22Electronics1899.00899.00
1002C0022024-02-12Furniture580.00400.00
1003C0022024-02-12Furniture580.00400.00
1004โ€”2024-03-18NULL2250.00500.00
1005C0032024-04-01Office Supplies4120.00480.00
1006C0042024-06-22Office Supplies80150.0012000.00
1007C0012024-06-28Apparel345.00135.00
1008C0052024-02-12Electronics11200.001200.00

Spot the issues: row 1002/1003 duplicate, row 1004 missing customer + category (yellow), row 1006 quantity 80 ร— $150 should be $12,000 but the unit price is clearly off by 10ร— โ€” that 12000 is an outlier (red). Each step below catches one of them.

Step 1: Structural Audit โ€” What Shape Is the Data?

Before any fix, ask: how big, what types, what memory? This is the inventory check. You catch that transaction_date arrived as object (string) not datetime, and that total_amount should equal quantity ร— unit_price but doesn't for row 1006.

print(df.shape)          # (10, 7)  โ€” rows, columns
print(df.info())         # dtypes + non-null counts โ€” date shows as object!
print(df.dtypes)         # quick type scan
print(df.memory_usage(deep=True).sum() / 1024, "KB")

Output tells you: 10 rows ร— 7 columns, transaction_date object needs parsing, and df['total_amount'] != df['quantity']*df['unit_price'] for at least one row โ€” flag it.

Step 2: Missing Data & Hygiene โ€” Where Are the Holes?

One missing value can silently drop a whole group from a groupby. Quantify holes as percent per column, sorted worst first โ€” then decide: drop, fill, or investigate.

# Percent missing per column, worst first
missing = (df.isnull().sum() / len(df) * 100).sort_values(ascending=False)
print(missing)
# customer_id         10.0   โ† row 1004
# product_category    10.0   โ† row 1004
# transaction_date     0.0
# ...

# Hygiene fixes for the sample:
df = df.drop_duplicates()                          # removes duplicate 1002/1003 โ†’ 9 rows
df = df.dropna(subset=['customer_id'])             # drop row with no customer (or fill with 'UNKNOWN')
print(f"After dedup + dropna: {df.shape[0]} rows")  # 8 rows

This is Quiz 3 Q06 โ€” that exact chained expression is the answer. Keep it as your first hygiene line in every notebook.

Step 3: Univariate Inspection โ€” What Does Each Column Look Like Alone?

Run one summary per column type and read it like a story โ€” not a dump.

print(df.describe())              # numeric: count / mean / std / min / Q1 / median / Q3 / max
print(df['product_category'].value_counts())  # categorical: how many per category
print(df['order_amount'].value_counts(bins=5)) # quick histogram in text

# Skewness check: is the distribution's tail on the right?
# If mean >> median, the mean is being pulled by a few huge orders.
print(f"mean={df['total_amount'].mean():.0f}  median={df['total_amount'].median():.0f}")
# mean=589  median=440  โ†’ mean > median โ†’ right-skewed (Quiz 3 Q07, Q14)
# Rule: for skewed spend, report median as "typical" โ€” mean misleads.

Right-skew vs left-skew: Right-skew = long tail to large values (most customers spend $100, a few spend $12,000 โ†’ tail to the right, positive skew). Left-skew is the mirror. The $12,000 order in row 1006 is what creates the right tail here.

Step 4: Outlier Detection โ€” Tukey's IQR Rule (and When to Cap, Not Drop)

An outlier is not "a big number" โ€” it is a value far from the middle 50% of the data. Tukey's rule formalizes that using the interquartile range:

# Picture 10 spend values sorted: [45, 90, 135, 400, 440, 480, 500, 899, 1200, 12000]
Q1 = df['total_amount'].quantile(0.25)   # 25th percentile โ€” 25% of orders are below this
Q3 = df['total_amount'].quantile(0.75)   # 75th percentile โ€” 75% are below this
IQR = Q3 - Q1                            # middle 50% spread

lower_bound = Q1 - 1.5 * IQR             # anything below this is an outlier
upper_bound = Q3 + 1.5 * IQR             # anything above this is an outlier

outliers = df[(df['total_amount'] < lower_bound) | (df['total_amount'] > upper_bound)]
print(f"Q1={Q1:.0f} Q3={Q3:.0f} IQR={IQR:.0f} โ†’ bounds [{lower_bound:.0f}, {upper_bound:.0f}]")
print(outliers[['customer_id','total_amount']])
#    customer_id  total_amount
# 5         C004         12000   โ† the $12K entry โ€” flagged correctly

# Two ways to handle it โ€” pick based on the business question:
# Option A: filter out (row disappears) โ€” for "typical order" analysis
clean = df[(df['total_amount'] >= lower_bound) & (df['total_amount'] <= upper_bound)]

# Option B: cap to the bound (row stays, value trimmed) โ€” for "keep every customer" reports
df['total_amount_capped'] = df['total_amount'].clip(lower=lower_bound, upper=upper_bound)
# C004's 12000 becomes  upper_bound  (e.g. 1420) โ€” preserves row count, tames the tail

This is Quiz 3 Q02 (IQR = Q3โˆ’Q1), Q03 (upper = Q3+1.5ยทIQR), and Q09 (clip()). The boxplot from Module 2 draws these same bounds visually โ€” the dots beyond the whiskers are these outliers.

Step 5: Bivariate & Categorical Relationships โ€” How Do Columns Relate?

One column tells you "what". Two columns tell you "why". Two quick checks:

# Categorical ร— numeric: does spend differ by product category?
print(pd.crosstab(df['product_category'], df['customer_tier']))   # counts
print(df.groupby('product_category')['total_amount'].median())    # vs median spend

# Visual: one box per category โ€” same IQR logic as Step 4, now split by group
import seaborn as sns
sns.boxplot(data=df, x='product_category', y='total_amount')
# Apparel median $135, Electronics median ~$899 โ€” very different shapes

If "Office Supplies" median suddenly jumps because of the $12K row and you haven't handled Step 4 yet, the bivariate chart lies. Order matters โ€” outliers first, comparisons second.

Step 6: Temporal Patterns โ€” When Does the Business Peak?

Dates arrived as strings ("2024-06-22"). Until parsed, you cannot ask "which month sold most?" or "what hour had the most orders?"

df['transaction_date'] = pd.to_datetime(df['transaction_date'])  # now datetime64

df['month']    = df['transaction_date'].dt.month       # 2, 4, 6
df['weekday']  = df['transaction_date'].dt.day_name()  # Monday, Saturday
df['hour']     = df['transaction_date'].dt.hour        # if timestamps include time

print(df.groupby('month')['total_amount'].sum())        # revenue per month
print(df.groupby('weekday')['total_amount'].mean())     # avg spend per weekday
# The .dt accessor unlocks: .year .month .day .day_name() .hour .quarter .is_month_end

This powers Quiz 3 Q13 and the notebook's Task 2. It also feeds Step 5 โ€” groupby month/hour then bar/line chart, exactly as the Sales EDA video does after this blueprint.

Checklist before any chart: shape + types โ†’ missing % โ†’ describe/median โ†’ IQR + clip decision โ†’ crosstab/boxplot โ†’ parse dates + groupby time. Run it in this order. The notebook Tasks 1โ€“4 follow exactly this sequence for 280 rows.

Loading progress...