Skip to content

🛠 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ẽ:

  1. Audit dataset bằng df.info(), df.describe(), isnull().sum() — phát hiện mọi vấn đề
  2. Handle missing values — chọn strategy phù hợp cho từng cột (drop, fillna, interpolate)
  3. Remove duplicates & fix dtypesdrop_duplicates(), astype(), pd.to_datetime()
  4. Clean strings.str.strip(), .str.title(), .str.replace() cho tên, địa chỉ, phone
  5. Merge 3 DataFramespd.merge() orders + customers + products
  6. Export & validate — xuất CSV sạch + kiểm tra data quality cuối cùng

🧰 Yêu cầu

Yêu cầuChi 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)
PythonPython 3.8+
Thư việnpandas, numpy (Colab đã có sẵn)
Thời gian60–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ộtKiểu mong đợiMô tảVấn đề data
order_idintMã đơn hàng (unique)Có ~200 dòng duplicate do sync lỗi
customer_idstrMã khách hàng~5% bị NaN (khách vãng lai)
product_idstrMã sản phẩmCó dòng viết "P001", có dòng "p001"
quantityintSố lượng~2% bị NaN, có giá trị âm (-1, -3)
unit_pricefloat/strĐơn giáLưu dạng string "1,500,000", có "N/A"
order_datestrNgày đặt hàng3 format: "2026-01-15", "15/01/2026", "Jan 15, 2026"
statusstrTrạng thái"completed", "Completed", "COMPLETED", "cancelled"
payment_methodstrPhươ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ộtKiểu mong đợiMô tảVấn đề data
customer_idstrMã khách hàng (unique)Chuẩn, không lỗi
namestrTên khách hàngDấu cách thừa, hoa/thường lộn xộn
emailstrEmail~3% bị NaN
phonestrSố điện thoạiFormat lộn xộn: "0901234567", "090-123-4567", "+84..."
regionstrKhu vực"Bắc", "bắc", " Bắc ", "Miền Bắc"
segmentstrPhân khúc"Premium", "Standard", "Basic"

Bảng 3: products — Sản phẩm (500 dòng)

CộtKiểu mong đợiMô tảVấn đề data
product_idstrMã sản phẩm (unique)Chuẩn uppercase: "P001"
product_namestrTên sản phẩmOK
categorystrDanh mục"Điện Tử", "Thời Trang", "Mỹ Phẩm", "Gia Dụng", "Thực Phẩm"
brandstrThương hiệu~8% bị NaN (no-brand products)
cost_pricefloatGiá vốnOK

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()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_id NaN, ~2% quantity NaN, ~5% unit_price = "N/A", 200+ duplicates, 3 date formats, mixed case status/payment/product_id
  • customers: ~3% email NaN, messy names, inconsistent regions
  • products: ~8% brand NaN
  • → 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:

#FileMô tả
1HoTen_Buoi08_DataCleaning.ipynbJupyter Notebook hoàn chỉnh — chạy Restart & Run All thành công
2shopvn_orders_cleaned.csvDataset đã clean — output từ Cell 21

📊 Rubric — Thang điểm

Tiêu chíĐiểmMô tả
Data Audit (Phần 1)15info(), describe(), isnull().sum() đầy đủ
Missing Values (Phần 2)20Strategy hợp lý cho từng cột, fillna() đúng
Duplicates & Types (Phần 3)20drop_duplicates() đúng key, to_numeric/to_datetime chạy
String Cleaning (Phần 4)15Status/name/region chuẩn hóa, dùng .str methods
Merge (Phần 5)15pd.merge() đúng how/on, revenue tính đúng
Export & Quality (Phần 6)10CSV xuất thành công, quality report đầy đủ
Notebook Clean5Markdown sections, Restart & Run All OK, no debug cells
Bonus+15Outlier detection (+5), Pivot table (+5), Quality score (+5)
Tổng100 + 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