Giới thiệu
Trong lĩnh vực Trí tuệ Nhân tạo và Học máy, một trong những bài toán kinh điển nhất cho người mới bắt đầu là dự báo giá nhà. Chúng ta thường làm việc với bộ dữ liệu Ames Housing, một bộ dữ liệu phức tạp chứa 80 đặc trưng (thông tin) khác nhau về mỗi căn nhà, từ diện tích, năm xây dựng, đến cả chất lượng của tầng hầm.
Mục tiêu của bài viết này là trình bày một quy trình chuyên nghiệp, từng bước một, để giải quyết bài toán này. Chúng ta sẽ đi từ một tệp dữ liệu thô, bừa bộn đến một mô hình dự đoán có độ chính xác cao. Bài viết sẽ tập trung sâu vào hai giai đoạn quan trọng nhất: Tiền xử lý Dữ liệu và Kỹ thuật Đặc trưng, sau đó giới thiệu các mô hình đã được huấn luyện và phân tích kết quả của chúng.
Quy trình của chúng ta sẽ bao gồm bốn giai đoạn chính:
-
Tiền xử lý và Làm sạch Dữ liệu
-
Kỹ thuật Đặc trưng (Feature Engineering)
-
Huấn luyện và Đánh giá Mô hình
-
Phân tích Kết quả và Sự đánh đổi

Giai đoạn 1: Tiền xử lý Dữ liệu (Data Preprocessing)
Đây là bước nền tảng. Dữ liệu trong thế giới thực không bao giờ sạch sẽ. Mục tiêu của chúng ta là làm cho nó nhất quán và sẵn sàng cho mô hình học.
Gộp Dữ liệu
Bước đầu tiên chúng tôi thực hiện là gộp hai tệp train.csv (dữ liệu huấn luyện) và test.csv (dữ liệu kiểm tra) lại thành một DataFrame tổng. Lý do là để đảm bảo rằng mọi bước xử lý, ví dụ như cách chúng ta điền dữ liệu bị thiếu, đều được áp dụng đồng nhất trên cả hai tập dữ liệu.


Xử lý Giá trị Thiếu (Missing Values)
Đây là một bước quan trọng. Trong bộ dữ liệu này, giá trị bị thiếu (NaN) không phải lúc nào cũng là một lỗi.
-
Trường hợp 1: NaN có nghĩa là "Không có"
Nhiều cột bị thiếu dữ liệu vì căn nhà đơn giản là không có đặc điểm đó. Ví dụ, cộtPoolQC(Chất lượng hồ bơi) bị thiếu không phải là lỗi, mà vì căn nhà không có hồ bơi. -
Cách xử lý (Phân loại): Đối với các cột dạng chữ như
PoolQChayFence(Hàng rào), chúng ta điền giá trịNA(viết tắt của Not Available).
df['PoolQC'] = df['PoolQC'].fillna('NA')
df['Fence'] = df['Fence'].fillna('NA')
- Cách xử lý (Số): Đối với các cột dạng số như
GarageArea(Diện tích gara), chúng ta điền giá trị0.
df['GarageArea'] = df['GarageArea'].fillna(0)
-
Trường hợp 2: NaN thực sự là Lỗi dữ liệu
Một số ít cột bị thiếu dữ liệu do lỗi nhập liệu, ví dụ nhưMSZoning(Phân loại khu vực). -
Cách xử lý thông minh: Thay vì điền một giá trị cố định, chúng ta điền giá trị
NaNnày bằng giá trị phổ biến nhất (mode) của chính khu vực lân cận (Neighborhood) mà căn nhà đó thuộc về.
# Mình sẽ dựa vào cột Neighborhood để điền giá trị cho MSZoning
# Handling missing values in MSZoning
for i in df.Neighborhood.unique():
if (df.MSZoning[df.Neighborhood == i].isnull().sum()) > 0:
df.loc[df.Neighborhood == i,'MSZoning'] = df.loc[df.Neighborhood == i,'MSZoning'].fillna(df.loc[df.Neighborhood == i,'MSZoning'].mode()[0])
Biến đổi Biến mục tiêu (Target Transformation)
Giá nhà (SalePrice) thường bị lệch rất nhiều (có nhiều nhà giá rẻ và một vài nhà siêu đắt). Điều này làm cho mô hình khó học. Chúng ta áp dụng phép biến đổi logarit (np.log1p) lên cột SalePrice để phân phối của nó trở nên cân bằng hơn.
# log transform the sale price
print("Biến đổi log cho y (SalePrice)...")
## After prdictions are made - we need to convert them back to exponential form
y = np.log1p(y)
Điều này cũng thay đổi thước đo lỗi của chúng ta: thay vì dự đoán sai lệch tuyệt đối (ví dụ: 10,000 đô la), mô hình sẽ học cách dự đoán sai lệch theo tỷ lệ phần trăm (RMSLE).
Giai đoạn 2: Kỹ thuật Đặc trưng (Feature Engineering)
Đây là phần sáng tạo và quan trọng nhất. Chúng ta không chỉ dùng 80 cột có sẵn, mà còn tự tạo ra các thông tin mới từ chúng.
Chiến lược 1: Mã hóa Thứ tự (Ordinal Encoding)
Nhiều đặc trưng dạng chữ có tính thứ bậc. Ví dụ, ExterQual (Chất lượng ngoại thất) có các giá trị như Ex (Tuyệt vời), Gd (Tốt), TA (Trung bình). Sẽ rất lãng phí nếu chỉ để mô hình coi đây là các chuỗi ký tự khác nhau.
Vì vậy, chúng ta thực hiện mã hóa thủ công: gán cho chúng các giá trị số có ý nghĩa (Ex=5, Gd=4, TA=3, Fa=2, Po=1) [cite: 1, cell 21]. Điều này cung cấp cho mô hình thông tin quý giá về chất lượng.
Chiến lược 2: Tạo Đặc trưng Mới
Chúng ta tạo ra các cột mới bằng cách kết hợp các cột hiện có:
TotalSF: Tổng diện tích sử dụng (Tổng diện tích tầng hầm + tầng 1 + tầng 2).
df = df.assign(
YrBltAndRemod = df['YearBuilt'] + df['YearRemodAdd'],
TotalSF_SF = df['TotalBsmtSF'] + df['1stFlrSF'] + df['2ndFlrSF'],
Total_sqr_footage = (df['BsmtFinSF1'] + df['BsmtFinSF2'] + df['1stFlrSF'] + df['2ndFlrSF']),
haspool = df['PoolArea'].apply(lambda x: 1 if x > 0 else 0),
has2ndfloor = df['2ndFlrSF'].apply(lambda x: 1 if x > 0 else 0),
hasgarage = df['GarageArea'].apply(lambda x: 1 if x > 0 else 0),
hasbsmt = df['TotalBsmtSF'].apply(lambda x: 1 if x > 0 else 0),
hasfireplace = df['Fireplaces'].apply(lambda x: 1 if x > 0 else 0)
TotalBath: Tổng số phòng tắm (bao gồm cả phòng tắm đầy đủ và 0.5 cho phòng tắm phụ).
df['TotBathrooms'] = df.FullBath + (df.HalfBath*0.5) + df.BsmtFullBath + (df.BsmtHalfBath*0.5)
Age: Tuổi của căn nhà (Năm bánYrSold- Năm xây dựngYearBuilt).
df["Age"] = 2020 - df["YearBuilt"]
Remodeled: Một cột nhị phân (1 hoặc 0) cho biết căn nhà đã được sửa chữa hay chưa.
# If YearRemodAdd != YearBuilt, then a remodeling took place at some point.
df["Remodeled"] = (df["YearRemodAdd"] != df["YearBuilt"]) * 1
Neighborhood_Good: Một cột nhị phân (1 hoặc 0) để đánh dấu các căn nhà ở những khu vực lân cận tốt nhất.
df.loc[df.Neighborhood == 'NridgHt', "Neighborhood_Good"] = 1
df.loc[df.Neighborhood == 'Crawfor', "Neighborhood_Good"] = 1
df.loc[df.Neighborhood == 'StoneBr', "Neighborhood_Good"] = 1
df.loc[df.Neighborhood == 'Somerst', "Neighborhood_Good"] = 1
df.loc[df.Neighborhood == 'NoRidge', "Neighborhood_Good"] = 1
df["Neighborhood_Good"].fillna(0, inplace=True)
Chiến lược 3: Đơn giản hóa Đặc trưng
Đôi khi, quá nhiều chi tiết lại gây nhiễu. Các cột có 10 bậc (như OverallQual - Chất lượng tổng thể) được chúng ta gom lại thành các nhóm đơn giản hơn, ví dụ 3 nhóm (Tốt, Trung bình, Kém), và lưu vào cột mới OverallQual_simple [cite: 1, cell 25].
OverallQual_simple = df.OverallQual.replace({1:1, 2:1, 3:1,4:2, 5:2, 6:2, 7:3, 8:3, 9:3, 10:3})
Chiến lược 4: Biến đổi và Mã hóa Cuối cùng
- Biến đổi Logarit Đặc trưng: Tương tự như
SalePrice, nhiều cột số khác (nhưLotArea- Diện tích lô đất) cũng bị lệch. Chúng ta cũng áp dụngnp.log1pcho tất cả các cột này [cite: 1, cell 33].
skewness = df_num.apply(skew)
skewness = skewness[abs(skewness) > 0.5]
skewed_features = skewness.index
df_num[skewed_features] = df_num[skewed_features].applymap(np.log1p)
- Mã hóa One-Hot: Đối với các cột phân loại còn lại (như
MSZoning), chúng ta sử dụngpd.get_dummiesđể tách chúng thành các cột nhị phân (Có/Không).
# One hot encoding the categorical variables
df_cat = pd.get_dummies(df_cat)
Kết quả của Giai đoạn 1 và 2 là một bộ dữ liệu từ 80 cột gốc đã được xử lý và mở rộng thành 876 cột đặc trưng, sẵn sàng cho việc huấn luyện.

Giai đoạn 3: Huấn luyện và Đánh giá Mô hình
Chúng tôi đã huấn luyện 8 mô hình khác nhau để so sánh, bao gồm các mô hình tuyến tính (như Ridge, Lasso), các mô hình dựa trên cây (như XGBoost, LightGBM) và một mô hình tổng hợp (Ensemble) đặc biệt.
-
Mô hình
StackedRegressor: Đây là một mô hình tiên tiến. Thay vì tự dự đoán, nó là một mô hình quản lý, học hỏi từ các dự đoán của 7 mô hình còn lại để đưa ra quyết định cuối cùng. -
Phương pháp Đánh giá: Chúng tôi sử dụng phương pháp Kiểm định chéo K-Fold (với k=10). Dữ liệu được chia làm 10 phần, mô hình được huấn luyện 10 lần, mỗi lần trên 9 phần và kiểm tra trên 1 phần còn lại. Điều này giúp chúng ta có được một điểm số hiệu suất ổn định và đáng tin cậy.
-
Thước đo: Chúng tôi sử dụng 3 thước đo chính: RMSLE (Lỗi phần trăm), MALE (Lỗi tuyệt đối trung bình trên thang log) và R² (Hệ số xác định, đo mức độ mô hình giải thích được dữ liệu, từ 0 đến 1).
Giai đoạn 4: Phân tích Kết quả và Sự Đánh đổi
Sau khi chạy tất cả 8 mô hình, chúng ta thu được một bảng kết quả chi tiết.

Hiệu suất Mô hình
Dựa trên kết quả, mô hình XGBoost là mô hình hoạt động tốt nhất, đạt điểm RMSLE trung bình là 0.00936 (rất thấp) và R² là 0.905 (rất cao). Điều này có nghĩa là mô hình có khả năng giải thích được 90.5% sự biến động của giá nhà.
Một phát hiện thú vị là các mô hình tuyến tính đơn giản như Lasso và ElasticNet cũng đạt được R² rất cao, xấp xỉ 0.90.
Điều này chứng tỏ Giai đoạn 2 (Kỹ thuật Đặc trưng) đã vô cùng hiệu quả; các đặc trưng mới được tạo ra đã giúp các mô hình đơn giản cũng có thể học rất tốt.
Phân tích Sự Đánh đổi (Trade-off)
Độ chính xác không phải là tất cả. Chúng ta cần xem xét sự đánh đổi giữa thời gian huấn luyện và hiệu suất:
-
Mô hình Chính xác nhất (
StackedRegressor): Mô hình này có R² là 0.859, nhưng mất tới 5618 giây (khoảng 1.56 giờ) để huấn luyện. -
Mô hình Nhanh nhất (
SVR): Mô hình này chỉ mất 6 giây để huấn luyện, nhưng R² chỉ đạt 0.772, thấp nhất trong các mô hình. -
Mô hình Tốt nhất Toàn diện (
XGBoost): Đạt R² cao nhất (0.905) chỉ trong 36.7 giây. -
Mô hình Hiệu quả nhất (
LightGBM): Đạt R² cao (0.897) chỉ trong 11.8 giây.
Kết luận
Bài phân tích này cho thấy rõ rằng Kỹ thuật Đặc trưng (Feature Engineering) là một trong những bước có giá trị nhất trong một dự án Học máy.
Việc lựa chọn mô hình cuối cùng phụ thuộc vào mục tiêu của bạn. Nếu bạn cần tốc độ tối đa cho việc huấn luyện lại liên tục, LightGBM là lựa chọn tuyệt vời. Tuy nhiên, nếu bạn muốn sự cân bằng tốt nhất giữa độ chính xác hàng đầu và thời gian huấn luyện hợp lý, XGBoost là người chiến thắng rõ ràng trong dự án này.
Tham khảo code tại đây: LINK
Missing value - xử lý ở trường hợp 2
Thay vì điền một giá trị cố định, chúng ta điền giá trị NaN này bằng giá trị phổ biến nhất (mode) của chính khu vực lân cận (Neighborhood) mà căn nhà đó thuộc về.
Tại sao mình lại dùng mode để đưa vào thay thế cho giá trị NaN. Chắc chắn khi làm sẽ gặp trường hợp: bản ghi đó là giá trị hiếm nhưng quan trọng,
Liệu việc dùng mode có bị ảnh hưởng bởi thiên kiến (bias) hay không.
Nếu missing do MNAR, thì cách này mình thấy áp dụng không được
Nếu thay thế sai thì sẽ dẫn đến chọn mô hình sai và đưa ra dự đoán không đúng.