Data Analytics
ProjectIntermediate 60 min· Insights and storytelling · 10 of 10

Project: sales dashboard for a coffee chain

Build a portfolio project from scratch: generate six months of sales data for three Brew & Bean outlets, check it, analyse it with SQL, build a one-page dashboard in Power BI or Looker Studio, and write a short insight summary with recommendations.

Goal: act as the analyst for Brew & Bean, which now has three outlets in Bengaluru: Indiranagar, Koramangala and HSR Layout. The owner asks: "How are we doing, which store and which items matter, and what should I do next?"

The finished result: (1) a sales.csv data file, (2) a short script that checks the data, (3) SQL queries that answer the owner's questions, (4) a one-page dashboard with KPI cards, a monthly trend, store comparison, category split, top items and filters, and (5) a half-page insight summary a non-technical owner can act on. Together these make a strong portfolio piece.

Prerequisites: the earlier lessons in this track (SQL for analysts, Cleaning messy data, Business metrics and KPIs, Dashboards with Power BI and Looker Studio, Data storytelling). You need Python 3 with pandas (pip install pandas; Google Colab has it) and either Power BI Desktop (Windows) or a Google account for Looker Studio.

Note: the data is made up for learning. It does not describe any real business, and the patterns in it come from the settings in the generator script.

Stage 1: create the data

Real analysts rarely get a perfect file, but a clean, known dataset lets you focus on the analysis. The script below creates one row per item in each order for the first six months of 2026 (181 days), for three stores, using the menu from the Brew & Bean project. It is seeded, so you get the same file each time. Weekend days get more orders, and Indiranagar is busiest, Koramangala next, HSR Layout smallest.

generate_sales.py
import csv
import random
from datetime import date, timedelta

random.seed(42)

MENU = [
    ("espresso", "Espresso", "coffee", 120),
    ("cappuccino", "Cappuccino", "coffee", 180),
    ("cold-brew", "Cold Brew", "coffee", 220),
    ("filter-coffee", "Filter Coffee", "coffee", 80),
    ("masala-chai", "Masala Chai", "tea", 90),
    ("croissant", "Butter Croissant", "bakery", 150),
    ("banana-bread", "Banana Bread", "bakery", 130),
    ("veg-sandwich", "Veg Grilled Sandwich", "food", 160),
]
STORES = {"Indiranagar": 1.0, "Koramangala": 0.85, "HSR Layout": 0.6}
WEIGHTS = [10, 18, 9, 16, 15, 9, 6, 8]

start = date(2026, 1, 1)
rows = []
order_id = 1000
for d in range(181):
    day = start + timedelta(days=d)
    weekend = day.weekday() >= 5
    for store, factor in STORES.items():
        orders = int(random.randint(35, 50) * factor * (1.25 if weekend else 1.0))
        for _ in range(orders):
            order_id += 1
            for _ in range(random.randint(1, 3)):
                item = random.choices(MENU, weights=WEIGHTS)[0]
                qty = random.choice([1, 1, 1, 2])
                rows.append([order_id, day.isoformat(), store, item[0], item[1],
                             item[2], item[3], qty, item[3] * qty])

with open("sales.csv", "w", newline="", encoding="utf-8") as f:
    w = csv.writer(f)
    w.writerow(["order_id", "order_date", "store", "item_id", "item_name",
                "category", "unit_price", "qty", "revenue"])
    w.writerows(rows)
print(len(rows), "rows")

Run python generate_sales.py. It writes sales.csv (about 2.8 MB) in the same folder.

Expected output
40134 rows

If your row count differs, you probably edited the script or changed the seed. That is fine, but your numbers later will differ from the ones shown in this lesson. Open sales.csv in a spreadsheet and look at the first few rows so you know what you are working with.

Stage 2: check the data before analysing it

Prompt · Ask AI for a data check plan
I have a CSV called sales.csv with columns order_id, order_date, store, item_id, item_name, category, unit_price, qty, revenue. One row is one item in an order. Write pandas code that checks: row count, missing values, duplicate rows, whether revenue equals unit_price * qty, the date range, and the distinct values of store and category. Print the results clearly. Do not state any numbers, I will run it.

What to expect: Compare with the reference script below. Check that the AI used distinct order_id for order counts, not row counts.

check_data.py
import pandas as pd

df = pd.read_csv("sales.csv", parse_dates=["order_date"])

print("Rows:", len(df))
print("Missing values per column:")
print(df.isna().sum())
print("Duplicate rows:", df.duplicated().sum())
print("Revenue != unit_price * qty:", (df["revenue"] != df["unit_price"] * df["qty"]).sum())
print("Date range:", df["order_date"].min().date(), "to", df["order_date"].max().date())
print("Stores:", sorted(df["store"].unique()))
print("Categories:", sorted(df["category"].unique()))
print("Distinct orders:", df["order_id"].nunique())

Requires pandas. Duplicate rows may be greater than zero here: the same item can appear twice in one order, which is legitimate in this data.

What you should see (with the unedited script)
Rows: 40134
Missing values per column: all 0
Duplicate rows: some, because the same item can be added twice in one order
Revenue != unit_price * qty: 0
Date range: 2026-01-01 to 2026-06-30
Stores: ['HSR Layout', 'Indiranagar', 'Koramangala']
Categories: ['bakery', 'coffee', 'food', 'tea']
Distinct orders: 20089

Do not delete 'duplicates' blindly

A duplicate row is only an error if it should not exist. Here two identical rows mean the customer added the same item twice in one order, so removing them would understate revenue. Decide from the business meaning, not from a tool's default.

Stage 3: answer the owner's questions with SQL

The owner asks six things: overall KPIs, revenue by month, performance by store, category mix, top items, and weekday versus weekend. You will load the CSV into SQLite, a database built into Python, and write one query for each. SQLite keeps this free and local; the same SQL runs with small changes in PostgreSQL or MySQL.

analysis.py
import sqlite3

import pandas as pd

df = pd.read_csv("sales.csv")
conn = sqlite3.connect("brew.db")
df.to_sql("sales", conn, if_exists="replace", index=False)


def show(title, sql):
    print(title)
    print(pd.read_sql_query(sql, conn).to_string(index=False))
    print()


show("KPIs", """
SELECT SUM(revenue) AS total_revenue,
       COUNT(DISTINCT order_id) AS orders,
       ROUND(1.0 * SUM(revenue) / COUNT(DISTINCT order_id), 2) AS avg_order_value
FROM sales
""")

show("Revenue by month", """
SELECT strftime('%Y-%m', order_date) AS month,
       SUM(revenue) AS revenue
FROM sales
GROUP BY month
ORDER BY month
""")

show("By store", """
SELECT store,
       SUM(revenue) AS revenue,
       COUNT(DISTINCT order_id) AS orders,
       ROUND(1.0 * SUM(revenue) / COUNT(DISTINCT order_id), 2) AS avg_order_value
FROM sales
GROUP BY store
ORDER BY revenue DESC
""")

show("By category", """
SELECT category,
       SUM(revenue) AS revenue,
       ROUND(100.0 * SUM(revenue) / (SELECT SUM(revenue) FROM sales), 1) AS pct_of_revenue
FROM sales
GROUP BY category
ORDER BY revenue DESC
""")

show("Top items", """
SELECT item_name, SUM(qty) AS units, SUM(revenue) AS revenue
FROM sales
GROUP BY item_name
ORDER BY revenue DESC
LIMIT 5
""")

show("Weekday vs weekend, per day", """
SELECT CASE WHEN strftime('%w', order_date) IN ('0', '6') THEN 'Weekend' ELSE 'Weekday' END AS day_type,
       COUNT(DISTINCT order_date) AS days,
       ROUND(1.0 * SUM(revenue) / COUNT(DISTINCT order_date), 0) AS revenue_per_day
FROM sales
GROUP BY day_type
""")

conn.close()

Run python analysis.py. It creates brew.db and prints six result tables. In SQLite, strftime('%w', date) gives the weekday number, 0 for Sunday and 6 for Saturday.

Results with the unedited data
KPIs
 total_revenue  orders  avg_order_value
       6886830   20089           342.82

Revenue by month
  month  revenue
2026-01  1204850
2026-02  1038440
2026-03  1183780
2026-04  1141870
2026-05  1177800
2026-06  1140090

By store
      store  revenue  orders  avg_order_value
Indiranagar  2822540    8211           343.75
Koramangala  2377410    6961           341.53
 HSR Layout  1686880    4917           343.07

By category
category  revenue  pct_of_revenue
  coffee  4258100            61.8
  bakery  1168490            17.0
     tea   766800            11.1
    food   693440            10.1

Top items
       item_name  units  revenue
      Cappuccino   9979  1796220
       Cold Brew   4992  1098240
     Masala Chai   8520   766800
Butter Croissant   4975   746250
   Filter Coffee   8806   704480

Weekday vs weekend, per day
day_type  days  revenue_per_day
 Weekday     129          35588.0
 Weekend      52          44154.0

Read these results as an analyst, before you build anything:

- February is lower than other months, but it has only 28 days. A fairer comparison is revenue per day. - Average order value is almost the same in all three stores (about ₹342 to ₹344). So HSR Layout earns less because it has fewer orders, not because customers spend less per order. - Coffee brings in about 62% of revenue. Cold Brew sells about half as many units as Cappuccino but earns a lot per unit. - A weekend day earns more than a weekday: roughly ₹44,000 against ₹36,000 across all stores.

These observations will become your dashboard's story.

Stage 4: build the dashboard

Choose Power BI Desktop or Looker Studio. Follow the steps in *Dashboards with Power BI and Looker Studio*, using sales.csv as the source (for Looker Studio, upload the CSV with the File Upload connector or put it in Google Sheets; the file has about 40,000 rows, which Sheets can handle). Use the same three measures: total revenue, distinct orders, and average order value as revenue divided by orders.

Prompt · Ask AI to critique your layout plan
I'm building a one-page dashboard for the owner of a 3-outlet coffee chain, who looks at it each Monday. Metrics: revenue, orders, average order value. Dimensions: month, store, category, item. Here is my planned layout: [describe where each card and chart goes]. Tell me what to move, remove or retitle so the owner can see within 10 seconds how the business is doing and where to look. Do not add more charts.

What to expect: Take only suggestions that make the page simpler. Reject any that add clutter.

Build the page

  1. 1Top row: three KPI cards, Total Revenue, Orders, Average Order Value. Format revenue as ₹ with thousands separators.
  2. 2Left, middle: a line chart of revenue by month, titled with the message (for example 'Revenue is steady at about ₹10 to 12 lakh a month').
  3. 3Right, middle: a sorted bar chart of revenue by store, with orders as a second bar chart or a table beside it.
  4. 4Bottom left: a bar chart of revenue by category. Bottom right: a Top 5 items bar chart by revenue.
  5. 5Add two filters: store and month (or a date range). Make sure every visual responds to both.
  6. 6Add a small text box with the data period (1 Jan to 30 Jun 2026) and a note that the data is sample data.
  7. 7Use one accent colour to highlight HSR Layout in the store chart and keep everything else muted.
  8. 8Click through every filter combination and confirm that the cards match your SQL results when no filter is applied.

Dashboard acceptance test

  1. With no filters: total revenue 6,886,830, orders 20,089 and average order value about 342.82 (if you used the unedited script).
  2. Selecting HSR Layout shows revenue of 1,686,880.
  3. Every chart has a clear title with units.
  4. Bars are sorted; the axis starts at zero.
  5. Nothing on the page is there just because the tool made it easy.

Stage 5: write the insight summary

A dashboard shows what happened. The owner also needs to know what it means and what to do. Use the answer, evidence, recommendation, caveat structure from *Data storytelling and presenting insights*. Put the summary at the top of your project README or in a text box beside the dashboard.

Prompt · Draft the summary, then fix it
I am an analyst writing for the owner of Brew & Bean, a 3-outlet coffee shop in Bengaluru. Using ONLY the facts below, write a 120-word summary with a headline, three evidence bullets, one recommendation and two caveats. Do not add numbers or causes that I did not give.

Facts:
- Total revenue Jan to Jun 2026 was ₹68.9 lakh from 20,089 orders; average order value about ₹343.
- Average order value is about the same in all three stores, so HSR Layout's lower revenue comes from fewer orders (4,917 against 8,211 in Indiranagar).
- Coffee is about 62% of revenue. Cappuccino is the top item by revenue.
- A weekend day earns about ₹44,000 versus about ₹36,000 for a weekday.
- This is sample data covering six months.

What to expect: Check that every number matches your outputs, and rewrite any sentence that claims a cause. A good recommendation could be to try a limited-time offer at HSR Layout aimed at getting more orders, measured by orders per day, not average order value. Remember that this data cannot tell you why HSR Layout gets fewer orders.

Here is one reference version. Compare it with yours.

Headline: Revenue is steady at about ₹10 to 12 lakh a month, and HSR Layout's gap is about the number of orders, not the size of each bill.

Evidence: Average order value is about ₹343 at every store. HSR Layout had 4,917 orders against 8,211 at Indiranagar. Coffee is 62% of revenue; weekend days earn about 24% more than weekdays.

Recommendation: Run a two-week offer at HSR Layout designed to bring in more orders (for example a pickup combo), and compare orders per day with the previous two weeks.

Caveats: Six months of sample data; we cannot yet say why HSR Layout has fewer orders (location, footfall and opening hours were not measured); a before-and-after comparison is weaker than a test (see *A/B testing and experiments*).

Stage 6: check your work with AI as a reviewer

Before you publish, use the routine from *Using AI as an analyst, and checking its work*: recompute totals a second way, look for double counting, and ask an AI tool to act as a sceptical reviewer of your summary. Do not share the AI's text without checking each number.

Final review

  1. KPIs on the dashboard match the SQL output.
  2. The summary has no number you cannot trace to a query or card.
  3. You say 'associated with' rather than 'caused by'.
  4. The data is clearly labelled as sample data.
  5. The project folder contains generate_sales.py, check_data.py, analysis.py, the dashboard file or screenshots, and a short README.

Show it to people

Export the dashboard as a PDF or take screenshots, and put them with your scripts in a GitHub repository with a README that states the question, the method, the findings and what you would do next. Interviewers like projects where you explain your choices and limits.

Stretch goals

Try it: Compare months fairly

February has fewer days. Add a revenue per day measure (total revenue divided by the number of distinct days) to the dashboard and to a SQL query by month. Does February still look low?

Try it: Add a price-change what-if

Suppose Cold Brew rises from ₹220 to ₹240. Using the data, estimate the extra revenue if units sold stay the same. Then write two sentences on why units might not stay the same.

Try it: Design an A/B test

Write a one-page plan to test a 'pickup in 10 minutes' button message on the ordering page. State the hypothesis, primary metric, guardrail metrics, sample size approach and decision rule.

Try it: Add a new data column

Create a version of the generator that also records the hour of each order and adds a 'rush hour' flag, then answer: which store is busiest between 8 and 10 am?

Key takeaways

  1. A complete analytics project goes from question to data check, analysis, dashboard, summary and recommendation.
  2. Check data quality first; never remove 'duplicates' without understanding what they mean.
  3. Count orders as distinct order IDs, and compare periods fairly (revenue per day, not just per month).
  4. Look for the cause of a gap in the parts of the metric: here, orders rather than average order value.
  5. The dashboard shows what happened; the summary says what it means and what to do next.
  6. Verify every number with a second method and state limits and caveats honestly.

Check yourself

Finishing the quiz marks the lesson as done.

1. HSR Layout's revenue is lower than Indiranagar's, but average order value is nearly equal. What does that suggest?

2. February's total revenue is lowest. What is the fair way to check whether it is really a weak month?

3. The check script reports duplicate rows. What should you do?

4. Which sentence belongs in the owner's summary?

5. Your dashboard shows total revenue different from your SQL output with no filter applied. What is the first thing to check?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free