DS & DA

[DS & DA Blog #60] Làm Chủ Thư Viện Pandas & Quy Trình Làm Sạch Dữ Liệu Thực Chiến (Data Cleaning 101): Xử Lý Missing Values, Duplicates, loc vs. iloc & Mẹo Giảm 70% RAM

1 views
[DS & DA Blog #60] Làm Chủ Thư Viện Pandas & Quy Trình Làm Sạch Dữ Liệu Thực Chiến (Data Cleaning 101): Xử Lý Missing Values, Duplicates, loc vs. iloc & Mẹo Giảm 70% RAM

1. Bối cảnh thực tế (Context & Problem Statement)

Trong thực tế doanh nghiệp, dữ liệu không bao giờ sạch sẽ, đẹp đẽ và sẵn sàng sử dụng như trên các cuộc thi học thuật:

  • Dữ liệu xuất từ file Excel của phòng kinh doanh thường chứa đầy các ô trống vô nghĩa, cột ngày tháng bị gõ sai định dạng, giá tiền bị dính ký tự chữ cái (ví dụ: "150,000 VND" hoặc "$1,200").
  • Dữ liệu tải từ hệ thống web về thường bị nhân đôi bản ghi (Duplicates) do mạng chập chờn khiến khách hàng bấm nút gửi 2 lần.
  • Khoảng 70%−80%70\% - 80\% thời gian làm việc hàng ngày của một Data Analyst, Data Engineer hay Data Scientist là dành cho việc Làm sạch và tiền xử lý dữ liệu (Data Cleaning & Wrangling).

Nếu bạn không làm chủ thư viện Pandas trong Python, bạn sẽ tốn hàng giờ đồng hồ viết các vòng lặp for chậm chạp, dễ gây lỗi sai lệch số liệu và thường xuyên gặp lỗi tràn bộ nhớ MemoryError khi xử lý các file dữ liệu lớn.


2. Các Khái Niệm & Cơ Chế Cốt Lõi

2.1. Cấu Trúc Dữ Liệu Cốt Lõi: Series vs. DataFrame

Pandas tổ chức dữ liệu dựa trên 2 cấu trúc nền tảng:

  1. Pandas Series (Mảng 1 chiều): Là một cột dữ liệu duy nhất kèm theo nhãn chỉ mục (Index).
  2. Pandas DataFrame (Bảng 2 chiều): Là một bảng gồm nhiều hàng và nhiều cột (tương đương một trang tính Excel hoặc một bảng trong SQL). Mỗi cột trong DataFrame thực chất là một Series!
PANDAS SERIES (1 Chiều):            PANDAS DATAFRAME (2 Chiều):
Index    Value                     Index    Name       Age    Salary
  0  ---> "An"                       0  ---> "An"       25    15000000
  1  ---> "Bình"                     1  ---> "Bình"     30    22000000
  2  ---> "Cường"                    2  ---> "Cường"    28    18000000
                                            ^           ^        ^
                                            |-----------+--------|
                                            Mỗi cột là một Series!

2.2. Phân Biệt Tuyệt Đối: df.loc[] vs. df.iloc[]

Đây là câu hỏi phỏng vấn kinh điển nhất về Pandas mà rất nhiều ứng viên trả lời nhầm lẫn:

  • df.loc[] (Label-based Selection - Truy xuất theo TÊN/NHÃN):
    • Dựa vào Tên của dòng (Index Label) và Tên của cột (Column Name).
    • Cú pháp: df.loc[row_label, column_label].
    • Lưu ý: Khi cắt lát (slice), loc lấy chặn cả hai đầu mút (Inclusive) (ví dụ: df.loc['a':'c'] lấy cả dòng c).
  • df.iloc[] (Integer Index-based Selection - Truy xuất theo VỊ TRÍ SỐ NGUYÊN):
    • Dựa vào Vị trí chỉ số số nguyên (0, 1, 2, ...) của dòng và cột, bất kể nhãn tên của nó là gì.
    • Cú pháp: df.iloc[row_index, column_index].
    • Lưu ý: Khi cắt lát (slice), iloc hoạt động giống như Python thông thường, không lấy phần tử cuối (Exclusive) (ví dụ: df.iloc[0:3] chỉ lấy dòng 0, 1, 2).

2.3. Xử Lý Dữ Liệu Khuyết Thiếu (Missing Values / NaN)

Trong Pandas, giá trị bị trống được đại diện bằng NaN (Not a Number) hoặc None.

  1. Phát hiện dữ liệu trống:
    • df.isna().sum(): Đếm tổng số lượng ô trống trên từng cột.
  2. Chiến lược Xóa bỏ (df.dropna()):
    • df.dropna(): Xóa sạch toàn bộ dòng nếu dòng đó có ít nhất 1 ô bị trống.
    • df.dropna(subset=['user_id', 'order_date']): Chỉ xóa dòng nếu cột quan trọng bị trống.
    • Khi nào nên xóa? Khi số lượng dòng bị khuyết rất nhỏ (<2%< 2\% tổng dữ liệu) hoặc khi cột bị thiếu là khóa chính định danh bắt buộc.
  3. Chiến lược Điền khuyết (df.fillna()):
    • Điền giá trị thống kê: Điền bằng df['age'].median() (nếu dữ liệu lệch/có outlier) hoặc df['age'].mean() (nếu dữ liệu chuẩn).
    • Điền giá trị lân cận:
      • df.ffill() (Forward Fill): Lấy giá trị của dòng phía trên điền cho dòng phía dưới (rất tối ưu cho dữ liệu chuỗi thời gian).
      • df.bfill() (Backward Fill): Lấy giá trị dòng dưới điền ngược lên trên.
    • Điền nhãn riêng biệt: df['category'].fillna('Unknown') (cho biến phân loại).

2.4. Xử Lý Dữ Liệu Trùng Lặp (Duplicate Records)

  • Kiểm tra trùng lặp: df.duplicated(subset=['user_id', 'order_id']).
  • Xóa trùng lặp: df.drop_duplicates(subset=['user_id'], keep='last'):
    • keep='first' (Mặc định): Giữ lại bản ghi xuất hiện đầu tiên, xóa các bản ghi sau.
    • keep='last': Giữ lại bản ghi xuất hiện cuối cùng (thường là bản cập nhật mới nhất).

2.5. Các Phép Gộp Bảng: pd.merge() vs. pd.concat()

  • pd.merge() (Tương đương câu lệnh SQL JOIN):
    • Ghép 2 bảng lại với nhau dựa trên một hoặc nhiều Cột khóa chung (Common Key / Foreign Key).
    • Hỗ trợ các kiểu kết nối: how='inner', how='left', how='right', how='outer'.
  • pd.concat() (Tương đương câu lệnh SQL UNION ALL hoặc ghép cột vật lý):
    • Nối các bảng lại với nhau bằng cách xếp chồng lên nhau theo chiều dọc (axis=0) hoặc ghép ngang cạnh nhau theo chiều ngang (axis=1).

3. Các Bảng Markdown So Sánh Chi Tiết

📊 Bảng 1: So Sánh Toàn Diện df.loc[] vs. df.iloc[]

Tiêu chí kỹ thuậtdf.loc[]df.iloc[]
Cơ chế truy xuấtDựa trên Tên / Nhãn (Label-based)Dựa trên Vị trí số nguyên (Position-based: 0, 1, 2...)
Truy xuất dòngdf.loc['row_name']df.iloc[0]
Truy xuất cộtdf.loc[:, 'column_name']df.iloc[:, 1]
Cơ chế cắt lát (slice)Lấy cả điểm cuối (Inclusive: 'a':'c' lấy cả 'c')Bỏ điểm cuối (Exclusive: 0:3 chỉ lấy 0, 1, 2)
Lọc theo điều kiện logicdf.loc[df['age'] > 20, ['name', 'age']]Phức tạp hơn (Phải đổi điều kiện thành danh sách boolean index)
Lỗi thường gặpBị lỗi nếu gõ sai chính tả tên cộtBị lỗi IndexError nếu chỉ số vượt quá số dòng/cột

📊 Bảng 2: So Sánh Các Chiến Lược Điền Dữ Liệu Khuyết Thiếu (fillna)

Chiến lược điềnCâu lệnh Pandas mẫuƯu điểmNhược điểm / Rủi roKhi nào nên sử dụng?
Điền Mean (Trung bình)df['col'].fillna(df['col'].mean())Đơn giản, giữ nguyên giá trị trung bình tổng thểBị méo mó nếu dữ liệu có Outliers lớnDữ liệu số phân phối chuẩn đối xứng
Điền Median (Trung vị)df['col'].fillna(df['col'].median())An toàn, không bị ảnh hưởng bởi OutliersLàm phẳng đỉnh nhọn của phân phốiDữ liệu số bị lệch (Thu nhập, Giá nhà, Tuổi)
Forward Fill (ffill)df['col'].ffill()Tự nhiên, bảo toàn tính liên tụcGây sai lệch nếu chuỗi bị khuyết một đoạn quá dàiDữ liệu chuỗi thời gian, Tỷ giá hối đoái, Giá chứng khoán
Gán nhãn riêngdf['col'].fillna('Missing')Không đoán mò thông tin, giữ tính trung thựcTạo thêm 1 nhóm danh mục mớiDữ liệu chữ/danh mục (Categorical data)

📊 Bảng 3: So Sánh Các Phép Nối Bảng Trong Pandas

Thao tácCú pháp chínhBản chất cơ chếTương đương câu lệnh SQL
pd.merge()pd.merge(df1, df2, on='id', how='left')Ghép dữ liệu dựa trên Giá trị của Cột khóa chungSELECT * FROM df1 LEFT JOIN df2 ON df1.id = df2.id
pd.concat(axis=0)pd.concat([df_jan, df_feb], axis=0)Xếp chồng các dòng lên nhau theo chiều dọcSELECT * FROM df_jan UNION ALL SELECT * FROM df_feb
pd.concat(axis=1)pd.concat([df1, df2], axis=1)Dán 2 bảng nằm cạnh nhau theo chỉ số IndexKhông có trong SQL chuẩn (Tương tự nối ngang)
df.join()df1.join(df2, on='id')Phím tắt ghép bảng dựa trên Chỉ số IndexTương tự SQL JOIN trên Primary Key Index

4. Thực Hành Thực Chiến: Quy Trình Làm Sạch Dữ Liệu Bẩn Bằng Python (Walkthrough)

📌 Dữ liệu bẩn ban đầu (Messy Data):

Giả sử bạn nhận được một tập dữ liệu đơn hàng bị lỗi: Cột price bị dính ký tự $ và dấu phẩy, cột customer_name có khoảng trắng thừa, có các dòng bị trùng lặp và chứa giá trị rỗng NaN:

import pandas as pd
import numpy as np

# 1. Tạo tập dữ liệu bẩn mô phỏng:
raw_data = {
    'order_id': [101, 102, 103, 103, 104, 105],
    'customer_name': ['  Nguyen Van An  ', 'Le Thi Binh', 'TRAN VAN CUONG', 'TRAN VAN CUONG', np.nan, 'Pham Thi Dung'],
    'price': ['$1,200.50', '$450.00', '$25.99', '$25.99', '$80.00', 'MISSING'],
    'age': [25, np.nan, 32, 32, 45, 120], # Có 1 người 120 tuổi (Outlier)
    'order_date': ['2026-09-01', '2026/09/02', '03-09-2026', '03-09-2026', '2026-09-04', '2026-09-05']
}
df = pd.DataFrame(raw_data)
print("=== DỮ LIỆU BAN ĐẦU ===")
print(df)

🛠️ Script Làm Sạch Dữ Liệu Chuẩn Mực Từng Dòng (Data Cleaning Pipeline):

# BƯỚC 1: Xóa các bản ghi bị trùng lặp hoàn toàn:
df = df.drop_duplicates()

# BƯỚC 2: Chuẩn hóa cột họ tên (Xóa khoảng trắng thừa 2 đầu và viết hoa chữ cái đầu):
df['customer_name'] = df['customer_name'].astype(str).str.strip().str.title()
# Thay thế chuỗi 'Nan' vừa bị convert thành giá trị khuyết thiếu thực sự:
df['customer_name'] = df['customer_name'].replace('Nan', np.nan)

# BƯỚC 3: Làm sạch cột giá tiền (Bỏ dấu '$', bỏ dấu phẩy, ép kiểu về float):
df['price'] = df['price'].astype(str).str.replace('$', '', regex=False).str.replace(',', '', regex=False)
# Chuyển chuỗi không hợp lệ 'MISSING' thành NaN bằng errors='coerce':
df['price'] = pd.to_numeric(df['price'], errors='coerce')
# Điền giá trị giá tiền bị thiếu bằng Median của cột:
df['price'] = df['price'].fillna(df['price'].median())

# BƯỚC 4: Chuẩn hóa cột ngày tháng về đúng định dạng chuẩn YYYY-MM-DD:
df['order_date'] = pd.to_datetime(df['order_date'], format='mixed').dt.strftime('%Y-%m-%d')

# BƯỚC 5: Xử lý Outlier và Missing Value ở cột Tuổi (Age):
# Thay thế tuổi bất hợp lý (> 100 tuổi) thành NaN:
df.loc[df['age'] > 100, 'age'] = np.nan
# Điền tuổi bị thiếu bằng Median:
df['age'] = df['age'].fillna(df['age'].median()).astype(int)

# BƯỚC 6: Xóa bỏ dòng không có tên khách hàng (vì customer_name là trường quan trọng):
df = df.dropna(subset=['customer_name'])

print("\n=== DỮ LIỆU SAU KHI ĐÃ ĐƯỢC LÀM SẠCH HOÀN HẢO ===")
print(df)

💡 Mẹo Đỉnh Cao: Giảm Tới 70% Bộ Nhớ RAM Cho DataFrame Bằng Cách Ép Kiểu

Khi đọc file dữ liệu lớn, Pandas mặc định gán kiểu int64 (8 bytes8\text{ bytes}) cho số nguyên và kiểu object (chuỗi văn bản tốn rất nhiều RAM):

  1. Chuyển các cột số nguyên nhỏ sang int8, int16 hoặc int32:
    • Nếu cột age chỉ từ 00 đến 100100, chỉ cần kiểu int8 (lưu được từ −128-128 đến 127127, tốn đúng 1 byte1\text{ byte} thay vì 8 bytes8\text{ bytes}).
  2. Chuyển các cột danh mục lặp đi lặp lại sang kiểu category:
    • Cột gender ('Nam', 'Nu') hoặc cột city ('Hà Nội', 'TP.HCM') bị lặp lại hàng triệu lần. Chuyển sang df['city'] = df['city'].astype('category') sẽ giúp Pandas lưu trữ dưới dạng mã số nguyên nội bộ, tiết kiệm ngay lập tức 80%80\% RAM của cột đó!
  3. Đọc theo từng phần nhỏ (chunksize):
    # Đọc file 10GB trên máy tính 8GB RAM mà không bị sập:
    chunk_size = 50000 # Đọc mỗi lần 50,000 dòng
    for chunk in pd.read_csv("giant_file.csv", chunksize=chunk_size):
        process_clean_data(chunk) # Xử lý xong dòng nào giải phóng dòng đó
    

5. Góc Ôn Luyện Phỏng Vấn (Interview Corner)

❓ Câu hỏi 1: Phân biệt sự khác nhau giữa df.loc[] và df.iloc[] trong Pandas. Nếu tập dữ liệu có chỉ số Index là một dãy số ngẫu nhiên không liên tục (ví dụ: Index = [10, 50, 99]), thì df.loc[0] và df.iloc[0] sẽ trả về kết quả gì?

  • Gợi ý trả lời ngắn gọn, chuẩn xác:
    • Sự khác biệt cốt lõi:
      • df.loc[] truy xuất dựa trên Nhãn tên thực tế (Label) được hiển thị trên bảng.
      • df.iloc[] truy xuất dựa trên Vị trí số nguyên vật lý (Integer Position) từ trên xuống dưới bắt đầu từ vị trí số 0.
    • Trường hợp Index = [10, 50, 99]:
      • df.iloc[0]: Luôn luôn chạy thành công và trả về Dòng đầu tiên của bảng (tương ứng với dòng có index nhãn là 10).
      • df.loc[0]: Sẽ bị báo lỗi KeyError: 0, bởi vì loc sẽ đi tìm dòng nào có dán nhãn tên chính xác là số 0. Do trong danh sách index chỉ có nhãn 10, 50, 99 mà không có nhãn 0, lệnh này bị thất bại! Để lấy dòng đầu tiên bằng loc, bạn phải viết là df.loc[10].

❓ Câu hỏi 2: Khi làm sạch dữ liệu, khi nào bạn nên xóa các dòng chứa giá trị NaN (dropna) và khi nào bạn nên điền giá trị thay thế (fillna)? Nếu điền giá trị số, bạn sẽ ưu tiên chọn Mean hay Median?

  • Gợi ý trả lời:
    • Nên dùng dropna() khi:
      • Số lượng dòng chứa NaN là rất nhỏ (dưới 1%−2%1\% - 2\% tổng dữ liệu), việc xóa bỏ không làm ảnh hưởng đến tính toàn vẹn và phân phối của mẫu.
      • Giá trị bị khuyết nằm ở các cột định danh quan trọng không thể tự suy đoán (ví dụ: user_id, transaction_id, hoặc biến mục tiêu YY cần dự đoán).
    • Nên dùng fillna() khi:
      • Tỷ lệ dữ liệu bị khuyết đáng kể (ví dụ 10%−30%10\% - 30\%). Nếu xóa hết sẽ làm mất đi một lượng lớn thông tin giá trị của các cột khác trong cùng dòng đó.
    • Lựa chọn Mean hay Median khi điền số:
      • Ưu tiên chọn Median (Trung vị): Vì trong thực tế, hầu hết các biến số kinh doanh (Thu nhập, Chi tiêu, Giá sản phẩm, Tuổi tác) đều bị lệch và có chứa các giá trị ngoại lai (Outliers). Median là giá trị trung tâm ổn định nhất, không bị kéo lệch bởi các giá trị bất thường.
      • Chỉ chọn Mean (Trung bình cộng): Khi biết chắc chắn dữ liệu có phân phối chuẩn đối xứng hoàn hảo (hình chuông cân bằng) và hoàn toàn không có Outliers.

❓ Câu hỏi 3: Khi bạn cần đọc một file CSV dung lượng 5GB vào Pandas trên một máy tính cá nhân chỉ có 8GB RAM, chương trình Python lập tức bị crash với lỗi MemoryError. Hãy trình bày 3 kỹ thuật cụ thể trong Pandas để giải quyết triệt để vấn đề này.

  • Gợi ý trả lời (3 kỹ thuật tối ưu hóa bộ nhớ chuyên nghiệp):
    1. Chỉ tải các cột thực sự cần thiết bằng tham số usecols:
      • File CSV có thể có 50 cột, nhưng bài toán phân tích chỉ cần 5 cột.
      • Dùng pd.read_csv('file.csv', usecols=['user_id', 'order_date', 'amount']) giúp giảm ngay 90%90\% dung lượng RAM nạp vào ban đầu.
    2. Chỉ định trước kiểu dữ liệu tối ưu bằng tham số dtype:
      • Mặc định Pandas gán int64 (8 bytes8\text{ bytes}) và float64 (8 bytes8\text{ bytes}).
      • Chủ động ép kiểu dữ liệu nhỏ hơn: Cột số nguyên nhỏ dùng int16 (2 bytes2\text{ bytes}), số thực dùng float32 (4 bytes4\text{ bytes}), và đặc biệt các cột chữ bị lặp lại nhiều dùng kiểu 'category' →\rightarrow Cắt giảm từ 60%−70%60\% - 70\% dung lượng RAM của DataFrame.
    3. Đọc và xử lý dữ liệu theo từng khối nhỏ với tham số chunksize:
      • Không nạp toàn bộ 5GB cùng một lúc. Thiết lập pd.read_csv('file.csv', chunksize=100000).
      • Sử dụng vòng lặp duyệt qua từng chunk 100,000 dòng: Tính toán hoặc làm sạch xong dòng nào thì ghi kết quả tóm tắt ra file mới hoặc nạp vào Database, sau đó bộ nhớ RAM tự động giải phóng trước khi nạp chunk tiếp theo.

6. Tóm Tắt Ghi Nhớ Nhanh (Key Takeaways)

  1. loc = Tìm theo Tên/Nhãn (Lấy cả điểm mút cuối); iloc = Tìm theo Vị trí số nguyên (Bỏ điểm mút cuối).
  2. Xử lý NaN: Dùng Median cho dữ liệu số bị lệch; dùng Mode hoặc nhãn 'Unknown' cho dữ liệu danh mục; chỉ dropna khi lượng thiếu rất nhỏ.
  3. Dùng pd.merge() khi cần ghép bảng theo cột khóa chung (tương đương SQL JOIN); dùng pd.concat() khi cần xếp chồng các dòng lên nhau (SQL UNION ALL).
  4. Tối ưu RAM: Luôn chuyển cột text lặp lại sang kiểu category, ép số nguyên sang kiểu bit nhỏ hơn và đọc file lớn qua chunksize.
  5. Luôn làm sạch dữ liệu theo luồng: Xóa trùng lặp →\rightarrow Xóa khoảng trắng →\rightarrow Chuẩn hóa định dạng số/ngày →\rightarrow Xử lý ô trống →\rightarrow Lọc ngoại lai.
0Claps