Appearance
🛠 Workshop — Data Cleaning Pipeline
Từ dataset e-commerce 10,000+ dòng "bẩn" → xây dựng pipeline cleaning 6 bước → xuất CSV sạch sẵn sàng cho EDA. Tất cả trong Jupyter Notebook!
🎯 Mục tiêu workshop
Sau khi hoàn thành workshop này, bạn sẽ:
- Audit dataset bằng
df.info(),df.describe(),isnull().sum()— phát hiện mọi vấn đề - Handle missing values — chọn strategy phù hợp cho từng cột (drop, fillna, interpolate)
- Remove duplicates & fix dtypes —
drop_duplicates(),astype(),pd.to_datetime() - Clean strings —
.str.strip(),.str.title(),.str.replace()cho tên, địa chỉ, phone - Merge 3 DataFrames —
pd.merge()orders + customers + products - Export & validate — xuất CSV sạch + kiểm tra data quality cuối cùng
🧰 Yêu cầu
| Yêu cầu | Chi tiết |
|---|---|
| Kiến thức | Đã học Buổi 7 (Python basics) + Buổi 8 lý thuyết (Pandas, Numpy) |
| Công cụ | Jupyter Notebook (local) HOẶC Google Colab (online) |
| Python | Python 3.8+ |
| Thư viện | pandas, numpy (Colab đã có sẵn) |
| Thời gian | 60–90 phút |
💡 Naming convention
Đặt tên notebook: HoTen_Buoi08_DataCleaning.ipynb Chia notebook thành Markdown sections rõ ràng — tuân thủ Notebook Best Practices đã học!
📦 Dataset: Dữ liệu E-commerce ShopVN
Mô tả
Bạn nhận được 3 files CSV từ team Data Engineering của sàn e-commerce ShopVN — dữ liệu tháng 01–02/2026. Dữ liệu được export từ nhiều hệ thống khác nhau nên có rất nhiều vấn đề về chất lượng.
Bảng 1: orders — Đơn hàng (10,000+ dòng)
| Cột | Kiểu mong đợi | Mô tả | Vấn đề data |
|---|---|---|---|
order_id | int | Mã đơn hàng (unique) | Có ~200 dòng duplicate do sync lỗi |
customer_id | str | Mã khách hàng | ~5% bị NaN (khách vãng lai) |
product_id | str | Mã sản phẩm | Có dòng viết "P001", có dòng "p001" |
quantity | int | Số lượng | ~2% bị NaN, có giá trị âm (-1, -3) |
unit_price | float/str | Đơn giá | Lưu dạng string "1,500,000", có "N/A" |
order_date | str | Ngày đặt hàng | 3 format: "2026-01-15", "15/01/2026", "Jan 15, 2026" |
status | str | Trạng thái | "completed", "Completed", "COMPLETED", "cancelled" |
payment_method | str | Phương thức thanh toán | "COD", "cod", "credit_card", "Credit Card" |
Bảng 2: customers — Khách hàng (8,500 dòng)
| Cột | Kiểu mong đợi | Mô tả | Vấn đề data |
|---|---|---|---|
customer_id | str | Mã khách hàng (unique) | Chuẩn, không lỗi |
name | str | Tên khách hàng | Dấu cách thừa, hoa/thường lộn xộn |
email | str | ~3% bị NaN | |
phone | str | Số điện thoại | Format lộn xộn: "0901234567", "090-123-4567", "+84..." |
region | str | Khu vực | "Bắc", "bắc", " Bắc ", "Miền Bắc" |
segment | str | Phân khúc | "Premium", "Standard", "Basic" |
Bảng 3: products — Sản phẩm (500 dòng)
| Cột | Kiểu mong đợi | Mô tả | Vấn đề data |
|---|---|---|---|
product_id | str | Mã sản phẩm (unique) | Chuẩn uppercase: "P001" |
product_name | str | Tên sản phẩm | OK |
category | str | Danh mục | "Điện Tử", "Thời Trang", "Mỹ Phẩm", "Gia Dụng", "Thực Phẩm" |
brand | str | Thương hiệu | ~8% bị NaN (no-brand products) |
cost_price | float | Giá vốn | OK |
Tạo dataset mẫu
Copy đoạn code sau vào Cell 1 của notebook:
python
# Cell 1: Tạo 3 DataFrames mẫu với intentional data issues
import pandas as pd
import numpy as np
np.random.seed(42)
# === BẢNG PRODUCTS (500 sản phẩm) ===
categories = ["Điện Tử", "Thời Trang", "Mỹ Phẩm", "Gia Dụng", "Thực Phẩm"]
brands_by_cat = {
"Điện Tử": ["Samsung", "Apple", "Dell", "Logitech", "Sony", None],
"Thời Trang": ["Uniqlo", "H&M", "Zara", "Local Brand", None],
"Mỹ Phẩm": ["Innisfree", "L'Oréal", "Maybelline", None, None],
"Gia Dụng": ["Sunhouse", "Kangaroo", "Panasonic", None],
"Thực Phẩm": ["Vinamilk", "TH True Milk", "Nestlé", None, None],
}
products_data = []
for i in range(500):
cat = categories[i % 5]
brand_list = brands_by_cat[cat]
products_data.append({
"product_id": f"P{i+1:03d}",
"product_name": f"Sản phẩm {cat} #{i+1}",
"category": cat,
"brand": np.random.choice(brand_list),
"cost_price": round(np.random.uniform(50000, 5000000), -3),
})
products = pd.DataFrame(products_data)
# === BẢNG CUSTOMERS (8,500 khách hàng) ===
first_names = ["Nguyễn", "Trần", "Lê", "Phạm", "Hoàng", "Vũ", "Đặng", "Bùi", "Đỗ", "Ngô"]
middle_names = ["Văn", "Thị", "Hoàng", "Minh", "Đức", "Thanh", "Quốc", "Anh"]
last_names = ["An", "Bình", "Cường", "Dung", "Em", "Phong", "Giang", "Hà", "Khoa", "Linh",
"Mai", "Nam", "Oanh", "Phúc", "Quân", "Sơn", "Tâm", "Uyên", "Vinh", "Xuân"]
regions_messy = ["Bắc", "bắc", " Bắc ", "Miền Bắc", "Nam", "nam", " Nam", "Miền Nam",
"Trung", "trung", "Miền Trung", " Trung "]
segments = ["Premium", "Standard", "Basic"]
customers_data = []
for i in range(8500):
fn = np.random.choice(first_names)
mn = np.random.choice(middle_names)
ln = np.random.choice(last_names)
# Messy name formats
name_formats = [
f" {fn} {mn} {ln} ", # extra spaces
f"{fn.upper()} {mn.upper()} {ln.upper()}", # ALL CAPS
f"{fn.lower()} {mn.lower()} {ln.lower()}", # all lower
f"{fn} {mn} {ln}", # double space
f"{fn} {mn} {ln}", # normal (30%)
]
name = np.random.choice(name_formats, p=[0.15, 0.15, 0.15, 0.15, 0.40])
phone_formats = [
f"09{np.random.randint(10000000, 99999999)}",
f"09{np.random.randint(10000000, 99999999)}"[:3] + "-" + f"{np.random.randint(100, 999)}" + "-" + f"{np.random.randint(1000, 9999)}",
f"+849{np.random.randint(10000000, 99999999)}"[:13],
]
customers_data.append({
"customer_id": f"C{i+1:04d}",
"name": name,
"email": f"user{i+1}@email.com" if np.random.random() > 0.03 else None,
"phone": np.random.choice(phone_formats),
"region": np.random.choice(regions_messy),
"segment": np.random.choice(segments, p=[0.2, 0.5, 0.3]),
})
customers = pd.DataFrame(customers_data)
# === BẢNG ORDERS (10,200 đơn hàng — có duplicates) ===
statuses_messy = ["completed", "Completed", "COMPLETED", "cancelled", "Cancelled"]
payments_messy = ["COD", "cod", "credit_card", "Credit Card", "bank_transfer", "Bank Transfer"]
date_formats_funcs = [
lambda d: d.strftime("%Y-%m-%d"), # 2026-01-15
lambda d: d.strftime("%d/%m/%Y"), # 15/01/2026
lambda d: d.strftime("%b %d, %Y"), # Jan 15, 2026
]
orders_data = []
for i in range(10000):
cid = f"C{np.random.randint(1, 8501):04d}" if np.random.random() > 0.05 else None
pid = f"P{np.random.randint(1, 501):03d}"
pid = pid.lower() if np.random.random() > 0.7 else pid # 30% lowercase
base_date = pd.Timestamp("2026-01-01") + pd.Timedelta(days=np.random.randint(0, 59))
date_func = np.random.choice(date_formats_funcs)
date_str = date_func(base_date)
qty = int(np.random.choice([1, 2, 3, 4, 5, -1, -3], p=[0.35, 0.25, 0.15, 0.1, 0.05, 0.05, 0.05]))
price = round(np.random.uniform(50000, 15000000), -3)
# Make ~5% of prices messy strings
if np.random.random() < 0.05:
price_str = "N/A"
elif np.random.random() < 0.3:
price_str = f"{int(price):,}" # "1,500,000" string format
else:
price_str = str(price)
orders_data.append({
"order_id": i + 1,
"customer_id": cid,
"product_id": pid,
"quantity": qty if np.random.random() > 0.02 else None,
"unit_price": price_str,
"order_date": date_str,
"status": np.random.choice(statuses_messy, p=[0.4, 0.15, 0.05, 0.3, 0.1]),
"payment_method": np.random.choice(payments_messy),
})
# Add ~200 duplicate rows (random copy)
dup_indices = np.random.choice(len(orders_data), size=200, replace=True)
for idx in dup_indices:
orders_data.append(orders_data[idx].copy())
np.random.shuffle(orders_data)
orders = pd.DataFrame(orders_data)
print("✅ Datasets đã được tạo thành công!")
print(f"📦 orders: {orders.shape[0]} rows × {orders.shape[1]} cols")
print(f"👤 customers: {customers.shape[0]} rows × {customers.shape[1]} cols")
print(f"📦 products: {products.shape[0]} rows × {products.shape[1]} cols")⚠️ Chạy Cell 1 trước!
Cell 1 tạo 3 DataFrames trong memory. Nếu Restart Kernel, cần chạy lại Cell 1 trước khi chạy các phần sau.
Phần 1: Data Audit — Khám bệnh cho dataset
Trước khi clean, phải biết dataset "bẩn" ở đâu. Đây là bước không bao giờ được bỏ qua.
Bước 1.1: Overview với info() và shape
python
# Cell 2: Data Audit — Overview
print("=" * 60)
print("📊 ORDERS TABLE AUDIT")
print("=" * 60)
print(f"Shape: {orders.shape}")
print()
orders.info()python
# Cell 3: Xem sample data
print("\n📋 First 5 rows:")
print(orders.head())
print("\n📋 Random 5 rows:")
print(orders.sample(5))Bước 1.2: Missing Values Audit
python
# Cell 4: Missing values audit
print("=" * 60)
print("🔍 MISSING VALUES AUDIT")
print("=" * 60)
for table_name, df in [("orders", orders), ("customers", customers), ("products", products)]:
print(f"\n📦 {table_name} ({df.shape[0]} rows):")
missing = df.isnull().sum()
missing_pct = (df.isnull().sum() / len(df) * 100).round(1)
missing_report = pd.DataFrame({
"missing_count": missing,
"missing_pct": missing_pct,
})
# Chỉ hiện cột có missing
missing_report = missing_report[missing_report["missing_count"] > 0]
if len(missing_report) > 0:
print(missing_report)
else:
print(" ✅ No missing values!")Bước 1.3: Data Quality Summary với describe()
python
# Cell 5: Descriptive statistics
print("=" * 60)
print("📈 DESCRIPTIVE STATISTICS — ORDERS")
print("=" * 60)
print(orders.describe(include="all"))Bước 1.4: Unique Values Check
python
# Cell 6: Check inconsistent values
print("=" * 60)
print("🔎 CONSISTENCY CHECK")
print("=" * 60)
print("\n📌 orders['status'] unique values:")
print(orders["status"].value_counts())
print("\n📌 orders['payment_method'] unique values:")
print(orders["payment_method"].value_counts())
print("\n📌 customers['region'] unique values:")
print(customers["region"].value_counts())
print("\n📌 orders['product_id'] sample — mixed case:")
print(orders["product_id"].head(20).tolist())💡 Kết quả mong đợi từ Audit
Sau khi chạy audit, bạn sẽ phát hiện:
- orders: ~5%
customer_idNaN, ~2%quantityNaN, ~5%unit_price= "N/A", 200+ duplicates, 3 date formats, mixed case status/payment/product_id - customers: ~3%
emailNaN, messy names, inconsistent regions - products: ~8%
brandNaN - → Viết ra danh sách vấn đề trước khi clean!
Phần 2: Handle Missing Values
Strategy: Không xóa bừa! Chọn phương pháp phù hợp cho từng cột.
Bước 2.1: Quyết định strategy
python
# Cell 7: Missing Values Strategy
"""
MISSING VALUES STRATEGY:
| Table | Column | Missing % | Strategy | Lý do |
|-----------|-------------|-----------|-------------------------|-------------------------------------|
| orders | customer_id | ~5% | fillna("GUEST") | Khách vãng lai — cần giữ đơn hàng |
| orders | quantity | ~2% | fillna(median) | Ít missing, median ổn định |
| orders | unit_price | ~5% | Xử lý ở bước dtype | "N/A" sẽ thành NaN khi convert |
| customers | email | ~3% | fillna("unknown@na.com")| Giữ customer record |
| products | brand | ~8% | fillna("No Brand") | No-brand products — hợp lý |
"""
print("Strategy defined ✅")Bước 2.2: Apply fillna
python
# Cell 8: Handle Missing Values
# --- ORDERS ---
# customer_id: khách vãng lai
orders["customer_id"] = orders["customer_id"].fillna("GUEST")
print(f"✅ orders.customer_id NaN after: {orders['customer_id'].isnull().sum()}")
# quantity: fillna với median (= 2)
qty_median = orders["quantity"].median()
print(f" quantity median = {qty_median}")
orders["quantity"] = orders["quantity"].fillna(qty_median)
print(f"✅ orders.quantity NaN after: {orders['quantity'].isnull().sum()}")
# --- CUSTOMERS ---
customers["email"] = customers["email"].fillna("unknown@na.com")
print(f"✅ customers.email NaN after: {customers['email'].isnull().sum()}")
# --- PRODUCTS ---
products["brand"] = products["brand"].fillna("No Brand")
print(f"✅ products.brand NaN after: {products['brand'].isnull().sum()}")
print("\n🎉 Missing values handled!")⚠️ Đừng quên unit_price!
unit_price có giá trị "N/A" (string) — isnull() không detect được. Cần xử lý ở bước Fix Data Types (Phần 3).
Phần 3: Remove Duplicates & Fix Data Types
Bước 3.1: Remove Duplicates
python
# Cell 9: Remove Duplicates
print("=" * 60)
print("🔄 REMOVE DUPLICATES")
print("=" * 60)
print(f"Before: {len(orders)} rows")
# Check duplicate count
dup_count = orders.duplicated(subset=["order_id"]).sum()
print(f"Duplicate order_ids: {dup_count}")
# Remove — keep first occurrence
orders = orders.drop_duplicates(subset=["order_id"], keep="first")
print(f"After: {len(orders)} rows")
print(f"Removed: {dup_count} duplicate rows ✅")Bước 3.2: Fix unit_price — String to Numeric
python
# Cell 10: Fix unit_price dtype
print("=" * 60)
print("🔧 FIX DATA TYPES — unit_price")
print("=" * 60)
print(f"Before dtype: {orders['unit_price'].dtype}")
print(f"Sample values: {orders['unit_price'].head(10).tolist()}")
# Step 1: Remove commas from string prices
orders["unit_price"] = orders["unit_price"].astype(str).str.replace(",", "", regex=False)
# Step 2: Convert to numeric — "N/A" becomes NaN
orders["unit_price"] = pd.to_numeric(orders["unit_price"], errors="coerce")
print(f"After dtype: {orders['unit_price'].dtype}")
print(f"NaN count (from 'N/A'): {orders['unit_price'].isnull().sum()}")
# Step 3: Fill price NaN with median by product_id (if possible) or global median
price_median = orders["unit_price"].median()
orders["unit_price"] = orders["unit_price"].fillna(price_median)
print(f"NaN after fillna: {orders['unit_price'].isnull().sum()}")
print(f"✅ unit_price is now {orders['unit_price'].dtype}")Bước 3.3: Fix quantity — Handle negative values
python
# Cell 11: Fix quantity — negative values
print("=" * 60)
print("🔧 FIX quantity — negative values")
print("=" * 60)
# Convert to int (in case it's float after fillna)
orders["quantity"] = orders["quantity"].astype(int)
# Check negative values
neg_qty = (orders["quantity"] < 0).sum()
print(f"Negative quantity count: {neg_qty}")
# Strategy: negative quantity likely means returns → convert to absolute
orders["quantity"] = orders["quantity"].abs()
print(f"After abs(): min={orders['quantity'].min()}, max={orders['quantity'].max()}")
print("✅ No more negative quantities")Bước 3.4: Fix order_date — Multiple formats to datetime
python
# Cell 12: Fix order_date — convert to datetime
print("=" * 60)
print("🔧 FIX order_date — multi-format to datetime")
print("=" * 60)
print(f"Before dtype: {orders['order_date'].dtype}")
print(f"Sample values: {orders['order_date'].head(10).tolist()}")
# pd.to_datetime with format="mixed" handles multiple formats
orders["order_date"] = pd.to_datetime(orders["order_date"], format="mixed", dayfirst=True)
print(f"After dtype: {orders['order_date'].dtype}")
print(f"Date range: {orders['order_date'].min()} → {orders['order_date'].max()}")
print("✅ order_date is now datetime64")Bước 3.5: Fix product_id — Uppercase
python
# Cell 13: Standardize product_id to uppercase
print("=" * 60)
print("🔧 FIX product_id — standardize case")
print("=" * 60)
print(f"Before — unique count: {orders['product_id'].nunique()}")
print(f"Sample: {orders['product_id'].head(10).tolist()}")
orders["product_id"] = orders["product_id"].str.upper()
print(f"After — unique count: {orders['product_id'].nunique()}")
print("✅ All product_ids are now uppercase")Phần 4: String Cleaning
Bước 4.1: Clean status & payment_method
python
# Cell 14: Standardize categorical columns
print("=" * 60)
print("🧹 STRING CLEANING — Categorical Columns")
print("=" * 60)
# --- status ---
print(f"Before status unique: {orders['status'].unique()}")
orders["status"] = orders["status"].str.lower().str.strip()
print(f"After status unique: {orders['status'].unique()}")
# --- payment_method ---
print(f"\nBefore payment unique: {orders['payment_method'].unique()}")
orders["payment_method"] = orders["payment_method"].str.lower().str.strip().str.replace(" ", "_", regex=False)
print(f"After payment unique: {orders['payment_method'].unique()}")
print("\n✅ Categorical columns standardized")Bước 4.2: Clean customer names
python
# Cell 15: Clean customer names
print("=" * 60)
print("🧹 STRING CLEANING — Customer Names")
print("=" * 60)
print(f"Before sample:\n{customers['name'].head(10).tolist()}")
# Chain: strip → remove special chars → normalize spaces → title case
customers["name"] = (
customers["name"]
.str.strip() # Remove leading/trailing spaces
.str.replace(r"[^a-zA-ZÀ-ỹ\s]", "", regex=True) # Remove special characters
.str.replace(r"\s+", " ", regex=True) # Normalize multiple spaces to single
.str.title() # Title Case
)
print(f"After sample:\n{customers['name'].head(10).tolist()}")
print("✅ Customer names cleaned")Bước 4.3: Clean region
python
# Cell 16: Standardize region
print("=" * 60)
print("🧹 STRING CLEANING — Region")
print("=" * 60)
print(f"Before unique: {customers['region'].unique()}")
# Strip + lower + map to standard values
customers["region"] = customers["region"].str.strip().str.lower()
region_mapping = {
"bắc": "Bắc",
"miền bắc": "Bắc",
"nam": "Nam",
"miền nam": "Nam",
"trung": "Trung",
"miền trung": "Trung",
}
customers["region"] = customers["region"].map(region_mapping)
print(f"After unique: {customers['region'].unique()}")
print(f"NaN after mapping: {customers['region'].isnull().sum()}")
print("✅ Region standardized to: Bắc, Nam, Trung")💡 Tại sao dùng .map() thay vì .replace()?
.map() đảm bảo chỉ có giá trị trong mapping được giữ — nếu có giá trị "bất ngờ" (VD: typo "Băc"), nó sẽ thành NaN → dễ phát hiện. .replace() giữ nguyên giá trị không match → có thể bỏ sót.
Phần 5: Merge DataFrames
Bước 5.1: Merge orders + customers
python
# Cell 17: Merge orders + customers
print("=" * 60)
print("🔗 MERGE — Orders + Customers")
print("=" * 60)
print(f"orders shape: {orders.shape}")
print(f"customers shape: {customers.shape}")
# Left join — giữ tất cả orders
df_merged = pd.merge(
orders,
customers,
on="customer_id",
how="left",
suffixes=("", "_customer"),
)
print(f"After merge shape: {df_merged.shape}")
print(f"Columns: {df_merged.columns.tolist()}")
# Check unmatched
unmatched = df_merged["name"].isnull().sum()
print(f"Orders without customer info: {unmatched} (includes GUEST orders)")Bước 5.2: Merge with products
python
# Cell 18: Merge with products
print("=" * 60)
print("🔗 MERGE — + Products")
print("=" * 60)
df_merged = pd.merge(
df_merged,
products,
on="product_id",
how="left",
suffixes=("", "_product"),
)
print(f"Final shape: {df_merged.shape}")
print(f"Columns: {df_merged.columns.tolist()}")
# Check unmatched products
unmatched_prod = df_merged["product_name"].isnull().sum()
print(f"Orders without product info: {unmatched_prod}")
print("\n📋 Final merged sample:")
print(df_merged.head())Bước 5.3: Calculate revenue
python
# Cell 19: Calculate revenue column
print("=" * 60)
print("💰 CALCULATE REVENUE")
print("=" * 60)
df_merged["revenue"] = df_merged["unit_price"] * df_merged["quantity"]
print(f"Revenue column dtype: {df_merged['revenue'].dtype}")
print(f"Total revenue: {df_merged['revenue'].sum():,.0f} VNĐ")
print(f"Average order value: {df_merged['revenue'].mean():,.0f} VNĐ")
print(f"Max single order: {df_merged['revenue'].max():,.0f} VNĐ")
print(f"Min single order: {df_merged['revenue'].min():,.0f} VNĐ")Phần 6: Export & Validate
Bước 6.1: Final Data Quality Check
python
# Cell 20: Final Data Quality Report
print("=" * 60)
print("📊 FINAL DATA QUALITY REPORT")
print("=" * 60)
print(f"\n📦 Dataset shape: {df_merged.shape}")
print(f"\n🔍 Data Types:")
print(df_merged.dtypes)
print(f"\n🔍 Missing Values:")
missing_final = df_merged.isnull().sum()
missing_final = missing_final[missing_final > 0]
if len(missing_final) > 0:
print(missing_final)
else:
print(" ✅ No missing values!")
print(f"\n🔍 Duplicates: {df_merged.duplicated(subset=['order_id']).sum()}")
print(f"\n🔍 Value Counts — status:")
print(df_merged["status"].value_counts())
print(f"\n🔍 Value Counts — region:")
print(df_merged["region"].value_counts())
print(f"\n📈 Revenue Summary:")
print(df_merged["revenue"].describe())Bước 6.2: Export to CSV
python
# Cell 21: Export cleaned dataset
output_file = "shopvn_orders_cleaned.csv"
# Select and reorder columns for export
export_cols = [
"order_id", "order_date", "customer_id", "name",
"region", "segment", "product_id", "product_name",
"category", "brand", "quantity", "unit_price",
"revenue", "status", "payment_method",
]
# Filter to only existing columns (in case merge didn't find all)
export_cols = [col for col in export_cols if col in df_merged.columns]
df_export = df_merged[export_cols].sort_values("order_date")
df_export.to_csv(output_file, index=False, encoding="utf-8-sig")
print(f"✅ Exported to {output_file}")
print(f"📦 {df_export.shape[0]} rows × {df_export.shape[1]} columns")
print(f"📋 Columns: {export_cols}")💡 Tại sao encoding="utf-8-sig"?
utf-8-sig thêm BOM (Byte Order Mark) vào đầu file — giúp Excel tự nhận diện UTF-8 khi mở file CSV. Nếu dùng utf-8 thuần, Excel sẽ hiển thị ký tự tiếng Việt bị lỗi.
🌟 Bonus Challenges
Bonus 1: Outlier Detection với Numpy
python
# Bonus Cell 1: Detect revenue outliers using IQR
import numpy as np
print("=" * 60)
print("🔍 BONUS 1: OUTLIER DETECTION — IQR Method")
print("=" * 60)
revenue = df_merged["revenue"].dropna().values
Q1 = np.percentile(revenue, 25)
Q3 = np.percentile(revenue, 75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
print(f"Q1: {Q1:,.0f}")
print(f"Q3: {Q3:,.0f}")
print(f"IQR: {IQR:,.0f}")
print(f"Lower bound: {lower_bound:,.0f}")
print(f"Upper bound: {upper_bound:,.0f}")
# Flag outliers
outlier_mask = (df_merged["revenue"] < lower_bound) | (df_merged["revenue"] > upper_bound)
outlier_count = outlier_mask.sum()
print(f"\n🚨 Outliers detected: {outlier_count} ({outlier_count/len(df_merged)*100:.1f}%)")
# View top outliers
if outlier_count > 0:
print("\nTop 5 outliers by revenue:")
print(df_merged[outlier_mask].nlargest(5, "revenue")[["order_id", "product_name", "revenue"]])Bonus 2: Pivot Table — Doanh thu theo Category × Region
python
# Bonus Cell 2: Pivot Table
print("=" * 60)
print("📊 BONUS 2: PIVOT TABLE — Revenue by Category × Region")
print("=" * 60)
# Filter completed orders only
df_completed = df_merged[df_merged["status"] == "completed"]
pivot = pd.pivot_table(
df_completed,
values="revenue",
index="category",
columns="region",
aggfunc="sum",
fill_value=0,
margins=True, # Add row/column totals
margins_name="Tổng",
)
# Format as VNĐ
print(pivot.applymap(lambda x: f"{x:,.0f}"))Bonus 3: Data Quality Score Function
python
# Bonus Cell 3: Data Quality Score Function
import numpy as np
def data_quality_score(df, id_column=None):
"""Tính điểm chất lượng dữ liệu theo 5 dimensions của ISO 8000.
Args:
df (pd.DataFrame): DataFrame cần đánh giá.
id_column (str, optional): Tên cột ID duy nhất.
Returns:
dict: Scores cho từng dimension (0-100) và overall score.
"""
scores = {}
# 1. Completeness (0-100): % cells không bị NaN
total_cells = df.shape[0] * df.shape[1]
non_null_cells = df.notna().sum().sum()
scores["completeness"] = round(non_null_cells / total_cells * 100, 1)
# 2. Uniqueness (0-100): % rows không bị duplicate
if id_column and id_column in df.columns:
dup_count = df.duplicated(subset=[id_column]).sum()
else:
dup_count = df.duplicated().sum()
scores["uniqueness"] = round((1 - dup_count / len(df)) * 100, 1)
# 3. Consistency (0-100): % cột có dtype đúng (không phải object khi nên là numeric/datetime)
numeric_cols = df.select_dtypes(include=[np.number]).columns
datetime_cols = df.select_dtypes(include=["datetime64"]).columns
typed_cols = len(numeric_cols) + len(datetime_cols)
total_cols = len(df.columns)
scores["consistency"] = round(typed_cols / total_cols * 100, 1) if total_cols > 0 else 100
# 4. Accuracy (0-100): % numeric values trong reasonable range (no negatives where unexpected)
if len(numeric_cols) > 0:
negative_counts = (df[numeric_cols] < 0).sum().sum()
total_numeric = df[numeric_cols].notna().sum().sum()
scores["accuracy"] = round((1 - negative_counts / total_numeric) * 100, 1) if total_numeric > 0 else 100
else:
scores["accuracy"] = 100.0
# 5. Timeliness: check if datetime columns are within expected range
if len(datetime_cols) > 0:
scores["timeliness"] = 100.0 # Simplified — assume timely if datetime exists
else:
scores["timeliness"] = 50.0 # No datetime = can't verify
# Overall score (weighted average)
weights = {"completeness": 0.3, "uniqueness": 0.25, "consistency": 0.2, "accuracy": 0.15, "timeliness": 0.1}
overall = sum(scores[dim] * weights[dim] for dim in weights)
scores["overall"] = round(overall, 1)
return scores
# Test on cleaned dataset
print("=" * 60)
print("📊 BONUS 3: DATA QUALITY SCORE")
print("=" * 60)
scores = data_quality_score(df_merged, id_column="order_id")
for dim, score in scores.items():
bar = "█" * int(score // 5) + "░" * (20 - int(score // 5))
emoji = "✅" if score >= 80 else "⚠️" if score >= 60 else "❌"
print(f" {emoji} {dim:15s} {bar} {score}%")📋 Deliverable
Sau khi hoàn thành workshop, nộp:
| # | File | Mô tả |
|---|---|---|
| 1 | HoTen_Buoi08_DataCleaning.ipynb | Jupyter Notebook hoàn chỉnh — chạy Restart & Run All thành công |
| 2 | shopvn_orders_cleaned.csv | Dataset đã clean — output từ Cell 21 |
📊 Rubric — Thang điểm
| Tiêu chí | Điểm | Mô tả |
|---|---|---|
| Data Audit (Phần 1) | 15 | info(), describe(), isnull().sum() đầy đủ |
| Missing Values (Phần 2) | 20 | Strategy hợp lý cho từng cột, fillna() đúng |
| Duplicates & Types (Phần 3) | 20 | drop_duplicates() đúng key, to_numeric/to_datetime chạy |
| String Cleaning (Phần 4) | 15 | Status/name/region chuẩn hóa, dùng .str methods |
| Merge (Phần 5) | 15 | pd.merge() đúng how/on, revenue tính đúng |
| Export & Quality (Phần 6) | 10 | CSV xuất thành công, quality report đầy đủ |
| Notebook Clean | 5 | Markdown sections, Restart & Run All OK, no debug cells |
| Bonus | +15 | Outlier detection (+5), Pivot table (+5), Quality score (+5) |
| Tổng | 100 + 15 bonus |
⚠️ Lưu ý quan trọng
- Restart & Run All trước khi nộp — notebook phải chạy từ đầu đến cuối không lỗi
- Mỗi phần có Markdown header giải thích mục đích
- Code có comments giải thích logic quan trọng
- Không có cell test/debug còn sót lại