原文:
annas-archive.org/md5/d23e06d989b2f11c724cc2cf921f15d7译者:飞龙
协议:CC BY-NC-SA 4.0
本书的功能目标是展示如何使用 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 扫描筛查肺癌可能需要数千美元;然而,对患者病史进行数学计算来筛查肺癌的成本则低得多。本书的计划是向你展示如何进行这些计算。
医疗保健质量涵盖了患者在接受医疗服务后的满意度。在资本主义体系中(如美国的医疗体系),提高质量的有效方法之一是通过公正和客观地衡量不同提供者的表现,以便患者能做出更明智的治疗决策。
现在我们已经定义并介绍了医疗保健分析,重要的是要提供一些背景知识,来说明它的基础。医疗保健分析可以被视为三个领域的交集:医疗保健(医疗保健分析)、数学(数学)和计算机科学(计算机科学),如下图所示。让我们依次探索这三个领域:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/23c2df20-1b18-4f68-8fc2-3ee8cb2be369.png
医疗保健是医疗保健分析的领域知识支柱。以下是构成医疗保健分析的一些重要医疗保健知识领域:
医疗服务与政策:了解医疗行业的结构、主要参与者及其财务激励机制,能帮助我们改进医疗分析工作。
医疗数据:无论是结构化数据还是非结构化数据,医疗数据都丰富且复杂。然而,医疗数据的收集通常遵循特定模板。了解典型的病史与体格检查(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 个结果,中间的几年里,结果稳步增加:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/a855ae1b-1219-404c-afdc-1fcf7c649b08.png
有几个因素促成了生物医学机器学习研究的加速。第一个是机器学习算法的数量和可用性的增加。神经网络只是其中一个例子。在 1990 年代,医学研究人员开始使用各种替代算法,包括最近开发的决策树、随机森林和支持向量机算法,除了传统的统计模型,如逻辑回归和线性回归。
第二个因素是电子临床数据的可用性增加。在 2000 年前,几乎所有的医学数据都是以纸质图表的形式存在,进行计算机化的机器学习研究意味着需要数小时将数据手动输入计算机。电子病历的增长和最终普及使得使用这些数据来构建机器学习模型变得更加简单。此外,更多的数据意味着更准确的模型。
这让我们来到了今天,医疗分析正经历一个激动人心的时刻。今天的现代神经网络(通常被称为深度学习网络)在比心电图解读更复杂的任务中,常常超过人类的表现,比如从 X 光图像中识别癌症和预测患者未来医疗事件的序列。深度学习通常通过使用数百万个患者记录,并结合并行计算技术来实现这一点,使得在更短的时间内训练大型模型成为可能,同时还有新开发的技术用于调整、正则化和优化机器学习模型。另一个令人兴奋的现象是,政府激励措施的引入,旨在消除医疗过度支出和误诊现象。这些激励措施不仅引起了学术研究者的兴趣,也吸引了工业参与者和公司,他们希望为医疗机构节省资金(并为自己赚取一些利润)。
尽管医疗分析和机器算法尚未重新定义医疗护理,但医疗分析的未来前景看起来非常光明。就我个人而言,我喜欢想象这样一个场景:医院配备摄像头,私密且安全地记录患者护理的每一个方面,包括患者和医生之间的对话,以及患者在听到自己医学检查结果时的面部表情。这些文字和图像可以传送给机器学习算法,用来预测患者对未来结果的反应,以及这些结果会是什么。但我们现在有点超前了;在我们到达那个日子之前,还有很多工作要做!
为了让你更好地了解医疗分析的涵盖范围,以下是一些医疗分析使用案例的例子,展示了现代医疗分析的广度和深度。
分析通常被分为三个子组件——描述性分析、预测性分析和规范性分析。描述性分析包括使用之前讨论的分析技术,更好地描述或总结正在研究的过程。了解护理是如何提供的就是一个能够从描述性分析中受益的过程。
我们如何利用描述性分析更好地理解医疗服务?以下是一个例子,展示了一名幼儿在出现哮喘加重症状时就诊急诊科(emergency department,ED)的护理记录可视化(Basole 等人,2015)。该可视化使用了电子病历(EMR)系统中常见的结构化临床数据,概括了他们在急诊科经历的护理事件的时间关系。该可视化由四种类型的活动组成——行政(黄色)、诊断(绿色)、药物(蓝色)和实验室检查(红色)。这些活动通过颜色和y-位置进行编码。x-轴表示时间。顶部的黑色条形按垂直刻度分为每小时一个区块。这名患者的就诊持续了略超过两个小时。患者信息在黑色时间条之前显示。
尽管像这样的描述性分析研究可能不会直接影响成本或医疗建议,但它们为探索和理解患者护理提供了一个起点,并且通常为启动更具体、可操作的分析方法铺平了道路:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/ed9eb8af-13f5-41c9-ac2d-ec67dd951cb3.png
医学中的一个核心问题是如何识别出有发展某种疾病风险的患者。通过识别高风险患者,可以采取措施延缓或阻止疾病的发生,甚至完全预防。这是预测性分析的一种应用——利用来自以往事件的信息来预测未来的情况。有些疾病特别适合做预测研究:充血性心力衰竭、心肌梗死、肺炎和慢性阻塞性肺病就是一些高死亡率、高成本的疾病,它们可以从早期识别高风险患者中受益。
我们不仅关心未来将发生哪些疾病,还希望识别出那些有可能需要高成本治疗的患者,如医院再入院和看病就诊。通过识别这些患者,我们可以采取节省费用的措施,主动降低这些高风险治疗的风险,并且可以奖励那些做得好的医疗机构。
这是一个包含多个未知因素的广泛示例。首先:我们想预测的具体事件(或疾病)是什么?其次:我们将使用哪些数据来进行预测?目前,结构化临床数据(以表格形式组织的数据)是最受欢迎的数据来源;其他可能的数据包括非结构化数据(医学文本)、医学或 X 光影像、生物信号(EEG、EKG)、来自设备的数据,甚至是社交媒体的数据。第三:我们将使用什么机器学习算法?
尽管制作美观的可视化或预测代表了医疗分析的性感方面,但还有其他类型的分析同样重要。有时,这归结为良好的老式数字分析。使用医疗措施监测医生和医疗机构的表现就是这种分析技术的一个很好的例子。医疗措施提供了一个机制,使个人能够衡量和比较参与者对基于证据的医疗建议的遵从情况。例如,广泛接受的建议是,糖尿病患者每三个月接受一次由医生进行的足部检查以检测糖尿病足溃疡。
国家赞助的医疗措施可能会指定计算接受治疗的糖尿病患者人数的指南,并确定那些接受适当足部护理的患者的百分比。类似的措施也适用于常见的心脏、肺部和关节疾病等多种疾病。这提供了一种识别提供最高质量护理的提供者的方法,这些建议可以下载供公众消费。我们将在第六章,《医疗质量测量》中讨论具体的医疗措施。
在罕见情况下,医疗分析包括用于实际治疗疾病的医疗技术,而不仅仅是对其进行研究。神经假肢就是一个例子。神经假肢可以定义为使用人造设备增强神经系统功能。神经假肢研究使患有失明或截瘫等残疾的患者能够恢复部分失去的功能。例如,一个瘫痪的患者可能能够通过他们的脑信号移动屏幕上的计算机光标,而不是用手!在这个特定的应用中,获取特定神经元的电活动记录,并使用机器学习模型确定光标在哪个方向移动,根据神经元的发射。类似的分析可以用于视力障碍,或者用于可视化人类正在看到的内容。第二个例子包括在身体中植入设备,以在癫痫发作之前检测到,并主动给予预防药物。显然,通过分析驱动的治疗有无限的可能性。
在本节中,我们将下载、安装和探索 Anaconda 和 SQLite,这些是本书中用于 Python 和 SQL 的发行版。
本书中的示例需要使用 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):
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/3d3d8e6a-b575-41fe-a377-b8b144e8990a.png
点击下载。你的浏览器将开始下载文件。下载完成后,点击浏览器中的文件或操作系统文件管理器中的文件。
会弹出一个窗口(如下图所示)。点击“下一步>”按钮:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/19a89272-a8ca-41a1-800e-df938253adfb.png
继续按照提示操作,包括接受许可协议、选择安装用户、选择文件存储位置以及选择各种选项。
Anaconda 将开始安装。由于安装包数量较多,这可能需要一段时间。
安装完成后,关闭 Anaconda 窗口。
现在你已经安装了 Anaconda,你可以通过在 Windows 任务栏中搜索Anaconda Navigator来访问它的功能,或者在 Mac 的应用程序文件夹中找到 Anaconda Navigator。点击图标后,稍等片刻,你将看到如下图所示的界面:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/eee0d6ec-a18d-4c42-9b48-d829cf9977ce.png
你当前在“首页”选项卡中,这里列出了 Anaconda 中包含的不同应用程序。你可以从此屏幕访问 Jupyter notebook,以及 Spyder IDE。
要查看已安装的软件库,请点击左侧的“环境”选项卡。你可以使用此选项卡下载和升级特定的库,如下图所示:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/c03ca3cb-3a17-4f6e-983d-a48039109425.png
现在,让我们来探索 Jupyter notebook,这是本书中大部分时间会使用的 Python 编程工具。返回“首页”选项卡,点击 Jupyter 图标中的“启动”按钮。你的默认浏览器中应会打开一个新的标签页,类似于下图所示:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/5af8fb36-ae54-4ce8-925d-888dd27a74ee.png
这是 Jupyter 应用的文件标签,你可以通过它浏览电脑的目录来启动新的 Jupyter 笔记本,打开已有的笔记本,或管理你的目录。
让我们创建一个新的 Jupyter 笔记本。定位到控制台右上角的 New 下拉菜单并点击它。在下拉菜单中,点击 Python 3。另一个标签页将打开,显示类似于以下截图的内容:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/28965a23-0333-439b-a435-655b50935768.png
标记为 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 会记住每个会话中输入到控制台的所有命令。要清除内存,你必须关闭并重新启动内核:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/0174eaff-3c93-4c9b-8b2f-a29d62430ed8.png
现在,让我们结束当前的会话。返回浏览器中的主页标签,点击左上角的 Running 标签。在 Notebooks 菜单下,你应该能看到 Untitled.ipynb 正在运行。点击右侧的关闭按钮,笔记本就会消失。
暂时就到这里,关于 Jupyter 的内容你将在接下来的章节中更深入了解。
Spyder IDE 提供了一个完整的 Python 开发环境,包括文本编辑器、变量探索器、IPython 控制台,并且可选择性地提供命令行提示符,具体请参见以下截图:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/82a9be00-c8e1-4a46-bd22-cc77367da587.png
屏幕的左半部分是编辑器窗口。你将在这里编写 Python 代码。当我们完成脚本编写后,将使用上方工具栏中的绿色播放按钮来运行它们。
屏幕的右半部分横向分为两部分。右上方的窗口,最常用的形式是作为变量探索器(如图所示)。这个窗口列出了当前 Python 环境中每个变量的名称、类型、大小和值(例如,在内存中)。通过点击窗口底部的标签,你还可以将窗口切换为文件浏览器或查看 Python 的帮助文档。
右下角窗口是控制台,显示的是 Python 命令提示符。这对于运行单个 Python 命令非常有用;它还可以用来运行 Python 脚本和执行其他功能。此窗口的第三个选项是之前输入的命令历史记录。
本书中我们不会广泛使用 Spyder;然而,了解它是如何工作的也有助于你以后用于其他项目。
医疗数据通常存储在数据库中。为了操作和提取这些数据库中的所需数据,你应该了解 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)通常处理患者的整体健康状况并监督所有器官系统;在许多医疗服务模式中,他们充当“守门人”,将患者引导至二级和三级保健提供者。二级保健指由专门治疗特定疾病或器官系统的医生提供的治疗,如内分泌科医生或心胸外科医生。三级保健通常是在专科医生转诊后提供,通常发生在住院环境下,专门治疗非常特定的病症,常常通过手术进行。
在医疗保健领域,为了提供最佳的患者护理,需要一支由多名专业人员组成的团队,他们各自承担不同的角色。医生、医师助理、护士执业者、护士、案例经理、社会工作者、实验室技术人员和信息技术专业人员等,都是你将在医疗分析领域直接或间接合作的其他人员。
一百年前,钱直接从患者流向提供医疗服务的机构。然而,今天医疗保健融资更加复杂,雇主和政府的参与越来越多,且与医生报酬相关的新模式不断出现。在美国,医疗保健融资不再完全是私人的;为了帮助贫困和老年群体,州政府和联邦政府利用从公民那里征收的税款资助医疗补助和医疗保险,这分别是政府资助的支付贫困和老年人医疗保健的方式。一旦钱到达各个第三方(保险公司和/或政府),或者仍然在患者手中,必须通过各种支付模式将钱分配给医生。在以下图表中,我们提供了美国医疗保健系统中资金流动的简化概述。
医疗保健中的许多分析是对越来越重视医生绩效和医疗质量(而非数量)的回应:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/371c1fb6-570f-447d-a99c-9111ca655eff.png
传统上,医生是通过按服务收费(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)。当医院希望将数据提供给第三方以进行进一步分析时,通常会将数据以数据库格式发布到云端。
一旦数据被捕捉到数据库系统中,数据分析专家可以使用各种工具来可视化、透视、分析并构建预测模型:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/3969a86f-90ed-4b86-a845-ff8b2d90c016.png
在接下来的子章节中,我们将描述这两种临床记录的关键方面。
如前所述,病史和体格检查是患者可获得的最全面的文档类型,通常在患者入院时和/或见到新的门诊医生时进行。H&P 临床记录的标准部分将在以下章节中讨论。
元数据包括关于患者就诊的基本信息,例如患者的姓名、出生日期、入院日期/时间以及接收医院和主治医生的名称。
主诉是患者就诊/住院的原因,通常是患者自己的话。例如:“我感到胸部不适。”这个主诉可能会或不会被病史记录员翻译成相应的医学术语,例如“胸痛”。
HPI 包括与主诉相关的详细信息。这个部分通常分为以下两个段落:
第一段提供了关于主要投诉的即时细节,通常使用从患者那里获得的信息。第一句通常会提供关于患者的重要人口统计学信息以及任何相关的过去病史,除此之外还包括主要投诉。例如:
“史密斯先生是一位 53 岁的白人男性,有高血压、高脂血症、糖尿病和吸烟史,现因胸痛前来急诊。”
关于剩余部分,第一段病史(HPI)通常包含列出的七个标准元素。这七个元素通常假定主要投诉为某种类型的疼痛;有些主要投诉(例如闭经)需要不同的问题集。这七个元素在下表中进行了总结:
第二段应包含患者为其病症已经接受的所有医疗治疗。典型问题包括:患者是否已经看过医生或住院治疗过?进行过哪些实验室检查和测试?患者的相关病情控制得如何?曾尝试过哪些治疗?是否有 X 光片的副本?
本部分列出了所有影响患者的当前和过去的医疗状况,包括但不限于住院(无论是因医疗、外科还是精神原因)。
本节提供当前的处方药和非处方药(OTC)信息,通常包括以下内容:药物名称、剂量、给药途径和使用频率。列出的每种药物应与患者过去病史中的某一现有病症相对应。给药途径和频率通常使用缩写形式;请参考下表获取常见缩写的列表。
家族史包括患者两代以内家庭成员的疾病史,重点是慢性疾病以及与主诉和受影响器官系统相关的疾病。
社会历史提供了 HPI 中未获取的社会和风险因素信息。此部分包括之前未提及的人口统计因素、职业(如适用,涉及有害物质的职业暴露)、社会支持(婚姻、子女、依赖者)以及物质使用/滥用(烟草、酒精、娱乐/非法药物)。
过敏部分通常包括患者对某些物质的过敏反应,这些物质包括药物,以及相应的过敏反应。如果患者没有已知的药物过敏,通常会用缩写 NKDA 表示。
系统回顾(ROS)作为在获取其他历史信息后,最后筛查显著症状的一项检查。在此部分,医生会询问患者是否出现与不同功能器官系统相关的症状(例如,胃肠、心血管和呼吸系统)。重点放在与主要症状相关的器官系统和症状上。可能涉及多达 14 个不同器官系统的症状。
医生继续检查患者并在此部分记录检查结果。描述通常从患者的总体健康状况和外貌开始,然后是相关的生命体征(参见表格获取详细信息),之后检查头部、眼睛、耳朵、鼻子和喉咙(HEENT),并继续检查身体的具体器官/器官系统。
体格检查标志着所谓客观数据的开始,即由医生观察、解释并记录的关于患者的数据。这与主观数据相对,主观数据是患者直接提供给医生的信息,包括患者病史。在体格检查后,会提供关于患者的所有其他客观数据。这些数据包括任何实验室检查结果、如适用的影像学研究结果以及可能进行的与当前疾病相关的其他检查。常见的影像学检查包括X 光(XR)、计算机断层扫描(CT)以及磁共振成像(MRI)扫描,针对感兴趣的身体区域。
这是 H&P 的最后部分。在评估部分,医生整合前述主观和客观数据,简明总结主诉,并结合病史、体检和额外检查的显著发现。医生列出病人病情的最可能原因,按每个不同的症状/发现进行项目化列出。在计划部分,医生讨论治疗病人的蓝图,同样是按项目形式列出。
如前所述,SOAP 记录通常是每天为住院病人完成的,并包括其缩写中的每个字母对应的一个部分:主观、客观、评估和计划(SOAP)。主观部分侧重于病人当前或前一晚出现的新症状。客观部分包含前一天的日常体检、专注的体检结果以及实验室、影像学检查和检测结果。评估和计划类似于病历记录中的 H&P 部分,在更新时会考虑到当天所有的事件。
在病历文档的记录过程中,关于病人的有价值信息已经被收集并录入到电子病历(EMR)中。然而,在数据汇总之前,它通常会与临床编码集进行整合。我们将在下一部分讨论临床编码集。
从哲学角度思考,每一个具有重要性的已知对象都有一个名字。你用来阅读这些文字的器官被称为眼睛。文字被写在称为页面的纸张上。为了翻阅这些页面,你使用你的双手。这些都是我们为便于识别而命名的对象。
在医疗健康领域,重要的实体——例如疾病、手术、实验室检测、药物、症状、细菌种类等,也都有其名称和身份。例如,心脏瓣膜未能有效地将血液泵送到全身被称为心力衰竭。ACE 抑制剂是一类用于治疗心力衰竭的药物。
然而,问题出现在当医疗行业的工作人员将同一实体与不同的身份关联时。例如,一位医生可能将“心力衰竭”称为“充血性心力衰竭”,而另一位医生可能称之为“CHF”。此外,还存在不同的具体化层次:第三位医生可能称其为“收缩性心力衰竭”,以表示这一功能障碍发生在心脏跳动的收缩期。在医学中,准确性和特异性至关重要。那么,我们如何确保所有医疗团队成员讨论和思考的是同一件事呢?答案就在于临床编码。
临床代码可以看作是医疗概念的唯一标识。每个代码通常由一对对象组成:一个字母数字代码和描述该代码所代表实体的文字。例如,在 ICD10-CM 编码系统中,代码 I50.9 代表“未指定的心力衰竭”。当心力衰竭的诊断更为具体时,还有更多更详细的代码。
世界上可能存在成千上万种不同的编码系统,其中许多仅在它们被构思出来的特定医疗机构中使用。幸运的是,为了减少混乱并促进互操作性,存在一些被视为国家/国际标准的知名编码系统。一些更重要的标准化编码系统包括用于医疗诊断的国际疾病分类(ICD)、用于医疗程序的当前程序术语(CPT)、用于实验室测试的逻辑观察标识符名称和代码(LOINC)、用于药物治疗的国家药品编码(NDC),以及用于所有这些及更多内容的医学系统化命名法(SNOMED)。在本节中,我们将更详细地探讨这些编码系统。
疾病和病症通常使用 ICD 编码系统进行编码。ICD 始于 1899 年,由世界卫生组织(WHO)每十年修订一次并维护。截至 2016 年,第十次修订版(ICD-10)是最新的,并包含超过 68,000 个独特的诊断代码,超过任何先前的修订版。
ICD-10 代码最多可能包含八个字母数字字符。前三个字符表示主要的疾病类别;例如,“N18”指定慢性肾脏病。接下来的字符后跟一个句点,然后是其余字符,这些字符可以提供大量临床细节(Braunstein,2014)。例如,代码“C50.211”指定“右侧女性乳房上内象限的恶性肿瘤”。凭借其精确性,ICD-10 促进了医疗保健中分析应用的实施。
医疗、外科、诊断和治疗程序使用 CPT 编码系统进行编码。CPT 由美国医学会(AMA)开发,CPT 代码由四个数字字符组成,后跟一个字母数字字符。常用的 CPT 代码包括门诊就诊、外科手术、放射学检查、麻醉程序、病史和体检以及新兴技术的代码。与 ICD 不同,CPT 不是一个分层编码系统。然而,某些概念根据不同因素(如就诊时长(门诊就诊)或切除的组织量(外科手术))有多个代码。
实验室测试和观察结果使用 LOINC 编码系统进行编码。该系统由 Regenstrief 研究所编写和维护,共有超过 70,000 个代码,每个代码是一个六位数字,最后一位数字通过连字符与其他数字分隔。像 CPT 代码一样,特定类型的实验室测试(例如,白细胞计数(WBC))通常有多个不同的代码,具体取决于样本采集时间、测量单位、测量方法等因素。虽然每个代码都包含大量信息,但当没有所有相关信息时,寻找如 WBC 计数等实验室测试的代码可能会成为一个问题。
NDC 由美国 FDA 维护。每个代码由 10 位数字组成,并且包含三个子组件:
标签商组件,用于标识药物的制造商/分销商
产品组件,用于标识标签商提供的实际药物,包括剂量、服用方式和制剂
包装代码,用于标识特定的包装形状和大小
将这三个子组件合在一起,可以唯一地标识任何由 FDA 批准的药物。
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 中列出的管理计划:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/d58ad818-5ab4-4107-b0f8-bd9a7e551a91.png
下面是另一个结合了诊断和治疗的算法示例(Haggstrom, 2014; Kirk et al., 2014)。在这个关于未知位置妊娠的诊断/治疗算法中,对于一位没有疼痛的血流动力学稳定患者(即心血管功能稳定的患者),会在就医后 0 小时和 48 小时分别抽取血清 hCG。根据结果,会给出几种可能的诊断,并相应提供管理计划。
请注意,在临床中,这些树可能是错误的;这些情况被称为预测错误。构建任何树的目标是选择最佳的变量/切点,以最小化错误:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/084c9ce1-2a8e-4e6c-be6b-802ca2c4f7de.png
算法具有许多优点。首先,它们将人类诊断推理建模为一系列层次化的决策或判断。此外,它们的目标是通过强迫护理人员在每个决策点提供二元答案来消除不确定性。研究表明,算法可以改善医疗实践中的标准化护理,并且如今已广泛应用于许多医疗条件,不仅在门诊/住院治疗中,而且在急救医疗技术员(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 列联表。
假设以下表格是 400 位就诊患者中胸痛与心肌梗死的分布情况:
在上表中,有四个数字单元,分别标记为TP、FP、FN和TN。这些缩写分别代表真阳性、假阳性、假阴性和真阴性。第一个词(真/假)表示测试结果是否与通过金标准测量的疾病存在匹配。第二个词(阳性/阴性)表示测试结果是什么。真阳性和真阴性是期望的结果;这意味着测试结果是正确的,且这些数字越高,测试效果越好。另一方面,假阳性和假阴性是不可取的结果。
从真阳性/假阳性/真阴性/假阴性中可以计算出两个重要的量,即敏感性和特异性。敏感性是衡量测试在检测疾病方面的能力。它表示阳性测试结果占患病总人数的比率:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/f57c38f2-5d63-427c-bdfa-a4e23dcd1f97.png
另一方面,特异性是衡量测试识别没有疾病患者能力的指标。它的计算公式如下:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/0a94bae4-db71-474c-b262-9b03ce14ca85.png
这些概念最初可能会让人感到困惑,因此在你习惯它们之前,可能需要一些时间和多次迭代,但敏感性和特异性是生物统计学和机器学习中的重要概念。
似然比是衡量测试如何改变患病可能性的指标。它通常分为两个量:阳性测试的似然比(LR+)和阴性测试的似然比(LR-)。
依据阳性胸痛结果,心肌梗死的似然比由以下公式给出:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/d17412ae-1c9b-4868-81d8-271832e712c6.pnghttps://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/6f713f3f-8fd0-49d7-a536-d6ac8ad7ee18.pnghttps://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/51b1818a-f713-4af4-bada-fa2e065f26fb.png
根据阴性胸痛结果,心肌梗死的似然比由以下公式给出:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/057e9976-eeea-424a-96a1-051227e920de.pnghttps://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/89c5d985-f456-4233-ab6c-a0d71a0391b4.pnghttps://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/1ddd20d5-f5d5-4aa0-878e-c86a2c91bcf9.png
由于患者有胸痛症状,因此在这种情况下仅适用 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):肺部的血栓是一种危及生命的情况。请注意,评分表不仅为每个相关临床发现提供积分值,还给出了解释总分的阈值:
请注意,评分表倾向于使用易于添加的整数。显然,这样做是为了医生在看病人时使用标准方便。如果我们能够某种方式确定每个因素的最佳点值以及最佳阈值会发生什么?值得注意的是,被称为逻辑回归的机器学习方法正是如此。
逻辑回归是一种流行的统计机器学习算法,通常用于二元分类任务。它是一种称为广义线性模型的模型类型。
要理解逻辑回归,我们必须首先理解线性回归。在线性回归中,第i个输出变量(y-hat)被建模为p个个体预测变量*x[i]*的加权和:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/98c7a9b8-9862-47b0-b8af-3b36482b8687.png
变量的权重(也称为系数)可以通过以下方程确定:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/edd0183b-6d4d-44ad-9d3a-1a02b73cafe1.png
逻辑回归就像线性回归,只不过它对输出变量进行了转换,将其范围限制在 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 列联表中,所有观察值都被分为四类,具体内容将在下图中进一步讨论:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/4d8cf94a-5970-48b9-be2f-ca1e51974339.png
对于二值目标变量(例如分类问题),将有四种类型的观察值:
这些是实际为阳性的结果,我们预测也是阳性结果
这些是实际为阳性的结果,但我们预测为阴性结果
这些是实际为阴性的结果,但我们预测为阳性结果
这些是实际为阴性的结果,我们预测也是阴性结果
这四类观察值分别称为:
真阳性(TP)
假阴性(FN)
假阳性(FP)
真阴性(TN)
然后,可以从这四个量中计算出各种性能衡量指标。我们将在以下部分介绍一些常见的指标。
灵敏度,也称为召回率,回答了这个问题:“我的模型在错误地检测到病症阳性观察值方面有多有效?”
它的公式如下:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/8e474c83-a92f-4d77-8186-7780c26623a4.png
特异性回答了这个问题:“我的模型在错误地检测到病症阴性观察值方面有多有效?”
它的公式如下:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/af2e3b6f-7279-4f3d-bf77-a7324ba933d1.png
灵敏度和特异性是互补的性能衡量标准,通常一起报告,用来衡量模型的性能。
阳性预测值(PPV),也称为精确度,回答了这个问题:“给定我模型的阳性预测,它正确的可能性有多大?”
它的公式如下:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/e922c36c-f6e9-42fe-8e65-487f69e26c41.png
阴性预测值(NPV)回答了这个问题:“给定我模型的阴性预测,它正确的可能性有多大?”
它的公式如下:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/be226f63-bfec-4049-ab9a-a8c90dc8fa8e.png
假阳性率(FPR)回答了这个问题:“给定一个阴性观察值,我的模型将其分类为阳性的可能性有多大?”
它的公式如下:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/fdddf257-28aa-45c1-9a5b-b31df515e570.png
它也等于特异性(1 - Sp)。
准确度(Acc)回答了这个问题:“给定任何观察值,我的模型正确分类它的可能性有多大?”它可以作为模型性能的独立衡量标准。
它的公式如下:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/6dbbe2cb-7130-48ec-b7dd-feb3e67a6524.png
当目标变量为二值时,许多机器学习算法会以一个从 0 到 1 之间的分数形式返回观察值的预测结果。因此,预测的正负值取决于我们在该范围内设置的阈值。例如,如果我们建立一个模型来预测癌症恶性程度,并确定某个患者的恶性可能性为 0.65,那么选择一个 0.60 的正阈值将对该患者做出正向预测,而选择 0.70 的阈值则会做出负向预测。所有的性能评分都取决于我们设置阈值的位置。某些阈值会比其他阈值带来更好的表现,这取决于我们检测的目标。例如,如果我们关注癌症检测,将阈值设置为 0.05 这样较低的值会提高模型的灵敏度,虽然这会以特异性为代价,但这可能是我们想要的,因为我们可能不介意假阳性,只要我们能够识别出所有可能面临癌症风险的患者。
也许最常见的二值结果变量的性能评估范式是构建接收者操作特征(ROC)曲线。在这条曲线中,我们绘制两个度量值的值:假阳性率和灵敏度,随着阈值从 0 变化到 1。灵敏度通常与假阳性率成反比关系,导致大多数情况下出现一个小写的 r 形曲线。模型越强,灵敏度越高,假阳性率通常越低,曲线下面积(AUC)趋向于接近 1。因此,AUC 可以用来比较模型(在相同使用场景下),同时消除阈值值的依赖。
以下示例 ROC 图(示例 ROC 曲线,2016 年)显示了两条 ROC 曲线,一条较暗,一条较亮。由于红色(暗)曲线的曲线下面积大于较亮的曲线,因此可以认为由暗曲线衡量的模型性能优于由亮曲线反映的模型性能:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/d70319db-e8f3-4126-9056-fb88d1f540c7.png
精度-召回曲线是当目标变量不平衡(例如,当正负比例非常低或非常高)时,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 来检索和更新通常分布在多个相关表格中的临床数据。有关数据库的更多全面资源,请参考本章末尾的参考文献部分。
对于本章内容,假设你获得了一个预测分析的任务,任务对象是位于美国的心脏病科诊所。诊所希望你预测哪些病人在就诊后 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表格,如下图所示,包含了病人的人口统计学和识别信息——包括姓名、联系方式、生日和生物性别。在这个例子中,只有五个记录和 11 列;而在实际操作中,这个表格会包含所有与医疗机构相关的病人信息。这个表格的行数可能从几百行到几万行不等,同时表格可能包含几十列详细的人口统计学信息:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/46661fa9-00b7-42a5-8c7b-ad93ecf6056c.png
在数据库中,每个独特的病人都会被分配一个标识符(字段标记为Pid),在我们这个例子中,标识符简单地编号为 1 至 5。Pid列让我们能够在不同表格之间跟踪病人。同时,注意每个独特的病人 ID 只会有一条记录。
在确定了必要的标识符列后,重点应该放在保留哪些变量和丢弃哪些变量上。当然,年龄和性别是死亡率的重要人口学预测因素。如果种族数据存在于此表格中,那将是另一个重要的人口学变量。
该表中另一个值得注意的变量是邮政编码。越来越多的社会经济数据被用于机器学习分析。邮政编码可能与公开的普查数据相关联;这些数据可以与该表中的邮政编码进行联接,并可能提供有关每个患者所在邮政编码区域的平均教育水平、收入和医疗保障的信息。甚至有组织出售家庭级的信息;然而,使用这些数据时也伴随着对隐私保护和数据安全的重大责任。为了简化示例,我们将省略邮政编码。
我们最终表格中将省略的信息包括姓名、街道地址和电话号码。只要我们有患者 ID,这些字段对目标变量的预测影响应该不大。
虽然PATIENT表包含了每个患者的基本行政信息,但我们的任务是基于每次就诊预测死亡风险。VISIT表包含每次患者就诊的一个观察值,并包含一些关于每次就诊的临床信息:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/4ba30aec-3223-4a93-af82-f720bd647816.png
注意,患者 ID 不再是该表的主要标识符,因为患者#1 有两次就诊;相反,表中有一个Visit_id字段,示例中的编号从10001到10006,每次就诊对应一个独立的 ID。
该表还包含Visit_date字段。由于心脏病学科表示希望了解患者就诊后的 6 个月内死亡风险,因此在计算目标变量时,我们将需要使用该字段。
该表中的两个字段包含了 ICD(诊断)代码。实际的表格可能会包含每次就诊的数十个代码。对于每个编码字段,都有一个对应的名称字段,包含该代码所代表的病症名称。医疗行业中一种流行的方法是,在最终的表格中,为我们关注的每一个临床代码创建一个列(Futoma et al., 2015; Rajkomar et al., 2018)。我们将在本章后面采用这种方法。
最后,我们注意到表中包括了主治医生的名字,这可以用来衡量医生的绩效。
MEDICATIONS表包含了我们的五位患者每一项正在服用的药物的记录。在这个例子中,没有一个单独的列充当该表的主键。如以下截图所示,该表包括药物名称、剂量、频率、途径、开药医生和开药日期等信息。每种药物的 NDC 代码也被包括在内;我们在第二章,医疗基础中讲解了 NDC 代码:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/07e90df1-dec5-4169-988a-82198225d7ea.png
在最终表格中包含药物信息并不简单。例如,表格中的信息没有显示每种药物的类别。虽然有 NDC 代码,但 NDC 代码比药物名称更为详细,因为它包括了给药途径和剂量,从而使每个代码唯一;因此,不同剂型的依那普利可能会有不同的 NDC 代码。为了为每种药物创建一列,我们可以为每种药物分别创建一个表格,包含组成该药物的所有药物信息,然后将这些信息合并到我们的表格中。
如果我们选择包含剂量信息,该字段将需要一些清理。注意,病人#3 正在服用一种抗高血压联合药物——缬沙坦成分的剂量为 160 毫克,而氢氯噻吨成分的剂量为 12.5 毫克。这可能被编码为两种不同的药物,但编写一个脚本将联合药物拆分为两行并不简单。
实验室信息是临床诊断的重要部分,许多实验室测试结果是很好的预测变量(Donze et al., 2013;Sahni et al., 2018)。LABS表包含描述实验室测试名称、缩写、LOINC 代码和结果的字段:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/70660895-ce3a-4f59-a96c-3a7720579f59.png
包含实验室信息的最终表格有几种不同的处理方式。一种方法是将原始实验室结果作为连续变量包含进来。然而,这会导致一个问题,因为大多数实验室的结果会是 NULL。我们可以通过在缺失时填充一个正常范围的值来解决这个问题。另一种方法是为异常范围的实验室检测结果设置一个二进制变量。这解决了缺失数据的问题,因为如果结果缺失,它会显示为零。然而,使用这种方法,1,000 的 BNP 值(表示严重 CHF)与 350 的 BNP 值(表示轻度 CHF)没有区别。本章将演示这两种方法。
另外,Lab_value字段有时包含特殊字符,例如在肌钙蛋白结果中。这些字符需要删除,实验室值也需要相应地进行解释。培养结果(在此示例中未包括)完全是文本的,通常会命名特定的细菌菌株,而不是数字。
再次强调,这是一个简化的示例,许多常见的实验室检查(例如,白细胞计数、血红蛋白、钠、钾等)在此示例中未包含。
生命体征是反映病人健康状态的重要指标,并且在医疗健康机器学习模型中可能是很好的预测变量(Sahni et al., 2018)。生命体征通常在每次病人就诊时都进行测量,因此可以很容易以原始(数值)形式包含进来,以保持数据的细粒度。
在下方的表格截图中,我们注意到尽管身高和体重信息存在,但身体质量指数(BMI)缺失。我们将在第五章《计算基础 - Python 入门》中演示 BMI 的计算。其次,访问 #10004 缺少体温读数。在医疗中这很常见,可能是由于护理中的疏漏导致的:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/d0320c9d-5191-414e-b101-76ba65cc2243.png
VITALS 表
在本章后面,我们将为这次就诊推算正常的体温。
最后,我们来到了包含目标变量的表。MORT 表仅包含两个字段:患者标识符和患者去世的日期。未列在此表中的患者可以假定为存活:
https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/55885bd2-47e9-4317-992a-4dc302a4aee6.png
稍后,我们将学习如何将这些表中的信息转移到一个二进制目标变量中。
我们将用于转换数据库的数据库引擎是SQLite。在第一章《医疗分析入门》中,我们讲解了安装说明以及基本的 SQLite 命令。需要提到的是,SQL 有多种变体,而特定于 SQLite 的 SQL 与特定于 MySQL 或 SQL Server 数据库的 SQL 有一些小的差异。然而,所有 SQL 方言的基本原理保持一致。
此时,执行以下操作:
使用 cd 命令,导航到包含 sqlite3.exe 程序的目录。
输入 sqlite3 mortality.db 并按 Enter 键。你应该看到类似以下的提示符:sqlite>。这个提示符表明你已进入 SQLite 程序。
在本章剩余的部分,我们将创建一些表并在 SQLite 程序中执行一些 SQLite 命令。
随时退出会话,输入 .exit 并按 Enter 键。
现在让我们来看一下如何使用 SQLite 执行数据工程。首先,我们需要在数据库中创建表。然后,我们将一个一个地操作这些表,直到获得最终的目标表。
在这个模拟任务中,假设下载心脏科诊所数据的门户网站无法使用。相反,技术人员会向你发送一些 SQLite 命令,你可以用这些命令来创建六个表。你可以跟着书中的步骤,一一手动输入每个命令。或者,你可以访问书籍的官方代码库,从那里下载命令。
创建表的一种方法是手动指定其架构。让我们在这里使用第一个表 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(<https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/132125d6-1eb7-42e6-9fb1-55b899cf2b65.png>),其中https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/719756ac-ad5e-4df0-b962-c416cf4860ff.png是该列包含的最大字符数。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,数据元素应该用单引号括起来。
现在,让我们创建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');
要创建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');
要创建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');
请注意,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);
要创建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');
为了确认某个表(例如,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
我们编写的第一个查询将使用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子句的实际应用。
在这一部分,我们将演示两种添加额外列的方法。一种方法使用ALTER TABLE语句,而第二种方法使用JOIN操作。
现在我们已经将来自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);
虽然 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;
到目前为止,我们的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);
在我们的示例数据库中,VISIT 表格包含了就诊的诊断代码。尽管在我们的示例中这些诊断代码没有单独的表格,但它们是许多分析问题中最重要的信息之一。一方面,它们允许我们选择与模型相关的观测值。例如,如果我们正在构建一个预测恶性癌症的模型,我们需要诊断代码来告诉我们哪些患者患有癌症,并过滤掉其他患者。其次,它们通常是很好的预测变量(Futoma 等,2015)。例如,正如我们将在第七章 医疗健康预测模型的构建中看到的那样,许多慢性病大大增加了不良健康结果的可能性。显然,我们必须利用诊断代码中提供的信息来优化我们的预测模型。
我们将在这里介绍两种针对编码变量的转换。第一种转换,分箱,将分类变量转换为一系列二元变量,表示特定的诊断。第二种转换,聚合,将多个二元分箱变量组合为一个单一的二元或数值变量。这些转换不仅适用于诊断代码,还适用于程序、药物和实验室代码。以下是这两种转换的示例。
在这里,我们可以看到充血性心力衰竭(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';
在这里,我们看到对于我们五名患者数据集中的每个诊断代码,都进行相同类型的转换。高血压、心绞痛、糖尿病和肺动脉高压的分箱查询如下:
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';
尽管分箱在区分个别诊断时非常重要,但在实践中,我们通常希望将相似或几乎相同的诊断代码组合成一个单一变量。聚合将两个或多个二元变量合并为一个二元/数值变量。在这里,我们使用+运算符将数据集中所有的心脏诊断代码(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;
在查询集#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(如下所示)中,我们首先使用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);
一些研究文章发现,实验室值是临床结果(如再入院)的重要预测因素(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 中,我们将研究填补缺失值的基本方法。
虽然查询集#6 中呈现的方法解决了实验室缺失数据的问题,但实际实验值中的所有信息都被丢弃了。例如,对于 BNP,只有两名患者没有值,而对于温度这一生命体征,只有一名患者缺失。
一些先前的研究已经实验过这一原理,并且在使用预测模型时取得了良好的效果。在(Donze 等,2013)中,一些患者的出院数据(约 1%)存在缺失。这些数据通过假设其在正常范围内来填补。
在 SQL 中,单一填补可以轻松实现。我们在这里演示这一点。
在这里,我们使用UPDATE语句将温度变量设置为98.6,用于填补缺失值:
sqlite> UPDATE MORT_FINAL_2 SET Temp_f = 98.6
WHERE Temp_f IS NULL;
在这里,我们使用均值填补而不是常规值填补来填补缺失的温度值。因此,查询集#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;
虽然在我们的示例中填补单个缺失的温度值并不困难,但填补两个缺失的 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;
我们的表格几乎完成了。我们已经处理了所有数据。剩下要添加的就是目标变量。请看以下内容:
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);
为了可视化我们的最终结果,我们可以执行以下操作:
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 基础语言的特性有基本的了解,这对于医疗分析和机器学习至关重要。你还将了解如何开始使用 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 中,+ 运算符对两个字符串进行操作时,执行的是拼接操作(将第二个字符串附加到第一个字符串的末尾),而不是相加。
其他作用于字符串的运算符包括 * 运算符(用于重复字符串 https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/htcr-anal-mdsim/img/14655201-6690-4b4c-ad90-85b3c37d6601.png 多次,例如,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 编程中还有很多其他方面,如 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], '
',
"Patty's height: ", heights[1], '
',
"John's BMI: ", test_patients[0].calculate_bmi(), '
',
"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。
到目前为止,我们讨论的几乎所有功能都是 基础 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 类似于 R 中的 dataframe 或 SQL 中的表。与传统矩阵和其他 Python 数据结构相比,它的优势包括可以在同一 DataFrame 中包含不同类型的列、提供广泛的预定义函数以便于数据操作,以及支持快速转换为其他文件格式(包括数据库、平面文件格式和 NumPy 数组)的单行接口(便于与 scikit-learn 的机器学习功能集成)。因此,pandas确实是连接许多机器学习管道的粘合剂,从数据导入到算法应用。
pandas 的局限性包括较慢的性能以及缺乏内建的并行处理功能。因此,如果你正在处理数百万或数十亿个数据点,Apache Spark(spark.apache.org/)可能是一个更好的选择,因为它的语言内置了并行处理功能。
在本节中,我们演示了如何通过字典、平面文件和数据库将数据加载到 Python 中。
使用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
因为医疗保健数据通常采用平面文件格式,如.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库还支持从 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 操作。有关更多操作的描述,请参阅官方的 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 也可以水平连接(如果它们包含相同数量的行),通过指定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
要将列的内容提取到列表中,可以使用tolist()函数。转换为列表后,数据可以通过for循环和推导式进行迭代:
my_list = df3['new_col3'].tolist()
print(my_list)
输出结果如下:
[5, 7, 9, 11, 13, 15, 17]
pandas库提供了两种主要方法来选择性地获取和设置 DataFrame 中的值:loc和iloc。loc方法主要用于基于标签的索引(例如,使用索引/列名识别行/列),而iloc方法主要用于基于整数的索引(例如,使用行/列在 DataFrame 中的整数位置来识别)。您希望访问的行和列的具体标签/索引通过方括号紧跟在 DataFrame 名称后面提供,行标签/索引位于列标签/索引之前,并由逗号分隔。让我们来看一些示例。
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属性与.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
如果我们确定只希望获取/设置 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 中处理异构类型表格的人来说,转向使用 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
在第四章,计算基础 - 数据库中,我们讨论了使用 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.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 (www.numpy.org) 是 Python 的矩阵库。通过使用numpy.array()及类似的构造,可以创建大型矩阵并对其进行各种数学操作(包括矩阵加法和乘法)。NumPy 还具有许多用于操作矩阵形状的函数。NumPy 的另一个特点是提供了熟悉的数学函数,如sin()、cos()和exp()。
SciPy (www.scipy.org) 是一个包含许多高级数学模块的工具箱。与机器学习相关的子包包括cluster、stats、sparse和optimize。SciPy 是一个重要的包,它使 Python 能够进行科学计算。
matplotlib (matplotlib.org) 是一个流行的 Python 二维绘图库。根据其官网介绍,用户“只需几行代码就可以生成图表、直方图、功率谱、条形图、误差图、散点图等。”它的绘图库提供了丰富的选项和功能,支持高度自定义。
在本章中,我们快速浏览了基础的 Python 语言,以及两个在数据分析中非常重要的 Python 库:pandas 和 scikit-learn。我们现在已经完成了本书的基础章节。
在第六章《衡量医疗质量》中,我们将深入探讨一些真实的医疗服务提供者的表现数据,并使用 pandas 进行分析。