sklearn预测pima糖尿病
文章目录
-
-
数据集描述
-
- 准备工作
- 实验环境和工具
-
预测分析
-
- 探索性数据分析
-
- 数据描述
-
可视化分析
- 构建baseline
- 数据预处理
-
- 离群值处理
-
缺失值处理
-
特征工程
-
数据标准化
- 模型构建与调参优化
- 完整代码
-
数据集描述
该数据集包含十个属性字段
Pregnancies: 表示怀孕次数
Glucose值: 血糖浓度测量
BloodPressure: 表示舒张压(以毫米汞柱为单位)
SkinThickness: 评估肱三头肌皮肤褶皱厚度(毫米)
Insulin水平: 记录两个小时内的血清胰岛素水平(微单位/毫升)
BMI值: 计算公式为体重除以身高平方
Diabetes Pedigree Function: 衡量遗传因素与糖尿病相关的疾病风险指数
是否与遗传相关性如何
Height记录身高(厘米)
Above表示研究对象的年龄
Outcome结果中0代表健康个体,1代表患病个体。
任务:建立机器学习模型以准确预测数据集中的患者是否患有糖尿病
准备工作
通过查阅资料了解到各属性的数据值标准,在后续的数据分析和处理过程中具有重要意义。
实验环境和工具
在jupyter环境中运行的python3.x.x版本(如Python 3.6及以上)通常会搭配jupyter notebook运行环境一起使用;作为数据分析工具使用的pandas和numpy库是数据科学的基础组件;提供可视化的matplotlib和seaborn库能够帮助生成高质量的数据展示效果;用于机器学习建模的sklearn框架则提供了丰富的算法集合以支持各种建模需求
预测分析
探索性数据分析
数据描述
首先观察基本的数据类型,以及数据是否存在缺失情况,简要统计信息
all_data.shape
all_data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 768 entries, 0 to 767
Data columns (total 10 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Pregnancies 768 non-null int64
1 Glucose 768 non-null int64
2 BloodPressure 768 non-null int64
3 SkinThickness 768 non-null int64
4 Insulin 768 non-null int64
5 BMI 768 non-null float64
6 DiabetesPedigreeFunction 768 non-null float64
7 Age 768 non-null int64
8 Height 766 non-null object
9 Outcome 768 non-null int64
dtypes: float64(2), int64(7), object(1)
memory usage: 60.1+ KB
由于数据量规模较小(仅限于)768个实例,在分析过程中能够观察到除Height外的所有其他属性均为数值型变量。在后续的数据预处理流程中涉及对Height属性的数据类型转换操作。经过初步分析未发现任何缺失值的存在。
# height 数值类型 为object 需要转化为 数值型
all_data = all_data.astype({'Height':'float64'})
all_data.describe()

发现两个问题:
-
缺失现象
通过查看各特征的最小数值可以看出,在Glucose、BloodPressure、SkinTinckness、Insulin和BMI等特征中都存在取零的情况(虽然Pregnancies这一指标在实际情况下是可以取零的)。但在常规范围内这些数值明显不合理;因此也可以将其视为一种特殊的缺失现象;后续的数据预处理阶段需要对这些异常数值进行合理填充。 -
Outliers
Glucose, BloodPressure, SkinTinckness, Insulin等特征的max值与75%分位点值之间的差异较大(即max - Q3),以及min值与25%分位点值之间的差异也较大(即Q1 - min),初步判断可能存在outliers(具体可见图4中用红色框标注的部分)。
为了便于后续统一处理缺失数据,在检测到异常数据时将这些outliers统一进行imputation处理并替换为np.nan。
import numpy as np
#缺失值替换 经分析,除怀孕次数,其他特征的0值表示缺失值 替换为np.nan
replace_list = ['Glucose', 'BloodPressure', 'SkinThickness', 'Insulin', 'BMI', 'Height']
all_data.loc[:,replace_list] = all_data.loc[:,replace_list].replace({0:np.nan})
#各特征缺失数量统计
null_count = all_data.isnull().sum().values
# 缺失值情况
plt.figure()
sns.barplot(x = null_count, y = all_data.columns)
for x, y in enumerate(null_count):
plt.text(y, x, "%s" %y, horizontalalignment='center', verticalalignment='center')
plt.show()

通过观察Glucose、Insulin、SkinThickness、BMI和Height等五个特征的表现可以看出它们均存在缺失值问题。其中Insulin和SkinThickness两项指标的缺失比例较高分别达到了48%和30%因此后期的数据预处理环节同样至关重要
可视化分析
接下来采用更为细致/深入的具体可视化方法进行进一步分析其在不同时间段的表现及其与其他指标的关系
# 患病和不患病情况下 箱线图查看数据分散情况
for col in all_data.columns:
plt.figure(figsize = (10,6))
if all_data[col].unique().shape[0] > 2:
sns.boxplot(x="Outcome", y=col, data=all_data.dropna())
else:
sns.countplot(col,hue = 'Outcome',data = all_data.dropna())
plt.title(col)
plt.show()
部分输出:


观察患病和不患病情况下 各特征值或者人数分布
label接近2:1 存在一定的分布不平衡
像insulin之类的特征离群值是比较多的,由于离群值会对模型评估产生影响,所以后续可能要做处理,剔除偏离较大的离群值
# 患病和不患病情况下 各特征的分布情况
for col in all_data.drop('Outcome',1).columns:
plt.figure()
sns.displot(data = all_data, x = col,hue = 'Outcome',kind='kde')
plt.show()
部分输出:



基于数据样本自身的角度分析其分布特征,在患病与未患病两种情况下也观察到某些特性的密度分布较为接近。特别地,在height的分布图中发现两条曲线非常接近;似乎与标签变量之间的相关程度并不显著。
同时,在分析过程中也会遇到一些右偏现象(其中skewness(偏度)被定义为衡量数据分布形态对称性的统计指标)。考虑到许多机器学习模型对于输入的数据要求较高, 因此后续的数据预处理步骤中需要针对存在明显偏差的特征求取相应的解决方案以提高模型性能。
# 观察各特征分布和患病的关系
corr = all_data.corr()
plt.figure(figsize = (8,6))
sns.heatmap(corr,annot = True,cmap = 'Blues')
plt.show()

该函数能够清晰地用明暗程度来表示数据值的大小
其功能能够通过视觉化的方式呈现数据差异
- 经观察可知色调普遍较浅,在此基础之上无论是各特征间还是特征与outcome标签间的关联程度均未呈现显著性。
- 经分析可知其余各特征变量中与outcome具有显著关联度的指标中最高者为Glucose 属性值达到0.49次之则为Height属性值仅为0.059。
- 进一步观察特征间相互关联关系后可发现Insulin不仅与Glucose显示出较高的关联度(具体数值达0.58)更与BMI及SkinThickness呈现了较为显著的相关性(分别为0.65)。值得注意的是Insulin这一特性因其重要程度较高且其与其他指标间的紧密联系可被视为一种有效的缺失值填充方法。
plt.figure()
sns.scatterplot(x = 'Insulin', y = 'Glucose', data = all_data)
plt.show()
sns.scatterplot(x = 'Insulin', y = 'BMI', data = all_data)
plt.show()
sns.scatterplot(x = 'Insulin', y = 'Age', data = all_data)
plt.show()
plt.figure()
sns.scatterplot(x = 'SkinThickness', y = 'BMI', data = all_data)
plt.show()
sns.scatterplot(x = 'SkinThickness', y = 'Glucose', data = all_data)
plt.show()
sns.scatterplot(x = 'SkinThickness', y = 'BloodPressure', data = all_data)
plt.show()
部分输出:


构建baseline
因为决策树很少需要进行数据预处理。相比之下,其他方法通常需要进行数据标准化、生成虚拟变量以及排除缺失值的影响.
# 读取数据
all_data = pd.read_csv('data.csv')
# height 数值类型 为object 需要转化为 数值型
all_data = all_data.astype({'Height':'float64'})
#
all_data.dropna(inplace = True)
# 特征
feature_data = all_data.drop('Outcome',1)
# 标签
label = all_data['Outcome']
base_model = DecisionTreeClassifier()
base_scores = cross_validate(base_model, feature_data, label,cv=5,return_train_score=True)
print(base_scores['test_score'].mean())
6954248366013072
数据预处理
综合前面分析,先做了以下处理
# 读取数据
all_data = pd.read_csv('data.csv')
# height 数值类型 为object 需要转化为 数值型
all_data = all_data.astype({'Height':'float64'})
# 理论缺失值0替换为np.nan
replace_list = ['Glucose', 'BloodPressure', 'SkinThickness', 'Insulin', 'BMI', 'Height']
all_data.loc[:,replace_list] = all_data.loc[:,replace_list].replace({0:np.nan})
# 删除相关性低的Height
all_data.drop('Height',1,inplace = True)
离群值处理
- 经过前面的分析发现数据是存在部分离群值的,虽然实验本身就是关于疾病预测,异常值的存在属于正常现象。但是对于一些可能超出人体接受范围的值,衡量对预测的影响之后,由于数据量比较小,这里选择删除极端异常点。
- 极端异常点 :上限的计算公式为Q3+3(Q3-Q1) 下界的计算公式为Q1-3(Q3-Q1))。
# remove the outliers
# 异常点 上须的计算公式为Q3+1.5(Q3-Q1);下须的计算公式为Q1-1.5(Q3-Q1)
# 极端异常点 :上限的计算公式为Q3+3(Q3-Q1) 下界的计算公式为Q1-3(Q3-Q1)
# 由于数据量比较少 所以选择删除极端异常值
def remove_outliers(feature,all_data):
first_quartile = all_data[feature].describe()['25%']
third_quartile = all_data[feature].describe()['75%']
iqr = third_quartile - first_quartile
# 异常值下标
index = all_data[(all_data[feature] < (first_quartile - 3*iqr)) | (all_data[feature] > (first_quartile + 3*iqr))].index
all_data = all_data.drop(index)
return all_data
outlier_features = ['Insulin', 'Glucose', 'BloodPressure', 'SkinThickness', 'BMI', 'DiabetesPedigreeFunction']
for feat in outlier_features:
all_data = remove_outliers(feat,all_data)
处理之后的数据基本的统计信息

缺失值处理
为了避免数据泄露,在划分训练集与测试集后即停止对整体数据的预处理操作;因此最好对缺失数值填充的过程进行单独处理
缺失值处理这里考虑
- 直接删除处理
def drop_method(all_data):
median_fill = ['Glucose', 'BloodPressure','SkinThickness', 'BMI','Height']
for column in median_fill:
median_val = all_data[column].median()
all_data[column].fillna(median_val, inplace=True)
all_data.dropna(inplace = True)
return all_data
- 中值填充
def median_method():
for column in list(all_data.columns[all_data.isnull().sum() > 0]):
median = all_data[column].median()
all_data[column].fillna(median, inplace=True)
- KNNImputer填充
def knn_method():
# 先将缺失值比较少的特征用中值填充
values = {'Glucose': all_data['Glucose'].median(),'BloodPressure':all_data['BloodPressure'].median(),'BMI':all_data['BMI'].median()}
all_data.fillna(value=values,inplace=True)
# 用KNNImputer 填充 Insulin SkinThickness
corr_SkinThickness = ['BMI', 'Glucose','BloodPressure', 'SkinThickness']
# 权重按距离的倒数表示。在这种情况下,查询点的近邻比远处的近邻具有更大的影响力
SkinThickness_imputer = KNNImputer(weights = 'distance')
all_data[corr_SkinThickness] = SkinThickness_imputer.fit_transform(all_data[corr_SkinThickness])
corr_Insulin = ['Glucose', 'BMI','BloodPressure', 'Insulin']
Insulin_imputer = KNNImputer(weights = 'distance')
all_data[corr_Insulin] = Insulin_imputer.fit_transform(all_data[corr_Insulin])
- 随机森林填充
from sklearn.ensemble import RandomForestRegressor
from sklearn.impute import SimpleImputer # 用来填补缺失值
def predict_method(feature):
# 复制一份数据 避免对原数据做出不必要的修改
copy_data = all_data.copy()
# 缺失了的下标
predict_index = copy_data[copy_data[feature].isnull()].index
# 没缺失的下标
train_index = copy_data[feature].dropna().index
# 用作预测 的训练集标签
train_label = copy_data.loc[train_index,feature]
copy_data = copy_data.drop(feature,axis=1)
# 对特征先用中值填充
imp_median = SimpleImputer(strategy='median')
# 用作预测的训练集特征
train_feature = copy_data.loc[train_index]
train_feature = imp_median.fit_transform(train_feature)
# 需要进行预测填充处理的缺失值
pre_feature = copy_data.loc[predict_index]
pre_feature = imp_median.fit_transform(pre_feature)
# 选取随机森林模型
fill_model = RandomForestRegressor()
fill_model = fill_model.fit(train_feature,train_label)
# 预测 填充
pre_value = fill_model.predict(pre_feature)
all_data.loc[predict_index,feature] = pre_value
#用随机森林的方法填充缺失值较多的 SkinThickness 和 Insulin 缺失值
predict_method("Insulin")
predict_method("SkinThickness")
# 其余值中值填充
for column in list(all_data.columns[all_data.isnull().sum() > 0]):
median = all_data[column].median()
all_data[column].fillna(median, inplace=True)
特征工程
# 特征
feture_data = all_data.drop('Outcome',1)
# 标签
label = all_data['Outcome']
# 利用BMI和身高构造weight特征
# BMI = weight(kg) / height(m)**2
feture_data['weight'] = (0.01*feture_data['Height'])**2 * feture_data['BMI']
数据标准化
# 标准化
Std = StandardScaler()
feture_data = Std.fit_transform(feture_data)
模型构建与调参优化
用到的模型
from sklearn.svm 导入 Support\ Vector\ Machine\ (SVM)\ model 和 SVR
from sklearn.tree 导入 Decision\ Tree\ Model
from sklearn.linear\_model 导入 Logistic\ Regression\ Model
从 sklearn.ensemble 导入 Random\ Forest\ Model 和 Stacked\ Learning\ Framework
调参方法
from sklearn.model_selection import GridSearchCV
评估指标 Accuracy roc_auc
该库中的make_scorer函数被引入。
该库中的accuracy_score函数被引入。
该库中的roc_auc_score函数被引入。
def train(model, params):
grid_search = GridSearchCV(estimator = model, param_grid = params,scoring=scores,refit='Accuracy')
grid_search.fit(feture_data,label)
print(grid_search.best_estimator_)
return grid_search
def paint(x,y):
plt.figure()
sns.lineplot(x=x,y=y)
plt.show()
SVC
#调参时先尝试一个大范围,确定比较小的范围,然后在小范围里搜索
model = SVC()
params = {'C':np.linspace(0.1, 2, 100)}
SVC_grid_search = train(model,params)
paint([x for x in range(100)],SVC_grid_search.cv_results_['mean_test_Accuracy'])
paint([x for x in range(100)],SVC_grid_search.cv_results_['mean_test_AUC'])
print("test_Accuracy : {}\ntest_AUC : {}".format(SVC_grid_search.cv_results_['mean_test_Accuracy'].mean(),SVC_grid_search.cv_results_['mean_test_AUC'].mean()))
LogisticRegression
model = LogisticRegression()
params = {"C":np.linspace(0.1,2,100)}
LR_grid_search = train(model,params)
paint([x for x in range(100)],LR_grid_search.cv_results_['mean_test_Accuracy'])
paint([x for x in range(100)],LR_grid_search.cv_results_['mean_test_AUC'])
print("test_Accuracy : {}\ntest_AUC : {}".format(LR_grid_search.cv_results_['mean_test_Accuracy'].mean(),LR_grid_search.cv_results_['mean_test_AUC'].mean()))
RandomForestClassifier
model = RandomForestClassifier()
params = {"n_estimators":[x for x in range(30,50,2)],'min_samples_split':[x for x in range(4,10)]}
RFC_grid_search = train(model,params)
print("test_Accuracy : {}\ntest_AUC : {}".format(RFC_grid_search.cv_results_['mean_test_Accuracy'].mean(),RFC_grid_search.cv_results_['mean_test_AUC'].mean()))
StackingClassifier
estimators = [
('SVC',SVC_grid_search.best_estimator_),
('NB', LR_grid_search.best_estimator_),
('RFC', RFC_grid_search.best_estimator_)
]
model = StackingClassifier(estimators=estimators, final_estimator=SVC())
model_score = cross_validate(model,feture_data, label,scoring=scores)
print("test_Accuracy : {}\ntest_AUC : {}".format(model_score['test_Accuracy'].mean(),model_score['test_AUC'].mean()))
SVC预测结果:
依次剔除数据集中的缺失值以及异常值,并按照上下四分位法计算出上限Q3+1.5倍的(Q3-Q₁)及下限Q₁-上述公式的差额进行剔除;其中上四分位数与下四分位数之间的距离乘以系数得到的区间即为此判断标准


分别计算并删除超出范围的异常值(即使用阈值确定超出范围的样本);下限则通过Q1-3倍IQR来确定上限。
模型参数设置为C=1.405(五位小数)。
测试集准确率(test_Accuracy)为约79.5%;测试集AUC(test_AUC)约为71.3%。


基于中位数的填补以及基于四分位距的异常点剔除方法用于数据预处理;其上限计算方式是 Q₃ 加上三倍四分位间距(即 Q₃ - Q₁),而下限确定采用的是 Q₁ 减去同样的三倍间距。支持向量分类器参数设置为 C = 1.79(近似值)。在测试集上进行模型评估时发现:分类器在测试集上的准确率评估结果为 0.78...;同时模型在测试集上的 AUC 值评估结果则达到了 0.72...


其余略
总结:
一些删除数据值得到的影响是会使得各类别比例失衡的结果会导致模型对数量较大的类别出现过拟合现象预测结果倾向于数量较多类别的分类从而降低模型的整体泛化能力表现上表现为准确率较高但roc_auc_score较低的情况如使用支持向量机(SVC)所得到的结果所示很好地说明了这一现象。
反而是由于存在较多的数据缺失反而使得各种填补策略的效果不如直接去除数据的效果(也有可能是我还没有找到合适的填补策略)
在处理离群值时主要通过三种方法:直接删除、填补缺失以及中位数填补等方式进行操作其中由于填补缺失部分效果不佳最终选择了直接去除异常点经过对roc_auc_score和accuracy两个指标进行优化之后最终仅去除极端异常点
关于样本0/1比例的问题本文没有涉及上采样或下采样相关内容
完整代码
https://github.com/wang-hui-shan/Pima_Diabetes_Predict
特别鸣谢:评论区提建议的小伙伴>-<
