Mathew K Analytics

Lesson 15 · Python for Retail E-commerce Analytics

Assessing Retail Data Quality in Python for E-commerce Analytics | Step-by-Step Guide

Understand why clean and accurate retail data drives business decisions Learn how to detect and resolve common retail data quality issues Practice finding…

⬇ Download notebookOpen in Colab ↗

What you'll learn

Data

No separate download needed — the notebook creates or downloads everything it uses.

📓 Full notebook

Download .ipynb

Assessing Retail Data Quality in Python#

  • Understand why clean and accurate retail data drives business decisions
  • Learn how to detect and resolve common retail data quality issues
  • Practice finding missing, duplicate, and inconsistent sales records
  • Gain skills to power reliable sales and marketing analytics
  • Produce checks and metrics that boost trust in your data
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings('ignore')

Core Concepts in Retail Data Quality#

  • Retail data includes transactions, orders, products, and customers
  • Sales records connect what was bought, by whom, and when
  • Revenue is calculated as quantity times price per item
  • Data errors may include missing values, wrong types, or duplicate entries
  • Mistakes can lead to bad business decisions
# Beginner Example 1: Load retail transactions data
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/00502/online_retail_II.xlsx'
df_retail = pd.read_excel(url, sheet_name='Year 2010-2011')
df_retail['InvoiceDate'] = pd.to_datetime(df_retail['InvoiceDate'])
print('Shape:', df_retail.shape)
print(df_retail.head(3))
Shape: (541910, 8)
  Invoice StockCode                         Description  Quantity  \
0  536365    85123A  WHITE HANGING HEART T-LIGHT HOLDER         6   
1  536365     71053                 WHITE METAL LANTERN         6   
2  536365    84406B      CREAM CUPID HEARTS COAT HANGER         8   

          InvoiceDate  Price  Customer ID         Country  
0 2010-12-01 08:26:00   2.55      17850.0  United Kingdom  
1 2010-12-01 08:26:00   3.39      17850.0  United Kingdom  
2 2010-12-01 08:26:00   2.75      17850.0  United Kingdom  
# Beginner Example 2: Check for missing values
missing_counts = df_retail.isnull().sum()
print('Missing values by column:')
print(missing_counts)
Missing values by column:
Invoice             0
StockCode           0
Description      1454
Quantity            0
InvoiceDate         0
Price               0
Customer ID    135080
Country             0
dtype: int64
# Beginner Example 3: Count duplicated invoices and lines
dup_invoices = df_retail['Invoice'].duplicated().sum()
dup_fullrows = df_retail.duplicated().sum()
print('Duplicate invoices:', dup_invoices)
print('Fully duplicated rows:', dup_fullrows)
Duplicate invoices: 516010
Fully duplicated rows: 5268
# Intermediate Example 1: Summarize null Customer IDs by Country
cust_missing_by_country = df_retail[df_retail['Customer ID'].isnull()]['Country'].value_counts()
print('Missing Customer IDs, by Country:')
print(cust_missing_by_country.head())
Missing Customer IDs, by Country:
Country
United Kingdom    133600
EIRE                 711
Hong Kong            288
Unspecified          202
Switzerland          125
Name: count, dtype: int64
# Intermediate Example 2: Find transactions with negative or zero Quantity
invalid_qty = df_retail[df_retail['Quantity'] <= 0]
print('Transactions with invalid (negative or zero) quantity:')
print(invalid_qty[['Invoice', 'Description', 'Quantity']].head())
Transactions with invalid (negative or zero) quantity:
     Invoice                       Description  Quantity
141  C536379                          Discount        -1
154  C536383   SET OF 3 COLOURED  FLYING DUCKS        -1
235  C536391    PLASTERS IN TIN CIRCUS PARADE        -12
236  C536391  PACK OF 12 PINK PAISLEY TISSUES        -24
237  C536391  PACK OF 12 BLUE PAISLEY TISSUES        -24
# Intermediate Example 3: Look for price outliers
Q1 = df_retail['Price'].quantile(0.25)
Q3 = df_retail['Price'].quantile(0.75)
IQR = Q3 - Q1
outlier_rows = df_retail[(df_retail['Price'] < (Q1 - 1.5 * IQR)) | (df_retail['Price'] > (Q3 + 1.5 * IQR))]
print('Prices below Q1-1.5*IQR or above Q3+1.5*IQR:')
print(outlier_rows[['StockCode', 'Description', 'Price']].head())
Prices below Q1-1.5*IQR or above Q3+1.5*IQR:
    StockCode                      Description  Price
20      22622   BOX OF VINTAGE ALPHABET BLOCKS   9.95
45       POST                          POSTAGE  18.00
65      21258       VICTORIAN SEWING BOX LARGE  10.95
141         D                         Discount  27.50
151     22839  3 TIER CAKE TIN GREEN AND CREAM  14.95
# Intermediate Example 4: Check unique StockCodes per product description
desc_code_counts = df_retail.groupby('Description')['StockCode'].nunique().sort_values(ascending=False)
print('Descriptions associated with multiple StockCodes:')
print(desc_code_counts.head())
Descriptions associated with multiple StockCodes:
Description
check      146
?           47
damages     43
damaged     43
found       25
Name: StockCode, dtype: int64
# Intermediate Example 5: Detect invoices with missing descriptive information
missing_desc = df_retail[df_retail['Description'].isnull()]
print('Invoices with missing product descriptions:')
print(missing_desc[['Invoice', 'StockCode', 'Description']].head())
Invoices with missing product descriptions:
     Invoice StockCode Description
622   536414     22139         NaN
1510  536545     21134         NaN
1985  536547     37509         NaN
1986  536546     22145         NaN
2022  536552     20950         NaN
# Intermediate Example 6: Summarize unique countries in data
unique_countries = df_retail['Country'].nunique()
print('Number of unique countries in this sales data:', unique_countries)
Number of unique countries in this sales data: 38
# Advanced Example 1: Combine with a product catalog and spot mismatches
np.random.seed(42)
categories = ['Electronics','Clothing','Home','Sports','Beauty']
product_ids = list(range(1001,1101))
product_categories = np.random.choice(categories,100)
product_prices = np.round(np.random.uniform(5,500,100),2)
catalog = pd.DataFrame({'StockCode':product_ids,'Category':product_categories,'Cat_Price':product_prices})
joined = df_retail.merge(catalog, how='left', left_on='StockCode', right_on='StockCode')
missing_catalogs = joined[joined['Category'].isnull()]
print('Retail transactions missing a product catalog entry:')
print(missing_catalogs[['Invoice', 'StockCode', 'Description']].head())
Retail transactions missing a product catalog entry:
  Invoice StockCode                          Description
0  536365    85123A   WHITE HANGING HEART T-LIGHT HOLDER
1  536365     71053                  WHITE METAL LANTERN
2  536365    84406B       CREAM CUPID HEARTS COAT HANGER
3  536365    84029G  KNITTED UNION FLAG HOT WATER BOTTLE
4  536365    84029E       RED WOOLLY HOTTIE WHITE HEART.
# Advanced Example 2: Create a data quality summary table
quality_summary = pd.DataFrame({
    'missing_customer_id': [df_retail['Customer ID'].isnull().mean()],
    'missing_description': [df_retail['Description'].isnull().mean()],
    'duplicate_invoices': [df_retail['Invoice'].duplicated().mean()],
    'negative_quantity': [(df_retail['Quantity'] <= 0).mean()]
})
print('Fraction of problem rows by issue type:')
print((quality_summary * 100).round(2), '%')
Fraction of problem rows by issue type:
   missing_customer_id  missing_description  duplicate_invoices  \
0                24.93                 0.27               95.22   

   negative_quantity  
0               1.96   %
# Advanced Example 3: Visual inspection for missing values
import matplotlib.pyplot as plt
null_counts = df_retail.isnull().mean().sort_values(ascending=False)
plt.figure(figsize=(10,4))
null_counts.plot(kind='bar', color='cornflowerblue')
plt.title('Fraction of Missing Values per Column')
plt.xlabel('Column')
plt.ylabel('Fraction Missing')
plt.show()
No description has been provided for this image

Error Handling and Debugging in Retail Data#

  • Spot empty rows and why they break sales analysis
  • Look for dropped transactions after a groupby or merge
  • Learn how wrong units (quantity, currency) confuse reports
  • Practice debugging with pandas info() and describe()
# Error Example 1: Use info() to debug missing data
df_retail.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 541910 entries, 0 to 541909
Data columns (total 8 columns):
 #   Column       Non-Null Count   Dtype         
---  ------       --------------   -----         
 0   Invoice      541910 non-null  object        
 1   StockCode    541910 non-null  object        
 2   Description  540456 non-null  object        
 3   Quantity     541910 non-null  int64         
 4   InvoiceDate  541910 non-null  datetime64[ns]
 5   Price        541910 non-null  float64       
 6   Customer ID  406830 non-null  float64       
 7   Country      541910 non-null  object        
dtypes: datetime64[ns](1), float64(2), int64(1), object(4)
memory usage: 33.1+ MB
# Error Example 2: Use describe() to surface unusual values
desc_stats = df_retail.describe(include='all')
print(desc_stats.T[['count', 'unique', 'top', 'freq']] if 'unique' in desc_stats.T.columns else desc_stats)
                count   unique                                 top    freq
Invoice      541910.0  25900.0                            573585.0  1114.0
StockCode      541910     4070                              85123A    2313
Description    540456     4223  WHITE HANGING HEART T-LIGHT HOLDER    2369
Quantity     541910.0      NaN                                 NaN     NaN
InvoiceDate    541910      NaN                                 NaN     NaN
Price        541910.0      NaN                                 NaN     NaN
Customer ID  406830.0      NaN                                 NaN     NaN
Country        541910       38                      United Kingdom  495478
# Error Example 3: Check if grouping drops transactions
sales_by_invoice = df_retail.groupby('Invoice').agg({'Quantity': 'sum', 'Price': 'mean'})
missing_invoices = set(df_retail['Invoice']) - set(sales_by_invoice.index)
print('Invoices missing after groupby:', len(missing_invoices))
Invoices missing after groupby: 0
# Error Example 4: Detect wrong unitsidentify outliers in Quantity
q_low = df_retail['Quantity'].quantile(0.01)
q_high = df_retail['Quantity'].quantile(0.99)
suspect_units = df_retail[(df_retail['Quantity'] < q_low) | (df_retail['Quantity'] > q_high)]
print('Possible unit errors in Quantity:')
print(suspect_units[['Invoice', 'Description', 'Quantity']].head())
Possible unit errors in Quantity:
    Invoice                      Description  Quantity
96   536378  PACK OF 72 RETROSPOT CAKE CASES       120
178  536387                    CHILLI LIGHTS       192
179  536387   LIGHT GARLAND BUTTERFILES PINK       192
180  536387       WOODEN OWLS LIGHT GARLAND        192
181  536387    FAIRY TALE COTTAGE NIGHTLIGHT       432

Best Practices & Proven Patterns for Retail Data Quality#

  • Always check missing values, duplicates, and data types before analysis
  • Segment customers to isolate quality issues in key business groups
  • Compare product catalog and transaction records for alignment
  • Use dashboard visualizations to monitor cleanliness over time
  • Automate data validation for ongoing reporting
# Pattern Example 1: Segmenting customers by data quality
seg_q = (df_retail.assign(is_missing_customer=df_retail['Customer ID'].isnull())
    .groupby('Country')['is_missing_customer'].mean().sort_values(ascending=False))
print('Fraction of transactions with no customer per country:')
print((seg_q*100).round(2))
Fraction of transactions with no customer per country:
Country
Hong Kong               100.00
Unspecified              45.29
United Kingdom           26.96
Israel                   15.82
Bahrain                  10.53
EIRE                      8.67
Switzerland               6.24
Portugal                  2.57
France                    0.77
Canada                    0.00
Australia                 0.00
Brazil                    0.00
Belgium                   0.00
Austria                   0.00
Finland                   0.00
European Community        0.00
Greece                    0.00
Germany                   0.00
Denmark                   0.00
Czech Republic            0.00
Channel Islands           0.00
Cyprus                    0.00
Lebanon                   0.00
Japan                     0.00
Italy                     0.00
Iceland                   0.00
Norway                    0.00
Lithuania                 0.00
Netherlands               0.00
Malta                     0.00
Saudi Arabia              0.00
RSA                       0.00
Poland                    0.00
Singapore                 0.00
Sweden                    0.00
Spain                     0.00
United Arab Emirates      0.00
USA                       0.00
Name: is_missing_customer, dtype: float64
# Pattern Example 2: Validate that product codes match catalog
valid_refs = df_retail['StockCode'].isin(catalog['StockCode'])
valid_pct = valid_refs.mean()
print('Percentage of retail rows with a matching product catalog code:', round(valid_pct*100,2),'%')
Percentage of retail rows with a matching product catalog code: 0.0 %
# Pattern Example 3: Market basket analysis - check data shape and missingness
np.random.seed(42)
products = ['Bread','Milk','Butter','Eggs','Apples','Chicken','Rice','Cheese']
transaction_ids = np.repeat(np.arange(1,301),3)
product_choices = np.random.choice(products,len(transaction_ids))
df_basket = pd.DataFrame({'TransactionID':transaction_ids,'Product':product_choices})
print('Market basket dataset shape:', df_basket.shape)
print('Missing values in basket data:', df_basket.isnull().sum().sum())
Market basket dataset shape: (900, 2)
Missing values in basket data: 0
# Pattern Example 4: Do a seasonal trend check on sales dates
df_retail['Month'] = df_retail['InvoiceDate'].dt.month
monthly_count = df_retail.groupby('Month').size()
print('Transactions per month:')
print(monthly_count)
Transactions per month:
Month
1     35147
2     27707
3     36748
4     29916
5     37030
6     36874
7     39518
8     35284
9     50226
10    60742
11    84711
12    68007
dtype: int64

End-to-End Data Quality Workflow: Find Top-Selling Products with Clean Data#

  • Remove sales with missing product or customer info
  • Drop duplicate and invalid entries
  • Aggregate total sales by product
  • Find which products sold the most
  • Deliver clear, reliable insight for the business team
# Workflow Step 1: Filter out incomplete or duplicate transactions
clean_sales = (df_retail
    .dropna(subset=['Description','Customer ID'])
    .drop_duplicates()
    .loc[df_retail['Quantity']>0]
)
print('Cleaned transactions shape:', clean_sales.shape)
Cleaned transactions shape: (392733, 9)
# Workflow Step 2: Aggregate total sales by product
clean_sales['Revenue'] = clean_sales['Quantity'] * clean_sales['Price']
product_sales = (clean_sales.groupby('Description')['Revenue'].sum()
    .sort_values(ascending=False))
print('Top 5 selling products by revenue:')
print(product_sales.head(5))
Top 5 selling products by revenue:
Description
PAPER CRAFT , LITTLE BIRDIE           168469.60
REGENCY CAKESTAND 3 TIER              142264.75
WHITE HANGING HEART T-LIGHT HOLDER    100392.10
JUMBO BAG RED RETROSPOT                85040.54
MEDIUM CERAMIC TOP STORAGE JAR         81416.73
Name: Revenue, dtype: float64
 

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.