Lesson 1 · Real-World Data Analytics
Python Data Analytics #01: Acquiring & Auditing a Real Retail Dataset (Pandas Tutorial)
Video one of a hundred-video real-world data analytics series. The real dataset: over half a million actual transaction line items from a real UK-based…
- CourseReal-World Data Analytics
- Lesson1 of 26
- Video28 min
- FormatJupyter notebook · 30 code cells
- Data1 dataset
What you'll learn
Datasets used in this lesson
Save these next to the notebook. In Google Colab, upload them with the 📁 icon on the left first.
- online_retail.csv45.5 MB
📓 Full notebook
Download .ipynbData Analytics 100, Video 1: Acquiring and Auditing a Real Retail Transactions Dataset#
- Video one of a hundred-video real-world data analytics series.
- The real dataset: over half a million actual transaction line items from a real UK-based online gift retailer.
- Before any analysis, a real analyst always audits the data first. Let's get into it.
Part 1: Where This Real Data Actually Comes From#
import pandas as pd
df = pd.read_csv('online_retail.csv', parse_dates=['InvoiceDate'])
df.shape
Part 2: The Real Columns and Their Types#
df.columns.tolist()
df.dtypes
Part 3: A Real First Look at the Rows#
df.head(10)
Part 4: Auditing Real Missing Values#
df.isna().sum()
df.isna().mean().round(3) * 100
Part 5: Investigating the Real Missing Customer IDs#
missing_cust = df[df['CustomerID'].isna()]
missing_cust.shape[0]
missing_cust['Country'].value_counts().head()
Part 6: Real Cancelled Invoices#
is_cancelled = df['InvoiceNo'].astype(str).str.startswith('C')
is_cancelled.sum()
df[is_cancelled].head()
Part 7: Real Negative Quantities#
(df['Quantity'] < 0).sum()
(df.loc[df['Quantity'] < 0, 'InvoiceNo'].astype(str).str.startswith('C')).mean().round(3) * 100
Part 8: Real Zero and Negative Prices#
(df['UnitPrice'] <= 0).sum()
df.loc[df['UnitPrice'] <= 0, 'Description'].value_counts().head(10)
Part 9: Real Duplicate Rows#
df.duplicated().sum()
df[df.duplicated(keep=False)].sort_values('InvoiceNo').head(6)
Part 10: Real Non-Product Stock Codes#
non_numeric = df[~df['StockCode'].astype(str).str[0].str.isdigit()]
non_numeric['StockCode'].value_counts().head(10)
non_numeric.shape[0]
Part 11: Real Country Distribution#
df['Country'].nunique()
df['Country'].value_counts().head(10)
(df['Country'] == 'United Kingdom').mean().round(3) * 100
Part 12: The Real Date Range#
df['InvoiceDate'].min(), df['InvoiceDate'].max()
df.set_index('InvoiceDate').resample('ME').size()
Part 13: The Naive Revenue Trap#
naive_revenue = (df['Quantity'] * df['UnitPrice']).sum()
round(naive_revenue, 2)
cancelled_revenue = (df.loc[is_cancelled, 'Quantity'] * df.loc[is_cancelled, 'UnitPrice']).sum()
round(cancelled_revenue, 2)
Part 14: Building a Real Cleaning Function#
def clean_retail(raw):
out = raw.copy()
out = out[~out['InvoiceNo'].astype(str).str.startswith('C')]
out = out[out['Quantity'] > 0]
out = out[out['UnitPrice'] > 0]
out = out[out['StockCode'].astype(str).str[0].str.isdigit()]
out = out.dropna(subset=['CustomerID'])
out = out.drop_duplicates()
out['CustomerID'] = out['CustomerID'].astype(int)
out['Revenue'] = out['Quantity'] * out['UnitPrice']
return out.reset_index(drop=True)
Part 15: Applying It to the Real Data#
clean = clean_retail(df)
clean.shape
round(100 * (1 - clean.shape[0] / df.shape[0]), 2)
clean_revenue = clean['Revenue'].sum()
round(clean_revenue, 2)
round(naive_revenue - clean_revenue, 2)
Part 16: Real Top Products, Now That the Data Is Trustworthy#
clean.groupby('Description')['Revenue'].sum().sort_values(ascending=False).head(10)
clean.groupby('CustomerID')['Revenue'].sum().sort_values(ascending=False).head(10)
Part 17: The Real Scope Now on the Table#
clean['CustomerID'].nunique()
clean['InvoiceNo'].nunique()
clean['InvoiceDate'].min(), clean['InvoiceDate'].max()
Part 18: Saving the Real Cleaned Dataset#
clean.to_csv('online_retail_clean.csv', index=False)
reloaded = pd.read_csv('online_retail_clean.csv', parse_dates=['InvoiceDate'])
reloaded.shape == clean.shape
Part 19: One Real Chart Before We Wrap Up#
import matplotlib.pyplot as plt
top_countries = clean['Country'].value_counts().head(10)
plt.figure(figsize=(9, 5))
plt.bar(top_countries.index, top_countries.values, color='steelblue')
plt.xticks(rotation=45, ha='right')
plt.title('Real Cleaned Transaction Rows by Country')
plt.tight_layout()
plt.savefig('retail_top_countries.png', dpi=120)
plt.close()
Part 20: Real Basket Size per Invoice#
basket_size = clean.groupby('InvoiceNo').size()
basket_size.describe()
basket_size.sort_values(ascending=False).head(5)
Part 21: Real Repeat vs One-Time Customers#
orders_per_customer = clean.groupby('CustomerID')['InvoiceNo'].nunique()
(orders_per_customer == 1).sum()
(orders_per_customer > 1).sum()
round(100 * (orders_per_customer > 1).mean(), 2)
Part 22: Real StockCode-to-Description Consistency#
desc_per_code = clean.groupby('StockCode')['Description'].nunique()
(desc_per_code > 1).sum()
desc_per_code.sort_values(ascending=False).head(5)
clean.loc[clean['StockCode'] == desc_per_code.idxmax(), 'Description'].unique()
Part 23: Real Average Order Value#
order_value = clean.groupby('InvoiceNo')['Revenue'].sum()
order_value.mean().round(2)
order_value.median().round(2)
Part 24: Real Revenue by Day of the Week#
clean['DayOfWeek'] = clean['InvoiceDate'].dt.day_name()
clean.groupby('DayOfWeek')['Revenue'].sum().sort_values(ascending=False)
Part 25: Real Quantity and Price Extremes#
clean[['Quantity', 'UnitPrice']].describe()
clean.sort_values('Quantity', ascending=False)[['Description', 'Quantity', 'CustomerID']].head(5)
Part 26: A Real Written Summary, Saved to Disk#
summary_lines = [
f'Raw rows: {df.shape[0]}',
f'Cleaned rows: {clean.shape[0]}',
f'Rows removed by the audit: {df.shape[0] - clean.shape[0]}',
f'Naive revenue: {round(naive_revenue, 2)}',
f'Trustworthy cleaned revenue: {round(clean_revenue, 2)}',
f'Unique real customers: {clean["CustomerID"].nunique()}',
]
with open('online_retail_audit_summary.txt', 'w') as f:
for line in summary_lines:
f.write(line + chr(10))
for line in summary_lines:
print(line)
Part 27: Confirming the Real Summary File#
import os
os.path.exists('online_retail_audit_summary.txt')
os.path.getsize('online_retail_audit_summary.txt') > 0
Part 28: One Last Real Sanity Check#
clean.isna().sum().sum()
(clean['Quantity'] > 0).all() and (clean['UnitPrice'] > 0).all()
Wrap-Up: What You Learned#
- Real production data always needs an audit before any number gets reported: nulls, cancellations, negative quantities, zero prices, non-product codes, and duplicates.
- This real UK online retailer's cancelled invoices genuinely start with the letter C, and negative quantities mostly, but not entirely, overlap with them.
- A naive revenue sum on unaudited real data can be meaningfully wrong; the real cleaned figure is the one worth trusting.
- The real cleaned dataset is now saved once, so the rest of this retail series builds on trustworthy data from here on.
- Next video: real RFM segmentation, turning this same cleaned customer-and-revenue data into actual customer segments.
Found this useful?
All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.



