【版权声明】版权所有,严禁转载,严禁用于商业用途,侵权必究。
作者:厦门大学计算机科学与技术系2023级研究生 李云倩
指导老师:厦门大学数据库实验室 林子雨 博士/副教授
时间:2024年6月
相关教材:林子雨、郑海山、赖永炫编著《Spark编程基础(Python版,第2版)》(访问教材官网)
相关案例:基于Python语言的Spark数据处理分析案例集锦(PySpark)
数据集和代码下载:从百度网盘下载本案例数据集和代码。(提取码是ziyu)
1.实验目的
根据世界卫生组织 (WHO) 的数据,中风是全球第二大死亡原因,约占总死亡人数的 11%。因此,通过数据分析和预测模型进行中风的早期预警和预防具有重要的公共卫生意义。本实验对中风数据集进行数据清理、处理和分析,并且通过特征工程和机器学习模型提升中风预测的准确性,最后总结并可视化数据分析的过程和结果。
2.实验环境
本次实验在Linux虚拟机上搭建,主要环境如下,其他需要的第三方库及对应版本见requirements.txt。
(1)Linux: Ubuntu 20.04.6
(2)Python: 3.8
(3)JDK: 1.8
(4)Hadoop: 3.3.5
(5)Spark: 3.2.0
(6)运行软件:VS Code
3.数据预处理
3.1 数据集预览
本实验所采用的数据集Stroke Prediction Dataset来自Kaggle平台。该数据集用于根据性别、年龄、各种疾病和吸烟状况等输入参数来预测患者是否可能患中风。数据中的每一行都提供有关患者的相关信息。具体的字段信息见下表所示。
使用Pandas库将csv文件读入并保存为DateFrame的格式,通过df.head()方法来查看数据数据集的部分内容。
3.2数据清洗
3.2.1删除缺失值
通过df.info()方法来查看数据的基本信息:列名(Column)、非空值的数量(Non-Null Count)、数据类型(Dtype)。可以看到该数据集总共有11列的属性,且‘bmi’字段中的非空值的数量与其他列有所不同,需要进一步的处理。
进一步通过df.isna().sum()方法来查看每一个字段中空值的数量,如下图所示,‘bmi’列中有201个空值。
通过使用df.dropna()方法来删除‘bmi’列中的空值,并且再次查看该数据集的统计信息如下。
3.2.2删除重复值
因为‘id’是每一个元祖的唯一标识符,所以通过检查与删除重复的‘id’来删除重复值。具体是通过df.drop_duplicates()方法,删除后的数据集统计信息如下。
3.2.3删除异常值
往往数据因为统计或者存储过程中的差错,导致出现一些异常值,所以异常值的排查也十分重要。对于此数据集,我们按照日常常识对一些字段有如下约束:
年龄(age):应该在0~120岁之间;
平均血糖水平(avg_glucose_level):应该在0~300mg/dl之间;
体重指标(bmi):应该在10~100之间。
除此之外,该数据集中性别(gender)字段的值除了Male(男)、Famale(女)之外,还出现了其他(Other)。我们接受这样的样本,但是因为gender为Other的记录只有一条,对数据处理和分析没有作用,所以我们从数据集中剔除掉此条记录。
根据上述分析及约束条件,删除异常值的代码如下,观察删除后的数据集统计信息,发现被删除掉了一条记录。
3.2.4选取部分字段
由于‘id’字段对于后续的数据分析来说没有意义,所以将该字段通过df.drop()方法删除。删除后的数据集中剩下性别(gender),年龄(age)、高血压(hypertension)、心脏病(heart_disease)、是否结婚(ever_married)、工作类型(work_type)、居住类型(Residence_type)、平均血糖水平(avg_glucose_level)、体重指数(bmi)、吸烟状态(smoke_status)、是否中风(stoke)总共10个字段。
3.2.5保存预处理结果
把处理后的DataFrame以utf-8编码的格式存入新的csv表格中,命名为‘stroke-data.csv’。
利用以下代码(只展示了部分代码,具体见data_preprocess.ipynb),来统计每个字段中值的信息,以便于更好的掌握数据集的信息,并且观察数据预处理工作是否完善,统计结果如下图所示。
3.3数据伪分布式存储
利用HDFS将预处理过后的csv文件以伪分布式的形式存储。具体流程如下图所示,首先启动HDFS,并通过jps观察 DataNode、NameNode的状态以确保HDFS可以正常工作。
通过‘hdfs dfs -put source_path destination_path’来将文件存入HDFS中,如下图所示,我们的‘stroke-data.csv’已被成功存储到了HDFS的input目录下。
4.Spark数据分析
4.1基于Spark SQL组件的分析
本实验使用Apache Spark的核心组件SparkSession。它的优势是统一了DataFrame和Dataset API,简化了应用程序的创建和配置,同时包含了SparkContext的所有功能,可以直接从pyspark.sql导入。如下面展示的代码所示,创建SparkSession对象,并使用read方法来读取存储在HDFS中的csv文件。此时读入的文件就是DateFrame格式,进一步通过createOrReplaceTempView()方法来创建临时视图以供SQL查询。
4.1.1统计各字段不同属性的中风/非中风人数
为初步探究与刻画中风与为中风人群的人物特征,我们先统计各字段不同属性的中风人数。使用SparkSession.sql()方法,通过SQL语句来统计每个字段中的中风(stroke_count)和非中风(no_stroke_count)的数量。如下两张图中分别展示统计不同性别和不同年龄段的中风与非中风的人数的SQL语句。其中对于年龄,我们划分了0~18、18~35、35~50、50~65、66+五个年龄段来分别观察。
在使用SQL语句查询与统计结束后可以通过show()方法在终端展示结果,并进一步通过write()方法实现永久性的存储,以便后续的数据分析与可视化。
4.1.2对比不同人群的BMI分布
该数据集中除了中风数据之外,不同人群的BMI也是一个值得观测与统计的数据,可以探索不同人群的BMI分布的差异。也是通过SparkSession.sql()方法先进行查询且统计,下面的代码截图中展示了对于性别和工作类型两种角度的BMI统计过程。
4.1.3对比不同人群的平均血糖水平分布
基于和4.1.2同样的思想,我们认为平均血糖水平(avg_glucose_level)也是一个值得探索的数据,所以也做了类似的统计。部分代码截图如下。
4.1.4分析不同工作类型BMI值的特征
对于像BMI、平均血糖水平这种类型的数据,除了可以观察不同人群在此值上的分布之外,也有其他的数据特性可以挖掘,比如平均值、最大值、最小值、中位数等统计值,也是可以有价值的数据特征,所以我们又利用SQL语句中计算这些值的对应的函数,来提取这些值。具体的代码如下所示,我们分别统计了不同工作类型的BMI和平均血糖水平的有关值。
4.1.5统计不同年龄段的吸烟比例
在4.1.1中有提到将年龄划分成了0~18、18~35、35~50、50~65、66+五个年龄段,可以进一步观察不同年龄段的不同吸烟状态所占的比例。可以分析在哪个年龄段的人更喜好吸烟等现象。
4.2基于Spark MLlib组件的分析
4.2.1数据预处理
首先进行数据预处理,主要是包含三个步骤:
编码分类变量:对字符串类型的分类变量进行编码;
SMOTE过采样:为了解决中风和非中风样本数量的不平衡;
拆分数据集:以8:2的比例将数据集拆分为训练集和测试集。
重点代码截图如下,其他细节代码见Binary_Classification_X.py文件。
4.2.2 训练并预测模型
首先我们不对数据进行过采样,直接拿去训练分类器的时候,从以下评估结果的截图中可以看出,模型没有正确预测中风的能力。这是因为模型过拟合多数类别,导致模型在未见过的数据上泛化能力较差,对少数类别的分类准确率下降。
所以为了解决数据不平衡带来的问题,我们采用SMOTE过采样,将数据集中的两类样本数量维持在一个相对平衡的状态。以下是过采样后的样本数量的对比。
本实验分别采用逻辑回归、决策树、随机森林三个经典的机器学习分类算法,来训练中风的二分类预测任务模型。分别通过构建LogisticRegression、DecisionTreeClassifier、RandomForestClassifier三种分类器来训练分类模型,代码分别见Binary_Classification_LogisticRegression.py, BinaryClassification DecisionTreeClassifier.py, Binary_Classification_RandomForestClassifier.py。训练后在测试集上的预测结果如下图所示。可以看出随机森林算法训练的中风二分类模型的性能更好。
为了后续的预测,我们将模型保存到了HDFS的weights文件夹中。
4.2.3相关性分析
使用pyspark.ml.stat中的Correlation,计算特征之间的斯皮尔曼系数,具体代码见Correlation.py,最终绘制的相关性矩阵热图如下所示。
年龄、高血压、心脏病、bmi以及平均血糖水平与中风都有较强的相关性系数,也就是说,中风可以从一些身体的相关指标的控制来预防。
5.可视化
本实验使用 Flask + HTML + ECharts 进行数据可视化,使得此可视化的平台易于集成、跨平台支持、交互性丰富,能够实现动态更新和个性化的可视化界面。接下来的小节中将依次展示数据可视化的结果。
在可视化之前,需要将存入伪分布式的所有csv格式的数据文件下载到本地,具体的脚本见文件download_and_merge_all.sh,部分截图如下图所示。注意因为是伪分布式存储,所以需要将HDFS中子目录中的分文件合并,并且过程中产生的.crc文件。
5.1 统计各字段不同属性的中风人数
以下个8张图分别是各字段的不同属性的中风人数和非中风人数的条形统计图。可以看出在患中风的人群中,有心脏病、高血压的人更多,结婚过的人更多,工作类型为私营企业员工(Self-employed)和自营(Private)的人比重更大。
5.2 对比不同人群的BMI分布
对于男/女两种不同性别的人群来说,BMI分布的趋势比较相似,但女性(Female)的BMI集中在21-27,男性集中在24-27。
下图展示的是不同工作类型人群的BMI分布,可以很明显地观察到私营企业员工(Self-employed)和自营(Private)两个工作类型的人的BMI的峰值更偏向X轴的右侧,即这个两个工作类型的大部分人的BMI是比较高的。同时也可以观察到小孩(children)和从没有工作(Never_worked)的人群的BMI都相对偏小。
5.3 对比不同工作类型的平均血糖水平分布
对于不同工作类型的人群来说,平均血糖水平的分布趋势较为相似,但观察该图的右侧可以发现,私营企业员工(Self-employed)、自营(Private)以及Govt_job(政府工作人员)都有一小部分人的平均血糖水平超标(甚至超过了200)。
5.4 分析不同工作类型BMI值的特征
我们还使用了雷达图来更直观地比较BMI和平均血糖水平的平均值(Average)、最大值(Max)、最小值(Min)和中位数(Median)四个特征值。如下两个图所示分别是不同工作类型的BMI和平均血糖水平的雷达图统计结果。
在BMI的雷达图中,儿童(children)的每一个特征值都是最小的,在最内侧。而私营企业员工(Self-employed)和自营(Private)的平均值和中位数不分伯仲且都在雷达图的最外侧,说明这两个人群的BMI偏高。
在平均血糖水平的雷达图中,私营企业员工(Self-employed)、自营(Private)以及Govt_job(政府工作人员)的四个特征值都比较接近,而儿童(children)和从未工作(Never_worked)的人群的血糖的平均值偏小。
5.5 统计不同年龄段的吸烟比例
不同年龄段的不同吸烟状态我们用饼图来进行可视化,这样可以清楚地看到每个年龄段的每种吸烟状态所占的比例,如下5张图所示。
观察图表易得,儿童(children)吸烟状态为未知(Unknown)的比例很大,且吸烟(smokes)的比例也是最小的(只有1.45%),儿童吸烟有害健康成长,吸烟占比虽小,但还是要进一步干预。19-35、35-50、51-65这三个年龄段的吸烟(smokes)的比例都超过18%,吸烟比重很大。而66+的年龄段的人曾经吸烟(formerly smoked)的占比很大,说明大部分的老人年曾经有过吸烟史。同时还有11.42%左右的老年人依旧在吸烟。
5.6 中风预测
除了数据图表的可视化,本实验还通过Flask连接了前后端,将前段的相关参数数值传递给后端,后端利用训练好的中风模型进行中风预测,并且将预测结果返回给前端并显示,如下是一个测试的案例。在填写好所有的字段值之后,点击Predict按钮即可把这些值作为参数传给后端模型,最终将预测结果‘No Stroke’显示在界面中。
5.7 Web界面
以下是可视化界面整体的预览。左侧是统计分析后的数据的可视化,通过下拉框可以选择不同的可视化结果在其下方展示。右侧是中风预测功能的交互界面。因左侧采用Echarts进行图表的可视化,所以拥有灵动的动态效果,且将鼠标悬停在图上可以查看具体的数值,同时图表可以通过下载按钮直接保存在本地。
6.总结
在本次实验中,我基于Spark进行了中风数据的处理与分析。整个实验流程从数据预处理开始,包括数据清洗、删除缺失值和异常值等步骤。接着,将预处理后的数据存储在HDFS中,利用Spark SQL和Spark MLlib进行了详细的数据分析和机器学习建模。使用了逻辑回归、决策树和随机森林三种分类算法进行中风预测模型的训练,并通过SMOTE算法解决数据不平衡问题,显著提升了模型的预测性能。此外,还利用ECharts进行可视化,将数据分析结果以图表形式直观呈现。最后,通过Flask构建了一个简单的Web界面,实现了中风预测功能,用户可以输入相关参数并获取中风预测结果。