先聊个真实的场景。上次有个朋友问我:手里有一份医疗表格数据,要预测一个人是不是有糖尿病风险,模型到底用XGBoost还是随机森林?我说你先别在这两个里纠结,你把随机森林、XGBoost、LightGBM,再加一个逻辑回归,做成两层结构的Stacking模型,效果往往比单选任何一个都稳。他接着问:那领导要解释,这个黑箱模型凭什么说这个人风险高?这就轮到SHAP分析了。
所以今天这篇文章不聊虚的,直接用一份经典的糖尿病数据集,从数据清洗到Stacking模型训练,再到SHAP解释,把这条完整链路走一遍。文章的风格就是标题那句话——先来点实在的,直接上代码,关键地方给你接地气地解释为什么这么干。适合谁看?已经会跑sklearn基础模型、但对集成策略和模型解释还停留在“听说过”阶段的读者,看完就能照着复现。
1. 糖尿病数据不是拿来就用,先处理几个要命的细节
1.1 为什么选了这份数据集,而不是sklearn自带的糖尿病数据
很多教程一上来就用load_diabetes,那个其实是“糖尿病进展程度”回归数据集,预测的是一个连续数值,任务类型和大多数人想做的“是否患病”完全不同。今天我们用的是Pima印第安人糖尿病数据集,二分类任务,预测一个人是否患有糖尿病,输出0或1。
这个数据集在Kaggle和GitHub上都很容易找到,原始仓库地址是jblearning的经典数据文件,直接用pandas读取CSV就能用。特征一共8个:怀孕次数、血糖、血压、皮肤厚度、胰岛素、BMI、糖尿病遗传函数、年龄,标签是Outcome。样本量只有768条,正例占比约35%,是个典型的小数据集二分类问题。
选择这份数据还有一个重要原因:样本量小、特征少(8个),意味着我们既能跑Stacking模型,也能在合理时间内跑完SHAP分析。你要是换一份几十万行、几百个特征的数据集,下面的代码跑起来会非常吃力,尤其SHAP部分,这点后面会专门说。
1.2 那些0值不是真零,是缺失
这是第一道坎。很多人拿到数据直接丢给模型,结果发现效果奇差无比,其实问题出在数据本身。Pima数据集里,血糖(Glucose)、血压(BloodPressure)、皮肤厚度(SkinThickness)、胰岛素(Insulin)、BMI这几列都存在0值。
医学上,空腹血糖不可能是0,血压也不可能是0,BMI更不可能。这些0本质上就是缺失值,只是当时采集的时候用0占位了。如果不处理,模型会把“血糖为0”当成一种有意义的取值模式去学习,结果就是灾难。
处理方式很常规:把这些列里的0替换成NaN,再用中位数填充。为什么用中位数而不是均值?因为这几列尤其是胰岛素,分布非常偏斜,均值容易被极端值带偏,中位数更稳健。代码很简单:
python复制import pandas as pd
import numpy as np
url = 'https://raw.githubusercontent.com/jbrownlee/Datasets/master/pima-indians-diabetes.data.csv'
cols = ['Pregnancies', 'Glucose', 'BloodPressure', 'SkinThickness',
'Insulin', 'BMI', 'DiabetesPedigreeFunction', 'Age', 'Outcome']
df = pd.read_csv(url, header=None, names=cols)
zero_cols = ['Glucose', 'BloodPressure', 'SkinThickness', 'Insulin', 'BMI']
for col in zero_cols:
df[col] = df[col].replace(0, np.nan)
df[col] = df[col].fillna(df[col].median())
注意,怀孕次数(Pregnancies)里的0是合法值,没怀孕过就是0,不能动。糖尿病遗传函数和年龄也不存在0值问题,所以只处理上面五列。这步做完,再查看数据分布就会顺眼很多。
1.3 数据切分:留出一个独立的“质检员”
数据量小,切分更要讲究。我用train_test_split,test_size=0.2,同时加stratify=y做分层抽样,保证训练集和测试集里正负样本比例一致,都是35%左右的正例。random_state=42固定下来,方便复现。
python复制from sklearn.model_selection import train_test_split
X = df.drop('Outcome', axis=1)
y = df['Outcome']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
print(X_train.shape, X_test.shape)
print(y_train.mean(), y_test.mean())
这里有一个关键意识:测试集一旦切出来,在整个建模过程中就只能用来做最终评估,不能再参与任何训练决策。你在调参、做特征工程时反复看测试集的效果,本质上就是在“偷看答案”,最后的结果会虚高
