import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['font.family'] = 'Times New Roman'
plt.rcParams['axes.unicode_minus'] = False
df = pd.read_csv("WA_Fn-UseC_-Telco-Customer-Churn.csv")
df = df.drop(["customerID"], axis=1)
df.head()
使用 Kaggle 上的 Telco Customer Churn 數據集,數據集包含了豐富的客戶特征及流失信息,在數據分析的起步階段,需要對數據進行清洗和轉換,以確保數據適用于后續(xù)的建模和分析
數據基本信息輸出
df.info()
數據集包含 7043 條記錄和 20 列特征,其中 tenure、MonthlyCharges 和 TotalCharges 是數值類型,其余的特征多為分類類型(object),目標標簽為 Churn,用來預測客戶是否流失
數據類型轉換
# 處理 TotalCharges 列的空字符串或非數值數據
df['TotalCharges'] = pd.to_numeric(df['TotalCharges'], errors='coerce')
# 將數據類型轉換為 float64
df['TotalCharges'] = df['TotalCharges'].astype('float64')
df['TotalCharges'].dtype
可以發(fā)現 TotalCharges 列在dataframe中展示為數值但是實際為object數據類型存在數據類型混亂,將 TotalCharges 列中的非數值數據和空字符串轉換為缺失值(NaN),并將其數據類型轉換為 float64 以便后續(xù)數值處理
缺失值檢驗
df.isnull().sum()
這里可以發(fā)現 TotalCharges 列存在11個缺失值數量由于占比較小,簡化工作直接刪除存在缺失的樣本行即可
df.dropna(subset=['TotalCharges'], inplace=True)
數據編碼
from sklearn.preprocessing import LabelEncoder
# 自動選擇數據類型為 'object' 的列
columns_to_encode = df.select_dtypes(include=['object']).columns
# 初始化字典來存儲每列的編碼信息
label_mappings = {}
# 對需要編碼的列進行標簽編碼
for column in columns_to_encode:
le = LabelEncoder()
df[column] = le.fit_transform(df[column])
# 將編碼的類別及其對應的值保存到字典中
label_mappings[column] = dict(zip(le.classes_, le.transform(le.classes_)))
# 輸出每個特征列的編碼信息
for column, mapping in label_mappings.items():
print(f"Feature: {column}")
for category, code in mapping.items():
print(f" {category}: {code}")
print("\n")
由于原始數據存在大量類別數據,使用 LabelEncoder 對數據中的類別特征進行編碼,將這些字符型特征轉換為機器學習模型可以理解的數值型數據
樣本采樣
from imblearn.over_sampling import SMOTE
# 將特征 (X) 和標簽 (y) 分開
X = df.drop(columns=['Churn']) # 特征數據,去掉 'Churn' 列
y = df['Churn'] # 目標標簽,即 'Churn'
# 初始化 SMOTE
smote = SMOTE(random_state=42)
# 對數據進行過采樣
X_res, y_res = smote.fit_resample(X, y)
# 輸出過采樣后的類別分布
print("原始數據類別分布:\n", y.value_counts())
print("過采樣后的類別分布:\n", y_res.value_counts())
在數據預處理的最后一步,使用SMOTE方法來平衡類別分布,防止模型因為類別不平衡而產生偏差
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout
X = X_res
y = y_res
from sklearn.model_selection import train_test_split
X_temp, X_test, y_temp, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y_res)
X_train, X_val, y_train, y_val = train_test_split(X_temp, y_temp, test_size=0.125, random_state=42, stratify=y_temp)
# 輸入形狀為 (samples, features)
input_shape = (X_train.shape[1],)
# 構建全連接神經網絡模型
model = Sequential()
# 添加全連接層
model.add(Dense(units=64, input_shape=input_shape, activation='relu'))
model.add(Dropout(0.2)) # 添加 Dropout 防止過擬合
# 添加第二個全連接層
model.add(Dense(units=32, activation='relu'))
model.add(Dropout(0.2))
# 添加輸出層,使用sigmoid作為激活函數處理二分類
model.add(Dense(units=1, activation='sigmoid'))
# 編譯模型
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
# 訓練模型
history = model.fit(X_train, y_train, validation_data=(X_val, y_val), epochs=100, batch_size=32)
# 繪制訓練和驗證的損失曲線
plt.plot(history.history['loss'], label='train loss')
plt.plot(history.history['val_loss'], label='val loss')
plt.title('Loss over epochs')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()
# 打印模型摘要
model.summary()
選擇一個簡單的全連接神經網絡架構來處理這個分類任務。模型包含兩個隱藏層,每層分別有 64 和 32 個神經元,并使用 ReLU 激活函數。為了防止過擬合,在每一層后加入了 Dropout 層,選擇 binary_crossentropy 作為損失函數,并使用 Adam 優(yōu)化器。模型的評價指標為準確率(accuracy),模型訓練使用了 100 個 Epoch,并在訓練集中使用了 80% 的數據進行訓練,剩余的 20% 作為驗證集
分類報告
# 使用模型在測試集上進行預測
y_pred_prob = model.predict(X_test)
# 將概率轉換為二分類標簽 (如果概率 >= 0.5,預測為 1,否則為 0)
y_pred = (y_pred_prob >= 0.5).astype(int)
from sklearn.metrics import classification_report
# 輸出模型報告, 查看評價指標
print(classification_report(y_test, y_pred))
混淆矩陣
from sklearn.metrics import confusion_matrix
# 計算混淆矩陣
confusion_matrix = confusion_matrix(y_test, y_pred)
# 繪制混淆矩陣
fig, ax = plt.subplots(figsize=(10, 7),dpi=1200)
cax = ax.matshow(confusion_matrix, cmap='Blues')
fig.colorbar(cax)
# 設置英文標簽
ax.set_xlabel('Predicted')
ax.set_ylabel('Actual')
ax.set_xticks(np.arange(2))
ax.set_yticks(np.arange(2))
ax.set_xticklabels(['Class 0', 'Class 1'])
ax.set_yticklabels(['Class 0', 'Class 1'])
for (i, j), val in np.ndenumerate(confusion_matrix):
ax.text(j, i, f'{val}', ha='center', va='center', color='black')
plt.title('Confusion Matrix Heatmap')
plt.savefig('Confusion Matrix Heatmap.pdf', format='pdf', bbox_inches='tight')
plt.show()
ROC曲線
from sklearn.metrics import roc_curve, auc
# 預測概率
y_score = model.predict(X_test).ravel() # 確保將輸出展平為1D
# 計算ROC曲線
fpr, tpr, _ = roc_curve(y_test, y_score)
roc_auc = auc(fpr, tpr)
# 繪制ROC曲線
plt.figure(dpi=1200)
plt.plot(fpr, tpr, color='darkorange', lw=2, label='ROC curve (area = %0.2f)' % roc_auc)
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver Operating Characteristic')
plt.legend(loc="lower right")
plt.savefig('Receiver Operating Characteristic.pdf', format='pdf', bbox_inches='tight')
plt.show()
訓練完成后,模型在測試集上的性能評估使用了混淆矩陣、ROC 曲線以及分類報告,通過這些指標,能夠了解模型的整體性能表現。但由于深度學習模型本質上是黑盒模型,僅通過這些指標很難解釋模型的決策過程。這就引出了下一步:如何使用 SHAP 來解釋模型預測
SHAP是一種基于博弈論的解釋方法,可以為每個特征分配一個重要性分數,解釋模型的預測結果。SHAP 的核心思想是通過對每個特征的貢獻進行分解,計算特征對模型輸出的邊際貢獻值,使用 SHAP,我們能夠可視化模型對每個樣本的預測依據,讓模型更加透明
import shap
# 1. 創(chuàng)建 SHAP 解釋器
# 使用訓練集的一部分作為背景數據
background = X_train.sample(n=100, random_state=42) # 根據數據量調整樣本數量
# 將背景數據和解釋數據轉換為 NumPy 數組
background_np = background.to_numpy()
X_explain_np = X_test[:100].to_numpy() # 選擇要解釋的樣本
首先,從訓練集中抽取了 100 個樣本作為背景數據(背景數據用于模型的解釋計算,它代表了模型在訓練時見過的數據范圍),然后將背景數據和要解釋的測試數據轉換為 NumPy 數組,供 SHAP 后續(xù)計算使用,背景數據是解釋模型全局行為的關鍵,解釋器會以背景數據為基礎來計算每個樣本中的特征對預測結果的貢獻
# 使用 shap.Explainer 自動選擇合適的解釋器
explainer = shap.Explainer(model, background_np)
接著,通過 shap.Explainer 創(chuàng)建了一個 SHAP 解釋器,shap.Explainer 會根據輸入的模型和背景數據,自動選擇合適的 SHAP 算法(比如針對深度學習模型,通常會選擇基于深度模型的 SHAP 解釋方法),model 是之前訓練好的深度學習模型,background_np 是背景數據
# 2. 計算 SHAP 值
# 計算shap值為numpy.array數組
shap_values_numpy = explainer.shap_values(X_explain_np)
在這一步,使用 SHAP 解釋器來計算測試集前 100 個樣本的 SHAP 值,SHAP 值本質上是每個特征對預測結果的邊際貢獻值,shap_values_numpy 是一個 NumPy 數組,包含了每個樣本每個特征的 SHAP 值,這些值表示該特征如何影響模型的預測結果
# 計算shap值為Explanation格式
shap_values_raw = explainer(X_explain_np)
這一步通過 explainer() 方法生成 shap_values_raw,它是一個包含更多信息的 SHAP 解釋結果對象,稱為 shap.Explanation,與 NumPy 數組不同,shap.Explanation 可以直接用于繪圖和可視化,它包含了 SHAP 值、基準值(base value,模型的平均預測值),以及樣本的特征數據
feature_names = X_test.columns
# 手動創(chuàng)建一個 shap.Explanation 對象,并傳遞特征名
shap_values_Explanation = shap.Explanation(values=shap_values_raw.values,
base_values=shap_values_raw.base_values,
data=X_explain_np, # 樣本數據
feature_names=feature_names) # 特征名稱
在這一部分,手動創(chuàng)建一個 shap.Explanation 對象,該對象將 SHAP 值、基準值(base_values),輸入數據(data),以及每個特征的名稱(feature_names)整合在一起,這個對象將會被用來繪制 SHAP 圖,解釋每個特征對模型預測的貢獻情況
SHAP 摘要圖
plt.figure(figsize=(10, 5), dpi=1200)
# 使用 shap_values_Explanation 繪制摘要圖,顯示每個特征的影響力
shap.summary_plot(shap_values_Explanation, X_test[:100], feature_names=feature_names, plot_type="dot", show=False)
plt.savefig("SHAP_Summary_Plot.pdf", format='pdf', bbox_inches='tight')
plt.show()
SHAP特征重要性柱狀圖
# 繪制SHAP值總結圖(Summary Plot)
plt.figure(figsize=(10, 5), dpi=1200)
shap.summary_plot(shap_values_numpy, X_test[:100], plot_type="bar", show=False)
plt.title('SHAP_numpy Sorted Feature Importance')
plt.savefig("SHAP_numpy Sorted Feature Importance.pdf", format='pdf',bbox_inches='tight')
plt.tight_layout()
plt.show()
總結來說,這個柱狀圖說明了模型認為哪些特征對預測客戶流失(Churn)最重要,并且量化了每個特征的重要性
SHAP瀑布圖
plt.figure(figsize=(10, 5), dpi=1200)
# 繪制第1個樣本的 SHAP 瀑布圖,并設置 show=False 以避免直接顯示
shap.plots.waterfall(shap_values_Explanation[1], show=False, max_display=10)
# 保存圖像為 PDF 文件
plt.savefig("SHAP_Waterfall_Plot_Sample_1.pdf", format='pdf', bbox_inches='tight')
plt.tight_layout()
plt.show()
模型輸出的初始值:
每個特征的影響:
通過這個 SHAP 瀑布圖,能夠清晰地看到哪些特征對于預測結果的重要性,以及它們具體對模型輸出是增加還是減少,這個可視化有助于解釋黑箱模型的決策邏輯,特別是在理解每個樣本單獨預測時,各個特征的貢獻
SHAP力圖
# 繪制單個樣本的SHAP解釋(Force Plot)
sample_index = 1 # 選擇一個樣本索引進行解釋
expected_value = 0.504
shap.force_plot(expected_value, shap_values_numpy[sample_index], X_test[:100].iloc[sample_index], matplotlib=True,show=False)
plt.savefig("Shap Force.pdf", format='pdf',bbox_inches='tight')