Python-现实世界的项目-全-
Python 现实世界的项目(全)
原文:
zh.annas-archive.org/md5/67ac28ee128f2ff9d180d06916e1389b
译者:飞龙
第一章:Python 实战项目
用可部署的应用程序打造你的 Python 作品集
Steven F. Lott
伯明翰—孟买
"Python"和 Python 标志是 Python 软件基金会的商标。
Python 实战项目
版权© 2023 Packt Publishing
版权所有。未经出版者事先书面许可,本书的任何部分不得以任何形式或通过任何手段进行复制、存储在检索系统中或以任何方式传输,但简要引用在评论或评论文章中除外。
在准备本书的过程中,我们已尽最大努力确保所提供信息的准确性。然而,本书中的信息销售时不附带任何明示或暗示的保证。作者、Packt Publishing 及其经销商和分销商不对由此书直接或间接造成的任何损害承担责任。
Packt Publishing 已尽力通过适当使用大写字母提供本书中提到的所有公司和产品的商标信息。但是,Packt Publishing 不能保证此信息的准确性。
产品经理助理: Kunal Sawant
出版产品经理: Akash Sharma
高级编辑: Kinnari Chohan
高级内容开发编辑: Rosal Colaco
技术编辑: Maran Fernandes
文字编辑: Safis Editing
项目助理: Deeksha Thakkar
校对员: Safis Editing
索引员: Pratik Shirodkar
生产设计师: Shyam Sundar Korumilli
业务发展执行: Debadrita Chatterjee
开发者关系市场营销执行: Sonia Chauhan
首次出版:2023 年 9 月
生产参考:1 310823
由 Packt Publishing Ltd. 出版。Grosvenor House 11 St Paul’s Square Birmingham B3 1RB
ISBN 978-1-80324-676-5
贡献者
关于作者
史蒂文·F·洛特自计算机庞大、昂贵且稀有时就开始编程。几十年来从事高科技工作使他接触到了许多想法和技术;其中一些是糟糕的,但大多数对他人都有用且有帮助。
Steven 自 90 年代以来一直在使用 Python,构建了各种工具和应用。他为 Packt Publishing 编写了包括《精通面向对象 Python》、《现代 Python 食谱》和《函数式 Python 编程》在内的多本书籍。
他是一位技术游牧民,通常居住在美国东海岸的一艘船上。他试图按照以下格言生活:“除非你有故事,否则不要回家。”
关于审稿人
克里斯·格里菲斯是一位拥有 12 年 Python 经验的资深软件工程师。他的开源 Python 项目已被下载超过一百万次,他是代码灾难博客的主要撰稿人。克里斯在业余时间喜欢摄影室摄影,以及数字化复古杂志和 8mm 电影。
加入我们的社区 Discord 空间
加入我们的 Python Discord 工作空间,讨论并了解更多关于这本书的信息:
第二章:目录
项目零:其他项目的模板
1.1 关于质量
1.1.1 关于质量的更多阅读
1.2 建议的项目迭代
1.2.1 启动
1.2.2 细化,第一部分:定义完成
1.2.3 细化,第二部分:定义组件和测试
1.2.4 构建
1.2.5 过渡
1.3 交付物清单
1.4 开发工具安装
1.5 项目 0 - 带测试用例的 Hello World
1.5.1 描述
1.5.2 方法
1.5.3 交付物
1.5.4 完成定义
1.6 总结
1.7 附加内容
1.7.1 静态分析 - mypy, flake8
1.7.2 命令行界面功能
1.7.3 日志记录
1.7.4 Cookiecutter
项目概述
2.1 通用数据获取
2.2 通过提取获取
2.3 检查
2.4 清理、验证、标准化和持久化
2.5 总结和分析
2.6 统计建模
2.7 数据合约
2.8 总结
项目 1.1:数据获取基础应用
3.1 描述
3.1.1 用户体验
3.1.2 关于源数据
3.1.3 关于输出数据
3.2 架构方法
3.2.1 类设计
3.2.2 设计原则
3.2.3 功能设计
3.3 交付物
3.3.1 验收测试
3.3.2 附加验收场景
3.3.3 单元测试
3.4 总结
3.5 附加内容
3.5.1 日志增强
3.5.2 配置扩展
3.5.3 数据子集
3.5.4 另一个数据源示例
数据获取功能:Web API 和抓取
4.1 项目 1.2:从网络服务获取数据
4.1.1 描述
4.1.2 方法
4.1.3 交付物
4.2 项目 1.3:从网页抓取数据
4.2.1 描述
4.2.2 关于源数据
4.2.3 方法
4.2.4 交付物
4.3 总结
4.4 附加内容
4.4.1 定位更多 JSON 格式数据
4.4.2 其他要提取的数据集
4.4.3 处理模式变化
4.4.4 命令行界面增强
4.4.5 日志记录
数据获取功能:SQL 数据库
5.1 项目 1.4:本地 SQL 数据库
5.1.1 描述
5.1.2 方法
5.1.3 交付物
5.2 项目 1.5:从 SQL 提取获取数据
5.2.1 描述
5.2.2 对象关系映射(ORM)问题
5.2.3 关于源数据
5.2.4 方法
5.2.5 交付物
5.3 总结
5.4 附加内容
5.4.1 考虑使用其他数据库
5.4.2 考虑使用 NoSQL 数据库
5.4.3 考虑使用 SQLAlchemy 定义 ORM 层
项目 2.1:数据检查笔记本
6.1 描述
6.1.1 关于源数据
6.2 方法
6.2.1 函数的笔记本测试用例
6.2.2 单独模块中的常用代码
6.3 交付物
6.3.1 笔记本.ipynb 文件
6.3.2 执行笔记本的测试套件
6.4 总结
6.5 附加内容
6.5.1 使用 pandas 检查数据
数据检查功能
7.1 项目 2.2:验证基数域——度量、计数和持续时间
7.1.1 描述
7.1.2 方法
7.1.3 交付物
7.2 项目 2.3:验证文本和代码——名义数据和序数
7.2.1 描述
7.2.2 方法
7.2.3 交付物
7.3 项目 2.4:查找参考域
7.3.1 描述
7.3.2 方法
7.3.3 交付物
7.4 总结
7.5 附加内容
7.5.1 带有日期和数据源信息的 Markdown 单元格
7.5.2 演示材料
7.5.3 使用 JupyterBook 或 Quarto 生成更复杂的输出
项目 2.5:模式和元数据
8.1 描述
8.2 方法
8.2.1 定义 Pydantic 类并发出 JSON Schema
8.2.2 在 JSON Schema 表示法中定义预期的数据域
8.2.3 使用 JSON Schema 验证中间文件
8.3 交付成果
8.3.1 模式验收测试
8.3.2 扩展验收测试
8.4 总结
8.5 附加内容
8.5.1 修订所有之前的章节模型以使用 Pydantic
8.5.2 使用 ORM 层
项目 3.1:数据清洗基础应用
9.1 描述
9.1.1 用户体验
9.1.2 源数据
9.1.3 结果数据
9.1.4 转换和处理
9.1.5 错误报告
9.2 方法
9.2.1 模型模块重构
9.2.2 Pydantic V2 验证
9.2.3 验证函数设计
9.2.4 增量设计
9.2.5 命令行界面应用
9.3 交付成果
9.3.1 验收测试
9.3.2 模型功能的单元测试
9.3.3 应用于清理数据和创建 NDJSON 临时文件
9.4 总结
9.5 附加内容
9.5.1 创建包含拒绝样本的输出文件
数据清洗功能
10.1 项目 3.2:验证和转换源字段
10.1.1 描述
10.1.2 方法
10.1.3 交付成果
10.2 项目 3.3:验证文本字段(以及数字编码字段)
10.2.1 描述
10.2.2 方法
10.2.3 交付成果
10.3 项目 3.4:验证不同数据源之间的引用
10.3.1 描述
10.3.2 方法
10.3.3 交付成果
10.4 项目 3.5:将数据标准化为常用代码和范围
10.4.1 描述
10.4.2 方法
10.4.3 交付成果
10.5 项目 3.6:集成以创建获取管道
10.5.1 描述
10.5.2 方法
10.5.3 交付成果
10.6 总结
10.7 附加内容
10.7.1 假设检验
10.7.2 通过过滤拒绝不良数据(而不是记录日志)
10.7.3 不连续子实体
10.7.4 创建扇出清理管道
项目 3.7:临时数据持久化
11.1 描述
11.2 总体方法
11.2.1 设计幂等操作
11.3 交付物
11.3.1 单元测试
11.3.2 验收测试
11.3.3 清理可重运行的应用设计
11.4 总结
11.5 附加内容
11.5.1 使用 SQL 数据库
11.5.2 使用 NoSQL 数据库进行持久化
项目 3.8:集成数据采集 Web 服务
12.1 描述
12.1.1 数据系列资源
12.1.2 创建下载数据
12.2 总体方法
12.2.1 OpenAPI 3 规范
12.2.2 从笔记本查询的 RESTful API
12.2.3 POST 请求开始处理
12.2.4 获取处理状态的 GET 请求
12.2.5 获取结果的 GET 请求
12.2.6 安全考虑
12.3 交付物
12.3.1 验收测试用例
12.3.2 RESTful API 应用
12.3.3 单元测试用例
12.4 总结
12.5 附加内容
12.5.1 在 POST 请求中添加过滤条件
12.5.2 将 OpenAPI 规范分为两部分以使用 $REF 对输出架构进行引用
12.5.3 使用 Celery 而不是 concurrent.futures
12.5.4 直接调用外部处理而不是运行子进程
项目 4.1:可视化分析技术
13.1 描述
13.2 总体方法
13.2.1 通用笔记本组织
13.2.2 用于总结的 Python 模块
13.2.3 PyPlot 图形
13.2.4 迭代和演进
13.3 交付物
13.3.1 单元测试
13.3.2 验收测试
13.4 总结
13.5 附加内容
13.5.1 使用 Seaborn 进行绘图
13.5.2 调整调色板以强调数据的关键点
项目 4.2:创建报告
14.1 描述
14.1.1 幻灯片和演示
14.1.2 报告
14.2 整体方法
14.2.1 准备幻灯片
14.2.2 准备报告
14.2.3 创建技术图表
14.3 交付物
14.4 摘要
14.5 附加内容
14.5.1 带有 UML 图表的书面报告
项目 5.1:建模基础应用
15.1 描述
15.2 方法
15.2.1 设计摘要应用
15.2.2 描述分布
15.2.3 使用清洗后的数据模型
15.2.4 重新思考数据检查函数
15.2.5 创建新的结果模型
15.3 交付物
15.3.1 验收测试
15.3.2 单元测试
15.3.3 应用次要特征
15.4 摘要
15.5 附加内容
15.5.1 形状度量
15.5.2 创建 PDF 报告
15.5.3 从数据 API 提供 HTML 报告
项目 5.2:简单多元统计
16.1 描述
16.1.1 相关系数
16.1.2 线性回归
16.1.3 图表
16.2 方法
16.2.1 统计计算
16.2.2 分析图表
16.2.3 在最终文档中包含图表
16.3 交付物
16.3.1 验收测试
16.3.2 单元测试
16.4 摘要
16.5 附加内容
16.5.1 使用 pandas 计算基本统计
16.5.2 使用 pandas 的 dask 版本
16.5.3 使用 numpy 进行统计
16.5.4 使用 scikit-learn 进行建模
16.5.5 使用函数式编程计算相关性和回归
下一步
17.1 整体数据处理
17.2 “决策支持”的概念
17.3 元数据和来源的概念
17.4 迈向机器学习的下一步
前言
我们如何提高我们对 Python 的了解?也许一个更重要的问题是“我们如何向他人展示我们用 Python 编写软件的能力有多强?”
这两个问题的答案是一样的。我们通过完成项目来构建我们的技能并展示这些技能。更具体地说,我们需要完成一些符合专业发展广泛接受标准的项目。要被视为专业人士,我们需要超越学徒级练习,并展示我们无需大师工匠的指导也能工作的能力。
我把它想象成第一次独自驾船,船上没有更有经验的船长或教师。我想象成完成一双可以一直穿到袜子完全磨损、无法再修补的手织袜子。
完成项目包括实现一系列目标。其中最重要的之一是将它发布到公共仓库,如 SourceForge (sourceforge.net
) 或 GitHub (github.com
),以便潜在的雇主、资金来源或商业伙伴可以看到。
我们将区分一个完成项目针对的三个受众:
-
一个个人项目,可能适合工作小组或几个同伴。
-
一个适合在整个企业中使用的项目(例如,企业、组织或政府机构)
-
一个可以发布在 Python 包索引 PyPI (
pypi.org
) 上的项目。
我们在创建 PyPI 包和创建在企业内部可用的包之间划了一条细线。对于 PyPI,软件包必须可以通过PIP工具安装;这通常需要大量的测试来确认包将在最广泛的各种环境中工作。这可能是一个沉重的负担。
对于这本书,我们建议遵循常用于“企业”软件的实践。在企业环境中,创建不通过PIP安装的包通常是可接受的。相反,用户可以通过克隆仓库来安装包。当人们为一个共同的企业工作时,克隆包允许用户提交带有建议更改或错误修复的拉取请求。软件使用的不同环境数量可能非常少。这减少了全面测试的负担;企业软件的潜在用户群体比通过 PyPI 提供的包要小。
这本书面向的对象
这本书是为那些希望通过完成专业级别 Python 项目来提高技能的资深程序员而写的。它也适用于需要通过展示作品集来展示技能的开发者。
这本书并非旨在作为 Python 教程。本书假设读者对语言和标准库有一定了解。对于 Python 的基础介绍,可以考虑阅读《Python 编程学习指南,第三版》:www.packtpub.com/product/learn-python-programming-third-edition/9781801815093
。
本书中的项目描述较为概括,需要你自己填充设计细节并完成编程。每一章都会花更多时间在期望的方法和交付成果上,而不是你需要编写的代码。本书将详细说明测试用例和验收标准,让你自由完成通过建议测试的工作示例。
本书涵盖的内容
我们可以将本书分解为五个一般主题:
-
我们将从从来源获取数据开始。前六个项目将涵盖从各种来源获取用于分析处理的数据的项目。
-
一旦我们有了数据,我们通常需要检查和调查。接下来的五个项目将探讨一些检查数据的方法,以确保其可用性,并诊断异常问题、异常值和异常情况。
-
通用分析流程继续到清洗、转换和****标准化。有八个项目解决这些密切相关的问题。
-
有用的结果从展示摘要开始。这里有很多变化,所以我们只提供两个项目想法。在许多情况下,你将希望提供自己独特的解决方案来展示你所收集的数据。
-
本书以两个涵盖统计建模基础的小项目结束。在某些组织中,这可能是更复杂的数据科学和机器学习应用的起点。我们鼓励你继续学习 Python 在数据科学领域的应用。
第一部分有两个预备章节,帮助定义交付成果以及项目的大致范围。第一章,项目 0:其他项目的模板是一个基准项目。其功能是一个“Hello, World!”应用程序。然而,单元测试、验收测试以及使用像tox或nox这样的工具执行测试的附加基础设施是重点。
下一章,第二章,项目概述,展示了本书将遵循的一般方法。这将展示数据从获取到清洗、分析再到报告的流程。本章将“数据分析”这个大问题分解为若干个可以独立解决的问题。
从第三章、项目 1.1:数据采集 基础应用开始的章节序列构建了多个不同的数据采集应用。这个序列从从 CSV 文件中获取数据开始。在第四章、数据采集功能:Web API 和抓取的第一种变化中,探讨了从网页获取数据的方法。
接下来的两个项目合并为第五章、数据采集功能: SQL 数据库。本章构建了一个示例 SQL 数据库,然后从中提取数据。这个示例数据库让我们能够探索企业数据库管理概念,以便更全面地理解与关系数据工作的一些复杂性。
一旦获取了数据,项目就过渡到数据检查。第六章、项目 2.1:数据检查笔记本创建了一个初始检查笔记本。在第七章、数据检查功能中,一系列项目为不同类型的数据添加了基本检查笔记本的功能。
这个主题以第八章、项目 2.5:模式和元数据项目结束,为数据源和获取的数据创建一个正式的模式。使用 JSON Schema 标准,因为它似乎很容易适应企业数据处理。这种模式形式化将成为后续项目的一部分。
第三个主题——清理——从第九章、项目 3.1:数据清理 基础应用开始。这是清理获取数据的基础应用。它引入了Pydantic包作为提供显式数据验证规则的方式。
第十章、数据清理功能有几个项目为核心数据清理应用添加功能。前几章中的许多示例数据集提供了非常干净的数据;这使得本章看起来像是无用的过度设计。如果你提取样本数据然后手动破坏它,以便你有无效和有效数据的示例,这可能会很有帮助。
在第十一章、项目 3.7:临时数据持久化中,我们将探讨保存清理数据以供进一步使用。
获取和清理管道通常被打包成一个网络服务。在第十二章、项目 3.8:集成数据采集网络服务中,我们将创建一个网络服务器,提供后续处理所需的清理数据。围绕长期运行的获取和清理过程的这种网络服务包装提出了许多有趣的设计问题。
下一个主题是数据分析。在第13 章,项目 4.1:可视化分析技术中,我们将探讨如何利用JupyterLab的力量生成报告、图表和图形。
在许多组织中,数据分析可能导致正式的文档或报告,显示结果。这可能有一个广泛的利益相关者和决策者受众。在第14 章,项目 4.2:创建报告中,我们将探讨如何使用JupyterLab笔记本中的计算从原始数据生成优雅的报告。
最后一个主题是统计建模。这始于第十五章,项目 5.1:建模基础应用,创建一个体现检查笔记本和分析笔记本项目中学到的经验的程序。有时我们可以在这些项目之间共享 Python 编程。然而,在其他情况下,我们只能共享学到的经验;随着我们对数据结构的理解不断演变,我们经常改变数据结构并应用其他优化,这使得简单地共享一个函数或类定义变得困难。
在第十六章,项目 5.2:简单多元统计分析中,我们扩展了单变量建模,以添加多元统计分析。这种建模保持简单,以强调基础设计和架构细节。如果你对更高级的统计感兴趣,我们建议构建基本的应用程序项目,使其工作,然后向已经工作的基线项目添加更复杂的建模。
最后一章,第十七章,下一步,提供了一些更复杂应用的指导。在许多情况下,一个项目会从探索发展到监控和维护。模型将继续被验证和改进,这将有一个长长的尾巴。在某些情况下,当模型被替换时,长尾巴才会结束。看到这个长尾巴可以帮助分析师理解在每个阶段投入时间创建稳健、可靠的软件的价值。
关于所需技能的注意事项
这些项目需要广泛的各种技能,包括软件和数据架构、设计、Python 编程、测试设计,甚至文档编写。这种技能的广度反映了作者在企业软件开发方面的经验。开发者被期望是通才,能够跟随技术变化并适应新技术。
在一些早期的章节中,我们将提供一些关于软件设计和构建的指导。这些指导将假设对 Python 有实际的知识。它将指导你查看各种 Python 包的文档以获取更多信息。
我们还将提供一些关于如何最好地构建单元测试和验收测试的细节。这些主题可能具有挑战性,因为测试通常被低估。刚从学校毕业的开发者常常抱怨现代计算机科学教育似乎没有彻底涵盖测试和测试设计。
本书将强调使用pytest进行单元测试和behave进行验收测试。使用behave意味着用 Gherkin 语言编写测试场景。这是cucumber工具使用的语言,有时这种语言也被称为 Cucumber。这可能是一个新概念,我们将通过更详细的示例强调这一点,尤其是在前五章。
一些项目将实现统计算法。我们将使用类似x的符号来表示变量x的均值。有关数据分析的基本统计学信息,请参阅Statistics for Data Science:
www.packtpub.com/product/statistics-for-data-science/9781788290678
为了充分利用这本书
本书假设你对 Python 3 和应用程序开发的一般概念有所了解。由于项目是一个完整的工作单元,它将超出 Python 编程语言的范围。本书将经常挑战你学习更多关于特定 Python 工具和包的知识,包括pytest、mypy、tox以及许多其他工具。
这些项目中的大多数都使用探索性数据分析(EDA)作为问题领域,以展示函数式编程的价值。对基本概率和统计学的了解将有助于这一点。只有少数例子涉及到更深入的数据科学。
预期使用 Python 3.11。出于数据科学的目的,通常使用conda工具来创建和管理虚拟环境是有帮助的。然而,这不是必需的,你应该能够使用任何可用的 Python。
通常使用pip
安装额外的包。命令看起来像这样:
% python -m pip install pytext mypy tox beautifulsoup4
完成额外内容
每一章都包含一些“额外内容”,这些内容可以帮助你扩展章节中的概念。这些额外项目通常探索设计替代方案,并通常引导你为给定问题创建更多、更完整的解决方案。
在许多情况下,额外内容部分甚至需要更多的单元测试用例来确认它们确实解决了问题。将章节的核心测试用例扩展到包括额外功能是一个重要的软件开发技能。
下载示例代码文件
本书代码包托管在 GitHub 上,网址为github.com/PacktPublishing/Python-Real-World-Projects
。我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/
找到。查看它们吧!
使用的约定
本书中使用了多种文本约定。
CodeInText
: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。例如:“Python 有其他语句,例如global
或nonlocal
,这些语句会修改特定命名空间中变量的规则。”
粗体: 表示新术语、重要单词或你在屏幕上看到的单词,例如在菜单或对话框中。例如:“基础情况表明零长度序列的和是 0。递归情况表明序列的和是第一个值加上序列其余部分的和。”
代码块设置为以下格式:
print("Hello, World!")
任何命令行输入或输出都按照以下方式编写:
% conda create -n functional3 python=3.10
警告或重要提示如下所示。
技巧和窍门如下所示。
联系我们
我们始终欢迎读者的反馈。
一般反馈: 请通过反馈邮箱发送邮件,并在邮件主题中提及书籍标题。如果你对本书的任何方面有疑问,请通过问题邮箱与我们联系。
勘误: 尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告,我们将不胜感激。请访问订阅帮助页面,点击提交勘误按钮,搜索您的书籍,并输入详细信息。
盗版: 如果你在互联网上以任何形式遇到我们作品的非法副本,如果你能提供位置地址或网站名称,我们将不胜感激。请通过版权邮箱与我们联系,并提供材料的链接。
如果您有兴趣成为作者:如果您在某个主题上有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问作者页面。
分享你的想法
一旦您阅读了《Python Real-World Projects》,我们很乐意听到您的想法!请点击此处直接进入该书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区都很重要,并将帮助我们确保我们提供高质量的内容。
下载本书的免费 PDF 副本
感谢您购买本书!
你喜欢在路上阅读,但无法携带你的印刷书籍到处走?你的电子书购买是否与你的选择设备不兼容?
不要担心,现在,随着每本 Packt 书籍,你都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何设备上阅读。直接从你喜欢的技术书籍中搜索、复制和粘贴代码到你的应用程序中。
优惠远不止于此,您将获得独家折扣、新闻通讯以及每天收件箱中的优质免费内容
按照以下简单步骤获取这些好处:
-
扫描下面的二维码或访问以下链接
-
提交您的购买证明
-
就这样!我们将直接将您的免费 PDF 和其他优惠发送到您的电子邮件中
第一章
项目零:其他项目的模板
这是一本项目书。为了使每个项目成为一份优秀的投资组合作品,我们将每个项目视为一个企业级软件产品。你可以构建可以发布到公司(或组织)内部存储库的东西。
对于这本书,我们将定义一些标准,这些标准将适用于所有这些项目。这些标准将确定可交付成果为文件、模块、应用程序、笔记本和文档文件的组合。虽然每个企业都是独特的,但这里描述的标准与我在各种企业作为顾问的经验是一致的。
我们想划出一个非正式的界限,以避免发布到 PyPI 网站所需的一些步骤。我们的重点是具有测试用例和足够文档来解释其功能的产品。我们不希望完全创建一个 PyPI 项目。这使我们能够避免构建系统的复杂性以及相关的pyproject.toml
文件。
这些项目不是为了产生通用、可重用的模块。它们是特定于问题域和数据集的应用程序。虽然这些都是特定解决方案,但我们不想阻止任何感到有动力将项目推广为通用和可重用的人。
本章将展示每个项目的总体概述。然后我们将查看可交付成果的集合。本章以项目零结束——一个作为其他项目模板的初始项目。我们将涵盖以下主题:
-
对我们将尝试强调的软件质量原则的概述。
-
完成项目作为一系列项目冲刺的建议方法。
-
每个项目可交付成果列表的一般概述。
-
一些建议的工具。这些不是必需的,一些读者可能有其他选择。
-
作为后续项目模板的样本项目。
我们将从高质量软件的一些特性概述开始。目的是为每个项目的可交付成果建立一些标准。
1.1 关于质量
有一个清晰的期望定义是有帮助的。对于这些期望,我们将依赖 ISO 25010 标准来定义每个项目的质量目标。更多详情,请参阅iso25000.com/index.php/en/iso-25000-standards/iso-25010
。
ISO/IEC 25010:2011 标准描述了系统和软件 质量要求和评估(SQuaRE)。本标准提供了软件的八个特性。这些特性如下:
-
功能性适宜性。它是否做了我们需要的?它是完整的、正确的,并且适合用户表达(和暗示)的需求?这是每个项目描述的重点。
-
性能效率. 它是否工作得快?它是否使用最少的资源?它是否有足够的容量来满足用户的需求?我们不会在这里深入讨论这个问题。我们将讨论编写性能测试和解决性能问题的方法。
-
兼容性. 它能否与其他软件共存?它是否能够正确地与其他应用程序交互?在一定程度上,Python 可以帮助确保应用程序与其他应用程序礼貌地交互。我们将强调我们在选择文件格式和通信协议时的兼容性问题。
-
可用性. 有许多子特性有助于我们理解可用性。本书中的许多项目都专注于命令行界面(CLI),以确保最低限度的可学习性、可操作性、错误保护和可访问性。一些项目将包括网络服务 API,而其他项目将利用 JupyterLab 的 GUI 界面提供交互式处理。
-
可靠性. 当用户需要时它是否可用?我们能否检测和修复问题?我们需要确保我们拥有所有必要的部件和组件,以便我们可以使用该软件。我们还需要确保我们有一套完整的测试来确认它将正常工作。
-
安全性. 就像可用性一样,这是一个深奥的话题。我们将在一个项目中讨论一些安全性的方面。其余的项目将使用命令行界面(CLI),这样我们就可以依赖操作系统的安全模型。
-
可维护性. 我们能否诊断问题?我们能否扩展它?我们将把文档和测试案例视为维护的关键。我们还将利用一些额外的项目文件,以确保我们的项目可以被他人下载和扩展。
-
可移植性. 我们能否迁移到新的 Python 版本?新的硬件?这非常重要。Python 生态系统正在快速发展。由于所有库和包都处于不断变化的状态,我们需要能够精确地定义我们的项目依赖于哪些包,并确认它与新候选包集兼容。
这两个特性(兼容性和可移植性)是 Python 的特性。明智的接口选择确保这些特性得到满足。这些有时被描述为架构决策,因为它们影响多个应用程序如何协同工作。
对于安全性,我们将依赖操作系统。同样,对于可用性,我们将限制自己使用命令行界面(CLI)应用程序,依赖长期的设计原则。
性能的概念在这里我们不会强调。我们将指出在处理大数据集时需要仔细设计的地方。数据结构和算法的选择是另一个独立的话题。本书的目标是向您展示可以激发对性能问题进行深入研究的项目。
这三个质量特性——功能性适宜性、可靠性和可维护性——是这些项目的真正焦点。这些似乎是良好软件设计的必要元素。这些是你可以展示你的 Python 编程技能的地方。
另一个视角来自《十二要素应用》(12factor.net
)。这个视角主要关注 Web 应用程序。这些概念提供了对上述质量特性的更深入见解和更具体的技术指导:
-
代码库。 “在一个代码库中进行版本控制,进行多次部署。” 我们将使用Git和GitHub或可能使用sourceforge支持的另一个版本管理器。
-
依赖项。 “明确声明并隔离依赖项。” 传统上,Python 的
requirements.txt
文件用于此目的。在这本书中,我们将继续使用pyproject.toml
文件。 -
配置。 “将配置存储在环境中。” 我们不会强调这一点,但 Python 提供了多种处理配置文件的方法。
-
后备服务。 “将后备服务视为附加资源。” 我们在几个地方提到了这一点。存储、消息、邮件或缓存的工作方式不是我们将深入研究的内容。
-
构建、发布、运行。 “严格分离构建和运行阶段。” 对于命令行应用程序来说,这意味着我们应该将应用程序部署到“生产”环境以使用高价值数据并产生企业所需的结果。我们希望避免在我们的桌面开发环境中运行这些应用程序。
-
进程。 “以一个或多个无状态进程执行应用程序。” CLI 应用程序通常无需额外努力就按这种方式构建。
-
端口绑定。 “通过端口绑定导出服务。” 我们不会强调这一点;它非常特定于 Web 服务。
-
并发。 “通过进程模型进行扩展。” 这是感兴趣读者想要处理非常大的数据集的主题。我们不会在正文中进行强调。我们将在某些章节的“额外内容”部分建议一些这些主题。
-
可用性。 “通过快速启动和优雅关闭最大化鲁棒性。” CLI 应用程序也倾向于以这种方式构建。
-
开发/生产一致性。 “尽可能保持开发、测试和生产环境的相似性。” 虽然我们不会深入强调这一点,但我们的 CLI 应用程序的意图是通过命令行参数、shell 环境变量和配置文件来揭示开发和生产之间的区别。
-
日志。 “将日志视为事件流。” 我们将建议应用程序记录日志,但在这本书中不会提供更详细的指导。
-
管理进程。 “将管理/管理任务作为一次性进程运行。” 一些项目将需要一些额外的管理编程。这些将作为可交付的 CLI 应用程序构建,包括完整的验收测试套件。
我们的目标是提供项目描述和交付物清单,这些清单试图符合这些质量标准。正如我们之前提到的,每个企业都是独特的,一些组织可能无法达到这些标准,而一些组织可能会超过它们。
1.1.1 关于质量的更多阅读
除了 ISO 标准之外,IEEE 1061 标准也涵盖了软件质量。虽然它自 2020 年以来一直处于不活跃状态,但它包含了一些好想法。该标准侧重于质量指标,深入分析了软件质量因素。
阅读以下链接以了解 ISO 标准的起源背景可能会有所帮助:en.wikipedia.org/wiki/ISO/IEC_9126
。
在深入研究这个主题时,识别以下三个术语可能会有所帮助:
-
因素是软件的外部视角。它们反映了用户的理解。一些基本的质量特性对用户来说可能并不直接可见。例如,可维护性可能看起来像是可靠性或可用性问题,因为软件难以修复或扩展。
-
标准来自软件的内部视角。质量标准是项目交付物的焦点。我们的项目代码应该反映上述列出的八个质量特性。
-
指标是我们如何控制用户所见因素的方法。我们不会强调质量指标。在某些情况下,像pylint这样的工具可以提供静态代码质量的实质性度量。这并不是一般软件质量的全面工具,但它为与复杂性和可维护性相关的几个关键指标提供了一个简单的起点。
在确定了高质量软件的标准之后,我们可以将注意力转向构建这些文件的步骤顺序。我们将建议一个你可以遵循的阶段顺序。
1.2 建议的项目冲刺
我们犹豫是否提供构建软件的详细分步过程。对于更有经验的开发者,我们的步骤顺序可能不符合他们的当前实践。对于经验较少的开发者,建议的过程可以通过提供一个合理的顺序来帮助构建交付物。
曾经有一段时间,一份包含详细具体任务列表的“工作说明书”是软件开发工作的一个核心部分。这通常是“瀑布”方法的一部分,其中需求流向分析师,他们编写规范,然后规范流向设计师,设计师编写高级设计,最后流向编码者。这不是构建软件的好方法,已经被敏捷方法在很大程度上取代。有关敏捷的更多信息,请参阅agilemanifesto.org
。
敏捷方法让我们既能将项目视为一系列要完成的步骤,也能将其视为需要创建的一系列交付物。我们首先描述步骤,避免过多强调细节。我们将重新访问交付物,在这些部分中,我们将更深入地探讨最终产品需要是什么。
建议的方法遵循“敏捷统一流程”(www.methodsandtools.com/archive/archive.php?id=21
),它有四个一般阶段。我们将细分其中一个阶段以区分两种重要的交付物。
我们建议将每个项目分为以下五个阶段:
-
启动阶段。准备工具。组织项目目录和虚拟环境。
-
详细阐述,第一部分:定义完成。这通过验收测试用例来实现。
-
详细阐述,第二部分:定义组件和一些测试。这通过为需要构建的组件实现单元测试用例来实现。
-
构建。构建软件。
-
过渡阶段。最终清理:确保所有测试通过,文档可读。
这些努力并不是以简单的线性方式进行。通常需要在详细阐述和构建之间迭代,以分别创建功能。
它通常如图 1.1所示。
图 1.1:开发阶段和周期
此图提供了一个非常粗略的概述,我们将讨论以下活动。重要概念是在详细阐述和构建阶段之间迭代。在构建所有代码之前,很难完全设计一个项目。设计一点,构建一点,根据需要重构,会更容易。
对于复杂项目,可能会有一系列过渡到生产的步骤。通常,会创建一个“最小可行产品”来展示一些概念。随后将会有更多功能或更专注于用户需求的产品。理想情况下,它将具有这两种改进:更多功能和更好地关注用户需求。
我们将更详细地查看这四个阶段,从启动阶段开始。
1.2.1 启动阶段
通过创建项目的父目录开始启动阶段,然后是一些常用的子目录(docs
、notebooks
、src
、tests
)。将有一些顶级文件(README.md
、pyproject.toml
和tox.ini
)。预期目录和文件的列表在本书的交付物列表中描述得更详细。我们将在交付物部分查看这些文件和目录的内容。
在README.md
文件中捕捉任何初始想法很有帮助。稍后,这将被重构为更正式的文档。最初,这是记录笔记和提醒的完美地方。
为项目构建一个全新的虚拟环境。每个项目都应该有自己的虚拟环境。环境基本上是免费的:最好构建它们以反映每个项目的独特方面。
这里有一个可以用来构建环境的conda命令。
% conda create -n project0 --channel=conda-forge python=3.10
启动阶段的一个重要部分是为项目开始编写文档。这可以使用 Sphinx 工具完成。
虽然Conda Forge提供了 Sphinx,但这个版本落后于PyPI存储库中可用的版本。由于这个滞后,最好使用PIP安装 Sphinx:
% python -m pip install sphinx
安装 Sphinx 后,初始化和发布项目的文档很有帮助。开始这个过程允许在工作的过程中发布和共享设计想法。在docs
目录中,执行以下步骤:
-
运行
sphinx-quickstart
命令以填充文档。请参阅www.sphinx-doc.org/en/master/usage/quickstart.html#setting-up-the-documentation-sources
。 -
更新
index.rst
的目录(TOC)以包含两个条目:“概述”和“API”。这些是将在单独的文件中出现的部分。 -
编写一个
overview.rst
文档,其中包含完成的定义:将要完成什么。这应该涵盖项目的核心“谁-什么-何时-何地-为什么”。 -
在 API 文档中添加一个标题,并添加一个
.. todo::
笔记给自己。随着你向项目中添加模块,你将添加到这份文档中。 -
在详细说明阶段,你将更新
index.rst
以添加架构和设计决策的部分。 -
在建设过程中,当你编写代码时,你将添加到 API 部分。
-
在过渡阶段,你将在
index.rst
中添加一些“如何”部分:如何测试它,以及如何使用它。
以此为起点,make html
命令将构建一个 HTML 格式的文档集。这可以与利益相关者共享,以确保对项目有一个清晰、共同的理解。
在有了骨架目录和一些记录想法和决策的初始位置后,开始详细阐述初始目标,并决定将构建什么,以及它将如何工作,是有意义的。
1.2.2 详细说明,第一部分:定义完成
有一个清晰的“完成”定义很有帮助。这有助于将建设努力引导到一个明确的目标。将“完成”的定义写成正式的、自动化的测试套件很有帮助。为此,Gherkin 语言很有用。behave工具可以执行 Gherkin 功能来评估应用程序软件。Gherkin 的替代方案是使用pytest工具与pytest-bdd插件来运行验收测试。
Gherkin 的两个主要优点是能够将功能描述结构化到场景中,并以英语(或任何其他自然语言)编写描述。将预期的行为框架化到离散的操作场景中,迫使我们清晰地思考应用程序或模块的使用方式。用英语(或其他自然语言)编写使得与其他人共享定义以确认我们的理解变得更容易。它还有助于将完成的定义集中在问题域上,而不会陷入技术考虑和编程。
每个场景可以有三个步骤:Given、When 和 Then。Given 步骤定义一个上下文。When 步骤定义一个动作或软件的请求。Then 步骤定义预期的结果。这些步骤定义可以像需要的那样复杂,通常涉及多个由And
连接的子句。可以通过表格提供示例,以避免复制和粘贴具有不同值集的场景。一个单独的模块提供英语步骤文本的 Python 实现。
请参阅behave.readthedocs.io/en/stable/gherkin.html#gherkin-feature-testing-language
,了解使用 Gherkin 编写的场景的多个示例。
通过创建一个基于概述描述的tests/features/project.feature
文件来开始这一部分的详细阐述。不要使用像project
这样的无聊名称。一个复杂的项目可能有多个功能,因此功能文件名称应反映功能。
要使用pytest,在tests
目录中编写一个(或多个)验收测试脚本。
功能由步骤支持。这些步骤位于tests/steps
目录中的模块中。一个tests/steps/hw_cli.py
模块为功能文件中的步骤提供了必要的 Python 定义。模块的名称不重要;我们建议使用类似hw_cli
的名称,因为它实现了 hello-world 命令行界面的步骤。
Behave工具使用的底层机制是函数装饰器。这些装饰器将功能文件中的文本与定义实现该步骤的函数相匹配。这些装饰器可以进行通配符匹配,以允许在措辞上具有灵活性。装饰器还可以从文本中解析出参数值。
需要一个tests/environment.py
文件,但对于简单的测试来说,它可以保持为空。此文件提供测试上下文,并定义了Behave工具用于控制测试设置和拆卸的一些函数。
一旦编写了场景,运行Behave工具查看验收测试失败是有意义的。最初,这让你能够调试步骤定义。
对于这个应用程序,步骤必须正确执行应用程序程序并捕获输出文件。因为应用程序尚不存在,所以在这个阶段预期会出现测试失败。
包含应用程序场景的特征文件是完成工作的一个工作定义。当测试套件运行时,它将显示软件是否工作。从无法工作的特性开始意味着其余的构建阶段将是调试失败并修复软件,直到应用程序通过验收测试套件。
在项目 0 – 带测试用例的 Hello World中,我们将查看一个 Gherkin 语言特性的示例,匹配步骤定义,以及一个用于运行测试套件的tox.ini
文件。
1.2.3 细化,第二部分:定义组件和测试
验收测试套件通常相对“粗糙”——测试会整个地锻炼应用程序,并避免内部错误条件或微妙的边缘情况。验收测试套件很少会锻炼所有单个软件组件。正因为如此,如果没有每个单元的详细单元测试,调试复杂应用程序中的问题可能会很困难——每个包、模块、类和函数。
在编写通用的验收测试套件之后,做两件事会有帮助。首先,开始编写一些可能解决问题的骨架代码。这个类或函数将包含一个文档字符串来解释想法。可选地,它可以包含一个pass
语句的主体。编写这个骨架之后,第二步是通过编写组件的单元测试来扩展文档字符串中的想法。
假设我们已经编写了一个包含将执行名为src/hello_world.py
的应用程序的步骤的场景。我们可以创建这个文件,并包含如下骨架类定义:
class Greeting:
"""
Created with a greeting text.
Writes the text to stdout.
.. todo:: Finish this
"""
pass
这个示例展示了一个具有设计想法的类。这需要通过一个明确的行为声明来扩展。这些期望应该以这个类的单元测试的形式出现。
一旦编写了一些骨架和测试,就可以使用pytest工具来执行这些测试。
单元测试可能会失败,因为骨架代码不完整或不工作。在测试完整但类不工作的情况下,你就可以开始构建阶段了。
在设计不完整或测试不完整的情况下,对于这些类、模块或函数,保持细化阶段是有意义的。一旦测试被理解,构建就有了一个清晰和可实现的目标。
我们并不总是第一次就能得到正确的测试用例,我们必须随着学习的深入而改变它们。我们很少第一次就能得到正确的代码。如果测试用例先出现,它们确保我们有明确的目标。
在某些情况下,如果没有先编写一些“试探性解决方案”来探索替代方案,可能难以清晰地表达设计。一旦试探性解决方案工作正常,编写测试来证明代码正常工作是有意义的。
更多关于创建试探性解决方案的信息,请参阅www.extremeprogramming.org/rules/spike.html
。
到目前为止,您已经对软件的设计有了概念。测试用例是将设计形式化为目标的一种方式。现在是开始构建的时候了。
1.2.4 构建阶段
构建阶段完成了在细化阶段开始类和函数(以及模块和包)的定义。在某些情况下,随着定义的扩展,可能需要添加测试用例。
随着我们越来越接近解决问题,通过测试的数量将会增加。
测试的数量也可能增加。通常,我们会意识到类定义的草图是不完整的,需要额外的类来实现状态或策略设计模式。作为另一个例子,我们可能意识到需要子类来处理特殊情况。这种新的理解将改变测试套件。
当我们查看几天内的进展时,我们应该看到通过测试的数量接近总测试数量。
我们需要多少测试?这里有很多不同的意见。为了展示高质量的工作,测试覆盖 100%的代码是一个好的起点。对于某些行业,一个更严格的规则是覆盖代码中 100%的逻辑路径。这个更高的标准通常用于像机器人技术和医疗保健这样的应用,在这些应用中,软件故障的后果可能涉及伤害或死亡。
1.2.5 过渡
对于企业应用,从开发团队到正式运维有一个过渡。这通常意味着将部署到具有真实用户社区及其数据的实际生产环境中。
在拥有良好持续集成/持续部署(CI/CD)实践的组织中,将正式执行tox
命令以确保一切正常:所有测试都通过。
在一些企业中,还会运行make html
命令来创建文档。
通常,技术运维团队需要文档和README.md
文件中的特定主题。运维人员可能需要诊断和解决数百个应用程序的问题,他们需要非常具体的建议,以便他们可以立即找到。我们在这本书中不会强调这一点,但当我们完成我们的项目时,重要的是要考虑我们的同事将使用这个软件,我们希望他们的工作生活愉快且富有成效。
最后一步是将您的项目发布到您选择的公共仓库。
您已经完成了您的一部分作品集。您希望潜在的商业伙伴、招聘经理或投资者看到这一点,并认识到您的技能水平。
我们可以将一个项目视为一系列步骤的序列。我们也可以将一个项目视为由这些步骤创建的文件集的交付成果。在下一节中,我们将更详细地查看这些交付成果。
1.3 交付成果清单
我们将再次审视项目,这次是从创建哪些文件的角度来看。这将与上一节中显示的活动概述相平行。
以下大纲展示了完成项目中的许多文件:
-
docs
目录中的文档。那里将会有其他文件,但你的重点将放在以下文件上:-
Sphinx 的
index.rst
起始文件,其中包含对概述和 API 部分的引用。 -
一个包含项目总结的
overview.rst
部分。 -
一个包含
..`` automodule::
命令的api.rst
部分,用于从应用程序中提取文档。
-
-
tests
目录中的一组测试用例。-
针对 Behave(或 Gherkin 的pytest-bdd插件)的验收测试。当使用 Behave 时,将会有两个子目录:一个
features
目录和一个steps
目录。此外,还将有一个environment.py
文件。 -
使用pytest框架编写的单元测试模块。这些模块的名称都以
test_
开头,以便pytest能够轻松找到它们。理想情况下,使用Coverage工具来确保 100%的代码被测试。
-
-
src
目录中的最终代码。对于一些项目,一个模块就足够了。其他项目可能需要几个模块。(熟悉 Java 或 C++的开发者在这里通常会创建太多的模块。Python 中的module概念更接近 Java 中的package概念。将每个类定义放入单独的模块文件中并不是常见的 Python 实践。) -
任何 JupyterLab 笔记本都可以放在
notebooks
文件夹中。并非所有项目都使用 JupyterLab 笔记本,因此如果没有笔记本,可以省略此文件夹。 -
一些其他项目文件位于顶级目录中。
-
应该使用
tox.ini
文件来运行pytest和behave测试套件。 -
pyproject.toml
提供了有关项目的许多信息。这包括运行项目所需安装的详细包和版本列表,以及开发测试所需的包。有了这些信息,tox工具就可以使用requirements.txt
或pip-tools工具来构建虚拟环境并测试项目。实际上,这也会被其他开发者用来创建他们的工作桌面环境。 -
一个
environment.yml
文件可以帮助其他开发者使用conda创建他们的环境。这将重复requirements-dev.txt
的内容。对于小型团队来说,这可能没有帮助。然而,在大型企业工作团队中,这可以帮助其他人加入你的项目。 -
此外,一个包含总结的
README.md
(或README.rst
)文件是必不可少的。在许多情况下,这是人们首先查看的内容;它需要为项目提供一个“电梯演讲”(见www.atlassian.com/team-playbook/plays/elevator-pitch
))。
-
有关结构复杂项目的额外建议,请参阅github.com/cmawer/reproducible-model
。
我们按照这个顺序呈现文件,是为了鼓励首先编写文档的方法。这之后是创建测试用例,以确保文档能够满足编程需求。
我们已经研究了开发活动和将要创建的产品审查。在下一节中,我们将探讨一些建议的开发工具。
1.4 开发工具安装
本书中的许多项目都专注于数据分析。数据分析的工具通常最容易通过 conda 工具安装。这不是必需的,熟悉 PIP 工具的读者通常能够在没有 conda 工具的帮助下构建他们的工作环境。
我们建议以下工具:
-
Conda 用于安装和配置每个项目的独特虚拟环境。
-
Sphinx 用于编写文档。
-
Behave 用于验收测试。
-
Pytest 用于单元测试。pytest-cov 插件可以帮助计算测试覆盖率。
-
Pip-Tool 用于从
pyproject.toml
项目定义中构建一些工作文件。 -
Tox 用于运行测试套件。
-
Mypy 用于类型注解的静态分析。
-
Flake8 用于代码的静态分析,以确保遵循一致的风格。
可交付成果之一是 pyproject.toml
文件。它将项目的所有元数据集中在一个地方。它列出了应用程序所需的包,以及用于开发和测试的工具。它有助于锁定确切的版本号,使得某人更容易重建虚拟环境。
一些 Python 工具(如 PIP)与从 pyproject.toml
文件派生出的文件一起工作。pip-tools 从 TOML 文件中的源信息创建这些派生文件。
例如,我们可能会使用以下输出从 pyproject.toml
中提取开发工具信息并将其写入 requirements-dev.txt
。
% conda install -c conda-forge pip-tools
% pip-compile --extra=dev --output-file=requirements-dev.txt
通常的做法是使用 requirements-dev.txt
安装类似这样的包:
% conda install --file requirements-dev.txt --channel=conda-forge
这将尝试安装所有命名的包,从社区 conda-forge
频道拉取。
另一个替代方案是像这样使用 PIP:
% python -m pip install --r requirements-dev.txt
这种环境准备是每个项目启动阶段的一个关键组成部分。这意味着 pyproject.toml
经常是第一个创建的可交付成果。从这个文件中,可以提取 requirements-dev.txt
来构建环境。
为了使前面的步骤和可交付成果更加具体,我们将通过一个初始项目进行讲解。这个项目将有助于展示剩余项目应该如何完成。
1.5 项目 0 – 带测试用例的“Hello World”
这是我们的第一个项目。这个项目将展示本书所有项目的模式。它将包括以下三个要素。
-
描述:描述部分将阐述一个问题,以及为什么用户需要软件来解决它。在某些项目中,描述将包含非常具体的信息。在其他项目中,可能需要更多的想象力来创造解决方案。
-
方法:方法部分将提供一些关于架构和设计选择的指导。对于某些项目,存在权衡,额外部分将探讨一些其他选择。
-
可交付成果:可交付成果部分列出了最终应用程序或模块的期望。它通常会提供一些 Gherkin 功能定义。
对于这个初步项目,描述不会很复杂。同样,这个第一个项目的“方法”部分也将简短。我们将对可交付成果进行一些额外的技术讨论。
1.5.1 描述
用户需要解决的问题是如何最好地将新开发者引入团队。一个良好的入职流程通过使新成员尽快变得高效来帮助我们的用户。此外,这样的项目还可以用于让经验丰富的成员了解新工具。
我们需要指导我们的团队成员安装核心开发工具集,创建一个可工作的模块,并在冲刺结束时展示他们的完成工作。这个第一个项目将使用最重要的工具,并确保每个人都对工具和可交付成果有一个共同的理解。
每个开发者都将构建一个项目来创建一个小型应用程序。这个应用程序将有一个命令行界面(CLI)来编写愉快的问候语。
以下示例显示了期望:
% python src/hello_world.py --who "World"
Hello, World!
此示例展示了如何通过命令行参数--who
"world"运行应用程序,在控制台上产生响应。
1.5.2 方法
对于这个项目,目标是创建一个 Python 应用程序模块。该模块需要几个内部函数。如果看起来更合适,可以将这些函数组合成一个类。这些函数包括:
-
一个用于解析命令行选项的函数。这将使用
argparse
模块。默认的命令行参数值可以在sys.argv
中找到。 -
一个用于编写愉快问候语的函数。这可能只是一行代码。
-
一个具有明显名称(如
main()
)的整体函数,用于获取选项并编写问候语。
整个模块将包含函数(或类)定义。它还将包含一个if __name__ == "__main__":
块。这个块将保护对表达式main()
的评估,使模块更容易进行单元测试。
对于一个简单的问题来说,这需要相当多的工程。有些人可能会称之为过度设计。目的是创建一个足够复杂的系统,以至于需要多个单元测试用例。
1.5.3 可交付成果
如上所述在可交付成果列表,对于一般项目,有许多可交付成果文件。以下是本项目的建议文件:
-
README.md
总结了项目。 -
pyproject.toml
定义了项目,包括开发工具、测试工具和其他依赖项。 -
docs
包含文档。如上所述,这应由sphinx-quickstart
工具构建,并应包含至少概述和 API 部分。 -
tests
包含测试用例;文件包括以下内容:-
test_hw.py
包含模块的函数或类的单元测试。 -
features/hello_world.feature
包含作为场景集合的整体验收测试。 -
steps/hw_cli.py
包含了场景中步骤的 Python 定义。 -
environment.py
包含控制 behave 测试设置和拆卸的函数。对于简单的项目,它可能为空。
-
-
tox.ini
配置用于运行完整测试套件的 tox 工具。 -
src
包含hello_world.py
模块。
我们将在以下子节中详细查看这些文件中的几个。
pyproject.toml 项目文件
pyproject.toml
文件在单个位置包含大量项目元数据。此文件的最小内容是用于构建和安装包的 “build-system” 描述。
对于本书的目的,我们可以使用以下两行来定义构建系统:
[build-system]
requires = ["setuptools", "wheel"] # PEP 508 specifications.
这指定了使用 setuptools
模块创建包含项目代码的 “wheel”。pyproject.toml
不需要进一步详细定义分发包。本书不强调创建分发包或使用 Python 包索引 PyPI 管理包。
此文件的其余部分应包含有关项目的信息。您可以包含以下类似的部分:
[project]
name = "project_0"
version = "1.0.0"
authors = [
{name = "Author", email = "author@email.com"},
]
description = "Real-World Python Projects -- Project 0."
readme = "README.md"
requires-python = ">=3.10"
显然,您将想要更新 authors
部分以包含您的信息。您可能正在使用较新的 Python 版本,可能需要更改 requires-python
字符串以指定您独特解决方案所需的最小版本。
[project]
部分需要其他三块信息:
-
执行您的应用程序所需的包。
-
任何测试您的应用程序所需的包或工具。
-
开发您的应用程序所需的任何包或工具。
这三个依赖项组织如下:
dependencies = [
# Packages required -- None for Project Zero.
]
[project.optional-dependencies]
dev = [
# Development tools to work on this project
"sphinx==7.0.1",
"sphinxcontrib-plantuml==0.25",
"pip-tools==6.13.0"
]
test = [
# Testing tools to test this project
"pytest==7.2.0",
"tox==4.0.8",
"behave==1.2.6"
]
dependencies
行列出了执行应用程序所需的依赖项。一些项目——比如这个项目——依赖于标准库,无需添加更多内容。[project.optional-dependencies]
部分包含两个额外的包列表:开发所需的包和测试所需的包。
注意,我们在该文件中放置了特定的版本号,以确保我们绝对确定将使用哪些包。随着这些包的发展,我们需要测试新版本并升级依赖项。
如果您在本书中看到的版本号落后于 PyPI 或 Conda-Forge 上的当前技术水平,请随意使用最新版本。
使用pip-compile命令很有帮助。该命令作为pip-tools的一部分安装。此命令从pyproject.toml
文件中创建提取文件,供pip或conda使用。
对于开发者来说,我们通常希望安装所有的“额外”组件。这通常意味着执行以下命令以创建一个requirements-dev.txt
文件,该文件可以用来构建开发环境。
% pip-compile --all-extras -o requirements-dev.txt
为了运行tox工具,通常还会创建一个仅包含所需包和工具的测试子集。使用以下命令:
% pip-compile --extra test -o requirements.txt
这将创建用于检测和管理tox测试所使用的虚拟环境的requirements.txt
文件。
文档目录
如上所述在建议的项目冲刺中,此目录应使用sphinx-quickstart
构建。在创建初始文件集之后,进行以下更改:
-
添加一个
api.rst
文件作为 Sphinx 生成的 API 文档的占位符。这将使用.. automodule::
指令从您的应用程序中提取文档。 -
添加一个
overview.rst
文件,概述项目。 -
将
index.rst
更新为在目录表中包含这两个新文件。 -
将
conf.py
更新为将src
目录添加到sys.path
中。此外,还需要将sphinx.ext.autodoc
扩展添加到该文件中的extensions
设置。
在docs
目录中的make html
命令可以用来构建文档。
tests/features/hello_world.feature
文件
features
目录将包含功能的 Gherkin 语言定义。每个功能文件将包含一个或多个场景。对于较大的项目,这些文件通常以从问题描述或架构概述中提取的语句开始,这些语句后来被细化成更详细的步骤来描述应用程序的行为。
对于此项目,其中一个功能文件应该是features/hello_world.feature
。此文件的 内容应包括功能的描述和至少一个场景。它看起来像以下示例:
Feature: The Cheerful Greeting CLI interface provides a greeting
to a specific name.
Scenario: When requested, the application writes the greeting message.
When we run command "python src/hello_world.py"
Then output has "Hello, World!"
在此场景中没有Given
步骤;没有初始化或准备要求。每个步骤只有一个子句,因此也没有And
步骤。
此示例与描述中的示例不完全匹配。有两个可能的原因:两个示例中的一个可能是错误的,或者,更宽容地说,此示例暗示了第二个功能。
此示例所暗示的想法是,如果没有提供--who
命令行选项,则存在默认行为。这表明应该为该功能添加第二个场景——一个带有--who
选项的场景。
tests/steps/hw_cli.py
模块
steps
目录包含定义功能文件中自然语言短语的模块。在hello_world.feature
文件中,When
和Then
步骤用纯英语写出了短语:
-
我们运行命令”
python src/hello_world.py
” -
输出为”
Hello, World!
”
steps/hw_cli.py
模块将步骤的短语映射到 Python 函数。它通过使用装饰器和模式匹配来指定步骤的类型(@given
、@when
或@then
)以及要匹配的文本。文本中存在{parameter}
将匹配文本并提供作为参数匹配到步骤函数的值。函数名称无关紧要,通常为step_impl()
。
通常,@given
步骤将在测试上下文对象中累积参数值。最佳实践建议只有一个@when
步骤;这将执行所需操作。对于这个项目,它将运行应用程序并收集输出文件。@then
步骤可以使用assert
语句将实际结果与特征文件中显示的预期结果进行比较。
下面是steps/hw_cli.py
模块可能的样子:
import subprocess
import shlex
from pathlib import Path
@when(u’we run command "{command}"’)
def step_impl(context, command):
output_path = Path("output.log")
with output_path.open(’w’) as target:
status = subprocess.run(
shlex.split(command),
check=True, text=True, stdout=target, stderr=subprocess.STDOUT)
context.status = status
context.output = output_path.read_text()
output_path.unlink()
@then(u’output has "{expected_output}"’)
def step_impl(context, expected_output):
assert context.status.returncode == 0
assert expected_output in context.output
这假设有一个相对较小的输出文件,可以被收集到内存中。对于较大的文件,让@when
步骤创建一个临时文件并将文件对象保存在上下文中是有意义的。@then
步骤可以读取并关闭这个文件。tempfile
模块对于创建在关闭时会被删除的文件很有用。
另一个选择是创建一个Path
对象并将其保存在上下文中。@when
步骤可以将输出写入此路径。@then
步骤可以读取并检查由Path
对象命名的文件的 内容。
当测试步骤检测到assert
语句的问题时,它可能不会完全完成。使用Path
对象的方法需要小心以确保临时文件被删除。environment.py
模块可以定义一个after_scenario(context, scenario)
函数来删除临时文件。
tests/environment.py
文件
此模块将包含一些behave使用的函数定义。对于这个项目,它将是空的。模块必须存在;一个模块文档字符串适合解释它是空的。
对于这个示例,tests/steps
模块将包含可以重构为两个可能可重用函数的示例,用于执行应用程序和检查应用程序输出的特定文本。这项额外的设计工作不包含在这个项目中。你可能会在完成几个这样的项目后找到这样做很有帮助。
一旦特性、步骤和环境就绪,就可以使用behave程序来测试应用程序。如果没有应用程序模块,测试将失败。在src
目录中创建一个骨架应用程序模块将允许测试用例执行并失败,因为输出不是预期的。
tests/test_hw.py
单元测试
单元测试可以作为使用capsys
固定装置的pytest函数实现,该固定装置用于捕获系统输出。单元测试用例期望应用程序有一个解析命令行选项的main()
函数。
下面是一个建议的单元测试函数:
import hello_world
def test_hw(capsys):
hello_world.main([])
out, err = capsys.readouterr()
assert "Hello, World!" in out
注意对main()
函数的测试提供了一个显式的空参数值列表。在pytest运行时,覆盖任何可能存在的sys.argv
值是必要的。
此测试导入hello_world
模块。此导入有两个重要的后果:
-
src/hello_world.py
模块必须有一个if`` __name__`` ==`` "__main__":
部分。一个简单的 Python 脚本(没有这个部分)在导入时将完全执行。这可能会使测试变得困难。 -
src
目录必须是PYTHONPATH
环境变量的一部分。这由tox.ini
文件处理。
此测试将容忍除所需的愉快问候语外的额外输出。使用类似`"Hello, World!"
== out.strip()
的东西可能是有意义的。
main()
函数的实现细节对此测试来说是透明的。这个main()
函数可以创建一个类的实例;它也可以使用类的静态方法。
The src/tox.ini file
现在测试已经存在,我们可以运行它们。tox(和nox)工具非常适合运行一系列测试。
这里是一个示例tox.ini
文件:
[tox]
min_version = 4.0
skipsdist = true
[testenv]
deps = pip-tools
pytest
behave
commands_pre = pip-sync requirements.txt
setenv =
PYTHONPATH=src
commands =
pytest tests
behave tests
此文件列出了用于测试的工具:pip-tools、pytest和behave。它提供了PYTHONPATH
的设置。commands_pre
将使用pip-tools包中的pip-sync命令准备虚拟环境。给定的命令序列定义了测试套件。
The src/hello_world.py file
这是期望的应用模块。测试框架有助于确认它确实可以工作,并且——更重要的是——它符合*.feature
文件中提供的完成定义。
正如我们上面提到的,单元测试将把这个应用作为模块导入。相比之下,验收测试将运行应用。这意味着if`` __name__`` ==`` "__main__":
部分是必不可少的。
对于这样一个小型应用程序,应用程序的实际工作应该封装在一个main()
函数中。这允许主模块以以下片段结束:
if __name__ == "__main__":
main()
这确保了模块在导入时不会起飞并开始运行。它只有在从命令行调用时才会执行有用的工作。
1.5.4 完成定义
此项目通过运行tox
命令进行测试。
当所有测试执行完毕时,输出将如下所示:
(projectbook) slott@MacBookPro-SLott project_0 % tox
py: commands[0]> pytest tests
...
py: commands[1]> behave tests
...
py: OK (0.96=setup[0.13]+cmd[0.53,0.30] seconds)
congratulations :) (1.55 seconds)
此输出省略了来自pytest和behave的详细信息。tox工具的输出是重要的总结py:`` OK
。这告诉我们所有测试都通过了。
一旦完成,我们可以运行以下命令来创建 API 文档:
% (cd docs; make html)
使用()
将两个命令包裹起来可能会有所帮助,这样cd`` docs
命令就不会离开docs
目录的会话。一些开发者更喜欢打开两个窗口:一个在顶级目录中运行tox工具,另一个在docs
子目录中运行sphinx工具的make命令。
1.6 总结
在本章中,我们探讨了以下主题:
-
我们将尝试强调的软件质量原则概述。
-
完成项目作为一系列项目冲刺的建议方法。
-
每个项目交付物列表的一般概述。
-
建议用于创建这些示例的工具。
-
一个作为后续项目模板的示例项目。
在创建这个初始项目之后,下一章将探讨一般的项目集合。目标是创建一个包含多个紧密相关项目的完整数据分析工具集。
1.7 额外内容
这里有一些想法供您添加到这个项目中。
1.7.1 静态分析 - mypy, flake8
有几种常见的静态分析工具,与自动化测试一样重要:
-
mypy检查类型注解,以确保函数和类将正确交互。
-
flake8执行其他语法检查,以确保代码避免了一些常见的 Python 错误。
-
black可用于检查格式,以确保其遵循推荐的风格。
black
应用程序还可以用于重新格式化新文件。 -
isort可用于将长序列的
import
语句放入一致的顺序。
一旦应用程序通过了*.feature
文件中的功能测试,可以应用这些额外的非功能性测试。这些额外的测试通常有助于发现更细微的问题,这些问题可能会使程序难以适应或维护。
1.7.2 CLI 功能
命令语言界面允许一个选项,即--who
选项,提供名称。
添加一个场景来练习此选项是有意义的。
如果没有提供--who
的值,会发生什么?以下做法是否合适?
(projectbook) slott@MacBookPro-SLott project_0 % python src/hello_world.py
--who
usage: hello_world.py [-h] [--who WHO]
hello_world.py: error: argument --who/-w: expected one argument
是否应该扩展帮助说明以阐明所需内容?
考虑添加以下场景(及其实现代码):
-
为
--help
选项添加一个场景,该选项由argparse
模块自动提供。 -
为
--who
无值错误添加一个场景。
1.7.3 日志记录
考虑一个更复杂的应用程序,其中可能需要额外的调试输出。为此,通常会在--verbose
选项中设置日志级别为logging.DEBUG
,而不是默认的logging.INFO
级别。
添加此选项需要添加日志功能。考虑对此模块进行以下更改:
-
导入
logging
模块并创建一个全局日志记录器用于应用程序。 -
更新
main()
函数,根据选项设置日志记录器的级别。 -
更新
__name__ == "__main__"
块,使其包含两行:logging.basicConfig()
和main()
。最好将日志配置与应用程序的其他处理部分隔离。
1.7.4 Cookiecutter
cookiecutter
项目(见 cookiecutter.readthedocs.io/en/stable/
)是一种构建模板项目的方法。这可以通过共享单个模板来帮助团队成员开始工作。随着工具版本或解决方案架构的变化,可以开发和使用额外的 cookie-cutter 模板。
可用的 cookie-cutter 模板有成千上万。找到适合的简单模板可能很困难。可能更好的做法是创建自己的模板,并在后续章节中引入新概念时不断添加到其中。
第二章
项目概述
我们的一般计划是构建分析、决策支持模块和应用。这些应用通过向利益相关者提供可用数据的摘要来支持决策。决策的范围从揭示变量之间新的关系到确认数据变化在狭窄范围内是随机的噪声。处理将从获取数据并通过几个阶段移动开始,直到可以展示统计摘要。
处理将被分解为几个阶段。每个阶段将作为一个核心概念应用来构建。将会有后续项目来增加核心应用的功能。在某些情况下,一些功能将被添加到几个项目中,所有这些项目都合并到一个章节中。
阶段设计灵感来源于提取-转换-加载(ETL)架构模式。本书中的设计在 ETL 设计的基础上增加了多个额外步骤。由于旧术语可能具有误导性,因此对这些词进行了更改。这些特性——通常对于现实世界的实用应用是必需的——将作为管道中的额外阶段插入。
一旦数据被清理和标准化,本书将描述一些简单的统计模型。分析将在这里停止。建议您转向更高级的书籍,这些书籍涵盖了人工智能和机器学习。
有 22 个不同的项目,其中许多基于先前结果。不需要按顺序完成所有项目。但是,在跳过一个项目时,阅读该项目描述和交付成果非常重要。这有助于更全面地理解后续项目的背景。
本章将介绍我们创建完整数据分析程序序列的整体架构方法。我们将采用以下多阶段方法:
-
数据采集
-
数据检查
-
清洗数据;这包括验证、转换、标准化和保存中间结果
-
总结和建模数据
-
创建更复杂的统计模型
阶段组合方式如图图 2.1所示。
图 2.1:数据分析管道
这背后的一个核心思想是关注点的分离。每个阶段都是一个独立的操作,每个阶段可以独立于其他阶段发展。例如,可能有多个数据来源,导致几个不同的数据采集实现,每个实现都创建一个共同的内部表示,以便使用单个、统一的检查工具。
同样,数据清洗问题似乎在组织中几乎随机出现,导致需要添加独特的验证和标准化操作。这个想法是在管道的这个阶段分配对语义特殊情况和异常的责任。
建筑理念之一是将自动化应用程序和几个手动 JupyterLab 笔记本混合成一个整体。笔记本对于解决故障问题或问题至关重要。对于优雅的报告和演示,笔记本也非常有用。虽然 Python 应用程序可以生成整洁的 PDF 文件和完善的报告,但似乎发布带有分析和发现内容的笔记本要容易一些。
我们将从处理阶段的获取阶段开始。
2.1 通用数据获取
所有数据分析处理都始于从源获取数据的必要步骤。
上述声明似乎有些荒谬,但在此方面的失败往往会导致后续复杂的返工。认识到数据存在这两种基本形式至关重要:
-
可用于分析程序的 Python 对象。虽然明显的候选者是数字和字符串,但这包括使用Pillow等包以 Python 对象的形式操作图像。像librosa这样的包可以创建表示音频数据的对象。
-
Python 对象的序列化。这里有很多选择:
-
文本。某种字符串。有无数的语法变体,包括 CSV、JSON、TOML、YAML、HTML、XML 等。
-
Pickled Python 对象。这些是由
pickle
模块创建的。 -
二进制格式。像 Protobuf 这样的工具可以将原生 Python 对象序列化为字节流。类似地,一些 YAML 扩展可以将对象序列化为非文本的二进制格式。图像和音频样本通常以压缩的二进制格式存储。
-
源数据的格式——几乎普遍地——不是由任何规则或惯例固定。基于假设源数据总是是 CSV 格式文件的假设编写应用程序,当需要新的格式时可能会导致问题。
最好将所有输入格式视为可能发生变化。一旦获取数据,就可以将其保存为分析管道使用的通用格式,并且独立于源格式(我们将在清洁、验证、标准化和持久化中讨论持久化)。
我们将从项目 1.1:“获取数据”开始。这将构建数据获取基础应用程序。它将获取 CSV 格式数据,并作为在后续项目中添加格式的依据。
数据获取的方法有很多种。在接下来的几章中,我们将探讨一些替代的数据提取方法。
2.2 通过提取获取
由于数据格式处于不断变化的状态,了解如何添加和修改数据格式很有帮助。这些项目都将基于项目 1.1,通过向基础应用程序添加功能来构建。以下项目围绕数据的不同来源设计:
-
项目 1.2:“从 API 获取 Web 数据”。此项目将使用 JSON 格式从网络服务获取数据。
-
项目 1.3:“从 HTML 获取 Web 数据”。此项目将通过抓取 HTML 从网页获取数据。
-
两个独立的项目是收集来自 SQL 数据库的数据的一部分:
-
项目 1.4:“构建本地数据库”。这是一个必要的辅助项目,用于构建本地 SQL 数据库。这是必要的,因为公众可访问的 SQL 数据库是罕见的。构建我们自己的演示数据库更安全。
-
项目 1.5:“从本地数据库获取数据”。一旦数据库可用,我们就可以从 SQL 提取中获取数据。
-
这些项目将专注于以文本形式表示的数据。对于 CSV 文件,数据是文本;应用程序必须将其转换为更有用的 Python 类型。HTML 页面也是纯文本。有时,还提供了额外的属性,表明文本应被视为数字。SQL 数据库通常填充了非文本数据。为了保持一致,SQL 数据应序列化为文本。获取应用程序都采用处理文本的共同方法。
这些应用还将最小化对源数据应用的转换。为了一致地处理数据,转向一个通用格式是有帮助的。正如我们将在第三章中看到的,项目 1.1:数据获取基础应用,NDJSON 格式提供了一个有用的结构,这通常可以映射回源文件。
在获取新数据后,进行手动检查是谨慎的做法。这通常在应用开发初期进行几次。之后,检查仅用于诊断源数据的问题。接下来的几章将涵盖检查数据的项目。
2.3 检查
数据检查需要在开发初期进行。确保新数据确实是解决用户问题的必要条件至关重要。常见的挫折是不完整或不一致的数据,这些问题需要尽快揭露,以避免浪费时间和精力创建处理不存在数据的软件。
此外,数据还通过手动检查来揭露问题。重要的是要认识到数据源处于不断变化的状态。随着应用的演变和成熟,用于分析的数据将发生变化。在许多情况下,数据分析应用通过无效数据在事后发现其他企业变化。了解通过良好的数据检查工具的演变是很重要的。
检查是一个本质上手动的过程。因此,我们将使用 JupyterLab 创建笔记本来查看数据并确定一些基本特征。
在隐私至关重要的罕见情况下,开发者可能不允许进行数据检查。更有特权的人——拥有查看支付卡或医疗细节的权限——可能参与数据检查。这意味着检查笔记本可能是由开发者创建的,供利益相关者使用。
在许多情况下,数据检查笔记本可以是完全自动化数据清洗应用程序的开始。开发者可以将笔记本单元提取为函数,构建一个既可以从笔记本也可以从应用程序中使用的模块。单元结果可以用来创建单元测试用例。
管道中的这一阶段需要多个检查项目:
-
项目 2.1:“检查数据”。这将构建一个核心数据检查笔记本,具有足够的特性来确认一些获取的数据可能是有效的。
-
项目 2.2:“检查数据:基数域”。此项目将为测量、日期和时间添加分析功能。这些是反映测量和计数的基数域。
-
项目 2.3:“检查数据:名义和普通域”。此项目将为文本或编码数值数据添加分析功能。这包括名义数据和有序数值域。重要的是要认识到美国邮政编码是数字字符串,而不是数字。
-
项目 2.4:“检查数据:参考数据”。此笔记本将包括在处理已标准化并分解为具有编码“键”值子集的数据时查找参考域的功能。
-
项目 2.5:“定义可重用架构”。作为最后一步,它可以帮助使用 JSON Schema 标准定义正式架构和相关元数据。
虽然这些项目似乎是一次性的努力,但它们通常需要谨慎编写。在许多情况下,当出现问题时,需要重复使用笔记本。提供充分的解释和测试用例有助于刷新人们对数据细节和已知问题区域的记忆。此外,笔记本可以作为测试用例和自动化清洗、验证或标准化数据的 Python 类或函数设计的示例。
经过详细检查后,我们就可以构建应用程序来自动化清洗、验证和标准化值。下一批项目将解决这个管道阶段的问题。
2.4 清洗、验证、标准化和持久化
一旦从一般意义上理解了数据,编写应用程序来清理任何序列化问题,并执行更正式的测试以确保数据确实有效是有意义的。一个令人沮丧的常见问题是收到重复的数据文件;这可能会发生在企业中的某个地方预定处理被打断,并且之前时期的数据被重新用于分析。
验证测试有时是清理的一部分。如果数据包含任何意外的无效值,可能需要拒绝它。在其他情况下,已知问题可以作为分析的一部分通过用有效数据替换无效数据来解决。一个例子是美国邮政编码,它们(有时)被转换为数字,并且前导零丢失。
这些数据分析管道阶段由多个项目描述:
-
项目 3.1:“清洗数据”。这构建了数据清洗基础应用程序。设计细节可以来自数据检查笔记本。
-
项目 3.2:“清洗和验证”。这些功能将验证并转换数值字段。
-
项目 3.3:“清洗和验证文本和代码”。验证文本字段和数值编码字段需要更复杂的设计。
-
项目 3.4:“清洗和验证引用”。当数据从不同的来源到达时,验证这些来源之间的引用是至关重要的。
-
项目 3.5:“标准化数据”。某些数据源需要标准化以创建通用的代码和范围。
-
项目 3.6:“获取和清洗管道”。将获取、清洗、验证和标准化集成到单个管道中通常很有帮助。
-
项目 3.7:“获取、清洗和保存”。此管道的一个关键架构特性是将中间文件保存为通用格式,与数据源不同。
-
项目 3.8:“数据提供者 Web 服务”。在许多企业中,内部 Web 服务和 API 被视为分析数据的来源。此项目将数据获取管道封装为 RESTful Web 服务。
在这些项目中,我们将从获取应用程序中转换文本值到更有用的 Python 对象,如整数、浮点值、十进制值和日期时间值。
数据清洗和验证完成后,探索可以继续。第一步是总结数据,再次使用 Jupyter 笔记本创建可读的、可发布的报告和演示文稿。下一章将探讨总结数据的工作。
2.5 总结和分析
以有用的形式总结数据更多的是艺术而非技术。可能很难知道如何最好地向人们展示信息以帮助他们做出更有价值或更有帮助的决策。
有几个项目用于捕捉总结和初步分析的核心:
-
项目 4.1:“数据仪表板”。这个笔记本将展示多种可视化分析技术。
-
项目 4.2:“已发布的报告”。可以将笔记本保存为 PDF 文件,创建一个易于分享的报告。
总结和创建共享、发布的报告的初步工作为更正式、自动化的报告奠定了基础。下一组项目将构建提供更深入和更复杂统计模型的模块。
2.6 统计建模
数据分析的目的在于消化原始数据,向人们展示信息以支持他们的决策。管道的先前阶段准备了两个重要的事情:
-
原始数据已经过清洗和标准化,以提供相对容易分析的数据。
-
检查和总结数据的过程帮助分析师、开发人员和最终用户理解信息的意义。
数据与更深层次含义的结合为企业创造了显著的价值。分析过程可以继续作为更正式的统计建模。这反过来又可能导致人工智能(AI)和机器学习(ML)应用。
处理管道包括这些项目,以收集单个变量以及变量组合的摘要:
-
项目 5.1:“统计模型:核心处理”。此项目构建了应用统计模型和保存数据参数的基础应用程序。这将侧重于如均值、中位数、众数和方差这样的摘要。
-
项目 5.2:“统计模型:关系”。通常想知道变量之间的关系。这包括变量之间的相关度等度量。
这个阶段的序列产生高质量的数据,并提供诊断和调试数据源问题的方法。项目的序列将说明如何使用自动化解决方案和交互式检查来创建有用、及时、有洞察力的报告和分析。
2.7 数据合同
在这个管道的各个阶段,我们将涉及数据合同。例如,这个应用程序的数据获取可能与数据提供商有一个正式的合同。也可能只有非正式的数据合同,如模式定义或 API。
在第八章,项目 2.5:模式与元数据中,我们将考虑一些模式发布的相关问题。在第第十一章,项目 3.7:临时数据持久化中,我们将考虑提供给下游应用的模式。这两个主题与正式数据合同相关,但本书不会深入探讨数据合同、它们的创建方式或可能的使用方式。
2.8 摘要
这个数据分析管道将数据从源通过一系列阶段移动,以创建干净、有效、标准化的数据。一般的流程支持各种需求,并允许大量的定制和扩展。
对于对数据科学或机器学习感兴趣的开发者,这些项目涵盖了数据科学或机器学习有时所说的“数据整理”部分。当数据被理解,数据源之间的差异得到解决和探索时,这可能会成为一个重大的复杂性。这些是在构建可用于 AI 决策制定的模型之前——有时是困难的——准备步骤。
对于对网络感兴趣的读者来说,这种数据处理和提取是通过网络应用程序 API 或网站展示数据的一部分。项目 3.7 创建了一个网络服务器,这将特别引起人们的兴趣。因为网络服务需要干净的数据,所以前面的项目有助于创建可以发布的数据。
对于对自动化或物联网感兴趣的人,第二部分解释了如何使用 Jupyter Notebooks 收集和检查数据。这是一个常见需求,当处理受温度和电压变化影响的现实世界设备时,清理、验证和标准化数据的各个步骤变得更加重要。
我们已经研究了以下多阶段方法来进行数据分析:
-
数据采集
-
数据检查
-
清理、验证、标准化和持久化
-
概括和分析
-
创建统计模型
此管道遵循提取-转换-加载(ETL)概念。术语已经改变,因为旧术语有时会误导。我们的采集阶段与通常所说的“提取”操作重叠。对于一些开发者来说,“提取”仅限于数据库提取;我们希望超越这一点,包括其他数据源转换。我们的清理、验证和标准化阶段通常合并为“转换”操作。保存干净数据通常是“加载”的目标;我们不是强调数据库加载,而是将使用文件。
在整本书中,我们将描述每个项目的目标,并提供一个可靠的技术方法的基础。实施细节由你决定。我们将列出可交付成果;这可能会重复一些来自第一章、项目零:其他项目的模板的信息。本书提供了大量关于验收测试用例和单元测试用例的信息——完成的标准。通过涵盖这种方法,我们为你留下了设计和实现所需应用程序软件的空间。
在下一章中,我们将构建第一个数据采集项目。这将使用 CSV 格式的文件。后续的项目将使用数据库提取和 Web 服务。
第三章
项目 1.1:数据采集基础应用
数据管道的开始是从各种来源获取原始数据。本章有一个单一的项目,创建一个命令行 应用程序(CLI),从 CSV 格式的文件中提取相关数据。这个初始应用程序将把原始数据重新结构化为更有用的形式。后续的项目(从第九章开始)将添加数据清理和验证的功能。
本章的项目涵盖了以下基本技能:
-
通用应用程序设计。这包括面向对象的设计和 SOLID 设计原则,以及功能设计。
-
一些 CSV 文件处理技术。这是一个很大的主题领域,项目重点是重新构建源数据为更可用的形式。
-
命令行应用程序构建。
-
使用 Gherkin 语言和behave步骤定义创建验收测试。
-
使用模拟对象创建单元测试。
我们将从对应用程序的描述开始,然后转到讨论架构和构建。这将随后是一个详细的交付成果列表。
3.1 描述
分析师和决策者需要获取数据以进行进一步分析。在许多情况下,数据以 CSV 格式文件的形式可用。这些文件可能是数据库的提取或从网络服务下载的。
为了测试目的,从相对较小的事物开始是有帮助的。一些 Kaggle 数据集非常大,需要复杂的应用程序设计。其中最有趣的小数据集之一是 Anscombe 的四重奏。这可以作为测试案例来理解获取原始数据的问题和关注点。
我们对应用程序获取数据的一些关键特性感兴趣:
-
当从多个来源收集数据时,将其转换为通用格式至关重要。数据来源多种多样,并且随着软件升级通常会发生变化。采集过程需要根据数据来源的灵活性,避免对格式的假设。
-
命令行应用程序允许各种自动化可能性。例如,命令行应用程序可以被“包装”以创建一个网络服务。它可以手动从命令行运行,也可以通过企业作业调度应用程序进行自动化。
-
应用程序必须可扩展以反映源变化。在许多情况下,企业变更的沟通并不充分,数据分析应用程序通过“艰难的方式”发现变化——数据源突然包括意外的或看似无效的值。
3.1.1 用户体验
用户体验(UX)将是一个带有调整收集数据选项的命令行应用程序。这种基本的 UX 模式将用于本书的许多项目。它很灵活,几乎可以在任何地方运行。
我们期望的命令行应该看起来像以下这样:
% python src/acquire.py -o quartet Anscombe_quartet_data.csv
-o
quartet
参数指定了结果提取写入的目录。源文件包含四个独立的数据系列。每个系列可以给一个不太引人注目的名字,如 quartet/series_1.json
。
位置参数 Anscombe_quartet_data.csv
是下载的源文件名。
虽然目前只有一个文件,但良好的设计将能够处理多个输入文件和多种源文件格式。
在某些情况下,一个更复杂的“仪表板”或“控制面板”应用可能更受欢迎,作为监控数据采集过程的一种方式。使用基于 Web 的 API 可以提供非常丰富的交互体验。另一种选择是使用像 rich 或 Textual 这样的工具来构建一个小型的基于文本的显示。这两种选择都应构建为执行基本 CLI 应用的子进程的包装器。
现在我们已经了解了应用的目的和用户体验概述,让我们来看看源数据。
3.1.2 关于源数据
这是我们将使用的数据集链接:
www.kaggle.com/datasets/carlmcbrideellis/data-anscombes-quartet
你需要注册 Kaggle 才能下载这些数据。
Kaggle URL 提供了一个包含 CSV 格式文件信息的页面。点击 下载 按钮将数据小文件下载到你的本地计算机。
数据也存在于本书 GitHub 仓库的 data
文件夹中。
数据下载后,你可以打开 Anscombe_quartet_data.csv
文件来检查原始数据。
文件包含每行四个 (x,y) 对的数据系列。我们可以想象每一行有 [(x[1],y[1]),(x[2],y[2]),(x[3],y[3]),(x[4],y[4])]。然而,它被压缩了,如下所示。
我们可以用一个实体关系图来描述这一数据背后的理念,如图 图 3.1 所示。
图 3.1:概念实体关系图
有趣的是,数据并非以四个独立的 (x,y) 对的形式组织。下载的文件组织如下:
我们可以在 ERD 中描述实际的源实体类型,如图 图 3.2 所示。
图 3.2:源实体关系图
本应用的一部分目的是将四个系列数据分开到不同的文件中。这迫使我们编写一些转换处理程序,以重新排列每一行的数据元素到四个独立的数据集中。
然后将单独的系列保存到四个单独的文件中。我们将在第十一章、项目 3.7:临时数据持久化中更深入地探讨为单独项目创建单独文件的细节。对于这个项目,四个输出文件可以采用任何文件格式;ND JSON 序列化通常是理想的。
我们鼓励你在继续考虑如何将其转换为不同的输出文件之前查看该文件。
给定这个源数据的压缩文件,下一节将查看扩展的输出文件。这些文件将分离每个系列,以便更容易处理。
3.1.3 关于输出数据
ND JSON 文件格式在ndjson.org
和jsonlines.org
中描述。想法是将每个单独的实体放入一个作为单行编写的 JSON 文档中。这与 Python json.dumps()
函数的工作方式相符:如果没有为indent
参数提供值(或者如果值为indent=None
),文本将尽可能紧凑。
series_1.json
输出文件应该像这样开始:
{"x": "10.0", "y": "8.04"}
{"x": "8.0", "y": "6.95"}
{"x": "13.0", "y": "7.58"}
...
每一行都是一个独特的、小的 JSON 文档。该行由输入文件中的字段子集构建而成。值是字符串:我们不会在第九章、项目 3.1:数据清洗基础应用程序中的清理和验证项目之前尝试任何转换。
我们将要求运行此应用程序的用户创建输出目录并在命令行上提供目录名称。这意味着如果目录实际上不存在,应用程序需要显示有用的错误消息。pathlib.Path
类对于确认目录存在非常有帮助。
此外,应用程序应谨慎覆盖任何现有文件。pathlib.Path
类对于确认文件是否已存在非常有帮助。
本节探讨了此应用程序的输入、处理和输出。在下一节中,我们将探讨软件的整体架构。
3.2 架构方法
在审视我们的方法时,我们将借鉴 C4 模型(c4model.com
)的一些指导。
-
上下文:对于这个项目,上下文图将显示用户从源提取数据。你可能觉得绘制这个图会有所帮助。
-
容器:此项目将在用户的个人计算机上运行。与上下文一样,图很小,但一些读者可能觉得花时间绘制它会有所帮助。
-
组件:我们将在下面讨论这些问题。
-
代码:我们将简要介绍一些建议的方向。
我们可以将软件架构分解为这两个重要组件:
-
model
:此模块包含目标对象的定义。在这个项目中,这里只有一个类。 -
图 3.3:获取应用程序模型
extract
:此模块将读取源文档并创建模型对象。
-
一个用于解析命令行选项的函数。
-
一个用于解析选项和执行文件处理的
main()
函数。
此外,还需要这些额外的函数:
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
main()
理念是编写main()
函数以最大化重用。避免日志初始化意味着其他应用程序可以更容易地导入此应用程序的main()
函数以重用数据获取功能。
在main()
函数中初始化日志可能会撤销之前的日志初始化。虽然有一些方法可以让复合应用程序容忍每个main()
函数进行另一次日志初始化,但将此功能重构到重要处理之外似乎更简单。
对于这个项目,我们将探讨模型和提取组件的两种通用设计方法。我们将利用这个机会来强调遵循 SOLID 设计原则的重要性。
未在图中显示的是其他三个子类,它们使用不同的列对从四个系列中创建XYPair
对象。
3.2.1 类设计
如果使用csv.DictReader
,源将从list[str]
变为
我们已将此更改所需的修订作为您的设计工作的一部分。
在许多情况下,似乎使用csv.DictReader
是一个更好的选择。如果 CSV 文件的第一行没有名称,可以提供列名。
model
模块包含一个用于原始XYPair
的单个类定义。稍后,这可能会扩展和改变。目前,它可能看起来像是过度设计。
acquisition
模块包含多个类,它们协同工作以构建任何四个系列中的XYPair
对象。抽象的PairBuilder
类定义了创建XYPair
对象的一般特性。
PairBuilder
类的每个子类都有略微不同的实现。具体来说,Series1Pair
类有一个from_row()
方法,它从x[1,2,3]和y[1]值组装一个对。
这里的大部分图示和示例使用list[str]
作为 CSV 读取器中一行的类型。
首先,我们将展示一个使用类定义的对象式设计。之后,我们将展示一个仅使用函数和无状态对象的功能式设计。
如第一章**中建议的项目零:其他项目的模板*,日志初始化通常如下所示:
本应用程序的类和函数的一个可能结构如图图 3.3所示。
dict[str, str]
。这个虽小但重要的变化将在所有示例中产生连锁反应。
整个Extract
类体现了使用PairBuilder
类的一个实例和一行源数据来构建XYPair
实例的各种算法。build_pair(list[str])
-> XYPair
方法从 CSV 文件解析的一行中提取单个项目。
main()
函数的职责是创建四个PairBuilder
子类的实例。然后,这些实例被用来创建四个Extract
类的实例。这四个Extract
对象可以构建来自每个源行的四个XYPair
对象。
可以使用dataclass.asdict()
函数将XYPair
对象转换为dict[str, str]
对象。这可以通过json.dumps()
序列化并写入适当的输出文件。这种转换操作似乎是一个很好的选择,可以在抽象的PairBuilder
类的方法中实现。这可以用来将XYPair
对象写入打开的文件。
顶层函数main()
和get_options()
可以放在一个名为acquisition
的单独模块中。此模块将从model
和csv_extract
模块导入各种类定义。
审查 SOLID 设计原则通常很有帮助。特别是,我们将仔细研究依赖倒置原则。
3.2.2 设计原则
我们可以通过查看 SOLID 设计原则来确保面向对象设计遵循这些原则。
-
单一职责:每个类似乎只有一个职责。
-
开闭原则:每个类似乎都可以通过添加子类来扩展。
-
里氏替换:
PairBuilder
类层次结构遵循此原则,因为每个子类都与父类相同。 -
接口隔离:每个类的接口都进行了最小化。
-
依赖倒置:关于类之间依赖的问题很微妙。我们将对此进行一些详细的研究。
SOLID 设计原则之一建议避免PairBuilder
子类和XYPair
类之间的紧密耦合。想法是提供一个XYPair
类的协议(或接口)。在类型注解中使用协议将允许提供实现了协议的任何类型给类。使用协议将打破PairBuilder
子类和XYPair
类之间的直接依赖。
这个面向对象设计问题经常出现,通常会导致对类之间的关系和 SOLID 设计原则进行长时间的、仔细的思考。
我们有以下几种选择:
-
在
PairBuilder
类内部直接引用XYPair
类。这将违反依赖倒置原则。 -
使用
Any
作为类型注解。这将def from_row(row: list[str]) -> Any:
。这使得类型注解不那么信息丰富。 -
尝试为结果类型创建一个协议,并在类型注解中使用它。
-
引入一个类型别名,目前只包含一个值。在
model
模块未来的扩展中,可能会引入其他类型。
第四种替代方案为我们提供了所需的类型注解检查的灵活性。想法是在 model
模块中包含一个类似以下的类型别名:
from dataclasses import dataclass
from typing import TypeAlias
@dataclass
class XYPair:
# Definition goes here
RawData: TypeAlias = XYPair
随着替代类的引入,RawData
的定义可以扩展以包含这些替代项。这可能会演变成以下形式:
from dataclasses import dataclass
from typing import TypeAlias
@dataclass
class XYPair:
# Definition goes here
pass
@dataclass
class SomeOtherStructure:
# Some other definition, here
pass
RawData: TypeAlias = XYPair | SomeOtherStructure
这允许在 model
模块演变过程中扩展到 PairBuilder
子类。随着新类的引入,需要更改 RawData
定义。注解检查工具如 mypy 无法检测到 RawData
类型别名替代定义中任何类的无效使用。
在应用程序的其余部分,类和函数可以使用 RawData
作为抽象类定义。这个名字代表了许多替代定义,其中任何一个都可能在使用时被使用。
使用这种 RawData
定义,PairBuilder
子类可以使用以下形式的定义:
from model import RawData, XYPair
from abc import ABC, abstractmethod
class PairBuilder(ABC):
target_class: type[RawData]
@abstractmethod
def from_row(self, row: list[str]) -> RawData:
...
class Series1Pair(PairBuilder):
target_class = XYPair
def from_row(self, row: list[str]) -> RawData:
cls = self.target_class
# the rest of the implementation...
# return cls(arguments based on the value of row)
对于 main()
函数,也有类似的结论。这可以直接与 Extract
类以及 PairBuilder
类的各种子类相关联。在运行时,基于命令行参数注入这些类非常重要。
目前,最简单的方法是提供类名作为默认值。以下函数可能用于获取选项和配置参数:
def get_options(argv: list[str]) -> argparse.Namespace:
defaults = argparse.Namespace(
extract_class=Extract,
series_classes=[Series1Pair, Series2Pair, Series3Pair, Series4Pair],
)
defaults
命名空间作为 ArgumentParser.parse_args()
方法的参数值提供。这个默认值集合在整个应用程序中充当一种依赖注入。main
函数可以使用这些类名构建给定提取类的实例,然后处理给定的源文件。
更高级的 CLI 可以提供选项和参数来定制类名。对于更复杂的应用程序,这些类名将来自配置文件。
面向对象设计的替代方案是函数式设计。我们将在下一节中探讨这个替代方案。
3.2.3 函数式设计
在 类设计 部分中显示的一般模块结构也适用于函数式设计。具有单个类定义的 model
模块也是函数式设计的一部分;这种包含数据类定义集合的模块通常是理想的。
如上所述在 设计原则 部分,model
模块最好通过使用类型变量 RawData
作为可能开发的其他类型的占位符来提供服务。
csv_extract
模块将使用一系列独立的函数来构建 XYPair
对象。每个函数的设计将相似。
这里有一些带有类型注解的示例函数:
def series_1_pair(row: list[str]) -> RawData:
...
def series_2_pair(row: list[str]) -> RawData:
...
def series_3_pair(row: list[str]) -> RawData:
...
def series_4_pair(row: list[str]) -> RawData:
...
这些函数然后可以被extract()
函数使用,为源文件单行表示的四个系列中的每一个创建XYPair
对象。
一种可能的方法是使用以下类型的定义:
SeriesBuilder: TypeVar = Callable[[list[str]], RawData]
def extract(row: list[str], builders: list[SeriesBuilder]) -> list[RawData]:
...
此extract()
函数可以将所有给定的构建函数(从series_1_pair()
到series_4_pair()
)应用到给定的行上,为每个系列创建XYPair
对象。
此设计还需要一个函数来应用dataclass.asdict()
和json.dumps()
,将XYPair
对象转换为可以写入 NDJSON 文件的字符串。
因为使用的函数作为参数值提供,所以应用程序中由各种函数组成的各种函数之间出现依赖问题的可能性很小。设计中的重点是避免在任意位置绑定特定的函数。main()
函数应向extract
函数提供行构建函数。这些函数可以通过命令行参数、配置文件提供,或者在没有提供覆盖值的情况下作为默认值。
我们已经研究了项目的整体目标,以及两种建议的架构方法。现在我们可以转向具体的交付物列表。
3.3 交付物
此项目有以下交付物:
-
docs
文件夹中的文档。 -
tests/features
和tests/steps
文件夹中的验收测试。 -
tests
文件夹中模型模块类的单元测试。 -
csv_extract
模块测试的模拟对象将是单元测试的一部分。 -
tests
文件夹中csv_extract
模块组件的单元测试。 -
应用程序用于从
src
文件夹中的 CSV 文件获取数据。
一个简单的方法是从零目录克隆项目开始这个项目。确保在克隆时更新pyproject.toml
和README.md
;作者经常被旧项目元数据过时的副本所困惑。
我们将更详细地查看其中的一些交付物。我们将从创建验收测试的建议开始。
3.3.1 验收测试
验收测试需要从用户的角度描述整体应用程序的行为。场景将遵循命令行应用程序获取数据和写入输出文件的 UX 概念。这包括成功以及失败时的有用输出。
功能将类似于以下内容:
Feature: Extract four data series from a file with
the peculiar Anscombe Quartet format.
Scenario: When requested, the application extracts all four series.
Given the "Anscombe_quartet_data.csv" source file exists
And the "quartet" directory exists
When we run
command "python src/acquire.py -o quartet Anscombe_quartet_data.csv"
Then the "quartet/series_1.json" file exists
And the "quartet/series_2.json" file exists
And the "quartet/series_3.json" file exists
And the "quartet/series_3.json" file exists
And the "quartet/series_1.json" file starts with
’{"x": "10.0", "y": "8.04"}’
这个更复杂的功能将需要几个步骤定义。这些包括以下内容:
-
@given(‘The “{name}” source file exists’)
。此函数应将示例文件从源数据目录复制到用于运行测试的临时目录。 -
@given(‘the “{name}” directory exists’)
。此函数可以在运行测试的目录下创建名为的目录。 -
@then(‘the “{name}” file exists’)
。此函数可以检查输出目录中是否存在名为的文件。 -
@then(‘the`` "quartet/series_1.json"`` file`` starts`` with`` …)
。此函数将检查输出文件的第一行。如果测试失败,显示文件内容将有助于调试问题。简单的assert
语句可能不是最佳选择;需要更复杂的if
语句来写入调试输出并引发AssertionError
异常。
因为正在测试的应用程序消耗和生成文件,最好利用behave工具的environment.py
模块定义两个函数来创建(并销毁)在运行测试时使用的临时目录。以下两个函数被behave用于此目的:
-
before_scenario(context,
scenario)
:此函数可以创建一个目录。tempfile
模块的mkdtemp()
函数可以处理此操作。需要将目录放入上下文中,以便可以删除。 -
after_scenario(context,
scenario)
:此函数可以删除临时目录。
其中一个Then
子句的格式存在微小的内部不一致性。以下使用"
和’
的混合来明确指出值插入文本的位置:
And the "quartet/series_1.json" file starts with’{"x": "10.0", "y": "8.04"}’
有些人可能会被这种不一致性所困扰。一个选择是始终一致地使用’
。当特征文件不多时,这种普遍的改变很容易实现。在整个书中,我们将保持不一致,将是否进行更改以保持一致性的决定留给您。
此外,请注意When
子句命令相当长且复杂。在编写此类测试时的一般建议是使用命令的摘要并将细节推入步骤实现函数。我们将在命令变得更长和更复杂时在后面的章节中讨论这一点。
除了应用程序正常工作的情况外,我们还需要考虑当出现问题时应用程序的行为。在下一节中,我们将讨论各种可能出错的方式以及应用程序应该如何表现。
3.3.2 其他验收场景
建议的验收测试仅覆盖一个场景。这个单一的场景——即一切正常工作——可以称为“快乐路径”。明智的做法是包括各种错误发生的场景,以确保应用程序在面对问题时是可靠和健壮的。以下是一些建议的错误场景:
-
假设
Anscombe_quartet_data.csv
源文件不存在。 -
假设
quartet
目录不存在。 -
当我们运行命令
python`` src/acquire.py`` --unknown`` option
-
假设
Anscombe_quartet_data.csv
源文件存在,但文件格式不正确。存在多种格式问题。-
文件为空。
-
文件不是一个合适的 CSV 文件,而是某种其他格式。
-
文件内容处于有效的 CSV 格式,但列名与预期的列名不匹配。
-
每个不愉快的路径都需要检查日志文件,以确保它包含预期的错误消息。behave工具可以捕获日志信息。每个步骤函数中可用的context
具有包含捕获的日志输出的属性。具体来说,context.log_capture
包含一个可以搜索错误消息的LogCapture
对象。
请参阅behave.readthedocs.io/en/stable/api.html#behave.runner.Context
以了解上下文的内容。
这些不愉快的路径场景将与以下内容相似:
Scenario: When the file does not exist, the log has the expected
error message.
Given the "Anscombe_quartet_data.csv" source file does not exist
And the "quartet" directory exists
When we run command "python src/acquire.py -o quartet
Anscombe_quartet_data.csv"
Then the log contains "File not found: Anscombe_quartet_data.csv"
这也将需要一些新的步骤定义来处理新的Given
和Then
步骤。
当使用 Gherkin 时,建立清晰的语言和一致的术语是有帮助的。这可以允许几个步骤定义适用于大量场景。在编写几个场景之后,识别相似性是一种常见经验,然后选择修改场景以简化并标准化步骤。
behave工具将提取缺失的功能定义。代码片段可以复制并粘贴到步骤模块中。
接受测试覆盖了应用程序的整体行为。我们还需要测试作为单独代码单元的各个组件。在下一节中,我们将查看单元测试以及这些测试所需的模拟对象。
3.3.3 单元测试
在架构方法中有两种建议的应用架构。基于类的设计包括两个功能和多个类。这些类和功能都应该单独进行测试。
功能设计包括多个功能。这些需要在单独的情况下进行测试。一些开发者发现,为了单元测试,隔离功能定义更容易。这通常是因为类定义可能有明确的依赖关系,这些依赖关系很难打破。
我们将详细查看一些测试模块。我们将从model
模块的测试开始。
单元测试模型
model
模块只有一个类,而这个类实际上并没有做很多。这使得测试相对容易。一个类似于以下测试函数应该是足够的:
from unittest.mock import sentinel
from dataclasses import asdict
def test_xypair():
pair = XYPair(x=sentinel.X, y=sentinel.Y)
assert pair.x == sentinel.X
assert pair.y == sentinel.Y
assert asdict(pair) == {"x": sentinel.X, "y": sentinel.Y}
此测试使用unittest.mock
模块中的sentinel
对象。每个sentinel
属性——例如,sentinel.X
——是一个独特的对象。它们作为参数值很容易提供,并且在结果中很容易被发现。
除了测试model
模块之外,我们还需要测试csv_extract
模块以及整个acquire
应用程序。在下一节中,我们将查看提取单元测试用例。
单元测试 PairBuilder 类层次结构
在遵循面向对象设计时,建议的方法是创建一个PairBuilder
类层次结构。每个子类将执行略微不同的操作来构建XYPair
类的实例。
理想情况下,PairBuilder
子类的实现不应与 XYPair
类紧密耦合。在 设计原则 部分有一些关于如何通过类型注解支持依赖注入的建议。具体来说,model
模块最好通过使用类型变量 RawData
作为可能开发的任何额外类型的占位符来提供服务。
在测试时,我们希望用模拟类替换这个类,以确保对 RawData
类族(目前仅有一个类,XYPair
)的接口得到尊重。
一个 Mock
对象(使用 unittest.mock
模块构建)作为替换类效果很好。它可以用于 PairBuilder
类的子类中的 XYPair
类。
测试将类似于以下示例:
from unittest.mock import Mock, sentinel, call
def test_series1pair():
mock_raw_class = Mock()
p1 = Series1Pair()
p1.target_class = mock_raw_class
xypair = p1.from_row([sentinel.X, sentinel.Y])
assert mock_raw_class.mock_calls == [
call(sentinel.X, sentinel.Y)
]
想法是使用 Mock
对象来替换 Series1Pair
类中定义的特定类。在 from_row()
方法评估后,测试用例确认模拟类恰好被调用一次,并带有预期的两个 sentinel
对象。进一步的检查将确认 xypair
的值也是一个模拟对象。
这种 Mock
对象的使用保证了没有对对象进行额外的、意外的处理。创建新的 XYPair
的接口由 Series1Pair
类正确执行。
对其他配对构建类也需要进行类似的测试。
除了测试 model
和 csv_extract
模块外,我们还需要测试整个 acquire
应用程序。在下一节中,我们将查看 acquire
应用程序单元测试用例。
单元测试剩余组件
对整个 Extract
类的测试用例也需要使用 Mock
对象来替换 csv.reader
和 PairBuilder
子类的实例。
如上所述在 功能设计 部分中,main()
函数需要避免显式命名类或函数。名称需要通过命令行参数、配置文件或默认值提供。
单元测试应该使用 Mock
对象来测试 main()
函数,以确保它具有灵活性和扩展性。
3.4 摘要
本章介绍了第一个项目,数据采集基础应用程序。该应用程序从一个具有复杂结构的 CSV 文件中提取数据,从一个文件中创建四个独立的数据点系列。
为了使应用程序完整,我们包括了命令行界面和日志记录。这将确保应用程序在受控的生产环境中表现良好。
过程的一个重要部分是设计一个可以扩展以处理来自各种来源和格式的数据的应用程序。基本应用程序包含实现非常小的模块,作为后续扩展的基础。
可能这个项目最困难的部分是创建一套验收测试来描述适当的行为。开发者通常会将测试代码的量与应用程序代码的量进行比较,并声称测试占用了“太多”的时间。
实用主义地讲,没有自动化测试的程序是不可信的。测试与它们所执行的代码一样重要。
单元测试——表面上——更简单。使用模拟对象确保每个类都是独立测试的。
这个基础应用程序作为接下来几章的基石。下一章将添加 RESTful API 请求。之后,我们将对这个基础进行数据库访问。
3.5 额外内容
这里有一些想法供您添加到这个项目中。
3.5.1 日志增强
我们简要地提到了日志记录,只是建议它很重要,并且日志的初始化应该与main()
函数内的处理保持分离。
虽然logging
模块非常复杂,但探索它是有帮助的。我们将从日志“级别”开始。
我们的大多数日志消息都将使用INFO
级别的日志创建。例如:
logger.info("%d rows processed", input_count)
此应用程序有许多可能出现的错误情况,最好用错误级别的日志来反映。
此外,还有一个命名的日志记录器树。根日志记录器,名为""
,具有适用于所有较低级别日志记录器的设置。这个树通常与对象继承通常用于创建类和子类的方式平行。这可以使得为每个类创建日志记录器具有优势。这允许将日志级别设置为调试,以便于多个类中的一个,从而允许更集中的消息。
这通常通过日志配置文件来处理。此文件提供了日志配置,并避免了通过命令行选项设置日志功能可能带来的潜在复杂性。
有三个额外内容需要添加到这个项目中:
-
为每个单独的类创建日志记录器。
-
添加调试级别的信息。例如,
from_row()
函数是一个可能有助于理解为什么输出文件不正确的调试位置。 -
从初始化文件中获取日志配置。考虑使用TOML格式的文件作为INI格式的替代,后者是
logging
模块的一部分。
3.5.2 配置扩展
我们已经简要描述了此应用程序的 CLI。本章提供了一些预期行为的示例。除了命令行参数外,拥有一个提供应用程序工作方式的缓慢变化细节的配置文件也有帮助。
在设计原则部分的讨论中,我们仔细研究了依赖倒置。目的是避免类之间有显式的依赖关系。我们希望“反转”这种关系,使其变得间接。想法是在运行时通过参数注入类名。
初始时,我们可以做如下操作:
EXTRACT_CLASS: type[Extract] = Extract
BUILDER_CLASSES: list[type[PairBuilder]] = [
Series1Pair, Series2Pair, Series3Pair, Series4Pair]
def main(argv: list[str]) -> None:
builders = [cls() for vls in BUILDER_CLASSES]
extractor = EXTRACT_CLASS(builders)
# etc.
这提供了参数化的基础级别。一些全局变量被用来“注入”运行时类。这些初始化可以移动到ArgumentParser.parse_args()
方法的argparse.Namespace
初始化值中。
这个argparse.Namespace
对象的初始值可以是字面值,本质上与上一个示例中显示的全局变量参数化相同。
从一个与应用程序代码分开的参数文件中获取初始值更为灵活。这允许在不接触应用程序的情况下更改配置,并通过无意中的打字错误引入错误。
对于配置文件,有两种流行的替代方案可以用来微调应用程序。这些是:
-
一个由应用程序导入的独立的 Python 模块。对于这个用途,模块名如
config.py
很受欢迎。 -
一个由应用程序读取的非 Python 文本文件。由
tomllib
模块解析的 TOML 文件格式是理想的。
从 Python 3.11 版本开始,tomllib
模块作为标准库的一部分直接可用。较旧版本的 Python 应升级到 3.11 或更高版本。
当与 TOML 文件一起工作时,类名将是一个字符串。将类名从字符串转换为类对象的一个简单可靠的方法是使用eval()
函数。另一种方法是提供一个包含类名字符串和类对象的字典。可以通过这种映射解析类名。
一些开发者担心eval()
函数允许一类邪恶超级天才以某种方式修改配置文件,从而导致应用程序崩溃。
这些开发者没有注意到整个 Python 应用程序是纯文本的。邪恶超级天才更容易编辑应用程序,并且不需要对参数文件进行复杂的、恶意的操作。
此外,普通的操作系统级别的所有权和权限可以限制对参数文件的访问,仅限于少数可信赖的个人。
不要忘记包括解析参数文件的单元测试用例。此外,一个无效参数文件的验收测试用例将是这个项目的重要部分。
3.5.3 数据子集
要处理大文件,将需要提取数据的一个子集。这涉及到添加以下类似的功能:
-
创建一个
Extract
类的子类,该子类对创建的行数有一个上限。这涉及到许多单元测试。 -
更新 CLI 选项以包括一个可选的上限。这也将涉及一些额外的单元测试用例。
-
更新验收测试用例以显示使用上限的操作。
注意,从Extract
类切换到SubsetExtract
类应该基于一个可选的命令行参数。如果没有提供--limit
选项,则使用Extract
类。如果提供了--limit
选项(并且是一个有效的整数),则使用SubsetExtract
类。这将导致一系列有趣的单元测试案例,以确保命令行解析正常工作。
3.5.4 另一个示例数据源
对于这个应用来说,最重要的额外工作可能是找到另一个对你感兴趣的数据源。
请参阅CO2 PPM —大气二氧化碳趋势数据集,可在datahub.io/core/co2-ppm
找到,这是一些相对较大的数据。这个数据集有几个奇特的特殊值,我们将在第六章,项目 2.1:数据检查笔记本中进行探讨。
这个项目将需要你手动下载和解压文件。在后面的章节中,我们将探讨如何自动化这两个步骤。具体请参阅第四章,数据获取功能:Web API 和抓取,其中将扩展这个基础项目,以正确地从 CSV 文件获取原始数据。
重要的是找到一种 CSV 格式的数据源,并且足够小,可以在几秒钟内处理。对于大型文件,将需要提取数据的一个子集。有关处理大量数据集的建议,请参阅数据子集。
第四章
数据获取功能:Web API 和抓取
数据分析通常与来自多个来源的数据一起工作,包括数据库、Web 服务和由其他应用程序准备好的文件。在本章中,您将指导完成两个项目,以向上一章的基线应用程序添加额外的数据源。这些新源包括 Web 服务查询和从网页抓取数据。
本章的项目涵盖了以下基本技能:
-
使用requests包进行 Web API 集成。我们将探讨 Kaggle API,它需要注册以创建 API 令牌。
-
使用Beautiful Soup包解析 HTML 网页。
-
向现有应用程序添加功能并扩展测试套件以涵盖这些新的替代数据源。
认识到这个应用程序在数据获取方面有狭窄的焦点是很重要的。在后面的章节中,我们将验证数据并将其转换为更有用的形式。这反映了以下不同关注点的分离:
-
从源下载和提取数据是本章和下一章的重点。
-
检查从第六章、项目 2.1:数据检查笔记本开始。
-
验证和清理数据从第九章、项目 3.1:数据清理基础应用开始。
处理流程中的每个阶段都分配给不同的项目。更多背景信息,请参阅第二章、项目概述。
我们将首先探讨使用 API 和 RESTful Web 服务获取数据。这将侧重于 Kaggle 网站,这意味着您需要注册 Kaggle 以获取自己的唯一 API 密钥。第二个项目将从不提供有用 API 的网站上抓取 HTML 内容。
4.1 项目 1.2:从 Web 服务获取数据
需要由 Web API 提供的数据是很常见的。一种常见的 Web 服务设计方法称为 RESTful;它基于与使用 HTTP 协议传输对象状态表示相关的一系列概念。
更多关于 RESTful 服务的信息,请参阅构建 RESTful Python Web 服务(www.packtpub.com/product/building-restful-python-web-services/9781786462251
)。
RESTful 服务通常涉及使用 HTTP 协议响应用户应用程序的请求。请求类型包括 get、post、put、patch 和 delete 等动词。在许多情况下,服务以 JSON 文档响应。也有可能接收一个包含 NDJSON 文档流的文件,甚至是一个包含数据 ZIP 存档的文件。
我们将从对应用程序的描述开始,然后转向讨论架构方法。这将随后是一个详细的交付物清单。
4.1.1 描述
分析师和决策者需要获取数据以进行进一步分析。在这种情况下,数据可以从 RESTful 网络服务中获得。最有趣的小数据集之一是安斯康姆四重奏 – www.kaggle.com/datasets/carlmcbrideellis/data-anscombes-quartet
本应用程序的部分是第九章项目 3.1:数据清洗基础应用程序中项目的扩展。此应用程序的基本行为将与之前的类似。本项目将使用 CLI 应用程序从源抓取数据。
用户体验(UX)也将是一个命令行应用程序,具有微调正在收集的数据的选项。我们期望的命令行可能如下所示:
% python src/acquire.py -o quartet -k ~/Downloads/kaggle.json \
--zip carlmcbrideellis/data-anscombes-quartet
-o
quartet
参数指定了一个目录,四个结果将被写入其中。这些将具有类似 quartet/series_1.json
的名称。
-k
kaggle.json
参数是包含用户名和 Kaggle API 令牌的文件名。这个文件与应用程序软件分开保存。在示例中,该文件位于作者的 下载
文件夹中。
--zip
参数提供了“参考”——所有者和数据集名称——以打开和提取。这些信息可以通过检查 Kaggle 界面的详细信息来找到。
另一个功能是获取 Kaggle 数据集的筛选列表。这应该是一个单独的 --search
操作,可以捆绑到一个单一的应用程序中。
% python src/acquire.py --search -k ~/Downloads/kaggle.json
这将对一些搜索标准进行应用,以输出符合要求的数据集列表。这些列表往往相当庞大,因此需要谨慎使用。
文件中的凭证用于发出 Kaggle API 请求。在接下来的几节中,我们将一般性地查看 Kaggle API。之后,我们将查看定位目标数据集引用所需的具体请求。
Kaggle API
有关 Kaggle API 的信息,请参阅 www.kaggle.com/docs/api
。这份文档描述了一些使用 API 的命令行代码(Python 语言)。
RESTful API 请求的技术细节可以在 github.com/Kaggle/kaggle-api/blob/master/KaggleSwagger.yaml
找到。这份文档描述了 Kaggle API 服务器端的请求和响应。
要使用 RESTful API 或命令行应用程序,您应该在 Kaggle 上注册。首先,在 Kaggle.com
上注册。然后,导航到公共个人资料页面。在这个页面上,有一个 API 部分。这个部分有您将使用的按钮,用于为您注册的用户名生成唯一的 API 令牌。
第三步是点击 创建新令牌 按钮,以创建令牌文件。这将下载一个包含您注册的用户名和唯一密钥的小型 JSON 文件。这些凭证是 Kaggle REST API 所必需的。
文件的所有权可以被所有者更改为只读。在 Linux 和 macOS 上,这可以通过以下命令完成:
% chmod 400 ~/Downloads/kaggle.json
不要将名为 kaggle.json
的 Kaggle 凭证文件移动到包含你代码的目录中。这样做很诱人,但这是一个严重的安全错误,因为文件可能会被保存到代码仓库中,任何人浏览你的代码时都能看到。在一些企业中,即使在内部仓库中发布密钥也是安全漏洞,这可能是解雇员工的好理由。
由于 Git 保存了非常完整的历史记录,因此很难删除包含密钥的提交。
请将凭证文件与你的代码分开存放。
还有一个好主意是将 kaggle.json
添加到 .gitignore
文件中,以确保它不会作为提交的一部分被上传。
关于源数据
本项目将探索两种不同的源数据。这两个源都有相同的 www.kaggle.com/api/v1/
基本路径。尝试查询此基本路径不会提供有用的响应;它只是构建用于定位特定资源的路径的起点。
-
包含数据集摘要或数据集元数据的 JSON 文档。这些来自于将
datasets/list
添加到基本路径。 -
包含我们将用作示例的数据的 ZIP 归档。这来自于将
datasets/download/{ownerSlug}/{datasetSlug}
添加到基本路径。ownerSlug
的值是 ”carlmcbrideellis”。datasetSlug
的值是 ”data-anscombes-quartet”。给定数据集有一个ref
值,作为具有所需 ”ownerSlug/datasetSlug” 格式的参考字符串。
JSON 文档需要一个函数来提取一些相关的字段,如 title
、ref
、url
和 totalBytes
。这个可用的元数据子集可以更容易地定位有用的、有趣的数据集。还有许多其他可用于搜索的属性,如 usabilityRating
;这些属性可以区分好的数据集与实验或课堂作业。
建议的数据集——Anscombe 四重奏——作为一个包含单个项目的 ZIP 压缩归档提供。这意味着应用程序必须处理 ZIP 归档并展开归档中的文件。Python 提供了 zipfile
包来处理在归档中定位 CSV 文件。一旦找到这个文件,就可以使用上一章中现有的编程(第三章,项目 1.1:数据获取基础应用程序)。
Kaggle 上有成千上万的数据集。我们将在 附加内容 中建议一些 Anscombe 四重奏的替代方案。
本节探讨了该应用程序的输入、处理和输出。在下一节中,我们将探讨该软件的整体架构。
4.1.2 方法
在审视我们的方法时,我们将借鉴 C4 模型(c4model.com
)的一些指导原则:
-
上下文:对于这个项目,上下文图将显示用户从源提取数据。你可能觉得绘制这个图有帮助。
-
容器:一个是用户的个人电脑。另一个容器是 Kaggle 网站,它提供数据。
-
组件:我们将讨论以下组件。
-
代码:我们将简要提及,以提供一些建议的方向。
考虑这个应用程序是第三章,项目 1.1:数据采集基础应用项目的扩展是很重要的。该章节提供了架构设计的基线。
在这个项目中,我们将添加一个新的kaggle_client
模块以下载数据。acquire
模块中的整体应用程序将改变以使用这个新模块。其他模块应保持不变。
遗留组件图如图图 4.1所示。
图 4.1:遗留组件
新架构可以处理 JSON 数据集列表的检查以及单个 ZIP 文件的下载。这如图图 4.2所示。
图 4.2:修订后的组件设计
这里的新模块是kaggle_client
模块。它有一个名为RestAccess
的类,提供了访问 Kaggle 数据的方法。它可以访问 Kaggle 数据集集合并检索所需的 ZIP 文件。还可以添加其他方法来检查数据集列表或获取数据集元数据。
RestAccess
类使用kaggle.json
文件的内容进行初始化。作为初始化的一部分,它可以创建所有后续调用所需的认证对象。
在以下章节中,我们将查看RestAccess
类的这些功能:
-
以通用方式发出 API 请求。
-
获取 ZIP 存档。
-
获取数据集列表。
-
处理速率限制响应。
我们将从最重要的功能开始,即以通用方式发出 API 请求。
发出 API 请求
组件图显示requests
包是访问 RESTful API 的首选方式。这个包应该添加到项目的pyproject.toml
文件中,并在项目的虚拟环境中安装。
使用urllib
包发出 RESTful API 请求也是合理的。这是标准库的一部分。它工作得很好,不需要额外的安装。然而,代码可能会变得相当复杂,因此它不如requests
包那样被高度推荐。
使用requests
的基本好处是创建一个认证对象并在每个请求中提供它。我们经常使用如下示例代码:
import json
from pathlib import Path
import requests.auth
keypath = Path.home() / "Downloads" / "kaggle.json"
with keypath.open() as keyfile:
credentials = json.load(keyfile)
auth = requests.auth.HTTPBasicAuth(
credentials[’username’], credentials[’key’]
)
这可以是RestAccess
类的__init__()
方法的一部分。
这里创建的auth
对象可以用来进行所有后续请求。这将提供必要的用户名和 API 令牌以验证用户。这意味着其他方法可以使用requests.get()
,并带有auth=self.auth
的关键字参数值。这将正确构建每个请求中所需的Authorization
头。
一旦类被正确初始化,我们就可以查看下载 ZIP 存档的方法
下载 ZIP 存档
RestAccess
类需要一个get_zip()
方法来下载 ZIP 文件。参数是请求的数据集的下载 URL。
为此数据集构建此 URL 的最佳方法是将三个字符串组合起来:
-
API 的基本地址,
https://www.kaggle.com/api/v1
。 -
下载路径,
/datasets/download/
。 -
参考是一个具有以下形式的字符串:
{ownerSlug}/{datasetSlug}
。
这是在 Python f-string 替换 URL 模式中引用的理想位置。
get_zip()
方法的输出应该是一个Path
对象。在某些情况下,ZIP 存档非常大,无法完全在内存中处理。在这些极端情况下,需要更复杂的分块下载。对于本项目使用的这些较小的文件,下载可以完全在内存中处理。一旦 ZIP 文件被写入,这个RestAccess
类的客户端就可以打开它并提取有用的成员。
一个单独的客户端函数或类将处理 ZIP 存档文件的内容。以下内容不属于RestAccess
类,而是属于使用RestAccess
类的某个客户端类或函数的一部分。
使用两个嵌套的with
上下文可以处理存档的元素。它们将这样工作:
-
外部
with
语句使用zipfile
模块打开存档,创建一个ZipFile
实例。 -
内部
with
语句可以打开包含安斯康姆四重奏 CSV 文件的特定成员。在这个上下文中,应用程序可以创建一个csv.DictReader
并使用现有的Extract
类来读取和处理数据。
重要的是我们不需要解压 ZIP 存档,也不需要在我们的存储中散布未解压的文件。应用程序可以使用ZipFile.open()
方法打开并处理元素。
除了下载 ZIP 存档外,我们还可能想调查可用的数据集。为此,一个特殊的迭代方法很有帮助。我们将在下一节中查看它。
获取数据集列表
数据集目录是通过以下路径找到的:
www.kaggle.com/api/v1/datasets/list
RestAccess
类可以有一个dataset_iter()
方法来遍历数据集集合。这对于定位其他数据集很有帮助。对于寻找安斯康姆四重奏,这不是必需的,因为ownerSlug
和datasetSlug
引用信息已经已知。
此方法可以通过requests.get()
函数向此 URL 发出GET
请求。响应将是可用的 Kaggle 数据集的第一页。结果以分页形式提供,并且每个请求都需要提供一个页面号参数以获取后续页面。
每页的结果将是一个包含一系列字典对象的 JSON 文档。它具有以下类型的结构:
[
{"id": some_number, "ref": "username/dataset", "title": ...},
{"id": another_number, "ref": "username/dataset", "title": ...},
etc.
]
这种两层结构——包含每个页面内的页面和项目——是使用生成器函数遍历页面的理想场所。在外部循环中,内部迭代可以生成每个页面的单个数据集行。
这种嵌套迭代可能看起来像以下代码片段:
def dataset_iter(url: str, query: dict[str, str]) ->
Iterator[dict[str, str]]:
page = 1
while True:
response = requests.get(url, params=quert | {"page": str(page)})
if response.status_code == 200:
details = response.json()
if details:
yield from iter(details)
page += 1
else:
break
elif response.status_code == 429:
# Too Many Requests
# Pause and try again processing goes here...
pass
else:
# Unexpected response
# Error processing goes here...
break
这显示了嵌套处理的while
语句何时结束,即当响应包含一个结果为零的页面时。处理过多请求的步骤被省略。同样,意外响应的记录也被省略。
客户端函数将使用RestAccess
类来扫描数据集,如下面的示例所示:
keypath = Path.home()/"Downloads"/"kaggle.json"
with keypath.open() as keyfile:
credentials = json.load(keyfile)
reader = Access(credentials)
for row in reader.dataset_iter(list_url):
print(row[’title’], row[’ref’], row[’url’], row[’totalBytes’])
这将处理由RestReader
对象reader
返回的所有数据集描述。dataset_iter()
方法需要接受一个query
参数,该参数可以限制搜索范围。我们鼓励您阅读 OpenAPI 规范,以了解query
参数可能的选项。这些值将成为 HTTP GET
请求中的查询字符串的一部分。
这是接口的正式定义:
github.com/Kaggle/kaggle-api/blob/master/KaggleSwagger.yaml
一些查询参数包括以下内容:
-
filetype
查询有助于定位 JSON 或 CSV 格式的数据。 -
maxSize
查询可以限制数据集的大小在合理范围内。对于初始探索,1MB 是一个良好的上限。
初始的峰值解决方案——不考虑速率限制——至少会显示 80 页可能的数据集。处理速率限制响应会产生更广泛的结果,但会花费一些等待时间。在下一节中,我们将扩展此方法以处理错误响应。
速率限制
与许多 API 一样,Kaggle API 通过速率限制来避免拒绝服务(DoS)攻击。更多信息请参阅cheatsheetseries.owasp.org/cheatsheets/Denial_of_Service_Cheat_Sheet.html
。
每个用户每秒都有一定数量的请求限制。虽然对于大多数用途来说这个限制很宽松,但它往往会防止对所有数据集进行简单扫描。
Kaggle 响应中的状态码 429 告诉客户端应用程序已发出过多请求。这个“过多请求”错误响应将包含一个键为Retry-After
的标题。此标题的值是下一次请求可以发出之前的超时间隔(以秒为单位)。
一个可靠的应用程序将有一个能够优雅处理 429 与 200 响应的结构。前一个示例中有一个简单的 if
语句来检查条件 if response.status_code == 200
。这需要扩展以处理这三个替代方案:
-
状态码 200 是一个良好的响应。如果页面有任何详细信息,可以对其进行处理;
page
的值可以增加。否则,没有更多数据,这使得从包含的while
语句中退出是合适的。 -
状态码 429 表示请求过多。获取
Retry-After
的值并在此期间休眠。 -
任何其他状态码表示存在问题,应该记录或抛出异常。
一个可能的算法用于返回行和处理速率限制延迟,如图 4.3 所示。
图 4.3:Kaggle 速率限制分页
处理速率限制将使应用程序更容易使用。它还将产生更完整的结果。使用有效的搜索过滤器将行数减少到合理的水平将节省大量的重试延迟等待时间。
main()
函数
当前应用程序设计具有以下独特功能:
-
从本地 CSV 文件中提取数据。
-
下载 ZIP 存档并从存档的 CSV 成员中提取数据。
-
(可选)调查数据集列表以找到其他有趣的数据集进行处理。
这表明我们的 main()
函数应该是一个容器,包含三个实现每个单独功能的独立函数。main()
函数可以解析命令行参数,然后做出一系列决定:
-
本地提取:如果存在
-o
选项(没有-k
选项),则这是一个本地文件提取。这是前面章节中的解决方案。 -
下载和提取:如果存在
-k
和-o
选项,则这将是一个下载和提取操作。它将使用RestAccess
对象获取 ZIP 存档。一旦存档打开,成员处理是前面章节中的解决方案。 -
调查:如果存在
-k
和-s
(或--search
)选项,则这是对有趣数据集的搜索。鼓励您设计参数以向应用程序提供所需的查询参数。 -
其他情况:如果上述任何模式都不匹配选项,这是不连贯的,应该抛出异常。
这些功能中的每一个都需要一个不同的函数。一个常见的替代设计方案是使用 命令 模式,并创建一个类层次结构,其中每个功能作为一个父类的不同子类。
一个中心思想是保持 main()
函数小巧,并将详细工作分配给其他函数或对象。
另一个核心思想是不要重复自己(DRY)。这一原则使得在“下载并提取”功能和“本地提取”功能之间永远不要复制和粘贴代码变得至关重要。“下载并提取”处理必须通过子类继承或从一个函数调用另一个函数来重用“本地提取”处理。
现在我们有了技术方法,是时候看看这个项目的交付成果了。
4.1.3 交付成果
本项目有以下交付成果:
-
docs
文件夹中的文档。 -
tests/features
和tests/steps
文件夹中的验收测试。 -
一个微型 RESTful Web 服务,提供测试响应,将是验收测试的一部分。
-
tests
文件夹中的应用程序模块的单元测试。 -
requests
模块的模拟对象将是单元测试的一部分。 -
下载并从 RESTful Web 服务获取数据的程序。
确保在pyproject.toml
文件中包含额外的包,如requests
和beautifulsoup4
。可以使用pip-compile命令创建一个requirements.txt
文件,该文件可用于tox工具进行测试。
我们将更详细地查看其中的一些交付成果。
RestAccess 类的单元测试
对于单元测试,我们不希望涉及requests
模块。相反,我们需要为requests
模块创建一个模拟接口,以确认应用程序RestAccess
模块正确使用了requests
类。
有两种策略用于插入模拟对象:
-
实现一种依赖注入技术,其中目标类在运行时命名。
-
使用猴子补丁在测试时注入模拟类。
当与外部模块(我们无法控制其设计的模块)一起工作时,猴子补丁通常比尝试使用依赖注入技术更容易。当我们在一个模块中构建类时,我们经常需要通过子类扩展定义。创建独特、定制软件的一个原因是为了快速实现应用程序独特功能的变更。非独特功能(在本例中为 RESTful API 请求)变化非常缓慢,并且不受益于灵活性。
我们想创建两个模拟类,一个用于替换requests.auth.HTTPBasicAuth
类,另一个用于替换requests.get()
函数。HTTPBasicAuth
类的模拟不做任何事情;我们想检查模拟对象是否被一次且仅一次以正确的参数调用。requests.get()
函数的模拟需要为各种测试场景创建模拟Response
对象。
我们需要使用pytest
模块的monkeypatch
配置来用模拟对象替换真实对象,以便进行单元测试。
策略是创建具有以下示例类似结构的单元测试:
from unittest.mock import Mock, sentinel, call
def test_rest_access(monkeypatch):
mock_auth_class = Mock(
name="Mocked HTTPBasicAuth class",
return_value=sentinel.AUTH
)
monkeypatch.setattr(’requests.auth.HTTPBasicAuth’, mock_auth_class)
mock_kaggle_json = {"username": sentinel.USERNAME, "key": sentinel.KEY}
access = RestAccess(mock_kaggle_json)
assert access.credentials == sentinel.AUTH
assert mock_auth_class.mock_calls == [
call(sentinel.USERNAME, sentinel.KEY)
]
此测试用例为HTTPBasicAuth
类创建了一个模拟对象。当调用该类以创建实例时,它返回一个可以被测试用例验证的sentinel
对象。
monkeypatch
固定装置用模拟对象替换了requests.auth.HTTPBasicAuth
类。在此修补程序应用后,当RestAccess
类初始化尝试创建HTTPBasicAuth
类的实例时,它将调用模拟,并得到一个sentinel
对象。
该用例确认了RestAccess
实例使用了sentinel
对象。测试用例还确认了模拟类恰好被调用了一次,使用从kaggle.json
文件加载的模拟值。
这个测试用例依赖于查看RestAccess
实例内部。这不是编写单元测试的最佳策略。一个更好的方法是提供一个模拟对象给requests.get()
。测试用例应确认requests.get()
使用关键字参数auth
,其参数值为sentinel.AUTH
对象。这种测试策略的想法是检查RestAccess
类的外部接口,而不是查看内部状态变化。
验收测试
验收测试需要依赖于一个模拟 Kaggle 网络服务的固定装置。模拟将在你的本地计算机上作为一个进程,这使得停止和启动模拟服务以测试应用程序变得容易。使用地址127.0.0.1:8080
而不是www.kaggle.com
将使 RESTful API 请求返回到你的计算机。可以使用localhost:8080
这个名字代替数字地址127.0.0.1:8080
。(这个地址被称为环回地址,因为请求会回环到创建它们的同一主机,允许在没有外部网络流量的情况下进行测试。)
注意,URL 方案也将从https:
更改为http:
。我们不想为验收测试实现完整的 Socket 安全层(SSL)。为了我们的目的,我们可以信任这些组件是有效的。
这种对 URL 的更改表明应用程序应该设计成以配置参数提供每个 URL 的https://www.kaggle.com
部分。然后验收测试可以使用http://127.0.0.1:8080
而不需要对代码进行任何更改。
模拟服务必须提供 Kaggle 服务的一些功能。本地服务需要正确响应dataset/download
请求,提供一个包含预期状态码和正确 ZIP 存档字节的回复。
这个模拟服务将作为一个独立的应用程序运行。它将由behave启动(并停止),以应对需要固定装置的场景。
我们首先将查看这个服务在功能文件中的描述方式。这将引导我们了解如何构建模拟服务。之后,我们可以查看这是如何通过behave步骤定义来实现的。
功能文件
下载功能显然与数据获取功能是分开的。这表明需要一个新的.feature
文件来提供描述此功能的场景。
在这个新功能文件中,我们可以有具体命名所需固定装置的场景。一个场景可能看起来像以下示例:
@fixture.kaggle_server
Scenario: Request for carlmcbrideellis/data-anscombes-quartet
extracts file from ZIP archive.
A typical download command might be
"python src/acquire.py -k kaggle.json -o quartet \
--zip carlmcbrideellis/data-anscombes-quartet"
Given proper keys are in "kaggle.json"
When we run the kaggle download command
Then log has INFO line with "header: [’mock’, ’data’]"
And log has INFO line with "count: 1"
@fixture.
标签遵循将特定固定装置与场景关联的常见标记约定。除了指定要使用的固定装置外,还有许多其他用途可以标记场景。
在以前的项目中,在When
步骤中提供了一个运行应用的命令。对于这个场景(以及许多其他场景),命令文本变得太长,无法在 Gherkin 文本中有用。这意味着实际的命令需要由实现此步骤的函数提供。
此场景的Then
步骤检查应用程序创建的日志以确认文件内容。
测试场景是整体应用需求设计的组成部分。
在提供的描述中,没有提到日志。这种类型的差距很常见。提供的测试场景对从普通测试描述中省略的功能进行了额外的定义。
有些人喜欢将文档更新得完整且完全一致。我们在处理涉及众多利益相关者的企业应用时,鼓励灵活性。在初始演示或文档中获取每个人的意见可能很困难。有时,在详细讨论更具体的问题,如预期的操作场景时,需求会在过程中出现。
标签信息将被behave工具使用。我们将探讨如何编写一个before_tag()
函数来启动(并停止)任何需要它的特殊模拟服务器。
在我们查看通过步骤定义进行behave集成之前,我们将探讨两种测试客户端应用的方法。核心概念是创建数据获取应用使用的 Kaggle API 的少数元素的模拟。这个模拟必须返回测试场景使用的响应。有两种方法:
-
创建一个网络服务应用。对于验收测试,此服务必须启动和停止。获取应用可以通过配置
http://localhost:8080
URL 连接到测试服务器,而不是 Kaggle 服务器。(“localhost”地址有一些常见的变体,包括127.0.0.1
和0.0.0.0
。) -
另一种方法是提供一个替换
requests
模块的模拟版本的方法。这个模拟模块会返回适合测试场景的响应。这可以通过操作sys.path
变量来实现,将包含模拟requests
版本的目录放在site-packages
目录之前,后者包含真实版本。也可以通过提供一些可以替换为模拟包的应用配置设置来实现。
创建一个将实现固定装置的完整服务的方法之一。
注入requests
包的模拟
替换requests
包需要在获取应用程序中使用依赖注入技术。以下代码示例中会出现静态依赖:
import requests
import requests.auth
在模块的后面,可能会有像requests.get(...)
或requests.auth.HTTPBasicAuth(...)
这样的代码。requests
模块的绑定由import
语句以及requests
和requests.auth
的引用固定。
importlib
模块允许更动态地绑定模块名称,从而提供一些运行时灵活性。例如,以下内容可以用来定制导入。
if __name__ == "__main__":
# Read from a configuration file
requests_name = "requests"
requests = importlib.import_module(requests_name)
main(sys.argv[1:])
全局变量requests
被分配了导入的模块。这个模块变量必须是全局的;在尝试为验收测试配置应用程序时,这是一个容易忽视的要求。
注意,requests
模块(或模拟版本)的导入与剩余的import
语句是分开的。这可能是后来阅读此代码的人的一些困惑的来源,因此适当的注释对于阐明这种依赖注入的工作方式非常重要。
当我们查看RestAccess 类的单元测试时,我们使用了pytest的monkeypatch
固定装置来正确隔离模块进行测试。
Monkey patching 并不是一个很好的验收测试技术,因为被测试的代码并不完全是最终将使用的代码。虽然 monkey patching 和依赖注入很受欢迎,但总是会有关于测试修补软件而不是实际软件的问题。在某些行业——尤其是那些人类生命可能因计算机控制机械而处于危险中的行业——测试中存在修补程序可能是不允许的。在下一节中,我们将探讨构建模拟服务以创建和测试获取应用程序,而不需要任何修补或更改。
创建模拟服务
可以使用任何 Web 服务框架构建模拟服务。其中有两个是标准库的一部分:http.server
包和wsgiref
包。这两个都可以响应 HTTP 请求,可以用来创建本地服务,模拟 Kaggle Web 服务以允许测试我们的客户端。
此外,任何知名的 Web 服务框架都可以用来创建模拟服务。使用像flask或bottle这样的工具可以稍微简化构建合适的模拟服务的过程。
为了使服务器尽可能简单,我们将使用bottle框架。这意味着在pyproject.toml
文件的[project.optional-dependencies]
部分添加bottle==0.12.23
。这个工具仅由开发者需要。
RESTful API 的Bottle实现可能看起来像这样:
import csv
import io
import json
import zipfile
from bottle import route, run, request, HTTPResponse
@route(’/api/v1/datasets/list’)
def datasets_list(name):
# Provide mock JSON documents and Rate throttling
@route(’/api/v1/datasets/download/<ownerSlug>/<datasetSlug>’)
def datasets_download(ownerSlug, datasetSlug):
# Provide mock ZIP archive
if __name__ == "__main__":
run(host=’127.0.0.1’, port=8080)
虽然 Kaggle 服务有众多路径和方法,但这个数据获取项目应用程序并不使用所有这些。模拟服务器只需要提供应用程序实际使用的路径的路由。
datasets_list
函数可能包含以下示例响应:
@route(’/api/v1/datasets/list’)
def datasets_list(name):
page = request.query.page or ’1’
if page == ’1’:
mock_body = [
# Provide attributes as needed by the application under test
{’title’: ’example1’},
{’title’: ’example2’},
]
response = HTTPResponse(
body=json.dumps(mock_body),
status=200,
headers={’Content-Type’: ’application/json’}
)
else:
# For error-recovery scenarios, this response may change.
response = HTTPResponse(
body=json.dumps([]),
status=200,
headers={’Content-Type’: ’application/json’}
)
return response
HTTPResponse
对象包含由获取应用程序的下载请求看到的响应的基本特征。每个响应都有内容、状态码和用于确认响应类型的标题。
为了进行更全面的测试,添加另一种带有状态码 429 和包含{'Retry-After': '30'}
的标题字典的响应是有意义的。在这种情况下,response
的两个值将更加明显地区分开来。
下载需要提供一个模拟的 ZIP 存档。这可以通过以下示例中的方式完成:
@route(’/api/v1/datasets/download/<ownerSlug>/<datasetSlug>’)
def datasets_download(ownerSlug, datasetSlug):
if ownerSlug == "carlmcbrideellis" and datasetSlug ==
"data-anscombes-quartet":
zip_content = io.BytesIO()
with zipfile.ZipFile(zip_content, ’w’) as archive:
target_path = zipfile.Path(archive, ’Anscombe_quartet_data.csv’)
with target_path.open(’w’) as member_file:
writer = csv.writer(member_file)
writer.writerow([’mock’, ’data’])
writer.writerow([’line’, ’two’])
response = HTTPResponse(
body=zip_content.getvalue(),
status=200,
headers={"Content-Type": "application/zip"}
)
return response
# All other requests...
response = HTTPResponse(
status=404
)
return response
此函数只会响应一个特定请求的ownerSlug
和datasetSlug
组合。其他组合将返回 404 响应,这是无法找到资源的状态码。
io.BytesIO
对象是一个内存缓冲区,可以像文件一样处理。它被zipfile.ZipFile
类用于创建 ZIP 存档。存档中写入一个成员,该成员包含标题行和一行数据,这使得在 Gherkin 场景中描述变得容易。响应由文件中的字节、状态码 200 和告知客户端内容为 ZIP 存档的标题构建。
此服务可以在桌面上运行。您可以使用浏览器与此服务器交互并确认它工作得足够好,可以测试我们的应用程序。
现在我们已经看到了代表 Kaggle.com 的模拟服务,我们可以看看如何使behave工具在测试特定场景时运行此服务。
Behave fixture
我们已将fixture.kaggle_server
添加到场景中。要使此标签启动给定场景的服务进程,有两个步骤。这些步骤是:
-
定义一个生成器函数。这将启动一个子进程,产生一些东西,然后终止子进程。
-
定义一个
before_tag()
函数,用于将生成器函数注入到步骤处理中。
这是一个生成器函数,它将更新上下文并启动模拟 Kaggle 服务。
from collections.abc import Iterator
from typing import Any
import subprocess
import time
import os
import sys
from behave import fixture, use_fixture
from behave.runner import Context
@fixture
def kaggle_server(context: Context) -> Iterator[Any]:
if "environment" not in context:
context.environment = os.environ
context.environment["ACQUIRE_BASE_URL"] = "http://127.0.0.1:8080"
# Save server-side log for debugging
server = subprocess.Popen(
[sys.executable, "tests/mock_kaggle_bottle.py"],
)
time.sleep(0.5) # 500 ms delay to allow the service to open a socket
yield server
server.kill()
在yield
语句之前的函数部分在场景设置期间使用。这将向上下文中添加用于启动测试应用程序的值。在Behave运行器消耗了已产生的值之后,场景执行。当场景完成后,从这个生成器请求另一个值;这个请求将执行yield
语句之后的语句。没有后续的yield
语句;StopIteration
是这个函数预期的行为。
当存在@fixture
标签时,必须使用此kaggle_server()
函数在场景中。以下函数将这样做:
from behave import use_fixture
from behave.runner import Context
def before_tag(context: Context, tag: str) -> None:
if tag == "fixture.kaggle_server":
# This will invoke the definition generator.
# It consumes a value before and after the tagged scenario.
use_fixture(kaggle_server, context)
当存在@fixture.kaggle_server
标签时,此函数将通过运行器将kaggle_server()
生成器函数注入到整体处理流程中。运行器将向kaggle_server()
生成器函数发出适当的请求以启动和停止服务。
这两个函数被放置在environment.py
模块中,以便behave工具可以找到并使用它们。
现在我们有了接受测试套件,我们可以转向实现acquire
应用所需的功能。
Kaggle 访问模块和重构后的主应用
当然,目标有两个:
-
添加一个
kaggle_client.py
模块。单元测试将确认其工作正常。 -
将第三章,项目 1.1:数据 获取基础应用中的
acquire.py
模块重写,以添加下载功能。
方法部分提供了构建应用的某些设计指导。此外,上一章第三章,项目 1.1:数据获取基础应用提供了应添加新获取功能的基线应用。
接受测试将确认应用运行正确。
给定这种扩展功能,我们鼓励您寻找更多有趣的数据集。新应用可以修订和扩展以获取其他格式的有趣数据。
现在我们已经以整洁、易于使用的格式从网络获取了数据,我们可以查看获取不那么整洁的数据。在下一节中,我们将探讨如何从 HTML 页面中抓取数据。
4.2 项目 1.3:从网页抓取数据
在某些情况下,我们希望获取由没有整洁 API 的网站提供的数据。这些数据通过 HTML 页面提供。这意味着数据被 HTML 标记 所包围,这些标记描述了数据的语义或结构。
我们将首先描述该应用,然后继续讨论架构方法。接下来将是一个详细的交付物列表。
4.2.1 描述
我们将继续描述旨在获取用于进一步分析的数据的项目。在这种情况下,我们将查看来自网站的数据,但这些数据被嵌入到周围的 HTML 标记中。我们将继续关注 Anscombe 的四重奏数据集,因为它很小,诊断问题相对简单。更大的数据集会引入与时间和存储相关的问题。
该应用的某些部分是对项目 1.2:从网络服务 获取数据项目的扩展。该应用的基本行为将与之前的类似。本项目将使用 CLI 应用从源抓取数据。
用户体验(UX)也将是一个命令行应用,具有调整收集数据的选项。我们期望的命令行可能如下所示:
% python src/acquire.py -o quartet --page "https://en.wikipedia.org/wiki/Anscombe’s_quartet" --caption "Anscombe’s quartet"
-o
quartet
参数指定一个目录,四个结果将被写入其中。这些结果将具有如quartet/series_1.json
的名称。
表格隐藏在由--page
参数给出的 URL 的 HTML 中。在这个 HTML 中,目标表格有一个独特的<caption>
标签:<caption>Anscombe 的四重奏</caption>
。
4.2.2 关于源数据
这种嵌入 HTML 标记的数据通常使用<table>
标签进行标记。表格通常具有以下标记:
<table class="wikitable" style="text-align: center; margin-left:auto;
margin-right:auto;" border="1">
<caption>Anscombe’s quartet</caption>
<tbody>
<tr>
<th colspan="2">I</th>
etc.
</tr>
<tr>
<td><i>x</i></td>
<td><i>y</i></td>
etc.
</tr>
<tr>
<td>10.0</td>
<td>8.04</td>
etc.
</tr>
</tbody>
</table>
在这个例子中,整体的<table>
标签将有两个子标签,一个<caption>
和一个<tbody>
。
表格的主体,在<tbody>
内,有多个由<tr>
标签包裹的行。第一行有<th>
标签的标题。第二行也有标题,但它们使用<td>
标签。其余行有数据,也在<td>
标签中。
这种结构具有很大的规律性,使得可以使用像Beautiful Soup这样的解析器来定位内容。
输出将与之前项目所做的提取处理相匹配。参见第三章,项目 1.1:数据获取基础应用,了解数据获取应用的本质。
本节探讨了该应用的输入和处理。输出将与早期项目相匹配。在下一节中,我们将探讨软件的整体架构。
4.2.3 方法
在审视我们的方法时,我们将借鉴 C4 模型(c4model.com
)的一些指导:
-
上下文: 对于这个项目,上下文图将显示用户从源中提取数据。你可能觉得绘制这个图会有所帮助。
-
容器: 一个容器是用户的个人电脑。另一个容器是维基百科网站,它提供数据。
-
组件: 我们将讨论以下组件。
-
代码: 我们将简要提及,以提供一些建议的方向。
将此应用视为对第三章,项目 1.1:数据获取基础应用中项目的扩展是很重要的。该章节提供了架构设计的基线。
在这个项目中,我们将添加一个新的html_extract
模块来捕获和解析数据。acquire
模块中的整体应用将改为使用新功能。其他模块应保持不变。
在图 4.4中展示了一种新的架构,该架构处理 HTML 数据的下载和从源数据中提取表格。
图 4.4:修订后的组件设计
此图建议了新html_extract
模块的类。Download
类使用urllib.request
打开给定的 URL 并读取内容。它还使用bs4
模块(Beautiful Soup)解析 HTML,定位具有所需标题的表格,并提取表格的主体。
PairBuilder
类层次结构有四种实现,每种实现适用于四个数据系列中的一个。回顾第三章,项目 1.1:数据 获取基础应用,维基百科页面上显示的数据表与早期项目中显示的 CSV 源文件之间存在深刻差异。这种数据组织差异需要稍微不同的配对构建函数。
使用urllib.request
进行 HTML 请求
读取网页的过程直接由urllib.request
模块支持。url_open()
函数将对给定的 URL 执行 GET 请求。返回值是一个文件-like 对象——具有read()
方法——可以用来获取内容。
这比制作一个通用的 RESTful API 请求简单得多,在通用的 RESTful API 请求中,有各种信息需要上传,以及可能下载的各种结果。当处理常见的 GET 请求时,这个标准库模块优雅地处理了普通处理。
操作的第一步的一个建议设计如下函数:
from urllib.request import urlopen
from bs4 import BeautifulSoup, Tag
def get_page(url: str) -> BeautifulSoup:
return BeautifulSoup(
urlopen(url), "html.parser"
)
urlopen()
函数将 URL 作为文件-like 对象打开,并将该文件提供给BeautifulSoup
类以解析生成的 HTML。
未显示用于处理潜在问题的try:
语句。在接触网络服务并尝试解析结果内容时,有无数潜在的问题。鼓励添加一些简单的错误报告。
在下一节中,我们将查看从解析的 HTML 中提取相关表格。
HTML 抓取和 Beautiful Soup
美味汤数据结构有一个find_all()
方法来遍历结构。这将查找具有特定类型属性的标签。这可以检查标签、属性,甚至标签的文本内容。
请参阅www.crummy.com/software/BeautifulSoup/bs4/doc/#find-all
。
在这个例子中,我们需要找到一个包含caption
标签的<table>
标签。这个caption
标签必须包含所需的文本。这个搜索导致对结构的更复杂调查。以下函数可以定位所需的表格。
def find_table_caption(
soup: BeautifulSoup,
caption_text: str = "Anscombe’s quartet"
) -> Tag:
for table in soup.find_all(’table’):
if table.caption:
if table.caption.text.strip() == caption_text.strip():
return table
raise RuntimeError(f"<table> with caption {caption_text!r} not found")
一些表格缺少标题。这意味着表达式table.caption.text
在字符串比较中不会工作,因为table.caption
可能有一个None
值。这导致嵌套的if
语句级联,以确保在检查标签的文本值之前有一个<caption>
标签。
strip()
函数用于从文本中移除前导和尾随空白,因为 HTML 中的文本块可能被不显示的空白包围,这使得它作为内容的一部分出现时令人惊讶。移除前导和尾随空白使得匹配更容易。
其余的处理工作留给你来设计。这个处理包括找到所有的<tr>
标签,代表表格的行。在每一行(除了第一行)中,将会有一个<td>
标签的序列,代表行内的单元格值。
一旦文本被提取,它与csv.reader
的结果非常相似。
在考虑了技术方法之后,是时候看看这个项目的可交付成果了。
4.2.4 可交付成果
本项目有以下可交付成果:
-
docs
文件夹中的文档。 -
在
tests/features
和tests/steps
文件夹中的验收测试。 -
tests
文件夹中应用模块的单元测试。 -
单元测试的模拟 HTML 页面将是单元测试的一部分。
-
从 HTML 页面获取数据的应用程序。
我们将更详细地查看其中的一些可交付成果。
html_extract 模块的单元测试。
urlopen()
函数支持http:
和https:
方案。它还支持file:
协议。这允许测试用例使用形式为file:///path/to/a/file.html
的 URL 来读取本地 HTML 文件。这通过避免通过互联网访问数据的复杂性来简化测试。
对于测试,准备具有预期 HTML 结构和无效结构的文件是有意义的。有了几个本地文件作为示例,开发者可以快速运行测试用例。
通常,模拟BeautifulSoup
类被认为是一种最佳实践。固定值会对各种find_all()
请求做出模拟标签对象的响应。
然而,当与 HTML 一起工作时,提供模拟 HTML 似乎更好。野外看到的 HTML 的广泛多样性表明,与真实 HTML 一起花费的时间对于调试来说价值巨大。
创建BeautifulSoup
对象意味着单元测试更像是集成测试。能够测试各种奇怪和不同寻常的 HTML 的好处似乎比打破单元测试理想环境的花费更有价值。
拥有示例 HTML 文件与pytest固定值的工作方式相得益彰。一个固定值可以创建一个文件,并以 URL 的形式返回文件的路径。测试完成后,固定值可以删除该文件。
一个包含测试 HTML 页面的固定值可能看起来像这样:
from pytest import fixture
from textwrap import dedent
@fixture
def example_1(tmp_path):
html_page = tmp_path / "works.html"
html_page.write_text(
dedent("""\
<!DOCTYPE html>
<html>
etc.
</html>
"""
)
)
yield f"file://{str(html_page)}"
html_page.unlink()
此固定值使用tmp_path
固定值来提供对仅用于此测试的临时目录的访问。创建文件works.html
,并填充 HTML 页面。测试用例应包含多个<table>
标签,其中只有一个期望的<caption>
标签。
dedent()
函数是一种方便的方法,可以提供与当前 Python 缩进匹配的长字符串。该函数从每一行中移除缩进空格;生成的文本对象没有缩进。
此固定值的返回值可以由urlopen()
函数使用,以打开和读取此文件。测试完成后,最后的步骤(在yield
语句之后)将删除文件。
一个测试用例可能看起来像以下这样:
def test_steps(example_1):
soup = html_extract.get_page(example_1)
table_tag = html_extract.find_table_caption(soup, "Anscombe’s quartet")
rows = list(html_extract.table_row_data_iter(table_tag))
assert rows == [
[],
[’Keep this’, ’Data’],
[’And this’, ’Data’],
]
测试用例使用 example_1
固定值来创建一个文件,并返回一个指向该文件的 URL。该 URL 被提供给正在测试的函数。html_extract
模块中的函数用于解析 HTML,定位目标表格,并提取单个行。
返回值告诉我们函数能够正确地一起定位和提取数据。我们鼓励您为好和坏的示例编写必要的 HTML。
验收测试
如上所述,在 HTML 提取模块单元测试 中,验收测试用例的 HTML 页面可以是本地文件。一个场景可以为应用程序提供一个指向本地 file://
URL,并确认输出包含正确解析的数据。
Gherkin 语言允许将大块文本作为场景的一部分包含在内。
我们可以想象在功能文件中编写以下类型的场景:
Scenario: Finds captioned table and extracts data
Given an HTML page "example_1.html"
"""
<!DOCTYPE html>
<html>
etc. with multiple tables.
</html>
"""
When we run the html extract command
Then log has INFO line with "header: [’Keep this’, ’Data’]"
And log has INFO line with "count: 1"
HTML 提取命令相当长。内容作为步骤定义函数的 context.text
参数可用。以下是这个给定步骤的步骤定义看起来像:
from textwrap import dedent
@given(u'an HTML page "{filename}"')
def step_impl(context, filename):
context.path = Path(filename)
context.path.write_text(dedent(context.text))
context.add_cleanup(context.path.unlink)
步骤定义将路径放入上下文中,然后将 HTML 页面写入给定的路径。dedent()
函数移除可能由 behave 工具留下的任何前导空格。由于路径信息在上下文中可用,它可以由 When 步骤使用。context.add_cleanup()
函数将添加一个函数,可以在场景完成后用于清理文件。另一种选择是使用环境模块的 after_scenario()
函数进行清理。
此场景需要实际路径名来注入提供的 HTML 页面。为了使这一过程顺利进行,步骤定义需要构建一个命令。以下是一个方法:
@when(u’we run the html extract command’)
def step_impl(context):
command = [
’python’, ’src/acquire.py’,
’-o’, ’quartet’,
’--page’, ’$URL’,
’--caption’, "Anscombe’s quartet"
]
url = f"file://{str(context.path.absolute())}"
command[command.index(’$URL’)] = url
print(shlex.join(command))
# etc. with subprocess.run() to execute the command
在这个例子中,命令被分解成单个参数字符串。其中一个字符串必须替换为实际的文件名。这做得很好,因为 subprocess.run()
函数与解析的 shell 命令配合得很好。可以使用 shlex.split()
函数将一行分解成单个参数字符串,同时遵守 shell 的复杂引号规则。
现在我们有了验收测试套件,我们可能会发现 acquire
应用程序没有通过所有测试。通过验收测试定义完成,然后开发所需的 HTML 提取模块和重构主应用程序是有帮助的。我们将接下来查看这两个组件。
HTML 提取模块和重构主应用
本项目的目标是双重的:
-
添加一个
html_extract.py
模块。单元测试将确认此模块工作正常。 -
将
acquire.py
模块从 第三章、项目 1.1:数据获取基础应用程序 重新编写,以添加 HTML 下载和提取功能。
方法部分提供了构建应用程序的一些设计指导。此外,上一章第三章 项目 1.1:数据获取基础应用程序提供了应添加新获取功能的基本应用程序。
接受测试将确认应用程序能够正确地从 Kaggle API 收集数据。
由于这种扩展功能,您可以搜索网页中呈现的数据集。由于维基百科的一致性,它是一个良好的数据来源。许多其他网站提供了相对一致且包含有趣数据的 HTML 表格。
在这两个项目中,我们扩展了从各种来源获取数据的能力。
4.3 摘要
本章的项目展示了数据获取应用程序的以下功能示例:
-
通过requests包进行 Web API 集成。我们以 Kaggle API 为例,展示了提供数据下载和分析的 RESTful API。
-
使用Beautiful Soup包解析 HTML 网页。
-
向现有应用程序添加功能,并扩展测试套件以涵盖这些新的替代数据源。
这两个项目的挑战性部分之一是创建一套接受测试来描述适当的行为。从实用主义的角度来看,没有自动化测试的程序是不可信的。测试与它们所执行的代码一样重要。
在某些企业中,完成的定义是轻松且非正式的。可能会有一个演示文稿或内部备忘录或白皮书来描述所需的软件。将这些概念正式化为可触摸的测试用例通常是一项重大努力。达成协议可能成为冲突的来源,因为利益相关者逐渐细化对软件行为的理解。
创建模拟 Web 服务充满困难。一些 API 允许下载包含 API 定义及其示例的openapi.json
文件。由 API 提供者提供的具体示例使得创建模拟服务变得容易得多。模拟服务器可以加载 JSON 规范,导航到示例,并提供官方响应。
缺乏带有示例的 OpenAPI 规范,开发者需要编写激进行为的解决方案来下载详细响应。然后可以使用这些响应来构建模拟对象。强烈建议您编写侧边栏应用程序来探索 Kaggle API,以了解其工作方式。
在下一章中,我们将继续这一数据提取之旅,包括从 SQL 数据库中提取数据。一旦我们获取了数据,我们就会想要检查它。第六章 项目 2.1:数据检查笔记本将介绍检查步骤。
4.4 额外内容
这里有一些想法供您添加到这些项目中。
4.4.1 定位更多 JSON 格式数据
在 Kaggle 上搜索将出现一些其他有趣的 JSON 格式数据集。
-
www.kaggle.com/datasets/rtatman/iris-dataset-json-version
:这个数据集很著名,并以多种不同的格式提供。
其中之一是 JSON 下载。其他两个是包含 JSON 格式内容的 ZIP 存档。
这将需要修改应用程序的架构,以提取 JSON 格式数据而不是 CSV 格式数据。
这里一个有趣的复杂性在于 CSV 数据和 JSON 数据之间的区别:
-
CSV 数据是纯文本,需要后续转换才能制作成有用的 Python 对象。
-
一些 JSON 数据通过解析器转换为 Python 对象。一些数据(如日期戳)将保留为文本。
在获取数据时,这不会产生重大影响。然而,当我们到达第九章,项目 3.1:数据清洗基础应用时,我们必须考虑以纯文本形式存在的数据,这些数据与经过某些转换的数据是不同的。
菊花数据集相当著名。你可以在此基础上扩展本章的设计,从各种来源获取菊花数据。以下步骤可以遵循:
-
从 JSON 格式的 Kaggle 数据集开始。构建所需的模型和提取模块以处理此格式。
-
在其他格式中定位此数据集的其他版本。构建所需的提取模块以处理这些替代格式。
一旦核心获取项目完成,你可以利用这个其他著名的数据集作为后续项目的实现选择。
4.4.2 其他要提取的数据集
请参阅datahub.io/core/co2-ppm
上的CO****2 PPM — 大气二氧化碳趋势数据集,其中包含一些相对较大的数据。此页面有一个链接到包含数据的 HTML 表格。
请参阅datahub.io/core/co2-ppm/r/0.html
上的页面,其中包含完整数据集的 HTML 表格。这个数据集比 Anscombe 的四重奏更大、更复杂。在第六章,项目 2.1:数据检查笔记本中,我们将讨论这个数据集中的某些特殊情况。
4.4.3 处理模式变化
本章中的两个项目各自反映了源数据的独特模式。
一个 CSV 格式可以通过一个实体-关系图(ERD),如图 4.5**.所示来表示。
图 4.5:源实体关系图
一列,x_123
,是三个不同系列中的 x 值。另一列,x_4
,是一个系列中的 x 值。
如图 4.6**.所示,HTML 格式的 ERD 表示。
图 4.6:概念实体关系图
x 值按需重复。
这种差异需要几种不同的方法来提取源数据。
在这些项目中,我们已将这种区别作为PairBuilder
超类的不同子类来实现。
一种替代设计是创建具有公共类型签名的不同函数:
PairBuilder: TypeVar = Callable[[list[str]], XYPair]
将每个转换变成一个函数消除了类定义的开销。
这次重写可以是一个大的简化。它不会改变任何验收测试。然而,它将需要对单元测试进行许多更改。
功能设计在类设计的基础上提供了一些简化。鼓励您执行本书中建议的功能重设计。
4.4.4 CLI 增强
这两个项目的 CLI 被留得很开放,允许有很大的设计灵活性和替代方案。由于 CLI 是外部可见行为的一部分,因此有必要为各种 CLI 选项和参数编写验收测试。
如附加验收场景中所述,有一些验收测试场景不在“快乐路径”上,即应用程序正常工作。这些额外的场景旨在记录应用程序的许多错误使用。
随着更多功能的添加和 CLI 变得更加复杂,这一点变得更加重要。鼓励您为无效的 CLI 使用编写验收测试。
4.4.5 记录
记录是数据获取的重要部分。这两个项目暴露了许多潜在问题。网站可能无响应,或 API 可能已更改。HTML 可能以某种微妙的方式重新格式化。
应该提供一个 调试 或 详细 模式,以便暴露与外部服务的交互,确保 HTTP 状态码和头信息。
此外,应显示计数值以总结下载的字节数、检查的文本行数以及创建的XYPair
对象数量。这个想法是描述输入、各种处理步骤和输出。
这些计数对于确认数据是否正确处理和过滤至关重要。它们是使处理部分更易于观察的重要工具。用户希望确认所有下载的数据要么是结果的一部分,要么是因合理原因而被过滤和丢弃。
鼓励您在日志中包含输入、处理和输出的计数。
第五章
数据获取功能:SQL 数据库
在本章中,您将指导完成两个项目,展示如何将 SQL 数据库作为分析数据源进行操作。这将建立在前面两章中构建的基础应用之上。
本章将重点介绍 SQL 提取。由于企业 SQL 数据库通常非常私密,我们将指导读者首先创建一个 SQLite 数据库。这个数据库将作为私有企业数据库的替代品。一旦有了数据库,我们将研究如何从数据库中提取数据。
本章的项目涵盖了以下基本技能:
-
构建 SQL 数据库。
-
从 SQL 数据库中提取数据。
第一个项目将为第二个项目构建一个 SQL 数据库。
在企业环境中,源数据库已经存在。
在我们自己的个人电脑上,这些数据库并不存在。因此,我们将在第一个项目中构建一个数据库,并在第二个项目中从数据库中提取数据。
我们将首先查看如何将数据放入 SQL 数据库。这将是一个非常小且简单的数据库;项目将避免 SQL 数据的众多复杂设计问题。
第二个项目将使用 SQL 查询从数据库中提取数据。目标是生成与上一章项目一致的数据。
5.1 项目 1.4:本地 SQL 数据库
我们经常需要存储在数据库中的数据,这些数据通过 SQL 查询语言访问。使用搜索字符串“SQL 是通用语言”来找到提供更多关于 SQL 通用性的文章。这似乎是获取企业数据以进行进一步分析的主要方法之一。
在上一章第四章,数据获取功能:Web API 和爬取中,项目从公开可用的 API 和网页中获取数据。公开可用的 SQL 数据源并不多。在许多情况下,可以使用 SQLite 数据库的转储(或导出)来构建数据库的本地副本。直接访问远程 SQL 数据库并不常见。与其试图找到访问远程 SQL 数据库的方法,不如创建一个本地 SQL 数据库简单。SQLite 数据库作为 Python 标准库的一部分提供,使其成为容易选择。
您可能想检查其他数据库,并将它们的特性与 SQLite 进行比较。虽然一些数据库提供了许多功能,但进行 SQL 提取很少似乎需要比基本的 SELECT
语句更复杂的东西。使用另一个数据库可能需要一些更改,以反映该数据库的连接和 SQL 语句执行。在大多数情况下,Python 中的 DB-API 接口被广泛使用;对于 SQLite 之外的数据库,可能有一些独特功能。
我们将从填充数据库的项目开始。一旦数据库可用,你就可以继续进行更有趣的项目,使用 SQL 语句提取数据。
5.1.1 描述
本章的第一个项目将为分析准备一个包含数据的 SQL 数据库。这是读者在具有可访问 SQL 数据库的企业环境外工作的必要准备步骤。
最有趣的小数据集之一就是安斯康姆四重奏。
www.kaggle.com/datasets/carlmcbrideellis/data-anscombes-quartet
上面的 URL 提供了一个包含 CSV 格式文件信息的页面。点击下载按钮将数据小文件下载到您的本地计算机。
数据也存放在本书 GitHub 仓库的data
文件夹中。
为了加载数据库,第一步是设计数据库。我们将从查看一些表定义开始。
数据库设计
SQL 数据库以数据表的形式组织。每个表都有一个固定的列集,这些列作为整体数据库模式的一部分进行定义。一个表可以有无限数量的数据行。
关于 SQL 数据库的更多信息,请参阅www.packtpub.com/product/learn-sql-database-programming/9781838984762
和courses.packtpub.com/courses/sql
。
安斯康姆四重奏由四组(x,y)对组成。在一个常用的源文件中,三个系列共享共同的x值,而第四个系列具有不同的x值。
关系型数据库通常将复杂的实体分解为一系列更简单的实体。目标是最大限度地减少关联类型的重复。安斯康姆四重奏信息有四个不同的数据值系列,可以表示为以下两种类型的实体:
-
系列由多个单独的值组成。一个名为
series_value
的表可以存储属于系列的单个值。 -
一个单独的实体为整个系列提供标识信息。一个名为
sources
的表可以存储标识信息。
这种设计需要引入关键值来唯一标识系列,并将每个系列的值与系列的总结信息连接起来。
对于安斯康姆四重奏数据,一个系列的总结信息仅略多于一个名称。
这种整体总结和支持细节的设计模式如此常见,对于本项目来说,反映这种常见模式是至关重要的。
见图 5.1以查看显示实现这些实体及其关系的 ERD。
图 5.1:数据库模式
此项目将创建一个小应用程序来构建这两个表的架构。然后,该应用程序可以加载数据到这些表中。
数据加载
加载数据的过程涉及三个独立的操作:
-
从 CSV(或其他格式)文件中读取源数据。
-
执行 SQL
INSERT
语句以在表中创建行。 -
执行
COMMIT
以最终化事务并将数据写入底层数据库文件。
在这些步骤之前,必须使用 CREATE TABLE
语句定义模式。
在实际应用中,也常见提供组合操作以删除表、重新创建模式,然后加载数据。重建通常发生在探索或实验数据库设计时。很多时候,最初的设计可能无法令人满意,需要做出改变。此外,构建(和重建)小型数据库的想法也将是任何数据采集应用程序验收测试的一部分。
在下一节中,我们将探讨如何创建一个 SQL 数据库,它可以作为大型企业生产数据库的替代品。
5.1.2 方法
对于此类测试或演示应用程序,处理 SQL 数据库有两种一般方法:
-
创建一个小应用程序来构建和填充数据库。
-
通过文本格式创建一个 SQL 脚本,并通过数据库的 CLI 应用程序运行它。请参阅
sqlite.org/cli.html
。
该小应用程序将利用数据库客户端连接来执行 SQL 语句。在这种情况下,可以使用一个具有占位符的单个通用 INSERT
语句模板。客户端连接可以提供占位符的值。虽然应用程序并不复杂,但它将需要单元和验收测试用例。
SQL 脚本替代方案使用一个小应用程序将数据行转换为有效的 INSERT
语句。在许多情况下,文本编辑器的搜索和替换功能可以将数据文本转换为 INSERT
语句。对于更复杂的情况,可以使用 Python f-strings。f-string 可能看起来像以下这样:
print(
f"INSERT INTO SSAMPLES(SERIES, SEQUENCE, X, Y)"
f"VALUES({series}, {sequence}, ’{x}’, ’{y}’)"
)
这通常很成功,但存在一个潜在的严重问题:SQL 注入攻击。
SQL 注入攻击通过在数据值中包含一个字符串字面量结尾的单引号 ’
来工作。这可能导致无效的 SQL 语句。在极端情况下,它允许注入额外的 SQL 语句,将 INSERT
语句转换为脚本。更多信息,请参阅 owasp.org/www-community/attacks/SQL_Injection
。还可以参阅 xkcd.com/327/
以了解 SQL 注入攻击的另一个示例。
虽然 SQL 注入可能被恶意使用,但它也可能是一个令人沮丧的常见事故。如果文本数据值中恰好有 ’
,那么这可能会在 SQL 脚本文件中创建一个具有无效语法的语句。SQL 清洗只是将问题推迟到可能复杂的 SQL 清洗函数。
首先避免构建 SQL 文本会更简单。一个小型应用程序可以摆脱构建 SQL 文本的复杂性。
我们将首先查看这个小模式的数据定义。然后我们将查看数据操作语句。这将为设计构建模式和加载数据的小型应用程序奠定基础。
SQL 数据定义
SQL 中的基本数据定义是一个具有多个列(也称为 属性)的表。这是通过一个 CREATE TABLE
语句定义的。列的列表由该语句提供。除了列之外,该语言还允许表约束进一步细化表的使用方式。就我们的目的而言,两个表可以定义为如下:
CREATE TABLE IF NOT EXISTS series(
series_id INTEGER,
name TEXT,
PRIMARY KEY (series_id)
);
CREATE TABLE IF NOT EXISTS series_sample(
series_id INTEGER,
sequence INTEGER,
x TEXT,
y TEXT,
PRIMARY KEY (series_id, sequence),
FOREIGN KEY (series_id) REFERENCES series(series_id)
);
要删除模式,使用 DROP TABLE IF EXISTS series_sample
和 DROP TABLE IF EXISTS series
语句即可完成所需操作。由于外键引用,某些数据库在删除 series
行之前,需要先删除所有相关的 series_sample
行。
在调试时,IF EXISTS
和 IF NOT EXISTS
子句非常有用。例如,我们可能会更改 SQL 并在某个 CREATE TABLE
语句中引入语法错误。这可能会导致一个不完整的模式。在解决问题后,只需重新运行整个 CREATE TABLE
语句序列,就会只创建缺失的表。
本例 SQL 数据模型的一个基本特征是简化了涉及的数据类型。series_sample
表中的两列数据都被定义为 TEXT
。这是一个罕见的情况;大多数 SQL 数据库都会使用可用的某种数值类型。
虽然 SQL 数据有多种有用的类型,但来自其他应用程序的原始数据却不是数值型的。CSV 文件和 HTML 页面只提供文本。因此,这个应用程序的结果也需要是文本的。一旦定义了表,应用程序就可以插入行。
SQL 数据操作
使用 INSERT
语句创建新行。虽然 SQLite 允许省略一些细节,但我们将坚持使用稍微冗长但更明确的语句。以下是在两个表中创建行的方式:
INSERT INTO series(series_id, name) VALUES(:series_id, :name)
INSERT INTO series_sample(series_id, sequence, x, y)
VALUES(:series_id, :sequence, :x, :y)
前缀为冒号的标识符,如 :x
、:y
、:series_id
等,是将在执行语句时被替换的参数。由于这些替换不依赖于 SQL 文本规则——例如使用引号来结束字符串——可以使用任何值。
从这些表中删除行的情况很少见。在替换数据时,删除并重新创建表通常更简单(有时也更快)。
SQL 执行
Python 的 SQLite 接口是 sqlite3
模块。这符合数据库访问的 PEP-249 标准([peps.python.org/pep-0249/
](https://peps.python.org/pep-0249/))。应用程序通常会在创建数据库连接。它将使用连接来创建一个 游标,可以查询或更新数据库。
连接是通过连接字符串完成的。对于许多数据库,连接字符串将包括托管数据库的服务器以及数据库名称;它还可能包括安全凭证或其他选项。对于 SQLite,连接字符串可以是一个完整的 URI,形式为 file:filename.db
。这有一个方案,file:
和数据库文件的路径。
这对该应用程序不是必需的,但将 SQL 语句隔离到配置文件中是一种常见的做法。使用 TOML 格式可以是一种方便的方法,将处理与实现处理的 SQL 语句分开。这种分离允许在不更改源文件的情况下进行小的 SQL 变更。对于编译型语言,这是必需的。对于 Python,这是一种在更改数据库时使 SQL 更容易找到的有帮助的方法。
创建模式的函数可能看起来像这样:
CREATE_SERIES = """
CREATE TABLE IF NOT EXISTS series(
-- rest of the SQL shown above...
"""
CREATE_VALUES = """
CREATE TABLE IF NOT EXISTS series_sample(
-- rest of the SQL shown above...
"""
CREATE_SCHEMA = [
CREATE_SERIES,
CREATE_VALUES
]
def execute_statements(
connection: sqlite3.Connection,
statements: list[str]
) -> None:
for statement in statements:
connection.execute(statement)
connection.commit()
CREATE_SCHEMA
是构建模式所需的语句序列。可以定义类似的语句序列来删除模式。这两个序列可以组合起来,作为普通数据库设计和实验的一部分来删除和重新创建模式。
主程序可以使用类似以下代码创建数据库:
with sqlite3.connect("file:example.db", uri=True) as connection:
schema_build_load(connection, config, data_path)
这需要一个名为 schema_build_load()
的函数,用于删除并重新创建模式,然后加载单个数据行。
我们将转向下一步,即加载数据。这从加载系列定义开始,然后继续填充每个系列的数值。
加载 SERIES 表
SERIES
表中的值基本上是固定的。有四行来定义四个系列。
执行 SQL 数据操作语句需要两个东西:语句和用于语句中占位符的值的字典。
在下面的代码示例中,我们将定义语句,以及四个带有占位符值的字典:
INSERT_SERIES = """
INSERT INTO series(series_id, name)
VALUES(:series_id, :name)
"""
SERIES_ROWS = [
{"series_id": 1, "name": "Series I"},
{"series_id": 2, "name": "Series II"},
{"series_id": 3, "name": "Series III"},
{"series_id": 4, "name": "Series IV"},
]
def load_series(connection: sqlite3.Connection) -> None:
for series in SERIES_ROWS:
connection.execute(INSERT_SERIES, series)
connection.commit()
连接对象的 execute()
方法被赋予带有占位符的 SQL 语句和一个用于占位符的值的字典。SQL 模板和值被提供给数据库以插入行到表中。
然而,对于单个数据值,还需要更多。在下一节中,我们将查看从源 CSV 数据到用于 SQL 语句的参数值字典的转换。
加载 SERIES_VALUE 表
可以参考 第三章 和 项目 1.1:数据获取基础应用 中的项目来帮助理解。在这一章中,我们定义了一个 (x,y) 对的数据类,并称之为 XYPair
。我们还定义了一个 PairBuilder
类层次结构,用于从 CSV 行对象创建 XYPair
对象。
使用与应用软件提取数据软件可疑相似的应用软件加载数据可能会令人困惑。
这种困惑通常出现在我们被迫构建演示数据库的情况下。
这也可能出现在需要测试数据库以用于复杂分析应用程序的情况下。
在大多数企业环境中,数据库已经存在并且已经充满了数据。测试数据库仍然需要来确认分析应用程序是否工作。
上面的 INSERT
语句(在 SQL 数据操作 中显示)有四个占位符。这意味着连接的 execute()
方法需要一个包含四个参数的字典。
dataclasses
模块包含一个函数 asdict()
,用于将 XYPair
对象转换为字典。这包含了所需的两个参数,:x
和 :y
。
我们可以使用 |
操作符将两个字典合并在一起。一个字典包含对象的基本属性,由 asdict()
创建,另一个字典是 SQL 负载,包括 :series_id
的值和 :sequence
的值。
下面是一个代码片段,展示了这可能的工作方式:
for sequence, row in enumerate(reader):
for series_id, extractor in SERIES_BUILDERS:
param_values = (
asdict(extractor(row)) |
{"series_id": series_id, "sequence": sequence}
)
connection.execute(insert_values_SQL, param_values)
reader
对象是源 CSV 数据的 csv.DictReader
。SERIES_BUILDERS
对象是一个包含系列编号和函数(或可调用对象)的序列,用于提取适当的列并构建 XYPair
实例。
为了完整性,以下是 SERIES_BUILDERS
对象的值:
SERIES_BUILDERS = [
(1, series_1),
(2, series_2),
(3, series_3),
(4, series_4)
]
在这种情况下,已经定义了单独的函数来从 CSV 源字典中提取所需的列并构建一个 XYPair
实例。
上述代码片段需要构建为适当的函数,并由整体的 main()
函数使用,以删除模式、构建模式、插入 SERIES
表的值,然后插入 SERIES_VALUE
行。
一个有用的最终步骤是查询以确认数据已加载。考虑以下内容:
SELECT s.name, COUNT(*)
FROM series s JOIN series_sample sv
ON s.series_id = sv.series_id
GROUP BY s.series_id
这应该报告四个系列的名字和 11 行数据的存在。
5.1.3 交付成果
这个迷你项目有两个交付成果:
-
为下一个项目使用的数据库。主要目标是创建一个数据库,作为企业使用的生产数据库的替代品。
-
一个可以构建(并重建)此数据库的应用程序。这个次要目标是实现主要目标的方法。
当然,单元测试也强烈推荐。当应用程序设计为可测试时,这效果很好。这意味着两个特性是必不可少的:
-
数据库连接对象是在
main()
函数中创建的。 -
连接对象作为参数值传递给所有与数据库交互的其他函数。
将连接作为参数值提供,使得可以独立于数据库连接的开销测试各种函数。对于与数据库交互的每个应用程序函数,都提供了一个模拟连接对象。大多数模拟连接对象都有一个模拟的 execute()
方法,它返回一个没有行的模拟游标。对于查询,模拟的 execute()
方法可以返回模拟的数据行,通常是一些简单的哨兵
对象。
在执行一个函数之后,可以检查模拟的 execute()
方法,以确保应用程序向数据库提供了语句和参数。
对于这种一次性使用的应用程序,进行正式的验收测试似乎过于繁琐。似乎更容易运行应用程序,并用 SQL SELECT
查询查看结果。由于应用程序删除并重新创建模式,可以在结果可接受之前重复运行。
5.2 项目 1.5:从 SQL 提取获取数据
到目前为止,你现在有一个有用的 SQL 数据库,其中包含模式和数据。下一步是编写应用程序,将数据从该数据库提取到有用的格式中。
5.2.1 描述
使用运行中的数据库进行分析处理可能会很困难。在正常操作期间,锁定用于确保数据库更改不会相互冲突或覆盖。这种锁定可能会干扰从数据库中收集数据用于分析。
从运行中的数据库中提取数据有多种策略。一种技术是备份运行中的数据库,并将其恢复到一个临时的克隆数据库中,用于分析目的。另一种技术是使用任何复制功能,并在复制的数据库中进行分析工作。
我们将采取的策略是“表扫描”方法。通常可以在不取出任何数据库锁的情况下进行快速查询。由于查询运行时正在进行的进程事务,数据可能不一致。在大多数情况下,不一致实体数量是可用数据的极小部分。
如果需要在特定时间点获得一个完整且一致的快照,应用程序就需要考虑到这一点进行设计。在由设计不良的应用程序执行更新的情况下,确定繁忙数据库的状态可能非常困难。在某些情况下,完整和一致的定义可能难以表述,因为状态变化的领域在细节上了解不够充分。
与设计不良的数据库一起工作可能会令人沮丧。
教育潜在的分析软件用户了解获取数据的复杂性通常很重要。这种教育需要将数据库复杂性转化为对决策的影响以及支持这些决策的数据。
用户体验(UX)将是一个命令行应用程序。我们期望的命令行可能看起来像以下这样:
% python src/acquire.py -o quartet --schema extract.toml \
--db_uri file:example.db -u username
Enter your password:
-o
quartet
参数指定一个目录,四个结果将被写入其中。这些文件名可能像quartet/series_1.json
。
--schema
extract.toml
参数是包含构成数据库查询基础的 SQL 语句的文件名。这些语句与应用程序分开,以便在无需重写应用程序程序的情况下更容易地应对数据库结构的变化。
--db_uri
file:example.db
参数提供数据库的 URI。对于 SQLite,URI 方案为file:
,指向数据库文件的路径。对于其他数据库引擎,URI 可能更复杂。
-u
参数提供用于连接数据库的用户名。密码将通过交互式提示请求。这可以隐藏密码。
上面的 UX 包括用户名和密码。
虽然 SQLite 实际上不需要它,但它对于其他数据库将是必需的。
5.2.2 对象关系映射(ORM)问题
关系型数据库设计将复杂的数据结构分解为多个简单的实体类型,这些类型以表格的形式表示。将数据结构分解为实体的过程称为规范化。许多数据库设计符合称为第三范式的模式;但还有其他规范化形式。此外,还有充分的理由打破一些规范化规则以提高性能。
关系规范化通过简单的表格和列来保证数据的一致表示。每一列都将有一个不可再分解的原子值。任意复杂度的数据都可以通过相关集合的平坦、规范化表格来表示。
有关数据库设计活动的更多信息,请参阅www.packtpub.com/product/basic-relational-database-design-video/9781838557201
。
获取复杂结构的过程是通过关系连接操作完成的。来自不同表的行被连接到一个结果集中,从中可以构建原始 Python 对象。这个连接操作是SELECT
语句的一部分。它出现在FROM
子句中,作为一条规则,说明如何将一个表中的行与另一个表中的行匹配。
这种关系设计和对象设计之间的区别有时被称为对象关系阻抗不匹配。有关更多背景信息,请参阅wiki.c2.com/?ObjectRelationalImpedanceMismatch
。
从关系型数据库中读取复杂数据的一种通用方法是创建一个 ORM 层。这个层使用 SQL SELECT 语句从多个表中提取数据,以构建一个有用的对象实例。ORM 层可能使用一个单独的包,或者它可能是应用程序的一部分。虽然 ORM 设计可能会设计得不好——即 ORM 相关的操作可能会随意散布——但这个层在任何应用程序中都始终存在。
Python 包索引(PyPI)中有许多包提供优雅的通用 ORM 解决方案。SQLAlchemy ( www.sqlalchemy.org
)包非常受欢迎。它提供了一个全面的解决方案,用于整个创建、检索、更新和删除(CRUD)操作套件。
有两种条件表明需要手动创建 ORM 层:
-
对数据库的只读访问。完整的 ORM 将包括不会使用的操作功能。
-
一个设计奇特的模式。有时很难为设计不符合 ORM 内置假设的现有模式制定 ORM 定义。
在糟糕的数据库设计和令人困惑的数据库设计之间有一条很细的界限。糟糕的设计具有 ORM 内置假设无法成功描述的奇特特性。令人困惑的设计可以描述,但它可能需要使用 ORM 包的“高级”功能。在许多情况下,构建 ORM 映射需要了解足够的 ORM 功能,以便区分糟糕的和令人困惑的。
在许多情况下,关系模式可能涉及大量相互关联的表,有时来自广泛的学科领域。例如,可能有产品和产品目录、产品的销售记录以及关于产品的库存信息。一个“产品”类的适当边界是什么?它应该包括数据库中与产品相关的所有内容吗?或者它应该由某个有界上下文或问题域限制?
对现有数据库的考虑应导致与用户就问题域和上下文进行广泛的对话。它还导致与创建数据的应用程序的所有者进行进一步的对话。所有对话的目的是理解用户的观念如何与现有数据源重叠。
从关系型数据库中获取数据可能是一个挑战。
关系型规范化会导致复杂性。重叠上下文的存在可能导致进一步的复杂性。
好像有帮助的是,提供从数据库的技术世界到用户想要做出信息和决策的信息的清晰翻译。
5.2.3 关于源数据
见图 5.2以查看提供所需实体的两个表的 ERD:
图 5.2:数据库模式
在上述设计中,两个表格分解了Series
类的实例。以下是 Python 类定义:
from dataclasses import dataclass
@dataclass
class SeriesSample:
x: str
y: str
@dataclass
class Series:
name: str
samples: list[SeriesSample]
这里的想法是一组SeriesSample
对象是单个复合Series
对象的一部分。从包含的Series
中分离出来的SeriesSample
对象在孤立状态下没有用处。许多SeriesSample
实例依赖于一个Series
对象。
从一个规范化的表集合中检索信息有三种一般方法:
-
单个 SQL 查询。这迫使数据库服务器将多个表中的行连接起来,提供一个单一的结果集。
-
一系列查询从单独的表中提取数据,然后使用 Python 字典进行查找。
-
嵌套 SQL 查询。这些使用更简单的 SQL,但可能导致大量的数据库请求。
在所有情况下,这两种选择都不是完美的解决方案。许多数据库设计者会坚持认为数据库连接操作是神奇的最快方式。一些实际的计时信息表明,Python 字典查找可以更快。许多因素影响查询性能,谨慎的设计是实施替代方案并比较性能。
影响性能的因素数量庞大。不存在简单的“最佳实践”。只有实际测量才能帮助做出设计决策。
用于检索数据的连接查询可能看起来是这样的:
SELECT s.name, sv.x, sv.y
FROM series s JOIN series_sample sv ON s.series_id = sv.series_id
s.name
的每个不同值将导致创建一个不同的Series
对象。sv.x
和sv.y
值的每一行都成为Series
对象内的SeriesSample
实例。
使用两个单独的SELECT
语句构建对象涉及两个更简单的查询。以下是获取单个系列的“外循环”查询:
SELECT s.name, s.series_id
FROM series s
这是获取特定系列行的“内循环”查询:
SELECT sv.x, sv.y
FROM series_sample sv
WHERE sv.series_id = :series_id
ORDER BY sv.sequence
第二个SELECT
语句有一个依赖于第一个查询结果的占位符。当应用程序对series_sample
表中的特定系列子集进行嵌套请求时,必须提供此参数。
还要注意,输出预期是纯文本,将保存在 ND JSON 文件中。这意味着 SQL 数据库的复杂结构将被删除。
这也将使中间结果与 CSV 文件和 HTML 页面保持一致,在这些文件和页面上,数据仅是文本。输出应类似于第三章中 CSV 提取的输出,项目 1.1:数据获取基础应用:一个小型 JSON 文档的文件,具有"x"
和"y"
键。目标是去除可能由数据持久化机制(本项目为 SQL 数据库)强加的结构。数据被简化为共同的文本基础。
在下一节中,我们将更仔细地研究从 SQL 数据库获取数据的技术方法。
5.2.4 方法
当我们审视我们的方法时,我们将从 C4 模型(c4model.com
)中获取一些指导。
-
上下文:对于这个项目,上下文图将显示用户从源提取数据。读者可能会发现绘制此图很有帮助。
-
容器:一个容器是用户的个人电脑。另一个容器是运行在同一台电脑上的数据库服务器。
-
组件:我们将在下面讨论组件。
-
代码:我们将简要提及以提供一些建议方向。
此项目添加了一个新的db_client
模块来从数据库中提取数据。acquire
模块中的整体应用将改变以使用此新模块。其他模块——大部分——将保持不变。
图 5.3中的组件图显示了此项目的处理方法。
图 5.3:组件图
此图显示了底层model
的修订。此图扩展了model
模块,以区分复合系列对象和整体系列中的单个样本。它还将旧的XYPair
类重命名为更具信息量的SeriesSample
类。
这种系列之间的区别是前几章项目中隐含的一部分。在此阶段,区分样本集合和单个样本可能是有帮助的。
一些读者可能反对在一系列紧密相关的项目中中途更改类名。这种改变——根据作者的经验——非常常见。我们开始时有一个理解,随着我们对问题领域、用户和技术的深入了解,这个理解会不断发展和完善。为概念挑选一个很好的名字非常困难。在我们学习的过程中逐步确定名字更为谨慎。
新模块将使用两个 SQL 查询来执行提取。我们将在下一节中查看这些嵌套请求。
从 SQL 数据库中提取
从数据库的提取构建了两个部分的系列。第一部分是获取Series
类的属性。第二部分是获取每个单独的SeriesSample
实例。
下面是潜在类设计的概述:
import model
import sqlite3
from typing import Any
from collections.abc import Iterator
class Extract:
def build_samples(
self,
connection: sqlite3.Connection,
config: dict[str, Any],
name: str
) -> model.Series:
...
def series_iter(
self,
connection: sqlite3.Connection,
config: dict[str, Any]
) -> Iterator[model.Series]:
...
series_iter()
方法遍历可以从数据库创建的Series
实例。build_samples()
方法创建属于系列的各个样本。
下面是build_samples()
方法的初稿实现:
def build_samples(
self,
connection: sqlite3.Connection,
config: dict[str, Any],
name: str
) -> list[model.SeriesSample]:
samples_cursor = connection.execute(
config[’query’][’samples’],
{"name": name}
)
samples = [
model.SeriesSample(
x=row[0],
y=row[1])
for row in samples_cursor
]
return samples
此方法将根据名称提取系列中的样本集合。它依赖于config
对象中的 SQL 查询。使用列表推导从查询结果构建样本列表。
这个初稿实现依赖于SeriesSample
类名。这是另一个 SOLID 设计问题,类似于第三章类设计中提到的项目 1.1:数据采集基础应用的问题。
一个更好的实现将用可以在运行时注入的依赖项替换这个直接依赖项,从而允许更好的单元测试隔离。
这里是series_iter()
方法的实现:
def series_iter(
self,
connection: sqlite3.Connection,
config: dict[str, Any]
) -> Iterator[model.Series]:
print(config[’query’][’names’])
names_cursor = connection.execute(config[’query’][’names’])
for row in names_cursor:
name=row[0]
yield model.Series(
name=name,
samples=self.build_samples(connection, config, name)
)
此方法将从数据库中提取每个序列。它同样从配置对象config
中获取 SQL 语句。配置对象是一个字典的字典。这种结构在 TOML 文件中很常见。
想法是有一个 TOML 符号的配置文件,看起来像这样:
[query]
summary = """
SELECT s.name, COUNT(*)
FROM series s JOIN series_sample sv ON s.series_id = sv.series_id
GROUP BY s.series_id
"""
detail = """
SELECT s.name, s.series_id, sv.sequence, sv.x, sv.y
FROM series s JOIN series_value sv ON s.series_id = sv.series_id
"""
names = """
SELECT s.name FROM series s
"""
samples = """
SELECT sv.x, sv.y
FROM series_sample sv JOIN series s ON s.series_id = sv.series_id
WHERE s.name = :name
ORDER BY sv.sequence
"""
此配置有一个[query]
部分,包含几个用于查询数据库的单独 SQL 语句。由于 SQL 语句通常相当长,因此使用三引号来界定它们。
在 SQL 语句非常长的情况下,将它们放入单独的文件中可能看起来很有帮助。这会导致一个更复杂的配置,包含多个文件,每个文件包含一个单独的 SQL 语句。
在我们查看可交付成果之前,我们将简要讨论为什么这个数据采集应用程序与以前的项目不同。
与 CSV 处理不同的 SQL 相关处理
注意一些重要的区别,这些区别在于处理 CSV 数据和处理 SQL 数据。
首先,CSV 数据始终是文本。当与 SQL 数据库一起工作时,底层数据通常具有与原生 Python 类型愉快映射的数据类型。SQL 数据库通常有一些数值类型,包括整数和浮点数。一些数据库将处理映射到 Python 的decimal.Decimal
类的十进制值;这不是一个通用功能,一些数据库强制应用程序在decimal.Decimal
和文本之间进行转换,以避免浮点值固有的截断问题。
第二个重要的区别是变化的节奏。SQL 数据库模式通常变化缓慢,变化通常涉及对变化影响的审查。在某些情况下,CSV 文件是由交互式电子表格软件构建的,并且使用手动操作来创建和保存数据。不出所料,电子表格的交互式使用会导致在短时间内出现小的变化和不一致性。虽然一些 CSV 文件是由高度自动化的工具生成的,但可能对列的顺序或名称的审查较少。
第三个重要的区别与电子表格的设计与数据库的设计相关。关系数据库通常高度规范化;这是一个避免冗余的尝试。而不是重复一组相关值,实体被分配到一个单独的表中,并具有主键。通过主键引用该值组以避免值的重复。将规范化规则应用于电子表格的情况较少见。
由于电子表格数据可能没有完全规范化,从电子表格中提取有意义的数据通常成为一个相当复杂的问题。当电子表格被手动调整或设计突然改变时,这种情况可能会加剧。为了反映这一点,本书中的设计建议使用类层次结构——或相关函数集合——从电子表格行构建一个有用的 Python 对象。在历史分析中,通常需要保持一个大量的构建者池,以处理变体电子表格数据。
之前显示的设计有一个 PairBuilder
子类来创建单个样本对象。这些设计使用 Extract
类来管理从源文件构建样本的整体构建。这为处理电子表格数据提供了灵活性。
数据库提取不太可能需要灵活的对象层次结构来创建有用的 Python 对象。相反,所需的灵活性通常通过更改 SQL 语句以反映模式更改或对可用数据的更深入理解来实现。因此,我们鼓励使用 TOML 格式文件来保存 SQL 语句,允许在不添加更多子类到 Python 应用程序的情况下进行一些更改。TOML 格式的配置文件可以在文件名(和注释)中包含版本号,以清楚地说明它们是为哪个数据库模式设计的。
现在我们有了设计方法,确保我们有一个可交付成果列表,作为“完成”的定义是很重要的。
5.2.5 可交付成果
本项目有以下可交付成果:
-
docs
文件夹中的文档。 -
tests/features
和tests/steps
文件夹中的验收测试。 -
验收测试将涉及创建和销毁示例数据库作为测试固定装置。
-
tests
文件夹中的应用程序模块的单元测试。 -
数据库连接的模拟对象将是单元测试的一部分。
-
应用程序用于从 SQL 数据库获取数据。
我们将更详细地探讨这些可交付成果中的几个。
测试的模拟数据库连接和游标对象
对于数据获取应用程序,提供模拟连接对象以暴露提供给数据库的 SQL 和参数是至关重要的。此模拟对象还可以提供一个模拟游标作为查询结果。
如前所述,在 可交付成果 中,这意味着连接对象只应在 main()
函数中创建。这也意味着连接对象应该是任何执行数据库操作的其他函数或方法的参数。如果连接对象被一致引用,通过提供模拟连接对象进行测试就会变得更容易。
我们将分两部分来探讨这个问题:首先,是概念性的“给定”和“当”步骤;之后,我们将探讨“然后”步骤。这有时被称为“安排-行动-断言”。以下是 PyTest 测试用例的开始部分:
import sqlite3
from typing import Any, cast
from unittest.mock import Mock, call, sentinel
from pytest import fixture
import db_extract
import model
def test_build_sample(
mock_connection: sqlite3.Connection,
mock_config: dict[str, Any]
):
extract = db_extract.Extract()
results = list(
extract.series_iter(mock_connection, mock_config)
)
断言确认结果直接来自模拟对象,没有被转换、删除或因测试代码中的某些错误而损坏。断言看起来像这个例子:
assert results == [
model.Series(
name=sentinel.Name,
samples=[
model.SeriesSample(sentinel.X, sentinel.Y)
]
)
]
assert cast(Mock, mock_connection).execute.mock_calls == [
call(sentinel.Names_Query),
call(sentinel.Samples_Query, {’name’: sentinel.Name})
]
模拟连接对象必须提供具有适当结构的哨兵对象的结果,使其看起来像 SQLite3 执行数据库查询时返回的 Cursor
可迭代对象。
模拟连接似乎相当复杂,因为它涉及两个独立的模拟游标和一个模拟连接。以下是一个模拟连接的典型代码示例:
@fixture
def mock_connection() -> sqlite3.Connection:
names_cursor: list[tuple[Any, ...]] = [
(sentinel.Name,)
]
samples_cursor: list[tuple[Any, ...]] = [
(sentinel.X, sentinel.Y)
]
query_to_cursor: dict[sentinel, list[tuple[Any, ...]]] = {
sentinel.Names_Query: names_cursor,
sentinel.Samples_Query: samples_cursor
}
connection = Mock(
execute=Mock(
side_effect=lambda query, param=None: query_to_cursor[query]
)
)
return cast(sqlite3.Connection, connection)
模拟游标以简单的列表形式提供。如果测试代码使用了游标的其他功能,则需要一个更复杂的 Mock
对象。query_to_cursor
映射将结果与特定的查询关联起来。这里的想法是查询将是哨兵对象,而不是长的 SQL 字符串。
connection
对象使用了 Mock
对象的副作用特性。当 execute()
方法被评估时,调用会被记录,结果来自副作用函数。在这种情况下,它是一个使用 query_to_cursor
映射来定位适当游标结果的自定义对象。
这种使用副作用特性的做法避免了在测试单元的内部工作方式上做出过多的假设。SQL 将是一个哨兵对象,结果将包含哨兵对象。
在这种情况下,我们坚持认为,正在测试的单元不对从数据库检索的值进行任何额外的处理。在其他应用程序中,如果正在进行额外的处理,可能需要更复杂的模拟对象或测试字面量。
使用类似 (11,`` 13)
而不是 (sentinel.X,`` sentinel.Y)
来检查计算是否正确执行的情况并不罕见。然而,将 SQL 结果上的计算隔离到单独的函数中会更好。这允许将这些函数作为独立的单元进行测试。可以使用模拟函数对这些额外的计算进行测试。
此外,请注意使用 typing
模块中的 cast()
函数来告诉像 mypy 这样的工具,此对象可以像 Connection
对象一样使用。
新采集模块的单元测试
在这一系列章节中,整体 acquisition
模块变得更加灵活。目的是允许分析项目使用广泛的数据源。
实际上,修改应用程序以支持多种不同的 CSV 格式或多种不同的数据库模式更为常见。当 RESTful API 发生变化时,引入新的类作为现有类的替代方案,通常是一个好的策略。简单地修改或替换旧的定义——从某种意义上说——会抹去关于 API 为什么以及如何工作的有用历史。这是 SOLID 设计原则中的开放/封闭原则:设计对扩展是开放的,但对修改是封闭的。
从各种不同的数据源获取数据——如这些项目所示——比单一数据源的变化可能性要小。随着企业从电子表格迁移到中心数据库和 API,分析工具应跟随数据源。
对灵活数据获取的需求推动了为获取模块编写单元测试的需要,以覆盖预期的案例,并覆盖使用中可能出现的潜在错误和错误域。
使用 SQLite 数据库进行验收测试
验收测试需要创建(并销毁)一个测试数据库。测试通常需要在测试数据库中创建、检索、更新和删除数据,以安排给定步骤的数据或断言 Then 步骤的结果。
在本书的背景下,我们从项目 1.4:本地 SQL 数据库项目开始构建测试数据库。可轻松访问的、公开的关系数据库中可提取的数据不多。在大多数情况下,这些数据库被 RESTful API 所包装。
之前项目中构建的数据库有两个相反的使用案例:
-
它是用于测试目的的,可以自由删除和重建。
-
这个数据库必须被视为珍贵的商业数据,不应被删除或更新。
当我们将项目 1.4:本地 SQL 数据库中创建的数据库视为生产数据时,我们需要保护它免受意外变化的影响。
这意味着我们的验收测试必须构建一个独立的、小的测试数据库,与之前项目创建的“生产”数据库分开。
测试数据库不得与珍贵的商业数据冲突。
避免测试数据库与企业数据库冲突有两种常见策略:
-
在文件系统中使用 OS 级别的安全性来使损坏构成共享数据库的文件变得困难。此外,使用严格的命名约定可以将测试数据库放入一个不会与生产数据库冲突的单独命名空间中。
-
在Docker 容器中运行测试,以创建一个无法接触生产数据的虚拟环境。
正如我们在方法中提到的,数据库背后的想法涉及两个容器:
-
为提取数据的应用程序组件创建一个容器。
-
为提供数据的数据库组件创建一个容器。验收测试可以创建一个短暂的数据库服务。
然而,使用 SQLite,没有明确的数据库服务容器。数据库组件成为应用程序组件的一部分,并在应用程序的容器中运行。没有单独的服务容器意味着 SQLite 打破了适用于大型企业数据库的概念性双容器模型。我们无法为测试目的创建一个临时的、模拟的数据库服务。
由于 SQLite 数据库不过是一个文件,我们必须关注操作系统级别的权限、文件系统路径和命名约定,以确保我们的测试数据库与早期项目中创建的生产数据库分开。我们强调这一点,因为与更复杂的数据库引擎(如 MySQL 或 PostgreSQL)一起工作也将涉及相同的权限、文件路径和命名约定考虑。更大的数据库将增加更多的考虑因素,但基础将是相似的。
在创建数据分析应用程序时,避免干扰生产操作是至关重要的。
构建和销毁临时 SQLite 数据库文件暗示了使用@fixture
来创建数据库并填充所需的表、视图、索引等模式。单个场景的给定步骤可以提供测试所需的数据排列的摘要。
我们将探讨如何将其定义为特征。然后,我们可以查看实现配置文件所需的步骤以及步骤定义。
特征文件
下面是一种似乎能够捕捉 SQL 提取应用程序本质的场景:
@fixture.sqlite
Scenario: Extract data from the enterprise database
Given a series named "test1"
And sample values "[(11, 13), (17, 19)]"
When we run the database extract command with the test fixture database
Then log has INFO line with "series: test1"
And log has INFO line with "count: 2"
And output directory has file named "quartet/test1.csv"
@fixture.
标签遵循将特定、可重用配置文件与场景关联的常见命名约定。除了指定要使用的配置文件之外,还有许多其他目的可以用于标记场景。在这种情况下,配置文件信息用于构建一个具有空模式的 SQLite 数据库。
给定步骤提供一些数据以加载到数据库中。对于这个验收测试,使用了一个只有几个样本的单个系列。
标签信息可以被behave工具使用。我们将探讨如何编写before_tag()
函数来为任何需要它的场景创建(并销毁)临时数据库。
sqlite 配置文件
配置文件通常定义在behave工具使用的environment.py
模块中。before_tag()
函数用于处理特征或特征内的场景的标签。这个函数让我们可以将特定的特征函数与场景关联起来:
from behave import fixture, use_fixture
from behave.runner import Context
def before_tag(context: Context, tag: str) -> None:
if tag == "fixture.sqlite":
use_fixture(sqlite_database, context)
use_fixture()
函数告诉behave运行器使用给定的参数值调用给定的函数sqlite_database()
,在这种情况下,是context
对象。sqlite_database()
函数应该是一个生成器:它可以准备数据库,执行yield
语句,然后销毁数据库。behave运行器将消费生成的值作为设置测试的一部分,并在需要清理测试时再消费一个值。
创建(并销毁)数据库的函数具有以下轮廓:
from collections.abc import Iterator
from pathlib import Path
import shutil
import sqlite3
from tempfile import mkdtemp
import tomllib
from behave import fixture, use_fixture
from behave.runner import Context
@fixture
def sqlite_database(context: Context) -> Iterator[str]:
# Setup: Build the database files (shown later).
yield context.db_uri
# Teardown: Delete the database files (shown later).
我们已经将这个函数分解为三个部分:设置、允许测试场景继续的yield
,以及清理。我们将分别查看设置:构建数据库文件
和清理:删除数据库文件
这两个部分。
sqlite_database()
函数的设置处理过程如下所示:
# Get Config with SQL to build schema.
config_path = Path.cwd() / "schema.toml"
with config_path.open() as config_file:
config = tomllib.load(config_file)
create_sql = config[’definition’][’create’]
context.manipulation_sql = config[’manipulation’]
# Build database file.
context.working_path = Path(mkdtemp())
context.db_path = context.working_path / "test_example.db"
context.db_uri = f"file:{context.db_path}"
context.connection = sqlite3.connect(context.db_uri, uri=True)
for stmt in create_sql:
context.connection.execute(stmt)
context.connection.commit()
配置文件是从当前工作目录读取的。创建数据库和执行数据操作的 SQL 语句是从模式中提取的。数据库创建的 SQL 将在标签发现期间执行。操作 SQL 将放入上下文中,供稍后执行的 Given 步骤使用。
此外,上下文还加载了一个工作路径,该路径将用于数据库文件以及输出文件。上下文将有一个db_uri
字符串,数据提取应用程序可以使用该字符串定位测试数据库。
一旦上下文已填充,可以执行单个 SQL 语句来构建空数据库。
在yield
语句之后,以下片段显示了sqlite_database()
函数的清理处理:
context.connection.close()
shutil.rmtree(context.working_path)
在删除文件之前,必须关闭 SQLite3 数据库。shutil
包包含在文件和目录上工作的高级函数。rmtree()
函数会删除整个目录树以及树中的所有文件。
此固定装置创建了一个工作数据库。我们现在可以编写依赖于此装置的步骤定义。
步骤定义
我们将展示两个步骤定义来将系列和样本插入数据库。以下示例显示了Given
步骤之一的实现:
@given(u’a series named "{name}"’)
def step_impl(context, name):
insert_series = context.manipulation_sql[’insert_series’]
cursor = context.connection.execute(
insert_series,
{’series_id’: 99, ’name’: name}
)
context.connection.commit()
上面的步骤定义使用 SQL 在series
表中创建一个新行。它使用上下文中的连接;这是由sqlite_database()
函数创建的,该函数是通过before_tag()
函数成为测试序列的一部分。
以下示例显示了另一个Given
步骤的实现:
@given(u’sample values "{list_of_pairs}"’)
def step_impl(context, list_of_pairs):
pairs = literal_eval(list_of_pairs)
insert_values = context.manipulation_sql[’insert_values’]
for seq, row in enumerate(pairs):
cursor = context.connection.execute(
insert_values,
{’series_id’: 99, ’sequence’: seq, ’x’: row[0], ’y’: row[1]}
)
context.connection.commit()
上面的步骤定义使用 SQL 在series_sample
表中创建一个新行。它还使用来自上下文的关系。
一旦系列和样本已插入数据库,When
步骤可以使用上下文中的数据库 URI 信息运行数据采集应用程序。
Then
步骤可以确认应用程序运行的结果与由固定装置种子的数据库和Given
步骤匹配。
在此测试框架到位后,您可以运行验收测试套件。在做出任何编程更改之前运行验收测试是常见的;这揭示了acquire
应用程序未能通过所有测试。
在下一节中,我们将查看数据库提取模块并重写主应用程序。
数据库提取模块和重构
此项目建议对先前项目编写的代码进行三种类型的更改:
-
修改
model
模块以扩展“系列”的含义:它是一个具有名称和子对象列表的父对象。 -
将
db_extract
模块添加到从 SQL 数据库中抓取数据的任务中。 -
更新
acquire
模块以从任何可用的来源收集数据并创建 CSV 文件。
重构model
模块会对其他项目产生连锁反应,需要更改这些模块以更改数据结构名称。
正如我们在方法中提到的,通常项目开始时对理解的认识是逐步发展和演变的。对问题领域、用户和技术的更多了解会改变我们的理解。这个项目反映了理解的转变,并导致需要改变之前完成的项目实现。
这其中的一个后果是暴露了系列的名称。在之前章节的项目中,四个系列具有由应用程序程序任意指定的名称。可能文件名会是"series_1.csv"
或类似的东西。
与 SQL 数据一起工作暴露了一个新的属性,即系列名称。这导致在处理这个新属性时有两个深刻的抉择:
-
忽略这个新属性。
-
修改之前的工程以引入系列名称。
系列名称应该是文件名吗?这似乎是个糟糕的想法,因为系列名称可能包含空格或其他不规则的标点符号。
似乎还需要一些额外的元数据来保留系列名称并将系列名称与文件名关联起来。这可能是一个额外的文件,可能是作为提取操作的一部分创建的 JSON 或 TOML 格式的文件。
5.3 摘要
本章的项目涵盖了以下两个基本技能:
-
构建 SQL 数据库。这包括构建生产数据库的代表性以及构建测试数据库。
-
从 SQL 数据库中提取数据。
这当然需要学习一些 SQL。SQL 有时被称为数据处理领域的通用语言。许多组织都有 SQL 数据库,并且必须提取数据进行分析。
另一个重要的是学会在珍贵生产数据的存在下工作。考虑命名约定、文件系统路径以及与数据库服务器和正在使用的文件相关的权限非常重要。试图提取分析数据并不是与生产操作冲突的好理由。
编写使用临时数据库的验收测试所需的工作量是一项重要的额外技能。能够为测试目的创建数据库允许通过识别有问题的数据,围绕它创建测试用例,然后在隔离的开发环境中进行调试。此外,拥有临时数据库允许检查可能有助于分析或解决生产数据不确定性的生产数据库的更改。
在下一章中,我们将从大量获取数据过渡到理解数据的相对完整性和有用性。我们将构建一些工具来检查我们获取的原始数据。
5.4 补充内容
这里有一些想法供读者添加到这个项目中。
5.4.1 考虑使用另一个数据库
例如,MySQL 或 PostgreSQL 是不错的选择。这些可以在个人计算机上下载并安装,用于非商业目的。管理开销并不算过分。
认识到这些工具相当庞大且复杂是至关重要的。对于 SQL 新手来说,在尝试安装、配置和使用这些数据库时,有很多东西要学习。
请参阅dev.mysql.com/doc/mysql-getting-started/en/
获取有关安装和使用 MySQL 的一些建议。
请参阅www.postgresql.org/docs/current/tutorial-start.html
获取有关安装和使用 PostgreSQL 的建议。
在某些情况下,探索在虚拟机上使用 Docker 容器运行数据库服务器是有意义的。请参阅www.packtpub.com/product/docker-for-developers/9781789536058
了解更多关于使用 Docker 作为在隔离环境中运行复杂服务的方法。
请参阅dev.mysql.com/doc/refman/8.0/en/docker-mysql-getting-started.html
获取在 Docker 容器中使用 MySQL 的方法。
请参阅www.docker.com/blog/how-to-use-the-postgres-docker-official-image/
获取有关在 Docker 容器中运行 PostgreSQL 的信息。
5.4.2 考虑使用 NoSQL 数据库
NoSQL 数据库提供了许多数据库功能——包括可靠持久的数据和共享访问——但避免(或扩展)了关系数据模型,并取代了 SQL 语言。
这导致的数据获取应用程序与本章中的示例有些相似。有一个连接到服务器,并从服务器提取数据的请求。这些请求不是 SQL SELECT
语句。结果也不一定是完全规范化的数据结构中的数据行。
例如,MongoDB。数据结构不是行和表,而是 JSON 文档。请参阅www.packtpub.com/product/mastering-mongodb-4x-second-edition/9781789617870
了解更多信息。
使用 MongoDB 将数据获取转变为定位 JSON 文档,然后从数据库中的源数据构建所需的文档。
这将导致两个项目,类似于本章中描述的两个项目,用于向“生产”Mongo 数据库中填充一些要提取的数据,然后编写获取程序从数据库中提取数据。
另一个选择是使用带有 JSON 对象的数据列值的 PostgreSQL 数据库。这提供了类似于 MongoDB 的功能,使用 PostgreSQL 引擎。请参阅www.postgresql.org/docs/9.3/functions-json.html
获取有关此方法的更多信息。
这里是一些常见的 NoSQL 数据库类别:
-
文档数据库
-
键值存储
-
列式数据库
-
图数据库
鼓励读者在这些类别中搜索代表性产品,并考虑本章的两部分内容:加载数据库和从数据库获取数据。
5.4.3 考虑使用 SQLAlchemy 定义一个 ORM 层
在对象关系映射(ORM)问题中,我们讨论了 ORM 问题。在那个部分,我们提出了使用工具为现有数据库配置 ORM 包有时可能会出现问题的观点。
然而,这个数据库非常小。它是学习简单 ORM 配置的理想候选者。
我们建议从 SQLAlchemy ORM 层开始。请参阅docs.sqlalchemy.org/en/20/orm/quickstart.html
以获取有关配置可映射到表的类定义的建议。这将消除从数据库进行提取时编写 SQL 的需求。
对于 Python,也有其他 ORM 包可用。读者可以自由地定位一个 ORM 包,并使用 ORM 数据模型在本章中构建提取项目。
第六章
项目 2.1:数据检查笔记本
我们经常需要对源数据进行临时检查。特别是,当我们第一次获取新数据时,我们需要查看文件以确保它符合预期。此外,调试和问题解决也受益于临时数据检查。本章将指导您使用 Jupyter 笔记本来调查数据并找到属性的结构和域。
前几章主要关注一个简单的数据集,其中数据类型看起来像是明显的浮点值。对于这样一个简单的数据集,检查不会非常复杂。
从一个简单的数据集开始,关注工具及其如何协同工作可能会有所帮助。因此,我们将继续使用相对较小的数据集,让您了解工具,而无需承担同时理解数据的负担。
本章的项目涵盖了如何创建和使用 Jupyter 笔记本进行数据检查。这提供了极大的灵活性,这在首次查看新数据时通常是非常需要的。当诊断意外发生变化的数据问题时,这也是至关重要的。
Jupyter 笔记本本质上是交互式的,使我们免于设计和构建交互式应用程序。相反,我们需要自律,只使用笔记本来检查数据,绝不能用于应用更改。
本章有一个项目,即构建检查笔记本。我们将从描述笔记本的用途开始。
6.1 描述
当面对从源应用程序、数据库或 Web API 获取的原始数据时,仔细检查数据以确保它确实可以用于所需的分析是明智的。通常会发现数据并不完全符合给定的描述。也可能发现元数据已过时或不完整。
本项目的根本原则如下:
我们并不总是知道实际数据看起来像什么。
数据可能存在错误,因为源应用程序有缺陷。可能有“未记录的功能”,这些功能类似于缺陷,但具有更好的解释。用户可能已经采取了行动,引入了新的代码或状态标志。例如,一个应用程序可能在应付账款记录上有一个“注释”字段,会计人员可能发明了自己的编码值,并将它们放在该字段的最后几个字符中。这定义了一个企业软件之外的手动流程。这是一个重要的业务流程,包含有价值的数据;它不是任何软件的一部分。
构建一个有用的 Jupyter 笔记本的一般过程通常包括以下阶段:
-
从简单的选定行显示开始。
-
然后,显示看似数值字段的范围。
-
之后,在单独的分析笔记本中,我们可以在数据清理后找到中心趋势(均值、中位数和标准差)值。
使用笔记本使我们摆脱了之前章节对 CLI 应用程序的关注。这是必要的,因为笔记本是交互式的。它旨在允许在很少的限制下进行探索。
用户体验(UX)有两个一般步骤:
-
运行数据采集应用程序。这是任何前几章项目中 CLI 命令之一。
-
启动 Jupyter Lab 服务器。这是启动服务器的第二个 CLI 命令。
jupyter lab
命令将启动一个浏览器会话。其余的工作通过浏览器完成:-
通过点击笔记本图标创建一个笔记本。
-
通过在单元格中输入一些 Python 代码来加载数据。
-
通过创建单元格来显示数据和展示数据的属性,以确定数据是否有用。
-
更多关于 Jupyter 的信息,请参阅www.packtpub.com/product/learning-jupyter/9781785884870
。
6.1.1 关于源数据
这里一个基本的要素是,所有数据采集项目必须以一致的形式产生输出。我们建议使用 NDJSON(有时称为 JSON NL)作为保留原始数据的格式。有关文件格式的更多信息,请参阅第三章,项目 1.1:数据采集基础应用程序。
必须审查之前项目的验收测试套件,以确保有一个测试来确认输出文件具有正确、一致的形式。
为了回顾数据流,我们做了以下工作:
-
从某些源读取。这包括文件、RESTful API、HTML 页面和 SQL 数据库。
-
以基本文本形式保留了原始数据,去除了可能由 SQL 数据库或 RESTful JSON 文档强加的任何数据类型信息。
检查步骤将查看这些文件中值的文本版本。后续项目,从第九章开始,将研究将数据从文本转换为对分析工作更有用的形式。
检查笔记本通常需要做一些数据清理,以便展示数据问题。这将足够清理以了解数据,而无需更多。后续项目将扩展清理以涵盖所有数据问题。
在许多数据采集项目中,在初步检查之前尝试任何数据转换是不明智的。这是因为数据高度可变且文档记录不佳。一种有纪律的三步法将数据采集和检查与数据转换和处理的尝试分开。
我们可能在数据源中发现各种意想不到的事物。例如,CSV 文件可能有一个意外的标题,导致一行错误数据。或者,CSV 文件有时可能缺少标题,迫使获取应用程序提供默认标题。一个被描述为 CSV 的文件可能没有分隔符,但可能有固定大小的文本字段,用空格填充。可能有可以忽略的空行。可能有空行分隔有用的数据,并将其与脚注或其他非数据文件分开。ZIP 存档可能包含除所需数据文件之外的大量无关文件。
可能最糟糕的问题之一是尝试处理未使用广泛使用的字符编码(如 UTF-8)准备的文件。使用 CP-1252 编码的文件,当解码器假设它是 UTF-8 编码时,可能会有一些看起来奇怪的字符。Python 的 codecs
模块提供了一些替代文件编码形式来处理这类问题。这个问题似乎很少见;一些组织会注意文本的编码以防止问题。
检查笔记本通常在数据采集过程中以 print()
函数开始,用于显示数据。这里的想法是稍微扩展这个概念,用交互式笔记本代替 print()
来查看数据,并确认它符合预期。
并非所有经理都同意花时间构建检查笔记本。通常,这会与现实假设产生冲突,以下是一些潜在的后果:
-
经理可能认为数据不会有惊喜;数据将完全符合数据合同或其他模式定义中的指定。
-
当数据不符合预期时,数据检查笔记本将是调试工作的有益部分。
-
在不太可能的情况下,数据确实符合预期时,数据检查笔记本可以用来证明数据是有效的。
-
-
经理可能认为数据不太可能是正确的。在这种情况下,数据检查笔记本将被视为揭示不可避免问题的有用工具。
笔记本通常从 print()
或日志输出开始,以确认数据是有用的。这种调试输出可以迁移到一个非正式的笔记本——成本较低——并演变成更完整、更专注于检查和数据质量保证的东西。
这个初始项目不会构建一个复杂的笔记本。目的是提供一个交互式的数据展示,允许探索和研究。在下一节中,我们将概述这个项目的方法,以及一般使用笔记本的方法。
6.2 方法
我们将在查看我们的方法时借鉴 C4 模型(c4model.com
)。
-
上下文:对于这个项目,上下文图有两个用例:采集和检查
-
容器:有一个容器用于各种应用程序:用户的个人电脑
-
组件:存在两组显著不同的软件组件集合:获取程序和检查笔记本
-
代码:我们将简要介绍以提供一些建议方向
该应用程序的上下文图显示在图 6.1中。
图 6.1:上下文图
数据分析师将使用 CLI 运行数据获取程序。然后,分析师将使用 CLI 启动 Jupyter Lab 服务器。使用浏览器,分析师可以使用 Jupyter Lab 检查数据。
组件分为两大类。组件图显示在图 6.2中。
图 6.2:组件图
该图显示了数据分析师看到的接口,即terminal
和browser
。这些接口以统一建模语言(UML)中的边界图标表示。
组件Acquisition
组包含各种模块和整体获取应用程序。这是从命令行运行以从适当的来源获取原始数据的。db_extract
模块与外部 SQL 数据库相关联。api_download
模块与外部 RESTful API 相关联。可以在此图的这部分添加额外的来源和处理模块。
组件Acquisition
组执行的处理创建了Storage
组中显示的数据文件。此组描述了由acquire
应用程序获取的原始数据文件。这些文件将由进一步的分析应用程序进行精炼和处理。
Inspection
组显示jupyter
组件。这是整个 Jupyter Lab 应用程序,总结为一个图标。notebook
组件是我们将在本应用程序中构建的笔记本。此笔记本依赖于 Jupyter Lab。
browser
组件以边界图标显示。其目的是通过浏览器来表征笔记本交互,从而体现用户体验。
notebook
组件将使用多个内置 Python 模块。此笔记本的单元可以分解为两种更小的组件类型:
-
用于从获取文件中收集数据的函数。
-
用于显示原始数据的函数。
collections.Counter
类对此非常有用。
您需要为该项目定位(并安装)一个 Jupyter Lab 版本。这需要添加到requirements-dev.txt
文件中,以便其他开发者知道安装它。
当使用conda
管理虚拟环境时,命令可能如下所示:
% conda install jupyterlab
当使用其他工具管理虚拟环境时,命令可能如下所示:
% python -m pip install jupyterlab
安装jupyter
产品后,必须从命令行启动。此命令将启动服务器并打开一个浏览器窗口:
% jupyter lab
关于使用 Jupyter Lab 的信息,请参阅jupyterlab.readthedocs.io/en/latest/
。
如果你不太熟悉 Jupyter,现在是时候使用教程学习基础知识,然后再继续这个项目。
许多笔记本示例将包括import
语句。
对于刚开始使用 Jupyter 笔记本的开发者来说,不应该将这视为在笔记本中多个单元格中重复import
语句的建议。
在实际的笔记本中,导入可以收集在一起,通常在一个单独的单元格中引入所有需要的包。
在一些企业中,使用启动脚本提供一系列紧密相关的笔记本的共同导入集。
我们将在第十三章、项目 4.1:可视化分析技术中返回更多灵活的方式来处理笔记本中的 Python 库。
对于这个项目,有两个其他重要的考虑因素:为笔记本编写自动化测试的能力以及 Python 模块和笔记本的交互。我们将在单独的章节中探讨这些主题。
6.2.1 函数的笔记本测试用例
对于 Python 包来说,通常需要单元测试用例。为了确保测试用例有意义,一些企业坚持测试用例要练习模块中 100%的代码行。对于某些行业,所有逻辑路径都必须经过测试。更多信息,请参阅第一章、项目零:其他项目的模板。
对于笔记本来说,自动化测试可能比 Python 模块或包更复杂。复杂之处在于笔记本可能包含任意代码,这些代码没有考虑到可测试性。
为了有一个有纪律、可重复的创建笔记本的方法,有助于在一系列阶段中开发笔记本,逐步发展成为一个支持自动化测试的笔记本。
笔记本是一种软件,没有测试用例,任何软件都是不可信的。在罕见的情况下,笔记本的代码足够简单,我们可以检查它以发展对其整体适用性的某种感觉。在大多数情况下,复杂的计算、函数和类定义需要测试用例来证明代码可以信赖能正确工作。
笔记本演变的阶段通常如下所示:
-
在零阶段,笔记本通常以单元格中的任意 Python 代码开始,并且几乎没有函数或类定义。这是一个很好的开始开发的方式,因为笔记本的交互性提供了即时结果。一些单元格中可能会有错误或不良的想法。处理单元格的顺序并不是简单地从上到下。这段代码难以(或不可能)通过任何自动化测试进行验证。
-
第一阶段会将单元格表达式转换为函数和类定义。这个版本的笔记本也可以包含使用函数和类的示例单元格。顺序更接近严格的上至下;有较少的单元格包含已知错误。示例的存在作为验证笔记本处理的基础,但自动测试不可用。
-
第二阶段有更健壮的测试,使用正式的
assert
语句或doctest
注释来定义可重复的测试过程。在做出任何更改后,从笔记本的开始处重新运行笔记本将执行assert
语句以验证笔记本。所有单元格都是有效的,笔记本处理是严格的上至下。 -
当有更复杂或可重用的处理时,可能有助于将函数和类定义从笔记本中移出,放入模块中。该模块将有一个单元测试模块,或者可以通过 doctest 示例进行测试。这个新模块将被笔记本导入;笔记本更多地用于展示结果,而不是新想法的开发。
自动测试的一条简单途径是在函数和类定义中包含 doctest 示例。例如,我们可能有一个笔记本单元格包含以下函数定义:
def min_x(series: Series) -> float:
"""
>>> s = [
... {’x’: ’3’, ’y’: ’4’},
... {’x’: ’2’, ’y’: ’3’},
... {’x’: ’5’, ’y’: ’6’}]
>>> min_x(s)
2
"""
return min(int(s[’x’]) for s in series.samples)
函数文档字符串中标记为 >>>
的行会被 doctest 工具识别。这些行会被评估,并与文档字符串中的示例进行比较。
笔记本中的最后一个单元格可以执行 doctest.testmod()
函数。这将检查笔记本中的所有类和函数定义,定位它们的 doctest 示例,并确认实际结果与预期相符。
想要了解更多帮助笔记本测试的工具,请参阅 testbook.readthedocs.io/en/latest/
。
这种从记录好想法的地方到工程化解决方案的演变并非简单线性。常常有探索和学习的机会,导致变化和关注点的转移。将笔记本作为跟踪好想法和坏想法的工具是很常见的。
笔记本也是一个展示数据是否满足用户期望的最终、清晰图像的工具。在这个第二个用例中,分离函数和类定义变得更加重要。我们将在下一节简要讨论这一点。
6.2.2 在单独模块中的常用代码
如我们之前所述,笔记本允许一个想法通过几种形式演变。
我们可能有一个包含以下内容的单元格
x_values = []
for s in source_data[1:]:
x_values.append(float(s[’x’]))
min(x_values)
注意,这个计算跳过了序列中的第一个值。这是因为源数据有一个由 csv.reader()
函数读取的标题行。切换到 csv.DictReader()
可以礼貌地跳过这一行,但也会改变结果结构,从字符串列表变为字典。
这个最小值的计算可以重新表述为一个函数定义。由于它做了三件事——删除第一行,提取 ’x’
属性,并将其转换为浮点数——可能将其分解为三个函数会更好。它还可以重构以在每个函数中包含 doctest 示例。参见函数的笔记本测试用例以获取示例。
之后,这个函数可以从笔记本单元格中剪切并粘贴到一个单独的模块中。我们假设整体函数被命名为 min_x()
。我们可能会将其添加到一个名为 series_stats.py
的模块中。然后笔记本可以导入并使用这个函数,将定义作为侧边栏细节:
from series_stats import min_x
当将笔记本重构为可重用模块时,重要的是要使用 剪切 和粘贴,而不是复制和粘贴。函数的副本如果其中一个副本被修改以改进性能或修复问题,而另一个副本保持未更改,则会导致问题。这有时被称为 不要重复 自己 (DRY) 原则。
当与仍在开发中的外部模块一起工作时,对模块的任何更改都需要停止笔记本内核,并从非常开始重新运行笔记本以删除和重新加载函数定义。这可能会变得尴尬。有一些 iPython 扩展可以用来重新加载模块,甚至在源模块更改时自动重新加载模块。
另一个选择是在将笔记本重构为单独模块之前,等待函数或类看起来成熟且不太可能更改。通常,这个决定是在创建用于显示有用结果的最终演示笔记本时做出的。
我们现在可以查看这个项目的具体交付物清单。
6.3 交付物
这个项目有以下交付物:
-
一个
pyproject.toml
文件,用于标识所使用的工具。对于这本书,我们使用了jupyterlab==3.5.3
。请注意,在本书准备出版期间,已经发布了 4.0 版本。这种组件的持续进化使得您找到最新版本而不是这里引用的版本变得很重要。 -
docs
文件夹中的文档。 -
tests
文件夹中任何新应用程序模块的单元测试。 -
src
文件夹中任何新应用程序模块的代码,这些代码将被检查笔记本使用。 -
一个用于检查从任何来源获取的原始数据的笔记本。
在第一章、项目零:其他项目的模板中建议的项目目录结构。在交付物清单中查看更多信息。前几章没有使用任何笔记本,因此这个目录可能一开始就没有创建。对于这个项目,需要 snotebooks
目录。
让我们更详细地看看这些交付物中的几个。
6.3.1 笔记本 .ipynb
文件
笔记本可以是(并且应该是)Markdown 单元格提供笔记和上下文,以及计算单元格显示数据的混合体。
跟随项目到这一点的读者可能有一个包含需要读取以构建有用 Python 对象的 NDJSON 文件的目录。一个很好的方法是为读取文件中的行定义一个函数,并使用json.loads()
将文本行转换成包含有用数据的小字典。
没有充分的理由使用model
模块的类定义来进行这个检查。类定义可以帮助使数据更容易访问。
检查过程从命名文件的单元格开始,创建Path
对象。
以下示例中的函数代码可能会有所帮助:
import csv
from collections.abc import Iterator
import json
from typing import TextIO
def samples_iter(source: TextIO) -> Iterator[dict[str, str]]:
yield from (json.loads(line) for line in source)
这个函数将遍历获取到的数据。在许多情况下,我们可以使用迭代器扫描大量样本,选择单个属性值或样本的子集。
我们可以使用以下语句从给定路径创建列表-字典结构:
from pathlib import Path
source_path = Path("/path/to/quartet/Series_1.ndjson")
with source_path.open() as source_file:
source_data = list(samples_iter(source_file))
我们可以在笔记本的几个单元格中开始这些基础知识。有了这个基础,后续的单元格可以探索可用的数据。
用于分析数据的单元格和函数
对于这个初步检查项目,分析需求很小。前几章的示例数据集是人工数据,旨在展示探索性数据分析需要使用图形技术。
对于其他数据集,可能存在各种奇怪或不寻常的问题。
例如,CO2 PPM — 大气二氧化碳趋势数据集,可在datahub.io/core/co2-ppm
找到,数据中包含多个“缺失值”代码。以下有两个示例:
-
二氧化碳的平均值有时使用-99.*99 作为不可用测量时的占位符。在这些情况下,统计过程使用相邻月份的数据来插补缺失值。
-
此外,对于一个月的总结中有效数据的数量并未记录,使用-1 值表示。
这个数据集需要更多的注意,以确保每列的值以及列的含义。
在这里,捕捉给定列的值域是有帮助的。collections
模块中的Counter
对象非常适合理解特定列中的数据。
一个单元格可以使用三步计算来查看值的域:
-
使用
samples_iter()
函数产生源文档。 -
创建一个具有样本属性值的生成器。
-
创建一个
Counter
来总结这些值。
这可能导致笔记本中的一个单元格包含以下语句:
from collections import Counter
values_x = (sample[’x’] for sample in source_data)
domain_x = Counter(values_x)
笔记本中的下一个单元格可以显示 domain_x
值。如果使用 csv.reader()
函数,它将显示标题以及值域。如果使用 csv.DictReader()
类,则此集合将不包括标题。这允许对样本集合中的各种属性进行整洁的探索。
检查笔记本不是尝试更复杂数据分析的地方。计算均值或中位数应仅针对清洗后的数据进行。我们将在 第十五章、项目 5.1:建模基础应用 中再次讨论这个问题。
带有 Markdown 解释的单元格
包含使用 Markdown 编写的单元格,以提供有关数据的信息、见解和经验教训。
关于 Markdown 语言的更多信息,请参阅 Daring Fireball 网站:daringfireball.net/projects/markdown/basics
。
如本章前面所述,笔记本有两种一般类型:
-
探索性:这些笔记本是一系列关于数据和探索、检查数据过程的博客文章。由于它们是工作过程中的作品,因此并非所有单元格都能正常工作。
-
展示性:这些笔记本是对数据或问题的更精致、最终报告。导致死胡同的路径应修剪成经验教训的总结。
这两种类型的笔记本之间有一条清晰的界限。区分因素是笔记本的可重复性。对于展示有用的笔记本可以从头到尾运行,无需手动干预来修复问题或跳过带有语法错误或其他问题的单元格。否则,笔记本是探索的一部分。通常需要复制并编辑探索性笔记本以创建一个专注于展示的衍生笔记本。
通常,用于展示的笔记本使用 Markdown 单元格创建类似于书籍或期刊文章章节的叙事流程。我们将在 第十四章、项目 4.2:创建报告 中回到更正式的报告。
带有测试用例的单元格
之前,我们介绍了一个名为 samples_iter()
的函数,但该函数缺少任何单元测试或示例。在笔记本中提供 doctest 字符串会更有帮助:
def samples_iter(source: TextIO) -> Iterator[dict[str, str]]:
"""
# Build NDJSON file with two lines
>>> import json
>>> from io import StringIO
>>> source_data = [
... {’x’: 0, ’y’: 42},
... {’x’: 1, ’y’: 99},
... ]
>>> source_text = [json.dumps(sample) for sample in source_data]
>>> ndjson_file = StringIO(’\\n’.join(source_text))
# Parse the file
>>> list(samples_iter(ndjson_file))
[{’x’: 0, ’y’: 42}, {’x’: 1, ’y’: 99}]
"""
yield from (json.loads(line) for line in source)
此函数的文档字符串包含一个广泛的测试用例。该测试用例从一个包含两个字典的列表中构建一个 NDJSON 文档。然后,测试用例应用 samples_iter()
函数来解析 NDJSON 文件并恢复原始的两个样本。
要执行此测试,笔记本需要一个单元格来检查笔记本中定义的所有函数和类的文档字符串:
import doctest
doctest.testmod()
这之所以有效,是因为笔记本的全局上下文被当作一个具有默认名称__main__
的模块来处理。这个模块将由textmod()
函数检查,以找到看起来包含 doc test 示例的 doc 字符串。
让最后一个单元格运行doctest工具使得运行笔记本、滚动到末尾并确认所有测试都已通过变得容易。这是一种极好的验证方式。
6.3.2 执行笔记本的测试套件
Jupyter 笔记本本质上是交互式的。这使得对笔记本进行自动验收测试可能具有挑战性。
幸运的是,有一个命令可以执行笔记本以确认它从头到尾都能正常工作,没有任何问题。
我们可以使用以下命令来执行笔记本,以确认所有单元格都将执行而不会出现任何错误:
% jupyter execute notebooks/example_2.ipynb
笔记本可能需要处理大量数据,这使得作为一个整体测试笔记本非常耗时。这可能导致使用一个单元格来读取配置文件,并使用这些信息来使用数据子集进行测试目的。
6.4 摘要
本章的项目涵盖了创建和使用 Jupyter Lab 笔记本进行数据检查的基础。这提供了巨大的灵活性,这在首次查看新数据时通常是需要的要求。
我们还探讨了向函数添加doctest示例,并在笔记本的最后一个单元格中运行doctest工具。这让我们能够验证笔记本中的代码很可能工作正常。
现在我们已经得到了一个初步检查笔记本,我们可以开始考虑正在获取的具体数据类型。在下一章中,我们将向这个笔记本添加功能。
6.5 额外内容
这里有一些想法供您添加到这个项目中。
6.5.1 使用 pandas 检查数据
交互式数据探索的一个常用工具是pandas
包。
更多信息请参阅pandas.pydata.org
。
此外,还可以参阅www.packtpub.com/product/learning-pandas/9781783985128
以获取更多关于 pandas 的学习资源。
使用 pandas 来检查文本的价值可能有限。pandas 的真实价值在于对数据进行更复杂的统计和图形分析。
我们鼓励您使用 pandas 加载 NDJSON 文档,并对数据值进行初步调查。
第七章
数据检查功能
数据域大致分为三种类型:基数、顺序和名义。本章的第一个项目将指导您检查基数数据;这些数据值如重量、测量和持续时间是连续的,以及计数数据是离散的。第二个项目将指导推理者检查涉及日期等内容的顺序数据,其中顺序很重要,但数据不是正确的测量;它更像是一个代码或标识符。名义数据是一个使用数字的代码,但不代表数值。第三个项目将涵盖在单独的数据源之间匹配键的更复杂情况。
在查看新数据时需要一份检查笔记本。这是一个记录笔记和所学知识的好地方。在诊断更成熟的分析流程中出现的问题时,它非常有帮助。
本章将涵盖与数据检查技术相关的一系列技能:
-
使用 Python 表达式进行的基本笔记本数据检查功能,这些功能是从上一章扩展而来的。
-
用于检查基数数据的
statistics
模块。 -
用于检查顺序和名义数据的
collections.Counter
类。 -
一些额外的
collections.Counter
用于匹配主键和外键。
对于在第三章、第四章和第五章中使用的 Ancombe 的四重奏示例数据集,两个属性值都是基数数据。这是一个对某些检查有帮助的数据集,但我们需要查看本章后面项目中的其他数据集。我们将首先查看一些用于基数数据的检查技术。专注于其他数据集的读者需要辨别哪些属性代表基数数据。
7.1 项目 2.2:验证基数域——测量、计数和持续时间
大量的数据在本质上都是基数。基数用于计数,例如集合的元素。这个概念可以推广到包括表示重量或测量的实数。
在这里可以找到一个非常有趣的数据集:www.kaggle.com/datasets/rtatman/iris-dataset-json-version
。这个数据集包含了不同物种的花瓣和雄蕊的多次测量样本。由于提供了单位 mm,这些测量是可以识别的。
另一个有趣的数据集在这里可以找到:datahub.io/core/co2-ppm
。这个数据集包含了以 ppm(百万分之一)为单位测量的二氧化碳水平数据。
我们需要区分计数和度量与仅用于对事物进行排序或排列的数字,这些数字被称为序数。此外,类似数字的数据有时只是一个代码。例如,美国邮政编码只是一串数字;它们不是正确的数值。我们将在项目 2.3:验证文本和代码——名义数据和序数中查看这些数值。
由于这是一个检查笔记本,主要目的是仅了解基数数据的值范围。更深入的分析将在以后进行。目前,我们想要一个笔记本来证明数据是完整和一致的,并且可以用于进一步处理。
如果一个企业正在使用数据合同,这个笔记本将演示符合数据合同的情况。使用数据合同时,重点可能略微从显示“一些不可用的数据”转移到显示“发现不符合合同的数据。”在合同对于分析消费者来说不足够的情况下,笔记本可能进一步转向显示“有用但符合的数据。”
我们将从描述要添加到检查笔记本中的单元格类型开始。然后,我们将讨论架构方法,并以详细的交付成果列表结束。
7.1.1 描述
本项目的目的是检查原始数据,以了解它是否实际上是基数数据。在某些情况下,可能使用了浮点值来表示名义数据;数据看起来像是一种度量,但实际上是一个代码。
电子表格软件倾向于将所有数据转换为浮点数;许多数据项可能看起来像基数数据。
一个例子是美国邮政编码,它们是一串数字,但可以通过电子表格转换为数值。
另一个例子是银行账户号码,虽然非常长,但可以转换为浮点数。浮点值使用 8 个字节的存储空间,但可以舒适地表示大约 15 位十进制数字。虽然这节省了存储空间,但可能会混淆数据类型,并且存在(小的)账户号码被浮点截断规则更改的可能性。
用户体验是一个 Jupyter Lab 笔记本,可以用来检查数据,展示原始数据值的一些基本特征,并确认数据确实看起来是基数数据。
基数数据有几种常见的子类型:
-
计数;由整数值表示。
-
货币和其他与金钱相关的值。这些通常是十进制值,而
float
类型可能不是一个好主意。 -
持续时间值。这些通常以天、小时和分钟来衡量,但代表一个时间间隔或对时间点应用的“增量”。这些可以归一化为秒或天,并以浮点值表示。
-
更通用的度量标准不属于之前的任何类别。这些通常用浮点数表示。
对于这个项目来说,重要的是要有一个数据的概览。后续的项目将查看清理和转换数据以供进一步使用。这个笔记本仅设计用于预览和检查数据。
我们首先查看一般度量,因为原则适用于计数和持续时间。货币以及持续时间值要复杂一些,我们将单独查看它们。日期时间戳将在下一个项目中查看,因为它们通常被认为是序数数据,而不是基数数据。
7.1.2 方法
这个项目基于初始检查笔记本,来自第六章,项目 2.1:数据检查笔记本。笔记本中的一些基本单元格内容将被重用。我们将向早期章节中显示的组件添加组件——特别是,samples_iter()
函数,用于遍历打开文件中的样本。这个特性将是处理原始数据的核心。
在上一章中,我们建议避免使用转换函数。在开始检查数据的过程中,最好是先不预设任何假设,先查看文本值。
源数据值中存在一些常见模式:
-
值看起来都是数值。
int()
或float()
函数适用于所有值。这里有两个子情况:-
所有值似乎都是一些预期的范围内的适当计数或度量。这是理想的。
-
存在一些“异常”值。这些值似乎超出了预期值范围。
-
-
一些值不是有效数字。它们可能是空字符串,或者是一条代码行“NULL”,“None”,或“N/A”。
数值异常值可能是测量错误,也可能是数据中隐藏的有趣现象。异常值也可能是表示样本已知缺失或不可用值的数值代码。在二氧化碳数据的例子中,存在异常值 -99..99 百分之一,这些值编码了特定种类的缺失数据情况。
许多数据集将伴随元数据来解释值域,包括非数值值,以及正在使用的数值代码。一些企业数据源可能没有完整或详细解释的元数据。这意味着分析师需要提问以找到非数值值或特殊代码在基数数据中出现的根本原因。
第一个问题——所有值都是数值吗?——可以用以下代码处理:
from collections import defaultdict
from collections.abc import Iterable, Callable
from typing import TypeAlias
Conversion: TypeAlias = Callable[[str], int | float]
def non_numeric(test: Conversion, samples: Iterable[str]) -> dict[str, int]:
bad_data = defaultdict(int)
for s in samples:
try:
test(s)
except ValueError:
bad_data[s] += 1
return bad_data
策略是应用转换函数,通常是 int()
或 float()
,但对于货币数据或其他具有固定小数位数的其他数据,decimal.Decimal()
可能很有用。如果转换函数失败,异常数据将保存在显示计数的映射中。
鼓励您尝试以下字符串序列:
data = ["2", "3.14", "42", "Nope", None, ""]
non_numeroc(int, data)
这种测试用例将让您看到该函数如何与良好(和不良)的数据一起工作。它可以帮助将测试用例转换为文档字符串,并将其包含在函数定义中。
如果 non_numeric()
函数的结果是一个空字典,那么非数值数据的缺失意味着所有数据都是数值的。
测试函数首先提供,以遵循像 map()
和 filter()
这样的高阶函数的模式。
此函数的变体可以用作数值过滤器,以通过数值值并拒绝非数值值。这看起来如下:
from collections.abc import Iterable, Iterator, Callable
from typing import TypeVar
Num = TypeVar(’Num’)
def numeric_filter(
conversion: Callable[[str], Num],
samples: Iterable[str]
) -> Iterator[Num]:
for s in samples:
try:
yield conversion(s)
except ValueError:
pass
此函数将静默拒绝无法转换的值。省略数据的效果是创建一个不参与进一步计算的 NULL。另一种选择是将无效值替换为默认值。更复杂的选择是使用相邻值插值替换值。省略样本可能对后续处理阶段使用的统计度量有重大影响。此 numeric_filter()
函数允许使用其他统计函数来定位异常值。
对于有良好文档或数据合同的数据,像-99.*99 这样的异常值很容易被发现。对于没有良好文档的数据,可能更适合进行统计测试。有关定位异常值的方法的详细信息,请参阅 www.itl.nist.gov/div898/handbook/eda/section3/eda35h.htm
。
对于小数据集,一种合适的方法是使用基于中值的 Z 分数。我们将深入研究一个基于许多常见统计测量的算法。这将涉及使用内置 statistics
包中的函数计算中位数。
关于数据分析的基本统计信息,请参阅 *Statistics for Data Science。
www.packtpub.com/product/statistics-for-data-science/9781788290678
。
样本的常规 Z 分数,Z[i],基于均值,Ȳ,和标准差,σ[Y]。它被计算为 Z[i] = 。它衡量一个值与均值的距离有多少个标准差。与此并行的是基于中值的 Z 分数的概念,M[i]。基于中值的 Z 分数使用中值,Ỹ,和中值绝对偏差,MAD[Y]。
这被计算为 M[i] = 。这衡量一个值与样本中位数的距离有多少个“MAD”单位。
MAD 是中位数与中位数偏差的绝对值的均值。它需要计算一个整体中位数,Ỹ,然后计算所有与整体中位数,Y [i] −Ỹ的偏差。从这个中位数偏差的序列中,选择中值值以确定所有中值绝对偏差的中心值。这被计算为 MAD[Y] = median(|Y [i] −Ỹ|)。
基于乘数M[i]的过滤器寻找任何从 MAD[Y]的偏差绝对值大于 3.5 的样本,|M[i]| > 3.5. 这些样本可能是异常值,因为它们与中位数之间的绝对偏差异常大。
为了完整,这里有一个读取源数据的单元格:
with series_4_path.open() as source_file:
series_4_data = list(samples_iter(source_file))
这可以接着是一个计算中位数和中位数绝对偏差的单元格。中位数计算可以使用statistics
模块完成。然后可以使用生成器计算偏差,从中计算中位数绝对偏差。单元格看起来如下:
from statistics import median
y_text = (s[’y’] for s in series_4_data)
y = list(numeric_filter(float, y_text))
m_y = median(y)
mad_y = median(abs(y_i - m_y) for y_i in y)
outliers_y = list(
filter(lambda m_i: m_i > 3.5, ((y_i - m_y)/mad_y for y_i in y))
)
y_text
的值是一个生成器,它将从 NDJSON 文件中每个原始数据样本中映射到’y’
键的值中提取值。从这些文本值中,通过应用numeric_filter()
函数计算y
的值。
有时显示len(y)`` ==`` len(y_text)
有助于证明所有值都是数值的。在某些数据集中,非数值数据的存在可能是一个警告,表明存在更深层次的问题。
m_y
的值是y
值的中位数。这用于计算 MAD 值,即从中位数到绝对偏差的中位数。这个中位数绝对偏差提供了一个围绕中位数的预期范围。
outliers_y
的计算使用生成器表达式来计算基于中位数的 Z 分数,然后只保留那些与中位数超过 3.5 个 MAD 的分数。
安斯康姆四重奏的第四系列数据似乎遭受了一个更复杂的异常值问题。虽然“x”属性有一个潜在的异常值,但“y”属性的 MAD 为零。这意味着超过一半的“y”属性值是相同的。这个单一值就是中位数,对于大多数样本,与中位数之间的差异将为零。
这个异常将成为笔记本中一个有趣的部分。
处理货币和相关值
世界上的大多数货币都使用固定的小数位数。例如,美国货币精确到两位小数。这些是十进制值;对于这些值,float
类型几乎总是错误的数据类型。
Python 有一个decimal
模块,其中包含Decimal
类型,必须用于货币。
不要使用float
来处理货币或任何与货币相关的计算。
税率、折扣率、利率和其他与金钱相关的字段也是十进制值。它们通常与货币值一起使用,计算必须使用十进制算术规则进行。
当我们将Decimal
值相乘时,结果可能在小数点右边有额外的数字。这需要应用舍入规则来确定如何舍入或截断额外的数字。这些规则对于获得正确的结果至关重要。float
类型的round()
函数可能无法正确执行此操作。decimal
模块包含各种舍入和截断算法。
考虑一个价格为$12.99 的商品,在一个对每次购买征收 6.25%销售税的地区。这并不是$0.811875 的税款金额。税款金额必须四舍五入;会计师们普遍使用许多四舍五入规则。了解需要哪种规则来计算正确的结果是至关重要的。
由于货币背后的基本假设是十进制计算,因此float
不应用于货币金额。
当涉及电子表格数据时,这可能会成为一个问题。电子表格软件通常使用具有复杂格式规则的float
值来生成看起来正确的答案。这可能导致 CSV 提取中看起来奇怪的值,例如,对于应该有货币值的属性,值为 12.999999997。
此外,货币可能带有货币符号,如$、£或€。根据地区,还可能有分隔符字符。对于美国地区,这可能意味着在大数字中可能存在多余的”,”字符。
货币值可能有的文本装饰表明,non_numeric()
或numeric_filter()
函数使用的转换函数将必须比简单使用Decimal
类更为复杂。
由于这些异常,数据检查是数据获取和分析中的关键步骤。
处理区间或持续时间
一些日期将包括以"12:34"
形式表示的持续时间数据,这意味着 12 小时和 34 分钟。这看起来就像一天中的某个时间点。在某些情况下,它可能具有12h 34m
的形式,这更容易解析。如果没有元数据来解释属性是持续时间还是一天中的某个时间点,这可能是无法理解的。
对于持续时间,将值表示为单个、通用的时间单位很有帮助。秒是一个流行的选择。天也是一个常见的选择。
我们可以创建一个带有给定字符串的单元格,例如:
time_text = "12:34"
给定这个字符串,我们可以创建一个单元格来计算秒数,如下所示:
import re
m = re.match(r"(\d+):(\d+)", time_text)
h, m = map(int, m.groups())
sec = (h*60 + m) * 60
sec
这将从源文本time_text
计算出一个持续时间为 45,240 秒的sec
。Jupyter 笔记本单元格中的最终表达式sec
将显示此变量的值,以确认计算工作正常。这种基数值计算非常优雅。
对于格式化目的,逆计算可能很有帮助。一个浮点值如 45,240 可以转换回一个整数序列,如(12, 34, 0),这可以格式化为”12:34”或”12h 34m 0s”。
它可能看起来像这样:
h_m, s = divmod(sec, 60)
h, m = divmod(h_m, 60)
text = f"{h:02d}:{m:02d}"
text
这将从sec
变量给出的秒数生成字符串12:34
。单元格中的最终表达式text
将显示计算值,以帮助确认单元格工作正常。
将持续时间字符串和看起来复杂的时态归一化到单个浮点值是很重要的。
现在我们已经查看了一些棘手的基数数据字段,我们可以从整体上查看笔记本。在下一节中,我们将查看重构笔记本以创建一个有用的模块。
提取笔记本函数
普通 Z 分数和基于中位数的 Z 分数的计算在几个方面是相似的。以下是一些我们可能想要提取的常见特征:
-
提取中心和方差。这可能是指使用
statistics
模块的均值和标准差,或者可能是中位数和 MAD。 -
创建一个函数,从平均值或中位数计算 Z 分数。
-
应用
filter()
函数定位异常值。
当查看具有大量属性的数据或查看大量相关数据集时,首先在笔记本中编写这些函数是有帮助的。一旦调试完成,它们可以从笔记本中剪切出来并收集到一个单独的模块中。然后可以修改笔记本以导入这些函数,使其更容易重用这些函数。
由于源数据被推入一个具有字符串键的字典中,因此可以考虑在一系列键值上工作的函数。我们可能有如下示例的单元格:
for column in (’x’, ’y’):
values = list(
numeric_filter(float, (s[column] for s in series_4_data))
)
m = median(values)
print(column, len(series_4_data), len(values), m)
这将分析周围for
语句中命名的所有列。在这个例子中,x 和 y 列名被提供为要分析的列的集合。结果是包含列名、原始数据大小、过滤数据大小和过滤数据中位数的值的小表。
描述性统计集合的想法暗示了一个类来保存这些。我们可能添加以下数据类:
from dataclasses import dataclass
@dataclass
class AttrSummary:
name: str
raw_count: int
valid_count: int
median: float
@classmethod
def from_raw(
cls: Type["AttrSummary"],
column: str,
text_values: list[str]
) -> "AttrSummary":
values = list(numeric_filter(float, text_values))
return cls(
name=column,
raw_count=len(text_values),
valid_count=len(values),
median=median(values)
)
类定义包括一个类方法,用于从一组原始值构建该类的实例。将实例构建器放入类定义中使得添加额外的检查属性及其计算这些属性所需的函数稍微容易一些。一个构建AttrSummary
实例的函数可以用来总结数据集的属性。此函数可能看起来如下:
from collections.abc import Iterator
from typing import TypeAlias
Samples: TypeAlias = list[dict[str, str]]
def summary_iter(
samples: Samples,
columns: list[str]
) -> Iterator[AttrSummary]:
for column in columns:
text = [s[column] for s in samples]
yield AttrSummary.from_raw(column, text)
这种类型的函数使得在复杂数据集中重用多个属性的检查代码成为可能。在查看建议的技术方法后,我们将转向本项目的交付成果。
7.1.3 交付成果
本项目有以下交付成果:
-
一个
requirements-dev.txt
文件,用于标识使用的工具,通常是jupyterlab==3.5.3
。 -
docs
文件夹中的文档。 -
对使用中的模块的任何新更改进行单元测试。
-
任何新的应用模块,其中包含用于检查笔记本的代码。
-
一个用于检查似乎具有基数数据的属性的笔记本。
本项目需要一个notebooks
目录。参见交付成果列表以获取有关此结构的更多信息。
我们将更详细地查看其中的一些交付成果。
检查模块
鼓励将 samples_iter()
、non_numeric()
和 numeric_filter()
等函数重构为单独的模块。此外,AttrSummary
类和与之密切相关的 summary_iter()
函数也是移动到包含有用的检查类和函数的单独模块的好候选。
笔记本可以被重构以从单独的模块导入这些类和函数。
将此模块放入 notebooks
文件夹中以便更容易访问是最简单的方法。另一种选择是将 src
目录包含在 PYTHONPATH
环境变量中,使其在 Jupyter Lab 会话中可用。
另一个选择是在终端提示符下使用 ipython profile create
命令创建一个 IPython 配置文件。这将创建一个包含默认配置文件的 ~/.ipython/profile_default
目录。添加一个 startup
文件夹允许包括将 src
目录添加到 sys.path
列表中的脚本,以便查找模块。
请参阅 ipython.readthedocs.io/en/stable/interactive/tutorial.html#startup-files
。
模块的单元测试用例
将各种函数从笔记本重构为单独的模块需要单元测试。在许多情况下,函数将包含 doctest 示例;整个笔记本将有一个 doctest 单元。
在这种情况下,pytest 命令的额外选项将执行这些测试。
% pytest --doctest-modules notebooks/*.py
--doctest-modules
选项将查找 doctest 示例并执行它们。
另一个选择是直接使用 Python 的 doctest
命令。
% python -m doctest notebooks/*.py
当然,测试从笔记本提取的代码以确保其正常工作并且可以信赖是至关重要的。
这个修订和扩展的检查笔记本允许分析师检查未知的数据源,以确认值可能是基数数,例如,度量或计数。使用过滤器函数可以帮助定位无效或其他异常文本。一些统计技术可以帮助定位异常值。
在下一个项目中,我们将探讨非基数数据。这包括名义数据(即,不是数字的数字字符串),以及表示排名或排序位置的序数值。
7.2 项目 2.3:验证文本和代码 - 名义数据和序数
7.2.1 描述
在上一个项目(项目 2.2:验证基数域 - 度量、计数和持续时间)中,我们研究了包含基数数据的属性 - 度量和计数。我们还需要查看序数和名义数据。序数数据通常用于提供排名和排序。名义数据最好理解为由数字字符串组成的代码。如美国邮政编码和银行账户号码这样的值属于名义数据。
当我们查看datahub.io/core/co2-ppm
提供的CO****2 PPM——大气二氧化碳趋势数据集时,它提供了两种形式的日期:作为year-month-day
字符串和作为十进制数字。这个十进制数字将月份的第一天定位在整年中的位置。
使用序数日期来计算每个日期的唯一值并与提供的“十进制日期”值进行比较是有教育意义的。整数日期可能比十进制日期值更有用,因为它避免了截断到三位小数。
类似地,从berkeleyearth.org/data/
提供的许多数据集中,包含复杂的日期和时间值。查看源数据,berkeleyearth.org/archive/source-files/
中的数据集包含用于编码降水类型或其他历史天气细节的名义值。更多数据,请参阅www.ncdc.noaa.gov/cdo-web/
。所有这些数据集的日期格式各不相同。
对于这个项目来说,重要的是要了解涉及日期和名义代码值的数据概览。未来的项目将查看清理和转换数据以供进一步使用。这个笔记本仅用于预览和检查数据。它用于证明数据是完整和一致的,并且可以用于进一步处理。
日期和时间
日期、时间和组合的日期时间值代表一个特定的时间点,有时称为时间戳。通常,这些是通过 Python 的datetime
对象来建模的。
单独的日期通常可以被视为午夜时间的datetime
对象。单独的时间通常是在数据中其他地方声明的日期的一部分,或者从上下文中推断出来的。理想情况下,日期时间值被拆分为单独的数据列,没有很好的理由,并且可以组合。在其他情况下,数据可能更难追踪。例如,整个日志文件可能隐含一个日期——因为每个日志文件都是从 UTC 午夜开始的——并且时间值必须与(隐含的)日志的日期结合。
日期时间值非常复杂且充满奇怪的特性。为了使格里高利历与星星和月亮的位置保持一致,会定期添加闰日。Python 中的datetime
库是处理日历的最佳方式。
通常,在datetime
包之外进行任何日期时间计算都不是一个好主意。
自行编写的日期计算很难正确实现。
datetime.datetime
对象的toordinal()
函数提供了日期和序数之间的明确关系,可以使用它来对日期进行排序。
由于月份不规则,存在几种常见的日期计算方法:
-
日期加上或减去以月为单位给出的持续时间。月份的天数通常会被保留,除非在 2 月 29 日、30 日或 31 日这种不寻常的情况下,将适用临时规则。
-
日期加上或减去以天或周为单位给出的持续时间。
这类计算可能会导致出现在不同年份的日期。对于基于月的计算,需要从日期计算序数月份值。给定一个日期d,其年份为d.y,月份为d.m,序数月份m[o]是d.y × 12 + d.m − 1。计算后,divmod()
函数将恢复结果的年份和月份。请注意,月份通常从 1 开始编号,但序数月份计算从 0 开始编号。这导致从日期创建序数月份时为-1,从序数月份创建日期时为+1。如上所述,当结果月份是二月时,需要做一些处理来处理试图构建一个可能无效的日期的异常情况,该日期的日期数在给定年份的二月中是无效的。
对于基于天或周的运算,toordinal()
函数和fromordinal()
函数将正确地排序和计算日期之间的差异。
所有日历计算都必须使用序数值。
这里有三步:
-
可以使用
datetime
对象的内置toordinal()
方法,或者计算序数月份。 -
将持续时间偏移应用于序数值。
-
可以使用
datetime
类的内置fromordinal()
类方法,或者使用divmod()
函数来计算序数月份的年份和月份。
对于一些开发者来说,使用序数词表示日期可能会感觉复杂。使用if
语句来判断日期的偏移量是否属于不同的年份不太可靠,并且需要更广泛的边缘情况测试。使用类似year, month = divmod(date, 12)
的表达式要容易测试得多。
在下一节中,我们将探讨时间和本地时间的问题。
时间值、本地时间和 UTC 时间
本地时间受到许多看似复杂的规则的影响,尤其是在美国。一些国家只有一个时区,简化了本地时间的构成。然而,在美国,每个县都决定它属于哪个时区,导致出现非常复杂的情况,这些情况不一定遵循美国州界。
一些国家(包括美国和欧洲,以及一些其他地方)在一年中的部分时间会调整时间(通常但并非普遍为一个小时)。这些规则并不一定是全国性的;加拿大、墨西哥、澳大利亚和智利的部分地区没有夏令时调整。纳瓦霍部落——位于美国亚利桑那州境内——不调整时钟。
规则在这里:data.iana.org/time-zones/tz-link.html
。这是 Python datetime
库的一部分,并且已经在 Python 中可用。
这种复杂性使得使用通用协调时间(UTC)变得至关重要。
为了分析目的,应将本地时间转换为协调世界时(UTC)。
请参阅www.rfc-editor.org/rfc/rfc3339
,了解可以包含本地时间偏移的时间格式。
UTC 可以转换回本地时间以供用户显示。
7.2.2 方法
日期和时间通常有令人困惑的格式。这在美国尤其如此,那里的日期通常以月/日/年的数字格式书写。使用年/月/日将值按重要性顺序排列。使用日/月/年是重要性顺序的反向。美国的顺序只是奇怪。
这使得在没有元数据解释序列化格式的情况下对完全未知的数据进行检查变得困难。像 01/02/03 这样的日期可能意味着几乎任何事情。
在某些情况下,对许多类似日期的值的调查将揭示一个范围在 1-12 之间的字段,另一个范围在 1-31 之间的字段,允许分析师区分月份和日期。剩余的字段可以被视为截断的年份。
在没有足够的数据来确认月份或日期的情况下,需要其他线索。理想情况下,有元数据来定义日期格式。
datetime.strptime()
函数可以在格式已知时用于解析日期。直到日期格式已知,数据必须谨慎使用。
这里有两个可以帮助解析日期的 Python 模块:
仔细检查日期解析的结果,以确保结果是合理的非常重要。有一些混淆因素。
例如,年份可以提供两位或四位数字。例如,处理旧数据时,注意使用两位数编码方案非常重要。在 2000 年之前几年,日期的年份可能被给出为一个复杂的两位数转换。在一个方案中,从 0 到 29 的值代表 2000 年到 2029 年的年份。从 30 到 99 的值代表 1930 年到 1999 年的年份。这些规则通常是临时的,不同的企业可能使用了不同的年份编码。
此外,为了保持时钟与行星运动保持一致,日历上已经几次添加了跳秒。与闰年不同,这些是天文学家持续研究的结果,并不是按照闰年的定义来确定的。
请参阅www.timeanddate.com/time/leapseconds.html
获取更多信息。
跳秒的存在意味着像1972-06-30T23:59:60
这样的时间戳是有效的。秒的 60 值代表额外的跳秒。截至本书首次出版时,共有 26 个跳秒,所有这些都在给定年份的 6 月 30 日或 12 月 31 日添加。这些值很少见但有效。
名义数据
名义数据不是数值,可能由数字字符串组成,这可能导致混淆的来源,在某些情况下 — 无用的数据转换。虽然名义数据应被视为文本,但电子表格可能将美国邮政 ZIP 码视为数字并截断前导零。例如,North Adams,MA 的 ZIP 码为 01247。电子表格可能会丢失前导零,使代码变为 1247。
虽然通常最好将名义数据视为文本,但在某些情况下可能需要重新格式化 ZIP 码、账户号码或零件号码以恢复前导零。这可以通过多种方式完成;也许最好的方法是使用 f-string 在左侧填充前导 "0" 字符。像 f"{zip:0>5s}"
这样的表达式使用 zip
值创建一个字符串,格式为 0>5s
。此格式具有填充字符 0
,填充规则 >
,以及目标大小 5
。最后的字符 s
是预期的数据类型;在这种情况下,是一个字符串。
另一个选择是将给定的 zip
值填充到 5 个位置,例如 (5*"0" + zip)[-5:]
。它会先添加零,然后取最右边的五个字符。这看起来不如 f-string 那么优雅,但可能更加灵活。
扩展数据检查模块
在上一个项目 项目 2.2:验证基数域 — 度量、计数和持续时间 中,我们考虑添加一个包含一些有用函数的模块来检查基数数据。我们也可以添加用于序数和名义数据的函数。
对于给定的问题域,日期解析可以定义为单独的、小型的函数。这有助于避免看起来复杂的 strptime()
函数。在许多情况下,只有少数日期格式,解析函数可以尝试不同的选项。它可能看起来像这样:
import datetime
def parse_date(source: str) -> datetime.datetime:
formats = "%Y-%m-%d", "%y-%m-%d", "%Y-%b-%d"
for fmt in formats:
try:
return datetime.datetime.strptime(source, fmt)
except ValueError:
pass
raise ValueError(f"datetime data {source!r} not in any of {formats}
format")
此函数尝试使用三种日期格式来转换数据。如果没有任何格式与数据匹配,将引发 ValueError
异常。
对于排名顺序数据和代码,笔记本中的一个单元格可以依赖于一个 collections.Counter
实例来获取值的域。对于简单的数字和名义代码,不需要更复杂的处理。
7.2.3 可交付成果
此项目有以下可交付成果:
-
一个
requirements-dev.txt
文件,用于标识使用的工具,通常是jupyterlab==3.5.3
。 -
docs
文件夹中的文档。 -
对使用中的模块任何新更改的单元测试。
-
任何新的应用程序模块,其中包含用于检查笔记本的代码。
-
一个用于检查似乎具有序数或名义数据的属性的笔记本。
在 第一章,项目零:其他项目的模板 中建议的项目目录结构中提到了一个 notebooks
目录。参见 可交付成果列表 获取更多信息。对于这个项目,需要笔记本目录。
我们将更详细地查看这些可交付成果中的几个。
修订后的检查模块
日期转换和清理名义数据的函数可以编写在一个单独的模块中。或者它们可以在笔记本中开发,然后移动到检查模块。正如我们在描述部分所指出的,这个项目的目标是支持数据的检查和特殊案例、数据异常和异常值的识别。
之后,我们可以将这些函数重构为一个更正式和完整的数据清洗模块。这个项目的目标是检查数据并编写一些用于检查过程的实用函数。这将创建一个更完整解决方案的种子。
单元测试用例
日期解析可能是——也许——更复杂的问题之一。虽然我们常常认为我们已经看到了所有的源数据格式,但上游应用程序的一些微小变化可能导致数据分析目的上的意外变化。
每当出现新的日期格式时,就有必要通过不良数据扩展单元测试,然后调整解析器以处理不良数据。这可能导致大量日期时间示例的意外增加。
当面对许多非常相似的案例时,pytest
参数化固定装置非常方便。这些固定装置提供了一系列测试用例的示例。
固定装置可能看起来像以下这样:
import pytest
EXAMPLES = [
(’2021-01-18’, datetime.datetime(2021, 1, 18, 0, 0)),
(’21-01-18’, datetime.datetime(2021, 1, 18, 0, 0)),
(’2021-jan-18’, datetime.datetime(2021, 1, 18, 0, 0)),
]
@pytest.fixture(params=EXAMPLES)
def date_example(request):
return request.param
每个示例值都是一个包含输入文本和预期datetime
对象的二元组。这对值可以通过测试用例进行分解。
使用这个充满示例的固定装置进行的测试可能看起来像以下这样:
def test_date(date_example):
text, expected = date_example
assert parse_date(text) == expected
这种测试结构允许我们随着新格式的发现而添加新的格式。EXAMPLES
变量中的测试用例很容易通过额外的格式和特殊情况进行扩展。
现在我们已经检查了检查基数、序数和名义数据,我们可以转向一种更专门的名义数据形式:用于在单独的数据集之间跟踪引用的关键值。
7.3 项目 2.4:寻找参考域
在许多情况下,数据被分解以避免重复。在第五章,数据获取特性:SQL 数据库中,我们提到了将数据分解为规范化的想法。
例如,考虑这个目录中的数据集:www.ncei.noaa.gov/pub/data/paleo/historical/northamerica/usa/new-england/
有三个单独的文件。当我们访问网页时,我们看到的是以下内容。
这里是/pub/data/paleo/historical/northamerica/usa/new-england
文件的索引:
|
|
|
|
|
名称 | 最后修改 | 大小 | 描述 |
---|
|
|
|
|
|
父目录 | - | ||
---|---|---|---|
new-england-oldweather-data.txt | 2014-01-30 13:02 | 21M | |
readme-new-england-oldweather.txt | 2014-01-29 19:22 | 9.6K | |
town-summary.txt | 2014-01-29 18:51 | 34K |
|
|
|
|
|
readme-new-england-oldweather.txt
文件描述了主数据集中使用的许多代码及其含义。该“readme”文件提供了一系列从键到值的映射。键用于庞大的“oldweather-data”文件中,以减少数据的重复。
这些映射包括以下内容:
-
温度代码键
-
降水量类型键
-
降水量关键值
-
雪量关键值
-
相似值代码键
-
压力代码键
-
天空覆盖键
-
天空分类键
-
位置代码键
这是对主数据到编码值的一种相当复杂的分解。
7.3.1 描述
在数据分解或规范化的情况下,我们需要确认项目之间的引用是否有效。关系通常是单向的——一个样本将引用另一个数据集中的一项。例如,气候记录可能有一个引用“Town Id”(TWID)的值,如NY26
。第二个包含“位置代码键”的数据集提供了关于NY26
城镇 ID 定义的详细信息。没有从位置代码数据集到该位置所有气候记录的反向引用。
我们通常将这种关系表示为 ERD。例如,图 7.1**.。
图 7.1:一个规范化的关系
许多天气数据记录引用单个位置定义。
数据库设计者会将位置的“TWID”属性称为主键。WeatherData 的 ID 属性被称为外键;它是不同类别的实体的主键。这些通常缩写为 PK 和 FK。
关于实体之间关系的问题有两个密切相关:
-
关系的基数是多少?这必须从两个方向来考虑。有多少主键实体与外键实体有关联?有多少外键实体与主键实体有关联?
-
关系的可选性是什么?同样,我们必须从两个方向来询问这个问题。主实体是否必须有任何外键引用?外键项是否必须有主键引用?
虽然可能的组合很多,但有一些常见的模式。
-
强制多对一关系。这以历史天气数据为例。许多天气数据记录必须引用单个位置定义。有两种常见的变体。在一种情况下,一个位置必须有一个或多个天气记录。另一种常见变体可能没有与位置相关的任何天气数据。
-
可选的一对一关系。这在天气数据示例中不存在,但我们可能有带有付款的发票和没有付款的发票。这种关系是一对一的,但付款可能尚未存在。
-
多对多关系。一个多对多关系的例子是一个产品实体具有多个特性。特性在产品之间被重复使用。这需要一个单独的多对多关联表来跟踪关系链接。
这导致以下两个详细的检查:
-
主键值的域。例如,每个位置上的“TWID”属性。
-
外键值的域。例如,每个天气数据记录的 ID 属性。
如果这两个集合相同,我们可以确信外键都有匹配的主键。我们可以计算共享外键的行数,以确定关系的基数(和可选性)。
如果这两个集合不相同,我们必须确定哪个集合有额外的行。让我们称这两个集合为P和F。进一步,我们知道P≠F。存在多种情况:
-
P ⊃ F:这意味着有一些主键没有任何外键。如果关系是可选的,那么就没有问题。P∖F是未使用实体的集合。
-
F ⊂ P:这意味着有一些外键没有关联的主键。这种情况可能是对键属性的理解错误,或者这可能意味着数据缺失。
对于这个项目来说,重要的是要有一个键值及其关系的概述。这个笔记本仅设计用于预览和检查数据。它用于证明数据是完整和一致的,并且可用于进一步处理。
在下一节中,我们将探讨如何在笔记本中构建单元格来比较键并确定关系的基数。
7.3.2 方法
要处理像www.ncei.noaa.gov/pub/data/paleo/historical/northamerica/usa/new-england/
这样的数据集,我们需要比较键。
这将在检查笔记本中导致两种类型的数据摘要单元格:
-
在
Counter
对象中总结主键。 -
总结外键对这些主键的引用,也使用
Counter
。
一旦有了Counter
摘要,那么.keys()
方法将具有不同的主键或外键值。这可以转换成 Python set
对象,允许优雅的比较、子集检查和集合减法操作。
我们首先将查看收集键值和键引用的编程。然后,我们将查看有用的摘要。
收集和比较键
核心检查工具是collections.Counter
类。让我们假设我们已经完成了两个独立的数据获取步骤。第一个从readme-new-england-oldweather.txt
文件中提取了位置定义。第二个将所有new-england-oldweather-data.txt
天气数据记录转换成了单独的文件。
检查笔记本可以加载位置定义并收集TWID
属性值。
用于加载键定义的一个单元格可能如下所示:
from pathlib import Path
from inspection import samples_iter
location_path = Path("/path/to/location.ndjson")
with location_path.open() as data_file:
locations = list(samples_iter(data_file))
用于检查城镇键定义的单元格可能如下所示:
import collections
town_id_count = collections.Counter(
row[’TWID’] for row in locations
)
town_id_set = set(town_id_count.keys())
这创建了包含使用的 ID 集合的 town_id_set
变量。town_id_counts
变量的值是每个 ID 的位置定义数量。由于这是一个主键,它应该只有每个值的单个实例。
引用城镇键的数据可能比键的定义大得多。在某些情况下,将所有数据加载到内存中并不实际,因此检查需要与所选列的摘要一起工作。
对于这个例子,这意味着不会创建一个包含天气数据的 list
对象。相反,使用生成器表达式提取相关列,然后使用这个生成器构建最终的 Counter
对象摘要。
引用外键的数据行可能看起来像这样:
weather_data_path = Path("/path/to/weather-data.ndjson")
with weather_data_path.open() as data_file:
weather_reports = samples_iter(data_file)
weather_id_count = collections.Counter(
row[’ID’] for row in weather_reports
)
一旦创建了 weather_id_count
摘要,以下单元格可以计算键引用的域,如下所示:
weather_id_set = set(weather_id_count.keys())
需要注意的是,这个例子明确不创建单个天气报告样本的列表。那样会将大量数据一次性加载到内存中。相反,这个例子使用生成器表达式从每一行中提取 ‘ID’
属性。这些值用于填充 weather_id_count
变量。这用于提取天气报告中使用的 ID 集合。
由于我们有两个集合,我们可以使用 Python 的集合操作来比较这两个集合。理想情况下,一个单元格可以断言 weather_id_set
==
town_id_set
。如果这两个集合不相等,则可以使用集合减法操作来定位异常数据。
汇总键的数量
第一个摘要是比较主键和外键。如果这两个集合不匹配,缺失的外键列表可能有助于定位问题的根本原因。
此外,外键计数的范围提供了一些关于其基数和可选性的线索。当一个主键没有外键值引用它时,这种关系看起来是可选的。这应该通过阅读元数据描述来确认。外键计数的上下限提供了基数范围。这个范围有意义吗?元数据中是否有关于基数的提示?
这个项目的数据源示例包括一个包含摘要计数的文件。town-summary.txt
文件有四列:“STID”,“TWID”,“YEAR”和“Records”。“STID”来自位置定义;它是美国州。 “TWID”是城镇 ID。“YEAR”来自天气数据;它是报告的年份。最后,“Records”属性是给定位置和年份的天气报告数量。
城市 ID 和年份形成一组逻辑值,可以用来构建一个collections.Counter
对象。然而,要完全复制此表,需要位置定义来将城市 ID,“TWID”,映射到相关的州,“STID”。
虽然也可以将“TWID”键分解以从前两个字符中提取州信息,但这不是一个好的设计选择。这种复合键是一种不常见的键设计。主键通常是原子性的,没有内部信息可用。良好的设计将键视为不透明的标识符,并从readme
文件中的相关位置定义表中查找状态信息。
7.3.3 交付物
本项目有以下交付物:
-
一个
requirements-dev.txt
文件,用于标识使用的工具,通常是jupyterlab==3.5.3
。 -
docs
文件夹中的文档。 -
对任何新更改的模块进行的单元测试。
-
任何新的应用程序模块,其中包含用于检查笔记本的代码。
-
一个用于检查似乎具有外键或主键的属性的笔记本。
在第一章,项目零:其他项目的模板中建议的项目目录结构中提到一个notebooks
目录。参见交付物清单以获取更多信息。对于这个项目,需要笔记本目录。
我们将更详细地查看一些这些交付物。
修订后的检查模块
检查主键和外键的函数可以编写在一个单独的模块中。通常,首先在笔记本中开发这些函数是最容易的。由于误解可能会出现一些奇怪的不一致。一旦键检查工作正常,就可以将其移动到检查模块。正如我们在描述中提到的,本项目的目标是支持数据检查和特殊案例、数据异常和异常值的识别。
单元测试用例
创建针对最常见的键问题(没有外键的主键和没有主键的外键)的测试用例通常很有帮助。这些复杂情况在现成的、精心整理的数据集中不常出现;它们通常出现在文档不完整的企业数据中。
这可能导致相当长的固定装置,其中包含两个源对象集合。不需要很多行数据就能揭示缺失的键;两行数据就足以显示一个存在的键和一个缺失的键。
同样,将这些测试用例与基数数据处理和序数数据转换的测试用例分开也是至关重要的。由于键是一种名义数据,键基数检查可能依赖于一个单独的函数来清理损坏的键值。
例如,真实数据可能需要在检查交易列表以找到账户交易之前,先给账户号码添加前导零。这两个操作需要独立构建和测试账户号码键。数据清理应用程序可以结合这两个函数。目前,它们是两个独立的问题,有独立的测试用例。
修订后的笔记本,使用重构的检查模型
在数据获取应用中,未能解决外键是一个长期存在的问题。这通常是由于各种情况,并且没有单一的数据检查过程。这意味着笔记本中可能包含一系列信息。我们可能会看到以下任何一种类型的单元格:
-
一个单元格解释了键集匹配,数据可能可用。
-
一个单元格解释了一些没有外键数据的主键。这可能包括对这部分样本的总结,与有外键引用的样本分开。
-
一个单元格解释了一些没有主键的外键。这些可能反映了数据中的错误。它可能反映了键之间更复杂的关系。它可能反映了更复杂的数据模型。它可能反映了数据缺失。
在所有情况下,都需要额外的一个单元格,用一些 Markdown 解释结果。将来你会感到庆幸,因为在过去,你已经在你的笔记本中留下了对异常的解释。
7.4 摘要
本章扩展了检查笔记本的核心功能。我们探讨了处理基数数据(度量值和计数)、序数数据(日期和排名)以及名义数据(如账户号码之类的代码)。
我们的主要目标是获得数据的完整视图,在我们正式化分析流程之前。次要目标是为我们自己留下关于异常值、异常、数据格式问题和其他复杂情况的笔记。这项工作的一个愉快的结果是能够编写一些函数,这些函数可以用于下游的清洁和标准化我们找到的数据。
从第九章,项目 3.1:数据清理基础应用开始,我们将探讨重构这些检查函数,以创建一个完整且自动化的数据清理和标准化应用。该应用将基于创建检查笔记本时学到的经验教训。
在下一章中,我们将探讨从初步检查中经常学到的一个额外课程。我们经常发现多个、多样化的数据源背后的潜在模式。我们将探讨通过 JSONSchema 形式化模式定义,并使用模式来验证数据。
7.5 额外内容
这里有一些想法,你可以添加到本章的项目中。
7.5.1 带有日期和数据源信息的 Markdown 单元格
检查笔记本的一个小特点是识别数据的日期、时间和来源。有时从上下文中可以清楚地知道数据源;例如,可能有一个明显的数据路径。
然而,在许多情况下,并不完全清楚正在检查哪个文件或它是如何获得的。作为一个一般性的解决方案,任何处理应用程序都应该生成一个日志。在某些情况下,元数据文件可以包括处理步骤的详细信息。
在审查数据检查笔记本或与他人分享数据初步检查时,关于源数据和处理步骤的附加元数据可能很有帮助。在许多情况下,这些额外数据被粘贴到普通的 Markdown 单元格中。在其他情况下,这些数据可能是扫描日志文件以查找总结处理的键INFO
行的结果。
7.5.2 演示材料
一个常见的请求是为用户或同行定制演示文稿,以解释新的数据源或解释现有数据源中发现的异常。这些演示文稿通常涉及在线会议或面对面会议,并有一些强调演讲者观点的“幻灯片”。
Keynote 或 PowerPoint 等专有工具常用于这些幻灯片。
更好的选择是仔细组织笔记本,并将其导出为reveal.js
幻灯片。
Jupyter 的 RISE 扩展在此方面很受欢迎。见rise.readthedocs.io/en/stable/
。
如果笔记本也是业务所有者和用户的幻灯片演示,这将提供很大的灵活性。我们不需要复制粘贴来将数据从检查笔记本移动到 PowerPoint(或 Keynote),我们只需要确保每个幻灯片有关于数据的几个关键点。如果幻灯片有数据样本,它只有几行,这为演讲者的评论提供了支持证据。
在许多企业中,这些演示文稿被广泛共享。确保演示文稿中的数据直接来自源数据,并且不受复制粘贴错误和遗漏的影响是有益的。
7.5.3 使用 JupyterBook 或 Quarto 进行更复杂的输出
在某些情况下,数据初步检查可能需要学习大量关于数据源、编码方案、缺失数据和数据集之间关系的内容。这些信息通常需要组织并发布。
有许多方式可以传播关于数据学到的经验教训:
-
分享笔记本。对于某些用户社区,笔记本的交互性邀请进一步探索。
-
导出笔记本以供发布。一个选择是创建一个可以共享的 PDF。另一个选择是创建 RST、Markdown 或 LaTeX,并使用发布管道构建一个最终可共享的文档。
-
使用像 Jupyter{Book}这样的工具来正式化可共享文档的发布。
-
使用 Quarto 发布一个最终可共享的文档。
关于 Jupyter{Book},请参阅jupyterbook.org/en/stable/intro.html
。更大的“Executable{Books}”项目(executablebooks.org/en/latest/tools.html
)描述了包括 Myst-NB、Sphinx 以及一些相关 Sphinx 主题在内的 Python 相关工具集合。关键成分是使用 Sphinx 来控制最终的发布。
关于 Quarto,请参阅quarto.org
。它与 Quarto CLI 的集成更为紧密:只需下载一次 Quarto CLI。Quarto 工具利用 Pandoc 生成最终的、优雅的、可直接发布的文件。
我们鼓励您探索将共享笔记本提升为优雅报告的方法,以便广泛分享。
第八章
项目 2.5:模式和元数据
将数据模式与共享该模式的各个应用程序保持分离是有帮助的。实现这一目标的一种方法是为套件中的所有应用程序创建一个具有类定义的单独模块。虽然这对简单项目有帮助,但在更广泛地共享数据模式时可能会有些尴尬。Python 语言模块在共享 Python 环境之外的数据时尤其困难。
本项目将使用 JSON Schema 语法定义一个模式,首先通过构建 pydantic
类定义,然后从类定义中提取 JSON。这将允许您发布正在创建的数据的正式定义。该模式可以被各种工具用于验证数据文件,并确保数据适合进一步的分析使用。
模式对于诊断数据源的问题也很有用。像 jsonschema
这样的验证工具可以提供详细的错误报告,有助于识别源数据中的更改,这些更改可能是由于错误修复或软件更新。
本章将涵盖与数据检查技术相关的多个技能:
-
使用 Pydantic 模块进行清晰、完整的定义
-
使用 JSON Schema 创建一个可导出且语言无关的定义,任何人都可以使用
-
创建用于使用正式模式定义的测试场景
我们将首先探讨正式模式为什么有帮助的原因。
8.1 描述
在应用程序之间移动数据时,数据验证是一个常见的要求。拥有一个明确的数据有效性的定义非常有帮助。当这个定义存在于特定的编程语言或平台之外时,帮助就更大了。
我们可以使用 JSON Schema (json-schema.org
) 来定义一个适用于由获取过程创建的中间文档的模式。使用 JSON Schema 可以使 JSON 数据格式的使用更加自信和可靠。
JSON Schema 定义可以在不同的 Python 项目和非 Python 环境中共享和重用。它允许我们将数据质量检查集成到获取管道中,以积极确认数据确实符合分析和处理的要求。
使用模式提供的附加元数据通常包括数据的来源和属性值是如何导出的详细信息。这不是 JSON 模式的一部分,但我们可以向 JSON 模式文档中添加一些包含来源和处理描述的详细信息。
随后的数据清洗项目应使用源架构验证输入文档。从第九章项目 3.1:数据清洗基础应用开始,应用应使用目标分析架构验证其输出。一个应用既创建样本记录又验证这些记录是否符合架构可能看起来有些荒谬。重要的是架构将是共享的,并随着数据消费者的需求而发展。另一方面,数据采集和清洗操作随着数据源的发展而发展。一个临时的数据问题解决方案看起来很好,但可能创建无效数据的情况非常普遍。
验证输入和输出是否符合可见的、达成一致的架构很少会创建新的问题。验证操作会有一些开销,但大部分处理成本是由输入和输出的时间决定的,而不是数据验证。
展望第十二章项目 3.8:集成数据采集 Web 服务,我们将看到正式定义的架构的更多用途。我们还将揭示使用 JSON Schema 描述 ND JSON 文档时存在的一个小问题。目前,我们将专注于使用 JSON Schema 描述数据的需求。
我们将首先添加一些模块,以便更容易创建 JSON Schema 文档。
8.2 方法
首先,我们需要一些额外的模块。jsonschema
模块定义了一个验证器,可以用来确认文档是否符合定义的架构。
此外,Pydantic模块提供了一种创建可以发出 JSON Schema 定义的类定义的方法,这样我们就不必手动创建架构。在大多数情况下,手动创建架构并不特别困难。然而,在某些情况下,架构和验证规则可能难以直接编写,并且有 Python 类定义可用可以简化这个过程。
这需要添加到requirements-dev.txt
文件中,以便其他开发者知道安装它。
当使用conda管理虚拟环境时,命令可能如下所示:
% conda install jsonschema pydantic
当使用其他工具管理虚拟环境时,命令可能如下所示:
% python -m pip install jupyterlab
JSON Schema 包需要一些补充的类型存根。这些由mypy工具使用,以确认应用程序正在一致地使用类型。使用以下命令添加存根:
% mypy --install-types
此外,pydantic
包包括一个mypy插件,它将扩展mypy的类型检查功能。这将发现使用pydantic
定义的类中更多细微的潜在问题。
要启用插件,将pydantic.mypy
添加到mypy配置文件mypy.ini
中插件列表。mypy.ini
文件应如下所示:
[mypy]
plugins = pydantic.mypy
(此文件应放在项目目录的根目录下。)
此插件是 pydantic 下载的一部分,并且与从 0.910 版本开始的 mypy 兼容。
使用这两个包,我们可以定义具有详细信息的类,这些信息可用于创建 JSON Schema 文件。一旦我们有了 JSON Schema 文件,我们就可以使用该模式定义来确认样本数据的有效性。
有关 Pydantic 的更多信息,请参阅 docs.pydantic.dev
。
核心概念是使用 Pydantic 来定义具有详细字段定义的数据类。这些定义可以用于 Python 中的数据验证。定义也可以用来生成一个 JSON Schema 文档,以便与其他项目共享。
模式定义对于定义 OpenAPI 规范也很有用。在 第十二章,项目 3.8:集成数据采集网络服务 中,我们将转向创建提供数据的网络服务。此服务的 OpenAPI 规范将包括来自此项目的模式定义。
使用 Pydantic 不是必需的。然而,它对于创建可以通过 JSON Schema 描述的模式来说非常方便。它节省了大量与 JSON 语法细节的纠缠。
我们将开始使用 Pydantic 创建一个有用的数据模型模块。这将扩展早期章节中为项目构建的数据模型。
8.2.1 定义 Pydantic 类并生成 JSON Schema
我们将从对早期章节中使用的数据模型定义进行两个深刻的修改开始。一个变化是将从 dataclasses
模块切换到 pydantic.dataclasses
模块。这样做需要显式使用 dataclasses.field
进行单个字段定义。这通常是对 import
语句的一个小改动,使用 from pydantic.dataclasses import dataclass
。数据类的 field()
函数也需要一些更改,以添加 pydantic 所使用的额外细节。这些更改对现有应用程序应该是完全透明的;所有测试在更改后都将通过。
第二个变化是为类添加一些重要的元数据。在 dataclasses.field(...)
定义中使用的地方,可以添加 metadata={}
属性,以包含一个包含 JSON Schema 属性的字典,如描述、标题、示例、值的有效范围等。对于其他字段,必须使用 pydantic.Field()
函数来提供标题、描述和其他字段约束。这将为我们生成大量的元数据。
有关可用的各种字段定义细节,请参阅 docs.pydantic.dev/usage/schema/#field-customization
。
from pydantic import Field
from pydantic.dataclasses import dataclass
@dataclass
class SeriesSample:
"""
An individual sample value.
"""
x: float = Field(title="The x attribute", ge=0.0)
y: float = Field(title="The y attribute", ge=0.0)
@dataclass
class Series:
"""
A named series with a collection of values.
"""
name: str = Field(title="Series name")
samples: list[SeriesSample] = Field(title="Sequence of samples
in this series")
我们在这个模型定义模块中提供了几个额外的细节。这些细节包括:
-
每个类的文档字符串。这些将成为 JSON Schema 中的描述。
-
每个属性的字段。这些字段也成为了 JSON Schema 中的描述。
-
对于
SeriesSample
类定义的x
和y
属性,我们添加了一个ge
值。这是一个范围规范,要求值大于或等于零。
我们还对模型进行了极其深刻的变化:我们从源数据描述——即多个str
值——转变为目标数据描述,使用float
值。
在这里的核心是,我们对每个模型有两种变体:
-
获取:这是我们在“野外”找到的数据。在本书的例子中,一些源数据变体是纯文本,迫使我们使用
str
作为通用类型。一些数据源将包含更有用的 Python 对象,允许使用除str
之外的其他类型。 -
分析:这是用于进一步分析的数据。这些数据集可以使用原生 Python 对象。大部分时间,我们将关注那些容易序列化为 JSON 的对象。例外的是日期时间值,它们不能直接序列化为 JSON,但需要从标准的 ISO 文本格式进行一些额外的转换。
上面的类示例并不替换我们应用程序中的model
模块。它们形成了一个更有用数据的第二个模型。建议的方法是将初始获取模型的模块名称从model
更改为acquisition_model
(或者可能是更短的source_model
)。这个属性主要用字符串值描述模型。这个第二个模型是analysis_model
。
对数据的初步调查结果可以为分析模型类定义提供更窄和更严格的约束。参见第七章,数据检查功能中的一些检查,这些检查有助于揭示属性值的预期最小值和最大值。
Pydantic库附带了许多自定义数据类型,可以用来描述数据值。请参阅docs.pydantic.dev/usage/types/
以获取文档。使用pydantic
类型可能比将属性定义为字符串并尝试创建有效值的正则表达式要简单。
注意,源值验证不是Pydantic的核心。当提供 Python 对象时,Pydantic模块完全有可能执行成功的数据转换,而我们在希望抛出异常的地方可能没有。一个具体的例子是将 Python float
对象提供给需要int
值的字段。float
对象将被转换;不会抛出异常。如果需要这种非常严格的 Python 对象验证,则需要一些额外的编程。
在下一节中,我们将创建我们模型的 JSON Schema 定义。我们可以从类定义中导出定义,或者我们可以手动构建 JSON。
8.2.2 使用 JSON Schema 表示法定义预期数据域
一旦我们有了类定义,我们就可以导出一个描述类的模式。请注意,Pydantic数据类是一个围绕底层pydantic.BaseModel
子类定义的包装器。
我们可以通过在模块底部添加以下行来创建一个 JSON Schema 文档:
from pydantic import schema_of
import json
if __name__ == "__main__":
schema = schema_of(Series)
print(json.dumps(schema, indent=2))
这些行将数据定义模块转换为一个脚本,该脚本将 JSON Schema 定义写入标准输出文件。
schema_of()
函数将从上一节创建的数据类中提取一个模式。(参见定义 Pydantic 类并生成 JSON Schema。)底层的pydantic.BaseModel
子类还有一个schema()
方法,它将类定义转换为一个详细丰富的 JSON Schema 定义。当与pydantic数据类一起工作时,pydantic.BaseModel
不可直接使用,必须使用schema_of()
函数。
当执行终端命令python src/analysis_model.py
时,将显示模式。
输出开始如下:
{
"title": "Series",
"description": "A named series with a collection of values.",
"type": "object",
"properties": {
"name": {
"title": "Series name",
"type": "string"
},
"samples": {
"title": "Sequence of samples in this series",
"type": "array",
"items": {
"\$ref": "#/definitions/SeriesSample"
}
}
},
"required": [
"name",
"samples"
],
...
}
我们可以看到标题与类名匹配。描述与文档字符串匹配。属性集合与类中的属性名称匹配。每个属性定义都提供了数据类中的类型信息。
$ref
项是对 JSON Schema 中稍后提供的另一个定义的引用。这种引用的使用确保其他类定义是单独可见的,并且可用于支持此模式定义。
一个非常复杂的模型可能有多个定义,这些定义在多个地方共享。这种$ref
技术使结构标准化,因此只提供一个定义。对单个定义的多次引用确保了类定义的正确重用。
JSON 结构乍一看可能看起来不寻常,但它并不令人畏惧地复杂。查看json-schema.org
将提供有关如何在不使用Pydantic模块的情况下最佳创建 JSON Schema 定义的信息。
8.2.3 使用 JSON Schema 验证中间文件
一旦我们有了 JSON Schema 定义,我们可以将其提供给其他利益相关者,以确保他们理解所需或提供的数据。我们还可以使用 JSON Schema 创建一个验证器,该验证器可以检查 JSON 文档,并确定该文档是否真的符合模式。
我们可以使用pydantic
类定义来做这件事。有一个parse_obj()
方法,它将检查字典以创建给定pydantic
类的实例。parse_raw()
方法可以将字符串或字节对象解析为给定类的实例。
我们也可以使用jsonschema
模块来做这件事。我们将将其视为pydantic
的替代方案,以展示共享 JSON Schema 如何允许其他应用程序与分析模型的正式定义一起工作。
首先,我们需要从模式中创建一个验证器。我们可以将 JSON 数据导出到一个文件中,然后从文件中重新加载 JSON 数据。我们还可以通过直接从由 Pydantic 创建的 JSON 模式创建验证器来省略一个步骤。以下是简短版本:
from pydantic import schema_of
from jsonschema.validators import Draft202012Validator
from analysis_model import *
schema = schema_of(SeriesSample)
validator = Draft202012Validator(schema)
这将使用最新的 JSON 模式版本,即 2020 草案。 (该项目正在成为标准,并且随着其成熟已经通过了多个草案。)
这是我们可能编写一个函数来扫描文件以确保 NDJSON 文档都正确符合定义的模式的示例:
def validate_ndjson_file(
validator: Draft202012Validator,
source_file: TextIO
) -> Counter[str]:
counts: Counter[str] = Counter()
for row in source_file:
document = json.loads(row)
if not validator.is_valid(document):
errors = list(validator.iter_errors(document))
print(document, errors)
counts[’faulty’] += 1
else:
counts[’good’] += 1
return counts
此函数将读取给定源文件中的每个 NDJSON 文档。它将使用给定的验证器来检查文档是否存在问题或是否有效。对于有问题的文档,它将打印文档和整个验证错误列表。
这种类型的函数可以嵌入到单独的脚本中以检查文件。
类似地,我们可以为源模型创建模式,并使用 JSON 模式(或 Pydantic)在尝试处理源文件之前对其进行验证。
我们将转向更完整的验证和清理解决方案,见 第九章,项目 3.1:数据清理基础应用。该项目是更完整解决方案的基础组件之一。
我们将在下一节中查看这个项目的交付成果。
8.3 交付成果
本项目有以下交付成果:
-
一个
requirements.txt
文件,用于标识使用的工具,通常是pydantic==1.10.2
和jsonschema==4.16.0
。 -
docs
文件夹中的文档。 -
包含源和分析模式的 JSON 格式文件。建议将这些文件放在单独的
schema
目录中。 -
模式的接受测试。
我们将详细查看模式接受测试。然后我们将查看如何使用模式扩展其他接受测试。
8.3.1 模式接受测试
要知道模式是否有用,必须要有接受测试用例。随着新的数据源集成到应用程序中,以及旧的数据源通过常规的错误修复和升级而发生变化,文件将发生变化。新文件通常会引发问题,问题的根本原因将是意外的文件格式变化。
一旦确定文件格式发生变化,最小的相关示例需要转换为接受测试。当然,测试将失败。现在,数据获取管道可以修复,因为有一个精确的完成定义。
首先,接受测试套件应该有一个有效的示例文件和一个无效的示例文件。
正如我们在 第四章,数据获取功能:Web API 和抓取 中所提到的,我们可以将一大块文本作为 Gherkin 场景的一部分提供。我们可以考虑以下场景:
Scenario: Valid file is recognized.
Given a file "example_1.ndjson" with the following content
"""
{"x": 1.2, "y": 3.4}
{"x": 5.6, "y": 7.8}
"""
When the schema validation tool is run with the analysis schema
Then the output shows 2 good records
And the output shows 0 faulty records
这使我们能够提供 NDJSON 文件的内容。HTML 提取命令相当长。内容作为步骤定义函数的 context.text
参数提供。参见验收测试以获取更多如何编写步骤定义以创建用于此测试用例的临时文件的示例。
当然,故障记录的场景也是必不可少的。确保模式定义能够拒绝无效数据是很重要的。
8.3.2 扩展验收测试
在第三章、第四章和第五章中,我们编写了验收测试,通常查看应用程序活动的日志摘要,以确保它正确获取了源数据。我们没有编写专门查看数据的验收测试。
使用模式定义进行测试允许对文件中的每个字段和记录进行完整分析。这种检查的完整性具有极大的价值。
这意味着我们可以为现有场景添加一些额外的“然后”步骤。它们可能看起来像以下这样:
# Given (shown earlier)...
# When (shown earlier)...
Then the log has an INFO line with "header: [’x’, ’y’]"
And log has INFO line with "Series_1 count: 11"
And log has INFO line with "Series_2 count: 11"
And log has INFO line with "Series_3 count: 11"
And log has INFO line with "Series_4 count: 11"
And the output directory files are valid
using the "schema/Anscombe_Source.json" schema
额外的“然后输出目录文件有效...”行需要一个步骤定义,该定义必须执行以下操作:
-
加载命名的 JSON 模式文件并构建一个
Validator
。 -
使用
Validator
对象检查 ND JSON 文件的每一行,以确保它们是有效的。
将模式作为验收测试套件的一部分使用,将并行于数据供应商和数据消费者如何使用模式来确保数据文件有效的方式。
需要注意的是,本章前面给出的模式定义(在定义 Pydantic 类并生成 JSON 模式中)是从未来项目的数据清理步骤中输出的。该示例中显示的模式不是之前数据获取应用的输出。
要验证数据获取的输出,您需要使用第三章、第四章和第五章中各种数据获取项目的模型。这将与本章前面显示的示例非常相似。虽然相似,但它将在本质上有所不同:它将使用 str
而不是 float
作为序列样本属性值。
8.4 概述
本章的项目展示了数据获取应用以下功能的一些示例:
-
使用 Pydantic 模块进行清晰、完整的定义
-
使用 JSON 模式创建一个可导出且语言无关的定义,任何人都可以使用
-
创建测试场景以使用正式的模式定义
通过正式化模式定义,可以记录有关数据处理应用程序以及应用于数据的转换的更多详细信息。
类定义的文档字符串成为模式中的描述。这允许记录有关数据来源和转换的详细信息,这些信息对所有数据用户都是可见的。
JSON Schema 标准允许记录值示例。Pydantic包有方法在字段定义和类配置对象中包含此元数据,这有助于解释奇怪或不寻常的数据编码。
此外,对于文本字段,JSONSchema 允许包含一个格式属性,该属性可以提供用于验证文本的正则表达式。Pydantic包对文本字段的这种额外验证提供了第一级支持。
我们将在第九章、项目 3.1:数据清洗基础应用和第十章、数据清洗功能的细节中返回数据验证的细节。在这些章节中,我们将更深入地探讨Pydantic的各种验证功能。
8.5 额外内容
这里有一些想法供您添加到这个项目中。
8.5.1 修订所有之前的章节模型以使用 Pydantic
前几章使用了dataclasses
模块中的dataclass
定义。这些可以转换为使用pydantic.dataclasses
模块。这应该对之前的项目影响最小。
我们也可以将所有之前的验收测试套件转换为使用正式的源数据模式定义。
8.5.2 使用 ORM 层
对于 SQL 提取,ORM 非常有用。pydantic
模块允许应用程序从中间 ORM 对象创建 Python 对象。这种双层处理似乎很复杂,但允许在Pydantic对象中进行详细的验证,这些对象不受数据库处理。
例如,一个数据库可能有一个没有提供任何范围的数值列。Pydantic类定义可以提供一个带有ge
和le
属性的字段定义来定义一个范围。此外,Pydantic允许定义具有独特验证规则的唯一数据类型,这些规则可以应用于数据库提取值。
首先,查看docs.sqlalchemy.org/en/20/orm/
以获取关于 SQLAlchemy ORM 层的详细信息。这提供了一个类定义,从中可以派生出 SQL 语句,如CREATE TABLE
、SELECT
和INSERT
。
然后,查看Pydantic文档中的docs.pydantic.dev/usage/models/#orm-mode-aka-arbitrary-class-instances
“ORM 模式(也称为任意类实例)”部分,了解如何将更有用的类映射到中间 ORM 类。
对于一个古怪、设计不佳的数据库中的旧数据,这可能会成为一个问题。另一方面,对于从一开始就设计有 ORM 层的数据库,这可以简化 SQL。
第九章
项目 3.1:数据清理基础应用程序
数据验证、清理、转换和标准化是将从源应用程序获取的原始数据转换为可用于分析目的所需的步骤。由于我们开始使用一个非常干净的小数据集,我们可能需要稍作改进以创建一些“脏”原始数据。一个好的替代方案是寻找更复杂、更原始的数据。
本章将指导您设计一个数据清理应用程序,与原始数据获取分开。许多清理、转换和标准化的细节将留给后续项目。这个初始项目通过添加功能来扩展基础。目标是准备一个完整的数据管道的目标,从获取开始,并通过一个单独的清理阶段传递数据。我们希望利用 Linux 原则,即通过共享缓冲区连接应用程序,通常称为 shell 管道。
本章将涵盖与数据验证和清理应用程序设计相关的多个技能:
-
CLI 架构以及如何设计流程管道
-
验证、清理、转换和标准化原始数据的核心概念
本章不会涉及转换和标准化数据的所有方面。第十章数据清理功能的项目将扩展许多转换主题。第十二章项目 3.8:集成数据 获取 Web 服务的项目将解决集成管道的概念。目前,我们希望构建一个可扩展的基础应用程序,可以添加功能。
我们将从对理想化数据清理应用程序的描述开始。
9.1 描述
我们需要构建一个数据验证、清理和标准化的应用程序。数据检查笔记本是进行此设计工作的便捷起点。目标是创建一个完全自动化的应用程序,以反映从检查数据中学到的经验教训。
数据准备管道具有以下概念任务:
-
验证获取的源文本以确保其可用性,并标记无效数据以进行修复。
-
在必要时清理任何无效的原始数据;这扩大了在可以定义合理清理的情况下可用的数据。
-
将验证和清理后的源数据从文本(或字节)转换为可用的 Python 对象。
-
在必要时,标准化源数据的代码或范围。这里的要求因问题域而异。
目标是创建干净、标准化的数据,以便进行后续分析。意外情况时常发生。有几个来源:
-
上游软件文件格式的问题。获取程序的目标是隔离物理格式问题。
-
源数据的数据表示问题。本项目的目的是隔离值的验证和标准化。
一旦清理完毕,数据本身可能仍然包含令人惊讶的关系、趋势或分布。这是通过后续项目创建分析笔记本和报告来发现的。有时,惊喜来自于发现零假设是正确的,而数据只显示出不显著的随机变化。
在许多实际情况下,前三个步骤——验证、清理和转换——通常被合并成一个单独的函数调用。例如,当处理数值时,int()
或 float()
函数将验证并转换一个值,对于无效的数字将引发异常。
在一些边缘情况下,需要单独考虑这些步骤——通常是因为验证和清理之间存在复杂的交互。例如,某些数据受到美国邮政编码前导零丢失的困扰。这可能是一个复杂的问题,数据表面上无效,但在尝试验证之前可以可靠地清理。在这种情况下,验证邮政编码是否与官方代码列表一致是在清理之后而不是之前。由于数据将保持为文本格式,在清理和验证组合步骤之后没有实际的转换步骤。
9.1.1 用户体验
整体 用户体验 (UX) 将是两个命令行应用程序。第一个应用程序将采集原始数据,第二个将清理数据。每个应用程序都有选项来微调 acquire
和 cleanse
步骤。
在前面的章节中展示了 acquire
命令的几种变体。最值得注意的是,第三章,项目 1.1:数据采集基础应用程序,第四章,数据采集功能:Web API 和抓取,以及 第五章,数据采集功能:SQL 数据库。
对于 clean 应用程序,预期的命令行应该类似于以下内容:
% python src/clean.py -o analysis -i quartet/Series_1.ndjson
-o
analysis
指定了一个目录,结果清理后的数据将被写入该目录。
-i
quartet/Series_1.ndjson
指定了源数据文件的路径。这是一个由采集应用程序编写的文件。
注意,我们不是使用位置参数来命名输入文件。对于文件名使用位置参数是许多——但不是所有——Linux 命令的常见规定。避免使用位置参数的原因是为了使它更容易适应成为处理阶段的一部分的管道。
具体来说,我们希望以下内容也能正常工作:
% python src/acquire.py -s Series_1Pair --csv source.csv | \
python src/clean.py -o analysis/Series_1.ndjson
这条 shell 行包含两个命令,一个用于原始数据获取,另一个用于验证和清理。获取命令使用-s``Series_1Pair
参数来命名一个特定的系列提取类。这个类将被用来创建单个系列作为输出。--csv``source.csv
参数命名了要处理的输入文件。其他选项可以命名 RESTful API 或提供数据库连接字符串。
第二个命令读取第一个命令的输出并将其写入文件。文件名由第二个命令中的-o
参数值命名。
这个管道概念,通过 shell 的|
操作符提供,意味着这两个进程将并发运行。这意味着数据在可用时从一个进程传递到另一个进程。对于非常大的源文件,在获取数据时清理数据可以显著减少处理时间。
在项目 3.6:创建获取管道的集成中,我们将扩展这个设计,包括一些并发处理的想法。
现在我们已经看到了应用程序目的的概述,让我们来看看源数据。
9.1.2 源数据
早期项目以大约一致的形式产生了源数据。这些项目专注于获取文本数据。单个样本被转换成小型的 JSON 友好字典,使用 NDJSON 格式。这可以简化验证和清理操作。
NDJSON 文件格式在ndjson.org
和jsonlines.org
中描述。
acquire应用程序背后有两个设计原则:
-
尽可能保留原始源数据。
-
在获取过程中进行最少的文本转换。
保留源数据在源应用程序出现意外变化时稍微容易一些找到问题。同样,最小化文本转换,使数据更接近源。从多种表示形式到单一表示形式简化了数据清理和转换步骤。
所有数据获取项目都涉及从源表示形式到 ND JSON 的某种文本转换。
|
|
|
|
|
章节 | 节 | 来源 |
---|
|
|
|
|
|
3 | 章节 3,项目 1.1:数据获取基础应用程序 | CSV 解析 | |
---|---|---|---|
4 | 项目 1.2:从网络服务获取数据 | 压缩 CSV 或 JSON | |
4 | 项目 1.3:从网页抓取数据 | HTML | |
5 | 项目 1.5:从 SQL 提取获取数据 | SQL 提取 |
|
|
|
|
|
在某些情况下——例如,提取 HTML——从数据中剥离标记的文本更改是深刻的。SQL 数据库提取涉及撤销数据库对数字或日期的内部表示,并将值作为文本写入。在某些情况下,文本转换是微小的。
9.1.3 结果数据
清洗后的输出文件将是 ND JSON;类似于原始输入文件。我们将在第十一章****项目 3.7:临时数据持久化中详细讨论这种输出文件格式。对于这个项目,最简单的方法是坚持编写 Pydantic 数据类的 JSON 表示。
对于 Python 的本地dataclasses
,dataclasses.asdict()
函数将从数据类实例生成字典。json.dumps()
函数将此转换为 JSON 语法的文本。
然而,对于 Pydantic 数据类,asdict()
函数不能使用。没有内置的方法来生成pydantic
数据类实例的 JSON 表示。
对于Pydantic的版本 1,需要稍作修改来写入 ND JSON。以下表达式将生成pydantic
数据类的 JSON 序列化:
import json
from pydantic.json import pydantic_encoder
from typing import TextIO, Iterable
import analysis_model
def persist_samples(
target_file: TextIO,
analysis_samples: Iterable[analysis_model.SeriesSample | None]
) -> int:
count = 0
for result in filter(None, analysis_samples):
target_file.write(json.dumps(result, default=pydantic_encoder))
target_file.write("\n")
count += 1
return count
这个核心功能是json.dumps()
函数的default=pydantic_encoder
参数值。这将处理将数据类结构正确解码为 JSON 记法。
对于pydantic的版本 2,将采用略有不同的方法。这使用RootModelclassname
结构从对象中提取给定类的根模型。在这种情况下,RootModelSeriesSample.model_dump()
将创建一个可以生成嵌套字典结构的根模型。对于版本 2,不需要特殊的pydantic_encoder
。
现在我们已经了解了输入和输出,我们可以调查处理概念。额外的处理细节将留待后续项目。
9.1.4 转换和处理
对于这个项目,我们正在尝试最小化处理复杂性。在下一章第十章****数据清洗功能中,我们将探讨一些额外的处理需求,这些需求将增加复杂性。作为下一章项目的预告,我们将描述一些可能需要的字段级验证、清理和转换的类型。
我们关注的一个例子,Anscombe 的四重奏数据,需要转换为一系列浮点值。虽然这很明显,但我们推迟了从文本到 Python float
对象的转换,以说明将获取数据的复杂性从分析数据的复杂性中分离出来的更普遍原则。此应用程序的输出将具有每个结果 ND JSON 文档,其中包含float
值而不是string
值。
JSON 文档中的区别将非常小:对于原始数据字符串使用"
。对于float
值,这将被省略。
这个小小的细节很重要,因为每个数据集都会有独特的转换要求。数据检查笔记本将揭示数据域,如文本、整数、日期时间戳、持续时间以及更多专业领域的混合。在信任任何关于数据的模式定义或文档之前,检查数据是至关重要的。
我们将探讨三种常见的复杂情况:
-
必须分解的字段。
-
必须组合的字段。
-
单一集合中样本类型的并集。
-
用于替换特别敏感信息的“不透明”代码。
一种复杂情况是当多个源值合并到一个单一字段时。这个单一源值需要分解成多个值以进行分析。在 Kaggle 提供的非常干净的数据集中,分解的需求是不常见的。另一方面,企业数据集通常会有字段没有正确分解成原子值,这反映了优化或遗留处理需求。例如,产品 ID 代码可能包括业务线和引入年份作为代码的一部分。例如,一艘船的船体 ID 号码可能包括“421880182”,这意味着它是一艘 42 英尺的船体,序列号 188,1982 年 1 月完成。三个不同的项目都被编码为数字。出于分析目的,可能需要将构成编码值的项分开。在其他情况下,需要将几个源字段组合在一起。当查看潮汐数据时,可以找到一个将时间戳分解为三个单独字段的数据集示例。
查看美国潮汐预测的tidesandcurrents.noaa.gov/tide_predictions.html
。此网站支持多种格式的下载,以及针对潮汐预测的 RESTful API 请求。
年度潮汐表中每个潮汐事件都有一个时间戳。时间戳被分解为三个单独的字段:日期、星期几和当地时间。星期几是有帮助的,但它完全来自日期。日期和时间需要组合成一个单一的日期时间值,以便使这些数据有用。通常使用datetime.combine(date, time)
将单独的日期和时间值合并成一个值。
有时一个数据集将会有多种子类型记录合并到一个单一集合中。这些不同类型通常通过字段的值来区分。一个财务应用程序可能包括发票和付款的混合;许多字段重叠,但这两类交易类型的意义却截然不同。一个字段具有“ I”或“ P”代码值的记录可能是区分表示的业务记录类型的唯一方式。
当存在多个子类型时,该集合可以称为子类型的区分 联合;有时简单地称为联合。区分器和子类型表明需要一个类层次结构来描述样本类型的多样性。需要一个公共基类来描述包括区分器在内的公共字段。每个子类都有对子类独特字段的特定定义。
另一个额外的复杂性来自“不透明”的数据源。这些是可以用于相等比较的字符串字段,但不能用于其他目的。这些值通常是数据分析方法掩码、去标识化或匿名化的结果。有时这也被称为“标记化”,因为不透明的标记已经替换了敏感数据。例如,在银行业务中,分析数据将账户号码或支付卡号码转换为不透明值是常见的。这些可以用于汇总行为,但不能用于识别个人账户持有人或支付卡。这些字段必须被视为字符串,不能进行其他处理。
目前,我们将推迟这些复杂性的实现细节到后面的章节。这些想法应该为初始、基础应用程序的设计决策提供信息。
除了干净的有效数据外,应用程序还需要产生关于无效数据的信息。接下来,我们将查看日志和错误报告。
9.1.5 错误报告
该应用程序的核心功能是输出用于分析目的的有效、有用的数据文件。我们已经省略了一些关于获取的文档实际上不可用时会发生什么细节。
这里有一些与无效数据可观察性相关的选择:
-
抛出一个总体异常并停止。这在处理像 Anscombe 四重奏这样的精心策划的数据集时是合适的。
-
使所有坏数据可观察,无论是通过日志还是将坏数据写入单独的拒绝样本文件。
-
悄悄拒绝坏数据。这通常用于没有对源数据进行策划或质量控制的大型数据源。
在所有情况下,获取的数据、可用的分析数据和清理以及拒绝的数据的总结计数都是必不可少的。确保读取的原始记录数被计入,并且清理和拒绝的数据的来源清晰。在许多情况下,总结计数是观察数据源变化的主要方式。非零错误计数可能非常重要,以至于它被用作清理应用程序的最终退出状态代码。
除了坏数据可观察性之外,我们可能能够清理源数据。这里也有几个选择:
-
记录每个对象在清理过程中所做的详细情况。这通常用于来自电子表格的数据,其中意外的数据可能是需要手动纠正的行。
-
计算没有支持细节的已清洗项目数量。这通常用于数据源变化频繁的大型数据源。
-
将不良数据作为预期、正常的操作步骤进行静默清理。这通常用于原始数据直接来自不可靠环境中的测量设备时,例如在太空或海底。
此外,每个字段可能都有不同的规则,以确定清洗不良数据是否是一个重要的问题或一个常见、预期的操作。可观察性和自动化清洗的交集有许多替代方案。
数据清洗和标准化的解决方案通常需要与用户进行深入、持续的对话。每个数据采集管道在错误报告和数据清洗方面都是独特的。
有时需要命令行选项来选择记录每个错误或简单地总结错误数量。此外,当发现任何不良记录时,应用程序可能会返回非零退出代码;这允许父应用程序(例如,shell 脚本)在出现错误时停止处理。
我们已经探讨了整体处理、源文件、结果文件以及可能使用的某些错误报告替代方案。在下一节中,我们将探讨我们可以用来实现此应用的一些设计方法。
9.2 方法
当我们审视我们的方法时,我们将从 C4 模型(c4model.com
)中汲取一些指导。
-
上下文:对于本项目,上下文图已扩展到三个用例:获取、检查和清洗。
-
容器:有一个容器用于各种应用:用户的个人电脑。
-
组件:有两个显著不同的软件组件集合:采集程序和清洗程序。
-
代码:我们将简要提及,以提供一些建议的方向。
该应用的上下文图如图图 9.1所示。
图 9.1:上下文图
转换应用的组件图不会像采集应用的组件图那样复杂。一个原因是读取、提取或下载原始数据文件没有选择。源文件是由采集应用创建的 ND JSON 文件。
转换程序通常更简单的原因是它们通常依赖于内置的 Python 类型定义,以及像pydantic
这样的包来提供所需的转换处理。解析 HTML 或 XML 源的复杂性被隔离在采集层,允许此应用专注于问题域数据类型和关系。
该应用的组件如图图 9.2所示。
图 9.2:组件图
注意,我们使用了点划线的“依赖于”箭头。这并不显示从获取到清洗的数据流。它显示了清洗应用如何依赖于获取应用的输出。
清洗应用的设计通常涉及几乎完全功能性的设计。当然,可以使用类定义。当应用处理涉及无状态、不可变对象时,类似乎没有帮助。
在罕见情况下,清洗应用可能需要进行数据的大规模重组。可能需要从各种交易中积累细节,更新复合对象的状态。例如,可能存在多个付款项需要合并以进行对账。在这种应用中,关联付款和发票可能需要通过复杂的匹配规则进行操作。
注意,清洗应用和获取应用将共享一组共同的数据类。这些类代表源数据,获取应用的输出。它们还定义了清洗应用的输入。另一组数据类代表用于后续分析应用的作业值。
我们的目标是创建三个模块:
-
clean.py
:主要应用。 -
analytical_model.py
:一个包含用于纯 Python 对象的 dataclass 定义的模块。这些类通常将由具有字符串值的 JSON 友好字典创建。 -
conversions.py
:一个包含任何专用验证、清洗和转换函数的模块。
如果需要,可能需要任何特定应用转换函数来将源值转换为“清洗”的、可用的 Python 对象。如果无法完成此操作,函数可以改为抛出ValueError
异常,以遵循 Python 内置float()
函数等函数的既定模式。此外,当整个对象无效时,TypeError
异常可能很有帮助。在某些情况下,使用assert
语句,并可能抛出AssertionError
来指示无效数据。
对于这个基线应用,我们将坚持使用更简单、更常见的模式。我们将查看结合验证和清洗的单独函数。
9.2.1 模型模块重构
我们似乎有两个不同的模型:带有文本字段的“获取”模型和带有适当 Python 类型(如float
和int
)的“待分析”模型。模型存在多个变体意味着我们需要很多不同的类名,或者需要两个不同的模块作为命名空间来组织类。
清洗应用是唯一一个同时使用获取和分析模型的应用。其他应用要么获取原始数据,要么处理清洗后的分析数据。
之前的例子中有一个单独的 model.py
模块,用于存储获取的数据的 dataclasses。此时,已经更加明确,这并不是一个长期的好决定。因为数据模型有两种不同的变体,所以通用的 model
模块名称需要重构。为了区分获取的数据模型和分析数据模型,前缀应该足够:模块名称可以是 acquire_model
和 analysis_model
。
(英语的词性不完全匹配。我们宁愿不用输入 “acquisition_model”。略短的名称似乎更容易处理,并且足够清晰。)
在这两个模型文件中,类名可以是相同的。我们可能有 acquire_model.SeriesSample
和 analysis_model.SeriesSample
这样的不同类名。
在一定程度上,我们有时可以复制获取的模型模块来创建分析模型模块。我们需要将 from dataclasses import dataclass
改为 Pydantic 版本的 from pydantic import dataclass
。这是一个非常小的改动,使得开始变得容易。在某些较旧的 Pydantic 和 mypy 版本中,Pydantic 版本的 dataclass
没有以对 mypy 工具透明的方式暴露属性类型。
在许多情况下,导入 BaseModel
并将其用作分析模型的父类通常可以更好地与 mypy 工具共存。当从 dataclasses 升级到利用 pydantic 包时,这需要较大的改动。由于它在使用 mypy 工具时很有益,所以我们推荐遵循此路径。
此 Pydantic 版本的 dataclass
引入了一个单独的验证器方法,该方法将(自动)用于处理字段。对于从获取类到分析类的映射相对清晰的简单类定义,需要修改类定义。
对于这个新的分析模型类,以下是一个 Pydantic 版本 1 的常见设计模式示例:
from pydantic import validator, BaseModel, Field
class SeriesSample(BaseModel):
"""
An individual sample value.
"""
x: float = Field(title="The x attribute", ge=0.0)
y: float = Field(title="The y attribute", ge=0.0)
@validator(’x’, ’y’, pre=True)
def clean_value(cls, value: str | float) -> str:
match value:
case str():
for char in "\N{ZERO WIDTH SPACE}":
value = value.replace(char, "")
return value
case float():
return value
此设计定义了一个类级别的方法 clean_value()
,用于处理当数据是字符串时的数据清洗。验证器使用 @validator()
装饰器提供该函数应用的属性名称,以及操作序列中的特定阶段。在这种情况下,pre=True
表示此验证在各个字段被验证并转换为有用类型之前进行。
这将在 Pydantic 版本 2 中被许多更加灵活的替代方案所取代。新版本将放弃用于确保在内置处理程序访问字段之前完成此操作的 pre=True
语法。
Pydantic 2 版本的发布将引入一种使用注解来指定验证规则的根本性新方法。它还将保留一个与旧版本 1 验证非常相似的装饰器。
一种迁移路径是将 validator
替换为 field_validator
。这将需要将 pre=True
或 post=True
更改为更通用的 mode=’before’
或 mode=’after’
。这种新方法允许编写在前后处理中“包装”转换处理器的字段验证器。
要使用 Pydantic 第二版,使用 @field_validator(‘x’,‘y’,mode=‘before’)
来替换示例中的 @validator
装饰器。import
也必须更改以反映装饰器的新名称。
这个验证器函数处理源数据的字符串版本可能包含 Unicode U+200B
的情况,这是一个称为零宽度的特殊字符。在 Python 中,我们可以使用 "\N{ZERO WIDTH SPACE}"
来使这个字符可见。虽然名称很长,但似乎比神秘的 "\u200b"
好一些。
(有关此字符的详细信息,请参阅 www.fileformat.info/info/unicode/char/200b/index.htm
。)
当一个函数在 pre=True
或 mode=’before’
阶段工作时,那么 pydantic 将自动应用最终的转换函数来完成验证和转换的基本工作。因此,可以设计这个额外的验证器函数,仅专注于清洗原始数据。
验证器函数的想法必须反映这个类两个不同的用例:
-
清洗和转换获取的数据,通常是字符串,到更有用的分析数据类型。
-
加载已经清洗的分析数据,其中不需要类型转换。
我们目前的主要兴趣在于第一个用例,即清洗和转换。稍后,从第 第十三章 开始,我们将切换到第二个用例,即加载清洗数据。
这两个用例反映在验证器函数的类型提示中。参数定义为 value: str | float
。第一个用例,转换,期望值为 str
类型。第二个用例,加载清洗数据,期望值为 float
类型的清洗值。这种类型的联合对验证器函数很有帮助。
分析模型的实例将从 acquire_model
对象构建。因为获取的模型使用 dataclasses
,我们可以利用 dataclasses.asdict()
函数将源对象转换为字典。这可以用于执行 Pydantic 验证和转换以创建分析模型对象。
我们可以在数据类定义中添加以下方法:
@classmethod
def from_acquire_dataclass(
cls,
acquired: acquire_model.SeriesSample
) -> "SeriesSample":
return SeriesSample(**asdict(acquired))
此方法从获取数据模型版本的 SeriesSample
类中提取字典,并使用它来创建分析模型变体类的实例。此方法将所有验证和转换工作推送到 Pydantic 声明。此方法还需要 from dataclasses import asdict
来引入所需的 asdict()
函数。
在字段名称不匹配或需要其他转换的情况下,可以使用更复杂的字典构建器来替换asdict(acquired)
处理。我们将在第十章、数据清理功能中看到这些示例,其中获取的字段需要在转换之前进行组合。
我们将在第十一章、项目 3.7:中间数据持久化中重新审视这个设计决策的一些方面。然而,首先,我们将查看pydantic版本 2 的验证,它为验证函数提供了一条更为明确的路径。
9.2.2 Pydantic V2 验证
虽然pydantic版本 2 将提供一个与遗留的@validator
装饰器非常相似的@field_validator
装饰器,但这种方法存在一个令人烦恼的问题。列出应用验证规则的字段可能会令人困惑。由于字段定义和验证字段值的函数之间的分离,可能会产生一些混淆。在我们的示例类中,验证器应用于x
和y
字段,这是一个在首次查看类时可能难以注意到的细节。
以下示例展示了 Pydantic 版本 2 的分析模型类的新设计模式:
from pydantic import BaseModel
from pydantic.functional_validators import field_validator, BeforeValidator
from typing import Annotated
def clean_value(value: str | float) -> str | float:
match value:
case str():
for char in "\N{ZERO WIDTH SPACE}":
value = value.replace(char, "")
return value
case float():
return value
class SeriesSample(BaseModel):
x: Annotated[float, BeforeValidator(clean_value)]
y: Annotated[float, BeforeValidator(clean_value)]
我们省略了from_acquire_dataclass()
方法定义,因为它没有变化。
清理函数是在类外部定义的,这使得它在复杂的应用程序中更容易重用,在这些应用程序中,许多规则可能在多个模型中被广泛重用。Annotated[]
类型提示将基本类型与一系列验证器对象结合起来。在这个例子中,基本类型是float
,验证器对象是包含要应用函数的BeforeValidator
对象。
为了减少明显的重复,可以使用TypeAlias
。例如,
from typing import Annotated, TypeAlias
CleanFloat: TypeAlias = Annotated[float, BeforeValidator(clean_value)]
使用别名允许模型使用类型提示CleanFloat
。例如x: CleanFloat
。
此外,Annotated
提示是可组合的。一个注解可以给之前定义的注解添加功能。在基础注解之上构建更复杂的注解的能力,为以简洁和表达的方式定义类提供了巨大的潜力。
现在我们已经看到了如何实现单个验证,我们需要考虑替代方案,以及一个应用程序可能需要多少种不同的验证函数。
9.2.3 验证函数设计
pydantic
包提供了基于注解的大量内置转换。虽然这些可以覆盖大量常见情况,但仍有一些情况需要特殊的验证器,甚至可能需要特殊的类型定义。
在转换和处理中,我们考虑了一些可能需要的处理类型。这些包括以下类型的转换:
-
将源字段分解为其原子组件。
-
将分离的源字段合并以创建适当的值。例如,日期和时间通常是这样做的。
-
样本流中可能存在多个子实体。这可以称为区分联合:整个流是不同类型的唯一组合,而区分值(或值)区分各种子类型。
-
字段可能是一个“令牌”,用于去除原始源信息。例如,用于替换驾照号码的替换令牌可以替换真实政府颁发的号码,以使个人匿名。
此外,我们可能需要考虑可观察性,这会引导我们编写自己的独特验证器,以便写入所需的日志条目或更新计数器,显示特定验证发现问题的次数。这种增强的可视性有助于确定数据中经常不规则或质量控制不佳的问题。
我们将在第十章数据清洗功能中更深入地探讨这些概念。在第十章数据清洗功能中,我们还将探讨处理主键和外键的功能。目前,我们将专注于 Python 内置函数和标准库中的内置类型转换函数。但我们需要认识到,可能会有扩展和例外。
我们将在下一节中探讨整体设计方法。
9.2.4 增量设计
没有对源数据的详细了解,很难最终确定清洗应用程序的设计。这意味着清洗应用程序依赖于通过制作数据检查笔记本学到的经验教训。一个理想化的工作流程从“理解需求”开始,然后进行“编写代码”,将这两个活动视为独立的、隔离的步骤。这个概念工作流程有点谬误。通常,没有对实际源数据进行详细审查以揭示存在的怪癖和异常,很难理解需求。数据审查通常会导致数据验证函数的第一稿。在这种情况下,需求将以代码草案的形式出现,而不是一份精心制作的文档。
这导致了一种在临时检查和正式数据清洗应用程序之间的来回。这种迭代工作通常会导致一个函数模块来处理问题域的数据。这个模块可以被检查笔记本以及自动化应用程序共享。适当的工程遵循DRY(不要重复自己)原则:代码不应在模块之间复制和粘贴。它应该放入共享模块中,以便可以正确地重用。
在某些情况下,两个数据清理函数可能相似。发现这一点表明,某种分解是合适的,以将公共部分与独特部分分开。通过有一套单元测试来确认在将函数转换为删除重复代码时没有破坏旧功能,可以简化重构和重构。
创建清理应用程序的工作是迭代和逐步的。罕见特殊情况很少见,而且通常在处理管道似乎完成很久之后才会出现。意外出现的特殊情况数据类似于观鸟者在其预期栖息地外看到的鸟。将数据检查笔记本想象成观鸟者的大号望远镜,用来仔细观察一只意外、罕见的鸟,通常是在一群具有相似觅食和栖息偏好的鸟群中。罕见鸟的存在成为鸟类学家(和业余爱好者)的新数据点。在意外数据的情况下,检查笔记本的教训成为转换模块的新代码。
数据清理应用程序的整体主模块将实现命令行界面(CLI)。我们将在下一节中探讨这一点。
9.2.5 CLI 应用程序
该应用程序的用户体验表明,它操作在以下不同的上下文中:
-
作为独立应用程序。用户运行
src/acquire.py
程序。然后,用户运行src/clean.py
程序。 -
作为处理管道的一个阶段。用户运行一个 shell 命令,将
src/acquire.py
程序输出的内容管道到src/clean.py
程序。这是项目 3.6:集成 创建获取管道的主题。
这导致以下两个运行时上下文:
-
当应用程序提供输入路径时,它被用作独立应用程序。
-
当没有提供输入路径时,应用程序从
sys.stdin
读取。
类似的分析可以应用于获取应用程序。如果提供了输出路径,应用程序将创建并写入命名的文件。如果没有提供输出路径,应用程序将写入sys.stdout
。
这的一个基本后果是所有日志必须写入sys.stderr
。
仅使用stdin和stdout作为应用程序数据,不使用其他任何内容。
使用一致、易于解析的文本格式,如 ND JSON,用于应用程序数据。
将所有控制和错误消息的输出目的地设置为stderr。
这意味着print()
可能需要file=sys.stderr
来将调试输出定向到stderr。或者,避免使用简单的print()
,而使用logger.debug()
代替。
对于这个项目,独立选项就足够了。然而,了解将在后续项目中添加的替代方案很重要。参见项目 3.6:集成创建获取管道以了解这种更紧密集成的替代方案。
重定向标准输出
Python 提供了一个方便的工具来管理“写入打开文件”和“写入 stdout”之间的选择。它涉及以下基本设计原则。
总是为处理数据的函数和方法提供类似文件的对象。
这表明需要一个类似以下的数据清理函数:
from typing import TextIO
def clean_all(acquire_file: TextIO, analysis_file: TextIO) -> None:
...
此函数可以使用 json.loads()
解析来自 acquire_file
的每个文档。它使用 json.dumps()
将每个文档保存到 analysis_file
以供后续分析使用。
整个应用可以选择以下四种可能的方式来使用此 clean_all()
函数:
-
独立:这意味着
with
语句管理从作为参数值提供的Path
名称创建的打开文件。 -
管道 头部:一个
with
语句可以管理传递给acquire_file
的打开文件。analysis_file
的值是sys.stdout
。 -
管道 尾部:获取的输入文件是
sys.stdin
。一个with
语句管理用于analysis_file
的打开文件(写入模式)。 -
管道 中间:
acquire_file
是sys.stdin
;analysis_file
是sys.stdout
。
现在我们已经探讨了多种技术方法,接下来将在下一节中转向本项目的可交付成果列表。
9.3 可交付成果
此项目有以下可交付成果:
-
docs
文件夹中的文档。 -
tests/features
和tests/steps
文件夹中的接收测试。 -
tests
文件夹中的应用模块的单元测试。 -
应用到清理一些获取的数据,并对几个字段进行简单的转换。后续项目将添加更复杂的验证规则。
我们将更详细地查看其中一些可交付成果。
当开始一种新的应用时,通常从接收测试开始是有意义的。后来,当添加功能时,新的接收测试可能不如新的单元测试重要。我们将首先查看这种新应用的新场景。
9.3.1 接收测试
正如我们在第四章、数据获取功能:Web API 和抓取中提到的,我们可以将一大块文本作为 Gherkin 场景的一部分提供。这可以是输入文件的内容。我们可以考虑以下场景。
Scenario: Valid file is recognized.
Given a file "example_1.ndjson" with the following content
"""
{"x": "1.2", "y": "3.4"}
{"x": "five", "z": null}
"""
When the clean tool is run
Then the output shows 1 good records
And the output shows 1 faulty records
这种场景让我们能够定义具有有效数据的源文档。我们还可以定义具有无效数据的源文档。
我们可以使用 Then
步骤来确认输出的一些额外细节。例如,如果我们决定使所有清理操作都可见,测试场景可以确认输出包含所有已应用的清理操作。
坏数据示例的多样性和好坏数据组合的数量表明,这种应用可能有多种场景。每次出现新的获取数据,但无法清理时,新的示例将被添加到这些接收测试用例中。
在某些情况下,广泛发布场景可能非常有帮助,以便所有利益相关者都能理解数据清理操作。Gherkin 语言被设计成让技术技能有限的人能够为测试用例做出贡献。
我们还需要场景来从命令行运行应用程序。这些场景的When
步骤定义将是subprocess.run()
来调用清理应用程序,或者调用包含清理应用程序的 shell 命令。
9.3.2 模型特征的单元测试
对于模型定义类,拥有自动化的单元测试是很重要的。
同样重要的是不要测试pydantic
组件。例如,我们不需要测试pydantic
模块已经完成的普通字符串到浮点数的转换;我们可以信任这会完美工作。
我们必须测试我们编写的验证器函数。这意味着提供测试用例来测试验证器的各种功能。此外,任何整体的from_acquire_dataclass()
方法都需要有测试用例。
每个这些测试场景都与一个给定的获取文档和原始数据一起工作。当from_acquire_dataclass()
方法被评估时,可能会有异常,或者会创建一个分析模型文档。
异常测试可以利用pytest.raises()
上下文管理器。测试是用with
语句编写的,用于捕获异常。
请参阅docs.pytest.org/en/7.2.x/how-to/assert.html#assertions-about-expected-exceptions
以获取示例。
当然,我们还需要测试正在进行的处理。按照设计,这类应用中涉及的处理并不多。大部分处理可能只有几行代码来消费原始模型对象并生成分析对象。大部分工作将委托给json
和pydantic
等模块。
9.3.3 应用以清理数据并创建 NDJSON 临时文件
现在我们有了验收和单元测试套件,我们需要创建clean
应用程序。最初,我们可以创建一个占位符应用程序,只是为了看到测试套件失败。然后我们可以填充各个部分,直到应用程序作为一个整体工作。
在这个应用程序中,灵活性至关重要。在下一章(第十章,数据清理功能)中,我们将介绍大量数据验证场景。在第十一章(项目 3.7:临时数据持久化)中,我们将重新审视保存清理数据的想法。现在,创建干净的数据是至关重要的;稍后,我们可以考虑哪种格式可能最好。
9.4 总结
本章已经涵盖了数据验证和清理应用的一些方面:
-
CLI 架构以及如何设计简单的流程管道。
-
核心概念包括验证、清理、转换和标准化原始数据。
在下一章中,我们将更深入地探讨一些数据清理和标准化的功能。这些项目都将基于这个基础应用程序框架。在这些项目之后,接下来的两章将更仔细地研究分析数据持久性选择,并为提供清洗数据给其他利益相关者提供一个集成网络服务。
9.5 额外内容
这里有一些想法供您添加到这个项目中。
9.5.1 创建一个包含拒绝样本的输出文件
在错误报告中,我们建议有时创建一个拒绝样本的文件是合适的。对于本书中的例子——其中许多是从精心整理、仔细管理的数据集中抽取的——设计一个会拒绝数据的程序可能会感觉有点奇怪。
对于企业应用来说,数据拒绝是一个常见需求。
可以看看这样的数据集:datahub.io/core/co2-ppm
。这个数据集包含与使用 ppm(百万分之一)单位测量的 CO2 水平相同的测量数据。
这里有部分样本在月份的天数上无效。还有一些样本没有记录月度 CO2 水平。
使用拒绝文件将这个数据集划分为清晰可用的记录和不太清晰可用的记录可能会有所启发。
输出将不会反映分析模型。这些对象将反映获取模型;它们是从获取结构到所需分析结构无法正确转换的项目。
第十章
数据清洗功能
有许多验证数据并将其转换为后续分析的原生 Python 对象的技术。本章将指导您了解这三种技术,每种技术适用于不同类型的数据。本章接着讨论标准化思想,将异常或不典型值转换为更有用的形式。本章最后将获取和清洗集成到一个复合管道中。
本章将在第九章、项目 3.1:数据清洗基础应用程序的基础上扩展项目。以下附加技能将被强调:
-
CLI 应用程序扩展和重构以添加功能。
-
验证和转换的 Python 方法。
-
发现关键关系的技巧。
-
管道架构。这可以被视为向处理DAG(有向无环图)迈出的第一步,其中各个阶段相互连接。
我们将从一个描述开始,以扩展之前章节中关于处理的内容。这将包括一些新的Pydantic功能,以处理更复杂的数据源字段。
10.1 项目 3.2:验证和转换源字段
在第九章、项目 3.1:数据清洗基础应用程序中,我们依赖于Pydantic包的基础行为,将源文本中的数值字段转换为 Python 类型,如int
、float
和Decimal
。在本章中,我们将使用包含日期字符串的数据集,以便我们可以探索一些更复杂的转换规则。
这将遵循早期项目的设计模式。它将使用一个不同的数据集,以及一些独特的数据模型定义。
10.1.1 描述
本项目的目的是执行数据验证、清洗和标准化。本项目将扩展pydantic
包的功能,以进行更复杂的数据验证和转换。
这个新的数据清洗应用程序可以设计成一个数据集,例如tidesandcurrents.noaa.gov/tide_predictions.html
。美国周围的潮汐预测包括日期,但字段被分解,我们的数据清洗应用程序需要将它们合并。
具体示例请见tidesandcurrents.noaa.gov/noaatideannual.html?id=8725769
。注意,下载的.txt
文件是一个带有非常复杂的多行标题的制表符分隔的 CSV 文件。这需要一些类似于在第三章、项目 1.1:数据获取基础应用程序中展示的复杂获取处理。
另一个例子是 CO2 PPM——大气二氧化碳趋势数据集,可在datahub.io/core/co2-ppm
找到。这个数据集提供了两种日期形式:一种是年-月-日
字符串,另一种是十进制数。如果我们能够重现十进制数值,我们就能更好地理解这些数据。
第二个示例数据集是datahub.io/core/co2-ppm/r/0.html
。这是一个 HTML 文件,需要一些类似于第四章中示例的获取处理。
这个清理应用程序的使用案例与第九章,项目 3.1:数据清理基础应用程序中显示的描述相同。获取的数据——纯文本,从源文件中提取——将被清理以创建具有有用 Python 内部字段的Pydantic模型。
我们将快速查看tidesandcurrents.noaa.gov
网站上的潮汐表数据。
NOAA/NOS/CO-OPS
Disclaimer: These data are based upon the latest information available ...
Annual Tide Predictions
StationName: EL JOBEAN, MYAKKA RIVER
State: FL
Stationid: 8725769
ReferencedToStationName: St. Petersburg, Tampa Bay
ReferencedToStationId: 8726520
HeightOffsetLow: * 0.83
HeightOffsetHigh: * 0.83
TimeOffsetLow: 116
TimeOffsetHigh: 98
Prediction Type: Subordinate
From: 20230101 06:35 - 20231231 19:47
Units: Feet and Centimeters
Time Zone: LST_LDT
Datum: MLLW
Interval Type: High/Low
Date Day Time Pred(Ft) Pred(cm) High/Low
2023/01/01 Sun 06:35 AM -0.13 -4 L
2023/01/01 Sun 01:17 PM 0.87 27 H
etc.
要获取的数据有两个有趣的结构问题:
-
有一个包含一些有用元数据的 19 行序言。第 2 到 18 行具有标签和值的格式,例如,
State: FL
。 -
数据是制表符分隔的 CSV 数据。看起来有六个列标题。然而,查看制表符,有八个标题列后面跟着九个数据列。
获取的数据应该符合以下类定义片段中的数据类定义:
from dataclasses import dataclass
@dataclass
class TidePrediction:
date: str
day: str
time: str
pred_ft: str
pred_cm: str
high_low: str
@classmethod
def from_row(
cls: type["TidePrediction"],
row: list[str]
) -> "TidePrediction":
...
示例省略了from_row()
方法的细节。如果使用 CSV 读取器,此方法需要从 CSV 格式文件中挑选列,跳过通常为空的列。如果使用正则表达式解析源行,此方法将使用匹配对象的组。
由于这看起来像许多以前的项目,我们接下来将探讨独特的技术方法。
10.1.2 方法
数据清理应用程序的核心处理应该与早期示例非常相似——除了少数模块更改。为了参考,请参阅第九章,项目 3.1:数据清理基础应用程序,特别是方法。这表明clean
模块应该与早期版本有最小的更改。
主要差异应该是对acquire_model
和analysis_model
的两种不同实现。对于潮汐数据示例,在描述部分展示了一个可用于获取模型的类。
在获取的数据(通常是文本)和用于后续分析的数据(可以是更多有用的 Python 对象类型混合)之间保持清晰的区分是很重要的。
从源数据到中间获取数据格式的两步转换,以及从获取数据格式到清洁数据格式的转换,有时可以优化为单一转换。
将处理合并为单一步骤的优化也可能使调试更加困难。
我们将展示一种定义潮汐状态值枚举集的方法。在源数据中,使用’H’
和’L’
代码。以下类将定义这个值的枚举:
from enum import StrEnum
class HighLow(StrEnum):
high = ’H’
low = ’L’
我们将依靠枚举类型和另外两种注解类型来定义一个完整的记录。在展示整个记录之后,我们将回到注解类型。一个完整的潮汐预测记录看起来如下:
import datetime
from typing import Annotated, TypeAlias
from pydantic import BaseModel
from pydantic.functional_validators import AfterValidator, BeforeValidator
# See below for the type aliases.
class TidePrediction(BaseModel):
date: TideCleanDateTime
pred_ft: float
pred_cm: float
high_low: TideCleanHighLow
@classmethod
def from_acquire_dataclass(
cls,
acquired: acquire_model.TidePrediction
) -> "TidePrediction":
source_timestamp = f"{acquired.date} {acquired.time}"
return TidePrediction(
date=source_timestamp,
pred_ft=acquired.pred_ft,
pred_cm=acquired.pred_cm,
high_low=acquired.high_low
)
这显示了在验证之前,源列的date
和time
是如何合并成一个单一文本值的。这是通过from_acquire_dataclass()
方法完成的,因此它发生在调用TidePrediction
构造函数之前。
TideCleanDateTime
和TideCleanHighLow
类型提示将利用注解类型为这些属性中的每一个定义验证规则。以下是两个定义:
TideCleanDateTime: TypeAlias = Annotated[
datetime.datetime, BeforeValidator(clean_date)]
TideCleanHighLow: TypeAlias = Annotated[
HighLow, BeforeValidator(lambda text: text.upper())]
TideCleanDateTime
类型使用clean_date()
函数在转换之前清理date
字符串。同样,TideCleanHighLow
类型使用 lambda 将值转换为大写字母,然后在验证HighLow
枚举类型之前。
clean_date()
函数通过将一个(且仅一个)预期的日期格式应用于字符串值来工作。这不是为了灵活或宽容而设计的。它是为了确认数据与预期完全匹配。
函数看起来是这样的:
def clean_date(v: str | datetime.datetime) -> datetime.datetime:
match v:
case str():
return datetime.datetime.strptime(v, "%Y/%m/%d %I:%M %p")
case _:
return v
如果数据与预期格式不匹配,strptime()
函数将引发ValueError
异常。这将被纳入pydantic.ValidationError
异常中,该异常列举了遇到的全部错误。match
语句将非字符串值传递给pydantic处理程序进行验证;我们不需要处理任何其他类型。
此模型也可以用于清洁数据的分析。(参见第十三章**,项目 4.1:可视化分析技术*。)在这种情况下,数据将已经是有效的datetime.datetime
对象,不需要进行转换。使用类型提示str`` |`` datetime.datetime
强调了此方法将应用于两种类型的值。
这个“合并和转换”的两步操作被分解为两步,以适应Pydantic设计模式。这种分离遵循最小化复杂初始化处理和创建更具声明性、更少主动性的类定义的原则。
保持转换步骤小而独立通常是有帮助的。
在需要更改时,过早地优化以创建一个单一的复合函数通常是一个噩梦。
为了显示目的,日期、星期几和时间可以从中提取单个datetime
实例。没有必要将许多与日期相关的字段作为TidePrediction
对象的一部分保留。
潮汐预测提供了两个不同的度量单位。在此示例中,我们保留了两个单独的值。从实用主义的角度来看,英尺的高度是厘米高度乘以。
对于某些应用,当英尺高度值很少使用时,属性可能比计算值更有意义。对于其他应用,当两种高度都广泛使用时,同时计算这两个值可能会提高性能。
10.1.3 可交付成果
此项目有以下可交付成果:
-
docs
文件夹中的文档。 -
在
tests/features
和tests/steps
文件夹中的验收测试。 -
对
tests
文件夹中的应用模块进行单元测试。 -
应用以清理一些获取的数据,并对几个字段进行简单的转换。后续的项目将添加更复杂的验证规则。
其中许多可交付成果在之前的章节中已有描述。具体来说,第九章**,项目 3.1:数据清洗基础应用*涵盖了此项目可交付成果的基础。
验证函数的单元测试
Pydantic 类使用的独特验证器需要测试用例。对于示例中所示,验证器函数用于将两个字符串转换为日期。
边界值分析表明,日期转换有三个等价类:
-
语法无效的日期。标点符号或数字的位数是错误的。
-
语法有效,但日历无效的日期。例如,2 月 30 日是无效的,即使格式正确。
-
有效的日期。
上述类列表导致至少有三个测试用例。
一些开发者喜欢探索日期内的每个字段,提供 5 个不同的值:下限(通常是 1),上限(例如,12 或 31),略低于上限(例如,0),略高于上限(例如,13 或 32),以及一个在范围内且有效的值。然而,这些额外的测试用例实际上是在测试datetime
类的strptime()
方法。这些案例是对datetime
模块的重复测试。这些案例是不必要的,因为datetime
模块已经有了足够多的针对日历无效日期字符串的测试用例。
不要测试应用程序外模块的行为。这些模块有自己的测试用例。
在下一节中,我们将探讨一个验证名义数据的项目。这可能比验证序数或基数数据更复杂。
10.2 项目 3.3:验证文本字段(以及数值编码字段)
对于名义数据,我们将使用pydantic将验证器函数应用于字段值的技巧。在字段包含仅由数字组成的代码的情况下,可能会有些模糊,即值是否为基数。一些软件可能将任何数字序列视为数字,忽略前导零。这可能导致需要使用验证器来恢复字段(字符串形式的数字,但不是基数)的合理值。
10.2.1 描述
本项目的目的是执行数据验证、清洗和标准化。本项目将扩展Pydantic包的功能,以进行更复杂的数据验证和转换。
我们将继续使用类似tidesandcurrents.noaa.gov/tide_predictions.html
的数据集。美国的潮汐预测包括日期,但日期被分解为三个字段,我们的数据清洗应用程序需要将它们合并。
对于一个具体的例子,请参阅tidesandcurrents.noaa.gov/noaatideannual.html?id=8725769
。请注意,下载的.txt
文件实际上是一个带有复杂标题的制表符分隔的 CSV 文件。这需要一些类似于第三章中所示示例的复杂获取处理。
对于具有相对较小唯一值域的数据,Python 的enum
类是一个非常方便的方式来定义允许的值集合。使用枚举允许pydantic
进行简单、严格的验证。
一些数据——例如账户号码,仅举一例——具有可能处于变化状态的大量值域。使用enum
类意味着在尝试处理任何数据之前,将有效的账户号码集合转换为枚举类型。这可能并不特别有用,因为很少有必要确认账户号码的有效性;这通常是关于数据的一个规定。
对于像账户号码这样的字段,可能需要验证潜在值,而不必列举所有允许的值。这意味着应用程序必须依赖于文本的模式来确定值是否有效,或者值是否需要清理才能使其有效。例如,可能需要一定数量的数字,或者代码中嵌入的校验位。在信用卡号码的情况下,信用卡号码的最后一位数字用作确认整体号码有效的一部分。更多信息,请参阅www.creditcardvalidator.org/articles/luhn-algorithm
。
在考虑了一些需要执行的其他验证之后,我们将探讨为清洗应用程序添加更复杂验证的设计方法。
10.2.2 方法
对于对该应用的一般方法的参考,请参阅第九章,项目 3.1:数据清洗基础应用,特别是方法。
模型可以使用pydantic包定义。此包提供了两种验证字符串值与有效值域的方法。这些替代方案是:
-
定义包含所有有效值的枚举。
-
为字符串字段定义一个正则表达式。这具有定义非常大的有效值域的优势,包括可能无限的值域。
枚举是一个优雅的解决方案,它将值列表定义为类。如前所述,它可能看起来像这样:
import enum
class HighLow(StrEnum):
high = 'H'
low = 'L'
这将定义一个包含两个字符串值“L”和“H”的域。此映射提供了更容易理解的名称,Low
和High
。此类可以被pydantic用于验证字符串值。
当我们需要应用一个带有BeforeValidator
注解的类型时,一个例子可能是某些带有小写“h”和“l”而不是正确的大写“H”或“L”的海潮数据。这允许验证器在内置数据转换之前清理数据之前。
我们可能使用一个注解类型。在先前的示例中看起来是这样的:
TideCleanHighLow: TypeAlias = Annotated[
HighLow, BeforeValidator(lambda text: text.upper())]
注解类型提示描述了基类型HighLow
以及在进行pydantic转换之前要应用的验证规则。在这种情况下,它是一个将文本转换为上标的 lambda 函数。我们强调了使用显式枚举进行枚举值的验证,因为这对于建立给定属性允许的代码的完整集合是一个重要的技术。枚举类型的类定义通常是记录关于编码值的笔记和其他信息的便捷位置。
现在我们已经了解了该方法的各种方面,我们可以将注意力转向本项目的可交付成果。
10.2.3 可交付成果
本项目有以下可交付成果:
-
docs
文件夹中的文档。 -
tests/features
和tests/steps
文件夹中的验收测试。 -
tests
文件夹中的应用模块的单元测试。 -
应用到多个领域的源数据清洗。
其中许多可交付成果在之前的章节中已有描述。具体来说,第九章,项目 3.1:数据清洗基础应用涵盖了本项目可交付成果的基础内容。
验证函数的单元测试
pydantic 类使用的唯一验证器需要测试用例。对于前面显示的示例,验证器函数用于验证潮汐的状态。这是一个小的枚举值域。有三个核心类型的测试用例:
-
有效的代码,如
’H’
或’L’
。 -
可靠清理的代码。例如,小写代码
’h’
和’l’
是不含糊的。数据检查笔记本也可能揭示出非代码值,如’High’
或’Low’
,这些也可以可靠地清理。 -
无效的代码如
’’
或’9’
。
能够被正确清理的值域是会经历很大变化的。当上游应用发生变化时,发现问题和使用检查笔记来揭示新的编码是很常见的。这将导致额外的测试用例,然后是额外的验证处理以确保测试用例通过。
在下一个项目中,我们将探讨数据必须与外部定义的值集进行验证的情况。
10.3 项目 3.4:验证不同数据源之间的引用
在 第九章,项目 3.1:数据清理基础应用 中,我们依赖于 Pydantic 的基本行为将源文本字段转换为 Python 类型。下一个项目将探讨更复杂的验证规则。
10.3.1 描述
本项目的目的是执行数据验证、清理和标准化。本项目将扩展 pydantic
包的功能,以进行更复杂的数据验证和转换。
data.census.gov
上的数据集包含 ZIP Code Tabulation Areas(ZCTAs)。对于某些地区,这些美国邮政编码可以(并且应该)有前导零。然而,在某些数据变体中,ZIP 码被当作数字处理,前导零丢失了。
data.census.gov
上的数据集包含关于马萨诸塞州波士顿市的信息,该市有多个带前导零的美国邮政编码。在 data.boston.gov/group/permitting
可用的食品经营场所检查提供了波士顿地区餐馆的见解。除了邮政编码(这是名义数据)之外,这些数据还涉及许多包含名义数据和有序数据的字段。
对于具有相对较小唯一值域的数据,Python 的 enum
类是一个非常方便的方式来定义允许的值集合。使用枚举允许 Pydantic 进行简单、严格的验证。
一些数据——例如账户号码——具有可能处于变化状态的大值域。使用 enum
类意味着在尝试处理任何数据之前将有效的账户号码集合转换为枚举。这可能并不特别有用,因为很少有必要确认账户号码的有效性;这通常是对数据的一个简单规定。
这导致需要验证潜在值,而不需要列出允许的值。这意味着应用程序必须依赖于文本的模式来确定值是否有效,或者值是否需要被清理以使其有效。
当应用程序清理邮政编码数据时,清理有两个不同的部分:
-
清理邮政编码,使其具有正确的格式。对于美国 ZIP 代码,通常是 5 位数字。有些代码是 5 位数字,一个连字符,然后是 4 位数字。
-
将代码与一些主列表进行比较,以确保它是一个有意义的代码,引用了实际的邮局或位置。
重要的是将这些内容分开,因为第一步已经被上一个项目涵盖,并且不涉及任何特别复杂的事情。第二步涉及到一些额外的处理,用于将给定的记录与允许值的总列表进行比较。
10.3.2 方法
对于此应用的通用方法,请参阅第九章、项目 3.1:数据清洗基础应用,特别是方法。
当我们必须引用外部数据的名义值时,我们可以称这些为“外键”。它们是对一个外部实体集合的引用,其中这些值是主键。例如,邮政编码就是一个这样的例子。存在一个有效的邮政编码列表;在这个集合中,编码是主键。在我们的样本数据中,邮政编码是对定义集合的外键引用。
其他例子包括国家代码、美国州代码和美国电话系统区域代码。我们可以编写一个正则表达式来描述键值的潜在域。例如,对于美国州代码,我们可以使用正则表达式r’\w\w’
来描述州代码由两个字母组成。我们可以通过使用r’[A-Z]{2}’
来稍微缩小这个域,以要求州代码只使用大写字母。只有 50 个州代码,加上一些领土和地区;进一步限制会使正则表达式变得非常长。
这里的问题在于当主键需要从外部源加载时——例如,数据库。在这种情况下,简单的@validator
方法依赖于外部数据。此外,这些数据必须在任何数据清理活动之前加载。
我们有两种方法来收集有效键值集:
-
创建一个包含有效值的
Enum
类。 -
定义一个
@classmethod
来初始化 pydantic 类中的有效值。
例如,data.opendatasoft.com
有一个有用的美国邮政编码列表。查看美国邮政编码点的 URL data.opendatasoft.com/api/explore/v2.1/catalog/datasets/georef-united-states-of-america-zc-point@public/exports/csv
,这是可以下载并转换为枚举或用于初始化类的文件。创建Enum
类的操作只是创建一个包含枚举标签和值的元组列表。Enum
定义可以通过以下示例代码构建:
import csv
import enum
from pathlib import Path
def zip_code_values() -> list[tuple[str, str]]:
source_path = Path.home() / "Downloads" / "georef-united-states-of-
america-zc-point@public.csv"
with source_path.open(encoding=’utf_8_sig’) as source:
reader = csv.DictReader(source, delimiter=’;’)
values = [
(f"ZIP_{zip[’Zip Code’]:0>5s}", f"{zip[’Zip Code’]:0>5s}")
for zip in reader
]
return values
ZipCode = enum.Enum("ZipCode", zip_code_values())
这将从下载的源文件中大约 33,000 个 ZIP 代码创建一个枚举类ZipCode
。枚举标签将是类似于ZIP_75846
的 Python 属性名。这些标签的值将是美国邮政编码,例如‘75846’
。":0>5s"
字符串格式将在需要的地方强制添加前导零。
zip_code_values()
函数使我们免于编写 30,000 行代码来定义枚举类ZipCode
。相反,这个函数读取 30,000 个值,创建一个用于创建Enum
子类的成对列表。
utf_8_sig
这种奇特的编码是必要的,因为源文件有一个前导字节顺序标记(BOM)。这是不寻常的,但符合 Unicode 标准。其他 ZIP 代码的数据源可能不包括这个奇特的工件。编码优雅地忽略了 BOM 字节。
utf_8_sig
这种不寻常的编码是一个特殊情况,因为这个文件恰好处于一个奇特的格式。
文本有许多编码。虽然 UTF-8 很流行,但它并不是通用的。
当出现不寻常的字符时,找到数据来源并询问他们使用了什么编码是很重要的。
通常情况下,给定一个样本文件是无法发现编码的。ASCII、CP1252 和 UTF-8 之间有大量的有效字节码映射重叠。
这种设计需要相关的数据文件。一个潜在的改进是从源数据创建一个 Python 模块。
使用Pydantic功能验证器使用与上面显示的类似算法。验证初始化用于构建一个保留一组有效值的对象。我们将从使用注解类型的小模型的目标开始。模型看起来像这样:
import csv
from pathlib import Path
import re
from typing import TextIO, TypeAlias, Annotated
from pydantic import BaseModel, Field
from pydantic.functional_validators import BeforeValidator, AfterValidator
# See below for the type aliases.
ValidZip: TypeAlias = Annotated[
str,
BeforeValidator(zip_format_valid),
AfterValidator(zip_lookup_valid)]
class SomethingWithZip(BaseModel):
# Some other fields
zip: ValidZip
模型依赖于ValidZip
类型。这个类型有两个验证规则:在转换之前应用zip_format_valid()
函数,在转换之后使用zip_lookup_valid()
函数。
我们在这个Pydantic类中只定义了一个字段,zip
。这将让我们专注于验证-by-lookup 设计。一个更健壮的例子,可能基于上面显示的波士顿健康检查,将会有一些额外的字段来反映要分析的数据源。
在验证之前的函数zip_format_valid()
,将 ZIP 代码与正则表达式比较以确保其有效性:
def zip_format_valid(zip: str) -> str:
assert re.match(r’\d{5}|\d{5}-d{4}’, zip) is not None,
f"ZIP invalid format {zip!r}"
return zip
zip_format_valid()
函数可以被扩展以使用 f-string,例如f"{zip:0>5s}"
,来重新格式化缺少前导零的 ZIP 代码。我们将把这个集成到这个函数中留给你。
后验证函数是一个可调用对象。它是定义了__call__()
方法的类的一个实例。
这里是核心类定义和实例创建:
class ZipLookupValidator:
"""Compare a code against a list."""
def __init__(self) -> None:
self.zip_set: set[str] = set()
def load(self, source: TextIO) -> None:
reader = csv.DictReader(source, delimiter=’;’)
self.zip_set = {
f"{zip[’Zip Code’]:0>5s}"
for zip in reader
}
def __call__(self, zip: str) -> str:
if zip in self.zip_set:
return zip
raise ValueError(f"invalid ZIP code {zip}")
zip_lookup_valid = ZipLookupValidator()
这将定义 zip_lookup_valid
可调用对象。最初,内部 self.zip_set
属性没有新值。这必须使用一个评估 zip_lookup_valid.load(source)
的函数来构建。这将填充有效值的集合。
我们将这个函数命名为 prepare_validator()
,其代码如下:
def prepare_validator() -> None:
source_path = (
Path.home() / "Downloads" /
"georef-united-states-of-america-zc-point@public.csv"
)
with source_path.open(encoding=’utf_8_sig’) as source:
zip_lookup_valid.load(source)
这种复杂验证的想法遵循 SOLID 设计原则。它将 SomethingWithZip
类的基本工作与 ValidZip
类型定义分开。
此外,ValidZip
类型依赖于一个单独的类,ZipLookupValidator
,它处理加载数据的复杂性。这种分离使得更改验证文件或更改用于验证的数据格式变得相对容易,而不会破坏 SomethingWithZip
类及其使用它的应用程序。此外,它提供了一个可重用的类型,ValidZip
。这可以用于模型的多字段或多个模型。
在了解了技术方法之后,我们将转向查看本项目的可交付成果。
10.3.3 可交付成果
本项目有以下可交付成果:
-
docs
文件夹中的文档。 -
在
tests/features
和tests/steps
文件夹中的验收测试。 -
在
tests
文件夹中的应用模块单元测试。 -
用于对外部数据进行清洗和验证的应用程序。
许多这些可交付成果在之前的章节中已有描述。具体来说,第九章,项目 3.1:数据清洗基础应用涵盖了本项目可交付成果的基础。
数据收集和验证的单元测试。
Pydantic 类使用的独特验证器需要测试用例。对于所展示的示例,验证器函数用于验证美国 ZIP 代码。有三种核心类型的测试用例:
-
在 ZIP 代码数据库中找到的五位数有效 ZIP 代码。
-
语法上有效的五位数 ZIP 代码,这些代码在 ZIP 代码数据库中没有找到。
-
语法上无效的 ZIP 代码,它们没有五个数字,或者无法——通过添加前导零——变成有效代码。
10.4 项目 3.5:将数据标准化为常用代码和范围
数据清洗的另一个方面是将原始数据值转换为标准化值。例如,使用的代码随着时间的推移而演变,旧的数据代码应该标准化以匹配新的数据代码。如果关键信息被视为异常值并被拒绝或错误地标准化,那么标准化值的观念可能是一个敏感话题。
我们还可以考虑使用新的值来填补缺失值,作为一种标准化技术。当处理缺失数据或可能代表某些测量误差而非分析的基本现象的数据时,这可能是一个必要的步骤。
这种转换通常需要仔细、深思熟虑的论证。我们将展示一些编程示例。处理缺失数据、插补值、处理异常值和其他标准化操作等更深层次的问题超出了本书的范围。
查看towardsdatascience.com/6-different-ways-to-compensate-for-missing-values-data-imputation-with-examples-6022d9ca0779
以了解处理缺失或无效数据的一些方法概述。
10.4.1 描述
创建标准化值处于数据清理和验证的边缘。这些值可以描述为“派生”值,从现有值计算得出。
标准化种类繁多;我们将探讨两种:
-
为基数数据计算一个标准化值,或 Z 分数。对于正态分布,Z 分数的平均值为 0,标准差为 1。它允许比较不同尺度上测量的值。
-
将名义值合并为单个标准化值。例如,用单个当前产品代码替换多个历史产品代码。
这些方法中的第一种,计算 Z 分数,很少会引起关于标准化值统计有效性的疑问。计算公式,Z = ,是众所周知的,并且具有已知的统计特性。
第二种标准化,用标准化代码替换名义值,可能会引起麻烦。这种替换可能只是纠正了历史记录中的错误。它也可能掩盖了一个重要的关系。在需要标准化的数据集中,数据检查笔记本揭示异常值或错误值并不罕见。
企业软件可能存在未修复的 bug。一些业务记录可能具有不寻常的代码值,这些值映射到其他代码值。
当然,使用的代码可能会随时间而变化。
一些记录可能包含反映两个时代的值:修复前和修复后。更糟糕的是,当然,可能已经尝试了多次修复,导致更复杂的时序。
对于这个项目,我们需要一些相对简单的数据。Ancombe 的四重奏数据将很好地作为示例,从中可以计算派生的 Z 分数。更多信息,请参阅第三章,项目 1.1:数据采集基础 应用。
目标是计算 Anscombe 的四重奏系列样本中包含的两个值的标准化值。当数据呈正态分布时,这些派生的标准化 Z 分数将具有零平均值和标准差为 1。当数据不是正态分布时,这些值将偏离预期值。
10.4.2 方法
对于参考此应用的通用方法,请参阅第九章**,项目* 3.1:数据清理基础应用,特别是方法。
为了用首选的标准化值替换值,我们已经在以前的项目中看到了如何清理不良数据。例如,参见项目 3.3:验证文本字段(以及 数值编码字段)。
对于 Z 分数标准化,我们将计算一个导出值。这需要知道一个变量的平均值,μ,和标准差,σ,以便可以计算 Z 分数。
这种导出值的计算表明,在分析数据模型类定义上有以下两种变体:
-
一个“初始”版本,缺乏 Z 分数值。这些对象是不完整的,需要进一步计算。
-
一个“最终”版本,其中已经计算了 Z 分数值。这些对象是完整的。
处理不完整和完整对象之间区别的两种常见方法:
-
这两个类是不同的。完整版本是不完整版本的子类,定义了额外的字段。
-
导出的值被标记为可选。不完整的版本以
None
值开始。
第一种设计是一种更传统的面向对象的方法。使用一个明确标记数据状态的独立类型的形式化是一个显著的优势。然而,额外的类定义可能被视为杂乱,因为不完整的版本是暂时性的数据,不会创造持久的价值。不完整的记录足以计算完整的版本,然后可以删除该文件。
第二种设计有时用于函数式编程。它节省了子类定义,这可以看作是一种轻微的简化。
from pydantic import BaseModel
class InitialSample(BaseModel):
x: float
y: float
class SeriesSample(InitialSample):
z_x: float
z_y: float
@classmethod
def build_sample(cls, m_x: float, s_x: float,
m_y: float, s_y: float, init:
InitialSample)-> "SeriesSample":
return SeriesSample(
x=init.x, y=init.y,
z_x=(init.x - m_x) / s_x,
z_y=(init.y - m_y) / s_y
)
这两个类定义展示了如何将最初清理、验证和转换的数据与包含标准化 Z 分数的完整样本之间的区别形式化。
这可以被视为三个单独的操作:
-
清理和转换初始数据,写入包含
InitialSample
实例的临时文件。 -
读取临时文件,计算变量的平均值和标准差。
-
再次读取临时文件,从
InitialSample
实例和计算出的中间值构建最终样本。
合理的优化是将前两个步骤结合起来:清理和转换数据,累积可用于计算平均值和标准差的价值。这样做是有帮助的,因为statistics
模块期望的是一个可能不适合内存的对象序列。平均值涉及求和和计数,相对简单。标准差需要累积总和以及平方和。
x的均值,m[x],是x值的总和除以x值的计数,表示为n。
x 的标准差 s[x] 使用 x² 的和、x 的和以及数值的数量,n。
这个标准差的公式存在一些数值稳定性问题,并且有更好的设计变体。参见 en.wikipedia.org/wiki/Algorithms_for_calculating_variance
。
我们将定义一个类来累积均值和方差的值。从这个类中,我们可以计算标准差。
import math
class Variance:
def __init__(self):
self.k: float | None = None
self.e_x = 0.0
self.e_x2 = 0.0
self.n = 0
def add(self, x: float) -> None:
if self.k is None:
self.k = x
self.n += 1
self.e_x += x - self.k
self.e_x2 += (x - self.k) ** 2
@property
def mean(self) -> float:
return self.k + self.e_x / self.n
@property
def variance(self) -> float:
return (self.e_x2 - self.e_x ** 2 / self.n) / (self.n - 1)
@property
def stdev(self) -> float:
return math.sqrt(self.variance)
这个 variance
类执行均值、标准差和方差的增量计算。每个单独的值通过 add()
方法表示。在所有数据都表示之后,可以使用属性来返回汇总统计信息。
如以下代码片段所示:
var_compute = Variance()
for d in data:
var_compute.add(d)
print(f"Mean = {var_compute.mean}")
print(f"Standard Deviation = {var_compute.stdev}")
这提供了一种在不使用大量内存的情况下计算汇总统计的方法。它允许在第一次看到数据时优化计算统计信息。此外,它反映了一个设计良好的算法,该算法在数值上是稳定的。
现在我们已经探讨了技术方法,是时候看看本项目必须创建的可交付成果了。
10.4.3 可交付成果
本项目有以下可交付成果:
-
docs
文件夹中的文档。 -
tests/features
和tests/steps
文件夹中的验收测试。 -
tests
文件夹中的应用模块单元测试。 -
应用以清理获取的数据并计算派生的标准化 Z 分数。
许多这些可交付成果在之前的章节中已有描述。具体来说,第九章,项目 3.1:数据清洗基础应用 讲述了本项目可交付成果的基础。
标准化函数的单元测试
标准化过程有两个部分需要单元测试。第一部分是均值、方差和标准差的增量计算。这必须与 statistics
模块计算的结果进行比较,以确保结果正确。pytest.approx
对象(或 math.isclose()
函数)对于断言增量计算与标准库模块的预期值匹配非常有用。
此外,当然,包括标准化 Z 分数在内的最终样本的构建也需要进行测试。测试用例通常很简单:给定 x、y、x 的均值、y 的均值、x 的标准差和 y 的标准差的单个值需要从不完全形式转换为完整形式。派生值的计算足够简单,以至于可以通过手工计算预期结果来检查结果。
即使这个类看起来非常简单,测试这个类也很重要。经验表明,这些看似简单的类是+
替换-
的地方,而且这种区别并没有被审查代码的人注意到。这种小错误最好通过单元测试来发现。
接受测试
对于这种标准化处理的接受测试套件将涉及一个主程序,该程序创建两个输出文件。这表明在场景之后清理需要确保中间文件被应用程序正确删除。
清理应用程序可以使用tempfile
模块创建一个在关闭时会被删除的文件。这相当可靠,但如果揭示问题的文件被自动删除,调试非常难以捉摸的问题可能会很困难。这不需要任何额外的接受测试步骤来确保文件被删除,因为我们不需要测试tempfile
模块。
清理应用程序还可以在当前工作目录中创建一个临时文件。这可以在正常操作中解除链接,但为了调试目的而保留。这将需要至少两个场景来确保文件被正常删除,并确保文件被保留以支持调试。
最终的实现选择——以及相关的测试场景——留给你决定。
10.5 项目 3.6:集成创建获取管道
在用户体验中,我们探讨了双步用户体验。一个命令用于获取数据。之后,使用第二个命令来清理数据。另一种用户体验是单一的 shell 管道。
10.5.1 描述
本章前面的项目已经将清理操作分解为两个不同的步骤。还有一个非常理想的用户体验替代方案。
具体来说,我们希望以下内容也能正常工作:
% python src/acquire.py -s Series_1Pair --csv source.csv | python src/clean.py -o analysis/Series_1.ndjson
想法是让获取应用程序将一系列 NDJSON 对象写入标准输出。清理应用程序将读取标准输入的 NDJSON 对象序列。这两个应用程序将并发运行,从进程到进程传递数据。
对于非常大的数据集,这可以减少处理时间。由于将 Python 对象序列化为 JSON 文本和从文本反序列化 Python 对象的开销,管道的运行时间不会是两个步骤串行执行时间的一半。
多次提取
在 CSV 提取 Anscombe 四重奏数据的情况下,我们有一个获取应用程序,能够同时创建四个文件。这并不适合 shell 管道。我们有两个架构选择来处理这种情况。
一个选择是实现一个“扇出”操作:acquire 程序将数据扇出到四个单独的清洁应用程序。这很难用 shell 管道集合来表示。为了实现这一点,父应用程序使用 concurrent.futures
、队列和处理池。此外,acquire 程序需要写入共享队列对象,而 clean 程序则从共享队列中读取。
另一个选择是同时只处理 Anscombe 系列中的一个。引入 -s
Series_1Pair
参数允许用户命名一个可以从源数据中提取单个序列的类。同时处理单个序列允许将管道描述为 shell 命令。
这个概念通常对于梳理企业数据是必要的。企业应用程序——通常是有机演化的——作为公共记录的一部分,常常包含来自不同问题域的值。
我们将在下一节转向技术方法。
10.5.2 方法
对于此应用程序的一般方法的参考,请参阅第九章、项目 3.1:数据清理基础应用程序,特别是方法。
写入标准输出(从标准输入读取)表明,这些应用程序将有两种不同的操作模式:
-
打开一个命名的文件进行输出或输入。
-
使用现有的、已打开的、未命名的文件——通常是由 shell 创建的管道——进行输出或输入。
这表明,应用程序设计的大部分需要集中在打开的文件-like 对象上。这些通常由 TextIO
的类型提示描述:它们是可以读取(或写入)文本的文件。
顶层 main()
函数必须设计为打开一个命名的文件,或者提供 sys.stdout
或 sys.stdin
作为参数值。各种文件组合被提供给一个将执行更有用工作的函数。
这种模式看起来像以下片段:
if options.output:
with options.output.open(’w’) as output:
process(options.source, output)
else:
process(options.source, sys.stdout)
process()
函数要么由上下文管理器打开的文件提供,要么函数被提供已经打开的 sys.stdout
。
Python 应用程序能够成为 shell 管道的一部分,这对于创建更大、更复杂的复合过程非常有帮助。这种高级设计努力有时被称为“大型编程”。
能够从管道中读取和写入是 Unix 操作系统的核心设计特性,并且继续是所有各种 GNU/Linux 变体的核心。
这种管道感知设计具有稍微容易进行单元测试的优势。process()
函数的输出参数值可以是一个 io.StringIO
对象。当使用 StringIO
对象时,文件处理完全在内存中模拟,从而实现更快,可能更简单的测试。
这个项目为未来的项目奠定了基础。参见第十二章,项目 3.8:集成数据采集 Web 服务中可以利用此管道的 Web 服务。
考虑创建管道的包。
创建 shell 管道的 Python 应用程序可能需要相当多的编程来创建共享一个公共缓冲区的两个子进程。这由 shell 优雅地处理。
另一个选择是cgarciae.github.io/pypeln/
。PypeLn包是一个将subprocess
模块包装起来的包示例,这使得父应用程序创建一个执行两个子应用程序(acquire和clean)的管道变得更加容易。
使用高级 Python 应用程序启动 acquire-to-clean 管道可以避免 shell 编程的潜在陷阱。它允许具有出色日志记录和调试功能的 Python 程序。
既然我们已经看到了技术方法,现在适当地回顾可交付成果。
10.5.3 可交付成果
本项目有以下可交付成果:
-
docs
文件夹中的文档。 -
tests/features
和tests/steps
文件夹中的验收测试。 -
tests
文件夹中的应用程序模块的单元测试。 -
可以作为两个并发进程的管道处理的修订应用程序。
许多这些可交付成果在之前的章节中已有描述。具体来说,第九章,项目 3.1:数据清洗基础应用程序涵盖了本项目可交付成果的基本内容。
验收测试
验收测试套件需要确认两个应用程序可以作为独立命令使用,以及可以在管道中使用。确认管道行为的一种技术是使用像cat
这样的 shell 程序提供模拟另一个应用程序输入的输入。
例如,When
步骤可能执行以下类型的命令:
cat some_mock_file.ndj | python src/clean.py -o analysis/some_file.ndj
clean应用程序在一个上下文中执行,它是整体管道的一部分。管道的头部不是acquire应用程序;我们使用了cat some_mock_file.ndj
命令作为其他应用程序输出的有用模拟。这种技术允许在多种 shell 上下文中测试应用程序的很大灵活性。
使用管道可以允许一些有用的调试,因为它将两个复杂的程序分解为两个较小的程序。程序可以在隔离的情况下构建、测试和调试。
10.6 摘要
本章在第九章,项目 3.1:数据清洗基础应用程序的项目基础上进行了扩展。以下增加了以下附加处理功能:
-
对基数值的验证和转换的 Python 方法。
-
验证和转换名义值和序数值的方法。
-
揭示关键关系和验证必须正确引用外键的数据的技术。
-
使用 shell 管道的管道架构。
10.7 额外内容
这里有一些想法供您添加到这些项目中。
10.7.1 假设检验
计算均值、方差、标准差和标准化 Z 分数涉及浮点值。在某些情况下,浮点值的普通截断误差可能会引入显著的数值不稳定性。在大多数情况下,选择合适的算法可以确保结果是有用的。
除了基本的算法设计外,有时进行额外的测试也是有益的。对于数值算法,Hypothesis包特别有帮助。请参阅hypothesis.readthedocs.io/en/latest/
。
具体来看项目 3.5:将数据标准化为常见代码和 范围,方法部分建议了一种计算方差的方法。这个类定义是 Hypothesis 模块有效测试设计的绝佳例子,以确认提供三个已知值序列的结果会产生预期的计数、总和、平均值、方差和标准差。
10.7.2 通过过滤拒绝不良数据(而不是记录日志)
在本章的示例中,并未深入讨论如何处理因无法处理而引发异常的数据。有三种常见的选择:
-
允许异常停止处理。
-
遇到每个问题行时都记录下来,并将其从输出中丢弃。
-
将错误数据写入单独的输出文件,以便可以使用数据检查笔记本进行检查。
第一个选项相当激进。这在一些数据清理应用中很有用,在这些应用中,有合理的预期数据非常干净,经过适当整理。在某些企业应用中,这是一个合理的假设,无效数据是导致应用程序崩溃和解决问题的原因。
第二个选项具有简单性的优势。可以使用try:
/except:
块来为错误数据写入日志条目。如果问题数量很少,那么在日志中定位问题并解决它们可能是合适的。
当存在大量可疑或不良数据时,通常会使用第三个选项。这些行会被写入文件以供进一步研究。
鼓励您实现这个第三个策略:为拒绝的样本创建单独的输出文件。这意味着为将至少导致一行错误数据被拒绝的文件创建验收测试。
10.7.3 不相交子实体
当源文档不反映单个结果数据类时,会出现一个更复杂的数据验证问题。这种情况通常发生在非相交子类型合并到一个单一数据集中时。这类数据是不同类型的并集。数据必须涉及一个“判别器”字段,以显示正在描述哪种类型的对象。
例如,我们可能有一些具有日期、时间和文档 ID 的字段,这些字段对所有样本都是通用的。除了这些字段外,一个 document_type
字段提供了一套代码,用于区分不同类型的发票和不同类型的支付。
在这种情况下,转换函数涉及两个转换阶段:
-
识别子类型。这可能涉及转换公共字段和判别器字段。剩余的工作将委托给特定子类型的转换。
-
转换每个子类型。这可能涉及与每个判别器值相关联的一组函数。
这导致了一个如活动图图 10.1所示的功能设计。
图 10.1:子实体验证
10.7.4 创建一个扇出清理管道
对于 获取 和 清理 应用程序的并发处理,有两种常见的替代方案:
-
连接 获取 应用程序和 清理 应用程序的 Shell 管道。这两个子进程并发运行。获取应用程序写入的每个 ND JSON 行立即可供 清理 应用程序处理。
-
由
concurrent.futures
管理的工作者池。获取应用程序创建的每个 ND JSON 行被放入队列中,供某个工作者消费。
Shell 管道在图 10.2中显示。
图 10.2:Shell 管道的组成部分
Shell 创建了两个子进程,它们之间有一个共享的缓冲区。对于获取子进程,共享缓冲区是 sys.stdout
。对于清理子进程,共享缓冲区是 sys.stdin
。随着两个应用程序的运行,每个写入的字节都可以被读取。
我们在这些图中包含了针对 Python 运行的明确引用。这有助于阐明我们的应用程序如何成为整体 Python 环境的一部分。
管道创建是 Shell 的一个优雅特性,可以用来创建复杂并发处理的序列。这是一种将大量转换分解为多个并发转换的有用方法。
在某些情况下,管道模型并不理想。这通常发生在我们需要非对称工作者集合时。例如,当一个进程比另一个进程快得多时,拥有多个慢进程的副本以跟上快进程是有帮助的。这由 concurrent.futures
包礼貌地处理,它允许应用程序创建一个“工作者池”。
池可以是线程或进程,这取决于工作的性质。大部分情况下,进程池更倾向于使用 CPU 核心,因为操作系统调度通常是针对进程的。Python 的全局解释器锁(GIL)通常禁止计算密集型线程池有效地使用 CPU 资源。
对于大型数据集,工作池架构可以提供一些性能改进。序列化和反序列化 Python 对象以在进程间传递值会产生开销。这种开销对多进程的好处施加了一些限制。
实现工作进程池的组件在图 10.3中展示。
图 10.3:工作池的组件
这个设计是对acquire.py
和clean.py
应用程序之间关系的重大修改。当acquire.py
应用程序创建进程池时,它使用同一父进程内可用的类和函数定义。
这表明clean.py
模块需要一个处理恰好一个源文档的函数。此函数可能像以下这样简单:
from multiprocessing import get_logger
import acquire_model
import analysis_model
def clean_sample(
acquired: acquire_model.SeriesSample
) -> analysis_model.SeriesSample:
try:
return analysis_model.SeriesSample.from_acquire_dataclass(acquired)
except ValueError as ex:
logger = get_logger()
logger.error("Bad sample: %r\n%r\n", acquired, ex)
return None
此函数使用分析模型定义SeriesSample
来执行获取数据的验证、清理和转换。这可能会引发异常,这些异常需要被记录。
子进程使用父应用程序的日志配置副本创建。multiprocessing.get_logger()
函数将检索在创建工作进程池时初始化到进程中的记录器。
acquire.py
应用程序可以使用高阶map()
函数将请求分配给执行器池中的工作者。以下是不完整的代码片段展示了通用方法:
with target_path.open(’w’) as target_file:
with concurrent.futures.ProcessPoolExecutor() as executor:
with source_path.open() as source:
acquire_document_iter = get_series(
source, builder
)
clean_samples = executor.map(
clean.clean_sample,
acquire_document_iter
)
count = clean.persist_samples(target_file, clean_samples)
这通过分配一定数量的资源来实现,从要写入的目标文件开始,然后是写入清洁数据记录到文件的进程池,最后是原始、原始数据样本的来源。每个这些都有上下文管理器以确保在所有处理完成后正确释放资源。
我们使用ProcessPoolExecutor
对象作为上下文管理器,以确保当源数据被map()
函数完全消耗,并且从创建的Future
对象中检索所有结果时,子进程得到适当的清理。
get_series()
函数是一个迭代器,它构建每个SeriesSample
对象的获取版本。这将使用适当配置的Extractor
对象来读取源并从中提取序列。
由于生成器是惰性的,直到消耗了 acquire_document_iter
变量的值,实际上并没有发生任何事情。executor.map()
会消耗源数据,将每个文档提供给工作池以创建一个反映由单独子进程执行的工作的 Future
对象。当子进程的工作完成时,Future
对象将包含结果并准备好接受另一个请求。
当 persist_samples()
函数从 clean_samples
迭代器中消耗值时,每个 Future
对象都会产生它们的结果。这些结果对象是由 clean.clean_sample()
函数计算出的值。结果序列被写入目标文件。
concurrent.futures
进程池的 map()
算法将保留原始顺序。进程池提供了其他方法,可以在计算完成后立即使结果就绪。这可能会重新排序结果;这可能是或可能不是后续处理相关的。
第十一章
项目 3.7:临时数据持久化
我们的目标是创建干净、转换后的数据文件,然后我们可以使用这些文件进行进一步的分析。在某种程度上,创建干净数据文件的目标是所有前几章的一部分。我们避免深入查看获取和清洗的中间结果。在这一章中,我们将正式化一些在早期章节中被默默假设的处理过程。在这一章中,我们将更仔细地探讨两个主题:
-
文件格式和数据持久化
-
应用程序的架构
11.1 描述
在前面的章节中,尤其是从第九章、项目 3.1:数据清洗基础应用开始的章节,"持久化"问题被随意处理。前面的章节都将清洗后的样本写入 ND JSON 格式的文件。这避免了深入研究替代方案和各种可用的选择。是时候回顾以前的项目并考虑持久化所选择的文件格式了。
重要的是从获取到分析的数据整体流程。数据的概念流程在图 11.1中展示。
图 11.1:数据分析流程
这与第二章、项目概述中显示的图表不同,那里的阶段定义并不那么明确。一些获取和清洗数据的经验有助于阐明关于保存和使用数据的考虑。
图表显示了持久化临时数据的选择之一。更完整的格式选择列表包括以下内容:
-
CSV
-
TOML
-
JSON
-
Pickle
-
SQL 数据库
-
YAML
有其他格式,但这个列表包含了在 Python 中有直接实现的格式。请注意,YAML 很受欢迎,但不是 Python 标准库的内置功能。其他格式包括协议缓冲区(protobuf.dev
)和 Parquet(parquet.apache.org
)。这两种格式在序列化和反序列化 Python 数据之前需要更多的工作来定义结构;我们将它们排除在这个讨论之外。
CSV 格式有两个缺点。其中最明显的问题是所有数据类型都以简单字符串的形式表示。这意味着任何类型的转换信息都必须在 CSV 文件之外的元数据中提供。Pydantic 包通过类定义的形式提供所需的元数据,使得这种格式可以容忍。次要问题是数据缺乏更深层次的结构。这迫使文件具有扁平的原始属性序列。
JSON 格式并不直接序列化 datetime 或 timedelta 对象。为了使这一过程可靠,需要额外的元数据来从支持的 JSON 值(如文本或数字)反序列化这些类型。这个缺失的功能由Pydantic包提供,并且工作得非常优雅。datetime.datetime
对象将序列化为字符串,并且类定义中的类型信息被用来正确解析这个字符串。同样,datetime.timedelta
被序列化为浮点数,但根据类定义中的类型信息正确地转换为datetime.timedelta
。
TOML 格式相对于 JSON 格式有一个优势。具体来说,TOML 格式有一个整洁的方式来序列化 datetime 对象,这是 JSON 库所缺乏的。然而,TOML 格式的缺点是它没有提供将多个 TOML 文档直接放入单个文件的方法。这限制了 TOML 处理大量数据集的能力。使用包含简单值数组的 TOML 文件将限制应用程序处理的数据量,只能处理适合内存的数据量。
可以使用Pydantic包与 pickle 格式一起使用。这个格式具有保留所有 Python 类型信息的优势,并且也非常紧凑。与 JSON、CSV 或 TOML 不同,它不是人类友好的,并且难以阅读。shelve
模块允许构建一个方便的数据库文件,其中包含多个 pickle 对象,可以保存和重复使用。虽然从 pickle 文件中读取时技术上可能执行任意代码,但获取和清理应用程序的流程不涉及任何未知机构提供来源不明的数据。
Pydantic包也支持 SQL 数据库,通过使用 ORM 模型来实现。这意味着需要并行定义两个模型。一个模型用于 ORM 层(例如,SQLAlchemy)来创建表定义。另一个模型,是pydantic.BaseModel
的子类,使用原生的Pydantic特性。Pydantic类将有一个from_orm()
方法,用于从 ORM 层创建原生对象,执行验证和清理。
YAML 格式提供了序列化任意 Python 对象的能力,这一特性使得持久化原生 Python 对象变得容易。同时,这也引发了一些安全问题。如果小心避免处理来自不安全来源的上传 YAML 文件,那么序列化任意 Python 代码的能力就不再是潜在的安全问题。
在这些文件格式中,似乎通过 JSON 可以提供最丰富的功能集。由于我们经常希望在一个文件中记录许多单独的样本,因此换行符分隔(ND)JSON 似乎是最理想的。
在某些情况下——尤其是当用于分析目的时——CSV 格式提供了一些价值。从复杂的 Jupyter Notebook 转移到电子表格的想法并不是我们支持的。电子表格缺乏自动测试功能表明它们不适合自动化数据处理。
11.2 总体方法
参考以下内容:第九章、项目 3.1:数据清洗基础应用程序,特别是 方法。这表明 clean
模块应该与早期版本保持最小变化。
清洗应用程序将具有对数据的几个不同视图。至少有四个观点:
-
源数据。这是上游应用程序管理的原始数据。在企业环境中,这可能是一个包含宝贵业务记录的事务型数据库,这些记录是日常运营的一部分。数据模型反映了这些日常运营的考虑。
-
数据获取中间数据,通常以文本为中心的格式。我们建议使用 ND JSON,因为它允许整洁的字典样式的键值对集合,并支持相当复杂的 Python 数据结构。在某些情况下,我们可能对原始数据进行一些汇总以标准化分数。这些数据可能用于诊断和调试上游源的问题。也有可能这些数据仅存在于获取和清洗应用程序之间的管道中的共享缓冲区中。
-
清洗后的分析数据,使用包括
datetime
、timedelta
、int
、float
和boolean
在内的原生 Python 数据类型。这些数据类型通过 Pydantic 类定义进行补充,这些定义作为值的正确解释的元数据。这些数据将被用于支持决策,也可能用于训练用于自动化某些决策的 AI 模型。 -
决策者对可用信息的理解。当尝试收集、组织和展示数据时,这种观点往往在用户讨论中占主导地位。在许多情况下,随着数据的展示,用户的理解会迅速增长和适应,导致需求格局的变化。这需要极大的灵活性,以便在正确的时间向正确的人提供正确的数据。
acquire 应用程序与这些模型中的两个重叠:它消耗源数据并生成中间表示。clean 应用程序也与这些模型中的两个重叠:它消耗中间表示并生成分析模型对象。区分这些模型并使用它们之间显式、正式的映射是至关重要的。
这种对清晰分离和明显映射的需求是我们建议在模型类中包含一个“构建器”方法的主要原因。我们通常将其称为 from_row()
或 from_dict()
或其他暗示模型实例是通过显式分配单个属性从其他数据源构建的方法。
从概念上讲,每个模型都有一个类似于以下片段中所示的模式:
class Example:
field_1: SomeElementType
field_2: AnotherType
@classmethod
def from_source(cls, source: SomeRowType) -> "Example":
return Example(
field_1=transform_1(source),
field_2=transform_2(source),
)
在使用 pydantic.BaseModel
时,转换函数 transform1()
和 transform2()
通常是不显式的。这是对这个设计模式的一种有帮助的简化。然而,基本思想并没有改变,因为我们经常重新排列、组合和拆分源字段以创建有用的数据。
当最终输出格式是 CSV 或 JSON 时,pydantic.BaseModel
有两个有用的方法。这些方法是 dict()
和 json()
。dict()
方法创建一个原生的 Python 字典,可以被 csv.DictWriter
实例用来写入 CSV 输出。json()
方法可以直接用来写入 ND JSON 格式的数据。对于 ND JSON 来说,确保 json.dump()
函数使用的 indent
值是 None
是至关重要的。indent
参数的任何其他值都会创建多行 JSON 对象,破坏 ND JSON 文件格式。
acquire 应用经常需要应对不可靠数据源的复杂性。应用应该保存每次尝试获取数据的历史记录,并且只获取“缺失”的数据,避免重新读取良好数据带来的开销。如果没有简单的方法来请求数据子集,这可能会变得复杂。
当与 API 一起工作时,例如,有一个 Last-Modified
标头可以帮助识别新数据。请求上的 If-Modified-Since
标头可以避免读取未更改的数据。同样,Range
标头可能由 API 支持,允许在连接断开后检索文档的部分。
当与 SQL 数据库一起工作时,一些 SELECT
语句的变体允许使用 LIMIT
和 OFFSET
子句来检索数据的不同页面。跟踪数据页面可以简化重启长时间运行的查询。
同样,clean 应用需要避免在它没有完成并且需要重新启动的极不可能事件中重新处理数据。对于非常大的数据集,这可能意味着扫描之前的不完整输出,以确定从哪里开始清理原始数据,从而避免重新处理行。
我们可以将这些操作视为在它们完全且正确运行的情况下是“幂等的”。我们希望能够在不损坏中间结果文件的情况下运行(并重新运行)“获取”应用程序。我们还想添加一个额外的功能,即在文件正确且完整之前继续添加到文件中。(这并不是“幂等”的精确定义;我们应该限制这个术语,以说明正确的完整文件不会被重新运行应用程序所损坏。)同样,设计“清理”应用程序时,应该使其能够运行——并且可以重新运行——直到所有问题都得到解决,而不会覆盖或重新处理有用的结果。
11.2.1 设计幂等操作
理想情况下,我们的应用程序提供的用户体验可以概括为“从上次离开的地方继续”。应用程序将检查输出文件,并避免破坏之前获取或清理的数据。
对于许多精心策划的 Kaggle 数据集,源数据将不会发生变化。可以通过检查 Kaggle API 中的元数据来避免耗时的下载,以确定之前下载的文件是否完整且仍然有效。
对于处于不断变化状态的企业数据,处理必须提供一个明确的“截至日期”或“操作日期”,作为运行时参数提供。使这个日期(或日期和时间)显而易见的一种常见方法是将其作为文件元数据的一部分。最明显的地方是文件名。我们可能有一个名为2023-12-31-manufacturing-orders.ndj
的文件,其中截至日期显然是文件名的一部分。
幂等性要求数据获取和清理管道中的程序检查现有输出文件,并避免在未明确命令行选项允许覆盖的情况下覆盖它们。它还要求应用程序读取输出文件以找出它包含多少行。可以使用现有行数来调整处理,以避免重新处理现有行。
考虑一个从数据库读取以获取原始数据的程序。例如,“截至日期”是 2022-01-18。当应用程序运行且网络出现问题时,数据库连接可能在处理了一部分行之后丢失。我们将想象在网络故障导致应用程序崩溃之前,输出文件已经写入了 42 行。
当检查日志并清楚应用程序失败时,它可以重新运行。程序可以检查输出目录并找到包含 42 行的文件,这意味着应用程序正在以恢复模式运行。应该有两个重要的行为变化:
-
在
SELECT
语句中添加LIMIT -1 OFFSET 42
子句以跳过已检索的 42 行。(对于许多数据库,LIMIT -1 OFFSET 0
将检索所有行;这可以用作默认值。) -
以“追加”模式打开输出文件,以将新记录添加到现有文件的末尾。
这两个更改允许应用程序根据需要重新启动多次以查询所有所需数据。
对于其他数据源,查询中可能没有简单的“limit-offset”参数。这可能导致一个读取并忽略一定数量的记录然后处理剩余记录的应用程序。当输出文件不存在时,处理前的偏移量值为零。
正确处理日期时间范围非常重要。
确保日期和日期时间范围是正确的半开区间至关重要。起始日期和时间包含在内。结束日期和时间不包含。
考虑每周的数据提取。
一个范围是 2023-01-14 到 2023-01-21。14 日包含在内。21 日不包含。下一周,范围是 2023-01-21 到 2023-01-28。21 日包含在本提取中。
使用半开区间可以更容易确保没有日期被意外遗漏或重复。
现在我们已经考虑了编写临时数据的方案,我们可以看看这个项目的可交付成果。
11.3 可交付成果
将现有应用程序重构为正式化临时文件格式会导致现有项目发生变化。这些变化将波及到单元测试的更改。在重构数据模型模块时,不应有任何接收测试的更改。
另一方面,添加“从上次离开的地方继续”功能将导致应用程序行为的变化。这将在接收测试套件以及单元测试中得到反映。
可交付成果取决于你已完成的项目以及哪些模块需要修订。我们将探讨这些可交付成果的一些考虑因素。
11.3.1 单元测试
创建输出文件的功能需要具有两个不同的测试用例。一个测试用例将包含输出文件的版本,另一个测试用例将不包含输出文件。这些测试用例可以建立在pytest.tmp_path
测试用例之上。该测试用例提供了一个唯一的临时目录,可以填充所需文件以确认现有文件被追加而不是覆盖。
一些测试用例需要确认现有文件已被正确扩展。其他测试用例将确认当文件不存在时,文件被正确创建。一个边缘情况是长度为零的文件的存在——它被创建,但没有写入数据。在没有以前数据可读取以发现以前状态的情况下,这可能具有挑战性。
另一个边缘情况是文件末尾存在损坏的、不完整的行数据。这需要巧妙地使用打开文件的seek()
和tell()
方法来选择性地覆盖文件的不完整最后记录。一种方法是读取每个样本之前使用tell()
方法。如果文件解析器引发异常,则跳转到最后报告的tell()
位置,并从那里开始写入。
11.3.2 接收测试
验收测试场景需要不可靠的数据源。回顾第四章,数据采集功能:Web API 和抓取,特别是验收测试,我们可以看到验收测试套件涉及使用bottle
项目创建一个非常小的网络服务。
场景有两个方面,每个方面都有不同的结果。两个方面是:
-
服务或数据库提供所有结果,或者未能提供完整的结果集。
-
工作文件不存在——我们可以称之为“干净启动”模式——或者存在部分文件,并且应用程序正在恢复模式下工作。
由于每个方面有两个替代方案,因此该功能有四种场景组合:
-
现有的场景是工作目录为空,而 API 或数据库工作正常。所有行都得到适当保存。
-
一个新的场景,其中工作目录为空,而服务或数据库返回部分结果。返回的行被保存,但结果被标记为不完整,可能在日志中有一个错误条目。
-
一个新的场景,其中给定的工作目录有部分结果,而 API 或数据库工作正常。新行被追加到现有行中,从而得到完整的结果。
-
一个新的场景,其中给定的工作目录有部分结果,而服务或数据库返回部分结果。累积收集的行是可用的,但结果仍然被标记为不完整。
模拟 RESTful 过程的某个版本可以返回一些行,甚至在之后返回 502 状态码。不完整结果场景的数据库版本具有挑战性,因为 SQLite 在运行时很难崩溃。与其尝试创建一个超时或崩溃的 SQLite 版本,不如依靠带有模拟数据库的单元测试来确保崩溃得到适当处理。四个验收测试场景将证明工作文件被扩展而没有被覆盖。
11.3.3 清理后的可重运行应用程序设计
具有“从上次离开的地方继续”功能的最终应用程序可以非常方便地创建鲁棒、可靠的分析工具。关于“我们如何恢复?”的问题应该涉及很少(或没有)思考。
通常,创建“幂等”应用程序允许鲁棒和可靠的处理。当应用程序不工作时,必须找到并修复根本原因,然后可以再次运行应用程序以完成失败的尝试中未完成的工作。这使得分析师能够专注于出了什么问题——并修复它——而不是必须弄清楚如何完成处理。
11.4 概述
在本章中,我们探讨了数据采集管道的两个重要部分:
-
文件格式和数据持久性
-
应用程序的架构
对于 Python 数据,有许多可用的文件格式。看起来换行分隔(ND)JSON 可能是处理复杂记录的大型文件的最佳方式。它与 Pydantic 的功能很好地配合,并且数据可以很容易地由 Jupyter Notebook 应用程序处理。
在处理大型数据提取和缓慢处理时,能够重试失败的操作而不丢失现有数据可能很有帮助。能够在不等待先前处理的数据再次处理的情况下重新运行数据获取可能非常有帮助。
11.5 额外内容
这里有一些想法供您添加到这些项目中。
11.5.1 使用 SQL 数据库
使用 SQL 数据库来存储清洗后的分析数据可以是综合数据库中心数据仓库的一部分。当基于 Pydantic 实现时,需要本地的 Python 类以及映射到数据库的 ORM 类。
它还要求在处理企业数据时对重复查询进行一些小心处理。在普通文件系统中,文件名可以有处理日期。在数据库中,这通常分配给数据的属性。这意味着多个时间段的数据占用单个表,通过行的“as-of”日期来区分。
常见的数据库优化是提供一个“时间维度”表。对于每个日期,提供相关的星期日期、财政周、月份、季度和年份作为属性。使用这个表可以节省计算任何日期属性。它还允许使用企业财政日历来确保正确使用 13 周的季度,而不是相当任意的日历月份边界。
这种额外的处理不是必需的,但在考虑使用关系数据库进行分析数据时必须考虑。
这个额外项目可以使用 SQLAlchemy 为 SQLite 数据库定义一个 ORM 层。ORM 层可以用来创建表并将分析数据的行写入这些表。这允许使用 SQL 查询来检查分析数据,并且可能使用复杂的 SELECT-GROUP
查询来执行一些分析处理。
11.5.2 使用 NoSQL 数据库的持久性
可用的 NoSQL 数据库有很多。像 MongoDB 这样的产品使用基于 JSON 的文档存储。像 PostgreSQL 和 SQLite3 这样的数据库引擎具有在数据库表的列中存储 JSON 文本的能力。我们将将我们的重点缩小到基于 JSON 的数据库,以避免查看大量可用的数据库。
我们可以使用 SQLite3 BLOB 列来存储 JSON 文本,使用 SQLite3 存储引擎创建一个类似 NoSQL 的数据库。
一个包含两列的小表:doc_id
和 doc_text
,可以创建一个类似 NoSQL 的数据库。SQL 定义看起来像这样:
CREATE TABLE IF NOT EXISTS document(
doc_id INTEGER PRIMARY KEY,
doc_text BLOB
)
这个表将有一个自动填充整数值的主键列。它有一个可以存储 JSON 文档序列化文本的文本字段。
在插入 JSON 文档时应该使用 SQLite3 的json()
函数:
INSERT INTO document(doc_text) VALUES(json(:json_text))
这将确认提供的json_text
值是有效的 JSON,并且还会最小化存储,移除不必要的空白。这个语句通常与参数{"json_text":`` json.dumps(document)
一起执行,以便将原生 Python 文档转换为 JSON 文本,然后可以将其持久化到数据库中。
可以使用 SQLite 的->>
运算符来查询 JSON 对象的属性,从而从 JSON 文档中提取字段。对于具有特定值的命名字段的文档的查询将如下所示:
SELECT doc_text FROM document WHERE doc_text ->> ’field’ = :value
在上述 SQL 中,字段名field
作为 SQL 的一部分是固定的。这可以在设计模式以支持少量查询时完成。在更一般的情况下,字段名可能作为参数值提供,导致如下查询:
SELECT doc_text FROM document WHERE doc_text ->> :name = :value
这个查询需要一个包含“name”和“value”键的小字典,这将提供用于定位匹配文档的字段名和字段值。
这种数据库设计让我们能够编写类似于文档存储库的一些功能的过程,而不需要安装文档存储数据库的开销。JSON 文档可以插入到这个文档存储中。查询语法使用了一些 SQL 关键字作为开销,但大部分的处理可以通过基于 JSON 的文档查询来定位所需的可用文档子集。
这里的想法是使用基于 JSON 的文档存储而不是 ND JSON 格式的文件。SQLite3 的文档存储接口应该是一个模块,可以在 JupyterLab 笔记本中重复使用以获取和分析数据。虽然数据库接口需要单元测试,但还需要对验收测试套件进行一些更改以确认这种设计变更。
第十二章
项目 3.8:集成数据获取 Web 服务
在许多企业应用程序中,数据提供给多个消费者。一种方法是为后续使用定义一个 API,该 API 提供数据(以及元数据)。在本章中,我们将指导您将项目 2.5 的模式信息转换为更大的 OpenAPI 规范。我们还将构建一个小型的 Flask 应用程序,将其核心获取-清理-转换过程作为 Web 服务提供。
本章我们将介绍多个技能:
-
为获取和下载数据的服务创建 OpenAPI 规范
-
编写一个实现 OpenAPI 规范的 Web 服务应用程序
-
使用处理池来委托长时间运行的后台任务
这与获取和清理数据的直接路径略有偏差。在某些企业中,这种偏差是必要的,以便将有用的数据发布给更广泛的受众。
我们将从描述这个 RESTful API 服务器的行为开始。
12.1 描述
在第八章,项目 2.5:模式与元数据中,我们使用了Pydantic来生成分析数据模型的模式。这个模式提供了一个正式的、与语言无关的数据定义。然后可以广泛共享来描述数据,解决有关数据、处理来源、编码值的含义、内部关系和其他主题的问题或歧义。
这个模式规范可以扩展为创建一个完整的 RESTful API 规范,该 API 提供符合模式的数据。此 API 的目的是允许多个用户通过requests
模块查询 API 以获取分析数据以及分析结果。这可以帮助用户避免处理过时数据。组织创建大型 JupyterLab 服务器,以便在比普通笔记本电脑大得多的机器上进行分析处理。
此外,API 为整个获取和清理过程提供了一个方便的包装器。当用户第一次请求数据时,可以启动处理步骤并将结果缓存。后续的请求可以从文件系统缓存中下载可用数据,提供快速访问。在出现故障的情况下,日志可以作为最终数据的替代提供。
我们不会深入探讨 REST 设计概念。有关 RESTful 设计的更多信息,请参阅hub.packtpub.com/creating-restful-api/
。
通常,RESTful API 定义了一系列资源路径。给定的路径可以通过多种方法访问,其中一些方法将获取资源。其他方法可能用于发布、修补、更新或删除资源。定义的 HTTP 方法提供了方便的映射,以对应于常见的创建-检索-更新-删除(CRUD)概念操作。
这里是一些常见的情况:
-
没有最终标识符的路径,例如,
/series/
。这里有两种常见情况:-
GET
方法将检索给定类型的可用资源列表。 -
POST
方法可以用来创建该类型的新实例。这是概念上的“创建”操作。
-
-
带有标识符的路径。例如,
/series/Series_4
。这是一个特定的资源。可能实现的方法有几种:-
GET
方法将检索资源。这是“检索”概念操作。 -
PUT
和PATCH
方法可以用来替换或更新资源。这是概念上的“更新”操作的两种形式。 -
DELETE
方法可以用来删除资源。这是“删除”概念操作。
-
将 RESTful 网络服务视为资源集合变得至关重要。谈论资源可能会使谈论启动处理的 RESTful 请求变得困难。它提出了一个问题:哪个资源描述了处理样本等活动。我们将首先考虑数据序列作为此服务提供的重要资源。
12.1.1 数据序列资源
此 API 的主要资源是数据序列。如前所述,OpenAPI 3 规范,可以使用 /2023.02/series/<id>
路径来提取命名序列的数据。2023.02 前缀允许 API 向新版本进化,同时为兼容性目的保留旧路径。
语义版本控制(semver)的使用很常见,许多 API 的路径中都包含类似“v1”的内容。另一种选择是将版本信息包含在 Accept
头部中。这意味着 URI 从不改变,但响应的架构可以根据头部中提供的版本信息进行更改。
各种“系列”路由提供了直接访问数据资源的方式。这似乎是合适的,因为这是服务的主要目的。
还有一个可能引起兴趣的资源类别:用于创建数据的后台处理。如上所述,第十一章、项目 3.7:临时数据持久化 是此 RESTful API 执行处理的基本基础。获取和清理应用程序可以在后台运行以创建下载数据。
资源的关注对于创建有用的 RESTful API 至关重要。
即使在描述处理或状态变化时,焦点也必须放在经历状态变化的那一资源上。
HTTP 中可用的方法(例如 GET
、POST
、PUT
、PATCH
和 DELETE
)实际上是 API 语言的动词。资源是名词。
12.1.2 创建下载数据
RESTful API 的主要目的是存储和下载用于分析工作的干净数据。这可能是一个相对简单的应用程序,提供来自知名目录的数据文件。这项工作包括将 RESTful 请求与可用文件匹配,并在请求不存在文件时返回适当的状态码。
第二个目的是自动化数据的创建以供下载。RESTful API 可以是完整获取、清理和持久化管道的包装器。为此,API 将有两种不同的请求类型:
-
下载现有、缓存数据的请求。这里的资源类型是明确的。
-
请求启动新数据的创建;这将导致可用于下载的缓存数据。处理资源类型并不明确。
一个操作或动作确实有一些静态资源,可以使用 RESTful API 来使用。以下是两种常见的活动资源类型:
-
一个“当前状态”资源,反映正在进行的工作
-
一个“处理历史”资源,反映已完成的工作:这通常是获取处理的日志文件
通过创建和检查作为独立资源类型的处理状态或历史,RESTful API 可以控制处理:
-
使用 POST 请求的路径将启动一个异步的后台进程。这也会创建一个新的处理历史资源。响应体提供了一个事务标识符,指向这个新的处理历史。
-
使用事务标识符和 GET 请求的路径将返回后台处理详情;这应包括当前或最终状态以及日志。
对于复杂的客户端处理,可以创建一个 WebSocket 来接收来自后台进程的持续状态报告。对于不太复杂的客户端,可以每几秒钟轮询一次,以查看处理是否完成以及数据是否可供下载。
在处理历史资源和数据资源的情况下,需要以下两组路径:
-
/series/<id>
路径指向特定的系列,这些系列已在缓存中可用。这些资源仅通过 GET 方法访问以下载数据。 -
/creation/<id>
路径指向创建新数据系列的背景处理作业。这些资源将使用 POST 方法启动后台作业,并使用 GET 方法检查作业状态。
这组路径(和相关方法)允许用户控制处理并检查处理结果。用户可以请求可用的数据集并下载特定数据集进行分析。
12.2 总体方法
在审视我们的方法时,我们将借鉴 C4 模型(c4model.com
)的一些指导。
-
上下文 对于这个项目,上下文图有几个用例:列出可用数据、下载可用数据、启动获取数据的过程以及检查获取数据的过程的状态。
-
容器 理想情况下,这在一个容器上运行,该容器托管网络服务以及处理。在某些情况下,需要多个容器,因为处理需求如此巨大。
-
组件 有两组显著不同的软件组件集合:网络服务和在后台运行以获取和清理数据的应用程序程序。
-
代码 获取和清理应用程序已经被描述为独立的项目。我们将重点关注网络服务。
我们将把网络服务应用程序分解成几个组件。以下图显示了 RESTful API 服务和运行以获取和清理数据的应用程序之间的关系。
组件图显示在图 12.1中。
图 12.1:应用程序组件
此图显示了三个独立的过程:
-
处理来自客户端的 HTTP 请求的RESTful API过程。
-
由
concurrent.futures
模块管理的工作池集合。每个工作进程将运行一个单独的函数,如acquire_series
,该函数定义在RESTful API服务所在的同一模块中。 -
由工作池中的工作进程执行后台过程。这使用
subprocess
模块运行现有的 CLI 应用程序。
当 API 服务启动时,它使用concurrent.futures
创建一个工作进程池。一个获取和清理数据的请求将使用池的submit()
方法创建一个未来。这个未来是对一个子进程的引用,该子进程最终将返回获取和清理作业的最终状态。实现未来的子进程将评估与 RESTful API 应用程序位于同一模块中的acquire_series()
函数来完成工作。
当acquire_series()
函数完成处理时,它将创建一个可以下载的文件。通过未来对象,它还将提供一些状态信息给 RESTful API 服务,以指示处理已完成。
acquire_series()
函数的一个建议实现是使用subprocess.run()
来执行获取和清理应用程序以收集和净化源数据。还有一些其他的选择可用。最重要的替代方案是导入这两个其他模块,并直接执行它们,而不是创建一个子进程。这种直接执行的优势是比创建子进程稍微快一些。它的缺点是每次执行获取和清理应用程序时,创建单独的日志文件会更复杂。
我们首先将查看 RESTful API 的 OpenAPI 规范。这有助于描述整体 UX。
12.2.1 OpenAPI 3 规范
RESTful API 需要明确描述请求和响应。OpenAPI 规范是 RESTful 网络服务的正式定义。请参阅www.openapis.org
。这份文档有一个版本标识符以及关于整个服务的一些信息。对于这个项目来说,最重要的部分是paths部分,它列出了各种资源类型以及定位这些资源的路径。components部分提供了所需的模式定义。
OpenAPI 文档通常具有如下大纲:
{
"openapi": "3.0.3",
"info": {
"title": "The name of this service",
"description": "Some details.",
"version": "2023.02"
}
"paths": {
"..."
}
"components": {
"parameters": {"..."},
"schemas": {"..."}
}
}
路径和组件的详细信息已从概述中省略。(我们用"..."
代替了详细信息。)目的是展示 OpenAPI 规范的一般结构。虽然 JSON 是这些规范常用的底层格式,但它可能难以阅读。因此,通常使用 YAML 符号表示 OpenAPI 规范。
将 OpenAPI 规范视为一个约束性合同。
接受测试套件应该是与 OpenAPI 规范有非常直接映射的 Gherkin 场景。
更多关于 OpenAPI 到 Gherkin 的想法,请参阅medium.com/capital-one-tech/spec-to-gherkin-to-code-902e346bb9aa
。
OpenAPI 路径定义了 RESTful API 提供的资源。在这种情况下,资源是经过清理的文件,准备进行分析。
我们经常在paths部分看到类似以下 YAML 片段的条目:
"/2023.02/series":
get:
responses:
"200":
description: All of the available data series.
content:
application/json:
schema:
$ref: "#/components/schemas/series_list"
这显示了从 API 版本号(在这个例子中,使用的是日历版本号,“calver”)和资源类型series
开始的路径。任何给定的路径都可以通过多种方法访问;在这个例子中,只定义了get方法。
为这个路径和方法的组合请求定义了一种响应类型。响应将具有状态码 200,表示正常、成功的完成。描述用于解释这个资源是什么。响应可以定义多种内容类型;在这个例子中,只定义了application/json
。这个模式在 OpenAPI 规范的其它地方提供,在文档的components/schemas
部分。
在规范中使用$ref
标签允许将常见的定义,如模式参数,收集在components
部分下,允许重用。这遵循了软件设计中的DRY(不要重复自己)原则。
在 OpenAPI 规范中正确获取语法可能很困难。拥有一个验证规范的编辑器非常有帮助。例如,editor.swagger.io
提供了一个编辑器,可以帮助确认规范在内部是一致的。对于使用 JetBrains 的 PyCharm 等工具的读者,有一个插件编辑器:plugins.jetbrains.com/plugin/14837-openapi-swagger-editor
。
当路径中包含标识符时,则显示为形式为 "/2023.02/series/<series_id>"
的路径名称。<series_id>
在此请求的 parameters
部分中定义。由于参数有时会被重复使用,因此有一个对具有共同定义的组件的引用是有帮助的。
整个请求可能开始如下:
"/2023.02/series/<series_id>":
get:
description:
Get series data as text ND JSON.
parameters:
- $ref:
"#/components/parameters/series_id"
responses:
...
此示例中省略了 响应 部分的详细信息。参数定义——在 components
部分中——可能看起来像这样:
series_id:
name: series_id
in: path
required: true
description: Series name.
schema:
type: string
这提供了关于 series_id
参数的大量细节,包括描述和正式的架构定义。对于简单的 API,参数的名称和 components
下的参考标签通常相同。在更复杂的情况下,参数名称可能被重复使用,但在不同的上下文中具有不同的语义。一个通用的词如 id
可能在几个不同的路径中使用,导致参考标签比 id
更具有描述性。
ND JSON 的内容被视为标准 MIME 类型的扩展。因此,包含数据的响应的内容定义可能看起来像这样:
content:
application/x-ndjson:
schema:
$ref: "#/components/schemas/samples"
架构是一个挑战,因为它推动了 JSON Schema 可以描述的边界。它看起来如下:
samples:
description: >
Acquired data for a series in ND JSON format.
See http://ndjson.org and https://jsonlines.org.
type: string
format: "(\\{.*?\\}\\n)+"
格式信息描述了 ND JSON 数据的物理组织结构,但并未提供有关每个单独行架构结构的任何细节。额外的架构细节可以添加到描述中,或者使用与其他 JSON 架构标签不同的单独标签,例如,“ndjson-schema:”。
12.2.2 从笔记本查询的 RESTful API
RESTful API 服务必须是围绕应用程序编程的包装器,能够执行所需的处理。理念是将尽可能少的处理放入 RESTful API。它作为非常薄——几乎是透明——的应用程序“真实工作”的接口。因此,如第十一章**、项目 3.7:临时数据持久性**等项目是这个 RESTful API 的基本基础。
如图 12.1 所示,后台处理完全在 RESTful API 之外。这种关注点的分离绝对必要。可以使用 CLI 或通过 RESTful API 执行样本的一般处理,并创建相同的结果。
如果 RESTful 服务执行了额外的处理——例如,额外的清理——那么就有一些结果无法从 CLI 中重现。这意味着验收测试套件会有不同的结果。当对底层获取或清理应用程序进行更改,并且之前被强行塞入 RESTful 服务的“额外”处理现在看起来是损坏的,这将会导致问题。
企业软件中常见的问题之一是未能遵守接口分离设计原则。一个复杂的应用程序可能由几个协作的组织支持。当一个组织对变更请求响应缓慢时,另一个组织可能会介入并做出错误的设计决策,在 API 接口中实现本应属于具有适当 CLI 接口的背景模块的处理。对客户响应的迫切需求往往掩盖了关注点分离的重要性。
对于这个项目,服务器可以构建为一个单一进程,避免需要分布式缓存。此外,由于数据序列和处理日志都是简单的文件,因此不需要数据库;本地文件系统非常适合这个服务。
为了创建一个更可扩展的解决方案,可以使用如celery这样的库来创建一个更健壮的分布式工作池。然而,对于小型服务器来说,这并不是必需的。
在下一节中,我们将回顾如何通过 RESTful API 启动处理。
12.2.3 POST 请求开始处理
创建新资源的一般方法是向路径发送一个 POST
请求。这将返回一个 400 错误状态或发出一个重定向(301)到新的路径以检索后台处理的状态。这种模式称为后重定向获取设计模式。它允许用户通过浏览器与交互时使用后退按钮再次执行 GET
方法;它防止后退按钮提交重复请求。
对于通过 requests
发起请求的客户端应用程序,重定向基本上是不可见的。请求历史将揭示重定向。此外,响应中记录的完整 URL 将反映重定向。
因此,这个路由的一般处理过程如下:
-
验证所有参数以确保它们描述了数据序列和数据来源。如果有任何问题,必须返回一个包含问题详细信息的 JSON 响应,状态码为 400,以指示请求无效且必须更改。
-
使用工作池的
submit()
方法创建一个Future
对象。这个对象可以通过 RESTful API 保存到本地缓存中。这个Future
对象的缓存可以查询以查看当前正在进行的后台处理。未来的结果通常表明成功或失败;例如,子进程的返回码——通常为零表示成功。 -
使用 Bottle 框架中的
redirect()
函数返回状态码,以将客户端重定向到另一个 URL 以获取刚刚创建的Future
对象的状态。单独,GET 请求将准备一个包含创建数据作业状态的 JSON 文档。
当使用 Bottle 等框架时,此函数通过@post("/2023.02/creation")
装饰器进行标记。这命名了 POST 方法和将由函数处理的路径。
处理的日志文件可以是处理历史的长期存储库。状态请求的 GET 请求将返回日志以及可能的活动Future
对象的状态。我们将在下一节中查看此请求。
12.2.4 对处理状态进行 GET 请求
初始的 POST 请求以开始处理,将重定向到 GET 请求,以揭示处理状态。初始响应可能除了处理作业已开始之外几乎没有其他细节。
此状态路径应返回以下两种事物之一:
-
如果进程 ID 未知,则返回 404 状态。这意味着没有使用此标识符发出之前的请求,也没有当前请求具有此标识符。
-
带有 JSON 内容的 200 状态,包括以下两种事物的组合:未来对象的状态和日志文件。
大多数用户只关心Future
对象的状态。然而,对于开发人员来说,他们正在向数据获取或数据清理应用程序添加功能,那么日志可能是观察性的重要支持。
当使用 Bottle 等框架时,此函数通过@get("/2023.02/creation/<job_id>")
装饰器进行标记。这提供了将由函数处理的方法和路径。使用<job_id>
解析路径的这一部分,并将值作为单独的参数提供给实现此路由的函数。
一旦处理完成,后续请求可以提供数据。我们将在下一节中查看这一点。
12.2.5 对结果进行 GET 请求
此路径应返回以下两种事物之一:
-
如果系列标识符未知,则返回 404 状态。
-
带有 ND JSON 内容的 200 状态。这具有
application/x-ndjson
的 MIME 类型,以表明它是标准 MIME 类型集合的扩展。
当使用 Bottle 等框架时,此函数通过@get("/2023.02/series/<series_id>")
装饰器进行标记。使用<series_id>
解析路径的这一部分,并将值作为单独的参数提供给实现此路由的函数。
更复杂的实现可以检查请求中的Accept
头。此头将声明首选的 MIME 类型,可能使用text/csv
而不是application/x-ndjson
。使用此头允许客户端以应用程序认为最有用的格式请求数据。
12.2.6 安全性考虑
一个 RESTful API 需要一些注意,以确保请求与整体企业信息访问策略相匹配。在某些情况下,这可能意味着个人访问控制,以确保每个人都可以访问允许的数据。有许多单点登录(SSO)产品可以处理个人的身份。
另一种常见的方法是让 API 与分配的 API 密钥一起工作。支持 API 的团队可以为已知用户或团队提供唯一的 API 密钥值。在大多数企业中,对于面向内部 API 的 API 密钥的自动化分配几乎没有需求。有效的 API 密钥集可能需要减少或增加,以反映组织合并和分裂。
API 密钥值是从客户端发送到服务器,以验证发起请求的用户。它们永远不会从服务器发送到客户端。API 密钥可以保存在一个简单的文本文件中;文件权限应限制为只读访问,由处理整个服务的账户进行管理。这要求管理员采取措施来管理 API 密钥文件,以避免损坏它或将其泄露给未经授权的用户。
当与 API 密钥一起工作时,客户端有多种方式可以在每次 API 请求中提供密钥。其中一种更受欢迎的技术是使用这些互补的安全功能:
-
HTTPS 协议,其中客户端和服务器应用程序之间的所有通信都是加密的。
-
使用基本授权的 HTTP 授权头。此头将包含用户名和 API 密钥作为密码。
对于客户端工具来说,使用授权头通常非常简单。许多库——例如,requests库——提供了一个包含用户名和 API 密钥的对象类。在请求函数上使用auth=
参数将构建适当的头。
使用 HTTPS 包括传输层安全性(TLS)来保护授权头的内容。requests包会礼貌地处理这一点。
在服务器端,这些都需要由我们的 RESTful API 应用程序来处理。使用 HTTPS 最好是通过在另一个服务器内运行Bottle应用程序来实现。例如,我们可以创建一个 NGINX 和 uWSGI 配置,在包含服务器内运行我们的 RESTful 应用程序。另一个选择是使用基于 Python 的服务器,如 Paste 或 GUnicorn 来包含Bottle应用程序。拥有一个容器服务器来处理 HTTPS 协商的细节是至关重要的。
处理授权头最好在 RESTful API 中进行。一些路由(例如,openapi.yaml
)不应包含任何安全考虑。其他路由——特别是那些导致状态变化的路由——可能仅限于所有用户的一个子集。
这表明用户列表包括一些权限以及它们的 API 密钥。每个路由都需要确认授权头包含一个已知的用户和正确的密钥。request
对象的request.auth
属性是一个包含用户名和 API 密钥值的二元组。这可以用来决定请求是否通常可接受,以及是否允许给定的用户进行状态改变的POST操作。这种处理通常实现为一个装饰器。
我们不会深入探讨这个装饰器的设计。对于这个项目,由于资源很少,每个函数内部重复的if
语句是可以接受的。
12.3 可交付成果
这个项目有以下可交付成果:
-
docs
文件夹中的文档 -
tests/features
和tests/steps
文件夹中的验收测试 -
tests
文件夹中应用程序模块的单元测试 -
一个用于 RESTful API 处理的程序
我们首先将查看验收测试用例。由于我们需要在用客户端请求访问之前启动 RESTful API 服务,所以它们将相当复杂。
12.3.1 验收测试用例
回到第四章,数据获取功能:Web API 和抓取,特别是使用 SQLite 数据库的验收测试,我们探讨了描述涉及数据库服务的场景的方法。
对于这个项目,我们需要编写将导致启动 RESTful API 服务的步骤定义的场景。
关于设置 RESTful API 服务器状态有一个重要的问题。设置状态的一种方法是在场景中作为一系列请求的一部分。这对于这个应用程序通常是合适的。
如果服务器状态反映在文件系统中,那么通过播种适当的文件可以控制 API 服务器状态。而不是运行获取和清理过程,测试场景可以将适当的状态和日志文件注入到工作目录中。
一些开发者有一种感觉,RESTful API 需要数据库(或分布式缓存)。在实践中,通常情况下,共享文件系统就足够了。
在实践中使用文件并不罕见。对于 RESTful API,并不总是需要共享状态的数据库。
使用文件系统进行状态使得验收测试工作得很好。可以创建适当的文件来初始化由测试场景中给定的步骤描述的状态下的服务。
一个复杂的场景可能如下所示:
@fixture.REST_server
Scenario: Service starts and finishes acquiring data.
Given initial request is made with path "/api/2023.02/creation" and
method "post" and
body with {"series": "2", "source": "Anscombe_quartet_data.csv"}
And initial response has status "200", content-type "application/json"
And initial response has job-id
When polling every 2 seconds with path "/api/2023.02/creation/job-id" and
method "get" finally has response body with status "Done"
Then response content-type is "application/json"
And response body has log with more than 0 lines
And response body has series "Series_2"
And response body has status "done"
关于创建固定值的更多背景信息,请参阅第四章,数据获取功能:Web API 和抓取中的验收测试。此场景引用了一个名为REST_server
的固定值。这意味着environment.py
必须定义此固定值,并提供一个before_tag()
函数,以确保使用该固定值。
给定的步骤指定了一个初始查询和响应。这应该在 API 服务器中设置所需的状态。此处理请求将启动获取和清理处理。《When》步骤指定了一系列动作,包括定期轮询,直到请求的处理完成。
注意When
语句中提供的路径。文本job-id
位于场景的路径中。步骤定义函数必须用实际的作业标识符替换此模板字符串。此标识符将在给定步骤的初始请求中给出。《Given》步骤的定义函数必须将值保存在上下文中,以便在后续步骤中使用。
Then
步骤确认系列数据已被返回。此示例并未展示对结果的完整检查。鼓励您扩展此类验收测试场景,以便更完整地检查实际结果是否与预期结果相符。
对于某些应用,检索一个微小的测试案例数据集可能是一个有助于测试应用的特性。用户想要的普通数据集可能相当大,但也可以提供一个特别小、异常小的数据集,以确认所有部分都在协同工作。
自检资源对于健康检查、诊断和一般站点可靠性通常是必不可少的。
网络负载均衡器通常需要探测服务器以确保其能够处理请求。一个自检 URI 可以为此目的提供帮助。
当尝试停止此服务时,会出现一个非常微妙的问题。它包含一个工作池,父进程需要使用 Linux 的wait()
来正确地终止子进程。
一种可靠的方法是在启动服务的函数中使用server.send_signal(signal.SIGINT)
来创建场景的固定装置。这意味着固定装置函数将有以下轮廓:
@fixture
def rest_server(context: Context) -> Iterator[Any]:
# Create log file, base URI (code omitted)
server = subprocess.Popen([sys.executable, "src/service.py"],
shell=False, stdout=context.log_file, stderr=subprocess.STDOUT)
time.sleep(0.5) # 500 ms delay to allow the service to open a socket
yield server # Scenario can now proceed.
# 100 ms delay to let server’s workers become idle.
time.sleep(0.10)
server.send_signal(signal.SIGINT)
# 100 ms delay to let API’s subprocesses all terminate.
time.sleep(0.10)
各种sleep()
时间是对服务器子进程完成各种启动和关闭任务所需时间的宽松估计。在某些情况下,操作系统调度器会优雅地处理这种情况。然而,在其他情况下,断开的子进程可能会留在运行进程的列表中。这些“僵尸进程”需要手动终止,这是我们希望避免的。
在大多数基于 Linux 的操作系统上,ps -ef
命令将显示所有进程。ps -ef | grep python
管道将显示所有 Python 进程。
从此列表中,任何僵尸工作池进程应该都很明显。
signal.SIGINT
是控制-C 中断信号。Python 进程将此作为一个不会处理的异常。当此异常从创建进程池的with
语句退出时,将完成完整的清理,并且不会留下僵尸进程在运行。
现在我们已经查看定义了适当行为的验收测试,我们可以查看 RESTful API 服务器应用。
12.3.2 RESTful API 应用
RESTful API 应用程序可以使用任何可用的框架来构建。由于前一章 (第四章,数据获取功能:Web API 和 抓取)) 使用了 Bottle 框架,你可以继续使用这个小型框架。因为 Bottle 和 Flask 非常相似,当需要额外功能时,升级到 Flask 并不复杂。
使用 Flask 为此应用程序的一个优点是集成了用于编写单元测试用例的客户端。Bottle 项目可以完成所需的一切,但它缺少测试客户端。在查看单元测试时,我们还将查看 Bottle 框架的单元测试工具。
在 OpenAPI 3 规范 中,我们查看了一个特定路径的 OpenAPI 规范。以下是该规范可以如何实现:
from bottle import response, get
@get(’/api/2023.02/series’)
def series_list():
series_metadata = [
{"name": series.stem, "elements": series_size(series)}
for series in DATA_PATH.glob("*.ndj")
]
response.status = 200
response.body = json.dumps(series_metadata, indent=2)
response.content_type = "application/json"
return response
此函数构建了一系列元数据字典。每个项目都有一个系列名称,该名称用于单独的请求来获取数据。大小是通过一个小函数计算得出的,该函数读取系列并找到样本数量。
response
对象并不总是像这个示例中那样被操作。这是一个极端案例,其中要返回的值不是一个 Python 字典。如果返回值是字典,Bottle 框架会自动将其转换为 JSON,并将内容类型设置为 application/json
。在这种情况下,结果是字典列表;Bottle 框架不会自动将对象序列化为 JSON 表示法。
设计的一个重要部分是缓存,以保留 Future
对象直到处理完成,数据可用。处理这种需求的一种方法是用数据类来保存请求的参数、将产生结果的 Future
对象以及分配的工作标识符。
每个 Future
对象的结构可能看起来像以下示例:
from conccurrent import futures
from dataclasses import dataclass, field
from pathlib import Path
import secrets
@dataclass
class AcquireJob:
series: str
source_path: Path
output_path: Path
future: futures.Future = field(init=False)
job_id: str = field(default_factory=lambda:
\secrets.token_urlsafe(nbytes=12))
这保留了请求的参数以及处理细节。series
、source_path
和 output_path
的值是从初始请求时提供的参数构建的。路径是从提供的名称构建的,并包括服务器正在使用的作业目录的基本路径。在这个示例中,用户的输入仅限于系列名称和数据源。这些值来自一个有效的值域,这使得验证这些值相对容易。
然后,RESTful API 可以在适当的数据清理目录中创建输出路径。
当创建 AcquireJob
类的实例时,自动计算 job_id
属性的值。
当使用 submit()
方法提交处理请求到等待的工作池时,设置 future
属性的值。
在 RESTful API 执行任何工作之前,需要创建工作池。启动可能看起来像以下示例:
from conccurrent import futures
import urllib.parse
WORKERS: futures.ProcessPoolExecutor
# Definitions of all of the routes
if __name__ == "__main__":
# Defaults...
acquire_uri = "http://localhost:8080"
# Parse a configuration, here; possibly overriding defaults
uri = urllib.parse.urlparse(acquire_uri)
with futures.ProcessPoolExecutor() as WORKERS:
run(host=uri.hostname, port=uri.port)
每个路由都由一个单独的函数处理。因此,Bottle(以及 Flask)框架期望工作池是一个由所有路由处理函数共享的全局对象。在多线程服务器的情况下,在写入WORKERS
全局之前必须使用锁。
同样,AcquireJob
实例的缓存也预期是一个全局对象。这个缓存只由处理路由的函数更新,以处理初始化处理请求。这个缓存将由显示处理请求状态的路由查询。在多线程服务器的情况下,在向全局工作作业缓存添加新项之前必须使用锁。
在某些情况下,当负载特别重时,可能需要为 RESTful API 实现中各种函数执行的处理使用线程局部存储。特别是request
和response
对象已经在线程局部存储中。理想情况下,这些函数执行的处理非常少,最小化需要创建并保留在threading.local
实例中的对象数量。
对于这个项目的单元测试有一些特殊考虑。我们将在下一节中探讨这些内容。
12.3.3 单元测试用例
一些框架——如Flask——提供了一个测试客户端,可以用来在没有启动服务器和工作池开销的情况下测试应用程序。
Bottle框架不提供测试客户端。一个相关项目,boddle,提供了一种构建模拟request
对象以支持单元测试的方法。请参阅github.com/keredson/boddle
。
WebTest项目是编写单元测试的替代方案。一个WebTest固定配置包含 Bottle 应用程序,并通过内部 WSGI 接口提供请求和响应。这避免了启动完整服务器的需要。它还允许对 Bottle 应用程序进行一些猴子补丁以模拟组件。请参阅docs.pylonsproject.org/projects/webtest/en/latest/
。
使用Pylons框架中包含的非常复杂的WebTest
客户端似乎是最好的选择。这个客户端可以执行单元测试。
有时注意到带有装饰器的函数是复合对象是有帮助的。这意味着“单元”测试并不是在彼此独立的情况下测试装饰和函数。这种缺乏单独测试的情况有时会导致调试测试用例失败的根本原因变得困难。问题可能出在函数上,也可能是@route
装饰器,或者可能是测试中作为复合函数一部分的任何授权装饰器。
使用适当的日志消息进行调试似乎更容易测试组合路由函数。虽然这并不严格遵循单独测试每个组件的想法,但它对于使用适当的模拟测试每个路由来说效果很好。例如,我们可以模拟工作池,避免在测试时启动子进程的开销。
下面是一个使用 WebTest 来测试 Bottle 路由的测试函数示例:
from unittest.mock import sentinel, Mock, call
from pytest import fixture, MonkeyPatch
from webtest import TestApp
import service
def test_test(monkeypatch: MonkeyPatch) -> None:
monkeypatch.setitem(service.ACCESS, "unit-test", "unit-test")
app = TestApp(service.app)
app.authorization = (
"Basic", ("unit-test", "unit-test")
)
response = app.get("/api/2023.02/test")
assert response.status_code == 200
assert response.json[’status’] == "OK"
service.app
是 RESTful API 应用程序中的全局 app
对象。这是一个 Bottle
类的实例。service.ACCESS
是全局的用户名及其预期 API 密钥的列表。测试通过 monkey-patch 强制输入特定的测试用户名和测试 API 密钥。这种初始设置可能是许多测试会用到的,应该定义为一个可重用的固定装置。
当发出 app.get()
请求时,测试工具将执行 route
函数并收集响应,以便由 test
方法进行检查。这直接调用函数,避免了网络请求的开销。
选择使用 Flask 而不是 Bottle 的原因之一是可用的测试客户端可以简化一些测试设置。
12.4 概述
本章在单个 RESTful API 的覆盖下集成了多个应用程序。为了构建合适的 API,需要几个重要的技能组:
-
创建 OpenAPI 规范。
-
编写实现 OpenAPI 规范的 Web 服务应用程序。
-
使用处理池来委托长时间运行的后台任务。在这个例子中,我们使用了
concurrent.futures
来创建结果的未来承诺,然后计算这些结果。
涉及的进程数量可能相当令人畏惧。除了 Web 服务之外,还有一个处理池,有多个子进程来执行获取和清理数据的工作。
在许多情况下,会构建额外的工具来监控 API,以确保其正常运行。此外,通常还会分配专用服务器来完成这项工作,并配置 supervisord
来启动整体服务并确保服务继续正常运行。
12.5 额外内容
这里有一些想法,您可以将其添加到这些项目中。
12.5.1 向 POST 请求添加过滤条件
初始化获取处理的 POST 请求相当复杂。参见 A POST 请求开始处理 了解它执行的处理。
我们可能将此路由的函数命名为 creation_job_post()
,以使其明确表示该函数创建工作以响应 HTTP POST 请求来获取数据。
此函数中的任务列表包括以下内容:
-
检查用户的权限。
-
验证参数。
-
使用参数构建一个
AcquireJob
实例。 -
使用
Future
对象更新AcquireJob
实例。该未来将评估acquire_series()
函数,该函数执行获取和清理数据的工作。 -
返回一个包含提交作业详细信息的 JSON 对象,以及用于重定向到获取作业状态的请求的头部和状态码。
一些 RESTful API 可能会有更复杂的参数。例如,用户可能希望在下载之前过滤数据以创建一个子集。这通过只提供所需数据来提高用户体验。它还允许分析师在不需要在分析师社区中共享过滤代码的情况下共享数据子集。
它还可以通过在更大的、更强大的服务器上执行过滤来提高用户体验。它可以防止需要在本地笔记本电脑上下载和过滤数据。
这绝对不是RESTful API 的特性。这必须首先作为一个读取和过滤干净数据的应用程序的特性来构建。这个新应用程序将创建一个新的数据集,准备下载。数据集名称可能是一个 UUID,相关的元数据文件将包含过滤参数。
实现需要 creation_job_post()
函数现在也要验证过滤标准。它必须在构建的 AcquireJob
实例中包含过滤标准,并将过滤标准提供给底层的 acquire_series()
函数。
acquire_series()
函数将会有最显著的变化。它将以子进程的形式运行获取、清理和过滤应用程序。你可能需要考虑一个集成的应用程序,该应用程序运行其他应用程序,简化 RESTful API。
这当然会导致接受测试用例的复杂性大大增加,以确保数据获取与这些额外的过滤标准一起以及不一起工作。
12.5.2 将 OpenAPI 规范分成两部分以使用 $REF 来引用输出模式
OpenAPI 规范包括多个模式。在 OpenAPI 3 规范 中,我们展示了该规范的一些关键特性。
对于分析师来说,下载整个规范并定位到 components.schemas.seriesList
模式并不太难。这种通过 JSON 文档的导航不涉及太多挑战。
虽然这并不繁重,但一些用户可能会反对。专注于商业问题的分析师不应该被要求也整理 OpenAPI 规范的结构。一个替代方案是将规范分解成几个部分,并分别提供这些部分。
特别地,"$ref"
引用出现的地方通常使用形式为 #/components/schemas/...
的路径。该路径是一个本地 URL,省略了主机名信息。这可以被替换为一个指向 RESTful API 服务器上模式详细信息的完整 URL。
我们可能使用 http://localhost:8080/api/schemas/...
来引用存储为单独 JSON 文档的各个模式文件。每个单独的模式定义将有一个独特的 URI,允许仅访问相关的模式,并忽略 OpenAPI 规范的其他方面。
这将 OpenAPI 规范分解为服务的整体规范以及描述可下载数据集的单独规范。它还要求添加一个路径到 RESTful API 服务,以便正确下载整体 OpenAPI 规范。
这导致需要一些额外的接受测试用例来提取模式以及整体 OpenAPI 规范。
12.5.3 使用 Celery 而不是 concurrent.futures
在 整体方法 中的建议是使用 concurrent.futures
模块来处理长时间运行的数据获取和清理过程。启动处理过程的 API 请求创建一个反映实际工作子进程状态的 Future
对象。在任务完成期间,RESTful API 可以自由响应额外的请求。
另一个用于实现此类后台处理的流行包是 celery
。请参阅 docs.celeryq.dev/en/stable/getting-started/introduction.html
。
这比使用 concurrent.futures
模块要复杂一些。它还优雅地扩展,允许大量独立的计算机组成可用工作池。这可以允许由相对较小的 RESTful API 应用程序控制非常大的处理负载。
使用 Celery 需要创建任务,使用 @task
装饰器。它还需要单独启动工作池。这意味着整个 RESTful API 现在有两个步骤才能开始:
-
芹菜工作池必须正在运行。
-
然后 RESTful API 可以启动。一旦运行,它可以委托工作给池中的工作进程。
对于非常大的工作负载,当工作池分布在多台计算机上时,需要使用 Celery 的复杂管理工具来确保池能够适当地启动和停止。
将工作提交给工作池的核心工作从 pool.submit()
更改为 celery_app.delay()
。这是一个小的编程更改,允许使用更复杂和可扩展的工作池。
对于此版本没有接受测试的更改。功能是相同的。
启动 RESTful API 所需的固定定义将更加复杂:它必须在启动 RESTful API 之前启动 Celery 工作池。它还需要关闭这两个服务。
12.5.4 直接调用外部处理而不是运行子进程
在 整体方法 中,我们建议工作应由 acquire_series()
函数完成。此函数将由 POOL.submit()
函数评估。这将委托工作给工作进程,并返回一个 Future
对象以跟踪完成状态。
在那个部分,我们建议 acquire_series()
函数可以使用 subprocess.run()
来执行处理管道的各个组件。它可以运行 src/acquire.py
应用程序,然后运行 src/clean.py
应用程序,使用 subprocess
模块。
这并不是唯一可行的方法。另一种方法是导入这些应用程序模块,并直接评估它们的 main()
函数。
这意味着用 acquire.main()
和 clean.main()
函数替换 subprocess.run()
函数。这可以避免在 Linux 上的微小开销。从概念上简化来看,我们可以看到 acquire_series()
函数是如何使用其他 Python 模块来创建数据的。
这并不涉及对验收测试用例的任何更改。但它确实涉及到单元测试用例的一些更改。当使用 subprocess.run()
时,单元测试必须使用一个捕获参数值并返回有用结果的模拟来 monkey-patch subprocess
模块。当用 acquire.main()
和 clean.main()
函数替换此处理过程时,这两个模块必须使用捕获参数值并返回有用结果的模拟来 monkey-patch。
第十三章
项目 4.1:可视化分析技术
在进行探索性数据分析(EDA)时,一个常见的做法是使用图形技术来帮助理解数据分布的性质。美国国家标准与技术研究院(NIST)的工程统计学手册强烈强调了图形技术的重要性。参见doi.org/10.18434/M32189
。
本章将创建一些额外的 Jupyter 笔记本来展示展示单变量和多变量分布的一些技术。
在本章中,我们将关注创建清理数据图表的一些重要技能:
-
额外的 Jupyter Notebook 技术
-
使用PyPlot展示数据
-
Jupyter Notebook 函数的单元测试
本章有一个项目,即构建一个更完整分析笔记本的开始。笔记本可以保存并导出为 PDF 文件,允许分析师分享初步结果以进行早期对话。在下一章中,我们将扩展笔记本以创建可以与同事分享的演示文稿。
看得更远一些,笔记本可以帮助识别需要持续监控的数据的重要方面。这里创建的计算通常将成为更全面自动化报告工具和通知的基础。这种分析活动是理解数据并设计数据模型的重要步骤。
我们将从分析笔记本的描述开始。
13.1 描述
在前面的章节中,创建的项目序列建立了一个获取并清理原始数据的管道。目的是构建自动化的数据收集作为 Python 应用程序。
我们指出,临时数据检查最好使用笔记本,而不是自动化的 CLI 工具。同样,为分析和展示创建命令行应用程序可能具有挑战性。分析工作似乎本质上就是探索性的,因此从查看结果中获得即时反馈是有帮助的。
此外,分析工作将原始数据转化为信息,甚至可能是洞察。分析结果需要共享以创造显著的价值。Jupyter 笔记本是一个探索环境,可以创建可读的、有帮助的演示文稿。
对原始数据进行的第一件事之一是创建图表来展示单变量数据的分布和多变量数据中变量之间的关系。我们将强调以下常见的图表类型:
-
直方图直方图总结了数据集中一个变量值的分布。直方图将在一个轴上有数据值,在另一个轴上有频率。
-
散点图散点图总结了数据集中两个变量值之间的关系。视觉聚类对普通观察者来说是显而易见的。
对于小数据集,散点图中的每个关系都可以用一个单独的点来表示。对于包含许多点且具有相似关系的较大数据集,创建“桶”来反映具有相同关系的点的数量可能是有帮助的。
有多种方法可以显示这些桶的大小。这包括使用颜色代码来表示更流行的组合。对于某些数据集,包围圆的大小可以显示类似值数据的相对浓度。鼓励读者查看替代方案,以帮助强调各种样本属性之间的有趣关系。
在处理图表时,使用Seaborn提供彩色样式也很重要。鼓励您探索各种调色板,以帮助强调有趣的数据。
13.2 总体方法
在审视我们的方法时,我们将借鉴 C4 模型(c4model.com
)的一些指导原则:
-
上下文:对于这个项目,上下文图有两个用例:获取-清理过程和这个分析笔记本。
-
容器:有一个用于分析应用的容器:用户的个人电脑。
-
组件:软件组件包括现有的分析模型,这些模型为 Python 对象提供了便捷的定义。
-
代码:代码分散在两个地方:支持模块以及笔记本本身。
本应用的上下文图显示在图 13.1中。
图 13.1:上下文图
分析师通常会需要与利益相关者分享他们的分析结果。一个初步的笔记本可能提供确认,表明某些数据不符合零假设,暗示了一个值得深入探索的有趣关系。这可能是一部分,为基于初步结果进行更多分析的理由分配预算。另一种可能的场景是分享一个笔记本以确认零假设很可能为真,并且数据的变化有很大概率是某种测量噪声。这可以用来结束一项调查并专注于替代方案。
在第六章、项目 2.1:数据检查笔记本中,描述了数据检查笔记本。提到了使用单个获取模型模块来处理数据,但 Python 实现的细节并没有被强调。在检查时,原始数据模块通常不会为数据提供多少有用的结构。
在进入更复杂的项目时,我们将看到笔记本与定义数据模型的模块之间的关系变得更加重要。
对于这个分析笔记本,分析数据模型,在 第九章,项目 3.1:数据清洗基础应用 中创建,将成为笔记本进行分析的核心部分。这将在分析笔记本中导入和使用。分析过程可能会导致分析模型的变化,以反映学到的经验教训。
由于目录结构,出现了一个技术复杂性。数据采集和清理应用程序位于 src
目录中,而笔记本保存在单独的 notebooks
目录中。
当在 notebooks
目录中处理笔记本时,很难让 Python 的 import
语句查看相邻的 src
目录。import
语句会扫描由 sys.path
值定义的目录列表。这个值由一些定义的规则、当前工作目录以及 PYTHONPATH
环境变量的值初始化。
有两种方法可以使 import
语句从相邻的 src
目录加载模块:
-
在启动 JupyterLab 之前,请将
../src
添加到PYTHONPATH
环境变量中。 -
在启动 JupyterLab 后,将
../src
的绝对路径添加到sys.path
列表中。
这两种方法等价。第一种方法可以通过更新个人的 ~/.zshrc
文件来实现,确保每次终端会话启动时都会设置 PYTHONPATH
环境变量。对于其他 shell,例如 ~/.bashrc
或经典 sh
shell 的 ./rc
,也有合适的文件。对于 Windows,有一个对话框允许用户编辑系统的环境变量。
另一种方法是更新 sys.path
,使用如下示例中的代码单元格:
import sys
from pathlib import Path
src_path = Path.cwd().parent / "src"
sys.path.append(str(src_path))
这个单元格将把对等 ../src
目录添加到系统路径中。完成此操作后,import
语句将引入 ../src
目录中的模块,以及内置标准库模块和用 conda 或 pip 安装的模块。
可以将初始化模块定义为 IPython 启动的一部分。该模块可以以一致的方式更改项目中多个相关笔记本的 sys.path
值。
虽然一些开发者反对在笔记本中篡改 sys.path
,但它具有明确的优势。
在个人的 ~/.zshrc
文件中设置 PYTHONPATH
是一个非常干净且可靠的解决方案。因此,在 README
文件中添加一个提醒变得很有必要,以便新团队成员也能将此更改应用到他们的个人主目录中。
当共享笔记本时,确保所有利益相关者都能访问笔记本所依赖的整个项目变得至关重要。这可能导致需要创建一个 Git 仓库,其中包含共享的笔记本、提醒、测试用例和所需的模块。
一旦我们正确定义了路径,笔记本就可以与其他应用程序共享类和函数。我们将继续查看分析笔记本的一种可能的组织结构。
13.2.1 笔记本的一般组织
分析笔记本通常是展示和共享结果的主要方法。它与“实验室”笔记本不同。实验室笔记本通常包含许多实验,其中一些是失败的,一些则提供了更有用的结果。与实验室笔记本不同,分析笔记本需要有一个整洁的组织,Markdown 单元格与数据处理单元格穿插其中。实际上,分析笔记本需要讲述一种故事。它需要展示角色、行动以及这些行动的后果。
笔记本中的单元格从开始到结束正确执行是至关重要的。作者应该能够在任何时间重新启动内核并运行所有单元格以重新进行计算。虽然这可能对于特别长时间运行的计算来说可能是不希望的,但这仍然是必须的。
笔记本可能有一些初步操作,这些操作对报告的大多数读者来说并不相关。一个具体的例子是将sys.path
设置为从相邻的../src
目录导入模块。利用 JupyterLab 折叠单元格的能力,将一些计算细节放在一边,以帮助读者关注关键概念,这可能是有帮助的。
使用 Markdown 单元格来正式化这些初步介绍似乎是合理的。本节剩余的单元格可以视觉上折叠,以最小化干扰。
初步介绍可以包括技术和相对不那么技术性的方面。例如,设置sys.path
完全是技术性的,而且很少的利益相关者需要看到这一点。另一方面,从 ND JSON 格式文件中读取SeriesSample
对象是相对更相关于利益相关者问题的初步步骤。
在初步介绍之后,笔记本的主要内容可以集中在两个主题上:
-
概述统计
-
可视化
我们将在下一节中查看概述统计。
13.2.2 用于总结的 Python 模块
对于初步报告,Python 的statistics
模块提供了一些方便的统计函数。此模块提供mean()
、median()
、mode()
、stdev()
、variance()
。还有许多其他函数,鼓励读者进行探索。
这些函数可以在单元格中评估,结果将显示在单元格下方。在许多情况下,这已经足够了。
然而,在少数情况下,需要截断结果以删除无意义的尾随数字。在其他情况下,使用 f-string 为结果提供标签可能会有所帮助。一个单元格可能看起来像以下这样:
f"mean = {statistics.mean(data_values): .2f}"
这提供了一个标签,并将输出截断到小数点后两位。
在某些情况下,我们可能希望将计算值纳入 Markdown 单元格中。Python Markdown扩展提供了一种整洁的方式来将计算值纳入 Markdown 内容。
查看 jupyter-contrib-nbextensions.readthedocs.io/en/latest/nbextensions/python-markdown/readme.html
。
笔记本中最重要的一部分是数据的图形可视化,我们将在下一节中转向这一点。
13.2.3 PyPlot 图形
matplotlib 包非常适合创建图像和图表。在这个广泛而复杂的图形库中,有一个较小的库,pyplot
,它专注于数据可视化。使用 pyplot
库允许用几行代码创建一个有用的数据显示。
该模块通常会被重命名以使其更容易输入。以下代码行显示了常见的约定:
import matplotlib.pyplot as plt
这使我们能够使用 plt
作为命名空间来引用 matplotlib
包中 pyplot
模块定义的函数。
在某些情况下,JupyterLab 可能没有为交互式使用准备 matplotlib
库。(当绘制的图像不在笔记本中显示时,这将是明显的。)在这些情况下,需要启用 matplotlib
库的交互式使用。在笔记本的一个单元格中使用以下魔法命令来启用交互式使用:
%matplotlib inline
%config InlineBackend.figure_formats = {’png’, ’retina’}
第一个命令使 matplotlib
库能够在笔记本中立即创建图形。它不会创建单独的文件或弹出窗口。第二个命令生成易于共享的 PNG 输出文件。它还帮助 MAC OS X 用户优化其高分辨率显示的图形。
这并不总是必需的。它仅适用于那些未在 Jupyter 笔记本中为交互式使用配置 matplotlib
库的安装。
在许多情况下,pyplot
库期望为各种绘图函数提供简单的值序列。例如,散点图的 x 和 y 值预期是相同长度的两个平行列表。这将导致需要一些额外的单元格来重新组织数据。
前几章中的数据清洗应用产生了一系列的复合样本对象。每个对象都是一个单独的样本,包含所有变量的相关值。我们需要将这种样本记录组织转换为单个变量的并行值序列。
这可能包括以下内容:
x = [s.x for s in series_1]
y = [s.y for s in series_1]
另一个选择是使用 operator.attrgetter()
函数。它看起来如下:
from operator import attrgetter
x = list(map(attrgetter(’x’), series_1))
y = list(map(attrgetter(’y’), series_1))
我们将从直方图开始,以展示单个变量的值分布。
数据频率直方图
通常,一个文档——比如一本书——有单独的图形。一个图形可能包含一个单一的图表。或者,它可能在一个图形中包含多个“子图”。pyplot
库提供了创建包含多个子图的单一图形的支持。一个包含单个图表的图形可以被视为这种通用方法的一个特例。
可以使用如下语句准备包含单个图表的图形:
fig, ax = plt.subplots()
fig
对象代表整个图形。ax
对象是图形内部子图的轴集合。虽然显式的 ax
对象可能看似不必要,但它是一个更通用方法的一部分,允许构建具有相关图表的图形。
更通用的情况是在一个图形中堆叠多个子图。subplots()
函数可以返回每个子图的轴。创建一个包含两个图表的图形的调用可能看起来像这样:
fig, (ax_0, ax_1) = plt.subplots(1, 2)
在这里,我们声明我们将有 1 行,其中包含两个并排堆叠的子图。fig
对象代表整个图形。ax_0
和 ax_1
对象是图形中这两个子图的轴集合。
这里是一个从四个 Anscombe 的四重奏系列之一中的 y
值创建单个直方图的例子:
fig, ax = plt.subplots()
# Labels and Title
ax.set_xlabel(’Y’)
ax.set_ylabel(’Counts’)
ax.set_title(’Series I’)
# Draw Histogram
_ = ax.hist(y, fill=False)
对于这个例子,y
变量必须是来自系列 I 的 y 属性值列表。系列数据必须从清理后的源文件中读取。
_`` =`` ax.hist(...)
语句将 hist()
函数的结果赋值给变量 _
,作为抑制在此单元格中显示结果值的一种方式。如果没有这个赋值语句,笔记本将显示 hist()
函数的结果,这并不很有趣,并且会弄乱输出。由于每个系列都有 x 和 y 值,堆叠两个显示这些值分布的直方图是有帮助的。鼓励读者开发一个包含两个堆叠子图的图形。
直方图条的颜色和边框选项数量在复杂性上令人叹为观止。尝试在单个笔记本中尝试几个变体,然后删除那些没有帮助的选项是有帮助的。
值的比较通常通过显示每个 (x,y) 对的散点图来展示。我们接下来会看看这个。
X-Y 散点图
散点图是展示两个变量之间关系的方式之一。这里是一个从四个 Anscombe 的四重奏系列之一的 x
和 y
值创建单个图表的例子:
fig, ax = plt.subplots()
# Labels and Title
ax.set_xlabel(’X’)
ax.set_ylabel(’Y’)
ax.set_title(’Series I’)
# Draw Scatter
_ = ax.scatter(x, y)
_`` =`` ax.scatter(...)
语句将 scatter()
函数的结果赋值给变量 _
,作为抑制显示值的一种方式。这使输出聚焦于图形本身。
上述示例假设数据已经通过类似于以下代码的方式从一系列 SeriesSample
实例中提取出来:
x = [s.x for s in series_1]
y = [s.y for s in series_1]
这反过来又假设 series_1
的值是从由 acquire 和 clean 应用程序创建的干净数据中读取的。
现在我们有了构建笔记本的方法,我们可以讨论笔记本通常会经历的发展方式。
13.2.4 迭代与演变
笔记本通常是迭代构建的。随着对数据的理解,单元格会被添加、删除和修改。
在纯技术层面上,单元格中的 Python 编程需要从好想法发展到可测试的软件。通常,通过将选定的单元格重写为函数来做这件事是最容易的。例如,定义一个函数来获取 ND JSON 数据。这可以通过doctest
注释来补充,以确认函数按预期工作。
如果需要,可以将函数集合重构为单独的可测试模块。这可以允许在多个笔记本中更广泛地重用好的想法。
同样重要的是避免过度设计笔记本。仔细指定笔记本的内容并编写满足这些规范的代码通常不值得花费时间和精力。创建和改进笔记本要容易得多。
在一些大型组织中,高级分析师可能会指导初级分析师的工作。在这种企业中,为初级分析师提供指导可能是有帮助的。当需要正式方法时,设计指导可以采取带有 Markdown 单元格的笔记本形式来解释期望的目标。
现在我们有了构建笔记本的方法,我们可以列出这个项目的可交付成果。
13.3 可交付成果
这个项目有以下可交付成果:
-
一个
requirements-dev.txt
文件,用于标识使用的工具,通常是jupyterlab==3.5.3
和matplotlib==3.7.0
。 -
docs
文件夹中的文档。 -
tests
文件夹中任何新的应用模块的单元测试。 -
src
文件夹中任何新的应用模块,其中包含用于检查笔记本的代码。 -
一个用于总结清洁数据的笔记本。在 Anscombe 的四重奏的情况下,显示均值和方差几乎相同,但散点图却截然不同。
我们将更详细地查看一些这些可交付成果。
13.3.1 单元测试
有两种不同的模块可能需要测试:
-
包含任何函数或类定义的笔记本。所有这些定义都需要单元测试。
-
如果函数从笔记本分解到支持模块中,这个模块将需要单元测试。许多先前项目都强调了这些测试。
带有计算单元的笔记本单元难以测试。hist()
或scatter()
函数的视觉输出似乎几乎无法以有意义的方式进行测试。
此外,还有许多无法自动化的可用性测试。例如,颜色选择不当可能会掩盖一个重要的关系。考虑以下问题:
-
它是否具有信息性?
-
它是否相关?
-
是否有任何误导之处?
在许多情况下,这些问题难以量化且难以测试。因此,最好将自动化测试集中在 Python 编程上。
避免测试matplotlib.pyplot
的内部结构是强制性的。
还剩下什么要测试的?
-
数据加载。
-
笔记本中作为一部分的任何临时变换。
数据加载应减少到单个函数,该函数从清洁数据的 ND JSON 文件的行中创建一系列 SeriesSample
实例。此加载函数可以包括一个测试用例。
我们可能将函数定义为如下:
def load(source_file):
"""
>>> from io import StringIO
>>> file = StringIO(’’’{"x": 2.0, "y": 3.0}\\n{"x": 5.0, "y": 7.0}’’’)
>>> d = load(file)
>>> len(d)
2
>>> d[0]
SeriesSample(x=2.0, y=3.0)
"""
data = [
SeriesSample(**json.loads(line)) for line in source_file if line
]
return data
这允许通过在笔记本中添加以下内容来进行测试:
import doctest
doctest.testmod()
此单元格将找到笔记本中定义的函数,从函数定义中的 docstrings 中提取任何 doctest 用例,并确认 doctest 用例通过。
对于更复杂的数值处理,hypothesis 库很有帮助。有关更多信息,请参阅 第十章 的 数据清洗功能 中的 假设测试。
13.3.2 验收测试
对于笔记本,自动验收测试难以定义。很难用 Gherkin 场景的简单语言来指定笔记本的有用性、意义或洞察力。
jupyter execute <filename>
命令将执行 .ipynb
笔记本文件。这种执行是完全自动化的,允许进行一种健全性检查,以确保笔记本运行到完成。如果有问题,命令将以返回码 1 退出,并将带有错误的单元格详细显示。这可以方便地确认笔记本没有简单损坏。
.ipynb
文件是一个 JSON 文档。一个应用程序(或 Behave 的步骤定义)可以读取该文件以确认其一些属性。例如,验收测试用例可能会查找错误消息,以查看笔记本是否未能正常工作。
"type": "code"
类型的单元格也将有 "outputs"
。如果一个输出有 "output_type": "error"
;则此单元格表示笔记本中存在问题。笔记本没有运行到完成,验收测试应被视为失败。
我们可以使用像 Papermill 这样的项目来自动化笔记本与新的数据刷新。此项目可以执行模板笔记本,并将结果保存为具有可用值和执行计算的最后输出笔记本。
有关更多信息,请参阅 papermill.readthedocs.io
。
13.4 摘要
本项目开始对清洁数据进行深入分析工作。它强调了几项关键技能,包括:
-
更高级的 Jupyter Notebook 技巧。这包括设置
PYTHONPATH
以导入模块,并使用带有绘图的可视化创建图表。 -
使用 PyPlot 展示数据。该项目使用流行的可视化类型:直方图和散点图。
-
对 Jupyter Notebook 函数进行单元测试。
在下一章中,我们将把笔记本正式化成一个可以展示给一组利益相关者的“幻灯片演示文稿”。
13.5 额外内容
这里有一些想法供读者添加到这些项目中。
13.5.1 使用 Seaborn 进行绘图
pyplot 包的替代方案是 Seaborn 包。此包还提供了统计绘图功能。它提供了更广泛的外观选项,允许创建更多色彩丰富(也许更信息丰富)的图表。
更多信息请参阅 seaborn.pydata.org
。
此模块基于 matplotlib
,使其与 JupyterLab 兼容。
注意,Seaborn 包可以直接与字典列表结构一起工作。这与用于获取和清理数据的 ND JSON 格式相匹配。
使用字典列表类型暗示可能最好避免分析模型结构,并坚持使用 clean 应用程序创建的字典。这样做可能会牺牲一些模型特定的处理和验证功能。
另一方面,pydantic
包提供了一个内置的 dict()
方法,可以将复杂的分析模型对象转换成一个单一的字典,便于与 Seaborn 包一起使用。这似乎是结合这些包的一个绝佳方式。我们鼓励读者探索这个技术栈。
13.5.2 调整颜色调色板以强调数据的关键点
pyplot 包和 Seaborn 包都提供了丰富的功能来为图表应用颜色。颜色选择有时可以帮助使区分变得明显,或者它可能会掩盖重要的细节。
您可以考虑替代样式,看看哪种看起来更有用。
在某些企业环境中,存在企业通信标准,包括广泛使用的颜色和字体。使用 Seaborn 的重要技巧之一是创建符合企业通信标准的样式。
许多网站提供网站配色方案和设计帮助。例如 paletton.com
或 colordesigner.io 提供互补的颜色调色板。通过一些努力,我们可以将这些类型的配色设计方案用于创建 Seaborn 样式,从而实现一致且独特的展示风格。
第十四章
项目 4.2:创建报告
一种分享美观结果简单的方法是使用 Jupyter notebook 的 Markdown 单元创建演示文稿。本章将创建一个可以分享和展示的“幻灯片集”。我们可以在此基础上使用额外的包如 Jupyter book 或 Quarto 来创建 PDF 报告。
在本章中,我们将查看数据分析的两个重要工作成果:
-
直接从 Jupyter Lab 笔记本构建幻灯片。
-
由笔记本数据和数据分析构建的 PDF 报告。
本章的项目将升级前一章创建的分析笔记本,以创建可以与同事分享的演示文稿。我们将首先查看分析师可能需要生成的报告类型。
14.1 描述
书中的前十二章创建了一个获取和清理原始数据的管道。一旦数据可用,我们现在可以对干净的数据进行更多分析工作。
目标是将原始数据转换为信息,甚至可能提供洞察力,帮助利益相关者做出适当的决策。分析结果需要共享才能具有价值。Jupyter Notebook 是创建可读、有用的演示和报告的坚实基础。
我们将首先将分析笔记本转换为幻灯片。然后你可以使用这个幻灯片集与利益相关者讨论我们的关键点,提供有助于他们理解的信息的视觉辅助。这些在企业环境中很常见。(有些人可能会认为它们太常见,包含太多错误类型的细节。)
我们将首先查看在 Jupyter Lab 中创建幻灯片和演示。
14.1.1 幻灯片和演示
Jupyter Notebook 可以导出为演示文稿文件。底层演示文稿将是一个基于 HTML 的个人页面仓库。Reveal.js 项目用于控制页面间的导航。有关此引擎如何工作的更多详细信息,请参阅 revealjs.com
。
在笔记本中,每个单元都有属性。右侧栏是属性检查器窗口,让我们管理这些属性。其中一个属性是幻灯片类型。这允许分析师标记单元格以包含在演示中。我们将在 准备 幻灯片 中查看技术细节。
有大量关于创建有用、信息丰富演示的指南和教程。作者喜欢关注三个关键点:
-
告诉他们你要告诉他们的内容。呈现一个主题列表(或议程或大纲)。这通常使用 Markdown 列表。
-
告诉他们。这应该从一般观察到演示的具体细节。这可能是一段 Markdown 文本和图表的混合体。
-
告诉他们你已经告诉他们的内容。呈现你信息的总结以及你希望他们采取的行动。这也会经常使用 Markdown 列表。
在这里重要的是使用幻灯片演示文稿来包含关键词和短语,以帮助观众记住关键点和行动号召。这通常意味着利用Markdown文本以粗体或斜体字体强调单词。这也可能意味着使用各种类型的Markdown列表。
另一个重要部分是避免在试图将太多要点塞进一个页面时产生的视觉杂乱。当有很多细节时,演示可能不是管理所有信息的最佳方法。报告文档可能比演示更有用。文档可以为简短的演示提供补充细节。
14.1.2 报告
演示和报告之间有一个模糊的界限。演示通常较短,侧重于关键词和短语。报告通常较长,用完整的句子书写。一个组织良好的包含完整句子的演示可以被视为简短的报告。一个包含简短段落和大量图表的报告看起来可能像演示。
Markdown格式提供了许多功能,可以创建高质量的排版文档。从Markdown到 HTML 再到浏览器或 PDF 渲染引擎的技术堆栈涉及多个转换步骤,以将笔记本单元格中的简单 Unicode 文本转换为丰富详细的渲染。这个堆栈是 Jupyter Lab 的第一流部分,可以通过像Quarto或Jupyter{Book}这样的工具来利用,以创建报告。
并非所有出版物使用的排版约定都通过Markdown源文件提供。例如,一些出版物风格指南可能包括一个具有较窄边距的摘要部分,有时字体也较小。这在Markdown语言中可能具有挑战性。一些作者可能会使用一个更简单的布局,缺乏所有关于边距和字体大小的视觉提示。
HTML 和 CSS 的力量如此之大,以至于愿意掌握技术堆栈的作者可以拥有大量的排版能力。鼓励读者探索Markdown、HTML 和 CSS 的能力。读者还被建议设定现实的目标;在结合Markdown和 CSS 以实现不会增强报告信息的排版效果时,可能会投入大量时间。
通常将每个段落放入单独的单元格中效果很好。这不是一个严格的规定:有时一组段落应该放入一个单元格中。
最高级标题通常应该单独放在单元格中。这可以使重新组织这些标题下的内容更容易。一些较低级别的标题应该与它们的介绍段落放在同一个单元格中,因为标题和单元格的内容不太可能分开。
我们可能有一个包含一级标题的单元格,看起来像这样:
# Title of our document
这个单元格只有标题,没有后续文本。下一个单元格可能有一个子标题,介绍文档。
一个低级别的单元格可能看起来像这样:
## Conclusion
The various **Anscombe Quartet** series all have consistent
means and standard deviations.
此单元格包含二级标题以及该文档本节的开头文字。它使用 **
Markdown 语法来显示某个短语应该有强烈的强调,通常使用粗体字体来实现。
在接下来的几节中,我们将讨论向 Jupyter 环境添加工具的技术方法,以便分析师可以创建演示文稿或报告。
14.2 总体方法
我们将讨论在 Jupyter Notebook 中创建演示文稿和报告的一般技术步骤。对于演示文稿,不需要额外的工具。对于一些简单的报告,文件菜单提供了将笔记本保存和导出为纯 Markdown、PDF 文件或 LaTeX 文档的功能。对于更复杂的报告,使用辅助工具来创建更精美的最终文档可能会有所帮助。
14.2.1 准备幻灯片
基于 HTML 的 Reveal.js 演示文稿是 Jupyter Notebook 的一个一流功能。文件菜单提供了将笔记本保存和导出为 Reveal.js 幻灯片的选项。这将创建一个 HTML 文件,作为演示文稿显示。
在 Jupyter 中,属性检查器用于设置单元格的幻灯片类型。页面右上角有一个两个齿轮交叉的图标,用于在右侧边栏中显示属性检查器。在视图菜单下,显示右侧边栏的选项也会显示属性检查器。
每个单元格都有几种幻灯片类型的选择。最重要的两个选择是“幻灯片”和“跳过”。
“幻灯片”将作为演示文稿的一部分显示。“跳过”单元格将从演示文稿中删除;这对于计算和数据准备来说非常好。其他选项允许将单元格组合成单个幻灯片,并拥有辅助演示幻灯片。
创建 Markdown 内容并将幻灯片类型设置为“幻灯片”将创建演示文稿的叙事文本部分。这些幻灯片将包括标题页、议程和要点:所有提示和要点都将包含在这些类型的单元格中。
对于数据可视化,我们可以使用 Seaborn 或 PyPlot 来创建图表。在属性检查器中将单元格输出类型设置为“幻灯片”,以包含可视化。
我们可以使用“跳过”幻灯片类型标记计算、函数定义和 doctest 单元格。这将省略这些细节从演示文稿中。
分析师可以将笔记本与希望查看支持细节的观众成员分享。
Reveal.js 拥有大量的功能。其中许多功能通过 HTML 标记实现。例如,自动动画功能将在单元格之间平滑过渡。由于 HTML 标记是 Markdown 的一部分,因此需要熟悉 HTML 才能使用最先进的功能。
最后一步是使用 CLI 将笔记本转换为幻灯片。文件菜单中的保存并另存为...选项,但这样往往会使所有代码都可见。可见的代码可能会分散对可视化基本信息的注意力。
以下命令隐藏了单元格输入值——代码——从演示文稿中:
jupyter nbconvert --to slides --no-input <notebook.ipynb>
使用笔记本的名称代替<notebook.ipynb>
。这将创建一个包含Reveal.js代码的 HTML 文件。
整个过程分为三个步骤:
-
编辑笔记本。
-
准备演示文稿。(Jupyter Lab 中的终端工具非常适合这个用途。)
-
查看演示文稿以发现问题。
这与像Keynote和PowerPoint这样的产品的工作方式不同。当使用 Jupyter Lab 时,将在浏览器窗口和笔记本窗口之间进行一些切换。将窗口放置在显示器的两侧可能会有所帮助。
确保在每次更改笔记本后刷新浏览器窗口。
14.2.2 准备报告
创建报告是 Jupyter Lab 的第一等部分。文件菜单提供了保存和导出纯 Markdown、PDF 文件或 LaTeX 文档的能力。
类似于pandoc这样的工具可以将Markdown文件转换为各种期望的格式。对于使用 LaTeX 格式创建输出的情况,需要一个 TeX 渲染包来从源文件创建 PDF 文件。TeXLive项目维护了多个用于渲染 LaTeX 的有用工具。对于 macOS 用户,MacTex项目提供了所需的二进制文件。在线工具Overleaf也很有用,可以处理 LaTeX。
在许多情况下,仅仅将笔记本保存为纯Markdown文件是不够的,需要更复杂的处理。我们可以在我们的环境中添加Jupyter{Book}工具。更多信息请参阅jupyterbook.org
。
需要将jupyter-book
组件添加到requirements-dev.txt
文件中,以便其他开发者知道安装它。
当使用conda管理虚拟环境时,命令可能看起来像以下这样:
% conda install --channel conda-forge jupyter-book
当使用其他工具管理虚拟环境时,命令可能看起来像以下这样:
% python -m pip install jupyter-book
一个 Jupyter 书可能比单个笔记本文件复杂得多。将会有配置和目录(TOC)文件来构建整体报告。内容可以以Markdown、reStructuredText 和笔记本文件等多种形式提供。此外,还提供了一个Markdown语言的扩展版本,MyST,可以添加广泛的语义标记功能。
开始的一种方式是使用jupyter-book
的create
命令创建一个模板项目。这个模板包括所需的所有_config.yml
和_toc.yml
文件。它还包括可能成为项目一部分的各种其他文件的示例。
_config.yml
文件包含标题和作者信息。这是开始自定义内容以提供正确报告和作者名称的地方。根据报告的发布方式,可能需要更改配置的其他部分。内置假设是上传 HTML 到公共仓库。对于许多报告来说,这是理想的。
然而,对于某些企业项目,向公共仓库报告并链接到公共 GitHub 是不可接受的。在这种情况下,必须更改_config.yml
文件以正确设置仓库选项,以引用内部仓库。
立即编辑_toc.yml
文件并开始创建报告的大纲通常很有帮助。通常,数据和笔记本已经存在。受众通常是已知的,受众成员需要吸收的关键点也很明确,这使得分析师可以立即创建大纲和占位符文档。
在某些情况下,分析师可以使用从分析笔记本中提取的笔记来填写大纲。这种内容重构可以帮助将工作笔记本缩减到必要的计算和可视化。叙述性文本可以分离到笔记本外的 MyST 或 Markdown 文件中。
一旦内容以草案形式存在,就可以使用jupyter-book build
命令创建一本书。这将使用配置和目录文件从各种来源构建完整的文档。默认文档是一个 HTML 页面。
到本书出版日期为止,版本 0.15.1 包含一个警告,即直接 PDF 生成仍在开发中,可能存在错误。创建 PDF 的更可靠方法是使用Jupyter{Book}创建 LaTeX 文件。可以使用操作系统本地的LaTeX
命令来构建 PDF。另一种选择是使用sphinx-jupyterbook-latex
包来包装将 LaTeX 转换为 PDF 的 TeX 工具。
这涉及到许多移动部件,安装过程可能令人望而却步。以下是涉及的一些步骤:
-
源Markdown文本由 Jupyter Book 转换为 LaTeX。
-
sphinx-jupyterbook-latex
包可能会执行一些中间工作。 -
最终的 PDF 是通过操作系统的
latex
命令创建的;这是 MacTeX 或 TeXLive 安装的 TeX 工具。
CLI 构建命令是jupyter-book build
命令,并附加一个--builder pdflatex
选项来指定使用 Sphinx 和 TeX 工具来渲染 PDF。
14.2.3 创建技术图表
技术图表,包括由 UML 定义的广泛图表,通常很难创建。流行的演示工具如Keynote和PowerPoint拥有巧妙的绘图工具,内置了大量的形状和选项,可以用于在幻灯片上定位这些形状。
对于演示文稿中的图表创建,有几个选择:
-
使用单独的图形应用程序创建
.PNG
或.jpg
文件,并将图形融入文档。本书中的许多图表都是使用 PlantUML 创建的,例如。请参阅plantuml.com
。 -
使用
matplotlib
,编写代码创建图像。这可能涉及大量的编程来绘制一些由箭头连接的框。
PlantWEB项目提供了一个 Python 接口到 PlantUML 网络服务。这使得分析师可以如下工作:
-
创建一个包含描述图像的领域特定语言(DSL)文本的文件。
-
使用 PlantUML 引擎渲染图像以创建
.jpg
文件。 -
将图像作为 IPython
SVG
对象导入笔记本。
图像渲染使用 PlantUML 服务器;这需要一个活跃的互联网连接。在分析师可能离线工作的情况下,PlantWEB文档建议使用Docker在本地 Docker 容器中运行本地服务。这将快速进行图表渲染,无需连接到互联网。
在考虑了创建幻灯片和报告的各种技术因素后,我们可以强调本项目的交付成果。
14.3 交付成果
本项目有两个交付成果:
-
一个演示文稿
-
一个报告
演示文稿应该是一个使用Reveal.js幻灯片的 HTML 文档。
报告应该是一个来自单个笔记本的 PDF 文档。它应包含可视化图表和一些解释图像的叙述性文本。
关于笔记本的单元测试和验收测试信息,请参阅第十三章,项目 4.1:可视化技术。本项目应基于前一个项目。它不涉及戏剧性的新编程。相反,它涉及大量组件的集成,以创建有意义的、有用的演示和报告。
14.4 摘要
在本章中,我们构建了数据分析的两个重要工作成果:
-
可以用作演示给感兴趣的用户和利益相关者的幻灯片
-
可以分发给利益相关者的 PDF 格式的报告
这两者之间的界限总是模糊的。有些演示文稿有很多细节,本质上是在小页面上呈现的报告。
有些报告充满了图表和项目符号;它们通常看起来像是纵向模式的演示文稿。
通常,演示文稿的细节深度不如详细报告。报告通常设计用于长期保留,并提供背景信息,以及参考文献,以帮助读者填补缺失的知识。这两者都是 Jupyter 笔记本的第一流部分,创建这些内容应该是每位分析师技能的一部分。
本章强调了创建卓越成果所需的额外工具。在下一章中,我们将转换方向,探讨数据建模的一些基本统计知识。
14.5 额外内容
这里有一些想法供读者添加到这些项目中。
14.5.1 带有 UML 图表的书面报告
在 创建技术图表 中,总结了创建 UML 图表的过程。鼓励读者使用 PlantUML 为他们的数据采集和清洗流程创建 C4 图表。然后,这些 .jpg
文件可以被纳入报告中作为 Markdown 图像。
关于 C4 模型的更多信息,请参阅 c4model.com
。
第十五章
项目 5.1:建模基础应用
在从获取到清洗和转换的管道中的下一步是数据分析和一些初步建模。这可能会引导我们使用数据构建更复杂的模型或机器学习。本章将指导您在三个阶段的管道中创建另一个应用程序,以获取、清洗和建模数据集合。这个第一个项目将创建一个具有更多详细和特定应用建模组件占位符的应用程序。这使得插入可以替换为更复杂处理的简单统计模型变得更加容易。
在本章中,我们将探讨数据分析的两个部分:
-
CLI 架构以及如何设计更复杂的流程来收集和分析数据
-
数据构建统计模型的核心概念
从远处看,所有分析工作都可以被认为是创建某些复杂过程重要特征的简化模型。即使是像计算平均值这样简单的事情,也暗示了一个变量集中趋势的简单模型。添加标准差暗示了变量值的预期范围,并且——进一步——为范围之外的值分配了概率。
模型当然可以更加详细。我们的目的是以构建灵活、可扩展的应用软件的方式开始建模之路。每个应用程序都将具有独特的建模需求,这取决于数据的性质以及关于数据的提问性质。对于某些过程,均值和标准差足以发现异常值。对于其他过程,可能需要更丰富和更详细的模拟来估计数据的预期分布。
我们将通过单独观察变量来开始建模,有时这被称为单变量统计学。这将考察数据的一些常见分布。这些分布通常有几个可以从给定数据中发现的参数。在本章中,我们还将探讨均值、中位数、标准差、方差和标准差等度量。这些可以用来描述具有正态或高斯分布的数据。目标是创建一个独立的 CLI 应用程序,用于展示分析结果,从而为建模提供更高的自动化程度。结果可以随后在分析笔记本中展示。
自动化模型创建有一个长期的影响。一旦创建了数据模型,分析师还可以查看模型的变化以及这些变化对企业运营方式的影响。例如,一个应用程序可能每月进行一次测试,以确保新数据与已建立的均值、中位数和标准差相匹配,这些均值、中位数和标准差反映了数据的预期正态分布。如果一批数据不符合已建立的模型,则需要进一步调查以找出这种变化的原因。
15.1 描述
此应用程序将创建一个关于数据集的报告,展示一系列统计数据。这自动化了分析笔记本的持续监控方面,减少了手动步骤并创建了可重复的结果。自动计算源于对数据的统计模型,通常在分析笔记本中创建,其中探索了不同的模型。这反映了具有预期范围内值的变量。
对于工业监控,这是称为量具重复性和再现性的活动的一部分。这项活动旨在确认测量是可重复和可再现的。这被描述为查看“测量仪器”。虽然我们经常认为仪器是机器或设备,但实际上定义非常广泛。调查或问卷是关注人们对问题回答的测量仪器。
当这些计算出的统计数据与预期不符时,这表明某些东西已经改变,分析师可以使用这些意外的值来调查偏差的根本原因。可能是一些企业流程发生了变化,导致某些指标发生了变化。或者,可能是一些企业软件进行了升级,导致用于创建干净数据的源数据或编码发生了变化。更复杂的是,可能仪器实际上并没有测量我们所认为的量;这种新的差异可能暴露了我们理解上的差距。
模型测量的可重复性是测量可用性的关键。考虑一把经过多年使用而磨损严重的尺子,它已经不再方或不准确。这个单一的工具将根据磨损端被用来进行测量的部分产生不同的结果。这种测量变异性可能会掩盖制造部件的变异性。理解变化的原因具有挑战性,可能需要“跳出思维定式”——挑战关于现实世界过程、过程测量以及这些测量的模型的假设。
探索性数据分析可能具有挑战性和令人兴奋,正是因为没有明显的简单答案来解释为什么测量发生了变化。
这个初步模型的实现是通过一个应用程序来完成的,它与数据获取和清理的前一阶段是分开的。通过一些精心设计,这个阶段可以与前述阶段合并,创建一个获取、清理和创建总结统计的联合操作序列。
这个应用程序将与分析笔记本和初始检查笔记本重叠。在那些早期的临时分析阶段中做出的某些观察将被转化为固定、自动化的处理。
这是创建更复杂机器学习数据模型的开端。在某些情况下,使用线性或逻辑回归的统计模型是足够的,不需要更复杂的人工智能模型。在其他情况下,无法创建简单的统计模型可能表明需要创建和调整更复杂模型的超参数。
本应用的目标是保存一个可以与其他总结报告汇总和比较的统计摘要报告。理想的结构将是一个易于解析的文档。建议使用 JSON,但其他易于阅读的格式,如 TOML,也是合理的。
关于数据分布有三个关键问题:
-
被测量的输出位置或预期值是什么?
-
这个变量的分布或预期变化是什么?
-
一般的形状是什么,例如,它是否是对称的或者以某种方式偏斜?
关于这些问题的更多背景信息,请参阅www.itl.nist.gov/div898/handbook/ppc/section1/ppc131.htm
这项总结处理将成为自动化获取、清理和总结操作的一部分。用户体验(UX)将是一个命令行应用程序。我们预期的命令行可能看起来像以下这样:
% python src/summarize.py -o summary/series_1/2023/03 data/clean/Series_1.ndj
-o
选项指定输出子目录的路径。添加到该路径的输出文件名将来自源文件名。源文件名通常包含有关提取数据适用日期范围的信息。
安斯康姆四重奏数据不会改变,并且实际上不会有“适用日期”的值。
我们介绍了周期性企业提取的想法。实际上,没有任何项目指定一个周期性变化的数据源。
一些网络服务,如www.yelp.com
,为餐饮业提供健康代码数据;这些数据是周期性变化的,并且是分析数据的好来源。
现在我们已经看到了期望,我们可以转向实现的方法。
15.2 方法
在审视我们的方法时,我们将借鉴 C4 模型(c4model.com
)的一些指导:
-
上下文:对于这个项目,一个上下文图将显示用户创建分析报告。你可能觉得绘制这个图会有所帮助。
-
容器:似乎只有一个容器:用户的个人电脑。
-
组件:我们将在下面讨论这些组件。
-
代码:我们将简要提及,以提供一些建议的方向。
该应用的核心是一个模块,它以让我们测试数据是否符合模型预期的方式总结数据。统计模型是对创建源数据的底层真实世界过程的简化反映。模型简化包括对事件、测量、内部状态变化以及观察到的处理细节的其他假设。
对于非常简单的情况——例如 Anscombe 的四重奏数据——只有两个变量,这使模型中只剩下一个关系。四重奏中的每个样本集合都有一个独特的关系。然而,许多总结统计量是相同的,这使得关系往往令人惊讶。
对于其他数据集,由于变量更多、关系更复杂,分析师有众多选择。NIST 工程统计手册提供了一种建模方法。请参阅www.itl.nist.gov/div898/handbook/index.htm
了解模型设计和模型结果分析。
作为初步工作的部分,我们将区分两种非常广泛的统计总结类别:
-
单变量统计分析:这些是孤立看待的变量。
-
多元统计分析:这些是成对(或更高阶分组)的变量,重点在于变量值之间的关系。
对于单变量统计,我们需要了解数据的分布。这意味着测量位置(中心或期望值)、范围(或尺度)和分布的形状。这些测量领域都有几个著名的统计函数,它们可以是总结应用的一部分。
我们将在下一章探讨多元统计分析。我们将从一般的应用入手,开始单变量处理,然后重点关注统计指标、输入和最终输出。
15.2.1 设计总结应用
此应用具有命令行界面,可以从清洗后的数据创建总结。输入文件是待总结的样本。总结必须以易于后续软件处理的形式存在。这可以是一个 JSON 或 TOML 格式的文件,包含总结数据。
总结将是“位置度量”,有时也称为“中心趋势”。请参阅www.itl.nist.gov/div898/handbook/eda/section3/eda351.htm
。
输出必须包含足够的信息以理解数据源和被测量的变量。输出还包括以合理的小数位数测量的值。当这些数字只是噪声时,避免在浮点值中引入额外的数字是很重要的。
此应用程序的次要功能是创建一个易于阅读的摘要展示。这可以通过使用像Docutils这样的工具将 reStructuredText 报告转换为 HTML 或 PDF 来实现。也可以使用Pandoc这样的工具将源报告转换为不仅仅是文本的内容。在第第十四章、项目 4.2:创建报告中探讨的技术是使用 Jupyter{Book}创建适合出版的文档。
我们将首先查看需要计算的一些位置度量标准。
15.2.2 描述分布
如上所述,变量的分布有三个方面。数据倾向于围绕一个中心趋势值分散;我们将称之为位置。将有一个预期的分散极限;我们将称之为范围。可能有一个对称或以某种方式偏斜的形状。分散的原因可能包括测量变异性,以及被测量的过程中的变异性。
NIST 手册定义了三个常用的位置度量标准:
-
均值:变量的值的总和除以值的计数:X =
。
-
中位数:位于分布中心位置的值。一半的值小于或等于这个值,另一半的值大于或等于这个值。首先,将值按升序排序。如果数量是奇数,那么就是中间的值。对于偶数个值,取两个最中心值之间的平均值。
-
众数:最常见值。对于 Anscombe 四重奏数据系列中的一些数据,这并不具有信息性,因为所有值都是唯一的。
这些函数是内置statistics
模块的一级部分,使得它们相对容易计算。
当数据受到异常值污染时,可能需要一些替代方案。有一些技术,如中值均值和截断均值,可以丢弃某些百分位数范围之外的数据。
“异常值”的问题是一个敏感的话题。异常值可能反映了一个测量问题。异常值也可能暗示被测量的处理过程比样本集中揭示的要复杂得多。另一个独立的样本集可能揭示不同的均值或更大的标准差。异常值的存在可能表明需要更多的研究来了解这些值的本质。
对于数据的规模或分布,有三个常用的度量标准:
-
方差和标准差。方差本质上是从平均值到每个样本平方距离的平均值:s² =
。标准差是方差的平方根。
-
范围是最大值和最小值之间的差异。
-
中值绝对偏差是每个样本与平均值距离的中值:MAD[Y] = median(|Y [i] −Ỹ|)。参见第七章,数据检查功能。
方差和标准差函数是内置statistics
模块的一级部分。范围可以使用内置的min()
和max()
函数来计算。可以使用statistics
模块中的函数构建一个中值绝对偏差函数。
对于分布的偏度和峰度也有相应的度量。我们将把这些作为额外的功能添加到应用中,一旦基础统计度量已经到位。
15.2.3 使用清洗数据模型
对于这个总结处理,使用清洗和归一化的数据是至关重要的。检查笔记本和这个更详细的分析之间存在一些重叠。初步检查也可能查看一些位置和范围的度量,以确定数据是否可以使用或包含错误或问题。在检查活动中,通常开始创建数据的直观模型。这导致了对数据的假设形成,并考虑实验来证实或拒绝这些假设。
这个应用形式化了假设检验。一些从初始数据检查笔记本中的函数可能被重构为可以使用清洗数据的形式。基本算法可能与函数的原始数据版本相似。然而,所使用的数据将是清洗后的数据。
这导致了一个侧边栏设计决策。当我们回顾数据检查笔记本时,我们会看到一些重叠。
15.2.4 重新思考数据检查函数
由于 Python 编程可以是一般的——独立于任何特定的数据类型——因此尝试统一原始数据处理和清洗数据处理是有诱惑力的。这种愿望表现为尝试编写一个算法的精确版本,例如可用于原始和清洗数据的中值绝对偏差函数。
这并不总是可以达到的目标。在某些情况下,甚至可能并不希望这样做。
处理原始数据的功能通常需要进行一些必要的清理和过滤。这些开销随后被重构并实现到管道中,以创建清洗数据。具体来说,用于排除不良数据的if
条件在检查期间可能是有帮助的。这些条件将成为清洁和转换应用的一部分。一旦完成,它们就不再与处理清洗数据相关了。
由于额外的数据清理对于检查原始数据是必需的,但对于分析清理后的数据则不是必需的,因此创建一个涵盖这两种情况的单个过程可能很困难。实现这些复杂性的努力似乎不值得。
有一些额外的考虑。其中之一是 Python 的statistics
模块遵循的一般设计模式。此模块与原子值的序列一起工作。我们的应用程序将读取(并写入)复杂的Sample
对象,这些对象不是原子的 Python 整数或浮点值。这意味着我们的应用程序将从复杂的Sample
对象序列中提取原子值序列。
另一方面,原始数据可能没有非常复杂的类定义。这意味着复杂对象的分解不是原始数据处理的一部分。
对于一些非常大型的数据集,复杂的多变量对象的分解可能会在读取数据时发生。而不是摄入数百万个对象,应用程序可能只提取一个属性进行处理。
这可能会导致以下模式的输入处理:
from collections.abc import Iterator, Callable
import json
from pathlib import Path
from typing import TypeAlias
from analysis_model import Sample
Extractor: TypeAlias = Callable[[Sample], float]
def attr_iter(some_path: Path, extractor: Extractor) -> Iterator[float]:
with some_path.open() as source:
for line in source:
document = Sample(**json.loads(line))
yield extractor(document)
def x_values(some_path: Path) -> list[float]:
return list(attr_iter(some_path, lambda sample: sample.x))
此示例定义了一个通用函数,attr_iter()
,用于读取 ND JSON 文件以构建某些类,Sample
的实例。(Sample
类的详细信息被省略。)
x_values()
函数使用通用的attr_iter()
函数和一个具体的 lambda 对象来提取特定变量的值,并创建一个列表对象。这个列表对象可以与各种统计函数一起使用。
虽然创建了多个Sample
对象,但它们不会被保留。只有x
属性的值被保存,从而减少了从大量复杂值创建汇总统计所使用的内存量。
15.2.5 创建新的结果模型
统计摘要包含三种类型的数据:
-
元数据用于指定用于创建汇总的源数据。
-
元数据用于指定正在使用的度量标准。
-
位置、形状和分布的计算值。
在某些企业应用中,源数据由一系列日期定义,这些日期定义了最早和最晚的样本。在某些情况下,需要更多细节来描述完整的上下文。例如,过去可能已经升级了获取原始数据的软件。这意味着旧数据可能不完整。这意味着数据处理的环境可能需要一些额外的细节,比如软件版本或发布信息,除了日期范围和数据源。
同样,所使用的度量标准可能会随时间而变化。例如,偏度的计算可能会从Fisher-Pearson公式切换到调整后的 Fisher-Pearson公式。这表明摘要程序的版本信息也应与计算出的结果一起记录。
每个这些元数据值都提供了关于数据源、收集方法和任何派生数据计算所必需的上下文和背景信息。这种上下文可能有助于揭示变化的原因。在某些情况下,上下文是记录关于过程或测量仪器的潜在假设的一种方式;看到这种上下文可能允许分析师质疑假设并定位问题的根本原因。
应用程序必须创建一个看起来像以下示例的结果文档:
[identification]
date = "2023-03-27T10:04:00"
[creator]
title = "Some Summary App"
version = 4.2
[source]
title = "Anscombe’s Quartet"
path = "data/clean/Series_1.ndj"
[x.location]
mean = 9.0
[x.spread]
variance = 11.0
[y.location]
mean = 7.5
[y.spread]
variance = 4.125
此文件可以被toml
或tomllib
模块解析,以创建嵌套的字典集合。总结应用程序的次要功能是读取此文件并生成报告,可能使用 Markdown 或 ReStructuredText,以提供适合发布的可读格式数据。
对于 Python 3.11 或更新的版本,tomllib
模块是内置的。对于较旧的 Python 安装,需要安装toml
模块。
现在我们已经看到了整体方法,我们可以看看具体的可交付成果文件。
15.3 可交付成果
本项目有以下可交付成果:
-
docs
文件夹中的文档。 -
tests/features
和tests/steps
文件夹中的接受测试。 -
tests
文件夹中模型模块类的单元测试。 -
csv_extract
模块测试的模拟对象将是单元测试的一部分。 -
tests
文件夹中csv_extract
模块组件的单元测试。 -
一个用于将清洗后的数据总结到 TOML 文件中的应用程序。
-
将 TOML 文件转换为包含总结的 HTML 页面或 PDF 文件的应用程序次要功能。
在某些情况下,特别是对于特别复杂的应用程序,总结统计可能最好作为一个单独的模块实现。然后,可以扩展和修改此模块,而无需对整体应用程序进行重大更改。
理念是要区分这个应用程序的以下方面:
-
命令行界面(CLI),包括参数解析和输入输出路径的合理处理。
-
统计模型,随着我们对问题域和数据理解的发展而发展。
-
描述样本结构的数据类,独立于任何特定目的。
对于某些应用程序,这些方面不涉及大量的类或函数。在定义较小的情况下,一个单独的 Python 模块就足够好了。对于其他应用程序,尤其是那些初始假设最终被证明无效且进行了重大更改的情况,拥有独立的模块可以提供更多的灵活性,以及在未来变化方面的更多敏捷性。
我们将更详细地查看一些这些可交付成果。我们将从创建接受测试的建议开始。
15.3.1 接受测试
验收测试需要从用户的角度描述整体应用程序的行为。场景将遵循命令行应用程序的 UX 概念来获取数据和写入输出文件。由于输入数据已被清理和转换,此应用程序的故障模式很少;对潜在问题的广泛测试不像早期数据清理项目那样重要。
对于相对简单的数据集,统计摘要的结果是事先已知的。这导致了一些可能看起来像以下示例的功能:
Feature: Summarize an Anscombe Quartet Series.
Scenario: When requested, the application creates a TOML summary of a series.
Given the "clean/series_1.ndj" file exists
When we run command "python src/summarize.py \
-o summary/series_1/2023/03 data/clean/Series_\1.ndj"
Then the "summary/series_1/2023/03/summary.toml" file exists
And the value of "summary[’creator’][’title’]" is "Anscombe Summary App"
And the value of "summary[’source’][’path’]" is "data/clean/Series_1.ndj"
And the value of "summary[’x’][’location’][’mean’]" is "9.0"
我们可以通过添加多个额外的Then
步骤来继续场景,以验证每个位置以及统计摘要的分布和形状。
步骤定义将与许多先前项目的步骤定义相似。具体来说,When
步骤将使用subprocess.run()
函数执行给定应用程序,并带有所需的命令行参数。
第一个Then
步骤需要读取并解析 TOML 文件。生成的摘要对象可以放置在context
对象中。随后的Then
步骤可以检查结构以定位单个值,并确认值符合验收测试的预期。
提取一小部分数据用于验收测试通常很有帮助。与其处理数百万行数据,几十行数据就足以确认应用程序已读取并汇总了数据。数据只需代表正在考虑的较大样本集即可。
由于所选子集是测试套件的一部分,它很少改变。这使得结果可预测。
随着数据收集过程的演变,数据源的变化很常见。这将导致数据清理的变化。这反过来又可能导致汇总应用程序的变化,因为必须正确处理新的代码或新的异常值。数据源的发展意味着测试数据集也需要发展,以暴露任何特殊、边缘或角落案例。
理想情况下,测试数据集是普通数据(没有惊喜)与每个特殊、不典型案例的代表例子的混合。随着测试数据集的发展,验收测试场景也将发展。
TOML 文件相对容易解析和验证。此应用程序的次要功能——扩展 TOML 输出以添加广泛的 Markdown——也适用于文本文件。这使得通过测试场景验证读写文本文件相对容易。
最终出版物,无论是通过 Pandoc 还是 Pandoc 和 LaTeX 工具链的组合完成,都不是自动化测试的最佳主题。一位优秀的校对员或可信赖的合作伙伴需要确保最终文档符合利益相关者的预期。
15.3.2 单元测试
对此应用程序特有的各种组件进行单元测试非常重要。例如,干净的数据类定义是由另一个应用程序创建的,它有自己的测试套件。此应用程序的单元测试不需要重复那些测试。同样,statistics
模块有广泛的单元测试;此应用程序的单元测试不需要复制任何那种测试。
这进一步表明应该用Mock
对象替换statistics
模块。这些模拟对象通常可以返回将出现在结果 TOML 格式摘要文档中的哨兵
对象。
这表明测试用例的结构类似于以下示例:
from pytest import fixture
from unittest.mock import Mock, call, sentinel
import summary_app
@fixture
def mocked_mean(monkeypatch):
mean = Mock(
return_value=sentinel.MEAN
)
monkeypatch.setattr(summary_app, ’mean’, mean)
return mean
@fixture
def mocked_variance(monkeypatch):
variance = Mock(
return_value=sentinel.VARIANCE
)
monkeypatch.setattr(summary_app, ’variance’, variance)
return variance
def test_var_summary(mocked_mean, mocked_variance):
sample_data = sentinel.SAMPLE
result = summary_app.variable_summary(sample_data)
assert result == {
"location": {"mean": sentinel.MEAN},
"spread": {"variance": sentinel.VARIANCE},
}
assert mocked_mean.mock_calls == [call(sample_data)]
assert mocked_variance.mock_calls == [call(sample_data)]
两个测试夹具提供模拟结果,使用哨兵
对象。使用哨兵
对象可以轻松比较,以确保模拟函数的结果没有被应用程序意外地操纵。
测试用例test_var_summary()
以另一个哨兵
对象的形式提供了一个模拟数据源。结果具有预期的结构和预期的哨兵
对象。
测试的最后部分确认了样本数据——未经修改——被提供给模拟的统计函数。这确认了应用程序没有以任何方式过滤或转换数据。结果是预期的哨兵
对象;这确认了模块没有污染statistics
模块的结果。最后的检查确认了模拟函数确实以预期的参数被调用了一次。
这种具有众多模拟的单元测试对于将测试集中在新的应用程序代码上至关重要,并避免测试其他模块或包。
15.3.3 应用次要功能
此应用程序的次要功能将 TOML 摘要转换为更易读的 HTML 或 PDF 文件。这个功能是 Jupyter Lab(及其相关工具如Jupyter {Book})所做报告类型的一种变体。
这两种类型的报告之间存在一个重要的区别:
-
Jupyter Lab 报告涉及发现。报告内容总是新的。
-
摘要应用程序的报告涉及对预期的确认。报告内容不应是新的或令人惊讶的。
在某些情况下,报告将被用来确认(或否认)预期的趋势是否持续。应用程序将趋势模型应用于数据。如果结果与预期不符,这表明需要采取后续行动。理想情况下,这意味着模型不正确,趋势正在变化。不太理想的情况是观察到提供源数据的应用程序中出现了意外的变化。
此应用程序将报告编写分解为三个不同的步骤:
-
内容:这是包含基本统计指标的 TOML 文件。
-
结构:次要功能在 Markdown 或 RST 格式中创建一个中间标记文件。它围绕基本内容有一个信息性的结构。
-
呈现:最终出版物文档是由结构化标记以及所需的任何模板或样式表创建的。
最终呈现与文档的内容和结构保持分离。
HTML 文档的最终呈现是由浏览器创建的。使用像 Pandoc 这样的工具将 Markdown 转换为 HTML 是——正确地——用另一种标记语言替换了另一种标记语言。
创建 PDF 文件要复杂一些。我们将把这个内容放在本章末尾的附加部分中。
创建格式良好的文档的第一步是从摘要创建初始 Markdown 或 ReStructuredText 文档。在许多情况下,这可以通过 Jinja 包最简单地完成。参见 jinja.palletsprojects.com/en/3.1.x/
一种常见的方法是以下步骤序列:
-
使用 Markdown(或 RST)编写报告的版本。
-
定位一个模板和样式表,当通过 Pandoc 或 Docutils 应用程序转换时,可以生成所需的 HTML 页面。
-
重构源文件,用 Jinja 占位符替换内容。这变成了模板报告。
-
编写一个解析 TOML 的应用程序,然后将 TOML 的详细信息应用到模板文件中。
当使用 Jinja 来启用模板填充时,它必须添加到 requirements.txt
文件中。如果使用 ReStructuredText (RST),那么 docutils 项目也非常有用,并且应该添加到 requirements.txt
文件中。
如果使用 Markdown 创建报告,那么 Pandoc 是处理从 Markdown 到 HTML 转换的一种方式。因为 Pandoc 还可以将 RST 转换为 HTML,所以不需要 docutils 项目。
因为解析后的 TOML 是一个字典,所以可以通过 Jinja 模板提取字段。我们可能有一个结构如下所示的 Markdown 模板文件:
# Summary of {{ summary[’source’][’name’] }}
Created {{ summary[’identification’][’date’] }}
Some interesting notes about the project...
## X-Variable
Some interesting notes about this variable...
Mean = {{ summary[’x’][’location’][’mean’] }}
etc.
{{`` some-expression`` }}
构造是占位符。这就是 Jinja 将评估 Python 表达式并将占位符替换为结果值的地方。由于 Jinja 的巧妙实现,名称 summary[’x’][’location’][’mean’]
也可以写成 summary.x.location.mean
。
带有 #
和 ##
的行是 Markdown 指定部分标题的方式。有关 Markdown 的更多信息,请参阅 daringfireball.net/projects/markdown/
。请注意,Markdown 有许多扩展,确保渲染引擎(如 Pandoc)支持您想要使用的扩展是很重要的。
Jinja 模板语言有许多选项用于条件性和重复的文档部分。这包括 {%`` for`` name`` in`` sequence`` %}
和 {%`` if`` condition`` %}
构造来创建极其复杂的模板。使用这些构造,单个模板可以用于许多密切相关的情况,并可选地包含特殊情况的章节。
将summary
对象中的值注入模板的应用程序不应比 Jinja 基础知识页面上的示例复杂得多。请参阅jinja.palletsprojects.com/en/3.1.x/api/#basics
以了解一些加载模板并注入值的示例。
这个程序的输出是一个名为summary_report.md
的文件。这个文件将准备好转换为大量其他格式。
将 Markdown 文件转换为 HTML 的过程由Pandoc应用程序处理。请参阅pandoc.org/demos.html
。命令可能像以下这样复杂:
pandoc -s --toc -c pandoc.css summary_report.md -o summary_report.html
pandoc.css
文件可以提供 CSS 样式,创建一个足够窄的正文,可以打印在普通的美国信函或 A4 纸上。
创建summary_report.md
文件的程序可以使用subprocess.run()
执行Pandoc应用程序并创建所需的 HTML 文件。这提供了一个命令行 UX,生成可读的文档,准备分发。
15.4 概述
在本章中,我们为构建和使用源数据的统计模型奠定了基础。我们探讨了以下主题:
-
设计和构建一个更复杂的流程,用于收集和分析数据。
-
创建某些数据统计模型背后的核心概念。
-
使用内置的
statistics
库。 -
发布统计测量的结果。
这个应用程序相对较小。各种统计值的实际计算利用了内置的statistics
库,并且通常非常小。它往往感觉在解析 CLI 参数值和创建所需的输出文件中涉及的编程比这个应用程序的“真正工作”要多得多。
这是我们一直在数据获取、清洗和分析中分离各种关注点的方式的结果。我们将工作划分为几个沿管道的独立阶段:
-
获取原始数据,通常是文本形式。这可能涉及数据库访问或 RESTful API 访问,或者复杂的文件解析问题。
-
清洗并将原始数据转换为更有用的、原生的 Python 形式。这可能涉及解析文本和拒绝异常值的问题。
-
总结和分析清洗后的数据。这可能侧重于数据模型,并报告关于数据的结论。
这里的想法是最终的应用程序可以随着我们对数据的理解成熟而增长和适应。在下一章中,我们将向总结程序添加功能,以深入了解可用数据。
15.5 补充内容
这里有一些想法,你可以添加到这个项目中。
15.5.1 形状度量
形状的测量通常涉及两个计算:偏度和峰度。这些函数不是 Python 内置的statistics
库的一部分。
重要的是要注意,存在大量独特、理解良好的数据分布。正态分布是数据可以分布的许多不同方式之一。
请参阅www.itl.nist.gov/div898/handbook/eda/section3/eda366.htm
。
偏度的一个度量如下:
其中Ȳ是均值,s是标准差。
对称分布将有一个接近零的偏度,g[1]。较大的数字表示与均值周围大量数据相对的“长尾”。
峰度的一个度量如下:
标准正态分布的峰度是 3。大于 3 的值表明尾部有更多数据;它比标准正态分布“更平坦”或“更宽”。小于三的值比标准分布“更高”或“更窄”。
这些指标可以添加到应用程序中,以计算一些额外的单变量描述性统计量。
15.5.2 创建 PDF 报告
在应用程序次要功能部分,我们探讨了创建包含基本内容、一些附加信息和组织结构的 Markdown 或 RST 文档。目的是使用Pandoc工具将 Markdown 转换为 HTML。浏览器可以渲染 HTML 以呈现易于阅读的摘要报告。
将此文档发布为 PDF 需要能够创建必要输出文件的工具。有两种常见选择:
-
使用ReportLab工具:
www.reportlab.com/dev/docs/
。这是一个包含一些开源组件的商业产品。 -
使用Pandoc工具与 LaTeX 处理工具相结合。
请参阅第十四章中的第十八部分“准备报告”,在项目 4.2:创建报告中,了解使用 LaTeX 创建 PDF 文件的一些额外想法。虽然这涉及到大量的独立组件,但它具有最强大功能的优势。
通常最好单独学习 LaTeX 工具。TeXLive 项目维护了多个用于渲染 LaTeX 的有用工具。对于 macOS 用户,MacTeX 项目提供了所需的二进制文件。Overleaf 等在线工具也适用于处理 LaTeX。通过创建小的hello_world.tex
示例文档来解决问题,以了解 LaTeX 工具的工作方式。
一旦 LaTeX 工具的基本功能正常工作,添加Pandoc工具到环境中是有意义的。
这两个工具都不是基于 Python 的,也不使用conda或pip安装程序。
如第十四章和项目 4.2:创建报告所述,这个工具链有很多组件。这需要管理大量单独的安装。然而,当从几个 CLI 交互创建最终的 PDF 时,结果可以非常出色。
15.5.3 从数据 API 提供 HTML 报告
在第十二章和项目 3.8:集成数据采集 Web 服务中,我们创建了一个 RESTful API 服务以提供清洗后的数据。
此服务可以扩展以提供其他几个功能。最显著的增加是 HTML 汇总报告。
创建汇总报告的过程看起来像这样:
-
用户请求给定时间段的汇总报告。
-
RESTful API 创建一个在后台执行的任务,并响应状态显示任务已创建。
-
用户定期检查以查看处理是否完成。一些巧妙的 JavaScript 编程可以在应用程序程序检查工作是否完成的同时显示动画。
-
一旦处理完成,用户可以下载最终报告。
这意味着需要将两个新的资源路径添加到 OpenAPI 规范中。这两个新资源是:
-
创建新汇总的请求。一个 POST 请求创建构建汇总的任务,一个 GET 请求显示状态。
2023.03/summarize
路径将与用于创建系列的2023.02/creation
路径并行。 -
请求汇总报告。一个 GET 请求将下载一个给定的统计汇总报告。也许一个
2023.03/report
路径是合适的。
随着我们向 RESTful API 添加功能,我们需要越来越仔细地考虑资源名称。第一波想法有时未能反映对用户需求增长的理解。
回顾起来,第十二章和项目 3.8:集成数据采集 Web 服务中定义的2023.02/create
路径可能不是最好的名称。
在创建资源请求和结果资源之间有一个有趣的紧张关系。创建系列的请求与结果系列明显不同。然而,它们都可以有意义地被视为“系列”的实例。创建请求是一种未来:一种将在以后得到满足的期望。
另一种命名方案是使用2023.02/creation
为系列命名,并使用2023.03/create/series
和2023.03/create/summary
作为不同的路径来管理执行工作的长时间运行的背景。
在后台执行的任务将执行多个步骤:
-
确定请求是否需要新数据或现有数据。如果需要新数据,则获取并清洗。这是获取一系列数据点的现有过程。
-
确定请求的总结是否已经存在。(对于新数据,当然不会存在。)如果需要总结,则创建它。
一旦处理完成,原始数据、清洗后的数据和总结都可以作为资源在 API 服务器上提供。用户可以请求下载这些资源中的任何一项。
在尝试将任务组件作为网络服务的一部分进行集成之前,确保每个组件都能独立工作是非常重要的。在复杂的网络服务世界之外,通过总结报告来诊断和调试问题要容易得多。
第十六章
项目 5.2:简单多元统计
变量是否相关?如果是,关系是什么?分析师试图回答这两个问题。一个否定的答案——零假设——不需要太多支持细节。另一方面,肯定的答案表明可以定义一个模型来描述这种关系。在本章中,我们将探讨简单相关性和线性回归作为描述变量之间关系建模的两个要素。
在本章中,我们将扩展数据分析的一些技能:
-
使用内置的
statistics
库来计算相关性和线性回归系数。 -
使用matplotlib库创建图像。这意味着在 Jupyter Lab 环境之外创建绘图图像。
-
在基础建模应用的基础上添加功能。
本章的项目将扩展早期项目。回顾第十三章项目 4.1:可视化分析技术,了解在 Jupyter Lab 环境中使用的某些图形技术。这些需要更加自动化。项目将添加多元统计和图表来展示变量之间的关系。
16.1 描述
在第十五章项目 5.1:建模基础应用中,我们创建了一个应用程序来生成包含一些核心统计信息的摘要文档。在那个应用程序中,我们研究了单变量统计来描述数据分布。这些统计包括对分布的位置、范围和形状的测量。均值、中位数、众数、方差和标准差等函数被强调为理解位置和范围的方法。通过偏度和峰度来描述形状则留作额外的练习。
前一章的基础应用需要扩展,包括对变量之间关系的澄清所必需的多元统计和图表。描述两个变量之间关系的可能函数数量众多。参见www.itl.nist.gov/div898/handbook/pmd/section8/pmd8.htm
了解可用的选择数量。
我们将限制自己使用线性函数。在最简单的情况下,创建线性模型有两个步骤:识别相关性并创建适合数据的线的系数。我们将在接下来的两个部分中查看这些步骤。
16.1.1 相关系数
相关系数衡量两个变量的值相互关联的程度。值为 1 表示完全相关;值为零表示没有可辨别的相关;值为-1 表示“反相关”:当一个变量达到最大值时,另一个变量处于最小值。
见图 16.1 了解相关系数如何描述两个变量的分布。
图 16.1:相关系数
系数的计算比较了变量的个别值,X[i]和Y[i],与这些变量的平均值,X和Ȳ。以下是一个公式:
均值X和Ȳ的计算可以分解到这一点,创建一个稍微复杂一些的版本,这种版本通常用于在单次通过数据时创建系数。
此功能在standard
库中作为statistics.correlation()
提供。
如果两个变量相互关联,那么一个线性函数将一个变量的值映射到另一个变量附近的值。如果相关系数为 1.0 或-1.0,映射将是精确的。对于其他相关系数值,映射将是接近的,但不是精确的。在下一节中,我们将展示如何将相关系数转换为直线的参数。
16.1.2 线性回归
一条线的方程是 y = mx + b。m和b的值是描述x和y变量之间特定线性关系的参数。
当拟合数据线时,我们正在估计线的参数。目标是使线与实际数据之间的误差最小化。“最小二乘”技术通常被使用。
两个系数,b和m,可以按以下方式计算:
这在标准库中作为statistics.linear_regression()
提供。这使我们免于编写这两个函数。
各种总和以及平方和并不是特别难以计算的值。内置的sum()
函数是大多数计算的基础。我们可以使用sum(map(lambda x: x**2, x_values))
来计算 ∑ X[i]²。
为了阐明这些多元关系,图表可以非常有帮助。在接下来的章节中,我们将探讨需要成为整体应用一部分的最重要类型的图表。
16.1.3 图表
显示多元数据的一个基本图表是 X-Y“散点”图。在第十三章,项目 4.1:可视化分析技术中,我们探讨了创建这些图表的方法。在那个章节中,我们依赖于 Jupyter Lab 将图表作为整体网页的一部分进行展示。对于这个应用,我们需要将图表嵌入到文档中。
这通常意味着将有一个包含对图表文件引用的标记文档。图表文件的格式可以是 SVG、PNG,甚至是 JPEG。对于技术图形,SVG 文件通常是最小的,并且缩放效果非常好。
每种标记语言,包括 Markdown、RST、HTML 和 LaTeX,都有独特的方式来标识需要插入图像的位置。在 Markdown 的情况下,通常需要使用 HTML 语法来正确地包括框架和标题。
现在我们已经看到了应用程序需要做什么,我们可以看看创建软件的方法。
16.2 方法
与上一个项目一样,这个应用程序分为两个不同的部分:
-
计算统计量并创建图表文件。
-
从模板中创建一个简化标记语言的报表文件,并插入详细信息。像Jinja这样的工具对此非常有帮助。
一旦有了标记语言(如 Markdown 或 RST)的报表文件,就可以使用像Pandoc这样的工具将标记文件转换为 HTML 页面或 PDF 文档。使用Pandoc这样的工具允许在最终格式选择上具有相当大的灵活性。它还允许以整洁、统一的方式插入样式表和页面模板。
作为标记语言的 LaTeX 语言提供了最全面的特性。然而,它的工作是具有挑战性的。像 Markdown 和 RST 这样的语言被设计为提供更少、更容易使用的特性。
这本书是用 LaTeX 编写的。
我们将探讨这个应用程序的三个方面:统计计算、创建图表,最后是创建包含图表的最终标记文件。我们将从对统计计算的快速回顾开始。
16.2.1 统计计算
统计摘要输出文件,在 TOML 表示法中,为每个变量及其变量的单变量统计提供了一个部分。
文件的这个部分看起来像以下 TOML 片段:
[x.location]
mean = 9.0
[x.spread]
variance = 11.0
[y.location]
mean = 7.5
[y.spread]
variance = 4.125
当解析时,x.location
和x.spread
的 TOML 语法创建了一个类似于以下 Python 对象片段的字典:
{
some metadata here...
’x’: {
’location’: {
’mean’: 9.0
},
’spread’: {
’variance’: 11.0
}
},
’y’: {
etc.
}
}
这种结构可以扩展以包括额外的位置和分布统计量。它还可以扩展以包括多元统计。statistics
模块有correlation()
和covariance()
函数,这使得包括这些度量变得容易。
对于变量较少的数据集,通常考虑一个包括变量之间所有协方差组合的矩阵。这导致这些附加统计量的两种替代表示:
-
为协方差矩阵单独划分一个部分。标签为
[covariance]
的部分可以跟随着嵌套的字典,包括所有变量的组合。由于协方差矩阵是对称的,不需要所有的n²组合;只需要n× (n− 1)个独特的值。 -
每个变量部分内的多元子部分。这意味着对于
x
变量,我们将有x.location
、x.spread
、x.covariance.y
和x.correlation.y
子部分。
对于变量较少的数据集,将协方差和相关性捆绑到给定变量的细节中似乎是合理的。在 Anscombe 的四重奏的情况下,只有两个变量,协方差和相关性似乎应该与其他统计数据放在一起。
对于具有更多变量的数据集,所有变量之间的协方差可能会变得令人困惑。在这些情况下,可能需要像寻找主成分这样的技术来将变量的数量减少到更易于管理的数量。在这种情况下,带有协方差和自相关的单独部分可能更有用。
结果模型通常是基于协方差矩阵的某些仔细思考的结果。因此,应该提供一个单独的 [model]
部分,其中包含有关模型结构和系数的一些详细信息。在线性模型的情况下,有两个系数,有时称为 β[0] 和 β[1]。我们称它们为 b 和 m。
对于 Python 3.11,包含的 tomllib
模块不会创建 TOML 格式的文件。因此,有必要正确格式化一个文本文件,该文件可以被 tomllib
模块解析。使用 Jinja 模板来做这件事是有帮助的。
16.2.2 分析图
图表必须首先创建。一旦创建,就可以将其包含在文档中。创建图表的过程几乎与 Jupyter Lab 中使用的方法相同。需要采取一些额外的步骤来将图表导出为可以导入到文档中的文件。
当在 Jupyter Lab 中工作时,需要一些加载数据的单元格来创建两个变量,x
和 y
,用于绘制值。在这些单元格之后,一个类似于以下示例的单元格将创建并显示一个散点图:
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
# Labels and Title
ax.set_xlabel(’X’)
ax.set_ylabel(’Y’)
ax.set_title(’Series I’)
# Draw Scatter
_ = ax.scatter(x, y)
这假设之前的单元格已经加载了干净的数据并提取了两个列表对象,x
和 y
,用于绘制值。
然而,上述代码示例并没有保存生成的 PNG 或 SVG 文件。要保存图形,我们需要执行两个额外的步骤。以下是创建来自图形的文件的代码行:
plt.savefig(’scatter_x_y.png’)
plt.close(fig)
将此单元格的代码转换为函数可能会有所帮助。这个函数有适当的类型注解,以便像 mypy 这样的工具可以确认类型被正确使用。它还可以有单元测试用例以确保它确实有效。
savefig()
函数将使用 PNG 格式写入包含图像的新文件。如果文件路径后缀是 ’.jpg’
,则将创建 SVG 格式的文件。
图形的尺寸由 figure()
函数定义。通常会有设计和页面布局考虑因素,这表明图形的适当大小。这个决定可以推迟,大小可以通过创建最终 PDF 文件或 HTML 页面时使用的标记来提供。然而,通常最好创建所需大小和分辨率的图形,以避免在最终出版物中发生任何意外的更改。
一旦创建了图表,Markdown 就需要引用图表的 PNG 或 SVG 文件,以便将其包含在文档中。我们将在下一节中查看一些此类示例。
16.2.3 在最终文档中包含图表
通过使用标记命令来显示图表应放置的位置,并提供有关标题和尺寸的其他信息,将图表包含在最终文档中。
Markdown 语言有一个整洁的格式,用于在文档中最简单的情况中包含图像:

根据样式表,这可能是完全可接受的。在某些情况下,图像的大小不适合其在文档中的作用。Markdown 允许直接使用 HTML 而不是
结构。包括图表通常如下所示:
<figure>
<img src="img/file.png"
alt="Alt text to include"
height="8cm">
<figcaption>Figure caption</figcaption>
</figure>
使用 HTML 可以通过引用 CSS 来对图像大小和位置进行更多控制。
当使用 RST 时,语法提供了更多选项,无需切换到 HTML。包括图表的方式如下:
.. figure:: path/to/file.png
:height: 8cm
:alt: Alt text to include
Figure caption
使用这种类型的标记技术可以提供相当大的自由度。报告的作者可以包括来自各种来源的内容。这可以包括不更改的样板文本、计算结果、基于计算的某些文本以及重要的图表。
标记的格式对最终文档的影响很小。浏览器渲染 HTML 的方式取决于标记和样式表,而不是源文件的格式。同样,在创建 PDF 文档时,这通常是通过 LaTeX 工具完成的,这些工具根据文档前言中的 LaTeX 设置创建最终文档。
现在我们有了方法,我们可以查看必须构建的可交付成果文件。
16.3 可交付成果
此项目有以下可交付成果:
-
文档位于
docs
文件夹中。 -
接受测试位于
tests/features
和tests/steps
文件夹中。 -
对位于
tests
文件夹中的模型模块类进行的单元测试。 -
将作为单元测试一部分的
csv_extract
模块测试的模拟对象。 -
对位于
tests
文件夹中的csv_extract
模块组件进行的单元测试。 -
一个扩展到 TOML 文件的总结的应用程序,包括带有图表的图形。
-
一个将 TOML 文件转换为包含总结的 HTML 页面或 PDF 文件的应用程序二级功能。
我们将更详细地查看一些这些可交付成果。我们将从创建接受测试的建议开始。
16.3.1 接受测试
如我们在前一章关于接受测试的章节中所述,接受测试,输出 TOML 文档可以被场景的Then
步骤解析和检查。由于本书中的示例是查看 Anscombe 的四重奏数据,因此为测试提取的数据子集实际上并没有太多意义。对于任何其他数据集,都应该提取子集并用于接受测试。
有时,提取用于验收测试的小子集是有帮助的。与其处理数百万行数据,不如处理几十行数据就足以确认应用程序已读取并总结了数据。数据应该是考虑的整个样本集的代表。
这个子集是测试套件的一部分;因此,它很少改变。这使得结果可预测。
该应用程序的次要功能——在 TOML 输出中扩展以添加广泛的 Markdown——也适用于文本文件。这使得通过读取和写入文本文件来确认正确行为的情况相对容易。在许多情况下,Then
步骤将查找结果文档的一些关键特征。它们可能会检查特定的部分标题或包含在模板文本中的几个重要关键词。当然,测试场景可以检查计算出的替换值,这些值是 TOML 摘要的一部分。
自动测试很难确认文档对潜在读者是有意义的。它不能确定所选的图形颜色是否使关系清晰。对于这种可用性测试,一个好的校对编辑器或可信赖的合作伙伴是必不可少的。
16.3.2 单元测试
对于创建图形的函数的单元测试做不了太多。它限于确认是否创建了 PNG 或 SVG 文件。自动测试很难“看”到图像以确保它有标题、轴标签和合理的颜色。
重要的是不要忽视确认输出文件已创建的单元测试用例。一个在 Jupyter 笔记本中看起来很棒的图形不会写入文件,除非 CLI 应用程序将图形保存到文件中。
对于某些应用,模拟plt
包函数以确保应用程序使用预期的参数值调用正确的函数是有意义的。请注意,模拟版本的plt.subplots()
可能需要返回一个包含多个Mock
对象的元组。
我们需要定义一个复杂的模拟对象集合来形成测试的固定装置。固定装置的创建可能看起来像以下示例:
@fixture
def mocked_plt_module(monkeypatch):
fig_mock = Mock()
ax_mock = Mock(
set_xlabel=Mock(),
set_ylabel=Mock(),
set_tiutle=Mock(),
scatter=Mock(),
)
plt_mock = Mock(
subplots=Mock(
return_value=(fig_mock, ax_mock)
),
savefig=Mock(),
close=Mock()
)
monkeypatch.setattr(summary_app, ’plt’, plt_mock)
return plt_mock, fig_mock, ax_mock
这个固定装置创建了三个模拟对象。plt_mock
是对整体plt
模块的模拟;它定义了三个将被应用程序使用的模拟函数。fig_mock
是对由subplots()
函数返回的图形对象的模拟。ax_mock
是对由subplots()
函数返回的轴对象的模拟。这个模拟的轴对象用于提供轴标签、标题并执行散点图请求。
然后这个由三个模拟对象组成的元组被测试如下:
def test_scatter(mocked_plt_module):
plt_mock, fig_mock, ax_mock = mocked_plt_module
summary_app.scatter_figure([sentinel.X], [sentinel.Y])
assert plt_mock.subplots.mock_calls == [call()]
assert plt_mock.savefig.mock_calls == [call(’scatter_x_y.png’)]
assert plt_mock.close.mock_calls == [call(fig_mock)]
这个测试函数评估应用程序的scatter_figure()
函数。然后测试函数确认来自plt
模块的各种函数是否使用预期的参数值被调用。
通过查看对ax_mock
对象的调用,可以继续测试以查看标签和标题请求是否按预期进行。这种详细程度——查看对坐标轴对象的调用——可能过于细致。当我们探索更改标题或颜色以更清晰地表达观点时,这些测试变得非常脆弱。
然而,使用模拟对象的整体使用有助于确保应用程序将创建所需的包含图像的文件。
16.4 摘要
在本章中,我们扩展了自动分析和报告,包括更多使用内置的statistics
库来计算相关性和线性回归系数。我们还利用了matplotlib库来创建揭示变量之间关系的图像。
自动报告的目标是设计用来减少手动步骤并避免遗漏或错误可能导致不可靠数据分析的地方。没有什么比重复使用上一期数据的图表更尴尬的了。在一系列分析产品中未能重建一个重要的笔记本是极其容易发生的。
自动化的程度需要得到极大的尊重。一旦报告应用程序构建并部署,就必须积极监控以确保其正常工作并产生有用、信息丰富的结果。分析工作从开发理解转变为监控和维护确认或拒绝该理解的工具。
在下一章中,我们将回顾从原始数据到经过精炼的应用程序套件的过程,这些应用程序获取、清理和总结数据。
16.5 补充内容
这里有一些想法供您添加到这个项目中。
16.5.1 使用 pandas 计算基本统计
pandas包提供了一套强大的工具用于数据分析。核心概念是创建包含相关样本的DataFrame
。需要安装pandas
包并将其添加到requirements.txt
文件中。
有将SeriesSample
对象序列转换为DataFrame
的方法。通常最好的方法是将每个pydantic对象转换为字典,然后从字典列表构建数据框。
想法类似于以下内容:
import pandas as pd
df = pd.DataFrame([dict(s) for s in series_data])
在这个例子中,series_data
的值是一系列SeriesSample
实例。
结果数据框中的每一列都将包含样本中的一个变量。给定此对象,DataFrame
对象的方法可以产生有用的统计数据。
例如,corr()
函数计算数据框中所有列之间的相关值。
cov()
函数计算数据框中列之间的成对协方差。
Pandas 不计算线性回归参数,但它可以创建各种描述性统计。
有关 Pandas 的更多信息,请参阅pandas.pydata.org
。
除了各种统计计算之外,这个包还设计用于交互式使用。它与 Juypyter Lab 工作得特别出色。感兴趣的读者可能想重新阅读第十三章,项目 4.1:可视化分析技术,使用 Pandas 而不是原生 Python。
16.5.2 使用 pandas 的 dask 版本
pandas包提供了一套强大的工具,用于进行数据分析。当数据量巨大时,同时处理数据集的一部分会有所帮助。Dask项目实现了pandas包,最大限度地提高了并发处理的机会。
dask
包需要安装并添加到requirements.txt
文件中。这将包括一个pandas
包,可以用来提高整体应用性能。
16.5.3 使用 numpy 进行统计
numpy包提供了一套用于在大数据数组上进行高性能处理的工具。这些基本工具通过统计和线性代数等众多库进行了增强。此包需要安装并添加到requirements.txt
文件中。
numpy包使用其自己的内部数组类型。这意味着SeriesSample
对象不会被直接使用。相反,可以为源序列中的每个变量创建一个numpy.array
对象。
转换可能看起来像以下这样:
import numpy as np
x = np.array(s.x for s in series_data)
y = np.array(s.y for s in series_data)
在这个例子中,series_data
的值是一系列SeriesSample
实例。
创建一个单一的多维数组也是合理的。在这种情况下,轴 0(即行)将是单个样本,轴 1(即列)将是样本中每个变量的值。
数组有mean()
等方法来返回值的平均值。当使用多维数组时,提供axis=0
参数是至关重要的,以确保结果来自对行集合的处理:
import numpy as np
a = np.array([[s.x, s.y] for s in series_data])
print(f"means = {a.mean(axis=0)}")
查看numpy.org/doc/stable/reference/routines.statistics.html#
使用最小二乘法计算直线的系数可能会令人困惑。numpy.linalg中的最小二乘求解器是一个非常通用的算法,可以应用于创建线性模型。numpy.linalg.lstsq()
函数期望一个包含“x”值的矩阵。结果将是一个与每个“x”矩阵长度相同的向量。“y”值也将是一个向量。
处理过程可能看起来像以下这样:
import numpy as np
A = np.array([[s.x, 1] for s in series_data])
y = np.array([s.y for s in series_data])
m, b = np.linalg.lstsq(A, y, rcond=None)[0]
print(f"y = {m:.1f}x + {b:.1f}")
A
的值是基于 x 值的小矩阵。y
的值是 y 值的简单数组。最小二乘算法返回一个包含系数、残差、源矩阵的秩和任何奇异值的四元组。在上面的例子中,我们只想得到系数向量,所以我们使用[0]
从四元组结果中提取系数值。
这进一步分解以提取最适合这组点的直线的两个系数。参见:numpy.org/doc/stable/reference/generated/numpy.linalg.lstsq.html
。
当处理非常大的数据集时,这种方法具有明显的优势。numpy 库非常快速,并设计用于扩展到极大规模的数据量。
16.5.4 使用 scikit-learn 进行建模
scikit-learn 库拥有大量专注于建模和机器学习的工具。这个库建立在 numpy 的基础上,因此需要安装这两个包。
数据需要转换为 numpy 数组。由于建模方法非常通用,假设可能有多个独立变量可以预测因变量的值。
转换可能看起来像以下这样:
import numpy as np
x = np.array([[s.x] for s in series_data])
y = np.array([s.y for s in series_data])
在这个例子中,series_data
的值是一系列 SeriesSample
实例。x
数组为每个样本使用一个非常短的向量;在这种情况下,只有一个值。它需要是一个向量,以便适合 scikit-learn 能够解决的广义最小二乘回归。
scikit-learn 库被设计成以非常通用的方式创建模型。模型不总是由一个具有系数和截距的简单直线定义的关系。正因为这种非常通用的建模方法,我们将创建 linear_model.LinearRegression
类的实例。该对象具有创建适合给定数据点的系数的方法。然后我们可以检查这些系数,或使用它们来插值新值。
代码可能看起来像以下这样:
from sklearn import linear_model
reg = linear_model.LinearRegression()
reg.fit(x, y)
print(f"y = {reg.coef_[0]:.1f}x + {reg.intercept_:.1f}")
线性模型的 coef_
属性是一个系数向量,其长度与 x
自变量值的每一行相同。即使行长度为 1,结果也是一个长度为 1 的向量。
由于它与 numpy 一起工作,它可以处理非常大的数据集。此外,scikit-learn 创建适合数据的模型的方法可以推广到多种机器学习方法。这通常是创建更丰富、更有用模型的下一步。
16.5.5 使用函数式编程计算相关性和回归
相关系数和直线的计算可以总结如下。首先,我们将定义一个函数 M(a;f()),该函数计算转换值序列的平均值。f() 函数将每个值 a[i] 进行转换。恒等函数 ϕ(a[i]) = a[i] 不进行任何转换:
我们还需要一个函数来计算变量 a 的标准差。
这允许我们在某些转换后定义一系列相关值作为平均值。
从这些个别值中,我们可以计算出相关系数 r[xy]。
除了上述值之外,我们还需要两个变量标准差的值。
从相关系数和两个标准差中,我们可以计算出直线的系数 m 和截距值 b。
这给出了方程 y = mx + b 的系数 m 和截距 b,该方程最小化了给定样本与直线之间的误差。这是通过一个高阶函数 M(a;f()) 和一个普通函数 S(a) 来计算的。这似乎并没有比其他方法有显著的改进。因为它使用了标准库函数和函数式编程技术,所以它可以应用于任何 Python 数据结构。这可以节省将数据转换为 numpy 数组对象的一步。
第十七章
下一步
从原始数据到有用信息的旅程才刚刚开始。通常还有更多步骤需要完成,才能获得可用于支持企业决策的见解。从这里开始,读者需要主动扩展这些项目,或者考虑其他项目。一些读者可能想要展示他们对 Python 的掌握,而另一些读者则可能更深入地研究探索性数据分析领域。
Python 被用于如此多的不同事物,以至于甚至很难建议一个深入理解语言、库以及 Python 各种使用方式的途径。
在本章中,我们将涉及一些与探索性数据分析相关的更多主题。本书中的项目只是日常需要解决的各种问题的一小部分。
每个分析师都需要在理解正在处理的企业数据、寻找更好的数据建模方法以及有效展示结果的方式之间平衡时间。这些领域都是知识技能的大领域。
我们将从回顾本书项目序列背后的架构开始。
17.1 整体数据处理
应用程序和笔记本的设计基于以下多阶段架构:
-
数据采集
-
数据检查
-
数据清洗;这包括验证、转换、标准化和保存中间结果
-
总结,以及数据建模的开始
-
创建更深入的分析和更复杂的统计模型
阶段如图 17.1所示相互配合。
图 17.1:数据分析流程
在这个流程中的最后一步当然不是最终的。在许多情况下,项目会从探索发展到监控和维护。模型将继续被确认,这将有一个漫长的尾巴。一些企业管理工作是这一持续确认的必要部分。
在某些情况下,长尾部分会被变化所中断。这可能会反映在模型的不准确性上。可能无法通过基本的统计测试。揭示变化及其原因正是企业管理工作对数据分析至关重要的原因。
这种分析长尾可能持续很长时间。责任可能会从分析师传递给分析师。利益相关者可能会来来去去。分析师通常需要花费宝贵的时间来证明一项正在进行的研究,以确认企业仍在正确的轨道上。
企业处理或软件的其他变化将导致分析处理工具的彻底失败。最显著的变化是对“上游”应用的变化。有时这些变化是软件的新版本。在其他时候,上游变化是组织性的,企业的一些基本假设需要改变。随着数据源的变化,此管道的数据采集部分也必须改变。在某些情况下,清洗、验证和标准化也必须改变。
由于支持工具——Python、JupyterLab、Matplotlib 等——的快速变化,定期重建和重新测试这些分析应用变得至关重要。requirements.txt
文件中的版本号必须与 Anaconda 发行版、conda-forge 和 PyPI 索引进行核对。变化的节奏和性质使得这项维护任务成为任何良好工程解决方案的必要部分。
企业监督和管理参与的想法有时被称为“决策支持”。我们将简要地看看数据分析和建模是如何作为决策者的服务的。
17.2 “决策支持”的概念
所有数据处理背后的核心概念,包括分析和建模,是帮助某个人做出决策。理想情况下,一个好的决策将基于可靠的数据。
在许多情况下,决策是由软件做出的。有时决策是简单的规则,用于识别不良数据、不完整的过程或无效的操作。在其他情况下,决策更为微妙,我们将“人工智能”这个术语应用于做出决策的软件。
虽然许多类型的软件应用做出了许多自动化决策,但最终——一个人仍然——对这些决策的正确性和一致性负责。这种责任可能表现为一个人审查决策的定期总结。
这个负责任的利益相关者需要了解应用程序软件所做的决策的数量和类型。他们需要确认自动化的决策反映了良好的数据以及所声明的政策、企业的治理原则以及企业运营中的任何法律框架。
这表明需要对元分析和更高层次的决策支持进行需求。操作数据被用来创建一个可以做出决策的模型。决策的结果成为关于决策过程的数据库;这需要分析和建模来确认操作模型的正确行为。
在所有情况下,最终消费者是需要数据来决定一个过程是否运行正确或是否存在需要纠正的缺陷的人。
多级数据处理的想法导致了对仔细跟踪数据源以了解数据的含义及其所应用的任何转换的想法。我们将探讨元数据主题,接下来。
17.3 元数据和来源的概念
数据集的描述包括三个重要方面:
-
数据的语法或物理格式和逻辑布局
-
数据的语义,或意义
-
数据来源,或数据的起源及其应用到的转换
数据集的物理格式通常使用知名文件格式的名称来总结。例如,数据可能以 CSV 格式存在。CSV 文件中列的顺序可能会改变,这导致需要具有标题或某些元数据来描述 CSV 文件中列的逻辑布局。
大部分这些信息都可以在 JSON 模式定义中进行枚举。
在某些情况下,元数据可能是一个具有列号、首选数据类型和列名的另一个 CSV 文件。我们可能有一个看起来像以下示例的二级 CSV 文件:
1,height,height in inches
2,weight,weight in pounds
3,price,price in dollars
这条元数据信息描述了一个包含相关数据的单独 CSV 文件的内容。这可以转换成 JSON 模式,以提供统一的元数据表示。
数据来源元数据有一系列更复杂的关系。PROV 模型(见[www.w3.org/TR/prov-overview/
](https://www.w3.org/TR/prov-overview/))描述了一个包括实体、代理和活动的模型,这些模型创建或影响了数据。在 PROV 模型中,存在许多关系,包括生成和派生,这些关系直接影响到正在分析的数据。
有几种方法可以序列化信息。PROV-N 标准提供了一个相对容易阅读的文本表示。PROV-O 标准定义了一个 OWL 本体,可以用来描述数据的来源。本体工具可以查询关系图,以帮助分析师更好地理解正在分析的数据。
鼓励读者查看https://pypi.org/project/prov/,了解描述数据来源的 PROV 标准的 Python 实现。
在下一节中,我们将探讨额外的数据建模和机器学习应用。
17.4 机器学习的下一步
我们可以在统计建模和机器学习之间划出一个大致的界限。这是一个热门的辩论话题,因为——从适当的距离来看——所有统计建模都可以描述为机器学习。
在本书中,我们划出了一个界限,以区分基于算法的方法,这些算法是有限的、确定的和有效的。例如,使用线性最小二乘法找到一个与数据匹配的函数的过程,通常可以精确地以封闭形式给出答案,不需要调整超参数,因此是可复制的。
即使在我们狭窄的“统计建模”领域内,我们也会遇到线性最小二乘法表现不佳的数据集。例如,最小二乘估计的一个显著假设是,所有自变量都完全已知。如果 x 值受到观测误差的影响,就需要更复杂的方法。
“统计建模”和“机器学习”之间的边界不是一个清晰、简单的区分。
我们将注意机器学习的一个特征:调整超参数。超参数的探索可能成为构建有用模型的一个复杂副主题。这个特性之所以重要,是因为统计模型和需要调整超参数的机器学习模型之间的计算成本跳跃。
这里是计算成本粗略范围上的两个点:
-
统计模型可能通过有限算法创建,将数据简化为几个参数,包括拟合数据的函数的系数。
-
机器学习模型可能涉及搜索不同的超参数值,以找到产生通过某些统计测试的模型组合。
超参数值的搜索通常涉及进行大量计算以创建模型的每个变体。然后进行额外的计算来衡量模型的准确性和通用效用。这两个步骤针对不同的超参数值迭代进行,以寻找最佳模型。这种迭代搜索可能会使某些机器学习方法的计算量变得很大。
这种开销和超参数搜索并不是机器学习的普遍特征。对于本书的目的来说,这是作者划定的界限,以限制项目的范围、复杂性和成本。
我们强烈建议您通过学习 scikit-learn 中可用的各种线性模型来继续您的研究项目。请参阅scikit-learn.org/stable/modules/linear_model.html
。
本书中的项目序列是创建从原始数据中获取有用理解的第一步。
订阅我们的在线数字图书馆,全面访问超过 7,000 本书籍和视频,以及领先的行业工具,帮助你规划个人发展并推进你的职业生涯。更多信息,请访问我们的网站。
第二十章:为什么订阅?
[nosep]通过来自超过 4,000 位行业专业人士的实用电子书和视频,节省学习时间,多花时间编码。根据你特别设计的技能计划提高你的学习效果。每月免费获得一本电子书或视频。全文搜索,方便快速获取关键信息。复制粘贴、打印和收藏内容
你知道 Packt 为每本书都提供电子书版本,包括 PDF 和 ePub 文件吗?你可以在 packt.com 升级到电子书版本,作为印刷书客户,你有权获得电子书副本的折扣。有关更多信息,请联系我们 customercare@packtpub.com。
在www.packtpub.com上,你还可以阅读一系列免费的技术文章,注册各种免费
你可能还会喜欢的其他书籍
如果你喜欢这本书,你可能还会对 Packt 的其他这些书籍感兴趣:
Causal Inference and Discovery in Python
Aleksander Molak
ISBN: 9781804612989
-
掌握因果推断的基本概念
-
揭开结构因果模型之谜
-
在 Python 中释放 4 步因果推断过程的强大功能
-
探索高级提升建模技术
-
使用 Python 解锁现代因果发现的秘密
-
使用因果推断为社会影响和社区利益做出贡献
Python for Geeks
Muhammad Asif
ISBN: 9781801070119
-
了解如何设计和管理复杂的 Python 项目
-
在 Python 中制定测试驱动开发(TDD)策略
-
探索 Python 中的多线程和多程序设计
-
使用 Apache Spark 和 Google Cloud Platform (GCP)进行 Python 数据处理
-
在 GCP 等公共云上部署无服务器程序
-
使用 Python 构建 Web 应用程序和应用程序编程接口
-
使用 Python 进行网络自动化和无服务器功能
-
掌握 Python 进行数据分析和机器学习
Python Data Analysis - 第三版
Avinash Navlani, Armando Fandango, Ivan Idris
ISBN: 9781789955248
-
探索数据科学及其各种流程模型
-
使用 NumPy 和 pandas 进行数据操作,以聚合、清理和处理缺失值
-
使用 Matplotlib、Seaborn 和 Bokeh 创建交互式可视化
-
以多种格式检索、处理和存储数据
-
使用 pandas 和 scikit-learn 了解数据预处理和特征工程
-
使用太阳黑子周期数据进行时间序列分析和信号处理
-
分析文本数据和图像数据以执行高级分析
-
使用 Dask 加速并行计算
Packt 正在寻找像您这样的作者
如果您有兴趣成为 Packt 的作者,请访问 authors.packtpub.com 并今天申请。我们已与成千上万的开发人员和科技专业人士合作,就像您一样,帮助他们将见解分享给全球科技社区。您可以提交一般申请,申请我们正在招募作者的特定热门话题,或者提交您自己的想法。
分享您的想法
您已经完成了《Python 实战项目》,我们非常乐意听到您的想法!如果您从亚马逊购买了这本书,请点击此处直接进入该书的亚马逊评论页面并分享您的反馈或在该购买网站上留下评论。
您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。
下载此书的免费 PDF 副本
感谢您购买此书!
您喜欢在路上阅读,但无法携带您的印刷书籍到处走吗?您的电子书购买是否与您选择的设备不兼容?
不要担心,现在每本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠远不止于此,您还可以获得独家折扣、时事通讯和每日收件箱中的优质免费内容。
按照以下简单步骤获取优惠:
-
扫描下面的二维码或访问以下链接
-
提交您的购买证明
-
就这些!我们将直接将您的免费 PDF 和其他优惠发送到您的电子邮件
第二十一章:目录
由于这个电子书版本没有固定的页码,下面的页码仅作为参考,基于本书的印刷版。
--zip 参数 120
-k kaggle.json 参数 120
-o 四重奏参数 120
API 请求
制作 129, 129
敏捷统一过程
阶段 11
参考链接 11
敏捷方法 11
安斯康姆四重奏
参考链接 119
后台进程 467
Beautiful Soup 数据结构 161, 162
Beautiful Soup 包 117
C4 模型
参考链接 353, 85, 125
命令行界面 (CLI) 应用程序 367
二氧化碳浓度 (PPM)
参考链接 255
二氧化碳浓度 (PPM) 285
创建、检索、更新和删除 (CRUD) 操作 200
拒绝服务 (DoS) 攻击 135
有向无环图 (DAG) 378
Docker 容器 218
Docutils 工具 556
不要重复自己 (DRY) 原则 251, 366
特征测试语言
参考链接 17
费舍尔-皮尔逊公式 562
Flask 489
HTML 请求
制作,使用 urllib.request 159, 161
HTML 抓取 161, 162
IEEE 1061 标准
软件质量 9
ISO 25010 标准
兼容性 4
可维护性 5
性能效率 3
可靠性 4
安全性 4
可用性 4
ISO 25010 标准 5
接口分离设计原则 473
JSON Schema
URL 316
发射 323
预期日期域,定义 326, 327, 328
用于中间文件验证 328, 330
使用 316
Jinja 包
参考链接 571
用于数据检查的 Jupyter 笔记本
方法 241, 244, 244, 247
数据源 236, 238, 239
可交付成果 252
描述 234, 235
函数测试用例 247, 247, 249
独立模块代码 250, 250, 252
Jupyter 笔记本用于数据检查,交付物
笔记本 .ipynb 文件 253, 253
笔记本测试套件,执行 259
Jupyter 笔记本
概述 496
Jupyter 笔记本,方法 502
Jupyter 笔记本,方法
PyPlot 图形 510, 513
Python 模块用于总结 508
笔记本的一般组织结构 507, 507
迭代和进化 517
Jupyter 笔记本,方法 504, 505, 506
Jupyter 笔记本,交付物
接受测试 522
单元测试 519, 521
Jupyter 笔记本,交付物 518
JupyterBook 312
Kaggle API
参考链接 122
Kaggle API 122
Kaggle 访问模块 154
Kaggle 重构主应用 154
MAD 273
Markdown 527
ND JSON 文件格式
参考链接 344, 82
美国国家标准与技术研究院 (NIST) 499
笔记本 .ipynb 文件
单元,带有 Markdown 257
单元,带有测试用例 258, 259
数据单元,分析 255, 255, 257
数据函数,分析 255, 256, 257
笔记本 .ipynb 文件 253, 253
ORM 层
条件 200
使用 337, 337
对象关系阻抗不匹配
参考链接 200
对象关系映射 (ORM) 问题 199, 201, 201
PROV
参考链接 624
PROV-N 标准 624
PROV-O 标准 624
PROV-概述
参考链接 624
PairBuilder 类层次结构 105, 106
Pandoc 工具 556
Pillow 包 59
PlantWEB 项目 541
PyPlot 图形
X-Y 散点图 516
数据频率直方图 514、515
PyPlot 图形 510、513
Pydantic V2 验证 362、363
Pydantic 类
定义 323、324、325
Pydantic 类 440
Pydantic 数据类 326
Pydantic 库 325
Pydantic 模块
参考链接 322
Pydantic 模块 318、322、322、326
Pydantic 包 335、439
Python Markdown 扩展 510
Python 包索引 (PyPI)200
Python 模块 290
Python 对象
序列化 60
Quarto
URL313
Quarto 313
RESTful API 流程 466
RESTful API 服务器,方法
处理状态的 GET 请求 475、476
结果的 GET 请求 477
OpenAPI 3 规范 468、469、470、471、472
POST 请求,处理 474、475
应用程序,处理 473、473
安全考虑 477、478、479
RESTful API 服务器,方法 464、467
RESTful API 服务器,交付成果
RESTful API 应用 484、485、486、488
接受测试用例 481、482、483、484
单元测试用例 488、489、490
RESTful API 服务器,交付成果 480
RESTful API 服务器,描述
下载数据,创建 461、463
数据系列资源 461
RESTful API 服务器,描述 458、460
RESTful 119
ReStructuredText (RST)572
RestAccess 类
API 请求,制作 129、129
ZIP 存档,下载 130、131
数据集列表,获取 131, 133, 134
功能 128
主要功能 138, 138
速率限制 135, 138
单元测试 141, 142, 143
Reveal.js 项目 528
Reveal.js 幻灯片集 542
SQL 数据定义 187, 188
SQL 数据操作 188
SQL 执行 189, 189, 190
SQL 提取,数据方法
SQL 数据库提取 208, 208, 209, 209, 210
与 SQL 相关的处理,区别于 CSV 处理 210, 211
SQL 提取,数据可交付成果
模拟数据库连接 213, 214, 215
接受测试,使用 SQLite 数据库 217, 217, 219
测试用的游标对象 213, 214, 215
数据库提取模块 226
定义 224, 224
功能文件 219, 220
模型模块,重构 226
sqlite 测试用例 220, 222, 223
单元测试,针对新获取模块 216
SQL 提取,数据
对象关系映射(ORM)问题 199, 201, 201
获取 196
方法 206, 207
可交付成果 212
描述 197, 197, 199
来源 202, 203, 205
SQLAlchemy
参考链接 200
Series_Value 表
加载 191, 192, 194
十二要素应用
特征 6, 7
参考链接 6
传输层安全(TLS)479
统一建模语言(UML)244
用户体验(UX)198, 342, 552, 77
WebTest 固定装置 489
WebTest 项目 489
工作池进程 466
ZIP 编码统计区域 (ZCTAs) 396
ZIP 归档
下载 130, 131
接受场景
错误 102
接受场景 102, 104
接受测试
特性 98
接受测试 166, 168, 169, 371, 372, 98, 99, 143, 144
获取应用 442
获取管道
创建 414
描述 414
获取管道,方法
包考虑,创建管道 418
获取管道,方法 416, 417
获取管道,可交付成果
接受测试 419, 420
获取管道,可交付成果 418
获取管道,描述
多次提取 415
获取
通过提取 61, 62
分析图 595, 595, 597
应用描述
用户体验 (UX) 77, 78
关键特性 76
输出数据 82, 84
源数据 78, 79, 81
应用描述 76
架构方法
类设计 88, 89, 89
设计原则 90
功能设计 95, 96
架构方法 85, 87
属性 186
基础应用
描述 550, 551, 553
基础应用,方法
清洗数据模型,使用 559
数据检查功能 560, 560, 561
分布,描述 556, 558
多变量统计 555
结果模型,创建 561, 563
摘要应用,设计 556, 556
单变量统计 555
基础应用程序,方法 553, 555
基础应用程序,交付成果
接受测试 566, 567
次级特征 569, 570, 572, 573
单元测试 568, 568, 569
基础应用程序,交付成果 563
behave 工具
after_scenario(context, scenario) 100
before_scenario(context, scenario) 99
behave 工具 16, 99
Bottle 框架 149
Bottle 工具 149
字节顺序标记 (BOM) 400
基数数据
子变体 267, 268
基数数据 266
基数域验证
计数 265, 266
持续时间 265, 266
度量 264, 266
基数数字 264
类设计 88, 89
清洁应用程序 442
clean_all() 函数
流程头部 369
流程中间 370
独立 369
流程尾部 370
命令行应用程序 (CLI) 75
命令行界面 (CLI) 4
兼容性 4
conda 318
施工阶段 20
核心检查工具 303
相关系数 587, 587
货币
处理 275
值,处理 275, 276
数据采集 59, 60
数据分析流程
阶段 66, 66
数据清洗应用程序
用户体验 (UX) 342, 343
转换和处理 347, 349, 350
描述 340, 342
错误报告 350, 351, 352
结果数据 346, 347
源数据 344
任务 340
数据清洗应用程序,方法
CLI 应用程序 367, 368
Pydantic V2 验证 362,363
增量设计 366,366
模型模块重构 357,358,359,360,361
验证函数设计 364
数据清理应用程序,方法 353,356,357
数据清理应用程序,交付成果
NDJSON 临时文件,创建 373
接受测试 371,372
数据应用,清理 373
模型特征的单元测试 372,373
数据清理应用程序,交付成果 370
数据合约 70
数据交付成果
Kaggle 访问模块和重构的主要应用程序 154
接受测试 143,144
功能文件 144,146,147
fixture.kaggle_server 151,152,153
模拟注入,针对请求包 148,149
模拟服务,创建 149,150,150,151
单元测试,针对 RestAccess 类 141,142,143
数据域
定义,在 JSON Schema 326,327,328
数据检查模块
扩展 292,293
数据检查
项目 63
数据检查 263,63,63,65
数据加载 184
数据持久化
方法 441,442,444
描述 435,437,439,440,440
数据持久化,方法
幂等操作,设计 444,447
数据持久化,交付成果
接受测试 448,450
可重运行的应用程序设计,清理 450,451
单元测试 448
数据持久化,交付成果 447,447
数据持久化 435
数据抓取方法
Beautiful Soup 数据结构 161, 162
HTML 请求,使用 urllib.request 制作 159, 161
HTML 抓取 161
数据抓取可交付成果
HTML 提取模块和重构主应用程序 169
接受测试 166, 168, 168
单元测试,针对 html_extract 模块 163, 165, 165
数据集列表
获得 131, 133, 134
数据集
参考链接 265
数据源引用
方法 398, 398, 399, 400, 400, 401, 402
可交付成果 403
描述 396, 397
数据源引用,可交付成果
数据收集的单元测试 404
数据验证的单元测试 404
数据验证 316
数据整理 618, 620
数据
从网络服务获取 119
从 SQL 提取获取 196
分析 67, 68
总结 67, 68
dataclass.asdict() 函数 89
datetime.strptime() 函数 290
决策支持 620
匿名化 350
可交付成果
接受场景 102, 104
接受测试 98, 99
列表 22
单元测试 104
可交付成果 96
设计原则
依赖倒置 91
接口隔离 91
Liskov 替换 90, 91
开放-封闭 90
单一职责 90
设计原则 90, 91, 93, 94
图表 591
领域特定语言 (DSL) 541
持续时间
处理 276, 277
探索性数据分析 (EDA) 499
扩展接受测试 332, 334
功能文件 144, 146
fixture.kaggle_server 151, 152, 153
Flask 工具 149
外键
领域 301
外键 299
功能设计 95
get_options() 函数 90
直方图 501
超参数调整 625
假设库 522
幂等操作
设计 444, 447
启动阶段 13, 14, 16
增量设计 365
初始目标
定义 16, 18, 18
检查模块
修订 294, 294, 308
单元测试用例 283, 284, 295, 296, 308, 309
检查模块 282
中间文件
验证,使用 JSON Schema 328, 330
区间
处理 276, 277
jsonschema 模块 318
键
收集 303, 304
比较 303, 304
计数,汇总 306
跳秒 291
librosa 包 60
线性回归 590, 591
本地 SQL 数据库方法
SQL 数据定义 186, 187, 188
SQL 数据操作 188
SQL 执行 189, 189, 190
本地 SQL 数据库
Series_Value 表,加载 191, 192, 194
方法 185, 186
数据加载 184
可交付成果 195, 196
描述 181
设计 182, 182
系列表,加载 190, 191
本地 SQL 数据库 180, 181
机器学习模型 626
机器学习
功能 625
机器学习 624, 626
main() 函数 90
可维护性 5
多对多关系 301
多对一关系 300
Markdown 单元
带有日期和数据源信息 311
遮蔽 350
matplotlib 库 585
matplotlib 包 510
模拟服务
创建 149, 150, 150, 151
模拟
注入,对于请求包 147, 149
模型模块 105
多变量统计
相关系数 587, 587
描述 586
图表 591
线性回归 588, 591
多变量统计,方法
分析图表 595, 596, 597
图表 597, 599
统计计算 593, 593, 595
多变量统计,方法 592
多变量统计,交付成果
接受测试 601
单元测试 603
单元测试 602
多变量统计,交付成果 599
多变量统计 555
mypy 插件 320
mypy 工具 358, 597
名义数据 291
归一化 199
笔记本函数
提取 278, 281, 281
笔记本
重构的检查模型,对于修订笔记本 309
笔记本测试套件
执行 259
一对一关系 300
异常值 269
性能效率 3
物理格式 622
pip-compile 命令 141
可移植性 5
演示材料 311, 312
主键
领域 301
主键 299
原始数据元数据 624
匿名化 350
pylint 工具 10
pytest 工具 16, 20
pytest-bdd 插件 17
查询参数 134
速率限制 135, 138
重构的检查模型
修订笔记本 309
参考域
发现 296, 298
关系
范数 300
可选性 300
可靠性 4
剩余组件 107
报告
准备 536, 539, 540
报告 530, 532
请求包
模拟,注入 147, 149
请求包 117
散点图 502
模式接收测试 331, 332
模式
定义 316
模式 315, 317
scikit-learn
参考链接 626
安全性 4
系列表
加载 191, 191
幻灯片集
创建 528, 530
描述 528
在 Jupyter Lab 中展示 528
幻灯片
准备 533, 535
软件组件
定义 19
软件测试
定义 19
源数据值
模式 269, 269
源数据 124, 124, 124
源字段
方法 383, 384, 385, 386
转换 379
可交付成果 387
描述 379, 380, 382
验证函数的单元测试 388
验证 379
针对解决方案
参考链接 20
标准化数据
接收测试 413, 414
方法 407, 408, 409, 411, 411
可交付成果 412, 412
描述 405, 406
标准化函数的单元测试 413
标准化数据 404, 405
状态设计模式 21
统计计算 593, 593, 595
统计建模 625
统计模型 625
标准输出
重定向 368
策略设计模式 21
建议的过程 10
目录 (TOC) 538
技术图表
创建 541, 542
文本和代码验证
UTC 时间 289
日期和时间 286, 287
本地时间 289
名义数据 285
序数 285
时间值 289
文本字段
方法 391, 393
可交付成果 394
描述 389, 390
验证函数的单元测试 395, 395
验证 389
分词 350
最高级()函数 90
过渡阶段 21
联合 350
单元测试用例
用于检查模块 283, 284
单元测试
PairBuilder 类层次结构 105, 106
对于 RestAccess 类 141, 142, 143
模型模块 104
剩余组件 107
单元测试 104
单变量统计 549, 555
世界协调时间 (UTC) 强制 289
urllib.request
使用,用于制作 HTML 请求 159, 161
可用性 4
验证测试 65
网页,数据抓取
方法 157, 159
可交付成果 162
描述 155
来源 156, 157
网页,数据抓取 154
网络服务,数据
获取 119
方法 125, 128, 128
可交付成果 140, 141
网络服务,数据描述 119, 122