# 機器學習實作範例
> [name=Claire Weng]
## 範例(一)
>參考來源
>[IBM HR Analytics💼Employee Attrition & Performance](https://www.kaggle.com/code/faressayah/ibm-hr-analytics-employee-attrition-performance/notebook)
>[Colab實作筆記本](https://colab.research.google.com/drive/1KqF0C7JC78Jz9RA_z_LZf2T0WrJvn5E4?usp=sharing)
---
## 實驗步驟
### 📌 環境設定 (Google Colab)
請先在 Google Colab 中執行以下指令來安裝必要的 Python 套件:
```python
from google.colab import drive
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 LabelEncoder, StandardScaler
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix, roc_auc_score, roc_curve
import warnings
warnings.filterwarnings('ignore')
```
### 📌 1. 下載並載入 IBM HR Analytics Employee Attrition & Performance 數據集
此專案使用 IBM HR Analytics Employee Attrition & Performance Dataset,數據集來自 Kaggle。
* 下載資料集:[IBM HR Analytics Employee Attrition & Performance](https://www.kaggle.com/code/faressayah/ibm-hr-analytics-employee-attrition-performance/notebook)
* 上傳到 Colab,然後執行以下程式碼:
:::success
==掛載 Google Drive==
<style>
.green {color: green;}
</style>
<style>
.orange {color: orange;}
</style>
<span class="green">from google.colab import drive</span>
<span class="green">drive.mount('/content/drive')</span>
:::
```python
# 讀取數據
df = pd.read_csv("/content/drive/MyDrive/HackMD/WA_Fn-UseC_-HR-Employee-Attrition.csv")
print("數據集前五筆:")
print(df.head())
# 基本數據探索
print("數據集基本資訊:")
print(df.info())
print("數據描述統計:")
print(df.describe())
# 可視化數據分佈
plt.figure(figsize=(8, 4))
sns.countplot(x='Attrition', data=df, palette='coolwarm')
plt.title('員工離職情況分佈')
plt.show()
```
### 📌 2. 資料預處理(特徵工程)
```python
print("缺失值檢查:")
print(df.isnull().sum()) # 檢查缺失值
# 移除無意義的欄位
df.drop(columns=['EmployeeNumber', 'Over18', 'StandardHours', 'EmployeeCount'], inplace=True)
# 碼号轉換 ('Attrition' 轉為數字)
df['Attrition'] = df['Attrition'].map({'Yes': 1, 'No': 0})
# 類別特徵的處理(標籤編碼 & 獨熱編碼)
categorical_cols = df.select_dtypes(include=['object']).columns.tolist()
# 檢查 Attrition 是否存在於類別欄位中
if 'Attrition' in categorical_cols:
categorical_cols.remove('Attrition') # 只有存在時才移除
# OneHotEncoder 更新修正
encoder = OneHotEncoder(sparse_output=False, drop='first') # 修正 sparse=False 為 sparse_output=False
df_encoded = pd.DataFrame(encoder.fit_transform(df[categorical_cols]))
# 設定新欄位名稱
df_encoded.columns = encoder.get_feature_names_out(categorical_cols)
# 移除原本的類別欄位,並合併新編碼的數據
df.drop(columns=categorical_cols, inplace=True)
df = pd.concat([df, df_encoded], axis=1)
# 分離特徵與目標變數
X = df.drop('Attrition', axis=1)
y = df['Attrition']
# 標準化特徵
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
```
### 📌 3. 資料集分割
```python
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y, test_size=0.2, random_state=42, stratify=y)
```
### 📌 4. 訓練模型
```python
rf_model = RandomForestClassifier(n_estimators=200, max_depth=10, random_state=42)
rf_model.fit(X_train, y_train)
gb_model = GradientBoostingClassifier(n_estimators=200, learning_rate=0.05, max_depth=4, random_state=42)
gb_model.fit(X_train, y_train)
```
### 📌 5. 模型評估
```python
y_pred_rf = rf_model.predict(X_test)
y_pred_gb = gb_model.predict(X_test)
# 準確度
accuracy_rf = accuracy_score(y_test, y_pred_rf)
accuracy_gb = accuracy_score(y_test, y_pred_gb)
print(f'隨機梯梯模型準確度: {accuracy_rf:.2f}')
print(f'梯度提升模型準確度: {accuracy_gb:.2f}')
# 分類報告
print('隨機梯梯分類報告:\n', classification_report(y_test, y_pred_rf))
print('梯度提升模型分類報告:\n', classification_report(y_test, y_pred_gb))
# 混淆矩陣
plt.figure(figsize=(12,5))
plt.subplot(1,2,1)
sns.heatmap(confusion_matrix(y_test, y_pred_rf), annot=True, fmt='d', cmap='Blues')
plt.title("隨機森林混淆矩陣")
plt.subplot(1,2,2)
sns.heatmap(confusion_matrix(y_test, y_pred_gb), annot=True, fmt='d', cmap='Oranges')
plt.title("梯度提升機混淆矩陣")
plt.show()
# ROC-AUC 分析
y_prob_rf = rf_model.predict_proba(X_test)[:,1]
y_prob_gb = gb_model.predict_proba(X_test)[:,1]
fpr_rf, tpr_rf, _ = roc_curve(y_test, y_prob_rf)
fpr_gb, tpr_gb, _ = roc_curve(y_test, y_prob_gb)
plt.figure(figsize=(8,6))
plt.plot(fpr_rf, tpr_rf, label=f'Random Forest (AUC={roc_auc_score(y_test, y_prob_rf):.2f})')
plt.plot(fpr_gb, tpr_gb, label=f'Gradient Boosting (AUC={roc_auc_score(y_test, y_prob_gb):.2f})')
plt.plot([0,1], [0,1], 'k--')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC Curve')
plt.legend()
plt.show()
```
### 📌 6. 特徵重要性分析
```python
feature_importances = pd.DataFrame({'Feature': X.columns, 'Importance': rf_model.feature_importances_})
feature_importances.sort_values(by='Importance', ascending=False, inplace=True)
plt.figure(figsize=(12,6))
sns.barplot(x='Importance', y='Feature', data=feature_importances[:10])
plt.title('Top 10 重要特徵')
plt.show()
```
## Troubleshooting(問題與解決方案)
在執行資料預處理時,遇到以下問題,並附上對應的解決方案。
---
### ❌ 問題 1:Attrition 這個欄位不是 object 類型
### 📌 問題描述
Attrition 這個欄位並不是 object 類型,已經被數值化(0/1),所以 categorical_cols.remove('Attrition') 會導致錯誤,因為 Attrition 不存在於 categorical_cols。
### ✅ 解決方案
1. **確認 categorical_cols內容**
先 print(categorical_cols) 來檢查確保 Attrition 是否真的存在其中。
1. **用 if 'Attrition' in categorical_cols: 檢查後再移除**
避免 remove 遇到不存在的元素時拋錯。
```python
# 類別特徵的處理(標籤編碼 & 獨熱編碼)
categorical_cols = df.select_dtypes(include=['object']).columns.tolist()
# 檢查 Attrition 是否存在於類別欄位中
if 'Attrition' in categorical_cols:
categorical_cols.remove('Attrition') # 只有存在時才移除
# 進行獨熱編碼
encoder = OneHotEncoder(sparse=False, drop='first')
df_encoded = pd.DataFrame(encoder.fit_transform(df[categorical_cols]))
# 設定新欄位名稱
df_encoded.columns = encoder.get_feature_names_out(categorical_cols)
# 移除原本的類別欄位,並合併新編碼的數據
df.drop(columns=categorical_cols, inplace=True)
df = pd.concat([df, df_encoded], axis=1)
```
### 重新執行仍出現錯誤訊息
### ❌ 問題 2:sparse 參數已經被新版 scikit-learn 移除
### 📌 問題描述
OneHotEncoder(sparse=False, drop='first'),其中 sparse 參數已經被新版 scikit-learn 移除,應該改用 sparse_output。
### ✅ 解決方案
```python
# OneHotEncoder 更新修正
encoder = OneHotEncoder(sparse_output=False, drop='first') # 修正 sparse=False 為 sparse_output=False
df_encoded = pd.DataFrame(encoder.fit_transform(df[categorical_cols]))
# 設定新欄位名稱
df_encoded.columns = encoder.get_feature_names_out(categorical_cols)
# 移除原本的類別欄位,並合併新編碼的數據
df.drop(columns=categorical_cols, inplace=True)
df = pd.concat([df, df_encoded], axis=1)
```
### 📌重新整理資料預處理程式碼
```python
print("缺失值檢查:")
print(df.isnull().sum()) # 檢查缺失值
# 移除無意義的欄位
df.drop(columns=['EmployeeNumber', 'Over18', 'StandardHours', 'EmployeeCount'], inplace=True)
# 碼号轉換 ('Attrition' 轉為數字)
df['Attrition'] = df['Attrition'].map({'Yes': 1, 'No': 0})
# 類別特徵的處理(標籤編碼 & 獨熱編碼)
categorical_cols = df.select_dtypes(include=['object']).columns.tolist()
# 檢查 Attrition 是否存在於類別欄位中
if 'Attrition' in categorical_cols:
categorical_cols.remove('Attrition') # 只有存在時才移除
# OneHotEncoder 更新修正
encoder = OneHotEncoder(sparse_output=False, drop='first') # 修正 sparse=False 為 sparse_output=False
df_encoded = pd.DataFrame(encoder.fit_transform(df[categorical_cols]))
# 設定新欄位名稱
df_encoded.columns = encoder.get_feature_names_out(categorical_cols)
# 移除原本的類別欄位,並合併新編碼的數據
df.drop(columns=categorical_cols, inplace=True)
df = pd.concat([df, df_encoded], axis=1)
```
---
## 範例(二)
>參考來源
>[Airbnb Analysis, Visualization and Prediction](https://www.kaggle.com/code/chirag9073/airbnb-analysis-visualization-and-prediction/notebook)
>[Colab實作筆記本](https://colab.research.google.com/drive/1PkiQUOiHv5CF_rDO-7dIJFgzWcNSHQRp?usp=sharing)
---
## 實驗步驟
### 📌 環境設定 (Google Colab)
請先在 Google Colab 中執行以下指令來安裝必要的 Python 套件:
```python
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.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error, r2_score
```
### 📌 1. 下載並載入 Airbnb Analysis, Visualization and Prediction 數據集
此專案使用 Airbnb Analysis, Visualization and Prediction Dataset,數據集來自 Kaggle。
* 下載資料集:[Airbnb Analysis, Visualization and Prediction](https://www.kaggle.com/code/chirag9073/airbnb-analysis-visualization-and-prediction/notebook)
* 上傳到 Colab,然後執行以下程式碼:
:::success
==掛載 Google Drive==
<style>
.green {color: green;}
</style>
<style>
.orange {color: orange;}
</style>
<span class="green">from google.colab import drive</span>
<span class="green">drive.mount('/content/drive')</span>
:::
```python
# 讀取數據
df = pd.read_csv('/content/drive/MyDrive/HackMD/AB_NYC_2019.csv')
# 顯示資料框的基本資訊
print(df.info())
# 資料探索性分析 (EDA)
print("資料集基本資訊:")
print(df.info())
print("\n前5筆資料:")
print(df.head())
# 檢查缺失值
print("\n缺失值統計:")
print(df.isnull().sum())
# 繪製價格分佈圖
plt.figure(figsize=(10, 6))
sns.histplot(df['price'], bins=50, kde=True)
plt.title('價格分佈')
plt.xlabel('Price')
plt.ylabel('Frequency')
plt.xlim(0, 500) # 過濾極端值以便更清晰地觀察
plt.show()
```
### 📌 2. 資料預處理(特徵工程)
```python
# 移除價格為0或異常高的數據
# 這是數據清理的一部分,確保價格範圍合理
df = df[(df['price'] > 0) & (df['price'] < 500)]
# 填補缺失值(以中位數填補數值型欄位)
# 這是處理缺失值的方法,確保 `reviews_per_month` 不會有 NaN
df['reviews_per_month'] = df['reviews_per_month'].fillna(df['reviews_per_month'].median())
# 將類別型變數轉為數值型(One-Hot Encoding)
# `neighbourhood_group` 和 `room_type` 是類別變數,轉換為數值以便模型使用
df = pd.get_dummies(df, columns=['neighbourhood_group', 'room_type'], drop_first=True)
# 選擇特徵與目標變數
# 這裡選擇了一些數值特徵,以及前面 One-Hot Encoding 產生的變數
features = ['latitude', 'longitude', 'minimum_nights', 'number_of_reviews',
'reviews_per_month', 'calculated_host_listings_count', 'availability_365'] + \
[col for col in df.columns if 'neighbourhood_group_' in col or 'room_type_' in col]
X = df[features] # 特徵矩陣
y = df['price'] # 目標變數
```
### 📌 3.訓練測試集切分
```python
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
```
### 📌 4.模型訓練與評估模型表現
```python
model = RandomForestRegressor(n_estimators=100, random_state=42)
model.fit(X_train, y_train)
# 預測與評估模型表現
y_pred = model.predict(X_test)
mse = mean_squared_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
print(f"均方誤差 (MSE): {mse}")
print(f"R-squared (R²): {r2}")
```
### 📌 5.特徵重要性分析
```python
feature_importances = pd.DataFrame({
'Feature': features,
'Importance': model.feature_importances_
}).sort_values(by='Importance', ascending=False)
plt.figure(figsize=(10, 8))
sns.barplot(x='Importance', y='Feature', data=feature_importances.head(10))
plt.title('Top 10 Feature Importances')
plt.show()
```
## Troubleshooting(問題與解決方案)
---
### ❌ 問題 1:模型訓練評估後才發現仍有未處理完的缺失值
### 📌 問題描述
**1. 缺失值對模型影響**
* 對訓練影響:如果訓練時數據仍有缺失值,某些模型(如 RandomForestRegressor)可能能夠處理缺失值,但仍可能影響模型學習到的模式,導致預測結果不穩定。
* 對測試影響:如果測試數據有缺失值,許多模型無法直接處理 NaN,這可能導致模型在預測時拋出錯誤,或是自動忽略某些數據,影響結果的可信度。
**2. 影響評估指標**
* 均方誤差(MSE)可能被低估或高估:未處理的缺失值可能會讓部分資料行為異常,使得 MSE 變大,或是在測試時部分數據被排除,導致 MSE 過低。
* R² 可能誤導結果:如果數據的變異性受缺失值影響,模型的解釋能力(R²)可能會出現偏差。
### ✅ 解決方案
1. **確認影響程度?**
* 檢查 df.isnull().sum():確保 X_train 和 X_test 內 完全沒有缺失值。
* 檢查 y_train.isnull().sum():確保目標變數 完全沒有 NaN。
2. **重新評估模型:若發現缺失值,應先填補或刪除後再重新訓練模型。**
**如果發現測試或訓練數據仍有缺失值,可以:**
* 數值型特徵:用 中位數 或 均值填補(df.fillna(df.median()))。
* 類別型特徵:用 "Unknown" 或 眾數填補(df.fillna("Unknown"))。
刪除缺失值過多的樣本(df.dropna())。
```python
# 缺失值統計
# 計算每個欄位的缺失值數量,方便確認哪些欄位需要處理
missing_values = data.isnull().sum()
print("缺失值統計:")
print(missing_values)
# 缺失值處理
# 填補 'name' 和 'host_name' 的缺失值
# 這些欄位為字串型,缺失時以 'Unknown' 來填補,避免 NaN 影響分析
data['name'] = data['name'].fillna('Unknown')
data['host_name'] = data['host_name'].fillna('Unknown')
# 填補 'last_review' 的缺失值
# 這是日期型欄位,缺失時用 'No Review' 作為占位符,表示沒有評論
# (也可考慮轉換為特定日期或直接刪除該行)
data['last_review'] = data['last_review'].fillna('No Review')
# 填補 'reviews_per_month' 的缺失值
# 這是數值型欄位,缺失時用 0 填補,表示該房源沒有評論過
data['reviews_per_month'] = data['reviews_per_month'].fillna(0)
# 確認處理後的缺失值統計
# 再次計算缺失值,確保所有需要填補的欄位都已處理
missing_values_after = data.isnull().sum()
print("\n處理後的缺失值統計:")
print(missing_values_after)
```
### ❌ 問題 2:初始模型訓練成績有待加強
### 📌 問題描述
在專案一開始,使用了 Random Forest Regressor 來進行房價預測,並採取了基本的數據清理與特徵工程。然而,在初步訓練後,發現模型仍有許多可以改進的地方。
### ✅ 解決方案
**1. 模型選擇**
一開始使用 **RandomForestRegressor**,而後改為 **GradientBoostingRegressor**,並且加入 **GridSearchCV** 進行超參數調優。
**2. 特徵工程改進**
原本使用**latitude** 和 **longitude**,然而新增了一個新特徵 **distance_to_manhattan**,**計算距離曼哈頓中心的距離**,提升模型的空間特徵表現。
```python
# 新增距離特徵
data['distance_to_manhattan'] = calculate_distance(data['latitude'], data['longitude'])
# 選擇作為特徵的變數,以及目標變數(價格)
features = ['latitude', 'longitude', 'minimum_nights', 'number_of_reviews',
'reviews_per_month', 'calculated_host_listings_count', 'availability_365',
'distance_to_manhattan'] + \
[col for col in data.columns if 'neighbourhood_group_' in col or 'room_type_' in col]
```
**3. 數據預處理改進**
* **標準化數據 (StandardScaler):**
* 對數據進行標準化處理 (StandardScaler()),讓數據更適合 **Gradient Boosting** 演算法,而原先版本沒有這個步驟。
```python
# 標準化特徵,讓不同數值範圍的變數能夠在相同尺度上進行學習
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
```
**4. 超參數調優**
* 原先**RandomForestRegressor**(n_estimators=100, random_state=42) 直接設定固定參數。
* 使用 **GridSearchCV** 對 **GradientBoostingRegressor** 進行**超參數搜索**,測試 n_estimators、learning_rate、max_depth 和 subsample 的最佳組合,提高模型表現。
**5. 模型評估**
* **透過 GridSearchCV 尋找最佳參數**,可能提升了模型的準確性。
```python
設定 Gradient Boosting Regressor 的超參數搜尋範圍
param_grid = {
'n_estimators': [100, 200], # 樹的數量
'learning_rate': [0.05, 0.1], # 學習率,控制每棵樹對最終預測的影響
'max_depth': [3, 5], # 樹的最大深度,控制模型複雜度
'subsample': [0.8, 1.0] # 每次訓練時使用的數據比例
}
# 初始化 Gradient Boosting 模型
model = GradientBoostingRegressor(random_state=42)
# 使用 GridSearchCV 進行超參數搜尋,採用 5 折交叉驗證,評估指標為 R²(判定係數)
grid_search = GridSearchCV(estimator=model, param_grid=param_grid, cv=5, scoring='r2')
grid_search.fit(X_train, y_train)
# 取得最佳模型(最優參數組合)
best_model = grid_search.best_estimator_
```
### ❌ 問題 3:改用GradientBoostingRegressor模型訓練後,成績仍然有待提升
### 📌 問題描述
再改用 GradientBoostingRegressor 來進行房價預測後,發現還是有可以優化模型的地方
### ✅ 解決方案
### 主要有以下幾項優化和改進:
### 1. 資料清理與預處理
* **第二版本:** 主要處理了價格異常值(大於0且小於500)以及填補了 reviews_per_month 欄位的缺失值,並進行了 One-Hot Encoding。
* **最終版本:** 除了第二版本的清理,還新增了更多的資料處理步驟,包括:
* 刪除缺失的 name 和 host_name 行,這有助於減少無意義的資料影響。
* **進一步限制 minimum_nights 的範圍,即只保留少於30夜的資料,這有助於清除不合理的預定紀錄。**
```python
# 處理缺失值
df['reviews_per_month'] = df['reviews_per_month'].fillna(0)
df.dropna(subset=['name', 'host_name'], inplace=True)
# 移除異常值
df = df[(df['price'] > 0) & (df['price'] < 500)]
df = df[df['minimum_nights'] < 30]
```
### 2. 特徵工程
* **第二版本:** 計算了與曼哈頓中心的距離,並進行簡單的 One-Hot Encoding 處理了類別變數。
* **最終版本:** 在距離計算之外,還新增了:
* **lat_lon** 特徵,這是將 latitude 和 longitude 相乘後得到的特徵,可能有助於捕捉地理位置間的互動效應。
```python
# 添加一個新的特徵,結合經度和緯度
df['lat_lon'] = df['latitude'] * df['longitude']
```
### 3. 資料轉換
* **第二版本:** 進行了標準化處理,並沒有額外對目標變數進行轉換。
* **最終版本:** 除了標準化特徵外,還使用了 **QuantileTransformer** 對目標變數 y 進行了正態分佈轉換,這有助於提升模型的穩定性和預測能力。
```python
# 使用 QuantileTransformer 轉換目標變數
quantile_transformer = QuantileTransformer(output_distribution='normal', random_state=42)
y_transformed = quantile_transformer.fit_transform(y.values.reshape(-1, 1)).flatten()
```
### 4. 模型訓練與調參
* **第二版本:** 使用了較簡單的 GridSearchCV 進行超參數調整,範圍相對較小(n_estimators、learning_rate、max_depth 和 subsample)。
* **最終版本:** 同樣使用了 GridSearchCV,但是進行了更廣泛的範圍測試,並**加入了 KFold 交叉驗證** 來提升模型的穩定性和泛化能力。此外,還進行了更多的超參數範圍設置,探索不同的學習率、樹的深度和樣本比例。
```python
# 使用 GradientBoostingRegressor 模型
param_grid = {
'n_estimators': [300, 400],
'learning_rate': [0.03, 0.05],
'max_depth': [4, 5],
'subsample': [0.7, 0.8],
'random_state': [42]
}
gbr = GradientBoostingRegressor()
# 使用 KFold 進行交叉驗證
kf = KFold(n_splits=5, shuffle=True, random_state=42)
# 使用 GridSearchCV 進行超參數調整
from sklearn.model_selection import GridSearchCV
grid_search = GridSearchCV(estimator=gbr, param_grid=param_grid, scoring='r2', cv=kf, n_jobs=-1)
grid_search.fit(X_train, y_train)
# 顯示最佳參數
print("Best parameters found: ", grid_search.best_params_)
```
### 5. 評估模型
* **第二版本:** 評估了 R-squared 和 MSE,並且進行了基礎的特徵重要性分析。
* **最終版本:** 同樣進行了 R-squared 和 MSE 的評估,**但對預測結果進行了反轉換**,將目標變數轉回原始的價格數值,使結果更具可解釋性。
```python
# 評估模型
best_model = grid_search.best_estimator_
y_pred_transformed = best_model.predict(X_test)
# 反轉換預測值
y_pred = quantile_transformer.inverse_transform(y_pred_transformed.reshape(-1, 1)).flatten()
# 計算 R-squared 和 MSE
r2 = r2_score(y_test, y_pred_transformed)
mse = mean_squared_error(y_test, y_pred_transformed)
print(f"R-squared: {r2}")
print(f"Mean Squared Error: {mse}")
```
### 6. 可視化
* **第二版本:** 沒有涉及結果的可視化。
* **最終版本:** **增加了可視化部分,使用了散點圖來顯示實際價格與預測價格的關係**,這有助於直觀地了解模型的表現。
```python
# 預測值 vs 實際值的散點圖
plt.figure(figsize=(8, 6))
sns.scatterplot(x=quantile_transformer.inverse_transform(y_test.reshape(-1, 1)).flatten(), y=y_pred)
plt.xlabel("Actual Prices")
plt.ylabel("Predicted Prices")
plt.title("Actual vs Predicted Prices")
plt.show()
```
---
## 範例(三)
>參考來源
>[Credit Card Default: a very pedagogical notebook](https://www.kaggle.com/code/lucabasa/credit-card-default-a-very-pedagogical-notebook/notebook)
>[Colab實作筆記本](https://colab.research.google.com/drive/1y3E7_UA5iF0XO0kd4i3Qk4Dg5BPg-wPs?usp=sharing)
---
## 實驗步驟
### 📌 環境設定 (Google Colab)
請先在 Google Colab 中執行以下指令來安裝必要的 Python 套件
```python
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
import seaborn as sns
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import RobustScaler
from sklearn.feature_selection import RFECV
from xgboost import XGBClassifier
from lightgbm import LGBMClassifier
from sklearn.ensemble import StackingClassifier
from sklearn.model_selection import StratifiedKFold, GridSearchCV
from sklearn.metrics import classification_report, roc_auc_score, confusion_matrix, RocCurveDisplay
import shap
import joblib
```
### 📌 1. 下載並載入 Credit Card Default: a very pedagogical notebook 數據集
此專案使用 Credit Card Default: a very pedagogical notebook Dataset,數據集來自 Kaggle。
* 下載資料集:[Credit Card Default: a very pedagogical notebook](https://www.kaggle.com/code/lucabasa/credit-card-default-a-very-pedagogical-notebook/notebook)
* 上傳到 Colab,然後執行以下程式碼:
:::success
==掛載 Google Drive==
<style>
.green {color: green;}
</style>
<style>
.orange {color: orange;}
</style>
<span class="green">from google.colab import drive</span>
<span class="green">drive.mount('/content/drive')</span>
:::
```python
# 安裝思源黑體字型(只需執行一次)
!apt-get -y install fonts-noto-cjk
# 設定中文字型
font_path = '/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc'
font_prop = fm.FontProperties(fname=font_path)
plt.rcParams['font.family'] = font_prop.get_name()
plt.rcParams['axes.unicode_minus'] = False
# 讀取資料
df = pd.read_csv('/content/drive/MyDrive/HackMD/UCI_Credit_Card.csv')
# 資料概況
print("欄位數量與名稱:")
print(len(df.columns), df.columns.tolist())
print("\n資料型態:")
print(df.dtypes)
print("\n缺失值數量:")
print(df.isnull().sum())
print("\n重複值數量:")
print(df.duplicated().sum())
print("\n基本統計量:")
print(df.describe())
# 類別目標欄位分布
plt.figure(figsize=(8,5))
sns.countplot(x='default.payment.next.month', data=df)
plt.title('目標變數分布 (是否違約)', fontproperties=font_prop)
plt.xlabel('是否違約 (1=是, 0=否)', fontproperties=font_prop)
plt.ylabel('筆數', fontproperties=font_prop)
plt.show()
# 基本統計分析
print("資料筆數與欄位數:", df.shape)
print("\n前五筆資料:")
print(df.head())
print("\n數據描述:")
print(df.describe(percentiles=[0.25, 0.5, 0.75, 0.95, 0.99]))
# 類別分布可視化(英文版)
plt.figure(figsize=(10,6))
sns.countplot(x='default.payment.next.month', data=df)
plt.title('Class Distribution')
plt.xlabel('Default', fontproperties=font_prop)
plt.ylabel('Count', fontproperties=font_prop)
plt.show()
# 特徵相關性分析
corr_matrix = df.corr()
plt.figure(figsize=(18,15))
sns.heatmap(corr_matrix, annot=False, cmap='coolwarm')
plt.title('特徵相關性矩陣', fontproperties=font_prop)
plt.show()
```
### 📌 2. 資料預處理
```python
# 替換特殊值(-1, -2)為 NaN,並補值
def handle_missing(df):
pay_cols = ['PAY_0','PAY_2','PAY_3','PAY_4','PAY_5','PAY_6']
bill_cols = [f'BILL_AMT{i}' for i in range(1,7)]
for col in pay_cols:
df[col] = df[col].replace([-2, -1], np.nan)
df[col].fillna(df[col].mode()[0], inplace=True)
for col in bill_cols:
df[col] = df[col].replace(-2, np.nan)
df[col].fillna(df[col].median(), inplace=True)
return df
df = handle_missing(df)
```
### 📌 3. 特徵工程:類別轉換與特徵縮放
```python
# 分箱
df['AGE_BIN'] = pd.cut(df['AGE'], bins=[20, 30, 40, 50, 60, 100])
# One-hot encoding
X = pd.get_dummies(df.drop(['ID', 'default.payment.next.month'], axis=1),
columns=['SEX', 'EDUCATION', 'MARRIAGE', 'AGE_BIN'],
drop_first=True)
feature_names = X.columns.tolist()
y = df['default.payment.next.month']
# 資料切分
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42)
# 數值縮放
scaler = RobustScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
```
### 📌 4. 特徵選擇與模型訓練(Stacking)
```python
# 特徵選擇
selector = RFECV(
estimator=XGBClassifier(tree_method='hist', device='cpu', eval_metric='auc'),
step=10,
cv=5,
scoring='roc_auc',
min_features_to_select=20
)
X_train_sel = selector.fit_transform(X_train, y_train)
X_test_sel = selector.transform(X_test)
# Stacking
base_models = [
('xgb', XGBClassifier(tree_method='hist', device='cpu', use_label_encoder=False)),
('lgbm', LGBMClassifier(device='cpu'))
]
meta_model = XGBClassifier(tree_method='hist', device='cpu', use_label_encoder=False)
stack_model = StackingClassifier(
estimators=base_models,
final_estimator=meta_model,
cv=StratifiedKFold(n_splits=3, shuffle=True, random_state=42),
stack_method='predict_proba'
)
# Grid Search
param_grid = {
'xgb__learning_rate': [0.05, 0.1],
'xgb__max_depth': [3, 5],
'final_estimator__n_estimators': [50, 100]
}
grid_search = GridSearchCV(stack_model, param_grid, cv=3, scoring='roc_auc', n_jobs=-1, verbose=1)
grid_search.fit(X_train_sel, y_train)
```
### 📌 5. 模型評估與視覺化報告
```python
# 選擇最佳模型
best_model = grid_search.best_estimator_
# 進行預測
y_pred = best_model.predict(X_test_sel)
y_proba = best_model.predict_proba(X_test_sel)[:,1]
# 評估指標
# AUC 計算:
print("AUC:", roc_auc_score(y_test, y_proba))
# 混淆矩陣:
print("\n混淆矩陣:\n", confusion_matrix(y_test, y_pred))
# 分類報告:
print("\n分類報告:\n", classification_report(y_test, y_pred))
# ROC曲線
RocCurveDisplay.from_estimator(best_model, X_test_sel, y_test)
plt.title("ROC Curve")
plt.show()
```
### 📌 6. SHAP 模型解釋
```python
# 解釋器與視覺化
explainer = shap.TreeExplainer(best_model.named_estimators_['xgb'])
shap_values = explainer.shap_values(X_test_sel)
shap.summary_plot(shap_values, X_test_sel, feature_names=selector.get_feature_names_out())
```
### 📌 7. 模型保存與部署函式設計
```python
# 儲存模型
joblib.dump({
'model': best_model,
'scaler': scaler,
'selector': selector
}, 'credit_model.pkl')
# 推論類別
class CreditDefaultPredictor:
def __init__(self, model_path):
artifacts = joblib.load(model_path)
self.model = artifacts['model']
self.scaler = artifacts['scaler']
self.selector = artifacts['selector']
def predict(self, X_new):
X_new_scaled = self.scaler.transform(X_new)
X_new_selected = self.selector.transform(X_new_scaled)
return self.model.predict_proba(X_new_selected)
```
## Troubleshooting(問題與解決方案)
---
### ❌ 問題 1:SHAP 模型解釋best_model 變數沒定義
### 📌 問題描述
best_model 變數沒定義 → 想用的是 grid_search.best_estimator_。
### ❌ 問題 2:selector.get_feature_names_out() 有時會出錯
### 📌 問題描述
selector.get_feature_names_out() 有時會出錯,因為 RFECV 物件不保留原始欄位名稱,需要從 X.columns 中對照原始索引。
### ✅ 解決方案
#### 正確且穩定的版本如下:
```python
# 取得最佳模型中,第一層的 XGBoost 模型
best_model = grid_search.best_estimator_
xgb_model = best_model.named_estimators_['xgb']
# 建立 SHAP 解釋器(Tree-based model 專用)
explainer = shap.Explainer(xgb_model, feature_names=X.columns[selector.get_support()])
# 計算 SHAP 值
shap_values = explainer(X_test_sel)
# summary_plot(橫向為特徵貢獻度排序)
shap.summary_plot(shap_values, features=X_test_sel, feature_names=X.columns[selector.get_support()])
```