EDA Case Study: Nigerian E-commerce
The Brief
You have just joined the data team at ShopNaija, a fictional Nigerian e-commerce marketplace similar to Jumia or Konga. The Head of Growth sends you this message:
"Revenue grew last year, but margins are thin and our cancellation rate feels high. Can you dig into the 2024 orders and tell me where we are winning, where we are losing money, and what we should look at first?"
This is a typical EDA assignment. There is no model to build yet: your job is to understand the business through its data and come back with clear, prioritised findings. Work through every step in your own notebook.
Open in Google Colab: ShopNaija Case Study
Open a new notebook, paste the dataset generator below into the first cell, and follow the seven steps. At the end you will have a complete EDA notebook for your portfolio.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_theme(style="whitegrid")
rng = np.random.default_rng(42)
n = 20000
# More orders in November (Black Friday) and December (festive season)
month_weights = np.array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1.1, 2.0, 1.5])
months = rng.choice(np.arange(1, 13), n, p=month_weights / month_weights.sum())
dates = pd.to_datetime(pd.DataFrame({"year": 2024, "month": months, "day": rng.integers(1, 29, n)}))
category = rng.choice(["Phones", "Fashion", "Groceries", "Electronics", "Beauty", "Home"], n, p=[.18, .24, .2, .1, .14, .14])
base_price = {"Phones": 180000, "Fashion": 18000, "Groceries": 9000, "Electronics": 250000, "Beauty": 12000, "Home": 35000}
state = rng.choice(["Lagos", "Abuja", "Rivers", "Oyo", "Kano", "Enugu", "Kaduna"], n, p=[.38, .17, .1, .1, .1, .08, .07])
payment = rng.choice(["Card", "Transfer", "Pay on Delivery", "Wallet"], n, p=[.3, .25, .35, .1])
price = np.array([base_price[c] for c in category]) * rng.lognormal(0, 0.45, n)
qty = rng.integers(1, 4, n)
cancel_p = np.where(payment == "Pay on Delivery", 0.22, 0.06) + np.where(np.isin(state, ["Kano", "Kaduna"]), 0.05, 0)
cancelled = rng.random(n) < cancel_p
delivery_days = np.clip(rng.normal(np.where(state == "Lagos", 2, 5), 1.5), 1, None).round()
orders = pd.DataFrame({
"order_id": np.arange(100000, 100000 + n), "order_date": dates, "category": category,
"state": state, "payment_method": payment, "unit_price": price.round(-1), "quantity": qty,
"delivery_days": delivery_days, "cancelled": cancelled,
})
orders.loc[rng.choice(n, 150, replace=False), "delivery_days"] = np.nan # some gaps to fill
orders = pd.concat([orders, orders.sample(60, random_state=1)]) # some duplicates to clean
orders.shape
Step 1: Understand the Structure
orders.info()
orders.describe()
orders.head()
Write down: how many rows, which columns, which types, and what one row represents (one order).
Step 2: Clean
print("Duplicates:", orders.duplicated(subset="order_id").sum())
orders = orders.drop_duplicates(subset="order_id")
print(orders.isnull().sum())
orders["delivery_days"] = orders["delivery_days"].fillna(
orders.groupby("state")["delivery_days"].transform("median")
)
orders["revenue"] = orders["unit_price"] * orders["quantity"]
orders["month"] = orders["order_date"].dt.to_period("M")
completed = orders[~orders["cancelled"]]
Notice that missing delivery times are filled with the median for that state, not the overall median. Lagos deliveries are much faster, so a single global median would be wrong for most rows.
Step 3: The Headline Numbers
print("Orders:", len(orders))
print("Completed revenue (N bn):", round(completed["revenue"].sum() / 1e9, 2))
print("Average order value (N):", round(completed["revenue"].mean()))
print("Cancellation rate:", round(orders["cancelled"].mean() * 100, 1), "%")
Every EDA report should open with three to five headline KPIs.
Step 4: Trends Over Time
monthly = completed.groupby("month")["revenue"].sum() / 1e6
monthly.plot(kind="line", marker="o", figsize=(10, 4), title="Monthly completed revenue (N million)")
plt.show()
What to look for: the November spike (Black Friday sales) and a strong December. Ask the follow-up question: did November revenue grow because of more orders or bigger orders? Split it:
completed.groupby("month").agg(orders=("order_id", "count"), aov=("revenue", "mean"))
Step 5: Where Does the Money Come From?
by_cat = completed.groupby("category").agg(
revenue=("revenue", "sum"), orders=("order_id", "count"), aov=("revenue", "mean")
).sort_values("revenue", ascending=False)
by_cat["revenue_share"] = (by_cat["revenue"] / by_cat["revenue"].sum() * 100).round(1)
by_cat
You should find that Phones and Electronics drive most of the revenue with relatively few orders, while Fashion and Groceries drive order volume. These are different businesses with different strategies.
Step 6: Where Are We Losing?
cancel = orders.pivot_table(values="cancelled", index="state", columns="payment_method", aggfunc="mean")
sns.heatmap(cancel, annot=True, fmt=".0%", cmap="Reds")
plt.title("Cancellation rate by state and payment method")
plt.show()
orders.groupby("payment_method")["cancelled"].mean().sort_values()
Pay on Delivery has a cancellation rate several times higher than prepaid methods, and some northern states are higher again. Each cancelled order still costs money for logistics, so this is where margin is leaking.
sns.boxplot(data=completed, x="state", y="delivery_days")
plt.title("Delivery time by state")
plt.show()
Step 7: Write the Findings
The final and most important step. Turn your charts into a short summary for the Head of Growth:
- Revenue is concentrated. Phones and Electronics produce most of the revenue from a minority of orders. Protecting stock availability and delivery reliability in those categories matters most.
- Pay on Delivery is the biggest leak. Its cancellation rate is several times that of card payments. Test a small discount or free delivery for prepaid orders.
- November drives the year. Plan inventory and logistics capacity for Black Friday at least two months ahead.
- Delivery outside Lagos is slow. Longer delivery times outside Lagos may be driving cancellations, which is worth testing with a regional fulfilment partner.
Notice the language: "test" and "may be". EDA reveals associations, and experiments confirm causes.
What You Practised
Loading, cleaning with segment-aware imputation, KPIs, time trends, segment analysis, pivot heatmaps and writing business findings: the complete EDA loop. Save the notebook to GitHub. It is a genuine portfolio piece.
Try it yourself
Key Takeaways
- An EDA project follows a repeatable loop: understand the structure, clean, compute KPIs, analyse trends, segment, find leaks, and write findings.
- Use segment-aware imputation, such as a per-state median with groupby().transform(), when a variable differs strongly across groups.
- Decompose headline metrics (revenue equals orders times average order value) to understand what really drove a change.
- Pivot heatmaps and segment comparisons show where the business is losing money, such as cancellations by payment method and region.
- Findings should be prioritised, business-focused and carefully worded: EDA shows associations, and experiments confirm causes.
Quick Quiz
1.Why were missing delivery_days values filled with the median for each state rather than the overall median?
2.November revenue nearly doubles. What is the best follow-up analysis?
3.Why does the findings section say 'test incentives for prepaid orders' rather than 'prepaid incentives will cut cancellations'?
Ready to go further?
CareerEx gives you structured 12-week training, live classes every Saturday and Sunday, real tutor feedback, and a certificate. Join the next cohort.
Join CareerEx