轻松学医疗保健分析-全-
轻松学医疗保健分析(全)
原文:
annas-archive.org/md5/d23e06d989b2f11c724cc2cf921f15d7译者:飞龙
前言
本书的功能目标是展示如何使用 Python 包进行数据分析;如何从电子健康记录(EHR)调查中导入、收集、清理和优化数据;以及如何利用这些数据制作预测模型,并辅以实际示例。
适合读者
简明医疗分析适合你,如果你是一个掌握 Python 或相关编程语言的开发者,即使你对医疗或医疗数据的预测建模不熟悉,仍然可以受益。对分析和医疗计算感兴趣的临床医生也会从本书中获益。本书还可以作为医疗机器学习入门课程的教科书。
本书内容概述
第一章,医疗分析入门,提供医疗分析的定义,列出一些基础话题,提供该主题的历史,给出医疗分析实际应用的例子,并包括本书中软件的下载、安装和基本使用说明。
第二章,医疗基础,包括美国医疗体系的结构和服务概述,提供与医疗分析相关的立法背景,描述临床患者数据和临床编码系统,并提供医疗分析的细分。
第三章,机器学习基础,描述了用于医学决策的部分模型框架,并描述了机器学习流程,从数据导入到模型评估。
第四章,计算基础 – 数据库,提供 SQL 语言的介绍,并通过医疗预测分析示例展示 SQL 在医疗中的应用。
第五章,计算基础 – Python 入门,提供 Python 及其在分析中重要库的基本概述。我们讨论 Python 中的变量类型、数据结构、函数和模块。还介绍了pandas和scikit-learn库。
第六章,衡量医疗质量,描述了医疗绩效评估中使用的指标,概述了美国的基于价值的计划,并展示如何在 Python 中下载和分析基于提供者的数据。
第七章,医疗中的预测模型制作,描述了公开可用的临床数据集所包含的信息,包括下载说明。然后,我们展示如何使用 Python、pandas和 scikit-learn 来制作预测模型。
第八章,医疗保健预测模型——回顾,回顾了通过比较机器学习结果和传统方法所得结果,当前在选择性疾病和应用领域中,医疗保健预测分析的进展。
第九章,未来—医疗保健与新兴技术,讨论了通过使用互联网推动医疗保健分析的一些进展,向读者介绍了医疗保健中的深度学习技术,并陈述了医疗保健分析面临的一些挑战和局限性。
充分利用本书的内容
需要了解的一些有用信息包括:
-
高中数学,如基本的概率论、统计学和代数
-
对编程语言和/或基本编程概念的基本了解
-
对医疗保健的基本了解以及一些临床术语的工作知识
请按照第一章的医疗保健分析简介中的说明设置 Anaconda 和 SQLite。
下载示例代码文件
您可以从您的账户中下载本书的示例代码文件,网址:www.packtpub.com。如果您从其他地方购买了本书,您可以访问www.packtpub.com/support并注册,文件将直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
登录或注册:www.packtpub.com。
-
选择“支持”选项卡。
-
点击“代码下载与勘误”。
-
在搜索框中输入书名,并按照屏幕上的指示操作。
下载文件后,请确保使用最新版本的工具解压或提取文件夹:
-
Windows 版的 WinRAR/7-Zip
-
Mac 版的 Zipeg/iZip/UnRarX
-
Linux 版的 7-Zip/PeaZip
本书的代码包也托管在 GitHub 上,地址是github.com/PacktPublishing/Healthcare-Analytics-Made-Simple。如果代码有更新,更新将发布在现有的 GitHub 仓库中。
我们还有来自我们丰富书籍和视频目录的其他代码包,您可以在github.com/PacktPublishing/查看。不要错过!
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以在此下载:www.packtpub.com/sites/default/files/downloads/HealthcareAnalyticsMadeSimple_ColorImages.pdf。
使用的约定
本书中使用了若干文本约定。
CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入以及 Twitter 账号。例如:“将下载的 WebStorm-10*.dmg 磁盘映像文件挂载为系统中的另一个磁盘。”
代码块如下所示:
string_1 = '1'
string_2 = '2'
string_sum = string_1 + string_2
print(string_sum)
当我们希望特别提醒你注意代码块中的某一部分时,相关行或项目会以粗体显示:
test_split_string = 'Jones,Bill,49,Atlanta,GA,12345'
output = test_split_string.split(',')
print(output)
粗体:表示新术语、重要词汇或你在屏幕上看到的词汇。例如,菜单或对话框中的词汇会以这种方式出现在文本中。这里有一个例子:“从管理面板中选择系统信息。”
警告或重要提示会以这种形式出现。
提示和技巧会以这种形式出现。
联系我们
我们始终欢迎读者的反馈。
一般反馈:发送电子邮件至 feedback@packtpub.com,并在邮件主题中注明书名。如果你对本书的任何内容有疑问,请通过 questions@packtpub.com 与我们联系。
勘误表:尽管我们已经尽最大努力确保内容的准确性,但错误还是会发生。如果你在本书中发现错误,请报告给我们。请访问 www.packtpub.com/submit-errata,选择你的书籍,点击勘误提交表单链接,并填写相关信息。
盗版:如果你在互联网上发现我们作品的任何非法复制形式,我们将感激你提供该位置地址或网站名称。请通过 copyright@packtpub.com 与我们联系,并提供相关资料链接。
如果你有兴趣成为作者:如果你在某个领域有专业知识,并且有意写书或为书籍贡献内容,请访问 authors.packtpub.com。
评论
请留下评论。一旦你阅读并使用了本书,为什么不在你购买书籍的网站上留下评论呢?潜在的读者可以通过看到并参考你的公正意见来做出购买决策,我们在 Packt 也能了解你对我们产品的看法,而我们的作者也可以看到你对他们书籍的反馈。谢谢!
如果你想了解更多关于 Packt 的信息,请访问 packtpub.com。
第一章:医疗分析简介
本章旨在为您介绍医疗分析领域,适合所有读者。到本章结束时,您将了解医疗分析的基本定义、医疗分析涵盖的主题、医疗分析的历史以及一些知名的应用领域。在本章的后半部分,我们将引导您安装所需的软件,并简要介绍 Anaconda 和 SQLite。
简而言之,本章将涵盖以下主题:
-
医疗分析基础
-
医疗分析的历史
-
医疗分析的实例
-
Anaconda、Jupyter Notebook 和 SQLite 介绍
什么是医疗分析?
不幸的是,医疗分析在韦氏词典中还没有定义。然而,我们对医疗分析的定义是利用先进的计算技术来改善医疗护理。让我们逐句分析这个定义。
医疗分析使用先进的计算技术
在写作本文时,我们接近 2020 年,计算机和手机已经占据了我们生活的许多方面,医疗行业也不例外。我们大部分的医疗数据正从纸质记录转移到电子记录,许多情况下,这是受到政府大力激励的推动。同时,无数的医疗移动应用程序正在被开发出来,用于追踪生命体征,包括心率和体重,甚至与医生进行沟通。虽然这一转变并非易事,但它将允许应用先进的计算技术,帮助打开改善每个人医疗护理的大门。
这些先进的计算技术有哪些?我们将在接下来的章节中讨论它们。
医疗分析作用于医疗行业(显然!)
如果你在寻找一本展示如何使用机器学习预测末日年份的书,很抱歉,这不是这本书。医疗分析涉及的是所有医疗相关的事物。
医疗分析改善医疗护理
到目前为止,我们正在使用计算机做一些医疗相关的事情。我们究竟在做什么?我们正在尝试改善医疗护理。嗯,这说起来很宽泛,不是吗?医疗护理的效果通常通过所谓的医疗三重目标来衡量:改善结果、降低成本和确保质量(虽然我们看到这里使用了不同的词)。让我们依次看看这三个目标。
更好的结果
从个人角度来看,每个人都能与更好的医疗结果产生共鸣。每当我们去看医生或住院时,我们都渴望获得更好的结果。具体来说,以下是我们关注的一些问题:
-
准确的诊断:当我们看医生时,通常是因为有健康问题。这个问题可能会在我们的生活中造成一些痛苦或焦虑。我们关心的是这个问题的根本原因能否被准确识别,以便能有效治疗。
-
有效治疗:治疗可能昂贵、耗时,并可能产生副作用;因此,我们希望确保治疗是有效的。我们不想再次请假去看医生,或者两个月后因同样的问题住院——这种经历在时间和金钱上都会非常昂贵(无论是医疗账单还是税款)。
-
无并发症:我们不希望在寻求治疗当前疾病时,突然感染新疾病或发生危险摔倒。
-
整体改善的生活质量:总结更好健康结果的概念,尽管政府机构和医生组织可能有不同的结果衡量方式,但我们追求的是一种没有痛苦和忧虑的生活质量和长寿的改善。
降低成本
所以目标是更好的健康结果,对吧?不幸的是,我们不能为每个人提供全天候的医疗服务,因为我们的经济会崩溃。我们不能提前进行全身 X 光检查以检测所有癌症。医疗保健中存在着在实现更好的健康结果和降低成本之间的微妙平衡。医疗保健分析的想法是,通过较为经济的技术,我们可以做得更多。胸部 CT 扫描筛查肺癌可能需要数千美元;然而,对患者病史进行数学计算来筛查肺癌的成本则低得多。本书的计划是向你展示如何进行这些计算。
确保质量
医疗保健质量涵盖了患者在接受医疗服务后的满意度。在资本主义体系中(如美国的医疗体系),提高质量的有效方法之一是通过公正和客观地衡量不同提供者的表现,以便患者能做出更明智的治疗决策。
医疗保健分析基础
现在我们已经定义并介绍了医疗保健分析,重要的是要提供一些背景知识,来说明它的基础。医疗保健分析可以被视为三个领域的交集:医疗保健(医疗保健分析)、数学(数学)和计算机科学(计算机科学),如下图所示。让我们依次探索这三个领域:

医疗保健
医疗保健是医疗保健分析的领域知识支柱。以下是构成医疗保健分析的一些重要医疗保健知识领域:
-
医疗服务与政策:了解医疗行业的结构、主要参与者及其财务激励机制,能帮助我们改进医疗分析工作。
-
医疗数据:无论是结构化数据还是非结构化数据,医疗数据都丰富且复杂。然而,医疗数据的收集通常遵循特定模板。了解典型的病史与体格检查(H&P)及其在病历中的组织方式,对于将这些数据转化为知识非常有帮助。
-
临床科学:了解医学术语和疾病有助于在浩瀚的医疗信息中识别出重要内容。临床科学通常分为两个领域:生理学,即人体正常功能的研究,和病理学,即人体在患病时的功能变化。掌握这两者的基础知识,有助于进行有效的医疗分析。
针对医疗分析的医疗入门将在第二章中介绍,医疗基础。
数学
我们的医疗分析三大支柱中的第二支柱是数学。我们并不想通过这个列表吓到你;详细了解以下所有领域并不是进行有效医疗分析的前提。然而,掌握高中数学的基础知识可能是必不可少的。其他领域则在理解那些帮助我们预测疾病的机器学习模型时特别有用。话虽如此,以下是构成医疗分析的几个重要数学领域:
-
高中数学:代数、线性方程和预备微积分等学科是医疗分析中更高级数学知识的基础。
-
概率与统计:信不信由你,每个医学生在训练过程中都会修一门生物统计学课程。是的,有效的医疗诊断和治疗在很大程度上依赖于概率与统计,包括敏感度、特异度和似然比等概念。
-
线性代数:在医疗数据上进行机器学习建模时,通常需要进行向量和矩阵运算。在使用 NumPy 和 scikit-learn 构建 Python 中的机器学习模型时,你将经常执行这些运算。
-
微积分与优化:这两个主题尤其适用于神经网络和深度学习,深度学习是一种机器学习类型,它由多个层次的线性和非线性数据变换组成。微积分和优化对于理解这些模型的训练过程非常重要。
针对医疗分析的数学与机器学习入门将在第三章中介绍,机器学习基础。
计算机科学
以下是构成医疗分析的一些重要计算机科学领域:
-
人工智能:在医疗分析的核心是人工智能,或称为研究与环境互动的系统。机器学习是人工智能中的一个子领域,它通过使用来自先前事件的信息对未来事件进行预测。我们将在本书后续部分研究的模型是机器学习模型。
-
数据库与信息管理:医疗数据通常通过关系型 数据库访问,这些数据可以通过电子病历(EMR)系统按需导出,或存储在云端。SQL(结构化查询语言的缩写)可用于选择我们感兴趣的特定数据,并对这些数据进行变换。
-
编程语言:编程语言为人类程序员与计算机内部的二进制数据提供了接口。编程语言允许程序员向计算机提供指令,对人类无法实际完成的数据进行计算。在本书中,我们将使用 Python,这是一种流行且新兴的编程语言,具有开源、全面的特点,并且拥有大量的机器学习库。
-
软件工程:你们中的许多人可能正在学习医疗分析,因为你们有兴趣在工作场所部署生产级别的医疗应用。软件工程是研究如何有效且高效地构建满足用户和客户需求的软件系统。
-
人机交互:医疗分析应用的最终用户通常不会使用编程来获取结果,而是依赖于可视化界面。人机交互是研究人类如何与计算机互动,以及如何设计此类接口的学科。当前医学领域的一个热点话题是如何使电子病历(EMR)应用更加直观和易于医生使用,而不是增加医生在为每个患者写记录时所需的鼠标点击次数。
计算机科学在医疗分析中的应用无处不在,几乎本书的每一章都会涉及到它。
医疗分析的历史
医疗分析的起源可以追溯到 1950 年代,距 1946 年世界上第一台计算机(ENIAC)发明仅几年。当时,医疗记录仍是纸质的,回归分析由人工完成,政府也没有为追求价值导向的医疗提供激励。尽管如此,人们仍对开发自动化应用程序来诊断和治疗人类疾病产生了浓厚的兴趣,这一点在当时的科学文献中有所体现。例如,1959 年,《科学》杂志发表了一篇题为《医学诊断的推理基础》的文章,作者是罗伯特·S·莱德利和李·B·拉斯特德,该文从数学角度解释了医生如何做出医学诊断(莱德利和拉斯特德,1959 年)。该文解释了许多现代生物统计学的核心概念,尽管有时使用了我们今天可能不太认得的术语和符号。
在 1970 年代,随着计算机的崛起并在学术研究中心变得更加普及,开发医学诊断决策支持(MDDS)系统的兴趣日益增长。这是一个广泛的、综合的计算机程序的统称,当输入患者信息时,这些程序能够准确地指出医学诊断。INTERNIST-1 系统是这些系统中最著名的,由匹兹堡大学的研究小组在 1970 年代开发(Miller 等,1982 年)。其发明者将 INTERNIST 系统描述为“一个用于计算机辅助诊断的实验程序,专注于一般内科学”,该系统经过 15 个人年的工作开发,并且进行了广泛的医学专家咨询。其知识库涵盖了 500 种个别疾病和 3,500 种临床表现,跨越所有医学亚专业。用户首先输入患者的阳性和阴性发现,然后可以查看差异诊断列表,并查看在添加新发现后这些诊断如何变化。该程序智能地请求特定的检验结果,直到得出明确的诊断。尽管它初期表现出了一定的潜力,激发了医学界的想象力,但最终未能进入主流,因为其推荐结果未能超越由一组领先医生提出的建议。它失败的其他原因(以及 MDDS 系统普遍的失败原因)可能包括缺乏吸引人的视觉界面(当时微软 Windows 尚未发明)以及现代机器学习技术尚未被发现。
在 1980 年代,人们重新关注了人工智能技术,而这种兴趣在 1960 年代末因感知机的局限性被马文·明斯基和西摩·帕特特在他们的著作《感知机》(Minsky and Papert, 1969)中阐明后,曾一度消退。大卫·E·鲁梅尔哈特、杰弗里·E·辛顿和罗纳德·J·威廉姆斯在 1986 年发表的论文《通过反向传播误差进行学习表示》(Learning representations by back-propagating errors)标志着反向传播训练的非线性神经网络的诞生,这种网络今天在许多人工智能任务中,如语音和数字识别,表现堪比人类(Rumelhart et al., 1986)。
仅仅几年后,这些技术就被应用于医学领域。1990 年,威廉·巴克斯特在《神经计算》期刊上发表了一项名为《使用人工神经网络进行临床决策分析:急性冠状动脉闭塞的诊断》的研究(Baxt, 1990)。在这项研究中,人工神经网络在诊断心脏病时表现超过了一组医学医生,使用的是心电图(EKG)的检查结果。这项开创性的研究帮助推动了生物医学机器学习研究的爆发,这一趋势一直持续至今。事实上,通过生物医学搜索引擎 PubMed 搜索“机器学习”在 1990 年只有 9 个结果,而在 2017 年则有超过 4000 个结果,中间的几年里,结果稳步增加:

有几个因素促成了生物医学机器学习研究的加速。第一个是机器学习算法的数量和可用性的增加。神经网络只是其中一个例子。在 1990 年代,医学研究人员开始使用各种替代算法,包括最近开发的决策树、随机森林和支持向量机算法,除了传统的统计模型,如逻辑回归和线性回归。
第二个因素是电子临床数据的可用性增加。在 2000 年前,几乎所有的医学数据都是以纸质图表的形式存在,进行计算机化的机器学习研究意味着需要数小时将数据手动输入计算机。电子病历的增长和最终普及使得使用这些数据来构建机器学习模型变得更加简单。此外,更多的数据意味着更准确的模型。
这让我们来到了今天,医疗分析正经历一个激动人心的时刻。今天的现代神经网络(通常被称为深度学习网络)在比心电图解读更复杂的任务中,常常超过人类的表现,比如从 X 光图像中识别癌症和预测患者未来医疗事件的序列。深度学习通常通过使用数百万个患者记录,并结合并行计算技术来实现这一点,使得在更短的时间内训练大型模型成为可能,同时还有新开发的技术用于调整、正则化和优化机器学习模型。另一个令人兴奋的现象是,政府激励措施的引入,旨在消除医疗过度支出和误诊现象。这些激励措施不仅引起了学术研究者的兴趣,也吸引了工业参与者和公司,他们希望为医疗机构节省资金(并为自己赚取一些利润)。
尽管医疗分析和机器算法尚未重新定义医疗护理,但医疗分析的未来前景看起来非常光明。就我个人而言,我喜欢想象这样一个场景:医院配备摄像头,私密且安全地记录患者护理的每一个方面,包括患者和医生之间的对话,以及患者在听到自己医学检查结果时的面部表情。这些文字和图像可以传送给机器学习算法,用来预测患者对未来结果的反应,以及这些结果会是什么。但我们现在有点超前了;在我们到达那个日子之前,还有很多工作要做!
医疗分析的例子
为了让你更好地了解医疗分析的涵盖范围,以下是一些医疗分析使用案例的例子,展示了现代医疗分析的广度和深度。
使用可视化来阐明患者护理
分析通常被分为三个子组件——描述性分析、预测性分析和规范性分析。描述性分析包括使用之前讨论的分析技术,更好地描述或总结正在研究的过程。了解护理是如何提供的就是一个能够从描述性分析中受益的过程。
我们如何利用描述性分析更好地理解医疗服务?以下是一个例子,展示了一名幼儿在出现哮喘加重症状时就诊急诊科(emergency department,ED)的护理记录可视化(Basole 等人,2015)。该可视化使用了电子病历(EMR)系统中常见的结构化临床数据,概括了他们在急诊科经历的护理事件的时间关系。该可视化由四种类型的活动组成——行政(黄色)、诊断(绿色)、药物(蓝色)和实验室检查(红色)。这些活动通过颜色和y-位置进行编码。x-轴表示时间。顶部的黑色条形按垂直刻度分为每小时一个区块。这名患者的就诊持续了略超过两个小时。患者信息在黑色时间条之前显示。
尽管像这样的描述性分析研究可能不会直接影响成本或医疗建议,但它们为探索和理解患者护理提供了一个起点,并且通常为启动更具体、可操作的分析方法铺平了道路:

预测未来的诊断和治疗事件
医学中的一个核心问题是如何识别出有发展某种疾病风险的患者。通过识别高风险患者,可以采取措施延缓或阻止疾病的发生,甚至完全预防。这是预测性分析的一种应用——利用来自以往事件的信息来预测未来的情况。有些疾病特别适合做预测研究:充血性心力衰竭、心肌梗死、肺炎和慢性阻塞性肺病就是一些高死亡率、高成本的疾病,它们可以从早期识别高风险患者中受益。
我们不仅关心未来将发生哪些疾病,还希望识别出那些有可能需要高成本治疗的患者,如医院再入院和看病就诊。通过识别这些患者,我们可以采取节省费用的措施,主动降低这些高风险治疗的风险,并且可以奖励那些做得好的医疗机构。
这是一个包含多个未知因素的广泛示例。首先:我们想预测的具体事件(或疾病)是什么?其次:我们将使用哪些数据来进行预测?目前,结构化临床数据(以表格形式组织的数据)是最受欢迎的数据来源;其他可能的数据包括非结构化数据(医学文本)、医学或 X 光影像、生物信号(EEG、EKG)、来自设备的数据,甚至是社交媒体的数据。第三:我们将使用什么机器学习算法?
衡量提供者的质量和绩效
尽管制作美观的可视化或预测代表了医疗分析的性感方面,但还有其他类型的分析同样重要。有时,这归结为良好的老式数字分析。使用医疗措施监测医生和医疗机构的表现就是这种分析技术的一个很好的例子。医疗措施提供了一个机制,使个人能够衡量和比较参与者对基于证据的医疗建议的遵从情况。例如,广泛接受的建议是,糖尿病患者每三个月接受一次由医生进行的足部检查以检测糖尿病足溃疡。
国家赞助的医疗措施可能会指定计算接受治疗的糖尿病患者人数的指南,并确定那些接受适当足部护理的患者的百分比。类似的措施也适用于常见的心脏、肺部和关节疾病等多种疾病。这提供了一种识别提供最高质量护理的提供者的方法,这些建议可以下载供公众消费。我们将在第六章,《医疗质量测量》中讨论具体的医疗措施。
面向患者的疾病治疗
在罕见情况下,医疗分析包括用于实际治疗疾病的医疗技术,而不仅仅是对其进行研究。神经假肢就是一个例子。神经假肢可以定义为使用人造设备增强神经系统功能。神经假肢研究使患有失明或截瘫等残疾的患者能够恢复部分失去的功能。例如,一个瘫痪的患者可能能够通过他们的脑信号移动屏幕上的计算机光标,而不是用手!在这个特定的应用中,获取特定神经元的电活动记录,并使用机器学习模型确定光标在哪个方向移动,根据神经元的发射。类似的分析可以用于视力障碍,或者用于可视化人类正在看到的内容。第二个例子包括在身体中植入设备,以在癫痫发作之前检测到,并主动给予预防药物。显然,通过分析驱动的治疗有无限的可能性。
探索软件
在本节中,我们将下载、安装和探索 Anaconda 和 SQLite,这些是本书中用于 Python 和 SQL 的发行版。
Anaconda
本书中的示例需要使用 Python 编程语言。有很多 Python 发行版可供选择。Anaconda 是一个免费的开源 Python 发行版,专为机器学习设计。它包括 Python 以及超过 1,000 个数据科学 Python 库(例如 NumPy、scikit-learn、pandas),这些库可以在基础 Python 语言上使用。它还包括Jupyter notebook,这是一个交互式的 Python 控制台,我们将在本书中广泛使用。Anaconda 附带的其他工具包括 Spyder IDE(交互式开发环境的简称)和 RStudio。
可以从www.continuum.io/downloads下载 Anaconda。
要下载 Anaconda 的 Python 发行版,请完成以下步骤:
-
请访问前述网站。
-
根据你的操作系统和所需的 Python 版本选择合适的 Python 下载版本。本书使用的是 Anaconda 5.2.0(Windows 的 64 位安装版本,包含 Python 3.6):

-
点击下载。你的浏览器将开始下载文件。下载完成后,点击浏览器中的文件或操作系统文件管理器中的文件。
-
会弹出一个窗口(如下图所示)。点击“下一步>”按钮:

-
继续按照提示操作,包括接受许可协议、选择安装用户、选择文件存储位置以及选择各种选项。
-
Anaconda 将开始安装。由于安装包数量较多,这可能需要一段时间。
-
安装完成后,关闭 Anaconda 窗口。
Anaconda Navigator
现在你已经安装了 Anaconda,你可以通过在 Windows 任务栏中搜索Anaconda Navigator来访问它的功能,或者在 Mac 的应用程序文件夹中找到 Anaconda Navigator。点击图标后,稍等片刻,你将看到如下图所示的界面:

你当前在“首页”选项卡中,这里列出了 Anaconda 中包含的不同应用程序。你可以从此屏幕访问 Jupyter notebook,以及 Spyder IDE。
要查看已安装的软件库,请点击左侧的“环境”选项卡。你可以使用此选项卡下载和升级特定的库,如下图所示:

Jupyter notebook
现在,让我们来探索 Jupyter notebook,这是本书中大部分时间会使用的 Python 编程工具。返回“首页”选项卡,点击 Jupyter 图标中的“启动”按钮。你的默认浏览器中应会打开一个新的标签页,类似于下图所示:

这是 Jupyter 应用的文件标签,你可以通过它浏览电脑的目录来启动新的 Jupyter 笔记本,打开已有的笔记本,或管理你的目录。
让我们创建一个新的 Jupyter 笔记本。定位到控制台右上角的 New 下拉菜单并点击它。在下拉菜单中,点击 Python 3。另一个标签页将打开,显示类似于以下截图的内容:

标记为 In 的框叫做单元格。单元格是 Jupyter 中 Python 编程的基本单位。你在单元格中输入代码,然后点击运行来执行它。看到结果后,你可以创建一个新的单元格并继续你的工作流,如果需要的话,可以在之前的结果基础上进行构建。
让我们尝试一个示例。点击单元格区域,并输入以下几行:
message = 'Hello World!'
print(message)
然后,在顶部工具栏找到播放按钮并点击。你应该会看到紧跟着单元格的 Hello World! 消息。你还会看到文本下方出现一个新的单元格。这就是 Jupyter 的工作方式。
现在,在新的单元格中输入以下内容:
modified_message = message + ' Also, Hello World of Healthcare Analytics!'
print(modified_message)
再次点击播放按钮。你应该会看到第二个单元格下方显示修改后的消息,并且出现了第三个单元格。请注意,第二个单元格能够识别 message 变量的内容,尽管它是在第一个单元格中赋值的。Jupyter 会记住每个会话中输入到控制台的所有命令。要清除内存,你必须关闭并重新启动内核:

现在,让我们结束当前的会话。返回浏览器中的主页标签,点击左上角的 Running 标签。在 Notebooks 菜单下,你应该能看到 Untitled.ipynb 正在运行。点击右侧的关闭按钮,笔记本就会消失。
暂时就到这里,关于 Jupyter 的内容你将在接下来的章节中更深入了解。
Spyder IDE
Spyder IDE 提供了一个完整的 Python 开发环境,包括文本编辑器、变量探索器、IPython 控制台,并且可选择性地提供命令行提示符,具体请参见以下截图:

屏幕的左半部分是编辑器窗口。你将在这里编写 Python 代码。当我们完成脚本编写后,将使用上方工具栏中的绿色播放按钮来运行它们。
屏幕的右半部分横向分为两部分。右上方的窗口,最常用的形式是作为变量探索器(如图所示)。这个窗口列出了当前 Python 环境中每个变量的名称、类型、大小和值(例如,在内存中)。通过点击窗口底部的标签,你还可以将窗口切换为文件浏览器或查看 Python 的帮助文档。
右下角窗口是控制台,显示的是 Python 命令提示符。这对于运行单个 Python 命令非常有用;它还可以用来运行 Python 脚本和执行其他功能。此窗口的第三个选项是之前输入的命令历史记录。
本书中我们不会广泛使用 Spyder;然而,了解它是如何工作的也有助于你以后用于其他项目。
SQLite
医疗数据通常存储在数据库中。为了操作和提取这些数据库中的所需数据,你应该了解 SQL。SQL 是一种语言,根据使用的数据库引擎不同,存在许多变体。我们将使用SQLite,这是一种免费的、公共领域的 SQL 数据库引擎。
要下载 SQLite,请执行以下操作:
-
访问 SQLite 首页(www.sqlite.org)。然后,点击顶部的“Downloads”标签。
-
下载适合你操作系统的预编译二进制文件。你需要下载的是捆绑包文件,而不是 DLL 文件(文件名格式为:
sqlite-tools-{Your OS}-x86-{Version Number}.zip)。 -
使用 shell 或命令提示符,导航到包含
sqlite3.exe程序的目录。 -
在提示符下,输入
sqlite3 test.db并按 Enter。
你现在已经进入 SQLite 程序。稍后我们将使用 SQLite 命令来创建、保存和操作模拟病人数据。SQLite 命令以一个句点开始,后跟一个小写单词,再接命令参数。
要退出 SQLite,输入 .exit 并按 Enter。
命令行工具
所有操作系统,无论是 Windows、MacOS 还是 Linux,都自带一个命令行工具用于输入命令。在 Mac 或 Linux 上,shell 程序接受 bash 命令。在 Windows 上,有一些与 bash 不同的 DOS 命令。对于本书,我们使用的是 Windows PC 和 DOS 命令提示符。在必要时,我们会在文本中提供我们使用的命令及其对应的 bash 命令。
安装文本编辑器
本书中使用的一些数据文件相当大,可能无法通过你计算机自带的标准文本编辑器打开。我们建议使用可下载的源代码编辑器。流行的选择包括 Sublime(适用于 Windows 和 Mac)或 Notepad++(适用于 Windows)。我们在本书中使用了 Notepad++。
总结
现在我们已经介绍了医疗分析的主题,并为本书的其余部分设置了你的计算机,接下来我们可以深入探讨医疗分析的基础知识。在第二章《医疗基础》中,我们将探讨一些医疗分析的基础。
参考文献
Basole RC, Kumar V, Braunstein ML, 等人(2015)。分析与可视化急诊科临床路径遵循情况。纳什维尔,TN:INFORMS 医疗会议,2015 年 7 月 29-31 日。
巴克斯特 WG (1990). "人工神经网络在临床决策中的数据分析应用:急性冠脉堵塞的诊断." 神经计算 2 (4): 480-489。
莱德利 RS, 拉斯特 LB (1959). "医学诊断的推理基础." 科学 130 (3366): 9-21。
米勒 RA, 波普尔 Jr. HE, 麦耶斯 JD (1982). "INTERNIST-1,一款用于普通内科的实验性计算机诊断顾问." 新英格兰医学杂志 307: 468-476。
明斯基 M, 帕珀特 SA (1969). "感知机." 剑桥, MA: 麻省理工学院出版社。
鲁梅尔哈特 DE, 休顿 GE, 威廉姆斯 RJ (1986). "通过反向传播误差学习表示." 自然 323(9): 533-536。
第二章:医疗保健基础
本章主要面向那些在医疗保健领域经验有限的开发人员。通过本章内容,你将能够描述美国医疗保健服务的基本特征,了解与分析相关的美国具体立法,理解医疗数据如何被构建、组织和编码,并且了解医疗保健分析的思维框架。
美国的医疗保健服务
医疗保健行业通过与我们自己、我们的亲人、家庭和朋友的互动,影响着我们每个人。医疗保健行业的高昂费用与当我们亲近的人生病或感到疼痛时所经历的身体、情感和精神创伤是紧密相连的。
在美国,医疗保健系统处于脆弱状态,因为医疗保健支出超过了全国 GDP 的 15%;这一比例远远超过其他发达国家,并预计到 2040 年将至少达到 20%(Braunstein, 2014;Bernaert, 2015)。美国及全球医疗成本的上升可以归因于多个因素。其一是人口结构的变化,老年人口逐渐增多。2011 年,寿命预期(LE)首次超过 80 岁,而 1970 年时为 70 岁(OECD, 2013)。尽管这是一个积极的进展,但老年患者通常更容易生病,因此在医疗系统眼中,这类患者的费用也较高。第二个原因是严重慢性疾病的发病率上升,如肥胖和糖尿病(OECD, 2013),这些慢性病增加了患其他慢性病的风险。患有慢性疾病的患者占据了医疗保健支出的绝大部分(Braunstein, 2014)。第三个原因是激励机制的不对称,这将在接下来的医疗服务提供商报销部分讨论。第四个原因是技术的进步,由于 MRI 成像和 CT 扫描等昂贵设备的成本在所有 OECD 国家中都在上涨(OECD, 2013)。
接下来,我们将讨论一些基本的医疗保健术语以及美国医疗保健的融资方式。
医疗保健行业基础
医疗保健可以大致分为住院护理,即在住院设施(如医院)内提供的护理,和门诊护理(或流动护理),即通常在医生诊所内提供的、当天完成的护理。住院护理通常用于治疗病情已经严重或需要复杂干预的情况,而且费用通常高于门诊护理;因此,医疗保健的一个核心目标是通过强调足够的预防措施,减少住院治疗的比例。
另一种描述医疗保健的方式是通过“医疗服务的阶段”。初级保健提供者(PCPs)通常处理患者的整体健康状况并监督所有器官系统;在许多医疗服务模式中,他们充当“守门人”,将患者引导至二级和三级保健提供者。二级保健指由专门治疗特定疾病或器官系统的医生提供的治疗,如内分泌科医生或心胸外科医生。三级保健通常是在专科医生转诊后提供,通常发生在住院环境下,专门治疗非常特定的病症,常常通过手术进行。
在医疗保健领域,为了提供最佳的患者护理,需要一支由多名专业人员组成的团队,他们各自承担不同的角色。医生、医师助理、护士执业者、护士、案例经理、社会工作者、实验室技术人员和信息技术专业人员等,都是你将在医疗分析领域直接或间接合作的其他人员。
医疗保健融资
一百年前,钱直接从患者流向提供医疗服务的机构。然而,今天医疗保健融资更加复杂,雇主和政府的参与越来越多,且与医生报酬相关的新模式不断出现。在美国,医疗保健融资不再完全是私人的;为了帮助贫困和老年群体,州政府和联邦政府利用从公民那里征收的税款资助医疗补助和医疗保险,这分别是政府资助的支付贫困和老年人医疗保健的方式。一旦钱到达各个第三方(保险公司和/或政府),或者仍然在患者手中,必须通过各种支付模式将钱分配给医生。在以下图表中,我们提供了美国医疗保健系统中资金流动的简化概述。
医疗保健中的许多分析是对越来越重视医生绩效和医疗质量(而非数量)的回应:

按服务收费报销
传统上,医生是通过按服务收费(FFS)支付系统获得报酬的,在这种系统下,医生因每项他们进行的测试或程序而获得补偿,无论患者在测试或程序后是否感觉更好。这种报销方法导致医生面临冲突的激励机制,因为他们需要有效地照顾患者,同时还要谋生。许多人将如今美国过高的医疗开支归咎于 FFS。此外,FFS 报销为每位医生单独支付,医生之间几乎没有协调。如果患者因同一病情看了两个医生,会发生什么情况?在 FFS 报销下,医生可能会要求重复检查,并且会分别得到报销。
基于价值的护理。
FFS 的不足之处促使了美国医疗保健的新愿景——基于价值的护理。在基于价值的报销系统下,医生的报酬基于他们提供的护理质量——这可以通过患者的治疗结果以及每位患者节省的费用来衡量。过度开具不必要检查和手术的激励消失了,患者和医生的共同目标也变得一致。基于价值的护理涵盖了一组医生报酬模式,这些模式根据提供的护理质量奖励医生,每种模式都有其独特之处。这些模式包括责任护理组织(ACOs)、打包支付和以患者为中心的医疗之家(PCMHs)。
本节需要记住的要点是:
-
在美国和大多数其他国家,医疗保健支出正按 GDP 的比例增长。
-
基于价值的护理正在慢慢成为医生报酬的新标准。
医疗保健政策。
医疗改革需要立法者的支持才能成功,幸运的是,它得到了正是这种支持。在本节中,让我们来看一些为患者权益和隐私、电子病历(EMR)的兴起、基于价值的护理以及大数据在医疗中的应用铺平道路的立法,这些都与医疗分析相关。
保护患者隐私和患者权益。
世界上许多国家已通过立法保护患者隐私。在美国,保护患者隐私的立法首次于 1996 年签署成为法律,称为健康保险可携带性与责任法案(HIPAA)。此法案自那时以来已被多次修订和更新。HIPAA 的两个主要组成部分是隐私规则和安全规则。
隐私规则规定了医疗数据可以使用的具体情境。特别是,任何可以用于识别患者的信息(称为受保护的健康信息(PHI))可以自由用于医疗治疗、账单支付或其他特定的医疗运营目的。其他用途的数据需要患者的书面授权。受保护实体是指需要遵守 HIPAA 法律的组织;受保护实体的例子包括护理提供者和保险计划。2013 年,最终总规则扩展了 HIPAA 的管辖范围,包括了受保护实体的业务合作伙伴或独立承包商(如果在美国与客户合作,大多数医疗数据分析专业人员都可归类于此)。因此,如果你在美国处理医疗数据,必须保护患者数据,否则可能面临罚款和/或监禁的风险。
如果你是医疗分析专业人员,应该如何保护你数据中的电子患者健康信息(e-PHI)?《安全法则》回答了这个问题。《安全法则》将保护方法分为三类:行政、物理和技术。具体来说,根据美国卫生与公共服务部的网站,医疗数据科学家应:
“确保其拥有的所有电子健康信息(e-PHI)的机密性、完整性和可用性”;防范“合理预期的威胁”对信息安全的影响,以及不当使用或泄露;并“确保其员工遵守规定”
(美国卫生与公共服务部,2017)。关于保护技术的更具体信息可以在 HHS 网站上找到,并包括以下指南:
-
被覆盖实体和商业合作伙伴应指定一名隐私官员,负责执行 HIPAA,并为有权访问电子健康信息(e-PHI)的员工提供培训计划
-
对包含电子健康信息(e-PHI)硬件和软件的访问应严格控制、规范,并仅限授权人员使用
-
通过开放网络(例如,通过电子邮件)传输的电子健康信息(e-PHI)必须加密
-
被覆盖实体和商业合作伙伴必须报告任何安全泄露事件,告知受影响的个人以及卫生与公共服务部
在美国以外的许多国家(尤其是加拿大和欧洲国家)已经制定了医疗隐私法律。无论你生活在哪个国家,保护患者数据和隐私被认为是医疗分析中的道德实践。
推动电子病历的采用
电子病历(EMRs)与医疗分析一起,被视为应对不断上涨的医疗成本的可能解决方案。在美国,推动电子病历使用的主要立法是健康信息技术促进经济与临床健康法(HITECH 法案),该法案于 2009 年作为美国复苏与再投资法案的一部分通过(Braunstein, 2014)。HITECH 法案向做出以下两项工作的医疗组织提供激励支付:
-
采用“认证”电子健康记录(EHRs)
-
以有意义的方式使用电子健康记录(EHRs)。从 2015 年起,未使用电子健康记录的医疗服务提供者将面临来自其医保报销的处罚
为了让 EHR 获得认证,它必须满足数十项标准。这些标准包括支持临床实践的要求,如允许计算机化的医生医嘱输入并记录关于患者的基本信息和临床信息,如药物清单、过敏史和吸烟状态等。其他标准则侧重于保障医疗信息的隐私与安全,要求提供安全访问、紧急访问以及在一段时间不活跃后自动注销。EHR 还应能够向相关部门提交临床质量度量。相关标准的完整列表可通过www.healthit.gov获取。
仅仅提供经过认证的电子健康记录(EHR)并不足够;为了获得激励支付,提供者必须按照有意义的方式使用 EHR,这一点在有意义使用要求中已有规定。再次强调,存在许多要求,其中一些是强制性的,另一些是可选的。这些要求分布在以下五个领域:
-
改善护理协调
-
减少健康差异
-
让患者及其家庭参与其中
-
改善人群健康和公共卫生
-
确保充分的隐私保护和安全性
受 HITECH 法案的推动,EHR 的兴起将导致前所未有的临床信息量可供后续分析,以期降低成本并改善治疗效果。本章后续部分将更详细地探讨如何创建和格式化这些临床信息。
推广基于价值的护理
患者保护与平价医疗法案 (PPACA),也称为平价医疗法案 (ACA),于 2010 年通过。这是一项庞大的立法,最为人所知的是其旨在减少无保险人群并为大多数公民提供健康保险补贴。然而,其中一些较少被宣传的条款引入了前面章节讨论的新型基于价值的报销模式(即捆绑支付和责任护理组织),并创建了四个最初的基于价值的计划:
-
医院基于价值的采购计划 (HVBP)
-
医院再入院减少计划 (HRRP)
-
医院获得的并发症减少计划 (HAC)
-
价值调整计划 (VM)
这些计划将在第六章,衡量医疗质量中详细讨论。
2015 年医疗保险访问和儿童健康保险计划再授权法案 (MACRA) 启动了质量支付计划,其中包括替代支付模型 (APM) 计划和基于绩效的激励支付系统 (MIPS) 计划。两个计划将在衡量提供者表现章节中详细讨论,它们将美国医疗系统从传统的按服务付费(FFS)报销模式逐步转向基于价值的报销模式。
推动医疗保健领域的分析技术
有一些与推动医疗分析相关的法律倡议。其中最为相关的是我们所有人计划(前身为精准医学计划),该计划于 2015 年实施,旨在到 2022 年收集来自一百万人的健康和基因数据,旨在推进精准医学和为个体量身定制的医疗。
此外,以下三项倡议虽然与分析学无直接关联,但可能间接增加医疗分析研究的资金支持。大脑计划,于 2013 年通过,旨在从根本上提高我们对大脑相关及神经系统疾病(如阿尔茨海默病和帕金森病)的理解。癌症突破 2020,于 2016 年通过,聚焦于寻找癌症疫苗和免疫疗法。2016 年的21 世纪疗法法案简化了食品和药品管理局(FDA)的药品审批流程,以及其他相关规定。
总体而言,过去三十年里讨论的立法为革命性地改变医疗分析的执行方式奠定了基础,并且创造了新的挑战,要求医疗分析来解决这些问题,不仅在美国,也在全球范围内。这些新的报销和融资方式要求我们解决一个问题:如何在已有数据的基础上,提升医疗服务的效率。
现在让我们换个角度,看看临床数据究竟包括什么。
患者数据——从患者到计算机的旅程
临床数据收集过程始于患者开始向医生陈述其病情。这被称为病史,由于它不是由医生直接观察的,而是由患者叙述,因此患者的故事被称为主观信息。相对而言,客观信息来自医生,并且包括医生对患者的观察,这些观察来自体检、实验室测试、影像学检查以及其他诊断程序。主观和客观信息合起来构成了临床记录。
医疗保健中使用了几种类型的临床记录。病史和体格检查(H&P)是最详细和全面的临床记录。通常在门诊医生首次接诊患者时,或患者首次住院时进行。收集患者的所有数据并在医院电脑上输入 H&P 可能需要 1-2 小时。通常,每位医生/每次住院只做一次 H&P。对于后续的门诊就诊,或住院时间持续几天的情况,会编写简短的临床记录。这些称为病程记录或SOAP 记录(SOAP 代表主观、客观、评估和计划)。在这些记录中,重点是自初次 H&P 或上一份病程记录以来发生的事件。
在患者数据出现在你的数据库之前,它经历了漫长的旅程,首先由医生团队解读患者病史。患者的病历与来自不同临床科室(例如实验室、影像学)的其他信息相结合,形成电子健康记录(EHR)。当医院希望将数据提供给第三方以进行进一步分析时,通常会将数据以数据库格式发布到云端。
一旦数据被捕捉到数据库系统中,数据分析专家可以使用各种工具来可视化、透视、分析并构建预测模型:

在接下来的子章节中,我们将描述这两种临床记录的关键方面。
病史和体格检查(H&P)
如前所述,病史和体格检查是患者可获得的最全面的文档类型,通常在患者入院时和/或见到新的门诊医生时进行。H&P 临床记录的标准部分将在以下章节中讨论。
元数据和主诉
元数据包括关于患者就诊的基本信息,例如患者的姓名、出生日期、入院日期/时间以及接收医院和主治医生的名称。
主诉是患者就诊/住院的原因,通常是患者自己的话。例如:“我感到胸部不适。”这个主诉可能会或不会被病史记录员翻译成相应的医学术语,例如“胸痛”。
当前疾病史(HPI)
HPI 包括与主诉相关的详细信息。这个部分通常分为以下两个段落:
第一段提供了关于主要投诉的即时细节,通常使用从患者那里获得的信息。第一句通常会提供关于患者的重要人口统计学信息以及任何相关的过去病史,除此之外还包括主要投诉。例如:
“史密斯先生是一位 53 岁的白人男性,有高血压、高脂血症、糖尿病和吸烟史,现因胸痛前来急诊。”
关于剩余部分,第一段病史(HPI)通常包含列出的七个标准元素。这七个元素通常假定主要投诉为某种类型的疼痛;有些主要投诉(例如闭经)需要不同的问题集。这七个元素在下表中进行了总结:
| 病史元素 | 对应问题 | 示例回答 |
|---|---|---|
| 位置 | 疼痛在哪里? | 疼痛位于左侧,并辐射至左臂和背部。 |
| 疼痛性质 | 疼痛感觉如何? | 患者报告为剧烈、刺痛的疼痛。 |
| 严重程度 | 在 1 到 10 的范围内,疼痛有多严重? | 疼痛严重程度为 8/10。 |
| 时间 | 起始:疼痛何时开始?频率:疼痛发生的频率如何?持续时间:每次疼痛持续多久? | 当前的发作开始于半小时前,过去几个月内在运动后发生,发作持续时间为 15-20 分钟。 |
| 加重因素 | 什么因素使疼痛加剧? | 运动会加重疼痛。 |
| 缓解因素 | 什么能缓解疼痛? | 休息和减重可以缓解疼痛。 |
| 相关症状 | 当疼痛出现时,是否有其他症状伴随? | 患者报告伴有呼吸困难的症状。 |
第二段应包含患者为其病症已经接受的所有医疗治疗。典型问题包括:患者是否已经看过医生或住院治疗过?进行过哪些实验室检查和测试?患者的相关病情控制得如何?曾尝试过哪些治疗?是否有 X 光片的副本?
过去病史
本部分列出了所有影响患者的当前和过去的医疗状况,包括但不限于住院(无论是因医疗、外科还是精神原因)。
药物
本节提供当前的处方药和非处方药(OTC)信息,通常包括以下内容:药物名称、剂量、给药途径和使用频率。列出的每种药物应与患者过去病史中的某一现有病症相对应。给药途径和频率通常使用缩写形式;请参考下表获取常见缩写的列表。
家族史
家族史包括患者两代以内家庭成员的疾病史,重点是慢性疾病以及与主诉和受影响器官系统相关的疾病。
社会历史
社会历史提供了 HPI 中未获取的社会和风险因素信息。此部分包括之前未提及的人口统计因素、职业(如适用,涉及有害物质的职业暴露)、社会支持(婚姻、子女、依赖者)以及物质使用/滥用(烟草、酒精、娱乐/非法药物)。
过敏
过敏部分通常包括患者对某些物质的过敏反应,这些物质包括药物,以及相应的过敏反应。如果患者没有已知的药物过敏,通常会用缩写 NKDA 表示。
系统回顾
系统回顾(ROS)作为在获取其他历史信息后,最后筛查显著症状的一项检查。在此部分,医生会询问患者是否出现与不同功能器官系统相关的症状(例如,胃肠、心血管和呼吸系统)。重点放在与主要症状相关的器官系统和症状上。可能涉及多达 14 个不同器官系统的症状。
体格检查
医生继续检查患者并在此部分记录检查结果。描述通常从患者的总体健康状况和外貌开始,然后是相关的生命体征(参见表格获取详细信息),之后检查头部、眼睛、耳朵、鼻子和喉咙(HEENT),并继续检查身体的具体器官/器官系统。
额外的客观数据(实验室测试、影像学检查和其他诊断测试)
体格检查标志着所谓客观数据的开始,即由医生观察、解释并记录的关于患者的数据。这与主观数据相对,主观数据是患者直接提供给医生的信息,包括患者病史。在体格检查后,会提供关于患者的所有其他客观数据。这些数据包括任何实验室检查结果、如适用的影像学研究结果以及可能进行的与当前疾病相关的其他检查。常见的影像学检查包括X 光(XR)、计算机断层扫描(CT)以及磁共振成像(MRI)扫描,针对感兴趣的身体区域。
评估与计划
这是 H&P 的最后部分。在评估部分,医生整合前述主观和客观数据,简明总结主诉,并结合病史、体检和额外检查的显著发现。医生列出病人病情的最可能原因,按每个不同的症状/发现进行项目化列出。在计划部分,医生讨论治疗病人的蓝图,同样是按项目形式列出。
进展(SOAP)临床记录
如前所述,SOAP 记录通常是每天为住院病人完成的,并包括其缩写中的每个字母对应的一个部分:主观、客观、评估和计划(SOAP)。主观部分侧重于病人当前或前一晚出现的新症状。客观部分包含前一天的日常体检、专注的体检结果以及实验室、影像学检查和检测结果。评估和计划类似于病历记录中的 H&P 部分,在更新时会考虑到当天所有的事件。
在病历文档的记录过程中,关于病人的有价值信息已经被收集并录入到电子病历(EMR)中。然而,在数据汇总之前,它通常会与临床编码集进行整合。我们将在下一部分讨论临床编码集。
标准化临床编码集
从哲学角度思考,每一个具有重要性的已知对象都有一个名字。你用来阅读这些文字的器官被称为眼睛。文字被写在称为页面的纸张上。为了翻阅这些页面,你使用你的双手。这些都是我们为便于识别而命名的对象。
在医疗健康领域,重要的实体——例如疾病、手术、实验室检测、药物、症状、细菌种类等,也都有其名称和身份。例如,心脏瓣膜未能有效地将血液泵送到全身被称为心力衰竭。ACE 抑制剂是一类用于治疗心力衰竭的药物。
然而,问题出现在当医疗行业的工作人员将同一实体与不同的身份关联时。例如,一位医生可能将“心力衰竭”称为“充血性心力衰竭”,而另一位医生可能称之为“CHF”。此外,还存在不同的具体化层次:第三位医生可能称其为“收缩性心力衰竭”,以表示这一功能障碍发生在心脏跳动的收缩期。在医学中,准确性和特异性至关重要。那么,我们如何确保所有医疗团队成员讨论和思考的是同一件事呢?答案就在于临床编码。
临床代码可以看作是医疗概念的唯一标识。每个代码通常由一对对象组成:一个字母数字代码和描述该代码所代表实体的文字。例如,在 ICD10-CM 编码系统中,代码 I50.9 代表“未指定的心力衰竭”。当心力衰竭的诊断更为具体时,还有更多更详细的代码。
世界上可能存在成千上万种不同的编码系统,其中许多仅在它们被构思出来的特定医疗机构中使用。幸运的是,为了减少混乱并促进互操作性,存在一些被视为国家/国际标准的知名编码系统。一些更重要的标准化编码系统包括用于医疗诊断的国际疾病分类(ICD)、用于医疗程序的当前程序术语(CPT)、用于实验室测试的逻辑观察标识符名称和代码(LOINC)、用于药物治疗的国家药品编码(NDC),以及用于所有这些及更多内容的医学系统化命名法(SNOMED)。在本节中,我们将更详细地探讨这些编码系统。
国际疾病分类(ICD)
疾病和病症通常使用 ICD 编码系统进行编码。ICD 始于 1899 年,由世界卫生组织(WHO)每十年修订一次并维护。截至 2016 年,第十次修订版(ICD-10)是最新的,并包含超过 68,000 个独特的诊断代码,超过任何先前的修订版。
ICD-10 代码最多可能包含八个字母数字字符。前三个字符表示主要的疾病类别;例如,“N18”指定慢性肾脏病。接下来的字符后跟一个句点,然后是其余字符,这些字符可以提供大量临床细节(Braunstein,2014)。例如,代码“C50.211”指定“右侧女性乳房上内象限的恶性肿瘤”。凭借其精确性,ICD-10 促进了医疗保健中分析应用的实施。
当前程序术语(CPT)
医疗、外科、诊断和治疗程序使用 CPT 编码系统进行编码。CPT 由美国医学会(AMA)开发,CPT 代码由四个数字字符组成,后跟一个字母数字字符。常用的 CPT 代码包括门诊就诊、外科手术、放射学检查、麻醉程序、病史和体检以及新兴技术的代码。与 ICD 不同,CPT 不是一个分层编码系统。然而,某些概念根据不同因素(如就诊时长(门诊就诊)或切除的组织量(外科手术))有多个代码。
逻辑观察标识符名称和代码(LOINC)
实验室测试和观察结果使用 LOINC 编码系统进行编码。该系统由 Regenstrief 研究所编写和维护,共有超过 70,000 个代码,每个代码是一个六位数字,最后一位数字通过连字符与其他数字分隔。像 CPT 代码一样,特定类型的实验室测试(例如,白细胞计数(WBC))通常有多个不同的代码,具体取决于样本采集时间、测量单位、测量方法等因素。虽然每个代码都包含大量信息,但当没有所有相关信息时,寻找如 WBC 计数等实验室测试的代码可能会成为一个问题。
国家药品编码(NDC)
NDC 由美国 FDA 维护。每个代码由 10 位数字组成,并且包含三个子组件:
-
标签商组件,用于标识药物的制造商/分销商
-
产品组件,用于标识标签商提供的实际药物,包括剂量、服用方式和制剂
-
包装代码,用于标识特定的包装形状和大小
将这三个子组件合在一起,可以唯一地标识任何由 FDA 批准的药物。
医学临床术语系统化命名(SNOMED-CT)
SNOMED-CT 是一个庞大的编码系统,能够唯一标识超过 300,000 个临床概念。这些概念可能包括疾病、手术、实验室、药物、器官、病原体、感染、症状、临床发现等。此外,SNOMED-CT 定义了超过 130 万个这些概念之间的关系。SNOMED-CT 由美国国立卫生研究院(NIH)维护,是一个更大的编码系统 SNOMED 的子集,后者包括一些与临床实践无关的概念。NIH 有一个叫做 MetaMap 的软件程序(metamap.nlm.nih.gov/),它可以用来标记文本中出现的临床概念,使其在医疗领域的自然语言处理上非常有用。
尽管编码系统不能唯一标识所有临床概念的所有变化和细微差别,但它们非常接近,并且通过这样做,使得医学中的某些活动(特别是账单和分析)变得更加容易。在第七章《医疗保健预测模型》中,我们将使用一些编码系统来构建医疗保健的预测模型。
现在我们已经讲解了医疗保健的基本概念,在接下来的章节中,我们将介绍专门用于思考医疗保健分析的框架。
解析医疗保健分析
所以你决定进入分析领域,并且知道你想专注于医疗行业。然而,这几乎没有缩小问题的范围,因为医疗领域有成百上千个正在用机器学习和其他分析工具解决的开放问题。如果你曾经在 Google 或 PubMed 中搜索过“医疗中的机器学习”这几个字,你可能已经发现,医疗领域中机器学习应用案例的海洋有多广泛。在学术界,出版物关注的问题范围从预测老年人痴呆症的发生到预测六个月内心脏病发作的发生,再到预测患者最能响应的抗抑郁药。你如何选择专注解决的问题呢?本节将帮助你解答这个问题。选择合适的问题解决是医疗分析的第一步。
在医疗领域,要解决的问题可以分为四类:
-
群体
-
医疗任务
-
数据格式
-
疾病
我们将在本节中回顾每一个组成部分。
群体
不幸的是,研究无法涵盖地球上每一位患者,机器学习模型也不例外。在医疗领域,患者群体是构成患者组的依据,因此,这些患者的数据信息和疾病特征——是同质化的。患者群体的例子包括住院患者、门诊患者、急诊室患者、儿童、成年人以及美国公民。从地理角度来看,群体甚至可以在州、城市或地方层面进行定义。
如果你尝试跨不同的群体进行建模,会发生什么呢?不同群体的数据几乎从不重叠。首先,可能很难在不同群体之间收集相同的特征数据。某些数据可能根本不会为某些群体收集。例如,如果你试图将住院患者和门诊患者的数据结合起来,你将无法获取门诊患者的每小时血压读数或摄入/排放测量数据。此外,另一个问题是,不同群体的数据很可能来自不同的来源,你可能已经知道,不同数据源共享许多共同特征的概率非常低。你如何基于没有相同特征的患者构建模型呢?即使有一个共享的实验室测试数据,例如,实验室数量的测量方式和单位的差异,使得产生一个同质且一致的数据集几乎不可能。
医疗任务
在医疗实践中,患者的评估和治疗可以细分为不同的认知子任务。每一项任务都有可能通过使用分析工具得到辅助。筛查、诊断、预后评估、结果测量以及对治疗的反应,都是这些基本任务的一部分,我们将依次讨论每一项。
筛查
筛查可以定义为在症状和体征出现之前识别患者的疾病。这一点很重要,因为在许多疾病中,特别是慢性疾病,早期发现与早期治疗、较好的结果以及降低医疗成本是相吻合的。
对某些疾病的筛查比对其他疾病的筛查具有更大的潜在利益。为了使疾病筛查有价值,必须满足以下几个条件(Martin et al.,2005):
-
在确定疾病时,结果必须是可改变的
-
筛查技术应具备成本效益
-
测试应具有较高的准确性(请参见第三章,机器学习基础,了解衡量医疗测试准确性的方法)
-
这种疾病应对人口造成较大负担
一个流行的筛查问题及其解决方案的例子是使用宫颈抹片筛查宫颈癌;建议女性在大部分生命中每 1-3 年接受一次这种具有成本效益的检查。肺癌筛查是一个尚未找到理想解决方案的问题;虽然使用 X 光筛查肺癌可能准确,并且在某些情况下可能导致更早的发现,但 X 光检查费用高,并且会让患者暴露于辐射中,而且没有强有力的证据表明早期发现会影响预后或结果(Martin et al.,2005)。越来越多的机器学习模型被开发出来,取代传统医学测试用于筛查癌症、心脏病和中风等疾病(Esfandiari et al.,2014)。
诊断
诊断可以定义为识别个体的疾病。与筛查不同,诊断可以发生在疾病的任何阶段。几乎每种疾病都需要诊断,因为它决定了如何治疗体征或症状(以及潜在的疾病)。例外情况是当某些疾病没有有效的治疗方法,或者当区分不同的疾病对治疗没有影响时。
机器学习在诊断问题中的一个常见应用是识别在面对神秘症状(如腹痛)时潜在的基础疾病原因。相比之下,建立一个机器学习模型来区分不同类型的精神性人格障碍可能效果有限,因为人格障碍很难有效治疗。
结果/预后
如本章前面讨论的,医疗健康主要关注以更低的成本产生更好的结果。通常,我们试图直接确定哪些患者处于较高的风险,而不一定关注他们体征和症状的具体原因。机器学习解决方案的常见应用包括预测哪些患者可能会再次住院、哪些患者可能会死亡、哪些患者可能会从急诊室被收治住院。正如我们将在第六章《医疗质量衡量》中看到的,许多这样的结果都被政府和医疗组织积极监控,并且在某些情况下,政府甚至提供财政激励来改善特定的结果。
通常,我们不是将结果分为两个类别(例如,再入院与非再入院),而是可以尝试根据患者疾病的特征来量化其在特定时间段内的生存几率。例如,对于癌症和心力衰竭患者,您可以尝试预测患者可能生存多少年。这被称为预后,也是医疗健康领域中一个受欢迎的机器学习问题。
治疗反应
在医疗健康领域,疾病通常有多种治疗方法,预测患者将对哪种治疗产生反应本身就是一个问题。例如,癌症患者可能需要接受不同的化疗方案,而抑郁症患者则有数十种药物治疗可供选择。虽然这仍然是一个处于初级阶段的机器学习问题,但它正在逐渐流行,并且被称为个性化医学。
数据格式
在医疗健康领域,机器学习的应用场景也有所不同,这取决于可用数据的格式。数据格式通常决定了可以使用哪些方法和算法来解决问题,因此它在确定应用场景时发挥着重要作用。
结构化数据
当我们想到机器学习时,通常会认为数据是结构化的。结构化数据是可以组织成行和列并且具有离散值的数据。很多电子健康记录中的患者数据可能已经存储为或被转换为这种格式。在医疗健康领域,个别患者或就诊记录通常构成行(或观察),而患者/就诊记录的各种特征(例如,人口统计变量、临床特征、实验室观察)则构成列。这样的格式特别有利于使用各种算法进行机器学习分析。
非结构化数据
不幸的是,电子健康记录(EHR)中的许多数据(如临床记录中的数据)是自由格式的文本;这就是所谓的非结构化数据。作为医疗服务的一部分生成的医生笔记,提供了有关患者和住院进展的广泛信息。根据诊断的复杂性,放射学报告、病理报告和其他诊断记录也会包含非结构化信息。虽然非结构化数据能够传达更多关于患者的广泛且有价值的信息,但分析这些数据比分析结构化数据要更具挑战性。
图像学
在某些专业领域,如放射学和病理学,数据是通过疾病的照片和图像进行收集的,使用的图像可以是病变的照片、病理切片或 X 光图像。一个新兴的领域是自动化分析这些图像数据,利用这些图像进行筛查、诊断和评估各种疾病的预后,包括良性和恶性癌症、心脏病和中风。我们将在本书的最后一章讨论这一领域的例子。
其他数据格式
电生理信号采集是医疗领域的另一种数据形式;采集和分析这些信号,无论是癫痫患者的脑电图(EEG)信号,还是心脏病患者的心电图(EKG)信号,对于疾病的诊断和预后测量具有重要价值。2014 年,知名的数据科学竞赛网站 Kaggle 为能够最有效预测癫痫患者癫痫发作的团队提供了 1 万美元的奖金,该预测是基于脑电图数据的。
疾病
医疗行业中,使用案例的第四种变化方式是根据疾病来分类。成千上万的医学疾病正在积极进行研究,每一种疾病都代表了机器学习模型的潜在目标。然而,在机器学习中,并非所有的疾病都具有相同的价值;一些疾病提供的潜在回报和机会远大于其他疾病。
急性病与慢性病
在医疗领域,疾病通常被分类为急性病或慢性病(Braunstein,2014)。这两类疾病都是预测建模的重要目标。急性疾病的特点是突然发作,通常是自限性的,患者经过适当治疗后通常能完全恢复。此外,急性病的风险因素通常不由患者的行为决定。急性疾病的例子包括流感、肾结石和阑尾炎。
慢性病相比之下通常有渐进的发病过程,并且会伴随病人的一生。它们受到病人行为的影响,如吸烟和肥胖,也受遗传因素的影响。慢性病的例子包括高血压、动脉硬化、糖尿病和慢性肾病。慢性病尤其危险,因为它们往往是互相关联的,并且会引发其他严重的慢性和急性疾病。慢性病也对社会造成了巨大的经济负担;每年花费数十亿美元用于预防和治疗常见的慢性病。
急性-慢性病在医疗健康预测建模中尤其常见。这些是由慢性病引发的急性、突发性疾病。例如,脑卒中和心肌梗死是由慢性病高血压和糖尿病引起的急性病症。急性-慢性病建模之所以受到青睐,是因为它可以让我们将人群筛选为一个高风险群体,这个群体拥有相应的慢性病,从而提高预测模型的效果。例如,如果你试图预测充血性心力衰竭(CHF)的发生,一个有用的起点是高血压患者,因为高血压是一个主要的风险因素。这会导致一个具有更高真实阳性比例的模型,而不是随便从人群中抽取样本。换句话说,如果我们试图预测 CHF 的发生,包含健康的 20 岁男性就不太有用了。
癌症
有几个原因使得癌症的预测建模成为一个重要的应用场景。首先,癌症是仅次于心脏病的第二大死亡原因。癌症的隐匿性发病过程和发展过程使得癌症的诊断往往出乎意料并且令人震惊。没有人会否认我们应该动用所有手段来对抗癌症,而这其中就包括机器学习方法。
其次,在癌症机器学习领域,有许多适合通过机器学习来解决的使用场景。例如,给定一个健康的病人,这个病人患上某种特定类型癌症的可能性有多大?如果一个病人刚刚被诊断为癌症,我们能否以低成本预测癌症是良性还是恶性?这个病人预期能存活多久?五年后他们可能还活着吗?十年后呢?该病人最有可能对哪种化疗/放疗方案产生反应?一旦癌症成功治愈,复发的几率是多少?像这些问题通常需要数学解答,而这些答案可能超出单个医生或甚至多个医生的推理能力。
其他疾病
当然,还有其他疾病也能从预测建模中受益。另一个要记住的点是,一些对社会特别有负担的疾病(例如哮喘和慢性肾病)对行政人员特别感兴趣,并且正在获得国家、州和地方公共机构以及私人企业的积极资助和研究。
汇总所有内容 – 指定一个使用案例
现在我们已经看到了机器学习问题在医疗领域可能如何变化,指定问题变得更加容易。一旦选择了一个人群、一个医疗任务、一个结果指标和疾病,你就可以用合理的具体性来制定一个机器学习问题。我们在讨论中没有包括算法的选择,因为从技术上讲,它与正在解决的问题是分开的,而且许多问题是通过使用多个算法来解决的。我们将在第三章和第七章中详细讨论具体的机器学习算法,这将为你选择算法提供一些背景知识。
以下是一些可以通过前述信息指定的使用案例:
“我想预测哪些健康的老年人可能会在未来五年内被诊断为阿尔茨海默病。”
“我们将建立一个模型,分析痣的图像,并预测这些痣是良性还是恶性。”
“我们能预测那些因哮喘到急诊室就诊的儿童患者是否会被送入医院或出院回家吗?”
总结
在第一章,医疗分析简介中,我们介绍了医疗、数学和计算机科学这三者的医疗分析三合一。在本章中,我们已经探讨了一些基础的医疗话题。在第三章,机器学习基础中,我们将探讨一些支撑医疗分析的数学和机器学习概念。
参考文献与进一步阅读
Bernaert, Arnaud (2015). "五个你不能忽视的全球健康趋势." UPS Longitudes. 2015 年 4 月 13 日 longitudes.ups.com/five-global-health-trends-you-cant-ignore/。
Braunstein, Mark (2014). 当代健康信息学. 芝加哥,伊利诺伊州:AHIMA 出版社。
Esfandiari N, Babavalian MR, Moghadam A-ME, Tabar VK (2014) 医学中的知识发现:当前问题与未来趋势. 专家系统应用 41(9): 4434–4463。
Martin, GJ (2005). "疾病筛查与预防。" 在 Kasper DL, Braunwald E, Fauci AS, Hauser SL, Longo DL, Jameson JL 主编的 哈里森内科学原理,第 16 版。纽约,纽约州:麦格劳-希尔出版公司。
经合组织(2013 年),《2013 年健康一瞥:经合组织指标》,经合组织出版。 dx.doi.org/10.1787/health_glance-2013-en。
史密斯,罗伯特·C(1996 年)。《病人的故事》。波士顿,马萨诸塞州:小布朗公司。
美国卫生与公众服务部(2017 年)。《专业人员的 HIPAA》。华盛顿特区:民权办公室。
第三章:机器学习基础
本章介绍了医疗分析和机器学习背后的数学基础。该内容主要面向那些对进行医疗分析所需的数学知识了解较少的医疗专业人员。通过本章的学习,您将熟悉以下内容:
-
医学决策制定范式
-
基本的机器学习流程
医学决策制定的模型框架
一个鲜为人知的事实是,除了必须完成的基础科学课程和临床轮转外,医生在培训期间还会学习生物统计学和医学决策制定课程。在这些课程中,未来的医生学习一些数学和统计学知识,帮助他们在整理不同的症状、体征和检查结果时做出诊断和治疗计划。许多医生已经被无尽的医学事实和知识淹没,他们对这些课程不以为意。然而,无论是通过这些课程还是通过自身的经验,医生在日常实践中使用的推理方法与一些常见的机器学习算法背后的数学原理非常相似。在这一部分中,我们将深入探讨这一论断,看看一些常见的医学决策制定框架,并将它们与机器学习方法进行比较。
类树推理
我们都熟悉类树推理;它涉及在遇到不同的决策点时分支到各种可能的行动。这里我们将更深入地看一下类树推理,并研究其机器学习对应物:决策树和随机森林。
使用算法和树进行分类推理
在一种医学决策制定范式中,临床问题可以通过树或算法来处理。在这里,算法并不是指计算机科学中的“机器学习算法”;它可以被看作是一个有结构、有序的规则集合,用于做出决策。在这种推理方式中,树的根代表患者接诊的开始。当医生通过提问获取更多信息时,他们会到达不同的分支或决策点,医生可以选择不同的路径继续前进。这些路径代表不同的临床测试或替代的提问方向。医生会反复做出决策,并选择下一个分支,直到到达一个终端节点,此时没有更多的分支。终端节点代表明确的诊断或治疗计划。
这里有一个关于体重和肥胖管理的临床管理算法示例(来源:国家心脏、肺和血液研究所,2010 年)。每个决策点(其中大多数是二元的)用菱形表示,而管理计划则用矩形表示。
例如,假设我们有一位女性患者,测量了几项临床变量:BMI = 27,腰围 = 90 厘米,心脏风险因素数 = 3。在节点#1 开始,我们从节点#2 直接跳到节点#4,因为 BMI > 25。在节点#5 时,答案再次是“是”。在节点#7 时,答案依然是“是”,这将引导我们到节点#8 中列出的管理计划:

下面是另一个结合了诊断和治疗的算法示例(Haggstrom, 2014; Kirk et al., 2014)。在这个关于未知位置妊娠的诊断/治疗算法中,对于一位没有疼痛的血流动力学稳定患者(即心血管功能稳定的患者),会在就医后 0 小时和 48 小时分别抽取血清 hCG。根据结果,会给出几种可能的诊断,并相应提供管理计划。
请注意,在临床中,这些树可能是错误的;这些情况被称为预测错误。构建任何树的目标是选择最佳的变量/切点,以最小化错误:

算法具有许多优点。首先,它们将人类诊断推理建模为一系列层次化的决策或判断。此外,它们的目标是通过强迫护理人员在每个决策点提供二元答案来消除不确定性。研究表明,算法可以改善医疗实践中的标准化护理,并且如今已广泛应用于许多医疗条件,不仅在门诊/住院治疗中,而且在急救医疗技术员(EMTs)到达医院之前也在使用。
然而,算法往往过于简化,并未考虑到医学症状、检查结果或测试结果可能无法提供 100%确定性的事实。当需要权衡多项证据以做出决策时,算法显得不足。
相应的机器学习算法——决策树和随机森林
在上述示意图中,您可能已经注意到,示例树可能使用了主观确定的切点来决定应该走哪条路径。例如,钻石图标#5 使用了 BMI 的 25 作为切点,而钻石图标#7 使用了 30 的 BMI 切点——都是很漂亮的整数!在决策分析领域,树通常是基于人类推理和讨论构建的。如果我们能够客观地确定最佳的变量(以及应在哪些切点进行切割),以最小化算法的误差,该怎么办呢?
这正是我们在使用机器学习算法训练正式决策树时所做的。决策树在 1990 年代发展起来,采用了信息理论的原则,优化了树的分支变量/节点,以最大化分类准确性。训练决策树的最常见且简单的算法采用了所谓的贪心方法。从第一个节点开始,我们基于每个变量使用不同的切点对数据的训练集进行划分。每次划分后,我们计算由划分所产生的熵或信息增益。无需担心如何计算这些量,只需知道它们衡量的是通过划分获得了多少信息,这与划分的均匀程度相关。例如,使用前面展示的 PUL 算法,一个结果为八个正常宫内妊娠和七个异位妊娠的划分,比一个结果为 15 个正常宫内妊娠和零个异位妊娠的划分更受青睐。一旦确定了最佳划分的变量和切点,我们就继续执行,并使用剩余的变量重复这一方法。为了防止模型对训练数据过拟合,当达到某些标准时,我们停止划分树,或者我们也可以训练一个包含多个节点的大树,然后去除(剪枝)一些节点。
决策树有一些局限性。首先,决策树在每一步都必须基于单一变量线性地划分决策空间。另一个问题是决策树容易发生过拟合。由于这些问题,决策树通常在最小化误差方面与大多数最先进的机器学习算法竞争力较弱。然而,随机森林,它基本上是由去相关的决策树组成的集成方法,目前是医学领域中最流行和最准确的机器学习方法之一。我们将在本书的第七章,医疗领域的预测模型构建中介绍决策树和随机森林。
概率推理和贝叶斯定理
另一种更为数学化的方式来接近患者是通过初始化患者的疾病基线概率,并根据每次新发现的临床信息更新该疾病的概率。这个概率是通过贝叶斯定理来更新的。
使用贝叶斯定理计算临床概率
简而言之,贝叶斯定理允许根据疾病的预试概率、测试结果和测试的 2 x 2 列联表来计算疾病的后验概率。在这种背景下,“测试”结果不必是实验室测试;它可以是通过病史和体检确认的任何临床发现的有无。例如,胸痛的有无、胸痛是否位于胸骨后、运动压力测试的结果和肌钙蛋白的结果都可以作为临床发现,基于这些可以计算后验概率。尽管贝叶斯定理可以扩展到包括连续值结果,但在计算概率之前,将测试结果二值化通常更为方便。
为了说明贝叶斯定理的应用,假设你是一位初级保健医生,一位 55 岁的患者走进来并说:“我胸口疼。”当你听到“胸痛”这两个字时,你首先担心的致命疾病是心肌梗死。你可以问:“这个患者发生心肌梗死的可能性有多大?”在这种情况下,胸痛的有无就是测试(这个患者是阳性),而心肌梗死的有无是我们试图计算的内容。
计算基础的心肌梗死概率
要计算胸痛患者发生心肌梗死(MI)的概率,我们需要知道三件事:
-
预试概率
-
针对该疾病的临床发现的 2 x 2 列联表(在此例中是心肌梗死)
-
该测试的结果(在本例中,患者有胸痛症状)
因为患者的其他发现尚未明确,我们可以将预试概率设为该人群中心肌梗死的基础患病率。假设在你诊所所在地区,对于 55 岁的人群,每年心肌梗死的基础患病率为 5%。因此,这位患者的心肌梗死预试概率为 5%。我们稍后会看到,这位患者的后验疾病概率是预试概率与阳性胸痛似然比(LR+)的乘积。为了得到 LR+,我们需要 2 x 2 列联表。
胸痛与心肌梗死的 2 x 2 列联表
假设以下表格是 400 位就诊患者中胸痛与心肌梗死的分布情况:
| 心肌梗死存在(D+) | 心肌梗死不存在(D-) | 总计 | |
|---|---|---|---|
| 胸痛存在(T+) | 15(TP) | 100(FP) | 115 |
| 胸痛不存在(T-) | 5(FN) | 280(TN) | 285 |
| 总计 | 20 | 380 | 400 |
解读列联表并计算灵敏度和特异度
在上表中,有四个数字单元,分别标记为TP、FP、FN和TN。这些缩写分别代表真阳性、假阳性、假阴性和真阴性。第一个词(真/假)表示测试结果是否与通过金标准测量的疾病存在匹配。第二个词(阳性/阴性)表示测试结果是什么。真阳性和真阴性是期望的结果;这意味着测试结果是正确的,且这些数字越高,测试效果越好。另一方面,假阳性和假阴性是不可取的结果。
从真阳性/假阳性/真阴性/假阴性中可以计算出两个重要的量,即敏感性和特异性。敏感性是衡量测试在检测疾病方面的能力。它表示阳性测试结果占患病总人数的比率:

另一方面,特异性是衡量测试识别没有疾病患者能力的指标。它的计算公式如下:

这些概念最初可能会让人感到困惑,因此在你习惯它们之前,可能需要一些时间和多次迭代,但敏感性和特异性是生物统计学和机器学习中的重要概念。
计算胸痛的似然比(+和-)
似然比是衡量测试如何改变患病可能性的指标。它通常分为两个量:阳性测试的似然比(LR+)和阴性测试的似然比(LR-)。
依据阳性胸痛结果,心肌梗死的似然比由以下公式给出:



根据阴性胸痛结果,心肌梗死的似然比由以下公式给出:



由于患者有胸痛症状,因此在这种情况下仅适用 LR+。为了得到 LR+,我们使用适当的数字:
LR+ = (TP/(TP + FN)) / (FP/(FP + TN))
= (15/(15 + 5)) / (100/(100 + 280))
= 0.750 / 0.263
= 2.85
在已知胸痛症状的情况下,计算心肌梗死的后测概率
现在我们得到了 LR+,我们将其乘以前测概率,以得到后测概率:
Post-Test Probability = 0.05 x 2.85 = 14.3%.
这种用于诊断和患者管理的方法看起来非常吸引人;能够计算出疾病的精确概率似乎消除了诊断中的许多问题!不幸的是,贝叶斯定理在临床实践中由于许多原因而无法应用。首先,每一步都需要大量的数据来更新概率。没有任何一位医生或数据库能访问到所有的应急表格,以便根据患者的每一个历史元素或实验室检查结果更新贝叶斯定理。其次,这种概率推理方法对于人类来说是不自然的。我们讨论的其他技术更有利于人类大脑的运作。第三,虽然该模型对于单一疾病有效,但当存在多种疾病和共病时,它的效果不好。最后,也是最重要的,贝叶斯定理所依赖的条件独立性和完备性、互斥性假设在临床世界中并不成立。现实情况是,症状和体征并非完全相互独立;某一症状的出现与否会影响其他许多症状的出现。综上所述,这些事实使得贝叶斯定理计算出的概率在大多数情况下是不准确的,甚至是误导性的,即使在成功计算后也是如此。尽管如此,贝叶斯定理在医学中对于许多子问题依然重要,特别是当有充足证据时(例如,使用胸痛特征来计算患者历史中的心肌梗死概率)。
对应的机器学习算法——朴素贝叶斯分类器
在前面的示例中,我们向您展示了如何根据预试验概率、似然性和测试结果计算后试验概率。被称为朴素贝叶斯分类器的机器学习算法会依次对给定观测值的每个特征进行此操作。例如,在前面的例子中,后试验概率是 14.3%。假设患者现在进行了肌钙蛋白检查,并且结果升高了。14.3%现在成为预试验概率,并根据肌钙蛋白和心肌梗死的应急表格计算新的后试验概率,这些应急表格来自训练数据。这一过程会一直持续,直到所有特征都被耗尽。再次强调,关键假设是每个特征与其他所有特征独立。对于分类器,后试验概率最高的类别(结果)会被分配给该观测值。
朴素贝叶斯分类器在特定应用领域中非常受欢迎。它的优点包括高可解释性、对缺失数据的鲁棒性以及训练和预测的简易性/快速性。然而,它的假设使得该模型无法与更先进的算法竞争。
判别表和加权和方法
我们将讨论的第三种医学决策模式是判别表及其与线性回归和逻辑回归的相似性。
评分表
使用评分表的原因之一是贝叶斯定理的另一个缺点:考虑每次仅考虑一个发现的顺序性质。有时,同时考虑许多因素以及疾病的可能性更方便。如果我们把诊断某种疾病想象成选择性因素的加法和呢?也就是说,在心肌梗死的例子中,患者因有正性胸痛而获得一分,因有正性应力试验历史而获得一分,等等。我们可以建立一个给予正性心肌梗死诊断的总分阈值。因为有些因素比其他因素更重要,我们可以使用加权总和,其中每个因素在添加之前都乘以重要性因子。例如,胸痛的存在可能值得三分,而正性应力试验的历史可能值得五分。这就是评分表的工作方式。
在以下表格中,我们以修改过的威尔斯评分为例。修改过的威尔斯评分(来源于临床预测,2017 年)用于确定患者是否可能患有肺栓塞(PE):肺部的血栓是一种危及生命的情况。请注意,评分表不仅为每个相关临床发现提供积分值,还给出了解释总分的阈值:
| 临床发现 | 评分 |
|---|---|
| 深静脉血栓形成的临床症状(下肢肿胀,压痛疼痛) | 3.0 |
| 替代诊断不太可能比肺栓塞 | 3.0 |
| 心率 > 100 次/分钟 | 1.5 |
| 长时间卧床超过 3 天或最近 4 周内手术 | 1.5 |
| 先前诊断为深静脉血栓形成/肺栓塞 | 1.5 |
| 咯血 | 1.0 |
| 患者患癌症 | 1.0 |
| 风险分层 | |
| 低风险的 PE | < 2.0 |
| 中等风险的 PE | 2.0 - 6.0 |
| 高风险的 PE | > 6.0 |
相应的机器学习算法 - 线性回归和逻辑回归
请注意,评分表倾向于使用易于添加的整数。显然,这样做是为了医生在看病人时使用标准方便。如果我们能够某种方式确定每个因素的最佳点值以及最佳阈值会发生什么?值得注意的是,被称为逻辑回归的机器学习方法正是如此。
逻辑回归是一种流行的统计机器学习算法,通常用于二元分类任务。它是一种称为广义线性模型的模型类型。
要理解逻辑回归,我们必须首先理解线性回归。在线性回归中,第i个输出变量(y-hat)被建模为p个个体预测变量x[i]的加权和:

变量的权重(也称为系数)可以通过以下方程确定:

逻辑回归就像线性回归,只不过它对输出变量进行了转换,将其范围限制在 0 和 1 之间。因此,它非常适合于分类任务中建模正响应的概率,因为概率也必须在 0 和 1 之间。
逻辑回归有许多实际优势。首先,它是一个直观简单的模型,易于理解和解释。理解其机制并不需要超过高中统计学的高级数学知识,并且可以很容易地向项目中的技术和非技术相关人员进行解释。
其次,从时间和内存的角度来看,逻辑回归并不计算密集。其系数仅仅是一个数字集合,长度与预测变量的数量相等,确定这些系数只涉及几次矩阵乘法(可以参考前面的第二个公式作为示例)。需要注意的是,当处理非常大的数据集时(例如,数十亿个数据点),矩阵可能会非常大,但这是大多数机器学习模型的共同特点。
第三,逻辑回归对变量的预处理要求较低(例如,居中或缩放)(尽管将预测变量转化为接近正态分布的形式可以提高性能)。只要变量是数值格式,就足以开始使用逻辑回归。
最后,逻辑回归,尤其是结合了像套索正则化这样的正则化技术时,在进行预测时可以表现出相当强的性能。
然而,在今天这个快速而强大的计算时代,逻辑回归已经在很大程度上被其他更强大且通常更准确的算法所取代。这是因为逻辑回归对数据和建模任务做出了许多重要的假设:
-
它假设每个预测变量与结果变量之间具有线性关系。在大多数数据集中,这显然并非如此。换句话说,逻辑回归在建模数据的非线性方面并不擅长。
-
它假设所有的预测变量相互独立。再说一次,这通常并非如此,例如,两个或多个变量可能会相互作用,以一种超过各个变量线性求和的方式影响预测结果。通过将预测变量的乘积作为交互项添加到模型中,可以部分缓解这一问题,但选择哪些交互项来建模并不是一件简单的任务。
-
它对多重相关的预测变量非常敏感,并且这种敏感性往往会导致过拟合。在这种数据存在的情况下,逻辑回归可能会导致过拟合。为了解决这个问题,存在一些变量选择方法,比如前向逐步逻辑回归、后向逐步逻辑回归和最佳子集逻辑回归,但这些算法要么不精确,要么计算量大。
最后,逻辑回归对缺失数据不具备鲁棒性,像某些分类器那样(例如,朴素贝叶斯)。
模式关联和神经网络
最后的医学决策框架直接触及我们对神经生物学理解的核心,即我们如何处理信息和做出决策。
复杂的临床推理
想象一个抱怨胸痛的老年患者见到了一位经验丰富的医生。医生慢慢地提出了适当的问题,并根据患者的体征和症状特征得出患者的情况。患者说自己有高血压病史,但没有其他心脏风险因素。胸痛的强度随着心跳变化(也叫作胸膜性胸痛)。患者还报告说刚从欧洲回到美国。他们还抱怨小腿肌肉肿胀。医生慢慢地将这些较低层次的信息(缺乏心脏风险因素、胸膜性胸痛、长时间不活动、霍曼氏征阳性)与以前患者的记忆和医生自己丰富的知识相结合,建立了对患者的更高层次理解,意识到患者正在发生肺栓塞。医生安排了 V/Q 扫描,并采取措施挽救患者的生命。
这样的故事每天都在全球的诊所、医院和急诊科发生。医生通过病史、检查和测试结果的信息,构建对患者的更高层次理解。他们是如何做到的呢?答案可能在于神经网络和深度学习。
对应的机器学习算法——神经网络和深度学习
人类如何思考并获得意识无疑是宇宙中的未解之谜。关于人类如何实现理性思维,或者医生如何做出复杂临床决策的知识非常有限。然而,也许到目前为止,我们最接近模仿人类大脑在常见认知任务中的表现的技术,就是通过神经网络和深度学习。
神经网络的模型灵感来源于哺乳动物的神经系统,其中预测变量连接到人工“神经元”的多个层次,这些神经元在发送经过非线性转换的输出到下一层之前,汇聚并加权输入数据。以这种方式,数据可能经过多个层次,最终产生一个结果变量,该变量表示目标值为正的可能性。权重通常通过反向传播技术训练,在每次迭代中,将正确输出与预测输出之间的负差值添加到权重中。
神经网络和反向传播技术最早在 1980 年代通过《自然》杂志上发表的一篇著名论文中报告(如在第一章,医疗保健分析导论中讨论过,Rumelhart 等,1986 年);进入 2010 年代后,现代计算能力与海量数据相结合,促使神经网络被重新命名为“深度学习”。随着计算能力的提升和数据的可获取性,机器学习任务在语音识别、图像和物体识别以及数字识别等方面取得了最先进的性能提升。
神经网络的根本优势在于它们能够处理数据中预测变量之间的非线性关系和复杂交互作用。这是因为神经网络中的每一层本质上都在对前一层的输出进行线性回归,而不仅仅是对输入数据本身进行回归。网络中的层数越多,网络能够建模的函数就越复杂。神经元中的非线性变换也有助于这一能力的实现。
神经网络也很容易应用于多类问题,即有超过两个可能结果的情况。识别数字 0 到 9 就是其中的一个例子。
神经网络也有其缺点。首先,它们的可解释性较差,且可能难以向项目中的非技术利益相关者解释。理解神经网络需要具备大学水平的微积分和线性代数知识。
其次,神经网络的调优可能会非常困难。通常涉及许多参数(例如,如何初始化权重、隐藏层的数量和大小、使用什么激活函数、连接模式、正则化以及学习率),并且系统地调节所有这些参数几乎是不可能的。
最后,神经网络容易发生过拟合。过拟合是指模型“记住”了训练数据,无法很好地推广到以前未见过的数据。如果参数/层数过多和/或数据被迭代过多次,就可能发生这种情况。
我们将在第七章,医疗保健中的预测模型构建中使用神经网络。
机器学习管道
在上一节中,我们花了大量时间讨论了机器学习模型及其与医学决策框架的关系。但是,究竟如何训练一个机器学习模型呢?在医疗领域,机器学习通常由一系列典型的任务组成。我们可以将这些任务的集合称为管道。虽然每个机器学习应用的管道都不完全相同,但管道使我们能够描述机器学习的过程。在这一节中,我们描述了许多简单机器学习项目通常遵循的一个通用管道,特别是在处理结构化数据(即可以组织成行和列的数据)时。
加载数据
在我们对数据进行计算之前,数据必须从存储位置(通常是数据库或实时数据流)加载到计算工作区。工作区允许用户使用流行的语言(包括 R、Python、Hadoop 和 Spark)来操作数据并构建模型。许多商业数据库具有专门的功能,方便将数据加载到工作区中。机器学习语言本身也有从文本文件中读取数据并连接和读取数据库的函数。有时,用户也可能更倾向于直接在数据库中进行数据质量控制和清理。这通常包括构建患者索引、数据标准化和数据清理等步骤。在第四章,计算基础——数据库中,我们讨论了如何使用结构化查询语言(SQL)操作数据库,而在第五章,计算基础——Python 简介中,我们讨论了如何将数据加载到 Python 工作区的方法。
清理和预处理数据
数据科学中有一句流行的说法,大致是:“每 10 个小时的数据科学家工作时间,其中 7 小时都在清理数据。”数据清理包含几个子任务,我们现在来看看这些任务。
聚合数据
数据通常以单独的表格形式在数据库中组织,这些表格可能通过共同的患者或就诊标识符连接在一起。机器学习算法通常一次只处理一个数据结构。因此,将来自多个表格的数据合并到一个最终表格中是一个重要的任务。在这个过程中,你需要做出一些决定,保留哪些数据(人口统计数据通常是不可或缺的),以及可以安全删除哪些数据(例如,如果你想预测癌症的发生,抗哮喘药物的具体使用时间戳可能并不重要)。
解析数据
有些情况下,我们需要的部分或全部数据是以紧凑的形式存在的。例如,健康调查数据的平面文件,每个调查被编码为一个N字符的字符串,每个位置的字符对应特定的调查响应。在这些情况下,我们需要将所需数据分解为其各个组成部分,并在使用之前转换为有用的格式。我们称这种活动为解析。即使使用特定的医疗编码系统表达的数据也可能需要一些解析。
转换类型
如果你对编程有所了解,你会知道数据可以存储为不同的变量类型,从简单的整数到复杂的小数再到字符串(字符)类型。这些类型在可以对它们执行的操作方面有所不同。例如,如果数字 3 和 5 存储为整数类型,我们可以轻松使用代码计算 3+5= 8。但是,如果它们存储为字符串类型,则将"3"加上"5"可能会导致错误,或者可能会得到"35",这会导致我们的数据出现各种问题,可以想象。清理和检查数据的一部分工作是确保每个变量都存储为其适当的类型。数值数据应对应数值类型,而大多数其他数据应对应字符串或分类类型。
除了变量类型外,在许多建模语言中,必须决定如何使用更复杂的数据容器存储数据,例如在 R 中的列表、向量和数据框架以及在 Python 中的列表、字典、元组和数据框架。各种导入和建模函数可能会假定不同的数据结构选择,因此,再次进行数据结构之间的相互转换通常是必要的,以实现期望的结果,这是数据清理的关键部分。我们将在第五章,《计算基础 - Python 入门》中讨论与 Python 相关的数据结构。
处理缺失数据
机器学习在医疗保健中如此独特困难的部分原因在于其缺失数据的倾向。住院数据收集通常依赖于护士和其他临床人员的全面完成,考虑到护士和其他临床人员的工作繁忙程度,难怪许多住院数据集有某些特征,如尿量和输出或药物管理时间戳,报告不一致。另一个例子是诊断编码:一个患者可能符合十几种医学诊断,但出于时间考虑,仅有五种被门诊医生输入到表中。当我们的数据中省略这些详细信息时,我们的模型在应用于实际患者时将会准确度大大降低。
比缺乏细节更为严重的问题是缺失数据对我们算法的影响。即便是一个数据框中缺失的单一值——这个数据框包含了成千上万的患者和数百个特征——也可能导致模型无法成功运行。一个简单的解决办法可能就是在缺失值的位置输入或填充一个零。但如果这个变量是血红蛋白的实验室值,显然血红蛋白为 0.0 是不可能的。那么我们应该用平均血红蛋白值来填补缺失数据吗?我们应该使用整体平均值还是性别特定的平均值?类似的问题正是处理缺失数据几乎成了数据科学独立领域的原因。必须强调的是,基本了解数据集中的缺失数据至关重要。特别是,需要清楚地知道零值数据和缺失数据之间的区别。此外,了解像零、NaN("不是一个数字")、NULL("缺失")或NA("不适用")这样的概念,以及它们在你选择的编程语言中如何表示,不论是 SQL、Python、R 还是其他语言,都是非常重要的。
数据清洗阶段的最终目标通常是一个单一的数据框架,它是一个组织数据的单一数据结构,将数据排列成类似矩阵的行列对象,其中行表示单个事件或观测,列反映观测的不同特征,并使用各种数据类型。在理想的情况下,所有变量都应该被检查并转换为适当的类型,而且应该没有缺失数据。需要注意的是,在达到最终目标之前,数据清洗、探索、可视化和特征选择可能会有多次往返迭代。数据探索/可视化和特征选择是我们接下来要讨论的两个步骤。
探索和可视化数据
数据探索与可视化是与数据解析和清洗紧密结合的,它是模型构建过程中非常重要的一部分。这个阶段很难明确定义——在探索数据时,我们到底在寻找什么?其背后的理论是,人类在某些方面比计算机做得更好——比如建立联系和识别模式。人们越是仔细查看和分析数据,就会越发现变量之间的关系以及如何利用这些变量预测目标变量。
这一步骤中一个常见的探索性活动是盘点所有预测变量;即,检查它们的格式(例如,是否是二元的、分类的或连续的)以及每个变量中缺失值的数量。对于二元变量,有助于统计正向响应和负向响应的数量;对于分类变量,有助于统计每个变量可以取的值的种类及其频率直方图;对于连续变量,计算一些集中趋势的度量(例如,均值、中位数、众数)和离散度的度量(例如,标准差、分位数)是一个不错的选择。
可以进行额外的探索和可视化活动,以阐明所选预测变量与目标变量之间的关系。具体的图示依据格式(如二元、分类、连续)而有所不同。例如,当预测变量和目标变量都是连续时,散点图是一种流行的可视化方式;为了绘制散点图,需将每个变量的值绘制在不同的坐标轴上。如果预测变量是连续的,而目标变量是二元或分类的,双重重叠频率直方图是一个不错的工具,箱线图也很有用。
在许多情况下,预测变量过多,以至于无法手动检查并可视化每个关系。在这些情况下,自动分析以及计算相关系数等度量和统计数据变得非常重要。
特征选择
在构建模型时,更多的特征并不总是更好。从实现的角度来看,实时临床环境中与多个设备、健康信息系统和源数据库交互的预测管道比起使用最小数量特征的简化版本,更可能失败。具体来说,在清理和探索数据时,你会发现并非所有的特征都与结果变量显著相关。
此外,许多变量可能与其他变量高度相关,并且对于进行准确预测提供的信息不多。将这些变量保留在模型中,实际上可能会降低模型的准确性,因为它们会为数据引入随机噪声。因此,机器学习管道中的一个常见步骤是进行特征选择,并从数据中删除不需要的特征。删除的特征数量及其选择依赖于多个因素,包括所选择的机器学习算法以及模型的可解释性要求。
有许多方法可以从最终模型中去除多余的特征。迭代方法中,特征被移除并建立结果模型,评估并与先前的模型进行比较是流行的,因为它们允许我们测量调整如何影响模型性能。选择特征的几种算法包括最佳子集选择、前向和后向逐步回归。还有许多特征重要性的度量,包括相对风险比、几率比、p-值显著性、套索正则化、相关系数和随机森林袋外错误,我们将在第七章,在医疗保健中制作预测模型中探讨其中一些度量。
训练模型参数
一旦我们有了最终的数据框架,我们可以将机器学习问题视为最小化误差函数。我们所试图做的就是在未见患者/接触的情况下做出最佳预测;我们试图最小化预测值与观察值之间的差异。例如,如果我们试图预测癌症发病,我们希望预测的癌症发生可能性在发展了癌症的患者中高,并且在未发展癌症的患者中低。在机器学习中,预测值与观察值之间的差异被称为误差函数或成本函数。成本函数可以采用各种形式,机器学习实践者通常在执行建模时调整它们。在最小化成本函数时,我们需要知道我们赋予某些特征的权重。在大多数情况下,与结果变量高度相关的特征应比与结果变量相关性较低的特征更重要。在简单的意义上,我们可以将这些“重要变量”称为权重或参数。监督机器学习的主要目标之一就是找到那组唯一的参数或权重,以最小化我们的成本函数。几乎每个机器学习算法都有自己分配权重给不同特征的方式。我们将在第七章,在医疗保健中制作预测模型中更详细地研究这部分流程。
评估模型性能
最后,在构建模型之后,评估其性能对于地面真实情况至关重要,这样我们可以根据需要进行调整,比较不同的模型,并向他人报告我们模型的结果。评估模型性能的方法取决于被预测目标变量的结构。
通常,评估模型的第一步是制作一个 2 x 2 列联表,以下是一个示例(《预防医学》,2016)。在 2 x 2 列联表中,所有观察值都被分为四类,具体内容将在下图中进一步讨论:

对于二值目标变量(例如分类问题),将有四种类型的观察值:
-
这些是实际为阳性的结果,我们预测也是阳性结果
-
这些是实际为阳性的结果,但我们预测为阴性结果
-
这些是实际为阴性的结果,但我们预测为阳性结果
-
这些是实际为阴性的结果,我们预测也是阴性结果
这四类观察值分别称为:
-
真阳性(TP)
-
假阴性(FN)
-
假阳性(FP)
-
真阴性(TN)
然后,可以从这四个量中计算出各种性能衡量指标。我们将在以下部分介绍一些常见的指标。
灵敏度(Sn)
灵敏度,也称为召回率,回答了这个问题:“我的模型在错误地检测到病症阳性观察值方面有多有效?”
它的公式如下:

特异性(Sp)
特异性回答了这个问题:“我的模型在错误地检测到病症阴性观察值方面有多有效?”
它的公式如下:

灵敏度和特异性是互补的性能衡量标准,通常一起报告,用来衡量模型的性能。
阳性预测值(PPV)
阳性预测值(PPV),也称为精确度,回答了这个问题:“给定我模型的阳性预测,它正确的可能性有多大?”
它的公式如下:

阴性预测值(NPV)
阴性预测值(NPV)回答了这个问题:“给定我模型的阴性预测,它正确的可能性有多大?”
它的公式如下:

假阳性率(FPR)
假阳性率(FPR)回答了这个问题:“给定一个阴性观察值,我的模型将其分类为阳性的可能性有多大?”
它的公式如下:

它也等于特异性(1 - Sp)。
准确度(Acc)
准确度(Acc)回答了这个问题:“给定任何观察值,我的模型正确分类它的可能性有多大?”它可以作为模型性能的独立衡量标准。
它的公式如下:

受试者工作特征(ROC)曲线
当目标变量为二值时,许多机器学习算法会以一个从 0 到 1 之间的分数形式返回观察值的预测结果。因此,预测的正负值取决于我们在该范围内设置的阈值。例如,如果我们建立一个模型来预测癌症恶性程度,并确定某个患者的恶性可能性为 0.65,那么选择一个 0.60 的正阈值将对该患者做出正向预测,而选择 0.70 的阈值则会做出负向预测。所有的性能评分都取决于我们设置阈值的位置。某些阈值会比其他阈值带来更好的表现,这取决于我们检测的目标。例如,如果我们关注癌症检测,将阈值设置为 0.05 这样较低的值会提高模型的灵敏度,虽然这会以特异性为代价,但这可能是我们想要的,因为我们可能不介意假阳性,只要我们能够识别出所有可能面临癌症风险的患者。
也许最常见的二值结果变量的性能评估范式是构建接收者操作特征(ROC)曲线。在这条曲线中,我们绘制两个度量值的值:假阳性率和灵敏度,随着阈值从 0 变化到 1。灵敏度通常与假阳性率成反比关系,导致大多数情况下出现一个小写的 r 形曲线。模型越强,灵敏度越高,假阳性率通常越低,曲线下面积(AUC)趋向于接近 1。因此,AUC 可以用来比较模型(在相同使用场景下),同时消除阈值值的依赖。
以下示例 ROC 图(示例 ROC 曲线,2016 年)显示了两条 ROC 曲线,一条较暗,一条较亮。由于红色(暗)曲线的曲线下面积大于较亮的曲线,因此可以认为由暗曲线衡量的模型性能优于由亮曲线反映的模型性能:

精度-召回曲线
精度-召回曲线是当目标变量不平衡(例如,当正负比例非常低或非常高)时,ROC 曲线的替代方案。在医疗保健中,许多用例的正负比率较低,因此你可能会经常看到这条曲线。它绘制了随着阈值从 0 变化到 1,灵敏度的正预测值。在大多数情况下,这会产生一个大写 L 形的曲线。
连续值目标变量
对于连续值目标变量(例如回归问题),没有真正的正例或假正例的概念,因此无法计算之前讨论的度量和曲线。相反,通常会计算残差平方和(RSS)误差:它是实际值与预测值之间的平方距离之和。
总结
在这一章中,我们回顾了一些进行医疗健康分析的机器学习和数学基础。在下一章中,我们将继续探索医疗健康分析的基础三角形,接着讨论计算部分。
参考文献和进一步阅读
临床预测(2017)。“Wells肺栓塞临床预测规则”。www.clinicalprediction.com/wells-score-for-pe/。访问日期:2018 年 6 月 6 日。
“文件:示例 ROC 曲线.png。”维基共享资源,免费媒体库。2016 年 11 月 26 日,05:26 UTC。2018 年 7 月 11 日,01:53 commons.wikimedia.org/w/index.php?title=File:Example_ROC_curves.png&oldid=219960771。
“文件:预防医学统计学 敏感性 TPR,特异性 TNR,PPV,NPV,FDR,FOR,准确率,似然比,诊断比率 2 Final.png。”维基共享资源,免费媒体库。2016 年 11 月 26 日,04:26 UTC。2018 年 7 月 11 日,01:42 commons.wikimedia.org/w/index.php?title=File:Preventive_Medicine_Statistics_Sensitivity_TPR,_Specificity_TNR,_PPV,_NPV,_FDR,_FOR,_ACCuracy,_Likelihood_Ratio,_Diagnostic_Odds_Ratio_2_Final.png&oldid=219913391。
Häggström, Mikael(2014)。“Mikael Häggström 医学画廊 2014”。《医学期刊》1(2)。DOI:10.15347/wjm/2014.008。ISSN 2002-4436。公有领域。
James G, Witten D, Hastie T, Tibshirani R(2014)。《统计学习导论》。纽约:Springer。
Kirk E, Bottomley C, Bourne T(2014)。“诊断异位妊娠及目前对不明位置妊娠的管理概念”。《人类生殖更新》20(2):250–61。DOI:10.1093/humupd/dmt047。PMID24101604。
Mark, DB(2005)。“临床医学中的决策制定。” 收录于 Kasper DL、Braunwald E、Fauci AS、Hauser SL、Longo DL、Jameson JL 编著。哈里森内科学原理(第 16 版)。纽约,NY:McGraw-Hill。
美国国家心脏、肺部和血液研究所(2010)。“治疗算法。” 超重和肥胖指南:电子教材。www.nhlbi.nih.gov/health-pro/guidelines/current/obesity-guidelines/e_textbook/txgd/algorthm/algorthm.htm。访问日期:2018 年 6 月 3 日。
Rumelhart DE, Hinton GE, Williams RJ(1986)。“通过反向传播错误学习表示。” 自然 323(9):533-536。
第四章:计算基础 - 数据库
本章将向你介绍数据库和结构化查询语言(SQL)。这本书主要面向医疗专业人员和初学者数据科学家、程序员,他们有兴趣使用医疗数据库。通过本章学习,你将了解什么是数据库,并掌握如何使用基本的 SQL 从临床数据库中提取和操作信息。我们将展示一个任务示例,并给出对操作五个病人样本数据库数据有用的 SQL 语句。
数据库简介
数据库可以定义为一组相关数据的集合(Elmasri 和 Navathe, 2010)。数据库通常可以分为SQL 数据库或NoSQL 数据库。在 SQL 数据库中,数据以表格的形式记录,并由行和列组成。相关数据可能会分布在多个表格中,这是为了在存储效率和便捷性之间做出权衡。数据库管理系统(DBMS)是一种软件,它使得数据库能够执行多种功能。首先,它允许使用 SQL 语言来检索数据(适用于 SQL 数据库)。另一个功能是在需要时更新数据,同样使用 SQL。DBMS 的其他功能还包括保护和确保数据的安全。
数据库管理是一个独立的复杂领域。在本书中,我们将重点讲解使用 SQL 来检索和更新通常分布在多个相关表格中的临床数据。有关数据库的更多全面资源,请参考本章末尾的参考文献部分。
使用 SQL 进行数据工程——一个案例示例
对于本章内容,假设你获得了一个预测分析的任务,任务对象是位于美国的心脏病科诊所。诊所希望你预测哪些病人在就诊后 6 个月内有死亡风险。他们以一个包含六个表格的数据库的形式将数据提供给你。为了简化,我们将数据库数据缩减为仅包含五位病人的信息。我们的任务是使用 SQL 语言对数据进行处理,将其合并成一个表格,以便用于机器学习。我们将首先介绍数据库中的病人和数据库结构。然后,我们将介绍基本的 SQL 概念,并操作数据使其适应机器学习的要求。
案例详情——预测心脏病科诊所的死亡率
你所工作的心脏病科诊所有两位医生:Johnson 博士和 Wu 博士。虽然诊所有许多病人,他们有兴趣识别哪些在未来 6 个月内有较高全因死亡风险的病人。2016 年曾有门诊访问的病人符合数据分析的纳入标准。目标变量是病人在就诊后 6 个月内是否去世。
现在我们已经回顾了建模任务的细节,接下来让我们看看数据库中的五个病人。由心脏病科诊所发送给您的初步数据包含了五个病人的信息,分布在六个表格中。以下是每个病人的病例简介。请注意,本部分包含了大量与心血管疾病相关的临床术语。我们建议您利用可用的在线资源来解答关于这些术语的问题。一本全面的临床参考书是《哈里森内科学原理》(Kasper 等,2005),相关信息会在章节末尾提供。
以下是关于患者的信息:
-
患者 ID-1:数据库中的患者 #1 是一名 65 岁的男性,患有充血性心力衰竭(CHF),这是一种慢性病,心脏无法有效地将血液泵送到身体其他部分。他还患有高血压(高血压是 CHF 的一个危险因素)。他于 2016 年 9 月 1 日和 1 月 17 日分别就诊于心脏病专家约翰逊医生。在 1 月 9 日的就诊中,他的血压升高(154/94),并且脑钠肽(BNP)值为 350,BNP 是 CHF 严重程度的标志。之后他开始服用利辛普利和呋塞米,这些是治疗 CHF 和高血压的一线药物。不幸的是,他于 2016 年 5 月 15 日去世。
-
患者 ID-2:患者 #2 是一名 39 岁的女性,患有心绞痛(运动时出现的心脏相关胸痛)和糖尿病史。糖尿病是心肌梗死(心脏病发作,动脉粥样硬化性心脏病的晚期表现,常常致命)的一个危险因素,而心绞痛可视为动脉粥样硬化性心脏病的早期表现。她于 2016 年 1 月 15 日就诊于她的心脏病专家吴医生,检查时发现她的血糖水平为 225,表明糖尿病控制不良。她开始服用二甲双胍治疗糖尿病,并且使用硝酸甘油、阿司匹林和美托洛尔治疗心绞痛。
-
患者 ID-3:患者 #3 是一名 32 岁的女性,因高血压接受约翰逊医生的治疗。在 2016 年 2 月 1 日的就诊中,她的血压升高,达到了 161/100。她开始服用缬沙坦/氢氯噻吨合剂进行抗高血压治疗。
-
患者 ID: 4:患者 #4 是一名 51 岁的男性,患有严重的充血性心力衰竭(CHF)并伴有肺动脉高压。他于 2016 年 2 月 27 日就诊于吴医生。在那次就诊时,他的体重为 211 磅,血压略有升高,为 143/84。其脑钠肽(BNP)水平显著升高,达到 1000。他被给予了利辛普利和呋塞米治疗心力衰竭,并且使用了地尔硫卓治疗肺动脉高压。不幸的是,他于 2016 年 6 月 8 日去世。
-
病人 ID-5:我们数据库中的最后一位病人,病人 #5,是一位 58 岁的男性,他于 2016 年 3 月 1 日就诊于吴医生,患有充血性心力衰竭(CHF)和 2 型糖尿病的病史。就诊时,他的血糖为 318,BNP 水平适度升高至 400。他开始服用利辛普利和呋塞米治疗 CHF,并服用二甲双胍治疗糖尿病。
临床数据库
现在,我们已经了解了数据库中包含的五位病人信息,我们可以描述数据库中包含的表格结构和字段,这些字段来自六个模拟表格:PATIENT、VISIT、MEDICATIONS、LABS、VITALS和MORT。尽管每个临床数据库都有不同的结构,我尽量使用一个在医疗行业中常见的结构。通常,表格是按照临床领域呈现的(有关使用这种分布式格式的研究表格示例,请参见 Basole 等,2015)。例如,通常有一个表格包含人口统计学和个人信息,一个表格用于实验室结果,一个用于药物治疗,依此类推,这就是我们在本例中构建数据库的方式。表格通常通过一个共同的标识符(在我们这个例子中是Pid字段)将它们联系在一起。
在描述这些表格时,我们必须牢记数据工程阶段的最终目标——将六个表格中的相关信息合并成一个单一的表格,表格的列不仅包括目标变量(在本例中是死亡率),还包括预测变量,这些变量应有助于预测目标变量。这将使我们能够使用流行的机器学习包,如 Python 的scikit-learn,来创建机器学习模型。考虑到这一点,我们将重点介绍一些对任务有用的字段。
PATIENT 表格
在我们的例子中,PATIENT表格,如下图所示,包含了病人的人口统计学和识别信息——包括姓名、联系方式、生日和生物性别。在这个例子中,只有五个记录和 11 列;而在实际操作中,这个表格会包含所有与医疗机构相关的病人信息。这个表格的行数可能从几百行到几万行不等,同时表格可能包含几十列详细的人口统计学信息:

在数据库中,每个独特的病人都会被分配一个标识符(字段标记为Pid),在我们这个例子中,标识符简单地编号为 1 至 5。Pid列让我们能够在不同表格之间跟踪病人。同时,注意每个独特的病人 ID 只会有一条记录。
在确定了必要的标识符列后,重点应该放在保留哪些变量和丢弃哪些变量上。当然,年龄和性别是死亡率的重要人口学预测因素。如果种族数据存在于此表格中,那将是另一个重要的人口学变量。
该表中另一个值得注意的变量是邮政编码。越来越多的社会经济数据被用于机器学习分析。邮政编码可能与公开的普查数据相关联;这些数据可以与该表中的邮政编码进行联接,并可能提供有关每个患者所在邮政编码区域的平均教育水平、收入和医疗保障的信息。甚至有组织出售家庭级的信息;然而,使用这些数据时也伴随着对隐私保护和数据安全的重大责任。为了简化示例,我们将省略邮政编码。
我们最终表格中将省略的信息包括姓名、街道地址和电话号码。只要我们有患者 ID,这些字段对目标变量的预测影响应该不大。
就诊(VISIT)表
虽然PATIENT表包含了每个患者的基本行政信息,但我们的任务是基于每次就诊预测死亡风险。VISIT表包含每次患者就诊的一个观察值,并包含一些关于每次就诊的临床信息:

注意,患者 ID 不再是该表的主要标识符,因为患者#1 有两次就诊;相反,表中有一个Visit_id字段,示例中的编号从10001到10006,每次就诊对应一个独立的 ID。
该表还包含Visit_date字段。由于心脏病学科表示希望了解患者就诊后的 6 个月内死亡风险,因此在计算目标变量时,我们将需要使用该字段。
该表中的两个字段包含了 ICD(诊断)代码。实际的表格可能会包含每次就诊的数十个代码。对于每个编码字段,都有一个对应的名称字段,包含该代码所代表的病症名称。医疗行业中一种流行的方法是,在最终的表格中,为我们关注的每一个临床代码创建一个列(Futoma et al., 2015; Rajkomar et al., 2018)。我们将在本章后面采用这种方法。
最后,我们注意到表中包括了主治医生的名字,这可以用来衡量医生的绩效。
药物(MEDICATIONS)表
MEDICATIONS表包含了我们的五位患者每一项正在服用的药物的记录。在这个例子中,没有一个单独的列充当该表的主键。如以下截图所示,该表包括药物名称、剂量、频率、途径、开药医生和开药日期等信息。每种药物的 NDC 代码也被包括在内;我们在第二章,医疗基础中讲解了 NDC 代码:

在最终表格中包含药物信息并不简单。例如,表格中的信息没有显示每种药物的类别。虽然有 NDC 代码,但 NDC 代码比药物名称更为详细,因为它包括了给药途径和剂量,从而使每个代码唯一;因此,不同剂型的依那普利可能会有不同的 NDC 代码。为了为每种药物创建一列,我们可以为每种药物分别创建一个表格,包含组成该药物的所有药物信息,然后将这些信息合并到我们的表格中。
如果我们选择包含剂量信息,该字段将需要一些清理。注意,病人#3 正在服用一种抗高血压联合药物——缬沙坦成分的剂量为 160 毫克,而氢氯噻吨成分的剂量为 12.5 毫克。这可能被编码为两种不同的药物,但编写一个脚本将联合药物拆分为两行并不简单。
实验室表
实验室信息是临床诊断的重要部分,许多实验室测试结果是很好的预测变量(Donze et al., 2013;Sahni et al., 2018)。LABS表包含描述实验室测试名称、缩写、LOINC 代码和结果的字段:

包含实验室信息的最终表格有几种不同的处理方式。一种方法是将原始实验室结果作为连续变量包含进来。然而,这会导致一个问题,因为大多数实验室的结果会是 NULL。我们可以通过在缺失时填充一个正常范围的值来解决这个问题。另一种方法是为异常范围的实验室检测结果设置一个二进制变量。这解决了缺失数据的问题,因为如果结果缺失,它会显示为零。然而,使用这种方法,1,000 的 BNP 值(表示严重 CHF)与 350 的 BNP 值(表示轻度 CHF)没有区别。本章将演示这两种方法。
另外,Lab_value字段有时包含特殊字符,例如在肌钙蛋白结果中。这些字符需要删除,实验室值也需要相应地进行解释。培养结果(在此示例中未包括)完全是文本的,通常会命名特定的细菌菌株,而不是数字。
再次强调,这是一个简化的示例,许多常见的实验室检查(例如,白细胞计数、血红蛋白、钠、钾等)在此示例中未包含。
生命体征表
生命体征是反映病人健康状态的重要指标,并且在医疗健康机器学习模型中可能是很好的预测变量(Sahni et al., 2018)。生命体征通常在每次病人就诊时都进行测量,因此可以很容易以原始(数值)形式包含进来,以保持数据的细粒度。
在下方的表格截图中,我们注意到尽管身高和体重信息存在,但身体质量指数(BMI)缺失。我们将在第五章《计算基础 - Python 入门》中演示 BMI 的计算。其次,访问 #10004 缺少体温读数。在医疗中这很常见,可能是由于护理中的疏漏导致的:

VITALS 表
在本章后面,我们将为这次就诊推算正常的体温。
MORT 表
最后,我们来到了包含目标变量的表。MORT 表仅包含两个字段:患者标识符和患者去世的日期。未列在此表中的患者可以假定为存活:

稍后,我们将学习如何将这些表中的信息转移到一个二进制目标变量中。
启动 SQLite 会话
我们将用于转换数据库的数据库引擎是SQLite。在第一章《医疗分析入门》中,我们讲解了安装说明以及基本的 SQLite 命令。需要提到的是,SQL 有多种变体,而特定于 SQLite 的 SQL 与特定于 MySQL 或 SQL Server 数据库的 SQL 有一些小的差异。然而,所有 SQL 方言的基本原理保持一致。
此时,执行以下操作:
-
使用
cd命令,导航到包含sqlite3.exe程序的目录。 -
输入
sqlite3 mortality.db并按 Enter 键。你应该看到类似以下的提示符:sqlite>。这个提示符表明你已进入 SQLite 程序。 -
在本章剩余的部分,我们将创建一些表并在 SQLite 程序中执行一些 SQLite 命令。
-
随时退出会话,输入
.exit并按 Enter 键。
数据工程,一次一个表,使用 SQL
现在让我们来看一下如何使用 SQLite 执行数据工程。首先,我们需要在数据库中创建表。然后,我们将一个一个地操作这些表,直到获得最终的目标表。
查询集 #0 – 创建六个表
在这个模拟任务中,假设下载心脏科诊所数据的门户网站无法使用。相反,技术人员会向你发送一些 SQLite 命令,你可以用这些命令来创建六个表。你可以跟着书中的步骤,一一手动输入每个命令。或者,你可以访问书籍的官方代码库,从那里下载命令。
查询集 #0a – 创建 PATIENT 表
创建表的一种方法是手动指定其架构。让我们在这里使用第一个表 PATIENT 表来演示:
sqlite> CREATE TABLE PATIENT(
Pid VARCHAR(30) NOT NULL,
Fname VARCHAR(30) NOT NULL,
Minit CHAR,
Lname VARCHAR(30) NOT NULL,
Bdate TEXT NOT NULL,
Street VARCHAR(50),
City VARCHAR(30),
State VARCHAR(2),
Zip VARCHAR(5),
Phone VARCHAR(10) NOT NULL,
Sex CHAR,
PRIMARY KEY (Pid)
);
在前面的示例中,请注意表名出现在CREATE TABLE短语之后。接下来是一个开括号,每一行命名一个新列(例如,Pid和Fname)。在每一行的列名后面,列出了每个列的数据类型。在本示例中,我们对大多数列使用VARCHAR(),其中
是该列包含的最大字符数。CHAR列只包含一个字符。最后,对于一些重要字段(例如姓名和标识符),我们不允许其为空,并通过使用NOT NULL短语来指定这一点。
现在我们已经创建了表的架构,下一步是向表中填充数据。正如我们所说,数据库中只有五个病人,因此PATIENT表将有五行。我们使用INSERT命令将每一行插入到表中,如下所示:
sqlite> INSERT INTO PATIENT (Pid, Fname, Minit, Lname, Bdate, Street, City, State, Zip, Phone, Sex)
VALUES ('1','John','A','Smith','1952-01-01','1206 Fox Hollow Rd.','Pittsburgh','PA','15213','6789871234','M');
sqlite> INSERT INTO PATIENT (Pid, Fname, Minit, Lname, Bdate, Street, City, State, Zip, Phone, Sex)
VALUES ('2','Candice','P','Jones','1978-02-03','1429 Orlyn Dr.','Los Angeles','CA','90024','3107381419','F');
sqlite> INSERT INTO PATIENT (Pid, Fname, Minit, Lname, Bdate, Street, City, State, Zip, Phone, Sex)
VALUES ('3','Regina','H','Wilson','1985-04-23','765 Chestnut Ln.','Albany','NY','12065','5184590206','F');
sqlite> INSERT INTO PATIENT (Pid, Fname, Minit, Lname, Bdate, Street, City, State, Zip, Phone, Sex)
VALUES ('4','Harold','','Lee','1966-11-15','2928 Policy St.','Providence','RI','02912','6593482691','M');
sqlite> INSERT INTO PATIENT (Pid, Fname, Minit, Lname, Bdate, Street, City, State, Zip, Phone, Sex)
VALUES ('5','Stan','P','Davis','1958-12-30','4271 12th St.','Atlanta','GA','30339','4049814933','M');
请注意,INSERT语句首先指定将要插入的字段,然后使用VALUES关键字,接着列出实际的数据元素。如果使用的是VARCHAR或CHAR,数据元素应该用单引号括起来。
查询集 #0b – 创建 VISIT 表
现在,让我们创建VISIT表。同样,首先使用CREATE TABLE语句,然后是六个INSERT语句:
sqlite> CREATE TABLE VISIT(
Pid VARCHAR(30) NOT NULL,
Visit_id VARCHAR(30) NOT NULL,
Visit_date DATE NOT NULL,
Attending_md VARCHAR(30) NOT NULL,
Pri_dx_icd VARCHAR(20) NOT NULL,
Pri_dx_name VARCHAR(100) NOT NULL,
Sec_dx_icd VARCHAR(20),
Sec_dx_name VARCHAR(100),
PRIMARY KEY (Visit_id)
);
sqlite> INSERT INTO VISIT (Pid, Visit_id, Visit_date, Attending_md, Pri_dx_icd, Pri_dx_name, Sec_dx_icd, Sec_dx_name)
VALUES ('1','10001','2016-01-09','JOHNSON','I50.9','Heart failure, unspecified','I10','Essential (primary) hypertension');
sqlite> INSERT INTO VISIT (Pid, Visit_id, Visit_date, Attending_md, Pri_dx_icd, Pri_dx_name, Sec_dx_icd, Sec_dx_name)
VALUES ('1','10002','2016-01-17','JOHNSON','I50.9','Heart failure, unspecified','I10','Essential (primary) hypertension');
sqlite> INSERT INTO VISIT (Pid, Visit_id, Visit_date, Attending_md, Pri_dx_icd, Pri_dx_name, Sec_dx_icd, Sec_dx_name)
VALUES ('2','10003','2016-01-15','WU','I20.9','Angina pectoris, unspecified','E11.9','Type 2 diabetes mellitus without complications');
sqlite> INSERT INTO VISIT (Pid, Visit_id, Visit_date, Attending_md, Pri_dx_icd, Pri_dx_name, Sec_dx_icd, Sec_dx_name)
VALUES ('3','10004','2016-02-01','JOHNSON','I10','Essential (primary) hypertension','','');
sqlite> INSERT INTO VISIT (Pid, Visit_id, Visit_date, Attending_md, Pri_dx_icd, Pri_dx_name, Sec_dx_icd, Sec_dx_name)
VALUES ('4','10005','2016-02-27','WU','I27.0','Primary pulmonary hypertension','I50.9','Heart failure, unspecified');
sqlite> INSERT INTO VISIT (Pid, Visit_id, Visit_date, Attending_md, Pri_dx_icd, Pri_dx_name, Sec_dx_icd, Sec_dx_name)
VALUES ('5','10006','2016-03-01','WU','I50.9','Heart failure, unspecified','E11.9','Type 2 diabetes mellitus without complications');
查询集 #0c – 创建 MEDICATIONS 表
要创建MEDICATIONS表,使用以下代码:
sqlite> CREATE TABLE MEDICATIONS(
Pid VARCHAR(30) NOT NULL,
Rx_name VARCHAR(50) NOT NULL,
Rx_dose VARCHAR(20),
Rx_freq VARCHAR(10),
Rx_route VARCHAR(10),
Prescribing_md VARCHAR(30) NOT NULL,
Rx_date DATE NOT NULL,
Rx_ndc VARCHAR(30)
);
sqlite> INSERT INTO MEDICATIONS (Pid, Rx_name, Rx_dose, Rx_freq, Rx_route, Prescribing_md, Rx_date, Rx_ndc)
VALUES ('1', 'LISINOPRIL','5 mg','bid','po','JOHNSON','01/09/2016','68180-513-01');
sqlite> INSERT INTO MEDICATIONS (Pid, Rx_name, Rx_dose, Rx_freq, Rx_route, Prescribing_md, Rx_date, Rx_ndc)
VALUES ('1', 'FUROSEMIDE','20 mg','bid','po','JOHNSON','01/09/2016','50742-104-01');
sqlite> INSERT INTO MEDICATIONS (Pid, Rx_name, Rx_dose, Rx_freq, Rx_route, Prescribing_md, Rx_date, Rx_ndc)
VALUES ('2', 'NITROGLYCERIN','0.4 mg','tid','sl','WU','01/15/2016','59762-3304-1');
sqlite> INSERT INTO MEDICATIONS (Pid, Rx_name, Rx_dose, Rx_freq, Rx_route, Prescribing_md, Rx_date, Rx_ndc)
VALUES ('2', 'METFORMIN','500 mg','bid','po','WU','01/15/2016','65162-175-10');
sqlite> INSERT INTO MEDICATIONS (Pid, Rx_name, Rx_dose, Rx_freq, Rx_route, Prescribing_md, Rx_date, Rx_ndc)
VALUES ('2', 'ASPIRIN','81 mg','qdaily','po','WU','01/15/2016','63981-563-51');
sqlite> INSERT INTO MEDICATIONS (Pid, Rx_name, Rx_dose, Rx_freq, Rx_route, Prescribing_md, Rx_date, Rx_ndc)
VALUES ('2', 'METOPROLOL TARTRATE','25 mg','bid','po','WU','01/15/2016','62332-112-31');
sqlite> INSERT INTO MEDICATIONS (Pid, Rx_name, Rx_dose, Rx_freq, Rx_route, Prescribing_md, Rx_date, Rx_ndc)
VALUES ('3', 'VALSARTAN HCTZ','160/12.5 mg','qdaily','po','JOHNSON','02/01/2016','51655-950-52');
sqlite> INSERT INTO MEDICATIONS (Pid, Rx_name, Rx_dose, Rx_freq, Rx_route, Prescribing_md, Rx_date, Rx_ndc)
VALUES ('4', 'DILTIAZEM HYDROCHOLORIDE','300 mg','qdaily','po','WU','02/27/2016','52544-693-19');
sqlite> INSERT INTO MEDICATIONS (Pid, Rx_name, Rx_dose, Rx_freq, Rx_route, Prescribing_md, Rx_date, Rx_ndc)
VALUES ('4', 'LISINOPRIL','10 mg','bid','po','WU','02/27/2016','68180-514-01');
sqlite> INSERT INTO MEDICATIONS (Pid, Rx_name, Rx_dose, Rx_freq, Rx_route, Prescribing_md, Rx_date, Rx_ndc)
VALUES ('4', 'FUROSEMIDE','40 mg','bid','po','WU','02/27/2016','68788-1966-1');
sqlite> INSERT INTO MEDICATIONS (Pid, Rx_name, Rx_dose, Rx_freq, Rx_route, Prescribing_md, Rx_date, Rx_ndc)
VALUES ('5', 'LISINOPRIL','5 mg','bid','po','WU','03/01/2016','68180-513-01');
sqlite> INSERT INTO MEDICATIONS (Pid, Rx_name, Rx_dose, Rx_freq, Rx_route, Prescribing_md, Rx_date, Rx_ndc)
VALUES ('5', 'FUROSEMIDE','20 mg','bid','po','WU','03/01/2016','50742-104-01');
sqlite> INSERT INTO MEDICATIONS (Pid, Rx_name, Rx_dose, Rx_freq, Rx_route, Prescribing_md, Rx_date, Rx_ndc)
VALUES ('5', 'METFORMIN','500 mg','bid','po','WU','03/01/2016','65162-175-10');
查询集 #0d – 创建 LABS 表
要创建LABS表,使用以下代码:
sqlite> CREATE TABLE LABS(
Pid VARCHAR(30) NOT NULL,
Lab_name VARCHAR(50),
Lab_abbrev VARCHAR(20),
Lab_loinc VARCHAR(10) NOT NULL,
Lab_value VARCHAR(20) NOT NULL,
Ordering_md VARCHAR(30),
Lab_date DATE NOT NULL
);
sqlite> INSERT INTO LABS (Pid, Lab_name, Lab_abbrev, Lab_loinc, Lab_value, Ordering_md, Lab_date)
VALUES ('1','Natriuretic peptide B','BNP','42637-9','350','JOHNSON','2016-01-09');
sqlite> INSERT INTO LABS (Pid, Lab_name, Lab_abbrev, Lab_loinc, Lab_value, Ordering_md, Lab_date)
VALUES ('2','Natriuretic peptide B','BNP','42637-9','100','WU','2016-01-15');
sqlite> INSERT INTO LABS (Pid, Lab_name, Lab_abbrev, Lab_loinc, Lab_value, Ordering_md, Lab_date)
VALUES ('2','Glucose','GLU','2345-7','225','WU','2016-01-15');
sqlite> INSERT INTO LABS (Pid, Lab_name, Lab_abbrev, Lab_loinc, Lab_value, Ordering_md, Lab_date)
VALUES ('2','Troponin I','TROP','10839-9','<0.004','WU','2016-01-15');
sqlite> INSERT INTO LABS (Pid, Lab_name, Lab_abbrev, Lab_loinc, Lab_value, Ordering_md, Lab_date)
VALUES ('4','Natriuretic peptide B','BNP','42637-9','1000','WU','2016-02-27');
sqlite> INSERT INTO LABS (Pid, Lab_name, Lab_abbrev, Lab_loinc, Lab_value, Ordering_md, Lab_date)
VALUES ('5','Natriuretic peptide B','BNP','42637-9','400','WU','2016-03-01');
sqlite> INSERT INTO LABS (Pid, Lab_name, Lab_abbrev, Lab_loinc, Lab_value, Ordering_md, Lab_date)
VALUES ('5','Glucose','GLU','2345-7','318','WU','2016-03-01');
查询集 #0e – 创建 VITALS 表
请注意,VITALS表使用了如FLOAT和INT等数值类型。要创建VITALS表,使用以下代码:
sqlite> CREATE TABLE VITALS(
Pid VARCHAR(30) NOT NULL,
Visit_id VARCHAR(30) NOT NULL,
Height_in INT,
Weight_lb FLOAT,
Temp_f FLOAT,
Pulse INT,
Resp_rate INT,
Bp_syst INT,
Bp_diast INT,
SpO2 INT
);
sqlite> INSERT INTO VITALS (Pid, Visit_id, Height_in, Weight_lb, Temp_f, Pulse, Resp_rate, Bp_syst, Bp_diast, SpO2)
VALUES ('1','10001',70,188.4,98.6,95,18,154,94,97);
sqlite> INSERT INTO VITALS (Pid, Visit_id, Height_in, Weight_lb, Temp_f, Pulse, Resp_rate, Bp_syst, Bp_diast, SpO2)
VALUES ('1','10002',70,188.4,99.1,85,17,157,96,100);
sqlite> INSERT INTO VITALS (Pid, Visit_id, Height_in, Weight_lb, Temp_f, Pulse, Resp_rate, Bp_syst, Bp_diast, SpO2)
VALUES ('2','10003',63,130.2,98.7,82,16,120,81,100);
sqlite> INSERT INTO VITALS (Pid, Visit_id, Height_in, Weight_lb, Temp_f, Pulse, Resp_rate, Bp_syst, Bp_diast, SpO2)
VALUES ('3','10004',65,120.0,NULL,100,19,161,100,98);
sqlite> INSERT INTO VITALS (Pid, Visit_id, Height_in, Weight_lb, Temp_f, Pulse, Resp_rate, Bp_syst, Bp_diast, SpO2)
VALUES ('4','10005',66,211.4,98.2,95,19,143,84,93);
sqlite> INSERT INTO VITALS (Pid, Visit_id, Height_in, Weight_lb, Temp_f, Pulse, Resp_rate, Bp_syst, Bp_diast, SpO2)
VALUES ('5','10006',69,150.0,97.6,77,18,130,86,99);
查询集 #0f – 创建 MORT 表
要创建MORT表,使用以下代码:
sqlite> CREATE TABLE MORT(
Pid VARCHAR(30) NOT NULL,
Mortality_date DATE NOT NULL,
PRIMARY KEY (Pid)
);
sqlite> INSERT INTO MORT (Pid, Mortality_date)
VALUES ('1', '2016-05-15');
sqlite> INSERT INTO MORT (Pid, Mortality_date)
VALUES ('4', '2016-06-08');
查询集 #0g – 显示我们的表
为了确认某个表(例如,PATIENT)是否正确创建,我们可以使用SELECT * FROM PATIENT;查询(我们将在查询集 #2 中进一步解释该语法):
sqlite> SELECT * FROM PATIENT;
1 John A Smith 1952-01-01 1206 Fox Hollow Rd. Pittsburgh PA 15213 6789871234 M
2 Candice P Jones 1978-02-03 1429 Orlyn Dr. Los Angele CA 90024 3107381419 F
3 Regina H Wilson 1985-04-23 765 Chestnut Ln. Albany NY 12065 5184590206 F
4 Harold Lee 1966-11-15 2928 Policy St. Providence RI 02912 6593482691 M
5 Stan P Davis 1958-12-30 4271 12th St. Atlanta GA 30339 4049814933 M
查询集 #1 – 创建 MORT_FINAL 表
我们编写的第一个查询将使用CREATE TABLE语句创建表。在某种版本的CREATE TABLE语句中,每个变量都明确指定了其对应的数据类型。我们在前面的示例中使用了这种版本来从零开始创建我们的六个表。或者,也可以通过复制现有的表来创建一个新表。在这里我们选择第二种方式。
现在我们已经回答了这个问题,接下来还有第二个问题——我们应该从哪个表中复制数据?可能会有诱惑直接将来自PATIENT表的病人信息复制到我们的最终表格中,因为该表每行代表一个病人,且包含基本的病人信息。然而,我们必须记住,我们的用例是基于每次就诊,而不是病人。因此,如果一个病人有两次就诊(例如病人#1),从技术上讲,这个病人将会得到两个风险评分:每次就诊一个。于是,我们应该从VISIT表开始复制信息。这将创建一个包含六行的表格,每行代表一次就诊。
因此,我们通过使用CREATE TABLE子句开始查询,MORT_FINAL是我们新表的名称。然后我们使用AS关键字。接下来的两行查询指定了要复制哪些信息,使用的是SELECT-FROM-WHERE构造:
sqlite> CREATE TABLE MORT_FINAL AS
SELECT Visit_id, Pid, Attending_md, Visit_date, Pri_dx_icd, Sec_dx_icd
FROM VISIT;
SELECT-FROM-WHERE语句是一种系统化的方式,用来从表格中选择我们需要的信息。SELECT部分充当了列选择器——在SELECT关键字后面是我们希望复制到新表格中的列。请注意,我们省略了诊断名称(Pri_dx_name、Sec_dx_name),因为它们从技术上来说不是预测变量,只要我们有每个代码并能参考它们的含义即可。FROM关键字指定了我们希望从中复制数据的表格(在这种情况下是VISIT)。WHERE关键字是一个可选的子句,允许我们只选择那些符合特定条件的行。例如,如果我们只对那些病人有心力衰竭的就诊感兴趣,我们可以写WHERE Pri_dx_code == 'I50.9'。因为在本例中我们想包含所有就诊记录,所以这个查询不需要WHERE子句。我们将在下一个查询集看到WHERE子句的实际应用。
查询集 #2 – 向 MORT_FINAL 添加列
在这一部分,我们将演示两种添加额外列的方法。一种方法使用ALTER TABLE语句,而第二种方法使用JOIN操作。
查询集 #2a – 使用 ALTER TABLE 添加列
现在我们已经将来自VISIT表的信息填充到MORT_FINAL表中,接下来是时候开始整合其他表格了。我们将从PATIENT表开始;具体来说,我们想要将这个表中的出生日期和性别添加到我们的数据中。我们先从出生日期开始。
在查询集 #2 中,我们展示了为表格添加新列(出生日期)的基本查询模式。我们从ALTER TABLE语句开始,接着是表名,操作(在这种情况下是ADD COLUMN),新列的名称以及变量类型。尽管标准 SQL 支持使用DATE变量类型表示日期,但在 SQLite 中,我们使用TEXT类型。日期总是以YYYY-MM-DD格式指定。
在我们通过 ALTER TABLE 语句初始化新列后,下一步是从 PATIENT 表中填充实际的出生日期。为此,我们使用 UPDATE 语句。我们指定要更新的表,然后是一个 SET 语句和我们要修改的列名,后面跟着等号。
SELECT-FROM-WHERE 块是 SQL 语言的基本检索查询。我们正在尝试从 PATIENT 表中检索信息,并将其填充到新的 Bdate 列中,因此我们在等号后使用一个 SELECT-FROM-WHERE 语句,语句用括号括起来。可以把 SQL 语句看作是向数据库发出以下指令的 SELECT 语句:“对于 MORT_FINAL 表中的每一行,找到 PATIENT 表中 Pid 等于 MORT_FINAL 表中 Pid 的出生日期。”
在对 Bdate 列执行 UPDATE 语句后,我们使用相同的查询序列(ALTER TABLE 和 UPDATE)来从 PATIENT 表中检索 Sex 列:
sqlite> ALTER TABLE MORT_FINAL ADD COLUMN Bdate TEXT;
sqlite> UPDATE MORT_FINAL SET Bdate =
(SELECT P.Bdate
FROM PATIENT AS P
WHERE P.Pid = MORT_FINAL.Pid);
sqlite> ALTER TABLE MORT_FINAL ADD COLUMN Sex CHAR;
sqlite> UPDATE MORT_FINAL SET Sex =
(SELECT P.Sex
FROM PATIENT AS P
WHERE P.Pid = MORT_FINAL.Pid);
查询集 #2b – 使用 JOIN 添加列
虽然 ALTER TABLE 和 UPDATE 序列是逐一向表中添加列的好方法,但当你需要从同一表中复制多个列时,它可能会变得很繁琐。JOIN 操作为我们提供了一个第二个选项,可以从同一表中复制多个列。
在 JOIN 操作中,两个表会被合并生成一个单一的表。在下面的示例查询中,VITALS 表中选定的列会被附加到 MORT_FINAL 表的末尾。
然而,MORT_FINAL 表和 VITALS 表都包含多个行。那么查询如何知道每个表中的哪一行对应彼此呢?这可以通过 ON 子句来指定(在查询的末尾)。ON 子句表示:“当连接表时,合并那些访问 ID 相等的行。”因此,对于 MORT_FINAL 表的每一行,都将有且仅有一行 VISITS 表的行与其对应:该行具有相同的访问 ID。这是合理的,因为我们关心的是从每个单独的访问中收集信息,并将其放入各自的独立行中。
另一个关于 JOIN 的知识点是,标准 SQL 中有四种不同的 JOIN 类型:LEFT JOIN、RIGHT JOIN、INNER JOIN 和 OUTER JOIN。这里使用的是 LEFT JOIN(在 SQLite 中称为 LEFT OUTER JOIN);它表示:“对于第一个表的每一行(在本例中为 MORT_FINAL),添加对应的 VISIT 列,其中访问 ID 相等,如果在 VISIT 表中没有对应的访问 ID,则添加 NULL 值。”换句话说,第一个表的所有行都会被保留,无论第二个表中是否存在对应的行。那些在第二个表中有行但在第一个表中没有的访问将被丢弃。
在 RIGHT JOIN 中,情况正好相反:第二个表中独特的访问 ID 被保留,并与第一个表中相应的访问 ID 对齐。在第一个表中出现但在第二个表中缺失的访问 ID 会被丢弃。INNER JOIN 只会在最终结果中包括同时存在于两个表中的访问 ID。OUTER JOIN 包括两个表中的所有行,并用 NULL 值替换所有缺失的条目。需要注意的是,RIGHT JOIN 和 OUTER JOIN 在 SQLite 中不被支持。
那么为什么选择了 LEFT JOIN 呢?从根本上讲,我们的任务是为每个访问记录指定一个预测,无论该访问是否记录了生命体征。因此,MORT_FINAL表中的每个访问 ID 都应该出现在最终结果中,而 LEFT JOIN 确保这一点。
在以下代码中,我们看到通过使用 JOIN,只需要一个总查询就能将VITALS表的八个列添加进来。那么这种方法有哪些缺点呢?首先,注意到创建了一个新表:MORT_FINAL_2。我们不能将数据追加到旧的MORT_FINAL表中;必须创建一个新表。此外,注意到我们必须列出所有希望在最终结果中保留的列。在 SQL 中,星号()表示添加所有*列自两个表;我们本可以写成SELECT * FROM MORT_FINAL ...。然而,如果使用了星号,就会有重复的列(例如,Visit_id列会出现两次,因为它在两个表中都存在)。
然后,我们需要通过SELECT语句排除重复的列。尽管如此,当第二个表中有许多列需要合并到第一个表时,JOIN 仍然是非常有用的:
sqlite> CREATE TABLE MORT_FINAL_2 AS
SELECT M.Visit_id, M.Pid, M.Attending_md, M.Visit_date, M.Pri_dx_icd, M.Sec_dx_icd, M.Bdate, M.Sex, V.Height_in, V.Weight_lb, V.Temp_f, V.Pulse, V.Resp_rate, V.Bp_syst, V.Bp_Diast, V.SpO2
FROM MORT_FINAL AS M LEFT OUTER JOIN VITALS AS V ON M.Visit_id = V.Visit_id;
查询集 #3 – 日期处理 – 计算年龄
到目前为止,我们的MORT_FINAL_2表包含 16 列:6 列来自VISIT表,2 列来自PATIENT表,8 列来自VITALS表(你可以通过使用SELECT * FROM MORT_FINAL_2;命令来验证)。在这个查询集中,我们将其中一个变量,即出生日期变量,通过日期处理转化为可用的形式:我们计算了患者的年龄。
正如我们在查询集 #2a 中所说的那样,日期在 SQLite 中存储为 TEXT 类型,并采用 YYYY-MM-DD 格式。计算年龄需要调用 julianday() 函数两次。在 SQLite 中,julianday() 函数将 YYYY-MM-DD 格式的日期作为输入,并返回自公元前 4714 年 11 月 24 日 12:00 PM 以来的天数(以浮动小数形式)。单独来看,这个值可能不太有用,但当与另一个 julianday() 调用和减号结合使用时,它可以帮助我们找出两日期之间的天数差。接下来,我们计算就诊日期与出生日期之间的儒略日差,并将结果除以 365.25,以得到患者的年龄(单位:年)。我们还对结果应用 ROUND() 函数,将其四舍五入到小数点后两位(即 2 表示在最终括号闭合之前的位数):
sqlite> ALTER TABLE MORT_FINAL_2 ADD COLUMN Age_years REAL;
sqlite> UPDATE MORT_FINAL_2 SET Age_years =
ROUND((julianday(Visit_date) - julianday(Bdate)) / 365.25,2);
查询集 #4 – 分箱和聚合诊断
在我们的示例数据库中,VISIT 表格包含了就诊的诊断代码。尽管在我们的示例中这些诊断代码没有单独的表格,但它们是许多分析问题中最重要的信息之一。一方面,它们允许我们选择与模型相关的观测值。例如,如果我们正在构建一个预测恶性癌症的模型,我们需要诊断代码来告诉我们哪些患者患有癌症,并过滤掉其他患者。其次,它们通常是很好的预测变量(Futoma 等,2015)。例如,正如我们将在第七章 医疗健康预测模型的构建中看到的那样,许多慢性病大大增加了不良健康结果的可能性。显然,我们必须利用诊断代码中提供的信息来优化我们的预测模型。
我们将在这里介绍两种针对编码变量的转换。第一种转换,分箱,将分类变量转换为一系列二元变量,表示特定的诊断。第二种转换,聚合,将多个二元分箱变量组合为一个单一的二元或数值变量。这些转换不仅适用于诊断代码,还适用于程序、药物和实验室代码。以下是这两种转换的示例。
查询集 #4a – 针对充血性心力衰竭(CHF)的诊断分箱
在这里,我们可以看到充血性心力衰竭(CHF)诊断的分箱转换。首先,我们通过 ALTER TABLE 语句将新的列 Chf_dx 初始化为整数。DEFAULT 0 语句意味着所有行都被初始化为零。接着,如果 Pri_dx_icd 列或 Sec_dx_icd 列中有与 CHF 对应的代码,我们将该列值设置为 1:
sqlite> ALTER TABLE MORT_FINAL_2 ADD COLUMN Chf_dx INTEGER DEFAULT 0;
sqlite> UPDATE MORT_FINAL_2 SET Chf_dx = 1
WHERE Pri_dx_icd = 'I50.9' OR Sec_dx_icd = 'I50.9';
查询集 #4b – 针对其他疾病的诊断分箱
在这里,我们看到对于我们五名患者数据集中的每个诊断代码,都进行相同类型的转换。高血压、心绞痛、糖尿病和肺动脉高压的分箱查询如下:
sqlite> ALTER TABLE MORT_FINAL_2 ADD COLUMN Htn_dx INTEGER DEFAULT 0;
sqlite> UPDATE MORT_FINAL_2 SET Htn_dx = 1
WHERE Pri_dx_icd = 'I10' OR Sec_dx_icd = 'I10';
sqlite> ALTER TABLE MORT_FINAL_2 ADD COLUMN Angina_dx INTEGER DEFAULT 0;
sqlite> UPDATE MORT_FINAL_2 SET Angina_dx = 1
WHERE Pri_dx_icd = 'I20.9' OR Sec_dx_icd = 'I20.9';
sqlite> ALTER TABLE MORT_FINAL_2 ADD COLUMN Diab_dx INTEGER DEFAULT 0;
sqlite> UPDATE MORT_FINAL_2 SET Diab_dx = 1
WHERE Pri_dx_icd = 'E11.9' OR Sec_dx_icd = 'E11.9';
sqlite> ALTER TABLE MORT_FINAL_2 ADD COLUMN Pulm_htn_dx INTEGER DEFAULT 0;
sqlite> UPDATE MORT_FINAL_2 SET Pulm_htn_dx = 1
WHERE Pri_dx_icd = 'I27.0' OR Sec_dx_icd = 'I27.0';
查询集#4c – 使用 SUM 聚合心脏诊断
尽管分箱在区分个别诊断时非常重要,但在实践中,我们通常希望将相似或几乎相同的诊断代码组合成一个单一变量。聚合将两个或多个二元变量合并为一个二元/数值变量。在这里,我们使用+运算符将数据集中所有的心脏诊断代码(CHF、 hypertension 和 angina 是心脏病)进行聚合。结果是统计每个五名患者的心脏诊断总数:
sqlite> ALTER TABLE MORT_FINAL_2 ADD COLUMN Num_cardiac_dx INTEGER;
sqlite> UPDATE MORT_FINAL_2 SET Num_cardiac_dx = Chf_dx + Htn_dx + Angina_dx;
查询集#4d – 使用 COUNT 聚合心脏诊断
在查询集#4b 和#4c 中,我们使用+运算符对列名分别进行分箱和聚合三个诊断代码。然而,我们可能会对分箱和聚合数十个、数百个甚至数千个诊断代码感兴趣。查询集#4b 和#4c 的方法对于大规模聚合来说很快变得不切实际。
在这里,我们使用COUNT函数和补充表格来聚合表格中列出的诊断代码。我们首先使用CREATE TABLE语句创建一个CARDIAC_DX表格。这个CREATE TABLE语句的格式与查询集#1 中的略有不同。在那个示例中,我们只是通过从现有表格复制列来创建一个表格。这里,我们从头开始创建表格,包含括号、列名、变量类型和NOT NULL语句。如果有多个列,它们会在括号内用逗号分隔。
创建表格后,我们使用INSERT语句将三个诊断代码插入其中:I50.9、I10和I20.9。然后,我们在MORT_FINAL_2表中添加一个名为Num_cardiac_dx_v2的列。
最终查询通过在原始UPDATE语句中的每个列使用SELECT-FROM-WHERE块,更新Num_cardiac_dx_v2列,添加出现在Pri_dx_icd或Sec_dx_icd列中的代码数量。因此,这种类型的查询被称为嵌套查询。在每个SELECT块中,COUNT(*)语句简单地返回查询结果的行数作为整数。例如,在访问#10001 中,Pri_dx_icd列中有一个心脏代码,Sec_dx_icd列中也有一个匹配项。第一个SELECT块将返回值1,因为如果没有COUNT,查询将返回一行的表格。通过将COUNT包裹在*周围,返回1作为整数。第二个SELECT块也检测到一个匹配项并返回值1。+运算符使最终结果为2。通过比较Num_cardiac_dx和Num_cardiac_dx_2列,我们发现结果完全相同。那么,哪种方法更好呢?对于小型、简单的聚合,第一个方法更容易,因为只需要为每个代码创建一个列,然后在一个语句中使用+运算符进行聚合。然而,在实践中,您可能希望频繁编辑哪些代码被一起聚合以创建特征。在这种情况下,第二种方法更容易:
sqlite> CREATE TABLE CARDIAC_DX(
Dx_icd TEXT NOT NULL);
sqlite> INSERT INTO CARDIAC_DX (Dx_icd)
VALUES ('I50.9'),('I10'),('I20.9');
sqlite> ALTER TABLE MORT_FINAL_2 ADD COLUMN Num_cardiac_dx_v2 INTEGER;
sqlite> UPDATE MORT_FINAL_2 SET Num_cardiac_dx_v2 =
(SELECT COUNT(*)
FROM CARDIAC_DX AS C
WHERE MORT_FINAL_2.Pri_dx_icd = C.Dx_icd) +
(SELECT COUNT(*)
FROM CARDIAC_DX AS C
WHERE MORT_FINAL_2.Sec_dx_icd = C.Dx_icd);
查询集#5 – 统计药物
现在我们将转到药物部分。我们将添加一个功能,简单地统计每个患者所服用的药物数量。在查询集#5(如下所示)中,我们首先使用ALTER TABLE语句添加Num_meds列。然后,我们在UPDATE语句中使用SELECT-FROM-WHERE块来查找每个患者所服用的药物数量。该查询通过统计MORT_FINAL_2表中每个患者 ID 的行数,其中MEDICATIONS表中的相应患者 ID 相等。同样,我们使用COUNT函数来获取行数。在此查询中,我们引入了一个新函数DISTINCT。DISTINCT会删除任何包含括号内列的重复值的行。例如,如果LISINOPRIL对某个患者列出了两次,DISTINCT(Rx_name)函数调用将确保只计数一次:
sqlite> ALTER TABLE MORT_FINAL_2 ADD COLUMN Num_meds INTEGER;
sqlite> UPDATE MORT_FINAL_2 SET Num_meds =
(SELECT COUNT(DISTINCT(Rx_name))
FROM MEDICATIONS AS M
WHERE MORT_FINAL_2.Pid = M.Pid);
查询集#6 – 分类异常实验室结果
一些研究文章发现,实验室值是临床结果(如再入院)的重要预测因素(Donze 等,2013)。然而,实验室结果是有问题的,因为大多数患者缺失这些数据。没有一种实验室结果类型会出现在每个患者中;例如,在我们的例子中,并非每个患者在访问期间都进行了抽血检查。事实上,在我们数据中存在的三种不同类型的实验室检查中,最常见的检查是 BNP,六个患者中的四个进行了此检查。那么,我们该如何处理另外两个患者呢?
解决这一问题的一种方法是为异常结果的存在设置“标志”。在查询集#6 中实现了这一点,用于葡萄糖实验。第一个查询通过ALTER TABLE语句添加了Abnml_glucose列,接下来的查询将结果设置为每次患者访问时该特定实验值超过 200 的次数。注意多个AND子句;它们对于选择正确的患者、日期和感兴趣的实验是必要的。因此,只有异常结果的访问才会有大于零的值。请注意,我们使用CAST()函数将值从TEXT类型转换为FLOAT类型,再进行值的测试:
sqlite> ALTER TABLE MORT_FINAL_2 ADD COLUMN Abnml_glucose INTEGER;
sqlite> UPDATE MORT_FINAL_2 SET Abnml_glucose =
(SELECT COUNT(*) FROM LABS AS L
WHERE MORT_FINAL_2.Pid = L.Pid
AND MORT_FINAL_2.Visit_date = L.Lab_date
AND L.Lab_name = 'Glucose'
AND CAST(L.Lab_value AS FLOAT) >= 200);
虽然这解决了缺失实验数据的问题,但该方法的局限性在于它将缺失结果和正常结果视为相同。在查询集#7 中,我们将研究填补缺失值的基本方法。
查询集#7 – 填补缺失的变量
虽然查询集#6 中呈现的方法解决了实验室缺失数据的问题,但实际实验值中的所有信息都被丢弃了。例如,对于 BNP,只有两名患者没有值,而对于温度这一生命体征,只有一名患者缺失。
一些先前的研究已经实验过这一原理,并且在使用预测模型时取得了良好的效果。在(Donze 等,2013)中,一些患者的出院数据(约 1%)存在缺失。这些数据通过假设其在正常范围内来填补。
在 SQL 中,单一填补可以轻松实现。我们在这里演示这一点。
查询集#7a – 使用常规范围填补缺失的温度值
在这里,我们使用UPDATE语句将温度变量设置为98.6,用于填补缺失值:
sqlite> UPDATE MORT_FINAL_2 SET Temp_f = 98.6
WHERE Temp_f IS NULL;
查询集#7b – 使用均值填补缺失的温度值
在这里,我们使用均值填补而不是常规值填补来填补缺失的温度值。因此,查询集#7a 中的98.6值被一个SELECT-FROM-WHERE语句替换,该语句在温度变量(在此为98.4)不缺失的地方找到均值。AVG()函数返回一组值的平均值。AVG()函数和类似的函数(如MIN()、MAX()、COUNT()、SUM()等)被称为聚合函数,因为它们描述了通过一个单一值对一组值进行聚合的操作:
sqlite> UPDATE MORT_FINAL_2 SET Temp_f =
(SELECT AVG(Temp_f)
FROM MORT_FINAL_2
WHERE Temp_f IS NOT NULL)
WHERE Temp_f IS NULL;
查询集#7c – 使用均匀分布填补缺失的 BNP 值
虽然在我们的示例中填补单个缺失的温度值并不困难,但填补两个缺失的 BNP 值却存在多个问题:
-
缺失 BNP 值的访问比例较高。
-
虽然正常体温范围简单地是 98.6,但 BNP 有一个范围较大的正常值,介于 100 - 400 pg/mL 之间。在进行常规值填补时,我们如何选择要填补的值?
-
我们数据集中 BNP 值的均值为 462.5,实际上是异常的。这意味着如果我们对这个变量进行均值填补,我们将为所有没有抽血的患者填补一个异常值,这是一个极不可能的情景。
虽然这个问题没有完美的答案,但如果我们尝试恢复原始的 BNP 值(这意味着填补缺失值),在这个查询集中,我们会从正常范围的均匀分布中填补缺失值:
sqlite> ALTER TABLE MORT_FINAL_2 ADD COLUMN Raw_BNP INTEGER;
sqlite> UPDATE MORT_FINAL_2 SET Raw_BNP =
(SELECT CAST(Lab_value as INTEGER)
FROM LABS AS L
WHERE MORT_FINAL_2.Pid = L.Pid
AND MORT_FINAL_2.Visit_date = L.Lab_date
AND L.Lab_name = 'Natriuretic peptide B');
sqlite> UPDATE MORT_FINAL_2 SET Raw_BNP =
ROUND(ABS(RANDOM()) % (300 - 250) + 250)
WHERE Raw_BNP IS NULL;
查询集 #8 – 添加目标变量
我们的表格几乎完成了。我们已经处理了所有数据。剩下要添加的就是目标变量。请看以下内容:
sqlite> ALTER TABLE MORT_FINAL_2 ADD COLUMN Mortality INTEGER DEFAULT 0;
sqlite> UPDATE MORT_FINAL_2 SET Mortality =
(SELECT COUNT(*)
FROM MORT AS M
WHERE M.Pid = MORT_FINAL_2.Pid
AND julianday(M.Mortality_date) -
julianday(MORT_FINAL_2.Visit_date) < 180);
查询集 #9 – 可视化 MORT_FINAL_2 表
为了可视化我们的最终结果,我们可以执行以下操作:
sqlite> .headers on
sqlite> SELECT * FROM MORT_FINAL_2;
Visit_id|Pid|Attending_md|Visit_date|Pri_dx_icd|Sec_dx_icd|Bdate|Sex|Height_in|Weight_lb|Temp_f|Pulse|Resp_rate|Bp_syst|Bp_diast|SpO2|Age_years|Chf_dx|Htn_dx|Angina_dx|Diab_dx|Pulm_htn_dx|Num_cardiac_dx|Num_cardiac_dx_v2|Num_meds|Abnml_glucose|Raw_BNP|Mortality
10001|1|JOHNSON|2016-01-09|I50.9|I10|1952-01-01|M|70|188.4|98.6|95|18|154|94|97|64.02|1|1|0|0|0|2|2|2|0|350|1
10002|1|JOHNSON|2016-01-17|I50.9|I10|1952-01-01|M|70|188.4|99.1|85|17|157|96|100|64.04|1|1|0|0|0|2|2|2|0|266|1
10003|2|WU|2016-01-15|I20.9|E11.9|1978-02-03|F|63|130.2|98.7|82|16|120|81|100|37.95|0|0|1|1|0|1|1|4|1|100|0
10004|3|JOHNSON|2016-02-01|I10||1985-04-23|F|65|120.0|98.44|100|19|161|100|98|30.78|0|1|0|0|0|1|1|1|0|291|0
10005|4|WU|2016-02-27|I27.0|I50.9|1966-11-15|M|66|211.4|98.2|95|19|143|84|93|49.28|1|0|0|0|1|1|1|3|0|1000|1
10006|5|WU|2016-03-01|I50.9|E11.9|1958-12-30|M|69|150.0|97.6|77|18|130|86|99|57.17|1|0|0|1|0|1|1|3|1|400|0
总结
在本章中,我们学习了如何使用 SQL 以数据库格式处理医疗保健数据。我们下载并安装了 SQLite,并编写了一些 SQL 查询,以便将数据转化为我们希望的模型格式。
接下来,在 第五章,计算基础 – Python 介绍,我们将继续讨论计算基础,探索 Python 编程语言。
参考文献与进一步阅读
Basole RC, Braunstein ML, Kumar V, Park H, Kahng M, Chau DH, Tamersoy A, Hirsh DA, Serban N, BostJ, Lesnick B, Schissel BL, Thompson M (2015)。在急诊科使用可视化分析理解儿科哮喘护理过程中的变异性。 Journal of the American Medical Informatics Association 22(2): 318–323, doi.org/10.1093/jamia/ocu016.
Donze J, Aujesky D, Williams D, Schnipper JL (2013). 可避免的 30 天住院再入院:医学患者的预测模型的推导与验证。 JAMA Intern Med 173(8): 632-638。
Elmasri R, Navathe S (2010)。数据库系统基础,第 6 版。波士顿,MA:Addison Wesley。
Futoma J, Morris J, Lucas J (2015)。预测早期住院再入院的模型比较。 Journal of Biomedical Informatics 56: 229-238。
Kasper DL, Braunwald E, Fauci AS, Hauser SL, Longo DL, Jameson JL (2005),主编。 Harrison's Principles of Internal Medicine, 第 16 版。纽约,NY:McGraw-Hill。
Rajkomar A, Oren E, Chen K, Dai AM, Hajaj N, Hardt M, 等 (2018)。使用电子健康记录进行可扩展且准确的深度学习。 npj Digital Medicine 1:18; doi:10.1038/s41746-018-0029-1。
Sahni N, Simon G, Arora R (2018). J Gen Intern Med 33: 921. doi.org/10.1007/s11606-018-4316-y
SQLite 首页。 www.sqlite.org/。访问时间:04/03/2017。
第五章:计算基础 – Python 入门
本章将介绍 Python 在分析中的应用。该部分主要面向对 Python 不熟悉的初学者程序员或开发人员。本章结束时,你将对 Python 基础语言的特性有基本的了解,这对于医疗分析和机器学习至关重要。你还将了解如何开始使用 pandas 和 scikit-learn,这两个 Python 数据分析的关键库。
如果你想跟随 Jupyter Notebook 操作,我们建议你参考第一章,医疗分析入门,来启动一个新的 Jupyter 会话。本章的笔记本也可以在书籍的官方代码库中在线获取。
变量和类型
Python 中的基本变量类型包括字符串和数字类型。在本节中,我们将介绍这两种类型。
字符串
在 Python 中,字符串是一种存储文本字符的变量类型,这些字符可以是字母、数字、特殊字符和标点符号。在 Python 中,我们使用单引号或双引号来表示一个变量是字符串,而不是数字:
var = 'Hello, World!'
print(var)
字符串不能用于数字的数学运算,但它们可以用于其他有用的操作,正如我们在以下示例中看到的:
string_1 = '1'
string_2 = '2'
string_sum = string_1 + string_2
print(string_sum)
上述代码的结果是打印字符串 '12',而不是 '3'。在 Python 中,+ 运算符对两个字符串进行操作时,执行的是拼接操作(将第二个字符串附加到第一个字符串的末尾),而不是相加。
其他作用于字符串的运算符包括 * 运算符(用于重复字符串
多次,例如,string_1 * 3)以及 < 和 > 运算符(用于比较字符串的 ASCII 值)。
要将数据从数字类型转换为字符串,我们可以使用 str() 方法。
由于字符串是字符序列,我们可以对其进行索引和切片(就像我们对其他数据容器所做的那样,稍后你会看到)。切片是字符串的连续部分。为了索引/切片它们,我们使用方括号中的整数来表示字符的位置:
test_string = 'Healthcare'
print(test_string[0])
输出如下所示:
H
要切片字符串,我们需要在方括号中包含起始位置和结束位置,二者用冒号隔开。请注意,结束位置会包含所有字符,直到但不包括该结束位置,正如我们在以下示例中看到的:
print(test_string[0:6])
输出如下:
Health
前面我们提到了 str() 方法。字符串有很多其他方法。它们的完整列表可以在在线 Python 文档中查看,网址是 www.python.org。这些方法包括大小写转换、查找特定子字符串和去除空格等操作。这里我们将讨论另外一个方法——split() 方法。split() 方法作用于字符串,并接受一个 separator 参数。
输出是一个字符串列表;列表中的每个项目是原始字符串的一个组成部分,按separator分隔开。这对于解析由标点符号(如,或;)分隔的字符串非常有用。我们将在下一节讨论列表。下面是split()方法的示例:
test_split_string = 'Jones,Bill,49,Atlanta,GA,12345'
output = test_split_string.split(',')
print(output)
输出结果如下:
['Jones', 'Bill', '49', 'Atlanta', 'GA', '12345']
数值类型
在 Python 中,最常用于分析的两种数值类型是整数和浮点数。要将数据转换为这些类型,可以分别使用int()和float()函数。常见的数值操作都可以通过常用运算符来实现:+、-、*、/、<和>。包含特殊数值方法的模块,如math和random,对于分析尤其有用。更多有关数值类型的信息,可以参考在线 Python 文档(参见上一节中的链接)。
请注意,在某些版本的 Python 中,使用/运算符对两个整数进行除法运算时,会执行向下取整除法(即忽略小数点后的部分);例如,10/4会等于2,而不是2.5。这是一个隐蔽而严重的错误,可能会影响数值计算。然而,在本书中使用的 Python 版本里,我们不需要担心这个问题。
布尔类型是一个特殊的整数类型,用于表示True和False值。要将整数转换为布尔类型,可以使用bool()函数。零会被转换为False;其他任何整数都会被转换为True。布尔变量的行为像 1(True)和 0(False),不过在转换为字符串时,它们分别返回True和False。
数据结构和容器
在上一节中,我们讲解了存储单一值的变量类型。接下来,我们将讨论能够存储多个值的数据结构。这些数据结构包括列表、元组、字典和集合。在 Python 中,列表和元组通常被称为序列。在本书中,我们将“数据结构”和“数据容器”这两个术语互换使用。
列表
列表是一个广泛使用的数据结构,可以包含多个值。我们来看一下列表的一些特点:
-
要创建一个列表,我们使用方括号
[]。示例:
my_list = [1, 2, 3]。 -
列表可以包含任意组合的数值类型、字符串、布尔类型、元组、字典,甚至其他列表。
示例:
my_diverse_list = [51, 'Health', True, [1, 2, 3]]。 -
列表和字符串一样,都是序列,支持索引和切片操作。
例如,在上面的示例中,
my_diverse_list[0]会等于51。my_diverse_list[0:2]会等于[51, 'Health']。要访问嵌套列表中的3,我们可以使用my_diverse_list[3][2]。 -
列表是可变的(不同于字符串和元组),这意味着我们可以通过索引来更改单个元素。
例如,如果我们输入了
my_diverse_list[2] = False命令,那么我们的新my_diverse_list将等于[51, 'Health', False, [1, 2, 3]]。
列表在数据分析中的显著优势包括其丰富的辅助方法,如 append()、extend() 和 join(),以及它们与 pandas 和 numpy 数据结构的互换性。
元组
元组类似于列表。要创建元组,我们使用圆括号 ()。示例:my_tuple = (1, 2, 3)。元组与列表的主要区别在于元组是 不可变的,因此我们不能更改元组中的任何元素。如果我们尝试 my_tuple[0] = 4,则会抛出错误。由于它们的值是不可变的,元组在设置常量变量时非常有用。
字典
字典是 Python 中常见的数据结构。它用于存储从键到值的单向映射。例如,如果我们想创建一个字典来存储病人姓名及其对应的房间号,我们可以使用以下代码:
rooms = {
'Smith': '141-A',
'Davis': '142',
'Williams': '144',
'Johnson': '145-B'
}
让我们更详细地讨论一下前面的代码片段:
-
rooms字典中的名称被称为 键。字典中的键必须是唯一的。要访问它们,我们可以使用keys()函数,rooms.keys()。 -
rooms字典中的房间号被称为 值。要访问所有值,我们可以使用values()函数,rooms.values()。要访问单个值,我们只需提供其键的名称,用方括号括起来。例如,rooms['Smith']将返回'141-A'。因此,我们可以说字典将键映射到其值。 -
要访问包含每个键及其对应值的嵌套元组列表,我们可以使用
items()函数,rooms.items()。 -
字典的值不一定只是字符串;事实上,值可以是任何数据类型/结构。键可以是特定的变量,例如整数或字符串。虽然值是可变的,但键是不可变的。
-
字典没有固有的顺序,因此不支持按数字索引和切片操作。
集合
虽然在 Python 中集合不像它的流行表亲列表那样受到关注,但集合在数据分析中扮演着重要角色,因此我们在这里包括它们。要创建集合,我们使用内置的 set() 函数。关于集合,你需要知道三件事:
-
它们是不可变的
-
它们是无序的
-
集合的元素是唯一的
因此,如果你熟悉基础集合论,Python 中的集合与其数学对应物非常相似。集合方法也复制了典型的集合操作,包括 union()、intersection()、add() 和 remove()。当你想对数据结构(如列表或元组)执行典型的集合操作时,这些函数会派上用场,前提是将其转换为集合。
Python 编程– 通过示例说明
在前面的章节中,我们讨论了变量类型和数据容器。Python 编程中还有很多其他方面,如 if/else 语句、循环和推导式的控制流;函数;以及类和面向对象编程。通常,Python 程序会被打包成模块,即独立的脚本,可以通过命令行运行执行计算任务。
让我们通过一个“模块”来介绍一些 Python 的概念(你可以使用 Jupyter Notebook 来实现):
from math import pow
LB_TO_KG = 0.453592
IN_TO_M = 0.0254
class Patient:
def __init__(self, name, weight_lbs, height_in):
self.name = name
self.weight_lbs = weight_lbs
self.weight_kg = weight_lbs * LB_TO_KG
self.height_in = height_in
self.height_m = height_in * IN_TO_M
def calculate_bmi(self):
return self.weight_kg / pow(self.height_m, 2)
def get_height_m(self):
return self.height_m
if __name__ == '__main__':
test_patients = [
Patient('John Smith', 160, 68),
Patient('Patty Johnson', 180, 73)
]
heights = [patient.get_height_m() for patient in test_patients]
print(
"John's height: ", heights[0], '\n',
"Patty's height: ", heights[1], '\n',
"John's BMI: ", test_patients[0].calculate_bmi(), '\n',
"Patty's BMI: ", test_patients[1].calculate_bmi()
)
当你运行这段代码时,你应该会看到以下输出:
John's height: 1.7271999999999998
Patty's height: 1.8541999999999998
John's BMI: 24.327647271211504
Patty's BMI: 23.74787410486812
上述代码是一个 Python 模块,打印出两个虚拟患者的身高和体重指数(BMI)。让我们更详细地看看这段代码的每个元素:
-
代码块的第一行是导入语句。这使我们能够导入其他模块中已编写的函数和类,这些模块可能是与 Python 一起分发的、开源软件编写的,或者是我们自己编写的。模块可以简单地理解为一个包含 Python 函数、常量和/或类的文件,它的扩展名为
.py。要导入整个模块,我们只需使用import关键字后跟模块名称,例如import math。请注意,我们还使用了from关键字,因为我们只想导入特定的函数——pow()函数。这样也避免了每次想计算幂时都需要输入math.pow()的麻烦。 -
接下来的两行包含了我们将用来进行单位转换的常量。常量通常用大写字母表示。
-
接下来,我们定义了一个
Patient类,包含一个构造函数和两个方法。构造函数接受三个参数——姓名、身高和体重——并将特定Patient实例的三个属性设置为这些值。它还将体重从磅转换为千克,将身高从英寸转换为米,并将这些值存储在两个额外的属性中。 -
这两个方法被编码为函数,使用
def关键字。calculate_bmi()返回患者的 BMI,而get_height()则简单地返回身高(以米为单位)。 -
接下来,我们有一个简短的
if语句。这个if语句的作用是:只有在作为命令行调用的主模块时才执行后续代码。其他if语句可能包含多个elif子句,还可以包含一个最终的else子句。 -
接下来,我们创建了一个包含两位患者的信息的列表,分别是 John Smith 和 Patty Johnson,以及他们的身高和体重数据。
-
下一行使用了列表推导式来创建两个患者的身高列表。推导式在 Python 编程中非常流行,也可以用于字典操作。
-
最后,我们的
print语句会将四个数字作为输出(两个身高和两个 BMI 值)。
本章末尾提供了更多关于基础 Python 编程语言的参考资料。你也可以访问在线文档 www.python.org。
pandas 简介
到目前为止,我们讨论的几乎所有功能都是 基础 Python 的功能;也就是说,使用这些功能不需要额外的包或库。事实是,本书中我们编写的大部分代码将涉及几个常用于分析的 外部 Python 包。pandas 库(pandas.pydata.org)是后续编程章节的核心部分。pandas 在机器学习中的功能有三方面:
-
从平面文件导入数据到你的 Python 会话
-
使用 pandas DataFrame 及其函数库来整理、操作、格式化和清洗数据
-
将数据从你的 Python 会话导出到平面文件
让我们逐一回顾这些功能。
平面文件是存储与医疗相关数据的常用方式(还有 HL7 格式,本书不涉及)。平面文件是数据的文本文件表示形式。使用平面文件,数据可以像数据库一样以行和列的形式表示,不同的是,标点符号或空白字符用作列的分隔符,而回车符则用作行的分隔符。我们将在第七章,在医疗领域创建预测模型 中看到一个平面文件的示例。
pandas 允许我们从各种其他 Python 结构和平面文件中导入数据到一个表格化的 Python 数据结构,称为 DataFrame,包括 Python 字典、pickle 对象、逗号分隔值(csv)文件、定宽格式(fwf)文件、Microsoft Excel 文件、JSON 文件、HTML 文件,甚至是 SQL 数据库表。
一旦数据进入 Python,你可以使用一些附加功能来探索和转换数据。需要对某一列执行数学运算,比如求和吗?需要执行类似 SQL 的操作,如 JOIN 或添加列(请参阅第三章,机器学习基础)?需要按条件过滤行吗?这些功能都可以通过 pandas 的 API 实现。我们将在第六章,衡量医疗质量 和第七章,在医疗领域创建预测模型 中充分利用 pandas 的一些功能。
最后,当我们完成数据探索、清洗和整理后,如果我们愿意,可以选择将数据导出为列出的多种格式之一。或者我们可以将数据转换为 NumPy 数组并训练机器学习模型,正如我们在本书后面会做的那样。
什么是 pandas DataFrame?
pandas DataFrame可以看作是一种二维的、类似矩阵的数据结构,由行和列组成。pandas DataFrame 类似于 R 中的 dataframe 或 SQL 中的表。与传统矩阵和其他 Python 数据结构相比,它的优势包括可以在同一 DataFrame 中包含不同类型的列、提供广泛的预定义函数以便于数据操作,以及支持快速转换为其他文件格式(包括数据库、平面文件格式和 NumPy 数组)的单行接口(便于与 scikit-learn 的机器学习功能集成)。因此,pandas确实是连接许多机器学习管道的粘合剂,从数据导入到算法应用。
pandas 的局限性包括较慢的性能以及缺乏内建的并行处理功能。因此,如果你正在处理数百万或数十亿个数据点,Apache Spark(spark.apache.org/)可能是一个更好的选择,因为它的语言内置了并行处理功能。
导入数据
在本节中,我们演示了如何通过字典、平面文件和数据库将数据加载到 Python 中。
从 Python 数据结构导入数据到 pandas
使用pandas DataFrame 的第一步是通过pandas构造函数DataFrame()来创建一个 DataFrame。构造函数接受多种 Python 数据结构作为输入。它还可以接收 NumPy 数组和 pandas 的Series,Series 是另一种一维的pandas数据结构,类似于列表。这里我们演示如何将一个字典的列表转换为 DataFrame:
import pandas as pd
data = {
'col1': [1, 2, 3],
'col2': [4, 5, 6],
'col3': ['x', 'y', 'z']
}
df = pd.DataFrame(data)
print(df)
输出如下:
col1 col2 col3
0 1 4 x
1 2 5 y
2 3 6 z
从平面文件导入数据到 pandas
因为医疗保健数据通常采用平面文件格式,如.csv或.fwf,所以了解read_csv()和read_fwf()函数非常重要,这两个函数分别用于将数据从这两种格式导入pandas。这两个函数都需要作为必需参数提供平面文件的完整路径,并且还有十多个可选参数,用于指定诸如列的数据类型、标题行、要包含在 DataFrame 中的列等选项(完整的函数参数列表可以在线查看)。通常,最简单的方法是将所有列导入为字符串类型,然后再将列转换为其他数据类型。在下面的示例中,使用read_csv()函数从一个包含一个标题行(row #0)的平面.csv文件中读取数据,并创建一个名为data的 DataFrame:
pt_data = pd.read_csv(data_full_path,header=0,dtype='str')
因为定宽文件没有显式的字符分隔符,read_fwf()函数需要一个额外的参数widths,它是一个整数列表,指定每一列的宽度。widths的长度应该与文件中的列数匹配。作为替代,colspecs参数接收一个元组列表,指定每列的起始点和终止点:
pt_data = pd.read_fwf(source,widths=data_widths,header=None,dtype='str')
从数据库导入数据到 pandas
pandas库还支持从 SQL 数据库直接导入表格的函数。这些函数包括read_sql_query()和read_sql_table()。在使用这些函数之前,必须先建立与数据库的连接,以便将其传递给函数。以下示例展示了如何使用read_sql_query()函数将 SQLite 数据库中的表读取到 DataFrame 中:
import sqlite3
conn = sqlite3.connect(pt_db_full_path)
table_name = 'TABLE1'
pt_data = pd.read_sql_query('SELECT * from ' + table_name + ';',conn)
如果你希望连接到标准数据库,如 MySQL 数据库,代码将类似,唯一不同的是连接语句,它将使用针对 MySQL 数据库的相应函数。
DataFrame 的常见操作
在本节中,我们将介绍一些对执行分析有用的 DataFrame 操作。有关更多操作的描述,请参阅官方的 pandas 文档,网址为pandas.pydata.org/。
添加列
添加列是数据分析中常见的操作,无论是从头开始添加新列还是转换现有列。这里我们将介绍这两种操作。
添加空白列或用户初始化的列
要添加一个新的 DataFrame 列,你可以在 DataFrame 名称后加上新列的名称(用单引号和方括号括起来),并将其设置为你喜欢的任何值。要添加一个空字符串或整数的列,你可以将列设置为""或numpy.nan,后者需要事先导入numpy。要添加一个零的列,可以将列设置为0。以下示例说明了这些要点:
df['new_col1'] = ""
df['new_col2'] = 0
print(df)
输出如下:
col1 col2 col3 new_col1 new_col2
0 1 4 x 0
1 2 5 y 0
2 3 6 z 0
通过转换现有列添加新列
在某些情况下,你可能希望添加一个新列,该列是现有列的函数。在以下示例中,新的列example_new_column_3作为现有列old_column_1和old_column_2的和被添加。axis=1参数表示你希望对列进行横向求和,而不是对列进行纵向求和:
df['new_col3'] = df[[
'col1','col2'
]].sum(axis=1)
print(df)
输出如下:
col1 col2 col3 new_col1 new_col2 new_col3
0 1 4 x 0 5
1 2 5 y 0 7
2 3 6 z 0 9
以下第二个示例使用 pandas 的apply()函数完成类似的任务。apply()是一个特殊的函数,因为它允许你将任何函数应用于 DataFrame 中的列(包括你自定义的函数):
old_column_list = ['col1','col2']
df['new_col4'] = df[old_column_list].apply(sum, axis=1)
print(df)
输出如下:
col1 col2 col3 new_col1 new_col2 new_col3 new_col4
0 1 4 x 0 5 5
1 2 5 y 0 7 7
2 3 6 z 0 9 9
删除列
要删除列,可以使用 pandas 的drop()函数。它接受单个列名或列名列表,在此示例中,额外的可选参数指示沿哪个轴删除列,并且是否在原地删除列:
df.drop(['col1','col2'], axis=1, inplace=True)
print(df)
输出如下:
col3 new_col1 new_col2 new_col3 new_col4
0 x 0 5 5
1 y 0 7 7
2 z 0 9 9
对多个列应用函数
要对 DataFrame 中的多个列应用函数,可以使用for循环遍历列的列表。在以下示例中,预定义的列列表从字符串类型转换为数字类型:
df['new_col5'] = ['7', '8', '9']
df['new_col6'] = ['10', '11', '12']
for str_col in ['new_col5','new_col6']:
df[[str_col]] = df[[str_col]].apply(pd.to_numeric)
print(df)
这是输出结果:
col3 new_col1 new_col2 new_col3 new_col4 new_col5 new_col6
0 x 0 5 5 7 10
1 y 0 7 7 8 11
2 z 0 9 9 9 12
合并 DataFrame
DataFrame 也可以彼此组合,只要它们在合并轴上有相同数量的条目。在此示例中,两个 DataFrame 被垂直连接(例如,它们包含相同数量的列,行按顺序堆叠)。DataFrame 也可以水平连接(如果它们包含相同数量的行),通过指定axis参数来完成。请注意,列名和行名应在所有 DataFrame 之间相互对应;如果不对应,则会形成新的列,并为任何缺失的值插入 NaN。
首先,我们创建一个新的 DataFrame 名称,df2:
df2 = pd.DataFrame({
'col3': ['a', 'b', 'c', 'd'],
'new_col1': '',
'new_col2': 0,
'new_col3': [11, 13, 15, 17],
'new_col4': [17, 19, 21, 23],
'new_col5': [7.5, 8.5, 9.5, 10.5],
'new_col6': [13, 14, 15, 16]
});
print(df2)
输出结果如下:
col3 new_col1 new_col2 new_col3 new_col4 new_col5 new_col6
0 a 0 11 17 7.5 13
1 b 0 13 19 8.5 14
2 c 0 15 21 9.5 15
3 d 0 17 23 10.5 16
接下来,我们进行连接操作。我们将可选的ignore_index参数设置为True,以避免重复的行索引:
df3 = pd.concat([df, df2] ignore_index = True)
print(df3)
输出结果如下:
col3 new_col1 new_col2 new_col3 new_col4 new_col5 new_col6
0 x 0 5 5 7.0 10
1 y 0 7 7 8.0 11
2 z 0 9 9 9.0 12
3 a 0 11 17 7.5 13
4 b 0 13 19 8.5 14
5 c 0 15 21 9.5 15
6 d 0 17 23 10.5 16
将 DataFrame 列转换为列表
要将列的内容提取到列表中,可以使用tolist()函数。转换为列表后,数据可以通过for循环和推导式进行迭代:
my_list = df3['new_col3'].tolist()
print(my_list)
输出结果如下:
[5, 7, 9, 11, 13, 15, 17]
获取和设置 DataFrame 值
pandas库提供了两种主要方法来选择性地获取和设置 DataFrame 中的值:loc和iloc。loc方法主要用于基于标签的索引(例如,使用索引/列名识别行/列),而iloc方法主要用于基于整数的索引(例如,使用行/列在 DataFrame 中的整数位置来识别)。您希望访问的行和列的具体标签/索引通过方括号紧跟在 DataFrame 名称后面提供,行标签/索引位于列标签/索引之前,并由逗号分隔。让我们来看一些示例。
使用基于标签的索引(loc)获取/设置值
DataFrame 的.loc属性用于通过条目的标签选择值。它可以用于从 DataFrame 中检索单个标量值(使用行列的单个字符串标签),或从 DataFrame 中检索多个值(使用行/列标签的列表)。还可以将单索引和多索引结合使用,从单行或单列中获取多个值。以下代码行演示了如何从df DataFrame 中检索单个标量值:
value = df3.loc[0,'new_col5']
print(value)
输出结果为7.0。
也可以使用.loc属性和等号设置单个/多个值:
df3.loc[[2,3,4],['new_col4','new_col5']] = 1
print(df3)
输出结果如下:
col3 new_col1 new_col2 new_col3 new_col4 new_col5 new_col6
0 x 0 5 5 7.0 10
1 y 0 7 7 8.0 11
2 z 0 9 1 1.0 12
3 a 0 11 1 1.0 13
4 b 0 13 1 1.0 14
5 c 0 15 21 9.5 15
6 d 0 17 23 10.5 16
使用基于整数标签的 iloc 获取/设置值
.iloc属性与.loc属性非常相似,只是它使用被访问的行和列的整数位置,而不是它们的标签。在以下示例中,第 101 行(不是第 100 行,因为索引从 0 开始)和第 100 列的值被转移到scalar_value中:
value2 = df3.iloc[0,5]
print(value2)
输出结果为7.0。
请注意,与.loc类似,包含多个值的列表可以传递给.iloc属性,以一次性更改 DataFrame 中的多个条目。
使用切片获取/设置多个连续的值
有时,我们希望获取或设置的多个值恰好位于相邻(连续)的列中。在这种情况下,我们可以在方括号内使用切片来选择多个值。通过切片,我们指定希望访问的数据的起始点和终止点。我们可以在.loc和.iloc中使用切片,尽管使用整数和.iloc的切片更为常见。以下代码行展示了如何通过切片从 DataFrame 中提取部分内容(我们也可以使用等号进行赋值)。请注意,切片也可以用于访问列表和元组中的值(如本章之前所述):
partial_df3 = df3.loc[1:3,'new_col2':'new_col4']
print(partial_df3)
输出如下:
new_col2 new_col3 new_col4
1 0 7 7
2 0 9 1
3 0 11 1
使用at和iat快速获取/设置标量值
如果我们确定只希望获取/设置 DataFrame 中的单个值,可以使用 .at 和 .iat 属性,分别配合单一标签/整数。只需记住,.iloc 和 .iat 中的 i 代表“整数”:
value3 = df3.iat[3,3]
print(value3)
输出结果是11。
其他操作
另外两个常见的操作是使用布尔条件筛选行和排序行。这里我们将回顾每个操作。
使用布尔索引筛选行
到目前为止,我们已经讨论了如何使用标签、整数和切片来选择 DataFrame 中的值。有时,选择符合特定条件的某些行会更加方便。例如,如果我们希望将分析限制在年龄大于或等于 50 岁的人群中。
pandas DataFrame 支持布尔索引,即使用布尔值的向量进行索引,以指示我们希望包含哪些值,前提是布尔向量的长度等于 DataFrame 中的行数。由于涉及 DataFrame 列的条件语句正是这样,我们可以使用此类条件语句来索引 DataFrame。在以下示例中,df DataFrame 被筛选,只包括age列值大于或等于50的行:
df3_filt = df3[df3['new_col3'] > 10]
print(df3_filt)
输出如下:
col3 new_col1 new_col2 new_col3 new_col4 new_col5 new_col6
3 a 0 11 1 1.0 13
4 b 0 13 1 1.0 14
5 c 0 15 21 9.5 15
6 d 0 17 23 10.5 16
条件语句可以使用逻辑运算符如|或&进行链式连接。
排序行
如果你希望按某一列的值对 DataFrame 进行排序,可以使用 sort_values() 函数;只需将列名作为第一个参数传递即可。ascending是一个可选参数,允许你指定排序方向:
df3 = df3.sort_values('new_col4', ascending=True)
print(df3)
输出如下:
col3 new_col1 new_col2 new_col3 new_col4 new_col5 new_col6
2 z 0 9 1 1.0 12
3 a 0 11 1 1.0 13
4 b 0 13 1 1.0 14
0 x 0 5 5 7.0 10
1 y 0 7 7 8.0 11
5 c 0 15 21 9.5 15
6 d 0 17 23 10.5 16
类似 SQL 的操作
对于那些习惯于在 SQL 中处理异构类型表格的人来说,转向使用 Python 进行类似的分析可能看起来是一项艰巨的任务。幸运的是,有许多 pandas 函数可以结合使用,从而得到与常见 SQL 查询相似的结果,使用诸如分组和连接等操作。pandas 文档中甚至有一个子部分(pandas.pydata.org/pandas-docs/stable/comparison_with_sql.html)描述了如何使用 pandas DataFrame 执行类似 SQL 的操作。在本节中,我们提供了两个这样的示例。
获取聚合行计数
有时,您可能希望获取某一列中特定值的出现次数或统计。例如,您可能拥有一个医疗保健数据集,想要知道在患者就诊期间,特定支付方式被使用了多少次。在 SQL 中,您可以编写一个查询,使用 GROUP BY 子句与聚合函数(在本例中为 COUNT(*))结合,来获取支付方式的统计信息:
SELECT payment, COUNT(*)
FROM data
GROUP BY payment;
在pandas中,您可以通过将groupby()和size()函数链式调用来实现相同的结果:
tallies = df3.groupby('new_col4').size()
print(tallies)
输出如下:
new_col4
1 3
5 1
7 1
21 1
23 1
dtype: int64
连接 DataFrame
在第四章,计算基础 - 数据库中,我们讨论了使用 JOIN 操作合并来自两个数据库表的数据。要使用 JOIN 操作,您需要指定两个表的名称,以及 JOIN 的类型(左、右、外部或内部)和连接的列:
SELECT *
FROM left_table OUTER JOIN right_table
ON left_table.index = right_table.index;
在 pandas 中,您可以使用 merge() 或 join() 函数来实现表连接。默认情况下,join() 函数基于表的索引连接数据;但是,可以通过指定 on 参数来使用其他列。如果连接的两个表中有重复的列名,您需要指定 rsuffix 或 lsuffix 参数,以重命名列,使它们不再具有相同的名称:
df_join_df2 = df.join(df2, how='outer', rsuffix='r')
print(df_join_df2)
输出如下(请注意第 3 行中的NaN值,这是df中不存在的一行):
col3 new_col1 new_col2 new_col3 new_col4 new_col5 new_col6 col3r \
0 x 0.0 5.0 5.0 7.0 10.0 a
1 y 0.0 7.0 7.0 8.0 11.0 b
2 z 0.0 9.0 9.0 9.0 12.0 c
3 NaN NaN NaN NaN NaN NaN NaN d
new_col1r new_col2r new_col3r new_col4r new_col5r new_col6r
0 0 11 17 7.5 13
1 0 13 19 8.5 14
2 0 15 21 9.5 15
3 0 17 23 10.5 16
scikit-learn 简介
整本书都围绕scikit-learn(scikit-learn.org/stable/)展开。scikit-learn 库包含许多子模块。本书将只使用其中的一些子模块(在第七章,在医疗保健中创建预测模型)。例如,包括 sklearn.linear_model 和 sklearn.ensemble 子模块。在这里,我们将概述一些更常用的子模块。为了方便起见,我们已将相关模块分组为数据科学管道的各个部分,这些部分在第一章,医疗保健分析简介中讨论过。
示例数据
scikit-learn 在sklearn.datasets子模块中包含了几个示例数据集。至少有两个数据集,sklearn.datasets.load_breast_cancer和sklearn.datasets.load_diabetes,是与健康相关的。这些数据集已经预处理过,且规模较小,仅包含几十个特征和几百个患者。在第七章《医疗保健中的预测模型制作》中,我们使用的数据要大得多,且更像现代医疗机构提供的数据。然而,这些示例数据集对于实验 scikit-learn 功能仍然非常有用。
数据预处理
数据预处理功能存在于sklearn.preprocessing子模块中,其他相关功能在以下章节中讨论。
分类变量的独热编码
几乎每个数据集都包含一些分类数据。分类数据是离散数据,其中值可以取有限数量的可能值(通常编码为“字符串”)。由于 Python 的 scikit-learn 只能处理数值数据,因此在使用 scikit-learn 进行机器学习之前,我们必须找到其他方法来对分类变量进行编码。
使用独热编码,也称为1-of-K 编码方案,一个具有k个可能值的单一分类变量被转换为k个不同的二元变量,每个二元变量仅在该观测值的列值等于它所代表的值时为正。在第七章《医疗保健中的预测模型制作》中,我们提供了独热编码的详细示例,并使用 pandas 的get_dummies()函数对真实的临床数据集进行独热编码。scikit-learn 也有一个类可以用于执行独热编码,这个类是sklearn.preprocessing模块中的OneHotEncoder类。
关于如何使用OneHotEncoder的说明,可以访问 scikit-learn 文档:scikit-learn.org/stable/modules/preprocessing.html#encoding-categorical-features。
缩放和中心化
对于一些机器学习算法,通常建议不仅转换类别变量(使用之前讨论过的独热编码),还需要转换连续变量。回顾第一章,《医疗分析导论》中提到,连续变量是数值型的,可以取任何有理数值(尽管在许多情况下它们限制为整数)。一个特别常见的做法是标准化每个连续变量,使得变量的均值为零,标准差为一。例如,考虑AGE变量:它通常范围从 0 到 100 左右,均值可能约为 40。假设对于某个人群,AGE变量的均值为 40,标准差为 20。如果我们对AGE变量进行中心化和重缩放,年龄为 40 的人在转换后的变量中将表示为零。年龄为 20 岁的人将表示为-1,年龄为 60 岁的人将表示为 1,年龄为 80 岁的人将表示为 2,年龄为 50 岁的人将表示为 0.5。这种转换可以防止具有更大范围的变量在机器学习算法中被过度表示。
scikit-learn 有许多内置类和函数,用于数据的中心化和缩放,包括sklearn.preprocessing.StandardScaler()、sklearn.preprocessing.MinMaxScaler()和sklearn.preprocessing.RobustScaler()。这些不同的工具专门用于处理不同类型的连续数据,如正态分布变量或具有许多异常值的变量。
有关如何使用缩放类的说明,您可以查看 scikit-learn 文档:scikit-learn.org/stable/modules/preprocessing.html#standardization-or-mean-removal-and-variance-scaling。
二值化
二值化是另一种转换方法,它将连续变量转换为二进制变量。例如,如果我们有一个名为AGE的连续变量,我们可以通过设置年龄 50 岁为阈值,对年龄进行二值化,将 50 岁及以上的年龄设为 1,将低于 50 岁的年龄设为 0。二值化在处理有大量变量时节省了时间和内存;然而,实际上,原始的连续值通常表现得更好,因为它们包含更多的信息。
虽然也可以使用之前演示过的代码在 pandas 中执行二值化,但 scikit-learn 提供了一个Binarizer类,也可以用来对特征进行二值化。有关如何使用Binarizer类的说明,您可以访问scikit-learn.org/stable/modules/preprocessing.html#binarization。
填充
在第一章,医疗分析入门中,我们提到了处理缺失数据的重要性。插补是处理缺失值的一种策略,通过用基于现有数据估算的值来填补缺失值。在医疗领域,常见的两种插补方法是零插补,即将缺失数据视为零(例如,如果某一诊断值为 NULL,很可能是因为该信息没有出现在病历中);以及均值插补,即将缺失数据视为现有数据分布的均值(例如,如果某患者缺少年龄,我们可以将其插补为 40)。我们在第四章,计算基础——数据库中演示了各种插补方法,我们将在第七章,在医疗保健中构建预测模型中编写我们自己的插补函数。
Scikit-learn 提供了一个 Imputer 类来执行不同类型的插补。你可以在 scikit-learn.org/stable/modules/preprocessing.html#imputation-of-missing-values 查看如何使用它的详细信息。
特征选择
在机器学习中,常常有一种误解,认为数据越多越好。对于观测数据(例如,数据集中的行数),这种说法通常是正确的。然而,对于特征来说,更多的特征并不总是更好。在某些情况下,使用较少的特征可能反而表现得更好,因为多个高度相关的特征可能会对预测产生偏差,或者特征的数量超过了观测值的数量。
在其他情况下,性能可能与使用一半特征时相同,或者稍微差一点,但较少的特征可能因为多种原因而更为可取,包括时间考虑、内存可用性,或者便于向非技术相关人员解释和解释。无论如何,通常对数据进行特征选择是一个好主意。即使你不打算删除任何特征,进行特征选择并对特征重要性进行排序,也能为你提供对模型的深入洞察,帮助理解其预测行为和性能。
sklearn.feature_selection 模块中有许多类和函数是为特征选择而构建的,不同的类集合对应于不同的特征选择方法。例如,单变量特征选择涉及测量每个预测变量与目标变量之间的统计依赖性,这可以通过 SelectKBest 或 SelectPercentile 类等实现。VarianceThreshold 类移除在观测值中方差较低的特征,例如那些几乎总是为零的特征。而 SelectFromModel 类在模型拟合后,修剪那些不满足一定强度要求(无论是系数还是特征重要性)的特征。
要查看 scikit-learn 中所有特征选择类的完整列表,请访问scikit-learn.org/stable/modules/feature_selection.html#univariate-feature-selection。
机器学习算法
机器学习算法提供了一种数学框架,用于对新的观测值进行预测。scikit-learn 支持数十种不同的机器学习算法,这些算法具有不同的优缺点。我们将在这里简要讨论一些算法及其相应的 scikit-learn API 功能。我们将在第七章中使用这些算法,在医疗保健中构建预测模型。
广义线性模型
正如我们在第三章中讨论的,机器学习基础,线性模型可以简单地理解为特征的加权组合(例如加权和),用于预测目标值。特征由观测值决定;每个特征的权重由模型决定。线性回归预测连续变量,而逻辑回归可以看作是线性回归的扩展形式,其中预测的目标值经过logit 变换,转化为一个范围在零到一之间的变量。这种变换对于执行二分类任务非常有用,比如当有两个可能的结果时。
在 scikit-learn 中,这两种算法由 sklearn.linear_model.LogisticRegression 和 sklearn.linear_model.LinearRegression 类表示。我们将在第七章中演示逻辑回归,在医疗保健中构建预测模型。
集成方法
集成方法涉及使用不同机器学习模型的组合来进行预测。例如,随机森林是由多个决策树分类器组成的集合,这些树通过为每棵树选择和使用特定的特征集来实现去相关。此外,AdaBoost是一种算法,它通过在数据上拟合许多弱学习器来做出有效预测。这些算法由sklearn.ensemble模块提供支持。
其他机器学习算法
其他一些流行的机器学习算法包括朴素贝叶斯算法、k-近邻算法、神经网络、决策树和支持向量机。这些算法在 scikit-learn 中分别由sklearn.naive_bayes、sklearn.neighbors、sklearn.neural_network、sklearn.tree和sklearn.svm模块提供支持。在第七章 在医疗保健中构建预测模型中,我们将使用临床数据集构建神经网络模型。
性能评估
最后,一旦我们使用所需的算法构建了模型,衡量其性能就变得至关重要。sklearn.metrics模块对于这一点非常有用。如第三章 机器学习基础中所讨论的,混淆矩阵对于分类任务特别重要,并且由sklearn.metrics.confusion_matrix()函数支持。确定接收者操作特征(ROC)曲线并计算曲线下面积(AUC)可以分别通过sklearn.metrics.roc_curve()和sklearn.metrics.roc_auc_score()函数完成。精确率-召回率曲线是 ROC 曲线的替代方法,特别适用于不平衡数据集,并且由sklearn.metrics.precision_recall_curve()函数提供支持。
其他分析库
在这里,我们提到三个常用于分析的主要包:NumPy、SciPy 和 matplotlib。
NumPy 与 SciPy
NumPy (www.numpy.org) 是 Python 的矩阵库。通过使用numpy.array()及类似的构造,可以创建大型矩阵并对其进行各种数学操作(包括矩阵加法和乘法)。NumPy 还具有许多用于操作矩阵形状的函数。NumPy 的另一个特点是提供了熟悉的数学函数,如sin()、cos()和exp()。
SciPy (www.scipy.org) 是一个包含许多高级数学模块的工具箱。与机器学习相关的子包包括cluster、stats、sparse和optimize。SciPy 是一个重要的包,它使 Python 能够进行科学计算。
matplotlib
matplotlib (matplotlib.org) 是一个流行的 Python 二维绘图库。根据其官网介绍,用户“只需几行代码就可以生成图表、直方图、功率谱、条形图、误差图、散点图等。”它的绘图库提供了丰富的选项和功能,支持高度自定义。
总结
在本章中,我们快速浏览了基础的 Python 语言,以及两个在数据分析中非常重要的 Python 库:pandas 和 scikit-learn。我们现在已经完成了本书的基础章节。
在第六章《衡量医疗质量》中,我们将深入探讨一些真实的医疗服务提供者的表现数据,并使用 pandas 进行分析。
第六章:测量医疗质量
本章面向所有读者,旨在向你展示美国当前基于价值的项目下,医疗提供者是如何被评估和奖励/处罚的。我们将查看从网络下载的实际提供者数据示例,并使用 Python 对这些数据进行整理,以提取我们需要的信息。通过本章学习后,你将能够定位到感兴趣的项目中基于提供者的数据,并使用pandas进行操作,识别出表现优秀的提供者以及那些可能从分析解决方案中受益的提供者。
医疗措施简介
医疗措施是对提供者的护理活动进行的计算,旨在显示护理人员提供的质量水平。随着提供者越来越多地根据他们提供的服务质量而非数量获得奖励,医疗措施在决定哪些护理提供者会受到奖励或处罚方面发挥着越来越重要的作用。医疗保险和医疗补助服务中心(CMS)是美国的联邦级机构之一,发布标准化的医疗措施;此外,各州也会发布相应的措施。提供者使用自己患者的数据计算这些措施,然后将计算结果提交给发布机构进行审计。结果在一定程度上决定了提供者从机构获得的报销金额。
医疗中的典型措施通常是与护理质量相关的比率或百分比。一个措施通常由两个部分组成:分子和分母。分母是指在特定时间范围内,提供者所接诊的符合资格的患者群体或接诊次数的量化。确定分母通常需要通过应用纳入标准和/或排除标准来筛选整个提供者群体,进而得到所需的测量群体或接诊池。一旦确定了分母,分子便是根据分母中获得某些积极或消极结果或事件的项数来计算的。
这个结果或事件通常由基础和/或临床研究建议作为患者护理的推荐部分(或不良护理的标志)。最后,将分子除以分母得到最终的百分比。这个百分比可以单独使用,也可以与其他措施结合,融入更复杂的公式和加权方案,以确定总体质量分数。
例如,一个机构希望评估某州门诊设施提供的糖尿病护理质量,可以通过调查文献中的糖尿病护理建议开始制定衡量标准。除其他事项外,糖尿病患者应该每年接受多次足部检查(检查溃疡和神经损伤)和糖化血红蛋白(HgbA1c)检测(检查血糖是否升高)。为了计算分母,纳入标准是患者在过去一年中至少接受过一次糖尿病(ICD 编码)诊断。该机构只希望考虑标准的成人群体;因此,18 岁以下的儿童和 65 岁以上的老年人将被排除在外。一个诊所可能有 4,000 名患者,其中 500 人符合这些标准;那么 500 就是该指标的分母。接下来需要计算两个分子:
-
这些患者中,在过去一年中接受了至少三次足部检查的数量
-
这些患者中,在过去一年中接受了至少两次 HgbA1c 检测的数量
例如,假设我们诊所的数字分别为 350 和 400。最终的指标是 350/500 = 0.70,代表糖尿病足部检查的表现,400/500 = 0.80,代表糖尿病血液检查的表现。然后,这些可以平均得出 0.75,作为该诊所糖尿病护理的总体评分。
衡量标准存在一些问题;没有一个标准能够避免漏洞,这些漏洞允许提供者操控他们的衡量分数,而不真正提高护理质量。此外,许多标准可能不公平地惩罚那些患者可能违背医疗建议或拒绝适当治疗的提供者。然而,如果要奖励护理质量,就必须有一种量化护理质量的方法,而在医疗领域,衡量标准是实现这一目标的重要手段。
美国医疗保险基于价值的项目
在第二章《医疗基础》中,我们讨论了按服务项目付费(FFS)报销模式在医学中的应用,在这种模式下,医生根据提供的护理量而非护理的价值获得报酬。最近,有一个推动趋势,旨在根据护理质量而非护理数量来奖励提供者。
为了促进从按服务项目付费(FFS)到基于价值的报销模式的转变,CMS 实施了基于价值的项目。这些项目根据医疗服务提供者为医疗保险患者提供的护理质量进行奖励或惩罚。2018 年,共有八个此类项目,具体如下:
-
医院基于价值的采购(HVBP)项目
-
医院再入院减少(HRR)项目
-
医院获得性疾病(HAC)项目
-
终末期肾病(ESRD)质量倡议项目
-
熟练护理设施基于价值的项目 (SNFVBP)
-
居家健康价值基础项目(HHVBP)
-
替代支付模型(APMs)
-
基于绩效的激励支付系统(MIPS)
在接下来的部分中,我们将详细介绍这些项目。
医院基于价值的采购(HVBP)项目
HVBP 项目根据医院提供的医疗服务质量对其进行奖励,激励支付给接受医保患者的医院。HVBP 项目通过 2010 年的《平价医疗法案》设立,并于 2012 年开始实施。
领域与测量标准
在 2018 年,HVBP 项目大约包含 20 项测量标准,涵盖医院护理质量的四个不同领域。该列表不断扩展,到 2023 年预计将包括约 25 项测量标准。让我们在此看看每个领域和测量标准。
临床护理领域
临床护理领域通过使用死亡率等指标来主要评估临床护理质量。死亡率指特定疾病患者的死亡率。该领域使用了五项死亡率测量指标(列举如下)。第六项测量标准是全髋关节/膝关节置换(即关节置换)手术的并发症率:
-
MORT-30-AMI:急性心肌梗死患者的 30 天死亡率
-
MORT-30-HF:心力衰竭患者的 30 天死亡率
-
MORT-30-PN:肺炎患者的 30 天死亡率
-
THA/TKA:发生并发症的全髋关节置换/全膝关节置换手术数量
-
MORT-30-COPD:慢性阻塞性肺病(COPD)患者的 30 天死亡率
-
MORT-30-CABG:接受冠状动脉旁路移植手术(CABG)的患者 30 天死亡率
以患者和护理人员为中心的护理体验领域
以患者和护理人员为中心的护理体验领域的测量标准是通过医院消费者评估医疗服务和系统(HCAHPS)调查获得的。HCAHPS 调查在患者出院后不久,对随机抽取的医保患者进行。超过十个问题集中在以下八个测量指标上:
-
与护士的沟通
-
与医生的沟通
-
医院员工的响应性
-
药物沟通
-
医院环境的清洁度和安静度
-
出院信息
-
医院整体评分
-
三项护理过渡
安全领域
该领域的测量标准评估医院的安全性,特别是不良事件和院内感染等问题。该领域的所有测量标准都将在后续的 HAC 项目部分中描述(PC-01 测量标准除外,具体描述如下):
-
AHRQ 复合指标(PSI-90):有关详细描述,请参见 HAC 项目部分。
-
导尿管相关尿路感染(CAUTI):有关详细描述,请参见 HAC 项目部分。
-
中心静脉导管相关血流感染(CLABSI):有关详细描述,请参见 HAC 项目部分。
-
艰难梭状芽孢杆菌感染(CDI):有关详细描述,请参阅 HAC 计划部分。
-
耐甲氧西林金黄色葡萄球菌感染(MRSA):有关详细描述,请参阅 HAC 计划部分。
-
手术部位感染(SSI):有关详细描述,请参阅 HAC 计划部分。
-
PC-01 – 39 周未满时的选择性分娩:指南建议妊娠尽可能接近 40 周时分娩。
效率和成本削减领域
本领域的四项指标检查与每家医院相关的护理成本。其中一项指标(MSPB)与每位患者的总体支出有关;其余三项指标涉及三种特定疾病的支出:
-
每位受益人 Medicare 支出(MSPB)
-
急性心肌梗死(AMI)支付
-
心力衰竭(HF)支付
-
肺炎(PN)支付
医院再入院减少(HRR)计划
测量住院病人护理质量的另一种方式是通过使用住院病人初次(起始)就诊时诊断为特定疾病的患者的再入院率。如果病人在医院获得了针对这些特定疾病的适当护理,那么预期再入院率应该等于或低于可接受的水平。高于基准再入院率的医院将面临较低的赔偿。因此,HRR 计划于 2012 年启动。该计划为减少住院病人在 30 天内因以下疾病再入院率的医院提供激励支付(最高可达其来自 Medicare 的住院支付的 3%):
-
急性心肌梗死(AMI)
-
心力衰竭(HF)
-
肺炎(PN)
-
慢性阻塞性肺疾病(COPD)
-
全髋/ 膝关节置换术(THA/ TKA)
-
冠状动脉旁路移植手术(CABG)
医院获得性疾病(HAC)计划
衡量住院病人护理质量的另一种方法是考虑该医院发生的院内感染或医源性疾病的数量。医源性疾病是指由医学检查或治疗引起的疾病,而院内感染则是指源自医院的疾病(通常是感染)。院内感染通常对多种抗生素具有耐药性,且相当难以治疗。
在 2014 年启动的 HACRP 计划下,如果医院的患者感染医院获得性感染的风险较高,则医院将面临总 Medicare 支付的 1%的处罚。更具体地说,医院如果符合特定得分阈值(基于患者发生五种常见医院获得性感染的频率以及其 AHRQ 患者安全指标(PSI)90 综合指标的表现),将有资格获得 Medicare 报销的 1%的减少。
HAC 项目包括六项措施,涵盖了两大护理领域。六项措施中有五项与医院患者的感染率相关。第六项措施是一个综合性指标,评估各种不利的患者安全事件。
我们现在将更详细地了解这些领域和措施。
医疗获得性感染领域
以下是五种医疗获得性感染:
-
导尿管相关尿路感染 (CAUTI):CAUTI 发生在使用不当(无菌)技术将尿管插入尿道时,导致细菌在尿路中繁殖。
-
中心静脉导管相关血流感染 (CLABSI):类似地,当中心静脉导管不当插入体内时,细菌便可进入血液并定植(败血症)。
-
艰难梭状芽孢杆菌感染 (CDI):住院治疗的病人在抗生素治疗后,原本的肠道菌群被消除,从而容易受到艰难梭状芽孢杆菌的感染,细菌在肠道内定植。医疗人员的卫生条件差和手部洗净不当是额外的风险因素。
-
耐甲氧西林金黄色葡萄球菌(MRSA)感染:MRSA 是一种常见且特别具有毒性的金黄色葡萄球菌株,通常感染皮肤和血液,并且对多种抗生素具有耐药性。它常常在医院传播,通过快速治疗和护理可以避免传播。
-
手术部位感染(SSI):由于手术过程中或术后灭菌技术不当,导致伤口或手术部位感染。
患者安全领域
PSI 90 是由医疗研究与质量局(AHRQ)发布的患者安全/并发症指标。2017 年,它通过 10 项指标衡量了医院的患者安全和并发症发生率:
-
PSI 03:压疮发生率:压疮是由于病人长时间保持同一姿势卧床所形成的皮肤损伤。它常常被用作衡量医院护理质量/忽视情况的指标。
-
PSI 06:医源性气胸发生率:气胸是指肺壁破裂,导致空气积聚在肺部周围的腔隙中,从而妨碍患者正常呼吸。有些气胸是由医院手术引起的,这些被称为医源性气胸。
-
PSI 08:住院跌倒伴髋部骨折发生率:跌倒在老年患者中常见,尤其是在手术或治疗后。采取一些预防措施可以防止患者跌倒,未能做到这一点的医院常被认为提供了低质量的护理。
-
PSI 09:围手术期出血或血肿发生率:此指标衡量患者在医院接受手术时发生过量出血的情况。
-
PSI 10: 术后急性肾损伤率:在手术或操作后,患者因血流减少或 X 光对比剂的影响而面临肾脏损伤的风险。
-
PSI 11: 术后呼吸衰竭发生率:手术后,患者可能出现呼吸衰竭,这是一种危及生命的病症,需要将患者置于麻醉状态下的呼吸机上,并在重症监护病房(ICU)进行持续监护。通过指导患者进行正确的呼吸练习,可以减少呼吸衰竭事件的发生。
-
PSI 12: 术后肺栓塞(PE)或深静脉血栓(DVT)发生率:深静脉血栓是在下肢肌肉的静脉中形成的血块。肺栓塞是指血块通过血液循环进入肺部,导致生命威胁的并发症。许多 DVT 可以通过在住院期间使用肝素和其他治疗方法,鼓励患者保持活动来预防。
-
PSI 13: 术后脓毒症发生率:该指标衡量了接受手术的患者术后发生感染的频率。脓毒症是一种危及生命的病症,表现为细菌感染血液,影响器官功能。
-
PSI 14: 术后伤口裂开率:伤口裂开是指手术部位未能正确闭合或愈合,通常是手术操作不当或术后营养不良的表现。
-
PSI 15: 未识别的腹腔/盆腔意外穿刺/撕裂率:此指标衡量在腹部或盆腔手术过程中,意外穿刺/撕裂发生的频率。
更多信息请访问www.qualityindicators.ahrq.gov/News/PSI90_Factsheet_FAQ.pdf。
终末期肾病(ESRD)质量激励计划
ESRD 质量激励计划衡量了 Medicare ESRD 患者在透析中心接受的护理质量。共有 16 项指标:11 项临床指标和 5 项报告指标,具体如下:
-
NHSN 血流感染在血液透析门诊患者中的发生率:不当的消毒技术可能导致血液透析时发生感染。该指标通过对比实际发生的感染数(分子)和预期感染数(分母),来评估感染的发生情况。
-
ICH CAHPS:该指标通过评估患者问卷调查反馈,审视透析中心提供护理的质量。
-
标准化再入院率:标准化再入院率是实际发生的非计划性 30 天再入院次数与预期非计划性 30 天再入院次数的比值。
-
Kt/V 透析充分性措施 – 血液透析:Kt/V 是一个公式,用于量化透析治疗的充分性。四项 Kt/V 措施检查有多少治疗会话符合不同透析患者群体的 Kt/V 阈值:
-
Kt/V 透析充分性措施 – 腹膜透析
-
Kt/V 透析充分性措施 – 儿童血液透析
-
Kt/V 透析充分性措施 – 儿童腹膜透析
-
-
标准化输血比率:此措施比较透析患者中实际与预期的红细胞输血数量(输血是透析的不良后果)。
-
血管通路 – 动静脉瘘:血管通路措施量化了是否为患者提供了适当的血液通路。动静脉瘘措施评估了使用两根针头进行血液通路的动静脉瘘部位数量。
-
血管通路 – 导管:导管措施确定有多少导管在患者体内存在超过 90 天,这是一种感染风险。
-
高钙血症:该措施衡量患者经历高钙血症的月数,这是一种透析的不良反应。
-
矿物质代谢报告:这五项报告措施检查每个设施如何报告透析患者护理的各个方面。措施包括矿物质代谢报告、贫血管理报告、疼痛评估、抑郁症筛查和个人流感疫苗接种报告:
-
贫血管理报告
-
疼痛评估和跟踪报告
-
临床抑郁症筛查和跟踪报告
-
个人流感疫苗接种报告
-
熟练护理设施价值导向项目(SNFVBP)
SNFVBP 计划定于 2019 年开始。该计划将基于两项与结果相关的措施,部分决定政府向 SNF 支付的医疗保险报销:
-
30 天内全因再入院率
-
30 天内可能可预防的再入院率
这些标准适用于入住 SNF 的居民,且他们已被转院到其他医院。当该项目启动时,SNF 可能会通过与机器学习专家合作,预测哪些患者有再入院风险,从而获益。
有关 SNFVBP 的更多信息,请访问以下链接:www.cms.gov/Medicare/Quality-Initiatives-Patient-Assessment-Instruments/Value-Based-Programs/Other-VBPs/SNF-VBP.html。
家庭健康价值导向项目(HHVBP)
HHVBP 计划于 2016 年 1 月在美国 50 个州中的 9 个州启动。它根据护理质量向获得 Medicare 认证的家庭健康机构 (HHAs)提供支付调整。该项目将使用 22 项措施来评估 HHAs 提供的护理质量。这些措施包括调查、过程和结果措施,并包括急诊利用和非计划性住院的相关措施。
优质激励支付系统(MIPS)
MIPS 是一个面向个体和团体门诊医师实践的基于价值的项目。该项目始于 2017 年,并通过 2015 年的《MACRA 法案》实施。与 APMs 项目一起,MIPS 构成了 Medicare 的质量支付项目(QPP)。它替代并整合了之前的基于价值的项目,如医师质量报告系统(PQRS)和价值调整(VM)项目。如果提供者的账单金额或 Medicare 患者数量达到一定要求,必须参与 MIPS。在 MIPS 中,提供者根据四个类别进行评估:
-
质量
-
推进护理信息
-
改善活动
-
成本
2017 年,确定实践最终 MIPS 得分的细分标准如下:60%为质量,25%为推进护理信息,15%为改善活动。从 2018 年开始,成本也将影响最终的 MIPS 得分。
让我们详细了解这四个绩效类别。
质量
在质量类别中,提供者从包含 271 项措施的列表中选择六项措施(截至 2018 年)。措施的例子包括急性外耳道炎(耳部感染):局部治疗和静脉曲张治疗伴大隐静脉消融:结果调查。所有医学专业都有代表,提供者可以选择最适合自己的措施。然后,提供者根据措施的规范收集并提交相关数据。
推进护理信息
这一类别包括与推进健康信息技术相关的措施。此类别包含 15 项措施。措施的例子包括核对患者信息、向数据注册处报告临床数据,以及电子处方。
改善活动
在这一类别中,提供者必须证明他们已采取措施来改善实践中的护理协调、患者参与和患者安全等方面。提供者必须证明他们在 3 个月内完成了最多四项措施。
成本
对于最终类别,护理成本将从索赔数据中确定,提供者提供最具效率的护理将获得奖励。该类别将从 2018 年开始计入 MIPS 最终得分。
其他基于价值的项目
除了上述由 CMS 管理的基于价值的项目外,还有其他由其他机构管理的附加项目。我们来看看这些项目。
医疗效果数据与信息集(HEDIS)
HEDIS 用于衡量健康保险计划的质量,由国家质量保证委员会(NCQA)管理。HEDIS 包括约 90 个衡量标准,涵盖几乎所有医学专业。许多衡量标准与之前讨论过的指标,或者与 MIPS 临床护理类别中的 271 个衡量标准有相似之处。
州级指标
在 2018 年,几乎每个州都有某种形式的基于价值的程序和激励措施。通常,这些计划适用于 Medicaid 患者,因为 Medicaid 通常由州级管理。许多州也采用联邦发布的指标,并对其进行调整以适应自己的需求。例如,乔治亚州资助了 Georgia Families 计划(dch.georgia.gov/georgia-families),该计划允许乔治亚州的 Medicaid 患者选择健康保险计划。它通过使用 HEDIS 指标设定目标并衡量效果。
使用 Python 比较透析设施
在前一节中,我们概述了 CMS 实施的基于价值的激励计划。其中一个计划是 ESRD 质量激励计划,该计划根据透析设施为患有 ESRD 的 Medicare 患者提供的护理质量进行财务奖励。我们描述了 16 项评估每个 ESRD 病例的指标。
在本节中,我们将下载 CMS 发布的关于美国透析中心绩效的数据。我们将使用 Python 命令处理这些数据,以提取我们可以用来找出哪些中心表现良好,哪些中心可能从分析解决方案中受益的信息。通过适当的市场营销和销售努力的精准定位,将提高分析解决方案的效率。
下载数据
要下载透析设施对比数据,请完成以下步骤。
-
在页面上找到标有“DOWNLOAD CSV FLAT FILES (REVISED) NOW”的蓝色按钮。(为了获取正确的年份,你可能需要选择“GET ARCHIVED DATA”按钮。)点击该按钮,
.zip文件将开始下载。 -
使用适当的 Windows/Mac 程序或 Linux 命令解压
.zip文件。 -
注意文件名为
ESRD QIP - Complete QIP Data - Payment Year 2018.csv的目录和路径。
将数据导入到你的 Jupyter Notebook 会话中
要将.csv文件导入 Jupyter Notebook 会话,请像我们在第一章《医疗分析简介》中所做的那样打开 Jupyter Notebook 程序。打开一个新笔记本。然后,在第一个单元格中输入以下内容(用你的文件路径替换这里显示的路径),并点击播放按钮:
import pandas as pd
df = pd.read_csv(
'C:\\Users\\Vikas\\Desktop\\Bk\\Data\\DFCompare_Revised_FlatFiles\\' +
'ESRD QIP - Complete QIP Data - Payment Year 2018.csv', header=0
)
上述代码使用了pandas库的read_csv()函数将.csv文件导入为 DataFrame。header参数告诉笔记本该文件的第一行包含列名。
注意到反斜杠是成对出现的。这是因为\是 Python 中的转义字符。同时注意到文件名太长,无法放在一行内。在 Python 中,只要换行由括号和其他特定标点符号包围,语句就可以跨越多行,而不需要特殊处理。
探索数据的行与列
让我们来探索数据。在下一个单元格中,输入以下内容:
print('Number of rows: ' + str(df.shape[0]))
print('Number of columns: ' + str(df.shape[1]))
输出结果如下:
Number of rows: 6825
Number of columns: 153
在 2018 年文件中,应有 6,825 行和 153 列。每一行对应美国的一个透析设施。这里我们使用了 DataFrame 的shape属性,它返回一个元组,包含行数和列数。
我们还可以通过使用head()函数来查看 DataFrame 的可视化效果。head()函数接受一个参数n,用于指定打印 DataFrame 的前几行。在下一个单元格中,输入以下内容并按下播放按钮:
print(df.head(n=5))
输出结果如下:
Facility Name CMS Certification Number (CCN) \
0 CHILDRENS HOSPITAL DIALYSIS 12306
1 FMC CAPITOL CITY 12500
2 GADSDEN DIALYSIS 12501
3 TUSCALOOSA UNIVERSITY DIALYSIS 12502
4 PCD MONTGOMERY 12505
...
你应该能够看到前五行中的一些列,如设施名称、地址和衡量得分。head()函数会打印出列的简要列表,从.csv文件的开始和结束处各选择一些列,并用省略号分隔。
让我们获取所有153列的完整列表。输入以下内容并按下Enter:
print(df.columns)
输出结果如下:
Index(['Facility Name', 'CMS Certification Number (CCN)', 'Alternate CCN 1',
'Address 1', 'Address 2', 'City', 'State', 'Zip Code', 'Network',
'VAT Catheter Measure Score',
...
'STrR Improvement Measure Rate/Ratio',
'STrR Improvement Period Numerator',
'STrR Improvement Period Denominator', 'STrR Measure Score Applied',
'National Avg STrR Measure Score', 'Total Performance Score',
'PY2018 Payment Reduction Percentage', 'CMS Certification Date',
'Ownership as of December 31, 2016', 'Date of Ownership Record Update'],
dtype='object', length=153)
这里,我们使用 DataFrame 的columns属性,它会以列表的形式提供 DataFrame 的列名。不幸的是,pandas又一次将输出进行了缩略,因此我们无法看到所有153列。为了查看所有列,我们需要更明确地使用for循环逐个打印每一列:
for column in df.columns:
print(column)
输出结果如下:
Facility Name
CMS Certification Number (CCN)
Alternate CCN 1
Address 1
Address 2
City
State
Zip Code
Network
VAT Catheter Measure Score
...
现在你将看到所有153列的名称。使用滚动条浏览所有列。你会发现每个 16 个衡量指标都与多个列相关联,此外还有像人口统计数据和总性能得分等附加列。
现在我们对数据集有了一个大致的概览,可以继续进行更深入的分析。
地理数据探索
在本节余下的部分,我们将使用许多类似 SQL 的操作来处理数据。这里是一些基本操作的 SQL 和pandas之间的转换表:
| 操作 | SQL 语法 | pandas函数 |
SQL 示例 | pandas示例 |
|---|---|---|---|---|
| 选择列 | SELECT |
[[]] |
SELECT col1, col2, FROM df; |
df[['col1','col2']] |
| 选择行 | WHERE |
loc(), iloc() |
SELECT * FROM df WHERE age=50; |
df.loc[df['age']==50] |
| 按列排序 | ORDER BY |
sort_values() |
SELECT * FROM df ORDER BY col1; |
df.sort_values('col1') |
| 按列聚合 | GROUP BY |
groupby() |
SELECT COUNT(*) FROM df GROUP BY col1; |
df.groupby('col1').size() |
| 限制行数 | LIMIT |
head() |
SELECT * FROM df LIMIT 5; |
df.head(n=5) |
考虑到这些转换,我们可以开始按地理位置探索数据。
首先,6,825 个透析设施已经是一个庞大的数量。让我们尝试通过州来缩小范围。首先,我们统计每个州的透析设施数量:
"""Equivalent SQL: SELECT COUNT(*)
FROM df
GROUP BY State;
"""
df_states = df.groupby('State').size()
print(df_states)
输出如下:
State
AK 9
AL 170
AR 69
AS 1
AZ 120
CA 625
CO 75
CT 49
DC 23
DE 27
...
你应该能看到一个包含 50 行的表格(每个州一行,每行显示相关的计数)。
现在让我们按降序对这些行进行排序:
"""Equivalent SQL: SELECT COUNT(*) AS Count
FROM df
GROUP BY State
ORDER BY Count ASC;
"""
df_states = df.groupby('State').size().sort_values(ascending=False)
print(df_states)
输出如下:
State
CA 625
TX 605
FL 433
GA 345
OH 314
IL 299
PA 294
NY 274
NC 211
MI 211
...
让我们进一步优化查询,将输出限制为 10 个州:
"""Equivalent SQL: SELECT COUNT(*) AS Count
FROM df
GROUP BY State
ORDER BY Count DESC
LIMIT 10;
"""
df_states = df.groupby('State').size().sort_values(ascending=False).head(n=10)
print(df_states)
根据结果,加利福尼亚州是拥有最多透析中心的州,其次是德克萨斯州。如果我们想要根据州来筛选透析设施,可以通过选择适当的行来实现:
"""Equivalent SQL: SELECT *
FROM df
WHERE State='CA';
"""
df_ca = df.loc[df['State'] == 'CA']
print(df_ca)
根据总表现显示透析中心
几乎所有对这种以提供者为中心的数据的探索都会包括根据质量得分分析设施。接下来我们将深入探讨这一点。
首先,让我们统计不同透析设施所获得的得分:
print(df.groupby('Total Performance Score').size())
输出如下:
Total Performance Score
10 10
100 30
11 2
12 2
13 1
14 3
15 1
...
95 15
96 2
97 11
98 8
99 12
No Score 276
Length: 95, dtype: int64
需要注意的一点是,Total Performance Score 列的格式是字符串而不是整数格式,因此为了进行数值排序,我们必须先将该列转换为整数格式。其次,在运行前面的代码后,你会注意到 276 个透析设施在 Total Performance Score 列中的值为 No Score。这些行在转换为整数格式之前必须被删除,以避免出现错误。
在以下代码中,我们首先删除了 No Score 行,然后使用 pandas 的 to_numeric() 函数将字符串列转换为整数列:
df_filt= df.loc[df['Total Performance Score'] != 'No Score']
df_filt['Total Performance Score'] = pd.to_numeric(
df_filt['Total Performance Score']
)
现在,我们创建一个新的 DataFrame,仅选择我们感兴趣的几个列并进行排序,将表现最差的中心排在最前面。例如,这样的代码块对识别表现最差的透析中心非常有帮助。我们展示前五个结果:
df_tps = df_filt[[
'Facility Name',
'State',
'Total Performance Score'
]].sort_values('Total Performance Score')
print(df_tps.head(n=5))
输出如下:
Facility Name State \
5622 462320 PRIMARY CHILDREN'S DIALYSIS CENTER UT
698 PEDIATRIC DIALYSIS UNIT AT UCSF CA
6766 VITAL LIFE DIALYSIS CENTER FL
4635 BELMONT COURT DIALYSIS - DOYLESTOWN CAMPUS PA
3763 WOODMERE DIALYSIS LLC NY
Total Performance Score
5622 5
698 7
6766 8
4635 8
3763 9
另外,如果我们希望分析每个州透析中心的平均总表现,可以通过结合使用 numpy.mean() 和 groupby() 来实现:
import numpy as np
df_state_means = df_filt.groupby('State').agg({
'Total Performance Score': np.mean
})
print(df_state_means.sort_values('Total Performance Score', ascending=False))
输出如下:
Total Performance Score
State
ID 73.178571
WY 71.777778
HI 70.500000
UT 70.421053
CO 70.173333
WA 70.146067
ME 70.058824
OR 70.046154
KS 69.480769
AZ 68.905983
...
根据这个查询的结果,爱达荷州和怀俄明州的透析中心表现最好。你也可以通过以下修改添加一列,显示每个州透析中心的数量:
import numpy as np
df_state_means = df_filt.groupby('State').agg({
'Total Performance Score': np.mean,
'State': np.size
})
print(df_state_means.sort_values('Total Performance Score', ascending=False))
输出如下:
Total Performance Score State
State
ID 73.178571 28
WY 71.777778 9
HI 70.500000 26
UT 70.421053 38
CO 70.173333 75
WA 70.146067 89
ME 70.058824 17
OR 70.046154 65
KS 69.480769 52
AZ 68.905983 117
...
结果表明,在只考虑拥有至少 100 个透析中心的州时,亚利桑那州的总表现最佳。
对透析中心的替代分析
本节中介绍的代码可以调整用于对透析中心进行各种不同类型的分析。例如,您可能希望根据透析中心所有者来衡量平均表现,而不是按 State 来衡量。这可以通过在最近的示例中更改分组的列来实现。或者,您也许想查看单个指标,而不是 Total Performance Score,这同样可以通过仅更改代码中的一列来完成。
现在,我们已经使用透析中心分析了服务提供商的表现,接下来我们将查看一个更复杂的数据集——住院医院表现数据集。
比较医院
在前面的示例中,我们使用 Python 分析了透析中心的表现。透析中心只是医疗服务提供者池中的一小部分——该池还包括医院、门诊诊所、疗养院、住院康复设施和临终关怀服务等。例如,当您从data.medicare.gov下载透析设施比较数据时,您可能会注意到这些其他设施的表现数据。现在我们将研究一个更复杂的设施类型:住院医院。医院比较数据集包含了 CMS 基于价值的三个项目的数据。它是一个庞大的数据集,我们将使用这些数据展示一些高级的 Python 和 pandas 特性。
下载数据
要下载医院比较数据集,请完成以下步骤:
-
在页面上找到标有“DOWNLOAD CSV FLAT FILES (REVISED) NOW” 的蓝色按钮。(如果要获取正确的年份,您可能需要选择“GET ARCHIVED DATA”按钮)。点击该按钮,
.zip文件将开始下载。 -
使用适当的 Windows/Mac 程序或 Linux 命令提取
.zip文件。 -
请注意包含已提取
.csv文件的路径。
将数据导入到您的 Jupyter Notebook 会话中
请注意,提取的医院比较文件夹包含 71 个文件,其中绝大多数是 .csv 文件。这是很多表格!让我们将一些表格导入到 Jupyter Notebook 中:
import pandas as pd
pathname = 'C:\\Users\\Vikas\\Desktop\\Bk\\Data\\Hospital_Revised_Flatfiles\\'
files_of_interest = [
'hvbp_tps_11_07_2017.csv',
'hvbp_clinical_care_11_07_2017.csv',
'hvbp_safety_11_07_2017.csv',
'hvbp_efficiency_11_07_2017.csv',
'hvbp_hcahps_11_07_2017.csv'
]
dfs = {
foi: pd.read_csv(pathname + foi, header=0) for foi in files_of_interest
}
上面的代码将与 HVBP 测量相关的表格加载到 Python 会话中。共有五个表格,其中四个表格对应该测量的四个领域,一个表格代表整体评分。
请注意,替代显式地创建和导入五个数据框,我们使用列表推导式创建了一个数据框字典。我们在 Python 章节中已经讲解了字典、列表和推导式。这在当前和即将到来的单元中节省了大量的输入工作。
探索表格
接下来,让我们探索表格,并检查每个表格中的行和列数:
for k, v in dfs.items():
print(
k + ' - Number of rows: ' + str(v.shape[0]) +
', Number of columns: ' + str(v.shape[1])
)
输出结果如下:
hvbp_tps_11_07_2017.csv - Number of rows: 2808, Number of columns: 16
hvbp_clinical_care_11_07_2017.csv - Number of rows: 2808, Number of columns: 28
hvbp_safety_11_07_2017.csv - Number of rows: 2808, Number of columns: 64
hvbp_efficiency_11_07_2017.csv - Number of rows: 2808, Number of columns: 14
hvbp_hcahps_11_07_2017.csv - Number of rows: 2808, Number of columns: 73
在前一个单元中,我们使用了字典的items()方法来遍历字典中的每个键-数据框对。
所有的表格都有相同的行数。由于每一行都对应着一个医院,因此可以安全地假设所有表格中的医院是相同的(我们稍后将验证这一假设)。
由于表格之间的分离,任何我们进行的分析都有其局限性。由于所有医院都是(假设)相同的,我们可以将所有列合并为一个表格。我们将使用pandas的merge()函数来实现这一点。使用pandas的merge()类似于使用SQL JOIN(你在第四章中学习了SQL JOIN,计算基础 – 数据库)。合并通过指定两个表格中共有的 ID 列来进行,这样就可以根据该列匹配行。为了查看五个 HVBP 表格中是否有共同的 ID 列,我们可以打印出每个表格的列名:
for v in dfs.values():
for column in v.columns:
print(column)
print('\n')
如果你浏览结果,你会注意到所有表格中都有Provider Number列。Provider Number是一个独特的标识符,可以用来链接这些表格。
合并 HVBP 表格
让我们尝试合并两个表格:
df_master = dfs[files_of_interest[0]].merge(
dfs[files_of_interest[1]],
on='Provider Number',
how='left',
copy=False
)
print(df_master.shape)
输出如下:
(2808, 43)
我们的合并操作似乎成功了,因为df_master中的列数是前两个数据框列数的总和,减去一(on列没有被复制)。我们来看一下新数据框的列:
print(df_master.columns)
输出如下:
Index(['Provider Number', 'Hospital Name_x', 'Address_x', 'City_x', 'State_x',
'Zip Code', 'County Name_x',
'Unweighted Normalized Clinical Care Domain Score',
'Weighted Normalized Clinical Care Domain Score',
'Unweighted Patient and Caregiver Centered Experience of Care/Care Coordination Domain Score',
'Weighted Patient and Caregiver Centered Experience of Care/Care Coordination Domain Score',
'Unweighted Normalized Safety Domain Score',
'Weighted Safety Domain Score',
'Unweighted Normalized Efficiency and Cost Reduction Domain Score',
'Weighted Efficiency and Cost Reduction Domain Score',
'Total Performance Score', 'Hospital Name_y', 'Address_y', 'City_y',
'State_y', 'ZIP Code', 'County Name_y',
'MORT-30-AMI Achievement Threshold', 'MORT-30-AMI Benchmark',
'MORT-30-AMI Baseline Rate', 'MORT-30-AMI Performance Rate',
'MORT-30-AMI Achievement Points', 'MORT-30-AMI Improvement Points',
'MORT-30-AMI Measure Score', 'MORT-30-HF Achievement Threshold',
'MORT-30-HF Benchmark', 'MORT-30-HF Baseline Rate',
'MORT-30-HF Performance Rate', 'MORT-30-HF Achievement Points',
'MORT-30-HF Improvement Points', 'MORT-30-HF Measure Score',
'MORT-30-PN Achievement Threshold', 'MORT-30-PN Benchmark',
'MORT-30-PN Baseline Rate', 'MORT-30-PN Performance Rate',
'MORT-30-PN Achievement Points', 'MORT-30-PN Improvement Points',
'MORT-30-PN Measure Score'],
dtype='object')
重复的列(Hospital Name、Address、City等)在合并后的表格中添加了后缀_x和_y,以指示它们来自哪个表格,确认了合并操作成功。
让我们使用一个for循环将其余的三个表格与df_master合并:
for df in dfs.values():
df.columns = [col if col not in ['Provider_Number'] else 'Provider Number'
for col in df.columns]
for num in [2,3,4]:
df_master = df_master.merge(
dfs[files_of_interest[num]],
on='Provider Number',
how='left',
copy=False
)
print(df_master.shape)
输出如下:
(2808, 191)
在这一单元中,首先我们使用一个循环将所有列名从Provider_Number重命名为Provider Number,以便我们可以清晰地连接表格。
然后我们使用一个循环将每个剩余的表格与df_master合并。最终表格中的列数等于所有表格的列数总和,减去四。
为了确认合并是否成功,我们可以打印出新表格的列:
for column in df_master.columns:
print(column)
滚动输出确认了五个表格中的所有列都已包含。
我们将留给你使用比较透析设施部分的代码示例,进行更多的分析。
总结
在本章中,我们回顾了一些当今塑造美国医疗行业的突出基于价值的项目。我们已经看到这些项目如何通过使用度量标准来量化提供者的表现。此外,我们下载了用于比较透析设施和医院的数据,并通过一些 Python 代码示例来展示如何分析这些数据。
有人可能会争辩,书中这一章的分析可以通过使用诸如 Microsoft Excel 之类的电子表格应用程序来完成,而不必编程。在第七章,在医疗保健中构建预测模型,我们将使用医疗保健数据集训练预测模型,以预测急诊科的出院状态。正如你将看到的,这种类型的分析几乎肯定需要编写代码。
参考文献
Data.Medicare.gov(2018)。于 2018 年 4 月 28 日访问自data.medicare.gov。
MIPS 概述(2018)。于 2018 年 4 月 28 日访问自qpp.cms.gov/mips/overview。
什么是基于价值的项目?(2017 年 11 月 9 日)。美国医疗保险和医疗补助服务中心。于 2018 年 4 月 28 日访问自www.cms.gov/Medicare/Quality-Initiatives-Patient-Assessment-Instruments/Value-Based-Programs/Value-Based-Programs.html。
第七章:在医疗领域构建预测模型
本章面向所有读者,是本书的核心内容之一。我们将通过示例数据和机器学习问题,演示如何为医疗领域构建预测模型。我们将一次处理一个特征的数据预处理。到本章结束时,你将理解如何准备并拟合一个机器学习模型到临床数据集。
医疗预测分析简介
在第一章《医疗分析简介》中,我们讨论了分析的三个子组成部分:描述性分析、预测性分析和规范性分析。预测性分析和规范性分析是医疗领域提升护理质量、降低成本和改善结果的核心。这是因为如果我们能够预测未来可能发生的不良事件,我们就可以将我们有限的资源转向预防这些不良事件的发生。
我们可以预测(并且随后预防)的医疗领域中的一些不良事件有哪些?
-
死亡:显然,任何可以预防或预测的死亡都应该避免。一旦预测到死亡的发生,预防措施可能包括将更多护士指派给该患者、为该案例聘请更多顾问,或尽早与家属沟通可选方案,而不是等到最后一刻。
-
不良临床事件:这些事件并不等同于死亡,但会大大增加发病率和死亡率的可能性。发病率指的是并发症,而死亡率则指死亡。不良临床事件的例子包括心脏病发作、心力衰竭加重、慢性阻塞性肺病(COPD)加重、肺炎和跌倒。那些可能发生不良事件的患者,可能是需要更多护理或预防性治疗的候选人。
-
再入院:再入院并不会直接对患者构成明显的威胁,但它们是昂贵的,因此应尽量避免可预防的再入院。此外,减少再入院是美国医疗保险和医疗补助服务中心(CMS)的一个重要激励目标,正如我们在第六章《衡量医疗质量》中看到的那样。预防性措施包括为高风险患者安排社会工作者和个案管理人员,确保他们在门诊提供者那里跟进,并购买所需的处方。
-
高利用率:预测那些可能再次发生大量医疗支出的患者,可能通过为他们的团队分配更多护理成员并确保频繁的门诊检查和随访,从而降低成本。
既然我们已经回答了“是什么?”的问题,下一个问题是,“怎么做?”换句话说,我们如何预测哪些护理提供者可以采取行动?
-
首先,我们需要数据:医疗服务提供者应当将其历史患者数据发送给你。数据可以是索赔数据、临床记录、电子健康记录(EHR)记录的导出,或这些数据的某种组合。无论数据类型如何,它最终应该能够转化为表格格式,其中每一行代表一个患者/就诊,每一列代表该患者/就诊的某个特征。
-
使用部分数据,我们训练一个预测模型:在第三章《机器学习基础》中,我们学习了在训练预测模型时到底在做什么,以及整个建模流程如何运作。
-
使用部分数据,我们测试模型的表现:评估我们模型的性能对于设定医疗服务提供者对模型准确性的预期非常重要。
-
然后,我们将模型部署到生产环境中,并为患者提供定期的实时预测:在这一阶段,应当有来自医疗服务提供者的数据定期流入到分析公司。公司随后会定期提供这些患者的预测结果。
在本章的剩余部分,我们将介绍如何构建医疗健康预测模型。首先,我们将描述我们的模拟建模任务。接着,我们将描述并获取公开可用的数据集。然后,我们将对数据集进行预处理,并使用不同的机器学习算法训练预测模型。最后,我们将评估我们模型的表现。虽然我们不会使用我们的模型对实时数据进行实际预测,但我们会描述实现这一目标所需的步骤。
我们的建模任务——预测急诊室患者的出院状态
每年,全国有数百万患者使用急诊科设施。这些设施的资源必须得到妥善管理——如果在某个时间点出现大量患者涌入,医院的工作人员和可用病房应当相应增加。资源与患者涌入之间的不匹配可能会导致浪费资金和提供不理想的护理。
在这个背景下,我们介绍了我们的示例建模任务——预测急诊室就诊患者的出院状态。出院状态指的是患者是被住院还是被送回家。通常,更严重的病例会被住院。因此,我们试图在患者住院期间尽早预测急诊就诊的结果。
使用这样的模型,医院的工作流程和资源流动都可以得到极大改善。许多先前的学术研究已经探讨了这个问题(例如,见 Cameron 等人,2015 年)。
你可能会想,为什么我们没有选择其他建模任务,比如再入院建模或预测充血性心力衰竭(CHF)加重。首先,公开可用的临床数据非常有限。我们选择的数据集是急诊科(ED)数据集;目前没有可以免费下载安装的住院数据集。尽管如此,我们选择的任务仍然能够展示如何构建预测性医疗模型。
获取数据集
在本节中,我们将提供逐步的说明,教你如何获取数据及其相关文档。
NHAMCS 数据集概览
我们为本书选择的数据集是 国家医院门诊医疗护理调查(NHAMCS)公开使用数据的一部分。该数据集由美国 疾病控制与预防中心(CDC)发布并维护。该数据集的主页是 www.cdc.gov/nchs/ahcd/ahcd_questionnaires.htm。
-
NHAMCS 数据是基于调查的数据;它通过向曾在医院就诊的病人和医疗服务提供者发送调查问卷进行收集。
-
数据文件为定宽格式。换句话说,它们是文本文件,每行在一个独立的行上,每列的字符数是固定的。每个特征的字符长度信息可以在相应的 NHAMCS 文档中找到。
-
数据文件分为不同的集,具体取决于数据是否来自门诊就诊或急诊科就诊。我们将在本章中使用急诊科格式。
-
数据附带了有关每个特征内容的详细文档。
-
每一行数据代表一次独立的急诊科病人就诊。
请参阅以下表格,了解我们将在本书中使用的 NHAMCS 急诊科数据文件的概要:
| 文件名 | 数据类型和年份 | 行数(就诊次数) | 列数(特征数量) | 广泛特征类别 |
|---|---|---|---|---|
| ED2013 | 急诊科就诊;2013 | 24,777 | 579 | 就诊日期和信息、人口统计学、烟草使用、到达方式、支付方式、生命体征、分诊、急诊科关系、就诊原因、伤害、诊断、慢性病、执行的服务、见诊的医务人员、处置、住院、插补数据、急诊科信息、社会经济数据 |
下载 NHAMCS 数据
原始数据文件和相关文档可以通过 CDC NHAMCS 主页访问:www.cdc.gov/nchs/ahcd/ahcd_questionnaires.htm(如下截图)。我们建议将所有文件下载到一个专门用于本书及其相关文件的目录中。此外,请记住文件下载到的目录位置:

下载 ED2013 文件
ED2013 文件包含原始数据。要下载它:
-
访问 CDC NHAMCS 主页:
www.cdc.gov/nchs/ahcd/ahcd_questionnaires.htm。 -
向下滚动页面,找到《公共数据文件:可下载数据文件》标题。
-
点击 NHAMCS 链接。它将把您带到 CDC 的 FTP 网站(ftp://ftp.cdc.gov/pub/Health_Statistics/NCHS/Datasets/NHAMCS)。该网站如下图所示。
-
找到名为
ED2013.zip的文件。点击它,文件将开始下载。 -
在文件资源管理器中找到该文件并解压。解压后的目录中,您应看到一个名为
ED2013的无扩展名文件。这就是数据文件。 -
将 ED2013 数据文件移动到与书本相关的文件目录中:

下载调查项目列表 – body_namcsopd.pdf
-
访问 CDC NHAMCS 主页:
www.cdc.gov/nchs/ahcd/ahcd_questionnaires.htm。 -
向下滚动页面,找到《调查项目列表,1973-2012》标题。
-
点击标签为 NAMCS 和 NHAMCS 调查内容手册[修订版 11/2012]的链接。
-
链接应将您带到位于
www.cdc.gov/nchs/data/ahcd/body_namcsopd.pdf的 PDF 页面。这是调查项目的列表。 -
使用浏览器下载文件。然后使用文件资源管理器将其移动到目标目录。
下载文档文件 – doc13_ed.pdf
-
访问 CDC NHAMCS 主页:
www.cdc.gov/nchs/ahcd/ahcd_questionnaires.htm。 -
向下滚动页面,找到《可下载文档》标题。
-
点击 NHAMCS(1992-2014)链接。它将把您带到 CDC 的文档 FTP 网站(ftp://ftp.cdc.gov/pub/Health_Statistics/NCHS/Dataset_Documentation/NHAMCS)。该网站如下图所示。
-
找到名为
doc13_ed.pdf的文件。点击它,PDF 将在浏览器中打开。此 PDF 文件包含 ED2013 数据文件的文档。 -
使用浏览器下载文件。然后使用文件资源管理器将其移动到目标目录:

启动 Jupyter 会话
接下来,我们将启动一个 Jupyter 会话,以便将数据导入 Python 并构建一个机器学习模型。第一章《医疗分析入门》中提供了创建新的 Jupyter Notebook 的详细示例。以下是步骤:
-
在您的计算机上找到 Jupyter 应用程序并启动它。
-
在默认浏览器中新打开的 Jupyter 标签页中,导航到您希望保存 Notebook 的目录。
-
在控制台的右上角找到“New”下拉菜单,点击它并选择 Python 3。
-
你应该看到一个名为 Untitled 的新笔记本。
-
要重命名笔记本,请点击左上角笔记本的名称。一个光标应该会出现,输入所需的名称。我们将笔记本命名为
ED_predict。
你现在可以将数据集导入到 Jupyter 中了。
导入数据集
在加载数据集之前,有一些关于数据的重要事实需要确认:
-
数据是固定宽度格式,这意味着没有分隔符。列宽需要手动指定。
-
没有包含列名的标题行。
-
如果你使用文本编辑器打开数据文件,你会看到包含数字的行。
因为导入 .fwf 文件需要列宽,我们必须先将这些列宽导入到我们的会话中。因此,我们创建了一个名为 ED_metadata.csv 的辅助 .csv 文件,包含每列的宽度、名称和变量类型。我们的数据只有 579 列,所以创建这样的文件只花了几个小时。如果你的数据集更大,可能需要依赖自动宽度检测方法和/或更多团队成员来完成创建数据架构的繁重工作。
加载元数据
在第一个单元格中,让我们导入元数据并打印一个小的预览:
import pandas as pd
pd.set_option('mode.chained_assignment',None)
HOME_PATH = 'C:\\Users\\Vikas\\Desktop\\Bk\\health-it\\ed_predict\\data\\'
df_helper = pd.read_csv(
HOME_PATH + 'ED_metadata.csv',
header=0,
dtype={'width': int, 'column_name': str, 'variable_type': str}
)
print(df_helper.head(n=5))
你应该看到以下输出:
width column_name variable_type
0 2 VMONTH CATEGORICAL
1 1 VDAYR CATEGORICAL
2 4 ARRTIME NONPREDICTIVE
3 4 WAITTIME CONTINUOUS
4 4 LOV NONPREDICTIVE
因此,ED_metadata.csv 文件仅仅是一个包含宽度、列名和变量类型的逗号分隔值文件,具体细节可参考文档。此文件可以从本书的代码仓库下载。
在下一个单元格中,我们将导入的 pandas DataFrame 的列转换为单独的列表:
width = df_helper['width'].tolist()
col_names = df_helper['column_name'].tolist()
var_types = df_helper['variable_type'].tolist()
加载 ED 数据集
接下来,我们将固定宽度数据文件的内容导入 Python,作为由字符串列组成的 pandas DataFrame,使用前一个单元格中创建的 widths 列表。然后,我们使用 col_names 列表命名列:
df_ed = pd.read_fwf(
HOME_PATH + 'ED2013',
widths=width,
header=None,
dtype='str'
)
df_ed.columns = col_names
让我们打印数据集的预览,以确认它已正确导入:
print(df_ed.head(n=5))
输出应类似于以下内容:
VMONTH VDAYR ARRTIME WAITTIME LOV AGE AGER AGEDAYS RESIDNCE SEX ... \
0 01 3 0647 0033 0058 046 4 -07 01 2 ...
1 01 3 1841 0109 0150 056 4 -07 01 2 ...
2 01 3 1333 0084 0198 037 3 -07 01 2 ...
3 01 3 1401 0159 0276 007 1 -07 01 1 ...
4 01 4 1947 0114 0248 053 4 -07 01 1 ...
RX12V3C1 RX12V3C2 RX12V3C3 RX12V3C4 SETTYPE YEAR CSTRATM CPSUM PATWT \
0 nan nan nan nan 3 2013 20113201 100020 002945
1 nan nan nan nan 3 2013 20113201 100020 002945
2 nan nan nan nan 3 2013 20113201 100020 002945
3 nan nan nan nan 3 2013 20113201 100020 002945
4 nan nan nan nan 3 2013 20113201 100020 002945
EDWT
0 nan
1 nan
2 nan
3 nan
4 nan
[5 rows x 579 columns]
查看文档中列值及其含义,确认数据已正确导入。nan 值对应数据文件中的空白空间。
最后,作为另一个检查,让我们统计数据文件的维度,并确认有 24,777 行和 579 列:
print(df_ed.shape)
输出应类似于以下内容:
(24777, 579)
现在数据已正确导入,让我们设置响应变量。
创建响应变量
在某些情况下,我们尝试预测的响应变量可能已经是一个单独且定义明确的列。在这些情况下,在将数据拆分为训练集和测试集之前,只需将响应变量从字符串转换为数字类型即可。
在我们的特定建模任务中,我们试图预测哪些前来急诊科的患者最终会住院。在我们的案例中,住院包括:
-
那些被接收进住院病房以便进一步评估和治疗的人
-
那些被转送到其他医院(无论是精神病医院还是非精神病医院)以便进一步治疗的人
-
那些被接收进入观察单元以进一步评估的人(无论他们最终是被接收还是在观察单元停留后被出院)
因此,我们必须进行一些数据处理,将所有这些不同的结果汇总成一个单一的响应变量:
response_cols = ['ADMITHOS','TRANOTH','TRANPSYC','OBSHOS','OBSDIS']
df_ed.loc[:, response_cols] = df_ed.loc[:, response_cols].apply(pd.to_numeric)
df_ed['ADMITTEMP'] = df_ed[response_cols].sum(axis=1)
df_ed['ADMITFINAL'] = 0
df_ed.loc[df_ed['ADMITTEMP'] >= 1, 'ADMITFINAL'] = 1
df_ed.drop(response_cols, axis=1, inplace=True)
df_ed.drop('ADMITTEMP', axis=1, inplace=True)
让我们详细讨论一下之前的代码示例:
-
第一行通过名称标识我们希望包含在最终目标变量中的列。如果这些列中的任何一列的值为
1,那么目标值应为1。 -
在第 2 行,我们将列从字符串类型转换为数值类型。
-
在第 3 到第 5 行,我们创建了一个名为
ADMITTEMP的列,它包含了五个目标列的逐行求和。然后我们创建了最终的目标列ADMITFINAL,当ADMITTEMP大于或等于1时,将其设置为1。 -
在第 6 到第 7 行,我们删除了五个原始响应列以及
ADMITTEMP列,因为我们现在已经有了最终的响应列。
将数据拆分为训练集和测试集
现在我们已经有了响应变量,下一步是将数据集分为训练集和测试集。在数据科学中,训练集是用来确定模型系数的数据。在训练阶段,模型会将预测变量的值与响应值一起考虑,以“发现”指导预测新数据的规则和权重。然后使用测试集来衡量我们模型的表现,正如我们在第三章中讨论的,机器学习基础。典型的拆分比例是 70%-80%用于训练数据,20%-30%用于测试数据(除非数据集非常大,在这种情况下可以分配较小比例的数据用于测试集)。
一些实践者还会有一个验证集,用于训练模型参数,例如随机森林模型中的树大小或正则化逻辑回归中的套索参数。
幸运的是,scikit-learn 库提供了一个非常方便的函数 train_test_split(),它在给定测试集百分比时会自动处理随机拆分。要使用这个函数,我们必须首先将目标变量从其他数据中分离出来,具体做法如下:
def split_target(data, target_name):
target = data[[target_name]]
data.drop(target_name, axis=1, inplace=True)
return (data, target)
X, y = split_target(df_ed, 'ADMITFINAL')
运行前面的代码后,y 保存了我们的响应变量,X 保存了我们的数据集。我们将这两个变量传递给 train_test_split() 函数,同时设置 test_size 为 0.25,并指定一个随机状态以确保结果可重现:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, random_state=1234
)
结果是一个 2 x 2 的拆分:X_train、X_test、y_train 和 y_test。我们现在可以使用 X_train 和 y_train 来训练模型,使用 X_test 和 y_test 来测试模型的性能。
一个重要的事情是要记住,在预处理阶段,任何对训练集所做的变换,在测试时也必须对测试集执行,否则模型对新数据的输出将是错误的。
作为一个简单检查,并且为了检测目标变量的不平衡情况,让我们检查响应变量中的正负响应数量:
print(y_train.groupby('ADMITFINAL').size())
输出如下:
ADMITFINAL
0 15996
1 2586
dtype: int64
我们的结果表明,测试集中的大约 1/7 的观测值具有正响应。虽然这不是一个完美平衡的数据集(在这种情况下,比例应该是 1/2),但它也不至于不平衡到需要进行任何上采样或下采样的数据处理。我们继续进行预测变量的预处理。
预测变量的预处理
让我们看看在医疗数据中常见的一些特定的预测变量组。
访问信息
ED2013 数据集中的第一个特征类别包含有关就诊时间的信息。包括月份、星期几和到达时间等变量。还包括等待时间和就诊时长变量(均以分钟为单位)。
月份
让我们更详细地分析VMONTH预测变量。以下代码打印出训练集中的所有值及其计数:
print(X_train.groupby('VMONTH').size())
输出如下:
VMONTH
01 1757
02 1396
03 1409
04 1719
05 2032
06 1749
07 1696
08 1034
09 1240
10 1306
11 1693
12 1551
dtype: int64
我们现在可以看到,月份被编号为01到12,正如文档中所说的,每个月都有代表。
数据预处理的一部分是进行特征工程——也就是说,以某种方式组合或转化特征,生成比之前更具预测性的新的特征。例如,假设我们有一个假设,急诊科患者在冬季月份的住院率较高。我们可以创建一个名为WINTER的预测变量,它是VMONTH预测变量的组合,只有当患者是在 12 月、1 月、2 月或 3 月就诊时,值为1。我们在下面的单元中做到了这一点。稍后,我们可以在评估变量重要性时检验这个假设,进行机器学习建模时:
def is_winter(vmonth):
if vmonth in ['12','01','02','03']:
return 1
else:
return 0
X_train.loc[:,'WINTER'] = df_ed.loc[:,'VMONTH'].apply(is_winter)
X_test.loc[:,'WINTER'] = df_ed.loc[:,'VMONTH'].apply(is_winter)
作为一个非正式的测试,让我们打印出WINTER变量的分布,并确认它是前四个月冬季月份的总和:
X_train.groupby('WINTER').size()
输出如下:
WINTER
0 12469
1 6113
dtype: int64
果然,我们得到了6113 = 1551 + 1757 + 1396 + 1409,确认我们正确地做了特征工程。在本章中,我们将看到其他特征工程的例子。
星期几
作为数据导入正确性的一个简单检查,让我们还探讨一下VDAYR变量,它表示患者就诊发生的星期几:
X_train.groupby('VDAYR').size()
输出如下:
VDAYR
1 2559
2 2972
3 2791
4 2632
5 2553
6 2569
7 2506
dtype: int64
正如我们预期的那样,存在七个可能的值,且这些观察值相对均匀地分布在所有可能值中。我们可以尝试制作一个WEEKEND特征,但特征工程可能非常耗时和占用内存,且通常收益甚微。这个练习我们留给读者去做。
到达时间
到达时间是数据中另一个包含的就诊信息变量。然而,在原始形式下,它可能没有什么用处,因为它是一个介于 0 到 2,359 之间的整数。让我们创建一个NIGHT变量,只有当患者在晚上 8 点到早上 8 点之间到达时,才为正。我们创建这个变量的原因是假设那些在非正常时间到达急诊科的患者病情更严重,因此更有可能被收治入院。我们可以使用以下代码来创建NIGHT变量:
def is_night(arrtime):
arrtime_int = int(arrtime)
if ((arrtime_int >= 0) & (arrtime_int < 800)):
return 1
elif ((arrtime_int >= 2000) & (arrtime_int < 2400)):
return 1
else:
return 0
X_train.loc[:,'NIGHT'] = df_ed.loc[:,'ARRTIME'].apply(is_night)
X_test.loc[:,'NIGHT'] = df_ed.loc[:,'ARRTIME'].apply(is_night)
X_train.drop('ARRTIME', axis=1, inplace=True)
X_test.drop('ARRTIME', axis=1, inplace=True)
在前面的例子中,我们首先编写一个函数,如果患者的到达时间在晚上 8 点到早上 8 点之间,则返回1,否则返回0。然后我们使用 pandas 的apply()函数将这个函数“应用”到ARRTIME列,生成NIGHT列。接着,我们删除原始的ARRTIME列,因为它的原始形式没有用处。
等待时间
在急诊科的等待时间是另一个可能与目标变量相关的就诊信息变量。假设患有更严重疾病的患者可能在分诊护士眼中表现得更加有症状,因此可能会被分配更高的分诊分数,从而导致其等待时间比那些病情较轻的患者更短。
在文档中,提到WAITTIME变量在空白和不可用时分别可能取值-9和-7。每当一个连续变量有这样的占位符值时,我们必须进行某种填补操作,以去除占位符值。否则,模型会认为患者的等待时间是-7分钟,从而导致整个模型的调整不利。
在这种情况下,均值填补是合适的操作。均值填补将这些负值替换为数据集其余部分的均值,这样在建模过程中,这些观察值就不会对该变量的系数产生影响。
为了执行均值填补,首先将列转换为数值类型:
X_train.loc[:,'WAITTIME'] = X_train.loc[:,'WAITTIME'].apply(pd.to_numeric)
X_test.loc[:,'WAITTIME'] = X_test.loc[:,'WAITTIME'].apply(pd.to_numeric)
接下来,我们编写一个名为mean_impute_values()的函数,该函数移除-7和-9的值,并将它们替换为该列的均值。我们将这个函数设计为通用型,以便以后在预处理其他列时使用:
def mean_impute_values(data,col):
temp_mean = data.loc[(data[col] != -7) & (data[col] != -9), col].mean()
data.loc[(data[col] == -7) | (data[col] == -9), col] = temp_mean
return data
X_train = mean_impute_values(X_train,'WAITTIME')
X_test = mean_impute_values(X_test,'WAITTIME')
然后我们在数据上调用这个函数,完成这个过程。接下来,我们将确认该函数是否已正确应用,但在此之前,让我们再看一下几个其他变量。
其他就诊信息
这个数据集中的最后一个访问信息变量是访问时长变量(LOV)。然而,访问时长只有在整个急诊访问结束后才能确定,而那时是否接收住院或者出院的决定已经做出。因此,必须删除那些在预测时无法获得的变量,出于这个原因,我们需要删除LOV。我们可以按照以下代码执行:
X_train.drop('LOV', axis=1, inplace=True)
X_test.drop('LOV', axis=1, inplace=True)
现在我们已经完成了访问信息的处理,接下来让我们转向人口统计变量。
人口统计变量
在医疗领域,人口统计变量通常与结果相关联。年龄、性别和种族是医疗领域中的主要人口统计变量。在这个数据集中,还包含了族裔和居住类型变量。让我们按如下方式整理这些变量。
年龄
随着人们年龄的增长,我们可以预期他们的健康状况会变差,并且更频繁地住院。这个假设将在我们查看模型的变量重要性结果后进行验证。
数据集中有三个变量反映年龄。AGE是一个整数值,表示以年为单位的年龄。AGEDAYS是一个整数值,如果患者不到 1 岁,表示以天为单位的年龄。AGER是年龄变量,但它已经被转换为分类变量。我们将AGE变量转换为数值类型,保持AGER变量不变,并删除AGEDAYS变量,因为在绝大多数情况下它不适用:
X_train.loc[:,'AGE'] = X_train.loc[:,'AGE'].apply(pd.to_numeric)
X_test.loc[:,'AGE'] = X_test.loc[:,'AGE'].apply(pd.to_numeric)
X_train.drop('AGEDAYS', axis=1, inplace=True)
X_test.drop('AGEDAYS', axis=1, inplace=True)
性别
在医疗领域,通常发现女性的预期寿命较长,整体健康状况也优于男性,因此我们要将SEX变量纳入我们的模型。它已经是分类变量,因此可以保持不变。
种族和族裔
数据中还包含了种族(西班牙裔/拉丁裔与非西班牙裔/拉丁裔)和族裔信息。通常,容易处于低社会经济地位的种族在医疗中的结果较差。让我们保持未填充的种族和族裔变量(ETHUN和RACEUN)不变。我们可以删除冗余的RACER变量以及填充后的种族和族裔变量(ETHIM和RACERETH):
X_train.drop(['ETHIM','RACER','RACERETH'], axis=1, inplace=True)
X_test.drop(['ETHIM','RACER','RACERETH'], axis=1, inplace=True)
其他人口统计信息
患者居住地包含在数据中。由于它是分类变量,因此无需做任何更改。
让我们查看目前为止的结果,并使用head()函数打印前五行:
X_train.head(n=5)
在输出中横向滚动时,你应该确认我们的所有转换和变量删除操作已正确执行。
分诊变量
分诊变量对急诊科建模任务非常重要。分诊包括根据患者的初始表现和生命体征为患者分配风险评分。通常由专业分诊护士完成,涵盖了主观和客观信息。分诊评分通常从 1(危急)到 5(非急诊)。IMMEDR变量(文档中的第 34 项)是本数据集中的分诊评分。我们当然会包括它。
我们可以将一些其他变量归类为分诊变量,包括患者是否通过急救医疗服务(ARREMS)到达(通常与较差的结果相关)以及患者是否在过去 72 小时内已经接受并出院(SEEN72)。我们也将在我们的模型中包括这些变量。
财务变量
患者的支付方式通常包含在医疗数据集中,且通常某些支付方式与更好或更差的结果相关。没有预期支付来源(NOPAY)、或通过医疗补助(PAYMCAID)或医疗保险(PAYMCARE)支付的患者,通常比拥有私人保险(PAYPRIV)或自行支付(PAYSELF)的患者健康状况差。让我们包括所有财务变量,除了PAYTYPER变量,它只是其他支付变量的非二元扩展:
X_train.drop('PAYTYPER', axis=1, inplace=True)
X_test.drop('PAYTYPER', axis=1, inplace=True)
生命体征
生命体征是医疗建模中患者信息的重要来源,原因有很多:
-
它们易于收集。
-
它们通常在临床接诊初期可用。
-
它们是客观的。
-
它们是患者健康的数值指标。
本数据集中包括的生命体征有体温、脉搏、呼吸频率、血压(收缩压和舒张压)、血氧饱和度百分比,以及是否使用氧气。身高和体重通常也被归类为生命体征,但它们不包括在我们的数据中。让我们逐一查看每项生命体征。
体温
体温通常在患者接诊初期使用温度计测量,可以记录为摄氏度或华氏度。98.6°F(37.1°C)的体温通常被认为是正常体温。显著高于此范围的体温可以称为发热或高热,通常反映感染、炎症或环境过度暴露于阳光下。低于正常体温一定程度的情况被称为低体温,通常反映暴露于寒冷的环境。体温偏离正常值越多,通常意味着疾病越严重。
在我们的数据集中,TEMPF温度被乘以 10 并以整数形式存储。同时,一些值为空(用-9表示),我们必须填补这些空值,因为温度是一个连续变量。接着,我们首先将温度转换为数值类型,使用我们之前写的mean_impute_values()函数填补TEMPF中的缺失值,然后使用 lambda 函数将所有温度值除以10:
X_train.loc[:,'TEMPF'] = X_train.loc[:,'TEMPF'].apply(pd.to_numeric)
X_test.loc[:,'TEMPF'] = X_test.loc[:,'TEMPF'].apply(pd.to_numeric)
X_train = mean_impute_values(X_train,'TEMPF')
X_test = mean_impute_values(X_test,'TEMPF')
X_train.loc[:,'TEMPF'] = X_train.loc[:,'TEMPF'].apply(lambda x: float(x)/10)
X_test.loc[:,'TEMPF'] = X_test.loc[:,'TEMPF'].apply(lambda x: float(x)/10)
让我们打印出此列的 30 个值,以确认我们的处理是否正确:
X_train['TEMPF'].head(n=30)
输出结果如下:
15938 98.200000
5905 98.100000
4636 98.200000
9452 98.200000
7558 99.300000
17878 99.000000
21071 97.800000
20990 98.600000
4537 98.200000
7025 99.300000
2134 97.500000
5212 97.400000
9213 97.900000
2306 97.000000
6106 98.600000
2727 98.282103
4098 99.100000
5233 98.800000
5107 100.000000
18327 98.900000
19242 98.282103
3868 97.900000
12903 98.600000
12763 98.700000
8858 99.400000
8955 97.900000
16360 98.282103
6857 97.100000
6842 97.700000
22073 97.900000
Name: TEMPF, dtype: float64
我们可以看到温度现在是浮动类型,并且它们没有被乘以 10。同时,我们看到均值98.282103已经代替了之前为空的值。接下来我们来看下一个变量。
脉搏
脉搏测量患者心跳的频率。正常范围为 60-100。脉搏超过100被称为心动过速,通常表明潜在的心脏功能障碍、血容量减少或感染(败血症)。脉搏低于60被称为心动过缓。
我们必须使用均值插补法来填补缺失值。首先,我们将脉搏转换为数值类型:
X_train.loc[:,'PULSE'] = X_train.loc[:,'PULSE'].apply(pd.to_numeric)
X_test.loc[:,'PULSE'] = X_test.loc[:,'PULSE'].apply(pd.to_numeric)
然后,我们编写一个类似于mean_impute_values()的mean_impute_vitals()函数,只是占位符值从-7和-9变为-998和-9:
def mean_impute_vitals(data,col):
temp_mean = data.loc[(data[col] != 998) & (data[col] != -9), col].mean()
data.loc[(data[col] == 998) | (data[col] == -9), col] = temp_mean
return data
X_train = mean_impute_vitals(X_train,'PULSE')
X_test = mean_impute_vitals(X_test,'PULSE')
呼吸频率
呼吸频率表示一个人每分钟的呼吸次数。18-20 次被认为是正常的。呼吸急促(异常增高的呼吸频率)在临床实践中常见,通常表明体内缺氧,通常由于心脏或肺部问题。缓呼吸是指异常低的呼吸频率。
在以下代码中,我们将RESPR变量转换为数值类型,然后对缺失值进行均值插补:
X_train.loc[:,'RESPR'] = X_train.loc[:,'RESPR'].apply(pd.to_numeric)
X_test.loc[:,'RESPR'] = X_test.loc[:,'RESPR'].apply(pd.to_numeric)
X_train = mean_impute_values(X_train,'RESPR')
X_test = mean_impute_values(X_test,'RESPR')
血压
血压测量血液对血管壁施加的单位面积的压力。血压由两个数字组成——收缩压(心跳收缩期的血压)和舒张压(心跳舒张期的血压)。正常的血压通常在收缩压 110 到 120 mmHg 之间,舒张压在 70 到 80 mmHg 之间。升高的血压称为高血压。高血压最常见的原因是原发性高血压,主要是遗传因素(但是多因素的)。低血压称为低血压。高血压和低血压都有复杂的病因,通常难以识别。
在我们的数据集中,收缩压和舒张压分别位于单独的列中(BPSYS和BPDIAS)。首先,我们处理收缩压,将其转换为数值类型,并像其他列一样进行均值插补:
X_train.loc[:,'BPSYS'] = X_train.loc[:,'BPSYS'].apply(pd.to_numeric)
X_test.loc[:,'BPSYS'] = X_test.loc[:,'BPSYS'].apply(pd.to_numeric)
X_train = mean_impute_values(X_train,'BPSYS')
X_test = mean_impute_values(X_test,'BPSYS')
收缩压稍微复杂一些。998 这个值表示压力为 PALP,意味着它低到无法通过血压计检测,但又足够高,能通过触诊(触摸)感受到。我们将其转换为数值类型后,会将 PALP 值替换为数值 40:
X_train.loc[:,'BPDIAS'] = X_train.loc[:,'BPDIAS'].apply(pd.to_numeric)
X_test.loc[:,'BPDIAS'] = X_test.loc[:,'BPDIAS'].apply(pd.to_numeric)
我们编写了一个新函数 mean_impute_bp_diast(),它将 PALP 值转换为 40,并将缺失值填充为均值:
def mean_impute_bp_diast(data,col):
temp_mean = data.loc[(data[col] != 998) & (data[col] != -9), col].mean()
data.loc[data[col] == 998, col] = 40
data.loc[data[col] == -9, col] = temp_mean
return data
X_train = mean_impute_values(X_train,'BPDIAS')
X_test = mean_impute_values(X_test,'BPDIAS')
血氧饱和度
血氧饱和度是衡量血液中氧气水平的指标,通常以百分比形式表示,值越高越健康。我们将其转换为数值类型并进行均值填充,过程如下:
X_train.loc[:,'POPCT'] = X_train.loc[:,'POPCT'].apply(pd.to_numeric)
X_test.loc[:,'POPCT'] = X_test.loc[:,'POPCT'].apply(pd.to_numeric)
X_train = mean_impute_values(X_train,'POPCT')
X_test = mean_impute_values(X_test,'POPCT')
让我们通过选择这些列并使用 head() 函数来检查目前为止我们所做的生命体征转换:
X_train[['TEMPF','PULSE','RESPR','BPSYS','BPDIAS','POPCT']].head(n=20)
输出如下:
| TEMPF | PULSE | RESPR | BPSYS | BPDIAS | |
|---|---|---|---|---|---|
| 15938 | 98.200000 | 101.000000 | 22.0 | 159.000000 | 72.000000 |
| 5905 | 98.100000 | 70.000000 | 18.0 | 167.000000 | 79.000000 |
| 4636 | 98.200000 | 85.000000 | 20.0 | 113.000000 | 70.000000 |
| 9452 | 98.200000 | 84.000000 | 20.0 | 146.000000 | 72.000000 |
| 7558 | 99.300000 | 116.000000 | 18.0 | 131.000000 | 82.000000 |
| 17878 | 99.000000 | 73.000000 | 16.0 | 144.000000 | 91.000000 |
| 21071 | 97.800000 | 88.000000 | 18.0 | 121.000000 | 61.000000 |
| 20990 | 98.600000 | 67.000000 | 16.0 | 112.000000 | 65.000000 |
| 4537 | 98.200000 | 85.000000 | 20.0 | 113.000000 | 72.000000 |
| 7025 | 99.300000 | 172.000000 | 40.0 | 124.000000 | 80.000000 |
| 2134 | 97.500000 | 91.056517 | 18.0 | 146.000000 | 75.000000 |
| 5212 | 97.400000 | 135.000000 | 18.0 | 125.000000 | 71.000000 |
| 9213 | 97.900000 | 85.000000 | 18.0 | 153.000000 | 96.000000 |
| 2306 | 97.000000 | 67.000000 | 20.0 | 136.000000 | 75.000000 |
| 6106 | 98.600000 | 90.000000 | 18.0 | 109.000000 | 70.000000 |
| 2727 | 98.282103 | 83.000000 | 17.0 | 123.000000 | 48.000000 |
| 4098 | 99.100000 | 147.000000 | 20.0 | 133.483987 | 78.127013 |
| 5233 | 98.800000 | 81.000000 | 16.0 | 114.000000 | 78.000000 |
| 5107 | 100.000000 | 95.000000 | 24.0 | 133.000000 | 75.000000 |
| 18327 | 98.900000 | 84.000000 | 16.0 | 130.000000 | 85.000000 |
查看前面的表格,似乎一切正常。我们可以看到每列的填充均值(值具有额外的精度)。现在我们来处理数据中的最后一个生命体征——疼痛级别。
疼痛级别
疼痛是人体出现问题的常见表现,疼痛程度通常会在每次医疗访谈中被询问,无论是初诊病史和体格检查,还是每日 SOAP 记录。疼痛程度通常用从 0(无痛)到 10(无法忍受)的等级来报告。我们首先将PAINSCALE列转换为数值类型:
X_train.loc[:,'PAINSCALE'] = X_train.loc[:,'PAINSCALE'].apply(pd.to_numeric)
X_test.loc[:,'PAINSCALE'] = X_test.loc[:,'PAINSCALE'].apply(pd.to_numeric)
现在,我们需要为疼痛值的均值填充编写一个单独的函数,因为它使用-8作为占位符,而不是-7:
def mean_impute_pain(data,col):
temp_mean = data.loc[(data[col] != -8) & (data[col] != -9), col].mean()
data.loc[(data[col] == -8) | (data[col] == -9), col] = temp_mean
return data
X_train = mean_impute_pain(X_train,'PAINSCALE')
X_test = mean_impute_pain(X_test,'PAINSCALE')
综合来看,生命体征为患者健康提供了一个重要的整体图像。最终,我们将看到在进行变量重要性分析时,这些变量所发挥的关键作用。
现在,我们可以进入下一个变量类别。
就诊原因代码
就诊原因变量编码了患者就诊的原因,可以视为就诊的主要诉求(我们在第二章,医疗基础中讨论了主要诉求)。在这个数据集中,这些原因使用一个名为门诊就诊原因分类的代码集进行编码(更多信息请参考 2011 年文档的第 16 页和附录二;附录第一页的截图已提供在本章结尾)。虽然确切的代码在患者就诊初期可能无法确定,但我们在这里包括它,因为:
-
它反映了患者就诊初期可用的信息。
-
我们想展示如何处理一个编码变量(其他所有编码变量出现在患者就诊的较晚阶段,无法用于本建模任务):

编码变量需要特别注意,原因如下:
-
表格中常常会有多个条目对应多个代码,原因-就诊代码也不例外。注意,这个数据集包含了三个 RFV 列(
RFV1、RFV2和RFV3)。例如,哮喘的代码可能会出现在这三列中的任何一列。因此,单独对这些列进行独热编码是不够的。我们必须检测每个代码在任何这三列中的出现情况,并且必须编写一个特殊的函数来处理这个问题。 -
代码是分类变量,但这些数字本身通常没有实际意义。为了便于解释,我们必须相应地命名这些列,使用适当的描述。为此,我们整理了一份特殊的
.csv文件,包含了每个代码的描述(可在本书的 GitHub 仓库中下载)。 -
一种可能的输出格式是每个代码对应一列,其中
1表示该代码的存在,0表示其不存在(如 Futoma 等,2015 年所做)。然后可以进行任何所需的组合/转换。我们在此采用了这种格式。
不再赘述,让我们开始转换我们的就诊原因变量。首先,我们导入 RFV 代码描述:
rfv_codes_path = HOME_PATH + 'RFV_CODES.csv'
rfv_codes = pd.read_csv(rfv_codes_path,header=0,dtype='str')
现在我们将开始处理 RFV 代码。
首先,为了正确命名列,我们从re模块导入sub()函数(re代表正则表达式)。
然后,我们编写一个函数,扫描任何给定的 RFV 列,检查是否存在指定的代码,并返回包含新列的数据集。如果代码存在,则列值为1,如果代码不存在,则列值为0。
接下来,我们使用for循环遍历.csv文件中的每个代码,有效地为每个可能的代码添加一个二进制列。我们对训练集和测试集都执行此操作。
最后,我们删除原始的 RFV 列,因为我们不再需要它们。完整的代码如下:
from re import sub
def add_rfv_column(data,code,desc,rfv_columns):
column_name = 'rfv_' + sub(" ", "_", desc)
data[column_name] = (data[rfv_columns] == rfv_code).any(axis=1).astype('int')
return data
rfv_columns = ['RFV1','RFV2','RFV3']
for (rfv_code,rfv_desc) in zip(
rfv_codes['Code'].tolist(),rfv_codes['Description'].tolist()
):
X_train = add_rfv_column(
X_train,
rfv_code,
rfv_desc,
rfv_columns
)
X_test = add_rfv_column(
X_test,
rfv_code,
rfv_desc,
rfv_columns
)
# Remove original RFV columns
X_train.drop(rfv_columns, axis=1, inplace=True)
X_test.drop(rfv_columns, axis=1, inplace=True)
让我们用head()函数查看一下转换后的数据集:
X_train.head(n=5)
注意,现在有 1,264 列。虽然完整的 DataFrame 已被截断,但如果你水平滚动,你应该能看到一些新的rfv_列被添加到 DataFrame 的末尾。
伤害代码
伤害代码也包含在数据中。虽然就诊原因代码适用于所有就诊,但伤害代码仅在患者遭受了身体伤害、中毒或医疗治疗的副作用(包括自杀未遂)时适用。由于伤害的确切原因可能在进行全面检查之前无法得知,而且该检查通常在决定是否住院后才会进行,因此我们将删除伤害代码变量,因为它们可能包含在预测时不可用的未来信息。不过,如果你希望在建模任务中使用这些代码,请记住,编码数据可以像之前所示的那样进行处理。有关伤害变量的更多细节,请参阅文档:
inj_cols = [
'INJURY','INJR1','INJR2','INJPOISAD','INJPOISADR1',
'INJPOISADR2','INTENT','INJDETR','INJDETR1','INJDETR2',
'CAUSE1','CAUSE2','CAUSE3','CAUSE1R','CAUSE2R','CAUSE3R'
]
X_train.drop(inj_cols, axis=1, inplace=True)
X_test.drop(inj_cols, axis=1, inplace=True)
诊断代码
数据集还包含 ICD-9-DM 代码,用于分类与每次就诊相关的诊断。请注意,数据集中有三个诊断代码列。这与我们在就诊原因代码部分提到的编码变量一致。由于 ICD-9 代码通常在进行检查并确定症状原因后分配给就诊,因此我们将需要将其从这个建模任务中排除:
diag_cols= [
'DIAG1','DIAG2','DIAG3',
'PRDIAG1','PRDIAG2','PRDIAG3',
'DIAG1R','DIAG2R','DIAG3R'
]
X_train.drop(diag_cols, axis=1, inplace=True)
X_test.drop(diag_cols, axis=1, inplace=True)
病史
正如我们在第二章《医疗基础》中讨论的那样,患有慢性疾病的人通常比没有慢性疾病的人健康状况较差,健康结果也较差。数据集中包括了每次就诊时 11 种常见慢性疾病的相关信息。这些疾病包括癌症、脑血管疾病、慢性阻塞性肺病、需要透析的疾病、充血性心力衰竭、痴呆、糖尿病、心肌梗死历史、肺栓塞或深静脉血栓历史,以及 HIV/AIDS。由于以前就诊的患者通常能够获取其病史信息,并且这些信息通常在患者分诊时就已明确,因此我们决定在此包含这些变量。因为这些变量已经是二元的,所以不需要进一步处理。
还有一个连续变量,名为 TOTCHRON,用于统计每位患者的慢性病总数,我们将其进行均值填充,如下所示:
X_train.loc[:,'TOTCHRON'] = X_train.loc[:,'TOTCHRON'].apply(pd.to_numeric)
X_test.loc[:,'TOTCHRON'] = X_test.loc[:,'TOTCHRON'].apply(pd.to_numeric)
X_train = mean_impute_values(X_train,'TOTCHRON')
X_test = mean_impute_values(X_test,'TOTCHRON')
测试
医学测试虽然重要,但发生在预测时间之后,因此必须在此用例中省略。它们可能会用于其他建模任务,如再入院预测或死亡率:
testing_cols = [
'ABG','BAC','BLOODCX','BNP','BUNCREAT',
'CARDENZ','CBC','DDIMER','ELECTROL','GLUCOSE',
'LACTATE','LFT','PTTINR','OTHERBLD','CARDMON',
'EKG','HIVTEST','FLUTEST','PREGTEST','TOXSCREN',
'URINE','WOUNDCX','URINECX','OTHRTEST','ANYIMAGE',
'XRAY','IVCONTRAST','CATSCAN','CTAB','CTCHEST',
'CTHEAD','CTOTHER','CTUNK','MRI','ULTRASND',
'OTHIMAGE','TOTDIAG','DIAGSCRN'
]
X_train.drop(testing_cols, axis=1, inplace=True)
X_test.drop(testing_cols, axis=1, inplace=True)
程序
我们省略了程序,因为与测试类似,它们通常发生在预测时间之后:
proc_cols = [
'PROC','BPAP','BLADCATH','CASTSPLINT','CENTLINE',
'CPR','ENDOINT','INCDRAIN','IVFLUIDS','LUMBAR',
'NEBUTHER','PELVIC','SKINADH','SUTURE','OTHPROC',
'TOTPROC'
]
X_train.drop(proc_cols, axis=1, inplace=True)
X_test.drop(proc_cols, axis=1, inplace=True)
药物编码
数据包括关于急诊科所给予的药物和/或出院时开出的药物的充足信息。实际上,最多可提供 12 种药物的信息,分布在不同的列中。显然,药物的给药是在决定是否接纳患者之后,因此我们不能在此用例中使用这些列。
尽管如此,如果你希望在自己的预测建模中使用此类信息,我们鼓励你查阅文档并阅读有关药物编码系统的相关内容:
med_cols = [
'MED1','MED2','MED3','MED4','MED5',
'MED6','MED7','MED8','MED9','MED10',
'MED11','MED12','GPMED1','GPMED2','GPMED3',
'GPMED4','GPMED5','GPMED6','GPMED7','GPMED8',
'GPMED9','GPMED10','GPMED11','GPMED12','NUMGIV',
'NUMDIS','NUMMED',
]
X_train.drop(med_cols, axis=1, inplace=True)
X_test.drop(med_cols, axis=1, inplace=True)
提供者信息
提供者列指示参与医疗接触的医疗提供者类型。我们已省略以下变量:
prov_cols = [
'NOPROVID','ATTPHYS','RESINT','CONSULT','RNLPN',
'NURSEPR','PHYSASST','EMT','MHPROV','OTHPROV'
]
X_train.drop(prov_cols, axis=1, inplace=True)
X_test.drop(prov_cols, axis=1, inplace=True)
处置信息
由于处置变量与结果直接相关,因此我们不能将其保留在数据中。我们在此省略它们(请回顾我们在创建最终目标列后已删除了若干处置变量):
disp_cols = [
'NODISP','NOFU','RETRNED','RETREFFU','LEFTBTRI',
'LEFTAMA','DOA','DIEDED','TRANNH','OTHDISP',
'ADMIT','ADMTPHYS','BOARDED','LOS','HDDIAG1',
'HDDIAG2','HDDIAG3','HDDIAG1R','HDDIAG2R','HDDIAG3R',
'HDSTAT','ADISP','OBSSTAY','STAY24'
]
X_train.drop(disp_cols, axis=1, inplace=True)
X_test.drop(disp_cols, axis=1, inplace=True)
填充列
这些列表示包含填充数据的列。大部分情况下,我们在数据中已经包括了未填充的对应列,因此不需要填充列,所以我们将其删除:
imp_cols = [
'AGEFL','BDATEFL','SEXFL','ETHNICFL','RACERFL'
]
X_train.drop(imp_cols, axis=1, inplace=True)
X_test.drop(imp_cols, axis=1, inplace=True)
识别变量
当结合在一起时,识别变量为每次接触提供唯一的键。虽然在许多情况下这很有用,但幸运的是,pandas DataFrame 已经为每一行唯一分配了一个整数,因此我们可以删除 ID 变量:
id_cols = [
'HOSPCODE','PATCODE'
]
X_train.drop(id_cols, axis=1, inplace=True)
X_test.drop(id_cols, axis=1, inplace=True)
电子病历状态栏
数据集中包含数十列指示患者就诊时所在医疗机构的技术水平。我们在第二章《EHR 技术与有意义的使用》部分中讨论了这一点,医疗基础。由于这些列是按医院而非就诊事件来评估的,因此我们将忽略这些列:
emr_cols = [
'EBILLANYE','EMRED','HHSMUE','EHRINSE','EDEMOGE',
'EDEMOGER','EPROLSTE','EPROLSTER','EVITALE','EVITALER',
'ESMOKEE','ESMOKEER','EPNOTESE','EPNOTESER','EMEDALGE',
'EMEDALGER','ECPOEE','ECPOEER','ESCRIPE','ESCRIPER',
'EWARNE','EWARNER','EREMINDE','EREMINDER','ECTOEE',
'ECTOEER','EORDERE','EORDERER','ERESULTE','ERESULTER',
'EGRAPHE','EGRAPHER','EIMGRESE','EIMGRESER','EPTEDUE',
'EPTEDUER','ECQME','ECQMER','EGENLISTE','EGENLISTER',
'EIMMREGE','EIMMREGER','ESUME','ESUMER','EMSGE',
'EMSGER','EHLTHINFOE','EHLTHINFOER','EPTRECE','EPTRECER',
'EMEDIDE','EMEDIDER','ESHAREE','ESHAREEHRE','ESHAREWEBE',
'ESHAREOTHE','ESHAREUNKE','ESHAREREFE','LABRESE1','LABRESE2',
'LABRESE3','LABRESE4','LABRESUNKE','LABRESREFE','IMAGREPE1',
'IMAGREPE2','IMAGREPE3','IMAGREPE4','IMAGREPUNKE','IMAGREPREFE',
'PTPROBE1','PTPROBE2','PTPROBE3','PTPROBE4','PTPROBUNKE',
'PTPROBREFE','MEDLISTE1','MEDLISTE2','MEDLISTE3','MEDLISTE4',
'MEDLISTUNKE','MEDLISTREFE','ALGLISTE1','ALGLISTE2','ALGLISTE3',
'ALGLISTE4','ALGLISTUNKE','ALGLISTREFE','EDPRIM','EDINFO',
'MUINC','MUYEAR'
]
X_train.drop(emr_cols, axis=1, inplace=True)
X_test.drop(emr_cols, axis=1, inplace=True)
详细的药物信息
这些列包含更详细的药物信息,包括药物类别的编码信息。我们必须忽略这些列,因为它们表示的是未来信息。然而,这些信息在其他机器学习问题中可能非常有用:
drug_id_cols = [
'DRUGID1','DRUGID2','DRUGID3','DRUGID4','DRUGID5',
'DRUGID6','DRUGID7','DRUGID8','DRUGID9','DRUGID10',
'DRUGID11','DRUGID12'
]
drug_lev1_cols = [
'RX1V1C1','RX1V1C2','RX1V1C3','RX1V1C4',
'RX2V1C1','RX2V1C2','RX2V1C3','RX2V1C4',
'RX3V1C1','RX3V1C2','RX3V1C3','RX3V1C4',
'RX4V1C1','RX4V1C2','RX4V1C3','RX4V1C4',
'RX5V1C1','RX5V1C2','RX5V1C3','RX5V1C4',
'RX6V1C1','RX6V1C2','RX6V1C3','RX6V1C4',
'RX7V1C1','RX7V1C2','RX7V1C3','RX7V1C4',
'RX8V1C1','RX8V1C2','RX8V1C3','RX8V1C4',
'RX9V1C1','RX9V1C2','RX9V1C3','RX9V1C4',
'RX10V1C1','RX10V1C2','RX10V1C3','RX10V1C4',
'RX11V1C1','RX11V1C2','RX11V1C3','RX11V1C4',
'RX12V1C1','RX12V1C2','RX12V1C3','RX12V1C4'
]
drug_lev2_cols = [
'RX1V2C1','RX1V2C2','RX1V2C3','RX1V2C4',
'RX2V2C1','RX2V2C2','RX2V2C3','RX2V2C4',
'RX3V2C1','RX3V2C2','RX3V2C3','RX3V2C4',
'RX4V2C1','RX4V2C2','RX4V2C3','RX4V2C4',
'RX5V2C1','RX5V2C2','RX5V2C3','RX5V2C4',
'RX6V2C1','RX6V2C2','RX6V2C3','RX6V2C4',
'RX7V2C1','RX7V2C2','RX7V2C3','RX7V2C4',
'RX8V2C1','RX8V2C2','RX8V2C3','RX8V2C4',
'RX9V2C1','RX9V2C2','RX9V2C3','RX9V2C4',
'RX10V2C1','RX10V2C2','RX10V2C3','RX10V2C4',
'RX11V2C1','RX11V2C2','RX11V2C3','RX11V2C4',
'RX12V2C1','RX12V2C2','RX12V2C3','RX12V2C4'
]
drug_lev3_cols = [
'RX1V3C1','RX1V3C2','RX1V3C3','RX1V3C4',
'RX2V3C1','RX2V3C2','RX2V3C3','RX2V3C4',
'RX3V3C1','RX3V3C2','RX3V3C3','RX3V3C4',
'RX4V3C1','RX4V3C2','RX4V3C3','RX4V3C4',
'RX5V3C1','RX5V3C2','RX5V3C3','RX5V3C4',
'RX6V3C1','RX6V3C2','RX6V3C3','RX6V3C4',
'RX7V3C1','RX7V3C2','RX7V3C3','RX7V3C4',
'RX8V3C1','RX8V3C2','RX8V3C3','RX8V3C4',
'RX9V3C1','RX9V3C2','RX9V3C3','RX9V3C4',
'RX10V3C1','RX10V3C2','RX10V3C3','RX10V3C4',
'RX11V3C1','RX11V3C2','RX11V3C3','RX11V3C4',
'RX12V3C1','RX12V3C2','RX12V3C3','RX12V3C4'
]
addl_drug_cols = [
'PRESCR1','CONTSUB1','COMSTAT1','RX1CAT1','RX1CAT2',
'RX1CAT3','RX1CAT4','PRESCR2','CONTSUB2','COMSTAT2',
'RX2CAT1','RX2CAT2','RX2CAT3','RX2CAT4','PRESCR3','CONTSUB3',
'COMSTAT3','RX3CAT1','RX3CAT2','RX3CAT3','RX3CAT4','PRESCR4',
'CONTSUB4','COMSTAT4','RX4CAT1','RX4CAT2','RX4CAT3',
'RX4CAT4','PRESCR5','CONTSUB5','COMSTAT5','RX5CAT1',
'RX5CAT2','RX5CAT3','RX5CAT4','PRESCR6','CONTSUB6',
'COMSTAT6','RX6CAT1','RX6CAT2','RX6CAT3','RX6CAT4','PRESCR7',
'CONTSUB7','COMSTAT7','RX7CAT1','RX7CAT2','RX7CAT3',
'RX7CAT4','PRESCR8','CONTSUB8','COMSTAT8','RX8CAT1',
'RX8CAT2','RX8CAT3','RX8CAT4','PRESCR9','CONTSUB9',
'COMSTAT9','RX9CAT1','RX9CAT2','RX9CAT3','RX9CAT4',
'PRESCR10','CONTSUB10','COMSTAT10','RX10CAT1','RX10CAT2',
'RX10CAT3','RX10CAT4','PRESCR11','CONTSUB11','COMSTAT11',
'RX11CAT1','RX11CAT2','RX11CAT3','RX11CAT4','PRESCR12',
'CONTSUB12','COMSTAT12','RX12CAT1','RX12CAT2','RX12CAT3',
'RX12CAT4'
]
X_train.drop(drug_id_cols, axis=1, inplace=True)
X_train.drop(drug_lev1_cols, axis=1, inplace=True)
X_train.drop(drug_lev2_cols, axis=1, inplace=True)
X_train.drop(drug_lev3_cols, axis=1, inplace=True)
X_train.drop(addl_drug_cols, axis=1, inplace=True)
X_test.drop(drug_id_cols, axis=1, inplace=True)
X_test.drop(drug_lev1_cols, axis=1, inplace=True)
X_test.drop(drug_lev2_cols, axis=1, inplace=True)
X_test.drop(drug_lev3_cols, axis=1, inplace=True)
X_test.drop(addl_drug_cols, axis=1, inplace=True)
其他信息
最后,数据集末尾有几个与我们目的无关的列,因此我们将它们删除:
design_cols = ['CSTRATM','CPSUM','PATWT','EDWT']
X_train.drop(design_cols, axis=1, inplace=True)
X_test.drop(design_cols, axis=1, inplace=True)
最终的预处理步骤
现在我们已经完成了所有变量组的处理,几乎可以开始构建我们的预测模型了。但首先,我们必须将所有类别变量扩展为二进制变量(也叫做独热编码或 1-of-K 表示法),并将数据转换为适合输入到scikit-learn方法的格式。接下来我们就来做这个。
独热编码
许多scikit-learn库的分类器要求类别变量进行独热编码。独热编码,或1-of-K 表示法,是指将一个有多个可能值的类别变量记录为多个变量,每个变量有两个可能的值。
例如,假设我们在数据集中有五个患者,我们希望对表示主要就诊诊断的列进行独热编码。在进行独热编码之前,这一列看起来是这样的:
patient_id |
primary_dx |
|---|---|
| 1 | copd |
| 2 | hypertension |
| 3 | copd |
| 4 | chf |
| 5 | asthma |
在进行独热编码后,这一列将被拆分成K列,其中K是可能值的数量,每一列的值为 0 或 1,具体取决于该观察值是否对应该列的值:
patient_id |
primary_dx_copd |
primary_dx_hypertension |
primary_dx_chf |
primary_dx_asthma |
|---|---|---|---|---|
| 1 | 1 | 0 | 0 | 0 |
| 2 | 0 | 1 | 0 | 0 |
| 3 | 1 | 0 | 0 | 0 |
| 4 | 0 | 0 | 1 | 0 |
| 5 | 0 | 0 | 0 | 1 |
请注意,我们已经将上一列的字符串转换为整数表示。这是有道理的,因为机器学习算法是基于数字而不是单词来训练的!这就是为什么独热编码是必要的原因。
scikit-learn在其preprocessing模块中有一个OneHotEncoder类。然而,pandas有一个get_dummies()函数,能够用一行代码完成独热编码。我们将使用pandas函数。在此之前,我们必须确定数据集中哪些列是类别变量,然后传递给该函数。我们通过使用元数据来识别类别列,并查看这些列与我们数据中剩余列的交集:
categ_cols = df_helper.loc[
df_helper['variable_type'] == 'CATEGORICAL', 'column_name'
]
one_hot_cols = list(set(categ_cols) & set(X_train.columns))
X_train = pd.get_dummies(X_train, columns=one_hot_cols)
我们还必须对测试数据进行独热编码:
X_test = pd.get_dummies(X_test, columns=one_hot_cols)
最后要提到的是,测试集可能包含在训练数据中未曾见过的类别值。这可能会导致在使用测试集评估模型性能时出现错误。为防止这种情况,您可能需要编写一些额外的代码,将测试集中缺失的列设置为零。幸运的是,在我们的数据集中我们不需要担心这个问题。
数字转换
现在让我们将所有列转换为数字格式:
X_train.loc[:,X_train.columns] = X_train.loc[:,X_train.columns].apply(pd.to_numeric)
X_test.loc[:,X_test.columns] = X_test.loc[:,X_test.columns].apply(pd.to_numeric)
NumPy 数组转换
最后的步骤是获取将直接传入机器学习算法的 pandas 数据框的 NumPy 数组。首先,我们保存最终的列名,这将在稍后评估变量重要性时帮助我们:
X_train_cols = X_train.columns
X_test_cols = X_test.columns
现在,我们使用pandas数据框的values属性来访问每个数据框的底层 NumPy 数组:
X_train = X_train.values
X_test = X_test.values
现在,我们已经准备好构建模型。
构建模型
在本节中,我们将构建三种类型的分类器并评估它们的性能:逻辑回归分类器、随机森林和神经网络。
逻辑回归
我们在第三章中讨论了逻辑回归模型的直觉和基本概念,机器学习基础。为了在我们的训练集上构建一个模型,我们使用以下代码:
from sklearn.linear_model import LogisticRegression
clfs = [LogisticRegression()]
for clf in clfs:
clf.fit(X_train, y_train.ravel())
print(type(clf))
print('Training accuracy: ' + str(clf.score(X_train, y_train)))
print('Validation accuracy: ' + str(clf.score(X_test, y_test)))
coefs = {
'column': [X_train_cols[i] for i in range(len(X_train_cols))],
'coef': [clf.coef_[0,i] for i in range(len(X_train_cols))]
}
df_coefs = pd.DataFrame(coefs)
print(df_coefs.sort_values('coef', axis=0, ascending=False))
在for循环之前,我们导入LogisticRegression类,并将clf设置为LogisticRegression实例。训练和测试过程发生在for循环中。首先,我们使用fit()方法拟合模型(例如,确定最优的系数),使用训练数据。接下来,我们使用score()方法评估模型在训练数据和测试数据上的表现。
在for循环的后半部分,我们打印出每个特征的系数值。通常,系数离零更远的特征与结果的相关性最强(无论是正相关还是负相关)。然而,我们在训练之前没有对数据进行缩放,因此有可能更重要的预测变量由于没有适当缩放而会有较低的系数。
代码的输出应如下所示:
<class 'sklearn.linear_model.logistic.LogisticRegression'>
Training accuracy: 0.888978581423
Validation accuracy: 0.884261501211
coef column
346 2.825056 rfv_Symptoms_of_onset_of_labor
696 1.618454 rfv_Adverse_effect_of_drug_abuse
95 1.467790 rfv_Delusions_or_hallucinations
108 1.435026 rfv_Other_symptoms_or_problems_relating_to_psy...
688 1.287535 rfv_Suicide_attempt
895 1.265043 IMMEDR_01
520 1.264023 rfv_General_psychiatric_or_psychological_exami...
278 1.213235 rfv_Jaundice
712 1.139245 rfv_For_other_and_unspecified_test_results
469 1.084806 rfv_Other_heart_disease
...
首先,让我们讨论一下训练集和测试集的表现。它们非常接近,这表明模型没有过拟合训练集。准确率约为 88%,与研究中的表现(Cameron 等人,2015 年)相当,用于预测急诊科状态。
查看系数后,我们可以确认它们符合直觉。系数最高的特征与怀孕中的分娩开始相关;我们都知道,分娩通常会导致住院。许多与严重精神疾病相关的特征几乎总是会导致住院,因为患者对自己或他人构成风险。IMMEDR_1特征的系数也很高;请记住,这个特征对应的是分诊量表中的 1,表示最紧急的情况:
...
898 -0.839861 IMMEDR_04
823 -0.848631 BEDDATA_03
625 -0.873828 rfv_Hand_and_fingers
371 -0.960739 rfv_Skin_rash
188 -0.963524 rfv_Earache_pain_
217 -0.968058 rfv_Soreness
899 -1.019763 IMMEDR_05
604 -1.075670 rfv_Suture__insertion_removal
235 -1.140021 rfv_Toothache
30 -1.692650 LEFTATRI
相反,滚动到页面底部会显示一些与入院呈负相关的特征。比如牙痛和需要拆除缝线的情况出现在这里,它们不太可能导致入院,因为这些情况并不是紧急问题。
我们已经训练了第一个模型。让我们看看一些更复杂的模型是否能带来改进。
随机森林
scikit-learn的一个便捷特性是,许多分类器具有相同的方法,因此可以互换使用模型。在下面的代码中,我们看到,当我们使用RandomForestClassifier类的fit()和score()方法来训练模型和评估其性能时:
from sklearn.ensemble import RandomForestClassifier
clfs_rf = [RandomForestClassifier(n_estimators=100)]
for clf in clfs_rf:
clf.fit(X_train, y_train.ravel())
print(type(clf))
print('Training accuracy: ' + str(clf.score(X_train, y_train)))
print('Validation accuracy: ' + str(clf.score(X_test, y_test)))
imps = {
'column': [X_train_cols[i] for i in range(len(X_train_cols))],
'imp': [clf.feature_importances_[i] for i in range(len(X_train_cols))]
}
df_imps = pd.DataFrame(imps)
print(df_imps.sort_values('imp', axis=0, ascending=False))
输出应该类似于以下内容:
<class 'sklearn.ensemble.forest.RandomForestClassifier'>
Training accuracy: 1.0
Validation accuracy: 0.885391444713
column imp
1 AGE 0.039517
13 PULSE 0.028348
15 BPSYS 0.026833
12 TEMPF 0.025898
16 BPDIAS 0.025844
0 WAITTIME 0.025111
14 RESPR 0.021329
17 POPCT 0.020407
29 TOTCHRON 0.018417
896 IMMEDR_02 0.016714
...
这次的验证准确率与逻辑回归模型相似,约为 88%。然而,训练数据上的准确率为 100%,这表明我们对模型进行了过拟合。我们将在本章末尾讨论一些改善模型的潜在方法。
查看特征重要性时,结果再次符合逻辑。这次,生命体征似乎是最重要的预测因子,还有年龄预测因子。滚动到页面底部显示了那些对预测没有影响的特征。请注意,与回归系数不同,在随机森林中,变量重要性的衡量标准不仅仅是系数的大小,还包括变量为正的频率;这可能解释了为什么IMMEDR_02的重要性排名高于IMMEDR_01。
神经网络
最后,我们来到了神经网络模型。请注意,我们的训练集只有大约 18,000 个样本;最成功的神经网络模型(例如,“深度学习”模型)通常使用数百万甚至数十亿个样本。尽管如此,让我们看看我们的神经网络模型的表现如何。对于神经网络,建议对数据进行适当缩放(例如,使其标准分布,均值为 0,标准差为 1)。我们使用StandardScaler类来完成这一操作:
from sklearn.preprocessing import StandardScaler
from sklearn.neural_network import MLPClassifier
# Scale data
scaler = StandardScaler()
scaler.fit(X_train)
X_train_Tx = scaler.transform(X_train)
X_test_Tx = scaler.transform(X_test)
# Fit models that require scaling (e.g. neural networks)
hl_sizes = [150,100,80,60,40,20]
nn_clfs = [MLPClassifier(hidden_layer_sizes=(size,), random_state=2345, verbose=True) for size in hl_sizes]
for num, nn_clf in enumerate(nn_clfs):
print(str(hl_sizes[num]) + '-unit network:')
nn_clf.fit(X_train_Tx, y_train.ravel())
print('Training accuracy: ' + str(nn_clf.score(X_train_Tx, y_train)))
print('Validation accuracy: ' + str(nn_clf.score(X_test_Tx, y_test)))
一旦你运行上述单元格,你将看到迭代完成,一旦迭代未能带来模型的改进,训练将停止,并打印出准确率。在我们的运行中,具有 150 个隐藏层单元的模型的验证准确率为 87%,高于其他隐藏层大小。
使用模型进行预测
我们已经完成了数据预处理、模型的构建和评分。AUC 与之前学术研究中预测 ED 结果的报告相似(见 Cameron 等,2015 年)。
下一步是保存并部署模型,并使用它进行实时预测。幸运的是,scikit-learn 库中的所有分类器都包含几个用于进行预测的函数:
-
对于大多数分类器,
predict()函数接受一个包含未标记数据的矩阵 X 作为输入,并简单地返回类预测结果,不包含其他信息。 -
predict_proba()函数接受一个包含未标记数据的矩阵 X 作为输入,并返回观察值属于各个类别的概率。这些概率对于每个观察值应该加起来等于1。 -
predict_log_proba()函数与predict_proba()函数类似,只不过它返回的是观察值属于每个类别的对数概率。
记住以下重要事实:在进行预测时,未标记的数据必须以与训练数据预处理相同的方式进行预处理。这包括:
-
列的添加与删除
-
列变换
-
缺失值的填充
-
缩放和中心化
-
独热编码
仅仅是一个没有正确预处理的列,可能会对模型预测产生极大的负面影响。
改进我们的模型
尽管在本章中我们构建了一个初步模型,其性能与学术研究中所报告的类似,但仍有改进的空间。以下是一些改善模型的想法,我们留给读者去实现这些建议以及其他可能的技巧或方法,以提高性能。你的性能能达到多高呢?
首先,当前的训练数据有大量的列。几乎总是会执行某种特征选择,尤其是对于逻辑回归和随机森林模型。对于逻辑回归,常见的特征选择方法包括:
-
使用具有最高系数的若干个预测变量
-
使用具有最低 p-值的若干个预测变量
-
使用 lasso 正则化并移除系数变为零的预测变量
-
使用贪心算法,例如前向或后向逐步逻辑回归,根据规则系统地移除/添加预测变量
-
使用暴力算法,例如最佳子集逻辑回归,测试给定数量的预测变量的所有排列/组合。
对于随机森林,使用变量重要性并选择具有最高重要性的若干个预测变量是非常常见的,进行网格搜索也是如此。
神经网络有其独特的改进技术。
-
一方面,更多的数据总是好的,特别是在神经网络中。
-
使用的具体优化算法可能会影响性能。
-
在这个例子中,我们的神经网络只有一个隐藏层。在行业中,具有多个隐藏层的模型变得越来越常见(尽管它们可能需要较长时间进行训练)。
-
使用的具体非线性激活函数也可能影响模型的性能。
总结
在本章中,我们构建了一个用于预测急诊科结果的预测模型。尽管医疗领域中有许多机器学习问题,但本练习展示了在预处理医疗数据、训练和评分模型以及使用无标签数据进行预测时通常会遇到的问题。本章标志着本书编码部分的结束。
既然我们已经亲自看到了预测模型的构建,接下来合乎逻辑的问题是,预测模型与传统统计风险评分在临床结果预测方面相比表现如何。我们将在下一章探讨这个问题。
参考文献及进一步阅读
Cameron A, Rodgers K, Ireland A 等(2015)。一项简单工具用于预测分诊时的入院情况。Emerg Med J 2015;32:174-179。
Futoma J, Morris J, Lucas J(2015)。预测早期住院再入院的模型比较。生物医学信息学杂志 56: 229-238。
第八章:医疗保健预测模型 – 综述
本章面向所有读者,将传统的风险评分模型(该模型通常用于医疗保健领域)与类似于第七章《医疗保健中的预测模型》一章中介绍的机器学习模型理论及特性相结合。如果你来自数据科学背景,本章将是一个很好的入门,帮助你了解一些广泛使用的临床风险评分,并指出哪些特征应该包含在你的模型中,无论是一般模型还是特定疾病的模型。如果你来自医疗保健背景,本章将回顾一些临床风险评分,并解释机器学习算法如何增强传统的风险评估。
医疗保健预测分析 – 最前沿
正如我们在第三章《机器学习基础》中提到的那样,医疗保健对复杂风险因素评估并不陌生。几乎每种主要疾病,都有几个风险评分模型被广泛应用于医生评估患病风险或患病后导致的致病/死亡风险。当我们使用“风险评分”一词时,我们通常指的是标准表格,在这些表格中,风险因素被赋予点值,所有风险因素的点数相加,给出基于总和的整体风险。这些评分系统在医学中广泛使用;有趣的是,其中许多基于涉及逻辑回归模型的研究(类似于第七章《医疗保健中的预测模型》中开发的模型)。过去几十年最关键的问题是,机器学习能否改善我们预测个体是否患病、该疾病需要多少护理以及患者是否会在特定时间内因该疾病而死亡的能力。
本章将解决这个问题。我们通过几个在发达国家已开发风险评分的主要致病和死亡原因来组织本章内容。这些领域包括整体心血管风险、充血性心力衰竭、癌症以及全因再入院。前三个领域是主要的死亡原因,第四个领域是衡量医疗质量的常见方式。随后,我们将探讨相关的机器学习文献,看看机器学习是否改善了该疾病的传统风险评估。通过本章的学习,你将对机器学习如何提升疾病预测有一个透彻的理解。
整体心血管风险
我们首先从整体心血管风险评估开始,因为它是个人健康中最重要的领域之一,而且心血管风险因素的探索有着悠久而丰富的历史。
心血管风险是指患上心血管疾病的风险。心血管疾病(CVD)是指由于动脉变窄和/或堵塞,导致血液供应到组织的循环系统功能障碍,这一过程被称为动脉粥样硬化。它涵盖了一系列心脏疾病,包括以下几种:
-
冠状动脉疾病(CAD):当供应心脏血液的血管因动脉粥样硬化而变窄时,发生冠状动脉疾病。冠状动脉疾病是致命的,因为它可能导致冠状动脉的突然堵塞,这被称为心肌梗死(或心脏病发作)。
-
充血性心力衰竭(CHF):这是指心脏无法将血液泵送到身体其他部位。它是由冠状动脉疾病长期影响心脏所致。其发生比心肌梗死更为渐进,但其过程往往以突然恶化和住院为特征,最终导致死亡。
-
外周血管疾病(PVD):当供应手臂或腿部的动脉变窄并发生阻塞时,称为外周血管疾病,这可能导致诸如疼痛(称为跛行)等问题症状,甚至可能导致截肢。
-
脑血管疾病,即供应大脑的血管发生动脉粥样硬化:这使个体更容易发生缺血性和出血性中风。中风是指血液供应到大脑被切断,这可能导致死亡,也可能导致严重的后遗症。
现在你知道了什么是心血管疾病(CVD),你还应该了解它对人类的破坏性影响。全球范围内,心血管疾病是导致发病率和死亡率的主要原因(Weng 等,2017)。仅充血性心力衰竭(CHF)就与 3-5%的住院人数相关,并且是医疗专业人员住院的主要原因,占发达国家医疗支出的高达 2%(Tripoliti 等,2016)。
尽管自 20 世纪初以来,心血管疾病一直是美国残疾和死亡的主要原因,但在 1940 年代,人们仍然不知道其成因。事实上,在那个时期,关于心血管疾病(以及一般疾病)的风险和预防几乎没有了解。当时,人们认为心血管疾病是任何患病者的命运,与生活方式的选择无关。
弗雷明汉风险评分
1948 年,美国国家心脏研究所与波士顿大学合作,启动了一个雄心勃勃的项目——Framingham 心脏研究。其目标是找出导致 CVD 的因素。1948 年,来自马萨诸塞州 Framingham 镇的 5209 名男女被招募参与,这些人当时尚未明显受到 CVD 的影响(Framingham 心脏研究,2018a)。每隔 2 年,这些人都要接受详细的病史记录、体检和实验室检测。随着时间的推移,新的患者群体不断加入,受试者也继续每 2 年返回进行评估,直到今天。
通过这项长期的前瞻性研究,心血管疾病(CVD)的风险因素首次被确定。吸烟与 CVD 之间的关联首次在 1960 年被报道(Framingham 心脏研究,2018b)。随后,胆固醇、高血压和糖尿病最终也与 CVD 有关联。从 1990 年代开始,开始发布特定类型 CVD(例如,心肌梗死、外周血管疾病、慢性心力衰竭)的风险评分。2008 年,发布了普遍适用的 Framingham 风险评分。Framingham 风险评分根据五个主要 CVD 风险因素:年龄、高血压、胆固醇水平、吸烟状态和糖尿病,给个体分配一个在 10 年内发生 CVD 事件的风险评分。以下是女性的一般心血管风险评分标准摘要(D'Agostino 等,2008)。
男性的标准与女性类似,但分值略有不同:
| 分数 | 年龄(岁) | HDL 胆固醇 | 总胆固醇 | 未经治疗的 SBP | 治疗中的 SBP | 吸烟者 | 糖尿病患者 |
|---|---|---|---|---|---|---|---|
| -3 | <120 | ||||||
| -2 | 60+ | ||||||
| -1 | 50-59 | <120 | |||||
| 0 | 30-34 | 45-49 | <160 | 120-129 | 否 | 否 | |
| 1 | 35-44 | 160-190 | 130-139 | ||||
| 2 | 35-39 | <35 | 140-149 | 120-129 | |||
| 3 | 200-239 | 130-139 | 是 | ||||
| 4 | 40-44 | 240-279 | 150-159 | 是 | |||
| 5 | 45-49 | 280+ | 160+ | 140-149 | |||
| 6 | 150-159 | ||||||
| 7 | 50-54 | 160+ | |||||
| 8 | 55-59 | ||||||
| 9 | 60-64 | ||||||
| 10 | 65-69 | ||||||
| 11 | 70-74 | ||||||
| 12 | 75+ |
如今,当我们去看医生时,这五个风险因素(其中四个是可以预防的)依然是医生们不断讲解的内容。
为了计算总评分,将六个风险因素(年龄、HDL 水平、总胆固醇、未经治疗的 SBP、治疗中的 SBP、吸烟者、糖尿病患者)的数值加在一起。下表显示了分值如何与 10 年风险评分对应(D'Agostino 等,2008):
| 分数 | 风险(%) | 分数 | 风险(%) | 分数 | 风险(%) |
|---|---|---|---|---|---|
| <-2 | <1 | 6 | 3.3 | 14 | 11.7 |
| -1 | 1.0 | 7 | 3.9 | 15 | 13.7 |
| 0 | 1.2 | 8 | 4.5 | 16 | 15.9 |
| 1 | 1.5 | 9 | 5.3 | 17 | 18.5 |
| 2 | 1.7 | 10 | 6.3 | 18 | 21.5 |
| 3 | 2.0 | 11 | 7.3 | 19 | 24.8 |
| 4 | 2.4 | 12 | 8.6 | 20 | 28.5 |
| 5 | 2.8 | 13 | 10.0 | 21 + | > 30 |
你可能会问,“这种疯狂背后有什么方法?”为了制定这些评分,研究的作者使用了 Cox 比例风险回归模型,它类似于逻辑回归,不同之处在于它不是确定变量与二元结果之间的关系,而是确定变量与事件发生前的时间量之间的关系。他们甚至计算了风险评分的C统计量(类似于第三章中讨论的曲线下面积,机器学习基础),结果为 0.76 到 0.79。这是一个非常好的评分,只需要通过患者病史、体检和一些简单的血液测试就能得到。
心血管风险与机器学习
如你所知,科学进步从不自满。当取得一个结果时,人们总会开始思考如何在此基础上进行改进。心血管风险评估也不例外。曾经被提出的关键问题包括以下几点:
-
有哪些其他的风险因素与《弗雷明汉风险评分》中的五个风险因素一样(或甚至更为重要)?
-
更新的机器学习算法能否超越统计模型,如回归分析,提供更高的辨识度和性能?
一项来自英国诺丁汉大学的研究探讨了这些问题(Weng et al., 2017)。这是一项前瞻性研究,监测了 2005 年到 2015 年间 378,256 名患者的电子病历。研究者使用了《弗雷明汉风险评分》中的 8 个风险因素的数据,以及从先前文献和与医生的咨询中认为与心血管风险相关的 22 个额外变量。这 22 个额外变量包括社会经济状态;包括肾脏病、关节炎和房颤等其他疾病的病史;如 C 反应蛋白和伽玛谷氨酰转移酶等较新的实验室检查;以及种族等信息。他们在患者数据上训练了四种类型的机器学习算法——逻辑回归、随机森林、神经网络和梯度提升机。四种算法的性能都优于基准风险预测算法;事实上,神经网络算法预测出了比现有算法多出 355 例正确的心血管事件。在查看重要变量时,虽然许多与《弗雷明汉标准》相似,但也有许多是新的。种族出现在所有算法的前三个重要变量中。社会经济状态(汤森德贫困指数)出现在四种算法的前十名中。慢性肾病也被发现与心血管风险相关。对于心血管风险,显然机器学习加深了我们对预测心脏事件的理解。
充血性心力衰竭
在总体心血管风险部分提到的所有心脏事件中,CHF 值得专门单独设立一个部分。这是由于三个主要原因:
-
CHF 是发达国家住院的最常见原因
-
其管理成本非常高,占总医疗支出的 2%左右
-
其诊断成本也非常高,需要进行昂贵的超声心动图检查,并由专业人员和医生进行解读和解释(Tripoliti 等,2016 年)
诊断充血性心力衰竭(CHF)
虽然在具有特定症状、风险因素、心电图表现和实验室结果的患者中,CHF 是可能的,但最终诊断只能通过超声心动图或心脏 MRI 来确立。超声心动图需要专业人员来进行测试,然后由专科医生(通常是心脏病专家或放射科医生)来解读结果,并评估心脏的泵血功能。这通常是通过估算射血分数(EF)来完成的,射血分数是指左心室在收缩过程中排出血液的比例。65%的 EF 被认为是正常的,40%表示心力衰竭,10-15%则见于 CHF 的晚期阶段。下图取自超声心动图,展示了心脏的四个腔室。你可以想象,使用声波产生的模糊图像来量化心脏功能可能是不可靠的:

心脏 MRI 虽然更昂贵,但在测量 EF 方面更为准确,并被视为 CHF 诊断的金标准;然而,它需要心脏病专家花费最多 20 分钟来解读单个扫描。在下图中,我们看到:
-
A) 展示心脏和用于心脏 MRI 成像平面的插图。
-
B) 心脏的三维血管造影图(血管造影是一种在注入染料后拍摄图像以更好地可视化血管的检查)。
-
C)、D) 和 E) 分别为正常、损伤和缺血性左心室的心脏 MRI 图像:

鉴于 CHF 诊断的复杂性,我们再次提出之前的问题:通过使用机器学习算法,是否可以发现新的风险因素,并在 CHF 诊断中实现更好的表现?
使用机器学习进行 CHF 检测
改进 CHF 检测的一个方法是避免使用昂贵且耗时的影像学检查。一项来自韩国启明大学的最新研究使用粗糙集、决策树和逻辑回归算法,并将其表现与医生的心脏病诊断进行比较,后者被作为金标准(Son 等,2012 年)。
粗糙集与最佳子集逻辑回归相似(例如,测试变量的子集以评估其信息量,并选择最具信息量的子集作为最终决策规则)。算法仅基于人口统计特征和实验室检查结果进行训练。该模型在区分充血性心力衰竭(CHF)与非 CHF 相关的呼吸急促时,达到了超过 97%的敏感性和 97%的特异性。这与人类表现极为接近,并且所用的数据、时间和资源大大减少。
我们应该提到的第二种方法是使用自动化算法读取用于诊断 CHF 的超声心动图和心脏 MRI 扫描。这一问题是 2015 年数据科学大赛的主题,该比赛由数据科学竞赛网站 Kaggle 和咨询公司 Booz Allen Hamilton 赞助。欲了解有关此竞赛的更多信息,请访问 Kaggle 竞赛网站:www.kaggle.com/c/second-annual-data-science-bowl。这显示了机器学习在医疗保健领域引起的关注。
机器学习在 CHF 中的其他应用
虽然人类疾病的检测和诊断始终是一个难题,但机器学习在 CHF 管理中的其他应用也不容忽视。这些应用在希腊 Ioannina 大学的一篇综述论文中有详细记录(Tripoliti 等,2017 年)。除了 CHF 检测外,该论文中涉及的与 CHF 管理相关的其他问题包括严重性评估、CHF 亚型分类、恶化和住院再入预测,以及死亡率。
癌症
在第二章,医疗基础设施中,我们列举了通过机器学习抗击癌症的几个原因,尽管癌症具有显著的全球发病率、死亡率和情感后果。 本节中,我们将更深入地探讨有关癌症的一些重要概念和背景,抗击癌症的潜在机器学习应用,癌症风险和生存模型中的重要特征,以及在乳腺癌领域已有的一些研究成果。
什么是癌症?
癌症可以描述为异常细胞的生长和繁殖。这些细胞与正常细胞的区别在于它们具有更强的繁殖能力,并且能够从正常细胞中夺取重要资源,如血液和营养。癌症通常分为良性和恶性两种类型。良性癌症意味着它局限在身体的某个局部区域,而恶性癌症则意味着它具有扩散到其他组织的能力。恶性癌症如果不治疗几乎总是会导致死亡,许多情况下即便治疗也难以避免死亡。死亡的进程依赖于多个因素,包括癌症的类型、发现时的临床分期、肿瘤的病理等级以及临床风险因素。良性癌症不像恶性癌症那样严重,尽管它们可能引发症状,且仍有可能导致死亡(例如,一种引起大脑高压的良性听神经瘤)。治疗通常包括化疗、放疗、生物制剂和/或肿瘤及周围组织和器官的外科切除。
正如你所知,癌症通常根据其首次出现的身体部位来分类。美国四种最致命的癌症类型是肺癌、乳腺癌、前列腺癌和结肠癌。
癌症的机器学习应用
有两篇综合性综述论文详细记录和总结了过去三十年内进行的癌症相关机器学习研究。第一篇综述论文来自加拿大阿尔伯塔大学,提供了 2006 年之前的相关研究的详细回顾(Cruz 和 Wishart, 2006)。第二篇则是来自希腊伊奥尼纳大学的较新论文(Kourou 等,2015)。这两篇论文很好地分解了癌症机器学习领域的子问题,并且与第二章《医疗基础》一章中讨论的一些子问题相匹配。这些问题包括:
-
癌症的早期检测/筛查:机器学习模型能否训练出来以识别在症状出现之前就有高风险发展为癌症的个体?
-
癌症诊断:机器学习如何帮助肿瘤学家/放射科医生做出癌症的明确诊断,并分类癌症的分期和分级?
-
癌症复发:对于已经通过初期治疗成功治愈的癌症患者,癌症复发的可能性有多大?
-
预期寿命和生存率:哪些患者的癌症可能导致死亡?哪些患者可能在五年后仍然存活?十年后呢?
-
肿瘤药物敏感性:哪些肿瘤可能对特定的癌症治疗(例如化疗、放疗、生物制剂或手术)产生反应?
癌症的重要特征
一些变量对于癌症相关机器学习的应用尤为重要。让我们现在来看一下这些变量。
常规临床数据
在所有患者的电子病历中常规可获得的临床数据尤为宝贵,因为这些数据成本低廉,且可以轻松纳入回顾性建模研究。这样的临床数据汇总在下表中。需要注意的是,这些规则有许多例外;例如,虽然高龄是大多数癌症的一个重要危险因素,但骨癌和一些白血病往往最常见于儿童(National Cancer Institute, 2015)。国家癌症研究所是有关癌症临床危险因素更详细分解的优秀资源(www.cancer.gov):
| 危险因素 | 增加癌症风险 | 减少癌症风险 |
|---|---|---|
| 老年 | × | |
| 家族史阳性 | × | |
| 高脂肪饮食 | × | |
| 高纤维饮食 | × | |
| 高水果和蔬菜饮食 | × | |
| 肥胖 | × | |
| 烟草使用 | × | |
| 酒精使用 | × | |
| 日晒 | × |
癌症特异性临床数据
在肿瘤的原发部位被确定后,几乎每个肿瘤都可以根据临床和病理学进一步分为亚型。肿瘤的临床分期对肿瘤的扩展程度进行分类。TNM 分期系统是最常见的;在这个系统下,分期是根据以下三个因素确定的:
-
肿瘤的大小(T)
-
附近淋巴结的受累情况(N)
-
肿瘤向其他部位的转移(M)
通常,生存率是根据临床分期来给出的,而临床分期是根据 TNM 分期标准来确定的,这对每个肿瘤有所不同。
肿瘤的病理分级关注的是其细胞特征。病理学家在设置肿瘤分级时所关注的事项包括肿瘤细胞外观与正常细胞外观的相似性(分化)和正常细胞器官的存在(National Cancer Institute, 2015)。
成像数据
来自 X 光、CT 扫描和 MRI 的放射影像学发现也可以在与癌症严重程度和预后相关的模型中使用。稍后在本节中,我们将探讨一项乳腺癌研究,该研究训练了一个神经网络,分析乳腺 X 光片特征,如乳腺肿瘤微钙化的存在和形状,以及周围皮肤特征(Ayer et al., 2010)。
基因组数据
尽管基因结果在前瞻性研究中并不总是容易获得,回顾性研究中也没有这些数据,但在有的情况下它们是重要的预测因素。具体特征包括患者 DNA 中单核苷酸多态性(SNPs;点突变)的存在,以及某些基因的存在(如遗传性乳腺癌中的 BRCA1)(Morin et al. Harrison's, 2005)。
蛋白质组数据
与肿瘤相关的特定蛋白质的存在也会影响风险。重要蛋白质的例子包括肿瘤相关生物标志物(例如,胰腺癌中的 CA 19-9)和激素(例如,乳腺癌中的 HER-2/neu)(Longo 等,2005)。
一个例子——乳腺癌预测
让我们看看机器学习如何增强乳腺癌的筛查和诊断——乳腺癌是全球最常见的癌症之一。
乳腺癌的传统筛查
乳腺癌筛查是一个复杂的问题。风险评分通常表现不佳;关于 Gail 模型在乳腺癌风险预测中的表现的文献综述发现,该模型的 AUC 值通常在 0.55 到 0.65 之间(一个随机分类器的 AUC 值为 0.5)(Wang 等,2018 年)。
乳腺癌筛查的下一种最低侵入性测试是临床乳腺检查或自我乳腺检查。乳腺检查的敏感性和特异性因年龄、乳腺密度和研究环境而异;有研究发现,敏感性最低可低至 57%,尽管同一研究发现特异性为 88%(Baines 等,1989)。因为好的筛查测试具有较高的敏感性,普遍认为仅凭体检进行乳腺癌筛查是不充分的。
影像学检查是乳腺癌筛查中的下一种最低侵入性测试。用于乳腺癌筛查的影像学方式包括乳腺 X 光检查、MRI 和超声波。2016 年,美国预防服务工作组(USPSTF)建议 50 岁及以上女性每两年做一次乳腺 X 光检查(见下图,图示为乳腺 X 光检查,显示出一个被诊断为胶样癌的白色区域;美国国家癌症研究所,1990 年),因为该年龄段的乳腺 X 光检查具有较高的敏感性(77%至 95%)和特异性(94%至 97%),并且它们对患者的潜在危害较低(美国预防服务工作组,2016 年)。
活检是乳腺癌检测中最具侵入性的选项,实际上,当筛查测试结果为阳性时,活检通常用于确诊。
乳腺癌筛查与机器学习
乳腺癌研究中的机器学习文献非常丰富(Cruz 和 Wishart,2006 年;Kourou 等,2015 年)。在这里,我们总结了一项研究,强调了机器学习在乳腺癌诊断中的潜力,尤其是当它与乳腺 X 光检查和电子病历(EMR)结合使用时。该研究来自威斯康星大学麦迪逊分校,包含了 48,744 张乳腺 X 光检查图像(Ayer 等,2010 年)。
对每个乳腺 X 光检查,收集了 36 个类别变量的信息,包括临床数据(年龄、既往病史、家族史)和乳腺 X 光发现,如肿瘤质量特征、周围皮肤和乳头特征、淋巴结检查以及钙化特征。一个由 1,000 个隐藏层组成的人工神经网络经过训练,并将生成的标签与良性与恶性的真实标签进行了比较。八名放射科医师也审核了不同数量的乳腺 X 光图像,并将其分类为良性或恶性。神经网络的总 AUC 为 0.965,而放射科医师的 AUC 为 0.939。结合我们在最后一章中将阅读的其他研究,这项研究展示了机器学习如何作为对抗癌症的有效工具:

预测再入院
预测所有原因的患者再入院可能性超出了典型临床医生的知识范围,因为它与特定的器官系统或疾病无关。然而,在医疗保健领域,这正变得越来越重要,因为可以预防的住院再入院是美国及其他国家医疗支出增加的主要原因之一。我们已经讨论了预测医院再入院的动机和理由,以及美国政府的医院再入院减少计划(HRRP),该内容在第六章,衡量医疗质量中有详细介绍。现在,让我们回顾一下机器学习算法如何用来增强简单的再入院风险评分。
LACE 和住院评分
最著名的再入院风险评分是 LACE 评分,该评分由加拿大研究人员于 2010 年开发(van Walraven 等人,2010)。"LACE"代表用于计算该评分的四个预测因子,完整的评分计算范围为 0-19,具体见下表。查尔森合并症指数是一个根据患者过往病史中某些疾病的存在为其打分的评分系统,包括心肌梗死、癌症、CHF(充血性心力衰竭)和 HIV 感染:
| 成分 | 属性 | 数值 | 分数 |
|---|---|---|---|
| L | 住院天数 | <1 | 0 |
| 1 | 1 | ||
| 2 | 2 | ||
| 3 | 3 | ||
| 4-6 | 4 | ||
| 7-13 | 5 | ||
| >13 | 7 | ||
| A | 急性(入院紧急性) | 是 | 3 |
| C | 合并症(查尔森合并症指数) | 0 | 0 |
| 1 | 1 | ||
| 2 | 2 | ||
| 3 | 3 | ||
| >3 | 5 | ||
| E | 最近 6 个月的急诊就诊 | 0 | 0 |
| 1 | 1 | ||
| 2 | 2 | ||
| 3 | 3 | ||
| ❤️ | 4 |
为了得出这个指数,研究作者将认为与再入院风险相关的十二个以上的变量输入到一个多变量逻辑回归模型中,针对 2,393 名患者得出了这四个显著的预测变量。然后,他们开发了评分系统,并使用来自加拿大患者数据库的 100 万个记录进行外部验证。他们报告的C统计量预测 30 天内的早期死亡或紧急再入院为 0.684。
另一个医院再入院风险评分的例子是较近期开发的 HOSPITAL 评分(Donze 等,2013;Donze 等,2016)。同样,“HOSPITAL”代表该评分中使用的七个预测因子,具体列在以下表格中:
| 组件 | 属性 | 值 | 分数 |
|---|---|---|---|
| H | 出院时血红蛋白 < 12 g/dL | 是 | 1 |
| O | 肿瘤科出院 | 是 | 2 |
| S | 出院时钠 < 135 mmol/L | 是 | 1 |
| P | 住院期间是否有手术 | 是 | 1 |
| IT | 入院类型:紧急 | 是 | 1 |
| A | 前一年住院次数 | 0-1 | 0 |
| 2-5 | 2 | ||
| >5 | 5 | ||
| L | 住院超过 5 天 | 是 | 2 |
HOSPITAL 评分范围为 0-13 分。该评分是根据与医院再入院独立相关的七个因素得出的,还使用了多变量逻辑回归模型(Donze 等,2013)。该评分通过四个国家的 117,065 个出院数据进行外部验证,作者报告了 C 统计量为 0.72(Donze 等,2016)。
再入院建模
让我们看看最近的两项研究,这些研究应用了机器学习来识别再入院风险问题。
第一项研究来自杜克大学(Futoma 等,2014)。该研究分析了来自新西兰的 330 万次住院数据。对于每次住院,使用了人口统计信息、背景信息、诊断相关组(DRG)代码和国际疾病分类(ICD)诊断与手术代码。然后,将该矩阵输入到六种不同的机器学习算法中:
-
逻辑回归
-
使用多步骤变量选择的逻辑回归
-
带惩罚的逻辑回归
-
随机森林
-
支持向量机
-
深度学习
在前五种方法中,随机森林的表现最佳,达到了 AUC 0.684。深度学习模型被用来预测五个患者群体的再入院:肺炎、慢性阻塞性肺疾病(COPD)、充血性心力衰竭(CHF)、心肌梗死(MI)和全髋/膝关节置换术。肺炎群体的表现最佳,AUC 为 0.734。需要注意的是,未对 LACE 或 HOSPITAL 风险评分进行直接比较。
第二项研究来自 Advocate Health Care 医院系统(Tong 等人,2016)。研究包括 162,466 例入院(例如初始)病例。对于每一例入院,收集了 19 个数据元素,包括四个 LACE 评分组件和其他 15 个电子病历变量,如既往病史、先前临床接触、就业状态和实验室结果。研究人员在这些数据上训练了三种不同的机器学习模型:
-
带有逐步前进-后退变量选择的逻辑回归
-
带有 Lasso 正则化的逻辑回归
-
提升法
他们发现,当使用 80,000 例入院病例作为训练/验证集时,LACE 模型的 AUC 约为 0.65,而三种机器学习模型的 AUC 均约为 0.73。结果表明,LACE 评分中使用的四个变量之外的其他变量具有重要意义,并进一步加强了机器学习在医疗保健中的应用。
其他条件和事件
还有许多其他疾病,机器学习算法已被用于改善风险评估和预测,超越传统的风险评分。慢性阻塞性肺病(COPD)、肺炎、脓毒症、中风和痴呆症只是其中的一些例子。
摘要
在本章中,我们回顾了不同类型的医疗保健模型及其使用的特征,以实现效果。我们在讨论中省略了一些医疗保健领域的最新趋势,包括使用新兴技术,如社交媒体、物联网和深度学习算法,进一步推动医疗预测的界限。我们将在下一章中讨论这些趋势。
参考文献与进一步阅读
Ayer T, Alagoz O, Chhatwal J, Shavlik JW, Kahn Jr. CE, Burnside ES (2010)。使用人工神经网络重新审视乳腺癌风险评估:区分度和校准。Cancer 116 (14): 3310-3321。
Baines CJ (1989)。乳房自我检查。Cancer 64(12 Suppl): 2661-2663。
Cruz JA, Wishart DS (2006)。机器学习在癌症预测与预后中的应用。Cancer Informatics 2: 59-77。
D'Agostino RB, Vasan RS, Pencina MJ, Wolf PA, Cobain M, Massaro JM, Kannel WB (2008)。初级保健中的一般心血管风险评估:Framingham 心脏研究。Circulation 117 (6): 743-753。
Donze J, Aujesky D, Williams D, Schnipper JL (2013)。医学患者的 30 天潜在可避免再入院:预测模型的推导与验证。JAMA Intern Med 173(8): 632-638。
Donze JD, Williams MV, Robinson EJ 等人(2016)。"HOSPITAL"评分在国际上预测医疗患者 30 天潜在可避免再入院的有效性。JAMA Intern Med 176(4): 496-502。
Framingham Heart Study (2018a)。Framingham Heart Study 的历史。www.framinghamheartstudy.org/fhs-about/history/。访问时间:2018 年 6 月 16 日。
Framingham 心脏研究 (2018b). 研究里程碑。www.framinghamheartstudy.org/fhs-about/research-milestones/。访问日期:2018 年 6 月 16 日。
Futoma J, Morris J, Lucas J (2015). 早期医院再入院预测模型比较。生物医学信息学期刊 56: 229-238。
Kourou K, Exarchos TP, Exarchos KP, Karamouzis MV, Fotiadis DI (2015). 机器学习在癌症预后和预测中的应用。计算与结构生物技术期刊 13: 8-17。
Lenes K (2005). 文件:心脏超声图像 4 腔.jpg。commons.wikimedia.org/wiki/File:Echocardiogram_4chambers.jpg。访问日期:2018 年 6 月 23 日。
Longo DL (2005). "癌症患者的处理方法" 见 Kasper DL, Braunwald E, Fauci AS, Hauser SL, Longo DL, Jameson JL 主编,哈里森内科学原理,第 16 版。纽约:McGraw-Hill。
Morin PJ, Trent JM, Collins FS, Vogelstein B (2005). "癌症遗传学。" 见 Kasper DL, Braunwald E, Fauci AS, Hauser SL, Longo DL, Jameson JL 主编,哈里森内科学原理,第 16 版。纽约:McGraw-Hill。
美国国家癌症研究所 (1990). 显示癌症的乳腺 X 光片。未知摄影师,美国放射学会。commons.wikimedia.org/wiki/File:Mammogram_showing_cancer.jpg。访问日期:2018 年 6 月 23 日。
美国国家癌症研究所 (2015a). 乳腺癌筛查(PDQ®)-健康专业版。www.cancer.gov/types/breast/hp/breast-screening-pdq#section/all。访问日期:2018 年 6 月 23 日。
美国国家癌症研究所 (2015b). 癌症的风险因素。www.cancer.gov/about-cancer/causes-prevention/risk。访问日期:2018 年 6 月 23 日。
美国国家心肺血液研究所 (2013). 文件:Cardiac_Mri.jpg。commons.wikimedia.org/wiki/File:Cardiac_mri.jpg。访问日期:2018 年 6 月 23 日。
Son C, Kim Y, Kim H, Park H, Kim M (2012). 基于粗集和决策树方法的充血性心力衰竭早期诊断决策模型。生物医学信息学期刊 45: 999-1008。
Tong L, Erdmann C, Daldalian M, Li J, Esposito T (2016). 30 天全因非择期再入院风险预测模型方法比较。BMC 医学研究方法论 2016(16): 26。
Tripoliti EE, Papadopoulos TG, Karanasiou GS, Naka KK, Fotiadis DI (2017). 心力衰竭:通过机器学习技术诊断、严重程度评估及不良事件预测。计算与结构生物技术期刊 15: 26-47。
美国预防服务工作组 (2016). 最终建议声明:乳腺癌:筛查。 www.uspreventiveservicestaskforce.org/Page/Document/RecommendationStatementFinal/breast-cancer-screening1。访问日期:2018 年 6 月 23 日。
van Walraven C, Dhalla IA, Bell C, Etchells E, Stiell IG, Zarnke K, Austin PC, Forster AJ (2010). 推导和验证一个预测早期死亡或在出院后重新入院的指数,以便预测从医院到社区的出院情况。加拿大医学会期刊 182(6): 551-557。
Wang X, Huang Y, Li L, Dai H, Song F, Chen K (2018). Gail 模型在预测乳腺癌风险中的表现评估:一项系统评价和元分析,结合试验顺序分析。乳腺癌研究 20: 18。
Weng SF, Reps J, Kai J, Garibaldi JM, Qureshi N (2017). 机器学习能否通过常规临床数据提高心血管风险预测? PLOS One 12(4): e0174944. doi.org/10.1371/journal.pone.0174944
第九章:未来 - 医疗保健和新兴技术
到目前为止,在本书中,我们已经探讨了医疗保健分析的激动人心历史以及它目前对我们医疗系统的影响方式。在本章中,我们将介绍该领域的最新发展情况,以及在不远的将来可能在医疗保健分析中出现的内容。我们将深入探讨医疗保健和互联网,特别是物联网(IoT)和社交媒体应用程序,看它们如何在改善健康方面发挥作用。接下来,我们将看一些新的算法(统称为“深度学习”),它们在医学预测任务中达到了最先进的性能。最后,尽管本书以一种充满希望和乐观的方式展示了医疗保健分析的现状,但仍然存在一些必须克服的重大障碍和障碍,以便医疗保健分析能够对我们的医疗系统产生积极的冲击。我们将在书的最后讨论这些问题。
医疗分析和互联网
物理上,互联网是连接全球数百万计算设备的特定计算机网络。从实际角度来看,它是为电子邮件、社交网络、数据存储和通信等应用提供服务的基础设施(Kurose 和 Ross,2013)。互联网在 1990 年代崛起,并且几乎影响了全球经济中的每个行业;医疗保健也不例外。正如我们在第二章中讨论的健康基础中所述,临床数据越来越多地以电子方式存储在计算机上,而对这些数据进行分析的第三方通常通过互联网接收数据,并使用云存储这些数据。此外,分析结果通常通过称为应用程序编程接口(API)的互联网技术向医疗组织传达,通过这种接口,医疗组织发送请求获取特定信息,并收到该信息。
除了本书中讨论的典型数据交换周期,显然高度依赖互联网外,还有一些边缘方法正在影响医疗保健。让我们在这里看看其中的两种方法:
-
物联网
-
社交媒体
医疗保健和物联网
尽管传统上我们认为互联网是一个计算机网络,但近年来,更多类型的设备也加入了这个行列。物联网是一个由嵌入传感器、软件和互联网连接的物理设备组成的网络,使得这些设备能够交换数据(Dimitrov, 2016)。迄今为止,物联网在医疗领域的应用已有许多进展,物联网对医疗保健的影响也相当显著。物联网的一个新兴功能是能够远程监测患者的健康状况(Ma 等人, 2017)。一个典型的例子是由 UCLA 的一组研究人员开发的,用于充血性心力衰竭(CHF)患者的体重、活动和血压监测系统WANDA(Suh 等人, 2011)。体重增加(由于液体潴留)、活动减少和血压失控是 CHF 失代偿的关键标志;WANDA 使用体重秤和血压监测仪,通过蓝牙将患者的测量数据传输到患者的手机。然后,手机将这些数据与来自手机应用的活动和症状信息一起发送到云端的后端数据库。随后,这些数据可以用于执行逻辑回归(类似于我们在第二章, 医疗基础 和第七章, 医疗中的预测模型构建中讨论的内容),以便在患者有 CHF 失代偿风险时提醒医生。这一切都在私密和安全的环境下完成。这是物联网如何与一种致命且常见的慢性病作斗争的一个典型例子。
尽管物联网(IoT)与医疗保健的合作前景可期,但仍面临一些主要障碍和挑战,包括有效的设备间通信(设备公司通常使用各自的专有语言)、患者隐私和安全的维护,以及患者使用的便捷性(Dimitrov, 2016)。毫无疑问,物联网领域将是我们展望未来时值得关注的一个方向。
医疗分析与社交媒体
另一种改善健康的互联网应用形式是利用社交媒体应用程序,如 Facebook 和 Twitter,来监测和预测疾病流行。流行病是指在某一社区中突然发生且广泛传播的特定疾病。
社交媒体非常适合追踪流行病,因为它们能够营造一种匿名感和自由表达个人感受与信息的氛围(Charles-Smith 等人, 2015)。对社交媒体平台上语言的分析揭示了多种疾病的爆发,包括酒精和药物滥用、充血性心力衰竭以及传染病(Brathwaite 等人, 2016)。在这里,我们将探讨社交媒体如何被用来对抗两种突出的流行病:流感和自杀。
流感监测与预测
约翰霍普金斯大学在马里兰州的一项最新研究旨在确定用户的“推文”或 Twitter 上的消息是否可以用来预测未来几周的流感发生率(Paul 等人,2014 年)。他们使用了一种基本的线性自回归模型,这与逻辑回归相似,只是其特征是前几周的流感发生率,系数由最小二乘回归法确定。他们比较了仅使用美国疾病控制与预防中心(CDC)公布的每周流感率的模型与同时使用 Twitter 流感监控系统分析推文的模型,该系统通过分析推文内容来判断其是否与流感感染有关。研究发现,结合 Twitter 数据有助于减少未来 10 周内流感发生率预测的误差,减少幅度为 15%至 30%。类似地,一项分析社交媒体用于疾病监控的综述研究发现,社交媒体网站和国家卫生统计数据的联合信息能够预测疫情爆发,领先于标准的疫情监测系统(Charles-Smith 等人,2015 年)。
使用机器学习预测自杀倾向
自杀是美国第十大死亡原因,而且其发生率正在上升,给受害者家庭带来了前所未有的情感和经济负担(Brathwaite 等人,2016 年)。在布里甘杨大学的上述研究中,研究人员通过亚马逊的机械土耳其平台(www.mturk.com)招募了 135 名参与者,并要求他们完成三个经过临床验证的自杀风险评估问卷。此外,研究人员使用语言分析工具分析了每位参与者的推文,该工具可以从文本中提取特征。这些特征主要衡量某些词汇类别的出现频率,包括“家庭”、“愤怒”和“悲伤”。然后,他们使用 Python 和 scikit-learn 库构建了模型,将这些“训练集”参与者分类为自杀倾向或非自杀倾向。为了在未见过的数据上测试他们的模型,他们采用了留一交叉验证方法,即每次仅用一个参与者作为测试数据,重复 135 次。其决策树模型获得了 91.9%的准确率,敏感性为 53%,特异性为 97%。显然,这对在自杀行为发生之前识别自杀患者是鼓舞人心的,尽管如何在这样的自杀监测系统中保护隐私仍有待观察。
医疗保健与深度学习
没有对医疗分析的讨论,就不能算是一本完整的书。近年来,深度学习算法在语音识别、人脸识别、语言理解和物体识别等领域取得了无与伦比的成果(Goodfellow 等,2016)。几乎与此并行的是医疗领域的突破;从病理切片和放射学扫描中的癌症检测到死亡率和再入院的预测,结果一次又一次地显现出,与(在某些情况下超越)专家委员会的表现相媲美。
我们已经简要讨论了深度学习的某些方面。在第一章,《医疗分析导论》中,我们讨论了许多人认为是深度学习作为一个领域的开创性事件的历史性论文,在第三章,《机器学习基础》中,我们讨论了深度神经网络作为医学决策框架,在第八章,《医疗预测模型回顾》中,我们讨论了使用深度学习算法获得结果的几项研究。现在让我们简要讨论一下什么是深度学习,以及它与传统机器学习和神经网络的区别。然后,我们将讨论一些使用以下深度学习算法子类型来得出结论的有前景的研究:
-
深度前馈网络
-
卷积神经网络(CNN)
-
递归神经网络(RNN)
我决定将深度学习理论的讨论排除在本书之外,因为一个简短的、简化的深度学习理论章节无法公正地呈现这一领域。然而,Coursera(www.coursera.org)上有一些优秀的课程,以非常易于理解的方式解释了深度学习在数学上的含义。
简要来说,什么是深度学习?
深度学习 被定义为一种人工智能方法,通过表达和组合更简单、低级的表示来获得高级理解(Goodfellow 等,2016)。
今天,使用术语深度学习来描述一种机器学习方法或算法通常意味着以下内容:
-
该算法松散地模仿人类神经元,使用人工神经元。换句话说,深度神经网络的构建块是一个人工神经元,它接受加权输入的总和(类似于回归模型),然后对该总和应用非线性变换。
-
与回归不同,深度神经网络通常具有多个神经元层,模型开始时有一个输入层,结束时有一个输出层,并且在输入层和输出层之间至少有一个隐藏层。一个层的输出被输入到下一个层,直到到达输出层。
-
为了正确预测模型的输出,我们必须使用大量的示例(在某些情况下是数十亿)。深度学习通常需要大量的训练示例。网络越深、越复杂,我们需要的训练示例就越多。
-
深度学习使用反向传播算法来正确地训练权重,而反向传播算法依赖于微积分和线性代数。
深度学习在医疗保健中的应用
已经有许多研究将这一类新算法应用于医疗保健问题。现在让我们进行一次参观。
深度前馈网络
在第八章中,医疗保健预测模型——回顾,我们讨论了杜克大学的一项研究,该研究使用多种算法预测医院再入院率(Futoma 等,2015 年)。在该研究的第二部分,他们使用了一个深度前馈网络(与之前示意图所示相似),该网络有三层隐藏层,每层包含 200-750 个神经元,具体数量取决于特定的病情。作为它们的非线性激活函数,除了输出层外,它们几乎为所有层使用了sigmoid函数,而在输出层使用了softmax函数(这是深度学习中的常见做法)。他们为五种 CMS 激励的病情(肺炎、充血性心力衰竭、慢性阻塞性肺病、心肌梗死、以及全髋/膝关节置换术)分别构建了一个模型,并使用与每种疾病相关的所有观察数据进行预训练(回想一下,总共有大约 3,000,000 个观察值)。
他们使用了许多其他技巧以达到最佳性能,包括岭回归惩罚、丢弃法和早停法(有关这些技术的解释,请参阅 Goodfellow 等,2016 年)。他们在五个条件下的表现优于最佳的非深度学习算法(尽管在五种疾病中,只有三种疾病的改进显著)。他们报告的 AUC 值比 LACE 和 HOSPITAL 再入院评分的先前报告值更好。因此,这是一个展示深度学习在解决医疗保健中复杂预测问题方面潜力的例子,尽管它也突显了这类模型训练的困难和复杂性。
用于图像的卷积神经网络
在前一节中提到,神经网络通过加权和随后进行非线性变换来确定下一层使用的值。虽然这对于前馈神经网络通常是正确的,但并非所有网络都必须遵循这一点;除了加权和之外,还可以使用不同的数学函数来确定下一层的输入。在一种称为 CNN 的不同类型的神经网络中,卷积操作被用来确定下一层的输入。通常,卷积操作后面紧接着是池化操作(例如最大池化,其中从一个离散区域中选择最大的值传递到下一层)。
使用这些类型操作的网络非常适合处理定期采样的数据,例如图像或时间序列数据。因此,卷积神经网络在医疗领域中广泛应用于处理来自病理切片、放射学扫描和其他图像的数据,并用于从中检测各种疾病。
病理学是医学的一个分支,专门研究从人体组织样本中提取的横截面显微镜切片的评估。切片检查通常用于将组织分类为癌性或非癌性。病理学家有时需要检查非常大的图像,以寻找癌变组织的迹象,这一过程既耗时又容易出错。谷歌公司的一项最新研究旨在确定卷积神经网络是否能比病理学家更好地检测乳腺癌在淋巴结组织中的表现(Liu et al., 2017)。在这项研究中,使用了高达 100,000 x 100,000 像素的切片。该模型能够实现 0.965 到 0.986 之间的 AUC,而病理学家的 AUC 为 0.966。更重要的是,该模型几乎是即时完成分类的,而病理学家花费了 30 小时!这是人工智能如何与人类广博且深入的知识结合,从病理图像中提升癌症检测能力的一个例子。类似的研究类型也可以应用于放射学扫描。
用于序列的递归神经网络
另一种专门的深度神经网络被称为递归神经网络,特别适用于顺序数据。递归神经网络的一个变体被称为长短期记忆网络(LSTM,简称)(Hochreiter 和 Schmidhuber,1997)。LSTM 网络包含LSTM 单元,这些单元接收输入向量并产生输出向量。LSTM 单元非常复杂,由各种“门”组成,这些“门”控制着单元的输出,这些“门”被称为输入门、输出门和遗忘门。这些门又在一定程度上受到前一时刻输入的控制。LSTM 是许多成功应用背后的网络,包括手写识别、语音识别、语言翻译和图像描述(Goodfellow 等,2016)。
最近的一项研究,由 Google 公司进行,采用了深度学习架构,包括 LSTM 架构,来预测住院死亡率、非计划性再入院、延长住院时间和最终出院诊断(Rajkomar 等,2018)。他们取得了最先进的成果。
障碍、伦理问题和局限性
鉴于我们在本书中大力推广近期分析和机器学习的成果,为什么医生还没有被计算机取代呢?事实是,实施分析技术在医疗领域面临许多障碍。此外,由这项技术带来的伦理问题也在激烈辩论之中,必须加以考虑。最后,虽然障碍意味着有可能克服,但这项技术也存在局限性,即一些方面可能在短期内无法解决。让我们在本节中讨论这三者。
障碍
为了实现分析技术在医疗保健中的广泛应用,当前有哪些障碍需要克服?
-
医疗行业在采用新兴技术方面传统上较为缓慢,这也是必须克服的挑战。医疗行业被描述为一个保守的领域,通常对变化采取较为谨慎的态度。例如,医院最初曾对使用电子血压袖带的想法表示抵制。此外,电子病历的进步也遭遇了怀疑和反对,因为人们担心这会减少患者与医生的互动,并增加书写记录所需的时间。分析和机器学习无疑也面临同样的问题;它只是另一项新的、不熟悉的技术,尽管像汽车制造和制造业这样的行业轻松接受了它,医疗行业可能会面临不同的局面。
-
也许医生抵制分析和机器学习的一个重要原因是他们害怕计算机会“接管”或“取代”医生。毫无疑问,考虑到金钱、技术和时间的限制,我们距离这一讨论还很远。我们在本书中讨论的机器学习研究都是针对非常具体的任务进行训练的,当然,它们在训练和解释时依赖于人类的直觉和判断。更可能的是,成功的健康分析和机器学习将通过团队合作来实现,其中人类的优势(如普遍性和知识的广度)与计算机的优势(速度和计算精度)相结合,以产生最佳结果。然而,医生担心被计算机取代的忧虑确实存在,虽然这种可能性还很遥远,我们必须找到让医生和人工智能合作而不是对立的方式。
-
对分析持怀疑态度的另一个原因是“希望与炒作”之争。像“大数据”和“深度学习”这样的流行语有时因为周围的炒作而带有负面含义。有些人认为,对这些领域的信仰被过度夸大了。具体来说,分析和机器学习的怀疑者认为,尽管许多大数据应用“听起来很酷”,但它们很少能够挽救生命或节省金钱。毫无疑问,这些担忧是合理的;所有机器学习研究都应该力求为社会做出积极贡献,而不仅仅是证明某些事情是可以做到的。
伦理问题
伦理一直是考虑新技术(包括计算机科学)的一部分,不能在这里忽视。医疗分析引入了哪些伦理问题?
-
首先,在我看来,无法对人类的情感和无痛感赋予价值或数值是一个重要问题。许多机器学习模型是通过成本函数进行训练的。成本函数应该是什么样的?它应该基于降低成本、提高质量和结果,还是减少痛苦和心碎?是谁来决定这些看似对立的目标应该如何平衡?
-
另一个由人工智能引发的伦理问题是责任问题。如果机器学习模型做出了错误的预测,应该由谁负责?应该是监督患者的医生吗?还是应该是开发模型的数据科学团队?
-
第三个问题涉及患者隐私领域。我们在第二章《医疗基础》中讨论了 HIPAA 法律。使用患者数据来训练模型是否合适?是否应该需要患者同意?哪些数据点应该被允许使用?
-
最后,还有偏见问题。人们担心在预测患者结果时,可能会依赖种族、性别和年龄等因素。这可能导致患者歧视。
限制
限制是医疗保健分析中可能永远无法克服的方面。医疗保健分析存在哪些限制?
-
机器人和计算机不是人类,且目前无法替代人类在面对疼痛、疾病或死亡时提供的安慰和同情。
-
像神经网络这样的技术,尽管它们可能提供准确的预测,但存在“黑箱”问题——它们无法向患者解释自己的推理或逻辑。因此,患者可能不会像信任一位好医生那样完全信任神经网络。
-
尽管机器学习领域在不断变化,但时间序列和自然语言处理在医疗保健领域尤为重要,这些领域是机器学习算法相对于结构化临床数据的薄弱环节。可能需要一些时间才能编写出能像人类一样读取文本、做出概括并提出相关问题的算法。
本书结论
本书重点关注具有医疗保健三重目标的医疗保健分析:降低成本、改善结果、提高护理质量。现在,您已经读完了这本书,希望您能更清楚地看到医疗保健分析如何实现这一目标。它可以通过正确诊断问题(借助廉价的机器学习算法)并防止不必要的检查来减少医疗保健成本。它可以通过更早地诊断问题而不是拖延来改善医疗保健结果,从而采取纠正措施。最后,它可以通过确定哪些医院提供适当且及时的护理,并奖励这些医院,从而提高质量。
本书旨在改善全球医疗保健,向您介绍了几位朋友,他们将帮助您实现这一任务:
-
Python 语言,包括基础语言和外部库,如 pandas 和 scikit-learn
-
SQL 语言
-
机器学习算法
-
医疗保健领域知识
-
一些数学知识
感谢您阅读关于已经成为我生活中心目标和使命的内容。现在让我们一步步改善医疗保健,逐个模型地进行。
参考文献与进一步阅读
Brathwaite SR, Giraud-Carrier C, West J, Barnes MD, Hanson CL (2016)。验证 Twitter 数据中机器学习算法与已建立的自杀倾向度量标准。JMIR Ment Health 3(2): e21。
Charles-Smith LE, Reynolds TL, Cameron MA, Conway M, Lau EHY, Olsen JM, Pavlin JA, Shigematsu M, Streichert LC, Suda KJ, Corley CD (2015)。利用社交媒体进行可操作的疾病监测与疫情管理:系统文献回顾。PLoS ONE 10(10): e0139701。
Dimitrov DV (2016)。医疗物联网和大数据在医疗保健中的应用。Healthcare Inform Res 22(3): 156-163。
Futoma J, Morris J, Lucas J (2015)。预测早期医院再入院的模型比较。J Biomedical Informatics 56: 229-238。
Goodfellow I, Bengio Y, Courville A(2016)。深度学习。波士顿,马萨诸塞州:MIT 出版社。
Liu Y, Gaepalli K, Norouzi M, Dahl GE, Kohlberger T, Boyko A, Venugopalan S, Timofeev A, Nelson PQ, Corrado GS, Hipp JD, Peng L, Stumpe MC(2017)。在千兆像素病理图像上检测癌症转移。arXiv:1703.02442 [cs.CV]
Ma J, Nguyen H, Mirza F, Neuland O(2017)。物联网传感器与云计算之间的双向架构用于远程健康监测应用。在第 25 届欧洲信息系统会议(ECIS)论文集,葡萄牙吉马良斯,2017 年 6 月 5-10 日(第 2834-2841 页)。进行中的研究论文。
Paul MJ, Dredze M, Broniatowski D(2014)。Twitter 改进流感预测。PLoS Curr 10 月 28 日;6。PubMed PMID: 25642377。
Rajkomar A, Oren E, Chen K, Dai AM, Hajaj N, Hardt M 等(2018)。具有可扩展性和准确性的深度学习与电子健康记录的结合。npj Digital Medicine 1:18;doi:10.1038/s41746-018-0029-1。
Suh M, Chen C, Woodbridge J, Tu MK, Kim JI, Nahapetian A, Evangelista LS, Sarrafzadeh M(2011)。用于充血性心力衰竭的远程患者监测系统。J Med Syst 35(5): 1165-1179。


浙公网安备 33010602011771号