Bài 3: Câu chuyện khuyết dữ liệu
🎯 Mục tiêu học tập
Sau khi hoàn thành bài này, bạn sẽ có thể:
- Hiểu được các loại dữ liệu khuyết và nguyên nhân gây ra chúng
- Phân biệt được MCAR, MAR, và MNAR
- Áp dụng các phương pháp xử lý phù hợp cho từng loại dữ liệu khuyết
- Sử dụng Python để phát hiện và xử lý dữ liệu khuyết trong thực tế
1. Khuyết dữ liệu (Missing values) là gì?
Dữ liệu bị khuyết hoặc không đầy đủ (thường được hiển thị như NaN, Null, N/A,… trong Pandas) là 1 thách thức có tác động lớn đến bất kỳ dự án khoa học dữ liệu nào. Việc khuyết dữ liệu xảy ra có thể bởi nhiều nguyên nhân như:
- Thông tin vốn không tồn tại
- Người dùng quên điền
- Lỗi từ phần mềm thu thập dữ liệu
- Dữ liệu bị mất trong quá trình chuyển thủ công từ cơ sở dữ liệu cũ ...
Mặc dù giải pháp tốt nhất cho việc khuyết dữ liệu là tránh ngay từ đầu, tức là phát triển các chính sách quản lý và thu thập dữ liệu thật tốt. Tuy nhiên, không phải lúc nào việc thu thập dữ liệu cũng dễ dàng và màu hường như vậy.
Xét về mặt chất lượng, dữ liệu bị khuyết có thể được chia về 4 loại:
- Dữ liệu bị khuyết về cấu trúc
- Dữ liệu bị khuyết hoàn toàn ngẫu nhiên (MCAR)
- Dữ liệu bị khuyết ngẫu nhiên (MAR)
- Dữ liệu bị khuyết không phải ngẫu nhiên (MNAR)
Trong bài viết này, ZootoPi sẽ giúp bạn hiểu được sự khác nhau giữa các loại khuyết dữ liệu này cũng như cách ta có thể xử lý với từng loại để mọi phân tích đều có ý nghĩa.
1.1 Dữ liệu bị khuyết về cấu trúc
Dữ liệu bị khuyết về mặt cấu trúc là dữ liệu bị khuyết vì một lý do hợp lý. Nói cách khác, đó là dữ liệu bị khuyết bởi vì nó không tồn tại.
-
Ví dụ: Kết quả thi 1 học kỳ tại 1 trường đại học bị khuyết điểm 1 số môn của 1 số sinh viên. Điều này có thể xảy ra khi học sinh đã bỏ học môn đó trước kỳ thi hoặc có thể vắng mặt trong buổi thi. Vì vậy, đây là một giá trị bị khuyết về mặt cấu trúc.
-
Giải pháp:
- Loại trừ những bản ghi có dữ liệu bị khuyết như vậy khỏi bất kỳ phân tích nào về các biến có giá trị bị khuyết về cấu trúc.
- Luận suy bằng cách chèn số 0 vào những chỗ còn khuyết đó.
1.2 Dữ liệu bị khuyết hoàn toàn ngẫu nhiên (MCAR)
Như tên gọi, dữ liệu bị khuyết trong trường hợp này xảy ra hoàn toàn do ngẫu nhiên, nghĩa là không tồn tại bất kỳ mối quan hệ hay sự liên quan nào giữa dữ liệu bị khuyết với các dữ liệu quan sát được.
1.3 Dữ liệu bị khuyết ngẫu nhiên (MAR)
Dữ liệu bị khuyết trong trường hợp này xảy ra do ngẫu nhiên, tuy nhiên vẫn có mối quan hệ giữa dữ liệu bị khuyết và dữ liệu quan sát được. Điều đó cũng có nghĩa là dữ liệu bị khuyết ngẫu nhiên có tác động đến sự sai lệch của dữ liệu cũng như độ tin cậy của mô hình về sau.
1.4 Dữ liệu bị khuyết không phải ngẫu nhiên (MNAR)
Dữ liệu bị khuyết không phải là ngẫu nhiên mà các giá trị này bị bỏ sót 1 cách có chủ ý, tức là có sự liên quan về mặt xu hướng 1 cách có hệ thống giữa giá trị bị khuyết và giá trị không bị khuyết trong một biến và điều đó có thể tác động đến sự sai lệch của dữ liệu.
2. Phân biệt các loại khuyết dữ liệu
Trong thực tế, khi làm việc với 1 tập dữ liệu và đối mặt với vấn đề mất mát hay khuyết dữ liệu, ta cần phân loại được dữ liệu đang bị khuyết theo cơ chế nào để từ đó đưa ra những hướng giải quyết phù hợp đảm bảo độ tin cậy của dữ liệu.
Thông thường, ta khó có thể phân biệt tường minh giữa các loại khuyết dữ liệu này, đặc biệt là MAR so với MNAR. Tuy nhiên, ta có thể kiểm tra xem dữ liệu khuyết theo MCAR hay MAR thông qua thử nghiệm tạo các biến giả rồi thử nghiệm t-test và kiểm định chi-squared test giữa biến này và các biến khác trong tập dữ liệu để xem liệu sự thiếu hụt trên biến này có liên quan đến giá trị của các biến khác hay không.
Ví dụ: nếu nữ giới thực sự ít có khả năng cho bạn biết cân nặng của họ hơn nam giới, thì chi-squared test sẽ cho bạn biết rằng tỷ lệ khuyết dữ liệu trên biến cân nặng ở phụ nữ cao hơn nam giới. Từ đó, ta có thể kết luận rằng trường cân nặng là MAR.
3. Phát hiện dữ liệu khuyết với Python
Trước khi xử lý dữ liệu khuyết, chúng ta cần phát hiện và hiểu được mức độ khuyết dữ liệu trong dataset. Dưới đây là các cách phổ biến:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# Tạo dataset mẫu để minh họa
data = {
'tuoi': [25, 30, np.nan, 35, 40, np.nan, 28],
'luong': [5000, 6000, 7000, np.nan, 8000, 5500, np.nan],
'gioi_tinh': ['Nam', 'Nữ', 'Nam', 'Nữ', np.nan, 'Nam', 'Nữ'],
'kinh_nghiem': [2, 5, np.nan, 8, 10, 3, 4]
}
df = pd.DataFrame(data)
# 1. Kiểm tra số lượng giá trị khuyết
print("Số lượng giá trị khuyết:")
print(df.isnull().sum())
# 2. Tỷ lệ phần trăm giá trị khuyết
print("\nTỷ lệ phần trăm giá trị khuyết:")
print(df.isnull().sum() / len(df) * 100)
# 3. Tổng số giá trị khuyết
print(f"\nTổng số giá trị khuyết: {df.isnull().sum().sum()}")
# 4. Trực quan hóa dữ liệu khuyết
plt.figure(figsize=(10, 6))
sns.heatmap(df.isnull(), cbar=True, yticklabels=False, cmap='viridis')
plt.title('Bản đồ dữ liệu khuyết')
plt.show()
# 5. Thống kê chi tiết về dữ liệu khuyết
print("\nThống kê chi tiết:")
print(df.info())4. Xử lý dữ liệu khuyết
4.1. Xử lý dữ liệu khuyết về cấu trúc
Phương pháp:
- Loại bỏ các bản ghi có dữ liệu khuyết về cấu trúc
- Hoặc điền giá trị 0 hoặc giá trị mặc định phù hợp
# Ví dụ: Loại bỏ các bản ghi có dữ liệu khuyết về cấu trúc
df_cleaned = df.dropna(subset=['tuoi']) # Loại bỏ nếu thiếu tuổi
# Hoặc điền giá trị mặc định
df['tuoi'].fillna(0, inplace=True) # Điền 0 cho tuổi khuyết4.2. Xử lý dữ liệu MCAR (Missing Completely At Random)
Phương pháp:
- Xóa dữ liệu (Deletion): Nếu tỷ lệ khuyết < 5%, có thể xóa an toàn
- Điền giá trị (Imputation): Mean/Median/Mode cho dữ liệu số, Mode cho dữ liệu phân loại
# Phương pháp 1: Xóa dữ liệu (chỉ khi tỷ lệ khuyết rất thấp)
df_dropped = df.dropna() # Xóa tất cả dòng có giá trị khuyết
df_dropped_cols = df.dropna(axis=1) # Xóa cột có giá trị khuyết
# Phương pháp 2: Điền giá trị trung bình (cho dữ liệu số)
df['luong'].fillna(df['luong'].mean(), inplace=True)
# Phương pháp 3: Điền giá trị trung vị (ít bị ảnh hưởng bởi outliers)
df['luong'].fillna(df['luong'].median(), inplace=True)
# Phương pháp 4: Điền giá trị mode (cho dữ liệu phân loại)
df['gioi_tinh'].fillna(df['gioi_tinh'].mode()[0], inplace=True)
# Phương pháp 5: Điền giá trị forward fill hoặc backward fill (cho dữ liệu thời gian)
df['kinh_nghiem'].fillna(method='ffill', inplace=True) # Forward fill
df['kinh_nghiem'].fillna(method='bfill', inplace=True) # Backward fill4.3. Xử lý dữ liệu MAR (Missing At Random)
Phương pháp:
- Imputation có điều kiện: Điền giá trị dựa trên các biến liên quan
- Mô hình dự đoán: Sử dụng regression hoặc classification để dự đoán giá trị khuyết
# Ví dụ: Điền lương dựa trên kinh nghiệm
# Tạo nhóm dựa trên kinh nghiệm
df['nhom_kinh_nghiem'] = pd.cut(df['kinh_nghiem'],
bins=[0, 3, 6, 10, np.inf],
labels=['Mới', 'Trung bình', 'Cao', 'Rất cao'])
# Điền lương trung bình theo nhóm kinh nghiệm
df['luong'] = df.groupby('nhom_kinh_nghiem')['luong'].transform(
lambda x: x.fillna(x.mean())
)
# Hoặc sử dụng mô hình dự đoán (ví dụ với Linear Regression)
from sklearn.linear_model import LinearRegression
from sklearn.impute import SimpleImputer
# Tách dữ liệu có và không có giá trị khuyết
df_with_salary = df[df['luong'].notna()]
df_missing_salary = df[df['luong'].isna()]
# Huấn luyện mô hình dự đoán lương dựa trên tuổi và kinh nghiệm
X_train = df_with_salary[['tuoi', 'kinh_nghiem']].fillna(df_with_salary[['tuoi', 'kinh_nghiem']].mean())
y_train = df_with_salary['luong']
X_test = df_missing_salary[['tuoi', 'kinh_nghiem']].fillna(df_missing_salary[['tuoi', 'kinh_nghiem']].mean())
model = LinearRegression()
model.fit(X_train, y_train)
predicted_salary = model.predict(X_test)
df.loc[df['luong'].isna(), 'luong'] = predicted_salary4.4. Xử lý dữ liệu MNAR (Missing Not At Random)
Phương pháp:
- Phân tích độ nhạy: So sánh kết quả với và không có dữ liệu khuyết
- Mô hình đặc biệt: Sử dụng các mô hình có thể xử lý dữ liệu khuyết (ví dụ: XGBoost)
- Thu thập thêm dữ liệu: Nếu có thể, thu thập lại dữ liệu để hiểu rõ nguyên nhân
# Ví dụ: Sử dụng XGBoost có thể xử lý dữ liệu khuyết tự động
from xgboost import XGBRegressor
# XGBoost có thể xử lý NaN tự động
model = XGBRegressor()
# model.fit(X, y) # Có thể train trực tiếp với NaN
# Hoặc sử dụng Multiple Imputation
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
imputer = IterativeImputer(random_state=42)
df_imputed = pd.DataFrame(imputer.fit_transform(df), columns=df.columns)5. Decision Tree: Chọn phương pháp xử lý
Bắt đầu
|
├─ Dữ liệu khuyết về cấu trúc?
| └─> Loại bỏ hoặc điền giá trị mặc định (0, "N/A")
|
├─ Tỷ lệ khuyết < 5%?
| └─> Có thể xóa an toàn
|
├─ Dữ liệu số?
| ├─> Mean/Median imputation
| └─> Hoặc sử dụng mô hình dự đoán
|
├─ Dữ liệu phân loại?
| └─> Mode imputation
|
├─ Dữ liệu thời gian?
| └─> Forward fill / Backward fill
|
└─ Dữ liệu phức tạp (MNAR)?
└─> Sử dụng mô hình đặc biệt hoặc Multiple Imputation
6. Best Practices và Common Mistakes
✅ Best Practices
-
Luôn kiểm tra tỷ lệ khuyết trước khi quyết định phương pháp
missing_percentage = df.isnull().sum() / len(df) * 100 if missing_percentage > 50: print("Cảnh báo: Tỷ lệ khuyết quá cao, cần xem xét lại") -
So sánh kết quả trước và sau khi xử lý
# Thống kê trước khi xử lý print("Trước khi xử lý:") print(df.describe()) # Xử lý df['luong'].fillna(df['luong'].median(), inplace=True) # Thống kê sau khi xử lý print("\nSau khi xử lý:") print(df.describe()) -
Lưu lại thông tin về dữ liệu khuyết để phân tích sau
# Tạo cột flag để đánh dấu giá trị đã được điền df['luong_was_missing'] = df['luong'].isnull() df['luong'].fillna(df['luong'].median(), inplace=True) -
Sử dụng cross-validation khi dùng mô hình dự đoán để điền giá trị
❌ Common Mistakes
- Xóa dữ liệu quá nhiều: Không nên xóa nếu tỷ lệ khuyết > 20%
- Điền giá trị không phù hợp: Không nên điền mean cho dữ liệu phân loại
- Bỏ qua phân tích nguyên nhân: Luôn cố gắng hiểu tại sao dữ liệu bị khuyết
- Không kiểm tra phân phối sau khi điền: Cần đảm bảo phân phối không bị thay đổi quá nhiều
7. Ví dụ thực tế: Xử lý dữ liệu khuyết trong dataset bất động sản
import pandas as pd
import numpy as np
# Giả sử có dataset bất động sản
data = {
'dien_tich': [50, 70, np.nan, 100, 120, np.nan, 80],
'gia': [2e9, 3e9, 4e9, np.nan, 6e9, 2.5e9, np.nan],
'quan': ['Q1', 'Q2', 'Q1', 'Q3', np.nan, 'Q2', 'Q1'],
'nam_xay': [2010, 2015, np.nan, 2020, 2018, 2012, 2016]
}
df = pd.DataFrame(data)
# Bước 1: Phân tích dữ liệu khuyết
print("Phân tích ban đầu:")
print(df.isnull().sum())
print(f"\nTỷ lệ khuyết: {df.isnull().sum() / len(df) * 100}")
# Bước 2: Xử lý từng cột
# Diện tích: Điền trung bình theo quận
df['dien_tich'] = df.groupby('quan')['dien_tich'].transform(
lambda x: x.fillna(x.mean())
)
# Giá: Sử dụng mô hình dự đoán dựa trên diện tích
from sklearn.linear_model import LinearRegression
df_with_price = df[df['gia'].notna()]
df_missing_price = df[df['gia'].isna()]
if len(df_with_price) > 0 and len(df_missing_price) > 0:
X_train = df_with_price[['dien_tich']].fillna(df_with_price['dien_tich'].mean())
y_train = df_with_price['gia']
X_test = df_missing_price[['dien_tich']].fillna(df_missing_price['dien_tich'].mean())
model = LinearRegression()
model.fit(X_train, y_train)
df.loc[df['gia'].isna(), 'gia'] = model.predict(X_test)
# Quận: Điền mode
df['quan'].fillna(df['quan'].mode()[0], inplace=True)
# Năm xây: Điền trung vị
df['nam_xay'].fillna(df['nam_xay'].median(), inplace=True)
print("\nSau khi xử lý:")
print(df.isnull().sum())✅ Tóm tắt
- 4 loại dữ liệu khuyết: Cấu trúc, MCAR, MAR, MNAR
- Phát hiện: Sử dụng
isnull(),info(), và visualization - Xử lý MCAR: Xóa hoặc điền mean/median/mode
- Xử lý MAR: Imputation có điều kiện hoặc mô hình dự đoán
- Xử lý MNAR: Mô hình đặc biệt hoặc Multiple Imputation
- Best Practice: Luôn phân tích nguyên nhân và so sánh kết quả
🧪 Thực hành
Hãy thử áp dụng các phương pháp trên với dataset của riêng bạn:
- Tải một dataset có dữ liệu khuyết (ví dụ: từ Kaggle)
- Phân tích và phân loại loại dữ liệu khuyết
- Áp dụng phương pháp xử lý phù hợp
- So sánh kết quả trước và sau khi xử lý
📚 Đọc thêm
- Pandas Documentation - Working with missing data
- Scikit-learn - Imputation
- Handling Missing Data in Python
➡️ Bước tiếp theo
Sau khi nắm vững xử lý dữ liệu khuyết, bạn có thể tiếp tục với:
- Feature Engineering
- Data Transformation
- Exploratory Data Analysis (EDA)
Chúc các bạn học tập vui vẻ!