原文:
zh.annas-archive.org/md5/5ca914896ff49b8bc0c3f25ca845e22b
译者:飞龙
前言
前言
从这本书的一位审稿人那里得到的一条有用的反馈是,它成为了他们攀登陡峭的 MLOps 学习曲线的“秘籍”。我希望本书的内容能帮助您成为更加了解机器学习工程和数据科学的从业者,同时也是您项目、团队和组织更具生产力的贡献者。
2021 年,主要技术公司公开表示努力“民主化”人工智能(AI),通过使深度学习等技术更容易接触到更广泛的科学家和工程师。遗憾的是,企业采取的民主化方法过分关注核心技术,而不足够关注将 AI 系统交付给最终用户的实践。因此,机器学习(ML)工程师和数据科学家准备充足,能够创建实验性的 AI 原型,但在成功将这些原型交付到生产环境中则做得不够。这一点从各种各样的问题中显而易见:从 AI 项目的失败率不可接受到关于成功交付给最终用户的 AI 系统的道德争议。我相信,要取得成功,民主化 AI 的努力必须超越对核心、启用技术(如 Keras、PyTorch 和 TensorFlow)的狭隘关注。MLOps 成为了一个统一的术语,用于将实验性的 ML 代码有效地运行到生产环境中。无服务器 ML 是主导的云原生软件开发模型,用于 ML 和 MLOps,抽象出基础架构,提高了从业者的生产力。
我还鼓励您使用附带本书的 Jupyter 笔记本。笔记本代码中使用的 DC 出租车费项目旨在为您提供成长为从业者所需的练习。祝阅读愉快,编码愉快!
致谢
我永远感激我的女儿 Sophia。你是我永恒的幸福和灵感之源。我的妻子 Alla,在我写第一本书时对我有着无限的耐心。你总是在那里支持我,为我加油打气。对我的父亲 Mikhael,没有你,我不会成为现在的我。
我也要感谢 Manning 公司的工作人员,使这本书得以出版:我的开发编辑 Marina Michaels;我的技术开发编辑 Frances Buontempo;我的技术校对 Karsten Strøbaek;我的项目编辑 Deirdre Hiam;我的副本编辑 Michele Mitchell;以及我的校对员 Keri Hales。
特别感谢技术同行审阅者:Conor Redmond,Daniela Zapata,Dianshuang Wu,Dimitris Papadopoulos,Dinesh Ghanta,Dr. Irfan Ullah,Girish Ahankari,Jeff Hajewski,Jesús A. Juárez-Guerrero,Trichy Venkataraman Krishnamurthy,Lucian-Paul Torje,Manish Jain,Mario Solomou,Mathijs Affourtit,Michael Jensen,Michael Wright,Pethuru Raj Chelliah,Philip Kirkbride,Rahul Jain,Richard Vaughan,Sayak Paul,Sergio Govoni,Srinivas Aluvala,Tiklu Ganguly 和 Todd Cook。您的建议帮助我们制作出更好的书籍。
关于本书
感谢您购买《规模化 MLOps 工程》。
谁应该阅读本书
要从本书中获得最大的价值,你需要具备使用 Python 和 SQL 进行数据分析的现有技能,并且具有一些机器学习经验。我期望,如果你正在阅读本书,你对作为机器学习工程师的专业知识感兴趣,并且计划将基于机器学习的原型部署到生产环境中。
这本书适用于信息技术专业人士或学术界人士,他们在机器学习方面有一定的接触,并且正在开发或有兴趣在生产中推出机器学习系统。附录 A 中提供了本书的机器学习先决条件复习。请记住,如果您对机器学习全新,您可能会发现同时学习机器学习和基于云的机器学习基础设施可能会让人不知所措。
如果你是软件工程师或数据工程师,并计划开始一个机器学习项目,这本书可以帮助你更深入地了解机器学习项目的生命周期。你会发现,尽管机器学习的实践依赖于传统信息技术(即计算、存储和网络),但在实践中与传统信息技术有所不同。前者比你作为软件工程师或数据专业人员所经历的实验性更强,更加迭代,你应该为结果可能较少事先了解而做好准备。在处理数据时,机器学习实践更像是科学过程,包括对数据形成假设,测试替代模型以回答假设问题,并排名和选择表现最佳的模型来部署到你的机器学习平台上。
如果你是一名机器学习工程师或从业者,或者是一名数据科学家,请记住,本书不是要让你成为更好的研究者。本书不旨在教育你关于机器学习科学前沿的知识。本书也不会试图重新教授你机器学习的基础知识,尽管你可能会发现附录 A 中针对信息技术专业人员的材料是一个有用的参考。相反,你应该期望使用本书成为你机器学习团队中更有价值的合作者。本书将帮助你更好地利用你已经掌握的关于数据科学和机器学习的知识,以便你能够为你的项目或组织提供可即用的贡献。例如,你将学会如何实现关于提高机器学习模型准确性的见解,并将其转化为生产就绪的能力。
本书的组织结构:路线图
本书分为三个部分。在第一部分,我概述了将机器学习系统投入生产所需的条件,描述了实验性机器学习代码与生产机器学习系统之间的工程差距,并解释了无服务器机器学习如何帮助弥合这一差距。到第一部分结束时,我将教你如何使用公共云(Amazon Web Services)的无服务器特性来开始一个真实的机器学习用例,为该用例准备一个工作机器学习数据集,并确保你已经准备好将机器学习应用于该用例。
-
第一章全面介绍了机器学习系统工程领域以及将系统投入生产所需的条件。
-
第二章向你介绍了华盛顿特区出租车行程数据集,并教你如何在亚马逊 Web Services(AWS)公共云中开始使用该数据集进行机器学习。
-
第三章运用 AWS Athena 交互式查询服务,深入挖掘数据集,发现数据质量问题,然后通过严格和原则性的数据质量保证流程加以解决。
-
第四章演示了如何使用统计量来总结数据集样本并量化它们与整个数据集的相似性。该章还涵盖了如何选择测试、训练和验证数据集的正确大小,并使用云中的分布式处理来准备用于机器学习的数据集样本。
在第二部分,我将教你如何使用 PyTorch 深度学习框架为结构化数据集开发模型,解释如何在云中分发和扩展机器学习模型训练,并展示如何部署经过训练的机器学习模型以满足用户需求的扩展。在这个过程中,你将学会评估和评估替代机器学习模型实现的性能,并选择适合用例的正确模型。
-
第五章介绍了 PyTorch 的基础知识,介绍了核心张量应用程序编程接口(API),并帮助您熟练使用该 API。
-
第六章专注于 PyTorch 的深度学习方面,包括自动微分支持、替代梯度下降算法和支持工具。
-
第七章解释了如何通过教授图形处理单元(GPU)特性来扩展您的 PyTorch 程序,并如何利用这些特性加速您的深度学习代码。
-
第八章介绍了分布式 PyTorch 训练的数据并行方法,并深入介绍了传统参数服务器方法和基于环形的分布式训练方法(例如,Horovod)之间的区别。
在第三部分中,我向您介绍了经过考验的机器学习实践技术,并介绍了特征工程、超参数调整和机器学习流水线组装。通过本书的结尾,您将建立一个机器学习平台,该平台摄取原始数据,为机器学习准备数据,应用特征工程,并训练高性能、超参数调整的机器学习模型。
-
第九章探讨了围绕特征选择和特征工程的用例,使用案例研究来建立对可以选择或设计为 DC 出租车数据集的特征的直觉。
-
第十章教您如何通过采用一个名为 PyTorch Lightning 的框架来消除 DC 出租车 PyTorch 模型实现中的样板工程代码。此外,该章节还介绍了训练、验证和测试增强的深度学习模型所需的步骤。
-
第十一章将您的深度学习模型与一个名为 Optuna 的开源超参数优化框架集成,帮助您基于备选超参数值训练多个模型,然后根据它们的损失和指标性能对训练好的模型进行排序。
-
第十二章将您的深度学习模型实现打包成一个 Docker 容器,以便通过整个机器学习流水线的各个阶段运行它,从开发数据集一直到准备用于生产部署的训练好的模型。
关于代码
您可以从我的 Github 仓库访问本书的代码:github.com/osipov/smlbook。该仓库中的代码以 Jupyter 笔记本的形式打包,并设计用于在基于 Linux 的 Jupyter 笔记本环境中使用。这意味着在执行代码时您有多种选择。例如,如果您拥有自己的本地 Jupyter 环境,那么使用 Jupyter 本机客户端(JupyterApp: github.com/jupyterlab/jupyterlab_app
) 或 Conda 分发(jupyter.org/install
)就很棒!如果您不使用本地 Jupyter 分发,您可以使用 Google Colab 或 Binder 等云服务从笔记本中运行代码。我的 Github 仓库 README.md 文件包括徽章和超链接,以帮助您在 Google Colab 中启动特定章节的笔记本。
我强烈建议您使用本地 Jupyter 安装,而不是云服务,特别是如果您担心您的 AWS 账户凭证的安全性。代码的一些步骤将要求您使用您的 AWS 凭证来执行诸如创建存储桶、启动 AWS Glue 提取转换加载(ETL)作业等任务。第十二章的代码必须在安装了 Docker 的节点上执行,因此我建议您计划在具有足够容量安装 Docker 的笔记本电脑或台式机上使用本地 Jupyter 安装。有关 Docker 安装要求的更多信息,请参阅附录 B。
liveBook 讨论论坛
购买 大规模工程中的 MLOps 包括免费访问 liveBook,Manning 的在线阅读平台。使用 liveBook 的独家讨论功能,您可以全局附加评论到书籍或特定章节或段落。为自己做笔记,提出和回答技术问题,并从作者和其他用户那里获得帮助都是轻而易举的。
要访问论坛,请转到livebook.manning.com/#!/book/mlops-engineering-at-scale/discussion
。一定要加入论坛并打个招呼!你还可以了解更多关于 Manning 论坛和行为规则的信息,请访问livebook.manning.com/#!/discussion
。
Manning 对我们的读者的承诺是提供一个场所,个人读者和读者与作者之间可以进行有意义的对话。这并不意味着作者有任何具体的参与承诺,他对论坛的贡献仍然是自愿的(且未付酬)。我们建议您尝试向作者提出一些具有挑战性的问题,以免他失去兴趣!只要图书仍在印刷中,论坛和以前的讨论存档将可以从出版商的网站访问。
关于作者
![]() | Carl Osipov 自 2001 年以来一直在信息技术行业工作,专注于大数据分析和多核分布式系统中的机器学习项目,比如面向服务的体系结构和云计算平台。在 IBM 期间,卡尔帮助 IBM 软件集团塑造了其围绕使用 Docker 和其他基于容器的技术进行无服务器云计算的策略,使用了 IBM Cloud 和 Amazon Web Services。在 Google,卡尔向世界顶尖的机器学习专家学习,并帮助管理公司努力通过 Google Cloud 和 TensorFlow 实现人工智能的大众化。卡尔是《专业,贸易和学术期刊》上的 20 多篇文章的作者;美国专利与商标局的六项专利的发明人;以及 IBM 获得的三项企业技术奖的获得者。 |
---|
关于封面插图
MLOps 规模工程封面上的图画标题为“Femme du Thibet”,即一位来自西藏的妇女。这幅插图取自雅克·格拉塞·德·圣索维尔(1757-1810)的《不同国家的服装》系列,该系列于 1797 年在法国出版。每幅插图都是精细绘制和手工上色的。格拉塞·德·圣索维尔的收藏丰富多样,生动地提醒了我们 200 年前世界各地城镇和地区在文化上的巨大差异。人们相互隔离,说着不同的方言和语言。在街上或在乡间地区,可以通过服装轻松地辨别他们居住的地方,以及他们的行业或社会地位。
自那时起,我们的着装方式已经发生了变化,当时地区的多样性已经消失。现在很难区分不同大陆的居民,更不用说不同的城镇,地区或国家。也许我们已经以文化多样性换取了更加多样化的个人生活——当然也换取了更加多样化和快节奏的技术生活。
在难以辨别一本电脑书与另一本之际,Manning 通过以格拉塞·德·圣索维尔的图片为基础的书籍封面,庆祝计算机商业的独创性和主动性,并将两个世纪前地区生活丰富多样性的丰盛多样性重新带回生活。
第一部分:掌握数据集
构建有效的机器学习系统取决于对项目数据集的彻底了解。如果您有过构建机器学习模型的经验,您可能会想跳过这一步骤。毕竟,机器学习算法不应该能够自动从数据中学习模式吗?然而,正如您将在本书中观察到的那样,成功在实际中应用的机器学习系统依赖于一个了解项目数据集的实践者,并以现代算法无法实现的方式应用对数据的人类洞察。
第一章:介绍无服务器机器学习
本章内容包括
-
什么是无服务器机器学习,以及为什么你应该关注
-
机器学习代码与机器学习平台之间的区别
-
如何本书教授关于无服务器机器学习的知识
-
本书的目标读者
-
你可以从本书中学到什么
一个大峡谷般的鸿沟将实验性机器学习代码和生产机器学习系统分隔开来。跨越“峡谷”的风景如画:当一个机器学习系统在生产中成功运行时,它似乎能够预见未来。我第一次开始在一个机器学习驱动的自动完成搜索栏中输入查询,看到系统预测我的词语时,我就被吸引住了。我一定试了几十种不同的查询来测试系统的工作效果。那么,穿越“峡谷”需要什么?
开始起步惊人地容易。在拥有正确的数据和不到一小时的编码时间的情况下,就可以编写实验性机器学习代码,并重现我使用预测我的词语的搜索栏时所体验到的非凡经历。在与信息技术专业人士的交流中,我发现许多人已经开始尝试机器学习。有关如何入门机器学习基础的在线课程,例如 Coursera 和 Andrew Ng 的课程,提供了丰富的信息。越来越多的公司在招聘信息技术工作时期望具有机器学习的入门经验。
尽管进行机器学习实验相对容易,但基于实验结果构建产品、服务或特性已被证明是困难的。一些公司甚至开始使用独角兽一词来描述那些具备启动生产机器学习系统所需技能的难以找到的从业者。具有成功启动经验的从业者通常具有涵盖机器学习、软件工程和许多信息技术专业的技能。
本书适合那些有兴趣从实验性机器学习代码走向生产机器学习系统的人。在本书中,我将教你如何组装机器学习平台的组件,并将它们用作生产机器学习系统的基础。在这个过程中,你将学到:
-
如何使用和整合公共云服务,包括来自亚马逊网络服务(AWS)的服务,用于机器学习,包括数据摄取、存储和处理
-
如何评估和实现结构化数据的机器学习数据质量标准
-
如何工程合成特征以提高机器学习效果
-
如何可重复地将结构化数据抽样为实验子集,以进行探索和分析
-
如何在 Jupyter 笔记本环境中使用 PyTorch 和 Python 实现机器学习模型
-
如何实现数据处理和机器学习流水线,以实现高吞吐量和低延迟
-
如何训练和部署依赖于数据处理流水线的机器学习模型
-
一旦机器学习系统投入生产,如何监视和管理其生命周期
为什么你要投入时间学习这些技能?它们不会让你成为著名的机器学习研究员,也不会帮助你发现下一个开创性的机器学习算法。然而,如果你从本书中学习,你可以为尽早、更高效地交付你的机器学习成果做好准备,并成长为你的机器学习项目、团队或组织更有价值的贡献者。
1.1 什么是机器学习平台?
如果你从未听说过信息技术行业中使用的“剃牦牛”的说法,² 这里是一个假设性的例子,说明它在机器学习实践者的日常生活中可能会如何出现:
我公司希望我们的机器学习系统一个月内上线……但我们训练机器学习模型花费的时间太长了……所以我应该通过启用图形处理单元(GPU)来加快速度……但我们的 GPU 设备驱动程序与我们的机器学习框架不兼容……所以我需要升级到最新版的 Linux 设备驱动程序以实现兼容性……这意味着我需要使用新版本的 Linux 发行版。
还有许多类似的情况,你需要“剃牦牛”来加速机器学习。在将基于机器学习的系统投入生产并使其保持运行的当代实践与剃牦牛的故事有太多共同之处。与专注于使产品取得骄人成功所需的特性不同,太多的工程时间花在了似乎不相关的活动上,比如重新安装 Linux 设备驱动程序或在网络上搜索正确的集群设置来配置数据处理中间件。
为什么会这样?即使你的项目拥有机器学习博士的专业知识,你仍然需要许多信息技术服务和资源的支持来启动该系统。“机器学习系统中隐藏的技术债务”是一篇 2015 年发表的同行评审文章,基于来自谷歌数十位机器学习实践者的见解,建议成熟的机器学习系统“最多是机器学习代码的 5%”(mng.bz/01jl
)。
本书使用术语“机器学习平台”来描述在整个系统中扮演支持但关键角色的那 95%。拥有正确的机器学习平台可以成就或毁掉你的产品。
如果你仔细观察图 1.1,你应该能够描述一些从机器学习平台中所需要的功能。显然,平台需要摄入和存储数据,处理数据(其中包括将机器学习和其他计算应用于数据),并向平台的用户提供由机器学习发现的洞见。不够明显的观察是,平台应该能够处理多个并发的机器学习项目,并使多个用户相互隔离地运行这些项目。否则,仅替换机器学习代码就意味着需要重做系统的 95%。
图 1.1 尽管机器学习代码是使你的机器学习系统脱颖而出的原因,但根据谷歌的 Sculley 等人在“机器学习系统中的隐含技术债务”中所描述的经验,它仅占系统代码的 5%。无服务器机器学习帮助你使用基于云的基础设施组装其余的 95%。
1.2 设计机器学习平台时的挑战
这个平台应该能够存储和处理多少数据?AcademicTorrents.com是一个致力于帮助机器学习实践者获取适用于机器学习的公共数据集的网站。该网站列出了超过 50TB 的数据集,其中最大的数据集大小在 1 到 5TB 之间。Kaggle 是一个流行的举办数据科学竞赛的网站,包括最大为 3TB 的数据集。你可能会忽略最大的数据集,并将关注点集中在千兆字节级别的常见数据集上。然而,你应该记住,在机器学习中取得成功通常是基于更大的数据集依赖的。Peter Norvig 等人在《数据的不合理有效性》中(mng.bz/5Zz4
)提出了利用更大的数据集的机器学习系统“简单的模型和大量的数据胜过基于较少数据的更为精细的模型”的观点。
一个预计在存储和处理上达到TB 到 PB 级别的机器学习平台,必须构建成使用多台相互连接的服务器组成的分布式计算系统,在集群中每个服务器处理数据集的一部分。否则,一个GB 到 TB 级别的数据集在典型硬件配置的单台服务器上处理时会导致内存溢出问题。将服务器集群作为机器学习平台的一部分还可以解决单个服务器的输入/输出带宽限制。大多数服务器每秒只能为 CPU 提供几 GB 的数据。这意味着机器学习平台执行的大多数数据处理可以通过将数据集拆分成由集群中的服务器并行处理的块(有时称为 shards)来加速。所描述的用于机器学习平台的分布式系统设计通常被称为 scaling out。
图 1.1 的重要部分是平台中使用的基础设施的服务部分。这是将机器学习代码产生的数据洞察力暴露给平台用户的部分。如果你曾经让你的电子邮件提供商将你的电子邮件分类为垃圾邮件或非垃圾邮件,或者如果你曾经使用过你最喜爱的电子商务网站的产品推荐功能,那么你就已经作为用户与机器学习平台的服务基础设施部分进行了交互。一个主要的电子邮件提供商或电子商务提供商的服务基础设施需要能够每秒为全球数百万用户做出决策,数百万次。当然,并不是每个机器学习平台都需要以这种规模运作。然而,如果你计划基于机器学习提供产品,你需要记住,基于数字产品和服务可以在数月内达到数亿用户的可能性。例如,Niantic 推出的机器学习驱动的视频游戏 Pokemon Go,在不到两个月的时间内就吸引了五亿用户。
在规模上启动和运营机器学习平台成本很高吗?就在 2000 年代,运行一个可扩展的机器学习平台需要显著的前期投资,包括服务器、存储、网络以及构建平台所需的软件和专业知识。我在 2009 年为一家客户开发的第一个机器学习平台花费超过 10 万美元,采用的是基于本地硬件和开源的 Apache Hadoop(和 Mahout)中间件。除了前期成本之外,机器学习平台的运营成本也可能很高,原因是资源浪费:大多数机器学习代码没有充分利用平台的能力。如您所知,机器学习的训练阶段对计算、存储和网络的利用率要求很高。然而,训练是间歇性的,在生产环境中,机器学习系统相对较少进行训练,平均利用率较低。用于服务的基础设施利用率因机器学习系统的特定用例而异,并根据一天中的时间、季节性、营销活动等因素而波动。
1.3 机器学习平台的公共云
好消息是,公共云计算基础设施可以帮助您创建一个机器学习平台,并解决前一节中描述的挑战。特别是,本书介绍的方法将利用像亚马逊网络服务(Amazon Web Services)、微软 Azure 或谷歌云这样的公共云,为您的机器学习平台提供以下功能:
-
安全隔离,使您平台的多个用户能够并行工作,处理不同的机器学习项目和代码
-
当您的项目需要时,能够获取数据存储、计算和网络等信息技术,并在需要的时间内持续使用
-
按消费计量,以便仅为您使用的资源结算机器学习项目的费用
本书将教您如何使用公共云基础设施创建一个机器学习平台,以亚马逊网络服务为主要示例。具体而言,我将教您:
-
如何使用公共云服务以高效低成本地存储数据集,无论数据集是由几千字节还是几百万个字节组成
-
如何优化您的机器学习平台计算基础设施的利用率和成本,以便使用所需的服务器
-
如何弹性地扩展您的服务基础设施,以降低机器学习平台的运营成本
1.4 什么是无服务器(Serverless)机器学习?
无服务器机器学习是一种机器学习代码的软件开发模型,旨在在托管在云计算基础设施中的机器学习平台上运行,并采用按使用量计量和计费的模式。
如果一个机器学习系统在基于服务器的云计算基础设施上运行,为什么这本书要讲述无服务器机器学习?在公共云中使用服务器的想法显然与无服务器(serverless)的前提相矛盾。无服务器机器学习?那怎么可能呢?
在你反对在定义中使用无服务器一词之前,请记住,与云计算平台一起工作的信息技术专业人员已经采用了无服务器作为一个名称,以描述一种使用云计算的方法,包括计算、存储和网络,以及其他云资源和服务,以帮助他们更有效地利用时间,提高生产率并优化成本。无服务器并不意味着没有服务器;它意味着在使用无服务器方法时,开发人员可以忽略云提供商中服务器的存在,专注于编写代码。
在本书中,无服务器(serverless)描述了一种构建机器学习系统的方法,使机器学习实践者尽可能多地花费时间编写机器学习代码,尽可能少地花费时间管理和维护计算、存储、网络和操作系统;中间件;或者任何其他承载和运行机器学习平台所需的底层信息技术的部分。无服务器机器学习也实现了云计算中的成本优化的一个关键理念:消费性计费。这意味着使用无服务器机器学习时,你只需要为你使用的资源和服务付费。
机器学习,无论是在学术界还是在信息技术行业中使用,涵盖了广泛的算法和系统,包括那些在古老的围棋游戏中击败顶级人类玩家的算法,赢得了电视节目危险边缘中的比赛,并生成了世界名人和领导人的深度伪造图像。本书专注于机器学习的特定子领域,即使用结构化数据(行和列的表)的监督学习。如果你担心这个子领域太窄,注意到在谷歌,也可以说是机器学习领域的领导者,超过 80%的生产机器学习系统都是使用监督学习从结构化数据集构建和使用的,而且这些系统在不同成熟阶段都有使用。
1.5 为什么选择无服务器机器学习?
在无服务器机器学习之前,涉及将机器学习代码投入生产的开发人员必须要么与运维组织的团队成员合作,要么自己担任运维角色(这在行业中被称为 DevOps)。开发角色的职责包括编写机器学习代码,例如执行推断的代码,例如从房地产物业记录中估算房屋销售价格的代码。一旦代码准备就绪,开发人员将其打包,通常作为机器学习框架(例如第二部分中的 PyTorch)的一部分,或与外部代码库一起,以便在服务器上作为应用程序(或微服务)执行,如图 1.2 所示。
图 1.2 在无服务器平台之前,大多数基于云的机器学习平台都依赖于基础设施即服务(IaaS)或平台即服务(PaaS)服务模型,如图所示。在 IaaS 的情况下,由于运维的作用,基础结构是基于服务器的,而在 PaaS 的情况下,基础结构是基于应用程序的。一旦它们开始运行,运维也需要负责管理基础架构的生命周期。
运维角色涉及实例化运行代码所需的基础设施,同时确保基础架构具有适当的容量(内存、存储、带宽)。该角色还负责配置服务器基础架构,包括操作系统、中间件、更新、安全补丁和其他先决条件。接下来,运维人员启动一个或多个应用程序实例来执行开发人员的代码。在代码启动和运行后,操作将管理代码执行,确保请求得到高可用性(即可靠)和低延迟性(即响应迅速)。 运维还被要求通过优化基础设施利用率来帮助降低成本。这意味着不断监视 CPU、存储、网络带宽和服务延迟水平,以改变基础架构容量(例如取消服务器)并实现目标利用率目标。
IaaS 等云计算服务模型用虚拟服务器代替物理服务器,从而使运维更加高效:与物理服务器相比,虚拟服务器的配置和销毁需要更少的时间和精力。云中的运维进一步自动化,具有自动扩展等功能,根据 CPU、内存和其他服务器级指标的准实时测量来自动配置和取消配置虚拟服务器。PaaS 是一种更抽象的云服务模型,通过为代码执行运行时预配置虚拟服务器,以及预安装中间件和操作系统,进一步减少了操作负担。
虽然像 IaaS 和 PaaS 这样的云计算服务模型在机器学习平台的服务基础设施部分工作良好,但在其他方面却表现不佳。在进行训练前的探索性数据分析时,机器学习工程师可能需要对数据执行数十个不同的查询才能找到正确的查询。在 IaaS 和 PaaS 模型中,这意味着数据分析查询的基础设施需要被预配(有时是由运维团队完成)甚至在第一个查询被执行之前就要进行预配。更糟糕的是,预配基础设施的使用完全取决于用户的心情。在极端情况下,如果机器学习工程师每天只运行一个数据分析查询,并且需要一个小时才能执行,那么数据分析基础设施可能在一天的其他 23 个小时内处于空闲状态,同时仍然产生成本。
1.5.1 Serverless 与 IaaS 和 PaaS 的比较
相比之下,图 1.3 中所示的 Serverless 方法有助于进一步优化机器学习平台的利用率和成本。 Serverless 平台消除了执行传统操作任务的需要。使用 Serverless 机器学习,机器学习平台接管了整个机器学习代码的生命周期,对其进行实例化和管理。这是通过平台为不同的编程语言和函数提供专用运行时来实现的。例如,存在一个服务运行时来执行 Python 代码以运行机器学习模型训练,另一个运行时则用于执行结构化数据查询的 SQL 代码,等等。
图 1.3 Serverless 平台消除了操作管理代码基础设施的需要。基于云的平台负责在运行时中实例化代码以服务请求,并管理基础设施以确保高可用性、低延迟和其他性能特征。
使用 Serverless 与 IaaS 或 PaaS 模型相比,最重要的影响是成本。在 IaaS 和 PaaS 模型中,公共云供应商根据预配容量计费。相比之下,在 Serverless 模型中,可以根据代码是否实际在平台上执行来优化机器学习平台的成本。
无服务器和机器学习存在于两种信息技术的交汇处。一方面,机器学习为新产品、新功能,甚至是基于以前在市场上不存在的能力重新发明的行业开启了潜力。另一方面,无服务器模型在生产率和定制性之间取得平衡,使开发人员能够专注于构建不同 iating 能力,同时重用云计算平台的现有组件。无服务器方法不仅仅是重用黑盒组件。它是关于快速组装特定项目的机器学习平台,可以通过代码进行定制,从而实现新产品和服务的开发。
1.5.2 无服务器机器学习生命周期
当机器学习为基础的系统能够以规模操作时,它们变得更有价值,能够对数据做出频繁且重复的决策,同时支持大量用户。要了解机器学习在这种规模上的运行情况,想象一下您的电子邮件提供商每秒为数百万封电子邮件分类为垃圾邮件或非垃圾邮件,并为全球数百万并发用户提供服务。或者考虑来自主要电子商务网站的产品推荐(“如果您购买了这个,您可能也会喜欢那个”)。
机器学习为基础的系统在规模越大时变得越有价值,就像任何软件项目一样,它们在小规模时仍应高效运行,并且如果成功,应能够扩展以支持增长。然而,大多数软件项目并不会一夜成名,也不会发展到达数十亿用户。尽管从成本的角度来看这可能听起来很昂贵,但在本书中,无服务器机器学习中的无服务器部分是关于确保你的项目可以从公共云计算的最初承诺中受益:只为你使用的部分付费,不多不少。
1.6 本书适合谁?
本书中描述的无服务器机器学习方法针对的是对构建和实施可能需要扩展到潜在的大量用户和大量请求和数据量,但也需要在必要时缩小以保持成本效益的机器学习系统感兴趣的团队和个人。即使您决定不在项目中使用机器学习算法,您仍然可以使用本书了解无服务器和云计算如何帮助您管理、处理和分析数据。
1.6.1 本书的收获
如果您计划将机器学习系统投入生产,那么在某个时候您必须决定是购买还是构建支持 95%的支持,换句话说,机器学习平台的组件。例如,“机器学习系统中的隐藏技术债务”中的示例包括服务基础设施、数据收集、验证、存储、监视等。
如果您计划构建大多数或全部机器学习平台,您可以将本书视为一系列设计用例或来自示例机器学习项目的启发性示例。本书演示了平台功能如何在来自各种公共云供应商的云计算平台中实现,包括 AWS、Google Cloud 和 Microsoft Azure。本书还将教您有关机器学习平台所需功能的知识,包括对象存储、数据仓库、交互式查询等。在可能的情况下,本书将突出显示您可以在平台构建中使用的开源项目。虽然本书不会为您提供构建机器学习平台的逐步说明,但您可以将其用作案例研究和指导,以了解您应该构建的架构组件。
如果您计划获取大多数机器学习平台的功能,本书将为您提供说明,并引导您完成构建一个示例机器学习项目并将其投入生产使用的过程。本书还将为您介绍机器学习平台的实施步骤,包括项目所需的源代码。在可能的情况下,本书的方法依赖于便携式开源技术,如 Docker(有关 Docker 的更多信息请参见附录 B)和 PyTorch(有关 PyTorch 的更多信息请参见第二部分),这将简化将项目迁移到其他云提供商(如 Google Cloud 和 Microsoft Azure)的过程。
1.7 本书的教学方法是什么?
机器学习领域存在于计算机科学和统计学的交叉点上。因此,介绍机器学习应用的替代途径并不奇怪。许多信息技术专业人士从 Andrew Ng 的著名 Coursera 课程开始他们的机器学习学习之旅(www.coursera.org/learn/machine-learning
)。具有统计学或学术背景的人通常会将 James 等人的《统计学习导论》(Springer,2013)引用为他们的第一本机器学习教材。
本书采用软件工程方法来对待机器学习。这意味着在本书中,机器学习 是指构建具有自动从数据中推断答案的能力的软件系统的实践,以增强并经常替代重复的数据驱动决策中人类的需求。对软件工程的关注也意味着对机器学习算法、技术和统计基础的细节将比其他提及的来源更少。相反,本书将重点介绍如何描述具有以机器学习为核心的生产就绪系统的工程化方法。
1.8 如果这本书不适合你?
根据你到目前为止阅读的一切,你可能会产生一个错误的印象,即无服务器机器学习适用于机器学习的每一个应用。那么,何时使用无服务器机器学习才是合理的呢?我将首先承认,它并不适用于每种情况。如果你正在开展一个实验性的、独特的项目,一个在范围、大小或持续时间上有限的项目,或者如果你的整个工作数据集小到可以放入虚拟服务器内存中,你应该重新考虑使用无服务器机器学习。你可能更适合使用一个专用的单虚拟服务器(单节点)和一个 Anaconda 安装中的 Jupyter 笔记本,Google Colaboratory 或类似的 Jupyter 笔记本托管环境。
无服务器方法确实有助于优化与在公共云上运行机器学习项目相关的成本;但是,这并不意味着从本书中重新实现项目是免费的。要从本书中获得最大收益,你将希望使用你的 AWS 账户来复制本书即将描述的示例。为此,你将需要花费约 45 美元来按照本书中描述的步骤重新创建项目。但是,为了从本书中受益,你并不需要固守于 AWS。在可能的情况下,本书将提及其他供应商(如 Google Cloud 和 Microsoft Azure)的替代功能。好消息是,本书的整个项目都可以在三家主要公共云供应商提供的免费信用额度内完成。或者,如果你选择不在公共云中实现本书中的代码示例或项目,你也可以依靠描述来获得关于如何在规模上启动机器学习系统的概念性理解。
请记住,如果你没有准备在投入生产后维护你的系统,就不应该使用本书中的方法。现实情况是,无服务器方法与公共云平台(如 AWS)的功能集成在一起,而这些功能,特别是它们的 API 和端点,会随着时间的推移而发生变化。虽然公共云供应商有一种提供给你一些端点稳定性的方法(例如,托管的淘汰计划),但你应该准备好供应商推出新功能和变更,这反过来意味着你应该准备好花费时间和精力来维护你的功能。如果你需要最小化和控制可维护性的范围,那么无服务器方法不适合你。
隐私问题可能会引发更多原因来避免在项目中使用基于公共云的基础设施。虽然大多数公共云提供商提供复杂的基于加密密钥的数据安全机制,并具有帮助满足数据隐私需求的功能,但在公共云中,您可以确保数据隐私的程度很高,但并不一定能完全保证您的数据和流程将安全。话虽如此,本书并不教您如何确保您在云中的数据的 100%安全性,如何提供身份验证和授权,也不会处理本书中描述的机器学习系统的其他类型的安全问题。尽管在可能的情况下,我会提供可以帮助您解决安全问题的参考资料,但本书的范围不包括教您数据和隐私安全方面的知识。
从可移植性的角度来看,本书描述的方法试图在理想的代码可移植性和尽量减少部署机器学习项目所需工作量之间取得平衡。如果可移植性是您首要考虑的因素,那么最好尝试另一种方法。例如,您可以依赖于复杂的基础设施管理堆栈,如 Kubernetes 或 Terraform,用于基础设施部署和运行时管理。如果您决心使用与本书中使用的堆栈不兼容的专有框架或技术,则也不应使用无服务器机器学习方法。本书将尽可能使用非专有、可移植和开源工具。
1.9 结论
这本书能为读者解决哪些问题,读者可以从中获得什么价值?当代的机器学习实践消耗了机器学习从业者太多的生产力。本书教导读者通过一个样本机器学习项目高效工作。与其冒险在机器学习平台的各种选择中徘徊,冒着错误或失败的风险,本书将读者直接传送到经验丰富的机器学习从业者已经走过的路上。与其自己重新发现机器学习的实践,您可以使用本书来利用适用于绝大多数机器学习项目需求的能力。
本书适用于已经具有一定机器学习经验的人,因为它不会从零开始教授机器学习。该书侧重于对机器学习的实际、务实理解,并为您提供足够的知识来理解和完成示例项目。到书末,您将完成您的机器学习项目,将其部署到公共云上的机器学习平台,将您的系统作为一个高可用的 Web 服务提供给互联网上的任何人,并为确保系统长期成功的下一步做好准备。
摘要
-
成功的机器学习系统由约 5% 的机器学习代码组成。其余的是机器学习平台。
-
公共云计算基础设施为机器学习平台提供了具有成本效益的可伸缩性。
-
无服务器机器学习是一种针对在云计算基础设施中托管的机器学习平台上运行的机器学习代码进行软件开发的模型。
-
无服务器机器学习可以帮助您通过快速组装机器学习系统来开发新产品和服务。
-
本书将帮助您从实验性机器学习代码导航到在公共云中运行的生产机器学习系统的路径。
^(1.)如果您需要或希望对机器学习基础知识进行复习,附录 A 中有关于该主题的部分。
^(2.)据说这个短语起源于 1990 年代的麻省理工学院人工智能实验室(参见 mng.bz/m1Pn
)。
第二章:开始使用数据集
本章涵盖了
-
引入一个机器学习用例
-
从对象存储开始使用无服务器机器学习
-
使用爬虫自动发现结构化数据模式
-
迁移到基于列的数据存储以实现更高效的分析。
-
尝试 PySpark 提取-转换-加载(ETL)作业
在上一章中,你学习了关于无服务器机器学习平台的知识以及它们为何能帮助你构建成功的机器学习系统的一些原因。在本章中,你将开始使用一个实用的、真实世界的无服务器机器学习平台用例。接下来,你被要求下载华盛顿特区几年的出租车乘车记录数据集,以构建一个适用于该用例的机器学习模型。当你熟悉数据集并了解如何使用它来构建机器学习模型的步骤时,你将了解到无服务器机器学习平台的关键技术,包括对象存储、数据爬虫、元数据目录和分布式数据处理(提取-转换-加载)服务。通过本章的结论,你还将看到使用代码和 shell 命令示例,演示这些技术如何与亚马逊网络服务(AWS)一起使用,以便你可以在自己的 AWS 账户中应用所学的知识。
2.1 引入华盛顿特区出租车乘车数据集
本节深入探讨了华盛顿特区出租车行业的业务领域和业务规则的细节。你可能会想跳过这些细节;毕竟,它们可能与你计划在机器学习项目中使用的数据集无关。然而,我鼓励你将本节视为一个案例研究,说明你在计划应用机器学习的任何业务领域中应该提出的问题种类。当你在本节中探索业务用例时,你可以更多地了解到 DC 出租车行程数据背后的因素,并更好地为构建机器学习模型做准备。
2.1.1 业务用例是什么?
想象一下,你是一名机器学习工程师,为一家年轻有为的初创公司工作,计划推出一款自动驾驶的无人驾驶汽车,以接管乘车共享行业,并超越 Waymo、Uber 和 Lyft 等公司。你的业务领导决定,你的服务首先将在华盛顿特区市场推出。由于你的初创公司希望提供与普通出租车竞争的价格,所以你被要求编写一些代码来估算乘客从一个位置到另一个位置乘坐普通出租车的费用,范围包括华盛顿特区及其附近的弗吉尼亚州和马里兰州。
2.1.2 业务规则是什么?
华盛顿特区出租车费用的计算业务规则可从 dc.gov 网站获取。¹ 规则如下:
-
前 1/8 英里的费用为 $3.50。
-
每增加 1/8 英里的费用为 0.27 美元,每英里累计费用为 2.16 美元。
-
特殊的按时计费为每小时 25 美元,并按 1 分钟递增。
基于持续时间的收费适用于出租车在交通拥堵中的情况,因此车费金额会随着时间的推移而持续增加。dc.gov 网站还列出了其他特殊费用(例如,下雪紧急情况的费用),但让我们暂时忽略它们。
2.1.3 业务服务的模式是什么?
对于出租车车费估算服务接口的更具体规范,软件工程师可以定义输入和输出值的数据类型,如表 2.1 所示。接口期望输入由接送位置(每个位置都是一对纬度和经度坐标)以及预计行程开始时间的时间戳组成。服务的输出仅是估计出租车费用的金额。表 2.1 中提供的示例值适用于短途半英里出租车行程,大约花费 6.12 美元。由于第 1/8 英里的固定费用为 3.50 美元,剩余的 3/8 距离的费用为 0.81 美元(0.27 美元*3),剩余的 1.81 美元可能是由于出租车在星期一的午间高峰期在华盛顿特区市中心繁忙地区花费的时间。
表 2.1 出租车车费估算服务接口的架构和示例值
输入 |
---|
名称 |
接送位置纬度 |
接送位置经度 |
接送位置经度 |
送达位置经度 |
预计行程开始时间 |
输出 |
名称 |
估计车费(美元) |
表 2.1 中行程的纬度和经度坐标对应于华盛顿特区 1814 N St. NW 的接送地址和 1100 New Hampshire Ave. NW 的送达地址。请注意,该服务不执行任何地理编码;换句话说,该服务期望接送位置和送达位置是纬度和经度坐标,而不是类似于 1100 New Hampshire Ave. NW 的人类可读地址。当然,您的服务的用户不需要键入坐标的纬度和经度值。相反,用户可以在您的移动应用程序中的地图上直观地放置接送位置的图钉。然后,可以直接使用图钉的纬度和经度与该服务一起使用。或者,还可以使用来自 Google 地图和类似服务的地理编码功能,但它们不在本书的范围之内。
2.1.4 实施业务服务的选项有哪些?
表 2.1 中示例的行程仅是华盛顿特区可能的许多出租车路线中的一种。对于出租车费用估算服务的目的,出租车行程可以跨越任何上车和下车位置,只要两者都在菱形边界内,该边界包括华盛顿特区的整个范围以及马里兰州和弗吉尼亚州的附近地区。交互式地图(osm.org/go/ZZcaT9
)上的区域包括本书中华盛顿特区出租车行程的所有可能的上车和下车位置。你创业公司的移动应用的用户可以在区域边界内放置上车和下车标记,以获取行程的估价。
在着手实现用于估算出租车费用的机器学习项目之前,请考虑一下传统的软件工程方法来构建费用估算服务。一个软件工程师(假设他们对机器学习不熟悉)可能会首先开发代码来使用业务规则来计算费用,并将代码与来自服务(如 Google Maps 或 Bing Maps)的路线规划应用程序接口(API)集成。这两个 API 都可以计算从一个位置到另一个位置的最短驾驶路线,并估算路线的距离和持续时间。出租车司机实际走的路线和行程持续时间可能会因交通、道路关闭、天气和其他因素而有所不同,但这种方法可以合理估计距离。接下来,API 返回的距离可以与业务规则结合起来计算出预估的出租车费用。
构建出租车费用估算服务的传统软件工程方法有几个优点。该服务很容易实现,即使对于初级软件工程师也是如此。全球范围内有大量具备交付实施技能的工程师。一旦实施,该服务应该会产生准确的估算结果,除非出租车乘车受到异常交通、天气或紧急事件的影响。
然而,对于创业公司来说,依赖路线规划服务可能会很昂贵。像 Google Maps 这样的服务每次 API 请求都要收费,以执行路线规划和计算距离,并根据交通选择路线。这些服务的成本可能会迅速累积起来。此外,还要考虑到您服务的用户的额外成本,他们将估算行程的价格而实际上并未乘坐车辆。虽然一家更大的公司可以探索通过在开源数据软件上进行构建或从供应商购买许可证来开发本地、内部部署的路线规划服务的选项,但对于创业公司来说,这样做的成本是禁止的。
本书不是依赖于传统软件工程来构建出租车费用估算服务,而是介绍了使用 AWS 的无服务器能力实现的机器学习方法。
2.1.5 什么数据资产可用于业务服务?
华盛顿特区首席技术官办公室维护着一个网站,该网站托管了在华盛顿特区范围内发生的出租车行程的数据。⁴ 在本书中,您将使用这个从 2015 年到 2019 年的出租车行程的历史数据集来构建机器学习模型,以估计在华盛顿特区乘坐出租车的成本。机器学习方法的关键优势在于,它不依赖于昂贵的外部服务进行路线规划和距离计算。该模型将从出租车行程数据中学习,以估计基于在华盛顿特区不同位置进行的出租车行程的费用。
本书的后续部分,您还将部署模型到 AWS 作为一个具有互联网可访问 API 的 Web 服务,用于出租车费用预测。该服务将处理包含接送地点的地理坐标的 HTTP(超文本传输协议)请求,并返回估计的出租车费用。该服务的 API 还将考虑行程的开始时间,以便模型能够正确调整预测的费用。例如,相同接送地点的多次行程的费用将根据一天中的时间(高峰时段与深夜)、一周中的日期(工作日与周末)甚至一年中的日期(假期与工作日)而变化。
您还会发现,机器学习方法可以通过最小的更改来适应服务扩展到支持其他地理区域。您不需要为您的创业公司想要推出的每个城市硬编码城市特定的业务规则,而只需将其他城市的出租车行程数据扩展到数据集中即可。
2.1.6 下载和解压数据集
从 opendata.dc.gov 下载并解压文件开始处理数据集。⁵ 下载文件后,您应该能够确认您拥有 2015 年到 2019 年的数据,每年都有一个单独的 zip 文件。请注意,2019 年的数据集仅限于一月至六月的数据。解压文件后,整个数据集应该占据不到 12 GiB 的磁盘空间。
注意:解压文件后,数据集的内容会被放置到单独的子目录中。在继续之前,请将数据集中的所有文件移动到一个单独的目录中。不用担心覆盖 README_DC_Taxicab_trip.txt 文件;对于数据集的每一年,该文件都有一个相同的副本。
本书中的指令假设你在 Linux 或 MacOS 中使用 bash(或类似的)shell 作为你的 shell 环境。当你下载并解压缩文件后,你可以使用 shell 的 du 命令确认数据集大约占据你磁盘的 12 GiB 空间。
du -cha --block-size=1MB
结果输出以以下内容开头。
列表 2.1 显示从 2015 年到 2019 年的 DC 出租车行程数据集的解压缩文件
8.0K ./README_DC_Taxicab_trip.txt
176K ./taxi_2015_01.txt
60M ./taxi_2015_02.txt
151M ./taxi_2015_03.txt
190M ./taxi_2015_04.txt
302M ./taxi_2015_05.txt
317M ./taxi_2015_06.txt
...
11986 total
为简洁起见,列表 2.1 中 du 命令的输出省略了数据集中的大部分文件,并用省略号代替。完整的列表可在 Github Gist(mng.bz/nrov
)上找到。
在 zip 文件中,数据被打包为一组文本文件(具有“.txt”扩展名),每行使用竖线(|)字符将列分隔开。机器学习从业者通常将这种文件称为管道分隔逗号分隔值(CSV)文件。尽管这个行业术语令人困惑,我将继续使用 CSV 这个缩写词来表达这个数据格式,直到本书结束。
DC 出租车数据集的 CSV 文件包含标题行,即每个文件的第一行为每一列的字符串标签,例如 MILEAGE、FAREAMOUNT 等。文件中的其余行是出租车行程记录,每行一次行程。每个 zip 文件还包含一个相同的 README_DC_Taxicab_trip.txt 文件副本,该文件提供了有关数据资产的一些附加文档。文档的关键部分将在本章后面介绍。
2.2 从数据集开始使用对象存储
本节将介绍本书中机器学习项目的第一个无服务器能力。你将在对传统文件系统的了解基础上,开始学习关于无服务器对象存储的知识。接下来,你将使用 AWS 的命令行界面为 DC 出租车数据集创建一个无服务器对象存储位置,并开始将 CSV 文件复制到该位置。你将熟悉如何使用公共云对象存储来处理你的机器学习数据集,并将 DC 出租车数据集转移到对象存储中进行进一步处理。
本节和本书的其余部分将使用来自 AWS 的简单存储服务(S3)的示例来解释无服务器对象存储如何帮助你进行机器学习项目。但是,你应该知道其他公共云供应商,如 Google Cloud 和 Microsoft Azure,也提供类似的功能。
2.2.1 理解对象存储和文件系统的区别
文件系统和对象存储之间有许多相似之处,因此,如果你首先专注于它们的区别(见表 2.2),你会发现更容易理解对象存储。请记住,文件系统设计用于在命名位置存储可变或可更改的数据。这意味着使用文件系统,你可以打开文件,导航到文件中的任何行或字节位置,更改所需数量的字节,然后将更改保存回文件系统。由于文件系统中的文件是可变的,所以在进行更改后,原始数据就不存在了,并且被你的更改替换在存储介质上(例如固态驱动器)。
表 2.2 虽然文件系统和对象存储服务都具有类似的功能,如文件夹层次结构和支持复制、删除和移动等常见操作,但在这个表中突出显示了一些重要的区别。
文件系统/文件 | 无服务器对象存储/对象 |
---|---|
可变的 | 不可变的 |
缺乏全局唯一名称 | 可以使用 URL 在全球范围内标识 |
跨多个存储设备的数据冗余 | 跨多个可用性区域(数据中心)和多个存储设备的数据冗余 |
相反,对象存储中的对象是不可变的。一旦在对象存储中创建了对象,它就会存储在创建对象时放入对象中的确切数据。你可以使用你的更改创建对象的新版本,但是就对象存储服务而言,新版本是一个额外的对象,占用额外的存储空间。当然,你也可以删除整个对象,释放存储空间。
与文件不同,像 AWS S3 这样的无服务器对象存储服务中的对象设计为可以使用 HTTP 协议在互联网上访问。默认情况下,对象的公共互联网访问是禁用的。但是,无服务器对象存储中的任何对象都可以通过公共 URL 提供。为了支持此功能,对象存储服务将对象组织到桶(在 Azure 中称为容器)中,这些桶是具有全局唯一标识符的命名位置。对象存储中的每个对象都必须存在于桶中,直接或在一些层次结构的类似文件夹的名称结构下。例如,如果是 S3 桶的全局唯一标识符名称,那么名为 dataset 的对象可以直接通过 S3 桶的 URL 访问,使用 https://.us-east-2.amazonaws.com/dataset,或者在桶内名为“2015”的文件夹下使用 https://.us-east-2.amazonaws.com/2015/dataset。
示例中的对象 URL 中的“us-east-2”部分是传统文件系统和对象存储之间另一个差异的原因。与依赖于同一物理服务器内的多个存储设备进行数据冗余的文件系统不同,AWS 等对象存储提供商会在多个存储设备和被称为可用区的多个物理数据中心之间复制数据。在一个大都市区域内相互连接的高带宽和低延迟网络上的冗余可用区集群称为区域。对象 URL 的“us-east-2”部分指定了存储对象的区域的 AWS 特定代码名称。
为什么您应该为 DC 出租车乘车数据集和出租车费用估算服务使用无服务器对象存储?对于您的机器学习项目,使用无服务器对象存储,您不必担心存储空间不足的问题。像 S3 这样的服务可以帮助您从 GB 到 PB 的数据集进行扩展。正如您从第一章对无服务器的定义中所记得的那样,使用无服务器对象存储可以确保您不需要管理任何存储基础设施,并且您将根据存储桶中保存的数据量收费。此外,由于对象存储可以为存储的对象提供基于 HTTP 的接口,因此访问和集成您存储的数据所需的工作量更少。
使用 Amazon Web Services 进行身份验证
本章中剩余的示例依赖于 AWS 服务。如果您计划从示例中运行代码,则应安装 AWS 软件开发工具包 (SDK),并了解您的 AWS 帐户的访问和密钥。SDK 安装的详细信息可在 AWS 文档 (docs.aws.amazon.com/cli
) 中找到。
如果您没有可用的 AWS 访问和密钥,可以通过转到 AWS 管理控制台 (console.aws.amazon.com/
),点击右上角下拉菜单中的用户名,然后选择“我的安全凭证”来生成新的一对。要创建新的密钥对,请点击“创建访问密钥”按钮。
本书的说明假定您已经配置了带有 AWS 环境变量的 shell 使用
export AWS_ACCESS_KEY_ID=█████████████████████
export AWS_SECRET_ACCESS_KEY=█████████████████
在运行任何依赖 AWS SDK 的命令之前。
注意 在本书中,所有的清单都使用一系列 █ 字符替换了敏感的账户特定信息。请务必使用您的账户特定值来替换 AWS 访问和密钥。
要验证您已指定有效值的环境变量 AWS_ ACCESS_KEY_ID 和 AWS_SECRET_ACCESS_KEY,您可以运行
aws sts get-caller-identity
在与 AWS 成功验证的情况下,应返回您的用户 ID、账户和 Arn 值:
{
"UserId": "█████████████████████",
"Account": "████████████",
"Arn": "arn:aws:iam::████████████:user/█████████"
}
创建无服务器对象存储存储桶
本节将指导您完成创建 S3 存储桶并将 DC 出租车数据文件作为对象上传到存储桶的步骤(图 2.1)。本节中的步骤使用 AWS 的命令行界面(CLI)完成,但如果您愿意,您也可以使用 AWS 管理控制台的图形用户界面完成相同的步骤(console.aws.amazon.com
)。本书专注于基于 CLI 的方法,因为它可以轻松解释、测试和重用作为脚本自动化的一部分的步骤。
图 2.1 要将 DC 出租车数据集传输到 AWS 的对象存储中,您将使用 aws s3api create-bucket 命令创建一个 S3 存储桶,指定区域和存储桶的全局唯一标识符。接下来,您将使用 aws s3 sync 将数据集文件上传到存储桶中名为“csv”的文件夹中。
选择存储桶的区域(以及作为结果的位置)对于访问存储在存储桶中的数据的低延迟非常重要。未来,您应该记住从与您放置 S3 存储桶相同的区域运行任何处理数据的代码。本节假设您将使用 us-east-2 区域存储数据集。
要导出 AWS_DEFAULT_REGION 变量的设置,该变量将用于指定存储桶的默认区域,请运行
export AWS_DEFAULT_REGION=us-east-2
echo $AWS_DEFAULT_REGION
这应该返回您选择的存储桶区域的值。
由于存储桶名称应该是全局唯一的,因此在代码清单中发布固定且相同的存储桶名称毫无意义。相反,清单 2.2 使用 $RANDOM 环境变量,该变量始终返回一个伪随机值。然后,使用 MD5 散列函数对值进行哈希处理,以得到由一系列数字和字符组成的唯一标识符。然后将 BUCKET_ID 变量设置为哈希字符串的前 32 个字符的值,如 cut 命令的输出所示。
清单 2.2 使用伪随机生成器生成存储桶 ID 的可能唯一值
export BUCKET_ID=$(echo $RANDOM | md5sum
➥ | cut -c -32) ❶
echo $BUCKET_ID
❶ 使用 Linux 伪随机数生成器的 MD5 哈希的前 32 个字符。
注意:如果您在 Mac OSX 或 BSD 上运行清单 2.2 中的命令,则可能需要使用 md5 而不是 md5sum。
此时,您应该已经导出了环境变量,指定了存储桶的全局唯一标识符(在 BUCKET_ID 中)和区域(在 AWS_DEFAULT_REGION 中)。
在创建存储桶之前,运行
aws sts get-caller-identity
以确保您的 shell 配置为有效的环境变量值,这些值是用于与 AWS 进行身份验证所需的 AWS_ACCESS_KEY_ID 和 AWS_SECRET_ACCESS_KEY。
请注意,以下命令创建存储桶时使用的是 aws s3api 而不是您可能期望的 aws s3。这是为了与传统的、细粒度的 AWS CLI 功能兼容,这些功能在引入 aws s3 命令之前就已经提供。
注意:如果想要使用 us-east-1(北弗吉尼亚地区)而不是 us-east-2,您需要在 aws s3api create-bucket 命令中删除 LocationConstraint 参数。
创建存储桶。
aws s3api create-bucket --bucket dc-taxi-$BUCKET_ID-$AWS_DEFAULT_REGION \
--create-bucket-configuration LocationConstraint=$AWS_DEFAULT_REGION
并确认使用您的 BUCKET_ID 和 AWS_DEFAULT_REGION 环境变量替换 █ 字符后,命令返回类似于以下 JavaScript 对象符号 (JSON) 响应:
{
"Location": "http:/dc-taxi-████████████████████████████████-█████████.s3
➥ .amazonaws.com/"
}
虽然 aws s3api create-bucket 命令的响应返回存储桶的 HTTP URL,但通常您将使用以 s3:// 前缀开头的 AWS 特定命名方案来引用存储桶。如果您迷失了名称,可以使用以下代码重新创建它:
echo s3://dc-taxi-$BUCKET_ID-$AWS_DEFAULT_REGION.
您还可以使用 AWS CLI 的 list-buckets 命令打印出 AWS 帐户中存在的所有存储桶。然而,打印的名称不会使用 s3:// 前缀:
aws s3api list-buckets
list-buckets 命令可以为您提供第二次确认,以确保存储桶已经成功创建。一旦您知道存储桶已经创建成功,将当前工作目录更改为包含列表 2.1 中数据集文件的目录。
接下来,使用 aws s3 sync 命令将数据集文件复制到存储桶中。该命令递归地传输新文件和修改后的文件到或从 S3 存储桶中的位置。在运行时,命令依赖于多个线程来加快传输速度。
使用下面的代码将 CSV 文件从本地工作目录传输到存储桶中的 csv 文件夹:
aws s3 sync . s3://dc-taxi-$BUCKET_ID-$AWS_DEFAULT_REGION/csv/
数据传输所需的时间取决于您可用的带宽。在大多数情况下,您应该预计需要超过 10 分钟,因此这是休息并在传输完成后继续的好时机。
sync 命令完成后,您可以使用 aws s3 ls 命令确认数据集文件存储在存储桶的 csv 文件夹下。与类 Unix 的操作系统一样,S3 中的 ls 命令列出文件夹的内容。试着运行以下命令:
aws s3 ls --recursive --summarize \
--human-readable s3://dc-taxi-$BUCKET_ID-$AWS_DEFAULT_REGION/csv/
注意你已经将 11.2 GiB 的 CSV 文件传输到了对象存储存储桶中。这个传输到存储桶的数据量应该与列表 2.1 的数据集内容大小匹配。
文件上传到对象存储后,可以下载和处理它们。然而,这些数据还不能与非结构化的二进制大对象 (BLOB) 区分开来。为了在 CSV 文件中编目数据结构,您将需要遍历文件并发现数据模式。
2.3 发现数据集的模式
在这一点上,您已在对象存储桶中创建了一个 csv 文件夹,并已将由 11.2 GiB 的 CSV 文件组成的 DC 出租车数据集传输到该文件夹中。在开始对文件进行分析之前,重要的是要识别和了解数据集的架构。虽然可以手动发现数据集的架构,例如,通过搜索 opendata.dc.gov 网站获取架构规范或直接探索 CSV 文件的内容,但自动化方法可以简化和加速架构发现的过程。在本节中,您将了解一个数据爬虫服务,该服务可以帮助您自动发现数据集的架构,以便您更好地跟上数据的架构变化。您还将爬取 DC 出租车数据集的 CSV 文件,并将数据集的架构持久化存储在数据目录中。
2.3.1 介绍 AWS Glue
Glue 是一个包含不同 AWS 服务工具包的总称,您可以使用这些工具包为数据集准备分析。在本书中,您将了解 Glue 数据目录、Glue 提取-转换-加载(数据处理)作业以及用于分布式数据处理的 Glue 库。
Glue 数据目录是一个设计用于存储有关数据资产、数据架构和数据来源的元数据存储库。数据目录由一个或多个数据库组成,这些数据库存在于一起组织一组表。由于 Glue 数据库和表设计用于存储元数据,因此您的项目数据必须存在于 Glue 之外的存储中。例如,Glue 表可以存储存储在对象存储中、关系(例如 MySQL 或 PostgreSQL)或 NoSQL 数据库中的数据的架构。
除了数据架构之外,Glue 表还保留了有关从数据推断架构的时间以及有关数据的一些基本统计信息,例如用于存储数据的对象存储中使用的对象数量、数据中的行数以及对象存储中一行占用的平均空间量。
虽然可以手动在 Glue 数据库中创建表,但在本节中,您将了解如何使用 Glue 爬虫自动创建表。如果您熟悉在网络搜索引擎的上下文中 爬虫 一词,请记住 Glue 爬虫是不同的。它们设计用于处理和分析结构化数据格式,而不是网页。Glue 爬虫是一个过程,它
-
建立与结构化数据存储位置的连接(例如,与对象存储桶的连接)
-
确定数据使用的格式(例如,CSV)
-
分析数据以推断数据架构,包括各种列数据类型,例如整数、浮点数和字符串
爬虫可以被定期调度以定期重新爬取数据,因此,如果您的数据架构随时间变化,下次运行爬虫时,爬虫将能够检测到该变化并更新表中的架构。
要创建一个爬虫,您需要提供一个爬虫配置,该配置指定一个或多个目标,换句话说,指定应该由爬虫处理的存储位置的唯一标识符。此外,AWS 中的爬虫必须假定一个安全角色,以访问爬虫配置目标中的数据。像 AWS 这样的云提供商要求您在应用程序、服务或过程(例如 AWS Glue 爬虫)代表您访问云资源时创建安全身份,称为角色。
2.3.2 授权爬虫访问您的对象
在为 DC 出租车数据创建爬虫之前,您应完成列表 2.3 中的步骤,以创建一个名为 AWSGlueServiceRole-dc-taxi 的角色。aws iam create-role 命令(列表 2.3 ❶)创建了一个角色,该角色具有允许 Glue 服务(aws.amazon.com/glue
)假定 AWSGlueServiceRole-dc-taxi 安全角色的策略文档。简而言之,策略文档指定 Glue 爬虫应使用 AWSGlueServiceRole-dc-taxi 角色。
列表 2.3 允许 AWS Glue 爬虫访问对象存储桶中的文件
aws iam create-role \
--path "/service-role/" \
--role-name AWSGlueServiceRole-dc-taxi ❶
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "glue.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}'
aws iam attach-role-policy \ ❷
--role-name AWSGlueServiceRole-dc-taxi \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSGlueServiceRole
aws iam put-role-policy \
--role-name AWSGlueServiceRole-dc-taxi \ ❸
--policy-name GlueBucketPolicy \
--policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:*"
],
"Resource": [
"arn:aws:s3:::dc-taxi-'$BUCKET_ID'-'$AWS_DEFAULT_REGION'/*"
]
}
]
}'
❶ 创建名为 AWSGlueServiceRole-dc-taxi 的安全角色。
❷ 将 AWS Glue 策略附加到 AWSGlueServiceRole-dc-taxi 角色。
❸ 为 AWSGlueServiceRole-dc-taxi 分配一个策略文件,以启用对数据集 S3 存储桶的爬取。
aws iam attach-role-policy 命令(列表 2.3 ❷)将 AWS Glue 定义的现有服务角色(AWSGlueServiceRole)附加到 AWSGlueServiceRole-dc-taxi 角色。附加角色确保 AWSGlueServiceRole-dc-taxi 角色可以访问 Glue 数据库和表,并执行与 AWS 资源的其他必需操作。AWSGlueServiceRole 规范的详细信息可从 AWS 文档(mng.bz/XrmY
)中获取。
aws iam put-role-policy 命令(列表 2.3 ❸)指定 AWSGlueServiceRole-dc-taxi 角色被允许访问您在本章早些时候创建并填充 DC 出租车 CSV 文件的对象存储桶的内容。
2.3.3 使用爬虫发现数据模式
在这一部分,您将在 Glue 中创建一个数据库和爬虫,配置爬虫以处理 DC 出租车数据,并运行爬虫以填充数据库,其中包含数据架构的表。您可以选择使用 AWS 的浏览器界面来完成这些步骤⁸。然而,本章中的列表 2.4 和即将出现的列表将解释基于 CLI 的命令,以创建一个 Glue 数据库和一个爬虫,并启动爬虫来发现 DC 出租车数据模式。
在列表 2.4 ❶中,aws glue create-database 命令创建了名为 dc_taxi_db 的 Glue 元数据数据库,该数据库将用于存储 DC 出租车数据集的模式以及基于该模式的表。
列表 2.4 创建数据库并确认数据库存在
aws glue create-database --database-input '{ ❶
"Name": "dc_taxi_db"
}'
aws glue get-database --name 'dc_taxi_db' ❷
❶ 创建名为dc_taxi_db
的数据库。
❷ 确认名为dc_taxi_db
的数据库已创建。
由于这是一个过程,Glue 爬虫会循环通过一系列状态。成功创建爬虫后,它会从 READY 状态开始存在。启动后,爬虫转移到 RUNNING 状态。在 RUNNING 状态下,爬虫会建立与爬虫配置中指定的存储位置的连接。根据爬虫配置,爬虫会识别在处理过程中包含或排除的存储位置,并使用这些位置的数据推断数据架构。RUNNING 状态通常是爬虫完成的时间最长的状态,因为爬虫在此状态下进行大部分工作。接下来,爬虫转移到 STOPPING 状态,以使用在过程中发现的架构和其他元数据填充 Glue 数据目录表。假设进程成功完成,爬虫将返回到 READY 状态。
列表 2.5 创建和启动 Glue 爬虫
aws glue create-crawler \
--name dc-taxi-csv-crawler \ ❶
--database-name dc_taxi_db \ ❷
--table-prefix dc_taxi_ \ ❸
--role $( aws iam get-role \
--role-name AWSGlueServiceRole-dc-taxi \ ❹
--query 'Role.Arn' \
--output text ) \
--targets '{
"S3Targets": [ ❺
{
"Path": "s3://dc-taxi-'$BUCKET_ID'-'$AWS_DEFAULT_REGION'/csv/",
"Exclusions": ["README*"] ❻
}]
}'
aws glue start-crawler --name dc-taxi-csv-crawler ❼
❶ 将dc-taxi-csv-crawler
用作爬虫名称。
❷ 将爬虫输出存储在dc_taxi_db
中。
❸ 爬虫创建的表名应以dc_taxi_
前缀开头。
❹ 使用AWSGlueServiceRole-dc-taxi
角色进行爬虫操作。
❺ 配置爬虫以爬取数据集存储桶的csv
文件夹。
❻ 排除爬虫中的README_DC_Taxicab_trip
文档文件。
❼ 启动爬虫。
在列表 2.5 中,使用dc-taxi-csv-crawler
❶创建爬虫,并配置将爬取过程中发现的元数据存储在dc_taxi_db
数据库中。还配置了爬虫在数据库中创建的任何表的表前缀为dc_taxi_
。
注意,列表 2.5 中指定的命令❹比本章遇到的其他 shell 命令更复杂。在 bash 中,用 $( ) 字符括起来的命令首先被评估,然后评估的输出用于原始命令中。因此,在 $( ) 中嵌套的aws iam get-role
命令用于查找您在列表 2.3 中创建的角色的亚马逊资源名称(Arn)。
在列表 2.5 中,配置爬虫以爬取您上传 DC 出租车数据文件的对象存储桶中的csv
文件夹,并注意忽略以❺README
前缀❻的对象。
最后,根据第❼条指示使用aws glue start-crawler
命令启动dc-taxi-csv-crawler
。
对于 DC 出租车数据集,爬取过程应该在一分钟左右完成。您可以使用浏览器中的 AWS 管理控制台监视爬虫的状态,或者运行以下命令来打印爬虫的状态:
aws glue get-crawler --name dc-taxi-csv-crawler --query 'Crawler.State' \
--output text
当爬虫正在运行时,状态应为运行中。一旦爬虫完成,它应该变为就绪状态。
注意:要每两秒打印一次爬虫的最新状态,您可以在aws glue get-crawler
命令之前输入“watch”。
一旦爬虫返回到 READY 状态,您可以使用以下方法确定爬取是否成功
aws glue get-crawler --name dc-taxi-csv-crawler --query 'Crawler.LastCrawl'
由–query 'Crawler.LastCrawl’参数请求的最后一次爬取详情包括一个状态消息,指示爬虫的最后一次运行是成功还是失败。
假设爬虫成功完成,您可以使用以下方法列出爬虫发现的模式的列名和列数据类型
aws glue get-table --database-name dc_taxi_db --name dc_taxi_csv.
请注意,表名“dc_taxi_csv”是由爬虫根据清单 2.5 中爬虫表前缀的组合和爬取存储桶中 csv 文件夹的名称自动分配的,如清单 2.5 中所示。
请记住,您也可以使用浏览器查看 aws glue get-table 命令打印的模式,方法是导航到 AWS 中的 Glue 服务,选择左侧边栏中的“数据目录 > 数据库 > 表”,然后在右侧单击“dc_taxi_csv”表。
此时,您的项目已经超越了将 DC 出租车数据视为 BLOB 集合的阶段,并为数据的结构创建了更详细的规范,列举了数据列及其数据类型。然而,到目前为止您一直使用的 CSV 数据格式并不适合进行高效和可扩展的分析。在本章的即将到来的部分,您将学习如何修改数据格式以减少分析数据查询的延迟。
迁移到列式存储以进行更高效的分析
在本书的下一章中,您将了解到一种交互式查询服务,该服务可以帮助您使用 Glue 爬虫发现的表和数据模式查询 DC 出租车数据集。然而,正如本节所解释的,针对行式数据存储格式(如 CSV)的分析查询在处理大型数据集时效率低下。虽然您可以立即深入分析 DC 数据集,但本节将首先向您介绍使用列式(列式)数据存储格式(如 Apache Parquet)而不是 CSV 进行分析的好处。在解释了列式格式的优缺点之后,本节的其余部分将涵盖 AWS 的另一个用于使用 PySpark(Apache Spark)进行分布式数据处理的无服务器功能。通过本节的结论,您将学习到一个典型的 PySpark 作业示例,该示例可以帮助您将 CSV 文件重新编码为 Parquet 格式,以便在即将到来的章节中更快、更高效地分析数据集。
引入列式数据格式用于分析
DC 出租车数据集使用的 CSV 数据格式是行定向格式的一个例子。对于 CSV 文件,文件中的每一行存储来自结构化数据集的单个数据行。行定向数据格式(如图 2.2 左侧所示)通常由传统关系型数据库使用,用于存储数据记录序列。行定向格式非常适合关系型数据库的事务工作负载。事务工作负载是对数据的单个行进行操作,并且通常一次仅操作一行。例如,考虑一个存储出租车行程记录的事务性数据库。如果乘客在行程的一半决定更改目的地,事务性数据库可以轻松处理识别关于行程的数据行、更新目的地的纬度和经度坐标,然后将更改保存回数据库。
图 2.2 行定向存储(左侧)由 CSV 文件和传统关系型数据库使用,设计用于事务处理,使得一次可以更改一行数据。列定向存储(右侧)由 Apache Parquet 和许多现代数据仓库使用,最适合于对不可变数据集进行分析查询。
分析工作负载与事务性工作负载有显著不同。在对数据集执行分析查询时,通常处理数据集中的所有行,例如识别一天中特定小时内的出租车行程或排除费用超过$20 的行程的行。分析查询通常包括聚合函数,这些函数在匹配行上处理一组值,并基于该组计算单个值。聚合函数的示例包括求和、平均值(算术平均值)、最小值和最大值。
要对行定向存储格式中的数据执行分析查询,处理节点需要一次获取并操作一块数据行。例如,考虑一个计算在上午 11:00 至下午 1:00 之间开始的出租车行程的平均持续时间的查询。为了筛选出具有匹配行程时间的行,需要将行块从存储传输到处理节点,尽管块中的大多数数据都与查询无关,例如上车和下车坐标、下车时间等。
除了数据在存储和节点之间转移的不必要长时间之外,行定向格式还浪费了处理器中宝贵的高速缓存内存。由于大多数每行的数据对于执行查询是无用的,因此需要经常不必要地将缓存内容驱逐出去,以替换为另一个数据块。
相比之下,列式数据格式(图 2.2 的右侧)将数据存储在列中而不是行中。大多数现代数据仓库系统使用列式存储,此格式也被像 Apache Parquet[¹⁰] 这样的开源项目采用,以提高分析工作负载的效率。
考虑如何在列式格式中执行分析查询以找到中午行程的平均出租车行程持续时间。要筛选匹配的行程时间,只需将行程开始时间列的数据传输到处理节点。一旦找到具有匹配开始时间的行程,只需获取行程持续时间列的相应条目以计算平均值。
在这两个步骤中,需要传输到处理节点及其缓存的数据量都会有显著的节省。此外,列式格式支持各种编码和压缩方案,将文本数据转换为二进制,以进一步减少数据占用的存储空间[¹¹]。
切记,列式格式并非设计用于事务工作负载。例如 Parquet 等格式所使用的压缩和编码方案相比于简单的文件附加或行特定更改(在 CSV 文件或传统数据库中可能出现的情况)会增加写入操作的延迟。如果你计划采用 Parquet 或其他列式格式,你需要记住这些格式最适用于不可变数据集。例如,DC 出租车行程数据的记录不太可能会更改,这使得 Parquet 成为更高效的存储和更低延迟分析查询的优秀选择。
2.4.2 迁移至基于列的数据格式
正如您在本章早些时候学到的,AWS Glue 包括创建和运行数据处理作业的能力,包括将数据提取转换加载(ETL)到目标存储进行分析的作业。在本节中,您将创建和使用 Glue 中的 ETL 作业将原始的、面向行的、基于 CSV 的 DC 出租车数据集转换为列式 Parquet 格式,并将结果 Parquet 对象加载到您 S3 存储桶中的位置。
可以使用 Python 编程语言实现 Glue 数据处理作业。由于 Glue 是无服务器的,作为一个机器学习从业者,你只需要实现作业并将作业代码提交到 Glue 服务。该服务将负责验证您的代码,确保其可执行,为您的作业提供分布式基础设施,使用基础设施完成作业,并在作业完成后拆除基础设施。
一个将 CSV 数据集转换为 Parquet 格式并将转换后的数据存储为 S3 对象的基于 Python 的作业示例在清单 2.6 中展示。
代码清单中,从文件开头到❶的代码是所需对象和函数的标准库导入。从❶到❷之间的代码是用于实例化 Glue 作业的样板头部,相当于根据作业实例传递的运行时参数对作业进行初始化。
代码中的关键步骤带有❸和❹的注释。❸处使用的 createOrReplaceTempView 方法修改了 Spark 会话的状态,声明了一个临时(非物化)视图,名称为 dc_taxi_csv,可以使用 SQL 语句进行查询。
位于❹的方法执行针对 dc_taxi_csv 视图的 SQL 查询,以便作业可以处理数据集中 CSV 文件的内容并输出一些列,同时将列的内容转换为 DOUBLE 和 STRING 数据类型。
位于❺的作业提交操作仅指示作业将转换的输出持久化到存储中。
清单 2.6 将清单中的代码保存到名为“dctaxi_csv_to_parquet.py”的文件中
import sys
from awsglue.transforms import *
from awsglue.utils import getResolvedOptions
from pyspark.context import SparkContext
from awsglue.context import GlueContext
from awsglue.job import Job ❶
args = getResolvedOptions(sys.argv, ['JOB_NAME',
'BUCKET_SRC_PATH',
'BUCKET_DST_PATH',
'DST_VIEW_NAME'])
BUCKET_SRC_PATH = args['BUCKET_SRC_PATH']
BUCKET_DST_PATH = args['BUCKET_DST_PATH']
DST_VIEW_NAME = args['DST_VIEW_NAME']
sc = SparkContext()
glueContext = GlueContext(sc)
logger = glueContext.get_logger()
spark = glueContext.spark_session
job = Job(glueContext)
job.init(args['JOB_NAME'], args) ❷
df = ( spark.read.format("csv")
.option("header", True)
.option("inferSchema", True)
.option("delimiter", "|")
.load("{}".format(BUCKET_SRC_PATH)) ) ❸
df.createOrReplaceTempView("{}".format(DST_VIEW_NAME))
query_df = spark.sql("""
SELECT
origindatetime_tr,
CAST(fareamount AS DOUBLE) AS fareamount_double,
CAST(fareamount AS STRING) AS fareamount_string,
origin_block_latitude,
CAST(origin_block_latitude AS STRING) AS origin_block_latitude_string,
origin_block_longitude,
CAST(origin_block_longitude AS STRING) AS origin_block_longitude_string,
destination_block_latitude,
CAST(destination_block_latitude AS STRING)
AS destination_block_latitude_string,
destination_block_longitude,
CAST(destination_block_longitude AS STRING)
AS destination_block_longitude_string,
CAST(mileage AS DOUBLE) AS mileage_double,
CAST(mileage AS STRING) AS mileage_string
FROM dc_taxi_csv
""".replace('\n', '')) ❹
query_df.write.parquet("{}".format(BUCKET_DST_PATH), mode="overwrite") ❺
job.commit()
❶ 导入 AWS Glue 作业以便稍后管理作业的生命周期。
❷ 检索传递给作业的 JOB_NAME 参数。
❸ 将位于 BUCKET_SRC_PATH 的 CSV 文件读取到 Spark DataFrame 中。
❹ 消除 Spark SQL 兼容性的 Python 多行字符串中的新行。
❺ 使用 Parquet 格式保存到由 BUCKET_DST_PATH 指定的对象存储位置。
请注意,您需要将清单 2.6 的内容保存到名为 dctaxi_csv_ to_parquet.py 的文件中。如清单 2.7 所示,您需要将作业源代码文件上传到 S3 存储桶中的位置,以确保 Glue 服务可以访问它以启动作业。
清单 2.7 将代码上传到项目的存储桶中的 glue/dctaxi_csv_to_parquet.py
aws s3 cp dctaxi_csv_to_parquet.py \
s3://dc-taxi-$BUCKET_ID-$AWS_DEFAULT_REGION/glue/ ❶
aws s3 ls \ ❷
s3://dc-taxi-$BUCKET_ID-$AWS_DEFAULT_REGION/glue/dctaxi_csv_to_parquet.py
❶ 将 PySpark 作业文件复制到 S3 存储桶的 Glue 文件夹中。
❷ 确认文件已按预期上传。
您应该期望类似于以下内容的输出,第一列中的时间戳可能不同:
upload: ./dctaxi_csv_to_parquet.py to
➥ s3://dc-taxi-████████████████████████████████-█████████/glue/
➥ dctaxi_csv_to_parquet.py
2020-04-20 14:58:22 1736 dctaxi_csv_to_parquet.py
上传作业文件后,应按清单 2.8 中所示创建并启动作业。
清单 2.8 创建并启动 dc-taxi-csv-to-parquet-job Glue 作业
aws glue create-job \
--name dc-taxi-csv-to-parquet-job \
--role `aws iam get-role \
--role-name AWSGlueServiceRole-dc-taxi \
--query 'Role.Arn' \
--output text` \
--default-arguments \
'{"--TempDir":"s3://dc-taxi-'$BUCKET_ID'-'$AWS_DEFAULT_REGION'/glue/"}' \
--command '{
"ScriptLocation": "s3://dc-taxi-'$BUCKET_ID'-'$AWS_DEFAULT_REGION'
➥ /glue/dctaxi_csv_to_parquet.py",
"Name": "glueetl",
"PythonVersion": "3"
}'
aws glue start-job-run \
--job-name dc-taxi-csv-to-parquet-job \
--arguments='--BUCKET_SRC_PATH="'$(
echo s3://dc-taxi-$BUCKET_ID-$AWS_DEFAULT_REGION/csv/
)'",
--BUCKET_DST_PATH="'$(
echo s3://dc-taxi-$BUCKET_ID-$AWS_DEFAULT_REGION/parquet/
)'",
--DST_VIEW_NAME="dc_taxi_csv"'
要监视作业的执行,可以直接使用以下命令,或者在其前面加上 watch 命令:
aws glue get-job-runs --job-name dc-taxi-csv-to-parquet-job \
--query 'JobRuns[0].JobRunState'
作业成功后,您可以使用以下命令列出存储桶中 parquet 文件夹的内容
aws s3 ls --recursive --summarize --human-readable \
s3://dc-taxi-$BUCKET_ID-$AWS_DEFAULT_REGION/parquet/
...
Total Objects: 99
Total Size: 940.7 MiB
并确认由于转换为 Parquet 格式而引起的压缩将数据大小从以行为导向格式存储的 11.2 GiB CSV 数据减少到 940.7 MiB。
然后,您可以在 Glue 数据目录中创建一个新表,并让该表描述以 Apache Parquet 格式存储的新创建数据。使用清单 2.5 中的方法,做一些更改,包括以下内容:
-
将爬虫重命名为 dc-taxi-parquet-crawler❶,❸,❹
-
将存储桶位置更改为使用 parquet 文件夹❷
-
删除排除选项,因为 Parquet 格式的数据不包括 README 文件❷
aws glue create-crawler \ ❶
--name dc-taxi-parquet-crawler \
--database-name dc_taxi_db \
--table-prefix dc_taxi_ \
--role `aws iam get-role --role-name AWSGlueServiceRole-dc-taxi
➥ --query 'Role.Arn' --output text` --targets '{
"S3Targets": [ ❷
{
"Path": "s3://dc-taxi-'$BUCKET_ID'-'$AWS_DEFAULT_REGION'/parquet/"
}]
}'
aws glue start-crawler \ ❸
--name dc-taxi-parquet-crawler
aws glue get-crawler --name dc-taxi-parquet-crawler --query 'Crawler.State'\
--output text ❹
❶ 创建 dc-taxi-parquet-crawler
爬虫实例。
❷ 爬取包含转换后数据集的 S3 存储桶的 parquet
子文件夹。
❸ 启动爬虫。
❹ 获取当前爬虫状态。
你可以确认从 CSV 到 Parquet 的数据转换是否生成了新的 Glue 表。如果你执行
aws glue get-table --database-name dc_taxi_db --name dc_taxi_parquet
然后,输出应该类似于当你针对 dc_taxi_csv
表运行 aws glue get-table
命令时的结果,唯一的区别是 Parameters.classification 键的值发生了变化。该值应该从 csv 变为 parquet。
总结
-
使用机器学习方法建立出租车费估算服务可以帮助你降低运营成本,并避免硬编码特定城市的商业规则。
-
你将使用一个公开的华盛顿特区出租车行程数据集来学习如何使用无服务器机器学习构建出租车费估算 API。
-
无服务器对象存储服务(如 S3)可以帮助你将管理文件系统上的文件数据的知识应用于将大型数据集(从千兆字节到拍字节的数据)作为对象存储在对象存储中。
-
AWS Glue 数据爬虫可以帮助你发现数据的模式,无论你的数据在文件系统、对象存储还是关系数据库中。
-
AWS Glue 提取-转换-加载(ETL)作业服务可以帮助你在不同存储位置之间移动数据,并在过程中对数据进行转换,为分析做好准备。
-
列式数据格式可以提高分析查询的数据处理效率。
^(1.)2018 年 1 月 archive.org 的出租车费快照:mng.bz/6m0G
。
^(2.) 模式数据类型使用 ANSI SQL 格式来说明。
^(3.) 时间戳以月/日/年 小时:分钟的格式存储为字符串。
^(4.) 2015 年至 2019 年期间华盛顿特区出租车行程目录:mng.bz/o8nN
。
^(5.)2015 年至 2019 年期间华盛顿特区出租车行程目录:mng.bz/o8nN
。
^(6.) 采用独立磁盘冗余阵列来确保服务器硬件中的数据冗余: mng.bz/v4ax
。
^(7.)Amazon 资源名称(ARN)是 AWS 专用的、全球唯一的资源标识符,包括 AWS 中的用户 ID 和账户。你可以从 mng.bz/4KGB
了解更多关于 ARN 的信息。
^(8.)AWS Glue 用户界面可从 console.aws.amazon.com/glue
访问。
^(9.)AWS Glue 用户界面可从 console.aws.amazon.com/glue
访问。
^(10.)Apache Parquet 是一种开放源代码的列式数据存储格式,由 Twitter 和 Cloudera 合作开发,由 Apache 软件基金会项目维护。你可以从 github.com/apache/parquet-mr
了解更多关于该格式的信息。
Apache Parquet 列式存储格式使用的编码和压缩方案示例可从mng.bz/yJpJ
获取。
第三章:探索和准备数据集
本章内容包括
-
使用 AWS Athena 进行互动查询入门
-
在手动指定数据模式和发现数据模式之间进行选择
-
使用 VACUUM 规范原则处理数据质量
-
通过互动查询分析 DC 出租车数据质量
-
在 PySpark 中实现数据质量处理
在上一章中,将 DC 出租车数据集导入 AWS,并将其存储在项目的 S3 对象存储桶中。您创建、配置并运行了一个 AWS Glue 数据目录爬虫,分析了数据集并发现了其数据模式。您还学习了基于列的数据存储格式(例如 Apache Parquet)及其在分析工作负载中相对于基于行的格式的优势。在章节的结尾,您使用在 AWS Glue 上运行的 PySpark 作业将 DC 出租车数据集的原始基于行的逗号分隔值(CSV)格式转换为 Parquet,并将其存储在 S3 存储桶中。
在本章中,您将学习关于 Athena 的内容,这是 AWS 的另一个无服务器功能,将使用标准查询语言(SQL)对 DC 出租车出行数据集进行分析将证明其价值。您将使用 Athena 开始探索性数据分析(EDA),并识别数据集中存在的一些数据质量问题。接下来,您将了解 VACUUM,这是一个关于数据清理和数据质量的一组规范原则的缩写词,用于有效的机器学习。遵循 VACUUM 原则,您将探索 DC 出租车数据集中存在的数据质量问题,并学习使用 Athena 来重复和可靠地对整个 DC 出租车数据集的子集进行抽样分析。最后,您将实现一个 PySpark 作业,创建一个干净且可以进行分析的数据集版本。
此外,您将学习有关表格数据集数据质量的基础知识,并在有效的机器学习项目中进行实践,这是一个重要的方面。在处理数据质量时,您将了解机器学习数据质量背后的原则,以及如何在机器学习平台上使用 SQL 和 PySpark 应用它们。
3.1 进行互动查询的入门
本节首先概述了数据查询的用例,与第二章中用于将 CSV 转换为 Parquet 的数据处理作业相对应。然后,当您介绍 AWS 的交互式查询服务 Athena 时,您将了解使用模式读取方法进行结构化数据查询的优缺点,并准备尝试使用示例出租车数据集,并将替代方案应用于该数据集。到本节结束时,您将准备好使用 AWS Athena 的基于浏览器的界面,并探索 DC 出租车车费数据集中的数据质量问题。在本节实现中,您将开始掌握对 DC 出租车车费数据集的探索性数据分析所需的技能,并开始练习在改进数据质量时有用的查询类型。
3.1.1 选择交互式查询的正确用例
本节澄清了 I/O 密集型和计算密集型工作负载之间的区别,以及如何从 AWS Glue、AWS Athena、Google Cloud DataProc 或 Google Cloud BigQuery 等技术中选择这两类工作负载。
要对何时使用交互式查询服务有直觉,首先要介绍数据处理中高吞吐量与低延迟的区别是很有价值的。请记住,既可以使用面向行的格式(如 CSV),也可以使用面向列的格式(如 Parquet)来存储结构化数据集,这些数据集被组织成行和列的表。本书使用术语记录来描述来自结构化数据集的单个数据行。将数据集描述为记录而不是行有助于避免混淆,即数据是存储在行向或列向格式中。换句话说,记录独立于底层数据存储格式。
在第二章中,您使用托管在 AWS Glue 上的 PySpark 作业执行了一个高吞吐量的工作负载,以将数据记录从 CSV 迁移到 Parquet。高吞吐量工作负载的一个特点是输入和输出记录之间的 一对多(有时是 一对任意)关系:用作工作负载输入的单个记录可能产生零个、一个或多个输出记录。例如,一个以 SELECT * 开头的简单 SQL 语句会为数据存储中的每个输入记录返回一个输出记录,带有 WHERE 子句的 SELECT 可以过滤一部分记录,而涉及 SELF JOIN 的更复杂的 SQL 语句可以将从表中返回的记录总数平方。在实践中,一对多关系意味着输出记录的数量与输入记录的数量具有相同的数量级,并且与输入记录的数量没有实质性的不同。这样的工作负载也可以描述为输入/输出密集型,因为执行工作负载的底层 IT 基础设施花费的时间用于读取和写入存储,而不是计算。
在第二章开始执行 PySpark 作业时,您可能已经注意到 CSV 到 Parquet 的重新编码工作量需要大约几分钟才能完成。工作量的高延迟(这里的延迟描述了 Glue 作业从开始到结束的持续时间)是由于为每个 CSV 输入记录写入 Parquet 输出记录引起的。工作量的高吞吐量描述了以输入和输出记录的数量之和为总量来处理的记录的总数量。由于处理输入和输出记录所花费的时间占此类工作负载总持续时间的相当大比例,因此它们也被描述为输入/输出(I/O)密集型。
与专为高吞吐量工作负载设计的 AWS Glue 相比,AWS 和 Google 的交互式查询服务(如 AWS Athena 和 BigQuery)旨在处理低延迟的多对一(或多对少)工作负载,其中许多输入记录(考虑表中的所有记录的大多数)被聚合到少数(或通常仅一个)输出记录中。多对一工作负载的示例包括使用 COUNT、SUM 或 AVG 等函数以及与 SQL GROUP BY 子句一起使用的其他聚合函数的 SQL 语句。通过使用 SQL 操作识别列的唯一值集合,多对少工作负载在 SELECT DISTINCT 时很常见。多对一和多对少工作负载也可以描述为计算密集型,因为底层 IT 基础设施花费更多时间执行计算(例如,计算算术平均值)而不是输入/输出操作(例如,读取或写入数据)。
3.1.2 介绍 AWS Athena
本节概述了 AWS Athena 交互式查询服务,并描述了 Athena 如何应用基于读取的模式来进行数据处理和分析。
Athena 是 AWS 的无服务器查询服务,主要用于使用 ANSI SQL 和 SQL 扩展对结构化和半结构化数据进行交互式分析。交互式分析意味着 Athena 被设计用于执行计算密集型 SQL 工作负载,并在几秒内返回结果。这也意味着,虽然可以使用 Athena 提取、转换和加载(ETL)数据,但你应该计划使用 PySpark 而不是 Athena 编写 ETL 代码,以支持高吞吐量而不是低延迟的数据处理。如果你曾经使用过关系数据库(如 MySQL 或 PostgreSQL)的交互式查询界面,你就知道 Athena 提供了类似的功能。尽管 Athena 面向通过基于浏览器的界面进行交互式分析的最终用户,但也支持基于 API 的访问。作为查询服务,Athena 在以下重要方面与传统的关系数据库和数据仓库有所不同:
-
Athena 依赖 AWS 服务进行数据存储,并不存储查询的源数据或元数据。例如,Athena 可以查询来自 S3 的数据集,以及来自 MySQL、DynamoDB 或其他提供 Athena 数据源连接器的数据源。当数据作为查询结果生成时,Athena 将数据存储到预配置的 S3 存储桶中。
-
Athena 软件基于 Facebook 工程师部分开发的开源 PrestoDB 分布式查询引擎。该实现已经被证明可以扩展到 Facebook 内部的工作负载,涉及对数百 PB 数据的查询。
-
Athena 不使用传统关系型数据仓库的写入时模式。这意味着 Athena 可以根据互斥的模式定义解释相同的数据;例如,Athena 可以将相同数据值的列查询为字符串或数字。这种方法通常被描述为读取时模式。
在第二章中,你学习了如何使用网络爬虫从数据中发现数据模式,并学习了如何根据发现的模式在数据目录中创建数据库和表。Athena 要求在服务查询由表描述的数据之前,必须在数据目录中定义表。如图 3.1 上的虚线所示,Athena 可用于为存储在数据存储服务中的数据定义模式,例如 S3。或者,Athena 可以查询根据爬虫发现的元数据定义的表。
图 3.1 Athena 查询服务既可以定义模式,也可以使用由 Glue 爬虫定义的模式来分析数据,使用相同数据集的替代模式定义。替代且互斥的模式可以帮助您为特定用例应用正确的模式。
依赖 Athena 为数据目录中的表定义模式既有优势也有劣势。由于许多存储在数据仓库中并用于机器学习的真实世界数据集是宽的(包含许多列),因此使用 Athena 定义表模式意味着需要为模式中的每一列显式指定 SQL 数据类型所需的工作量。虽然看起来工作量是有限的,但请记住,模式定义需要在基础数据集发生变化时进行维护和更新。然而,如果你需要能够使用包含互斥数据类型的数据模式查询相同的数据,那么使用 Athena 定义模式就是正确的选择。
相比之下,如果你使用基于爬虫的模式定义方法,你不需要显式指定数据类型,因为它们会被爬虫自动发现。爬虫还可以定期运行,根据数据的变化更新模式定义。使用爬虫的缺点在于,当你需要使用与自动发现的模式不同的替代数据模式来查询数据时,它就显得不那么相关了。在基于爬虫的方法中,这意味着要么使用 Athena 定义替代模式,要么实施一个 PySpark 作业,将替代模式应用于数据集。请记住,在第二章结束时你实施的 PySpark 作业重新编码了 STRING 数据类型(例如,对于车费金额)为 DOUBLE。
准备示例数据集
在本节中,你将开始使用一个小型 CSV 数据集,以更好地了解依赖 Athena 为其定义模式的优缺点。在数据集中,行包含表示出租车行程费用以及行程上车和下车位置的纬度和经度坐标的值。
要开始使用 Athena 查询此小型数据集,您需要首先将相应的 CSV 文件上传到您 S3 存储桶的一个文件夹中,该文件夹只包含五行数据。
在您本地文件系统上创建一个名为 trips_sample.csv 的 CSV 文件,并通过执行以下 bash 命令预览它:
wget -q https://gist.githubusercontent.com/osipov/
➥ 1fc0265f8f829d9d9eee8393657423a9/
➥ raw/9957c1f09cdfa64f8b8d89cfec532a0e150d5178/trips_sample.csv
ls -ltr trips_sample.csv
cat trips_sample.csv
假设 bash 命令成功执行,则 cat 的输出应该产生类似于表 3.1 的输出。
表 3.1 本数据集中数据值的类型解释²取决于你选择的模式。
车费金额 | 起点 | 终点 |
---|---|---|
纬度 | 经度 | 纬度 |
8.11 | 38.900769 | −77.033644 |
5.95 | 38.912609 | −77.030788 |
7.57 | 38.900773 | −77.03655 |
11.61 | 38.892101 | −77.044208 |
4.87 | 38.899615 | −76.980387 |
接下来,将文件内容复制到您 S3 对象存储桶中的 samples 文件夹中,并通过运行以下命令确认它已成功复制:
aws s3 cp trips_sample.csv s3://dc-taxi-$BUCKET_ID-$AWS_DEFAULT_REGION
➥ /samples/trips_sample.csv
aws s3 ls s3://dc-taxi-$BUCKET_ID-$AWS_DEFAULT_REGION/samples/
➥ trips_sample.csv
如果您正确上传了示例文件,则aws s3 ls
命令的输出应该报告其大小为 378 字节。
3.1.4 使用浏览器从 Athena 进行交互式查询
本节介绍了 AWS Athena 的基于浏览器的图形用户界面(GUI)。虽然可以使用 Athena GUI 执行本章中使用的查询,但使用基于命令行界面(CLI)的 Athena API 访问可以更直观地演示数据分析自动化和可重现性,而不是使用浏览器。因此,虽然本节涵盖了如何使用基于浏览器的界面,但后续章节将专注于脚本化基于 CLI 的查询。
要从浏览器访问 Athena 界面,请使用 AWS 控制台顶部菜单中的 AWS 服务下拉菜单导航到 Athena 服务。您应该能够点击到类似于图 3.2 所示的屏幕。
图 3.2 显示了 Athena 基于浏览器的界面的屏幕截图,说明了您需要了解的交互式查询的关键组件。
请注意,在 Athena 界面屏幕上,您需要确保您正在访问的 Athena 与您的 $AWS_DEFAULT_REGION 环境变量的值匹配的地区,也就是您上传 CSV 文件的地区。与其他 AWS 服务一样,您可以使用 AWS 控制台顶部菜单中右上角的下拉菜单更改地区。
图 3.2 中高亮显示的选择项 1 指定了您在第二章中创建的数据目录数据库。确保您已选择 dc_taxi_db 作为数据库。选择数据库后,请确认在高亮显示的选择项 2 中,您可以看到您在 dc_taxi_db 数据库中使用爬虫创建的表。表应命名为 dc_taxi_csv 和 dc_taxi_parquet。
Athena 的 SQL 查询使用图 3.2 中高亮显示的分栏 SQL 编辑器指定。如果这是您第一次使用 Athena,请在运行查询之前为服务指定查询结果位置。默认情况下,Athena 执行的每个查询的输出都保存到 S3 中的查询结果位置。执行以下 bash shell 命令并将输出复制到剪贴板:
echo s3://dc-taxi-$BUCKET_ID-$AWS_DEFAULT_REGION/athena/
从 shell 命令的输出中注意到,Athena 将查询位置结果存储到您的存储桶中的 athena 文件夹中。
在您运行第一个查询之前,您应该配置 S3 查询结果位置,首先点击图 3.2 中屏幕截图上部显示的“设置查询结果位置”超链接,然后将您刚刚复制到剪贴板的值粘贴到对话框中的结果位置文本字段中,最后点击保存。
3.1.5 使用示例数据集进行交互式查询
本节将解释如何使用 trips_sample.csv 文件中的少量记录在 Athena 中应用基于读取的模式。在后续章节中,您将能够将相同的技术应用于更大的数据集。
由于接下来的 Athena 示例依赖于使用脚本化的基于 CLI 的 Athena API 访问,请从配置 Athena 开始,将 Athena 文件夹配置为您 S3 存储桶中用于存储 Athena 查询结果的位置。 这意味着您应该从您的 shell 执行以下操作:
aws athena create-work-group --name dc_taxi_athena_workgroup \
--configuration "ResultConfiguration={OutputLocation=
➥ s3://dc-taxi-$BUCKET_ID-$AWS_DEFAULT_REGION/athena},
➥ EnforceWorkGroupConfiguration=false,PublishCloudWatchMetricsEnabled=false"
创建完 dc_taxi_athena_workgroup
后,您可以通过 CLI 开始使用 Athena。
由于 Athena 与 Glue 数据目录集成,因此可以在数据读取时(即查询数据时)应用数据目录表的数据库和模式定义,而不是在数据写入时。 然而,为了说明 Athena 的模式读取功能,而不是使用爬虫来发现五个样本行程的表模式,您将首先使用手动定义的模式填充数据目录。 您将创建的第一个表将所有的数据值都视为 STRING 数据类型,如列表 3.1 中所示。 后来,您将创建一个将相同值视为 DOUBLE 的第二个表。
列表 3.1 使用 STRING 类型为五个 DC 行程数据集定义模式
CREATE EXTERNAL TABLE IF NOT EXISTS dc_taxi_db.dc_taxi_csv_sample_strings(
fareamount STRING,
origin_block_latitude STRING,
origin_block_longitude STRING,
destination_block_latitude STRING,
destination_block_longitude STRING
)
ROW FORMAT DELIMITED FIELDS TERMINATED BY ','
LOCATION 's3://dc-taxi-$BUCKET_ID-$AWS_DEFAULT_REGION/samples/'
TBLPROPERTIES ('skip.header.line.count'='1');
使用列表 3.1 中的 SQL 语句定义 dc_taxi_db.dc_taxi_csv_sample_strings
表的模式,请从您的 bash shell 执行以下命令序列。
列表 3.2 使用 AWS CLI 对 AWS Athena 进行基于 Shell 的查询
SQL=" ❶
CREATE EXTERNAL TABLE IF NOT EXISTS dc_taxi_db.dc_taxi_csv_sample_strings(
fareamount STRING,
origin_block_latitude STRING,
origin_block_longitude STRING,
destination_block_latitude STRING,
destination_block_longitude STRING
)
ROW FORMAT DELIMITED FIELDS TERMINATED BY ','
LOCATION 's3://dc-taxi-$BUCKET_ID-$AWS_DEFAULT_REGION/samples/'
TBLPROPERTIES ('skip.header.line.count'='1');"
ATHENA_QUERY_ID=$(aws athena start-query-execution \
--work-group dc_taxi_athena_workgroup \
--query 'QueryExecutionId' \
--output text \
--query-string "$SQL") ❷
echo $SQL
echo $ATHENA_QUERY_ID
until aws athena get-query-execution \ ❸
--query 'QueryExecution.Status.State' \
--output text \
--query-execution-id $ATHENA_QUERY_ID | grep -v "RUNNING";
do
printf '.'
sleep 1;
done
❶ 将基于字符串的模式定义保存到 SQL shell 变量中。
❷ 根据 SQL 变量的内容启动 Athena 查询。
❸ 反复检查并报告 Athena 查询是否完成。
到目前为止,根据您使用 SQL 查询关系数据库的经验,您可能会尝试使用以 SELECT *
开头的 SQL 语句来查询 dc_taxi_csv_sample_strings
表。 然而,在处理列式数据存储时,尽可能避免使用 SELECT *
是更好的选择。 正如您在第二章中学到的,列式存储在多个文件以及文件的不同部分中维护数据的各个列。 通过仅指定您查询所需的列的名称,您可以将像 Athena 这样的列感知查询引擎指向仅处理您需要的数据部分,从而减少处理的数据总量。 对于 Athena 以及其他公共云供应商的无服务器查询服务,处理的数据量越少,成本就越低。 由于 Athena 是无服务器的,因此您按照 Athena 查询处理的数据量来计费。
此外,列表 3.2 中的脚本相当冗长。 为了保持本章中查询示例的简洁性,请继续下载 utils.sh 脚本:
wget -q https://raw.githubusercontent.com/osipov/smlbook/master/utils.sh
ls -l utils.sh
下载完成后,脚本将占用文件系统的 4,776 字节。 这个脚本在接下来的示例中使用 source utils.sh
命令加载,并通过向 athena_query_to_table
函数传递 Athena 的 SQL 查询来调用。
当 Athena 使用您刚刚创建的 dc_taxi_csv_sample_ strings 表的方案查询数据时,数据被处理为将纬度和经度坐标解释为字符串数据类型。将坐标值视为字符串类型可在将坐标对传递给网页脚本以在浏览器中显示交互式映射上的锥标时,非常有用。请注意,以下查询不涉及任何数据类型转换,因为数据是由 Athena 从源 CSV 数据作为 STRING 读取的。因此,可以直接在数据值上使用 ANSI SQL ||(双竖杠)操作来执行连接操作。
列出 3.3 使用 STRING 数据类型为坐标简化基于浏览器的用例
source utils.sh
SQL="
SELECT
origin_block_latitude || ' , ' || origin_block_longitude
AS origin,
destination_block_latitude || ' , ' || destination_block_longitude
AS destination
FROM
dc_taxi_db.dc_taxi_csv_sample_strings
"
athena_query_to_table "$SQL" \
"ResultSet.Rows[*].[Data[0].VarCharValue,Data[1].VarCharValue]"
这导致一个类似于以下内容的输出,其中每行都包含字符串数据类型,将纬度和经度值连接在一起:
origin | destination |
---|---|
38.900769,–77.033644 | 38.912239,–77.036514 |
38.912609,–77.030788 | 38.906445,–77.023978 |
38.900773,–77.03655 | 38.896131,–77.024975 |
38.892101000000004,–77.044208 | 38.905969,–77.06564399999999 |
38.899615000000004,–76.980387 | 38.900638,–76.97023 |
或者,Athena 可以使用不同的架构,把相同的坐标值作为浮点数据类型来处理,以计算数据集中最大和最小车费之间的差异。从 shell 中执行下面的 Athena 操作,以创建 dc_taxi_csv_sample_double 表,其中 trips_sample.csv 文件中的每个值都被解释为 SQL DOUBLE:
%%bash
source utils.sh ; athena_query "
CREATE EXTERNAL TABLE IF NOT EXISTS dc_taxi_db.dc_taxi_csv_sample_double(
fareamount DOUBLE,
origin_block_latitude DOUBLE,
origin_block_longitude DOUBLE,
destination_block_latitude DOUBLE,
destination_block_longitude DOUBLE
)
ROW FORMAT DELIMITED FIELDS TERMINATED BY ','
LOCATION 's3://dc-taxi-$BUCKET_ID-$AWS_DEFAULT_REGION/samples/'
TBLPROPERTIES ('skip.header.line.count'='1');
"
dc_taxi_csv_sample_double 表可以成为查询的数据源之后,您可以尝试处理源数据文件中的值作为双精度浮点数,例如,通过尝试查找五行数据集中的最大和最小车费之间的差异:
source utils.sh ; athena_query_to_pandas "
SELECT ROUND(MAX(fareamount) - MIN(fareamount), 2)
FROM dc_taxi_db.dc_taxi_csv_sample_double
"
列出中的 athena_query_to_pandas 函数将 Athena 查询的输出保存到文件系统上的临时/tmp/awscli.json 文件中。首先,按照下面的列表所示定义 Python 实用程序函数。
列出 3.4 报告 Athena 结果为 pandas DataFrame
import pandas as pd
def awscli_to_df():
json_df = pd.read_json('/tmp/awscli.json')
df = pd.DataFrame(json_df[0].tolist(), index = json_df.index, \
columns = json_df[0].tolist()[0]).drop(0, axis = 0)
return df
然后,您可以方便地将 tmp/awscli.json 文件的内容预览为 pandas DataFrame,以便调用 awscli_to_df() 输出以下结果:
_col0 |
---|
6.74 |
输出显示,数据集中的出租车费用的最大值和最小值之间存在 $6.74 的差异。此外,由于最后一个查询未使用 AS 关键字为结果中唯一列分配名称,因此 Athena 使用了自动生成的列名称 _col0。
3.1.6 查询 DC 出租车数据集
本节介绍如何使用 AWS Athena 查询 DC 出租车数据集,以便在即将到来的部分中,您可以准备分析 DC 出租车数据的质量。
正如你在第二章中回忆起的那样,DC 出租车数据的 Parquet 格式版本被存储为 dc_taxi_db 数据库中的 dc_taxi_parquet 表。让我们尝试使用 Athena CLI 查询这个表的 10 行数据:
source utils.sh ; athena_query_to_pandas "
SELECT fareamount_double,
origindatetime_tr,
origin_block_latitude_double,
origin_block_longitude_double,
destination_block_latitude_double,
destination_block_longitude_double
FROM dc_taxi_db.dc_taxi_parquet
LIMIT 10
"
不要忘记使用 awscli_to_df()函数使用 pandas 输出结果。
由于 Athena 执行的数据处理是并行和分布式的,所以 dc_taxi_parquet 表中的行顺序在每次执行最后一个查询时都会不同。因此,你将看到的查询结果中的 10 行与我的不同。然而,即使只有 10 行的结果,你也应该能够找到包含缺失值的行。缺失值将出现在一个或多个列中的空单元格或 None 值中。
例如,你可能会发现你的输出缺少起点的数值,但目的地坐标没有缺失。在某些情况下,结果中除了车费和行程起点的日期/时间值之外,其他值都会缺失。导入的 DC 出租车行程数据集存在数据质量问题。
虽然在第二章将 DC 出租车数据转换为 Parquet 格式有助于优化查询和分析性能,但你尚未对数据集进行任何质量检查。简而言之,你不知道可用的数据是否可信。解决这些质量问题意味着什么?应该或不应该修复哪些问题?你应该花多少精力清理数据?清理后的数据集在数据分析和机器学习方面何时达到足够好的质量?
3.2 开始进行数据质量测试
本章的这一部分与其他部分的写作方式不同。虽然大部分章节关注于技术知识和详细指导,以特定步骤的形式与无服务器机器学习技术一起使用,但本部分是规范化的而不是指导性的。换句话说,你首先应该了解机器学习中的数据质量应该是怎样的,然后学习将数据质量应用于机器学习数据集的步骤。我希望通过这一部分教会你应该在任何机器学习项目中使用的数据质量标准,无论数据集如何,因此本节主要涉及概念而不是代码。
可以说,数据清理是机器学习中重要但并非最令人兴奋的话题,为了将数据质量原则更加具体、容易记住,以及希望更有趣,这一部分主要依赖于实际案例和数据清理示例,你可以应用到下一个机器学习项目中。如果你愿意直接进入清理 DC 出租车数据的实际步骤,可以直接跳到 3.3 节。
3.2.1 从“垃圾进垃圾出”到数据质量
本小节说明了解决数据质量问题的理由,并描述了本章后面部分回答的数据质量问题。
“垃圾进,垃圾出”是信息技术行业中众所周知的陈词滥调。在本书的背景下,它意味着如果输入到您的机器学习系统中的是垃圾,那么机器学习算法将会在垃圾上进行训练,机器学习的输出也将是垃圾。这个陈词强调了对机器学习项目的数据质量的重要性,但它并没有证明垃圾进,垃圾出对于现实世界的数据分析和机器学习是至关重要或相关的。
2010 年,当全球经济仍在从几年前的金融危机中恢复时,两位哈佛经济学家卡门·M·莱因哈特和肯尼斯·S·罗戈夫发表了一篇研究论文,解构了可以帮助国家重新实现经济增长的政策。在这篇论文中,经济学家们认为,债务超过其国内生产总值(GDP)90% 的国家将面临经济衰退。部分基于这些经济学家的分析,一些欧盟(EU)国家采取了严厉的紧缩政策,削减工资并裁减了数千个工作岗位。结果证明,用于分析的数据是错误的。
政客们基于莱因哈特-罗戈夫(Reinhart-Rogoff)的结果制定政策,成为经典的垃圾进垃圾出问题的受害者。莱因哈特-罗戈夫惨败只是许多情况之一,其中低质量数据的分析导致数十亿美元的负面后果。即使在 COVID-19 疫情加速数字转型之前,备受尊敬的哈佛商业评论杂志也发表了一个引人注目的说法,即美国经济因糟糕数据而产生的总成本应该以数万亿美元来衡量。⁴
数据质量问题很重要,但作为一名机器学习从业者,你可能不会立即意识到自己正在使用低质量的数据集。你如何知道你的数据是垃圾还是足够质量以进行机器学习?
3.2.2 在开始处理数据质量之前
本小节帮助您了解在解决其中任何一个结构化(表格)数据集的数据质量问题之前应该回答的问题。
在开始处理数据质量之前,你需要的不仅仅是一个结构化数据集。你需要知道关于 DC 出租车数据的那些问题的答案:
-
数据集是否可以查询为一个或多个行列表? 换句话说,你是否正在查询使用结构化数据集格式存储的数据?回想一下,在第二章中,你了解了用于结构化数据的行向(例如 CSV)和列向(例如 Apache Parquet)存储格式的定义。由于 VACUUM 是用于结构化数据集的一套数据质量原则,因此它不适用于用于自然语言文本、图像、音频和视频的非结构化格式。
-
你需要基于哪些列回答哪些问题? 本书中基于 DC 出租车数据集的机器学习示例围绕着一个问题构建:“在你知道 DC 出租车行程的开始时间以及行程的上车和下车地点的纬度和经度坐标时,车费金额列的值是多少?” 知道你希望对数据提出的问题也有助于你了解数据集中的基本数据,换句话说,是用于训练机器学习系统以回答问题的数据范围。除了基本数据外,你的数据集还可能包含参考数据,这些数据对于确保你的基本数据的质量(特别是准确性)是有用的,但不需要以相同严格的程度进行清理。例如,DC 出租车数据集中里程表列中的值并不是回答问题所必需的,但作为参考来与车费金额列的值进行比较,并确保车费金额值具有正确的数据质量程度是有用的。
-
基本数据的模式是什么? 在你开始清理数据集之前,你需要在目录中创建一个数据模式,其中包含确保数据值使用适当的数据类型和约束进行指定的更改。数据模式指定了数据集的每一列的数据类型。然而,虽然数据类型规范对于模式来帮助确保数据质量是必要的,但它们并不足够。对于每种数据类型,你还应该能够指定它是否是可为空的。在这里,数据类型的可为空性等同于 DDL(数据定义语言)的可为空性,并指示值是否允许缺失。你还应该指定任何进一步限制可能值范围的约束:对于字符串类型,这些可以包括正则表达式,而对于整数类型,这些可以使用区间范围来指定。关于有效数据的部分使用实际示例说明了约束。
在上一章中,你使用了一个爬虫和一个数据目录来发现并存储 DC 出租车数据集的发现数据模式。目录中的模式类似于描述数据类型(如整数、浮点数、时间戳等)的 DDL 模式(SQL 标准的一部分)。请记住,发现的模式可能是正确的模式,也可能不是正确的模式。
那么什么是正确的模式呢?更准确地说,模式由适用于数据集的值的数据类型组成,这意味着什么?就像 DDL 模式一样,选择适当的数据类型是一种权衡考虑。一方面,模式应该使用足够通用的数据类型,以便保留数据值而不丢失信息。另一方面,数据类型应该支持数据值的预期操作(无需类型转换),同时高效利用存储空间。例如,DC 出租车数据集中的纬度和经度坐标应该在模式中指定为浮点数值(DOUBLE 数据类型),而不是 Unicode 字符串,以便坐标值可以用于距离计算。
3.2.3 数据质量的规范性原则
本节介绍了结构化数据质量的**有效、准确、一致、统一和完整模型(VACUUM)**背后的原则,以及作为案例研究的教训。这些原则是规范性的,意味着它们定义了数据质量应该是什么样子,而不是规定数据质量处理的具体步骤或代码实现。这些原则的价值在于通过充分且严谨地定义,为符合 VACUUM 标准的数据提供足够“干净”且能够用于机器学习的准备。
将 VACUUM 原则视为数据质量的一份指南、标准或度量的清单,作为机器学习项目的一部分进行探索。要记住,医生和飞行员(以及许多其他专业人士)使用清单,但拥有清单并不会使您成为飞行员或医生。如果您计划在数据质量方面开发专业技能,您需要培养数据清理的技能。一旦您具备了正确的经验,清单可以帮助您复习并确保不会错过重要的数据质量方面。
有效
2020 年 1 月 31 日,英国脱离了欧盟。那么,一个欧盟数据仓库是否应该将字符串值“United Kingdom”作为列名中的有效值存储起来?
您可以争辩说,从 2020 年 2 月 1 日开始,“United Kingdom”不应再是提到欧盟成员国的任何列中的有效数据值。然而,这种方法是适得其反的:排除“United Kingdom”作为有效值集合的一部分意味着与该列相关的任何历史数据(换句话说,任何日期早于 2020 年 2 月 1 日的记录)都与无效的值相关联。如果数据集中的某个值在其存在的任何时间点都是有效的,那么它应该保持有效。
注意 该定义没有指明是否多个列的多个有效值的组合是有效的;这个问题将在准确性部分的即将到来的章节中解决。
更准确地说,列中的数据值如果满足以下条件,则为有效:
-
与模式指定的列数据类型匹配。对于数据值而言,要有效必须与模式指定的数据类型匹配。模式中基于 SQL 的数据类型定义可能包括以下内容:
-
INTEGER(例如,存储电梯楼层编号的列)
-
DOUBLE(例如,点击网站上的订阅按钮的用户百分比)
-
TIMESTAMP(例如,网站上下订单的时间)
-
BOOLEAN(例如,出租车行程是否在机场结束)
-
STRING(例如,在调查的评论框中留下的评论文本)
-
-
匹配一个或多个以下约束:
-
可空性 —此约束适用于任何数据类型,并指定数据列中的值是否允许具有 NULL 值。例如,在驾驶执照数据库中存储出生日期的 TIMESTAMP 数据值必须是非可空的(即,不应允许具有 NULL 值),而客户配置文件网页上的用户 Twitter 用户名可以指定为可空,以处理用户名未知或未指定的情况。可空数据类型还可以包括 INTEGER(例如,乘客对出租车行程的评分,评分范围为 1—5,NULL 值表示没有评分)和其他数据类型。
-
枚举 —此约束适用于任何数据类型,并指定了数据类型的验证集、字典或有效值的枚举。对于 STRING 值,枚举可能包括美国州名或纽约市区域的主要机场名称,如 LGA、JFK、EWR。模式的枚举约束可以为电话号码数据集中的国家代码列指定 INTEGER 数据类型,并使用有效国家电话代码的枚举。请从本节开头的示例中回忆,枚举必须包括对该列曾经有效的所有值。因此,在 2020 年 2 月 1 日之前的任何数据集中,存储 EU 国家名称的数据列中,英国是一个有效值,而不管英国于 2020 年 1 月 31 日离开欧盟的事实如何。
-
范围 —此约束是数据类型特定的,可以是以下类型之一:
-
间隔约束 用于数字或日期/时间数据类型。作为有效整数数据值的示例,考虑一个用于摩天大楼中单个电梯的活动日志的数据集。数据集中的一个数据列存储电梯停靠的楼层数。由于这个假想摩天大楼中并非所有楼层都可由电梯到达,并且由于迷信原因编号系统跳过了第 13 层,因此可能值的约束包括从—3 到—1 的间隔表示停车场,以及从 1 到 12 和 14 到 42。这个间隔的典型表示法是 [[—3, —1] 或 (0, 12] 或 [14,42]],其中方括号表示值包含在间隔中,而圆括号表示间隔不包括与括号相邻的值。在这种情况下,“或”关键字表示集合并操作(换句话说,逻辑或)。
-
在使用 DOUBLE 和其他浮点数据类型时采用类似的方法。例如,可以使用区间范围约束指定概率值为 0.0 到 1.0,[0.0, 1.0]。
-
时间戳数据类型常见的间隔用于描述日期/时间范围,例如工作日、周末或假日(例如,日期:[2020-01-01 00:00:00, 2021-01-01 00:00:00])。
-
正则表达式约束 用于字符串数据类型的情况下,用于指定有效值的范围。例如,在存储社交媒体影响者 Twitter 账号的数据库中,正则表达式可以指定任何匹配 /^@[a-zA-Z0-9_]{1,15}$/ 的值是有效的。请注意,正则表达式也适用于许多看起来是数值的数据列;例如,IP 地址主要由数字组成,但通常存储为字符串。
-
-
规则 ——此约束适用于任何数据类型,并指定计算条件以确定值是否有效。例如,如果你曾经在网站上使用“保存我的付款以供以后使用”的功能,以允许符合 PCI-DSS 标准的⁵供应商存储你的信用卡号,你应该知道信用卡号的规则约束是基于 Luhn 算法的⁶,该算法计算出奇偶校验位以确保信用卡号有效。
-
到目前为止,你已经看到了指定条件和举例说明数据集中单个值有效或无效的含义。然而,很容易举出一个由完全有效值组成但存在明显数据质量问题的记录示例。以下是来自假设数据集的一个虚构记录,其中列出了带有大陆和国家信息的位置:
大陆 | 国家 | 纬度 | 经度 |
---|---|---|---|
南美洲 | 美国 | 38.91 | –77.03 |
所有值,包括南美洲、美国以及纬度/经度坐标,都对应着各自列的有效值。回想一下来自 VACUUM 的有效原则侧重于数据质量问题和单个值内的验证检查。要解决此示例中的数据质量问题,您需要了解准确性原则。
准确
当您了解有效数据时,您看到了有关欧盟成员国的记录数据集的示例。作为示例的一部分,您看到英国是欧盟国家列的有效值。假设您正在处理一个包含两列的数据记录:第一列是入会日期/时间,第二列是国家名称:
记录日期 | 成员国 |
---|---|
2020-01-31 | 英国 |
2020-02-01 | 英国 |
虽然示例中的所有值都是有效的,但是如果不使用外部(数据记录之外的)参考数据源,就不可能断言第二行是垃圾。参考数据应该能够处理整个记录中的值,并指示记录是否(或在何种程度上)不准确。
更准确地说,如果记录中的所有数据值都是有效的,并且记录中的值的组合与参考数据源一致,那么数据记录就是准确的。举个例子,考虑一个大学校友数据库,其中包括校友入学日期和毕业日期。检查数据库的准确性需要参考外部的真实数据源,例如招生数据库和成绩单数据库。在财务记录中,准确性问题可能是由信用卡号和 PIN 码之间的不匹配引起的。有时准确性问题是由于错误地连接多个表,例如,一条数据记录声称电影 泰坦尼克号 是由盖·里奇在 1920 年制作的。
对于诸如域名等值的准确性保证是一项特别困难的任务,因为参考数据源、域名注册和 DNS 数据库随着时间的推移而发生变化。例如,如果您尝试创建一个电子邮件邮寄列表,并使用正则表达式检查电子邮件的域名部分,那么列表中的数据可能是有效的,但从某种意义上来说并不准确,因为其中一些电子邮件没有映射到有效的域名。您可以尝试向邮寄列表中的地址发送电子邮件,以确认域名和电子邮件是否解析到准确的地址。甚至在发送电子邮件之前,您可能会尝试执行 DNS 查询来验证域名的准确性。
在英国退出欧盟的例子中,改善数据集中数据的质量意味着参考数据源必须存在于欧盟成员国开始和结束日期的时间戳的主记录中。然而,对于许多组织来说,参考数据源的挑战并不是它们太少,而是它们太多。下一节关于一致性将用更多的例子说明这个问题。
一致性
2005 年 1 月,印第安纳州波特郡约有 3 万名居民的小镇瓦尔帕莱索的一位小房屋业主收到通知,他房屋的年度税收评估价值被设定为 4 亿美元。这份通知还包括要求交纳 800 万美元税款的要求,对这座普通房屋的所有者来说,这是一个惊喜,因为就在前一年,税款金额仅为 1500 美元。尽管数据准确性的问题很快得到解决,但故事并没有就此结束。
瓦尔帕莱索的数据系统没有遵循数据质量一致性原则,因此原始的数据准确性问题传播到了该镇的预算系统中。这个小镇的预算假设了 800 万美元的税款,因此该镇不得不从学校、图书馆和其他预算资助单位中收回 310 万美元。那一年,波特郡有很多不满的学生和家长,因为学校不得不填补 20 万美元的预算缺口。
一致性问题在不同的数据孤岛(数据库、数据存储或数据系统)中使用不同和冲突的验证和准确性实现时会出现:虽然每个单独的孤岛可以根据孤岛特定的定义集合有效和准确,但实现一致性意味着在将跨越不同技术和组织边界的系统中的数据集成之前,确保有效和准确的数据的一个共同标准。
例如,英国在 2020 年 1 月 31 日晚上 11:30 是否是欧盟成员国?如果你对数据质量不够谨慎,答案可能取决于你的数据集。在英国的数据集中,你可以期待一条有效和准确的记录,显示英国在 2020 年 1 月 31 日晚上 11:30 不是欧盟成员国。然而,在欧盟的数据集中,相同的日期、时间和国家名称值的组合是一个欧盟成员国的准确记录。正如你可能已经猜到的那样,不一致是由于在不同的数据集中存储日期和时间值的假设不同。这个例子中的英国数据集使用格林尼治平均时区,而欧盟数据集使用中欧时间。
即使在单个数据集或数据隔离中连接表时,确保验证和准确性规则的一致性也很重要。典型的问题出现在使用电话号码和电子邮件作为用户的唯一标识符时:由于电话号码和电子邮件可能更换所有者,基于这些信息连接表可能会导致问题。另一个例子可能包括存储其他标识符的不同方法,比如电话号码。有些可能用国家代码唯一标识,而其他可能不是。这可能非常简单,比如在不同系统中使用不同的主键来标识同一个人,可能创建一个新的主键来连接,或者可能更微妙。例如,有些系统可能使用 5+4 的邮政编码,其他系统可能为每个个体使用一个五位数的邮政编码。
统一
火星气候轨道飞行器是一项耗资 1.25 亿美元的火星无人空间探测器,由 NASA 于 1998 年发射到火星。不到 12 个月后,在进行轨道变化机动时,它滑离了火星的大气层,从此销声匿迹。原因很简单:轨道飞行器的设计者集成了两个独立开发的系统,一个使用美国习惯(英制)度量单位,另一个基于国际单位制(公制)单位。因此,非统一的表被连结在一起(连接记录),数据集中出现非统一的记录。由于轨道飞行器使用的数据测量值在多个数据记录中不统一,NASA 浪费了 1.25 亿美元的预算。
一致性和统一性的区别微妙但重要。正如火星轨道飞行器的例子所示,确保跨数据隔离的验证和准确性规则的一致性是不足以解决数据质量问题的。统一原则规定数据集中的每一列,所有记录都应该使用相同(统一)的测量系统记录的数据。不再使用 NASA 的例子,考虑一个更贴近生活的场景,即创建用来分析用户对不同视频流媒体服务的满意度的数据集。
假设某些流媒体服务在每次观看后,提示用户对内容满意度进行 0—4 星的评分。其他服务可能使用 0 来表示 1—4 星的范围内没有回应。尽管两者的有效值规则相同,为了确保数据质量,仅仅指定客户满意度应该是一个[0, 4]的 DOUBLE 值,并一致应用于视频流媒体服务的数据隔离中是不够的。例如,如果每个服务的平均满意度分数按照每日记录并连接以准备聚合平均分数,则结果在数据集中的行之间不是统一的。特别是,使用 0 值表示没有回应的服务将在分析中受到处罚。
数据集的统一问题往往在数据集的生命周期中出现。考虑一个强制执行商店货架编码系统的杂货连锁店,在这个系统中,所有的商店都有标号为 1-8 的货架,每个货架对应一个产品类别,比如 1 代表奶制品,2 代表肉类,3 代表冷冻食品,以此类推。一旦有一个商店违反了货架编码系统,比如把奶制品编码为 2 而不是 1,整个杂货连锁店的统一性就被破坏了。
统一
在 1912 年的一本书中,有影响力的逻辑学家和哲学家贝特兰·罗素用一个故事来阐述归纳推理的问题,这是机器学习背后的一个基本原则。对罗素的话进行改写,以下是这个寓言的概括:
在 12 月 1 日,一个火鸡在美国出生了。它不是普通的火鸡。有人说它是有史以来最聪明的火鸡。这只天才火鸡很快就摸清了夜空的规律和太阳在投射阴影中的作用,并意识到每天早上 7 点它都会被喂食。它推理出食物对健康至关重要,因此开始思考是否值得策划一次逃亡,冒着饥饿和死亡的风险,还是继续作为一个受到良好喂养的囚徒。天才火鸡重新发明了统计学,收集数据,并逐渐增加信心,无论太阳、月亮、星星、温度、降水和其他因素的位置如何,它每天早上 7 点都会被喂食。可悲的是,在感恩节的早晨,食物没有来,天才火鸡的头落在了砧板上。
这个故事(主要是生动而不是悲惨)是为了帮助你记住,无论你创建多么复杂的机器学习模型,它们只不过是罗素火鸡的数字化版本。它们的成功完全取决于它们利用的可用数据的能力。相比之下,作为一个机器学习从业者,你可以通过好奇心、因果推理和演绎推理让你的机器学习项目更加成功:通过发现对项目使用案例来说是新颖且相关的事实和数据集,将发现的信息与手头的数据集合并,并扩大用于训练模型的相关训练数据的范围。你还可以通过发现和解决训练数据集中潜在的非明显系统性偏差的可能来源,将项目运行环境的文化价值观与数据集的内容统一和调整,以最大限度地减少机器学习项目成功的风险。
虽然你可以依靠机器学习模型进行有效的归纳推理,但是你有责任来执行统一原则,也就是说,你的数据集:
-
是否有一个单一的位置存放与你的项目的机器学习用例相关的数据
-
将您的用例使用的标准与用于机器学习模型训练的数据内容对齐,以实现无偏的数据驱动决策制定。
-
取决于用于机器学习模型训练的数据和用于已经训练的机器学习模型一起使用的数据的共同数据质量过程。
统一的原则是 VACUUM 的一部分,提醒您数据质量是一项旅程,而不是目的地。
3.3 将 VACUUM 应用于 DC 出租车数据
现在您已经了解了 VACUUM 数据质量原则,是时候将这些原则应用于 DC 出租车数据集了。在本节中,您将从单个数据表开始,并专注于如何实现数据质量查询,以确保数据集是有效,准确和统一的。
3.3.1 强制执行模式以确保有效值
本节介绍了您可以对 DC 出租车数据集执行的 SQL 语句,以检查无效值并将其排除在进一步分析之外。
表 3.2 中显示的模式与您在第二章中首次遇到的版本相匹配。模式使用 SQL 类型指定出租车费用估计服务接口所需的数据类型。在本书的后续章节中,当您开始从 DC 出租车数据中训练机器学习模型时,训练数据集中的 NULL 值可能会造成问题(考虑要求机器学习模型为 NULL 取车位置估算出租车费用!)。因此,该模式旨在确保不允许任何数据值为空。
表 3.2 出租车费用估计服务界面的模式和示例值
输入 |
---|
名称 |
取车位置纬度 |
取车位置经度 |
下车位置纬度 |
下车位置经度 |
旅行的预期开始时间 |
输出 |
名称 |
预估费用(美元) |
让我们通过使用以下查询语句从您的 shell 中找出 origindatetime_tr 列中 NULL 值的时间戳数,假设您执行了清单 3.4 中的 Python awscli_to_df()函数,并使用 pandas 输出查询结果:
source utils.sh ; athena_query_to_pandas "
SELECT
(SELECT COUNT(*) FROM dc_taxi_db.dc_taxi_parquet) AS total,
COUNT(*) AS null_origindate_time_total
FROM
dc_taxi_db.dc_taxi_parquet
WHERE
origindatetime_tr IS NULL
"
这将导致以下结果:
总数 | null_origindate_time_total |
---|---|
67435888 | 14262196 |
为了简洁起见,即将到来的代码片段将不再提醒您运行 source utils.sh;athena_query_to_pandas 或 awscli_to_df()。
请记住,SQL COUNT(*)函数⁹返回 NULL 和非 NULL 值的计数。但是,由于 SQL 查询的 WHERE 子句将输出限制为 origindatetime_tr 为 NULL 的行,因此 SQL 查询的输出报告了在整个数据集的 67435888 行中,有 14262196 行为空。
除了确保 origindatetime_tr 值为非 NULL 外,还必须确认值符合有效时间戳值的正则表达式定义。在实践中,这意味着应该可以解析 origindatetime_tr 列的非 NULL 值为时间戳的相关元素,包括年、月、日、小时和分钟。
幸运的是,您不必实现正则表达式解析规则来处理日期/时间。以下 SQL 查询计算数据集中行数与非 NULL 的 origindatetime_tr 值之间的差异,并且可以使用 SQL DATE_PARSE 函数正确解析,该函数使用 DC 出租车数据集中的 %m/%d/%Y %H:%i 格式:
SELECT
(SELECT COUNT(*) FROM dc_taxi_db.dc_taxi_parquet)
- COUNT(DATE_PARSE(origindatetime_tr, '%m/%d/%Y %H:%i'))
AS origindatetime_not_parsed
FROM
dc_taxi_db.dc_taxi_parquet
WHERE
origindatetime_tr IS NOT NULL;
这导致以下结果:
origindatetime_not_parsed |
---|
14262196 |
由于语句返回的差值也等于 14,262,196,这意味着时间戳的所有非 NULL 值都可以解析。此外,请注意,SQL 语句使用 SQL 子查询来计算数据集中的总行数,包括 NULL 和非 NULL 值,因为子查询不包括 WHERE 子句。外部 SQL 查询的结尾处的 WHERE 子句仅适用于计算 COUNT 函数,该函数计算 DATE_PARSE 函数可以正确解析的值的数量。
让我们继续将验证规则应用于起始点和目的地位置。由于在使用情况中,起始点和目的地位置的纬度和经度坐标是非空的,请看下面展示的验证规则对坐标值的影响。
列表 3.5 位置坐标的缺失频率
SELECT
ROUND(100.0 * COUNT(*) / (SELECT COUNT(*)
FROM dc_taxi_db.dc_taxi_parquet), 2)
AS percentage_null,
(SELECT COUNT(*)
FROM dc_taxi_db.dc_taxi_parquet
WHERE origin_block_longitude_double IS NULL
OR origin_block_latitude_double IS NULL)
AS either_null,
(SELECT COUNT(*)
FROM dc_taxi_db.dc_taxi_parquet
WHERE origin_block_longitude_double IS NULL
AND origin_block_latitude_double IS NULL)
AS both_null
FROM
dc_taxi_db.dc_taxi_parquet
WHERE
origin_block_longitude_double IS NULL
OR origin_block_latitude_double IS NULL
这导致以下结果:
percentage_null | either_null | both_null |
---|---|---|
14.04 | 9469667 | 9469667 |
根据查询结果,在数据集中,原始块纬度和原始块经度成对缺失(即,如果其中一个为 NULL,则另一个也为 NULL)的行数为 9,469,667,大约占数据集的 14.04%。
对目的地坐标的类似分析使用以下 SQL 语句:
SELECT
ROUND(100.0 * COUNT(*) / (SELECT COUNT(*)
FROM dc_taxi_db.dc_taxi_parquet), 2)
AS percentage_null,
(SELECT COUNT(*)
FROM dc_taxi_db.dc_taxi_parquet
WHERE destination_block_longitude_double IS NULL
OR destination_block_latitude_double IS NULL)
AS either_null,
(SELECT COUNT(*)
FROM dc_taxi_db.dc_taxi_parquet
WHERE destination_block_longitude_double IS NULL
AND destination_block_latitude_double IS NULL)
AS both_null
FROM
dc_taxi_db.dc_taxi_parquet
WHERE
destination_block_longitude_double IS NULL
OR destination_block_latitude_double IS NULL
这导致
percentage_null | either_null | both_null |
---|---|---|
19.39 | 13074278 | 13074278 |
这表明有 13,074,278 行的目的地坐标具有 NULL 值,大约占整个数据集的 19.39%。
起始点和目的地坐标的 NULL 值的比例显然非常显著。在缺少值的潜在最坏情况下,您可能会发现 42.4%(即 24.59% + 17.81%)的行的起点或目的地坐标缺失。然而,在数据集中,大部分缺失值是重叠的,这意味着如果起点或目的地任一坐标为 NULL,则另一个坐标也为 NULL。您可以使用以下方法找到缺失坐标的计数和比例:
SELECT
COUNT(*)
AS total,
ROUND(100.0 * COUNT(*) / (SELECT COUNT(*)
FROM dc_taxi_db.dc_taxi_parquet), 2)
AS percent
FROM
dc_taxi_db.dc_taxi_parquet
WHERE
origin_block_latitude_double IS NULL
OR origin_block_longitude_double IS NULL
OR destination_block_latitude_double IS NULL
OR destination_block_longitude_double IS NULL
这导致
total | percent |
---|---|
16578716 | 24.58 |
这显示出数据集中 24.58%,或者 16,578,716 行,没有有效的起点和终点坐标。由于乘车和下车位置是出租车费用估算服务规范的必需部分,让我们将数据质量工作集中在剩下的 75.42%具有可用乘车和下车坐标的行上。
3.3.2 清理无效的费用金额
本节将通过 SQL 语句对 fare_amount 列进行分析,并对列值强制执行验证规则。
填充 dc_taxi_parquet 表的 PySpark 作业对原始数据集执行了一些验证处理。如果您查询 Athena 以获取表的模式,请注意项目所需的值同时存在字符串和双精度类型。同时存在两种类型意味着,在某些情况下,值无法转换为所需的 DOUBLE 类型(例如,无法解析值为双精度数值),原始值将被保留并可用于数据故障排除。
根据第二章中描述的模式规范,每个出租车行程记录必须在车费金额、行程开始时间戳和起点和终点纬度/经度坐标中具有非 NULL 值。让我们从调查 fareamount_double 列包含 NULL 值的实例开始,这是根据模式不允许的。由于 fareamount_string 列是从 STRING 到 DOUBLE 解析失败的车费金额值的信息来源,您可以使用以下 SQL 语句了解更多有关问题值的信息。
列表 3.6fareamount_string 列的解析失败的值
SELECT
fareamount_string,
COUNT(fareamount_string) AS rows,
ROUND(100.0 * COUNT(fareamount_string) /
( SELECT COUNT(*)
FROM dc_taxi_db.dc_taxi_parquet), 2)
AS percent
FROM
dc_taxi_db.dc_taxi_parquet
WHERE
fareamount_double IS NULL
AND fareamount_string IS NOT NULL
GROUP BY
fareamount_string;
得到以下结果:
fareamount_string | 行数 | 百分比 |
---|---|---|
NULL | 334,964 | 0.5 |
列表 3.6 中的 SQL 语句过滤了 fareamount_string 值集合的一组值,仅关注 PySpark 无法解析车费金额的情况,或者更准确地说,fareamount_double(包含解析算法输出的列)的值为 NULL 而 fareamount_string(包含解析算法输入的列)的值不为 NULL 的行。
根据查询的输出,有 334,964 个条目的解析失败。所有这些对应的情况是 fareamount_string 等于’NULL’字符串值的情况。这是一个好消息,因为只有大约 0.5%的数据集受到这个问题的影响,而且没有额外的工作要做:‘NULL’值不能转换为 DOUBLE。如果列表 3.6 的输出发现了一些 DOUBLE 值没有解析成功,因为它们包含额外的字符,比如’$7.86’,那么就需要实现额外的代码来正确解析这样的值为 DOUBLE。
为了继续搜索无效的 fareamount 值,值得探索 fareamount_double 列的一些摘要统计信息。以下 SQL 查询将摘要统计计算移到一个单独的子查询中,使用两个 WITH 子句。请注意,数据特定的查询被打包为一个名为 src 的子查询,并且 stats 子查询引用来自 src 的结果。
列表 3.7 解耦统计查询与数据查询的可重用模式
WITH
src AS (SELECT
fareamount_double AS val
FROM
dc_taxi_db.dc_taxi_parquet),
stats AS
(SELECT
MIN(val) AS min,
APPROX_PERCENTILE(val, 0.25) AS q1,
APPROX_PERCENTILE(val ,0.5) AS q2,
APPROX_PERCENTILE(val, 0.75) AS q3,
AVG(val) AS mean,
STDDEV(val) AS std,
MAX(val) AS max
FROM
src)
SELECT
DISTINCT min, q1, q2, q3, max
FROM
dc_taxi_db.dc_taxi_parquet, stats
以下是结果:
分钟 | 四分位数 1 | 四分位数 2 | 四分位数 3 | 最大值 |
---|---|---|---|---|
–2064.71 | 7.03 | 9.73 | 13.78 | 745557.28 |
根据列表 3.7 中查询输出报告的数据集中的最小值,应清楚地看出,数据集受到了一类无效值的影响:出租车费用不应为负值或低于 3.25 美元。回想一下第二章中对 DC 出租车业务规则的审查,DC 的出租车乘车费用的最低收费为 3.25 美元。让我们找出数据集受影响的百分比:
WITH
src AS (SELECT
COUNT(*) AS total
FROM
dc_taxi_db.dc_taxi_parquet
WHERE
fareamount_double IS NOT NULL)
SELECT
ROUND(100.0 * COUNT(fareamount_double) / MIN(total), 2) AS percent
FROM
dc_taxi_db.dc_taxi_parquet, src
WHERE
fareamount_double < 3.25
AND fareamount_double IS NOT NULL
以下是结果:
百分比 |
---|
0.49 |
输出表明,只有 0.49%的行受到了负值或低于最小阈值的车费值的影响,因此它们可以被分析时轻松忽略。从验证的角度来看,这意味着验证规则的实施应该修改为使用大于或等于 3.25 的值。
3.3.3 提高准确性
在本节中,让我们通过将它们与行程里程值的参考数据源进行比较,来更仔细地查看 NULL 值的准确性。正如您在上一节中学到的,DC 出租车数据集中的 NULL 值仅占 0.5%。在 mileage_double 列中使用参考数据可以帮助您更好地了解行程里程导致 NULL 车费金额的情况。
列表 3.8 里程 _double 值的摘要统计
SELECT
fareamount_string,
ROUND( MIN(mileage_double), 2) AS min,
ROUND( APPROX_PERCENTILE(mileage_double, 0.25), 2) AS q1,
ROUND( APPROX_PERCENTILE(mileage_double ,0.5), 2) AS q2,
ROUND( APPROX_PERCENTILE(mileage_double, 0.75), 2) AS q3,
ROUND( MAX(mileage_double), 2) AS max
FROM
dc_taxi_db.dc_taxi_parquet
WHERE
fareamount_string LIKE 'NULL'
GROUP BY
fareamount_string
以下是结果:
fareamount_string | 最小值 | 四分位数 1 | 四分位数 2 | 四分位数 3 | 最大值 |
---|---|---|---|---|---|
NULL | 0.0 | 0.0 | 1.49 | 4.79 | 2591.82 |
列表 3.8 中的 SQL 语句仅报告了里程列的摘要统计信息(包括最小值、最大值和四分位值),仅适用于 fareamount_string 解析失败的情况,或者更具体地说,它等于’NULL’的情况。查询的输出表明,超过四分之一的情况(下四分位数,从最小值到第 25 百分位数的范围)对应于 0 英里的行程。至少四分之一的里程值(位于中间和上四分位数之间,包括 50 和 75 百分位数的范围)似乎在 DC 出租车的合理里程范围内。
此时,你可以考虑进行几项数据增强实验,试图通过从里程列计算车费的估算值来恢复丢失的 fareamount_double 数据值。这些实验可以使用估算值替换丢失的车费金额。例如,你可以将里程处于中四分位数范围内的缺失车费金额值替换为相同范围内已知车费金额的算术平均值(平均值)。也可以使用更复杂的估算器,包括机器学习模型。
然而,由于列表 3.8 中的输出表明,它将帮助解决数据集约 0.12%(= 0.25 * 0.49%)的问题,因此这些实验不太可能对车费估算模型的整体性能产生显著影响。
根据列表 3.7 中查询的输出,车费金额的最大值似乎是一个垃圾数据点。然而,从数据模式的角度来看,它是有效的,因为 745,557.28 小于 SQL DOUBLE 数据类型的最大值。
解决车费金额值上限问题需要应用准确性规则。请回忆,验证检查应该在没有外部数据源的参考情况下进行。
在华盛顿特区出租车数据集的情况下,最大车费金额未明确规定为一项业务规则。然而,通过一些常识推理和华盛顿特区出租车数据集之外的参考数据,你可以得出一些合理的最大车费金额上限:
-
估算 1。最大车费金额取决于出租车司机每个工作班次行驶的里程。一个快速的互联网搜索告诉我们,一名华盛顿特区出租车司机每 24 小时至少需要休息 8 小时。因此,假设性地,司机可能连续驾驶最长 16 小时。根据华盛顿特区、马里兰州和弗吉尼亚州的网站,这些地区的最高速度限制为 70 英里/小时。即使在司机以最高限速连续驾驶 16 小时的荒谬情况下,这段时间内的最大行驶里程也仅为 1,120 英里。
-
显然,一趟里程为 1,120 英里的出租车行程,估计车费为**$2,422.45**(1,120 英里 * $2.16/英里 + $3.25 基本车费),是一个不可能实现的上限,不会转化为准确的华盛顿特区出租车车费金额。然而,与其将此估算结果丢弃,正确的做法是加以考虑,并通过与更多估算结果的汇总来完善它。
-
估算 2。与其专注于行驶距离,你也可以根据时间来估算最大车费金额。考虑到华盛顿特区出租车车费规定,一辆出租车每小时可以收取$35。由于出租车司机被允许工作的最长时间为 16 小时,你可以计算出另一个与距离无关的、车费金额的上限估算值为$560 = 16 小时 * $35/小时。
-
估算 3。出租车费用的上限也可以基于数据集中两个最远角落之间的行程距离。第二章所述的 DC 出租车数据集边界大致是个以市中心为中心的正方形。使用以下查询可以找出正方形上左下角和右上角点的位置:
-
SELECT MIN(lat) AS lower_left_latitude, MIN(lon) AS lower_left_longitude, MAX(lat) AS upper_right_latitude, MAX(lon) AS upper_right_longitude FROM ( SELECT MIN(origin_block_latitude_double) AS lat, MIN(origin_block_longitude_double) AS lon FROM "dc_taxi_db"."dc_taxi_parquet" UNION SELECT MIN(destination_block_latitude_double) AS lat, MIN(destination_block_longitude_double) AS lon FROM "dc_taxi_db"."dc_taxi_parquet" UNION SELECT MAX(origin_block_latitude_double) AS lat, MAX(origin_block_longitude_double) AS lon FROM "dc_taxi_db"."dc_taxi_parquet" UNION SELECT MAX(destination_block_latitude_double) AS lat, MAX(destination_block_longitude_double) AS lon FROM "dc_taxi_db"."dc_taxi_parquet" )
-
得出以下结果:
lower_left_latitude lower_left_longitude upper_right_latitude upper_right_longitude 38.81138 –77.113633 38.994909 –76.910012 将查询报告的纬度和经度坐标插入 OpenStreetMap (
mng.bz/zEOZ
) 中,可得到 21.13 英里总行程,或者 $48.89 (21.13 X $2.16/英里 + $3.25) 的估算费用。 -
估算 4。对于另一种估算技术,根据统计学中的中心极限定理(CLT),随机抽样¹⁰所得到的车费值的算术平均数的总和(因此也是平均数)符合高斯(钟形)分布。根据 SQL 语句,你可以从数据中生成一千个的出租车里程的算术平均数样本(以后可以计算它们的平均数)。
列表 3.9 2015 年至 2019 年 DC 出租车行程数据集的解压文件。
WITH dc_taxi AS
(SELECT *,
origindatetime_tr
|| fareamount_string
|| origin_block_latitude_string
|| origin_block_longitude_string
|| destination_block_latitude_string
|| destination_block_longitude_string
|| mileage_string AS objectid
FROM "dc_taxi_db"."dc_taxi_parquet"
WHERE fareamount_double >= 3.25
AND fareamount_double IS NOT NULL
AND mileage_double > 0 )
SELECT AVG(mileage_double) AS average_mileage
FROM dc_taxi
WHERE objectid IS NOT NULL
GROUP BY
➥ MOD( ABS( from_big_endian_64( xxhash64( to_utf8( objectid ) ) ) ), 1000)
注意 GROUP BY 版本语句的 GROUP BY 部分中的复杂逻辑。数据集中的 objectid 列包含每个数据行的唯一标识符,用顺序排列的整数值表示。你可以使用 GROUP BY MOD(CAST(objectid AS INTEGER), 1000) 子句来替代列表 3.9 版本。然而,如果 objectid 值是基于数据集出租车行程的原始顺序排序,则每个结果样本都包含数据集中相距 1,000 行的里程值。这种有序的间隔结构抽样可能会在计算中引入意外偏差。例如,如果华盛顿特区每小时大约有 1,000 辆出租车,而开往纽约市的火车车站每小时在整点留下的出租车可能会占据一些样本。其他定期间隔样本可能包含太多日终出租车行程。
基于偏置问题而进行正常间隔抽样的随机抽样(基于计算中使用的伪随机值)可以解决。然而,在用如下 GROUP BY 子句将值分组时,使用伪随机数生成器有几个不足之处:
GROUP BY MOD(ABS(RAND()), 1000)
首先,由于随机数生成器不能保证确定性行为,所以无法准确地复制抽样结果。不能指定一个随机数种子,以在 SQL 语句的多个执行之间保证相同的伪随机值序列。
其次,即使您尝试为数据集中的每一行预先计算伪随机标识符,并将行与标识符一起保存到一个单独的表中以供将来重复使用,该表很快也会变得过时。例如,如果 DC 出租车数据集扩展到包括 2020 年的出租车行程,随后对数据进行的 Glue 爬行器索引将使源数据表失效,并迫使重新创建新的伪随机标识符。
相比之下,清单 3.9 中使用的方法以及在此处显示的方法具有数据集的伪随机洗牌的优点,消除了不必要的偏见,并且在数据集添加时无论查询如何都会产生相同的结果,只要可以唯一标识数据的每一行:
GROUP BY MOD(ABS(from_big_endian_64(xxhash64(to_utf8(objectid)))), 1000)
在 SQL 语句中,对 objectid 应用函数起到唯一标识符的作用。xxhash64 哈希函数和 from_big_endian_64 的组合实际上产生了一个伪随机但确定性的值。
作为对清单 3.9 中生成的车费金额样本的平均值近似于高斯分布的可视确认,图 3.3 中的以下直方图是基于清单中对伪随机数种子值的任意选择而绘制的。
图 3.3 显示清单 3.9 中的随机抽样依赖于 CLT 进行估算。
回顾一下,在平均里程列使用 1,000 个随机样本的原始意图是计算样本的平均值。由于在正态分布中大约有 99.99% 的值在距离平均值四个标准差之内,以下 SQL 语句产生了另一个上限估计值的统计估计值,从而得到了车费金额的另一个上限估计值:
WITH dc_taxi AS
(SELECT *,
origindatetime_tr
|| fareamount_string
|| origin_block_latitude_string
|| origin_block_longitude_string
|| destination_block_latitude_string
|| destination_block_longitude_string
|| mileage_string AS objectid
FROM "dc_taxi_db"."dc_taxi_parquet"
WHERE fareamount_double >= 3.25
AND fareamount_double IS NOT NULL
AND mileage_double > 0 ),
dc_taxi_samples AS (
SELECT AVG(mileage_double) AS average_mileage
FROM dc_taxi
WHERE objectid IS NOT NULL
GROUP BY
➥ MOD( ABS( from_big_endian_64( xxhash64( to_utf8( objectid ) ) ) ), 1000)
)
SELECT AVG(average_mileage) + 4 * STDDEV(average_mileage)
FROM dc_taxi_samples
此次执行产生了约 12.138 英里,或大约**$29.47**(12.01 * $2.16/英里 + $3.25)作为另一个上限车费估算。当然,本节中解释的统计方法的优点在于,它可以直接与 fareamount_double 列一起使用,如下 SQL 语句所示:
WITH dc_taxi AS
(SELECT *,
origindatetime_tr
|| fareamount_string
|| origin_block_latitude_string
|| origin_block_longitude_string
|| destination_block_latitude_string
|| destination_block_longitude_string
|| mileage_string AS objectid
FROM "dc_taxi_db"."dc_taxi_parquet"
WHERE fareamount_double >= 3.25
AND fareamount_double IS NOT NULL
AND mileage_double > 0 ),
dc_taxi_samples AS (
SELECT AVG(fareamount_double) AS average_fareamount
FROM dc_taxi
WHERE objectid IS NOT NULL
GROUP BY
➥ MOD( ABS( from_big_endian_64( xxhash64( to_utf8( objectid ) ) ) ), 1000)
)
SELECT AVG(average_fareamount) + 4 * STDDEV(average_fareamount)
FROM dc_taxi_samples
这产生了一个 $15.96 的上限。
尽管您可以继续探索替代的估算方法,但这是一个评估迄今为止车费平均上限的好时机。
使用 Python 中的简单平均实现
means = [15.96, 29.19, 48.89, 560, 2,422.45]
sum(means) / len(means)
179.748
表明出租车车费的估计上限为 $179.75
尽管确实可以继续思考更好的上限估计方法,但让我们估计在使用 $179.75 的上限后还剩下多少数据:
SELECT
100.0 * COUNT(fareamount_double) /
(SELECT COUNT(*)
FROM dc_taxi_db.dc_taxi_parquet
WHERE fareamount_double IS NOT NULL) AS percent
FROM
dc_taxi_db.dc_taxi_parquet
WHERE (fareamount_double < 3.25 OR fareamount_double > 179.75)
AND fareamount_double IS NOT NULL;
这导致以下结果:
百分比 |
---|
0.48841 |
请注意,根据边界,只有约 0.49% 的数据被排除在外。
然而,使用新的边界重新运行 fareamount_double 列的摘要统计信息会产生更有意义的摘要统计信息:
WITH src AS (SELECT fareamount_double AS val
FROM dc_taxi_db.dc_taxi_parquet
WHERE fareamount_double IS NOT NULL
AND fareamount_double >= 3.25
AND fareamount_double <= 180.0),
stats AS
(SELECT
ROUND(MIN(val), 2) AS min,
ROUND(APPROX_PERCENTILE(val, 0.25), 2) AS q1,
ROUND(APPROX_PERCENTILE(val, 0.5), 2) AS q2,
ROUND(APPROX_PERCENTILE(val, 0.75), 2) AS q3,
ROUND(AVG(val), 2) AS mean,
ROUND(STDDEV(val), 2) AS std,
ROUND(MAX(val), 2) AS max
FROM src)
SELECT min, q1, q2, q3, max, mean, std
FROM stats;
这导致以下结果:
最小值 | 四分位数 1 | 四分位数 2 | 四分位数 3 | 最大值 | 平均值 | 标准差 |
---|---|---|---|---|---|---|
3.25 | 7.03 | 9.73 | 13.78 | 179.83 | 11.84 | 8.65 |
现在,已经完成了对 fareamount 列的准确性检查,您应该准备好使用接送坐标重复进行准确性练习了。虽然可能根据值本身确定纬度和经度坐标是否有效,但是您需要一个参考数据源来决定一个值是否准确。用于在第二章生成 DC 出租车地图的 OpenStreetMap 服务也可以用于确认数据集中起始点和目的地坐标的准确性。
使用 SQL 语句和 OpenStreetMap (mng.bz/01ez
) 来检查原始纬度和经度列的最小和最大坐标,确认结果对(38.81138, —77.113633)和(38.994217, —76.910012)在 DC 边界内:
SELECT
MIN(origin_block_latitude_double) AS olat_min,
MIN(origin_block_longitude_double) AS olon_min,
MAX(origin_block_latitude_double) AS olat_max,
MAX(origin_block_longitude_double) AS olon_max,
MIN(destination_block_latitude_double) AS dlat_min,
MIN(destination_block_longitude_double) AS dlon_min,
MAX(destination_block_latitude_double) AS dlat_max,
MAX(destination_block_longitude_double) AS dlon_max,
FROM
dc_taxi_db.dc_taxi_parquet
这将输出以下内容:
olat_min | olon_min | olat_max | olon_max | dlat_min | dlon_min | dlat_max | dlon_max |
---|---|---|---|---|---|---|---|
38.81138 | –77.113633 | 38.994909 | –76.910012 | 38.81138 | –77.113633 | 38.994217 | –76.910012 |
3.4 在 PySpark 作业中实现 VACUUM
在这一节中,您将运用在 DC 出租车数据集中学到的数据质量知识,并将您的发现应用于实现一个 PySpark 作业。该作业的目的是使用 AWS Glue 提供的分布式 Apache Spark 服务器群集执行 dc_taxi_parquet 表的高吞吐量数据清理,该表在第二章中填充。该作业应该实现为一个名为 dctaxi_parquet_vacuum.py 的单个 Python 文件;然而,在这一节中,该文件被拆分成了几个单独的代码片段,这些片段将在接下来的段落中逐一解释。数据集的清理副本将由该作业保存到您 S3 存储桶中的 parquet/vacuum 子文件夹中。
该 PySpark 作业的代码片段的初始部分在列表 3.10 中。请注意,直到❶处的代码行与第二章中的 PySpark 作业中的代码是相同的。这应该不会让人感到惊讶,因为代码的这部分涉及到 PySpark 作业中的先决条件库的导入和常用变量的分配。带有❶注释的代码行是与第二章 PySpark 作业不同的第一个代码行。请注意,该行正在读取您在第二章末尾创建的 Parquet 格式数据集,并在本章中一直在使用 Athena 进行查询。
在 dctaxi_parquet_vacuum.py 中的列表 3.10 中的 PySpark DataFrame 读取代码
import sys
from awsglue.transforms import *
from awsglue.utils import getResolvedOptions
from pyspark.context import SparkContext
from awsglue.context import GlueContext
from awsglue.job import Job
args = getResolvedOptions(sys.argv, ['JOB_NAME',
'BUCKET_SRC_PATH',
'BUCKET_DST_PATH',
])
BUCKET_SRC_PATH = args['BUCKET_SRC_PATH']
BUCKET_DST_PATH = args['BUCKET_DST_PATH']
sc = SparkContext()
glueContext = GlueContext(sc)
logger = glueContext.get_logger()
spark = glueContext.spark_session
job = Job(glueContext)
job.init(args['JOB_NAME'], args)
df = ( spark
.read
.parquet(f"{BUCKET_SRC_PATH}") ) ❶
❶ 将源 Parquet 数据集读入 Spark DataFrame。
为了选择要清理的数据子集,从列表 3.11 中带有❶的行开始调用 Spark DataFramecreateOrReplaceTempView 方法。该方法创建一个名为 dc_taxi_parquet 的临时视图,作为 SparkSession 的一部分,可以通过 spark 变量访问。该视图使 Spark 能够查询在❶处创建的 DataFrame,使用从❷行开始的 SQL 查询,引用 dc_taxi_parquet 视图❸。
从 ❹ 开始的 WHERE 子句的内容不应该令人惊讶。对于 NULL 值的检查和 fareamount_double 列的范围边界检查恰好是在第 3.3 节中定义的条件。
在 ❺ 处调用 replace 方法,将多行字符串中的任何换行符实例替换为空字符。需要使用 replace 方法确保用于指定 PySpark 作业中 SQL 查询的多行字符串与 Spark 使用的 SQL 查询解析器兼容。
列表 3.11 PySpark 数据清理实现保存到 dc_taxi_vacuum.py
df.createOrReplaceTempView("dc_taxi_parquet") ❶
query_df = spark.sql(""" ❷
SELECT
origindatetime_tr,
fareamount_double,
origin_block_latitude,
origin_block_longitude,
destination_block_latitude,
destination_block_longitude
FROM
dc_taxi_parquet ❸
WHERE ❹
origindatetime_tr IS NOT NULL
AND fareamount_double IS NOT NULL
AND fareamount_double >= 3.25
AND fareamount_double <= 180.0
AND origin_block_latitude IS NOT NULL
AND origin_block_longitude IS NOT NULL
AND destination_block_latitude IS NOT NULL
AND destination_block_longitude IS NOT NULL
""".replace('\n', '')) ❺
❶ 将源数据集在 df 中别名为 dc_taxi_parquet,以供 Spark SQL API 使用。
❷ 创建一个基于此片段中 SQL 查询填充的 DataFrame 查询 _df。
❸ 查询 dc_taxi_parquet 以输出干净的值以进行进一步分析。
❹ 根据第 3.3 节中的 VACUUM 分析过滤记录。
❺ 消除 Python 多行字符串中的换行符,以确保与 Spark SQL API 的兼容性。
由于数据集中原始的 STRING 格式列 origindatetime_tr 需要格式化为机器学习的数值,列表 3.12 中的 PySpark DataFrame API 代码首先将该列转换为 SQL TIMESTAMP ❶,消除由于从 STRING 转换为 TIMESTAMP 失败而产生的任何 NULL 值。然后,衍生的列进一步分解为数字、INTEGER 列,包括出租车行程的年、月、星期几(dow)和小时。转换后的最后一步移除了临时的 origindatetime_ts 列,删除了任何缺失数据的记录,并消除了重复记录。
列表 3.12 PySpark 数据清理实现保存到 dc_taxi_vacuum.py
#parse to check for valid value of the original timestamp
from pyspark.sql.functions import col, to_timestamp, \
dayofweek, year, month, hour
from pyspark.sql.types import IntegerType
#convert the source timestamp into numeric data needed for machine learning
query_df = (query_df
.withColumn("origindatetime_ts", \ ❶
to_timestamp("origindatetime_tr", "dd/MM/yyyy HH:mm"))
.where(col("origindatetime_ts").isNotNull())
.drop("origindatetime_tr")
.withColumn( 'year_integer', ❷
year('origindatetime_ts').cast(IntegerType()) )
.withColumn( 'month_integer',
month('origindatetime_ts').cast(IntegerType()) )
.withColumn( 'dow_integer',
dayofweek('origindatetime_ts').cast(IntegerType()) )
.withColumn( 'hour_integer',
hour('origindatetime_ts').cast(IntegerType()) )
.drop('origindatetime_ts') )
#drop missing data and duplicates
query_df = ( query_df ❸
.dropna()
.drop_duplicates() )
❶ 使用 dd/MM/yyyy HH:mm 模式解析行程 origindatetime_tr 时间戳。
❷ 根据行程的年、月、星期几和小时构建数字列。
❸ 消除任何具有缺失或重复数据的记录。
PySpark 作业的结束部分,如列表 3.13 所示,将结果的 PySpark DataFrame 持久化为 Parquet 格式的数据集,保存在由 BUCKET_DST_PATH 参数指定的 AWS S3 位置。请注意,该列表声明了一个 save_stats_metadata 函数,该函数使用 PySpark describe 函数计算清理后数据集的摘要统计信息,并将统计信息保存为位于 AWS S3 位置下名为 .meta/stats 的 S3 子文件夹中的单个 CSV 文件。
列表 3.13 PySpark 数据清理实现保存到 dc_taxi_vacuum.py
(query_df
.write
.parquet(f"{BUCKET_DST_PATH}", mode="overwrite")) ❶
def save_stats_metadata(df, dest, header = 'true', mode = 'overwrite'):
return (df.describe()
.coalesce(1)
.write
.option("header", header)
.csv( dest, mode = mode ) )
save_stats_metadata(query_df,
f"{BUCKET_DST_PATH}/.meta/stats") ❷
job.commit()
❶ 将清理后的数据集持久化到 Parquet 格式的 BUCKET_DST_PATH。
❷ 将关于清理后数据集的元数据保存为单独的 CSV 文件。
为方便起见,显示了整个 PySpark 作业的描述。在执行此作业之前,请确保将代码列表的内容保存到名为 dc_taxi_vacuum.py 的文件中。
列表 3.14 PySpark 数据清理代码保存到 dc_taxi_vacuum.py
import sys
from awsglue.transforms import *
from awsglue.utils import getResolvedOptions
from pyspark.context import SparkContext
from awsglue.context import GlueContext
from awsglue.job import Job
args = getResolvedOptions(sys.argv, ['JOB_NAME',
'BUCKET_SRC_PATH',
'BUCKET_DST_PATH',
])
BUCKET_SRC_PATH = args['BUCKET_SRC_PATH']
BUCKET_DST_PATH = args['BUCKET_DST_PATH']
sc = SparkContext()
glueContext = GlueContext(sc)
logger = glueContext.get_logger()
spark = glueContext.spark_session
job = Job(glueContext)
job.init(args['JOB_NAME'], args)
df = ( spark
.read
.parquet(f"{BUCKET_SRC_PATH}") )
df.createOrReplaceTempView("dc_taxi_parquet")
query_df = spark.sql("""
SELECT
fareamount_double,
origindatetime_tr,
origin_block_latitude_double,
origin_block_longitude_double,
destination_block_latitude_double,
destination_block_longitude_double
FROM
dc_taxi_parquet
WHERE
origindatetime_tr IS NOT NULL
AND fareamount_double IS NOT NULL
AND fareamount_double >= 3.25
AND fareamount_double <= 180.0
AND origin_block_latitude_double IS NOT NULL
AND origin_block_longitude_double IS NOT NULL
AND destination_block_latitude_double IS NOT NULL
AND destination_block_longitude_double IS NOT NULL
""".replace('\n', ''))
#parse to check for valid value of the original timestamp
from pyspark.sql.functions import col, to_timestamp, \
dayofweek, year, month, hour
from pyspark.sql.types import IntegerType
#convert the source timestamp into numeric data needed for machine learning
query_df = (query_df
.withColumn("origindatetime_ts",
to_timestamp("origindatetime_tr", "dd/MM/yyyy HH:mm"))
.where(col("origindatetime_ts").isNotNull())
.drop("origindatetime_tr")
.withColumn( 'year_integer',
year('origindatetime_ts').cast(IntegerType()) )
.withColumn( 'month_integer',
month('origindatetime_ts').cast(IntegerType()) )
.withColumn( 'dow_integer',
dayofweek('origindatetime_ts').cast(IntegerType()) )
.withColumn( 'hour_integer',
hour('origindatetime_ts').cast(IntegerType()) )
.drop('origindatetime_ts') )
#drop missing data and duplicates
query_df = ( query_df
.dropna()
.drop_duplicates() )
(query_df
.write
.parquet(f"{BUCKET_DST_PATH}", mode="overwrite"))
def save_stats_metadata(df, dest, header = 'true', mode = 'overwrite'):
return (df.describe()
.coalesce(1)
.write
.option("header", header)
.csv(dest, mode = mode))
save_stats_metadata(query_df, f"{BUCKET_DST_PATH}/.meta/stats")
job.commit()
在第 3.3 节首次介绍的 utils.sh 脚本文件中包含了简化在 AWS Glue 中从 bash shell 执行 PySpark 作业的 bash 函数。请注意,在列表 3.15 中,列表 3.14 中的 PySpark 作业通过文件名 dctaxi_ parquet_vacuum.py 引用,并用于启动名为 dc-taxi-parquet-vacuum-job 的 AWS Glue 作业。该作业使用您在本章前面分析过的 Parquet 格式的 DC 出租车数据集,将数据的清理版本填充到 AWS S3 存储桶的 parquet/vacuum 子文件夹中。清理后的版本也以 Parquet 格式持久化存储。
列表 3.15 使用 bash 启动 dctaxi_parquet_vacuum.py 中的 PySpark 作业
%%bash
wget -q https://raw.githubusercontent.com/osipov/smlbook/master/utils.sh
source utils.sh
PYSPARK_SRC_NAME=dctaxi_parquet_vacuum.py \
PYSPARK_JOB_NAME=dc-taxi-parquet-vacuum-job \
BUCKET_SRC_PATH=s3://dc-taxi-$BUCKET_ID-$AWS_DEFAULT_REGION/parquet \
BUCKET_DST_PATH=s3://dc-taxi-$BUCKET_ID-$AWS_DEFAULT_REGION/parquet/vacuum \
run_job
假设列表 3.15 中的 PySpark 作业成功完成,您应该会观察到类似以下的输出:
{
"JobName": "dc-taxi-parquet-vacuum-job"
}
{
"Name": "dc-taxi-parquet-vacuum-job"
}
{
"JobRunId":
➥ "jr_8a157e870bb6915eef3b8c0c280d1d8596613f6ad79dd27e3699115b7a3eb55d"
}
Waiting for the job to finish..................SUCCEEDED
总结
-
交互式查询服务如 AWS Athena 可以帮助探索结构化数据集,其大小从几千兆字节到几百兆字节不等。
-
按需架构的方法使得交互式查询服务可以将多个不同的数据模式应用于同一个数据集。
-
与垃圾输入、垃圾输出方法相比,VACUUM 原则可以帮助您的机器学习项目发展成熟的数据质量实践。
-
交互式查询服务,比如基于 PrestoDB 的 AWS Athena 和基于 Apache Spark 的 AWS Glue,可以用来在公共云中实现数据集的 VACUUM 原则。
^(1.)尽管一项 2016 年的调查(mng.bz/Mvr2
)声称数据科学家 60%的时间用于解决数据质量问题,但最近的一项更大规模的调查将估计值降低到了 15%(mng.bz/ g1KR
)。有关这些常被引用的统计数据的更多见解,请查看 mng.bz/ePzJ
。
^(2.)五个 DC 出租车行程样本数据集的 CSV 文件可从 mng.bz/OQrP
获取。
^(3.)示例包括 Google BigQuery:cloud.google.com/bigquery
。
^(4.)2016 年,一篇有影响力的 哈佛商业评论 文章引用了另一项研究,“糟糕的数据每年为美国造成 3 万亿美元的损失”:mng.bz/Yw47
。
^(5.)《支付卡行业数据安全标准》具体规定了存储持卡人数据的要求:www.pcicomplianceguide.org/faq/#1
。
^(6.)Luhn 算法以 IBM 科学家汉斯·彼得·卢恩命名:spectrum.ieee.org/hans-peter-luhn-and-the-birth-of-the-hashing-algorithm
。
^(7.)切斯特顿论坛,印第安纳州的一家日报,发表了有关瓦尔帕莱索惨败的文章: mng.bz/GOAN
。
^(8.)时间戳以月/日/年 时:分 的格式存储为字符串。
^(9.)关于在本章中阅读到的关于 SELECT 的警告不适用于 COUNT(),因为在 SQL 中,这两者是基本不同的操作:前者返回每一行的所有列的值,而后者仅返回行数。
^(10.)样本量预计至少包含几十个记录,并且记录应该是独立的,并带有替换抽样。