Skip to content

🎮 Data Wrangler — Pandas Method Challenge!

Bạn là Data Analyst tại ShopVN — sàn e-commerce top 5 Việt Nam. Sáng nay, team Data Engineering gửi cho bạn dataset bán hàng tháng 2/2026 — nhưng data bẩn kinh khủng! CEO cần báo cáo doanh thu chiều nay. Mission: chọn đúng Pandas method để clean 7 vấn đề data trong thời gian ngắn nhất! 🧹


🎯 Mục tiêu học tập

Sau khi hoàn thành game, bạn sẽ:

  1. Xử lý missing values — chọn đúng giữa dropna(), fillna(), interpolate()
  2. Phát hiện & xóa duplicatesduplicated(), drop_duplicates()
  3. Chuyển đổi data typesastype(), pd.to_datetime(), pd.to_numeric()
  4. Làm sạch strings.str.strip(), .str.lower(), .str.replace()
  5. Xử lý inconsistent formats — chuẩn hóa ngày tháng, số điện thoại, tên
  6. Merge DataFramespd.merge() với đúng howon
  7. Phát hiện outliers — dùng Numpy boolean masking + IQR method

📜 Luật chơi

┌──────────────────────────────────────────────────────┐
│  BẠN = Data Wrangler 🧹🔧                            │
│  DATA = Dataset e-commerce bẩn 10,000+ dòng           │
│  MỖI VÒNG = 1 vấn đề data → chọn Pandas method đúng  │
│  3 LỰA CHỌN mỗi vòng — chỉ 1 đáp án tối ưu          │
│  MỤC TIÊU = Thu thập ≥ 80 XP để đạt hạng Gold 🥇    │
└──────────────────────────────────────────────────────┘

Cách tính điểm mỗi vòng:

Thành phầnXP
Trả lời đúng+10 XP (Vòng 1–2), +12 XP (Vòng 3–4), +15 XP (Vòng 5–6), +18 XP (Vòng 7)
Trả lời sai+0 XP
Không dùng hint+2 XP bonus ⚡
Giải thích đúng lý do+3 XP bonus 🧠

Tổng XP tối đa: 10+10+12+12+15+15+18 = 92 XP (chưa tính bonus)

Nguyên tắc quan trọng:

  • 🔒 Ưu tiên phương pháp không mất datafillna() thường tốt hơn dropna() khi % missing nhỏ
  • ⚖️ Hiểu trade-off mỗi method — mỗi cách clean đều có pros/cons

🏆 Bảng xếp hạng & Huy hiệu

Ranks

HạngXPMô tả
🥇 Gold — Data Wrangler Pro≥ 80 XPBạn clean data như chuyên gia! Sẵn sàng EDA
🥈 Silver — Data Cleaner≥ 55 XPTốt! Cần luyện thêm merge & outlier handling
🥉 Bronze — Data Rookie≥ 35 XPỔn cho ngày đầu — ôn lại Pandas basics
💀 Game Over< 35 XPData vẫn bẩn — quay lại đọc Buổi 8 nhé!

Huy hiệu đặc biệt

BadgeĐiều kiệnMô tả
🎯 Perfect CleanĐúng 1 vòng + không dùng hintSạch bóng!
🔥 Clean Streak3 vòng liên tiếp đúngChuỗi clean liên hoàn!
🧠 Method MasterGiải thích đúng lý do ≥ 5 vòngHiểu sâu từng method!
Speed CleanerHoàn thành 7 vòng trong < 10 phútNhanh như .dropna()!
🏆 Full PipelineĐúng tất cả 7 vòngData Wrangler huyền thoại!
🧹 First ScrubĐúng vòng đầu tiênBước đầu sạch sẽ!
💡 No Hints HeroKhông dùng hint cả gameTự clean bằng kiến thức!
🛡️ Comeback AnalystSai 2 vòng đầu, đúng 5 vòng sauKhông bao giờ bỏ cuộc!
🔗 Merge KingĐúng vòng merge (Vòng 6)Master of JOINs!

🎲 Chỉ số theo dõi

Chỉ sốIconMô tảMục tiêu
Method Accuracy🎯Số vòng chọn đúng method / tổng 7 vòng≥ 6/7
Data Quality After📊Mức improve data quality sau mỗi vòngTăng đều
Speed⏱️Tổng thời gian hoàn thành game< 15 phút

📋 Kịch bản chi tiết


🧹 Vòng 1: Missing Values — "Cột price bị NaN" ⭐ Dễ

Dataset đơn hàng có 8% cột price bị NaN — CEO cần tổng doanh thu!

💬 Slack từ Data Lead:

"Em ơi, cột price có ~800 dòng bị NaN trên tổng 10,000. Cần tính tổng doanh thu — xử lý missing values sao cho không mất quá nhiều data nhé!"

Data sample:

order_idproductcategorypricequantity
1001Laptop DellĐiện Tử150000001
1002Áo thunThời TrangNaN2
1003Chuột LogitechĐiện Tử3500003
1004Sữa rửa mặtMỹ PhẩmNaN1
1005Bàn phím cơĐiện Tử12000001

Vấn đề: 8% dòng có price = NaN. Cần tính tổng revenue = price × quantity.

Lựa chọn:

Pandas MethodCode
Adropna() — Xóa tất cả dòng có NaNdf.dropna(subset=["price"], inplace=True)
Bfillna(median) — Điền median theo categorydf["price"] = df.groupby("category")["price"].transform(lambda x: x.fillna(x.median()))
Cfillna(0) — Điền 0 vào NaNdf["price"].fillna(0, inplace=True)
💡 Hint (−2 XP)

Xóa 8% data = mất 800 đơn hàng → doanh thu bị thiếu. Điền 0 = giá sản phẩm là 0 (vô lý). Có cách nào ước lượng hợp lý dựa trên các sản phẩm cùng category không?

Đáp án & Giải thích

✅ Đáp án đúng: Bfillna(median) theo category

Lý do:

  • A saidropna() xóa 800 dòng (8%) → mất doanh thu đáng kể, vi phạm nguyên tắc "không mất data"
  • C saifillna(0) biến giá thành 0 → revenue = 0 → sai logic nghiệp vụ (không có sản phẩm giá 0)
  • B đúngmedian theo category ước lượng hợp lý — sản phẩm Điện Tử có median price riêng, Thời Trang có median riêng. Median bền hơn mean với outliers (ví dụ: laptop 15 triệu kéo mean lên)

XP: +10 | Bonus: +2 (no hint), +3 (đúng lý do)


🧹 Vòng 2: Duplicates — "Đơn hàng bị trùng" ⭐ Dễ

Có 342 dòng duplicate do hệ thống sync lỗi — cần loại bỏ!

💬 Slack từ Backend Dev:

"Sorry team, sync service bị retry nên có đơn hàng bị insert nhiều lần. Các bạn remove duplicates giúp nhé!"

Data sample:

order_idcustomer_idproductamountorder_date
5001C101Laptop Dell150000002026-02-01
5001C101Laptop Dell150000002026-02-01
5002C102Tai nghe Sony25000002026-02-01
5003C101Chuột Logitech4500002026-02-03
5003C101Chuột Logitech4500002026-02-03

Vấn đề: 342 dòng duplicate hoàn toàn (tất cả cột giống nhau). Nếu không xóa → doanh thu bị tính đúp.

Lựa chọn:

Pandas MethodCode
Adrop_duplicates() — toàn bộ cộtdf.drop_duplicates(inplace=True)
Bdrop_duplicates(subset=["order_id"]) — chỉ theo order_iddf.drop_duplicates(subset=["order_id"], keep="first", inplace=True)
Cdf[~df.duplicated()] — filter bằng boolean maskdf = df[~df.duplicated(subset=["order_id", "product"])]
💡 Hint (−2 XP)

Duplicate do hệ thống sync = toàn bộ dòng giống nhau. order_id là unique identifier. Cần giữ dòng đầu tiên (keep="first") và xóa các bản copy. Có method nào check theo order_id + giữ bản gốc không?

Đáp án & Giải thích

✅ Đáp án đúng: Bdrop_duplicates(subset=["order_id"], keep="first")

Lý do:

  • A saidrop_duplicates() không chỉ định subset → nếu 2 dòng khác nhau chỉ 1 cột nhỏ (VD: timestamp khác vài giây), sẽ không bị detect
  • C partial — hoạt động nhưng check 2 cột (order_id + product) thay vì dùng primary key order_id → có thể miss case 1 order mua 2 sản phẩm
  • B đúng vì dùng order_id (unique identifier) để detect, keep="first" giữ bản gốc, xóa bản copy

XP: +10 | Bonus: +2 (no hint), +3 (đúng lý do)


🧹 Vòng 3: Wrong Data Types — "Cột price là string" ⭐⭐ Trung bình

Cột price bị lưu dạng string "1,500,000" thay vì number — không tính toán được!

💬 Slack từ Data Lead:

"Em thử df['price'].sum() thì ra chuỗi '1,500,0002,500,000...' thay vì tổng. Hình như cột price bị lưu dạng text!"

Data sample:

order_idproductprice (dtype: object)quantity
1001Laptop"15,000,000"1
1002Áo thun"250,000"2
1003Chuột"350,000"3
1004Kem"N/A"1
1005Bàn phím"1,200,000"1

Vấn đề: Cột priceobject (string), có dấu phẩy ngăn cách hàng nghìn, và có giá trị "N/A".

Lựa chọn:

Pandas MethodCode
Aastype(float) trực tiếpdf["price"] = df["price"].astype(float)
Bstr.replace rồi to_numericdf["price"] = df["price"].str.replace(",", ""); df["price"] = pd.to_numeric(df["price"], errors="coerce")
Capply(lambda) customdf["price"] = df["price"].apply(lambda x: float(x) if x != "N/A" else 0)
💡 Hint (−2 XP)

String "15,000,000" có dấu phẩyastype(float) sẽ crash. Cần bỏ dấu phẩy trước. "N/A" là text, không phải số → cần xử lý riêng. pd.to_numeric() có parameter errors="coerce" biến giá trị không hợp lệ thành NaN.

Đáp án & Giải thích

✅ Đáp án đúng: Bstr.replace(",", "") + pd.to_numeric(errors="coerce")

Lý do:

  • A saiastype(float) crash khi gặp dấu phẩy: ValueError: could not convert string to float: '15,000,000'
  • C partial — hoạt động nhưng chỉ handle "N/A", không handle các invalid values khác (VD: "TBD", "-", " "). Dùng fillna(0) thay vì để NaN cũng không tối ưu
  • B đúng vì: (1) str.replace(",", "") bỏ dấu phẩy, (2) pd.to_numeric(errors="coerce") chuyển sang float, tự biến mọi giá trị invalid thành NaN — tổng quát và an toàn

XP: +12 | Bonus: +2 (no hint), +3 (đúng lý do)


🧹 Vòng 4: Messy Strings — "Tên customer viết lung tung" ⭐⭐ Trung bình

Cột customer_name có dấu cách thừa, viết hoa/thường lộn xộn, ký tự lạ!

💬 Slack từ Marketing:

"Chị cần gửi email cho top customers — nhưng tên khách hàng nhìn kinh quá, có tên viết HOA hết, có tên thừa dấu cách, có tên dính ký tự lạ."

Data sample:

customer_idcustomer_name (raw)email
C101" nguyễn văn an "an@email.com
C102"TRẦN THỊ BÌNH"binh@email.com
C103"lê hoàng cường "cuong@email.com
C104" Phạm Thị Dung"dung@email.com
C105"VÕ@@minh em"em@email.com

Vấn đề: Dấu cách thừa đầu/cuối, double spaces, viết hoa/thường không nhất quán, ký tự đặc biệt lạ.

Lựa chọn:

Pandas MethodCode
Astr.strip() — chỉ xóa dấu cách đầu/cuốidf["customer_name"] = df["customer_name"].str.strip()
BChain methods — strip + title + replacedf["customer_name"] = df["customer_name"].str.strip().str.replace(r"[^a-zA-ZÀ-ỹ\s]", "", regex=True).str.replace(r"\s+", " ", regex=True).str.title()
Cstr.upper() — chuyển hết thành chữ hoadf["customer_name"] = df["customer_name"].str.upper()
💡 Hint (−2 XP)

Vấn đề có nhiều lớp: dấu cách thừa + ký tự lạ + format chữ hoa/thường. Cần chain nhiều methods theo thứ tự: strip → remove ký tự lạ → chuẩn hóa spaces → title case. .str.title() viết hoa chữ cái đầu mỗi từ.

Đáp án & Giải thích

✅ Đáp án đúng: B — Chain methods: strip → remove special chars → normalize spaces → title

Lý do:

  • A sai vì chỉ xử lý 1 trong 4 vấn đề (dấu cách đầu/cuối). Không fix double spaces, ký tự lạ, hay format chữ
  • C saistr.upper() biến tất cả thành HOA → "NGUYỄN VĂN AN" — không professional cho email/báo cáo, và không fix spaces/ký tự lạ
  • B đúng vì xử lý tất cả vấn đề: (1) strip() bỏ spaces đầu/cuối, (2) regex bỏ ký tự đặc biệt, (3) \s+" " normalize spaces, (4) title() → "Nguyễn Văn An" — chuẩn format

Kết quả sau clean:

customer_idcustomer_name (cleaned)
C101"Nguyễn Văn An"
C102"Trần Thị Bình"
C103"Lê Hoàng Cường"
C104"Phạm Thị Dung"
C105"Võ Minh Em"

XP: +12 | Bonus: +2 (no hint), +3 (đúng lý do)


🧹 Vòng 5: Inconsistent Date Formats — "3 kiểu ngày khác nhau" ⭐⭐⭐ Khó

Cột order_date có 3 format khác nhau — cần chuẩn hóa để phân tích theo tháng!

💬 Slack từ BI Analyst:

"Anh cần group orders theo tháng nhưng cột order_date có dòng '2026-02-15', có dòng '15/02/2026', có dòng 'Feb 15, 2026'. Chuẩn hóa giúp anh nhé!"

Data sample:

order_idorder_date (raw)amount
1001"2026-02-01"5000000
1002"15/02/2026"2500000
1003"Feb 03, 2026"1200000
1004"2026-02-10"8000000
1005"28/02/2026"450000

Vấn đề: 3 format ngày khác nhau. Cần chuẩn hóa thành datetime64 để groupby theo tháng.

Lựa chọn:

Pandas MethodCode
Apd.to_datetime() với format cố địnhdf["order_date"] = pd.to_datetime(df["order_date"], format="%Y-%m-%d")
Bpd.to_datetime() với infer_datetime_formatdf["order_date"] = pd.to_datetime(df["order_date"], format="mixed", dayfirst=True)
Cstr.replace() rồi to_datetimedf["order_date"] = df["order_date"].str.replace("/", "-"); df["order_date"] = pd.to_datetime(df["order_date"])
💡 Hint (−2 XP)

3 format khác nhau → không thể dùng 1 format cố định. pd.to_datetime() có parameter format="mixed" cho phép parse nhiều format trong cùng 1 cột. dayfirst=True hữu ích khi có format dd/mm/yyyy.

Đáp án & Giải thích

✅ Đáp án đúng: Bpd.to_datetime(format="mixed", dayfirst=True)

Lý do:

  • A saiformat="%Y-%m-%d" chỉ parse được "2026-02-01", crash khi gặp "15/02/2026" hoặc "Feb 03, 2026"
  • C sai vì chỉ handle "/" → "-", không handle "Feb 03, 2026" format. Và "15-02-2026" không tự parse đúng nếu thiếu dayfirst
  • B đúngformat="mixed" cho phép Pandas parse nhiều format trong cùng 1 cột, dayfirst=True đảm bảo "15/02/2026" parse thành 15 tháng 2 (không phải tháng 15)

XP: +15 | Bonus: +2 (no hint), +3 (đúng lý do)


🧹 Vòng 6: Merge Conflicts — "JOIN orders với customers" ⭐⭐⭐ Khó

Cần merge bảng orders với customers — nhưng có orders không có customer!

💬 Slack từ Data Lead:

"Em merge 2 bảng nhưng mất 500 đơn hàng — hình như có orders mà customer chưa được nhập vào hệ thống. Cần giữ TẤT CẢ đơn hàng, kể cả đơn chưa có customer info!"

Data sample:

Bảng orders (10,000 dòng):

order_idcustomer_idproductamount
1001C101Laptop15000000
1002C102Áo thun500000
1003C999Chuột350000
1004C101Bàn phím1200000
1005NoneKem250000

Bảng customers (8,500 dòng):

customer_idnameregionsegment
C101Nguyễn Văn AnBắcPremium
C102Trần Thị BìnhNamStandard
C103Lê Hoàng CườngTrungPremium

Vấn đề: C999 không tồn tại trong customers. order_id=1005 không có customer_id. Cần giữ tất cả orders.

Lựa chọn:

Pandas MethodCode
Apd.merge() với how="inner"df_merged = pd.merge(orders, customers, on="customer_id", how="inner")
Bpd.merge() với how="left"df_merged = pd.merge(orders, customers, on="customer_id", how="left")
Cpd.concat()df_merged = pd.concat([orders, customers], axis=1)
💡 Hint (−2 XP)

Cần giữ TẤT CẢ đơn hàng kể cả đơn chưa có customer. Trong SQL, đây là LEFT JOIN — giữ toàn bộ bảng bên trái (orders), match được thì điền info, không match thì NaN. pd.merge() tương đương SQL JOIN.

Đáp án & Giải thích

✅ Đáp án đúng: Bpd.merge(how="left")

Lý do:

  • A saiinner chỉ giữ rows match được ở cả 2 bảng → mất 500 đơn hàng có customer_id không tồn tại trong customers
  • C saipd.concat(axis=1) ghép ngang theo index, KHÔNG match theo customer_id → dữ liệu lệch hoàn toàn
  • B đúngleft join giữ toàn bộ orders (bảng trái), match được customer thì điền thông tin, không match thì name/region/segment = NaN → không mất đơn hàng nào

Kết quả merge:

order_idcustomer_idproductamountnameregionsegment
1001C101Laptop15000000Nguyễn Văn AnBắcPremium
1002C102Áo thun500000Trần Thị BìnhNamStandard
1003C999Chuột350000NaNNaNNaN
1004C101Bàn phím1200000Nguyễn Văn AnBắcPremium
1005NoneKem250000NaNNaNNaN

XP: +15 | Bonus: +2 (no hint), +3 (đúng lý do)


🧹 Vòng 7: Outlier Detection — "Đơn hàng 5 tỷ?!" ⭐⭐⭐ Khó (Boss Round!)

CEO hỏi: "Tại sao có đơn hàng 5 tỷ trong dataset? Lọc outliers giúp tôi!"

💬 Slack từ CEO:

"Tổng doanh thu bất thường cao — hình như có đơn hàng giá trị cực lớn. Dùng phương pháp thống kê để phát hiện và flag outliers!"

Data sample (cột amount):

python
import numpy as np
import pandas as pd

amounts = pd.Series([
    500000, 1200000, 850000, 2500000, 350000,
    15000000, 780000, 5000000000, 920000, 1100000,
    # 5,000,000,000 (5 tỷ) = outlier rõ ràng!
])

print(f"Mean: {amounts.mean():,.0f}")
print(f"Median: {amounts.median():,.0f}")
print(f"Std: {amounts.std():,.0f}")

Vấn đề: Giá trị 5 tỷ kéo mean lên bất thường. Cần phát hiện outliers bằng phương pháp thống kê.

Lựa chọn:

MethodCode
AXóa giá trị > 100 triệu (hardcode)df = df[df["amount"] <= 100_000_000]
BIQR method với NumpyQ1 = np.percentile(df["amount"], 25); Q3 = np.percentile(df["amount"], 75); IQR = Q3 - Q1; mask = (df["amount"] >= Q1 - 1.5*IQR) & (df["amount"] <= Q3 + 1.5*IQR); df_clean = df[mask]
CZ-score > 3from scipy import stats; z = np.abs(stats.zscore(df["amount"])); df_clean = df[z < 3]
💡 Hint (−2 XP)

Hardcode threshold = magic number → không tổng quát. IQR (Interquartile Range) là phương pháp phổ biến nhất cho DA: Q1, Q3 → IQR = Q3−Q1 → outlier nếu < Q1−1.5×IQR hoặc > Q3+1.5×IQR. Dùng np.percentile() tính Q1, Q3. Z-score hoạt động nhưng giả định phân phối chuẩn — không phải lúc nào cũng đúng.

Đáp án & Giải thích

✅ Đáp án đúng: B — IQR method với Numpy

Lý do:

  • A sai vì 100 triệu là "magic number" — tại sao 100 triệu mà không phải 50 triệu hay 200 triệu? Không có cơ sở thống kê. Nếu dataset thay đổi, threshold cũng phải sửa tay
  • C partial — Z-score hoạt động nhưng: (1) cần scipy (dependency thêm), (2) giả định dữ liệu phân phối chuẩn (normal distribution) — doanh thu e-commerce thường lệch phải (right-skewed), Z-score kém hiệu quả
  • B đúng vì IQR: (1) không cần giả định phân phối, (2) robust với data skewed, (3) dùng numpy đã có sẵn, (4) formula chuẩn quốc tế (dùng trong boxplot). Kết quả: Q1, Q3, IQR tự tính từ data → tự điều chỉnh theo dataset

Minh họa IQR:

python
import numpy as np

amounts = np.array([500000, 1200000, 850000, 2500000, 350000,
                    15000000, 780000, 5000000000, 920000, 1100000])

Q1 = np.percentile(amounts, 25)
Q3 = np.percentile(amounts, 75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

print(f"Q1: {Q1:,.0f} | Q3: {Q3:,.0f} | IQR: {IQR:,.0f}")
print(f"Lower bound: {lower_bound:,.0f}")
print(f"Upper bound: {upper_bound:,.0f}")
print(f"Outliers: {amounts[(amounts < lower_bound) | (amounts > upper_bound)]}")
# → 5,000,000,000 bị flag là outlier ✅

XP: +18 | Bonus: +2 (no hint), +3 (đúng lý do)


🏁 Tổng kết

Bảng tổng hợp Pandas Methods

Vấn đề dataMethod chínhNhớ
Missing valuesfillna(median) theo groupKhông mất data, ước lượng hợp lý
Duplicatesdrop_duplicates(subset, keep)Dùng primary key, giữ bản gốc
Wrong dtypesstr.replace() + pd.to_numeric()Clean string trước, convert sau
Messy strings.str.strip().str.title() chainChain methods theo thứ tự
Inconsistent datespd.to_datetime(format="mixed")Tự detect nhiều format
Merge conflictspd.merge(how="left")Giữ tất cả bảng chính
OutliersIQR method + np.percentile()Không cần giả định phân phối

Tiếp theo

Bạn đã biết chọn đúng method cho từng vấn đề data. Giờ hãy áp dụng tất cả vào Workshop — Data Cleaning Pipeline để xây dựng pipeline cleaning hoàn chỉnh cho dataset e-commerce thực tế! 🚀

💡 Sau game này

  • ✅ Bạn biết 7 loại data problem phổ biến nhất
  • ✅ Bạn biết Pandas method tối ưu cho từng loại
  • ✅ Bạn hiểu trade-off: dropna vs fillna, inner vs left join, IQR vs Z-score
  • → Sẵn sàng cho Workshop: áp dụng tất cả vào 1 pipeline hoàn chỉnh!