Back to Portfolio

Cafe Performance Insights

Helping Cafe Owners transform raw sales data into actionable insights and data-driven strategies.

Tech Stack

PythonPandasPower BIData Cleaning

Coffee Sales Dashboard.pbix

Interactive Preview

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

In [1]:
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings("ignore", category=RuntimeWarning)

print("Setup Complete")
Out [1]:
Setup Complete
In [2]:
# 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"
In [3]:
# Fill in the line below: Read the file into a variable my_data
my_data = pd.read_csv(my_filepath)
In [4]:
# Print the first five rows of the data
my_data.head()
Out [4]:
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

In [6]:
# Visualize the unique values in my dataset.
for col in my_data.columns:
    print("\n")
    print("=====", col, "=====")
    print(my_data[col].value_counts())
Out [6]:


===== 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
In [7]:
# Replace it into NaN:
my_data = my_data.replace(["UNKNOWN", "ERROR"], np.nan)
In [8]:
my_data.dtypes
Out [8]:
Transaction ID      object
Item                object
Quantity            object
Price Per Unit      object
Total Spent         object
Payment Method      object
Location            object
Transaction Date    object
dtype: object
In [9]:
# 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"])
In [10]:
#Checks for Error, since we have replaced it into NaN (Null)
my_data.isnull().sum()
Out [10]:
Transaction ID         0
Item                 969
Quantity             479
Price Per Unit       533
Total Spent          502
Payment Method      3178
Location            3961
Transaction Date     460
dtype: int64
In [11]:
## Fill missing Quantity, Rule : Quantity = (Total Spent * Price Per Unit)
In [13]:
# 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]
Out [13]:
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

In [14]:
# 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"]
)
In [15]:
my_data.isnull().sum()
Out [15]:
Transaction ID         0
Item                 969
Quantity              38
Price Per Unit       533
Total Spent          502
Payment Method      3178
Location            3961
Transaction Date     460
dtype: int64
In [15]:
## Fill missing Price per Unit ## 
In [16]:
price_filldata = (
    my_data["Price Per Unit"].isnull()
    &
    my_data[["Quantity","Total Spent"]].notna().all(axis=1)
)
In [17]:
my_data.loc[price_filldata, "Price Per Unit"] = (
    my_data.loc[price_filldata, "Total Spent"]
    / 
    my_data.loc[price_filldata, "Quantity"]
)
In [18]:
my_data.isnull().sum()
Out [18]:
Transaction ID         0
Item                 969
Quantity              38
Price Per Unit        38
Total Spent          502
Payment Method      3178
Location            3961
Transaction Date     460
dtype: int64
In [19]:
## Fill missing Total Spent ## 
In [19]:
spent_filldata = (
    my_data["Total Spent"].isnull()
    &
    my_data[["Quantity","Price Per Unit"]].notna().all(axis=1)
)
In [20]:
# 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"]
)
In [21]:
my_data.isnull().sum()
Out [21]:
Transaction ID         0
Item                 969
Quantity              38
Price Per Unit        38
Total Spent           40
Payment Method      3178
Location            3961
Transaction Date     460
dtype: int64
In [22]:
Total_Spent = my_data[my_data["Total Spent"].isnull()]
Total_Spent
Out [22]:
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
In [23]:
# Drop Unfixed Data
my_data = my_data.dropna(subset=["Quantity","Total Spent", "Price Per Unit"]).copy()
In [24]:
# From 10000 to 9942, we didn't lose too much rows, but rather we fix a lot
my_data.shape
Out [24]:
(9942, 8)
In [25]:
## 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 colum
In [26]:
temp_my_data = my_data[["Item","Price Per Unit"]].drop_duplicates(subset=["Item"])
temp_my_data
Out [26]:
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
In [27]:
# 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)
Out [27]:
{2: 'Coffee', 3: 'Juice', 1: 'Cookie', 5: 'Salad', 4: 'Sandwich', 1.5: 'Tea'}
In [29]:
my_data["Item"] = my_data["Item"].fillna(
    my_data["Price Per Unit"].map(reverse_items)
)
In [30]:
my_data.isnull().sum()
Out [30]:
Transaction ID         0
Item                   0
Quantity               0
Price Per Unit         0
Total Spent            0
Payment Method      3158
Location            3940
Transaction Date     457
dtype: int64
In [31]:
## Fill Null Payment Method and Location into Unknown ##
In [31]:
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"])
In [32]:
my_data.isnull().sum()
Out [32]:
Transaction ID      0
Item                0
Quantity            0
Price Per Unit      0
Total Spent         0
Payment Method      0
Location            0
Transaction Date    0
dtype: int64
In [33]:
my_data.shape
Out [33]:
(9485, 8)
In [34]:
# 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.