📊 Section 3 · Data Viz 🟠 Project MODULE 18

Project 2 — Sales Dashboard for a Pakistani Business

⏱️ 80 min · hands-on
📖 Full Dashboard Walkthrough
🧩 3 Quiz Questions
🏗️ 1 Challenge · 3 tasks
Your progress in Section 3100%
🎯 The Project: This is the capstone of Section 3 — pandas from Section 2, plus every chart type from this section, in one workflow. You'll build a small illustrative sales dataset for an online store operating across Pakistan, clean it and derive a revenue column with pandas, then build a bar chart of revenue by city, a line chart of the monthly trend, a pie chart of revenue by category, and finally an interactive Plotly version — the same building blocks a real Dash dashboard, like the one from Lesson 17, would be wired around.

The Project Brief

Imagine a small online store that sells clothing and electronics, shipping to customers in Lahore, Karachi, and Islamabad. The owner wants a simple sales dashboard: which city sells the most, how revenue trends month to month, and which product category drives the business.

Everything in this lesson — the store, the exact numbers, the orders — is a plausible, illustrative example built to teach the workflow, not a real company or a real published dataset. The pandas, Matplotlib, Seaborn, and Plotly code is 100% real and runnable; the numbers are a stand-in for whatever sales export lands on your desk.

📅
order_date
The date each order was placed.
🏙️
city
Which of the three cities the order shipped to — Lahore, Karachi, or Islamabad.
🏷️
category
Clothing, Electronics, or Footwear.
🔢
quantity
Units sold in that order.
💰
unit_price
Price per unit, in PKR — revenue is derived from this, not stored directly.
1
Build and inspect the data
Construct the DataFrame, check its shape, and derive a revenue column.
2
Revenue by city — bar chart
groupby() + Matplotlib's ax.bar() to compare the three cities.
3
Revenue over time — line chart
Monthly trend with dt.to_period("M") and ax.plot().
4
Revenue by category — pie chart
ax.pie() to show each category's share of the total.
5
Spread by city — Seaborn boxplot
sns.boxplot() to see per-order spread, not just totals.
6
Make it interactive
The same city totals, rebuilt as a hoverable Plotly chart.

Build the Data and Derive Revenue

Real sales exports almost never include a ready-made "revenue" column — it's quantity × unit_price, computed after the fact. That's the first thing to do once the raw data is loaded.

load_data.py
PYTHON
import pandas as pd

df = pd.DataFrame({
    "order_date": pd.to_datetime([
        "2024-01-05", "2024-01-18", "2024-01-27",
        "2024-02-03", "2024-02-14", "2024-02-22",
        "2024-03-06", "2024-03-19", "2024-03-28",
        "2024-04-04", "2024-04-16", "2024-04-25",
        "2024-05-09", "2024-05-21", "2024-05-30",
        "2024-06-05", "2024-06-17", "2024-06-26",
    ]),
    "city": [
        "Lahore", "Karachi", "Islamabad",
        "Lahore", "Karachi", "Islamabad",
        "Lahore", "Karachi", "Islamabad",
        "Lahore", "Karachi", "Islamabad",
        "Lahore", "Karachi", "Islamabad",
        "Lahore", "Karachi", "Islamabad",
    ],
    "category": [
        "Clothing", "Electronics", "Footwear",
        "Electronics", "Clothing", "Clothing",
        "Footwear", "Electronics", "Clothing",
        "Clothing", "Footwear", "Electronics",
        "Electronics", "Clothing", "Footwear",
        "Clothing", "Electronics", "Clothing",
    ],
    "quantity": [3, 1, 5, 1, 4, 2, 6, 1, 3, 2, 4, 1, 1, 5, 2, 3, 1, 4],
    "unit_price": [1500, 32000, 1200, 28000, 1600, 1300, 1100, 55000, 1450, 1550, 1250, 45000, 38000, 1700, 1150, 1400, 50000, 1350],
})

df["revenue"] = df["quantity"] * df["unit_price"]

print(df.shape)
# (18, 6)

print(df.head())
df.head()
order_datecitycategoryquantityunit_pricerevenue
02024-01-05LahoreClothing315004500
12024-01-18KarachiElectronics13200032000
22024-01-27IslamabadFootwear512006000
32024-02-03LahoreElectronics12800028000
42024-02-14KarachiClothing416006400
📝
Why derive revenue instead of reading it from the file
A raw sales export almost always stores the components — how many units, at what price — rather than the total, partly because the total is redundant data that can drift out of sync with the source columns if stored separately. Computing df["revenue"] = df["quantity"] * df["unit_price"] is a one-line, always-correct derivation, and it's the pattern you'll reuse on almost every real sales dataset.

Revenue by City — Bar Chart

The first question: which city brings in the most revenue? A groupby() gets the totals, and a Matplotlib bar chart from Lesson 14 makes them easy to compare at a glance.

revenue_by_city.py
PYTHON
import matplotlib.pyplot as plt

city_sales = df.groupby("city")["revenue"].sum().sort_values(ascending=False)
print(city_sales)

fig, ax = plt.subplots(figsize=(7, 5))
ax.bar(city_sales.index, city_sales.values, color=["#4f9eff", "#2de8c0", "#fbbf24"])
ax.set_title("Total Revenue by City")
ax.set_ylabel("Revenue (PKR)")

plt.show()
city_sales
cityrevenue
Karachi153600
Lahore102500
Islamabad67450
Passing a list of colors to ax.bar()
ax.bar() accepts either a single color for every bar, or a list with one color per bar — here, three colors for three cities, matching the site's own accent, teal, and amber palette.

Revenue Over Time — Line Chart

Next: is revenue trending up, down, or flat across the first half of the year? Grouping by month with .dt.to_period("M") — the same technique from Lesson 13's challenge — turns individual orders into a clean monthly series to plot as a line.

revenue_trend.py
PYTHON
df["month"] = df["order_date"].dt.to_period("M")
monthly_sales = df.groupby("month")["revenue"].sum()
print(monthly_sales)

fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(monthly_sales.index.astype(str), monthly_sales.values, color="#a78bfa", marker="o", linewidth=2.5)
ax.set_title("Revenue Trend by Month")
ax.set_xlabel("Month")
ax.set_ylabel("Revenue (PKR)")

plt.show()
monthly_sales
monthrevenue
2024-0142500
2024-0241700
2024-0365750
2024-0452050
2024-0550450
2024-0671100
⚠️
.astype(str) before plotting a Period index
monthly_sales.index holds pandas Period objects, not plain strings. Matplotlib can usually plot them directly, but converting to str first with .astype(str) guarantees clean, readable x-axis labels like "2024-01" instead of leaving it to chance.

Revenue by Category — Pie Chart

For a small, fixed set of categories that make up a whole — here, three product categories summing to 100% of revenue — a pie chart communicates "share of total" more directly than a bar chart does. Matplotlib's ax.pie() builds one straight from the grouped totals.

revenue_by_category.py
PYTHON
category_sales = df.groupby("category")["revenue"].sum()
print(category_sales)

fig, ax = plt.subplots(figsize=(6, 6))
ax.pie(
    category_sales.values,
    labels=category_sales.index,
    autopct="%1.1f%%",
    colors=["#4f9eff", "#2de8c0", "#fbbf24"],
)
ax.set_title("Revenue Share by Category")

plt.show()
🖼️ What plt.show() displays
A circle split into three wedges — Electronics taking up the largest slice despite having the fewest orders, since each electronics order carries a much higher unit price than clothing or footwear. Each wedge is labeled with its category name and its percentage share, printed directly on the slice by autopct="%1.1f%%".
📝
autopct formats the percentage label
autopct="%1.1f%%" is a C-style format string: one digit before the decimal, one digit after, followed by a literal % sign (escaped as %% since a single % would otherwise start a new format directive). It's what turns each wedge's raw fraction into a readable label like "34.2%".

Order Spread by City — Seaborn Boxplot

Totals hide a detail: is Karachi's lead driven by one huge order, or consistently larger orders across the board? A Seaborn box plot from Lesson 15, one per city, shows the spread of individual order values, not just the sum.

order_spread.py
PYTHON
import seaborn as sns

sns.boxplot(data=df, x="city", y="revenue", hue="city", legend=False)
plt.title("Order Value Spread by City")
plt.ylabel("Revenue per Order (PKR)")

plt.show()
Totals and spread answer different questions
The bar chart in Step 2 answers "which city brings in more revenue overall?" The box plot here answers a different question: "are that city's orders consistently bigger, or is it one outlier order skewing the total?" Both are legitimate views of the same underlying data — which is exactly why Section 3 teaches more than one chart type.

Making It Interactive with Plotly

A static bar chart is fine for a report. For a real dashboard — like the Dash skeleton from Lesson 17 — the same city_sales totals become far more useful rebuilt as an interactive Plotly chart, ready to drop straight into a dcc.Graph.

interactive_city_chart.py
PYTHON
import plotly.express as px

city_sales_df = city_sales.reset_index()
city_sales_df.columns = ["city", "revenue"]

fig = px.bar(
    city_sales_df, x="city", y="revenue",
    color="city",
    title="Total Revenue by City (Interactive)",
)
fig.show()
📝
reset_index() turns a Series back into a DataFrame
city_sales from Step 2 is a Series, indexed by city — great for Matplotlib, but Plotly Express wants column names to pass as x=/y=. .reset_index() turns the city index back into a regular column, and renaming .columns gives both columns clear names before handing the DataFrame to px.bar().

This is exactly the figure that would sit behind dcc.Graph(figure=fig) in a Dash app — the same pattern from Lesson 17, now built on this project's own data instead of a toy example.

The Complete Script, Start to Finish

Every step from this lesson combined into one script — the kind of file you'd actually run to generate a batch of report charts, or refactor into a Dash app's callbacks.

sales_dashboard.py — COMPLETE PROGRAM
PYTHON
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px

# 1. Build and clean the data
df = pd.read_csv("sales_data.csv", parse_dates=["order_date"])
df["revenue"] = df["quantity"] * df["unit_price"]

# 2. Revenue by city — bar chart
city_sales = df.groupby("city")["revenue"].sum().sort_values(ascending=False)
fig1, ax1 = plt.subplots(figsize=(7, 5))
ax1.bar(city_sales.index, city_sales.values, color=["#4f9eff", "#2de8c0", "#fbbf24"])
ax1.set_title("Total Revenue by City")
fig1.savefig("revenue_by_city.png", dpi=300, bbox_inches="tight")

# 3. Revenue over time — line chart
df["month"] = df["order_date"].dt.to_period("M")
monthly_sales = df.groupby("month")["revenue"].sum()
fig2, ax2 = plt.subplots(figsize=(8, 5))
ax2.plot(monthly_sales.index.astype(str), monthly_sales.values, color="#a78bfa", marker="o")
ax2.set_title("Revenue Trend by Month")
fig2.savefig("revenue_trend.png", dpi=300, bbox_inches="tight")

# 4. Revenue by category — pie chart
category_sales = df.groupby("category")["revenue"].sum()
fig3, ax3 = plt.subplots(figsize=(6, 6))
ax3.pie(category_sales.values, labels=category_sales.index, autopct="%1.1f%%")
ax3.set_title("Revenue Share by Category")
fig3.savefig("revenue_by_category.png", dpi=300, bbox_inches="tight")

# 5. Order spread by city — Seaborn boxplot
fig4, ax4 = plt.subplots(figsize=(7, 5))
sns.boxplot(data=df, x="city", y="revenue", hue="city", legend=False, ax=ax4)
ax4.set_title("Order Value Spread by City")
fig4.savefig("order_spread.png", dpi=300, bbox_inches="tight")

# 6. Interactive Plotly version, dashboard-ready
city_sales_df = city_sales.reset_index()
city_sales_df.columns = ["city", "revenue"]
fig5 = px.bar(city_sales_df, x="city", y="revenue", color="city", title="Total Revenue by City")
fig5.write_html("revenue_by_city_interactive.html")

plt.show()
⚠️
ax= on a Seaborn function targets a specific subplot
Passing ax=ax4 to sns.boxplot() tells Seaborn to draw into that specific Matplotlib Axes rather than creating its own — the same trick that lets you mix Seaborn charts into a Matplotlib subplot grid from Lesson 14, instead of only ever getting one chart per figure.

Writing Up Findings

Charts alone aren't a dashboard's finished output — the last step is turning them into a few plain-English statements the store owner could actually act on. Treat these as an example of the kind of finding a dashboard produces, not as claims about any real business.

🏙️
Karachi leads on revenue
Karachi's total is the highest of the three cities in this sample, driven partly by a couple of high-value Electronics orders rather than a large order count.
📈
June is the strongest month
Revenue isn't flat month to month — March and June both spike above the surrounding months, worth investigating for a seasonal or promotional cause.
🏷️
Electronics drives revenue share despite fewer orders
Electronics has the fewest line items of the three categories but the largest slice of the pie chart — each electronics order carries a much higher unit price than clothing or footwear.
⚠️
A dashboard finding is a starting point, not an answer
"Karachi leads on revenue" is worth asking about further — is it consistent every month, or one big client? Good dashboards surface questions like this quickly; answering them for real usually means going back to groupby() with a narrower slice of the data, exactly like the "Try It Yourself" tasks below.
🧩 Knowledge Check — Lesson 18
A shorter check-in for this project lesson — 3 questions about the dashboard workflow.
1. Why is revenue computed as quantity * unit_price in pandas, rather than expected to already exist in the raw data?
2. Which chart type is the most natural fit for showing how revenue splits across a small, fixed set of categories as a share of the whole?
3. Before grouping orders by month with .dt.to_period("M"), what does the order_date column need to already be?
💪
Try It Yourself — Lesson 18
Extend the project · Intermediate Level

The base dashboard works end to end — now push it further. Use the df with the revenue and month columns from this lesson as your starting point for each task below.

Task 1: Break the city bar chart down by category 🏙️

Build a pivot table with pd.pivot_table(df, values="revenue", index="city", columns="category", aggfunc="sum", fill_value=0), then use it to draw a grouped (or stacked) bar chart — three cities on the x-axis, one bar per category within each city. Does the top category match across all three cities?
Task 2: Turn the bar chart into a live Dash app 🖥️

Using the Lesson 17 skeleton, build a Dash app with a dcc.Dropdown for category (Clothing, Electronics, Footwear) and a dcc.Graph showing a px.bar() of revenue by city, filtered to whichever category is selected in the dropdown — the same @app.callback pattern from that lesson, applied to this project's data.
Task 3: Find each city's best month 📅

Filter df down to a single city with boolean indexing (e.g. df[df["city"] == "Lahore"]), then re-run the monthly groupby("month")["revenue"].sum() from Section 4 on just that subset with .idxmax() to find its single best month. Repeat for all three cities — do they all peak in the same month, or different ones?
💡 Show hints if you're stuck
  • Task 1: pivot = pd.pivot_table(df, values="revenue", index="city", columns="category", aggfunc="sum", fill_value=0), then pivot.plot(kind="bar", ax=ax) is a fast way to chart a pivot table directly with Matplotlib.
  • Task 2: dcc.Dropdown(id="category-dropdown", options=[{"label": c, "value": c} for c in df["category"].unique()], value="Clothing"), then filter inside the callback with df[df["category"] == selected_category].
  • Task 3: lahore = df[df["city"] == "Lahore"], then lahore.groupby("month")["revenue"].sum().idxmax()
Finished this project?
Mark it complete to track your progress.
🎉

Section 3 Complete!

You've taken raw sales data all the way through cleaning, Matplotlib, Seaborn, and interactive Plotly charts — the full data visualization toolkit. Next up: Section 4, where you'll dig into the statistics and probability that power every model later in this course.

Module 18 of 18 Section 3 — Data Visualization: Matplotlib & Seaborn