---
# System prepended metadata

title: 機器學習實作範例

---

# 機器學習實作範例
> [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()])
```
