基于随机森林、逻辑回归、SVM的中风病人预测
本文是kaggle上一个关于中风案例的数据集建模分析,文章的主要内容:
- 不同字段的分布情况
- 基于随机森林的缺失值填充
- 中风和未中风的样本不均衡如何解决?
- 不同字段的编码工作
- 交叉验证
- 网格搜索调参
导入库
1 | import numpy as np |
数据基本信息
先把数据导进来,查看数据的基本信息
下面我们查看数据基本信息
In [3]:
1 | df.shape |
Out[3]:
1 | (5110, 12) |
In [4]:
1 | df.dtypes |
Out[4]:
1 | id int64 |
In [5]:
1 | df.describe() |
Out[5]:
字段分布
gender统计
In [6]:
1 | plt.figure(1, figsize=(12,5)) |
age分布
In [7]:
1 | px.violin(y=df["age"]) |
1 | fig = px.histogram(df, |
ever_married
In [9]:
1 | plt.figure(1, figsize=(12,5)) |
本数据集中的结婚人士大约是未结婚的两倍。
work-type
查看不同工作状态的人员数量
In [10]:
1 | plt.figure(1, figsize=(12,8)) |
Residence_type
In [11]:
1 | plt.figure(1, figsize=(12,8)) |
avg_glucose_level
血糖水平的分布
In [12]:
1 | fig = px.histogram(df, |
可以看到大部分人的血糖还是在100以下,说明是正常的
bmi
bmi指标的分布情况
In [13]:
1 | fig = px.histogram(df, |
bmi指标的均值大约在28左右,呈现一定的正态分布
smoking_status
抽烟情况的统计
In [14]:
1 | plt.figure(1, figsize=(12,8)) |
可以看到抽烟或者曾经抽烟的人相对来说是少一些的
缺失值情况
缺失值统计
In [15]:
1 | df.isnull().sum() |
Out[15]:
1 | id 0 |
In [16]:
1 | 201 / len(df) # 缺失比例 |
Out[16]:
1 | 0.03933463796477495 |
缺失值可视化
In [17]:
1 | plt.title('Missing Value Status',fontweight='bold') |
1 | import missingno as mso |
缺失值处理
使用决策树回归来预测缺失值的BMI值:通过年龄、性别和现有的bmi值来进行预测填充
In [19]:
1 | dt_bmi = Pipeline(steps=[("scale",StandardScaler()), # 数据标准化 |
In [20]:
1 | X = df[["age","gender","bmi"]].copy() |
取出非缺失值的部分进行训练:
1 | # 缺失值部分 |
1 |
|
Out[22]:
1 | Pipeline(steps=[('scale', StandardScaler()), |
In [23]:
1 | # 模型预测 |
Out[23]:
1 | array([29.87948718, 30.55609756, 27.24722222, 30.84186047, 33.14666667]) |
将预测的值转成Series,并且注意索引号:
1 | predict_bmi = pd.Series(y_pred, index=missing.index) |
Out[24]:
1 | 1 29.879487 |
填充到原来的df数据中:
In [25]:
1 | df.loc[missing.index, "bmi"] = predict_bmi |
进行上面的预测和填充之后,我们再次查看缺失值情况,发现已经没有任何缺失值:
In [26]:
1 | df.isnull().sum() |
Out[26]:
1 | id 0 |
数据EDA
In [27]:
1 | variables = [variable for variable in df.columns if variable not in ['id','stroke']] |
Out[27]:
1 | ['gender', |
连续型变量
In [28]:
1 | conts = ['age','avg_glucose_level','bmi'] |
几点结论:
- 年龄age:整体分布比较均衡,不同年龄段的人数差异小
- 血糖水平:主要集中在100以下
- bmi指标:呈现一定的正态分布
中风和未中风
上面我们查看了连续型变量的分布情况;可以看到bmi呈现明显的左偏态的分布。下面我们对比中风和未中风的情况:
1 | conts = ['age','avg_glucose_level','bmi'] |
从3个密度图中能够观察到:从上面的密度图中可以看出来:对于是否中风,年龄age是一个最主要的因素
对比不同年龄段的血糖和BMI指数
In [30]:
1 | px.scatter(df,x="age", |
年龄和血糖、bmi关系
1 | px.scatter(df,x="age", |
年龄和患病几率
从散点分布图中看到:年龄可能真的是一个比较重要的因素,和BMI以及平均的血糖水平有着一定的关系。
可能随着年龄的增长,风险在增加。果真如此吗?
In [32]:
1 | background_color = "#fafafa" |
上面的图形说明了两点:
- 年龄越大,中风的几率的确越来越高
- 中风的几率是非常低的(y轴的值很低),这是由于中风和未中风的样本不均衡造成的
原数据5000个样本中只有249个中风样本,比例接近1:20
样本不均衡
1 | from pywaffle import Waffle |
属性分布
整体变量情况
首先我们剔除gender中为Other的情况
In [34]:
1 | str_only = df[df['stroke'] == 1] # 中风 |
In [35]:
1 | len(str_only) |
Out[35]:
1 | 249 |
In [36]:
1 | # 剔除other |
下面的代码是比较在不同的属性下中风和未中风的情况:
1 | fig = plt.figure(figsize=(22,15)) |
建模
模型baseline
In [38]:
1 | len(str_only) |
Out[38]:
1 | 249 |
In [39]:
1 | 249 / len(df) |
Out[39]:
1 | 0.0487279843444227 |
说明总共有249个人是中风的。本数据的总人数是len(df),根据下面的表达式能够得到本次模型的baseline。
也就说,对于阳性中风患者的召回率,一个好的目标是4.8%。
字段编码
对4个字符型的字段进行编码工作:
In [40]:
1 | df['gender'] = df['gender'].replace({'Male':0, |
抽烟状态的独热码转换:
In [41]:
1 | df["smoking_status"].value_counts() |
Out[41]:
1 | never smoked 1892 |
In [42]:
1 | df = df.join(pd.get_dummies(df["smoking_status"])) |
数据分割
In [43]:
1 | # 选取特征 |
上采样
前文中提到,本案例中风和未中风的数据比例接近1:20,在这里我们采样基于SMOTE的上采样方法
In [44]:
1 | oversample = SMOTE() |
In [45]:
1 | len(y_train_smote) |
Out[45]:
1 | 2914 |
In [46]:
1 | len(X_train_smote) |
Out[46]:
1 | 2914 |
建模
采用3种不同的分类模型来建立模型:Random Forest, SVM, Logisitc Regression
In [47]:
1 | rf_pipeline = Pipeline(steps = [('scale',StandardScaler()), # 标准化 |
10折交叉验证
In [48]:
1 | rf_cv = cross_val_score(rf_pipeline, |
3种模型得分对比
In [49]:
1 | print('随机森林:', rf_cv.mean()) |
很明显:随机森林表现的最好!
模型训练fit
In [50]:
1 | rf_pipeline.fit(X_train_smote,y_train_smote) |
Out[50]:
1 | Pipeline(steps=[('scale', StandardScaler()), |
In [51]:
1 | # 3种模型预测 |
评价指标
In [52]:
1 | # 1、混淆矩阵 |
In [53]:
1 | print(rf_cm) |
In [54]:
1 | # 2、F_1得分 |
In [55]:
1 | print('RF mean :',rf_f1) |
随机森林模型的分类报告:
In [56]:
1 | from sklearn.metrics import plot_confusion_matrix, classification_report |
随机森林模型调参
基于网格搜索的参数调优:
In [57]:
1 | from sklearn.model_selection import GridSearchCV |
In [58]:
1 | grid = GridSearchCV(rfc,param_grid) |
Out[58]:
1 | GridSearchCV(estimator=RandomForestClassifier(), |
In [59]:
1 | grid.best_params_ # 找到最优的参数 |
Out[59]:
1 | {'bootstrap': False, 'max_features': 3, 'n_estimators': 200} |
In [60]:
1 | # 再次建立随机森林模型 |
In [61]:
1 | # 新的分类报告得分 |
逻辑回归模型调参
In [62]:
1 | penalty = ['l1','l2'] |
In [63]:
1 | grid.fit(X_train_smote,y_train_smote) |
Out[63]:
1 | GridSearchCV(estimator=LogisticRegression(), |
In [64]:
1 | grid.best_params_ |
Out[64]:
1 | {'C': 1, 'penalty': 'l2'} |
In [65]:
1 | logreg_pipeline = Pipeline(steps = [('scale',StandardScaler()), |
Out[65]:
1 | Pipeline(steps=[('scale', StandardScaler()), |
In [66]:
1 | logreg_new_pred = logreg_pipeline.predict(X_test) # 新预测 |
In [67]:
1 | print(classification_report(y_test,logreg_new_pred)) |
支持向量机调参
In [68]:
1 | svm_param_grid = { |
In [69]:
1 | grid.fit(X_train_smote,y_train_smote) |
Out[69]:
1 | GridSearchCV(estimator=SVC(random_state=42), |
In [70]:
1 | grid.best_params_ |
Out[70]:
1 | {'C': 100, 'gamma': 0.0001, 'kernel': 'rbf'} |
In [71]:
1 | svm_pipeline = Pipeline(steps = [('scale',StandardScaler()),('SVM',SVC(C=100,gamma=0.0001,kernel='rbf',random_state=42))]) |
In [72]:
1 | print(classification_report(y_test,svm_tuned_pred)) |
结论
- 在交叉验证的过程中,随机森林表现的最好。
- 3种模型的对比:随机森林的精度最好,但是F1-score缺失最低的
- 模型可能特点:更能预测哪些人将会中风,而不是哪些人不会中风