I. Tổng quan về project

Dự án Dự đoán giá nhà là một bài toán hồi quy trong machine learning, hướng tới việc ước lượng giá bất động sản từ tập hợp đặc trưng mô tả ngôi nhà và khu vực. Quy trình bao gồm: khám phá dữ liệu (EDA), tiền xử lý, xây dựng và hiệu chỉnh mô hình, sau đó đánh giá – so sánh để chọn phương án tối ưu.

Mục tiêu chính

  • Thực hiện EDA để nắm rõ cấu trúc, phân phối và mối quan hệ giữa các biến.
  • Xây dựng các mô hình hồi quy dự đoán giá nhà.
  • Đánh giá & so sánh hiệu suất mô hình bằng các thước đo chuẩn (ví dụ: RMSE, R²).
  • Áp dụng cải tiến (regularization, đặc trưng tương tác, pipeline hóa) nhằm nâng độ chính xác và tính tái lập.

II. Các bước thực thi code

Bước 1: Import thư viện và tải dữ liệu

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import OneHotEncoder, MinMaxScaler, PolynomialFeatures
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LinearRegression, Ridge, Lasso
from sklearn.metrics import mean_squared_error, r2_score
import warnings
warnings.filterwarnings('ignore')

Trong bước đầu tiên, các thư viện cần thiết cho xử lý dữ liệu, trực quan hóa và xây dựng mô hình được import, bao gồm

  • pandas, numpy: xử lý dữ liệu, tính toán số.
  • matplotlib.pyplot, seaborn: vẽ biểu đồ thăm dò.
  • train_test_split: chia dữ liệu train/test.
  • OneHotEncoder, MinMaxScaler, PolynomialFeatures: tiền xử lý – mã hoá biến phân loại, chuẩn hoá thang đo, tạo đặc trưng đa thức/interaction.
  • SimpleImputer: điền giá trị thiếu.
  • LinearRegression, Ridge, Lasso: các mô hình hồi quy tuyến tính (thường, L2, L1).
  • mean_squared_error, r2_score: thước đo đánh giá mô hình (MSE và R²).
  • warnings.filterwarnings('ignore'): ẩn bớt cảnh báo khi chạy.
data = pd.read_csv("train-house-prices-advanced-regression-techniques.csv")
data.head()
data.shape

Tiếp theo, tập dữ liệu House Prices – Advanced Regression Techniques được tải và đọc vào DataFrame bằng hàm read_csv() của Pandas.

Sau đó, một vài dòng đầu tiên được hiển thị để kiểm tra cấu trúc tổng quan, và kích thước của dữ liệu (1460, 81) cũng được xem nhanh bằng thuộc tính shape để nắm quy mô tập dữ liệu.

Bước 2: Khám phá dữ liệu

sns.displot(data['SalePrice'], kde=True, height=4.5, aspect=1.5)
plt.axvline(x=data['SalePrice'].mean(), color='red', linestyle="--", linewidth=2, label='Giá trị trung bình')

plt.title("Phân phối của giá bán (SalePrice)")
plt.xlabel("Giá bán (SalePrice)") 
plt.ylabel("Tần suất") 
plt.legend() 
plt.show()

image.png

Biểu đồ phân phối của biến SalePrice được trực quan hóa bằng sns.displot() kèm đường KDE để thấy xu hướng và mật độ phân tán của giá nhà. Đường thẳng đứng màu đỏ tại giá trị trung bình được thêm bằng plt.axvline(...) để so sánh vị trí mean với toàn bộ phân phối.

plt.figure(figsize=(30, 9))
sns.heatmap(
data.corr(numeric_only=True),
cmap='coolwarm',
linewidths=0.5,
center=0,
cbar_kws={"shrink": 0.8}
)
plt.title("Correlation Heatmap of Numerical features", fontsize=16, pad=15)
plt.show()

image.png

Ma trận tương quan (heatmap) trực quan hóa mức độ tương quan giữa các thuộc tính số. Thang màu coolwarm: xanh là tương quan âm, đỏ là tương quan dương, trắng (center=0) là quan hệ yếu. Đường kẻ mảnh (linewidths=0.5) phân tách các ô. Heatmap giúp nhận diện đặc trưng có mối quan hệ chặt chẽ, hỗ trợ lựa chọn biến khi xây dựng mô hình.

important_num_cols = [
"OverallQual", "GrLivArea", "TotalBsmtSF", "1stFlrSF", 
"FullBath", "TotRmsAbvGrd", "GarageCars", "GarageArea", "YearBuilt", "SalePrice"
]
fig, axes = plt.subplots(3, 4, figsize=(18,8))
axes = axes.flatten()

for i, col in enumerate(important_num_cols):
sns.boxplot(data=data, y=col, ax=axes[i], color='skyblue')
axes[i].set_title(col, fontsize=12, fontweight='bold')
axes[i].tick_params(labelsize=9)

for i in range(len(important_num_cols), len(axes)):
axes[i].set_visible(False)

plt.suptitle("Boxplot of Top Important Numerical Features", fontsize=16, fontweight="bold", y=1.03)
plt.tight_layout()
plt.show()

image.png

Trực quan hóa phân phối các đặc trưng số quan trọng qua biểu đồ hộp (boxplot). Mỗi boxplot thể hiện phân phối một đặc trưng, cho phép quan sát trung vị, các phần tư và phát hiện giá trị ngoại lệ. Màu skyblue thống nhất giúp dễ so sánh.

Biểu đồ này hữu ích để đánh giá sự phân tán và điểm bất thường của các đặc trưng có ảnh hưởng lớn, hỗ trợ phân tích và tiền xử lý trước khi xây dựng mô hình.

# Before
fig, ax = plt.subplots()
ax.scatter(x = data['GrLivArea'], y = data['SalePrice'])
plt.ylabel('SalePrice', fontsize=13)
plt.xlabel('GrLivArea', fontsize=13)
plt.show()

image.png

Xử lý ngoại lệ (outliers) dựa trên mối quan hệ giữa GrLivArea (diện tích sinh hoạt) và SalePrice (giá bán). Đầu tiên, biểu đồ phân tán (scatter plot) được vẽ để trực quan hóa mối tương quan giữa hai biến, giúp nhận diện điểm dữ liệu bất thường.

Chúng ta có thể thấy ở phía dưới bên phải có hai GrLivArea cực lớn nhưng có mức giá thấp. Những giá trị này là Outliers. Vì vậy, chúng ta có thể xóa chúng một cách an toàn.

# After
data = data.drop(data[(data['GrLivArea']>4000) & (data['SalePrice']<300000)].index)

fig, ax = plt.subplots()
ax.scatter(data['GrLivArea'], data['SalePrice'])
plt.ylabel('SalePrice', fontsize=13)
plt.xlabel('GrLivArea', fontsize=13)
plt.show()

image.png

Quy trình này cải thiện chất lượng dữ liệu, giảm ảnh hưởng của giá trị bất thường và nâng cao độ tin cậy cho phân tích cũng như mô hình dự báo.

missing = (data.isnull().sum() / len(data)) * 100
missing = missing.drop(missing[missing == 0].index).sort_values(ascending=False)[:30]
missing_data = pd.DataFrame({"Missing Ratio": missing})
missing_data.head(10)

# Trực quan hoá
plt.figure(figsize=(10, 8))
missing.plot.barh(color='skyblue', edgecolor='black') 
plt.title('Missing Data by Feature', fontsize=14)
plt.xlabel('Number of Missing Values')
plt.ylabel('Feature Name')
plt.grid(axis='x', linestyle='--', alpha=0.7) 
plt.gca().invert_yaxis() 
plt.show()

image.png

Bước này tính missing values của từng cột (theo %), loại các cột không thiếu, sắp xếp theo mức thiếu nhiều đến ít và lấy nhóm đứng đầu để xem nhanh.

Kết quả được trực quan hóa bằng biểu đồ thanh ngang, trong đó trục x là số giá trị thiếu và trục y là tên cột; trục y được đảo để các cột thiếu nhiều nằm trên cùng giúp dễ so sánh. Mục đích là nhanh chóng nhận diện và ưu tiên xử lý các biến thiếu dữ liệu (ví dụ: chọn chiến lược impute hay loại bỏ) trước khi tiếp tục tiền xử lý và mô hình.

Bước 3: Tiền xử lý dữ liệu

Loại bỏ đặc trưng có missing values vượt quá 50%

data = data.drop(["Id", "Alley", "PoolQC", "Fence", "MiscFeature", "MasVnrType"], axis=1)

Trong bước này, các đặc trưng có giá trị bị thiếu vượt quá 50% sẽ được loại bỏ khỏi tập dữ liệu. Việc này giúp giảm nhiễu và tránh ảnh hưởng tiêu cực đến quá trình huấn luyện mô hình, giữ lại những đặc trưng có thông tin đủ mạnh và tin cậy hơn

Chia tập dữ liệu train và test

train_df, test_df = train_test_split(
data,
test_size=0.25,
random_state=42

y_train = train_df["SalePrice"].values
y_test = test_df["SalePrice"].values
train_df = train_df.drop(["SalePrice"], axis=1)
test_df = test_df.drop(["SalePrice"], axis=1)

num_cols = [col for col in train_df.columns if train_df[col].dtype in ["float64", "int64"]]
cat_cols = [col for col in train_df.columns if train_df[col].dtype not in ["float64", "int64"]]

train_df[cat_cols] = train_df[cat_cols].fillna("none")
test_df[cat_cols] = test_df[cat_cols].fillna("none")

Dữ liệu được chia ra thành hai tập: tập huấn luyện 75% và tập test 25%, việc này nhằm đánh giá khả năng tổng quát hoá mô hình trên dữ liệu chưa từng thấy. Biến mục tiêu SalePrice được tách khỏi tập dữ liệu đầu vào để phục vụ quá trình huấn luyện.

Các đặc trưng được phân loại thành hai nhóm: dạng số (numerical) và dạng phân loại (categorical). Đối với các cột phân loại, giá trị khuyết được thay bằng nhãn “none” để đảm bảo tính nhất quán của dữ liệu trước khi mã hoá

Tiền xử lý dữ liệu

oder = OneHotEncoder(handle_unknown="ignore", sparse_output=False)
encoder.fit(train_df[cat_cols])

encoded_cols = list(encoder.get_feature_names_out(cat_cols))

train_df[encoded_cols] = encoder.transform(train_df[cat_cols])
test_df[encoded_cols] = encoder.transform(test_df[cat_cols])

imputer = SimpleImputer()
train_df[num_cols] = imputer.fit_transform(train_df[num_cols])
test_df[num_cols] = imputer.fit_transform(test_df[num_cols])

Trong bước này, đặc trưng phân loại được mã hoá bằng phương pháp One-Hot Encoding, chuyển đổi từng giá trị rời rạc thành biến nhị phân. Tuỳ chọn handle_unknown="ignore" được dùng nhằm bỏ qua nhãn chưa từng xuất hiện trong tập huấn luyện, tránh gây lỗi trong quá trình biến đổi dữ liệu kiểm tra.

Với đặc trưng dạng số, các giá trị bị khuyết được xử lý bằng kỹ thuật Simple Imputer bằng cơ chế thay giá trị thiếu bằng trung bình của cột tương ứng. Việc này giúp giảm thiểu mất mát thông tin và đảm bảo toàn bộ dữ liệu đầu vào đều sẵn sàng cho mô hình huấn luyện.

Chuẩn hoá dữ liệu

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

X_train = np.hstack([train_num_features, train_df[encoded_cols].values])
X_test = np.hstack([test_num_features, test_df[encoded_cols].values])

Trong bước này, ta sử dụng MinMaxScaler để chuẩn hóa các đặc trưng số. Cụ thể:

  • scaler.fit_transform(train_df[num_cols]) áp dụng chuẩn hóa trên các đặc trưng số trong tập huấn luyện (train_df) sao cho chúng nằm trong khoảng [0, 1].
  • scaler.fit_transform(test_df[num_cols]) làm tương tự cho tập kiểm tra (test_df).

Sau đó, ta kết hợp các đặc trưng đã chuẩn hóa (train_num_featurestest_num_features) với các đặc trưng phân loại đã mã hóa từ cả hai tập dữ liệu, sử dụng np.hstack() để tạo ra các ma trận đặc trưng hoàn chỉnh X_trainX_test cho mô hình.

Bước 4: Huấn luyện model không sử dụng đặc trưng đa thức

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

# Khởi tạo list lưu kết quả
train_rmse_results = []
test_rmse_results = []
train_r2_results = []
test_r2_results = []
model_names = []

for name, model in models.items():
regressor = model.fit(X_train, y_train)

# Dự đoán
y_train_pred = regressor.predict(X_train)
y_test_pred = regressor.predict(X_test)

# Tính RMSE
train_rmse = np.sqrt(mean_squared_error(y_train, y_train_pred))
test_rmse = np.sqrt(mean_squared_error(y_test, y_test_pred))

# Tính R²
train_r2 = r2_score(y_train, y_train_pred)
test_r2 = r2_score(y_test, y_test_pred)

# Lưu kết quả
model_names.append(name)
train_rmse_results.append(train_rmse)
test_rmse_results.append(test_rmse)
train_r2_results.append(train_r2)
test_r2_results.append(test_r2)

# Tạo DataFrame tổng hợp
df_results = pd.DataFrame({
"Model": model_names,
"Train_RMSE": train_rmse_results,
"Test_RMSE": test_rmse_results,
"Train_R2": train_r2_results,
"Test_R2": test_r2_results
}).sort_values(by="Test_R2", ascending=False)

df_results

Trong bước này, ta xây dựng và đánh giá 3 mô hình hồi quy: Linear Regression, Ridge, và Lasso. Quá trình diễn ra như sau:

  1. Khởi tạo mô hình: Ta tạo một dictionary models chứa ba mô hình hồi quy.
  2. Huấn luyện và dự đoán:
    - Dùng model.fit(X_train, y_train) để huấn luyện mô hình trên dữ liệu huấn luyện.
    - Dự đoán giá trị cho tập huấn luyện (y_train_pred) và tập kiểm tra (y_test_pred) với regressor.predict(...).
  3. Đánh giá mô hình:
    - Tính toán RMSE (Root Mean Squared Error) cho cả tập huấn luyện và kiểm tra bằng mean_squared_errornp.sqrt().
    - Tính toán (đánh giá độ phù hợp của mô hình) cho cả hai tập bằng r2_score.
  4. Lưu kết quả: Lưu các chỉ số (RMSE và R²) vào các danh sách train_rmse_results, test_rmse_results, train_r2_results, test_r2_results.
  5. Tạo bảng kết quả: Cuối cùng, kết quả được tổng hợp vào DataFrame df_results và sắp xếp theo R² của tập kiểm tra (Test_R2) để so sánh các mô hình.

Kết quả cuối cùng là bảng tổng hợp hiệu suất của từng mô hình, giúp chọn lựa mô hình tốt nhất dựa trên chỉ số R² và RMSE.

image.png

Bước 5: Huấn luyện model sử dụng đặc trưng đa thức

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])
scaler = MinMaxScaler()
train_poly_features = scaler.fit_transform(train_poly_features)
test_poly_features = scaler.transform(test_poly_features)

X_train_poly = np.hstack([train_poly_features, train_df[encoded_cols].values])
X_test_poly = np.hstack([test_poly_features, test_df[encoded_cols].values])

# making dictionary of models
models = {
"LinearRegression": LinearRegression(),
"Ridge": Ridge(),
"Lasso": Lasso()
}

# lists to store results
r2_results = []
rmse_results = []
train_rmse_results = []
train_r2_results = []
model_names = []

# training and evaluating each models
for name, model in models.items():
regressor = model.fit(X_train_poly, y_train)

# predict
y_train_pred = regressor.predict(X_train_poly)
y_test_pred = regressor.predict(X_test_poly)

# metrics
train_rmse = np.sqrt(mean_squared_error(y_train, y_train_pred))
test_rmse = np.sqrt(mean_squared_error(y_test, y_test_pred))
train_r2 = r2_score(y_train, y_train_pred)
test_r2 = r2_score(y_test, y_test_pred)

# store
model_names.append(name)
train_rmse_results.append(train_rmse)
rmse_results.append(test_rmse)
train_r2_results.append(train_r2)
r2_results.append(test_r2)

# create dataframe
df_results = pd.DataFrame({
"Model": model_names,
"Train_RMSE": train_rmse_results,
"Test_RMSE": rmse_results,
"Train_R2": train_r2_results,
"Test_R2": r2_results
}).sort_values(by="Test_R2", ascending=False)

df_results

Bước này mở rộng quy trình mô hình hóa bằng cách sử dụng đặc trưng đa thức (Polynomial Features) và chuẩn hóa lại dữ liệu trước khi huấn luyện và đánh giá mô hình. Quá trình diễn ra như sau:

  1. Tạo đặc trưng đa thức:
    - Dùng PolynomialFeatures(degree=2, interaction_only=True, include_bias=False) để tạo ra các đặc trưng đa thức bậc 2 cho các cặp tương tác giữa các biến số trong num_cols (mà không thêm bias).
    - Sử dụng fit_transform() trên train_dftransform() trên test_df để tạo các đặc trưng đa thức cho cả hai tập.
  2. Chuẩn hóa dữ liệu:
    - Áp dụng MinMaxScaler để chuẩn hóa các đặc trưng đa thức trong train_poly_featurestest_poly_features.
  3. Kết hợp dữ liệu:
    - Dùng np.hstack() để kết hợp các đặc trưng đa thức đã chuẩn hóa với các đặc trưng phân loại (encoded_cols) từ train_dftest_df, tạo ra các ma trận đặc trưng hoàn chỉnh X_train_polyX_test_poly.
  4. Huấn luyện và đánh giá mô hình:
    - Tạo một dictionary models với các mô hình hồi quy: LinearRegression, Ridge, Lasso.
    - Dùng các mô hình này để huấn luyện và dự đoán trên dữ liệu đã xử lý, rồi tính toán RMSE cho cả tập huấn luyện và kiểm tra.
  5. Lưu và tổng hợp kết quả:
    - Các kết quả RMSE và R² của từng mô hình được lưu vào các danh sách, sau đó tạo ra một DataFrame tổng hợp df_results để so sánh hiệu suất của các mô hình dựa trên Test_R² (R² của tập kiểm tra).

Kết quả cuối cùng là một bảng thể hiện hiệu suất của các mô hình sau khi áp dụng đặc trưng đa thức, giúp đánh giá xem mô hình nào hoạt động tốt nhất.

image.png

III. Cải tiến

1. Tạo SHAP và tóm tắt

import shap

explainer = shap.Explainer(model, X_train, feature_names=feature_names)
shap_values = explainer(X_test)

shap.summary_plot(shap_values, X_test, feature_names=feature_names)

image.png

Mục đích của biểu đồ:

Biểu đồ này cho thấy mức độ ảnh hưởng của từng đặc trưng (feature) đến kết quả dự đoán của mô hình. Nói cách khác: nó giúp ta hiểu mô hình đang “ra quyết định” dựa vào những yếu tố nào.

Đọc biểu đồ:

Trục Y — Các đặc trưng (features)

  • Mỗi hàng là một feature trong dữ liệu (ví dụ: 2ndFlrSF, YearBuilt, OverallQual, …).
  • Các feature được xếp theo độ quan trọng giảm dần — feature ở trên cùng có ảnh hưởng lớn nhất đến mô hình.

Trục X — Giá trị SHAP

  • Là mức độ tác động của feature đó đến đầu ra của mô hình. SHAP value càng lớn (dương hoặc âm) → tác động càng mạnh.
  • Giá trị dương (bên phải 0): làm tăng giá trị dự đoán.
  • Giá trị âm (bên trái 0): làm giảm giá trị dự đoán.
  • Trong bài toán dự đoán giá nhà (House Prices), ví dụ:
  • SHAP > 0 → yếu tố này làm tăng giá nhà.
  • SHAP < 0 → yếu tố này làm giảm giá nhà.

Màu sắc - Giá trị của feature

  • Đỏ (High): giá trị của feature cao.
  • Xanh (Low): giá trị của feature thấp.

Điều này giúp hiểu chiều hướng tác động. Ví dụ OverallQual (chất lượng tổng thể):

  • Màu đỏ ở bên phải → chất lượng cao → tăng giá nhà.
  • Màu xanh ở bên trái → chất lượng thấp → giảm giá nhà.

Ngược lại, nếu màu đỏ lại nằm bên trái → giá trị cao làm giảm kết quả dự đoán (tác động tiêu cực).

  • Nhìn biểu đồ :
  • 2ndFlrSF, BsmtFinSF1, YearBuilt, OverallQual, GrLivArea là những feature có tác động lớn nhất đến giá nhà.
  • Với OverallQual, các điểm đỏ (giá trị cao) nằm bên phải → chất lượng cao làm tăng giá.
  • Với YearBuilt, các điểm đỏ cũng nghiêng về bên phải → nhà xây gần đây (mới hơn) có xu hướng giá cao hơn.

2. SHAP Waterfall Plot

sample_idx = 0
print(X_test[sample_idx])
model.predict(X_test[sample_idx].reshape(1,-1))
shap.waterfall_plot(shap_values[sample_idx])

image.png

Giải thích biểu đồ SHAP Waterfall Plot

Biểu đồ trên minh họa quá trình mô hình đưa ra dự đoán cho một mẫu dữ liệu cụ thể (ví dụ: một ngôi nhà trong tập kiểm thử). Nó cho thấy từng đặc trưng (feature) đã đóng góp bao nhiêu vào giá trị dự đoán cuối cùng.

Cách đọc biểu đồ

$E[f(X)]$ (ở dưới cùng): giá trị dự đoán trung bình của mô hình trên toàn bộ tập dữ liệu.

$f(x)$ (ở trên cùng): giá trị dự đoán thực tế cho mẫu này.

Các thanh màu đỏ thể hiện những đặc trưng làm tăng giá trị dự đoán, còn thanh màu xanh thể hiện đặc trưng làm giảm giá trị dự đoán.

Mỗi feature có một SHAP value, biểu thị độ lớn và chiều hướng tác động đến kết quả.

Từng đặc trưng đóng góp (cộng hoặc trừ) vào kết quả

image.png

Tổng kết

  • Các yếu tố như mái Hip, không có tầng 2, và diện tích nhỏ khiến giá nhà giảm mạnh.
  • Tuy nhiên, tình trạng tổng thể tốt (OverallCond), tầng hầm hoàn thiện, và kiểu nhà 1 tầng giúp bù lại một phần.
  • Kết quả cuối cùng: mô hình dự đoán 156,321.64, thấp hơn ~12,000 so với trung bình toàn tập dữ liệu.

3. Partial Dependence

Sau khi dùng SHAP để xác định các đặc trưng quan trọng, tiếp tục sử dụng Partial Dependence Plot (PDP) để xem mối quan hệ trung bình giữa từng đặc trưng và giá trị dự đoán. PDP giúp trực quan hóa xu hướng — ví dụ, khi LotArea tăng, giá dự đoán của mô hình cũng tăng rõ rệt.

X_train_df = pd.DataFrame(X_train, columns=feature_names)

# Vẽ PDP
from sklearn.inspection import PartialDependenceDisplay
PartialDependenceDisplay.from_estimator(model, X_train_df, ['LotArea'])
plt.show()

image.png

Biểu đồ PDP thể hiện mối quan hệ trung bình giữa một đặc trưng đầu vào (LotArea) và đầu ra của mô hình (giá dự đoán), trong khi giữ các đặc trưng khác cố định.

  • Trục X (LotArea): giá trị của biến LotArea (đã được chuẩn hóa về khoảng 0–1).
  • Trục Y (Partial dependence): giá trị dự đoán trung bình của mô hình khi thay đổi LotArea, còn các biến khác giữ nguyên.

Khi LotArea tăng, giá nhà dự đoán cũng tăng tuyến tính.

Điều này nghĩa là:

Mô hình học được rằng diện tích lô đất càng lớn → giá trị ngôi nhà càng cao.

Do mô hình đã huấn luyện là LinearRegression, nên mối quan hệ giữa LotArea và giá nhà là tuyến tính tuyệt đối, dẫn đến đường đồ thị là đường thẳng.

Giải thích con số cụ thể trong biểu đồ

Ví dụ (dựa trên hình):

  • Khi LotArea ≈ 0.01 → dự đoán ≈ 177,500
  • Khi LotArea ≈ 0.07 → dự đoán ≈ 186,000

→ Tức là LotArea tăng khoảng 0.06 đơn vị (đã chuẩn hóa) thì giá tăng khoảng 8,500 đơn vị tiền.

IV. Mở rộng

Pipeline trong scikit-learn là một công cụ mạnh mẽ để đóng gói các bước tiền xử lý và mô hình học máy vào trong một quy trình liên tục. Nó giúp quy trình học máy trở nên mạch lạc, dễ quản lý và tránh các lỗi thường gặp trong quá trình huấn luyện mô hình.

1. Các thành phần chính trong phần mở rộng

  • Tiền xử lý dữ liệu (script)
  • Pipeline huấn luyện (phiên bản cơ bản & có PolynomialFeatures)
  • Lưu trữ model/metrics
  • Chuẩn bị dữ liệu cho Feast Feature Store
  • (Tuỳ chọn) Quản lý pipeline bằng DVC; tham số hoá qua params.yaml

Mã code minh hoạ

#params.yaml
data_processing:
# Các tham số cho việc xử lý dữ liệu (nếu có)

training:
test_size: 0.2
random_state: 42

model_poly:
degree: 2
# alpha: 1.0 # Có thể thêm cho Ridge/Lasso sau này

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

Mục tiêu: Làm sạch & điền khuyết, loại bỏ các cột không hữu ích/thiếu quá nhiều, chuẩn bị một bản CSV đã xử lý cho bước train.

Các bước xử lý:

  • Loại một số cột có nhiều NA hoặc ít giá trị thông tin (Alley, PoolQC, Fence, MiscFeature).
  • Với cột số: điền khuyết bằng mean.
  • Với cột phân loại: nếu NA có ý nghĩa “không có” (nhóm basement/garage/fireplace) thì điền 'None'; còn lại dùng mode.
  • Ghi ra data/processed/processed_data.csv.

Mã code minh hoạ

# Loại bỏ các cột không cần thiết hoặc có quá nhiều giá trị thiếu
df = df.drop(["Alley", "PoolQC", "Fence", "MiscFeature"], axis=1)

# Xử lý giá trị thiếu đơn giản (dựa trên notebook của bạn)
# Các cột số: điền bằng giá trị trung bình
num_cols_with_na = df.select_dtypes(include=np.number).columns[df.select_dtypes(include=np.number).isnull().any()]
for col in num_cols_with_na:
df[col].fillna(df[col].mean(), inplace=True)

# Các cột object: điền bằng 'None' hoặc giá trị mode
obj_cols_with_na = df.select_dtypes(include='object').columns[df.select_dtypes(include='object').isnull().any()]
for col in obj_cols_with_na:
# Các cột này giá trị NA có ý nghĩa là "không có"
if col in ['BsmtQual', 'BsmtCond', 'BsmtExposure', 'BsmtFinType1', 'BsmtFinType2', 'FireplaceQu', 'GarageType', 'GarageFinish', 'GarageQual', 'GarageCond']:
df[col].fillna('None', inplace=True)
else: # Các cột khác điền bằng mode
df[col].fillna(df[col].mode()[0], inplace=True)

# Tạo thư mục output nếu chưa có
os.makedirs(os.path.dirname(processed_data_path), exist_ok=True)

# Lưu dữ liệu đã xử lý
df.to_csv(processed_data_path, index=False)
print(f"Processed data saved to {processed_data_path}")

3. Pipeline huấn luyện – phiên bản cơ bản (train.py)

Mục tiêu: Xây dựng một scikit-learn Pipeline gồm tiền xử lý + mô hình LinearRegression, đảm bảo không rò rỉ dữ liệu & nhất quán train–test.

Các bước thực hiện:

  • Chia dữ liệu và tách biến
  • Tiền xử lý trong Pipeline

Code minh hoạ:

# Xác định các cột số và cột hạng mục
numeric_features = X_train.select_dtypes(include=np.number).columns.tolist()
categorical_features = X_train.select_dtypes(include='object').columns.tolist()

# Pipeline cho các feature số
numeric_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='mean')),
('scaler', MinMaxScaler())
])

# Pipeline cho các feature hạng mục
categorical_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='constant', fill_value='none')),
('onehot', OneHotEncoder(handle_unknown='ignore', sparse_output=False))
])

# Kết hợp các pipeline bằng ColumnTransformer
preprocessor = ColumnTransformer(
transformers=[
('num', numeric_transformer, numeric_features),
('cat', categorical_transformer, categorical_features)
],
remainder='passthrough' # Giữ lại các cột không được xử lý (nếu có)
)

# 5. Tạo pipeline hoàn chỉnh: Preprocessing -> Model
model_pipeline = Pipeline(steps=[
('preprocessor', preprocessor),
('regressor', LinearRegression())
])

# 6. Huấn luyện mô hình
model_pipeline.fit(X_train, y_train)

# 7. Đánh giá mô hình
y_pred = model_pipeline.predict(X_test)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
r2 = r2_score(y_test, y_pred)

4. Pipeline huấn luyện – có PolynomialFeatures (train_poly.py)

Mục tiêu: Mở rộng pipeline với PolynomialFeatures để nắm bắt quan hệ phi tuyến.

Tham số hoá qua params.yaml: Dễ dàng tái lập và điều chỉnh mà không sửa code.

Mã minh hoạ

5. Tạo pipeline hoàn chỉnh: Preprocessing -> Model
model_pipeline = Pipeline(steps=[
('preprocessor', preprocessor),
('poly', PolynomialFeatures(degree=model_params['degree'], include_bias=False)),
('regressor', LinearRegression())
])

# 6. Huấn luyện mô hình
model_pipeline.fit(X_train, y_train)

# 7. Đánh giá mô hình
y_pred = model_pipeline.predict(X_test)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
r2 = r2_score(y_test, y_pred)

# 8. Lưu model và metrics
os.makedirs('models', exist_ok=True)
joblib.dump(model_pipeline, 'models/model_poly.joblib')

metrics = {
'dataset_size': len(X_train),
'rmse': rmse,
'r2_score': r2
}

with open('models/metrics_poly.json', 'w') as f:
json.dump(metrics, f, indent=4)

print("Model and metrics saved to 'models/' directory.")

Pipeline đảm bảo fit các bộ biến đổi chỉ trên train, rồi áp y hệt lên test khi predict → tránh data leakage.

Ghi nhận kết quả mẫu

Ví dụ metrics (tham khảo file JSON):

{
"dataset_size": 1168,
"rmse": 32792.862469057596,
"r2_score": 0.8598010306384494
}

5. Chuẩn bị Feature Store với Feast (prepare_feast_data.py)

Bước chính

  • Đổi Idhouse_id (entity)
  • Thêm event_timestamp (dùng datetime.now() cho dữ liệu tĩnh)
  • Ghi feature_repo/data/house_features.parquet

Code minh hoạ

df = pd.read_csv('data/processed/processed_data.csv')
df.rename(columns={'Id': 'house_id'}, inplace=True)
df['event_timestamp'] = datetime.now()
df.to_parquet('feature_repo/data/house_features.parquet')

Quy trình Feast: cd feature_repo && feast apply để đăng ký feature view; về sau có thể materialize & get_historical_features cho train/inference đồng nhất.

6. Đánh giá, theo dõi và tái lập (MLOps)

  • Metrics: lưu dưới dạng JSON trong models/ giúp so sánh các run & tích hợp DVC/MLflow.
  • Params: tách cấu hình vào params.yaml ⇒ dễ tái lập, CI/CD.
  • DVC (tuỳ chọn): định nghĩa stage process_data & train_poly trong dvc.yaml để dvc repro tự động chạy lại khi code/data/params thay đổi.

7. Kết luận

Dùng Pipeline giúp quy trình ML đúng hơn, nhanh hơn và dễ triển khai: mọi bước tiền xử lý chỉ “học” trên tập train nên tránh rò rỉ dữ liệu, và được áp dụng nhất quán từ train đến test rồi ra production. Toàn bộ chuỗi biến đổi + mô hình được đóng gói và lưu lại, nên suy luận/triển khai không cần viết lại transform thủ công. Pipeline cũng giúp tái lập thí nghiệm và tối ưu tham số/CV cho cả tiền xử lý lẫn mô hình trong một khối, giảm lỗi do thao tác rời rạc, dễ bảo trì/kiểm toán, và tích hợp mượt với DVC/MLflow/CI-CD.

Tham khảo: Module_5_Project (GitHub)