Project 2 — Sales Dashboard for a Pakistani Business
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.
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.
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())
| order_date | city | category | quantity | unit_price | revenue | |
|---|---|---|---|---|---|---|
| 0 | 2024-01-05 | Lahore | Clothing | 3 | 1500 | 4500 |
| 1 | 2024-01-18 | Karachi | Electronics | 1 | 32000 | 32000 |
| 2 | 2024-01-27 | Islamabad | Footwear | 5 | 1200 | 6000 |
| 3 | 2024-02-03 | Lahore | Electronics | 1 | 28000 | 28000 |
| 4 | 2024-02-14 | Karachi | Clothing | 4 | 1600 | 6400 |
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.
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 | revenue |
|---|---|
| Karachi | 153600 |
| Lahore | 102500 |
| Islamabad | 67450 |
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.
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()
| month | revenue |
|---|---|
| 2024-01 | 42500 |
| 2024-02 | 41700 |
| 2024-03 | 65750 |
| 2024-04 | 52050 |
| 2024-05 | 50450 |
| 2024-06 | 71100 |
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.
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()
autopct="%1.1f%%".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.
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()
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.
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()
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.
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=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.
groupby() with a narrower slice of the data, exactly like the "Try It Yourself" tasks below.revenue computed as quantity * unit_price in pandas, rather than expected to already exist in the raw data?.dt.to_period("M"), what does the order_date column need to already be?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.
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?
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.
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), thenpivot.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 withdf[df["category"] == selected_category]. - Task 3:
lahore = df[df["city"] == "Lahore"], thenlahore.groupby("month")["revenue"].sum().idxmax()