接下來作者將嘗試復現該殘差可視化圖表,并擴展其應用,讓我們得以更加清晰地分析不同模型和數據集之間的預測效果差異,為模型優化提供了進一步的依據

代碼實現

數據導入及篩選

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
import warnings
warnings.filterwarnings("ignore")
df = pd.read_excel('實驗數據.xlsx')
df_1 = df[df['AgeBin'] == '年輕'].reset_index(drop=True)
df_2 = df[df['AgeBin'] == '中年'].reset_index(drop=True)
df_3 = df[df['AgeBin'] == '老年'].reset_index(drop=True)
df

讀取一個Excel文件中的數據,并根據特定的條件對數據進行篩選和分類,對不同的數據建立多個模型方便對該可視化的復現(后文會針對單一模型進行類似繪圖)

數據集劃分及模型建立

from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split

# 劃分特征和目標變量
X_1 = df_1.drop(['price', 'AgeBin'], axis=1)
y_1 = df_1['price']

X_2 = df_2.drop(['price', 'AgeBin'], axis=1)
y_2 = df_2['price']

X_3 = df_3.drop(['price', 'AgeBin'], axis=1)
y_3 = df_3['price']

# 對三個數據集分別進行訓練集、測試集劃分
X_train_1, X_test_1, y_train_1, y_test_1 = train_test_split(X_1, y_1, test_size=0.3, random_state=42)
X_train_2, X_test_2, y_train_2, y_test_2 = train_test_split(X_2, y_2, test_size=0.3, random_state=42)
X_train_3, X_test_3, y_train_3, y_test_3 = train_test_split(X_3, y_3, test_size=0.3, random_state=42)

# 創建隨機森林回歸器實例
rf_regressor_1 = RandomForestRegressor(
n_estimators=100,
criterion='squared_error', # 回歸模型的評估標準
max_depth=None,
min_samples_split=2,
min_samples_leaf=1,
min_weight_fraction_leaf=0.0,
random_state=42,
max_leaf_nodes=None,
min_impurity_decrease=0.0
)

rf_regressor_2 = RandomForestRegressor(
n_estimators=100,
criterion='squared_error',
max_depth=None,
min_samples_split=2,
min_samples_leaf=1,
min_weight_fraction_leaf=0.0,
random_state=42,
max_leaf_nodes=None,
min_impurity_decrease=0.0
)

rf_regressor_3 = RandomForestRegressor(
n_estimators=100,
criterion='squared_error',
max_depth=None,
min_samples_split=2,
min_samples_leaf=1,
min_weight_fraction_leaf=0.0,
random_state=42,
max_leaf_nodes=None,
min_impurity_decrease=0.0
)

# 分別訓練三個隨機森林回歸模型
rf_regressor_1.fit(X_train_1, y_train_1)
rf_regressor_2.fit(X_train_2, y_train_2)
rf_regressor_3.fit(X_train_3, y_train_3)

對三個不同年齡段的數據集分別創建和訓練了隨機森林回歸模型,用于預測目標變量 price

模型評價指標輸出

from sklearn import metrics
def evaluate_model(model, X_train, y_train, X_test, y_test, model_name):
# 預測
y_pred_train = model.predict(X_train)
y_pred_test = model.predict(X_test)
# 計算訓練集的指標
mse_train = metrics.mean_squared_error(y_train, y_pred_train)
rmse_train = np.sqrt(mse_train)
mae_train = metrics.mean_absolute_error(y_train, y_pred_train)
r2_train = metrics.r2_score(y_train, y_pred_train)
# 計算測試集的指標
mse_test = metrics.mean_squared_error(y_test, y_pred_test)
rmse_test = np.sqrt(mse_test)
mae_test = metrics.mean_absolute_error(y_test, y_pred_test)
r2_test = metrics.r2_score(y_test, y_pred_test)
# 輸出結果
print(f"{model_name} 訓練集評價指標:")
print("均方誤差 (MSE):", mse_train)
print("均方根誤差 (RMSE):", rmse_train)
print("平均絕對誤差 (MAE):", mae_train)
print("擬合優度 (R-squared):", r2_train)
print("\n")
print(f"{model_name} 測試集評價指標:")
print("均方誤差 (MSE):", mse_test)
print("均方根誤差 (RMSE):", rmse_test)
print("平均絕對誤差 (MAE):", mae_test)
print("擬合優度 (R-squared):", r2_test)
print("\n" + "-"*40 + "\n")
# 分別計算三個模型的指標
evaluate_model(rf_regressor_1, X_train_1, y_train_1, X_test_1, y_test_1, "模型 1 (年輕)")
evaluate_model(rf_regressor_2, X_train_2, y_train_2, X_test_2, y_test_2, "模型 2 (中年)")
evaluate_model(rf_regressor_3, X_train_3, y_train_3, X_test_3, y_test_3, "模型 3 (老年)")

輸出模型各數據集的評價指標

模型殘差計算

def calculate_residuals(model, X_test, y_test, model_name):
# 預測
y_pred_test = model.predict(X_test)
# 計算殘差
residuals = y_test - y_pred_test
# 記錄殘差到 DataFrame
residuals_df = pd.DataFrame({
'True Value': y_test,
'Predicted Value': y_pred_test,
'Residual': residuals
})
residuals_df['Model'] = model_name
return residuals_df
# 分別計算三個模型的殘差
residuals_1 = calculate_residuals(rf_regressor_1, X_test_1, y_test_1, "Model 1 (Young)")
residuals_2 = calculate_residuals(rf_regressor_2, X_test_2, y_test_2, "Model 2 (Middle Age)")
residuals_3 = calculate_residuals(rf_regressor_3, X_test_3, y_test_3, "Model 3 (Old)")
# 合并三個模型的殘差數據
residuals_all = pd.concat([residuals_1, residuals_2, residuals_3], ignore_index=True)
residuals_all

計算三個隨機森林回歸模型在測試集上的預測殘差,并將這些殘差合并為一個包含所有模型的 DataFrame

殘差 Mann-Whitney U 檢驗

from scipy.stats import mannwhitneyu
# 獲取模型的唯一組合
models = residuals_all['Model'].unique()
comparisons = [(models[i], models[j]) for i in range(len(models)) for j in range(i+1, len(models))]
# 存儲每個比較的結果
results = []
# 進行 Mann-Whitney U 檢驗
for group1, group2 in comparisons:
group1_data = residuals_all[residuals_all['Model'] == group1]['Residual']
group2_data = residuals_all[residuals_all['Model'] == group2]['Residual']
# 進行 Mann-Whitney U 檢驗
stat, p_value = mannwhitneyu(group1_data, group2_data, alternative='two-sided')
# 存儲結果
results.append([group1, group2, p_value])
# 將結果轉換為 DataFrame
mannwhitney_results = pd.DataFrame(results, columns=['group1', 'group2', 'p_value'])
# 輸出結果
mannwhitney_results

對三個模型的殘差進行兩兩比較,使用 Mann-Whitney U 檢驗判斷它們的殘差分布是否存在顯著差異,并將每次比較的結果(包括 p 值)匯總為一個 DataFrame,其中p 值用于比較兩個模型的殘差分布差異,具體而言,它表示兩個模型的預測誤差(殘差)是否來自相同的分布,通常情況下,將 p 值與顯著性水平(例如 0.05)進行比較:若 p 值小于 0.05,則可以認為這兩個模型的殘差分布存在顯著差異;若 p 值大于或等于 0.05,則無法拒絕兩個模型殘差分布相同的假設(即沒有顯著差異),顯著性差異存在表示兩個模型的預測誤差分布不同,模型性能表現有差異;顯著性差異不存在表示誤差分布相似,模型性能表現相近(所以作者認為也可以用在單模型上針對訓練集和測試集去做來判別是否存在顯著性差異,進一步說明模型是否過擬合,后文會給出該可視化代碼)

不同模型預測殘差的可視化(不帶p值)

import seaborn as sns
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
palette = ["C20", "#A184BC", "#ff7f0e"]
plt.figure(figsize=(12, 10), dpi=800)
# 創建 JointGrid 對象
g = sns.JointGrid(data=residuals_all, x="True Value", y="Predicted Value", height=10)
# 在主圖中繪制散點圖,使用指定的配色
g.plot_joint(sns.scatterplot, hue='Model', data=residuals_all, palette=palette, alpha=0.6)
# 分別繪制三個模型的堆疊邊緣密度圖,不顯示圖例
sns.kdeplot(data=residuals_all, x='True Value', hue='Model', ax=g.ax_marg_x, fill=True, common_norm=False, palette=palette, alpha=0.5, legend=False)
sns.kdeplot(data=residuals_all, y='Predicted Value', hue='Model', ax=g.ax_marg_y, fill=True, common_norm=False, palette=palette, alpha=0.5, legend=False)
# 添加 y=x 對角線
g.ax_joint.plot([residuals_all['True Value'].min(), residuals_all['True Value'].max()],
[residuals_all['True Value'].min(), residuals_all['True Value'].max()],
color='red', linestyle='--', label='y=x')
# 設置標簽和標題
g.set_axis_labels('True Values', 'Predicted Values', fontsize=14)
# 僅在主圖中顯示圖例
g.ax_joint.legend(title='Model', loc='upper left')
# 在右下角添加箱線圖,調整位置以避免重疊
ax_inset = inset_axes(g.ax_joint, width="40%", height="20%", loc='lower right',
bbox_to_anchor=(0.2, 0.05, 0.8, 0.8), # 調整這里的坐標來平移
bbox_transform=g.ax_joint.transAxes)
# 繪制橫向箱線圖
sns.boxplot(data=residuals_all, y='Model', x='Residual', palette=palette, ax=ax_inset)
# 設置箱線圖的標題和標簽
ax_inset.set_title('Residuals', fontsize=10)
ax_inset.set_xlabel('')
ax_inset.set_ylabel('')
# 關閉箱線圖 y 軸顯示
ax_inset.yaxis.set_visible(False)
plt.savefig("表1.pdf", format='pdf', bbox_inches='tight')
# 顯示圖像
plt.show()

不同模型預測殘差的可視化(帶p值)

# 定義整個可視化的配色方案
palette = {"Model 1 (Young)": "#1f77b4", "Model 2 (Middle Age)": "#9467bd", "Model 3 (Old)": "#ff7f0e"}
plt.figure(figsize=(12, 10), dpi=800)
# 創建 JointGrid 對象
g = sns.JointGrid(data=residuals_all, x="True Value", y="Predicted Value", height=10)
# 在主圖中繪制散點圖,使用指定的配色
g.plot_joint(sns.scatterplot, hue='Model', data=residuals_all, palette=palette, alpha=0.6)
# 分別繪制三個模型的堆疊邊緣密度圖,不顯示圖例
sns.kdeplot(data=residuals_all, x='True Value', hue='Model', ax=g.ax_marg_x, fill=True, common_norm=False, palette=palette.values(), alpha=0.5, legend=False)
sns.kdeplot(data=residuals_all, y='Predicted Value', hue='Model', ax=g.ax_marg_y, fill=True, common_norm=False, palette=palette.values(), alpha=0.5, legend=False)
# 添加 y=x 對角線
g.ax_joint.plot([residuals_all['True Value'].min(), residuals_all['True Value'].max()],
[residuals_all['True Value'].min(), residuals_all['True Value'].max()],
color='red', linestyle='--', label='y=x')
# 設置標簽和標題
g.set_axis_labels('True Values', 'Predicted Values', fontsize=14)
# 僅在主圖中顯示圖例
g.ax_joint.legend(title='Model', loc='upper left')
# 在右下角添加箱線圖,調整位置以避免重疊
ax_inset = inset_axes(g.ax_joint, width="40%", height="20%", loc='lower right',
bbox_to_anchor=(0.2, 0.05, 0.8, 0.8), # 調整這里的坐標來平移
bbox_transform=g.ax_joint.transAxes)
# 繪制橫向箱線圖
sns.boxplot(data=residuals_all, y='Model', x='Residual', palette=palette, ax=ax_inset)
# 設置箱線圖的標題和標簽
ax_inset.set_title('Residuals', fontsize=10)
ax_inset.set_xlabel('')
ax_inset.set_ylabel('')
# 關閉箱線圖 y 軸顯示
ax_inset.yaxis.set_visible(False)
# 準備文本內容并設置更緊湊的布局
y_offset = 0.2 # 調整整體垂直位置
x_offset_point1 = 0.44 # 第一個點的水平位置
x_offset_vs = 0.45 # 'vs' 文字的位置
x_offset_point2 = 0.47 # 第二個點的位置
x_offset_pvalue = 0.48 # p 值的位置
text_content = "" # 用于保存每一行的內容
for i, (group1, group2, p_value) in enumerate(results):
# 獲取每個模型對應的顏色
color1 = palette[group1]
color2 = palette[group2]
# 在文本框中添加帶顏色的點和 p 值
plt.gcf().text(x_offset_point1, y_offset, '●', fontsize=10, ha='right', va='top', color=color1)
plt.gcf().text(x_offset_vs, y_offset, 'vs', fontsize=8, ha='center', va='top')
plt.gcf().text(x_offset_point2, y_offset, '●', fontsize=10, ha='center', va='top', color=color2)
plt.gcf().text(x_offset_pvalue, y_offset, f'p = {p_value:.4f}', fontsize=8, ha='left', va='top')
y_offset -= 0.03 # 控制行間距,減小間距讓內容更緊湊
plt.savefig("表2.pdf", format='pdf', bbox_inches='tight')
plt.show()

對比 Nature 文章中的 b 圖,可以發現我們的 p 值可視化效果并不如原圖那般美觀。目前,作者暫時沒有通過代碼實現原圖中這種 p 值可視化的更好方法。如果有讀者能夠實現,作者非常歡迎指教。鑒于此,作者認為可以采用上一節中不帶 p 值的可視化結果,并通過 PPT 或 Photoshop 等軟件手動添加 p 值標注,這種方式比通過代碼實現更加便捷和靈活,在這里作者是通過在圖中手動添加文本標注,使用彩色圓點和 p 值來表示不同模型殘差之間的顯著性差異

針對單一模型訓練集、測試集可視化

def calculate_residuals(model, X, y, dataset_name):
# 預測
y_pred = model.predict(X)

# 計算殘差
residuals = y - y_pred

# 記錄殘差到 DataFrame
residuals_df = pd.DataFrame({
'True Value': y,
'Predicted Value': y_pred,
'Residual': residuals
})

# 添加列標記是訓練集或測試集
residuals_df['Dataset'] = dataset_name

return residuals_df

# 分別計算模型1在訓練集和測試集的殘差
residuals_train = calculate_residuals(rf_regressor_1, X_train_1, y_train_1, "Train")
residuals_test = calculate_residuals(rf_regressor_1, X_test_1, y_test_1, "Test")

# 合并訓練集和測試集的殘差數據
residuals_all = pd.concat([residuals_train, residuals_test], ignore_index=True)

# 定義整個可視化的配色方案
palette = ["#1f77b4", "#ff7f0e"] # 分別對應訓練集和測試集

# 設置更大的畫布
plt.figure(figsize=(12, 10), dpi=800)

# 創建 JointGrid 對象
g = sns.JointGrid(data=residuals_all, x="True Value", y="Predicted Value", height=10)

# 在主圖中繪制散點圖,區分訓練集和測試集
g.plot_joint(sns.scatterplot, hue='Dataset', data=residuals_all, palette=palette, alpha=0.6)

# 分別繪制兩個數據集的邊緣密度圖,不顯示圖例
sns.kdeplot(data=residuals_all, x='True Value', hue='Dataset', ax=g.ax_marg_x, fill=True, common_norm=False, palette=palette, alpha=0.5, legend=False)
sns.kdeplot(data=residuals_all, y='Predicted Value', hue='Dataset', ax=g.ax_marg_y, fill=True, common_norm=False, palette=palette, alpha=0.5, legend=False)

# 添加 y=x 對角線
g.ax_joint.plot([residuals_all['True Value'].min(), residuals_all['True Value'].max()],
[residuals_all['True Value'].min(), residuals_all['True Value'].max()],
color='red', linestyle='--', label='y=x')

# 設置標簽和標題
g.set_axis_labels('True Values', 'Predicted Values', fontsize=14)

# 僅在主圖中顯示圖例
g.ax_joint.legend(title='Dataset', loc='upper left')

# 在右下角添加箱線圖,分別顯示訓練集和測試集的殘差分布
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
ax_inset = inset_axes(g.ax_joint, width="40%", height="20%", loc='lower right',
bbox_to_anchor=(0.2, 0.05, 0.8, 0.8), # 調整這里的坐標來平移
bbox_transform=g.ax_joint.transAxes)

# 繪制橫向箱線圖,區分訓練集和測試集的殘差
sns.boxplot(data=residuals_all, y='Dataset', x='Residual', palette=palette, ax=ax_inset)

ax_inset.set_title('Residuals (Train vs Test)', fontsize=10)
ax_inset.set_xlabel('')
ax_inset.set_ylabel('')
ax_inset.yaxis.set_visible(False)
plt.savefig("表3.pdf", format='pdf', bbox_inches='tight')
plt.show()

這里可視化只展示了單個模型在訓練集和測試集上的預測結果及其殘差分布

# 分別獲取訓練集和測試集的殘差
train_residuals = residuals_train['Residual']
test_residuals = residuals_test['Residual']
# 使用 Mann-Whitney U 檢驗來比較訓練集和測試集的殘差
stat, p_value = mannwhitneyu(train_residuals, test_residuals, alternative='two-sided')
print(f"U-statistic: {stat}")
print(f"p-value: {p_value}")

可以發現p 值非常小(遠小于常用的顯著性水平 0.05),這意味著訓練集和測試集的殘差分布之間存在顯著差異,這表明模型在訓練集和測試集上的表現存在明顯不同,暗示模型可能存在過擬合問題,即模型在訓練集上擬合得很好,但在測試集上表現較差,實際上這和前文該模型的擬合優度給出的結果一致,訓練集上較高的??和較低的誤差表明模型在訓練集上擬合得很好,但在測試集上的較低??和較高誤差表明模型無法很好地泛化,這與 Mann-Whitney U 檢驗中訓練集和測試集殘差分布的顯著差異相一致,都認為模型存在過擬合的可能性

文章轉自微信公眾號@Python機器學習AI

上一篇:

SCI圖表復現:整合數據分布與相關系數的高級可視化策略

下一篇:

不止 SHAP 力圖:LIME 實現任意黑盒模型的單樣本解釋

我們有何不同?

API服務商零注冊

多API并行試用

數據驅動選型,提升決策效率

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

對比大模型API的內容創意新穎性、情感共鳴力、商業轉化潛力

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

對比大模型API的邏輯推理準確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費