Cafe Performance Insights
Helping Cafe Owners transform raw sales data into actionable insights and data-driven strategies.
Tech Stack
Coffee Sales Dashboard.pbix
The Data Cleaning Process
Step-by-step pipeline documenting extraction, dirty value mapping, and types reconciliation using python.
Setup
Run Import to access the Library
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings("ignore", category=RuntimeWarning)
print("Setup Complete")Setup Complete
# Fill in the line below: Specify the path of the CSV file to read
my_filepath = "/kaggle/input/datasets/ahmedmohamed2003/cafe-sales-dirty-data-for-cleaning-training/dirty_cafe_sales.csv"# Fill in the line below: Read the file into a variable my_data
my_data = pd.read_csv(my_filepath)# Print the first five rows of the data
my_data.head()| Transaction ID | Item | Quantity | Price Per Unit | Total Spent | Payment Method | Location | Transaction Date | |
|---|---|---|---|---|---|---|---|---|
| 0 | TXN_1961373 | Coffee | 2 | 2.0 | 4.0 | Credit Card | Takeaway | 2023-09-08 |
| 1 | TXN_4977031 | Cake | 4 | 3.0 | 12.0 | Cash | In-store | 2023-05-16 |
| 2 | TXN_4271903 | Cookie | 4 | 1.0 | ERROR | Credit Card | In-store | 2023-07-19 |
| 3 | TXN_7034554 | Salad | 2 | 5.0 | 10.0 | UNKNOWN | UNKNOWN | 2023-04-27 |
| 4 | TXN_3160411 | Coffee | 2 | 2.0 | 4.0 | Digital Wallet | In-store | 2023-06-11 |
Cleaning the data
# Visualize the unique values in my dataset.
for col in my_data.columns:
print("\n")
print("=====", col, "=====")
print(my_data[col].value_counts())
===== Transaction ID =====
Transaction ID
TXN_1961373 1
TXN_4831525 1
TXN_1228927 1
TXN_6486912 1
TXN_3447069 1
..
TXN_5680238 1
TXN_7766134 1
TXN_2617257 1
TXN_8993132 1
TXN_6170729 1
Name: count, Length: 10000, dtype: int64
===== Item =====
Item
Juice 1171
Coffee 1165
Salad 1148
Cake 1139
Sandwich 1131
Smoothie 1096
Cookie 1092
Tea 1089
UNKNOWN 344
ERROR 292
Name: count, dtype: int64
===== Quantity =====
Quantity
5 2013
2 1974
4 1863
3 1849
1 1822
UNKNOWN 171
ERROR 170
Name: count, dtype: int64
===== Price Per Unit =====
Price Per Unit
3.0 2429
4.0 2331
2.0 1227
5.0 1204
1.0 1143
1.5 1133
ERROR 190
UNKNOWN 164
Name: count, dtype: int64
===== Total Spent =====
Total Spent
6.0 979
12.0 939
3.0 930
4.0 923
20.0 746
15.0 734
8.0 677
10.0 524
2.0 497
9.0 479
5.0 468
16.0 444
25.0 259
7.5 237
1.0 232
4.5 225
1.5 205
UNKNOWN 165
ERROR 164
Name: count, dtype: int64
===== Payment Method =====
Payment Method
Digital Wallet 2291
Credit Card 2273
Cash 2258
ERROR 306
UNKNOWN 293
Name: count, dtype: int64
===== Location =====
Location
Takeaway 3022
In-store 3017
ERROR 358
UNKNOWN 338
Name: count, dtype: int64
===== Transaction Date =====
Transaction Date
UNKNOWN 159
ERROR 142
2023-06-16 40
2023-02-06 40
2023-07-21 39
...
2023-04-27 15
2023-09-24 15
2023-07-22 14
2023-03-11 14
2023-02-17 14
Name: count, Length: 367, dtype: int64
# Replace it into NaN:
my_data = my_data.replace(["UNKNOWN", "ERROR"], np.nan)my_data.dtypesTransaction ID object Item object Quantity object Price Per Unit object Total Spent object Payment Method object Location object Transaction Date object dtype: object
# Convert numeric columns to floats so you can do math on them
my_data[["Quantity", "Price Per Unit", "Total Spent"]] = my_data[["Quantity", "Price Per Unit", "Total Spent"]].astype(float)
# Convert the text date into an actual Date object
my_data["Transaction Date"] = pd.to_datetime(my_data["Transaction Date"])#Checks for Error, since we have replaced it into NaN (Null)
my_data.isnull().sum()Transaction ID 0 Item 969 Quantity 479 Price Per Unit 533 Total Spent 502 Payment Method 3178 Location 3961 Transaction Date 460 dtype: int64
## Fill missing Quantity, Rule : Quantity = (Total Spent * Price Per Unit)# It's like SQL :
# WHERE QUANTITY IS NULL
# AND PRICE PER UNIT IS NOT NULL
# AND TOTAL SPENT IS NOT NULL
quantity_filldata = (
my_data["Quantity"].isnull()
&
my_data[["Price Per Unit", "Total Spent"]].notna().all(axis=1)
)
my_data[quantity_filldata]| Transaction ID | Item | Quantity | Price Per Unit | Total Spent | Payment Method | Location | Transaction Date | |
|---|---|---|---|---|---|---|---|---|
| 20 | TXN_3522028 | Smoothie | NaN | 4.0 | 20.0 | Cash | In-store | 2023-04-04 |
| 55 | TXN_5522862 | Cookie | NaN | 1.0 | 2.0 | Credit Card | Takeaway | 2023-03-19 |
| 57 | TXN_2080895 | Cake | NaN | 3.0 | 3.0 | Digital Wallet | In-store | 2023-04-19 |
| 66 | TXN_8501819 | Juice | NaN | 3.0 | 6.0 | Cash | NaN | 2023-03-30 |
| 117 | TXN_2148617 | Juice | NaN | 3.0 | 9.0 | Digital Wallet | NaN | 2023-01-10 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 9932 | TXN_8502079 | Tea | NaN | 1.5 | 3.0 | Cash | NaN | 2023-04-20 |
| 9935 | TXN_9778251 | Tea | NaN | 1.5 | 6.0 | NaN | Takeaway | 2023-11-09 |
| 9944 | TXN_7495283 | Cake | NaN | 3.0 | 15.0 | Credit Card | Takeaway | 2023-04-14 |
| 9957 | TXN_6487003 | Coffee | NaN | 2.0 | 8.0 | Credit Card | Takeaway | 2023-11-15 |
| 9984 | TXN_3142496 | Smoothie | NaN | 4.0 | 4.0 | Cash | Takeaway | 2023-07-27 |
441 rows × 8 columns
# Locate and Replace Empty Quantity by multiply toal spent and price per unit
my_data.loc[quantity_filldata, "Quantity"] = (
my_data.loc[quantity_filldata, "Total Spent"]
/
my_data.loc[quantity_filldata, "Price Per Unit"]
)my_data.isnull().sum()Transaction ID 0 Item 969 Quantity 38 Price Per Unit 533 Total Spent 502 Payment Method 3178 Location 3961 Transaction Date 460 dtype: int64
## Fill missing Price per Unit ## price_filldata = (
my_data["Price Per Unit"].isnull()
&
my_data[["Quantity","Total Spent"]].notna().all(axis=1)
)my_data.loc[price_filldata, "Price Per Unit"] = (
my_data.loc[price_filldata, "Total Spent"]
/
my_data.loc[price_filldata, "Quantity"]
)my_data.isnull().sum()Transaction ID 0 Item 969 Quantity 38 Price Per Unit 38 Total Spent 502 Payment Method 3178 Location 3961 Transaction Date 460 dtype: int64
## Fill missing Total Spent ## spent_filldata = (
my_data["Total Spent"].isnull()
&
my_data[["Quantity","Price Per Unit"]].notna().all(axis=1)
)# Total Spent = Quantity * Price Per Unit
my_data.loc[spent_filldata,"Total Spent"] = (
my_data.loc[spent_filldata,"Quantity"]
*
my_data.loc[spent_filldata,"Price Per Unit"]
)my_data.isnull().sum()Transaction ID 0 Item 969 Quantity 38 Price Per Unit 38 Total Spent 40 Payment Method 3178 Location 3961 Transaction Date 460 dtype: int64
Total_Spent = my_data[my_data["Total Spent"].isnull()]
Total_Spent| Transaction ID | Item | Quantity | Price Per Unit | Total Spent | Payment Method | Location | Transaction Date | |
|---|---|---|---|---|---|---|---|---|
| 65 | TXN_4987129 | Sandwich | 3.0 | NaN | NaN | NaN | In-store | 2023-10-20 |
| 236 | TXN_8562645 | Salad | NaN | 5.0 | NaN | NaN | In-store | 2023-05-18 |
| 278 | TXN_3229409 | Juice | NaN | 3.0 | NaN | Cash | Takeaway | 2023-04-15 |
| 641 | TXN_2962976 | Juice | NaN | 3.0 | NaN | NaN | NaN | 2023-03-17 |
| 738 | TXN_8696094 | Sandwich | NaN | 4.0 | NaN | NaN | Takeaway | 2023-05-14 |
| 1674 | TXN_9367492 | Tea | 2.0 | NaN | NaN | Cash | In-store | 2023-06-19 |
| 1761 | TXN_3611851 | NaN | 4.0 | NaN | NaN | Credit Card | NaN | 2023-02-09 |
| 2229 | TXN_8498613 | Sandwich | 2.0 | NaN | NaN | NaN | NaN | 2023-11-08 |
| 2289 | TXN_7524977 | NaN | 4.0 | NaN | NaN | NaN | NaN | 2023-12-09 |
| 2585 | TXN_1259340 | Tea | 3.0 | NaN | NaN | Digital Wallet | NaN | 2023-02-24 |
| 2796 | TXN_9188692 | Cake | NaN | 3.0 | NaN | Credit Card | NaN | 2023-12-01 |
| 3162 | TXN_3577949 | Cake | 3.0 | NaN | NaN | NaN | Takeaway | 2023-04-25 |
| 3203 | TXN_4565754 | Smoothie | NaN | 4.0 | NaN | Digital Wallet | Takeaway | 2023-10-06 |
| 3224 | TXN_6297232 | Coffee | NaN | 2.0 | NaN | NaN | NaN | 2023-04-07 |
| 3401 | TXN_3251829 | Tea | NaN | 1.5 | NaN | Digital Wallet | In-store | 2023-07-25 |
| 3598 | TXN_2857444 | Smoothie | 1.0 | NaN | NaN | Cash | Takeaway | 2023-05-10 |
| 3673 | TXN_6500126 | Smoothie | 2.0 | NaN | NaN | NaN | NaN | NaT |
| 4021 | TXN_6424202 | Cookie | 2.0 | NaN | NaN | Credit Card | In-store | 2023-11-20 |
| 4152 | TXN_9646000 | NaN | 2.0 | NaN | NaN | NaN | In-store | 2023-12-14 |
| 4257 | TXN_6470865 | Coffee | NaN | 2.0 | NaN | Digital Wallet | Takeaway | 2023-09-18 |
| 5841 | TXN_5884081 | Cookie | NaN | 1.0 | NaN | Digital Wallet | In-store | 2023-07-05 |
| 7029 | TXN_4628338 | Coffee | NaN | 2.0 | NaN | Cash | NaN | 2023-12-25 |
| 7035 | TXN_8872984 | Salad | 5.0 | NaN | NaN | Credit Card | In-store | 2023-08-23 |
| 7230 | TXN_5118799 | Cookie | 2.0 | NaN | NaN | Cash | Takeaway | 2023-04-23 |
| 7244 | TXN_2253622 | Sandwich | 5.0 | NaN | NaN | Digital Wallet | Takeaway | 2023-09-30 |
| 7297 | TXN_9944500 | Smoothie | NaN | 4.0 | NaN | Cash | In-store | 2023-01-03 |
| 7568 | TXN_3705445 | Cookie | 5.0 | NaN | NaN | Cash | In-store | 2023-09-13 |
| 7686 | TXN_6905143 | Coffee | 5.0 | NaN | NaN | Cash | NaN | 2023-08-09 |
| 8021 | TXN_2428781 | Salad | NaN | 5.0 | NaN | NaN | In-store | 2023-05-09 |
| 8128 | TXN_6105807 | Smoothie | 3.0 | NaN | NaN | Credit Card | Takeaway | 2023-01-18 |
| 8443 | TXN_2023651 | Sandwich | NaN | 4.0 | NaN | Cash | In-store | 2023-05-25 |
| 8465 | TXN_9669616 | Coffee | NaN | 2.0 | NaN | NaN | NaN | 2023-06-03 |
| 8479 | TXN_1547245 | Sandwich | NaN | 4.0 | NaN | NaN | Takeaway | 2023-09-11 |
| 8497 | TXN_1525583 | Sandwich | 3.0 | NaN | NaN | Cash | Takeaway | 2023-05-20 |
| 8574 | TXN_2546684 | Juice | NaN | 3.0 | NaN | Digital Wallet | Takeaway | 2023-04-08 |
| 8732 | TXN_4550558 | Cookie | NaN | 1.0 | NaN | Credit Card | In-store | 2023-08-04 |
| 9212 | TXN_7322317 | Tea | 1.0 | NaN | NaN | NaN | NaN | 2023-03-16 |
| 9590 | TXN_9924732 | Sandwich | NaN | 4.0 | NaN | Credit Card | In-store | 2023-01-18 |
| 9869 | TXN_1975184 | Coffee | NaN | 2.0 | NaN | Digital Wallet | NaN | 2023-01-15 |
| 9893 | TXN_3809533 | Juice | 2.0 | NaN | NaN | Digital Wallet | Takeaway | 2023-02-02 |
# Drop Unfixed Data
my_data = my_data.dropna(subset=["Quantity","Total Spent", "Price Per Unit"]).copy()# From 10000 to 9942, we didn't lose too much rows, but rather we fix a lot
my_data.shape(9942, 8)
## Recover the Item Null value from Price Per Unit ##
# We can do it by mapping the Price back too the Item Name
# What we will do, is to create a dict of these values, and then we will affect them, and fix those missing values in Item columtemp_my_data = my_data[["Item","Price Per Unit"]].drop_duplicates(subset=["Item"])
temp_my_data| Item | Price Per Unit | |
|---|---|---|
| 0 | Coffee | 2.0 |
| 1 | Cake | 3.0 |
| 2 | Cookie | 1.0 |
| 3 | Salad | 5.0 |
| 5 | Smoothie | 4.0 |
| 6 | NaN | 3.0 |
| 7 | Sandwich | 4.0 |
| 17 | Juice | 3.0 |
| 42 | Tea | 1.5 |
# There's a NaN there, we will ignore it, we now can create the dict
# We can see that Cake and Juice have the same value, it's fine, we alter between them, same goes for Smoothie and Sandwich
items = {
"Coffee":2,
"Cake":3,
'Cookie':1,
"Salad":5,
"Smoothie":4,
"Sandwich":4,
"Juice":3,
"Tea":1.5
}
reverse_items = {
price : item
for item, price in items.items()
}
print(reverse_items){2: 'Coffee', 3: 'Juice', 1: 'Cookie', 5: 'Salad', 4: 'Sandwich', 1.5: 'Tea'}
my_data["Item"] = my_data["Item"].fillna(
my_data["Price Per Unit"].map(reverse_items)
)my_data.isnull().sum()Transaction ID 0 Item 0 Quantity 0 Price Per Unit 0 Total Spent 0 Payment Method 3158 Location 3940 Transaction Date 457 dtype: int64
## Fill Null Payment Method and Location into Unknown ##my_data["Payment Method"] = my_data["Payment Method"].fillna("Unknown")
my_data["Location"] = my_data["Location"].fillna("Unknown")
# drop rows that does not have transaction date
my_data = my_data.dropna(subset=["Transaction Date"])my_data.isnull().sum()Transaction ID 0 Item 0 Quantity 0 Price Per Unit 0 Total Spent 0 Payment Method 0 Location 0 Transaction Date 0 dtype: int64
my_data.shape(9485, 8)
# Save your DataFrame to a new file named 'cleaned_cafe_sales.csv'
my_data.to_csv('cleaned_cafe_sales.csv', index=False)Project Summary
A systematic overview of how the pipeline was designed, processed, and visualized to support data-driven decision making.
1. Stakeholder Business Goals
Collaborated with the Cafe Owner and Marketing Team to define clear performance indicators. The primary goal was to empower them with key insights to monitor daily revenue, identify best-selling offerings, and build effective, data-driven marketing strategies.
2. Data Cleaning & Integrity
The raw sales dataset contained massive amounts of missing, UNKNOWN, and ERROR entries. Standardized transaction records using Python and Pandas. This data cleaning step was vital to ensure the dataset met the integrity constraints required for downstream processing.
3. Power BI Visualization
Transformed the finalized cleaned datasets into an interactive Power BI Dashboard. The visuals were customized to be highly intuitive, ensuring that business stakeholders can easily scan metrics, interpret patterns, and track cash-flow trends.