Exploratory Data Analysis (EDA) is the process of getting to know a dataset before modelling it: checking shapes and types, hunting for missing values and outliers, and asking the dataset questions through summary statistics and plots. In this chapter we run three realistic case studies end-to-end so the cleaning and visualization skills from earlier chapters become muscle memory. Treat each one as a recipe you can apply to any new dataset you meet.
The classic Red Wine Quality dataset contains about 1,600 bottles of red wine, each described by 11 chemical features (fixed acidity, volatile acidity, citric acid, residual sugar, chlorides, free and total sulfur dioxide, density, pH, sulphates, alcohol) plus a quality score from 0 to 10.
Start one column at a time. Check each feature's data type, count of missing values, mean, median, and spread, then plot histograms. Two findings stand out immediately in this dataset: density is nearly constant (its histogram is a razor-thin spike) while residual sugar is heavily right-skewed. No values are missing — the dataset is clean — but the quality score itself is skewed toward 5 and 6, with very few wines scoring below 3 or above 8.
import pandas as pd
df = pd.read_csv("winequality-red.csv")
print(df.info())
print(df.describe().T)
df.hist(figsize=(12, 10), bins=30)
Next, relationships between features. A correlation heatmap reveals that alcohol correlates positively with quality, while volatile acidity and density correlate negatively with it. Sulfur dioxide features correlate strongly with each other (a redundancy to remember for modelling). Pairplots or scatter plots confirm whether these correlations are linear and whether any outliers are driving them.
Finally, relate everything to the target. Group the data by quality score and compare the average of each feature; a table of group means quickly shows which chemistry separates good wine from bad. A common modelling trick for this dataset is to convert quality into a binary target (“good” if score ≥ 7, “not good” otherwise), which gives a clean classification problem instead of an ordinal regression one.
Flight-price datasets (with columns like airline, source and destination cities, departure and arrival times, total stops, and price) are dirtier than the wine data: strings, timestamps, and clear interaction effects.
Load the data and inspect. The price column is numeric; check its distribution for a long right tail and note that a log transform may help later. Airline, source, and destination are categorical — decide which encoding to use (one-hot for low-cardinality columns).
The raw columns hide most of the signal, so we engineer new ones:
def parse_time(t):
h, m = t.split(":")
return int(h) + int(m) / 60
df["dep_hours"] = df["dep_time"].apply(parse_time)
df["arr_hours"] = df["arr_time"].apply(parse_time)
df["duration"] = (df["arr_hours"] - df["dep_hours"]) % 24
Cross-tabulate price against airline and stops, and box-plot price by day of week. These two visual checks immediately confirm which engineered features carry signal. Check for duplicate rows (identical flights listed twice are common in scraped data) and drop them before modelling.
The Google Play Store Apps dataset is a favourite for practising real-world cleaning because it is messier than the wine data and gets messier in the ways real data does: mixed types, units inside numbers, and a corrupted-looking price column.
The dataset holds app name, category, rating, reviews count, size, installs, price, content rating, and genres. The first sweep reveals the traps:
def clean_installs(s):
s = str(s).replace("+", "").replace(",", "")
if s == "0" or s == "Free":
return 0
return int(s)
df["installs_clean"] = df["installs"].apply(clean_installs)
installs to a clean numeric column with the function above; keep the categorical “bucket” version too if you want both views.df.dtypes and re-check missing-value counts.With clean columns, the analysis can finally happen. Now we answer real questions with plots and tables.
Most apps sit between 4.0 and 4.5 stars, and the rating distribution has a strong left tail of poorly rated apps. Log-scale the reviews count — a few blockbuster apps dominate, and the skew is dramatic.
Count apps per category with a bar chart: FAMILY, GAME, and TOOLS typically dominate, while a long tail of categories each hold a handful of apps. Then explore whether price differs by category — game apps skew free with a few premium outliers, while medical and business categories have more paid apps.
Scatter plots of size vs rating and installs vs rating usually show no strong linear trend — quality is only weakly tied to size or popularity. The big insight for any later model is that installs and reviews are heavily skewed, so a log transform will matter more than any single engineered column.
Cross-tab content rating against average rating, and compare the average installs for “Everyone” versus “Mature 17+” apps. Genres overlap categories, so inspect unique genre strings before deciding whether to keep both columns.
import seaborn as sns
sns.histplot(df_clean["rating"], bins=30)
sns.boxplot(data=df_clean, x="category", y="rating")
sns.scatterplot(data=df_clean, x="size_mb", y="installs_clean", alpha=0.3)