1. Khám phá Dữ liệu (EDA)

Bộ dữ liệu của chúng ta, Ames Housing, được thu thập tại Ames, Iowa, mô tả chi tiết đặc điểm của các căn nhà được bán từ năm 2006 đến 2010.

  • Quy mô: 1460 mẫu và 81 đặc trưng
  • Mục tiêu: Dự đoán SalePrice
  • Độ phức tạp: Dữ liệu bao gồm cả đặc trưng số và hạng mục.

1.1. Phân tích Biến mục tiêu: SalePrice

Biểu đồ phân phối cho thấy SalePrice bị lệch phải (right-skewed). Hầu hết các căn nhà tập trung ở mức giá thấp đến trung bình, và có một "đuôi dài" gồm các căn nhà rất đắt tiền.

Ảnh về phân bố của SalePrice
Hình 1: Biểu đồ phân bố của SalePrice

Giải pháp: Biến đổi logarit (log(SalePrice)) giúp giảm độ lệch, làm phân phối chuẩn hơn.

1.2. Xử lý Dữ liệu Khuyết (Missing Data)

Một số cột có tỷ lệ thiếu cực cao như:
- PoolQC: 99.5%
- MiscFeature: 96.3%
- Alley: 93.8%
- Fence: 80.7%

Ảnh về missing data
Hình 2: Biểu đồ Missing Data trong Dataset

Chiến lược: Loại bỏ các cột có quá nhiều giá trị khuyết (>50%).

1.3. Ma trận Tương quan (Correlation Matrix)


Hình 3: Ma trận Tương quan

  • OverallQual (0.8) và GrLivArea (0.7) tương quan mạnh với SalePrice.
  • GarageCarsGarageArea tương quan 0.9 → đa cộng tuyến.

Giải pháp: Sử dụng Regularization (Ridge, Lasso) để ổn định trọng số.

1.4. Phân tích Ngoại lệ (Outlier Analysis)


Hình 4: Hình Boxplot của những feature quan trọng

Từ hình bên trên ta có thể thấy một số căn có diện tích quá lớn (GrLivArea) so với giá → Có thể loại bỏ hoặc kiểm tra lại vì gây sai lệch mô hình.


2. Tiền xử lý dữ liệu (Data Preprocessing)

Mục tiêu: Làm sạch và chuẩn hóa dữ liệu để đảm bảo mô hình học được đúng mối quan hệ thực, không bị nhiễu bởi outlier, giá trị thiếu hay thang đo khác nhau.

2.1. Xử lý Outlier

Dữ liệu thực tế thường có các giá trị ngoại lệ (outlier) — ví dụ nhà cực lớn hoặc cực đắt, gây ảnh hưởng mạnh đến mô hình tuyến tính.

Giải pháp: Dùng Winsorizing (capping) với phương pháp IQR (Interquartile Range) để thay thế các giá trị vượt quá ngưỡng 1.5×IQR bằng giá trị biên hợp lý.

# Outlier Handling using Capping (Winsorizing) with IQR for numerical columns
for col in num_cols:
    Q1 = train_df[col].quantile(0.25)
    Q3 = train_df[col].quantile(0.75)
    IQR = Q3 - Q1

    # Define bounds for capping
    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR

    # Apply capping to training data
    train_df[col] = train_df[col].clip(lower=lower_bound, upper=upper_bound)

    # Apply capping to testing data using bounds from training data
    test_df[col] = test_df[col].clip(lower=lower_bound, upper=upper_bound)

Kết quả: Phân phối dữ liệu trở nên gọn hơn, giảm nhiễu và tránh việc mô hình bị "kéo" bởi các giá trị cực đoan.

2.2. Biến đổi Logarithm cho Dữ liệu Lệch (Skewed Data)

Nhiều biến số trong bộ dữ liệu (như GrLivArea, TotalBsmtSF) có phân phối lệch phải — gây khó khăn cho mô hình tuyến tính.

Giải pháp: Dùng log-transform (log1p) để làm phân phối chuẩn hơn, giúp mô hình học tốt hơn.

# Identify skewed numerical features
skewed_features = train_df[num_cols].skew().sort_values(ascending=False)
skewed_features = skewed_features[abs(skewed_features) > 0.5] # Consider features with absolute skewness > 0.5

print("Skewed numerical features:")
display(skewed_features)

# Apply log transformation to skewed features
for col in skewed_features.index:
    # Use np.log1p which applies log(1+x) to handle potential zero values
    train_df[col] = np.log1p(train_df[col])
    test_df[col] = np.log1p(test_df[col])

Kết quả: Sau khi log-transform, các đặc trưng có phân phối gần chuẩn, giúp cải thiện hiệu suất mô hình và giảm lỗi RMSE.

2.3. Xử lý Missing Data

Một số cột có giá trị bị thiếu, ví dụ GarageYrBlt, MasVnrArea, hoặc LotFrontage.Việc điền giá trị thiếu (imputation) giúp mô hình học được đầy đủ thông tin mà không loại bỏ hàng dữ liệu.

Giải pháp:Dùng Iterative Imputer với mô hình Random Forest Regressor, giúp ước lượng giá trị bị thiếu dựa trên mối quan hệ phi tuyến giữa các đặc trưng.

from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
from sklearn.ensemble import RandomForestRegressor

iterative_imputer = IterativeImputer(random_state=42, estimator=RandomForestRegressor(random_state=42))

train_df[num_cols] = iterative_imputer.fit_transform(train_df[num_cols])
test_df[num_cols] = iterative_imputer.transform(test_df[num_cols])

Kết quả: Dữ liệu không còn giá trị NaN, và các giá trị thay thế hợp lý hơn so với trung bình hay trung vị.

2.3. Feature Scaling

Để đảm bảo các biến có cùng thang đo (vì GrLivArea có thể hàng nghìn trong khi GarageCars chỉ vài đơn vị), ta cần chuẩn hóa dữ liệu.

Phương pháp: StandardScaler (chuẩn hóa z-score)

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
train_num_features = scaler.fit_transform(train_df[num_cols])
test_num_features = scaler.transform(test_df[num_cols])

Kết quả: Mỗi đặc trưng có giá trị trung bình = 0, độ lệch chuẩn = 1 → giúp mô hình hội tụ nhanh hơn, đặc biệt là với Ridge và Lasso.


3. Feature Engineering & PolynomialFeatures

Mục tiêu: Tạo thêm các đặc trưng mới (feature) có ý nghĩa thống kê để giúp mô hình học được các quan hệ phi tuyến tính giữa các biến đầu vào và SalePrice.

3.1. Feature Engineering

Từ việc phân tích tương quan và hiểu biết domain (bất động sản), ta tạo ra các biến tương tácbiến tổng hợp quan trọng:

# Combine some important numerical features
train_df["OverallQual_GrLivArea"] = train_df["OverallQual"] * train_df["GrLivArea"]
test_df["OverallQual_GrLivArea"] = test_df["OverallQual"] * test_df["GrLivArea"]

train_df["GarageCars_GarageArea"] = train_df["GarageCars"] * train_df["GarageArea"]
test_df["GarageCars_GarageArea"] = test_df["GarageCars"] * test_df["GarageArea"]

train_df["TotalBsmtSF_1stFlrSF"] = train_df["TotalBsmtSF"] * train_df["1stFlrSF"]
test_df["TotalBsmtSF_1stFlrSF"] = test_df["TotalBsmtSF"] * test_df["1stFlrSF"]

train_df["TotalSF"] = train_df["TotalBsmtSF"] + train_df["1stFlrSF"] + train_df["2ndFlrSF"]
test_df["TotalSF"] = test_df["TotalBsmtSF"] + test_df["1stFlrSF"] + test_df["2ndFlrSF"]

train_df["HouseAge"] = train_df["YrSold"] - train_df["YearBuilt"]
test_df["HouseAge"] = test_df["YrSold"] - test_df["YearBuilt"]

train_df["RemodAge"] = train_df["YrSold"] - train_df["YearRemodAdd"]
test_df["RemodAge"] = test_df["YrSold"] - test_df["YearRemodAdd"]

Giải thích:

  • OverallQual_GrLivArea: kết hợp chất lượng và diện tích → biểu diễn “mức sống” tổng thể.

  • TotalSF: tổng diện tích có thể sử dụng (basement + tầng 1 + tầng 2).

  • HouseAge: độ tuổi ngôi nhà tại thời điểm bán.

  • RemodAge: số năm kể từ lần tu sửa gần nhất.

Kết quả: Những đặc trưng mới này giúp mô hình nắm bắt được tương tác phi tuyến giữa các yếu tố vật lý và giá bán.

3.2. PolynomialFeatures

Sau khi thêm các đặc trưng cơ bản, ta mở rộng không gian đặc trưng bằng đa thức bậc 2 (Polynomial Features).

Điều này giúp mô hình tuyến tính có thể học được các mối quan hệ bậc cao (ví dụ: diện tích tăng gấp đôi không đồng nghĩa với giá gấp đôi).

from sklearn.preprocessing import PolynomialFeatures

poly_features = PolynomialFeatures(
    degree=2, interaction_only=True, include_bias=False
)

train_poly_features = poly_features.fit_transform(train_df[num_cols])
test_poly_features = poly_features.transform(test_df[num_cols])

Kết quả:
Số đặc trưng tăng mạnh (từ ~35 → vài trăm), biểu diễn được các quan hệ tương tác như:

  • GrLivArea * OverallQual

  • GarageArea * YearBuilt

  • TotalBsmtSF * 1stFlrSF

Điều này làm mô hình mạnh hơn, nhưng cũng cần regularization (Ridge, Lasso) để tránh overfitting.

3.3. Kết quả Mô hình Sau Feature Engineering

Ở đây mình dùng những mô hình Machine Learning đơn giản như Linear Regression, Lasso, Ridge.

from sklearn.linear_model import LinearRegression, Ridge, Lasso

models = {
    "LinearRegression": LinearRegression(),
    "Ridge": Ridge(),
    "Lasso": Lasso()
}

Kết quả của những mô hình Machine Learning sau quá trình feature engineering.

Model Train_RMSE Test_RMSE Train_R2 Test_R2
Ridge 17759.525682 27989.099849 0.948052 0.888172
Lasso 16470.224921 29594.118050 0.955321 0.874979
LinearRegression 14402.603230 31277.627194 0.965834 0.860350

Nhận xét:

  • Cả ba mô hình đều có Train $R^2$ > 0.94, chứng tỏ học tốt trên tập huấn luyện.

  • Ridge đạt Test $R^2$ cao nhất (0.888) → tổng quát tốt nhất trong nhóm tuyến tính.

  • Feature Engineering và Polynomial Features giúp mô hình học được mối quan hệ phi tuyến, cải thiện độ chính xác rõ rệt so với baseline.


4. Mô hình Nâng cao

4.1. Random Forest Regressor

Random Forest là mô hình ensemble của nhiều Decision Tree, mỗi cây học từ một mẫu con của dữ liệu (bootstrap sampling).

Kết quả dự đoán cuối cùng là trung bình của tất cả cây → giúp giảm phương sai (variance) và tránh overfitting.

Cấu hình sử dụng:

from sklearn.ensemble import RandomForestRegressor

RandomForestRegressor(n_estimators=100, random_state=42))

Ưu điểm:

  • Ổn định với nhiễu.
  • Xử lý tốt cả biến liên tục và rời rạc.
  • Cho phép trích xuất feature importance.

4.2. Gradient Boosting Regressor

Khác với Random Forest, Gradient Boosting xây dựng các cây nối tiếp nhau, trong đó mỗi cây mới học để sửa lỗi của cây trước.

Nhờ đó, mô hình đạt hiệu suất cao hơn nhưng dễ overfit nếu không regularize đúng.

Cấu hình sử dụng:

from sklearn.ensemble import GradientBoostingRegressor

GradientBoostingRegressor(
    n_estimators=100,
    learning_rate=0.1,
    max_depth=3,
    random_state=42
)RandomForestRegressor(n_estimators=100, random_state=42))

4.3. Kết quả so sánh

Model Train_RMSE Test_RMSE Train_$R^2$ Test_$R^2$
RandomForestRegressor 11,750 26,980 0.970 0.899
GradientBoostingRegressor 11,626 24,682 0.974 0.913

Gradient Boosting vượt trội hơn với Test $R^2 = 0.913$ — trở thành mô hình tốt nhất tính đến thời điểm này.


5. Random Forest cho feature selection

Sau khi thử nghiệm các mô hình nâng cao, bước tiếp theo là xác định đặc trưng nào quan trọng nhất.

5.1. Feature Importance

Dựa trên trọng số của Random Forest, top 10 đặc trưng ảnh hưởng mạnh nhất tới SalePrice là:

Thứ hạng Đặc trưng Importance
1 OverallQual_GrLivArea 0.072
2 TotalSF 0.065
3 GrLivArea 0.058
4 GarageCars_GarageArea 0.054
5 TotalBsmtSF_1stFlrSF 0.049
6 OverallQual 0.045
7 GarageArea 0.043
8 YearBuilt 0.040
9 1stFlrSF 0.039
10 FullBath 0.036

Nhận xét:

Những đặc trưng về chất lượng tổng thể (OverallQual), diện tích sử dụng (GrLivArea, TotalSF), và garage vẫn chiếm ưu thế.

Các đặc trưng mở rộng như OverallQual_GrLivArea (biến tương tác) chứng tỏ hiệu quả của Feature Engineering.

5.2. Feature Selection

Ta chọn Top 50 đặc trưng quan trọng nhất, huấn luyện lại mô hình trên tập này — giúp giảm chiều dữ liệu và cải thiện tốc độ.

6. Ensemble Learning

Để tối ưu hơn nữa, ta kết hợp nhiều mô hình khác nhau thành Ensemble — một chiến lược mạnh mẽ trong Machine Learning.

6.1. Voting Regressor

Kết hợp kết quả trung bình của:

  • Gradient Boosting

  • Random Forest

  • Lasso Regression

ensemble = VotingRegressor([
    ("GradientBoostingRegressor", GradientBoostingRegressor(n_estimators=100, random_state=42)),
    ("RandomForestRegressor", RandomForestRegressor(n_estimators=100, random_state=42)),
    ("Lasso", Lasso(alpha=10)),
])

6.2. Stacking Regressor

Stacking kết hợp nhiều mô hình "base learners" (XGB, LGBM) và một meta learner (Lasso) ở tầng trên cùng:

stack = StackingRegressor(
    estimators=[
        ("xgb", XGBRegressor(random_state=42)),
        ("lgb", LGBMRegressor(random_state=42))
    ],
    final_estimator=Lasso(alpha=10),
    passthrough=True
)

6.3. Kết quả Tổng hợp

Model Train_RMSE Test_RMSE Train_$R^2$ Test_$R^2$
Lasso 17,234 29,432 0.950 0.876
Ridge 17,759 27,989 0.948 0.888
Random Forest 11,750 26,980 0.970 0.899
Gradient Boosting 11,626 24,682 0.974 0.913
Voting Ensemble 10,980 23,910 0.978 0.919
Stacking Regressor 10,402 23,400 0.982 0.922

Kết luận:

  • Stacking cho kết quả tốt nhất với $R^2 = 0.922$, $RMSE \approx 23,400$.

  • Ensemble không chỉ giảm sai số mà còn giúp mô hình ổn định và tổng quát hóa tốt hơn.

7. Giải thích Mô hình (XAI)

Để hiểu vì sao mô hình dự đoán như vậy, ta áp dụng Explainable AI gồm hai kỹ thuật phổ biến: SHAPLIME.

7.1. SHAP (SHapley Additive Explanations)

SHAP cho phép xem mức độ đóng góp của từng đặc trưng tới giá trị dự đoán cụ thể.

import shap

def shap_ensemble_analysis(ensemble_model, X_train, X_test, feature_names, max_samples=100):
    X_train = pd.DataFrame(X_train, columns=feature_names)
    X_test = pd.DataFrame(X_test, columns=feature_names)

    # Create background dataset (sample from training data)
    data = X_train.sample(n=min(100, len(X_train)), random_state=42)

    # Initialize SHAP KernelExplainer
    explainer = shap.KernelExplainer(ensemble_model.predict, data)

    # Calculate SHAP values for test set sample
    test_sample = X_test.head(max_samples)

    shap_values = explainer.shap_values(test_sample)

    return explainer, shap_values, test_sample

Nhóm mình chạy code ở bên trên dùng Ensemble Model.

# Get the best performing model (Ensemble model)
best_model = advanced_models_selected["Ensemble"]

# Get the feature names for the selected features
feature_names_selected = [feature_names[i] for i in selected_indices]

# Perform SHAP analysis using the provided function
explainer, shap_values, test_sample = shap_ensemble_analysis(best_model, X_train_selected, X_test_selected, feature_names_selected)

# Generate SHAP summary plot
print("SHAP Summary Plot:")
shap.summary_plot(shap_values, test_sample, feature_names=feature_names_selected)
plt.show()

# Generate a SHAP force plot for the first test sample
print("\nSHAP Force Plot for the first test sample:")
shap.initjs() # Initialize JS for force plot
shap.force_plot(explainer.expected_value, shap_values[0,:], features=test_sample.iloc[0,:], feature_names=feature_names_selected)

Kết quả như ở dưới.


Hình 5: Kết qủa của Shap

Biểu đồ SHAP Summary cho thấy các đặc trưng ảnh hưởng mạnh nhất:

  • OverallQual

  • GrLivArea

  • TotalSF

  • GarageCars

  • YearBuilt

Dựa trên hình kết quả của Shap ta có thể hiểu được. Ví dụ, với một căn nhà có giá dự đoán $230,000:

Feature Đóng góp (ΔSalePrice) Ảnh hưởng
OverallQual +$20,000 Tăng giá
GrLivArea +$15,000 Tăng giá
GarageCars +$8,000 Tăng giá
HouseAge -$5,000 Giảm giá

7.2. LIME (Local Interpretable Model-Agnostic Explanations)

LIME giúp giải thích từng dự đoán cá nhân, bằng cách mô phỏng mô hình phức tạp bằng mô hình tuyến tính cục bộ quanh điểm cần giải thích.

from lime import lime_tabular

model_to_explain = advanced_models_selected["Ensemble"]

# Create a LIME explainer
explainer = lime_tabular.LimeTabularExplainer(
    training_data=X_train_selected,
    mode='regression',
    feature_names=feature_names_selected,
    random_state=42
)

# Choose an instance from the test set to explain 
instance_to_explain = X_test_selected[0]

# Explain the instance's prediction
explanation = explainer.explain_instance(
    data_row=instance_to_explain,
    predict_fn=model_to_explain.predict,
    num_features=10
)

# Visualize the explanation
print("LIME Explanation for the first test instance:")
explanation.as_pyplot_figure()
plt.show()

# Print the explanation as text
print("\nLIME Explanation (Text):")
print(explanation.as_list())


Hình 6: Kết qủa của LIME vởi 1 sample

8. Tổng Hợp

Giai đoạn Kỹ thuật chính Cải thiện
EDA Phân tích SalePrice, loại missing, xử lý outlier Làm sạch dữ liệu
Preprocessing Chuẩn hóa, log-transform, imputation Phân phối chuẩn
Feature Engineering Tạo biến tương tác, polynomial Bổ sung phi tuyến tính
Modeling Ridge, Lasso, Gradient Boosting $R^2 = 0.91$
Feature Selection Random Forest Importance Giảm chiều dữ liệu
Ensemble Voting + Stacking $R^2 = 0.922$
XAI SHAP + LIME Giải thích mô hình

Kết quả cuối cùng:
$R^2 = 0.922$, $RMSE = 23,400$
→ Một mô hình mạnh, cân bằng giữa độ chính xác và khả năng giải thích.

Tài liệu tham khảo

[1] AIO Vietnam 2025 – Project 5.1 Guidelines

[2] Kaggle: House Prices - Advanced Regression Techniques

[3] Lundberg & Lee (2017), “A Unified Approach to Interpreting Model Predictions”

[4] Ribeiro et al. (2016), “Why Should I Trust You?”: Explaining Predictions of Any Classifier