Python-数据清理和准备最佳实践-全-
Python 数据清理和准备最佳实践(全)
原文:
annas-archive.org/md5/5532fd447031f1db26ab91548948a023译者:飞龙
前言
在今天这个快节奏的数据驱动的世界里,人们容易被人工智能(AI)突破性进展和先进的机器学习(ML)模型所吸引。但问问任何经验丰富的数据科学家或工程师,他们都会告诉你同样的事情:任何成功数据项目的真正基础不是炫目的算法或复杂的模型——而是数据本身, 更重要的是,如何准备这些数据。
在我的职业生涯中,我学到数据预处理是数据科学中的默默无闻的英雄。它是一个细致且常常复杂的过程,将原始数据转化为可靠的资产,准备好进行分析、建模,最终用于决策。我亲眼见证了正确的预处理技术如何改变一个组织对数据的处理方式,将潜在的挑战转化为强大的机会。
然而,尽管数据预处理如此重要,它常常被忽视或低估。许多人将其视为一个繁琐的步骤,一个拖慢构建模型和提供洞察力的瓶颈。但我一直认为,这个阶段才是最关键的工作阶段。毕竟,即便是最复杂的算法也无法弥补数据质量差的不足。这就是为什么我将自己大部分的职业生涯都奉献给了掌握这一艺术——探索最佳的工具、技术和策略,使得预处理更加高效、可扩展,并且与不断发展的 AI 领域保持一致。
本书旨在揭开数据预处理过程的神秘面纱,既提供传统方法的扎实基础,也展望了新兴技术的前景。我们将探讨如何利用 Python 更有效地清理、转换和组织数据。我们还将关注大语言模型(LLMs)的出现,如何重新定义这个领域的可能性。这些模型已经证明是改变游戏规则的工具,自动化了曾经需要手工完成且耗时的任务,并提供了提升数据质量和可用性的新方法。
在本书的过程中,我将分享我的经验、遇到的挑战和一路上学到的教训。我希望不仅能为你提供一个技术性的路线图,还能让你更深入理解数据预处理在当今数据生态系统中的战略重要性。我坚信“通过实践学习”的理念,因此本书包含了大量的代码示例,供你跟随学习。我鼓励你尝试这些示例,实验代码,并挑战自己将这些技术应用到你自己的数据集中。
在本书的结尾,你将具备足够的知识和技能,不仅将数据预处理视为一个必要步骤,而是将其视为你整体数据策略中的一个关键组成部分。
所以,无论你是数据科学家、工程师、分析师,还是仅仅希望提升数据处理理解的人,我邀请你与我一起踏上这段旅程。我们将共同探索如何利用数据预处理的力量,释放数据的全部潜力。
本书适用对象
本书适合具有 Python 基础知识、较好掌握统计概念,并有一定数据操作经验的读者。本书不会从零开始,而是建立在现有技能的基础上,向你介绍复杂的预处理策略、实践代码示例和实际练习,要求读者对数据科学和分析的核心原理有一定的熟悉程度。
本书内容
第一章,数据摄取技术,提供了关于数据摄取过程的全面概述,强调了它在从各种来源收集和导入数据到存储系统进行分析中的作用。你将探索不同的摄取方法,如批量模式和流模式,比较实时和半实时数据摄取,并了解数据源背后的技术。本章突出了这些方法的优缺点及其实际应用。
第二章,数据质量的重要性,强调了数据质量在商业决策中的关键作用。它突出了使用不准确、不一致或过时数据的风险,这可能导致错误的决策、损害声誉和错失机会。你将了解为什么数据质量至关重要,如何在不同维度上衡量数据质量,以及数据孤岛对维持数据质量的影响。
第三章,数据剖析——理解数据结构、质量和分布,探索了数据剖析,重点审视和验证数据集,以理解其结构、模式和质量。你将学习如何使用工具如 pandas Profiler 和 Great Expectations 进行数据剖析,并了解何时使用每个工具。此外,本章还涉及了处理大量数据的方法,并比较了不同的剖析方法,以提高数据验证的效果。
第四章,清理混乱的数据和数据操作,重点介绍了清理和操作数据的关键策略,帮助实现高效准确的分析。内容包括重命名列、删除无关或冗余数据、修正不一致的数据类型和处理日期时间格式等技术。掌握这些方法后,你将学会如何提升数据集的质量和可靠性。
第五章,数据转换 – 合并与连接,探索了通过合并、连接和拼接数据集来转换和操作数据的技术。它涵盖了从多个来源合并数据集、有效处理重复数据并提高合并性能的方法。本章还提供了一些实用技巧,以简化合并过程,确保数据高效集成,便于洞察分析。
第六章,数据分组、聚合、过滤与应用函数,涵盖了数据分组和聚合的基本技术,这对于总结大数据集并生成有意义的洞察至关重要。本章讨论了通过聚合值、减少数据量和提高处理效率来处理缺失或噪声数据的方法。它还重点讲解了如何通过不同的键对数据进行分组、应用聚合和自定义函数,以及过滤数据,以创建有价值的特征,供深入分析或机器学习使用。
第七章,数据接收端,聚焦于数据处理中的关键决策,特别是选择适合存储和处理需求的数据接收端。它深入探讨了四个基本要素:选择合适的数据接收端、选择正确的文件类型、优化分区策略,以及理解如何设计一个可扩展的在线零售数据平台。本章为你提供了提高数据处理管道效率、可扩展性和性能的工具。
第八章,检测与处理缺失值与异常值,深入探讨了识别和处理缺失值及异常值的技术。它介绍了从统计方法到先进的机器学习模型的多种方法,以有效解决这些问题。本章的重点领域包括检测和处理缺失数据、识别单变量和多变量异常值,以及管理各种数据集中的异常值。
第九章,归一化与标准化,涵盖了诸如特征缩放、归一化和标准化等关键预处理技术,这些技术确保机器学习模型能够有效地从数据中学习。你将探索包括将特征缩放到某个范围、Z-score 缩放和使用鲁棒缩放器等不同技术,以应对机器学习任务中的各种数据挑战。
第十章,处理类别特征,讲解了管理类别特征的重要性,类别特征代表数据集中的非数值信息。你将学习多种编码技术,包括标签编码、一热编码、目标编码、频率编码和二进制编码,以将类别数据转化为机器学习模型可以使用的格式。
第十一章,时间序列数据分析,深入探讨了时间序列分析的基础知识,涵盖了各行业中的关键概念、方法论和应用。内容包括理解时间序列数据的组成部分和类型,识别和处理缺失值,以及分析趋势和模式的技术。本章还讲解了如何处理异常值和特征工程,以提高时间序列数据预测建模的效果。
第十二章,LLM 时代的文本预处理,重点介绍了掌握文本预处理技术,这些技术对于优化 LLMs(大语言模型)的性能至关重要。它涵盖了清洗文本、处理稀有词汇和拼写变化、分块以及分词策略的方法。此外,它还讨论了将词元转换为嵌入向量的过程,强调了调整预处理方法以最大化 LLMs 潜力的重要性。
第十三章,使用 LLMs 进行图像和音频预处理,探讨了针对非结构化数据,特别是图像和音频的预处理技术,以提取有意义的信息。它包括图像预处理方法,如 光学字符识别(OCR)和使用 BLIP 模型生成图像描述。本章还探讨了音频数据处理,包括使用 Whisper 模型将音频转换为文本,全面介绍了在 LLMs 背景下处理多媒体数据的相关内容。
为了最大限度地发挥本书的效用
为了充分利用本书的内容,你应该具备良好的 Python 基础,并掌握数据工程和数据科学的基本知识。
| 本书涉及的软件/硬件 | 操作系统要求 |
|---|---|
| Python 3 | Windows、macOS 或 Linux |
| Visual Studio Code(或你首选的 IDE) |
如果你使用的是本书的电子版,我们建议你自己输入代码,或者通过书中的 GitHub 仓库访问代码(链接会在下一个章节提供)。这样可以避免复制和粘贴代码时可能出现的错误。
GitHub 仓库遵循本书的章节结构,所有脚本按照每个章节中的部分进行编号。每个脚本是独立的,因此你可以无需事先运行所有脚本就继续往前进行。然而,强烈建议按照书中的流程进行操作,以确保你不会遗漏任何必要的信息。
下载示例代码文件
你可以从 GitHub 上下载本书的示例代码文件,链接为 github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices。如果代码有更新,GitHub 仓库会同步更新。
我们的丰富图书和视频目录中还有其他代码包,您可以访问github.com/PacktPublishing/查看。
使用的约定
本书中使用了多种文本约定。
文本中的代码:表示文本中的代码字、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。示例:“delete_entry() 函数用于删除条目,展示了如何从存储中删除数据”
代码块设置如下:
def process_in_batches(data, batch_size):
for i in range(0, len(data), batch_size):
yield data[i:i + batch_size]
当我们希望您注意代码块中的某个特定部分时,相关的行或项目会使用粗体显示:
user_satisfaction_scores = [random.randint(1, 5) for _ in range(num_users)]
任何命令行输入或输出如下所示:
$ mkdir data
pip install pandas
粗体:表示新术语、重要单词或您在屏幕上看到的词。例如,菜单或对话框中的单词以粗体显示。示例:“它涉及将数据存储在远程服务器上,可以通过互联网从任何地方访问,而不是在 本地设备上”
提示或重要注意事项
如此显示。
保持联系
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请通过电子邮件联系我们:customercare@packtpub.com,并在邮件主题中注明书名。
勘误:尽管我们已尽力确保内容的准确性,但错误仍然会发生。如果您在本书中发现错误,我们将非常感激您向我们报告。请访问www.packtpub.com/support/errata并填写表单。
盗版:如果您在互联网上发现我们的作品的非法复制版本,我们将非常感激您提供该位置地址或网站名称。请通过版权@packt.com 与我们联系,并附上该材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且有兴趣写书或为书籍做贡献,请访问authors.packtpub.com。
分享您的想法
一旦您阅读完《Python 数据清理与准备最佳实践》,我们很想听听您的想法!请点击此处直接进入 Amazon 书评页面并分享您的反馈。
您的书评对我们和技术社区都非常重要,将帮助我们确保提供优质内容。
下载本书的免费 PDF 版本
感谢您购买本书!
您是否喜欢在旅途中阅读,但又无法随身携带纸质书籍?
你的电子书购买是否与所选设备不兼容?
别担心,现在每本 Packt 书籍都会附带该书的 DRM-free PDF 版本,免费提供。
在任何地方、任何设备上阅读。搜索、复制并粘贴你最喜欢的技术书籍中的代码,直接应用到你的应用程序中。
优惠不仅仅如此,你还可以获得独家折扣、时事通讯和每天发送到你邮箱的精彩免费内容
按照这些简单的步骤即可享受福利:
- 扫描二维码或访问下面的链接

packt.link/free-ebook/9781837634743
-
提交你的购买证明
-
就是这样!我们会直接将免费的 PDF 和其他福利发送到你的邮箱
第一部分:上游数据摄取与清理
本部分聚焦于数据处理的基础阶段,从数据摄取开始,确保数据的质量和结构,以便于后续任务的处理。它指导读者完成导入、清理和转换数据的关键步骤,为有效的数据分析奠定基础。章节内容涵盖了多种数据摄取方法、如何保持高质量的数据集、如何进行数据剖析以获得更好的见解,并且如何清理杂乱数据以使其准备好进行分析。此外,还涉及了如合并、连接、分组和过滤数据等高级技术,同时介绍了如何选择合适的数据接收端或存储地,以优化处理管道。本部分的每个章节都为读者提供了处理原始数据、将其转化为干净、结构化且可用形式的知识。
本部分包含以下章节:
-
第一章**,数据摄取技术
-
第二章**,数据质量的重要性
-
第三章**,数据剖析 – 理解数据结构、质量和分布
-
第四章**,清理杂乱数据与数据操作
-
第五章**,数据转换 – 合并与连接
-
第六章**,数据分组、聚合、过滤与应用函数
-
第七章**,数据接收端
第一章:数据摄取技术
数据摄取是数据生命周期的一个关键组成部分,它为随后的数据转换和清理奠定了基础。它涉及从各种来源收集和导入数据到存储系统的过程,数据可以在此系统中访问和分析。有效的数据摄取对于确保数据的质量、完整性和可用性至关重要,这直接影响数据转换和清理过程的效率和准确性。在本章中,我们将深入探讨不同类型的数据源,探索各种数据摄取方法,并讨论它们各自的优缺点以及实际应用。
本章将涵盖以下主题:
-
批量模式的数据摄取
-
流式模式的数据摄取
-
实时摄取与半实时摄取
-
数据源技术
技术要求
你可以在以下 GitHub 仓库中找到本章的所有代码:
github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/tree/main/chapter01
你可以使用你喜欢的 IDE(VS Code、PyCharm、Google Colab 等)来编写和执行代码。
批量模式的数据摄取
批量摄取是一种数据处理技术,其中大量数据在预定的时间间隔内收集、处理并加载到系统中,而不是实时进行。通过将数据分成批次处理,组织可以高效地处理大量数据。例如,一家公司可能会在一天内收集客户交易数据,然后在非高峰时段将其作为一个批次处理。这种方法对于需要处理大量数据但不要求即时分析的组织特别有用。
批量摄取有益于优化系统资源,通过将处理负载分配到计划好的时间,通常是在系统空闲时进行。这减少了计算资源的压力,并且可以降低成本,尤其是在基于云的环境中,计算能力是按使用量计费的。此外,批处理简化了数据管理,因为它允许对大规模数据集应用一致的转换和验证。对于数据流规律可预测的组织,批量摄取提供了一种可靠、可扩展且具有成本效益的数据处理和分析解决方案。
让我们更详细地探讨批量摄取,从其优点和缺点开始。
优势与劣势
批量摄取提供了几个显著的优势,使其成为许多数据处理需求的有吸引力的选择:
-
效率是一个关键优势,因为批处理允许在一次操作中处理大量数据,优化了资源使用并最小化了开销。
-
成本效益是另一个好处,减少了对连续处理资源的需求,降低了运营成本。
-
简单性使得相对于实时摄取,更容易管理和实现周期性数据处理任务,后者通常需要更复杂的基础设施和管理。
-
鲁棒性,因为批处理非常适合进行复杂的数据转换和全面的数据验证,确保高质量、可靠的数据。
然而,批处理摄取也带来一些缺点:
-
数据生成和可用性之间存在固有的延迟,对于需要实时洞察力的应用程序来说,这可能是一个关键问题。
-
批处理窗口期间可能会出现资源峰值,导致高资源使用率和潜在的性能瓶颈。
-
可伸缩性也可能是一个问题,处理非常大的数据集可能需要大量的基础设施投资和管理。
-
最后,维护是批处理摄取的一个关键方面;它需要仔细的调度和持续的维护,以确保批处理作业的及时可靠执行。
让我们来看看批处理模式中数据摄取的一些常见用例。
批处理摄取的常见用例
任何数据分析平台,如数据仓库或数据湖,都需要定期更新的数据用于商业智能(BI)和报告。批处理摄取至关重要,因为它确保数据始终使用最新信息进行更新,使企业能够进行全面和最新的分析。通过批处理处理数据,组织可以高效处理大量的交易和运营数据,将其转换为适合查询和报告的结构化格式。这支持 BI 倡议,允许分析师和决策者生成深刻见解的报告,跟踪关键绩效指标(KPIs),并做出数据驱动的决策。
提取、转换和加载(ETL)过程是数据集成项目的基石,批量摄取在这些工作流程中起着关键作用。在 ETL 过程中,数据从各种来源提取,经过转换以适应目标系统的操作需求,然后加载到数据库或数据仓库中。批处理允许有效处理这些步骤,特别是在处理需要大量转换和清洗的大型数据集时。这种方法非常适合周期性数据整合,将来自不同系统的数据集成为统一视图,支持数据迁移、系统集成和主数据管理等活动。
批量数据导入在备份和归档中也得到了广泛应用,这些过程对于数据的保存和灾难恢复至关重要。定期的批处理允许按计划备份数据库,确保所有数据都能在定期间隔内被捕获并安全存储。这种方法最大限度地减少了数据丢失的风险,并在系统故障或数据损坏时提供可靠的恢复点。此外,批处理还用于数据归档,将历史数据定期从活动系统转移到长期存储解决方案中。这不仅有助于管理存储成本,还能确保重要数据被保留并可供合规、审计或历史分析使用。
批量数据导入的使用案例
批量数据导入是一个系统化的过程,涉及多个关键步骤:数据提取、数据转换、数据加载、调度和自动化。为了说明这些步骤,让我们以一个投资银行为例,探讨它如何处理和分析交易数据以确保合规性和生成绩效报告。
投资银行的批量数据导入
投资银行需要从多个金融市场收集、转换并加载交易数据到一个中央数据仓库。这些数据将用于生成每日合规报告、评估交易策略以及做出明智的投资决策。
数据提取
第一步是识别数据提取的来源。对于投资银行来说,这包括交易系统、市场数据提供商和内部风险管理系统。这些来源包含关键数据,如交易执行细节、市场价格和风险评估。一旦确定了来源,数据就会通过连接器或脚本进行收集。这涉及建立数据管道,从交易系统提取数据,导入实时市场数据流,并从内部系统提取风险指标。提取的数据将暂时存储在暂存区,待处理。
数据转换
提取的数据通常包含不一致、重复和缺失值。数据清洗会移除重复项、填补缺失信息并纠正错误。对于投资银行来说,这确保了交易记录的准确性和完整性,为合规报告和绩效分析提供了可靠的基础。清洗后,数据会进行如聚合、连接和计算等转换。例如,投资银行可能会聚合交易数据以计算每日交易量,连接交易记录与市场数据以分析价格波动,并计算关键指标如盈亏(P&L)和风险暴露。转换后的数据必须映射到目标系统的模式中。这涉及将数据字段与数据仓库的结构对齐。例如,交易数据可能会映射到代表交易、市场数据和风险指标的表中,确保与现有数据模型的无缝集成。
数据加载
转换后的数据以批处理方式进行处理,这使得投资银行能够高效地处理大量数据,在一次运行中执行复杂的转换和聚合。一旦处理完成,数据会被加载到目标存储系统中,如数据仓库或数据湖。对于投资银行来说,这意味着将清洗和转换后的交易数据加载到他们的数据仓库中,从而可以用于合规报告和绩效分析。
调度与自动化
为了确保批量摄取过程顺利且一致地运行,通常会使用调度工具,如 Apache Airflow 或 Cron 作业。这些工具自动化数据摄取工作流,并安排在固定的时间间隔运行,例如每晚或每天一次。这使得投资银行能够在没有人工干预的情况下获得最新的数据以供分析。实施监控至关重要,用于跟踪批处理作业的成功与性能。监控工具提供作业执行的洞察,帮助识别任何失败或性能瓶颈。对于投资银行来说,这确保了数据摄取过程中出现的任何问题都能及时被发现并解决,从而维护数据管道的完整性和可靠性。
带示例的批量摄取
让我们来看一个用 Python 编写的简单批处理摄取系统示例。这个示例将模拟 ETL 过程。我们将生成一些模拟数据,按批处理方式处理它,并将其加载到模拟数据库中。
你可以在 GitHub 仓库的github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter01/1.batch.py中找到这部分代码。要运行这个示例,我们不需要安装任何特别的库,只需要确保在标准的 Python 环境(Python 3.x)中运行即可。
-
我们创建一个
generate_mock_data函数,用于生成模拟数据记录的列表:def generate_mock_data(num_records): data = [] for _ in range(num_records): record = { 'id': random.randint(1, 1000), 'value': random.random() * 100 } data.append(record) return data A list of dictionaries is returned, each representing a data record. -
接下来,我们创建一个批处理函数:
def process_in_batches(data, batch_size): for i in range(0, len(data), batch_size): yield data[i:i + batch_size]该函数将数据(数据记录的列表)和
batch_size(每个批次的记录数)作为参数。该函数使用for循环以batch_size为步长遍历数据。使用yield关键字生成每个大小为batch_size的数据批次,返回一个生成器来产生数据批次。 -
我们创建一个
transform_data函数,用于转换批次中的每一条记录:def transform_data(batch): transformed_batch = [] for record in batch: transformed_record = { 'id': record['id'], 'value': record['value'], 'transformed_value': record['value'] * 1.1 } transformed_batch.append(transformed_record) return transformed_batch该函数的参数是批次,它是一个需要转换的数据记录列表。转换逻辑很简单:每个记录中添加一个新的
transformed_value字段,它是原始值乘以 1.1。最后,我们得到了一个转换后的记录列表。让我们看看一些转换后的记录:{'id': 558, 'value': 12.15160339587219, 'transformed_value': 13.36676373545941} {'id': 449, 'value': 99.79699336555473, 'transformed_value': 109.77669270211021} {'id': 991, 'value': 79.65999078145887, 'transformed_value': 87.62598985960477} -
接下来,我们创建一个
load_data函数来加载数据。这个函数模拟将每个转换后的记录加载到数据库中的过程:def load_data(batch): for record in batch: # Simulate loading data into a database print(f"Loading record into database: {record}")该函数以批次作为参数,批次是一个准备加载的转换后的数据记录列表。每条记录都会打印到控制台,以模拟将其加载到数据库中。
-
最后,我们创建一个
main函数。这个函数调用了所有前面提到的函数:def main(): # Parameters num_records = 100 # Total number of records to generate batch_size = 10 # Number of records per batch # Generate data data = generate_mock_data(num_records) # Process and load data in batches for batch in process_in_batches(data, batch_size): transformed_batch = transform_data(batch) print("Batch before loading:") for record in transformed_batch: print(record) load_data(transformed_batch) time.sleep(1) # Simulate time delay between batches这个函数调用
generate_mock_data来生成模拟数据,并使用process_in_batches将数据分成批次。对于每个批次,函数执行以下操作:-
使用
transform_data转换批次 -
打印批处理,以显示加载前的内容
-
使用
load_data模拟加载批次
-
现在,让我们从批处理转向流式处理模式。在流式处理中,数据在到达时被处理,而不是在预定义的批次中处理。
以流式模式摄取数据
流式摄取是一种数据处理技术,通过该技术,数据在生成时实时收集、处理并加载到系统中。与批处理摄取不同,批处理摄取是在预定的时间间隔内积累数据进行处理,流式摄取则是持续处理数据,允许组织立即分析和采取行动。例如,一家公司可以在客户交易发生时就处理交易数据,从而实现实时的洞察和决策。这种方法对于那些需要实时数据分析的组织尤其有用,比如金融交易、欺诈检测或传感器数据监控等领域。
流数据摄取具有优势,因为它可以实现数据的即时处理,减少延迟并使组织能够快速响应变化的环境条件。这在及时响应至关重要的场景中尤其有益,比如检测异常、个性化用户体验或应对实时事件。此外,流式处理可以通过将处理负载均匀地分布在时间上,而不是集中在特定的批处理窗口内,从而提高资源利用效率。在基于云的环境中,这也可以转化为成本节约,因为可以动态扩展资源以匹配实时数据流。对于数据流不规则或不可预测的组织,流数据摄取提供了一种灵活、响应迅速且可扩展的数据处理和分析方法。让我们来看一下它的一些优点和缺点。
优势与劣势
流数据摄取具有几个明显的优势,使其成为某些数据处理需求的必要选择:
-
其中一个主要的好处是能够从数据中获取实时洞察。这种即时性对于诸如欺诈检测、实时分析和动态定价等应用至关重要,其中及时的数据是至关重要的。
-
流数据摄取支持连续的数据处理,使系统能够在数据到达时进行处理,从而减少延迟并提高响应能力。
-
这种方法具有高度可扩展性,能够有效管理来自多个来源的高速数据流而不会产生显著延迟。
然而,流数据摄取也面临一些挑战:
-
实施流数据摄取系统可能是复杂的,需要精密的基础设施和专门的工具来有效管理数据流。
-
持续处理需要不断的计算资源,这可能是昂贵且资源密集型的。
-
在流式环境中确保数据一致性和准确性可能很困难,因为数据不断涌入,并且可能会出现乱序或重复记录。
让我们来看一下批量模式下数据摄取的常见应用场景。
流数据摄取的常见应用场景
虽然批量处理非常适合周期性的大规模数据更新和转换,但流数据摄取对实时数据分析和需要即时洞察的应用至关重要。以下是流数据摄取的常见应用场景。
实时欺诈检测和安全监控
金融机构使用流数据来实时检测欺诈活动,分析交易数据。即时异常检测帮助在欺诈造成重大损失之前予以阻止。流数据还用于网络安全领域,以便立即检测和应对威胁。对网络流量、用户行为和系统日志的持续监控有助于在发生安全漏洞时进行识别和缓解。
物联网和传感器数据
在制造业中,来自机器设备传感器的流式数据可以实现预测性维护。通过持续监控设备健康状况,公司能够防止设备故障并优化维护计划。
物联网和传感器领域的另一个有趣应用是智慧城市。来自城市中各种传感器(如交通、天气、污染等)的流式数据有助于实时管理城市运营,改善交通管理、应急响应等服务。
在线推荐与个性化
流式数据使电子商务平台能够根据用户当前的浏览和购买行为,向用户提供实时推荐。这提升了用户体验,并增加了销售额。像 Netflix 和 Spotify 这样的平台利用流式数据在用户与服务互动时更新推荐,实时提供个性化内容建议。
金融市场数据
股票交易员依赖流式数据获取关于股价和市场状况的最新信息,从而做出明智的交易决策。自动化交易系统利用流式数据根据预设标准执行交易,这需要实时数据处理以确保最佳性能。
电信
电信公司使用流式数据实时监控网络性能和使用情况,确保服务质量最佳并能迅速解决问题。流式数据还帮助跟踪客户互动和服务使用情况,实现个性化客户支持,提升整体体验。
实时物流与供应链管理
来自 GPS 设备的流式数据使物流公司能够实时追踪车辆位置并优化路线,提高交付效率。实时库存跟踪帮助企业维持最佳库存水平,减少过度库存和缺货现象,同时确保及时补货。
电子商务平台中的流式数据摄取
流式数据摄取是一个有序的过程,涉及多个关键步骤:数据提取、数据转换、数据加载以及监控与告警。为了说明这些步骤,让我们探讨一个电子商务平台的用例,该平台需要实时处理和分析用户活动数据,以实现个性化推荐和动态库存管理。
一个电子商务平台需要收集、转换并加载来自各种来源的用户活动数据,如网站点击、搜索查询和购买交易,将其导入到中央系统中。这些数据将用于生成实时个性化推荐、监控用户行为,并动态管理库存。
数据提取
第一步是识别数据将从哪些来源提取。对于电子商务平台,这些来源包括 Web 服务器、移动应用程序和第三方分析服务。这些来源包含关键信息,如用户点击、搜索查询和交易详情。一旦确定了数据来源,就可以使用流式连接器或 API 进行数据采集。这涉及到设置数据管道,从 Web 服务器、移动应用程序和分析服务中实时提取数据。提取的数据随后会流式传输到如 Apache Kafka 或 AWS Kinesis 等处理系统。
数据转换
提取的数据通常包含不一致和噪音。实时数据清洗会过滤掉不相关的信息,处理缺失值并纠正错误。对于电子商务平台来说,这确保了用户活动记录的准确性,并且对分析有意义。清洗之后,数据会进行诸如解析、丰富和聚合等转换。例如,电子商务平台可能会解析用户点击流数据,以识别浏览模式,用产品详情丰富交易数据,并聚合搜索查询以识别流行的产品。转换后的数据必须映射到目标系统的架构中。这涉及到将数据字段与实时分析系统的结构对齐。例如,用户活动数据可能会映射到表示会话、产品和用户档案的表格中,确保与现有数据模型的无缝集成。
数据加载
转换后的数据通过 Apache Flink 或 Apache Spark Streaming 等工具持续处理。持续处理使电子商务平台能够高效地处理高速数据流,实时执行转换和聚合。一旦处理完成,数据会被加载到目标存储系统中,如实时数据库或分析引擎,在那里可以进行个性化推荐和动态库存管理。
监控与告警
为确保流式数据摄取过程顺利且一致地运行,使用 Prometheus 或 Grafana 等监控工具。这些工具提供有关数据摄取管道性能和健康状况的实时洞察,能够识别任何故障或性能瓶颈。实现告警机制至关重要,以便及时发现并解决流式数据摄取过程中的问题。对于电子商务平台来说,这确保了数据流中断能迅速得到解决,从而保持数据管道的完整性和可靠性。
流式数据摄取示例
如我们所说,在流式处理中,数据是随着到达而被处理,而不是按照预定义的批次进行处理。让我们修改批处理示例,转向流式处理范式。为了简化起见,我们将持续生成数据,数据一到达就立即处理,进行转换,然后加载:
-
generate_mock_data函数使用生成器持续生成记录,并模拟每条记录之间的延迟:def generate_mock_data(): while True: record = { 'id': random.randint(1, 1000), 'value': random.random() * 100 } yield record time.sleep(0.5) # Simulate data arriving every 0.5 seconds -
process_stream函数处理来自数据生成器的每条记录,而无需等待批次填充:def process_stream(run_time_seconds=10): start_time = time.time() for record in generate_mock_data(): transformed_record = transform_data(record) load_data(transformed_record) # Check if the run time has exceeded the limit if time.time() – start_time > run_time_seconds: print("Time limit reached. Terminating the stream processing.") break -
transform_data函数在每条记录到达时分别进行转换:def transform_data(record): transformed_record = { 'id': record['id'], 'value': record['value'], 'transformed_value': record['value'] * 1.1 # Example transformation } return transformed_record -
load_data函数模拟通过处理每一条记录来加载数据,而不是像以前那样在批次中处理每一条记录:def load_data(record): print(f"Loading record into database: {record}")
让我们从实时处理转向半实时处理,可以将其视为在短时间间隔内的批处理。这通常称为微批处理。
实时与半实时数据摄取
实时数据摄取是指几乎即时地收集、处理和加载数据的过程,正如我们所讨论的那样。这种方法对于需要立即洞察和行动的应用程序至关重要,例如欺诈检测、股票交易和实时监控系统。实时数据摄取提供了最低延迟,使企业能够在事件发生时立即做出反应。然而,它需要强大的基础设施和持续的资源分配,这使得其维护复杂且可能昂贵。
半实时数据摄取,另一方面,也称为接近实时数据摄取,涉及在最小延迟下处理数据,通常是几秒钟或几分钟,而不是即时处理。这种方法在实时和批处理之间达成平衡,提供及时的洞察,同时减少了与真正的实时系统相关的资源强度和复杂性。半实时数据摄取适用于社交媒体监控、客户反馈分析和运营仪表板等应用,其中接近即时的数据处理是有益的,但并非至关重要的时间敏感。
接近实时摄取的常见用例
让我们来看一些常见的用例,其中可以使用接近实时的数据摄取。
实时分析
流式处理使组织能够持续监控数据流动,允许实时仪表板和可视化。这在金融行业尤为重要,因为股票价格、市场趋势和交易活动需要实时跟踪。它还允许即时生成报告,促进及时决策,并减少数据生成与分析之间的延迟。
社交媒体和情感分析
企业实时跟踪社交媒体上的提及和情感,以管理品牌声誉并迅速回应客户反馈。流式数据使得可以持续分析公众对品牌、产品或事件的情感,提供即时洞察,这些洞察可能影响营销和公关策略。
客户体验提升
近实时处理使支持团队能够访问客户问题和行为的最新信息,从而更快更准确地回应客户询问。企业还可以利用近实时数据更新客户档案,并在客户与网站或应用程序互动后不久触发个性化的营销信息,如电子邮件或通知。
带有示例的半实时模式
从实时数据处理过渡到半实时数据处理涉及调整示例,引入更结构化的方式来处理数据更新,而不是在每条记录到达时立即处理。可以通过在短时间间隔内批量处理数据更新来实现,这样可以更高效地处理数据,同时保持响应式的数据处理管道。让我们看看这个示例,和往常一样,您可以在 GitHub 仓库中找到代码 github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter01/3.semi_real_time.py:
-
连续生成模拟数据,与之前的示例没有变化。这个过程会持续生成模拟数据记录,并有轻微的延迟(
time.sleep(0.1))。 -
对于半实时处理,我们可以使用一个双端队列(deque)来缓存传入的记录。这个功能会在指定的时间间隔过去,或者缓冲区达到指定大小(
batch_size)时处理记录。然后,它将双端队列转换为列表(list(buffer)),再传递给transform_data,确保数据是以批处理的方式进行处理:def process_semi_real_time(batch_size, interval): buffer = deque() start_time = time.time() for record in generate_mock_data(): buffer.append(record) -
检查间隔是否已过,或缓冲区大小是否已达到:
if (time.time() - start_time) >= interval or len(buffer) >= batch_size: -
处理并清理缓冲区:
transformed_batch = transform_data(list(buffer)) # Convert deque to list print(f"Batch of {len(transformed_batch)} records before loading:") for rec in transformed_batch: print(rec) load_data(transformed_batch) buffer.clear() start_time = time.time() # Reset start time -
然后,我们转换批次中的每一条记录。与之前的示例没有变化,我们加载数据。
当您运行此代码时,它会持续生成模拟数据记录。记录会被缓冲,直到指定的时间间隔(interval)过去,或者缓冲区达到指定的大小(batch_size)。一旦条件满足,缓冲的记录将作为一个批次进行处理、转换,然后“加载”(打印)到模拟数据库中。
在讨论适合批处理、流式处理或半实时流处理的不同类型数据源时,必须考虑这些数据源的多样性和特性。数据可以来自多种来源,例如数据库、日志、物联网设备、社交媒体或传感器,正如我们将在下一部分看到的那样。
数据源解决方案
在现代数据分析和处理的世界中,可供摄取的数据源种类繁多,涵盖了从传统文件格式(如 CSV、JSON 和 XML)到强大的数据库系统(包括 SQL 和 NoSQL 系统)的广泛范围。此外,还包括动态 API(如 REST),以便实时获取数据。像 Kafka 这样的消息队列提供了可扩展的解决方案来处理事件驱动的数据,而 Kinesis 和 pub/sub 等流媒体服务则支持连续的数据流,这对于要求即时洞察的应用至关重要。理解并有效利用这些多样化的数据摄取来源,是构建支持广泛分析和操作需求的强大数据管道的基础。
让我们从事件处理开始。
事件数据处理解决方案
在实时处理系统中,数据几乎是即时摄取、处理并响应的,正如我们之前讨论过的。实时处理系统通常使用消息队列来处理传入的数据流,并确保数据按接收顺序处理,不会产生延迟。
以下 Python 代码展示了使用消息队列处理消息的基本示例,这是实时和半实时数据处理系统中的基础概念。Python queue 模块中的 Queue 类用于创建队列——一个遵循先进先出(FIFO)原则的数据结构。在队列中,消息(例如message 0、message 1 等)会按照顺序被添加。这模拟了事件或任务的生成,需要按到达的顺序进行处理。我们来看看代码的每个部分。你可以在 github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter01/4.work_with_queue.py 找到完整代码文件:
-
read_message_queue()函数使用来自queue模块的Queue类初始化一个队列对象q:def read_message_queue(): q = Queue() -
这个循环将 10 条消息添加到队列中。每条消息是一个格式为
message i的字符串,其中i从 0 到 9 不等:for i in range(10): # Mocking messages q.put(f"message {i}") -
这个循环会持续从队列中获取并处理消息,直到队列为空。
q.get()从队列中获取消息,q.task_done()表示已处理完获取的消息:while not q.empty(): message = q.get() process_message(message) q.task_done() # Signal that the task is done -
以下函数接收一个消息作为输入,并将其打印到控制台,模拟消息的处理:
def process_message(message): print(f"Processing message: {message}") -
调用
read_message_queue函数:read_message_queue()
在这里,read_message_queue 函数从队列中读取消息,并使用 process_message 函数逐一处理这些消息。这演示了基于事件的系统如何处理任务——通过将它们放入队列并在任务变得可用时进行处理。
while not q.empty() 循环确保每条消息都按照加入队列的顺序被处理。这在许多现实世界的应用中至关重要,例如处理用户请求或日志时,处理顺序非常重要。
q.task_done() 方法表示消息已被处理。在现实世界的系统中,这一点非常重要,因为跟踪任务完成情况对于确保系统的可靠性和正确性至关重要,尤其是在有多个工作线程或线程的系统中。
在现实世界的应用中,消息队列通常会与更复杂的数据流平台集成,以确保可扩展性、容错性和高可用性。例如,在实时数据处理中,像 Kafka 和 AWS Kinesis 这样的平台就发挥了作用。
使用 Apache Kafka 摄取事件数据
有多种技术可用于摄取和处理事件数据。我们将讨论的一种技术是 Apache Kafka。Kafka 是一个开源的分布式事件流平台,最初由 LinkedIn 开发,后来捐赠给了 Apache 软件基金会。它被设计用来实时处理大量数据,并提供一个可扩展、容错的系统,用于处理和存储数据流。

图 1.1 – Apache Kafka 的组件
让我们来看一下 Apache Kafka 的不同组件:
-
摄取😗:可以使用 Kafka 生产者将数据流摄取到 Kafka 中。生产者是将数据写入 Kafka 主题的应用程序,Kafka 主题是可以存储和组织数据流的逻辑通道。
-
处理:Kafka 可以使用 Kafka Streams 处理数据流,Kafka Streams 是一个用于构建实时流处理应用程序的客户端库。Kafka Streams 允许开发人员构建自定义的流处理应用程序,可以对数据流进行转换、聚合和其他操作。
-
存储😗:Kafka 将数据流存储在分布式的、容错的集群中,这些集群被称为 Kafka 经纪人(brokers)。经纪人将数据流存储在分区中,这些分区在多个经纪人之间进行复制,以确保容错性。
-
消费😗:可以使用 Kafka 消费者从 Kafka 获取数据流。消费者是从 Kafka 主题读取数据并根据需要处理数据的应用程序。
可以使用多个库与 Apache Kafka 进行交互;我们将在下一节中探讨其中最流行的几个。
应该使用哪个库来处理你的使用案例?
Kafka-Python 是 Kafka 协议的纯 Python 实现,提供了一个更加 Pythonic 的接口,用于与 Kafka 交互。它设计简洁、易于使用,尤其适合初学者。它的主要优点之一就是简便性,相比其他 Kafka 库,它更易于安装和使用。Kafka-Python 灵活且非常适合小型到中型应用,提供了进行基本 Kafka 操作所需的必要功能,而不涉及额外的复杂依赖。由于它是纯 Python 库,因此不依赖于任何超出 Python 本身的外部库,从而简化了安装和设置过程。
Confluent-kafka-python 是由 Confluent(Kafka 的原创开发者)开发并维护的一个库。它以高性能和低延迟能力而著称,利用 librdkafka C 库进行高效操作。该库提供了类似 Java Kafka 客户端的广泛配置选项,并与 Kafka 的功能集高度契合,常常率先支持 Kafka 的新特性。它特别适用于生产环境,在性能和稳定性至关重要的情况下,是处理高吞吐量数据流和确保关键应用中可靠消息处理的理想选择。
从事件数据处理转向数据库涉及将重点从实时数据流转移到持久数据存储和检索。事件数据处理强调处理连续的数据流,以便快速获得洞察或采取行动,而数据库则是结构化的存储库,旨在长期存储和管理数据。
从数据库中摄取数据
数据库,无论是关系型还是非关系型,都是数据管理系统的基础组成部分。经典数据库和 NoSQL 数据库是两种不同类型的数据库管理系统,它们在架构和特性上有所不同。经典数据库,也称为关系型数据库,将数据存储在具有固定模式的表格中。经典数据库非常适合需要复杂查询和事务一致性的应用,例如金融系统或企业应用。
另一方面,NoSQL 数据库不会将数据存储在具有固定模式的表格中。它们采用基于文档的方式,以灵活的模式格式存储数据。它们设计为可扩展并能处理大量数据,重点是高性能的数据检索。NoSQL 数据库非常适合需要高性能和可扩展性的应用,例如实时分析、内容管理系统和电子商务平台。
让我们从关系型数据库开始。
从关系型数据库执行数据摄取
关系数据库在批量 ETL 过程中非常有用,其中需要将来自各种源的结构化数据进行整合、转换并加载到数据仓库或分析系统中。在处理之前,基于 SQL 的操作可以有效地进行数据连接和聚合。让我们尝试了解 SQL 数据库如何使用表格、行和列来表示数据,使用代码示例。我们将使用 Python 字典来模拟基本的 SQL 数据库交互,以表示表格和行。您可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter01/5.sql_databases.py看到完整的代码示例:
-
我们创建一个
read_sql函数,模拟从 SQL 表格中读取行,这里表示为一个字典列表,其中每个字典对应表中的一行:def read_sql(): # Simulating a SQL table with a dictionary sql_table = [ {"id": 1, "name": "Alice", "age": 30}, {"id": 2, "name": "Bob", "age": 24}, ] for row in sql_table: process_row(row) -
process_row函数将一个行(字典)作为输入,并打印其内容,模拟从 SQL 表格中处理行的过程:def process_row(row): print(f"Processing row: id={row['id']}, name={row['name']}, age={row['age']}") read_sql() -
让我们以正确的格式打印我们的 SQL 表格:
print(f"{'id':<5} {'name':<10} {'age':<3}") print("-" * 20) # Print each row for row in sql_table: print(f"{row['id']:<5} {row['name']:<10} {row['age']:<3}")这将打印如下输出:
id name age ------------------ 1 Alice 30 2 Bob 24
从前面的例子中学习的关键是理解 SQL 数据库通过由行和列组成的表来结构化和管理数据,以及如何通过编程方式高效检索和处理这些行。这些知识是至关重要的,因为它为任何应用程序中的有效数据库管理和数据操作奠定了基础。
在实际应用程序中,这种交互通常由像Java 数据库连接(JDBC)或开放数据库连接(ODBC)这样的库和驱动程序实现,它们提供了连接和查询数据库的标准方法。这些库通常被 Python 中的更高级别框架或库包装,使开发人员能够从各种 SQL 数据库中导入数据,而不必担心底层连接细节。可以使用多个库与 Python 交互 SQL 数据库;我们将在下一节探讨最流行的几个。
对于您的用例应该使用哪个库?
让我们探索 Python 中用于与 SQL 数据库交互的不同库,并理解何时使用每个库:
-
SQLite (sqlite3)非常适合小到中型应用程序、本地存储和原型设计。它的零配置、无服务器架构使其非常适合轻量级、嵌入式数据库需求和快速开发周期。在不需要完整数据库服务器开销的场景中特别有用。避免在需要高并发或大量写操作的应用程序中使用 sqlite3,或者多用户需要同时访问数据库的场景。它不适合大规模应用或需要强大安全功能和高级数据库功能的场景。
-
SQLAlchemy 适用于需要对原始 SQL 进行高度抽象、支持多种数据库引擎以及复杂查询和数据模型的应用程序。它非常适合需要灵活性、可扩展性,并且能够在不同数据库之间切换而无需做大量代码修改的大规模生产环境。避免在小型轻量级应用程序中使用 SQLAlchemy,因为它的全面 ORM 功能会带来不必要的开销。如果您需要直接、低级地访问特定数据库的功能,并且能够编写原始 SQL 查询,那么像 sqlite3、Psycopg2 或 MySQL Connector/Python 这样的简单数据库适配器可能会更合适。
-
Psycopg2 是与 PostgreSQL 数据库交互的首选工具,适用于需要利用 PostgreSQL 高级功能(如 ACID 合规性、复杂查询和丰富数据类型)的应用程序。它非常适合需要可靠性和效率来处理 PostgreSQL 数据库的生产环境。如果您的应用程序不与 PostgreSQL 交互,请避免使用 Psycopg2。如果您需要与多个数据库系统兼容或需要更高层次的抽象,请考虑使用 SQLAlchemy。另外,对于不需要完整 PostgreSQL 设置的轻量级应用程序,Psycopg2 可能不是最佳选择。
-
mysql-connector-python)非常适合需要与 MySQL 数据库直接交互的应用程序。它适用于需要兼容性并获得 Oracle 官方支持的环境,也适用于利用 MySQL 功能(如事务管理和连接池)的应用程序。如果您的应用程序需要与多个数据库系统兼容或需要更高层次的抽象,请不要使用 MySQL Connector/Python。对于那些不需要完整 MySQL 设置的简单应用程序,或者不特别需要 MySQL 功能的应用程序,可以考虑其他轻量级的替代方案。
在了解了与 SQL 数据库交互的各种库及其使用场景后,探索在传统关系型 SQL 数据库可能不太适用的情况下的替代方案同样重要。这引出了 NoSQL 数据库,它们提供了处理非结构化或半结构化数据的灵活性、可扩展性和性能。让我们深入研究与流行的 NoSQL 数据库进行交互的关键 Python 库,并探讨何时以及如何有效地使用它们。
从 NoSQL 数据库进行数据摄取
非关系型数据库可以用于存储和处理大量半结构化或非结构化数据的批量操作。它们在模式可以演变或需要以统一方式处理多种数据类型时尤其有效。NoSQL 数据库在流式处理和半实时工作负载中表现出色,因为它们能够处理高吞吐量和低延迟的数据摄取。它们通常用于捕获和处理来自 IoT 设备、日志、社交媒体动态以及其他生成连续数据流的来源的实时数据。
提供的 Python 代码模拟了一个使用字典的 NoSQL 数据库,并处理每个键值对。让我们来看看代码的每个部分:
-
process_entry函数从数据存储中获取一个键及其关联的值,并打印出一条格式化的消息,展示该键值对的处理过程。它提供了一种简单的方法来查看或处理单个条目,突出显示如何基于键访问和操作数据:def process_entry(key, value): print(f"Processing key: {key} with value: {value}") -
以下函数以表格格式打印整个
data_store字典:def print_data_store(data_store): print(f"{'Key':<5} {'Name':<10} {'Age':<3}") print("-" * 20) for key, value in data_store.items(): print(f"{key:<5} {value['name']:<10} {value['age']:<3}")它首先打印
Key、Name和Age的列标题,然后跟随一条分隔线以增加清晰度。接着,它遍历data_store字典中的所有键值对,打印每个条目的键、名称和年龄。这个函数有助于可视化当前数据存储的状态。数据的初始状态如下:Initial Data Store: Key Name Age ----------------------- 1 Alice 30 data_store dictionary:def create_entry(data_store, key, value):
data_store[key] = value
return data_store
It takes a key and a value, then inserts the value into `data_store` under the specified key. The updated `data_store` dictionary is then returned. This demonstrates the ability to add new data to the store, showcasing the creation aspect of `update_entry` function updates an existing entry in the `data_store` dictionary:def update_entry(data_store, key, new_value):
if key in data_store:
data_store[key] = new_value
return data_store
It takes a key and `new_value`, and if the key exists in the `data_store` dictionary, it updates the corresponding value with `new_value`. The updated `data_store` dictionary is then returned. This illustrates how existing data can be modified, demonstrating the update aspect of CRUD operations. -
以下函数从
data_store字典中移除一个条目:def delete_entry(data_store, key): if key in data_store: del data_store[key] return data_store它接受一个键,如果该键在
data_store字典中找到,则删除相应的条目。更新后的data_store字典会被返回。 -
以下函数将所有过程整合在一起:
def read_nosql(): data_store = { "1": {"name": "Alice", "age": 30}, "2": {"name": "Bob", "age": 24}, } print("Initial Data Store:") print_data_store(data_store) # Create: Adding a new entry new_key = "3" new_value = {"name": "Charlie", "age": 28} data_store = create_entry(data_store, new_key, new_value) # Read: Retrieving and processing an entry print("\nAfter Adding a New Entry:") process_entry(new_key, data_store[new_key]) # Update: Modifying an existing entry update_key = "1" updated_value = {"name": "Alice", "age": 31} data_store = update_entry(data_store, update_key, updated_value) # Delete: Removing an entry delete_key = "2" data_store = delete_entry(data_store, delete_key) # Print the final state of the data store print("\nFinal Data Store:") print_data_store(data_store)这段代码演示了 NoSQL 数据库的核心原则,包括架构灵活性、键值对存储和基本的 CRUD 操作。它从
read_nosql()函数开始,该函数使用字典data_store模拟了一个 NoSQL 数据库,其中每个键值对代表一个唯一的标识符和相关的用户信息。最初,print_data_store()函数以表格格式显示数据,突出展示了 NoSQL 系统固有的架构灵活性。接着,代码演示了 CRUD 操作。首先,create_entry()函数添加了一个新条目,展示了如何将新数据插入存储中。随后,process_entry()函数检索并打印新添加条目的详细信息,展示了读取操作。接下来,update_entry()函数修改了一个现有条目,展示了 NoSQL 数据库的更新能力。delete_entry()函数则用于删除一个条目,展示了如何从存储中删除数据。最后,再次打印更新后的data_store字典,清晰地展示了数据在这些操作中的变化过程。 -
让我们执行整个过程:
read_nosql()这将返回最终的数据存储:
Final Data Store: Key Name Age ----------------------- 1 Alice 31 2 Charlie 28
在前面的示例中,我们演示了如何使用 Python 与一个模拟的 NoSQL 系统进行交互,以便展示 NoSQL 数据库的核心原则,例如架构灵活性、键值对存储和基本的 CRUD 操作。现在我们可以更好地理解 NoSQL 数据库在数据建模和高效处理非结构化或半结构化数据方面与传统 SQL 数据库的区别。
有多个库可以用来与 NoSQL 数据库进行交互。在接下来的章节中,我们将探索其中最受欢迎的几个。
你应该根据自己的使用案例选择哪一个库?
让我们探索在 Python 中与 NoSQL 数据库交互的不同库,并了解何时使用每一个:
-
pymongo是 MongoDB 的官方 Python 驱动,MongoDB 是一种因其灵活性和可扩展性而广受欢迎的 NoSQL 数据库。pymongo允许 Python 应用程序与 MongoDB 无缝互动,提供了一个简洁的 API 来执行 CRUD 操作、管理索引以及执行复杂查询。pymongo特别受到欢迎,因为它易于使用且与 Python 数据结构兼容,使其适用于从简单原型到大规模生产系统的各种应用。 -
cassandra-driver(Cassandra):cassandra-driver库为 Python 应用程序提供了直接访问 Apache Cassandra 的能力,Cassandra 是一个高度可扩展的 NoSQL 数据库,设计用于处理分布式普通服务器上的大量数据。Cassandra 的架构针对写密集型工作负载进行了优化,并提供了可调的一致性级别,适用于实时分析、物联网数据及其他需要高可用性和容错性的应用。
从数据库过渡到文件系统涉及将重点从结构化的数据存储和检索机制转向更灵活和多功能的存储解决方案。
从基于云的文件系统执行数据摄取
云存储是一种服务模型,允许通过互联网远程维护、管理和备份数据。它涉及将数据存储在远程服务器上,并通过互联网从任何地方访问,而不是存储在本地设备上。云存储彻底改变了我们存储和访问数据的方式。它为个人和组织提供了一种灵活且可扩展的解决方案,使他们能够存储大量数据,无需投资物理硬件。这对于确保数据始终可访问并且可以轻松共享尤其有用。
Amazon S3、Microsoft Azure Blob Storage 和 Google Cloud Storage 都是基于云的对象存储服务,允许你在云中存储和检索文件。基于云的文件系统因多个原因变得越来越流行。
首先,它们提供了一种灵活且可扩展的存储解决方案,可以轻松适应组织不断变化的需求。这意味着,随着数据量的增长,可以在不需要大量资本投资或物理基础设施更改的情况下增加额外的存储容量。因此,它可以帮助减少与维护和升级本地存储基础设施相关的资本支出和运营成本。
其次,基于云的文件系统提供高水平的可访问性和可用性。数据存储在云中,用户可以从任何有互联网连接的地方访问它,这使得跨不同团队、部门或地点的协作和信息共享变得更加容易。此外,基于云的文件系统设计了冗余和故障转移机制,确保数据在硬件故障或停机事件发生时仍然可用。最后,它们提供增强的安全功能,以防止未经授权的访问、数据泄露或数据丢失。云服务提供商通常具备先进的安全协议、加密和监控工具来保护数据,并确保遵守数据隐私法规。
云存储系统中的文件本质上与本地设备上的文件相同,但它们存储在远程服务器上并通过互联网访问。然而,这些文件在云存储系统中是如何组织的呢?接下来我们来讨论这个问题。
在云存储系统中组织文件
在云存储中组织文件的主要方法之一是使用文件夹结构,这类似于本地文件系统。用户可以创建文件夹和子文件夹,以系统地分类和存储文件。我们来看一下最佳实践:
-
创建一个符合逻辑且直观的层级结构,反映出你的工作方式或项目结构是至关重要的。这涉及设计一个与工作流程相匹配的文件夹结构,使文件的定位和管理更加简便。例如,你可以为不同的部门、项目或客户创建主文件夹,并为特定任务或文档类型创建子文件夹。这种层级组织不仅通过减少寻找文件所需的努力节省时间,还通过提供一个清晰且一致的框架,提升了团队成员之间的协作,便于大家快速导航。
-
使用一致的命名规范来命名文件夹和文件对于确保云存储中的文件便捷检索和保持有序至关重要。标准化的命名方案有助于避免混淆,减少错误,并加快查找特定文档的过程。例如,采用如
YYYY-MM-DD_ 项目名称 _ 文档类型这样的格式,可以提供即时的上下文信息,并使排序和搜索变得更加直接。一致的命名还促进了自动化和与其他工具的集成,因为可预测的文件名更易于被脚本和应用程序处理。 -
按项目或客户对文件进行分组是保持相关文档集中和简化项目管理的有效方法。这种方法涉及为每个项目或客户创建专门的文件夹,所有相关文件,如合同、通信和交付物,都存储在这些文件夹中。
-
许多云存储系统允许通过关键字或元数据为文件加标签,这大大增强了文件分类和搜索功能。标签本质上是你可以附加到文件上的标签,使得根据特定标准对文档进行分组和查找变得更加容易。元数据包括详细信息,如作者、日期、项目名称和文件类型,这些信息提供了额外的上下文,有助于更精确的搜索。通过使用相关标签和全面的元数据,你可以快速筛选并定位文件,无论它们位于文件夹层级的哪个位置。这种做法在大型存储系统中尤为有用,因为在这些系统中,传统的文件夹结构可能会变得笨重。
从讨论云存储系统开始,焦点现在转向探索 API 所提供的功能和集成机会。
APIs
近年来,API 变得越来越受欢迎,因为它们能够实现不同系统和服务之间的无缝通信和集成。API 为开发者提供了一种标准化且灵活的方式,能够访问其他系统的数据和功能,使他们能够轻松构建利用现有资源的新应用和服务。API 已成为现代软件开发的基础构件,广泛应用于各个行业和领域。
现在我们理解了 API 的含义,让我们继续了解requests Python 库,它使开发人员能够以编程方式访问和操作远程服务器上的数据。
requests库
在使用 Python 与 API 进行交互时,requests库是用于向 API 和其他网络服务发送 HTTP 请求的首选 Python 库。它使得用 Python 发送 HTTP/1.1 请求变得简单,并提供了许多方便的功能来处理 HTTP 响应。
运行以下命令来安装requests库:
pip install requests==2.32.3
让我们快速看看如何使用这个库:
-
导入
requests库:import requests -
指定 API 端点的 URL:
url = "https://jsonplaceholder.typicode.com/posts" -
对 API 端点发起
GET请求:response = requests.get(url) -
获取响应内容:
print(response.content)
在这里,我们向jsonplaceholder.typicode.com/posts的 API 端点发起了GET请求,并将response对象存储在response变量中。然后,我们可以使用response对象的content属性打印响应内容。requests库提供了许多其他方法和功能来发起 HTTP 请求,包括对POST、PUT、DELETE等 HTTP 方法的支持、处理头信息和 Cookies,以及处理重定向和身份验证。
现在我们已经解释了requests库,让我们来看看一个具体的例子,如何从Cocktail DB API 中获取玛格丽塔鸡尾酒的数据,这可以说明如何将实际的网页请求应用到访问和整合实时数据源到应用程序中。
学习如何制作玛格丽塔鸡尾酒!
本用例演示了如何使用 Python 从Cocktail DB API 中检索鸡尾酒数据。如果你想提高你的调酒技能并给朋友留下深刻印象,你可以使用开放 API 获取任何鸡尾酒所需成分的实时信息。为此,我们将使用Cocktail DB API 和requests库,看看我们需要哪些成分来制作玛格丽塔:
-
定义 API 端点 URL。我们正在向 Cocktail DB API 端点发出请求,搜索名称为
margarita的鸡尾酒:url = "https://www.thecocktaildb.com/api/json/v1/1/search.php?s=margarita" -
发起 API 请求。我们将 API 端点 URL 定义为字符串,并将其传递给
requests.get()函数来发起GET请求:response = requests.get(url) -
检查请求是否成功(状态码
200),并获取数据。API 响应以 JSON 字符串形式返回,我们可以通过调用response.json()方法来提取它。然后,我们将这个 JSON 数据赋值给一个名为data的变量:if response.status_code == 200: # Extract the response JSON data data = response.json() # Check if the API response contains cocktails data if 'drinks' in data: # Create DataFrame from drinks data df = pd.DataFrame(data['drinks']) # Print the resulting DataFrame print(df.head()) else: print("No drinks found.") -
如果请求不成功,请打印此错误信息:
else: print(f"Failed to retrieve data from API. Status code: {response.status_code}")
你可以将margarita搜索参数替换为其他鸡尾酒名称或成分,以获取不同饮品的数据。
到这里,我们已经结束了第一章。让我们总结一下到目前为止学到的内容。
总结
在本章中,我们涵盖了现代计算和数据管理中的重要技术。我们首先讨论了批量摄取,这是一种在预定时间间隔内收集和处理大量数据的方法,为数据流可预测的组织提供了高效且具成本效益的解决方案。与之相对,我们探讨了流式摄取,它允许数据实时处理,从而能够即时分析并迅速应对变化的条件。接着我们介绍了如 Kafka 等流式服务,用于实时数据处理。随后我们讲解了 SQL 和 NoSQL 数据库——如 PostgreSQL、MySQL、MongoDB 和 Cassandra——并突出了它们在结构化和灵活数据存储方面的优势。我们还探索了像 REST 这样的 API,以实现系统的无缝集成。此外,我们深入讨论了文件系统、文件类型及其属性,并介绍了如 Amazon S3 和 Google Cloud Storage 等云存储解决方案,强调了可扩展性和数据管理策略。这些技术共同推动了当今数字生态系统中强大、可扩展且高效的应用程序。
在即将到来的章节中,我们将深入探讨数据质量的关键方面及其在构建可靠数据产品中的重要性。我们将探索为什么确保高数据质量对于做出明智的商业决策、提升客户体验和维持运营效率至关重要。
第二章:数据质量的重要性
你知道数据是许多重要商业决策的支柱吗?没有准确、完整、一致的信息,企业可能会做出错误的判断,这可能损害企业的声誉、客户关系及整体业务。不同数据集之间的一致性问题可能会造成混乱,阻碍有意义的分析。无关或过时的数据可能会误导决策者的判断,导致做出次优选择。另一方面,构建高质量的数据产品则是一个强有力的资产,能够帮助组织做出明智的决策、发现有价值的洞察、识别趋势、降低风险并获得竞争优势。
在本章中,我们将深入探讨以下话题:
-
为什么数据质量很重要
-
用于衡量数据产品中数据质量的不同维度
-
数据孤岛对数据质量的影响
技术要求
你可以在以下的 GitHub 仓库中找到本章的所有代码:
github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/tree/main/chapter02
为什么数据质量很重要
让我来揭示一下为什么数据质量如此重要:
-
准确的数据能带来竞争优势:组织依赖数据来识别模式、趋势、偏好以及支配其生态系统的其他关键因素。如果你的数据质量不合格,结果分析和结论可能会失真,导致错误的决策,这可能会危及整个业务。
-
完整的数据是成本优化的基石:数据构成了自动化和优化的基础,若能得当地执行,能够提高生产力并降低费用。不完整或低质量的数据会导致瓶颈并增加成本。试想一下,如果数据录入标准更高,许多本可以避免的错误就不会浪费无数的人工时间去修正。
-
顶级数据能够带来长期忠诚的满意客户:每个企业的生命力依赖于满意的客户,客户的忠诚度能够确保企业的持续增长。关于客户的错误数据可能会导致个性化体验不匹配客户特征,甚至出现错误的账单和未满足的请求。这些失望的客户可能会把生意带到别处,导致公司面临生存困境。
-
合规的数据是避免不必要法律后果的必要条件:许多行业必须遵循有关数据精度、安全性和隐私的特定规则。遵守这些规则需要高质量的数据,以满足严格的指南,并防止法律处罚以及可能失去消费者信任。
-
为了避免数据孤岛,你需要信任你的数据:当企业中的多个实体必须协作利用数据时,确保数据的完整性至关重要。数据的不兼容或差异可能会妨碍合作,阻碍集成工作,并导致数据孤岛的形成。
-
数据质量实际上意味着信任:数据质量直接影响利益相关者对组织的信任和信誉。通过保持高质量的数据,组织可以在客户、合作伙伴和投资者之间建立信任。
现在我们更清楚数据质量为何重要,接下来进入下一部分,我们将深入探讨数据质量的不同维度。
数据质量的维度
如前所述,卓越的数据质量是构建明智决策和战略洞察力的基础。考虑到这一点,我们现在来探讨我们可以使用哪些关键绩效指标(KPIs)来衡量我们资产的数据质量。
完整性
完整性衡量数据的完整程度,即数据是否缺少任何值或字段。关键绩效指标可能包括缺失数据的百分比或每条记录缺失的数据点数。
以下代码将输出数据集中每列的完整性百分比。较高的百分比表示较高的完整性水平,而较低的百分比则表示更多缺失值:
-
我们将首先导入
pandas库以处理数据集:import pandas as pd -
接下来,我们创建一个包含以下列的示例数据集:
Name(姓名)、Age(年龄)、Gender(性别)和City(城市)。其中有些值故意缺失(用None表示):data = { 'Name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'], 'Age': [25, 30, None, 28, 22], 'Gender': ['Female', 'Male', 'Male', 'Male', 'Female'], 'City': ['New York', 'Los Angeles', 'Chicago', None, 'San Francisco'] } -
然后,我们创建一个 pandas DataFrame:
df = pd.DataFrame(data) -
然后,我们将使用
isnull()函数来识别每列的缺失值,并使用sum()函数计算每列缺失值的总数:completeness = df.isnull().sum() -
接下来,我们将计算完整性百分比:
total_records = len(df) completeness_percentage = (1- completeness / total_records) * 100这将打印以下输出:
Completeness Check: Name 0 Age 1 Gender 0 City 1 Completeness Percentage: Name 100.0 Age 80.0 Gender 100.0 City 80.0
完整性检查显示每列缺失值的数量,完整性百分比表示每列缺失值相对于总记录数的比例。该输出表明,Name(姓名)和Gender(性别)列的完整性为 100%,而Age(年龄)和City(城市)列的完整性为 80%。
注释 – 完整性百分比
完整性百分比通过将缺失值的数量除以数据集中的总记录数,然后乘以 100 来计算。它表示缺失值相对于数据集大小的比例。
完整性百分比越高, 越好!
准确性
准确性通过将数据与可信来源或基准进行比较,来评估数据的正确性。
以下代码将根据实际值与期望值的比较输出准确度百分比:
-
我们首先加载所需的库:
import pandas as pd -
接下来,我们创建一个名为
data的示例数据集和一个名为reference_data的参考数据集。两个数据集具有相同的结构(列:Name、Age、Gender和City):data = { 'Name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'], 'Age': [25, 30, 28, 28, 22], 'Gender': ['Female', 'Male', 'Male', 'Male', 'Female'], 'City': ['New York', 'Los Angeles', 'Chicago', 'New York', 'San Francisco'] } # Reference dataset for accuracy comparison reference_data = { 'Name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'], 'Age': [25, 30, 29, 28, 22], 'Gender': ['Female', 'Male', 'Male', 'Male', 'Female'], 'City': ['New York', 'Los Angeles', 'Chicago', 'New York', 'San Francisco'] } -
我们使用示例数据和参考数据分别创建两个名为
df和reference_df的 pandas 数据框:df = pd.DataFrame(data) reference_df = pd.DataFrame(reference_data) -
我们创建一个
accuracy_check变量,并将df和reference_df之间比较的结果赋值给它。此比较使用==运算符,它为匹配的值返回True,为不匹配的值返回False:accuracy_check = df == reference_df我们可以使用
==运算符将实际值列与期望值列进行比较。 -
我们然后通过对每一列的
accuracy_check数据框求平均值来计算准确率百分比。mean操作将True视为1,将False视为0,因此它有效地计算了每一列中匹配值的百分比:accuracy_percentage = accuracy_check.mean() * 100 -
最后,我们打印结果:
# Display the accuracy results print("Accuracy Check:") print(accuracy_check) print("\nAccuracy Percentage:") print(accuracy_percentage)输出如下:
Accuracy Check: Name Age Gender City 0 True True True True 1 True True True True 2 True False True True 3 True True True True 4 True True True True Accuracy Percentage: Name 100.0 Age 80.0 Gender 100.0 City 100.0准确性检查显示
True表示数据与参考数据集匹配,False表示不匹配。准确率百分比表示每列中匹配值相对于记录总数的比例。该输出表示在这种情况下,Age列是唯一需要更多关注的列。其他所有列的准确率为 100%。
注意 – 准确率百分比
准确率百分比可以通过对所有列的比较结果求平均值,并乘以 100 来计算。这个百分比代表了匹配数据值相对于总数据点数量的比例。
准确率越高,结果越好!
想知道如何构建基准数据集吗?
基准数据必须能够代表你要解决的任务。这意味着,取决于数据生命周期的不同阶段,基准数据的构建方式不同,所起的作用也不同。
构建基准数据集对于各种数据角色至关重要,包括数据工程师、数据分析师和机器学习从业者。对于数据工程师而言,基准数据对于数据验证和测试至关重要。好消息是,在大多数情况下,数据工程师可以通过历史数据构建基准标签,方法如下:
-
数据验证规则:建立验证规则和约束,以验证数据在流经管道时的准确性。
-
人工检查:手动检查数据样本以识别不一致性或错误,并创建经过验证和修正的数据集。这可以作为基准数据集。
数据分析师依赖基准数据来验证他们的发现的准确性,这些数据可以通过专家注释、历史数据和用户反馈获得,确保分析见解反映了现实世界的现象:
-
专家注释:如果处理的是非结构化或文本数据,领域专家可以手动为数据样本标注正确的标签或类别,作为分析的真实标签。
-
历史数据:使用具有良好文档化准确度的历史数据作为真实标签。这在分析趋势、模式或历史事件时尤其有价值。
-
调查和用户反馈:从调查或用户反馈中收集数据,以验证从数据中得出的见解和结论。这些可以作为定性真实标签。
最后,在机器学习的背景下,真实标签数据是模型训练和评估的基础:
-
手动标注:手动为数据样本添加注释,以创建标注数据集。这对于图像分类、情感分析或目标检测等任务很常见。
-
众包:使用众包平台收集来自多个人工工人的注释,他们共同建立真实标签数据。
-
现有数据集:许多机器学习任务从已被研究社区广泛使用的基准数据集中获益。你可以根据需要使用和更新这些数据集。
-
领域专家标签:请咨询领域专家,为数据提供标签或注释,尤其是在需要领域特定知识时。
-
合成数据生成:生成带有已知真实标签的合成数据,以开发和测试机器学习模型。这在没有真实标签数据的情况下尤其有用。
无论是哪种角色,创建、维护并不断评估真实标签数据的质量都是至关重要的,并要注意潜在的偏差和局限性。因为它对数据工程、分析和机器学习工作效果有着重要影响。
时效性
时效性评估数据的捕获、处理和可用的速度。时效性关键绩效指标(KPI)可能包括诸如数据延迟(数据捕获与可用之间的时间差)或遵循数据刷新计划等指标。
测量数据的时效性涉及评估数据在特定时间范围或事件中的新鲜度或有效性。让我们来看一个例子:
-
我们首先导入所需的库:
import pandas as pd import numpy as np from datetime import datetime, timedelta -
然后我们生成一个包含时间戳和值的随机数据集。时间戳在给定时间范围内随机分布,以模拟真实世界的数据:
np.random.seed(0) # For reproducibility n_samples = 100 start_time = datetime(2023, 10, 25, 9, 0, 0) end_time = datetime(2023, 10, 25, 16, 0, 0) timestamps = [start_time + timedelta(minutes=np.random.randint(0, (end_time - start_time).total_seconds() / 60)) for _ in range(n_samples)] values = np.random.randint(50, 101, n_samples) df = pd.DataFrame({'Timestamp': timestamps, 'Value': values}) -
我们定义一个参考时间戳,用来与数据集的时间戳进行对比:
reference_timestamp = datetime(2023, 10, 25, 12, 0, 0) -
我们设置了 30 分钟的时效性阈值。时间戳在参考时间戳 30 分钟以内的数据将被视为及时:
timeliness_threshold = 30 -
我们通过计算参考时间戳与每条记录时间戳之间的时间差(以分钟为单位),来计算数据集中每条记录的时效性。我们还创建了一个布尔列,用来指示记录是否符合时效性标准,根据阈值判断:
df['Timeliness'] = (reference_timestamp - df['Timestamp']).dt.total_seconds() / 60 df['Timely'] = df['Timeliness'] <= timeliness_threshold -
最后,我们计算数据集的平均时效性并显示结果:
average_timeliness = df['Timeliness'].mean() -
这将显示以下输出:
Dataset with Timestamps: Timestamp Value Timeliness Timely 0 2023-10-25 11:52:00 71 8.0 True 1 2023-10-25 09:47:00 98 133.0 False 2 2023-10-25 10:57:00 99 63.0 False 3 2023-10-25 12:12:00 55 -12.0 True 4 2023-10-25 14:23:00 91 -143.0 True Average Timeliness (in minutes): -23.8 Percentage of Timely Records: 61.0 %
此示例提供了一个更真实的数据集及时性模拟,其中包含随机生成的时间戳和及时性阈值。平均及时性表示数据集相对于 参考时间戳的平均时间偏差。
良好的及时性
低平均及时性和高及时记录的百分比表明数据集当前,并且与参考时间戳很好地对齐。这在实时应用或数据最新性至关重要的场景中是可取的。
这里的一个重要考虑因素是如何定义参考时间戳。参考时间戳是数据集时间戳与之比较的时间点。它代表数据的期望或预期时间。例如:在零售店扫描会员卡时创建记录的时间,参考时间是记录在数据库中登录的时间。因此,我们计算的是从事件实际创建到新条目存储在数据库中所需的时间。
参考阈值越小,应用程序需要越实时。另一方面,参考阈值越大,将数据带入系统所需的时间越长(批量应用)。实时处理和批处理之间的选择取决于应用程序的具体要求:
-
实时处理:当需要即时响应、低延迟以及能够在数据到达时立即采取行动时,选择实时处理是合适的。适用于需要做出时间敏感决策的应用场景。
-
批处理:当低延迟不是严格要求,且可以容忍数据处理中的一些延迟时,选择批处理通常更具成本效益,适合可以安排和自动化的任务。
不同数据角色中及时性定义的变化
及时性是数据质量的一个重要方面,适用于各种角色,从数据工程师到数据分析师和机器学习从业者。以下是每个角色如何在现实世界中利用及时性:
-
数据工程师:
-
数据管道监控:数据工程师可以将及时性作为监控数据管道的关键指标。他们可以设置自动警报或检查,确保数据按时到达,并识别和解决数据摄取中的延迟。
-
数据验证:数据工程师可以将及时性检查作为其数据验证流程的一部分,确保数据在用于下游流程之前满足指定的时间条件。
-
-
数据分析师:
-
实时分析:金融或电子商务等领域的分析师依赖实时数据做出信息化决策。他们需要确保分析的数据是最新的,反映当前的情况。
-
KPI 监控:及时性在监控关键绩效指标中至关重要。分析师使用及时数据来跟踪和评估各种业务指标的表现。
-
-
机器学习从业者:
-
特征工程:及时性在机器学习模型的特征工程中起着重要作用。保持特征尽可能更新对模型的训练和评分有直接影响。
-
模型训练和评估:在实时预测模型中,模型训练和评估依赖于及时数据。从业者必须确保训练数据是当前的,以建立有效的模型或执行实时推断。
-
概念漂移检测:及时性对于检测概念漂移至关重要,概念漂移是指数据内部关系随时间变化的情况。机器学习模型需要适应这些变化,及时的数据监控和检测变化至关重要。
-
这里是及时性的一些实际应用:
-
金融:在金融领域,及时性对股票交易、欺诈检测和风险管理至关重要,及时的数据可以带来更好的决策和降低风险。
-
医疗保健:及时性对于医疗数据至关重要,特别是在患者监测和实时健康数据分析中。
-
电子商务:及时的数据对电子商务公司来说至关重要,可以实时监控销售、客户行为和库存。
-
运输与物流:在供应链管理和物流中,实时跟踪和及时的数据对路线优化和库存管理至关重要。
让我们继续谈一谈一致性。
一致性
一致性衡量数据内部一致性的程度,包括确保数据在整个数据集中遵循建立的规则、标准和格式。具体而言,我们应检查以下内容:
-
相同格式:所有记录或列的数据都应该遵循相同的格式、结构和标准。这种一致性确保数据可以轻松处理和比较。
-
遵守标准:数据应符合预定义的规则、指南、命名约定和参考数据。例如,如果数据集包含产品名称,一致性要求所有产品名称都遵循标准化的命名约定。
-
数据类型和格式:一致性检查包括验证数据类型(例如文本、数字和日期)和数据格式(例如日期格式和数值表示)是否一致。
让我们通过一个例子更好地理解:
-
我们首先导入
pandas库:import pandas as pd -
然后我们创建一个包含产品信息的样本数据集,包括产品名称。在这个例子中,我们将检查所有产品名称是否按照命名约定以
PROD开头:data = { 'ProductID': [1, 2, 3, 4, 5], 'ProductName': ['PROD001', 'PROD002', 'Product003', 'PROD004', 'PROD005'], } df = pd.DataFrame(data) -
让我们定义预期的前缀:
expected_prefix = "PROD" -
我们通过确保所有产品名称以
PROD开头来检查ProductName列的一致性。不一致的名称将被标记:inconsistent_mask = ~df['ProductName'].str.startswith(expected_prefix) df['Consistency'] = ~inconsistent_mask -
然后我们计算一致行的百分比:
consistent_percentage = (df['Consistency'].sum() / len(df)) * 100 -
最后,我们展示结果,包括包含完整性检查结果的数据集:
print("Dataset with Consistency Check:") print(df)以下是最终输出:
Dataset with Consistency Check: ProductID ProductName Consistency 0 1 PROD001 True 1 2 PROD002 True 2 3 Product003 False 3 4 PROD004 True 4 5 PROD005 True Percentage of Consistent Rows: 80.00%
在这个特定的数据集中,五个产品名称中有三个符合命名规范,因此一致性率为 80%。Product003条目被标记为不一致,因为它没有以PROD开头
这种类型的完整性检查对于确保数据遵循特定标准或约定非常有用,计算出的百分比提供了一个定量衡量,表示有多少记录符合标准
注意
更高的一致性百分比意味着相应列中的值更加一致和符合标准。如果百分比很低,那么说明数据集中有许多不同的值,只要我们理解列代表的含义并且所有唯一值都有其背后的意义,这并不是一个错误
你是否在想,还可以应用哪些其他的完整性检查?


表 2.1 – 不同的完整性检查选项
让我们接下来讨论唯一性
唯一性
唯一性衡量数据集中唯一值的存在。它可以帮助识别异常,例如重复的键
代码将输出每一列的有效性结果,指示每一列中的值是否符合定义的有效性规则:
-
我们导入
pandas库:import pandas as pd -
然后我们创建一个包含电子邮件地址的示例数据集。我们想检查数据集中的所有电子邮件地址是否唯一:
# Create a sample dataset data = { 'Email': ['john.doe@example.com', 'jane.smith@example.com', 'james.doe@example.com', 'susan.brown@example.com'], } df = pd.DataFrame(data) -
我们检查
Email列的唯一性,确保数据集中的电子邮件地址没有重复:# Check uniqueness and create a Boolean mask for duplicated email addresses duplicated_mask = df['Email'].duplicated(keep='first') # Create a new column to indicate uniqueness df['Uniqueness'] = ~duplicated_mask -
接下来,我们计算唯一性百分比:
unique_percentage = (df['Uniqueness'].sum() / len(df)) * 100 -
最后,我们展示结果,包括包含唯一性检查结果的数据集。以下是输出:
Dataset with Uniqueness Check: Email Uniqueness 0 john.doe@example.com True 1 jane.smith@example.com True 2 james.doe@example.com True 3 susan.brown@example.com True Percentage of Unique Records: 100.00%
这个输出表示数据集中的值都是唯一的
唯一性检查在各个行业和使用案例中都很常见。以下是一些常见的现实场景中唯一性检查的例子:
-
客户编号:在客户数据库中,每位客户应有一个唯一的标识符(客户编号),以防止重复的客户记录
-
产品 SKU:在库存和电子商务数据库中,每个产品应该有一个唯一的 SKU,以便识别和管理产品,避免重复
-
电子邮件地址:电子邮件地址在邮件列表或用户数据库中应该是唯一的,以避免发送重复的邮件或创建多个相同电子邮件地址的账户
-
员工编号:在人力资源数据库中,每位员工通常有一个唯一的员工编号,用以区分员工并有效管理他们的记录
-
车辆识别号码(VIN):在汽车行业中,VIN 是每辆车的唯一标识符,用于追踪其历史和所有权
-
条形码和二维码:在零售和物流行业,条形码和二维码为产品、包裹和物品提供唯一的标识符,用于追踪和库存管理
-
用户名和用户 ID:在在线平台和应用中,用户名和用户 ID 对每个用户都是唯一的,用来区分用户并管理账户。
-
序列号:在制造业中,产品通常会有唯一的序列号,用于识别和跟踪单个物品。
-
交易 ID:在金融系统中,每笔交易都会分配一个唯一的交易 ID,以防止重复并确保正确的记录保存。
当数据集中有非唯一记录时,意味着存在具有相同关键属性(例如,ID、姓名或电子邮件地址)的重复条目或记录。非唯一记录可能会导致数据质量问题,甚至在数据分析和报告时产生错误。要修复数据集中的非唯一记录,你可以使用多种方法,包括去除重复、汇总数据或根据具体数据和需求解决冲突。我们将在后续章节讨论这些策略。
重复数据
重复数据评估数据集中的重复或冗余数据的存在。如果你的数据中有重复记录,意味着同一条信息或记录在数据集中出现多次。
示例——客户数据库
假设你在一个公司工作,公司拥有一个客户数据库,跟踪每个客户的信息,包括他们的联系详情、购买记录和互动信息。
问题——重复的客户记录
你发现数据库中有重复的客户记录。这些重复记录可能是由于数据输入错误、系统问题或其他原因造成的。例如,一个名为 John Smith 的客户有两个独立的记录,联系信息略有不同。一个记录的电子邮件地址是john.smith@email.com,另一个是jsmith@email.com。
这通常被认为是不可取的,原因有多个:
-
数据准确性:当你有多个相同信息的副本时,很难确定哪个版本是正确或最新的。这可能导致数据不一致和混乱。
-
存储效率:重复记录会占用不必要的存储空间。尤其在处理大数据集时,这可能导致存储成本增加和数据检索时间延长。
-
数据完整性:重复记录可能会影响数据完整性。在数据关系至关重要的情况下,重复记录可能会破坏数据模型的完整性。
-
高效的数据处理:分析、查询和处理重复较少的数据集会更高效。处理时间更短,结果也更有意义,因为你不需要处理重复信息。
-
数据分析:在进行数据分析或运行统计模型时,重复记录可能会扭曲结果,导致错误的结论。减少重复数据对于准确和有意义的分析至关重要。
-
成本节约:存储和管理重复记录会增加存储基础设施和数据管理的成本。消除重复记录可以带来成本节约。
现在假设我们将同样的问题推广到管理数百万客户的公司。你能想象引入重复记录会有多么昂贵和混乱吗?
尽管在数据集中最小化重复记录通常是一种最佳实践,但在某些特定场景下,接受或允许重复记录可能是一个合理的选择:
| 场景 | 数据表示 |
|---|---|
| 客户订单历史 | 每一行代表客户的单独订单。允许同一客户 ID 的多行以展示订单历史。 |
| 服务请求 | 记录代表服务请求,包括同一客户或地点随时间发生的多个请求。允许重复记录以保留详细历史。 |
| 传感器数据 | 每一行包含传感器读数,可能包括多个相同数据值的条目。允许重复记录以跟踪每次读数。 |
| 日志记录和审计跟踪 | 日志条目记录事件或操作,有些事件可能生成重复条目。保留重复记录以便进行详细的审计跟踪。 |
| 用户交互数据 | 记录捕捉用户与网站或应用程序的交互。重复记录可以代表重复的交互,用于分析用户行为。 |
| 变更历史 | 数据版本或文档变更导致多个记录,包括捕捉历史修订的重复记录。保留重复记录用于版本历史。 |
表 2.2 – 重复记录场景
在这些场景中,允许重复记录是为了实现特定的数据管理或分析目标,例如保留历史数据、记录变更或捕捉详细的用户交互。数据的表示方式与这些目标一致,重复记录是故意保留的,以支持这些用例。
让我们通过一个代码示例来看如何跟踪重复记录。代码将输出数据集中找到的重复记录数量:
-
首先,我们导入
pandas:import pandas as pd -
接下来,我们创建一个包含员工信息的示例数据集。我们故意引入重复的员工 ID 来演示如何识别重复记录:
data = { 'EmployeeID': [101, 102, 103, 101, 104, 105, 102], 'FirstName': ['Alice', 'Bob', 'Charlie', 'David', 'Eve', 'Frank', 'Bob'], 'LastName': ['Smith', 'Johnson', 'Brown', 'Davis', 'Lee', 'White', 'Johnson'], } df = pd.DataFrame(data) -
我们将使用 pandas 根据
EmployeeID列来识别并标记重复记录。duplicated()函数用于创建布尔掩码,True表示重复记录:duplicated_mask = df.duplicated(subset='EmployeeID', keep='first')subset='EmployeeID'参数指定用于检查重复的列。keep='first'会将重复记录标记为True,除了第一次出现的记录。你可以根据需要将此参数更改为last或False。 -
然后,我们在 DataFrame 中创建一个新的列
'IsDuplicate',用于指示每条记录是否为重复记录:df['IsDuplicate'] = duplicated_mask -
我们通过将重复记录的数量(在
IsDuplicate列中标记为True的记录)除以记录总数,然后乘以 100 来计算重复记录的百分比,并将其表示为百分比:duplicate_percentage = (df['IsDuplicate'].sum() / len(df)) * 100 -
最后,我们显示包含
IsDuplicate列的数据集,以查看哪些记录是重复的。以下是最终的输出:EmployeeID FirstName LastName IsDuplicate 0 101 Alice Smith False 1 102 Bob Johnson False 2 103 Charlie Brown False 3 101 David Davis True 4 104 Eve Lee False 5 105 Frank White False 6 102 Bob Johnson True Percentage of Duplicate Records: 28.57%
该输出表示数据集中的 28.57%的记录是重复的。
注意
重复记录越少, 越好!
数据集中什么样的重复记录数量被认为是“可接受”或“良好”的标准,可能会根据具体情况和数据管理或分析的目标而有所不同。这个问题没有统一的答案,因为它取决于数据类型、数据集的目的和行业标准等因素。
数据使用
数据使用评估数据在组织内的有效利用程度。数据使用的关键绩效指标(KPI)可以包括数据利用率、数据请求或查询的数量,或者关于数据可用性和质量的用户满意度调查。
场景 – 企业商业智能仪表板
假设一家大型企业依赖数据驱动决策来优化其运营、营销策略和财务表现。该公司有一个集中式的商业智能(BI)仪表板,向不同部门和团队提供各种数据分析和洞察。这个仪表板对于监控公司的业绩并做出明智决策至关重要。
在这种情况下,评估数据使用指标对于优化 BI 仪表板的有效性以及确保它满足组织的数据需求至关重要。以下是我们在代码示例中将要追踪的内容:
-
数据利用率:通过跟踪不同部门和团队的数据利用率,组织可以评估仪表板的访问频率以及其中数据的使用广度。例如,营销部门可能有较高的数据利用率,表明他们在依赖仪表板进行活动绩效分析。此指标有助于识别组织中数据驱动洞察最为关键的领域。
-
数据请求或查询的数量:监控用户发出的数据请求或查询的数量,可以为我们提供关于通过仪表板进行的数据分析量的洞察。较高的数据请求数量可能表明对数据驱动决策的强烈需求。此指标还可以帮助识别使用高峰时段和受欢迎的数据源。
-
用户满意度得分:通过调查收集用户满意度得分,可以衡量 BI 仪表板在多大程度上满足用户的期望。较低的平均用户满意度得分可能表示仪表板的功能或用户体验需要改进。来自用户的反馈可以指导仪表板的改进。
-
组织数据利用率:计算整个组织的总体数据利用率有助于评估仪表板的相关性及其在实现更广泛业务目标中的有效性。这还为衡量数据利用率的改进提供了一个基准。
要计算上个月的数据请求数量,你需要记录应用程序的数据请求及其相关的时间戳。我们来看一个例子:
-
首先,我们导入
random库:import random -
接下来,我们创建一个函数来模拟数据使用指标。在这个函数中,我们将组织中的用户数量设置为 500 个用户,但在实际场景中,你需要用组织中实际的用户数量来替代。让我们来看一下下面的函数:
def simulate_data_usage(): num_users = 500 data_utilization_rates = [random.uniform(20, 90) for _ in range(num_users)] data_requests = [random.randint(1, 100) for _ in range(num_users)] organization_data_utilization_rate = sum(data_utilization_rates) / num_users total_data_requests = sum(data_requests) user_satisfaction_scores = [for _ in range(num_users)] avg_user_satisfaction_score = sum(user_satisfaction_scores) / num_users return { "data_utilization_rates": data_utilization_rates, "organization_data_utilization_rate": organization_data_utilization_rate, "data_requests": data_requests, "total_data_requests": total_data_requests, "user_satisfaction_scores": user_satisfaction_scores, "avg_user_satisfaction_score": avg_user_satisfaction_score, }该函数的主要目标是模拟组织中每个用户的数据利用率。
random.uniform(20, 90)函数生成一个介于 20 和 90 之间的随机浮动数值。我们对每个用户都进行这种操作,结果是一个数据利用率的列表。同样,我们模拟每个用户所做的数据请求或查询的数量。在这里,我们使用random.randint(1, 100)为每个用户生成一个 1 到 100 之间的随机整数,表示数据请求的次数。接下来,我们计算两个组织级的指标,第一个是整个组织的平均数据利用率,第二个是所有用户的总数据请求或查询次数。我们使用 1 到 5 的评分来模拟用户满意度分数。每个用户都会得到一个随机的满意度分数。我们基于模拟的满意度分数来计算整个组织的平均用户满意度分数。 -
我们调用
simulate_data_usage()函数来运行模拟并将结果存储在data_usage_metrics变量中:data_usage_metrics = simulate_data_usage() -
最后,我们展示模拟的数据显示指标。输出结果如下:
Organization Data Utilization Rate: 54.83% Total Number of Data Requests or Queries: 25794 Average User Satisfaction Score: 2.93
捕捉不同数据产品的使用情况对于几个原因至关重要,特别是在那些依赖数据进行决策和提升运营效率的组织中:
-
优化资源:通过了解数据产品的使用情况,组织可以有效地分配资源。这包括识别哪些数据源被大量使用,哪些数据源可能未被充分利用。这有助于优化数据存储、处理和基础设施资源。
-
提升数据质量:监控数据使用情况可以突显数据质量问题。例如,如果某些数据产品很少被访问,可能表明这些数据质量较差或已不再相关。捕捉使用情况可以促进数据质量的改进。
-
识别趋势和模式:数据使用模式可以揭示数据如何被消费,以及哪些类型的分析或报告对用户最有价值。这些信息可以为数据产品的开发和改进策略提供依据。
-
成本管理:了解哪些数据产品需求量大有助于管理与数据相关的成本。它使组织能够明智投资资源,避免在维护或存储使用较少的数据上产生不必要的费用。
-
安全和合规性:跟踪数据使用对数据安全和合规性至关重要。组织可以识别未经授权访问或异常使用模式,这可能表明安全漏洞。它还有助于通过展示对数据访问的控制来遵守数据隐私法规。
-
用户满意度:了解数据产品的使用方式及其是否满足用户需求对用户满意度至关重要。它允许组织根据用户要求定制数据产品,从而提供更好的用户体验。
-
容量规划:捕捉使用数据有助于数据基础设施的容量规划。它确保在高峰使用期间有足够的容量处理数据流量,避免性能瓶颈。
-
投资回报率 (ROI) 测量:对于投资于数据产品的组织来说,跟踪使用情况对于衡量投资回报率至关重要。它有助于确定数据收集、处理和呈现所耗费的资源是否因其对决策和业务结果的影响而合理。
接下来,让我们讨论数据合规性。
数据合规性
数据合规性评估数据遵守法规要求、行业标准或内部数据治理政策的程度。合规性 KPI 可能涉及诸如非合规数据记录数量、符合特定法规的数据百分比或数据合规审计结果等指标。在今天的数据驱动和高度监管的业务环境中,数据合规性对几个重要原因尤为关键,如下表所示。
| 后果/挑战 | 描述 |
|---|---|
| 法律和监管后果 | 不合规可能导致法律诉讼、罚款和处罚 |
| 声誉损失 | 负面宣传和失去顾客及利益相关者的信任 |
| 财务影响 | 与罚款、法律费用、数据泄露通知等相关的成本 |
| 数据泄露 | 增加安全漏洞和未经授权访问的风险 |
| 数据质量问题 | 不准确或不完整的数据影响决策和效率 |
| 顾客流失 | 顾客中断与不合规组织的关系 |
| 法律责任 | 个人和组织可能面临的法律责任 |
| 额外的监控和监督 | 强化的法规监控和监督 |
| 难以扩展到国际市场 | 因国际非合规性而阻碍全球扩展 |
表 2.3 – 忽视数据合规性的后果
这里有一个 Python 示例,用于说明一个简化的情景,我们使用随机生成的数据检查数据记录是否符合特定规定的合规性:
-
我们首先导入
random库:import random -
接下来,我们创建一个函数来模拟带有合规性检查的数据集,数据记录的数量由给定的值决定:
def simulate_data_compliance(num_records): data_records = [] compliant_count = 0 # Counter for compliant records -
每条数据记录由诸如
Age和Consent Given等属性组成,这些属性是随机生成的:for _ in range(num_records): # Generate a random record (e.g., containing age and consent fields) age = random.randint(18, 100) consent_given = random.choice([True, False]) -
我们根据一个简化的场景定义这些属性的合规性规则,例如,个人必须年满 18 岁或以上才能提供同意:
age_rule = age >= 18 consent_rule = age >= 18 and consent_given -
我们检查是否符合特定的法规,并且对于每条数据记录,我们报告其是否符合
Age和Consent要求:age_compliant = "Age Compliant" if age_rule else "Age Non-Compliant" consent_compliant = "Consent Compliant" if consent_rule else "Consent Non-Compliant" # Define overall compliance status compliance_status = "Compliant" if age_rule and consent_rule else "Non-Compliant" -
我们引入了一个
compliant_count变量,用于跟踪符合合规性的记录数量:# Count compliant records if compliance_status == "Compliant": compliant_count += 1 data_records.append({ "Age": age, "Consent Given": consent_given, "Age Compliance": age_compliant, "Consent Compliance": consent_compliant, "Overall Compliance Status": compliance_status })在生成数据记录的循环内部,每当记录符合定义的规则时,我们就递增
compliant_count。 -
在生成所有记录后,我们计算符合合规性记录的百分比为
(compliant_count / num_records) * 100,并将其存储在percentage_compliant变量中:# Calculate the percentage of compliant records percentage_compliant = (compliant_count / num_records) * 100 return data_records, percentage_compliant -
我们定义了要模拟的记录数量,并通过调用我们的
simulate_data_compliance函数开始模拟合规性检查:# Define the number of data records to simulate num_records = 100 # Simulate data compliance checks data_records, percentage_compliant = simulate_data_compliance(num_records) -
最后,我们显示结果:
# Display the results for a sample of data records and the percentage of compliance sample_size = 10 for record in data_records[:sample_size]: print(record) print(f"\nPercentage of Compliant Records: {percentage_compliant:.2f}%")这将显示以下输出:
Percentage of Compliant Records: 49.00%
下面是一个总结常见合规性检查及其示例的表格:
| 合规性检查 | 描述 和示例 |
|---|---|
| 数据隐私合规性 | 确保个人身份信息(PII)的保护;一个例子是安全存储客户姓名和地址 |
| GDPR 合规性 | 遵守 GDPR;一个例子是处理用户数据访问和删除请求 |
| HIPAA 合规性 | 根据 HIPAA 确保医疗数据保护;一个例子是对电子受保护健康信息(ePHI)的安全处理 |
| PCI DSS 合规性 | 遵守 PCI DSS;一个例子是支付处理过程中加密信用卡信息 |
| 数据保留合规性 | 管理数据保留期限,并确保安全归档或删除 |
| 同意合规性 | 验证用户对数据收集和处理的明确同意;一个例子是电子邮件营销的选择同意 |
| 准确性与完整性合规性 | 定期检查和修正数据的准确性与完整性 |
| 数据分类与处理合规性 | 根据敏感性对数据进行标记并强制访问控制;一个例子是将数据分类为机密并限制访问 |
| 数据加密合规性 | 对敏感数据进行传输中和静态加密 |
| 访问控制合规性 | 实施基于角色的访问控制以限制数据访问 |
| 审计与日志记录合规性 | 维护数据访问和更改的审计日志 |
| 数据屏蔽与匿名化合规性 | 通过数据屏蔽或匿名化保护敏感数据 |
| 数据生命周期管理合规性 | 按照政策管理数据从创建到销毁的整个过程 |
| 数据伦理和道德合规 | 确保数据实践符合伦理标准 |
| 避免歧视合规性 | 避免数据的歧视性使用;例如,金融服务中的公平贷款实践 |
表 2.4 – 关键合规性检查
实际中,组织可能会选择按日、周、月或季度计算数据质量 KPI,包括完整性。在测量频率和有效评估所需资源之间取得平衡是非常重要的。定期监控和调整计算频率可以帮助确保数据质量根据业务需求持续评估和维护。
如果数据有频繁的更新,数据的监控指标也应当频繁。这确保了数据的任何变化或更新都能及时捕捉,并且质量指标保持最新。
数据越关键,监控指标更新的频率应该越高。这里是关键数据的定义:
| 特征 | 描述 |
|---|---|
| 对核心运营至关重要 | 对日常组织功能至关重要 |
| 决策的关键 | 在战略、战术和运营决策中起到关键作用 |
| 高价值和影响 | 与显著的财务价值和运营影响相关 |
| 敏感和机密 | 经常包含敏感和机密信息 |
| 业务连续性和灾难恢复 | 对于连续性规划和恢复措施至关重要 |
| 客户信任与满意度 | 直接影响信任与满意度 |
| 竞争优势 | 可能提供竞争优势 |
| 战略资产 | 被视为战略资源 |
表 2.5 – 关键数据定义
如果用于质量衡量的数据对关键决策过程或敏感操作至关重要,则可能需要更频繁地计算质量 KPI。
现在我们理解了如何根据不同的质量 KPI 评估我们的数据产品,让我们看看在数据生命周期的哪些节点需要应用这些指标。
实施数据生命周期中的质量控制
数据质量应当是数据整个生命周期中的根本考虑。从数据采集到下游分析团队的使用,数据经历了各种变化,确保每个步骤中的质量至关重要。以下是质量检查生命周期的示意图:

图 2.1 – 质量检查生命周期
让我们更深入地了解每个步骤需要发生的内容:
-
数据输入/采集:验证数据源,并确保数据在进入系统时准确、一致地捕获,可以减少下游过程中的错误。
数据角色 --> 数据工程师
-
数据转化:通过在数据转化层中加入质量检查,组织确保数据在从原始来源到最终目的地的整个过程中始终保持可靠、准确和一致。
数据角色 --> 数据工程师
-
数据整合:当从多个来源或系统合并数据时,数据整合可能会引入错误和不一致。此层次的质量检查有助于防止数据质量问题在数据生态系统中传播,并支持对整合数据的高度信任。
数据角色 --> 数据工程师和数据科学家
-
数据消费:分析和机器学习模型在很大程度上依赖于输入数据的质量。在当今以数据为驱动的环境中,这一点尤为重要,因为数据质量直接影响组织的成功与竞争优势。
数据角色 --> 数据科学家,分析师
如前面列表所示,数据在系统中流动。不同的团队合作定义质量指标并应用质量控制。现在,让我们看看如果不同团队之间没有合作,会发生什么。
数据孤岛及其对数据质量的影响
数据孤岛,也称为孤立的数据存储库,在今天的许多组织中普遍存在。数据孤岛是指将数据存储和管理在组织内部孤立或不连接的系统或部门中的做法。这些孤立的数据存储库随着时间的推移发展起来,不同部门或业务单元单独维护数据,导致数据整合变得复杂。组织越来越意识到数据孤岛带来的局限性,认识到它们妨碍了数据驱动的决策和运营效率。因此,打破数据孤岛、推动数据整合和质量提升的努力正在增加,旨在充分挖掘数据资源的潜力。
这些孤岛在我们已经讨论过的维度中给数据质量的维护带来了挑战:
-
不与组织其他部门共享数据会削弱其竞争优势:数据孤岛通过迫使员工花时间从不同的来源寻找数据,拖慢了决策过程,将他们的注意力从获取洞察力和采取行动转移开。通常,数据孤岛与重复性工作相关联,因为各团队独立完成相似任务,缺乏高效的协作和信息共享。不同的数据源之间常常会出现对指标的不同解读,导致团队之间的混淆和争议。假设和视角的不一致阻碍了进展和方向。建立清晰的沟通指南并执行标准化方法是确保对齐期望、促进整个组织理解的关键。
-
不与整个组织共享数据是非常昂贵的:数据孤岛由于维护跨组织的多个分散系统而增加了成本。维护这些不同的系统需要专门的资源,包括人员和基础设施(例如在多个地方的冗余存储)。由于数据存储分散,检索相关信息变得耗时,导致延迟。手动整合来自不同来源的数据会引入潜在错误。
现在,让我们总结一下本章所学的内容。
总结
在本章中,我们讨论了高质量数据的重要性,它为分析、机器学习和明智决策提供了坚实的基础。为了确保数据质量,组织在数据管道的各个阶段实施了一系列检查和措施:
-
数据录入/摄取:数据源会进行验证,以确保数据捕获的准确性和一致性,主要由数据工程师监管。
-
数据转化:质量检查被纳入转化层,以保持数据的可靠性和准确性,通常由数据工程师管理。
-
数据集成:检查可以防止数据质量问题的蔓延,并支持对集成数据的信任,涉及数据工程师和数据科学家。
-
数据消费:高质量的数据输入对于分析和机器学习至关重要,影响用户信任和竞争优势,由数据科学家和分析师推动。
这些质量检查确保数据遵循定义的标准,符合监管要求,并适合其预定用途。通过实施这些检查,组织保持数据的准确性、可靠性和透明度,从而促进更好的决策制定,确保数据驱动的成功。
在下一章,我们将探讨如何使用分析工具持续且自动地监控数据质量。
第三章:数据分析 – 理解数据结构、质量和分布
数据分析指的是对数据集进行细致检查、理解和验证,从中了解其潜在的结构、模式和质量。这是数据管理和数据摄取中的关键步骤,因为它能提高数据质量和准确性,并确保符合监管标准。在本章中,你将学习如何使用不同的工具进行数据分析,并了解如何随着数据量的增加调整策略。
在本章中,我们将深入探讨以下主题:
-
理解数据分析
-
使用 pandas profiler 进行数据分析
-
使用《远大前程》进行数据验证
-
比较《远大前程》和 pandas profiler – 何时使用哪一个
-
如何分析大数据量
技术要求
本章需要安装 Python 解释器,可以通过以下链接下载并按照说明进行安装:www.python.org/downloads/。
你可以在以下 GitHub 仓库找到本章的所有代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/tree/main/chapter03。
理解数据分析
如果在开始本章之前,你从未听说过 数据分析,它是一个全面的过程,涉及分析和检查来自不同来源的数据,以深入了解数据集的结构、质量和整体特征。让我们从描述数据分析的主要目标开始。
数据分析目标识别
数据分析帮助我们理解数据的结构和质量。因此,我们可以更好地了解如何组织不同的数据集,识别潜在的数据整合问题,评估数据质量,并识别和解决可能影响数据可靠性和可信度的问题。
让我们深入探讨数据分析的三个主要目标。
数据结构
数据分析的主要目标之一是理解数据的结构。这包括检查数据类型、格式和不同数据字段之间的关系。
这里有一个简单的表格结构示例。考虑一个名为 Employee 的表格,存储公司员工的信息:
EmployeeID |
FirstName |
LastName |
Position |
Department |
Salary |
|---|---|---|---|---|---|
1 |
John |
Doe |
软件工程师 |
IT |
75000 |
2 |
Jane |
Smith |
数据分析师 |
分析 |
60000 |
3 |
Bob |
Johnson |
项目经理 |
项目管理 |
85000 |
让我们来分析一下这个表格:
-
EmployeeID: 每个员工的唯一标识符 -
FirstName和LastName是存储员工姓名的字段 -
Position: 员工的职位或职称 -
Department: 员工所在的部门 -
Salary:员工的薪资
这个表格结构按行和列进行组织。每一行代表一个特定的员工,每一列代表员工的不同属性或信息。表格结构便于查询、过滤和连接数据。每一列中的值遵循特定的数据类型(例如整数、字符串等),并且可以通过键建立表格之间的关系。
这是一个简化的示例,但在实际应用中,表格可能包含更多列和复杂的关系。
数据质量
数据质量涉及评估数据的整体可靠性和可信度。通过数据分析,我们可以识别出各种数据质量问题,包括重复记录、错误或不一致的值、缺失值和异常值。通过量化这些问题,组织能够了解数据在分析中可以信赖和依赖的程度。
数据分布
理解每个字段或列中的数据分布是数据分析的另一个关键目标。通过分析数据分布,组织可以深入了解数据中的模式、频率和异常。
假设我们为一家电子商务公司工作,收集每天的销售收入数据。通过检查数据分布,我们可以识别出销售趋势:

图 3.1 – 每日销售收入分布
在这个直方图中,我们可以看到销售数据呈正态分布,说明靠近均值的数据比远离均值的数据出现的频率更高。通过这种方式,我们可以了解在正常情况下每天的平均销售额。
现在我们已经了解了数据分析可以解决的挑战,让我们来看看你可以通过哪些不同的方式进行数据分析。
探索性数据分析选项 – 分析工具与手动方法
在进行探索性数据分析(EDA)时,你可以采取不同的方法来理解你的数据,包括进行手动分析或使用分析工具。
手动探索性数据分析(EDA)涉及编写自定义代码或使用通用数据分析库(例如 Python 中的 pandas)来探索数据。它给予你更多的灵活性和对分析过程的控制。你可以根据具体需求和问题定制分析。手动 EDA 允许更深入的探索,包括自定义计算、特征工程和高级可视化。当处理复杂数据或你拥有特定领域知识时,手动 EDA 会很有帮助。
分析工具是专门用于分析和总结数据的工具或库。它自动化了许多 EDA 任务,并提供数据结构、汇总统计、缺失值、数据类型和分布的快速洞察。它通过自动化重复性任务并提供数据集的全面概览,节省了时间。
让我们更详细地了解何时使用什么:
| 手动 EDA | 数据分析工具 | |
|---|---|---|
| 优点 | 根据特定需求灵活探索数据 | 自动化流程,快速洞察 |
| 通过自定义代码深入理解数据 | 跨数据集的一致性和标准化分析 | |
| 更大控制权的分析技术和可视化 | 自动化的可视化和汇总统计 | |
| 数据质量问题和异常的识别 | ||
| 缺点 | 需要大量时间且需人工干预,过程重复 | 生成的报告自定义选项有限 |
| 更高的人工错误或偏见的可能性 | 可能无法捕捉复杂的关系或模式 | |
| 分析师或团队之间缺乏标准化 | 与手动分析相比,灵活性较差 | |
| 手动 EDA | 数据分析工具 | |
| 依赖预定义的算法和技术 |
表 3.1 – 手动 EDA 与使用分析工具的比较
随着数据量的增加,手动 EDA 变得越来越耗时,并容易出错,导致结果不一致,并可能忽视数据问题。手动方法也缺乏可扩展性和可重复性,处理大型数据集和有效协作变得困难。这就是为什么在本章其余部分,我们将重点介绍如何使用不同的分析工具对数据进行 EDA;但在实践中,通常会采用结合的方法。我们还将提供一些关于如何根据数据量改变工具的见解。
使用 pandas 的 ydata_profiling 进行数据分析
让我们通过一个 Python 示例来展示使用ydata-profiling库中的ProfileReport类进行数据分析。
首先,让我们从安装一些库开始:
pip install pandas
pip install ydata-profiling
pip install ipywidgets
在下面的代码示例中,我们将使用seaborn库中的iris数据集,它是一个开源数据集。
接下来,我们将读取数据集并使用最少的代码进行初步的 EDA!
-
我们将通过导入库并使用
pandas的read_csv()函数直接从 URL 加载数据集来开始:import pandas as pd import ydata_profiling as pp -
从
seaborn库加载iris数据集:iris_data = pd.read_csv('https: //raw. githubusercontent .com/mwaskom/ seaborn-data/master/ iris.csv') -
接下来,我们将通过使用
pandas_profiling中的ProfileReport()函数来执行数据分析:profile = pp.ProfileReport(iris_data) -
我们将使用
to_file()方法生成 HTML 报告,将分析结果导出为 HTML 文件,便于分享和进一步分析:profile.to_file("data_profile_report.html") -
可选地,我们可以将报告嵌入到笔记本中:
profile.to_notebook_iframe() -
将报告写入 JSON 文件是可选的,但这是一个最佳实践:
profile.to_file(output_path+"/pandas_profiler.json")
让我们逐一探索分析工具的结果。
概览
分析报告中的第一部分是概览部分。在概览部分,你可以看到多个标签,如下图所示:

图 3.2 – pandas 分析器结果概览
在分析器结果的概览标签中,我们可以看到以下内容:
-
变量数量:鸢尾花数据集包含五个变量——花萼长度、花萼宽度、花瓣长度、花瓣宽度和物种
-
观测数:数据集包含 150 行
-
缺失单元格:鸢尾花数据集中没有缺失值
-
重复行:有一行重复数据
然后,我们可以看到警告标签,如下图所示:

图 3.3 – ydata_profiling 分析器的警告标签
在sepal_length部分:

图 3.4 – 数值特征分析
在sepal_length部分的分析页面中,我们可以获取更多关于特定数值特征的详细信息。对数据集中的所有其他数值特征也进行了类似的分析。我们可以看到该特征有 35 个不同的值,且数据集中没有缺失值。所有值都是正数,这很合理,因为该特征代表花萼的长度,值的范围应在 4.3 到 7.9 之间。直方图展示了该特征的分布情况。

图 3.5 – 类别特征分析
在species部分的分析页面中,我们可以获取更多关于特定类别特征的详细信息。对数据集中的所有其他类别特征也进行了类似的分析。我们可以看到该特征有三个不同的值(sectosa、versicolor和virginica),且数据集中没有缺失值。从图表中可以看出,每个特征值的记录数相同(50 条)。
交互作用
分析报告中的另一个部分是交互作用部分,展示了数据集中不同列之间的关系和潜在的交互作用。这些图表特别有助于识别变量之间的潜在相关性或依赖性,通常以散点图的形式呈现。
下图展示了不同变量之间的交互作用。该图表可以针对所有不同的数值变量组合进行创建。我们来看一下花瓣长度与花瓣宽度的示例。

图 3.6 – 花瓣长度与花瓣宽度的交互作用图
在交互图中,我们可以看到一个变量如何影响另一个变量。例如,随着花瓣长度的增加,花瓣宽度也增加。因此,这两者之间存在着线性关系。由于这两个变量之间有强烈的相互作用,因此深入研究这种相互作用并详细检查这一对变量的相关性图表是一个好主意。
相关性
相关矩阵还描绘了变量之间的相互作用,每个单元格表示两个列之间的关系。单元格的颜色根据检测到的相互作用的强度或类型进行编码。这有助于识别两个变量之间的关系强度。正相关通常以一种颜色显示(例如,蓝色),而负相关则以另一种颜色显示(例如,红色),颜色的深浅表示相关性的强度。
例如,花瓣长度和花瓣宽度之间可能存在正相关,表明花瓣长度增加时,花瓣宽度也会增加。

图 3.7 – 数值变量之间的相关性图
从图表中可以看到,蓝色越深,变量之间的相关性越强。花瓣长度和花瓣宽度的正相关性超过 0.75,表明其中一个增加时另一个也会增加。在进行任何建模工作之前,我们需要注意这一点,因为我们可能不需要在数据集中保留这两个变量,因为拥有其中一个就能预测另一个。例如,如果两个变量高度相关,你可以删除其中一个,或者创建一个新特征来包含这两个变量的信息。在某些情况下,移除高度相关的特征可以加快某些机器学习算法的训练时间,因为算法不需要处理冗余信息;此外,简化模型也能让模型更容易理解,减少过拟合的风险。
注意
高相关性阈值:设置高相关性的阈值(例如,0.8 或 0.9)。相关系数高于此阈值的变量被视为高度相关。
缺失值
数据质量的另一个关键方面是缺失值。它指的是数据集中特定条目或变量中缺少数据。缺失值可能由于多种原因出现,例如数据输入错误、传感器故障或数据获取过程中的错误。如果忽视这些缺失值,可能导致偏倚和不准确的结果。
下图显示了数据中每一列非缺失值的百分比:

图 3.8 – 数据中非缺失值的百分比
在当前示例中,我们可以看到数据集中的所有值都是完整的,且所有特征都有 150 个非空值。因此,这对我们来说是个好消息,我们可以继续进行下一步检查。
重复行
数据集中的重复行是指每一列的值都与另一行相同的行。这意味着数据集中的每一列,在重复行中的值与它所重复的行的值完全一致。揭示重复行的存在和范围有助于我们快速识别潜在的数据质量问题。正如我们所说,重复行可能是由于各种原因产生的,例如数据整合问题、错误的去重过程,或只是数据收集过程的性质。
在分析报告中,我们可以在最常见的重复项下看到重复行,其中展示了数据集中重复项的一个示例。

图 3.9 – 数据中的重复行
一般来说,要查找重复行,您需要识别应当唯一的关键列或列的组合。通常,我们会根据数据集中的所有列来识别重复项。如果存在重复项,说明我们有重复的行。我们的示例数据集中只有两行重复数据,如前图所示。
在当前的分析阶段,我们并不处理重复项,因为我们的重点是理解数据的结构和特征。然而,我们需要调查这些重复项的性质和来源。鉴于它们在数据中所占比例很小,我们可以简单地删除每对相同的行中的一行。
示例数据集
抽样是指从较大的数据集中选择一个数据子集的过程,而不是使用整个数据集。在探索性数据分析(EDA)步骤中,我们通常会使用数据的一个样本,因为它可以提供初步的洞见,并帮助在进行全面分析之前提出假设。

图 3.10 – 示例数据集
现在我们已经了解了如何使用 ydata_profiling 库构建数据分析报告,接下来让我们更详细地看看一个非常流行且类似的分析工具——pandas 数据分析工具。
使用 pandas 数据分析工具分析大量数据
Pandas Profiling 是一个强大的库,用于生成数据集的详细报告。然而,对于大数据集来说,分析过程可能会变得耗时且占用大量内存。处理大数据集时,您可能需要考虑一些优化分析过程的策略:
-
抽样:与其对整个数据集进行分析,不如对数据进行随机抽样来生成报告。这可以显著减少计算时间和内存需求,同时仍然提供数据集的代表性概览:
from ydata_profiling import ProfileReport sample_df = iris_data.sample(n=1000) # Adjust the sample size as per your needs report = ProfileReport(sample_df) -
子集选择:如果你对数据集中的特定列或子集感兴趣,可以仅选择这些列进行分析。这会减少计算负载,并将关注点集中在感兴趣的变量上:
subset_df = iris_data [['sepal_length', 'sepal_width']] # Select columns to profile report = ProfileReport(subset_df) -
配置分析器选项:pandas profiling 库提供了多个配置选项,允许你精细调整分析过程。你可以调整这些选项,以限制分析的深度、减少计算量,或者跳过某些在分析中不必要的耗时任务:
report = ProfileReport(df, minimal=True) # Generate a minimal report -
并行处理:如果你的系统支持并行处理,你可以利用它加速分析过程。通过将工作负载分配到多个核心或机器上,你可以可能减少分析大数据集所需的时间:
import multiprocessing with multiprocessing.Pool() as pool: report = pool.map(ProfileReport, [df1, df2, df3]) # Profiling multiple DataFrames in parallel -
增量分析:如果你的数据集过大,无法完全加载到内存中,可以考虑通过将数据拆分为更小的块并分别进行分析来执行增量分析。然后,你可以将分析结果合并,获得整个数据集的概览:
chunk_size = 10000 chunks = [df[i:i + chunk_size] for i in range(0, len(df), chunk_size)] reports = [ProfileReport(chunk) for chunk in chunks] combined_report = ProfileReport(pd.concat(reports))
注意
这些策略中的一些旨在优化大数据集的分析过程,但与对整个数据集进行分析相比,可能会导致一些粒度和细节的丧失。必须在计算效率和所需的分析深度之间找到平衡。
我们接下来要审查的工具通常用于数据工程密集型的工作流程,因为它提供了大量的灵活性、自动化功能,并且可以与其他工具轻松集成。
使用 Great Expectations 库进行数据验证
Great Expectations 是一个开源的 Python 库,旨在促进数据验证和文档化。它提供了一个框架,用于定义、管理和执行数据质量检查,从而使得在整个数据管道中更容易确保数据的完整性和可靠性。质量检查可以在数据生命周期的不同阶段执行,如下图所示:

图 3.11 – 数据生命周期不同阶段的质量检查
让我们讨论数据生命周期中每个可以应用质量检查的接触点,如前面的图所示:
-
数据输入:在数据输入或数据收集过程中,会进行检查以确保数据的准确捕获和记录。这可能涉及验证数据的格式、范围和类型,以及根据预定义规则或标准进行验证检查。
-
数据转换:如果数据经历了任何转换或变换,如数据清洗或数据标准化,则需要执行质量检查以验证转换后数据的准确性。这有助于确保数据在整个过程中保持完整性。
-
数据集成:在将来自不同来源或系统的数据合并时,必须进行数据质量检查,以识别任何不一致或差异。这可能包括检查重复记录、解决缺失或不匹配的数据,以及调和任何冲突的信息。
-
数据消费:在进行任何数据分析或生成报告之前,必须运行数据质量检查以确保数据的完整性。这包括根据预定义的标准验证数据,检查异常值或不一致,并验证数据集的整体质量。
Great Expectations 允许你为数据设定期望或规则,并在数据生命周期的任何阶段验证数据是否符合这些期望。图 3.12更详细地展示了该库的功能。

图 3.12 – Great Expectations 从数据收集到数据质量结果的流程
如你所见,使用 Great Expectations 时,需要注意三个主要步骤:
-
收集/整理所有你希望应用期望的数据
-
编写期望并将其应用于不同的数据
-
享受干净、高质量和可信数据带来的好处
在下一节中,我们将介绍如何配置 Great Expectations 来验证数据集。
配置 Great Expectations 以适应你的项目
你可以使用 Great Expectations 对照定义的期望来验证数据。该库提供了执行这些验证的功能,并帮助识别数据中的任何不一致或问题。
你需要安装great-expectations库以进行数据分析。你可以使用以下命令在任何 IDE 或终端中安装该库:
pip install great-expectations==0.18.16
这应该会安装该库。我们将使用与之前相同的数据集,以便展示工具之间的差异:
-
让我们从设置项目开始。打开终端,导航到你希望设置新项目的位置,然后运行以下命令来创建一个新文件夹:
mkdir great_expectations -
然后,我们将进入新创建的文件夹,输入以下命令:
great_expectations in your project directory where we’ll store all the Expectations we are going to build. The second command will navigate you inside the great_expectations folder we just created. -
接下来,我们将创建一些文件夹来存储我们的数据和运行示例所需的代码。确保你在
great_expectations目录下,并运行以下命令:mkdir code mkdir data你应该已经创建了以下项目结构:

图 3.13 – Great Expectations 项目初始化
-
接下来,我们将运行以下命令来初始化一个新的 Great Expectations 项目。确保你在
great_expectations文件夹中:great_expectations.yml configuration file, the Expectations and data directories, and other project-specific files. The initialization step is a one-time setup that allows you to define and manage your data Expectations, create validation checkpoints, and generate documentation using Great Expectations. It helps you establish a project-specific configuration and directory structure that enables you to organize your Expectations and maintain consistency across your data pipelines. Once you have initialized the project, you can define Expectations, validate data, and generate reports based on those Expectations using Great Expectations.The preceding code will display the following output:

图 3.14 – Great Expectations 项目初始化
- 当提示时,按 Y,Great Expectations 将继续在
great_expectations文件夹中为我们构建项目结构。文件夹结构将如下所示:

图 3.15 – Great Expectations 文件夹结构
Great Expectations 的文件夹结构遵循特定的约定,用于组织与数据管道相关的配置、期望和数据文档。让我们来进一步了解一下结构:
-
/uncommitted/:该目录包含所有未提交的配置和验证文件。在这里,你定义和修改期望、验证以及数据文档。 -
/checkpoints/:该目录存储检查点文件,这些文件包含一组期望(Expectations),用于与特定数据批次进行验证。检查点对于在数据的特定部分或子集上运行验证非常有用。 -
/expectations/:该目录存储期望套件(Expectation Suites)和期望文件(Expectation files)。期望套件是相关期望的集合,而期望文件包含单个期望。你可以在此文件夹内创建子目录,以根据数据源或数据资产组织你的期望。 -
/plugins/:此文件夹用于存储你可能开发的自定义插件和扩展,用以扩展 Great Expectations 的功能。 -
great_expectations.yml:该配置文件存储 Great Expectations 的部署设置。它包含定义 Great Expectations 如何在你的部署环境中运行的必要信息和参数。
现在我们已经设置并初始化了一个 Great Expectations 项目,接下来让我们使用 Great Expectations 创建我们的第一个数据源。
创建你的第一个 Great Expectations 数据源
到目前为止,我们已经创建了项目结构来构建我们的期望(Expectations)。下一步是获取一些数据来构建期望。为了获取数据集,请访问 github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/tree/main/chapter03/great_expectations/code,获取 1.data_set_up.py 脚本,并将其保存在 great_expectations/code/ 文件夹中。现在,让我们通过运行以下 Python 脚本将一些测试数据写入文件夹:great_expectations/code/1.data_set_up.py。下面是该脚本的样子:
import numpy as np
import pandas as pd
# Load the 'iris' dataset from seaborn library
iris_data = pd.read_csv('https ://raw.githubusercontent .com/mwaskom /seaborn-data /master/iris. csv')
iris_data.to_csv('../data/iris_data.csv', index=False)
print("File written! :)")
在终端中,进入 great_expectations/code/ 目录,执行以下命令:
python 1.data_set_up.py
该脚本执行了一个简单的任务:使用 pandas 库从远程源加载 iris 数据集,该数据集位于 seaborn 库的 GitHub 仓库中。然后,它将此数据集保存为 iris_data.csv 文件,保存在 great_expectations/data 目录下。最后,它打印确认信息,表示文件已成功保存。
现在,我们需要告诉 Great Expectations 我们想使用哪些数据来构建 Great Expectations,以及在哪里找到这些数据。在终端中执行以下命令:
great_expectations datasource new
这将显示以下提示:

图 3.16 – Great Expectations 文件配置
请按照终端中的步骤操作,如图 3.16所示,确保选择1选项,因为我们将处理文件而不是 SQL 数据库。由于我们的数据集足够小,可以放入内存,因此我们可以使用 pandas 进行操作。所以,我们再次选择选项1。接下来,它会提示您输入数据集文件的路径,由于我们将数据集保存在data文件夹中,请输入../data。
完成此步骤后,Great Expectations 会自动为我们创建一个 Jupyter 笔记本以供探索!这个笔记本存储在great_expectations/gx/uncommitted/datasource_new.ipynb路径下,执行后,如果您不想维护不必要的代码,可以直接删除它。这个笔记本的目的是帮助您创建一个 pandas 数据源配置,避免任何人工错误。
打开笔记本,更新datasource_name,如以下截图所示,并执行笔记本中的所有单元格。

图 3.17 – Great Expectations – 自定义数据源名称
此时我们可以给它任何名称,但为了与传入的数据保持一致,我们将其命名为iris_data。从现在开始,当我们提到iris_data时,我们知道我们正在处理在前一步中创建的iris数据源的 Expectations。
注意
在 Expectation 验证和数据源之间保持一致和清晰的命名,可以提高可读性,减少错误,并简化维护和调试!
创建您的第一个 Great Expectations 套件
现在我们已经声明了要为其构建 Expectation 的数据源,接下来让我们为iris数据集构建第一个套件。
打开终端并执行以下命令:
great_expectations suite new
如下图所示,您可以通过多种方式创建 Expectation 套件。

图 3.18 – Great Expectations – 创建您的套件的选项
让我们探讨每个选项:
-
手动操作,不与样本批数据交互(默认):这种方法涉及手动定义 Expectations 并配置套件,而不直接与样本批数据交互。Expectations 通常基于您对数据的了解和项目的具体要求。通过指定条件、范围、模式和其他您期望数据满足的标准来定义 Expectations。此方法需要对数据和领域知识有透彻的了解,以定义准确的 Expectations。 -
交互式,使用样本数据批次: 在这种方法中,你将一个小的代表性数据批次加载到 Great Expectations 中,并使用它交互式地定义期望值。这使你能够直观地检查数据,识别模式,并探索各种数据统计信息。你可以基于对数据的观察和理解,迭代地构建和完善期望值。 -
自动化,通过数据助手: Great Expectations 提供了一个数据助手功能,该功能根据数据自动建议期望值(Expectations)。数据助手分析数据并生成一组建议的期望值,您可以查看并自定义这些期望值。当你对数据了解有限或希望快速生成期望值的起始点时,这个方法特别有帮助。你可以利用建议的期望值作为基础,并根据自己的领域知识和具体要求进一步优化它们。数据助手通过自动生成初始期望值来加速构建期望值套件的过程。
在这个例子中,我们将使用第三个选项自动构建套件。此功能类似于 pandas profiling 提供的功能,我们之前已经在使用 pandas’ ydata_profiling 进行数据分析部分中探讨过。所以,请继续在终端中选择选项3,如图 3.19所示。

图 3.19 – Great Expectations – 数据助手选项
下一步,你将被要求选择希望为其创建套件的数据源,这将是上一步的输出。键入1以选择我们之前构建的iris_data源,然后输入新期望值套件的名称:expect_iris。
执行前面的命令后,一个新的笔记本将会自动创建在great_expectations/gx/uncommitted/edit_expect_iris.ipynb。打开并阅读该笔记本以理解代码的逻辑;总的来说,这个笔记本帮助你从数据中选择你关心的列和其他因素,并让分析器为你创建一些可以稍后调整的期望值(Expectations)。
你可以选择为数据集中的所有列或其中的一部分创建期望值,如图 3.20所示。

图 3.20 – Great Expectations – 套件中包含的列
你可以将所有你不希望创建期望值的列名称添加到exclude_column_name列表中。对于没有添加到该列表中的任何列,great_expectations将为你构建期望值。在我们的例子中,我们希望为所有列创建期望值,因此我们将列表留空,如图 3.21所示。

图 3.21 – Great Expectations – 从套件中排除列
记得执行笔记本中的所有单元格,让我们来看看great_expectations为我们自动构建的所有不同期望。
Great Expectations 套件报告
让我们来看看由great_expectations创建的分析结果。如图3.22所示,已创建 52 个期望,且都已成功通过。我们可以在概览选项卡中监控成功百分比,以便快速了解每当新的数据流入您的数据管道时,有多少期望通过。

图 3.22 – 报告概览统计
让我们更仔细地看看我们正在验证数据的期望。首先要考虑的是跨表或表级期望,如下图所示:

图 3.23 – 表级期望
这些期望检查数据集中的列是否与给定的列名集匹配,并且数据集是否具有预期的列数。这对于确保传入的数据包含所有预期列非常有用。如果传入的数据未包含期望中的所有列,则该过程将失败。

图 3.24 – 列级期望
下一组期望是为表格中的每一列创建的,我们将其称为特征期望。

图 3.25 – 特征级期望
这些期望会针对每一列分别检查,它们可以包含特征的最小值和最大值、是否接受该列的空值以及其他许多内容。记住,到目前为止,所有的期望都是通过我们使用的工具自动生成的,这些工具并不理解数据的业务上下文。所以,记得根据对数据的业务理解来检查并更新期望,正如我们将在下一部分展示的那样。
手动编辑 Great Expectations
虽然自动生成的期望提供了一个很好的起点,但它们可能不足以满足生产环境下的数据验证需求。在这一阶段,进一步精炼和定制期望套件非常重要。您可以选择手动或交互式编辑期望套件。通常,当您清楚理解预期的数据属性,并希望高效、准确地定义期望时,手动编辑是首选。由于我们已经完成了数据的基本自动分析,因此我们将选择手动编辑方法。
打开终端并执行以下命令:
great_expectations suite edit expect_iris
您将被提示选择如何更新期望套件,可以选择手动或交互式更新。我们将选择手动进行更新。
提供必要的输入后,Great Expectations 会在以下位置打开可用的 Jupyter Notebook:great_expectations/gx/uncommitted/edit_expect_iris.ipynb。该 Notebook 显示了所有自动生成的 Expectations 的完整列表。这使你能够详细查看和检查 Expectations,清晰地了解 Great Expectations 从数据中推断出的验证规则。查看我们创建的所有 Expectations,并根据需要更新它们。如果你不想使用 Notebook,可以直接打开 great_expectations/gx/expectations/expect_iris.json 文件并在其中更新。
检查点
到目前为止,我们已经建立了与训练数据集的连接,并根据训练数据定义了 Expectations。下一步是将这些 Expectations 应用到新的数据流上,以验证新数据集,并确保其通过检查。因此,我们需要创建 Great Expectation 套件与新数据之间的连接以进行验证。我们可以通过检查点来实现这一点。为此,我们将首先模拟一些测试数据来应用 Expectations。你可以在以下位置找到脚本:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter03/great_expectations/code/2.mock_test_dataset.py。
将其保存在 great_expectations/code/ 文件夹中。脚本会自动将测试文件保存到所需的位置,即 great_expectations/data/。
在终端中,在 great_expectations/code/ 目录下,执行以下命令:
python 2.mock_test_dataset.py
让我们仔细看一下我们刚刚执行的代码,从导入语句开始:
import numpy as np
import pandas as pd
从 seaborn 库加载 iris 数据集:
iris_data = pd.read_csv('https: //raw.githubusercontent.com/ mwaskom/seaborn-data /master/iris. csv')
我们将做一些转换,这些转换会导致 Expectations 失败,在这种情况下,我们将把 sepal_length 的值更新为 60,这将打破我们的 Expectations:
iris_data['sepal_length'] = 60
我们还将重命名列名,以展示列名的更改,并进一步展示数据预期的模式:
iris_data.rename(columns={'petal_width': 'petal_w'}, inplace=True)
我们将编写一个 DataFrame,作为新的数据源来测试我们的 Expectations:
iris_data.to_csv('../data/iris_data_test.csv', index=False)
print("File written! :)")
然后,我们需要创建一个检查点,执行我们在测试数据集上创建的 Great Expectation Suite。要启动检查点,你可以在终端中运行以下命令:
great_expectations checkpoint new expect_iris_ckpnt
执行后,Great Expectations 会自动生成一个 Jupyter Notebook,提供有关检查点的有用信息,位置在:/great_expectations/gx/uncommitted/edit_checkpoint_expect_iris_ckpnt.ipynb。其中包含有关应用检查点的数据的详细信息。在执行 Notebook 之前,我们需要更新文件名,并指向测试文件,如下所示:
my_checkpoint_name = "expect_iris_ckpnt" # This was populated from your CLI command.
yaml_config = f"""
name: {my_checkpoint_name}
config_version: 1.0
class_name: SimpleCheckpoint
run_name_template: "%Y%m%d-%H%M%S-my-run-name-template"
validations:
- batch_request:
datasource_name: iris_data.csv
data_connector_name: default_inferred_data_connector_name
data_asset_name: iris_data_test.csv
data_connector_query:
index: -1
expectation_suite_name: expect_iris
"""
print(yaml_config)
取消注释最后两行代码,然后执行 Notebook 中的所有单元格:
context.run_checkpoint(checkpoint_name=my_checkpoint_name)
context.open_data_docs()
上述笔记本将把检查点应用于新的数据集,并创建一份报告,列出所有通过或失败的期望。让我们看看结果吧!

图 3.26 – 期望结果
正如预期的那样,我们的期望在列名和花瓣宽度上失败,因为它由于架构变化无法找到正确的列名。

图 3.27 – 由于架构变化导致的期望失败
它还提醒了我们 sepal_length 变量,因为所有值都不符合预期,超出了它所看到的可接受范围!

图 3.28 – 由于超出范围的值导致的期望失败
你能看到它能为我们节省多少问题吗?如果这批数据没有经过检查并直接被导入,后续的处理和集成管道会失败,并且需要大量工作来确定哪个流程失败以及原因。在我们的案例中,我们清楚地知道问题从哪里开始,并且有明确的修复方法。
注意
检查点设计为可重用的,因此你可以在多个数据批次到达时,使用相同的检查点配置来运行。这使得你能够始终如一地验证传入数据是否符合相同的期望集。此外,检查点可以通过各种操作进行增强,例如发送通知、更新数据文档(Data Docs),或根据验证结果触发下游流程。
现在,如果你对 Great Expectations 提供的自动化印象深刻,并希望了解如何将你迄今为止使用 pandas profiling 的所有内容迁移到 Great Expectations Suites 中,那么我们为你准备了相关内容。继续阅读吧。
使用 pandas profiler 构建你的 Great Expectations Suite
pandas profiler 具有一个功能,允许你通过 pandas profiling 过程构建 Expectation Suites。让我们看一下以下示例 great_expectations/code/3.with_pandas_profiler.py:
import pandas as pd
from ydata_profiling import ProfileReport
# Load the 'iris' dataset from seaborn library
iris_data = pd.read_csv('https: //raw.githubusercontent. com/mwaskom/seaborn-data /master/iris. csv')
# run Pandas Profiling
profile = ProfileReport(iris_data, title="Pandas Profiling Report", explorative=True)
# obtain an Expectation Suite from the profiling
suite = profile.to_expectation_suite(suite_name="my_pandas_profiling_suite")
在这段代码示例中,我们获取了数据并创建了一个 pandas profiling。接着,我们从之前创建的报告中获得了一个 Expectation Suite。我们可以使用这个套件进一步验证并检查另一批数据。
到目前为止,我们已经回顾了不同的分析工具及其工作原理。接下来的步骤是更好地理解何时使用哪种工具以及从哪里开始。
比较 Great Expectations 和 pandas profiler – 何时使用哪个
Pandas profiling 和 Great Expectations 都是数据分析和数据概况分析中有价值的工具,但它们各自有不同的优势和应用场景。以下是对这两种工具的比较。
| Pandas Profiler | Great Expectations | |
|---|---|---|
| 数据探索 | 提供快速的洞察和探索性数据总结 | 专注于数据验证和文档编制 |
| 数据验证 | 限制的数据验证能力 | 高级数据验证,具有明确的期望和规则 |
| 定制化 | 限制的定制选项 | 提供广泛的定制选项,用于定义期望和规则 |
| 学习曲线 | 相对容易使用 | 定义期望和配置时具有较陡的学习曲线 |
| 可扩展性 | 适用于小型到中型数据 | 可扩展至大数据环境,支持分布式处理 |
| 可视化 | 生成交互式可视化 | 更注重数据验证和文档编制,而非可视化 |
| 使用案例 | 快速数据探索和初步洞察 | 数据质量控制和强制数据一致性 |
表 3.2 – Great Expectations 与 pandas profiler 比较
Pandas profiling 非常适合快速的数据探索和初步洞察,而 Great Expectations 则在数据验证、文档编制和执行数据质量规则方面表现突出。Pandas profiling 更适合初学者,能提供即时的洞察,而 Great Expectations 则提供更多的定制选项,并且能够扩展到更大的数据集。选择两者之间的工具,取决于项目的具体需求以及所需的数据质量控制级别。
随着数据量的增加,我们需要确保所选择的工具也能够进行扩展。让我们看看如何使用 Great Expectations 实现这一点。
Great Expectations 与大数据
虽然《远大前程》可以有效地用于较小的数据集,但它也提供了机制来解决在大数据环境中扩展数据验证和文档编制的挑战。以下是随着数据量增加,扩展 Great Expectations 的一些注意事项:
-
分布式处理框架:Great Expectations 与流行的分布式处理框架(如 Apache Spark)无缝集成。通过利用这些框架的并行处理能力,Great Expectations 可以将数据验证工作负载分布到集群中,从而实现高效的处理和扩展性。
-
分区和采样:Great Expectations 简化了分区和采样大数据集的过程,并提高了性能和可扩展性。与需要在诸如 pandas profiling 等工具中手动进行分区不同,Great Expectations 自动创建数据子集或分区以供分析和验证。此功能使您能够验证数据的特定子集或分区,而无需一次处理整个数据集。通过自动化分区过程,Great Expectations 简化了分析流程,并消除了手动创建数据块的需求,节省了时间和精力。
-
增量验证:Great Expectations 支持增量验证,而不是每次都重新验证整个大数据集。这意味着当新数据被摄入或处理时,只需要验证相关部分或变化,从而减少整体验证的时间和精力。这是减少检查全部数据所需时间并优化成本的绝佳技巧!
-
缓存和记忆化:Great Expectations 采用缓存和记忆化技术,以优化在重复执行相同验证时的性能。当处理大数据集时,特别有益,因为先前计算的结果可以存储并重复使用,从而最小化冗余计算。
-
基于云的基础设施:利用基于云的基础设施和服务可以提升 Great Expectations 的可扩展性。通过使用云计算平台,如 AWS 或 Azure,你可以动态地扩展资源,以应对增加的数据量和处理需求。
-
高效数据存储:选择适合大数据的优化数据存储技术,如分布式文件系统或列式数据库,可以提升 Great Expectations 的性能和可扩展性。这些技术旨在高效处理大规模数据,并为验证和处理任务提供更快的访问速度。
注意
尽管 Great Expectations 提供了可扩展性选项,但具体的可扩展性措施可能取决于底层基础设施、数据存储系统和你所使用的大数据环境中的分布式处理框架。
总结
本章详细说明了数据分析在确保数据集质量、完整性和可靠性方面的重要性。该过程涉及对数据的深入分析,以了解数据结构、模式和潜在问题。为了进行有效的分析,诸如 pandas profiling 和 Great Expectations 等工具提供了强大的解决方案。Pandas profiling 自动生成综合报告,提供有关数据特征的宝贵见解。而 Great Expectations 则便于创建数据质量预期并允许系统化验证。虽然这些工具在小型数据集上表现出色,但将分析扩展到大数据需要专门的方法。学习数据抽样和并行处理等技巧,有助于在大数据集上进行高效且可扩展的分析。
在下一章中,我们将重点讨论如何清理和处理数据,确保数据格式正确,能够通过预期验证并成功摄入。
第四章:清理杂乱数据与数据处理
在本章中,我们将深入探讨数据处理的策略,重点介绍高效清理和修复杂乱数据集的技巧。我们将移除无关列,系统地处理不一致的数据类型,并修复日期和时间。
在本章中,我们将涵盖以下主题:
-
重命名列
-
移除无关或冗余的列
-
修复数据类型
-
处理日期和时间
技术要求
你可以在以下 GitHub 链接找到本章的所有代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/tree/main/chapter04。
每个文件都根据本章所涉及的相应章节命名。
重命名列
为列重命名更具描述性和意义的名称,使得每列的内容和目的更加容易理解。清晰直观的列名提高了数据集的可解释性,特别是在与他人共享或协作时。
为了更好地理解本章中介绍的所有概念,我们将在本章中使用一个场景。假设有一家电子商务公司,想要分析客户的购买数据,以优化其营销策略。数据集包含有关客户交易的信息,如购买金额、支付方式和交易时间戳。然而,数据集很杂乱,需要清理和处理才能提取有意义的洞察。
以下图展示了特征的分布。要构建以下的统计图表,请执行文件:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter04/1.descriptive_stats.py。运行此脚本后,数据和图表会自动生成。

图 4.1 – 数据转化前的特征分布
数据集包含五个列:
-
CustomerID:每个客户的唯一标识符。在这个示例中,客户 ID 的范围是1到11。 -
ProductName:表示购买的产品名称。在数据集中,考虑了三种产品:Product_A、Product_B和Product_C。 -
PurchaseAmount:表示客户在某个产品上的消费金额。金额使用的是任意货币。 -
PaymentMethod:描述客户用于购买的支付方式。支付方式包括Card、PayPal、Cash和Bank Transfer。 -
Timestamp:表示购买发生的日期和时间。它以datetime对象的格式呈现。
我们首先要检查和更新的是列名。让我们在接下来的部分开始这项工作。
重命名单个列
现在,这家电子商务公司决定重新品牌化其产品,需要更改与产品信息相关的列名。我们将从重命名一个列开始,然后进一步重命名多个列以配合品牌重塑的计划。有关重命名示例,请访问 github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter04/2.rename_columns.py。
让我们看看如何在数据集中重命名一个列:
df.rename(columns={'ProductName': 'OldProductName'}, inplace=True)
inplace=True 参数是 pandas DataFrame 方法中的一个可选参数,它允许你直接修改 DataFrame,而不必创建一个新的 DataFrame 对象。
当inplace设置为True时,DataFrame 会就地修改,这意味着更改会应用到原始的 DataFrame 对象上。这在你想更新或修改 DataFrame 而无需将修改后的 DataFrame 分配给新变量时非常有用。
如果没有指定inplace=True或者将其设置为False(这是默认行为),DataFrame 方法会返回一个新的修改后的 DataFrame 对象,原始 DataFrame 不会被改变。在这种情况下,你需要将修改后的 DataFrame 分配给一个新变量以保存更改。
注意
需要注意的是,使用inplace=True可能是一个破坏性操作,因为它会直接修改原始的 DataFrame。因此,建议谨慎使用,并确保在需要时有原始 DataFrame 的备份。如果你有一个大数据集,原地修改可以帮助节省内存。
在下一节中,我们将重命名多个列,以便与品牌重塑活动保持一致。
重命名所有列
在一次品牌重塑活动后,公司决定将OldProductName重命名为NewProductName,并将PurchaseAmount重命名为NewPurchaseAmount,以便与更新后的产品名称一致。此代码演示了如何一次性重命名多个列:
df.rename(columns={'OldProductName': 'NewProductName', 'PurchaseAmount': 'NewPurchaseAmount'}, inplace=True)
如果你想重命名 DataFrame 中的列,并确保过程顺利且无错误,我们可以添加错误处理。例如,确保你打算重命名的列确实存在于 DataFrame 中。如果某个列名拼写错误或不存在,重命名操作将引发错误:
if 'OldProductName' in df.columns:
try:
# Attempt to rename multiple columns
df.rename(columns={'OldProductName': 'NewProductName', 'PurchaseAmount': 'NewPurchaseAmount'}, inplace=True)
except ValueError as ve:
print(f"Error: {ve}")
else:
print("Error: Column 'OldProductName' does not exist in the DataFrame.")
注意
确保新的列名在 DataFrame 中不存在,以避免覆盖已有的列。
重命名列是我们让数据更整洁、易于理解的最简单操作之一。接下来,我们通常会做的是只保留我们需要的或关心的列,如下一节所讨论的那样。
删除无关或冗余的列
大型数据集通常包含大量列,其中一些可能与当前的分析或任务无关。通过删除这些列,我们可以获得一些显著的好处。首先,存储需求大幅减少,从而节省成本并提高资源的使用效率。此外,精简后的数据集使查询性能更快,内存使用更加优化,并且加快了复杂分析的处理时间。这不仅提高了数据处理任务的整体效率,也简化了大型数据集的管理和维护。此外,在基于云的环境中,存储成本是一个重要因素,删除不必要的列有助于提高成本效率。所以,让我们看看如何以高效的方式删除列。
在我们之前展示的电子商务数据集中,我们收集了有关客户购买的信息。然而,由于你的分析侧重于与产品相关的指标和客户行为,某些列,如CustomerID和Timestamp,可能对当前分析而言不相关。目标是通过删除这些列来精简数据集。你可以通过以下 Python 脚本进行操作:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter04/3.dropping_columns.py:
columns_to_drop = ['CustomerID', 'Timestamp'] # Replace with the names of the columns you want to drop
try:
# Drop columns considered irrelevant for the current analysis
df.drop(columns=columns_to_drop, inplace=True)
except KeyError as ke:
print(f"Error: {ke}")
现在,如果你查看数据集,删除前的列是这样的:
Index(['CustomerID', 'NewProductName', 'NewPurchaseAmount', 'PaymentMethod','Timestamp'],dtype='object')
删除这两列后,我们得到如下结果:
Index(['NewProductName', 'NewPurchaseAmount', 'PaymentMethod'], dtype='object')
注意
Python 默认区分大小写。这意味着ColumnName和columnname被视为不同的。
我们如前所示成功移除了不必要的列。为了进一步评估内存效率,我们可以计算删除列前后 DataFrame 的内存消耗。以下代码提供了一个 Python 示例,演示如何计算删除列前后 DataFrame 的内存使用量:
print("Initial Memory Usage:")
print(df.memory_usage().sum() / (1024 ** 2), "MB") # Convert bytes to megabytes
print("\nMemory Usage After Dropping Columns:")
print(df.memory_usage().sum() / (1024 ** 2), "MB") # Convert bytes to megabytes
初始时,DataFrame 的内存使用量大约为 0.00054 兆字节,删除列后,内存使用量降至约 0.00037 兆字节。内存使用量的减少展示了接近 31%的优化。
虽然这个示例涉及的是一个小数据集,但当这些原则扩展到大数据场景时,内存效率的影响更加显著。在大规模数据集中,删除不必要的列的影响将更加明显。
为了强调操作的重要性,考虑一个包含大量数据集的场景。最初,数据集的大小为 100,000 兆字节,经过去除不必要的列后,大小减少到 69,000 兆字节。为了执行相同的工作负载,最初的选择是使用 AWS EC2 实例类型r7g.4xlarge,其小时费率为$1.0064,内存为 128 GiB,因为我们需要 100GB 的内存才能加载数据集。然而,通过将数据集大小减少到 61GB,便可以选择一种更具成本效益的替代方案,使用r7g.2xlarge实例,小时费率为$0.5032,内存为 64 GiB。在五分钟的工作负载运行时间的背景下,操作前的数据处理成本如下:
Cost_before = (Hourly Rate/60) * Runtime(in minutes) = (1.0064/60) * 5 = 0.0838$
Cost_after = (Hourly Rate/60) * Runtime(in minutes) = (0.5032/60) * 5 = 0.041$
去除不必要的列后,解决方案的成本大约降低了 50%。这代表通过优化数据集并使用更合适的 AWS 实例类型所实现的成本节省。
这个例子的简洁性突显了一个重要的讯息:
通过关注真正重要的部分来简化你的数据操作,让这种简洁性 推动成本效益的提高。
从去除列到修复不一致数据类型的过渡涉及确保数据集中剩余列的质量和完整性。
处理不一致和错误的数据类型
在处理 DataFrame 时,确保每一列具有正确的数据类型非常重要。不一致或错误的数据类型可能会导致分析中的错误、意外的行为以及在执行操作时遇到困难。让我们看看如何处理这种情况。你可以在这里找到这个例子的代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter04/4.data_types.py。
检查列
检查数据中每一列的类型是识别任何不一致或错误数据类型的重要步骤。DataFrame 的dtypes属性提供了每一列的数据类型信息。让我们检查数据集中各列的数据类型:
print("\nUpdated Data Types of Columns:")
print(df.dtypes)
这里展示了几种类型:
CustomerID int64
ProductName object
PurchaseAmount int64
PaymentMethod object
Timestamp object
检查数据类型可以帮助你了解当前数据的表示方式,并判断是否需要进行数据类型转换或变换以便进一步分析或数据清理。接下来的章节中,我们将进行不同类型的转换。
列类型转换
在数据处理的世界里,astype方法是你的好帮手。你应该熟悉的最常见的类型转换将在接下来的章节中介绍。
转换为数值类型
在 pandas 中,astype() 函数用于将列转换为指定的数字数据类型。例如,要将名为PurchaseAmount的列转换为整数类型,可以使用以下方法:
df['PurchaseAmount'] = pd.to_numeric(df['PurchaseAmount'], errors='coerce')
现在,让我们看看如何将列转换为字符串。
转换为字符串类型
你可以使用astype()函数将列转换为字符串类型:
df['ProductName'] = df['ProductName'].astype('str')
现在,让我们看看如何将列转换为类别类型。
转换为类别类型
类别类型(categorical type)指的是表示类别或离散变量的数据类型。类别变量可以取有限的,通常是固定的不同类别或级别。这些变量通常表示定性数据或没有内在顺序的属性:
df['PaymentMethod'] = df['PaymentMethod'].astype('category')
我们将讨论的最后一个转换是布尔值(Boolean)。
转换为布尔类型
一个基于特定条件或标准的 True/False 值。这种转换通常用于创建二进制指示符或标志,使得数据更容易处理和分析:
df['HasDive'] = df['ProductName'].str.contains('Dive', case=False)
前面的代码部分检查 ProductName 列中的每个元素是否包含子字符串 Dive。它返回一个布尔序列,其中每个元素如果满足条件则为 True,否则为 False:
df['HasDive'] = df['HasDive'].astype('bool')
astype('bool') 方法用于显式地将 HasDive 列的数据类型转换为布尔类型。
使用 astype(bool) 时需要注意的事项
如果你遇到所有值都被转换为 True 的情况,可能是由于以下原因之一:
1. 在布尔上下文中,.astype(bool) 会将所有非零值转换为 True。在这种情况下,请考虑该列是否包含了意外或不必要的非零值。
2. 使用 .astype(bool) 时为 True。检查该列中是否存在缺失值,并考虑如何处理这些缺失值。在转换之前,可能需要填充或删除缺失值。
在本章的最后一部分,我们将讨论如何处理日期和时间。
处理日期和时间
想象一下,你有关于事件发生时间的数据——能够理解和处理这些时间相关的数据是理解模式和趋势的关键。它不仅仅是了解事件发生的时间,而是通过数据更轻松地进行可视化和讲述故事。无论是分析随时间变化的趋势,筛选特定时期的数据,还是使用机器学习进行预测,熟练掌握日期和时间是从涉及时间维度的数据集中解锁宝贵见解的关键。
现在我们理解了为什么处理日期和时间如此重要,下一步是学习如何获取与时间相关的信息并让它为我们所用。
导入并解析日期和时间数据
Python 提供了几种主要的日期解析函数,具体取决于输入日期字符串的格式和所需的输出。让我们讨论一些常用的日期解析函数。
pandas 库中的 pd.to_datetime()
此函数专门用于解析 pandas DataFrame 或 Series 中的日期字符串,但也可以独立使用。当处理表格数据时非常适用,并且允许同时处理多种日期格式:
df['Timestamp3'] = pd.to_datetime(df['Timestamp'], format='%Y-%m-%d %H:%M:%S')
format参数指定了输入字符串的预期格式。在此示例中,%Y表示四位数字的年份,%m表示月份,%d表示日期,%H表示小时,%M表示分钟,%S表示秒。
注意事项
如果您的数据集包含缺失或不一致的时间戳值,请考虑使用errors参数。例如,errors='coerce'将把解析错误替换为非时间(NaT)值。
尽管pd.to_datetime效率较高,但对于大数据集,它可能会对性能产生影响。为了提高性能,考虑使用infer_datetime_format=True参数来自动推断格式(对标准格式效果较好)。当infer_datetime_format设置为True,并且parse_dates启用时,Pandas 将尝试自动推断列中日期时间字符串的格式。如果成功,它将切换到更高效的解析方法,在某些场景下可能将解析速度提高 5 到 10 倍。
如果您的数据涉及不同的时区,请考虑使用utc和tz参数来处理协调世界时(UTC)转换和时区本地化。
在下一节中,我们将介绍另一种方法,strftime。此方法允许自定义日期时间值,从而创建特定且易于阅读的时间表示。
strftime()来自 datetime 模块
此函数用于根据指定的格式字符串将日期字符串解析为日期时间对象。当您有已知的日期格式并希望精确控制解析过程时,它非常适用:
df['FormattedTimestamp'] = df['Timestamp'].dt.strftime('%b %d, %Y %I:%M %p')
结果 DataFrame 如下:
Timestamp FormattedTimestamp
0 2022-01-01 08:30:45 Jan 01, 2022 08:30 AM
1 2022-01-02 14:20:30 Jan 02, 2022 02:20 PM
格式由格式说明符控制,每个说明符以百分号(%)字符开头,表示日期和时间的不同组成部分(例如,%Y表示年份,%m表示月份,%d表示日期,%H表示小时,%M表示分钟,%S表示秒等)。可以在 Python 文档中找到完整的格式说明符列表:strftime.org/。
与strftime所要求的严格结构不同,dateutil.parser.parse()在解释各种日期和时间表示方式方面表现出色,提供了一种动态的解决方案,用于解析多种不同的日期时间字符串,正如我们将在下一节中看到的那样。
dateutil.parser.parse()来自 dateutil 库
此函数提供了一种灵活的方法来解析日期字符串,自动推断输入的格式。当处理多种日期格式或格式未知时,它非常有用:
df['Timestamp2'] = df['Timestamp'].apply(parser.parse)
需要注意的是,这种方法的解析器可以推断并处理时区信息,使得处理来自不同时间区的数据变得更加便捷。
在下一部分中,我们不再处理日期和时间,而是转向将其分割为各个部分,如天、月和年。
提取日期和时间的组件
你可以使用 datetime 模块提供的属性提取 datetime 对象的特定组件,如年份、月份、日期、小时、分钟或秒:
df['Day'] = df['Timestamp'].dt.day
df['Month'] = df['Timestamp'].dt.month
df['Year'] = df['Timestamp'].dt.year
使用 .dt 访问器,我们可以从 Timestamp 列中提取天、月和年的组件,并创建新的列 Day、Month 和 Year,如下所示:
Timestamp Day Month Year
0 2022-01-01 08:30:45 1 1 2022
1 2022-01-02 14:20:30 2 1 2022
提取组件在以下情况下非常有用:
-
时间分析:如果你的分析涉及到跨天、跨月或跨年的模式或趋势,提取这些组件有助于进行更为专注的探索。
-
分组与聚合:当基于时间模式对数据进行分组时,提取组件可以方便地进行聚合和总结。
-
时间序列分析:对于时间序列分析,将日期时间值分解为各个组件对理解季节性和趋势至关重要。
继续计算时间差异和持续时间,将通过引入动态维度提升我们对时间数据的探索。
计算时间差异和持续时间
当使用减法计算两个 datetime 对象之间的时间差时,你可以利用 Python datetime 库的内在能力来生成一个 timedelta 对象。这个对象封装了两个时间戳之间的持续时间,以天、小时、分钟和秒为单位,提供了对时间差的全面表示。该部分的代码可以在此找到:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter04/8.time_deltas.py:
df['TimeSincePreviousPurchase'] = df['Timestamp'].diff()
这个 pandas 函数 .diff() 计算 Timestamp 列中每个元素与前一个元素之间的差异。它有效地计算了自上一个时间戳以来的时间差。
df['TimeUntilNextPurchase'] = -df['Timestamp'].diff(-1)
与第一行类似,这计算了 Timestamp 列中每个元素与下一个元素之间的差异。它计算了直到下一个时间戳的时间持续。负号应用于反转时间差的符号。这样做是为了获取直到下次购买的时间差的正表示。
让我们看看数据中时间差是如何表现的:
Timestamp TimeSincePreviousPurchase TimeUntilNextPurchase
0 2022-01-01 08:30:45 NaT 1 days 05:49:45
1 2022-01-02 14:20:30 1 days 05:49:45 1 days 05:54:40
如果你在考虑何时在数据工作流程中加入一些时间差异,可以阅读以下内容:
-
基于时间的分析:计算时间差异可以分析事件或时间戳之间的持续时间。它有助于量化不同过程、活动或间隔所花费的时间。
-
性能测量:通过测量任务或事件的持续时间,可以评估性能指标,如响应时间、处理时间或完成操作所需的时间。这些信息可以指导优化工作,并识别改进的领域。
-
事件排序:通过比较时间戳,您可以确定事件发生的时间顺序。这种排序有助于理解事件之间的关系及其依赖性。
-
服务级别协议(SLA)监控:时间差异对于 SLA 监控非常有用。通过比较与 SLA 指标相关的时间戳,例如响应时间或解决时间,您可以确保遵守约定的服务水平。监控时间差异有助于识别 SLA 违反并采取适当的措施。
pandas 中的.diff()方法主要用于计算 Series 或 DataFrame 中连续元素之间的差异。虽然计算一阶差异(即相邻元素之间的差异)是直接的,但还有其他需要考虑和探索的变体。
指定时间间隔
您可以自定义.diff()来计算特定时间间隔内元素之间的差异。这是通过传递periods参数来指定要移动的元素数量:
df['TimeDifference'] = df['Timestamp'].diff(periods=2)
让我们观察以下结果:
Timestamp TimeSincePreviousPurchase TimeDifference2periods
0 2022-01-01 08:30:45 NaT NaT
1 2022-01-02 14:20:30 1 days 05:49:45 NaT
2 2022-01-03 20:15:10 1 days 05:54:40 2 days 11:44:25
3 2022-01-04 12:45:30 0 days 16:30:20 1 days 22:25:00
如您所见,.diff(periods=2)计算了每个时间戳与之前两个位置之间的差异。periods参数允许您指定计算差异时要移动的元素数量。在这种情况下,它是periods=2,但您可以为其分配任何适合您用例的值。
处理缺失值
使用diff(periods=2)时,.diff()方法会为前两个元素引入 NaN,因为没有前一个元素来计算差异。您可以根据具体用例处理或填充这些缺失值:
df['TimeDifference'] = df['Timestamp']. diff(periods=2).fillna(0)
让我们观察结果:
Timestamp TimeDiff2periods_nonulls TimeDifference2periods
0 2022-01-01 08:30:45 0 NaT
1 2022-01-02 14:20:30 0 NaT
2 2022-01-03 20:15:10 2 days 11:44:25 2 days 11:44:25
3 2022-01-04 12:45:30 1 days 22:25:00 1 days 22:25:00
如您所见,fillna(0)将 NaN 值替换为0。
从时间差异和持续时间到时区和夏令时,我们现在将讨论如何处理跨不同区域的时间数据的细节。
处理时区和夏令时
处理时区在处理跨多个地理区域的数据或准确的时间表示至关重要。时区帮助标准化不同地点的时间,考虑到由于地理边界和夏令时调整导致的 UTC 偏移。在我们的示例数据集中,我们将演示如何使用 pandas 处理时区:
df['Timestamp_UTC'] = df['Timestamp'].dt.tz_localize('UTC')
我们将时间戳本地化到特定的时区,在这个例子中是'UTC'。
df['Timestamp_NY'] = df['Timestamp_UTC'].dt.tz_convert('America/New_York')
然后,我们将本地化的时间戳转换为不同的时区,在这个例子中是'America/New_York'。让我们观察以下结果:
Timestamp Timestamp_UTC Timestamp_NY
0 2022-01-01 08:30:45 2022-01-01 08:30:45+00:00 2022-01-01 03:30:45-05:00
1 2022-01-02 14:20:30 2022-01-02 14:20:30+00:00 2022-01-02 09:20:30-05:00
想了解管理时区的重要性吗?让我们来看看它为什么重要:
-
在处理来自不同时间区的数据时,必须处理时区以确保准确的分析和解读。如果没有正确的时区处理,分析结果可能会因为时间表示不一致而出现偏差。
-
对于需要精确时间表示的应用,如金融交易、日志条目或事件跟踪,时区处理变得至关重要。
-
在整合来自不同来源的数据或合并数据集时,时区处理变得必要,以确保时间戳的准确对齐。这确保了事件的正确时间顺序,并避免了基于时间的分析中的不一致。
-
如果你正在开发面向不同时间区用户的应用或服务,处理时区是至关重要的,能够为用户提供基于他们本地时间的准确和相关信息。
考虑事项
时区处理应该在数据处理流程中始终如一地实施,以避免不一致或错误。
让我们总结一下本章的学习内容。
总结
本章讨论了清理和处理数据的技巧。从混乱数据的挑战开始,我们介绍了如何删除无关的列以及如何处理不一致的数据类型。通过电子商务数据集展示了实际案例,展示了使用 Python 代码进行有效的数据转换。特别强调了删除不必要列的重要性,突出了潜在的成本降低和内存效率提升,尤其是在大数据环境下。数据类型转换,包括数字、字符串、分类和布尔值转换,通过实际示例进行了说明。接着,本章深入探讨了处理日期和时间的复杂问题,展示了诸如pd.to_datetime()、strftime和dateutil.parser.parse()等方法。
随着本章的结束,它为下一章的数据合并和转换奠定了坚实的基础。
第五章:数据转换 – 合并和拼接
理解如何转换和处理数据对于挖掘有价值的洞察至关重要。技术如连接、合并和附加使我们能够将来自不同来源的信息融合在一起,并组织和分析数据的子集。在本章中,我们将学习如何将多个数据集合并成一个单一的数据集,并探索可以使用的各种技术。我们将理解如何在合并数据集时避免重复值,并学习一些提升数据合并过程的技巧。
本章将涵盖以下主题:
-
连接数据集
-
处理数据合并中的重复项
-
合并时的性能优化技巧
-
拼接 DataFrame
技术要求
你可以在以下链接找到本章的所有代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/tree/main/chapter05。
每个章节后面都有一个具有类似命名约定的脚本,欢迎你执行脚本或通过阅读本章来跟随学习。
连接数据集
在数据分析项目中,常常会遇到数据分散在多个来源或数据集中的情况。每个数据集可能包含与某个共同实体或主题相关的不同信息片段。数据合并,也称为数据连接或数据拼接,是将这些独立的数据集合并成一个统一的数据集的过程。在数据分析项目中,常常会遇到某个特定主题或实体的信息分布在多个数据集中的情况。例如,假设你正在为一个零售企业分析客户数据。你可能有一个数据集包含客户的人口统计信息,如姓名、年龄和地址,另一个数据集包含他们的购买历史,如交易日期、购买的商品和总支出。每个数据集都提供了有价值的见解,但单独来看,它们无法完整展现客户的行为。为了获得全面的理解,你需要将这些数据集合并。通过根据一个共同的标识符(如客户 ID)将客户的人口统计信息与购买历史合并,你可以创建一个单一的数据集,从而进行更丰富的分析。例如,你可以识别出哪些年龄组购买了特定的产品,或支出习惯如何因地域而异。
选择正确的合并策略
选择正确的连接类型至关重要,因为它决定了输入 DataFrame 中哪些行会被包含在连接后的输出中。Python 的 pandas 库提供了几种连接类型,每种类型具有不同的行为。我们将介绍本章将要使用的用例示例,然后深入探讨不同类型的连接。
在本章中,我们的使用案例涉及员工数据和项目分配,适用于一个管理其员工和项目的公司。你可以执行以下脚本,详细查看数据框:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter05/1.use_case.py。
employee_data 数据框表示员工的详细信息,例如他们的姓名和部门,内容如下所示:
employee_id name department
0 1 Alice HR
1 2 Bob IT
project_data 数据框包含项目分配的信息,包括项目名称:
employee_id project_name
0 2 ProjectA
1 3 ProjectB
在接下来的章节中,我们将讨论不同的数据框合并选项,从内连接开始。
内连接
内连接只返回在指定的连接列中,两个数据框中都有匹配值的行。需要特别注意以下几点:
-
任何一个数据框中具有不匹配键的行将会被排除在合并结果之外
-
具有缺失值的键列中的行将被排除在合并结果之外
内连接的结果展示在下图中:

图 5.1 – 内连接
让我们来看看如何使用 pandas merge 函数实现上述结果,参考前一节中的示例:
merged_data = pd.merge(employee_data, project_data, on='employee_id', how='inner')
正如我们在前面的代码片段中看到的,pd.merge() 函数用于合并两个数据框。on='employee_id' 参数指定应使用 employee_id 列作为连接数据框的键。how='inner' 参数指定执行内连接。这种连接类型只返回在两个数据框中都有匹配值的行,在本例中就是 employee_id 在 employee_data 和 project_data 中匹配的行。以下表格展示了两个数据框内连接的结果:
employee_id name department project_name
0 2 Bob IT ProjectA
1 3 Charlie Marketing ProjectB
2 4 David Finance ProjectC
3 5 Eva IT ProjectD
这种方法确保了两个数据框的数据是基于公共键合并的,只有在两个数据框中都有匹配时,才会包含对应的行,从而遵循内连接的原则。
如果仍然不清楚,以下列表展示了在数据世界中,内连接至关重要的具体示例:
-
匹配表格:当你需要匹配来自不同表的数据时,内连接是理想的选择。例如,如果你有一个员工表和一个部门名称表,你可以使用内连接将每个员工与他们相应的部门匹配。
-
数据过滤:内连接可以作为过滤器,排除那些在两个表中没有对应条目的行。这在你只希望考虑那些在多个表中有完整数据的记录时非常有用。例如,只有在客户订单和产品详情都有记录的情况下,才匹配这两者。
-
查询执行效率:由于内部连接只返回两个表中具有匹配值的行,因此在查询执行时间方面可能比需要检查并处理非匹配条目的外部连接更有效。
-
减少数据重复:内部连接通过仅返回匹配的行来帮助减少数据重复,从而确保结果集中的数据是相关的,而不是冗余的。
-
简化复杂查询:在处理多个表格时,内部连接可用于通过减少需要检查和处理的行数来简化查询。这在复杂的数据库模式中特别有用,其中多个表格相互关联。
从内部连接转向外部连接扩展了合并数据的范围,合并所有可用的两个数据集的行,即使它们之间没有对应的匹配项。
外部合并
外部合并(也称为完全外部连接)返回两个数据帧的所有行,结合匹配的行以及不匹配的行。完全外部连接确保不会丢失来自任一数据帧的数据,但在其中一个数据帧中存在不匹配行时,可能会引入 NaN 值。
外部合并的结果如下图所示:

图 5.2 – 外部合并
让我们看看如何使用 pandas 的 merge 函数来实现前述结果,在上一节中提供的示例中:
full_outer_merged_data = pd.merge(employee_data, project_data, on='employee_id', how='outer')
正如我们在前面的代码片段中看到的那样,pd.merge() 函数用于合并这两个数据帧。参数 on='employee_id' 指定了应该使用 employee_id 列作为合并数据帧的键。参数 how='outer' 指定执行完全外部连接。这种连接类型返回两个数据帧中的所有行,并在没有匹配项的地方填充 NaN。在以下表格中,您可以看到这两个数据帧进行外部连接的输出:
employee_id name department project_name
0 1 Alice HR NaN
1 2 Bob IT ProjectA
2 3 Charlie Marketing ProjectB
3 4 David Finance ProjectC
4 5 Eva IT ProjectD
5 6 NaN NaN ProjectE
该方法确保合并来自两个数据帧的数据,允许全面查看所有可用数据,即使由于数据帧之间的不匹配导致部分数据不完整。
在以下列表中,我们提供了数据领域中外部合并至关重要的具体示例:
-
包含可选数据:当您希望包含另一个表格中具有可选数据的行时,外部连接是理想的选择。例如,如果您有一个用户表和一个单独的地址表,不是所有用户都可能有地址。外部连接允许您列出所有用户,并显示那些有地址的用户的地址,而不排除没有地址的用户。
-
数据完整性和完整性:在需要一个包含两张表中所有记录的全面数据集的场景中,无论是否在连接表中有匹配记录,外连接都是必不可少的。这确保了你能全面查看数据,特别是在需要展示所有实体的报告中,比如列出所有客户及其购买情况的报告,其中包括那些没有购买的客户。
-
数据不匹配分析:外连接可以用来识别表之间的差异或不匹配。例如,如果你在比较注册用户列表与事件参与者列表,外连接可以帮助识别未参与的用户和未注册的参与者。
-
复杂数据合并:在合并来自多个来源的数据时,这些数据无法完美对齐,外连接可以确保在合并过程中没有数据丢失。这在数据完整性至关重要的复杂数据环境中尤为有用。
从外连接过渡到右连接,缩小了合并数据的关注范围,强调包含右侧 DataFrame 中的所有行,同时保持左侧 DataFrame 中的匹配行。
右连接
右连接(也称为右外连接)返回右侧 DataFrame 中的所有行,以及左侧 DataFrame 中的匹配行。右连接的结果如下图所示:

图 5.3 – 右连接
让我们来看一下如何使用 pandas 的merge函数实现前述结果,参考上一节中提供的示例:
right_merged_data = pd.merge(employee_data, project_data, on='employee_id', how='right')
how='right' 参数指定执行右外连接。此类型的连接返回右侧 DataFrame(project_data)中的所有行,以及左侧 DataFrame(employee_data)中的匹配行。如果没有匹配,则结果中左侧 DataFrame 的列会显示为 NaN。在下表中,你可以看到前述两个 DataFrame 合并的输出结果:
employee_id name department project_name
0 2 Bob IT ProjectA
1 3 Charlie Marketing ProjectB
2 4 David Finance ProjectC
3 5 Eva IT ProjectD
4 6 NaN NaN ProjectE
在以下列表中,我们展示了数据领域中右连接至关重要的具体示例:
-
完成数据:当你需要确保保留右侧 DataFrame 中的所有条目时,右连接非常有用,这在右侧 DataFrame 包含必须保留的重要数据时尤其重要。
-
数据增强:这种类型的连接可用于通过从另一个数据集(左侧 DataFrame)中获取附加属性来丰富数据集(右侧 DataFrame),同时确保保留主数据集中的所有记录。
-
数据不匹配分析:与外连接类似,右连接可以帮助识别右侧 DataFrame 中哪些条目没有对应的左侧 DataFrame 条目,这对于数据清洗和验证过程至关重要。
从右连接转为左连接,改变了合并数据的视角,优先考虑包括左侧数据框的所有行,同时保持右侧数据框的匹配行。
左连接
左连接(也称为左外连接)返回左侧数据框的所有行以及右侧数据框的匹配行。左连接的结果如以下图所示:

图 5.4 – 左连接
让我们看看如何使用 pandas 的merge函数来实现前述结果,使用上一节中提供的示例:
left_merged_data = pd.merge(employee_data, project_data, on='employee_id', how='left')
how='left'参数指定应执行左外连接。这种类型的连接返回左侧数据框(employee_data)的所有行,以及右侧数据框(project_data)的匹配行。如果没有匹配项,结果将会在右侧数据框的列中显示NaN。在以下表格中,您可以看到前述两数据框合并的结果:
employee_id name department project_name
0 1 Alice HR NaN
1 2 Bob IT ProjectA
2 3 Charlie Marketing ProjectB
3 4 David Finance ProjectC
4 5 Eva IT ProjectD
如果你想知道何时使用左连接,那么之前关于右连接的考虑同样适用于左连接。现在我们已经讨论了合并操作,接下来我们来讨论在合并过程中可能出现的重复项如何处理。
合并数据集时处理重复项
在执行合并操作之前处理重复键非常重要,因为重复项可能导致意外结果,例如笛卡尔积,行数会根据匹配条目的数量而增加。这不仅会扭曲数据分析,还会因为结果数据框的大小增加而显著影响性能。
为什么要处理行和列中的重复项?
重复的键可能会导致一系列问题,这些问题可能会影响结果的准确性和数据处理的效率。让我们来探讨一下为什么在合并数据之前处理重复键是一个好主意:
-
如果任一表格中存在重复键,合并这些表格可能会导致笛卡尔积,即一个表格中的每个重复键与另一个表格中相同键的每个出现匹配,从而导致行数呈指数增长。
-
重复的键可能表示数据错误或不一致,这可能导致错误的分析或结论。
-
通过删除重复项来减少数据集的大小,可以加速合并操作的处理时间。
在理解了处理重复键的重要性后,让我们来看看在进行合并操作之前,有哪些有效的策略可以管理这些重复项。
删除重复行
在数据集中删除重复项涉及识别并删除基于特定键列的重复行,以确保每个条目都是唯一的。这一步不仅简化了后续的数据合并,还通过消除由重复数据引起的潜在错误来源,提高了分析的可靠性。为了展示删除重复项,我们将扩展我们一直在使用的示例,在每个 DataFrame 中添加更多的重复行。像往常一样,您可以在此查看完整代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter05/6a.manage_duplicates.py。
让我们首先创建一些具有重复 employee_id 键的示例员工数据:
employee_data = pd.DataFrame({
'employee_id': [1, 2, 2, 3, 4, 5, 5],
'name': ['Alice', 'Bob', 'Bob', 'Charlie', 'David', 'Eva', 'Eva'],
'department': ['HR', 'IT', 'IT', 'Marketing', 'Finance', 'IT', 'IT']
})
让我们还创建一些具有重复 employee_id 键的示例项目数据:
project_data = pd.DataFrame({
'employee_id': [2, 3, 4, 5, 5, 6],
'project_name': ['ProjectA', 'ProjectB', 'ProjectC', 'ProjectD', 'ProjectD', 'ProjectE']
})
现在,我们要合并这些数据集。但首先,我们将删除所有重复项,以使合并操作尽可能轻便。删除重复项后的合并操作在以下代码片段中展示:
employee_data = employee_data.drop_duplicates(subset='employee_id', keep='first')
project_data = project_data.drop_duplicates(subset='employee_id', keep='first')
如代码所示,drop_duplicates() 用于根据 employee_id 删除重复行。keep='first' 参数确保仅保留首次出现的记录,其他记录将被删除。
删除重复项后,您可以继续进行合并操作,如以下代码所示:
merged_data = pd.merge(employee_data, project_data, on='employee_id', how='inner')
合并后的数据集如下所示:
employee_id name department project_name
0 2 Bob IT ProjectA
1 3 Charlie Marketing ProjectB
2 4 David Finance ProjectC
3 5 Eva IT ProjectD
merged_data DataFrame 包含了来自 employee_data 和 project_data 两个 DataFrame 的列,显示了每个在两个数据集中都存在的员工的 employee_id、name、department 和 project_name 的值。重复项被删除,确保每个员工在最终合并的数据集中仅出现一次。drop_duplicates 操作对避免数据冗余和合并过程中可能出现的冲突至关重要。接下来,我们将讨论如何确保合并操作尊重键的唯一性并遵守特定的约束条件。
合并前验证数据
在合并数据集时,尤其是处理大型和复杂数据集时,确保合并操作的完整性和有效性至关重要。pandas 在 merge() 函数中提供了 validate 参数,用于强制执行合并键之间的特定条件和关系。这有助于识别并防止可能影响分析的无意重复或数据不匹配。
以下代码演示了如何使用validate参数来强制执行merge()约束,并在这些约束未满足时处理异常。你可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter05/6b.manage_duplicates_validate.py查看完整代码:
try:
merged_data = pd.merge(employee_data, project_data, on='employee_id', how='inner', validate='one_to_many')
print("Merged Data Result:")
print(merged_data)
except ValueError as e:
print("Merge failed:", e)
在前面的代码片段中,合并操作被包装在try-except代码块中。这是一种处理异常的方式,异常是指程序执行过程中发生的错误。try代码块包含可能引发异常的代码,在这种情况下是合并操作。如果发生异常,代码执行将跳转到except代码块。
如果合并操作未通过验证检查(在我们的例子中,如果左侧 DataFrame 中存在重复的键,而这些键应该是唯一的),将引发ValueError异常,并执行except代码块。except代码块捕获ValueError异常并打印Merge failed:消息,后跟 pandas 提供的错误信息。
执行上述代码后,你将看到以下错误消息:
Merge failed: Merge keys are not unique in left dataset; not a one-to-many merge
validate='one_to_many'参数包含在合并操作中。该参数告诉 pandas 检查合并操作是否符合指定类型。在这种情况下,one_to_many表示合并键在左侧 DataFrame(employee_data)中应唯一,但在右侧 DataFrame(project_data)中可以有重复项。如果验证检查失败,pandas 将引发ValueError异常。
何时使用哪种方法
当你需要精细控制重复项的识别和处理方式,或者当重复项需要特殊处理(例如基于其他列值的聚合或转换)时,使用手动删除重复项。
当你希望直接在合并操作中确保数据模型的结构完整性时,使用合并验证,尤其是在表之间的关系明确定义并且根据业务逻辑或数据模型不应包含重复键的简单情况。
如果数据中存在重复项是有充分理由的,我们可以考虑在合并过程中采用聚合方法,以合并冗余信息。
聚合
聚合是管理数据集重复项的强大技术,特别是在处理应唯一但包含多个条目的关键列时。通过在这些关键列上分组数据并应用聚合函数,我们可以将重复条目合并为单一的汇总记录。可以使用求和、平均值或最大值等聚合函数,以与分析目标对齐的方式来合并或汇总数据。
让我们看看如何利用聚合来有效地处理数据重复问题。为了帮助展示这个例子,我们稍微扩展一下数据集,具体如下所示。你可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter05/6c.merge_and_aggregate.py看到完整示例:
employee_data = pd.DataFrame({
'employee_id': [1, 2, 2, 3, 4, 5, 5],
'name': ['Alice', 'Bob', 'Bob', 'Charlie', 'David', 'Eva', 'Eva'],
'department': ['HR', 'IT', 'IT', 'Marketing', 'Finance', 'IT', 'IT'],
'salary': [50000, 60000, 60000, 55000, 65000, 70000, 70000]
})
# Sample project assignment data with potential duplicate keys
project_data = pd.DataFrame({
'employee_id': [2, 3, 4, 5, 7, 6],
'project_name': ['ProjectA', 'ProjectB', 'ProjectC', 'ProjectD', 'ProjectD', 'ProjectE']
})
现在,让我们进行聚合步骤:
aggregated_employee_data = employee_data.groupby('employee_id').agg({
'name': 'first', # Keep the first name encountered
'department': 'first', # Keep the first department encountered
'salary': 'sum' # Sum the salaries in case of duplicates
}).reset_index()
groupby()方法在employee_data上使用,employee_id作为键。这将 DataFrame 按employee_id分组,因为存在重复的employee_id值。
然后,agg()方法被应用于对不同列进行特定的聚合操作:
-
'name': 'first'和'department': 'first'确保在分组数据中保留这些列的首次遇到的值。 -
'salary': 'sum'对每个employee_id值的薪资进行求和,如果重复数据表示累计数据的拆分记录,这将非常有用。
在最后一步,使用pd.merge()函数通过在employee_id列上进行内连接,将aggregated_employee_data与project_data合并:
merged_data = pd.merge(aggregated_employee_data, project_data, on='employee_id', how='inner')
这确保了只有有项目分配的员工会被包含在结果中。合并后的结果如下所示:
employee_id name department salary project_name
0 2 Bob IT 120000 ProjectA
1 3 Charlie Marketing 55000 ProjectB
2 4 David Finance 65000 ProjectC
3 5 Eva IT 140000 ProjectD
pandas 中的agg()方法非常灵活,提供了许多超出简单“保留首个”方法的选项。这个方法可以应用各种聚合函数来汇总数据,比如对数值进行求和、求平均值,或选择最大值或最小值。我们将在下一章深入探讨agg()方法的多种功能,探索如何运用这些不同的选项来提升数据准备和分析的质量。
让我们从使用聚合来处理重复数据过渡到拼接重复行,这在处理文本或类别数据时非常有效。
拼接
将重复行的值拼接成一行是一种有用的技巧,特别是在处理可能包含多个有效条目的文本或类别数据时。这种方法允许你保留重复数据中的所有信息,而不会丢失数据。
让我们看看如何通过拼接行来有效地处理数据重复问题,在合并数据之前。为了展示这一方法,我们将使用以下 DataFrame:
employee_data = pd.DataFrame({
'employee_id': [1, 2, 2, 3, 4, 5, 5],
'name': ['Alice', 'Bob', 'Bob', 'Charlie', 'David', 'Eva', 'Eva'],
'department': ['HR', 'IT', 'Marketing', 'Marketing', 'Finance', 'IT', 'HR']
})
现在,让我们进行拼接步骤,如下面的代码片段所示:
employee_data['department'] = employee_data.groupby('employee_id')['department'].transform(lambda x: ', '.join(x))
在拼接步骤中,groupby('employee_id')方法按employee_id对数据进行分组。然后,transform(lambda x: ', '.join(x))方法应用于department列。此时,使用lambda函数通过逗号将每个组(即employee_id)的department列的所有条目合并成一个字符串。
此操作的结果替换了employee_data中原始的department列,现在每个employee_id都有一个包含所有原始部门数据合并为一个字符串的单一department条目,如下表所示:
employee_id name department
0 1 Alice HR
1 2 Bob Marketing, IT
3 3 Charlie Marketing
4 4 David Finance
5 5 Eva IT, HR
当你需要在重复条目中保留所有类别或文本数据,而不偏向某一条目时,可以使用连接。
这种方法有助于以可读且信息丰富的方式总结文本数据,特别是在处理可能具有多个有效值的属性时(例如,一个员工属于多个部门)。
一旦解决了每个数据框中的重复行,注意力就转向识别和解决跨数据框的重复列问题。
处理列中的重复
在合并来自不同来源的数据时,遇到列名重叠的数据框并不罕见。这种情况通常发生在合并类似数据集时。
扩展我们迄今为止使用的示例数据,我们将调整数据框(DataFrame)以帮助展示在处理多个数据框中共有列时可用的选项。数据可以在此查看:
employee_data_1 = pd.DataFrame({
'employee_id': [1, 2, 3, 4, 5],
'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eva'],
'department': ['HR', 'IT', 'Marketing', 'Finance', 'IT']
})
employee_data_2 = pd.DataFrame({
'employee_id': [6, 7, 8, 9, 10],
'name': ['Frank', 'Grace', 'Hannah', 'Ian', 'Jill'],
'department': ['Logistics', 'Marketing', 'IT', 'Marketing', 'Finance']
})
让我们看看如何通过应用不同的技巧来合并这些数据集,而不会破坏合并操作。
合并时处理重复列
前面展示的两个数据框中的列名相同,可能表示相同的数据。然而,我们决定在合并的数据框中保留两组列。这个决定基于这样的怀疑:尽管列名相同,但条目并不完全相同,这表明它们可能是相同数据的不同表示形式。这个问题我们可以在合并操作后再处理。
保持两个列集的最佳方法是使用merge()函数中的suffixes参数。这将允许你区分来自每个数据框的列,而不会丢失任何数据。以下是在 Python 中使用 pandas 实现这一点的方法:
merged_data = pd.merge(employee_data_1, employee_data_2, on='employee_id', how='outer', suffixes=('_1', '_2'))
pd.merge()函数用于在employee_id上合并两个数据框。how='outer'参数确保包括来自两个数据框的所有记录,即使没有匹配的employee_id值。suffixes=('_1', '_2')参数为每个数据框的列添加后缀,以便在合并后的数据框中区分它们。当列名相同但来自不同数据源时,这一点尤为重要。让我们回顾一下输出数据框:
employee_id name_1 department_1 name_2 department_2
0 1 Alice HR NaN NaN
1 2 Bob IT NaN NaN
2 3 Charlie Marketing NaN NaN
3 4 David Finance NaN NaN
4 5 Eva IT NaN NaN
5 6 NaN NaN Frank Logistics
6 7 NaN NaN Grace Marketing
7 8 NaN NaN Hannah IT
8 9 NaN NaN Ian Marketing
9 10 NaN NaN Jill Finance
这种方法在从不同来源合并数据时尤其有用,尤其是当涉及到列名重叠的情况,但同时也需要保留并清晰地区分这些列。另一个需要考虑的点是,后缀可以帮助识别数据来源的数据框,这在涉及多个来源的数据分析中非常有用。
在下一节中,我们将解释如何通过在合并之前删除列来处理重复列。
在合并前删除重复列
如果我们发现要合并的两个 DataFrame 中有相同列的副本,而且其中一个 DataFrame 中的列比另一个更可靠或足够使用,那么在合并操作之前删除其中一个重复列可能更为实际,而不是保留两个副本。做出这一决策的原因可能是简化数据集、减少冗余,或者当某一列对分析没有额外价值时。让我们看一下这个示例的数据:
employee_data_1 = pd.DataFrame({
'employee_id': [1, 2, 3, 4, 5],
'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eva'],
'department': ['HR', 'IT', 'Marketing', 'Finance', 'IT']
})
employee_data_2 = pd.DataFrame({
'employee_id': [1, 2, 3, 4, 5],
'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eva'],
'department': ['Human Resources', 'Information Technology', 'Sales', 'Financial', 'Technical']
})
如果我们仔细查看这些数据,可以发现两个 DataFrame 中的 department 列捕获了相同的信息,但格式不同。为了简化我们的示例,假设我们知道 HR 系统以第一个 DataFrame 中呈现的格式跟踪每个员工的部门。这就是为什么我们会更信任第一个 DataFrame 中的列,而不是第二个 DataFrame 中的列。因此,我们将在合并操作之前删除第二个列。下面是如何在合并之前删除列的操作:
employee_data_2.drop(columns=['department'], inplace=True)
在合并之前,employee_data_2 中的 department 列被删除,因为它被认为不够可靠。这是通过 drop(columns=['department'], inplace=True) 方法完成的。在删除了不需要的列之后,我们可以继续进行合并:
merged_data = pd.merge(employee_data_1, employee_data_2, on=['employee_id', 'name'], how='inner')
使用 pd.merge() 函数,以 employee_id 和 name 列作为键合并 DataFrame。使用 how='inner' 参数来执行内连接,只包含在两个 DataFrame 中具有匹配值的行。
为了优化合并过程并提高性能,通常在执行合并操作之前删除不必要的列是有益的,原因如下:
-
通过显著减少合并操作时的内存占用,这种做法可以提高性能,因为它最小化了需要处理和合并的数据量,从而加快了处理速度。
-
结果 DataFrame 变得更加简洁清晰,便于数据管理和后续分析。这种复杂度的减少不仅简化了合并操作,还减少了出错的可能性。
-
在资源受限的环境中,例如计算资源有限的情况,减少数据集大小在进行如合并等密集型操作之前,可以提高资源效率,并确保更顺畅的执行。
如果在 DataFrame 中存在相同的列,另一种选择是考虑是否可以将它们作为合并操作的键。
重复键
当遇到跨多个 DataFrame 的相同键时,一种智能的做法是基于这些共同列进行合并。让我们回顾一下前一节中提供的示例:
merged_data = pd.merge(employee_data_1, employee_data_2, on=['employee_id', 'name'], how='inner')
我们可以看到,这里我们使用了 ['employee_id', 'name'] 作为合并的键。如果 employee_id 和 name 是可靠的标识符,能够确保在 DataFrame 之间准确匹配记录,那么它们应该作为合并的键。这确保了合并后的数据准确地代表了两个来源的结合记录。
随着数据量和复杂性的不断增长,高效地合并数据集变得至关重要,正如我们在接下来的部分中将要学习的那样。
合并的性能技巧
在处理大型数据集时,合并操作的性能可能会显著影响数据处理任务的整体效率。合并是数据分析中常见且常常必需的步骤,但它可能是计算密集型的,尤其是在处理大数据时。因此,采用性能优化技术对于确保合并操作尽可能快速高效地执行至关重要。
优化合并操作可以减少执行时间,降低内存消耗,并带来更加流畅的数据处理体验。在接下来的部分,我们将探讨一些可以应用于 pandas 合并操作的性能技巧,如使用索引、排序索引、选择合适的合并方法以及减少内存使用。
设置索引
在 pandas 中使用索引是数据处理和分析中的一个关键方面,尤其是在处理大型数据集或进行频繁的数据检索操作时。索引既是标识工具,也是高效数据访问的工具,提供了多种好处,能够显著提高性能。具体来说,在合并 DataFrame 时,使用索引能够带来性能上的提升。与基于列的合并相比,基于索引的合并通常更快,因为 pandas 可以使用优化的基于索引的连接方法来执行合并操作,这比基于列的合并更高效。让我们回顾一下员工示例来证明这一概念。此示例的完整代码可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter05/8a.perfomance_benchmark_set_index.py中找到。
首先,让我们导入必要的库:
import pandas as pd
import numpy as np
from time import time
为每个 DataFrame 选择基准示例的行数:
num_rows = 5
让我们创建示例所需的 DataFrame,这些 DataFrame 的行数将由 num_rows 变量定义。这里定义了第一个员工 DataFrame:
employee_data_1 = pd.DataFrame({
'employee_id': np.arange(num_rows),
'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eva'],
'department': ['HR', 'IT', 'Marketing', 'Finance', 'IT'],
'salary': [50000, 60000, 70000, 80000, 90000]
})
第二个 DataFrame 如下所示:
employee_data_2 = pd.DataFrame({
'employee_id': np.arange(num_rows),
'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eva'],
'department': ['HR', 'IT', 'Sales', 'Finance', 'Operations'],
'bonus': [3000, 4000, 5000, 6000, 7000]
})
为了展示我们所应用的性能技巧的效果,我们最初将执行不利用索引的合并操作。我们将计算此操作所需的时间。接着,我们会在两个 DataFrame 中设置索引并重新执行合并操作,重新计算时间。最后,我们将展示结果。希望这个方法能产生预期的效果!开始计时:
start_time = time()
让我们在不使用索引的情况下执行合并操作,只通过['employee_id', 'name']进行内连接:
merged_data = pd.merge(employee_data_1, employee_data_2, on=['employee_id', 'name'], how='inner', suffixes=('_1', '_2'))
让我们计算执行合并所花费的时间:
end_time = time()
merge_time = end_time - start_time
Merge operation took around 0.00289 seconds
注意
执行程序的计算机可能会导致时间有所不同。这个想法是,优化后的版本比原始合并操作所需的时间更短。
通过将employee_id作为两个 DataFrame(employee_data_1和employee_data_2)的索引,我们让 pandas 使用基于索引的优化连接方法。这尤其有效,因为 pandas 中的索引是通过哈希表或 B 树实现的,具体取决于数据类型和索引的排序性,这有助于加速查找:
employee_data_1.set_index('employee_id', inplace=True)
employee_data_2.set_index('employee_id', inplace=True)
在设置索引后,我们再执行一次合并操作,并重新计算时间:
start_time = time()
merged_data_reduced = pd.merge(employee_data_1, employee_data_2, left_index=True, right_index=True, suffixes=('_1', '_2'))
end_time = time()
merge_reduced_time = end_time - start_time
Merge operation with reduced memory took around 0.00036 seconds
现在,如果我们计算从初始时间到最终时间的百分比差异,我们发现仅仅通过设置索引,我们就将时间缩短了约 88.5%。这看起来很令人印象深刻,但我们也需要讨论一些设置索引时的注意事项。
索引注意事项
选择合适的列进行索引设置非常重要,应基于查询模式。过度索引可能导致不必要的磁盘空间占用,并且由于维护索引的开销,可能会降低写操作性能。
重建或重组索引对于优化性能至关重要。这些任务解决了索引碎片问题,并确保随着时间推移性能的一致性。
虽然索引可以显著提高读取性能,但它们也可能影响写入性能。在优化读取操作(如搜索和连接)与保持高效的写入操作(如插入和更新)之间找到平衡至关重要。
多列索引或连接索引在多个字段经常一起用于查询时可能是有益的。然而,索引定义中字段的顺序非常重要,应反映出最常见的查询模式。
在证明了设置索引的重要性后,我们进一步讨论在合并前对索引进行排序的选项。
排序索引
在 pandas 中排序索引在你经常对大规模 DataFrame 进行合并或连接操作的场景中尤其有利。当索引被排序时,pandas 可以利用更高效的算法来对齐和连接数据,这可能会显著提升性能。让我们在继续代码示例之前深入探讨这一点:
-
当索引已排序时,pandas 可以使用二分查找算法来定位 DataFrame 之间的匹配行。二分查找的时间复杂度是 O(log n),这比未排序索引所需的线性查找要快得多,特别是当 DataFrame 的大小增加时。
-
排序索引有助于更快地对齐数据。这是因为 pandas 可以对数据的顺序做出一些假设,从而简化在合并时查找每个 DataFrame 中对应行的过程。
-
使用排序后的索引,pandas 可以避免进行不必要的比较,这些比较是当索引未排序时所必需的。这样可以减少计算开销,加速合并过程。
让我们回到代码示例,加入索引排序的步骤。原始数据保持不变;但是在本实验中,我们比较的是在设置索引后执行合并操作的时间与在设置并排序索引后执行合并操作的时间。以下代码展示了主要的代码组件,但和往常一样,你可以通过 github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter05/8b.performance_benchmark_sort_indexes.py 跟进完整示例:
employee_data_1.set_index('employee_id', inplace=True)
employee_data_2.set_index('employee_id', inplace=True)
让我们在不排序索引的情况下执行合并操作:
merged_data = pd.merge(employee_data_1, employee_data_2, left_index=True, right_index=True, suffixes=('_1', '_2'))
Merge operation with setting index took around 0.00036 seconds
让我们在排序索引后重复合并操作,并再次计算时间:
employee_data_1.sort_index(inplace=True)
employee_data_2.sort_index(inplace=True)
merged_data_reduced = pd.merge(employee_data_1, employee_data_2, left_index=True, right_index=True, suffixes=('_1', '_2'))
Merge operation after sorting took around 0.00028 seconds.
现在,如果我们计算从初始时间到最终时间的百分比差异,我们会发现通过排序索引,我们成功地将时间减少了大约 ~22%。这看起来很不错,但我们也需要讨论设置索引时的一些注意事项。
排序索引的注意事项
排序 DataFrame 的索引并不是没有计算成本的。初始的排序操作本身需要时间,因此当排序后的 DataFrame 在多个合并或连接操作中被使用时,这种做法最为有利,可以通过这些操作摊销排序的成本。
排序有时会增加内存开销,因为 pandas 可能会创建 DataFrame 索引的排序副本。在处理非常大的数据集时,若内存是一个限制因素,应该考虑这一点。
排序索引最有利的情况是,合并所用的键不仅是唯一的,而且具有一定的逻辑顺序,例如时间序列数据或有序的分类数据。
索引管理和维护是你在处理 pandas DataFrame 时需要考虑的关键因素,尤其是在处理大型数据集时。维护一个良好的索引需要谨慎考虑。例如,定期更新或重新索引 DataFrame 可能会引入计算成本,类似于排序操作。每次修改索引(通过排序、重新索引或重置)时,可能会导致额外的内存使用和处理时间,尤其是在大型数据集上。
索引需要以平衡性能和资源使用的方式进行维护。例如,如果你经常合并或连接 DataFrame,确保索引已正确排序并且是唯一的,可以显著加速这些操作。然而,持续维护一个已排序的索引可能会消耗大量资源,因此当 DataFrame 需要进行多次操作并利用已排序的索引时,这样做最为有利。
此外,选择合适的索引类型——无论是基于整数的简单索引、用于时间序列数据的日期时间索引,还是用于层次数据的多级索引——都可能影响 pandas 处理数据的效率。索引的选择应与数据的结构和访问模式相匹配,以最小化不必要的开销。
在接下来的部分,我们将讨论使用 join 函数而非 merge 如何影响性能。
合并与连接
虽然合并是根据特定条件或键来合并数据集的常用方法,但还有另一种方法:join 函数。这个函数提供了一种简化的方式,主要通过索引执行合并,为更通用的合并函数提供了一个更简单的替代方案。当涉及的 DataFrame 已经将索引设置为用于连接的键时,pandas 中的 join 方法特别有用,它能够高效、直接地进行数据组合,而无需指定复杂的连接条件。
使用 join 函数代替 merge 可能会以多种方式影响性能,主要是因为这两个函数的底层机制和默认行为:
-
pandas 中的
join函数针对基于索引的连接进行了优化,意味着它在通过索引连接 DataFrame 时被设计得更为高效。如果你的 DataFrame 已经按你想要连接的键进行了索引,那么使用join可以更高效,因为它利用了优化过的索引结构[2][6][7]。 -
Join 是 merge 的简化版本,默认按索引进行连接。这种简化可能带来性能上的优势,尤其是对于那些连接任务简单、合并复杂性不必要的场景。通过避免对非索引列的对齐开销,在这些情况下,join 可以更快速地执行[2][6]。
-
从底层实现来看,join 使用的是 merge[2][6]。
-
在连接大型 DataFrame 时,join 和 merge 处理内存的方式会影响性能。通过专注于基于索引的连接,join 可能在某些场景下更高效地管理内存使用,尤其是当 DataFrame 具有 pandas 可优化的索引时 [1][3][4]。
-
虽然 merge 提供了更大的灵活性,允许在任意列上进行连接,但这种灵活性带来了性能上的代价,尤其是在涉及多个列或非索引连接的复杂连接时。由于其更具体的使用场景,join 在较简单的基于索引的连接中具有性能优势 [2][6]。
总结来说,选择 join 还是 merge 取决于任务的具体需求。如果连接操作主要基于索引,join 可以因其针对基于索引的连接进行优化而提供性能优势,且其接口更为简洁。然而,对于涉及特定列或多个键的更复杂连接需求,merge 提供了必要的灵活性,尽管这可能会对性能产生影响。
连接 DataFrame
当你有多个结构相似(列相同或行相同)的 DataFrame,且想将它们合并成一个 DataFrame 时,连接操作非常适用。连接过程可以沿特定轴进行,按行(axis=0)或按列(axis=1)连接。
让我们深入了解按行连接,也称为附加(append)。
按行连接
按行连接用于沿 axis=0 将一个 DataFrame 连接到另一个 DataFrame。为了展示这个操作,可以看到两个结构相同但数据不同的 DataFrame,employee_data_1 和 employee_data_2:
employee_data_1 = pd.DataFrame({
'employee_id': np.arange(1, 6),
'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eva'],
'department': ['HR', 'IT', 'Marketing', 'Finance', 'IT']
})
employee_data_2 = pd.DataFrame({
'employee_id': np.arange(6, 11),
'name': ['Frank', 'Grace', 'Hannah', 'Ian', 'Jill'],
'department': ['Logistics', 'HR', 'IT', 'Marketing', 'Finance']
})
让我们执行按行连接,如以下代码片段所示:
concatenated_data = pd.concat([employee_data_1, employee_data_2], axis=0)
pd.concat() 函数用于连接两个 DataFrame。第一个参数是要连接的 DataFrame 列表,axis=0 参数指定连接应按行进行,将 DataFrame 堆叠在一起。
结果可以在这里看到:
employee_id name department
0 1 Alice HR
1 2 Bob IT
2 3 Charlie Marketing
3 4 David Finance
4 5 Eva IT
0 6 Frank Logistics
1 7 Grace HR
2 8 Hannah IT
3 9 Ian Marketing
4 10 Jill Finance
执行按行连接时,需要考虑的几点如下:
-
确保你要连接的列对齐正确。pandas 会自动按列名对齐列,并用
NaN填充任何缺失的列。 -
连接后,你可能希望重置结果 DataFrame 的索引,以避免重复的索引值,尤其是当原始 DataFrame 各自有自己的索引范围时。请在执行
reset操作之前,观察以下示例中的索引:employee_id name department 0 1 Alice HR 1 2 Bob IT 2 3 Charlie Marketing 3 4 David Finance 4 5 Eva IT 0 6 Frank Logistics 1 7 Grace HR 2 8 Hannah IT 3 9 Ian Marketing 4 10 Jill Finance concatenated_data_reset = concatenated_data.reset_index(drop=True)让我们再次查看输出:
employee_id name department 0 1 Alice HR 1 2 Bob IT 2 3 Charlie Marketing 3 4 David Finance 4 5 Eva IT 5 6 Frank Logistics 6 7 Grace HR 7 8 Hannah IT 8 9 Ian Marketing 9 10 Jill Finance重置索引会为连接后的数据框创建一个新的连续索引。使用
drop=True参数可以避免将旧索引作为列添加到新数据框中。这个步骤对于保持数据框的整洁至关重要,特别是当索引本身不携带有意义的数据时。一个连续的索引通常更容易操作,尤其是在索引、切片以及未来的合并或连接操作中。 -
连接操作可能会增加程序的内存使用,特别是当处理大型数据框时。需要注意可用的内存资源。
在下一节中,我们将讨论按列连接。
按列连接
在 pandas 中,按列连接数据框涉及将两个或更多的数据框并排组合,通过索引对齐它们。为了展示这个操作,我们将使用之前的两个数据框,employee_data_1和employee_data_2,操作可以像这样进行:
concatenated_data = pd.concat([employee_data_1, employee_performance], axis=1)
pd.concat()函数与axis=1参数一起使用,用于并排连接数据框。这通过索引对齐数据框,有效地将employee_performance中的新列添加到employee_data_1中。输出将显示如下:
employee_id name department employee_id performance_rating
0 1 Alice HR 1 3
1 2 Bob IT 2 4
2 3 Charlie Marketing 3 5
3 4 David Finance 4 3
4 5 Eva IT 5 4
在进行按列连接时,你需要考虑的几个事项如下:
-
要连接的数据框的索引会被正确对齐。在按列连接数据框时,结果数据框中的每一行应理想地代表来自同一实体的数据(例如,同一员工)。索引未对齐可能导致来自不同实体的数据被错误地组合,从而产生不准确和误导性的结果。例如,如果索引表示员工 ID,未对齐可能导致某个员工的详细信息与另一个员工的表现数据错误地配对。
-
如果数据框中包含相同名称的列,但这些列打算是不同的,考虑在连接之前重命名这些列,以避免在结果数据框中产生混淆或错误。
-
虽然按列连接通常不像按行连接那样显著增加内存使用,但仍然需要监控内存使用,尤其是对于大型数据框。
连接与连接操作的比较
连接主要用于沿轴(行或列)组合数据框,而不考虑其中的值。它适用于那些你只是想根据顺序将数据框堆叠在一起或通过附加列扩展它们的情况。
连接用于根据一个或多个键(每个数据框中的公共标识符)组合数据框。这更多是基于共享数据点合并数据集,允许更复杂和有条件的数据组合。
在探讨了 pandas 中拼接操作的细节之后,包括它在对齐索引方面的重要性,以及它与连接操作的对比,我们现在总结讨论的关键点,概括我们的理解,并突出我们在探索 pandas 中 DataFrame 操作时的关键收获。
总结
在本章中,我们探讨了 pandas 中 DataFrame 操作的各个方面,重点讨论了拼接、合并以及索引管理的重要性。
我们讨论了合并操作,它适用于基于共享键的复杂组合,并通过内连接、外连接、左连接和右连接等多种连接类型提供灵活性。我们还讨论了如何使用拼接操作在特定轴上(按行或按列)合并 DataFrame,这对于追加数据集或为数据添加新维度尤其有用。我们还讨论了这些操作的性能影响,强调了正确的索引管理可以显著提升这些操作的效率,特别是在处理大数据集时。
在接下来的章节中,我们将深入探讨如何利用groupby函数与各种聚合函数结合,从复杂的数据结构中提取有意义的洞察。
参考资料
-
stackoverflow.com/questions/40860457/improve-pandas-merge-performance -
pandas.pydata.org/pandas-docs/version/1.5.1/user_guide/merging.html
第六章:数据分组、聚合、过滤和应用函数
数据分组和聚合是数据清理和预处理中的基础技术,具有多个关键用途。首先,它们能够对大规模数据集进行总结,将庞大的原始数据转化为简洁、有意义的汇总,方便分析和洞察的提取。此外,聚合有助于处理缺失或噪声数据,通过平滑不一致性并填补数据空白。这些技术还帮助减少数据量,提高处理效率,并为进一步的分析或机器学习模型创建有价值的特征。
数据分组和聚合的主要组成部分包括分组键,它定义了数据的分段方式;聚合函数,它执行诸如求和、平均、计数等操作;以及输出列,它显示分组键和聚合后的值。
在本章中,我们将涵盖以下主要内容:
-
使用一个或多个键进行数据分组
-
对分组数据应用聚合函数
-
对分组数据应用函数
-
数据过滤
技术要求
你可以在以下 GitHub 仓库中找到本章的代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/tree/main/chapter06。
使用一个或多个键进行数据分组
在 pandas 中,数据分组是一项基础操作,它涉及根据一个或多个键将数据拆分为多个组,然后在每个组内执行操作。分组常用于数据分析,以便对数据子集进行汇总计算并获得洞察。让我们更深入地探讨数据分组,并通过示例来说明它们的使用。本节的代码可以在这里找到:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter06/2.groupby_full_example.py。
使用一个键进行数据分组
使用一个键进行数据分组是数据分析中的常见操作。
要使用一个键对数据进行分组,我们可以使用 DataFrame 的groupby()方法,并指定我们希望作为分组键的列:
grouped = df.groupby('column_name')
在分组之后,你通常需要执行一些聚合操作。常见的聚合函数包括:
-
grouped.sum():这会计算所有数值列的总和 -
grouped.mean():这会计算平均值(算术平均) -
grouped.count():这会统计非空值的数量 -
grouped.agg(['sum', 'mean', 'count']):这会同时应用多个聚合函数:sum、mean和count
让我们展示一个常见的应用案例,来应用我们的学习成果。假设我们为一家电子零售公司工作,需要分析不同产品的销售数据。以下是数据的一个样本:
Category Sub-Category Region Sales Date
0 Electronics Mobile North 200 2023-01-01
1 Electronics Laptop South 300 2023-01-02
2 Electronics Tablet East 250 2023-01-03
3 Electronics Laptop West 400 2023-01-04
4 Furniture Chair North 150 2023-01-05
5 Furniture Table South 350 2023-01-06
在数据分析中,某些列由于其类别性质,通常是进行分组的候选列。这些列通常表示类别、分类或时间相关的分段,适合围绕这些进行数据聚合:
-
类别列:表示数据中不同组别或类型的列。例如,产品类别、用户类型或服务类型。这些列有助于理解每个组的表现或行为。
-
地理列:表示地理区域的列,例如国家、地区、城市或商店位置。这些对于区域表现分析很有用。
-
时间列:表示与时间相关的信息的列,例如年份、季度、月份、周或天。按这些列进行分组有助于进行趋势分析。
-
人口统计列:描述人口统计属性的列,例如年龄段、性别或收入水平。这些列对于根据人口特征进行数据细分非常有用。
-
交易相关列:与交易性质相关的列,例如交易类型、支付方式或订单状态。这些列有助于理解交易数据的不同方面。
根据我们示例中的数据,适合进行分组的列包括类别、子类别和地区。如果我们每天有多个记录,并且想计算每日销售量,那么日期也可以作为一个候选列。在我们的例子中,经理要求我们报告每个类别的总销售量(销售额)。让我们看看如何计算这个:
category_sales = df.groupby('Category')['Sales'].sum().reset_index()
在这个代码示例中,我们按类别列对数据进行分组,对每个类别的销售列求和,并重置索引。结果的 DataFrame 如下所示:
Category Sales
0 Clothing 1070
1 Electronics 1370
2 Furniture 1270
现在我们已经看到了如何按单个键进行分组,让我们通过按类别和地区分组来增加一些复杂性。
使用多个键对数据进行分组
按多个键进行分组可以更细致、详细地检查数据。这种方法有助于发现仅使用单一键时可能隐藏的见解,从而更深入地理解数据集中的关系和模式。在我们的示例中,按地区和类别进行分组,不仅可以看到整体的销售表现,还能看到不同类别在每个地区的表现。这有助于识别哪些产品在特定地区受欢迎,从而根据地区特征调整营销策略。
要使用多个键对数据进行分组,我们将列名列表传递给groupby()方法。Pandas 将根据这些列的唯一组合来创建组:
category_region_sales = df.groupby(['Category', 'Region'])['Sales'].sum().reset_index()
在这段代码中,我们按Category和Region列对数据进行分组,然后通过对每个组的Sales列求和来执行聚合。最后,我们重置索引。让我们看看这次操作的输出:
Category Region Sales
0 Clothing East 420
1 Clothing North 100
2 Clothing South 250
3 Clothing West 300
4 Electronics East 250
5 Electronics North 420
6 Electronics South 300
7 Electronics West 400
8 Furniture East 200
9 Furniture North 150
10 Furniture South 350
11 Furniture West 570
只需一行代码,我们就能汇总并展示每个Category和Region值的所有销售数据,使我们的经理非常满意。现在,让我们看看在使用 groupby 语句时的一些最佳实践。
分组的最佳实践
在 pandas 中进行数据分组时,需要考虑几件事,以确保结果准确:
-
缺失数据:要注意用于分组的列中是否存在缺失数据。Pandas 会排除包含缺失数据的行,这可能会影响最终的计算结果。
-
MultiIndex:当按多个列分组时,pandas 会返回一个层次索引(MultiIndex)。在使用MultiIndex时要熟悉,并考虑在需要时重置索引,就像我们为了简化所做的那样。 -
运算顺序:执行分组和聚合的顺序可能会影响结果。请注意应用分组和聚合函数的顺序。
-
分组大数据集:对于大型数据集,分组可能会占用大量内存。考虑使用分块处理或并行处理等技术来管理内存使用和计算时间。
我们的管理团队看到了我们执行的 groupby 操作的效率,他们要求我们提供更详细的销售总结!通过设置多个键,我们可以通过对Sales列应用多个聚合函数,进一步增强我们的分析。这将为我们提供更详细的数据总结。
对分组数据应用聚合函数
在 pandas 中,使用groupby()方法对数据进行分组后,可以应用聚合函数对分组数据执行计算。聚合函数用于总结或计算每个组的统计信息,结果是一个新的 DataFrame 或 Series。让我们更深入地探讨如何在分组数据上应用聚合函数,并提供一些示例来说明其用法。
基本聚合函数
我们在第一部分中已经介绍了基本的聚合函数,因为没有聚合函数就无法执行 groupby。在本节中,我们将进一步探讨每个函数的作用,以及何时使用每个函数,首先展示以下表格中的所有可用函数:
| 聚合 函数 | 描述 | 使用时机 | 代码示例 |
|---|---|---|---|
sum |
对组中的所有值求和 | 当你需要每个组的总值时。示例:按类别计算总销售额。 | df.groupby('Category')['Sales'].sum() |
mean |
计算组中值的平均数 | 当你需要每个组的平均值时。示例:按区域计算平均销售额。 | df.groupby('Category')['Sales'].mean() |
count |
计算组中非空值的数量 | 当你需要知道每个组中出现次数时。示例:每个子类别的销售交易次数。 | df.groupby('Category')['Sales'].count() |
min |
查找组中的最小值 | 当你需要每个组中的最小值时。示例:每个地区的最小销售值。 | df.groupby('Category')['Sales'].min() |
| 聚合 函数 | 描述 | 何时 使用 | 代码示例 |
max |
查找组中的最大值 | 当你需要每个组中的最大值时。示例:每个类别的最大销售值。 | df.groupby('Category')['Sales'].max() |
median |
查找组中的中位数值 | 当你需要一个排序数字列表中的中间值时。示例:每个类别的中位销售值。 | df.groupby('Category')['Sales'].median() |
std(标准差) |
衡量组中数值的分布 | 当你需要了解数值的变化时。示例:每个地区的销售标准差。 | df.groupby('Category')['Sales'].std() |
表 6.1 – 基本聚合函数的汇总表
你可以逐个调用这些函数,也可以将它们一起调用,例如:
total_sales = df.groupby('Category')['Sales'].sum().reset_index()
这计算了每个类别的销售数量,正如我们所学的,如果这是你从数据集中提取的唯一聚合信息,那么这已经足够了。然而,如果你被要求为不同的产品类别生成多个销售聚合,一个更高效的方法是一次性执行所有的聚合:
category_region_sales_agg = df.groupby(['Category', 'Region'])['Sales'].agg(['sum', 'mean']).reset_index()
在这段代码中,我们对 Sales 列应用了多个聚合函数(sum 和 mean)。结果如下:
Category Region sum mean
0 Clothing East 420 210.0
1 Clothing North 100 100.0
2 Clothing South 250 250.0
3 Clothing West 300 300.0
4 Electronics East 250 250.0
5 Electronics North 420 210.0
6 Electronics South 300 300.0
7 Electronics West 400 400.0
8 Furniture East 200 200.0
9 Furniture North 150 150.0
10 Furniture South 350 350.0
11 Furniture West 570 285.0
注意
我们可以在分组子句中添加任意数量的聚合。
我们在计算管理团队要求的各种指标时非常高效,结果是他们现在热衷于理解每个地区和类别的销售指标以及唯一子类别的销售数量。接下来我们来做这个。
使用多个列的高级聚合
为了了解每个地区和类别的销售指标以及每个子类别的唯一销售数量,我们可以对额外的列进行分组,并对 Sales 和 Subcategory 列应用多个聚合:
advanced_agg = df.groupby(['Category', 'Region']).agg({
'Sales': ['sum', 'mean', 'count'],
'Sub-Category': 'nunique' # Unique count of Sub-Category
}).reset_index()
在这段代码中,我们通过 Category 和 Region 对 DataFrame 进行分组,并执行了几个聚合操作:
-
'Sales': ['sum', 'mean', 'count']计算每个组的总销售额、平均销售额和交易次数(行数)。 -
'Sub-Category': 'nunique'计算每个Category和Region组内唯一子类别的数量。
这里展示的是汇总结果:
Category Region Sales Sub-Category
sum mean count nunique
0 Clothing East 420 210.0 2 2
1 Clothing North 100 100.0 1 1
2 Clothing South 250 250.0 1 1
3 Clothing West 300 300.0 1 1
4 Electronics East 250 250.0 1 1
5 Electronics North 420 210.0 2 1
6 Electronics South 300 300.0 1 1
现在,你可能会想,我们通过这些计算学到了什么?让我来回答这个问题!我们计算了总销售额、平均销售额和交易次数,以了解不同类别-地区组合的财务表现。此外,Sub-Category 的唯一计数揭示了我们产品分销策略的关键方面。此分析有多个目的:它为每个类别-地区细分内产品的多样性提供了洞察。例如,在我们的数据背景下,了解在不同类别下,每个地区销售的独特产品(子类别)数量,有助于了解市场细分和产品组合策略。它还帮助评估市场渗透率,通过突出显示提供更多产品的地区,支持产品组合管理的战略决策,包括扩展和针对区域偏好的库存策略。
标准的聚合函数,如求和、平均值和计数,提供了基本统计信息。然而,自定义函数使你能够计算那些特定于你业务需求或分析目标的指标。例如,计算销售数据的范围或变异系数,可以揭示不同组内销售的分布和变异性。如你所见,我们被要求实现这些自定义指标,接下来我们将进行此操作。
应用自定义聚合函数
当聚合需要复杂的计算,超出简单统计时,自定义函数非常有价值。你可以在需要计算那些独特于你分析目标或业务背景的指标时使用它们。例如,在销售分析中,你可能希望计算利润率、客户生命周期价值或流失率,这些通常不是通过标准聚合函数能够获得的。
让我们回到示例中,构建我们被要求计算的指标:对于每个地区,我们要计算销售范围和销售变异性。让我们看看下面的代码:
-
我们创建一个计算销售范围(最大值与最小值的差)的函数:
def range_sales(series): return series.max() - series.min() -
然后,我们创建一个计算销售变异系数的函数,它衡量相对于均值的相对变异性:
def coefficient_of_variation(series): return series.std() / series.mean() -
df数据框随后按Region分组:advanced_agg_custom = df.groupby('Region').agg({ 'Sales': ['sum', 'mean', 'count', range_sales, coefficient_of_variation], 'Sub-Category': 'nunique' }).reset_index()Sales: ['sum', 'mean', 'count', range_sales, coefficient_of_variation]使用自定义函数计算总销售额、平均销售额、交易次数、销售范围和变异系数。'Sub-Category':'nunique'计算每个组内独特子类别的数量。然后,我们重置索引以扁平化df数据框,使其更易于处理。 -
最后,我们重命名聚合后的列,以便输出更加清晰和易于阅读:
advanced_agg_custom.columns = [ 'Region', 'Total Sales', 'Average Sales', 'Number of Transactions', 'Sales Range', 'Coefficient of Variation', 'Unique Sub-Categories' ] -
让我们打印最终的数据框:
print(advanced_agg_custom)
最终的数据框在这里呈现:
Region TotalSales SalesRange Coef Unique Sub-Categories
0 East 870 120 0.24 4
1 North 670 120 0.32 3
2 South 900 100 0.16 3
3 West 1270 230 0.34 4
让我们花点时间了解一下各个区域的销售波动性。每个区域内销售额的范围可以揭示差异或区别,即最高和最低销售额之间的差距。例如,较大的范围可能表明不同区域间消费者需求或销售表现的显著差异。变异系数有助于将销售波动性相对于其平均值进行标准化。较高的变异系数表明更大的相对波动性,这可能促使进一步调查影响销售波动的因素。
注意
我希望你能清楚地理解,只要一个函数能够从输入的值序列中计算出单一的聚合结果,你就可以将它作为自定义聚合函数来构建。该函数还应返回一个单一的标量值,这是该组聚合的结果。
现在,让我们来看一下在使用聚合函数时的一些最佳实践。
聚合函数的最佳实践
在使用 Pandas 中的聚合函数时,需要考虑一些事项,以确保结果的准确性:
-
编写高效的自定义函数,尽量减少计算开销,特别是在处理大型数据集时。避免不必要的循环或操作,这些操作可能会减慢处理时间。
-
清楚地记录自定义聚合函数的逻辑和目的。这有助于在团队或组织内部维护和共享代码,确保分析的透明性和可重复性。
-
通过将结果与已知基准或手动计算进行比较,验证自定义聚合函数的准确性。此步骤对于确保自定义指标的可靠性和正确实现至关重要。
在 Pandas 中,使用 .agg() 方法与 groupby 时,你定义的聚合函数理想情况下应该为每个操作的列返回单一的标量值。然而,在某些情况下,你可能希望返回多个值或执行更复杂的操作。虽然 Pandas 的 .agg() 方法期望返回标量值,但你可以通过使用返回元组或列表的自定义函数来实现更复杂的聚合。然而,这需要谨慎处理,并且在 Pandas 的原生聚合框架中通常并不简单。对于需要返回多个值或执行复杂计算的更复杂场景,我们可以使用 apply() 替代 agg(),它更灵活,正如我们将在下一节中看到的。
在分组数据上使用 apply 函数
Pandas 中的 apply() 函数是一个强大的方法,用于沿着 DataFrame 或 Series 的轴应用自定义函数。它非常灵活,可以用于各种场景,以根据自定义逻辑操作数据、计算复杂的聚合或转换数据。apply() 函数可以用于以下操作:
-
按行或按列应用函数
-
当与
groupby()配合使用时,将函数应用于数据组
在接下来的章节中,我们将重点讨论如何在数据分组后使用apply函数,首先按我们想要的列进行分组,然后执行apply操作。
注意
使用不带groupby的apply函数,可以直接对 DataFrame 的行或列应用函数。这在你需要执行不需要分组数据的行或列级别的操作时非常有用。应用相同的学习,只需跳过groupby子句。
在使用 pandas 的apply函数时,axis=0(默认)将函数应用于每一列,而axis=1则将其应用于每一行。我们来深入了解一下这一点。
axis=0将函数应用于行。换句话说,它独立处理每一列。当你想按列汇总数据(例如,对每列的值求和)时,通常会使用此方法,如下图所示:

图 6.1 – Apply()与 axis=0
如果我们回到我们的用例,管理团队希望了解每个类别中产品的实际销售数量,而不仅仅是销售总额。我们的示例变得越来越复杂,因此,用apply()实现这个功能是个好主意。我们来看一个代码示例,代码也可以在这里找到:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter06/3.apply_axis0.py。
-
让我们扩展我们的 DataFrame,添加
Quantity列:data = { 'Category': ['Electronics', 'Electronics', 'Furniture', 'Furniture', 'Clothing', 'Clothing'], 'Sub-Category': ['Mobile', 'Laptop', 'Chair', 'Table', 'Men', 'Women'], 'Sales': [100, 200, 150, 300, 120, 180], 'Quantity': [10, 5, 8, 3, 15, 12], 'Date': ['2023-01-01', '2023-01-02', '2023-01-03', '2023-01-04', '2023-01-05', '2023-01-06'] } df = pd.DataFrame(data) -
然后我们将
Date列转换为日期时间格式:df['Date'] = pd.to_datetime(df['Date']) -
现在,让我们定义一个自定义函数来计算
Sales和Quantity的多个统计量:def compute_statistics(series): sum_sales = series['Sales'].sum() mean_sales = series['Sales'].mean() std_sales = series['Sales'].std() cv_sales = std_sales / mean_sales sum_quantity = series['Quantity'].sum() mean_quantity = series['Quantity'].mean() std_quantity = series['Quantity'].std() cv_quantity = std_quantity / mean_quantity return pd.Series([sum_sales, mean_sales, std_sales, cv_sales, sum_quantity, mean_quantity, std_quantity, cv_quantity], index=['Sum_Sales', 'Mean_Sales', 'Std_Sales', 'CV_Sales', 'Sum_Quantity', 'Mean_Quantity', 'Std_Quantity', 'CV_Quantity'])这个自定义函数(
compute_statistics)现在计算了在每个由Category定义的组内,Sales和Quantity列的多个统计量(总和、均值、标准差、变异系数)。对于每个类别组(系列),它计算以下内容:-
Sum_Sales:销售总和 -
Mean_Sales:销售的均值 -
Std_Sales:销售的标准差 -
CV_Sales:Sum_Quantity:数量的总和 -
Mean_Quantity:数量的均值 -
Std_Quantity:数量的标准差 -
CV_Quantity:数量的变异系数
最终,它返回一个包含这些计算统计量的 pandas Series,并适当地进行索引。
-
-
接下来,我们将在
Category上执行groupby操作,并应用我们自定义的函数来计算Sales和Quantity的统计量:result_complex = df.groupby('Category').apply(compute_statistics).reset_index()我们将
apply()与groupby('Category')结合使用,将compute_statistics函数应用于由Category列定义的每组销售数据。该函数作用于整个组(系列),允许同时计算Sales和Quantity列的统计数据。最后,使用reset_index()将结果 DataFrame 展平,提供按类别划分的两个列的统计数据结构化输出。我们来看一下最终的 DataFrame:

通过按Category对数据进行分组,我们可以在类别层面分析销售和数量指标,这有助于我们理解不同类型的产品(电子产品、家具、服装)在销售和数量方面的表现。正如我们从呈现的结果中看到的,Furniture(家具)是主要的收入来源,因为它具有最高的Sum_Sales和Mean_Sales,这表明该类别包含受欢迎或高价值的产品。具有较低CV_Sales和CV_Quantity值的类别,如Clothing(服装),在销售和数量上更为稳定,表明需求稳定或销售模式可预测,而具有较高变动性的类别(Std_Sales和Std_Quantity)可能表示销售波动或季节性需求。
这在数据分析方面非常有用,但现在,我们需要做出一些与产品组合、定价策略和市场营销措施相关的战略决策。在这一点上,让我们更加富有创意:
-
高
Sum_Sales值且指标稳定(CV_Sales,CV_Quantity)的类别是扩展产品线或投资市场营销的最佳候选者 -
高变动性的类别(
Std_Sales,Std_Quantity)可能需要动态定价策略或季节性促销来优化销售 -
我们可以使用
Mean_Sales和Mean_Quantity的值来识别具有增长潜力的类别
在使用 pandas 中的apply()函数时,如果没有指定 axis 参数,默认行为是axis=0。这意味着该函数将应用于每一列(即,它将独立处理每一列)。这就是我们在之前示例代码中所采用的方法。根据你的具体使用情况,可以调整apply()来按行(axis=1)或按列(axis=0)操作。接下来,让我们关注axis=1。
axis=1沿列应用函数,因此它独立处理每一行。这通常用于你想要执行按行操作时(例如,为每一行计算自定义指标)。

图 6.2 – Apply() 使用 axis=1
按行应用函数允许进行行级转换和计算。让我们通过 axis=1 来查看一个代码示例。我们先定义一个要跨列(axis=1)应用的函数。代码可以在这里找到:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter06/4.apply_axis1.py:
-
row_summary函数将 DataFrame 的单行作为输入,并返回该行数据的汇总。该函数的输入是关键,理解这一点是至关重要的,它是作为 pandas Series 传入的 DataFrame 单行:def row_summary(row): total_sales_quantity = row['Sales'] + row['Quantity'] sales_quantity_ratio = row['Sales'] / row['Quantity'] if row['Quantity'] != 0 else np.nan return pd.Series( [total_sales_quantity,sales_quantity_ratio], index=['Total_Sales_Quantity', 'Sales_Quantity_Ratio'])total_sales_quantity变量将存储该行的Sales和Quantity的总和。sales_quantity_ratio变量将存储该行的Sales与Quantity的比率,如果数量为零,则为np.nan,以提供销售效率的洞察。 -
我们按行应用函数(
axis=1):df_row_summary = df.apply(row_summary, axis=1) Total_Sales_Quantity Sales_Quantity_Ratio 0 110.0 10.00 1 205.0 40.00 2 158.0 18.75 3 303.0 100.00 4 135.0 8.00 5 192.0 15.0这将生成一个新的
df_row_summaryDataFrame,其中每一行对应于原始df中每行的total_sales_quantity和sales_quantity_ratio计算值。 -
最后,我们按
Category分组,以计算每个类别的指标:category_metrics = df.groupby('Category')[['Total_Sales_Quantity', 'Sales_Quantity_Ratio']].mean().reset_index()
让我们看看最终结果:
Category Total_Sales_Quantity Sales_Quantity_Ratio
0 Clothing 163.5 11.500
1 Electronics 157.5 25.000
2 Furniture 230.5 59.375
total_sales_quantity 指标提供了一个简单但有效的衡量标准,帮助我们了解每笔交易的总体销售表现,理解销售数量(Quantity)和销售价值(Sales)的综合影响。通过分析 total_sales_quantity,我们可以识别出销售和数量都较高的交易,这可能表示受欢迎的产品类别或成功的销售策略。相反,它也有助于识别表现不佳的交易,从而指导库存管理和促销调整,以提高销售效率和产品表现。这种双重洞察有助于战略决策,以优化销售和库存管理。
sales_quantity_ratio 指标提供了每单位数量的销售效率的宝贵洞察,揭示了产品如何有效地将数量转化为收入。这个指标对于评估每单位销售所产生的价值至关重要。通过它,我们可以识别每单位产生高收入的产品,表明这些可能是值得优先考虑营销的高价值商品。相反,它有助于发现每单位收入较低的产品,提示可能需要调整价格、进行有针对性的促销,或重新评估产品组合,以优化盈利能力和销售表现。
注意
在可能的情况下,出于性能考虑,优先使用矢量化操作(内置的 pandas 方法或 NumPy 函数)而不是 apply。矢量化操作通常更快,因为它们利用了优化的 C 代码。
到目前为止我们探讨的概念和技巧直接体现了数据清理中筛选的重要性。一旦我们应用了转换或聚合数据,筛选就能帮助我们聚焦于对分析有意义的特定数据子集,或满足特定条件的子集。例如,在计算了不同产品类别的销售表现指标(如Total_Sales_Quantity和Sales_Quantity_Ratio)之后,筛选可以帮助我们识别需要进一步调查的类别或产品,比如那些具有异常高或低表现指标的产品。
数据筛选
数据筛选是数据处理中的一项基本操作,涉及根据指定条件或标准选择数据子集。它用于从较大的数据集中提取相关信息、排除不需要的数据点,或集中关注分析或报告所需的特定部分。
在下面的示例中,我们筛选了 DataFrame,仅保留Quantity列大于10的行。这个操作选择了销量超过 10 个单位的产品,重点分析潜在的高绩效产品。github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter06/5.simple_filtering.py:
filtered_data = df[df['Quantity'] > 10]
让我们来看一下筛选后的 DataFrame:
Category Sub-Category Sales Quantity Date
4 Clothing Men 120 15 2023-01-05
5 Clothing Women 180 12 2023-01-06
超越简单筛选可以帮助我们识别符合更复杂条件的电子产品,正如我们将在下一节看到的那样。
多重筛选条件
筛选可能涉及复杂的条件,比如结合逻辑AND和OR操作,或使用嵌套条件。假设管理团队要求我们识别高价值的电子产品(sales > 1000),且销售数量相对较低(quantity < 30)。
让我们看看如何使用多个筛选条件来完成这个操作(代码可以在这里找到:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter06/6.advanced_filtering.py):
filtered_data = df[(df['Sales'] > 1000) & (df['Quantity'] < 30)]
在这个示例中,我们定义了一个筛选条件,筛选出销售额大于1000且数量小于30的行。让我们来看一下筛选后的 DataFrame:
Category Sub-Category Sales Quantity Date
1 Electronics Laptop 1500 25 2023-01-02
筛选是一个直接的操作,但让我们探索一些最佳实践,以优化其效果。
筛选的最佳实践
让我们探讨一些最佳实践,以提升筛选操作的效果:
-
根据分析目标清晰定义筛选标准。使用那些具体且与您想要得出的见解相关的条件。
-
利用数据操作库(如 Python 中的 pandas 或数据库中的 SQL 查询)提供的内置过滤函数。这些函数在性能和易用性上进行了优化。
-
确保过滤条件不会排除那些可能对分析有价值的重要数据点。验证结果以确认它们与预期的结果一致。
-
记录过滤条件和应用步骤,以保持透明度并促进分析的可重复性。
随着数据集的增长,过滤变得至关重要,用于高效管理和提取洞察。没有有效的过滤策略,处理大量数据的操作可能会变得极其缓慢。通过减少每次需要存储和处理的数据量,过滤有助于优化资源利用,如内存和处理能力。
随着数据增长,性能考虑因素
让我们来看一下随着数据增长需要注意的事项:
-
过滤操作通过减少需要处理的行或列数来优化查询执行,从而加快数据查询和分析的响应速度。
-
大型数据集消耗大量内存和存储资源。过滤减少了存储在内存中或硬盘上的数据量,提高了效率,并降低了与数据存储相关的运营成本。
现在,让我们总结一下本章的学习内容。
总结
在本章中,我们探讨了一些强大的技术,例如分组、聚合和应用自定义函数。这些方法对于总结和转化数据至关重要,有助于深入洞察数据集。我们学习了如何根据类别变量(如Category和Region)高效地分组数据,并应用聚合函数(如求和、平均值和自定义指标)来得出有意义的总结。
此外,我们深入探讨了apply函数的多功能性,它允许进行行或列的自定义计算。强调了优化函数效率、处理缺失值和理解性能影响等最佳实践,以确保有效的数据处理。最后,我们讨论了过滤器的战略性应用,基于特定标准精炼数据集,提升数据分析精度。
在下一章中,我们将讨论设计和优化数据写入操作,以高效地存储转化和清洗后的数据。
第七章:数据接收端
在现代数据处理的世界中,关于数据管理、存储和处理的关键决策将决定成功的结果。在本章中,我们将深入探讨支撑高效数据处理管道的三大重要支柱:选择正确的数据接收端、选择最优的文件类型,以及掌握分区策略。通过讨论这些关键要素及其在实际应用中的体现,本章将为你提供所需的洞察和策略,帮助你在复杂的数据处理技术领域内设计优化效率、可扩展性和性能的数据解决方案。
在本章中,我们将讨论以下主题:
-
为你的使用案例选择正确的数据接收端
-
为你的使用案例选择正确的文件类型
-
导航分区
-
设计一个在线零售数据平台
技术要求
本章中,我们需要安装以下库:
pip install pymongo==4.8.0
pip install pyarrow
pip install confluent_kafka
pip install psycopg2-binary==2.9.9
和往常一样,你可以在本书的 GitHub 仓库中找到本章的所有代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/tree/main/chapter07。
每个部分后面都有一个类似命名规则的脚本,因此可以执行这些脚本或通过阅读本章进行跟进。
为你的使用案例选择正确的数据接收端
数据接收端指的是数据流向或存储的目标位置。术语“接收端”是用来比喻数据流入并被指定位置吸收的概念。数据接收端通常作为存储位置,数据可以在此永久或临时存储。这些存储可以是数据库、文件或其他数据结构的形式。
数据工程师和数据科学家通常根据其特定的任务和使用场景,使用多种数据接收端。让我们看看一些常见的数据接收端,并附带代码示例,同时考虑每种类型的优缺点。
关系型数据库
关系型数据库是一种数据库管理系统(DBMS),它将数据组织成具有行和列的表格,每行代表一条记录,每列代表一个字段。表格之间的关系通过键建立。主键唯一标识表格中的每一条记录,外键则在表格之间创建链接。
关系型数据库概述
以下是关系型数据库的关键组件的简要概述:
-
表格:数据被组织成表格,每个表格代表特定的实体或概念。例如,在一个图书馆的数据库中,可能会有关于书籍、作者和借阅者的表格。
-
行和列:每个表由行和列组成。行代表特定的记录(例如,一本书),每列代表该记录的特定属性或字段(例如,书名、作者和出版年份)。
-
键:键用于建立表之间的关系。主键唯一标识表中的每一条记录,而相关表中的外键则在它们之间创建连接。
-
结构化查询语言 (SQL):关系型数据库使用 SQL 进行数据查询和操作。SQL 允许用户检索、插入、更新和删除数据,同时定义和修改数据库的结构。
在数据领域,我们通常在以下场景中看到关系型数据库:
-
结构化数据:如果您的数据具有明确的结构,并且实体之间有清晰的关系,那么关系型数据库是一个合适的选择。
-
数据完整性要求:如果您的应用对于数据完整性有严格要求(例如,在金融系统或医疗应用中),关系型数据库提供机制来强制执行完整性约束。
-
原子性、一致性、隔离性和持久性 (ACID) 特性:原子性确保事务是“全有或全无”的操作:要么所有更改都提交,要么都不提交。例如,在账户之间转账时,原子性保证两个账户的余额要么都更新,要么都不更新。一致性意味着事务将数据库从一个有效状态转移到另一个有效状态,同时遵守完整性约束。如果违反了唯一客户 ID 等规则,事务将回滚以保持一致性。隔离性确保事务独立执行,防止并发事务之间的干扰和未提交更改的可见性,避免了脏读等问题。最后,持久性保证一旦事务提交,更改就会永久保留,即使系统发生故障,也能确保更新(如在线应用中的联系人信息)的持久性。如果您的应用需要遵守 ACID 特性,关系型数据库专门设计来满足这些需求。
-
复杂查询:如果您的应用涉及复杂的查询和报告需求,关系型数据库凭借其 SQL 查询功能,非常适合此类场景。
市面上有许多不同的构建关系型数据库的选项,接下来我们将看到这些选项。
关系型数据库管理系统的不同选项
市面上有许多不同的关系型数据库管理系统 (RDBMSs) 。我们在下表中总结了主要的几种:
| 数据库 | 描述 |
|---|---|
| MySQL | 一个以速度、可靠性著称的开源关系型数据库管理系统,广泛应用于网页开发 |
| PostgreSQL | 一个开源关系型数据库管理系统,具备高级功能、可扩展性,并支持复杂查询 |
| Oracle 数据库 | 一款商业 RDBMS,以其可扩展性、安全性以及全面的数据管理功能而著称 |
| Microsoft SQL Server | 微软推出的商业 RDBMS,集成了微软技术并支持商业智能 |
| SQLite | 一款轻量级、嵌入式、无服务器的 RDBMS,适用于数据库需求较低或中等的应用 |
| MariaDB | 一款从 MySQL 派生的开源 RDBMS,旨在兼容性同时引入新特性 |
表 7.1 – RDBMS 概述
现在,让我们看看如何快速设置本地关系型数据库、连接到它并创建一个新表的示例。
一个 PostgreSQL 数据库示例
首先,我们需要安装并设置 PostgreSQL。这根据操作系统(OS)有所不同,但逻辑保持一致。以下脚本自动化了在 macOS 或基于 Debian 的 Linux 系统上安装和设置 PostgreSQL 的过程:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter07/setup/setup_postgres.sh。
首先,它使用 uname 命令检测操作系统:
OS=$(uname)
如果检测到 macOS,它将使用 Homebrew 更新软件包列表,安装 PostgreSQL 并启动 PostgreSQL 服务。如果检测到基于 Debian 的 Linux 操作系统,它将使用 apt-get 更新软件包列表,安装 PostgreSQL 及其 contrib 包,并启动 PostgreSQL 服务。以下是安装 macOS 的代码:
if [ "$OS" == "Darwin" ]; then
echo "Detected macOS. Installing PostgreSQL via Homebrew..."
brew update
brew install postgresql
brew services start postgresql
如果你的操作系统不被该脚本支持,则会显示以下错误消息:
Unsupported OS. Please install PostgreSQL manually.
在这种情况下,你需要手动安装 PostgreSQL 并启动服务。完成后,你可以继续执行脚本的第二部分。然后,脚本切换到默认的 postgres 用户,以执行 SQL 命令,在该用户尚未存在时创建新数据库用户,创建一个由该用户拥有的新数据库,并授予该用户对该数据库的所有权限,如下所示:
psql postgres << EOF
DO \$\$
BEGIN
IF NOT EXISTS (
SELECT FROM pg_catalog.pg_user
WHERE usename = 'the_great_coder'
) THEN
CREATE USER the_great_coder
WITH PASSWORD 'the_great_coder_again';
END IF;
END
\$\$;
EOF
psql postgres << EOF
CREATE DATABASE learn_sql2 OWNER the_great_coder;
EOF
psql postgres << EOF
-- Grant privileges to the user on the database
GRANT ALL PRIVILEGES ON DATABASE learn_sql2 TO the_great_coder;
EOF
要执行上述代码,请按照以下步骤操作:
-
确保将仓库拉取到本地笔记本电脑。
-
转到存放仓库的文件夹。
-
在仓库文件夹位置打开终端。
-
执行以下命令以导航到正确的文件夹:
cd chapter7 setup_postgres.sh script, as shown here:maria.zevrou@FVFGR3ANQ05P chapter7 % cd setup
maria.zevrou@FVFGR3ANQ05P set up % ls
setup_postgres.sh
-
通过运行以下命令使脚本可执行:
chmod +x setup_postgres.sh -
最后,使用以下命令运行实际的脚本:
./setup_postgres.sh
执行脚本后,你应该看到一条确认消息,表示 PostgreSQL 设置(包括数据库和用户创建)已完成:
PostgreSQL setup completed. Database and user created.
现在,我们准备执行脚本,以便将传入的数据写入我们在前一步中创建的数据库。你可以在这里找到这个脚本:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter07/1.postgressql.py。
该脚本连接到我们之前创建的 PostgreSQL 数据库并管理其中的表。让我们开始吧:
-
我们首先导入必要的库:
import pandas as pd import psycopg2 from psycopg2 import sql -
接着,我们需要定义几个函数,从
table_exists开始。该函数检查指定的表是否已存在于数据库中:def table_exists(cursor, table_name): cursor.execute( sql.SQL("SELECT EXISTS ( \ SELECT 1 FROM information_schema.tables \ WHERE table_name = %s)"), [table_name] ) return cursor.fetchone()[0] -
我们需要的下一个函数是
create_table函数,如果表在特定模式下不存在,它将创建一个新表。在我们的例子中,它将有三列:id作为主键,name和age:def create_table(cursor, table_name): cursor.execute( sql.SQL(""" CREATE TABLE {} ( id SERIAL PRIMARY KEY, name VARCHAR(255), age INT ) """).format(sql.Identifier(table_name)) ) -
接着,我们必须定义
insert_data函数,该函数用于向表中插入数据行:def insert_data(cursor, table_name, data): cursor.executemany( sql.SQL("INSERT INTO {} (name, age) \ VALUES (%s, %s)" ).format(sql.Identifier(table_name)), data ) -
最后,我们必须使用以下函数来显示检索到的数据:
def print_table_data(cursor, table_name): cursor.execute( sql.SQL( "SELECT * FROM {}" ).format(sql.Identifier(table_name)) ) rows = cursor.fetchall() for row in rows: print(row)此时,脚本将创建一个包含示例数据(名称和年龄)的模拟 DataFrame:
data = { 'name': ['Alice', 'Bob', 'Charlie'], 'age': [25, 30, 22] } df = pd.DataFrame(data)它使用指定的连接参数(数据库名称、用户名、密码、主机和端口)建立与 PostgreSQL 数据库的连接。这些正是我们在之前的步骤中设置数据库时使用的详细信息,因此你这边无需做任何更改:
db_params = { 'dbname': 'learn_sql', 'user': 'the_great_coder', 'password': 'the_great_coder_again', 'host': 'localhost', 'port': '5432' } conn = psycopg2.connect(**db_params) cursor = conn.cursor() -
最后,它会检查名为
example_table的表是否存在,如有必要会创建它,然后将模拟数据插入到表中。在提交更改到数据库后,脚本从表中获取数据并打印,以确认成功插入,最后关闭数据库连接:table_name = 'example_table' if not table_exists(cursor, table_name): create_table(cursor, table_name) insert_data(cursor, table_name, df.values.tolist()) conn.commit() print_table_data(cursor, table_name) cursor.close() conn.close()
要执行前面的脚本,只需在 chapter7 文件夹中执行以下命令:
python 1.postgressql.py
重要说明
记得始终关闭连接,因为这有助于避免性能问题,并确保在需要时能够建立新连接。它使数据库能够释放与连接相关的资源,并确保任何未提交的事务得到适当处理。关闭连接将其返回到连接池,使其可以被应用程序的其他部分重用。
要查看在数据库中创建的表,可以打开终端中的 PSQL 进程,并通过执行以下命令连接到 learn_sql 数据库:
psql -h localhost -U the_great_coder -d learn_sql
然后,运行以下命令以列出所有可用的表:
\dt
你应该会看到类似如下内容:

图 7.1 – 列出数据库中的表
现在你还可以通过执行以下 SQL 命令与表进行交互:

图 7.2 – 显示表中的所有行
如果你在不先删除现有表的情况下重新运行相同的 Python 脚本,你不会看到创建一个新表;相反,新的行会被添加到相同的表中:

图 7.3 – 在脚本重新运行后显示表中的所有行
在了解如何设置关系型数据库并通过写入新数据将其用作存储后,我们深入探讨关系型数据库的优缺点。
关系型数据库的优缺点
在这一部分,我们将总结使用关系型数据库管理系统(RDBMS)的优缺点。
优点如下:
-
RDBMS 系统具有 ACID 属性,提供了一个强大的框架来保证可靠和安全的事务
-
RDBMS 技术已经存在了几十年,产生了成熟且完善的系统,拥有丰富的文档和社区支持
然而,它们也有各种缺点:
-
RDBMS 的严格模式在处理不断变化或动态数据结构时可能成为一种限制,因为它们需要模式修改。新数据可能需要进行模式更改。
-
RDBMS 主要设计用于结构化数据,可能不适合处理非结构化或半结构化数据。
如果你在想关系型数据库中的数据是以什么文件类型存储的,那么接下来的子部分会让你觉得很有趣。
关系型数据库文件类型
在关系型数据库中,存储数据的文件类型通常是抽象的,用户和开发人员不常直接与底层文件交互。关系型数据库通过其内部机制管理数据存储和检索,这些机制通常涉及专有的 文件格式。
在关系型数据库中,存储和组织数据的过程由数据库管理系统(DBMS)管理,用户使用 SQL 或其他查询语言与数据进行交互。DBMS 将物理存储细节从用户中抽象出来,提供一个逻辑层,允许数据操作和检索,而无需直接关注底层文件格式。
让我们讨论一下关系型数据库中文件类型的关键点:
-
关系型数据库供应商通常使用专有的文件格式来存储数据。每个数据库管理系统可能有它自己的内部结构和机制来 管理数据。
-
关系型数据库通常将数据组织成表空间,这是逻辑存储容器。这些表空间由存储数据的页或块组成。页的组织和结构由特定的数据库管理系统(DBMS)决定。
-
关系型数据库优先考虑 ACID 属性,以确保数据完整性和可靠性。内部文件格式被设计用来支持这些事务性保证。
-
关系数据库使用各种索引和优化技术来提升查询性能。包括 B 树或其他索引结构在内的内部文件结构被优化以实现高效的数据检索。
-
用户使用 SQL 命令与关系数据库进行交互。
虽然用户通常不会直接与底层文件格式交互,但理解表空间、页面及数据库管理系统(DBMS)如何管理数据存储的概念,对于数据库管理员和开发人员在优化性能或排查问题时是非常有用的。
从关系数据库管理系统(RDBMS)迁移到不仅仅是 SQL(NoSQL)数据库涉及数据建模、模式设计和查询方法的转变。我们将在接下来的部分中探讨这些差异。
NoSQL 数据库
NoSQL 数据库,也称为不仅仅是 SQL或非 SQL数据库,是一类提供灵活和可扩展的数据存储与处理方法的数据库系统。与传统的关系数据库不同,后者强制使用具有预定义表格、列和关系的结构化模式,NoSQL 数据库旨在处理多种数据模型,适应不同的数据结构和组织方式,并提供更加动态和灵活的数据建模方法。
NoSQL 数据库概述
下面是 NoSQL 数据库关键组件的快速概述:
-
NoSQL 数据库通常采用无模式或灵活模式的方法,允许数据在没有预定义模式的情况下进行存储。这种灵活性在数据结构不断变化或无法预先确定的情况下尤其有用。
-
NoSQL 数据库有不同类型,每种类型都有自己的数据模型,如面向文档的(如 MongoDB)、键值存储(如 Redis)、列族存储(如 Apache Cassandra)和图数据库(如 Neo4j)。数据模型因应不同类型的数据和使用场景而有所不同。面向文档的数据模型将数据存储为 JSON 文档,允许每个文档具有不同的结构,适用于半结构化或非结构化数据。键值数据模型将数据存储为键值对,其中值可以是简单类型或复杂结构,提供快速的数据检索,但查询能力有限。列族数据模型将数据按列而非行组织,使得存储和检索大规模数据集更加高效。最后,图数据模型将数据表示为节点和边,非常适合关注关系的应用,如社交网络和网络分析。
-
NoSQL 数据库通常设计为横向扩展,这意味着它们能够高效地将数据分布到多个节点。
-
NoSQL 数据库通常遵循 一致性、可用性和分区容忍性(CAP)定理,该定理表明分布式系统最多只能提供三项保证中的两项。NoSQL 数据库可能会优先考虑可用性和分区容忍性,而不是严格的一致性。
在数据领域,我们通常会发现 NoSQL 数据库作为以下情况下的“数据存储”:
-
当我们处理的数据模型可能经常变化或事先定义不明确时。
-
当一个应用程序预见到或经历快速增长时,在这种情况下,水平可扩展性至关重要。
-
当数据无法放入具有固定关系的表中时,这时需要一种更灵活的存储模型。
-
当快速开发和迭代至关重要时,在这种情况下,我们需要随时修改数据模型。
-
当某一特定类型的 NoSQL 数据库的具体特性和功能与应用程序的需求相符时(例如,面向内容的应用程序使用面向文档的数据库,缓存使用键值存储,关系数据使用图数据库)。
让我们看一个如何连接到 NoSQL 数据库并写入新表的示例。
一个 MongoDB 数据库的示例
在深入代码之前,我们先花些时间来解释 MongoDB 以及与之相关的一些重要概念:
-
文档:这是 MongoDB 中的数据基本单元,以 二进制 JSON(BSON)对象的形式表示。文档类似于关系数据库中的行,但可以具有不同的结构。文档由字段(键值对)组成。每个字段可以包含不同的数据类型,例如字符串、数字、数组或嵌套文档。
-
集合:MongoDB 文档的集合,类似于关系数据库中的表。集合包含文档,并作为组织数据的主要方法。集合不需要预定义的架构,这使得同一集合中的文档可以具有不同的结构。
-
数据库:集合的容器。MongoDB 数据库包含集合,并作为数据组织的最高层级。每个数据库与其他数据库隔离,这意味着一个数据库中的操作不会影响其他数据库。
现在我们对这些概念有了更清晰的理解,让我们来看看代码。要运行这个示例,请按照操作系统的文档在本地设置 MongoDB。对于 Mac 的安装说明,请访问这里:www.mongodb.com/docs/manual/tutorial/install-mongodb-on-os-x/。以下代码示例展示了如何创建数据库并使用 pymongo 在 MongoDB 中写入数据。请注意,pymongo 是 MongoDB 的官方 Python 驱动程序,提供了一个 Python 接口来连接 MongoDB 数据库,执行查询,并通过 Python 脚本操作数据。
开始吧:
-
安装完 MongoDB 后,打开你的终端并启动服务。这里提供的命令适用于 Mac;请根据你的操作系统,参考文档中的命令:
brew services start mongodb-community@7.0 -
通过执行以下命令验证服务是否正在运行:
brew services list mongodb-community@7.0 started maria.zervou ~/Library/LaunchAgents/h安装完成后,我们来设置一个 MongoDB 数据库。
-
在你的终端中,输入以下命令进入 MongoDB 编辑器:
no_sql_db:best_collection_ever:
db.createCollection("best_collection_ever")你应该看到类似以下的响应:
{ ok: 1 }
到此为止,我们已经准备好切换到 Python,并开始向这个集合中添加数据。你可以在这里找到代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter07/2.pymongo.py。请按照以下步骤操作:
-
首先,我们导入所需的库:
from pymongo import MongoClient -
每次连接到 NoSQL 数据库时,我们需要提供连接详情。更新
mongo_params字典中的所有参数值,字典包含了 MongoDB 服务器主机、端口、用户名、密码和认证源:mongo_params = { 'host': 'localhost', 'port': 27017, 'username': 'your_mongo_username', 'password': 'your_mongo_password', 'authSource': 'your_auth_database' } -
让我们来看一下在本例中用于将文档插入 MongoDB 数据库的不同函数。第一个函数在创建新集合之前,会检查集合是否存在于数据库中:
def collection_exists(db, collection_name): return collection_name in db.list_collection_names() -
以下函数将数据库和集合名称作为参数,并创建一个我们传递的名称的集合(在我们的例子中,我们提供了
collection_name作为名称):def create_collection(db, collection_name): db.create_collection(collection_name) -
最后,我们将使用之前创建的集合,它现在只是一个占位符,并向其中插入一些数据:
def insert_data(collection, data): collection.insert_many(data) -
让我们创建一些要插入到集合中的数据:
documents = [ {'name': 'Alice', 'age': 25}, {'name': 'Bob', 'age': 30}, {'name': 'Charlie', 'age': 22} ] -
让我们指定连接所需的参数并创建连接:
db_name = ' no_sql_db' collection_name = 'best_collection_ever' client = MongoClient(**mongo_params) db = client[db_name] -
现在,让我们检查是否存在具有提供名称的集合。如果集合存在,则使用现有集合;如果不存在,则创建一个新的集合并使用提供的名称:
if not collection_exists(db, collection_name): create_collection(db, collection_name) -
然后,获取集合并插入提供的数据:
collection = db[collection_name] insert_data(collection, documents) -
最后,关闭 MongoDB 连接:
client.close()
执行此脚本后,你应该能够看到记录被添加到集合中:
{'_id': ObjectId('66d833ec27bc08e40e0537b4'), 'name': 'Alice', 'age': 25}
{'_id': ObjectId('66d833ec27bc08e40e0537b5'), 'name': 'Bob', 'age': 30}
{'_id': ObjectId('66d833ec27bc08e40e0537b6'), 'name': 'Charlie', 'age': 22}
本脚本演示了如何与 MongoDB 数据库和集合进行交互。与关系型数据库不同,MongoDB 不需要预先创建表或模式。相反,你直接与数据库和集合进行操作。作为练习,为了更好地理解这种灵活的数据模型,尝试将不同结构的数据插入集合中。这与关系型数据库不同,后者是向具有固定模式的表中插入行。你可以在这里找到一个示例:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter07/3.pymongo_expand.py。
最后,脚本使用find方法查询并从集合中检索文档。与 SQL 查询相比,MongoDB 的查询更加灵活,特别是在处理嵌套数据时。
不要删除 MongoDB 数据库
请不要清理我们创建的 Mongo 资源,因为我们将在流式接收示例中使用它们。我们将在本章结束时清理所有资源。
在接下来的部分,我们将讨论 NoSQL 数据库所提供的优缺点。
NoSQL 数据库的优缺点
让我们总结一下使用 NoSQL 系统的优缺点。
NoSQL 数据库的优点如下:
-
可扩展性:NoSQL 数据库设计时考虑了水平扩展性,可以通过将数据分布到多个服务器来处理大量数据。这使得它们特别适用于大数据应用和云环境。
-
灵活性:与需要固定模式的 SQL 数据库不同,NoSQL 数据库提供灵活的模式。这使得结构化、半结构化和非结构化数据可以存储,从而更容易适应不断变化的数据模型,而无需进行大规模的重构。
-
性能:NoSQL 数据库在处理某些类型的查询时,尤其是处理大数据集时,可能提供更优的性能。它们通常针对高速数据检索进行了优化,并能处理每秒大量的事务。
-
成本效益:许多 NoSQL 数据库是开源的,并且可以使用普通硬件进行扩展,这相比于通常需要昂贵硬件来扩展 SQL 数据库的成本要低。
-
开发者灵活性:模式和数据模型的灵活性使得开发者可以快速迭代,适应新的需求,而无需进行大量的数据库管理。
然而,它们也有一些缺点:
-
缺乏标准化:NoSQL 数据库没有像 SQL 那样的标准化查询语言。这可能导致学习曲线较陡,并且使得在不同 NoSQL 系统之间切换变得具有挑战性。
-
有限的复杂查询支持:NoSQL 数据库通常缺乏 SQL 数据库的高级查询功能,如连接和复杂事务,这可能限制它们在需要复杂数据关系的应用中的使用。
-
数据一致性:许多 NoSQL 数据库优先考虑可用性和分区容忍性,而不是一致性(根据 CAP 定理)。这可能导致最终一致性模型,这对于需要严格数据完整性的应用可能不适用。
-
成熟度和社区支持:与 SQL 数据库相比,NoSQL 数据库相对较新,这意味着它们可能拥有不那么成熟的生态系统和较小的社区。这可能使得寻找支持和资源变得更加困难。
-
复杂的维护:NoSQL 数据库的分布式特性可能导致复杂的维护任务,如数据分布和负载均衡,这些任务需要专业的知识。
现在,让我们讨论在使用 NoSQL 数据库时可能遇到的文件格式。
NoSQL 数据库文件类型
最常见的文件格式是 JSON 和 BSON。JSON 是一种轻量级的、易于人类阅读的数据交换格式,采用键值对结构,并支持嵌套数据结构。由于其简洁性和易于解析,JSON 被广泛应用于基于 Web 的数据交换。JSON 是语言无关的,适用于各种编程语言。JSON 的灵活性和无模式特性与许多 NoSQL 数据库的灵活模式方法相契合,允许轻松处理不断变化的数据结构。NoSQL 数据库通常处理半结构化或非结构化数据,JSON 的层级结构能够很好地适应这种数据。以下是一个 JSON 数据文件的示例:
{
"person": {
"name": "John Doe",
"age": 30,
"address": {
"city": "New York",
"country": "USA"
},
"email": ["john.doe@email.com", "john@example.com"]
}
}
BSON 是一种二进制编码的序列化格式,用于类似 JSON 的文档,旨在提高存储和遍历的效率。它增加了 JSON 中没有的数据类型,如 date 和 binary。BSON 文件在存储前会被编码,在显示前会被解码。BSON 的二进制格式更适合存储和序列化,因此在需要紧凑表示数据的场景中非常合适。BSON 是 MongoDB 中使用的主要数据格式。让我们来看一下之前展示的文件的 BSON 表示:
\x16\x00\x00\x00
{
"person": {
"name": "John Doe",
"age": 30,
"address": {
"city": "New York",
"country": "USA"
},
"email": ["john.doe@email.com", "john@example.com"]
}
}\x00
在 NoSQL 数据库中,选择 JSON 还是 BSON 通常取决于数据库的具体需求和使用场景。虽然 JSON 在许多场景下更易于人类阅读和操作,但 BSON 的二进制效率在某些情况下,特别是在存储和序列化效率至关重要时,具有优势。
在接下来的部分,我们将讨论数据仓库,它们解决了哪些挑战,以及在使用时应该考虑实施的应用场景。
数据仓库
当数据的体积和复杂性以及对高级分析的需求超出了现有关系型或 NoSQL 数据库的能力时,转向数据仓库变得非常重要。如果您的关系型数据库在处理大量数据、复杂查询或在分析处理期间遇到性能问题,数据仓库可以为此类工作负载提供优化的存储和查询性能。同样,NoSQL 数据库虽然在处理非结构化或半结构化数据以及横向扩展方面表现优异,但可能缺乏深度分析和报告所需的复杂查询能力和性能。数据仓库旨在整合来自多个来源的数据,包括关系型和 NoSQL 数据库,促进全面分析和报告。它们为历史数据分析、复杂查询和数据治理提供强有力的支持,使其成为在需要提升数据整合、分析和报告能力时的理想解决方案,超越了传统数据库的能力。
数据仓库概览
数据仓库是一种专门的数据库系统,旨在高效地存储、组织和检索大量数据,这些数据用于商业智能和分析。与优化用于快速数据更新和单个记录检索的事务性数据库不同,数据仓库的结构是为了支持复杂的查询、聚合和对历史及当前数据的报告。
这里是数据仓库关键组件的快速概述:
-
各种数据源为数据仓库提供数据,包括事务性数据库、外部文件、日志等。
-
提取、转换和加载(ETL)过程用于从源系统收集数据,将其转换为一致的格式,并将其加载到数据仓库中。
-
数据仓库采用优化的存储方法,例如列式存储,以高效地存储大量数据。
-
索引和预聚合表用于优化查询性能。在数据仓库中,索引在优化查询性能方面起着至关重要的作用。索引是一种数据结构,通过创建数据的独立、有序子集来提高从表中检索数据的速度和效率。索引通常在一个或多个列上创建,以便加速查询。没有索引时,数据库必须扫描整个表才能定位相关行。索引帮助数据库快速找到符合查询条件的行。常见的索引候选列包括
WHERE子句、JOIN条件和ORDER BY子句中使用的列。然而,过度索引可能导致收益递减并增加维护负担。虽然索引提高了查询性能,但它们也消耗额外的存储空间,并且由于需要维护索引,可能会减慢写操作的速度。 -
诸如并行处理和索引等技术被用来提高分析查询的速度。
-
与商业智能工具的集成允许用户创建报告和仪表板,并执行数据分析。
-
数据通过多维模型进行组织,通常以星型或雪花型模式的形式出现,以支持分析和报告。
让我们扩展一下维度建模。它是一种在数据仓库中用于结构化数据的设计技术,使得数据能够支持高效的分析查询和报告。与传统的关系模型不同,维度模型经过优化,专注于查询性能和易用性。在接下来的部分中,我们将介绍维度模型中的主要模式类型。
维度模型中的模式类型
维度建模主要涉及两种模式:星型模式和雪花模式。星型模式是最简单的维度建模形式,其中一个中心事实表直接连接到多个维度表,形成类似星星的结构。这种模式直观易懂,便于导航,非常适合简单的查询和报告。每个星型模式中的维度表包含一个主键,与事实表中的外键相关联,为事实表中的定量数据提供描述性上下文。例如,销售星型模式可能包括一个中心销售事实表,外键链接到产品、客户、时间和商店等维度表,从而通过减少连接的数量简化复杂查询:

图 7.4 – 星型模式
另一方面,雪花模式是星型模式的一个更规范化版本,在这种模式下,维度表会进一步拆分成相关的子表,形成类似雪花的结构。这种结构减少了数据冗余,可以节省存储空间,尽管由于需要额外的连接,它在查询设计上引入了更多的复杂性。例如,雪花模式中的产品维度表可能被规范化为独立的产品类别和品牌表,从而形成多层次结构,确保更高的数据完整性并减少更新异常。虽然雪花模式在查询时可能稍显复杂,但在数据维护和可扩展性方面提供了优势,尤其是在数据一致性和存储优化至关重要的环境中:

图 7.5 – 雪花模式
星型模式通常用于较简单的层次结构,并且当查询性能优先时,而雪花模式则可能在更高效地使用存储和实现更高程度的规范化时被选用。
在理解维度建模中的模式类型之后,探索在各种组织环境中实施和利用数据仓库的多种选择至关重要。
数据仓库解决方案
市场上有许多不同的数据仓库选择。我们在下表中总结了主要的选项:
| 数据仓库 | 描述 |
|---|---|
| Databricks SQL | 一种基于云的、无服务器的数据仓库,将仓库功能带入数据湖。它以其可扩展性、性能和并行处理能力而闻名,并且具备内建的机器学习功能。 |
| Amazon Redshift | 一项完全托管的、可扩展的云数据仓库服务,优化用于高性能分析。 |
| Snowflake | 一种基于云的数据仓库,具有多集群共享架构,支持多种工作负载。 |
| Google BigQuery | 一种无服务器、高度可扩展的数据仓库,具有内置的机器学习功能。 |
| 数据仓库 | 描述 |
| Teradata | 一种支持扩展性、性能和并行处理的本地或基于云的数据仓库。 |
| Microsoft Azure Synapse Analytics | 一种基于云的分析服务,提供按需和预配资源。 |
表 7.2 – 数据仓库解决方案
在下一节中,我们将查看创建 BigQuery 表的示例,以说明数据仓库的实际应用。
数据仓库示例
让我们来学习如何在 BigQuery 中创建一个新表。Google Cloud 为包括 Python 在内的多种编程语言提供了与 BigQuery 交互的客户端库。为了准备好并运行以下示例,请访问 BigQuery 文档:cloud.google.com/python/docs/reference/bigquery/latest。让我们深入研究这个示例。我们将按照本章至今介绍的相同模式进行操作。让我们开始吧:
注意
要运行此示例,您需要拥有一个Google Cloud Platform(GCP)账户,并准备好一个 Google Storage 存储桶。
-
导入所需的库:
from google.cloud import bigquery from google.cloud.bigquery import SchemaField -
首先,我们将设置项目 ID。将
your_project_id替换为你的实际值:client = bigquery.Client(project='your_project_id') -
定义数据集和表名称,并更新以下字段:
dataset_name = 'your_dataset' table_name = 'your_table' -
检查数据集和表是否存在:
dataset_ref = client.dataset(dataset_name) table_ref = dataset_ref.table(table_name) table_exists = client.get_table( table_ref, retry=3, timeout=30, max_results=None ) is not None -
定义表的模式(如果你希望更新数据,可以替换为你的模式)。在本例中,我们将创建一个包含两列(
column1和column2)的表。第一列将是STRING类型,第二列将是INTEGER类型。第一列不能包含缺失值,而第二列可以:schema = [ SchemaField('column1', 'STRING', mode='REQUIRED'), SchemaField('column2', 'INTEGER', mode='NULLABLE'), # Add more fields as needed ] -
让我们检查是否存在同名的表。如果表不存在,则使用提供的名称创建它:
if not table_exists: table = bigquery.Table(table_ref, schema=schema) client.create_table(table) -
让我们创建一些将被插入到表中的模拟数据:
rows_to_insert = [ ('value1', 1), ('value2', 2), ('value3', 3) ] -
构建要插入的数据:
data_to_insert = [dict(zip([field.name for field in schema], row)) for row in rows_to_insert] -
插入数据并检查是否有错误。如果一切按预期工作,关闭连接:
errors = client.insert_rows(table, data_to_insert)如果发生任何插入错误,打印出错误信息:
print(f"Errors occurred during data insertion: {errors}") -
关闭 BigQuery 客户端:
client.close()
容器和表有什么区别?
容器(在不同系统中有不同的名称,如数据库、数据集或模式)是一种逻辑分组机制,用于组织和管理数据对象,例如表、视图和相关的元数据。容器提供了一种基于特定需求(如访问控制、数据治理或数据域的逻辑分离)分区和结构化数据的方式。另一方面,表是一种基本的数据结构,用于存储实际的数据记录,按行和列组织。表定义了模式(列名称和数据类型),并存储数据值。
现在,我们从了解数据仓库环境的基本组件转向讨论数据仓库在高效管理和分析大量数据时所提供的优缺点。
数据仓库的优缺点
让我们总结一下使用数据仓库的优缺点。
优点如下:
-
针对分析查询和大规模数据处理进行了优化
-
可以处理海量数据
-
与其他数据工具和服务的集成
下面是它们的缺点:
-
相比传统数据库,存储和查询的成本更高
-
可能在实时数据处理方面存在限制
数据仓库中的文件类型
就文件格式而言,可以准确地说,许多现代数据仓库使用专有的内部存储格式来写入数据。这些专有格式通常是列式存储格式,针对高效查询和分析进行了优化。
让我们来看看这些专有格式可能带来的差异:
-
数据仓库通常使用如 Parquet、ORC 或 Avro 等列式存储格式。虽然这些格式是开放且广泛采用的,但每个数据仓库可能有其内部优化或扩展。
-
这些列式存储格式的实际实现可能具有供应商特定的优化或功能。例如,某个特定数据仓库如何处理压缩、索引和元数据可能是特定于该供应商的。
-
用户通过标准接口与数据仓库进行交互,例如 SQL 查询。存储格式的选择通常不会影响用户,只要数据仓库支持常见的数据交换格式用于导入和导出。
因此,尽管内部存储机制可能有供应商特定的优化,但使用公认的、开放的且广泛采用的列式存储格式,确保了一定程度的互操作性和灵活性。用户通常使用标准 SQL 查询或数据交换格式(如 CSV、JSON 或 Avro)与数据仓库交互进行数据的导入/导出,这为这些系统的外部接口提供了一层标准化。
从传统数据仓库过渡到数据湖代表了一种战略转变,拥抱更灵活和可扩展的范式。在接下来的章节中,我们将深入探讨数据湖。
数据湖
从传统的数据仓库到数据湖的过渡,代表了组织处理和分析数据方式的转变。传统的数据仓库设计用于存储结构化数据,这些数据高度组织,并根据预定义的模式格式化,例如关系数据库中的表格和行列。结构化数据便于使用 SQL 进行查询和分析。然而,数据仓库在处理非结构化数据时遇到困难,非结构化数据没有预定义的格式或组织方式。非结构化数据的例子包括文本文件、电子邮件、图像、视频和社交媒体帖子。而数据湖则提供了一种更灵活、可扩展的解决方案,通过存储结构化和非结构化数据的原始格式,使组织能够摄取和存储大量数据,而无需立即进行结构化。这一过渡解决了数据仓库的局限性,提供了一种更具多样性和未来可扩展性的数据管理方法。
数据湖概述
数据湖是一个集中式存储库,允许组织存储大量原始和非结构化数据,且可以存储任何所需格式的数据。它们被设计用来处理多种数据类型,如结构化、半结构化和非结构化数据,并支持数据探索、分析和机器学习。数据湖解决了多个问题:它们使得不同数据源能够统一存储,打破了信息孤岛,并通过提供易于访问的所有类型数据支持高级分析。
请记住
可以将数据湖视为文件系统,就像在你的笔记本电脑上存储数据位置一样,不过规模要大得多。
数据湖解决方案
以下是数据空间中可用的数据湖解决方案的摘要:
| 数据湖 | 描述 |
|---|---|
| Amazon S3 | 亚马逊网络服务 (AWS) 提供的基于云的对象存储服务,通常作为数据湖的基础设施。 |
| Azure 数据湖存储 | 微软 Azure 提供的可扩展且安全的基于云的存储解决方案,旨在支持大数据分析和数据湖。 |
| Hadoop 分布式文件 系统 (HDFS) | 作为 Apache Hadoop 的存储层,HDFS 是一个分布式文件系统,Hadoop 是一个开源的大数据处理框架。 |
| Google Cloud Storage | Google Cloud 提供的对象存储服务,通常作为 GCP 中数据湖架构的一部分使用。 |
表 7.3 – 数据湖解决方案
从传统的数据仓库到数据湖的转变,代表了组织管理和分析数据方式的根本性变化。这一转变是由多个因素推动的,包括对更大灵活性、可扩展性以及处理多样化和非结构化数据类型的需求。下表突出显示了传统数据仓库和数据湖之间的主要区别:
| 数据仓库 | 数据湖 | |
|---|---|---|
| 数据种类和灵活性 | 传统的数据仓库设计用于处理结构化数据,对于处理多样化数据类型或非结构化数据的适应性较差。 | 数据湖的出现是为了应对数据量和数据种类的快速增长。它们为原始、非结构化和多样化的数据类型提供存储库,允许组织存储大量数据,而无需预定义的模式。 |
| 可扩展性 | 传统的数据仓库在处理海量数据时通常面临可扩展性挑战。扩展数据仓库可能会很昂贵,并且可能存在限制。 | 数据湖,尤其是基于云的解决方案,提供可扩展的存储和计算资源。它们可以有效地水平扩展,以应对日益增长的数据集和处理需求。 |
| 数据仓库 | 数据湖 | |
| 成本效益 | 传统的数据仓库在扩展时可能成本高昂,并且其成本结构可能不适合存储大量原始或结构较差的数据。 | 基于云的数据湖通常采用按需付费模式,允许组织通过按使用的资源付费来更有效地管理成本。这对于存储大量原始数据特别有利。 |
| 写时模式与读时模式 | 遵循写时模式(schema-on-write),数据在加载到仓库之前被结构化和转换。 | 遵循读时模式(schema-on-read),允许存储原始、未经转换的数据。模式在数据分析时应用,提供了更多的数据探索灵活性。 |
表 7.4 – 数据仓库与数据湖
Lakehouse 架构的出现通过解决数据湖相关的关键挑战,并将传统上与数据仓库相关的特性引入数据湖环境,从而进一步完善了摆脱数据仓库的转变。以下是这一演变的关键方面概述:
-
Lakehouse 将 ACID 事务集成到数据湖中,提供了传统上与数据仓库相关的事务处理能力。这确保了数据的一致性和可靠性。
-
Lakehouse 支持模式演变,允许随时间对数据模式进行更改,而无需对现有数据进行完全转换。这提高了灵活性,并减少了模式更改对现有流程的影响。
-
Lakehouse 引入了管理数据质量的特性,包括模式强制执行和约束,确保存储在数据湖中的数据符合指定的标准。
-
Lakehouse 旨在通过结合数据湖和数据仓库的优势,提供一个统一的分析平台。它允许组织在一个集中式存储库中对结构化和半结构化数据进行分析。
-
Lakehouse 增强了元数据目录,提供了数据血缘、质量和转换的全面视图。这有助于更好的治理和对湖中数据的理解。
Lakehouse 概念通过数据和分析社区的讨论不断发展,多个公司为 Lakehouse 原则的发展和采用做出了贡献。
数据湖示例
让我们探索一个如何在 S3 上写入 Parquet 文件的示例,S3 是 AWS 的云存储。要设置一切,访问 AWS 文档:docs.aws.amazon.com/code-library/latest/ug/python_3_s3_code_examples.html。现在,按照以下步骤进行操作:
注意
要运行此示例,您需要拥有一个 AWS 账户并准备好 S3 存储桶。
-
我们将开始导入所需的库:
import pandas as pd import pyarrow.parquet as pq import boto3 from io import BytesIO -
现在,我们将创建一些模拟数据,以便将其写入 S3:
data = {'Name': ['Alice', 'Bob', 'Charlie'], 'Age': [25, 30, 22], 'City': ['New York', 'San Francisco', 'Los Angeles']} df = pd.DataFrame(data) -
接下来,我们必须将 DataFrame 转换为 Parquet 格式:
parquet_buffer = BytesIO() pq.write_table(pq.Table.from_pandas(df), parquet_buffer) -
更新您的认证密钥和我们将要写入数据的存储桶名称:
aws_access_key_id = 'YOUR_ACCESS_KEY_ID' aws_secret_access_key = 'YOUR_SECRET_ACCESS_KEY' bucket_name = 'your-s3-bucket' file_key = 'example_data.parquet' # The key (path) of the file in S3 -
使用前一步的连接信息连接到 S3 存储桶:
s3 = boto3.client('s3', aws_access_key_id=aws_access_key_id, aws_secret_access_key=aws_secret_access_key) -
将 Parquet 文件上传到 S3:
s3.put_object(Body=parquet_buffer.getvalue(), Bucket=bucket_name, Key=file_key)
如前所述,Lakehouse 具有自己的优缺点。
其优势如下:
-
Lakehouse 提供了一个统一的平台,结合了数据湖和数据仓库的优势。这使得组织能够在一个环境中同时利用数据湖的灵活性和数据仓库的事务能力。
-
Lakehouse 遵循 schema-on-read 方法,允许存储原始的、未转换的数据。
-
Lakehouse 支持多种数据类型,包括结构化、半结构化和非结构化数据。
-
Lakehouse 集成了 ACID 事务,提供了事务能力,确保数据的一致性和可靠性。这对于数据完整性至关重要的使用场景尤为重要。
-
许多 Lakehouse 解决方案提供时间旅行功能,允许用户在特定时间点查询数据。数据的版本控制提供了历史背景并支持审计要求。
-
Lakehouse 通常实现优化的存储格式(例如 Delta 和 Iceberg),这有助于提高存储效率和查询性能,特别是对于大规模分析工作负载。
其缺点如下:
-
用户和管理员可能需要适应一种新的数据处理方式,同时考虑 schema-on-read 和 schema-on-write 模式。这可能需要培训和教育。
-
根据实现和云服务提供商的不同,存储、处理和管理 Lakehouse 架构中数据的成本可能会有所不同。组织需要仔细管理成本,以确保效率。
正如我们所讨论的,Lakehouse 拥有一个惊人的优势——它可以让任何类型的数据从结构化到半结构化再到非结构化数据都可以被摄取和存储。这意味着在摄取过程中我们可以看到任何文件类型,从 CSV 文件到 Parquet 和 Avro 文件。而在写入部分,我们可以利用 Lakehouse 提供的灵活性,将数据存储为优化过的开放表格文件格式。开放表格格式是一种用于存储表格数据的文件格式,能够让数据在各种数据处理和分析工具之间轻松访问和互操作。
数据湖中的文件类型
在 Lakehouse 架构中,我们有三种突出格式:Delta、Apache Iceberg 和 Apache Hudi。这些格式提供了 ACID 事务、模式演变、增量数据处理以及读写优化等特性。以下是这些格式的简要概述:
-
Delta Lake 是一个开源存储层,旨在提高数据湖中数据处理的可靠性和性能。它非常适合在 S3 或 Azure 存储等基础设施上构建数据湖,并且对 ACID 事务和数据版本控制有很强的支持。
-
Apache Iceberg 是另一种开源表格格式,专为快速查询性能优化。当需要查询效率时,它是一个不错的选择,并且它对模式演变和版本控制有出色的支持。
-
Apache Hudi(Hadoop 更新、删除和增量处理)是另一种开源数据湖存储格式,它为实时数据处理和流处理特性提供了很好的支持。尽管它可能不像 Delta Lake 或 Apache Iceberg 那样广为人知,但在 Apache Spark 和 Hadoop 生态系统中,Hudi 正在逐渐获得关注。
一般来说,这些格式都是为了解决相同的挑战而构建的,这也是它们有许多共同特性的原因。因此,在选择最适合你工作负载的格式之前,有几个因素你需要考虑,以确保你走在正确的方向:
-
考虑每种技术与现有数据处理生态系统和工具的兼容性。
-
评估每种技术在数据社区中的社区支持、持续开发和采用程度。
-
评估每种技术在特定使用案例中的性能特征,尤其是在读写操作方面。
最终,Delta Lake、Apache Iceberg 和 Apache Hudi 之间的选择应该由你的数据湖或湖仓环境的具体需求和优先事项驱动。通过实验和基准测试每个解决方案,结合你的数据和工作负载,可以做出更明智的决策。
我们将要讨论的最后一种接收器技术是流数据接收器。
流数据接收器
从批处理和微批处理到流处理技术的过渡,标志着数据处理和分析的重大进步。批处理涉及在预定的时间间隔内收集和处理大量离散数据,这可能导致数据可用性和洞察力的延迟。微批处理通过在更频繁的时间间隔内处理较小的批次来改进这一点,减少了延迟,但仍未实现实时数据处理。而流处理技术则能够实现数据的实时摄取和处理,使组织能够在数据到达时立即进行分析并采取行动。这一向流处理技术的转变,解决了当今快速变化的商业环境中对实时分析和决策的日益增长的需求,为数据管理提供了更加动态和响应迅速的方法。
流数据接收端概述
流数据接收端是实时消费和存储流数据的组件或服务。它们作为流数据被摄取、处理并持久化以供进一步分析或检索的终端。以下是流数据接收端及其主要组件的概述:
-
摄取组件:它负责接收和接纳传入的数据流
-
处理逻辑:这是一种定制的逻辑,可能包括数据丰富、转化或聚合的组件
-
存储组件:它用于持久化流数据,便于未来分析或检索
-
连接器:它们的主要作用是与各种数据处理或存储系统进行交互
我们通常在以下领域实施流数据接收端:
-
在实时分析系统中,允许组织在事件发生时实时获取数据洞察。
-
在系统监控中,流数据接收端捕获和处理实时指标、日志或事件,从而实现对问题或异常的即时告警和响应。
-
在金融交易或电子商务中,流数据接收端可用于通过分析交易数据中的模式和异常实现实时欺诈检测。
-
在物联网(IoT)场景中,流数据接收端处理来自传感器和设备的持续数据流,支持实时监控和控制。
现在,让我们来看看可用于流数据接收端的可选项。
流数据接收端解决方案
许多云平台提供托管的流数据服务,这些服务充当数据接收端,如亚马逊 Kinesis、Azure Event Hubs 和 Google Cloud Dataflow,如下表所示:
| 流数据接收端 | 描述 |
|---|---|
| 亚马逊 Kinesis | AWS 中用于实时流处理的完全托管服务。它支持数据流、分析和存储。 |
| Azure Event Hub | Azure 中用于处理和分析流数据的基于云的实时分析服务。 |
| Google Cloud Dataflow | GCP 上的完全托管流处理和批处理服务。 |
| Apache Kafka | 一个分布式流平台,既可以作为流数据的源,也可以作为流数据的接收器。 |
| Apache Spark Streaming | 一个实时数据处理框架,是 Apache Spark 生态系统的一部分。 |
| Apache Flink | 一种流处理框架,支持事件时间处理和各种接收器连接器。 |
表 7.5 – 不同的流数据服务
在下一节中,我们将使用最流行的流数据接收器之一 Kafka,了解在流接收器中写入数据的过程。
流数据接收器示例
首先,让我们对 Apache Kafka 的主要组件有一个初步了解:
-
代理是构成 Kafka 集群的核心服务器。它们处理消息的存储和管理。每个代理都有一个唯一的 ID。代理负责在集群中复制数据,以确保容错能力。
-
主题是 Kafka 中用于组织和分类消息的主要抽象。它们就像数据库中的表或文件系统中的文件夹。消息被发布到特定的主题,并从这些主题中读取。主题可以进行分区,以实现可扩展性和并行处理。
-
分区是 Kafka 中的并行单元。每个主题被划分为一个或多个分区,这样可以实现数据的分布式存储和处理。分区中的消息是有序且不可变的。
-
生产者是将消息发布(写入)到 Kafka 主题的客户端应用程序。它们可以选择将消息发送到哪个分区,或使用分区策略。生产者负责序列化、压缩数据,并在各个分区之间进行负载均衡。
-
消费者是从 Kafka 主题中订阅(读取)消息的客户端应用程序。它们可以从主题的一个或多个分区读取消息,并跟踪它们已经消费的消息。
-
ZooKeeper用于管理和协调 Kafka 代理。它维护关于 Kafka 集群的元数据。Kafka 的新版本正逐步去除对 ZooKeeper 的依赖。
现在我们已经对 Kafka 的主要组件有了更好的理解,接下来我们将开始我们的逐步指南。为了完成这个示例,我们需要安装几个组件,请跟随我一起完成整个过程。为了简化这个过程,我们将使用 Docker,因为它可以让你通过 docker-compose.yml 文件定义整个环境,轻松地设置 Kafka 和 ZooKeeper,并进行最小的配置。这避免了手动在本地机器上安装和配置每个组件的需要。请按照以下步骤操作:
-
按照公共文档下载 Docker:
docs.docker.com/desktop/install/mac-install/。 -
接下来,使用 Docker 设置 Kafka。为此,让我们查看位于
github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter07/setup/docker-compose.yml的docker-compose.yml文件。 -
此 Docker Compose 配置使用 Docker Compose 文件格式的第 3 版设置了一个简单的 Kafka 和 Zookeeper 环境。该配置定义了两个服务:
zookeeper和kafka。 -
Zookeeper 使用
confluentinc/cp-zookeeper:latest镜像。它将主机机器的端口2181映射到容器的端口2181,用于客户端连接。ZOOKEEPER_CLIENT_PORT环境变量设置为2181,指定 Zookeeper 将侦听客户端请求的端口:version: '3' services: zookeeper: image: confluentinc/cp-zookeeper:latest ports: - "2181:2181" environment: ZOOKEEPER_CLIENT_PORT: 2181 -
Kafka 使用
confluentinc/cp-kafka:latest镜像。它将主机机器的端口9092映射到容器的端口9092,用于外部客户端连接:kafka: image: confluentinc/cp-kafka:latest ports: - "9092:9092" environment: KAFKA_BROKER_ID: 1 KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092 KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1这里有一些配置 Kafka 的关键环境变量:
-
KAFKA_BROKER_ID设置为1,在 Kafka 集群中唯一标识此代理。 -
KAFKA_ZOOKEEPER_CONNECT指向 Zookeeper 服务 (zookeeper:2181),允许 Kafka 连接到 Zookeeper 管理集群元数据。 -
KAFKA_ADVERTISED_LISTENERS广告两个监听器:-
PLAINTEXT://kafka:29092用于 Docker 内部网络通信。 -
PLAINTEXT_HOST://localhost:9092用于来自 Docker 网络外部的连接(例如,来自主机机器)
-
-
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP确保两个公布的监听器使用PLAINTEXT协议,意味着没有加密或认证。 -
KAFKA_INTER_BROKER_LISTENER_NAME设置为PLAINTEXT,指定 Kafka 代理之间将使用哪个监听器进行通信。 -
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR设置为1,表示偏移主题(用于存储消费者组偏移量)不会跨多个代理复制,这在单代理设置中很典型。
这种设置非常适合本地开发或测试,您需要一个简单的单节点 Kafka 环境,而无需多节点生产级集群的复杂性。现在,我们准备运行容器。
-
-
让我们运行 Docker 容器以启动 Kafka 和 Zookeeper。在您的终端中,输入以下命令:
docker-compose up –d这将获取 Kafka 和 Zookeeper 镜像,并将它们安装在您的环境中。您应该在终端上看到类似以下的输出:
[+] Running 3/3 ✔ Network setup_default Created 0.0s ✔ Container setup-kafka-1 Started 0.7s ✔ Container setup-zookeeper-1 Started 0.6s
Kafka 生产者
现在,让我们回到 Python IDE,看看如何将数据推送到 Kafka 生产者。为此,我们将从 MongoDB 读取数据,并将其传递到 Kafka。你可以在此处找到代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter07/4a.kafka_producer.py。让我们开始吧:
-
首先,让我们导入所需的库:
from pymongo import MongoClient from confluent_kafka import Producer import json -
接下来,定义 MongoDB 连接:
mongo_client = MongoClient('mongodb://localhost:27017') db = mongo_client['no_sql_db'] collection = db['best_collection_ever']在这里,
MongoClient('mongodb://localhost:27017')连接到本地运行的 MongoDB 实例,默认端口为27017。这将创建一个客户端对象,允许与数据库进行交互。然后,db = mongo_client['no_sql_db']从 MongoDB 实例中选择no_sql_db数据库。最后,collection = db['best_collection_ever']从no_sql_db数据库中选择best_collection_ever集合。 -
让我们进行 Kafka 生产者配置,创建一个带有指定配置的 Kafka 生产者对象。该生产者将用于将消息(在此案例中为 MongoDB 文档)发送到 Kafka 主题:
kafka_config = { 'bootstrap.servers': 'localhost:9092' } producer = Producer(kafka_config) -
以下函数是一个回调函数,当 Kafka 生产者完成发送消息时将被调用。它检查消息传递过程中是否出现错误,并打印指示成功或失败的消息。此函数提供了有关消息是否成功发送到 Kafka 的反馈,对于调试和监控非常有用:
def delivery_report(err, msg): if err is not None: print(f'Message delivery failed: {err}') else: print(f'Message delivered to {msg.topic()} [{msg.partition()}]') -
从 MongoDB 读取并将文档通过
collection.find()发送到 Kafka:message = json.dumps(document, default=str) producer.produce('mongodb_topic', alue=message.encode('utf-8'), callback=delivery_report) producer.poll(0)前面的代码遍历了
best_collection_ever集合中的每个文档。find()方法从集合中检索所有文档。然后,message = json.dumps(document, default=str)将每个 MongoDB 文档(一个 Python 字典)转换为 JSON 字符串。default=str参数通过将无法序列化为 JSON 的数据类型转换为字符串,处理这些数据类型。接下来,producer.produce('mongodb_topic', value=message.encode('utf-8'), callback=delivery_report)将 JSON 字符串作为消息发送到mongodb_topicKafka 主题。消息采用 UTF-8 编码,delivery_report函数作为回调函数,用于处理投递确认。最后,producer.poll(0)确保 Kafka 生产者处理投递报告和其他事件。这是保持生产者活跃和响应所必需的。 -
这确保生产者队列中的所有消息在脚本退出前都被发送到 Kafka。如果没有这一步骤,可能会有未发送的消息残留在队列中:
producer.flush()
运行此脚本后,你应该会看到以下打印语句:
Message delivered to mongodb_topic [0]
Message delivered to mongodb_topic [0]
Message delivered to mongodb_topic [0]
Message delivered to mongodb_topic [0]
Message delivered to mongodb_topic [0]
Message delivered to mongodb_topic [0]
Message delivered to mongodb_topic [0]
Message delivered to mongodb_topic [0]
到目前为止,我们已经连接到 MongoDB 数据库,读取了集合中的文档,并将这些文档作为消息发送到 Kafka 主题。
Kafka 消费者
接下来,让我们运行消费者,以便它能够从 Kafka 生产者消费消息。完整代码可以在 github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter07/4b.kafka_consumer.py 找到:
-
让我们先导入所需的库:
from confluent_kafka import Consumer, KafkaError import json import time -
接下来,我们必须创建 Kafka 消费者配置,指定要连接的 Kafka broker。这里,它连接到运行在本地主机上的 Kafka broker,端口为
9092。在这种情况下,group.id设置消费者组 ID,这使得多个消费者能够协调并共享处理来自主题消息的工作。消息将在同一组的消费者之间分发。接下来,auto.offset.reset定义了在 Kafka 中没有初始偏移量或当前偏移量不存在时的行为。将此设置为earliest表示消费者将从主题中最早的可用消息开始读取:consumer_config = { 'bootstrap.servers': 'localhost:9092', 'group.id': 'mongodb_consumer_group', 'auto.offset.reset': 'earliest' } -
现在,我们将实例化一个 Kafka 消费者,并使用之前指定的配置。这里,
consumer.subscribe(['mongodb_topic'])将消费者订阅到mongodb_topicKafka 主题。这意味着消费者将接收来自此主题的消息:consumer = Consumer(consumer_config) consumer.subscribe(['mongodb_topic']) -
设置消费者运行的时长(以秒为单位):
run_duration = 10 # For example, 10 seconds start_time = time.time() print("Starting consumer...") -
以下代码开始一个无限循环,直到显式中断。这个循环不断地轮询 Kafka 是否有新消息。这里,
if time.time() - start_time > run_duration检查消费者是否已经运行超过了指定的run_duration。如果是,它会打印一条消息并退出循环,停止消费者:while True: if time.time() - start_time > run_duration: print("Time limit reached, shutting down consumer.") break msg = consumer.poll(1.0) if msg is None: continue if msg.error(): if msg.error().code() == KafkaError._PARTITION_EOF: print('Reached end of partition') else: print(f'Error: {msg.error()}') else: document = json.loads(msg.value().decode('utf-8')) print(f'Received document: {document}') consumer.close() print("Consumer closed.")
运行上述代码后,您应该会看到以下打印输出:
Starting consumer...
Received document: {'_id': '66d833ec27bc08e40e0537b4', 'name': 'Alice', 'age': 25}
Received document: {'_id': '66d833ec27bc08e40e0537b5', 'name': 'Bob', 'age': 30}
Received document: {'_id': '66d833ec27bc08e40e0537b6', 'name': 'Charlie', 'age': 22}
Received document: {'_id': '66d835aa1798a2275cecaba8', 'name': 'Alice', 'age': 25, 'email': 'alice@example.com'}
Received document: {'_id': '66d835aa1798a2275cecaba9', 'name': 'Bob', 'age': 30, 'address': '123 Main St'}
Received document: {'_id': '66d835aa1798a2275cecabaa', 'name': 'Charlie', 'age': 22, 'hobbies': ['reading', 'gaming']}
Received document: {'_id': '66d835aa1798a2275cecabab', 'name': 'David', 'age': 40, 'email': 'david@example.com', 'address': '456 Elm St', 'active': True}
Received document: {'_id': '66d835aa1798a2275cecabac', 'name': 'Eve', 'age': 35, 'email': 'eve@example.com', 'phone': '555-1234'}
本示例的目标是向您展示如何从 MongoDB 等 NoSQL 数据库中持续读取数据,并通过 Kafka 实时流式传输到其他系统。Kafka 充当消息系统,允许数据生产者(例如 MongoDB)与数据消费者解耦。此示例还说明了如何分阶段处理数据,从而实现可扩展和灵活的数据管道。
从实际应用场景来看,假设我们正在构建一个拼车应用。实时处理事件,例如乘车请求、取消请求和司机状态,对于高效地将乘客与司机匹配至关重要。MongoDB 存储这些事件数据,如乘车请求和司机位置,而 Kafka 将事件流式传输到各种微服务。这些微服务随后处理这些事件以做出决策,例如将司机分配给乘客。通过使用 Kafka,系统变得高度响应、可扩展且具备弹性,因为它将事件生产者(例如乘车请求)与消费者(例如司机分配逻辑)解耦。
总结我们迄今为止所看到的内容,与涉及具有定义模式的结构化数据的关系型接收端不同,Kafka 可以作为数据摄取的缓冲区或中介,允许解耦和可扩展的数据管道。NoSQL 接收端通常处理非结构化或半结构化数据,类似于 Kafka 对消息格式的灵活性。Kafka 处理高吞吐量数据流的能力与 NoSQL 数据库的可扩展性和灵活性相辅相成。
为了清理到目前为止使用的所有资源,请执行清理脚本:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter07/setup/cleanup_script.sh。
在下一部分,我们将深入探讨流式数据接收端所看到的文件格式。
流式数据接收端的文件类型
流式数据接收端主要处理消息或事件,而不是传统的文件存储。通过流式数据接收端传输的数据通常采用 JSON、Avro 或二进制等格式。这些格式在流式场景中被广泛用于数据的序列化和编码。它们高效并且支持模式演化。在本章的NoSQL 数据库部分,我们深入探讨了 JSON 文件格式。在这里,我们将讨论 Avro 和二进制格式。
Apache Avro 是一个在 Apache Hadoop 项目中开发的二进制序列化格式。它使用模式来定义数据结构,从而实现高效的序列化和反序列化。Avro 以其紧凑的二进制表示而著称,提供快速的序列化和高效的存储。在流式场景中,最小化数据大小对于高效的网络传输至关重要。Avro 的紧凑二进制格式减少了数据大小,提升了带宽利用率。Avro 还支持模式演化,允许数据结构随时间变化而不需要同时更新所有组件。Avro 的基于模式的方法使得不同系统和语言之间能够实现互操作性,适用于多样的生态系统。我们来看一个 Avro 文件示例:
{
"type": "record",
"name": "SensorData",
"fields": [
{"name": "sensor_id", "type": "int"},
{"name": "timestamp", "type": "long"},
{"name": "value", "type": "float"},
{"name": "status", "type": "string"}
]
}
二进制格式使用紧凑的二进制数据表示方式,从而实现高效的存储和传输。可以根据具体需求使用各种二进制协议,例如 Google 的协议缓冲区(protobuf)或 Apache Thrift。二进制格式通过最小化传输数据的大小,在流式场景中减少带宽使用。二进制序列化和反序列化通常比基于文本的格式更快,这在高吞吐量流式环境中至关重要。我们来看一个 protobuf 的二进制文件示例:
syntax = "proto3";
message SensorData {
int32 sensor_id = 1;
int64 timestamp = 2;
float value = 3;
string status = 4;
}
在流式数据接收端,选择 JSON、Avro 还是二进制格式取决于流式使用场景的具体需求,包括互操作性、模式演化和数据大小等因素。
到目前为止,我们已经讨论了数据工程师和数据科学家使用的最常见数据存储系统,以及我们通常会遇到的不同文件类型。在接下来的章节中,我们将总结所有讨论过的数据存储系统和文件类型,以及它们的优缺点和最佳使用时机。
哪种数据存储系统最适合我的使用场景?
让我们总结一下关于不同数据存储系统的知识,并深入理解何时使用哪种系统:
| 技术 | 优点 | 缺点 | 何时选择 | 使用场景 |
|---|---|---|---|---|
| 关系型数据库 | ACID 属性确保数据一致性。成熟的查询语言(SQL)支持复杂查询。支持复杂的事务和连接操作。 | 对于读密集型工作负载的可扩展性有限。架构更改可能具有挑战性,并容易导致停机。可能无法水平扩展。 | 具有明确定义架构的结构化数据。需要维护数据实体之间关系的情况。 | 事务应用程序。处理结构化数据的企业应用程序。 |
| NoSQL 数据库 | 灵活的架构,适用于半结构化或非结构化数据。可扩展性——水平扩展通常更容易。某些工作负载下具有高写入吞吐量。 | 缺乏标准化的查询语言,可能需要学习特定的 API。可能缺乏 ACID 一致性,而偏向最终一致性。对复杂事务的支持有限。 | 动态或不断变化的数据架构。快速开发和迭代。处理结构各异的大量数据。 | 用于内容管理的文档数据库。具有可变架构的实时应用程序。用于 Web 应用程序的 JSON 数据存储。 |
| 数据仓库 | 针对复杂分析和报表优化。高效的数据压缩和索引。适用于读密集型分析工作负载的可扩展性。 | 对于高吞吐量事务工作负载可能成本较高。实时查询可能存在较高延迟。可能需要专门的技能来维护和优化。 | 对大数据集进行分析处理。聚合和分析历史数据。 | 商业智能和报表工具。在 TB 级数据上运行复杂查询。 |
| 技术 | 优点 | 缺点 | 何时选择 | 使用场景 |
| Lakehouse | 结合了数据湖和数据仓库特征的统一平台。提供可扩展的存储和计算资源。可以高效地横向扩展,以应对不断增长的数据集和处理需求。按需付费模式,使组织能够通过为所用资源付费来更有效地管理成本。这对于存储大量原始数据尤其有利。采用按需读取模式,允许存储未经转换的原始数据。 | 管理按需读取和按需写入模式的复杂性。根据实现方式和云服务提供商,Lakehouse 架构中存储、处理和管理数据的成本可能有所不同。组织需要仔细管理成本,以确保高效性。 | 在灵活性和事务能力之间的平衡。 | 实时分析与长期存储。任何工程、机器学习和分析用例 |
| 流数据存储 | 实现实时处理和流数据分析。水平扩展以处理大量的输入数据。构建事件驱动架构的核心组成部分。 | 实现和管理流数据存储可能很复杂。流数据的处理和持久化会引入一些延迟。根据所选解决方案,基础设施成本可能是一个考虑因素。 | 实时持续摄取和处理数据 | 物联网、实时分析应用场景、系统监控 |
表 7.6 – 所有数据存储的总结表,包括它们的优缺点和使用场景
在表 7.8中,用例列提供了更多的背景信息和实际示例,说明如何在实际场景中有效地应用每种数据存储技术。
从选择合适的数据存储技术到选择适当的文件类型,这是设计有效数据处理管道的关键步骤。一旦确定了数据存储的位置(数据存储),接下来就需要考虑数据如何存储(文件类型)。文件类型的选择会影响数据存储效率、查询性能、数据完整性以及与其他系统的互操作性。
解码文件类型以实现最佳使用
在选择数据存储时,选择合适的文件类型对优化数据存储、处理和检索至关重要。我们至今未讨论过的一种文件类型,但它非常重要,因为它作为其他文件格式的底层格式使用,那就是 Parquet 文件。
Parquet 是一种列式存储文件格式,旨在大数据和分析环境中实现高效的数据存储和处理。它是一个开放标准文件格式,提供高压缩比、列式存储和对复杂数据结构的支持等优点。Parquet 在 Apache Hadoop 生态系统中得到了广泛应用,并且被各种数据处理框架所支持。
Parquet 以列式格式存储数据,这意味着来自同一列的值会一起存储。这种设计对于分析工作负载非常有利,尤其是查询通常涉及选择一部分列时。Parquet 还支持多种压缩算法,用户可以选择最适合自己需求的算法,从而减少存储空间并提升查询性能。Parquet 文件还能处理模式演化,使得可以在不完全重写数据集的情况下添加或删除列。这一特性在数据模式演化的场景中尤为重要。由于其诸多优势,Parquet 已成为大数据生态系统中广泛采用并标准化的文件格式,为 Delta 和 Iceberg 等其他优化格式奠定了基础。
在讨论完 Parquet 文件后,我们现在可以比较常见的文件类型,以及它们的优缺点,并为不同数据存储提供选择建议:
| 文件类型 | 优点 | 缺点 | 何时选择 |
|---|---|---|---|
| JSON | 人类可读 | 与二进制格式相比文件较大,序列化/反序列化速度较慢 | 需要半结构化或人类可读数据 |
| BSON | 紧凑的二进制格式,支持更丰富的数据类型 | 可能不像 JSON 那样易于人类阅读,并且在 MongoDB 以外的地方采用有限 | 存储和传输效率至关重要时选择 |
| Parquet | 列式存储,适合分析,压缩和编码减少文件大小 | 不像 JSON 那样易于人类阅读,不能直接更新表格 – 需要重写 | 分析处理、数据仓储 |
| Avro | 紧凑的二进制序列化基于模式,支持模式演化,跨不同系统互操作 | 与 JSON 相比,稍微不那么易于人类阅读 | 带宽高效的流处理和多语言支持 |
| Delta | 提供 ACID 事务以确保数据一致性,适用于数据湖的高效存储格式,支持模式演化和时间旅行查询 | 文件比 Parquet 大 | 实时分析与长期存储 |
| Hudi | 高效的增量数据处理,支持 ACID 事务以实现实时数据 | 文件比 Parquet 大 | 流数据应用和变更数据捕获 |
| Iceberg | 支持模式演化、ACID 事务,优化存储格式如 Parquet | 文件比 Parquet 大 | 时间旅行查询和模式演化 |
| 文件类型 | 优点 | 缺点 | 何时选择 |
| 二进制格式 | 紧凑且高效的存储,快速序列化和反序列化 | 不易于人类阅读,支持的模式演化有限 | 在带宽使用和处理速度方面对效率要求高时选择 |
表 7.7 – 所有文件格式的汇总表,包括它们的优缺点和使用案例
在下一节中,我们将讨论分区,这是数据存储中一个重要的概念,尤其是在分布式存储系统的上下文中。尽管分区这一概念与数据湖、数据仓库和分布式文件系统密切相关,但它在更广泛的数据存储讨论中也具有重要意义。
导航分区
数据分区是一种技术,用于将大型数据集划分并组织成更小、更易管理的子集,称为分区。当将数据写入存储系统时,例如数据库或分布式存储系统,采用适当的数据分区策略对优化查询性能、数据检索和存储效率至关重要。数据存储系统中的分区,包括基于时间、地理位置和混合分区,在读操作、更新和写操作方面提供了多个好处:
-
在查询数据时,分区使得系统能够迅速跳过无关数据。例如,在基于时间的分区中,如果你只关心某一特定日期的数据,系统可以直接访问与该日期对应的分区,从而提高查询速度。它确保只扫描必要的分区,减少了需要处理的数据量。
-
分区可以简化更新,尤其是当更新集中在特定分区时。例如,如果你需要更新特定日期或地区的数据,系统可以隔离受影响的分区,从而减少更新操作的范围。
-
分区可以提高写操作的效率,特别是在附加数据时。新数据可以写入适当的分区,而不影响现有数据,从而使写入过程更简单、更快速。
-
分区支持并行处理。不同的分区可以并行读取或写入,从而更好地利用资源并加快整体处理速度。
-
分区提供了数据的逻辑组织方式。它简化了数据管理任务,如归档旧数据、删除过时记录或根据访问模式将特定分区迁移到不同的存储层次。
-
使用分区,你可以根据使用模式优化存储。例如,频繁访问的分区可以存储在高性能存储中,而不常访问的分区则可以存储在低成本存储中。
-
分区支持修剪,系统可以在查询执行过程中排除整个分区的考虑。这种修剪机制进一步加速了查询性能。
让我们深入了解不同的分区策略。
水平分区与垂直分区
在讨论数据库或分布式系统中的分区策略时,我们通常提到两种主要类型:水平分区和垂直分区。每种方法以不同的方式组织数据,以提高性能、可扩展性或可管理性。让我们先从水平分区开始。
水平分区,也称为分片,涉及将表的行分割成多个分区,每个分区包含数据的一个子集。这种方法通常用于通过将数据分布到多个服务器上来扩展数据库,其中每个分片保持相同的模式,但包含不同的行。例如,一个大型应用中的用户表可以根据用户 ID 进行分片,ID 1 到 10,000 在一个分区中,ID 10,001 到 20,000 在另一个分区中。这种策略使得系统能够处理比单台机器更大的数据集,从而提升大规模应用中的性能。
另一方面,垂直分区涉及将表的列分割成不同的分区,每个分区包含一部分列,但包含所有行。当不同的列以不同的频率被访问或更新时,这种策略是有效的,因为它通过最小化查询过程中处理的数据量来优化性能。例如,在用户资料表中,基本信息如姓名和电子邮件可以存储在一个分区中,而像用户头像这样的二进制大数据列则存储在另一个分区中。这使得针对特定列的查询可以访问更小、更高效的数据集,从而提升性能。
这两种策略可以结合使用,以满足数据库系统的特定需求,具体取决于数据结构和访问模式。实际上,在数据领域,水平分区比垂直分区更常见和广泛采用。这在需要处理大量数据、高流量或地理分散的用户的大规模分布式数据库和应用程序中特别如此。在接下来的部分,我们将看到一些水平分区的例子。
基于时间的分区
基于时间的分区涉及根据时间戳来组织数据。每个分区代表一个特定的时间区间,例如一天、一小时或一分钟。它有助于高效地检索历史数据和进行基于时间的聚合操作,同时还便于实施数据保留和归档策略。
在这个例子中,你将学习如何使用 Parquet 文件在本地笔记本电脑上创建基于时间的分区。你可以在这里找到完整的代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter07/5.time_based_partitioning.py。按照以下步骤操作:
-
导入所需的库:
import os import pandas as pd import pyarrow as pa import pyarrow.parquet as pq from datetime import datetime -
定义一个示例数据集,包含两列:
timestamp和value。该数据集表示带有时间戳和相应值的时间序列数据:data = { "timestamp": ["2022-01-01", "2022-01-01", "2022-01-02"], "value": [10, 15, 12] } -
创建一个 pandas DataFrame:
df = pd.DataFrame(data) -
将
timestamp列转换为datetime类型。这确保了时间戳作为日期时间对象进行处理,以便进行准确的基于时间的操作:df["timestamp"] = pd.to_datetime(df["timestamp"]) -
更新路径以存储数据。使用现有路径:
base_path = " path_to_write_data" -
遍历 DataFrame,通过
timestamp列的date部分对行进行分组。将每个组转换为 PyArrow 表,并将其写入相应的分区路径,以 Parquet 格式存储:for timestamp, group in df.groupby(df["timestamp"].dt.date): -
如果目录不存在,创建该目录:
os.makedirs(base_path, exist_ok=True) partition_path = os.path.join(base_path, str(timestamp)) table = pa.Table.from_pandas(group) pq.write_table(table, partition_path)
执行此脚本后,你将在基础目录中看到两个 Parquet 文件被创建——每周的每一天对应一个文件:

图 7.6 – 基于时间的分区输出
让我们来看一下另一种常见的分区策略,称为地理分区(geographic partitioning)。
地理分区
地理分区涉及根据地理属性(如地区、国家或城市)对数据进行划分。这种策略在处理地理空间数据或基于位置的分析时非常有价值。它能够快速、精准地检索与特定地理区域相关的数据,从而支持空间查询和分析。
这是一个示例,展示了如何在本地笔记本电脑上使用 Parquet 文件创建基于地理位置的分区。你可以在这里找到完整的代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter07/6.geo_partitioning.py。按照以下步骤操作:
-
创建一个用于存储分区数据的基础目录:
base_directory = "/geo_data" os.makedirs(base_directory, exist_ok=True) -
将每个组(特定地区的数据)转换为 PyArrow 表。然后,将表写入相应的路径:
geo_data = {"region": ["North", "South", "East"], "value": [10, 15, 12]} geo_df = pd.DataFrame(geo_data) for region, group in geo_df.groupby("region"): -
在基础目录中为每个区域创建一个目录:
region_path = os.path.join(base_directory, region) -
将组转换为 PyArrow 表,并将其写入分区路径:
table = pa.Table.from_pandas(group) pq.write_table(table, region_path) -
执行此脚本后,你会在基础目录中看到三个 Parquet 文件被创建——每个文件对应数据中一个地理位置:

图 7.7 – 基于地理位置的分区输出
让我们来看一下最后一种常见的分区策略,称为混合分区。
注意
地理分区是一种基于类别分区的专门形式,它根据地理属性或空间标准来组织数据。类别分区是数据组织中的基本策略,它涉及根据特定的类别或属性对数据进行分组,例如客户人口统计、产品类型或交易特征。
混合分区
混合分区是指将多种分区策略结合起来,以优化特定使用场景的数据组织。例如,您可以先按时间进行分区,然后再根据某个键或地理位置进一步对每个时间段进行分区。它为处理复杂的查询模式和多样化的数据访问需求提供了灵活性。
下面是一个如何在本地笔记本电脑上使用 Parquet 文件创建混合分区的示例。您可以在这里找到完整的代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter07/7.hybrid_partitioning.py。请按照以下步骤操作:
-
创建一个基础目录来存储分区数据:
base_directory = "/hybrid_data" -
执行混合分区:
hybrid_data = { "timestamp": ["2022-01-01", "2022-01-01", "2022-01-02"], "region": ["North", "South", "East"], "value": [10, 15, 12]} hybrid_df = pd.DataFrame(hybrid_data) for (timestamp, region), group in hybrid_df.groupby( ["timestamp", "region"]): -
在基础目录中为每个时间戳和地区组合创建一个目录:
timestamp_path = os.path.join(base_directory, str(timestamp)) os.makedirs(timestamp_path, exist_ok=True) timestamp_region_path = os.path.join( base_directory, str(timestamp), str(region)) -
将分组转换为 PyArrow 表并写入分区路径:
table = pa.Table.from_pandas(group) pq.write_table(table, timestamp_region_path) -
执行此脚本后,您将在基础目录中看到创建了三个 Parquet 文件——两个是 2022 年 1 月 1 日的文件,一个是 2022 年 1 月 2 日的文件:

图 7.8 – 混合分区输出
请记住
到目前为止,我们已经探讨了多种分区类型,例如基于时间和地理位置的分区。然而,请记住,您可以根据数据、使用场景以及表的查询模式,选择任何有意义的列作为分区列。
现在我们已经讨论了不同的分区策略,接下来我们要讨论如何选择用于分区的数据列。
选择分区策略的考虑因素
为数据选择合适的分区策略需要考虑多种因素,以优化性能、查询效率和数据管理。以下是选择分区策略时的一些关键考虑因素:
-
查询模式:根据您的应用程序或分析平台最常执行的查询类型来选择分区策略。
-
数据分布:确保分区均匀分布,以防止数据热点和资源争用。
-
数据大小:考虑每个分区中存储的数据量。较小的分区可以提高查询性能,但过多的小分区可能会增加管理开销。
-
查询复杂性:某些查询可能会从混合分区中受益,特别是那些涉及多个属性的查询。
-
可扩展性:分区应支持未来的可扩展性,并能够随着时间推移适应数据增长。
数据分区是一个关键的架构决策,它可能会显著影响数据处理管道的效率和性能。通过采用适当的数据分区策略,可以确保数据按照查询模式进行组织,从而最大化所选数据接收技术的好处。
在接下来的部分,我们将通过描述一个实际案例并逐步讲解所有定义与数据接收和文件类型相关的最佳策略,将本章所学内容付诸实践。
设计一个在线零售数据平台
一家在线零售商希望创建一个分析平台,用于收集和分析其电子商务网站生成的所有数据。该平台旨在提供实时数据处理和分析能力,以改善客户体验、优化业务运营并推动在线零售业务的战略决策。
在与团队的长时间讨论后,我们确定了四个主要需求需要考虑:
-
处理大量交易数据:平台需要高效地摄取和转换大量交易数据。这需要考虑可扩展性、高吞吐量和成本效益。
-
提供实时洞察:业务分析师需要即时访问从交易数据中得出的实时洞察。平台应支持实时数据处理和分析,以支持及时决策。
-
需要结合批处理和流数据摄取,以处理实时网站数据和慢速更新的批量客户数据。
-
使用 AWS 作为云服务提供商:选择 AWS 作为云服务提供商,源于该零售商目前正在使用其他 AWS 服务,且希望继续使用同一提供商。
让我们快速看一下如何解决这些需求:
-
选择合适的数据接收技术:
-
思考过程:由于 Lakehouse 架构能够处理大规模数据,具备可扩展性、高吞吐量和成本效益,因此它是满足数据平台需求的理想解决方案。它利用分布式存储和计算资源,实现高效的数据摄取和转换。此外,该架构支持实时数据处理和分析,使业务分析师能够即时访问交易数据,以便及时决策。通过结合批处理和流数据摄取,Lakehouse 可以将实时网站数据与批量更新的客户数据无缝集成。
-
选择:选择 AWS 上的 Lakehouse 解决方案,因为其具有可扩展性、成本效益,并能够与其他 AWS 服务无缝集成。AWS 与 Lakehouse 架构兼容。
-
-
评估并选择数据文件格式:
-
数据特征:客户数据包括结构化的交易记录,包括客户 ID、产品 ID、购买金额、时间戳和地理位置。流数据包括客户 ID 和其他网站指标,例如每个客户当前在网站上浏览的内容。
-
选择:选择 Delta 文件格式是因为其事务能力和 ACID 合规性。它还支持批量和流式工作负载。
-
-
实现批量和流数据的摄取:
-
数据摄取:ETL 过程旨在将传入的交易数据转换为 Delta 文件。实时交易数据通过 AWS Kinesis 进行流式传输,以便即时处理,并作为 Delta 文件存储,同时来自不同系统的批量数据也被整合。
-
分区逻辑:批量和流数据正在处理并存储为 Delta 文件。流数据在写入时按日期进行分区。接下来,进行转换和数据整合,然后将其存储为最终的分析表。
-
-
为分析表定义分区策略:
-
查询模式:分析师通常基于特定时间段或基于产品类别的某些表进行数据查询。
-
选择:正如我们在选择分区策略的考虑因素一节中所学,我们需要考虑用户查询表的方式。为了获得最佳的查询读取性能,必须实现基于时间和类别的分区。在用户经常查询的分析表中,数据按日期分区,并进一步按产品类别进行分区。
-
-
监控与优化:
-
性能监控:定期使用 AWS 监控和日志服务监控查询性能、流式传输吞吐量和资源利用率。
-
优化:根据观察到的性能和变化的数据模式,持续优化批量和流组件。
-
架构演变:确保 Delta 架构能够适应流数据的变化,并与现有的批量数据保持兼容。
-
通过这种架构,在线零售分析平台能够以有效且具有成本优化的方式处理批量和实时数据。
总结
在本章中,我们重点讨论了数据写入操作的设计和优化组件。我们讨论了如何选择合适的数据接收技术,文件格式如何显著影响存储效率和查询性能,以及为什么选择正确的文件格式对你的使用案例至关重要。最后,我们讨论了为什么数据分区对于优化查询性能和资源利用率至关重要。
在下一章中,我们将开始转换已写入数据接收器的数据,通过检测和处理异常值和缺失值,为下游分析做好准备。
第二部分:下游数据清洗——消费结构化数据
本部分深入探讨了为分析准备结构化数据所需的清理过程,重点处理更精细数据集中常见的数据挑战。它提供了处理缺失值和离群值的实用技巧,通过归一化和标准化确保数据的一致性,并有效处理类别特征。此外,还介绍了处理时间序列数据的专业方法,这是一种常见但复杂的数据类型。通过掌握这些下游清理和准备技巧,读者将能够将结构化数据转化为可操作的见解,供高级分析使用。
本部分包含以下章节:
-
第八章**,检测和处理缺失值与离群值
-
第九章**,归一化与标准化
-
第十章**,处理类别特征
-
第十一章**,处理时间序列数据
第八章:检测和处理缺失值与离群值
本章讨论了处理缺失值和离群值的技术,这两个问题是数据分析中两个关键挑战,可能会显著影响我们数据产品的完整性和准确性。我们将探讨从统计方法到先进机器学习模型的广泛技术,以识别和管理这些数据异常。通过实践示例和真实数据集,我们将提出应对这些问题的策略,确保我们的分析具有稳健性、可靠性,并能够生成有意义的洞察。
本章的关键点如下:
-
检测和处理缺失数据
-
检测单变量和多变量离群值
-
处理单变量和多变量离群值
技术要求
你可以在以下链接中找到本章的所有代码:
github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/tree/main/chapter08
不同的代码文件对应章节的不同部分。让我们安装以下库:
pip install spacy==3.7.5
检测缺失数据
缺失数据是现实世界数据集中的常见且不可避免的问题。它发生在某个观察或记录中缺少一个或多个值。这种数据缺失可能会严重影响任何基于这些数据构建的分析或模型的有效性和可靠性。正如我们在数据领域所说:垃圾进,垃圾出,意味着如果你的数据不正确,那么基于这些数据创建的模型或分析也不会正确。
在接下来的部分中,我们将通过一个场景来演示如何检测缺失数据以及不同的填补方法是如何工作的。这个场景如下:
假设你正在分析一个包含学生信息的数据集,包括他们的年龄和测试分数。然而,由于各种原因,一些年龄和测试分数 是缺失的。
在这个脚本中,我们创建了整个章节中将使用的数据。让我们从导入语句开始:
import pandas as pd
让我们生成一些缺失年龄和测试分数的学生数据。这个字典数据包含两个键,Age 和 Test_Score,每个键都有一个值列表。其中一些值为 None,表示缺失的数据:
data = {
'Age': [18, 20, None, 22, 21, 19, None, 23, 18, 24, 40, 41, 45, None, 34, None, 25, 30, 32, 24, 35, 38, 76, 90],
'Test_Score': [85, None, 90, 92, None, 88, 94, 91, None, 87, 75, 78, 80, None, 74, 20, 50, 68, None, 58, 48, 59, 10, 5]}
df = pd.DataFrame(data)
数据集的前五行如下:
Age Test_Score
0 18.0 85.0
1 20.0 NaN
2 NaN 90.0
3 22.0 92.0
4 21.0 NaN
正如我们所看到的,数据集的两列中都有 NaN 值。为了了解数据集中缺失值的程度,让我们统计一下整个 DataFrame 中有多少缺失值:
missing_values = df.isnull()
df.isnull() 方法会创建一个与 df 形状相同的 missing_values DataFrame,其中每个单元格如果对应的 df 单元格是 None(缺失值),则为 True,否则为 False,如图所示:
Age Test_Score
0 False False
1 False True
2 True False
3 False False
4 False True
在之前的 DataFrame 中,任何包含 NaN 值的单元格现在都被替换为 True。以这种格式存储数据有助于我们计算出有多少个 NaN 值:
null_rows_count = missing_values.any(axis=1).sum()
print("Count of Rows with at least one Missing Value:", null_rows_count)
print(8/len(df))
missing_values.any(axis=1) 参数检查每一行是否包含缺失值,返回一个 True 或 False 的 Series 来表示每一行。然后 .sum() 统计这个 Series 中 True 的个数,从而得出至少有一个缺失值的行数:
Count of Rows with at least one Missing Value: 8
% of rows with at least one missing value: 33%
现在我们知道数据集中缺失了多少数据。这个练习的下一个目标是找到最好的填补方法来补充这些缺失值。
处理缺失数据
处理缺失数据涉及做出谨慎的决策,以最小化其对分析和模型的影响。最常见的策略包括以下几种:
-
移除包含缺失值的记录
-
使用各种技术来填补缺失值,比如均值、中位数、众数填补,或更先进的方法,如基于回归的填补或 k-近邻填补
-
引入二进制指示变量来标记缺失数据;这可以告诉模型哪些值是缺失的
-
利用主题领域的专业知识来理解缺失数据的原因,并做出有关如何处理缺失值的明智决策
让我们深入研究这些方法,并详细观察它们在前面部分中展示的数据集上的结果。
删除缺失数据
处理缺失数据的一种方法是简单地删除包含缺失值的记录(行)。这是一种快捷且简单的策略,通常在缺失数据的百分比较低且缺失数据随机分布时更为合适。
在开始删除数据之前,我们需要更好地了解我们的数据集。继续使用之前示例中的数据,我们先打印描述性统计信息,再开始删除数据点。该部分的代码可以在 github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter08/2.delete_missing_data.py 找到。
注意
为了使本章的篇幅适中,我们仅展示了关键的代码片段。要查看所有示例,请访问仓库。
为了生成描述性统计,我们可以直接调用 pandas 中的 .describe() 方法:
print(df.describe())
这里展示了描述性统计信息:
Age Test_Score
count 20.000000 19.000000
mean 33.750000 65.894737
std 18.903843 27.989869
min 18.000000 5.000000
25% 21.750000 54.000000
50% 27.500000 75.000000
75% 38.500000 87.500000
max 90.000000 94.000000
让我们也为数据集的每一列创建分布图。

图 8.1 – 变动前特征的分布
完成此分析后,我们可以获得一些关于数据集的关键见解。对于年龄,数据量为 20,平均年龄大约为 33.7 岁,标准差为 18.9 岁,显示出中等程度的变异性。年龄范围从 18 岁到 90 岁,年龄的中间 50%落在 21.75 岁到 38.5 岁之间。对于测试分数,基于 19 个值,均值约为 65.8,标准差为 27.9,显示出较高的变异性。测试分数范围从 5 到 94 分,四分位差(IQR)从 54 到 87.5。
现在,让我们看看如何删除缺失数据。让我们关注数据集的变化:
df_no_missing = df.dropna()
让我们探索数据删除后的特征分布:

图 8.2 – 数据删除后的特征分布
让我们再看一下修改后的数据集的总结统计:
print(df_no_missing.describe())
Age Test_Score
count 16.000000 16.000000
mean 36.500000 65.500000
std 20.109699 26.610775
min 18.000000 5.000000
25% 23.750000 56.000000
50% 32.000000 74.500000
75% 40.250000 85.500000
max 90.000000 92.000000
看到两个数据集的描述性统计后,观察到的变化如下:
-
计数变化:删除缺失值的行后,年龄和测试分数的观测数量都从 20 减少到 16。
-
均值变化:平均年龄从 33.75 增加到 36.50,而平均测试分数略微下降,从 65.89 降至 65.50。这一变化反映了删除数据后剩余数据集中的值。
-
标准差变化:年龄的标准差从 18.90 增加到 20.11,表明年龄的分布范围更广,而测试分数的标准差则从 27.99 降至 26.61。
-
最小值和最大值:最小年龄保持不变,仍为 18 岁,而最小测试分数保持为 5 分。年龄和测试分数的最大值都有轻微变化,测试分数的最大值从 94 降至 92。
-
百分位变化:由于数据集的变化,百分位值(25%、50%、75%)发生了变化:
-
年龄的第 25 百分位从 21.75 增加到 23.75,测试分数的第 25 百分位从 54.00 增加到 56.00。
-
年龄的中位数(第 50 百分位)从 27.50 增加到 32.00,而测试分数的中位数从 75.00 略微下降到 74.50。
-
年龄的第 75 百分位从 38.50 增加到 40.25,而测试分数的第 75 百分位从 87.50 降至 85.50。
-
删除缺失值的行导致数据集变小,剩余数据现在具有不同的统计特性。当缺失值占数据集的比例较小且删除它们对数据没有显著影响时,这种方法是适用的。
那么,什么算是一个小比例呢?
一个常见的经验法则是,如果数据缺失少于 5%,通常认为缺失比例较小,删除这些数据可能不会对分析产生重大影响。通过比较有缺失数据和无缺失数据的分析结果,可以评估删除数据所造成的变化的显著性。如果结果一致,删除可能就不那么重要。
在这些缺失数据较为严重的情况下,我们将在下一部分探讨其他填充方法或更高级的技术,可能会更为适用。
缺失数据的填充
填充通常用于当删除缺失记录会导致显著信息丢失的情况。填充是指用估算或计算出的值替代缺失值。常见的填充方法包括均值填充、中位数填充和众数填充,或使用更高级的技术。
让我们来看看针对我们场景的不同填充方法。
均值填充
均值填充将缺失值替换为观察到的值的均值。这是一种非常简单的方法,当缺失值完全随机时,它不会引入偏差。然而,该方法对异常值敏感,并且可能会扭曲特征的分布。你可以在这个链接找到相关代码。
让我们看看均值填充的代码示例。在这个示例中,我们将使用之前解释过的相同数据集:
df_mean_imputed = df.copy()
df_mean_imputed['Age'].fillna(round(df['Age'].mean()), inplace=True)
前一行将Age列中的任何缺失值填充为原始df数据框中Age列的均值。df['Age'].mean()参数计算Age列的均值,并将该均值四舍五入到最接近的整数。然后,fillna()方法使用这个四舍五入后的均值替换Age列中的任何NaN值。inplace=True参数确保更改直接在df_mean_imputed中进行,而不会创建新的数据框。
df_mean_imputed['Test_Score'].fillna(df['Test_Score'].mean(), inplace=True)
同样,前一行将df_mean_imputed中Test_Score列的任何缺失值填充为原始df数据框中Test_Score列的均值。
让我们来看一下填充后的数据集:
print(df_mean_imputed)
Age Test_Score
0 18.0 85.000000
1 20.0 65.894737
2 34.0 90.000000
3 22.0 92.000000
4 21.0 65.894737
5 19.0 88.000000
6 34.0 94.000000
7 23.0 91.000000
8 18.0 65.894737
9 24.0 87.000000
10 40.0 75.000000
如我们所见,四舍五入后的均值已替代了年龄特征中的所有NaN值,而绝对均值(abs mean)则替代了Test_Score列中的NaN值。我们对Age列的均值进行了四舍五入,以确保它表示的是有意义的内容。
这里展示的是更新后的分布:

图 8.3 – 均值填充后的特征分布
从图表中可以看到,两个变量的分布都有些微变化。让我们来看看填补数据集的描述性统计:
print(df_mean_imputed.describe())
Age Test_Score
count 24.000000 24.000000
mean 33.791667 65.894737
std 17.181839 24.761286
min 18.000000 5.000000
25% 22.750000 58.750000
50% 33.000000 66.947368
75% 35.750000 85.500000
max 90.000000 94.000000
在查看了两个数据集的描述性统计后,观察到的变化如下:
-
Age和Test_Score在填补后从 20 增加到 24,表示缺失值已经成功填补。 -
均值和中位数的变化:均值年龄保持稳定,略微从 33.75 增加到 33.79。均值测试分数保持在 65.89 不变。中位数年龄从 27.50 增加到 33.00,反映了年龄分布的变化。中位数测试分数略微从 75.00 下降到 66.95。
-
Age从 18.90 下降到 17.18,表明填补后的年龄变异性减小。Test_Score的标准差也从 27.99 下降到 24.76,反映出测试分数的变异性减少。 -
Age从 21.75 增加到 22.75,Test_Score的 Q1 从 54.00 增加到 58.75。Age从 38.50 略微下降到 35.75,而Test_Score的 Q3 则保持相对稳定,从 87.50 略微下降到 85.50。
均值填补保持了整体均值,并通过填补缺失值增加了数据集的大小。然而,它减少了变异性(如Age和Test_Score的标准差减少所示),并改变了数据的分布(尤其是在四分位数上)。这些变化是均值填补的典型特征,因为它倾向于低估变异性并平滑数据中的差异,这可能会影响某些对数据分布敏感的分析。
现在,让我们继续进行中位数填补,看看它如何影响数据集。
中位数填补
中位数填补通过填补缺失值为数据集的中位数,即将数据按顺序排列后的中间值。中位数填补在存在离群值时更为稳健,且在数据分布偏斜时是一个不错的选择。它能够保持分布的形状,除非遇到复杂的分布。相关代码可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter08/4.median_imputation.py找到。
让我们看一下中位数填补的代码示例:
df_median_imputed = df.copy()
以下代码行将df_median_imputed数据框中Age列的缺失值填补为原始df数据框中Age列的中位数。df['Age'].median()参数计算Age列的中位数(即中间值)。然后,fillna()方法将Age列中的任何NaN值替换为这个中位数。inplace=True参数确保更改直接应用到df_median_imputed中,而不创建新的数据框:
df_median_imputed['Age'].fillna(df['Age'].median(), inplace=True)
同样,以下行填充了 Test_Score 中的任何缺失值:
df_median_imputed['Test_Score'].fillna(df['Test_Score'].median(), inplace=True)
让我们来看看经过中位数填充后的数据集:
print(df_median_imputed)
Age Test_Score
0 18.0 85.0
1 20.0 75.0
2 27.5 90.0
3 22.0 92.0
4 21.0 75.0
5 19.0 88.0
6 27.5 94.0
7 23.0 91.0
8 18.0 75.0
9 24.0 87.0
10 40.0 75.0
如我们所见,中位数填充已替换了 Age 特征(27.5)和 Test_Score 列(75)中的所有 NaN 值。更新后的分布如下。

图 8.4 – 中位数填充后的特征分布
我们从图表中可以看到,这两个变量的分布略有变化。让我们看看填充后数据集的描述性统计:
print(df_median_imputed.describe())
Age Test_Score
count 24.000000 24.000000
mean 32.708333 67.791667
std 17.345540 25.047744
min 18.000000 5.000000
25% 22.750000 58.750000
50% 27.500000 75.000000
75% 35.750000 85.500000
max 90.000000 94.000000
在查看了两个数据集的描述性统计后,观察到的变化在这里呈现:
-
Age和Test_Score在经过中位数填充后分别从 20(年龄)和 19(测试分数)增加到 24,表明缺失值已成功填充。 -
均值变化:填充后,均值年龄从 33.75 降低到 32.71,均值测试分数略微增加,从 65.89 增加到 67.79。这些变化反映了填充后数据的特性。
-
Age从 18.90 降低到 17.35,表明年龄的变异性有所减小。Test_Score的标准差也从 27.99 降低到 25.05,反映出在填充后测试成绩的变异性较小。 -
Age从 21.75 稍微增加到 22.75,而Test_Score的 Q1 从 54.00 增加到 58.75。Age的 Q3(75%)从 38.50 降低到 35.75,Test_Score的 Q3 也略微减少,从 87.50 降低到 85.50。 -
Age保持稳定在 27.50,而Test_Score的中位数也保持在 75.00,突出了填充后数据的中央趋势得到了保持。
中位数填充成功地填补了缺失值,同时保持了 Age 和 Test_Score 的中位数。这导致均值发生了轻微变化并减少了变异性,这是中位数填充的典型特征。中央趋势(中位数)得到了保持,这是中位数填充的一个重要优势,特别是在偏斜分布的情况下。但它也减少了数据的分布,这对某些类型的分析可能具有影响。
在接下来的部分,我们将使用到目前为止学到的关于填充的内容。我们还将增加一个额外的步骤,即标记数据集中缺失值所在的位置,供后续参考。
创建指示变量
指标变量补全,也叫标志变量或虚拟变量补全,涉及创建一个二进制指标变量,标记某个观测值在特定变量中是否缺失。这个单独的虚拟变量在缺失值时取值为 1,在观察到的值时取值为 0。当缺失值存在某种模式时,指标变量补全很有用,它能帮助你明确建模并捕捉缺失值的情况。记住,我们是在添加一个全新的变量,创建了一个更高维的数据集。在创建完指标变量后,它们的作用是提醒我们哪些值是被补全的,哪些值不是,然后我们可以使用任意方法(例如中位数或均值)补全数据集。
让我们看看这种补全方法的代码示例。和往常一样,你可以在仓库中看到完整的代码:
另外,记住我们在整章中使用的是完全相同的数据框,因此这里省略了数据框的创建部分:
df['Age_missing'] = df['Age'].isnull().astype(int)
df['Test_Score_missing'] = df['Test_Score'].isnull().astype(int)
这段代码在 df 数据框中创建了新列,用以指示 Age 和 Test_Score 列中是否有缺失值(NaN)。df['Age'].isnull() 检查 Age 列中的每个值是否为 NaN(缺失)。它返回一个布尔型的序列,其中 True 表示缺失值,False 表示非缺失值。.astype(int) 方法将布尔型序列转换为整数型序列,True 变为 1(表示缺失值),False 变为 0(表示非缺失值)。df['Age_missing'] 数据框将这个整数序列存储在一个名为 Age_missing 的新列中。
类似地,df['Test_Score_missing'] 是用来指示 Test_Score 列中的缺失值:
df_imputed['Age'].fillna(df_imputed['Age'].mean(), inplace=True)
df_imputed['Test_Score'].fillna(df_imputed['Test_Score'].mean(), inplace=True)
这段代码将 df_imputed 数据框中 Age 和 Test_Score 列中的缺失值填充为各自列的均值,就像我们在前一部分学习的那样。让我们看看经过指标变量补全后的数据集:
print(df_imputed)
Age Test_Score Age_missing Test_Score_missing
0 18.00 85.000000 0 0
1 20.00 65.894737 0 1
2 33.75 90.000000 1 0
3 22.00 92.000000 0 0
4 21.00 65.894737 0 1
5 19.00 88.000000 0 0
6 33.75 94.000000 1 0
7 23.00 91.000000 0 0
8 18.00 65.894737 0 1
9 24.00 87.000000 0 0
10 40.00 75.000000 0 0
从补全后的数据集可以看出,我们添加了两个指标变量(Age_missing 和 Test_Score_missing),如果对应的变量缺失,则其值为 1,否则为 0。所以,我们主要标记了哪些原始行的值 是被补全的。
让我们看看指标变量的分布情况:

图 8.5 – 指标变量的分布
现在,让我们通过构建一些箱型图来探索指标变量与数据集中其他特征之间的关系:
import seaborn as sns
import matplotlib.pyplot as plt
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
sns.boxplot(x='Age_missing', y='Test_Score', data=df_imputed)
plt.title("Boxplot of Test_Score by Age_missing")
plt.subplot(1, 2, 2)
sns.boxplot(x='Test_Score_missing', y='Age', data=df_imputed)
plt.title("Boxplot of Age by Test_Score_missing")
plt.tight_layout()
plt.show()
创建的箱型图可以在图 8.6中看到:

图 8.6 – 箱型图比较指示变量与其他特征之间的关系
提示 – 如何读取箱型图
箱体范围:箱型图中的箱体表示四分位距(IQR),其中包含数据的中心 50%。箱内的值被视为典型值或正常范围内的值。
胡须:胡须从箱体延伸,显示典型值的范围。异常值通常定义为超出某一倍数(例如 1.5 倍)四分位距(IQR)的值。
异常值:超出胡须之外的个别数据点被视为潜在的异常值。异常值通常以单独的点或星号表示。
疑似异常值:有时,位于胡须之外的点可能被标记为疑似异常值,单独标记以表明它们是潜在的异常值,但并非极端值。
回到我们的例子,Test_Score 按 Age Missing 的箱型图显示,当数据中的年龄缺失时,Test_Score 的均值大约为 80,分布值介于 55 到 85 之间。当 Age 不缺失时,均值大约为 65,大部分值集中在 60 和 80 之间,少数异常值集中在 20 附近。现在,当分数缺失时,学生的平均年龄约为 20,而有分数的学生的平均年龄约为 35。
注意
在构建预测模型时,将指示变量作为附加特征以捕捉缺失值对目标变量的影响。评估包含和不包含指示变量的模型表现,以评估它们的贡献。
插补方法的比较
以下表格提供了根据数据特征和任务目标选择合适插补方法的指南。
记住,没有一种方法适用于所有情况!
| 插补方法 | 使用场景 | 优点 | 缺点 |
|---|---|---|---|
| 均值插补 | 正态分布数据,缺失值为 MCAR 或 MAR | 简单易行,保留分布的均值 | 对异常值敏感,若缺失不是随机的,可能扭曲分布 |
| 中位数插补 | 偏斜或非正态分布数据,存在异常值 | 对异常值具有鲁棒性,保留分布的中位数 | 忽略变量之间的潜在关系,可能对非偏斜数据精度较低 |
| 指示变量插补 | 缺失数据中的系统性模式 | 捕捉缺失模式 | 增加维度性 假设缺失模式有意义,但这并不总是成立 |
| 删除行 | MCAR 或 MAR 缺失机制,存在异常值 | 保留现有数据结构,当缺失是随机时有效 | 减少样本量,如果缺失不是完全随机,可能导致偏倚结果 |
表 8.1 – 各种填补方法的比较
在提供的示例中,我们一致地对数据集的每一列应用了相同的填补方法。然而,正如我们所展示的那样,我们的分析和考虑是针对每一列单独量身定制的。这意味着我们可以根据每一列的具体特征和需求来定制填补策略。作为一个实际练习,花些时间尝试为数据集中的不同列使用不同的填补方法,并观察这些选择如何影响你的结果。
为了在我们已经建立的填补策略基础上进一步发展,必须认识到数据清理不仅仅是处理缺失值。数据预处理的另一个关键方面是识别和管理离群值。在接下来的部分,我们将深入探讨如何检测和处理离群值,确保我们的数据集尽可能准确和可靠。
检测和处理离群值
离群值是指在数据集中与大多数数据点显示的总体模式或趋势显著偏离的数据点。它们位于数据分布中心异常远的位置,并且可能对统计分析、可视化和模型性能产生重大影响。定义离群值包括识别那些不符合数据预期行为的数据点,并理解它们发生的背景。
离群值的影响
离群值虽然通常只占数据集的一小部分,但它们对数据集的影响不成比例,可能会破坏数据集的完整性。它们的存在可能会扭曲统计总结、误导可视化,并对模型的性能产生负面影响。
让我们深入探讨离群值如何扭曲事实:
-
扭曲的统计汇总:离群值可能会显著扭曲统计汇总,给出数据中心趋势的误导性印象:
-
均值和中位数:均值作为一种常见的集中趋势测量,可能会受到离群值的极大影响。一个远高于或低于其他数据点的离群值可能会将均值拉向它。另一方面,中位数是通过排序数据集中的中间值来确定的。它有效地作为数据的中心点,将数据分为两等部分,因此不容易受到极端值的影响。
-
方差和标准差:离群值可能会膨胀方差和标准差,使数据看起来比实际更为分散。这可能会误导数据的大多数变异性。
-
-
误导性的可视化:离群值可能会扭曲可视化的尺度和形状,导致误解:
-
箱型图:离群值可能会导致箱型图过度延伸,使数据的大部分看起来被压缩。这可能会使分布看起来不如实际情况那样分散。
-
直方图:异常值可能导致创建仅包含少数极端值的区间,导致其他区间显得不成比例地小,且分布形态被扭曲。
-
-
对模型性能的影响:异常值可能会对预测模型的性能产生负面影响:
-
回归:异常值可能会严重影响回归线的斜率和截距,从而导致模型过度受到极端值的影响。
-
聚类:异常值可能会影响聚类的中心点和边界,可能导致创建无法准确表示数据分布的聚类。
-
异常值可以根据维度分为单变量异常值和多变量异常值。在下一节中,我们将使用第一部分中的示例,看看如何处理单变量异常值。
识别单变量异常值
单变量异常值发生在单个变量中观察到极端值时,与其他变量的值无关。它们基于单个变量的分布进行检测,通常使用可视化或统计方法(如 Z 分数或四分位距)来识别。
在下一部分中,我们将构建最常见的可视化图形之一,用于识别异常值。
识别异常值的经典可视化方法
在深入讨论识别异常值的统计方法之前,我们可以先创建一些简单的可视化图形来帮助识别它们。我们一直使用的数据示例仍然适用于这一部分,你可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter08/6.outliers_visualisation.py找到完整的代码。
我们从第一个可视化图——箱线图开始,其中异常值表现为箱须两侧的点。以下代码片段为每个变量创建箱线图:
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.title("Box Plot for 'Age'")
plt.boxplot(df['Age'].dropna(), vert=False)
plt.subplot(1, 2, 2)
plt.title("Box Plot for 'Test_Score'")
plt.boxplot(df['Test_Score'].dropna(), vert=False)
plt.tight_layout()
plt.show()
创建的箱线图如下所示:

图 8.7 – 箱线图用来识别异常值
在我们的示例中,我们可以看到Age特征有一些明显的异常值。
另一个经典的图形是小提琴图,如图 8.8所示。小提琴图是一种强大的可视化工具,结合了箱线图和核密度图的特点。要创建小提琴图,请运行以下代码片段:
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.title("Violin Plot for 'Age'")
plt.violinplot(df['Age'].dropna(), vert=False)
plt.subplot(1, 2, 2)
plt.title("Violin Plot for 'Test_Score'")
plt.violinplot(df['Test_Score'].dropna(), vert=False)
plt.tight_layout()
plt.show()
创建的小提琴图如下所示:

图 8.8 – 小提琴图用来识别异常值
提示 – 如何阅读小提琴图:
小提琴的宽度:小提琴的宽度表示数据在不同值处的密度。较宽的部分表示在特定值处数据点的密度较高,意味着该值在总体中出现的概率较高;而较窄的部分表示概率较低。
箱线图元素:在小提琴图内,你可能会看到一个类似于传统箱线图的箱线图。箱子表示 IQR(四分位距),而中位数通常以一条水平线显示在箱内。胡须从箱子延伸,表示数据的范围。
核密度估计(KDE):小提琴图的整体形状是 KDE 的镜像表示。KDE 提供了数据分布的平滑表示,帮助你观察数据的峰值和谷值。
异常值:异常值可能表现为超出胡须末端的点,或超出小提琴整体形状的点。
现在我们已经看过这些图表,开始对Age列中异常值的存在形成一些假设。下一步是使用一些统计方法验证这些假设,首先从 Z 分数方法开始。
Z 分数方法
Z 分数方法是一种统计技术,通过衡量单个数据点相对于均值的标准差偏离程度,用于识别数据集中的单变量异常值。数据点的 Z 分数使用以下公式计算:
Z = (X − Mean) / Standard Deviation
其中,X是数据点,Mean是数据集的平均值,Standard Deviation量化数据的离散程度。
通常,选择一个阈值 Z 分数来确定异常值。常用的阈值是Z > 3或Z < −3,表示偏离均值超过三个标准差的数据点被视为异常值。
让我们回到之前的代码示例,计算Age和Test_Score列的 Z 分数。我们将继续之前开始的示例。你可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter08/7.identify_univariate_outliers.py找到完整的代码。
让我们计算 Z 分数:
z_scores_age = np.abs(stats.zscore(df['Age'].dropna()))
stats.zscore(df['Age'].dropna())函数计算Age列的 Z 分数。Z 分数表示一个数据点距离均值多少个标准差。dropna()函数用于在计算 Z 分数之前排除NaN值:
z_scores_test_score = np.abs(stats.zscore(df['Test_Score'].dropna()))
np.abs()函数用于计算 Z 分数的绝对值。这是因为 Z 分数可以为负(表示值低于均值)或为正(表示值高于均值)。通过使用绝对值,我们只关注偏离均值的大小,而不考虑方向。
z_threshold = 3
outliers_age = np.where(z_scores_age > z_threshold)[0]
outliers_test_score = np.where(z_scores_test_score > z_threshold)[0]
np.where(z_scores_age > z_threshold)[0]识别年龄列中 Z-score 大于3的那些数据点的索引。最后的[0]用于提取索引作为数组。outliers_age和outliers_test_score变量分别存储年龄和测试成绩列中的异常值数据点索引。
如果我们绘制每个观察值和特征的 Z-scores,就可以开始发现一些异常值了。

图 8.9 – 使用 Z-score 进行异常值检测
在这些 Z-score 的散点图中,每个点代表一个数据点的 Z-score。红色虚线表示所选的 Z-score 阈值(在此案例中为3)。异常值被标识为高于此阈值的点。如我们所见,在年龄上,清晰地捕捉到了一个异常值。
如何选择合适的 Z-score 阈值?
Z-score 告诉你一个数据点距离均值有多少个标准差。在正态分布中,以下是成立的:
-
大约 68%的数据落在均值的一个标准差范围内。
-
大约 95%的数据落在两个 标准差内。
-
大约 99.7%的数据落在三个 标准差内。
这意味着3的 Z-score 阈值通常被使用,因为它捕捉到的是极度偏离均值的值,识别出最极端的异常值。在完美的正态分布中,只有 0.3%的数据点会有 Z-score 大于 3 或小于-3。这使得它成为检测不太可能属于正常数据分布的异常值的合理阈值。
现在,除了 Z-score,另一种常见的方法是 IQR,我们将在接下来的部分讨论这一方法。
IQR 方法
IQR 是统计离散度的一个衡量标准,表示数据集中 Q1 和 Q3 之间的范围。IQR 是一种稳健的离散度衡量方式,因为它对异常值的敏感性较低。此时,可以清楚地看出 IQR 是基于四分位数的。四分位数将数据集分为几个区间,由于 Q1 和 Q3 对极端值不那么敏感,因此 IQR 不容易受到异常值的影响。另一方面,标准差会受到每个数据点与均值偏差的影响。偏差较大的异常值会对标准差产生不成比例的影响。
提示 – 如何计算 IQR
计算 Q1(25 百分位数):确定数据中有 25%落在其下方的值。
计算 Q3(75 百分位数):确定数据中有 75%落在其下方的值。
计算 IQR:IQR = Q3 - Q1。
使用 IQR 识别潜在的异常值,请按以下步骤操作:
-
按照以下方式计算上下界:下界 = Q1 - 1.5 * IQR,上界 = Q3 + 1.5 * IQR。
-
任何低于或高于上下界的数据点都被视为潜在的异常值。
需要注意的是,乘数的选择(在本例中为1.5)是有些任意的,但在实际中已经广泛采用。调整这个乘数可以使得该方法对潜在离群值的敏感度更高或更低。例如,使用更大的乘数会导致边界更广,可能会识别出更多的潜在离群值,而较小的乘数则会使该方法对离群值的敏感度降低。
我们将使用之前的脚本,脚本可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter08/7.identify_univariate_outliers.py找到。让我们看看如何计算 IQR 并识别离群值:
def identify_outliers(column):
Q1 = df[column].quantile(0.25)
Q3 = df[column].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = df[(df[column] < lower_bound) | (df[column] > upper_bound)]
return outliers
这段代码定义了一个函数,用于通过 IQR 方法识别 DataFrame 中任何列的离群值。它计算 IQR,设定正常数据的上下限,然后过滤出那些列中的值落在这些边界之外的行。
然后,我们来识别并打印Age(年龄)列中的离群值:
age_outliers = identify_outliers('Age')
print("Outliers in 'Age':")
print(age_outliers)
识别并打印Test_Score(考试成绩)列中的离群值:
test_score_outliers = identify_outliers('Test_Score')
print("\nOutliers in 'Test_Score':")
print(test_score_outliers)
运行这段代码后,我们可以在打印语句中看到基于Age列识别出的离群值/行:
Age Test_Score
76.0 10.0
90.0 5.0
如前所述,IQR(四分位距)的简便性以及其对离群值的稳健性使其在各种分析场景中非常受欢迎。然而,它也有一定的缺点。一个限制是信息的丢失,因为 IQR 仅考虑数据集的中央 50%,忽略了整个范围。此外,IQR 对样本大小的敏感性,尤其是在较小的数据集里,可能会影响其反映数据真实分布的准确性。
最后,我们将简要讨论如何利用领域知识来识别离群值。
领域知识
为了更好地理解领域知识在离群值检测中的应用,我们以考试成绩为例。假设数据集代表的是学生的考试成绩,并且根据教育标准,考试成绩应该落在 0 到 100 的范围内。任何超出此范围的成绩都可以被认为是离群值。通过利用教育领域的知识,我们可以设定这些边界来识别潜在的离群值。例如,如果某个成绩记录为 120,那么它很可能会被标记为离群值,因为它超出了最高分 100 的范围。同样,负数的成绩或低于 0 的成绩也会被视为离群值。以这种方式整合领域知识,使我们能够为离群值检测设定有意义的阈值,确保分析符合教育领域中的预期规范。
处理单变量离群值
处理单变量异常值是指识别、评估和管理那些显著偏离数据集典型模式或分布的个别变量数据点的过程。其目的是减少这些极端值对数据产品的影响。
处理单变量异常值有几种方法。我们将从删除开始,始终使用本章开头的示例进行操作。
删除异常值
删除异常值是指从数据集中移除那些被认为异常极端或偏离数据整体模式的数据点。删除异常值有其利弊。一方面,这是处理极端值的最简单方法;另一方面,它会导致样本量的减少,并可能丧失宝贵的信息。此外,如果异常值不是错误数据,而是反映数据的合理波动,删除它们可能会引入偏差。
回到我们的示例,在使用均值填充缺失数据并计算 IQR 之后,我们删除了超过异常值阈值的异常值。让我们来看一下执行这些步骤的代码;你也可以在仓库中找到它:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter08/8.handle_univariate_outliers_deletions.py。
让我们计算 IQR 并利用它来设定正常数据范围的上下界限:
(IQR) Q1 = df['Test_Score'].quantile(0.25)
Q3 = df['Test_Score'].quantile(0.75)
IQR = Q3 - Q1
outlier_threshold = 1.5
让我们定义下限和上限异常值界限。任何超出此范围的值都将被标记为异常值:
lower_bound = Q1 - outlier_threshold * IQR
upper_bound = Q3 + outlier_threshold * IQR
最后一行过滤了 DataFrame(df),仅保留Test_Score值在计算出的下限和上限之间的行:
df_no_outliers = df[(df['Test_Score'] >= lower_bound) & (df['Test_Score'] <= upper_bound)].copy()
在以下图表中,我们可以看到删除异常值后的更新分布图:

图 8.10 – 删除异常值后的分布图
让我们看看删除异常值后的描述性统计数据:
Age Test_Score
count 22.000000 22.000000
mean 29.272727 71.203349
std 8.163839 17.794339
min 18.000000 20.000000
25% 22.250000 65.894737
50% 31.000000 71.000000
75% 33.937500 86.500000
max 45.000000 94.000000
删除异常值后观察到的变化如下所示:
-
平均年龄变化:删除异常值后,平均年龄从 33.75 略微下降至约 29.27。这一变化表明,删除的异常值是年龄较大的个体。
-
年龄标准差变化:年龄的标准差从 17.18 降至 8.16,表明删除异常值后年龄的分布略微变窄,可能是因为原数据中的异常值导致了较大的变异性。
-
最小和最大年龄值:最小年龄保持不变,仍为 18 岁,而最大年龄从 90 岁降至 45 岁,表明在处理异常值时,年龄较大的个体(潜在的异常值)被移除。
-
平均测试成绩变化:在删除异常值后,平均测试成绩从 65.89 轻微上升至 71.20,表明被删除的异常值是低分,拉低了原始的均值。
-
测试成绩的标准差变化:标准差从 24.76 降至 17.79,表明测试成绩的分布变得更为集中。
-
最低和最高测试成绩:最低测试成绩从 5.00 上升到 20.00,而最高测试成绩保持不变,为 94.00。这表明极低的分数在处理异常值时被移除。
删除异常值导致了均值和标准差的下降,同时平均测试成绩略有上升。虽然删除异常值可以提高数据质量,尤其是当异常值由于数据输入错误或测量不准确时,但它也会减少数据集的变异性。如果异常值代表了总体中的真实变异性,删除它们可能会扭曲数据的整体情况。因此,必须谨慎考虑异常值是否为真实数据点或错误数据。
注意
一些统计模型假设数据符合正态分布,因此可能对异常值非常敏感。删除异常值有助于满足某些模型的假设。因此,在删除之前,你需要更好地理解你正在解决的问题以及要使用的技术。
如果你不想完全删除数据中的异常值,还有其他方法可以处理它们。在接下来的部分,我们将讨论异常值的修剪和温莎化处理。
修剪
修剪是指从分布的两端删除一定比例的数据,然后计算均值。对于修剪,我们需要定义修剪比例,这个比例表示在计算修剪后的均值时,从分布的两端去除的数据比例。它用于排除一定比例的极端值(异常值)在均值计算中的影响。修剪比例的值介于 0 和 0.5 之间,满足以下条件:
-
0 表示不进行修剪(包括所有数据点)
-
0.1 表示从每个尾部修剪 10%的数据
-
0.2 表示从每个尾部修剪 20%的数据
-
0.5 表示从每个尾部修剪 50%的数据(排除最极端的值)
在我们的案例中,分析表明Age列存在最显著的异常值。为此,我们决定通过排除Age列中最上面和最下面的百分位数来修剪数据集。以下示例代码演示了这一修剪过程。我们仍在使用相同的数据集,因此这里跳过了 DataFrame 的创建。不过,你可以在以下链接查看完整代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter08/9.trimming.py。
让我们看看下面的代码片段,它创建了一个新的数据框(df_trimmed),只包括Age(年龄)值位于第 10 百分位和第 90 百分位之间的行。这实际上去除了Age列中最低的 10%和最高的 10%的值:
df_trimmed = df[(df['Age'] >= df['Age'].quantile(0.1)) & (df['Age'] <= df['Age'].quantile(0.9))]
现在让我们来计算每一列的修剪均值:
df_trimmed_mean = df_trimmed.mean()
在修剪数据后,最后一行计算了df_trimmed数据框中每列的均值。修剪后计算的均值被称为修剪均值。它表示去除最极端的 20%(每侧 10%)后的中央 80%数据的平均值。
注意
请记住,修剪比例是平衡修剪均值的稳健性与排除数据量之间的一个方式。你可能需要尝试不同的比例,以找到适合你数据的平衡点。
让我们看看修剪后的更新分布:

图 8.11 – 在 10% 阈值下去除异常值后的分布图
让我们也来看看更新后的数据统计信息:
Age Test_Score
count 18.000000 18.000000
mean 30.222222 69.309942
std 6.757833 18.797436
min 20.000000 20.000000
25% 24.000000 60.723684
50% 32.875000 66.947368
75% 33.937500 84.750000
max 41.000000 94.000000
在原始数据集中,Age列的均值为 33.75,标准差为 17.18,而修剪后的数据表现为更高的均值 30.22,且标准差大幅降低至 6.76。修剪数据中的最低年龄值从 18 增加到 20,表明去除了低值异常值。最高年龄值从 90 下降到 41,表明排除了高值异常值。
对于Test_Score(测试分数)列,原始数据集中的均值为 65.89,标准差为 24.76。在修剪后的数据中,均值上升至 69.31,标准差下降至 18.80,表明测试分数的分布范围变窄。最低测试分数从 5 增加到 20,表明去除了低值异常值,而最高测试分数保持在 94 不变。
总体而言,去除异常值导致了数据的集中趋势(均值)和分布范围(标准差)发生变化,Age(年龄)和Test_Score(测试分数)均如此。这表明修剪后的数据集变得更加集中在中间值周围,极端值被移除。
记住!
虽然修剪有助于减少极端值的影响,但它也意味着丢弃一部分数据。这可能导致信息丢失,而修剪后的变量可能无法完全代表原始数据集。
在接下来的部分,我们将介绍一种稍微不同的处理异常值的方法,叫做温莎化。
温莎化
与直接去除极端值的修剪不同,winsorizing(温莎化)是通过用较不极端的值替代它们。极端值被替换为接近分布中心的值,通常是在指定的百分位数。温莎化在你希望保留数据集的大小并帮助保持数据分布的整体形态时非常有用。
回到我们的示例用例,看看代码。你可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter08/10.winsorizing.py找到完整的代码:
winsorizing_fraction = 0.1
winsorizing_fraction设置为0.1,表示在数据分布的两端调整的数据比例。它以百分比的形式表示,值通常在 0 和 50%之间。确定 Winsor 化比例的过程涉及考虑你希望减少极端值的影响程度。一个常见的选择是将两端的某个百分比进行 Winsor 化,例如 5%或 10%。
这里还需要了解的一点是,Winsor 化过程是针对每一列单独且独立地进行的。记住:我们在这里以单变量的方式处理离群值:
df_winsorized = df.apply(lambda x: mstats.winsorize(x, limits=[winsorizing_fraction, winsorizing_fraction]))
limits=[winsorizing_fraction, winsorizing_fraction]参数指定了从分布两端 Winsor 化的数据比例。这里从下端和上端各调整 10%。极端值(最低的 10%和最高的 10%)将被替换为指定范围内的最近值,从而减少它们对统计量的影响。
这里展示了 Winsor 化后的更新分布:

图 8.12 – 经 Winsor 化后的离群值分布图
让我们看看数据的更新统计信息:
Age Test_Score Age_Winsorized
count 24.000000 24.000000 24.000000
mean 33.750000 65.894737 30.666667
std 17.181575 24.761286 8.857773
min 18.000000 5.000000 19.000000
25% 22.750000 58.750000 22.750000
50% 32.875000 66.947368 32.875000
75% 35.750000 85.500000 35.750000
max 90.000000 94.000000 45.000000
Age列的均值从 33.75 降至 30.67,表明由于极端高值被调整,数据分布向较低值偏移。标准差也从 17.18 显著降低至 8.86,说明数据集的变异性减少。最小值从 18 略微增加到 19,最大值从 90 降至 45,反映了极端值的限制。
至于Test_Score,Winsor 化后均值保持在 65.89,标准差保持在 24.76,表明测试分数的变异性未受 Winsor 化过程的影响。最大值保持不变,依然为 94,显示上端极端值没有发生变化。
总体而言,对Age列进行 Winsor 化后,数据的分布变得更加集中,标准差的减小也证明了这一点。Winsor 化成功地减少了极端值在Age列中的影响,使数据更加集中于中间范围。对于Test_Score列,Winsor 化并未对分布产生影响,可能是因为极端值已经在接受范围内。
接下来,我们将探讨如何通过数学变换来最小化离群值的影响。
数据变换
应用对数或平方根等数学变换是处理偏斜数据或稳定方差的常见技术。
提醒
偏度是分布不对称的度量。正偏度表示分布有右尾,而负偏度表示分布有左尾。
当数据右偏(正偏度)时,即大部分数据点集中在左侧,右侧有少数较大值时,应用对数变换会压缩较大值,使分布更对称,更接近正态分布。
类似于对数变换,平方根变换用于减少较大值的影响,并使分布更对称。当分布的右尾包含极端值时,特别有效。
另一点需要注意的是,当数据的方差随均值增加(异方差性)时,对数和平方根变换可以压缩较大值,减少极端值的影响,并稳定方差。
让我们回到我们的例子,并对数据集的两列进行对数变换。如往常一样,你可以在 github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter08/11.data_transformation.py 找到完整的代码。
让我们对 Age 和 Test_Score 应用对数变换:
df_log_transformed = df.copy()
df_log_transformed['Age'] = np.log1p(df_log_transformed['Age'])
df_log_transformed['Test_Score'] = np.log1p(df_log_transformed['Test_Score'])
np.log1p 是 NumPy 中的一个函数,用于计算 Age 和 Test_Score 列中每个值的 1 + x 的自然对数。log1p 函数用于处理数据集中的零值和负值,而不会出现错误,相比简单的对数函数 (np.log) 更为实用。在处理包含零值或非常小数值的数据时特别有用。这种变换可以减小偏斜度,并使分布更接近正态分布,这对于各种假设数据正态分布的统计技术非常有用。
更多变换的实施
在代码中,你会发现对数据应用了对数和平方根变换。花些时间探索和理解这两种方法之间的差异。通过考虑每种变换对数据分布和方差的影响,评估哪种变换更适合你的数据。
更新后的分布在以下图表中展示,其中对 Age 列进行了对数变换,对 Test_Score 列进行了平方根变换:

图 8.13 – 对数和平方根变换后的分布图
让我们也来看一下数据的更新统计信息:
Age Test_Score
count 24.000000 24.000000
mean 3.462073 4.059624
std 0.398871 0.687214
min 2.944439 1.791759
25% 3.167414 4.090143
50% 3.522344 4.218613
75% 3.603530 4.460095
max 4.510860 4.553877
描述性统计显示了对Age变量进行对数转换和对Test_Score变量进行平方根转换的影响。在转换之前,原始数据集中的Age呈右偏分布,均值为 33.75,标准差较大,为 17.18。Test_Score的均值为 65.89,范围从 5 到 94,标准差为 24.76,表明测试成绩分布较广。
在应用了转换后,两个变量的分布明显发生了变化:
-
对
Age进行对数转换后,值的分布被压缩,标准差从原始的 17.18 降至 0.40。转换后的值范围从 2.94 到 4.51,显示出极端值的压缩。 -
对于
Test_Score,对数据进行对数转换后,值的分布变得更加均匀,标准差从 24.76 降低到 0.69。数据变得更加紧凑且对称,范围从 1.79 到 4.55。
这些转换对两个变量产生了明显的平滑效应,减少了偏斜度和变异性。这一点从标准差的减少和范围的缩小可以看出,使得数据更加对称,接近正态分布。
然而,需要注意的是,转换,特别是对数转换,会压缩数值的尺度,可能影响可解释性。虽然它们通过减少偏斜度和异方差性,有助于满足统计方法的假设,但转换后的数据可能比原始数据尺度更难以理解。尽管如此,这种转换在准备回归模型或其他假设数据呈正态分布的分析时,仍然非常有用。
注意
请记住,对数转换不适用于包含零或负值的数据,因为对数在这些值上是未定义的。
本章的这一部分最后,我们汇总了一个表格,概述了处理异常值时使用的各种方法。该表格突出了每种技术的最佳使用场景,并提供了它们各自的优缺点概览。
| 技术 | 何时使用 | 优点 | 缺点 |
|---|---|---|---|
| 修剪 | 轻度异常值,保留整体数据结构 | 保留大部分数据集,保持数据完整性 | 减少样本量,可能影响代表性,修剪百分比的选择可能带有随意性 |
| 温莎化 | 中度异常值,保留整体数据 | 保持数据分布,减轻极端值的影响 | 改变数据值;可能扭曲分布;需要指定修剪的限度 |
| 删除数据 | 严重异常值 | 移除极端值的影响,简化分析 | 减少样本量,可能丧失信息;可能使结果偏向中心趋势 |
| 变换 | 偏斜或非正态分布 | 稳定方差,使数据更对称,适应传统统计技术 | 解释挑战,结果可能不太直观,变换方法的选择是主观的 |
表 8.2 – 单变量方法处理异常值的总结
在探讨了各种处理单变量异常值的技术后,包括从简单到复杂的方法,接下来的部分将深入探讨在处理含有异常值的数据时,一般更为偏好的不同统计量。
稳健统计
使用如中位数和中位数绝对偏差(MAD)等稳健的统计量而非均值和标准差,可以减少异常值的影响。
在处理包含异常值或偏斜分布的数据集时,选择稳健的统计量对于获取准确且具有代表性的总结至关重要。使用稳健的量度,如中位数和 MAD,在极端值可能影响传统量度(如均值和标准差)的场景中证明了其优势。中位数是排序后数据的中间值,它对异常值不那么敏感,提供了一个更可靠的集中趋势测量。此外,MAD 评估数据的分布,并且对异常值具有稳健性,从而进一步确保数据集变异性的更准确表示。
MAD
MAD 是一种衡量统计离散度的指标,用于量化数据集的离散程度或分布。它是通过计算每个数据点与数据集的中位数之间的绝对差的中位数来得出的。
该表总结了使用中位数和 MAD 与使用均值和标准差时的关键考虑因素、优缺点:
| 标准 | 中位数 和 MAD | 均值和 标准差 |
|---|---|---|
| 何时使用 | 异常值的存在 | 正态或对称分布 |
| 偏斜分布 | 测量的精确性 | |
| 优点 | 对异常值的稳健性 | 对正态分布的效率 |
| 对偏斜数据的适用性 | 解释的简便性 | |
| 缺点 | 没有异常值时缺乏敏感性 | 对异常值敏感 |
| 在存在异常值的情况下不稳健 | ||
| 考虑因素 | 当需要稳定的集中趋势时很有用 | 适用于极端值最少或没有极端值的数据集 |
| 提供在正态分布中的精确度量 |
表 8.3 – 哪些统计方法在处理异常值时更有效
本章接下来的部分将讨论如何识别多变量异常值。
识别多变量异常值
多元离群值发生在一个观测值在多个变量的上下文中同时是极端的。这些离群值不能仅通过分析单个变量来检测;相反,它们需要考虑变量之间的相互作用。检测多元离群值涉及在更高维空间中评估数据点。在接下来的部分中,我们将概述不同的方法来识别多元离群值,并为每种方法提供代码示例。
马哈拉诺比斯距离
马哈拉诺比斯距离是一种统计量,用于识别多元数据中的离群值。它考虑了变量之间的相关性,并计算每个数据点与数据集均值在缩放空间中的距离。然后,将这个距离与一个阈值进行比较,以识别那些显著偏离多元均值的观测值。
对于这个示例,我们创建了一个新的数据集,包含一些多元学生数据,以便我们可以以最佳方式展示这一技术。完整代码可以在仓库中查看:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter08/12.mahalanobis_distance.py。该过程的关键步骤如下:
-
让我们首先导入所需的库:
import pandas as pd import numpy as np import matplotlib.pyplot as plt from scipy.stats import chi2 from mpl_toolkits.mplot3d import Axes3D -
让我们生成多元学生数据:
np.random.seed(42) data = np.random.multivariate_normal(mean=[0, 0], cov=[[1, 0.5], [0.5, 1]], size=100)我们从一个多元正态分布中生成了 100 个样本,指定均值向量为
[0, 0],协方差矩阵为[[1, 0.5], [0.5, 1]]。 -
让我们引入离群值并创建数据框:
outliers = np.array([[8, 8], [9, 9]]) data = np.concatenate([data, outliers]) df = pd.DataFrame(data, columns=['X1', 'X2']) -
以下函数根据均值和协方差矩阵的逆计算每个数据点的马哈拉诺比斯距离:
def mahalanobis_distance(x, mean, inv_cov_matrix): centered_data = x - mean mahalanobis_dist = np.sqrt(np.dot(centered_data, np.dot(inv_cov_matrix, centered_data))) return mahalanobis_dist -
计算数据集的均值、协方差矩阵和协方差矩阵的逆:
mean = np.mean(df[['X1', 'X2']], axis=0) cov_matrix = np.cov(df[['X1', 'X2']], rowvar=False) inv_cov_matrix = np.linalg.inv(cov_matrix) -
为每个数据点计算马哈拉诺比斯距离,并将其作为新列添加到数据框中:
df['Mahalanobis_Distance'] = df.apply(lambda row: mahalanobis_distance(row[['X1', 'X2']], mean, inv_cov_matrix), axis=1) -
设置离群值检测的显著性水平:
alpha = 0.01显著性水平(
alpha)表示在零假设为真的情况下拒绝它的概率,在本上下文中,它指的是错误地将数据点识别为离群值的概率。alpha常见的选择值为0.01,意味着错误地将正常数据点归类为离群值的概率为 1%。较低的alpha值使得离群值检测更加保守,减少假阳性(正常点被标记为离群值)。相反,较高的alpha值使检测更加宽松,可能会识别出更多的离群值,但增加了假阳性的机会。 -
接下来,我们设置卡方阈值:
chi2_threshold = chi2.ppf(1 - alpha, df=2) # df is the degrees of freedom, which is the number of features卡方阈值是从卡方分布中得到的临界值,用于定义异常值检测的截止点。
chi2.ppf函数计算卡方分布的百分位点函数(累积分布函数的反函数)。自由度等于马氏距离计算中使用的特征或变量的数量。在这种情况下,是2(对于 X1 和 X2)。卡方阈值用于确定超过该值的马氏距离被认为过高,表示相应的数据点是异常值。例如,使用alpha = 0.01时,表示你正在寻找一个阈值,超过该阈值的只有 1%的数据点,假设数据是正态分布的。 -
这一步涉及将每个数据点的马氏距离与卡方阈值进行比较,以确定它是否为异常值:
outliers = df[df['Mahalanobis_Distance'] > chi2_threshold] df_no_outliers = df[df['Mahalanobis_Distance'] <= chi2_threshold]距离大于阈值的数据点被标记为异常值,并与其余数据分开。
-
现在让我们来可视化异常值:
fig = plt.figure(figsize=(10, 8)) ax = fig.add_subplot(111, projection='3d') ax.scatter(df_no_outliers['X1'], df_no_outliers['X2'], df_no_outliers['Mahalanobis_Distance'], color='blue', label='Data Points') ax.scatter(outliers['X1'], outliers['X2'], outliers['Mahalanobis_Distance'], color='red', label='Outliers') ax.set_xlabel('X1') ax.set_ylabel('X2') ax.set_zlabel('Mahalanobis Distance') ax.set_title('Outlier Detection using Mahalanobis Distance') plt.legend() plt.show()
在下面的图表中,我们可以看到所有数据点在 3D 空间中的投影,并且可以看到标记为x的异常值:

图 8.14 – 使用马氏距离绘制的数据
注意
在你的笔记本电脑上运行可视化程序,以便能够看到这个空间并在 3D 视图中移动,挺酷的!
从 3D 图中可以看出,数据中的异常值非常容易识别。马氏距离在处理涉及多个变量的数据集时最为有效,因为它考虑了变量之间的均值和协方差,并能够识别在单个变量中可能无法显现的异常值。在变量具有不同单位或尺度的情况下,马氏距离可以规范化变量间的距离,从而提供更有意义的异常值度量。与单变量方法不同,马氏距离对变量之间的关系非常敏感。它捕捉每个数据点与数据分布中心的距离,同时考虑了变量之间的相关性。
在多变量部分的下一节中,我们将讨论聚类方法如何帮助我们检测异常值。
聚类技术
聚类方法,如 k-means 或层次聚类,可以用于将相似的数据点分组。那些不属于任何聚类或形成小聚类的数据点,可能会被视为多变量异常值。
一种常见的异常值检测方法是使用基于密度的空间聚类应用与噪声(DBSCAN)算法。DBSCAN 可以识别密集的数据点簇,并将异常值分类为噪声。DBSCAN 的优势在于它不需要事先指定聚类的数量,并且能够基于密度有效地识别异常值。它是一个相对简单但功能强大的异常值检测方法,尤其在聚类可能不完全分离或异常值形成孤立点的情况下表现良好。
让我们深入了解 DBSCAN 的代码。与往常一样,你可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter08/13.clustering.py的代码库中找到完整的代码:
-
让我们导入所需的库:
import pandas as pd import numpy as np import matplotlib.pyplot as plt from sklearn.cluster import DBSCAN from sklearn.preprocessing import StandardScaler -
让我们生成用于该方法的示例数据集。数据集由 100 个样本组成,来自一个多元正态分布,均值向量为
[0, 0],协方差矩阵为[[1, 0.5], [0.5, 1]]。这将创建一个围绕原点的正态分布点簇,其中各特征之间存在一定的相关性:np.random.seed(42) data = np.random.multivariate_normal(mean=[0, 0], cov=[[1, 0.5], [0.5, 1]], size=100) outliers = np.random.multivariate_normal(mean=[8, 8], cov=[[1, 0], [0, 1]], size=10) data_with_outliers = np.vstack([data, outliers]) -
让我们将数据转换为 DataFrame:
df = pd.DataFrame(data_with_outliers, columns=['Feature1', 'Feature2']) -
通过去除均值并缩放到单位方差来标准化数据。
sklearn.preprocessing中的StandardScaler用于拟合和转换数据。标准化确保所有特征在距离计算中贡献相等,通过将它们缩放到均值为 0、标准差为 1 来实现。这对于基于距离的算法(如 DBSCAN)尤其重要:scaler = StandardScaler() data_scaled = scaler.fit_transform(df) -
应用 DBSCAN 进行异常值检测。
eps=0.4设置了被视为同一邻域的点之间的最大距离,min_samples=5指定了形成密集区域所需的最小点数。DBSCAN 是一种聚类算法,可以识别不属于任何簇的异常值。DBSCAN 将标记为-1的点视为异常值。eps和min_samples参数的选择会显著影响异常值的检测,这些值可能需要根据具体数据集进行调优:dbscan = DBSCAN(eps=0.4, min_samples=5) df['Outlier'] = dbscan.fit_predict(data_scaled)
在下图中,我们将所有数据点绘制在二维空间中,可以看到图表右侧的异常值:

图 8.15 – 基于 DBSCAN 的异常值检测聚类
在 DBSCAN 中有一个关键参数需要调整:eps。eps(epsilon)参数本质上定义了数据点周围的半径,所有位于该半径内的其他数据点都被视为该数据点的邻居。
在执行 DBSCAN 聚类时,算法首先选择一个数据点,并识别所有距离该点在eps范围内的数据点。如果在这个距离内的数据点数量超过指定的阈值(min_samples),则选中的数据点被视为核心点,所有在其 epsilon 邻域内的点将成为同一聚类的一部分。然后,算法通过递归地查找邻居的邻居,直到没有更多的点可以添加为止,从而扩展聚类。
eps的选择取决于数据集的特定特征和所需的聚类粒度。它可能需要一些实验和领域知识来找到适合的eps值。
使用 k-means 代替 DBSCAN 提供了另一种方法。K-means 是一种基于质心的聚类算法,需要预先指定聚类数量,因此必须有先验知识或进行探索性分析,以确定k的合适值。虽然它对异常值敏感,但 k-means 的简洁性和计算效率使其在某些场景中成为一个有吸引力的选择。当聚类之间分离良好并且具有相对均匀的结构时,k-means 可能特别适用。然而,必须注意,k-means 可能在处理不规则形状或重叠的聚类时表现不佳,并且在试图最小化平方距离和时,可能会受到异常值的影响。
发现多变量异常值后,我们需要决定如何处理这些异常值。这是下一部分的重点。
处理多变量异常值
处理多变量异常值涉及到解决在多个变量背景下显著偏离的数据点。在本章的这一部分,我们将提供不同方法来处理多变量异常值的解释和代码示例。
多变量修剪
该方法涉及基于多个变量的综合评估来限制极端值。例如,修剪的限制可以通过考虑马哈拉诺比斯距离来确定,马哈拉诺比斯距离考虑了变量之间的相关性。这种技术在处理跨多个变量存在异常值的数据集时尤其有用。其思路是在减少极端值影响的同时,保留数据的整体结构。
在这个例子中,我们将继续处理马哈拉诺比斯距离示例中的数据,在计算完马哈拉诺比斯距离后,我们将丢弃超过阈值的异常值。你可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter08/14.multivariate_trimming.py的代码库中找到完整代码:
-
让我们从导入库开始:
import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns from scipy.stats import chi2 from mpl_toolkits.mplot3d import Axes3D -
让我们生成多变量学生数据。
np.random.seed(42) data = np.random.multivariate_normal(mean=[0, 0], cov=[[1, 0.5], [0.5, 1]], size=100) outliers = np.array([[8, 8], [9, 9]]) data = np.concatenate([data, outliers]) df = pd.DataFrame(data, columns=['X1', 'X2']) -
定义计算马氏距离的函数,该距离衡量数据点与分布均值的距离,考虑特征之间的相关性:
def mahalanobis_distance(x, mean, inv_cov_matrix): centered_data = x - mean mahalanobis_dist = np.sqrt(np.dot(centered_data, np.dot(inv_cov_matrix, centered_data))) return mahalanobis_dist -
为异常值检测准备数据:
df[['X1', 'X2']] = df[['X1', 'X2']].astype(float) mean = np.mean(df[['X1', 'X2']], axis=0) cov_matrix = np.cov(df[['X1', 'X2']], rowvar=False) inv_cov_matrix = np.linalg.inv(cov_matrix) -
计算每个数据点的马氏距离:
df['Mahalanobis_Distance'] = df.apply(lambda row: mahalanobis_distance(row[['X1', 'X2']], mean, inv_cov_matrix), axis=1) -
设置异常值检测的阈值:
alpha = 0.1 chi2_threshold = chi2.ppf(1 - alpha, df=2) -
过滤数据框,分离出异常值与其余数据。
outliers = df[df['Mahalanobis_Distance'] > chi2_threshold] df_no_outliers = df[df['Mahalanobis_Distance'] <= chi2_threshold]
在处理异常值之前,让我们先展示分布图。

图 8.16 – 包含多变量异常值的分布图
原始数据的描述性统计如下:
X1 X2
count 102.000000 102.000000
mean 0.248108 0.281463
std 1.478963 1.459212
min -1.852725 -1.915781
25% -0.554778 -0.512700
50% 0.108116 0.218681
75% 0.715866 0.715485
max 9.000000 9.000000
在删除被认为是多变量异常值的数据后,我们可以观察到以下分布的变化:

图 8.17 – 移除多变量异常值后的分布图
最后,让我们来看看更新后的描述性统计:
X1 X2 Mahalanobis_Distance
count 100.000000 100.000000 100.000000
mean 0.083070 0.117093 1.005581
std 0.907373 0.880592 0.547995
min -1.852725 -1.915781 0.170231
25% -0.574554 -0.526337 0.534075
50% 0.088743 0.200745 0.874940
75% 0.699309 0.707639 1.391190
max 1.857815 2.679717 2.717075
在修剪掉异常值之后,让我们讨论数据中观察到的变化:
-
移除异常值后,观察的数量从 102 降至 100,因此我们丢弃了两条记录。
-
在
X1列中,均值从 0.248 降至 0.083,标准差从 1.479 降至 0.907。 -
在
X2列中,均值从 0.281 降至 0.117,标准差从 1.459 降至 0.881。 -
X1和X2的最大值分别被限制在 1.857815 和 2.679717,表明极端异常值已被移除。
总的来说,移除异常值后,数据集的变异性减小,尤其是在均值和标准差方面。极端值可能对分析产生偏差的风险已被减轻。
让我们总结本章的关键要点。
总结
在本章中,我们深入探讨了缺失值和异常值的处理。我们理解了缺失值如何扭曲我们的分析,并学习了从简单的均值插补到先进的基于机器学习的插补技术等多种插补方法。同样,我们认识到异常值可能会偏移我们的结果,并深入研究了在单变量和多变量背景下检测和管理异常值的方法。通过结合理论和实践示例,我们对确保数据质量和可靠性的考虑、挑战及策略有了更深入的理解。
拥有这些见解后,我们现在可以进入下一章,讨论特征的缩放、归一化和标准化。
第九章:归一化与标准化
特征缩放、归一化和标准化是机器学习中的重要预处理步骤,能够帮助确保机器学习模型能够有效地从数据中学习。这些技术解决了与数值稳定性、算法收敛性、模型性能等相关的问题,最终有助于在数据分析和机器学习任务中获得更好、更可靠的结果。
在本章中,我们将深入探讨以下主题:
-
将特征缩放到一个范围
-
Z-score 缩放
-
鲁棒缩放
技术要求
你可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/tree/main/chapter09找到本章的所有代码。
不同的代码文件遵循章节中不同部分的名称。
将特征缩放到一个范围
特征缩放是机器学习中的一种预处理技术,它对数据集的独立变量或特征进行范围重缩放。它的目的是通过将所有特征调整到一个共同的尺度上,确保所有特征在模型训练过程中对模型的贡献相同。特征缩放对于那些对输入特征尺度敏感的算法尤为重要,如 k 近邻算法和基于梯度下降的优化算法。
注意
在进行特征缩放时,我们实际上是在改变数据分布的范围。
让我们通过一个例子来帮助理解特征缩放的概念。假设你正在进行一个机器学习项目,目的是根据房屋的各种特征来预测房价,例如以下几个特征:
-
建筑面积(平方英尺)
-
到最近学校的距离(英里)
-
到最近公共交通站点的距离(英里)
现在,让我们来讨论一下在这个背景下特征缩放为什么如此重要。建筑面积这一特征的范围可能从几百平方英尺到几千平方英尺不等。而到最近学校的距离和到最近公共交通站点的距离可能从几分之一英里到几英里不等。如果不对这些特征进行缩放,算法可能会给较大值过高的权重,从而使得建筑面积在预测房价中占主导地位。像到最近学校的距离这样的特征可能会被不公平地忽略。
对于本章的所有部分,我们将使用上述示例来展示不同的缩放方法。让我们先来看一下这个示例的数据创建过程;代码可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter09/min_max_scaling.py找到:
-
让我们从导入所需的库开始:
import pandas as pd import numpy as np import matplotlib.pyplot as plt from sklearn.preprocessing import MinMaxScaler -
接下来,我们将创建一个与房价相关的特征数据集:
np.random.seed(42) num_samples = 100我们将创建以下影响房价的特征:
-
平方英尺面积:
square_footage = np.random.uniform(500, 5000, num_samples) -
到最近学校的距离(以英里为单位):
distance_to_school = np.random.uniform(0.1, 5, num_samples) -
通勤到工作的距离(以英里为单位):
commute_distance = np.random.exponential(5, num_samples) -
交通密度(偏态特征):
traffic_density = np.random.exponential(2, num_samples)
-
-
然后,我们创建一个包含所有特征的 DataFrame:
data = pd.DataFrame({ 'Square_Footage': square_footage, 'Distance_to_School': distance_to_school, 'Commute_Distance': commute_distance, 'Traffic_Density': traffic_density }) -
最后,我们绘制原始分布:
plt.figure(figsize=(12, 8))你可以在这里看到数据的原始分布:

图 9.1 – 房价预测用例的分布
-
让我们显示数据集的统计信息:
print("Original Dataset Statistics:") print(data.describe())这将打印以下输出:

图 9.2 – 原始数据集统计信息
现在让我们讨论最常见的缩放方法之一——min-max 缩放。
Min-max 缩放
Min-max 缩放,也称为归一化,将变量的值缩放到一个特定的范围,通常在 0 和 1 之间。Min-max 缩放在你想确保变量中的所有值都落在标准化范围内,使它们可以直接比较时非常有用。当变量的分布不假设为正态分布时,它通常被应用。
让我们看一下计算 min-max 缩放的公式:
X _ 缩放 = (X − X _ min) / (X _ max − X _ min)
从公式中可以看出,min-max 缩放保持了值的相对顺序,但将它们压缩到一个特定的范围。需要注意的是,这不是处理异常值的方法,如果数据中存在异常值,这些极端值可能会不成比例地影响缩放。因此,最好先处理异常值,然后再进行特征的缩放。
当满足以下条件时,缩放到特定范围是合适的:
-
你已经知道数据的大致上下限
-
你的数据在这个范围内遵循相对均匀或钟形分布
-
你选择的机器学习算法或模型通过将特征限制在特定范围内,通常是
[0, 1]或任何其他期望的范围,会受益。
这个场景的经典例子是年龄。年龄值通常从 0 到 90,整个范围内有大量个体。然而,不太建议对收入进行缩放,因为高收入个体的数量有限。如果你对收入进行线性缩放,缩放的上限将变得异常高,大多数数据点将集中在一个狭窄的范围内,导致信息丢失和失真。
让我们看一下我们之前讨论的房价预测用例的代码,了解 min-max 缩放器如何转换数据:
-
首先,我们使用
MinMaxScaler()来缩放数据:scaler = MinMaxScaler() data_scaled = pd.DataFrame(scaler.fit_transform(data), columns=data.columns) -
我们可以使用以下代码显示缩放后的数据集统计信息:
print("\nDataset Statistics After Scaling:") print(data_scaled.describe()) -
让我们绘制并观察缩放后的分布:
plt.figure(figsize=(12, 8))
让我们看看标准化后修改的数据分布:

图 9.3 – 经过最小最大标准化后的房价预测用例分布
我们应用了最小最大标准化,将每一列转化为标准化的 0 到 1 之间的范围。由于最小最大标准化保持了数据点之间的相对距离,因此原始特征分布的形状在标准化后保持不变。标准化对数据起到了归一化的作用,将所有特征带到了一个共同的尺度。这在特征具有不同单位或范围时尤为重要,可以防止某个特征支配其他特征。
注意
如果你的数据集是稀疏的(包含许多零值),则最小最大标准化可能不适用,因为它可能导致信息丢失。可以考虑使用替代方法,如MaxAbsScaler或鲁棒缩放器。
在接下来的部分,我们将讨论 Z 分数标准化。
Z 分数标准化
Z 分数标准化,也称为标准化,适用于当你想将数据转化为均值为 0,标准差为 1 的形式时。Z 分数标准化在统计分析和机器学习中被广泛应用,尤其是在使用 k-means 聚类或主成分分析(PCA)等算法时。
这是 Z 分数的公式:
X _ scaled =(X − mean(X)) / std(X)
让我们继续使用房价预测用例来展示 Z 分数标准化。代码可以在 github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter09/zscaler.py 找到:
-
我们首先执行 Z 分数标准化:
data_zscore = (data - data.mean()) / data.std() -
然后,我们打印数据集统计信息:
print("\nDataset Statistics after Z-score Scaling:") print(data_zscore.describe()) -
最后,我们可视化分布:
data_zscore.hist(figsize=(12, 10), bins=20, color='green', alpha=0.7) plt.suptitle('Data Distributions after Z-score Scaling') plt.show()
让我们看看标准化后修改的数据分布:

图 9.4 – 经过 Z 标准化后的房价预测用例分布
标准化后的每个特征的均值非常接近 0,这是 Z 分数标准化的预期结果。数据现在已围绕 0 进行集中。每个特征的标准差大约为 1,使得不同特征之间的尺度具有可比性。最小值和最大值已被转换,但保持了数据的相对分布。
何时使用 Z 分数标准化
让我们还讨论一些关于何时使用 Z 分数标准化的注意事项:
-
Z-score 缩放假设数据大致呈正态分布,或者至少围绕中心均值呈对称分布。如果数据高度偏斜或具有非标准分布,标准化可能在使数据更符合高斯分布方面效果不佳。如你所见,
Commute_Distance和Traffic_Density特征呈偏斜分布,且由于数据未围绕均值集中,z-score 缩放效果并不理想。 -
这种方法最适用于数值特征,而非分类或有序特征。确保你要标准化的数据是定量性质的。
-
极端异常值可能对均值和标准差产生重大影响,而这些是 z-score 缩放中使用的统计量。因此,在标准化之前处理异常值非常重要,因为它们可能扭曲缩放效果。
-
Z-score 缩放假设变量之间存在线性关系。如果底层关系是非线性的,其他缩放方法或变换可能更为合适。
-
Z-score 缩放假设变量是独立的,或至少不高度相关。如果变量之间高度相关,标准化可能不会提供额外的好处,且应单独考虑相关性结构。
-
Z-score 缩放会改变数据的原始单位,这可能会影响结果的可解释性。考虑在分析中是否需要保持原始单位。
对于小型数据集,z-score 缩放的影响可能更为显著。对非常小的数据集应用标准化时要小心,因为它可能会过度强调异常值的影响。
稳健缩放
稳健缩放,也叫稳健标准化,是一种特征缩放方法,特别适用于处理包含异常值的数据集。与可能对异常值敏感的最小-最大缩放和 z-score 缩放不同,稳健缩放旨在在存在极端值时保持稳健性。当你希望在最小化极端值影响的同时对特征进行规范化或标准化时,它尤为有用。稳健缩放也适用于那些特征不遵循正态分布、可能具有偏斜或重尾的数据集。
下面是稳健缩放的公式:
X _ scaled = (X − median) / IQR
在缩放过程中,通过减去中位数并除以四分位距(IQR)来规范化数据,使其围绕中位数进行中心化,并根据 IQR 所表示的分布范围进行缩放。这种规范化有助于减轻极端值的影响,使得缩放后的值更加代表整体分布。
正如我们在上一章中讨论的那样,中位数是当数据按顺序排列时位于中间的值,使得鲁棒缩放比其他依赖均值的缩放方法对极端值或异常值的敏感度较低。此外,四分位距(IQR)表示第一四分位数(Q1)和第三四分位数(Q3)之间的范围,亦对异常值具有鲁棒性。与完整范围或标准差不同,IQR 侧重于数据的中间 50%,因此不容易受到极端值的影响。
下面是如何使用 Python 代码应用鲁棒缩放的示例:
robust_scaler = RobustScaler()
data_scaled = robust_scaler.fit_transform(data)
让我们来看一下缩放后修改的数据分布:

图 9.5 – 鲁棒缩放后房价预测案例分布
经过鲁棒缩放处理后,我们可以观察到数据的中心趋势发生了变化,每个特征的均值现在更接近零。这是因为鲁棒缩放过程减去了中位数。至于数据的分布,它在除以四分位距(IQR)后发生了变化。每个特征的变异性现在以更一致的方式呈现,且对异常值更为稳健。每个特征的值范围现在被压缩,特别是对于那些初始范围较大的特征,防止了极端值特征的主导作用。
为了总结本章内容,我们制作了一个总结表格,展示了迄今为止讨论的所有技术,包括它们的使用时机、优缺点等信息。
方法比较
本图表提供了在不同数据情境下,适合使用哪种缩放技术的指导。
| 缩放方法 | 何时使用 | 优点 | 缺点 |
|---|---|---|---|
| 最小-最大缩放 | 特征具有明确且已知的范围假设数据服从正态分布数据不包含异常值 | 简单易懂保留相对关系节省内存 | 对异常值敏感 |
| Z-score 缩放 | 数据服从正态分布对范围没有强假设处理异常值不是重点 | 将数据标准化为零均值,标准差为 1 | 对异常值敏感可能不适合偏态数据 |
| 鲁棒缩放 | 数据包含异常值偏态分布或非正态数据平衡特征贡献对不同特征方差的弹性 | 对异常值较不敏感保留中心趋势和分布 | 计算开销更大 |
表 9.1 – 比较不同的缩放技术
计算复杂度
最小-最大缩放方法通常更节省内存,尤其是在处理大型数据集时。这是因为最小-最大缩放仅仅基于每个特征的最小值和最大值进行缩放,计算相对简单。
另一方面,z-score 标准化和稳健标准化都需要额外的计算,如均值、标准差(用于 z-score 标准化)、中位数和四分位间距(用于稳健标准化),这可能会导致更多的内存使用。尤其在处理大数据集时,z-score 标准化和稳健标准化的计算复杂性和内存需求可能会变得更加明显。
最后,让我们总结一下本章的学习内容,并为下一章获得启发。
总结
本章中,我们探讨了三种常见的数值特征标准化方法:最小-最大标准化、z-score 标准化和稳健标准化。最小-最大标准化将数据转换到一个特定的范围,使其适用于对特征大小敏感的算法。z-score 标准化将数据标准化为零均值和单位方差,提供一个标准化的分布。稳健标准化则通过使用中位数和四分位间距,能够抵抗离群值,适用于具有偏态分布或离群值的数据集。我们还讨论了在选择最适合你使用场景的方法时需要考虑的不同因素。
接下来,我们将在下一章将重点转向处理分类特征。
第十章:处理分类特征
处理分类特征涉及表示和处理那些本质上不是数值的信息。分类特征是可以取有限且固定数量值或类别的属性,它们通常定义数据集中的不同类别或组,例如产品类型、书籍类型或客户群体。有效地管理分类数据至关重要,因为大多数机器学习(ML)算法要求输入为数值。
在本章中,我们将涵盖以下主题:
-
标签编码
-
一热编码
-
目标编码(均值编码)
-
频率编码
-
二进制编码
技术要求
本章的完整代码可以在以下 GitHub 仓库中找到:
github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/tree/main/chapter10
让我们安装本章将使用的必要库:
pip install scikit-learn==1.5.0
pip install matplotlib==3.9.0
pip install seaborn==0.13.2
pip install category_encoders==2.6.3
标签编码
标签编码是一种处理分类数据的技术,通过将每个类别转换为唯一的整数。它适用于具有顺序关系的分类特征,即类别之间有明确的排名或顺序。
例如,当处理“高中”、“学士”、“硕士”和“博士”等教育水平时,可以使用标签编码,因为这些教育水平有一个从最低到最高的明确顺序。
用例 —— 员工绩效分析
人力资源(HR)部门希望分析员工绩效数据,以了解员工评分与薪资、工作年限和部门等其他因素之间的关系。他们计划使用机器学习根据这些因素预测员工评分。
数据
让我们快速浏览一下用于绩效分析的数据:
-
Employee Rating:具有Poor、Satisfactory、Good和Excellent值的分类特征 -
Salary:表示员工薪资的数值特征 -
Years of Experience:表示员工工作年限的数值特征 -
Department:表示员工所在部门的分类特征
让我们先看一下编码前的原始数据框:
Employee Rating Salary Years of Experience Department
0 Poor 35000 2 HR
1 Good 50000 5 IT
2 Satisfactory 42000 3 Finance
3 Excellent 60000 8 IT
4 Good 52000 6 Marketing
在理解了数据之后,我们可以进入用例的目标。
用例目标
该用例的目标是使用标签编码对Employee Rating特征进行编码,以便准备数据进行机器学习分析。让我们看看如何使用 scikit-learn 来完成这项工作,完整代码可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter10/1a.label_encoding.py找到:
-
让我们导入所需的库:
import pandas as pd from sklearn.preprocessing import LabelEncoder -
让我们创建一个示例数据集并将其转化为 DataFrame:
data = { 'Employee Rating': ['Poor', 'Good', 'Satisfactory', 'Excellent', 'Good'], 'Salary': [35000, 50000, 42000, 60000, 52000], 'Years of Experience': [2, 5, 3, 8, 6], 'Department': ['HR', 'IT', 'Finance', 'IT', 'Marketing'] } df = pd.DataFrame(data) -
初始化
LabelEncoder类:label_encoder = LabelEncoder() -
对
员工评级列应用标签编码:df['Employee Rating (Encoded)'] = label_encoder.fit_transform(df['Employee Rating'])
让我们看看我们创建的编码输出。
编码后的输出
在这个用例中,应用标签编码对员工评级特征进行转换,将其转换为数字值,同时保留序数关系。下表显示了编码操作的输出结果。
员工评级 |
薪资 |
工作经验年限 |
部门 |
员工评级(编码后) |
|
|---|---|---|---|---|---|
1 |
差 |
35000 |
2 |
人力资源 |
2 |
2 |
好 |
50000 |
5 |
信息技术 |
1 |
3 |
满意 |
42000 |
3 |
财务 |
3 |
4 |
优秀 |
60000 |
8 |
信息技术 |
0 |
5 |
好 |
52000 |
6 |
市场营销 |
1 |
表 10.1 – 标签编码后的输出数据集
如你所见,已添加了一个员工评级(编码后)特征,所有项目现在都变成了数字。让我们看一下编码列的分布图:

图 10.1 – 编码前后的分布
如我们所见,编码前后的分布没有变化。标签编码将类别标签转换为数值,同时保留原始数据分布。它只是为每个类别分配了唯一的整数值,并未改变其频率。然而,在视觉上,x 轴上的标签将从类别值变为数字值,但每个标签的计数(或频率)将保持不变。
注意
如果数据被打乱或在不同的编码器运行之间类别的顺序发生变化,编码后的值可能会不同。这是因为将整数分配给类别可能依赖于它们出现的顺序。此外,如果每次都初始化一个新的标签编码器实例,类别与整数之间的映射可能也会发生变化。为了确保结果一致,应该在第一次拟合编码器后使用它来进行数据转换。
编码后的值可以作为机器学习模型的输入特征,用于根据薪资、工作经验和部门预测员工评级。现在,让我们讨论在使用标签编码器编码特征时需要注意的一些事项。
标签编码的注意事项
在进行标签编码时,尤其是在处理大型数据集时,有几个重要的注意事项。确保类别特征具有有意义的顺序。如果类别之间没有自然的顺序,标签编码可能不适用。标签编码将整数值分配给类别,基于字母顺序。如果类别没有固有的顺序,可能会引发问题,模型可能会将数字值视为有序。例如,Poor、Good和Excellent可能会被编码为2、1和0,但Poor并不比Good大。正如前面提到的用例中所发生的那样。为了确保标签编码反映正确的顺序(即Poor < Satisfactory < Good < Excellent),我们可以通过手动设置顺序并指定所需的映射来解决这个问题,完整的代码可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter10/1b.label_encoding_forced.py找到:
-
定义具有前缀的类别的正确顺序:
ordered_categories = { 'Poor': '1.Poor', 'Satisfactory': '2.Satisfactory', 'Good': '3.Good', 'Excellent': '4.Excellent' } -
将
Employee Rating列映射到带前缀的类别:df['Employee Rating Ordered'] = df['Employee Rating'].map(ordered_categories)生成的 DataFrame 如下所示:
Employee Rating Ordered Employee Rating (Encoded) 0 Poor 0 1 Good 2 2 Satisfactory 1 3 Excellent 3 4 Good 2在编码时,始终保持一致性,尤其是在训练集和测试集之间。编码器应该在训练数据上拟合,并用于转换训练集和测试集。这可以防止在测试集中出现未见过的类别,从而导致错误或编码不正确。按照以下步骤作为最佳实践:
-
对
EmployeeRating列应用标签编码:df['Employee Rating (Encoded)'] = label_encoder.fit_transform(df['Employee Rating']) -
保存编码器:
joblib.dump(label_encoder, 'label_encoder.pkl') -
加载编码器(在另一个脚本或会话中):
loaded_encoder = joblib.load('label_encoder.pkl') -
转换新数据:
df['Employee Rating (Encoded)'] = loaded_encoder.transform(df['Employee Rating'])
最后一项要提到的重点是,在处理大型数据集时,标签编码通常比独热编码更节省内存,后者可能会创建许多二进制列。
虽然标签编码是一种将类别数据转换为数值形式的简单方法,但它可能会无意中在类别之间引入不存在的顺序关系。为了避免这个问题,并确保每个类别被独立处理,独热编码通常是更合适的方法。
独热编码
独热编码是一种将类别数据转换为二进制矩阵(1 和 0)的技术。每个类别都被转换为一个新列,并且在对应类别的列中放置 1,而所有其他列则放置 0。该方法在处理没有类别间顺序关系的类别数据时特别有用。
何时使用独热编码
一热编码适用于缺乏自然顺序或类别排名的类别数据。以下是一些适用的场景:
-
名义类别数据:处理名义数据时,类别是独立的,并且没有固有的顺序。
-
不处理序列数据的算法:一些机器学习算法(例如,决策树和随机森林)并非专门设计来正确处理序列数据。一热编码确保每个类别都被视为独立的实体。
-
防止误解:为了防止模型假设不存在的序列关系,采用一热编码(one-hot encoding)将类别数据表示为二进制值。
接下来,让我们看一下可以使用一热编码的用例。
用例 – 客户流失预测
一家电信公司正在经历较高的客户流失率,想要构建一个机器学习模型,预测哪些客户可能会离开其服务。他们收集了有关客户人口统计、合同详情和使用的服务的数据。
数据
让我们快速查看一下可用于分析的数据:
-
合同类型:具有月度、一年和两年等值的类别特征 -
互联网服务:具有DSL、光纤和无互联网服务等值的类别特征 -
支付方式:具有电子支票、邮寄支票、银行转账和信用卡等值的类别特征
让我们看一下用于此用例的示例数据:
Customer ID Contract Type Internet Service Payment Method
0 1 Month-to-Month DSL Electronic Check
1 2 One Year Fiber Optic Mailed Check
2 3 Month-to-Month DSL Bank Transfer
3 4 Two Year Fiber Optic Credit Card
在了解了数据后,我们可以进入用例的目标部分。
用例目标
用例的目标是使用一热编码对类别特征进行编码,为机器学习分析准备数据。此示例的代码可以在这里找到:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter10/2.one_hot_encoding.py。
请按以下步骤操作:
-
初始化
OneHotEncoder类:one_hot_encoder = OneHotEncoder(sparse_output=False, drop='first') -
拟合并转换类别列:
encoded_columns = one_hot_encoder.fit_transform(df[['Contract Type', 'Internet Service', 'Payment Method']]) -
创建一个包含一热编码列的新 DataFrame:
encoded_df = pd.DataFrame(encoded_columns, columns=one_hot_encoder.get_feature_names_out(['Contract Type', 'Internet Service', 'Payment Method'])) -
将一热编码后的 DataFrame 与原始 DataFrame 进行连接:
df_encoded = pd.concat([df, encoded_df], axis=1) -
删除原始类别列,因为它们现在已经编码:
df_encoded = df_encoded.drop(['Contract Type', 'Internet Service', 'Payment Method'], axis=1)
让我们看一下我们创建的编码输出。
编码输出
在这个用例中,我们正在为客户流失预测模型准备客户数据。类别特征如合同类型、互联网服务和支付方式被一热编码,转换为适合机器学习的二进制表示。这些编码后的特征可以用来训练预测模型,帮助电信公司识别有流失风险的客户,并采取主动措施留住他们。
让我们通过一些图表看看应用编码时特征分布的变化。首先来看一下编码前的原始分布:

图 10.2 – 独热编码前的分布
编码后,每个类别变量的值会被转化为一个独特的列,展示二进制值(0 或 1),反映该类别在数据集每一行中的存在情况。让我们看看Contract Type列的分布图:

图 10.3 – 独热编码后合同类型特征的分布
注意
可视化原始类别数据有助于理解数据分布并识别任何不平衡情况。可视化编码后的列可以确保转换已正确应用。每个二进制列应仅包含 0 或 1 的值。
现在让我们讨论一些在使用独热编码器编码特征时需要注意的事项。
独热编码的注意事项
在进行独热编码时,特别是在大数据集上,有几个重要的注意事项需要牢记:
-
独热编码会显著增加数据集的维度,尤其是在类别较多的情况下。这可能导致“维度灾难”,对于某些算法来说可能是个问题。
-
共线性:由于每个类别都表示为一个单独的二进制列,这些列之间可能会存在共线性。这意味着某些列可能高度相关,这可能会影响线性模型的性能。
-
处理缺失值:在应用独热编码之前,决定如何处理类别特征中的缺失值。你可以选择为缺失值创建一个单独的列,或者使用插补技术。
-
在大数据集上进行独热编码可能会很具挑战性,因为特征数量的增加和潜在的高内存使用。若数据集过大无法放入内存,可将数据分批处理。
从独热编码转向目标编码,特别是在处理高基数类别特征时,可以特别有益。让我们更详细地探讨目标编码。
目标编码(均值编码)
目标编码,也称为均值编码,是一种通过将每个类别替换为该类别对应的目标变量的均值(或其他相关聚合函数)来编码类别特征的方法。此方法对于处理高基数类别特征的分类任务特别有用,而使用独热编码会导致维度的大幅增加。
更具体地说,目标编码将类别值替换为每个类别的目标变量的均值(或其他聚合度量)。它利用类别特征和目标变量之间的关系来编码信息。
什么时候使用目标编码
当你的特征是类别型并且有很多独特的类别时,使用独热编码可能会导致数据集的维度过高。在这种情况下,目标编码可以是一个有效的替代方案。
如果类别特征与目标变量之间存在强关系,目标编码能够捕捉到这种关系,并可能提高预测能力。
当你有内存限制并且需要降低数据集的维度时,你也可以使用目标编码,因为目标编码不会创建额外的列。
用例 – 零售商店的销售预测
一家拥有多个商店的零售连锁店希望建立一个机器学习模型,以预测每个商店的日销售额。他们收集了关于多个特征的数据,其中包括具有高基数的 Store Type 特征。为了避免使用独热编码(这会导致特征数量过多),零售连锁决定使用目标编码来编码 Store Type 特征。
数据
让我们快速查看一下可用于分析的数据:
-
商店类型: 商店的类型(具有Type A、Type B、Type C和Type D值的类别变量) -
员工数量: 商店的员工数量(整数变量) -
广告预算: 商店为广告分配的预算(以美元为单位的连续变量) -
日销售额: 商店一天内的销售额(以美元为单位的目标变量)
让我们看看这个用例的数据样本:
Store Type Number of Employees Advertising Budget Daily Sales
0 Type C 21 23117.964192 16195.682148
1 Type D 13 9017.567238 851.127834
2 Type A 37 39945.667889 19274.801963
3 Type C 24 34990.429063 14670.084345
4 Type C 17 11817.711027 6442.646360
理解了数据之后,我们可以继续进行该用例的目标。
用例目标
该用例的目标是使用目标编码来编码类别特征,以准备数据进行机器学习建模。让我们看看如何使用 scikit-learn 来完成这一操作。这个示例的代码可以在这里找到:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter10/3.target_encoding.py。
确保你已经安装并导入了在章节开头的 技术要求 部分中提到的库。完成这些后,我们开始吧:
-
让我们创建一个样本大小为 1000 的合成数据集:
np.random.seed(42) n_samples = 1000 -
生成一些随机数据:
data = { 'Store Type': np.random.choice(['Type A', 'Type B', 'Type C', 'Type D'], size=n_samples), 'Number of Employees': np.random.randint(5, 50, size=n_samples), 'Advertising Budget': np.random.uniform(1000, 50000, size=n_samples), 'Daily Sales': np.random.uniform(500, 20000, size=n_samples) } -
将数据放入 DataFrame:
df = pd.DataFrame(data) -
定义目标变量和特征:
X = df.drop(columns=['Daily Sales']) # Features y = df['Daily Sales'] # Target variable -
将数据拆分为训练集和测试集:
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) -
初始化一个
TargetEncoder类:target_encoder = TargetEncoder(cols=['Store Type']) -
在训练数据上进行拟合和转换:
X_train_encoded = target_encoder.fit_transform(X_train, y_train)
注意
在 GitHub 提供的本节代码中,我们使用数据和编码特征来训练随机森林回归模型并计算验证指标。如果你有兴趣,可以在这里查看代码文件:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter10/3.target_encoding.py。
这种编码技术有助于捕捉不同商店类型与每日销售之间的关系,因此,让我们来看一下编码后的输出。
编码后的输出
让我们来看一下编码后的数据:
Store Type Number of Employees Advertising Budget
29 10025.134200 37 43562.535230
535 10190.055174 12 1940.421564
695 10025.134200 14 47945.600526
557 10190.055174 23 19418.525972
836 10560.489044 27 35683.919764
让我们重点关注现在已被编码为数值的 Store Type 列。我们可以通过以下图表更详细地查看编码前后的差异:

图 10.4 – 编码前后商店类型的分布
在这种情况下,目标编码具有优势,因为它能有效地对类别特征进行编码,使其适用于回归任务(例如销售预测),同时避免了与独热编码相关的维度问题。
现在让我们讨论一些在使用目标编码器编码特征时需要注意的事项。
目标编码的注意事项
在对大数据集执行目标编码时,有几个重要事项需要注意:
-
过拟合:如果目标编码没有谨慎应用,或者某些类别只有少量样本,可能会导致过拟合。为了缓解这种情况,通常会使用平滑或添加正则化项等技术。
-
平滑(正则化):平滑涉及将每个类别的目标变量的均值与全局均值进行混合。这可以减少训练数据中极端值或噪声的影响。平滑目标编码的公式通常如下所示:
平滑均值 = (n * 类别均值 + m * 全局均值) / (n + m)
在这里,我们有以下内容:
-
n 是类别中的观察值数量。
-
m 是一个超参数,用于控制平滑的强度。
调整 m 的值可以控制正则化的水平。较小的 m 值给予类别实际均值更多的权重,而较大的 m 值则给予全局均值更多的权重。
-
交叉验证:在交叉验证的每个折叠内执行目标编码。这有助于确保编码基于一个独立于被预测数据的部分数据。交叉验证可以为每个类别提供更可靠的目标变量分布估计。
-
留一法编码:在这种方法中,你计算排除当前观察的类别的目标变量的均值。它可能更抗过拟合,因为它考虑了类别的效应,但不包括正在编码的实例的目标值。
-
添加噪声:向编码值中引入少量随机噪声有助于减少过拟合。这通常被称为贝叶斯目标编码。
-
要注意数据泄露问题。在训练数据集上计算均值至关重要,并将相同编码应用于验证和测试数据集。
-
仅在训练数据上计算编码统计信息:仅基于训练数据集计算编码统计信息(例如均值)。这确保模型在无偏信息上训练。
-
应用相同的编码到所有数据集:一旦在训练数据上计算了编码统计信息,预处理验证和测试数据集时应使用相同的编码。不要单独为这些数据集重新计算统计信息。
-
虽然目标编码可以提高模型性能,但可能会降低模型的可解释性,因为丢失了原始的分类值。
在探索目标编码之后,处理高基数分类特征的另一种有效技术是频率编码。频率编码用数据集中每个类别的频率或计数替换每个类别,这有助于捕捉每个类别的固有重要性并维持数据的整体分布。让我们深入了解频率编码及其在处理分类变量中的优势。
频率编码
频率编码,也称为计数编码,是一种通过在数据集中用每个类别的频率或计数替换每个类别的技术。在这种编码方法中,类别出现的频率越高,其编码值就越高。在某些情况下,频率编码可以是一种有价值的工具,因为类别出现的频率携带了有价值的信息。
何时使用频率编码
可以考虑在以下情况下使用频率编码:
-
信息频率:类别的频率或计数具有信息量,与目标变量直接或间接相关。例如,在客户流失预测问题中,客户购买产品的频率可能与其流失的可能性相关。
-
效率:您需要一种高效的编码方法,相比独热编码,它需要较少的计算资源和内存。
这种编码方法通常与基于树的模型(如决策树、随机森林和梯度提升树)配合良好,因为这些模型能有效捕捉编码频率与目标变量之间的关系。
用例 - 客户产品偏好分析
一家零售公司希望基于顾客的购买历史分析顾客的产品偏好。他们拥有一个包含顾客购买信息的数据集,其中包括他们最常购买的产品类别。
数据
在这个例子中,我们将对产品类别特征使用频率编码,以确定顾客最常购买的产品类别。这种编码方法可以帮助零售公司分析顾客偏好,并了解如何根据热门产品类别优化产品推荐或营销策略。
让我们来看一下样本数据集:
Customer ID Product Category Total Purchases
0 1 Electronics 5
1 2 Clothing 2
2 3 Electronics 3
3 4 Books 8
4 5 Books 7
5 6 Clothing 4
在理解了数据之后,我们可以进入用例的目标部分。
用例的目标
该用例的目标是使用频率编码对类别特征进行编码,以便为机器学习建模准备数据。让我们看看如何使用 scikit-learn 实现这一目标:
-
让我们创建一个样本数据集:
data = { 'Customer ID': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'Product Category': ['Electronics', 'Clothing', 'Electronics', 'Books', 'Books', 'Clothing', 'Electronics', 'Books', 'Clothing', 'Books'], 'Total Purchases': [5, 2, 3, 8, 7, 4, 2, 5, 1, 6] } df = pd.DataFrame(data) -
定义特征:
X = df[['Customer ID', 'Product Category', 'Total Purchases']] -
将数据拆分为训练集和测试集:
X_train, X_test = train_test_split(X, test_size=0.2, random_state=42) -
初始化一个
CountEncoder类,用于产品类别:count_encoder = CountEncoder(cols=['Product Category']) -
拟合并转换训练数据:
X_train_encoded = count_encoder.fit_transform(X_train)
该公司希望使用频率编码对这个类别特征进行编码,以了解哪些产品类别是顾客最常购买的。让我们来看一下编码后的数据。
编码后的输出
让我们看一下编码后的数据:
Customer ID Product Category Total Purchases
5 6 1 4
0 1 3 5
7 8 4 5
2 3 3 3
9 10 4 6
让我们重点关注产品类别特征,它现在根据频率被编码成数值。我们可以通过以下图表更详细地查看编码前后的差异:

图 10.5 – 编码前后产品类别的分布
第一个子图展示了编码前训练集中产品类别的分布。第二个子图展示了编码后训练集中编码的产品类别特征的分布。正如我们所看到的,产品类别列中的每个类别都被该类别在训练集中的频率计数所替代。
注意
频率编码保留了数据集中每个类别的出现频率信息。
现在让我们讨论一些在使用频率编码器进行特征编码时需要注意的事项。
频率编码的注意事项
在执行频率编码时,有几个重要的注意事项需要记住:
-
频率编码可能会导致过拟合,尤其是在数据集较小或某些类别观察样本很少的情况下。这是因为模型可能会过度依赖频率计数,而这些计数在新数据上可能无法很好地泛化。
-
当两个或多个类别具有相同的频率时,它们将得到相同的编码值。如果这些类别对目标变量有不同的影响,这可能会成为一个限制。
-
频率编码通常不适用于线性模型,因为它不会在编码值和目标变量之间创建线性关系。如果你使用的是对特征缩放敏感的线性模型,可能需要对编码值进行归一化处理,使它们具有相似的尺度。
总的来说,频率编码实现简单,不像独热编码那样扩展特征空间,因此在处理高基数特征时非常高效,不会创建过多的新列。
虽然频率编码在处理高基数特征时提供了简便和高效的方法,但另一种有效的技术是二进制编码。二进制编码将类别表示为二进制数字,提供比独热编码更紧凑的表示方式,并且保留了有序关系。让我们探讨一下二进制编码如何进一步增强类别变量的处理。
二进制编码
二进制编码是一种通过将每个类别转换为二进制代码来编码类别特征的技术。每个独特的类别都由一个独特的二进制模式表示,其中模式中的每个数字(0 或 1)对应于该类别的存在或缺失。二进制编码在处理高基数类别特征的同时减少维度,非常有用。
何时使用二进制编码
在以下情况下,可以考虑使用二进制编码:
-
降维:你希望在减少数据集维度的同时,仍然能够保留类别特征中的信息。在这种情况下,二进制编码特别有用。
-
高效性:你需要一种高效的编码方法,能够以紧凑的方式表示类别数据,并且易于被机器学习算法处理。
我们来看一下一个使用场景。
使用场景 —— 客户订阅预测
一个订阅服务提供商希望根据各种特征预测客户是否会订阅高级计划,其中包括具有高基数的国家特征。二进制编码将被用来高效地编码国家特征。
数据
我们来看一下样本数据集:
-
国家:这个类别特征表示客户所在的国家。它有助于了解地理位置是否会影响订阅状态。 -
年龄:这个数值特征表示客户的年龄。年龄在确定客户是否订阅某项服务的可能性中可能是一个重要因素。 -
收入:这个数值特征表示客户的年收入。收入可以反映客户是否有经济能力订阅某项服务。 -
订阅:这个二进制目标变量表示客户是否订阅了服务。我们希望通过其他特征来预测这个目标变量。
我们来看一下该使用场景的数据样本:
Country Age Income Subscription
0 USA 25 50000 1
1 Canada 30 60000 0
2 USA 35 70000 1
3 Canada 40 80000 0
4 Mexico 45 90000 1
国家的分布可以在以下图表中看到:

图 10.6 – 编码前后国家分布
用例目标
本分析的目标是根据客户的国家、年龄和收入预测订阅状态。我们对Country特征使用二进制编码,将其从分类变量转换为可以在机器学习算法中使用的数值格式。此用例的代码可以在这里找到:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter10/5.binary_encoding.py。
请按照以下步骤操作:
-
让我们创建一个示例数据集:
data = { 'Country': ['USA', 'Canada', 'USA', 'Canada', 'Mexico', 'USA', 'Mexico', 'Canada'], 'Age': [25, 30, 35, 40, 45, 50, 55, 60], 'Income': [50000, 60000, 70000, 80000, 90000, 100000, 110000, 120000], 'Subscription': [1, 0, 1, 0, 1, 0, 1, 0] } df = pd.DataFrame(data) -
对
Country特征应用二进制编码:encoder = BinaryEncoder(cols=['Country']) df_encoded = encoder.fit_transform(df) -
显示编码后的数据框:
print(df_encoded)
让我们来看一下编码后的数据。
编码输出
在这个例子中,应用了二进制编码到Country特征,正如我们在以下输出中看到的:
Country_0 Country_1 Age Income Subscription
0 0 1 25 50000 1
1 1 0 30 60000 0
2 0 1 35 70000 1
3 1 0 40 80000 0
4 1 1 45 90000 1
5 0 1 50 100000 0
6 1 1 55 110000 1
7 1 0 60 120000 0
正如我们从编码输出中看到的,二进制数字被拆分成了单独的列。让我们也来看一下编码后分布的变化:

图 10.7 – 国家编码特征分布
现在让我们讨论在使用二进制编码器进行特征编码时需要注意的一些事项。
二进制编码的注意事项
在执行二进制编码时,需要考虑几个重要事项:
-
二进制编码没有提供直接的可解释性。与每个二进制特征对应一个特定类别的独热编码不同,编码后的二进制模式可能没有明确的意义。
-
对于具有非常高基数的类别,二进制表示可能变得复杂,因为二进制数字的数量会随着类别数量的增加而对数增加。
-
一些机器学习算法,特别是线性模型,可能不适用于二进制编码特征。需要仔细评估算法的兼容性。
现在我们已经探讨了不同编码方法的细节,让我们转向总结它们的主要区别以及在机器学习工作流中的实际应用考虑事项。
总结
在本章中,我们探讨了用于编码分类变量的各种技术,这些技术对于机器学习任务至关重要。标签编码为每个类别分配唯一的整数,方法简单明了,但可能会不自觉地赋予没有顺序关系的类别以顺序性。独热编码将每个类别转换为二进制特征,保持了类别的独立性,但可能会导致高维数据集。二进制编码将分类值压缩成二进制表示,平衡了可解释性和效率,尤其适用于高基数数据集。频率编码通过用类别的出现频率替换类别,捕捉了关于分布模式的有价值信息。目标编码将目标变量的统计信息融入到分类编码中,提高了预测能力,但需要谨慎处理以避免数据泄漏。
让我们在下表中总结我们的学习:
| 编码方法 | 高基数 | 保留顺序信息 | 冲突 | 可解释性 | 适用于 | 不适用于 |
|---|---|---|---|---|---|---|
| 标签编码 | 好 | 是 | 否 | 中等 | 基于树的模型 | 线性模型 |
| 独热编码 | 差 | 否 | 否 | 高 | 线性模型,神经 网络(NNs) | 高基数特征 |
| 目标编码 | 好 | 否 | 可能 | 低 | 大多数算法 | 小数据集(存在过拟合风险) |
| 频率编码 | 好 | 否 | 可能 | 中等 | 基于树的模型 | 线性模型 |
| 二进制编码 | 好 | 部分 | 可能 | 低 | 基于树的模型 | 线性模型 |
表 10.2 – 所有编码技术的比较
每种方法根据数据集的特点和建模任务的具体要求提供不同的优势。在下一章中,我们将重点讨论分析时间序列数据时需要考虑的问题和方法。时间序列数据引入了时间依赖性,要求使用专门的特征工程技术,正如我们在下一章中将要展开的内容。
第十一章:消耗时间序列数据
在本章关于时间序列分析的内容中,我们将探索时间序列的基本概念、方法论以及在不同行业中的实际应用。时间序列分析涉及研究随时间收集的数据点,以识别模式和趋势并进行预测。
在本章中,我们将深入探讨以下主题:
-
理解时间序列数据的组成部分
-
时间序列数据的类型
-
识别时间序列数据中的缺失值
-
处理时间序列数据中的缺失值
-
分析时间序列数据
-
处理离群值
-
使用时间序列数据进行特征工程
-
在不同行业应用时间序列技术
技术要求
本章的完整代码可以在本书的 GitHub 仓库中找到,网址为 github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/tree/main/chapter11。
运行以下代码以安装我们在本章中将使用的所有必要库:
pip install pandas
pip install numpy
pip install matplotlib
pip install statsmodels
理解时间序列数据的组成部分
时间序列数据是指一系列在时间上收集和记录的观察值或测量值。与非顺序数据不同,后者的观察值是在单一时间点采集的,时间序列数据则是在多个时间点按顺序捕捉信息。时间序列中的每个数据点都与特定的时间戳相关联,从而形成一个时间结构,允许分析随时间变化的趋势、模式和依赖关系。接下来,我们将讨论时间序列数据的不同组成部分,从趋势开始。
趋势
趋势组件表示数据中的长期变化或方向。它反映了一个持续较长时间的整体模式,指示值是普遍增加、减少,还是相对保持恒定。
趋势具有以下特征:
-
上升趋势:数值随时间系统性增加
-
下降趋势:数值随时间系统性减少
-
平稳趋势:数值在时间上保持相对恒定
确定趋势对做出关于所观察现象长期行为的明智决策至关重要。它提供了整体方向的洞察,并且对预测未来趋势具有重要价值。在接下来的部分中,我们将呈现一个灵感来自数据世界的用例,重点关注趋势组件。
分析长期销售趋势
在这个案例中,我们旨在分析十年来的销售趋势,以了解 2010 到 2020 年间企业的增长模式。你可以在本书的 GitHub 仓库中找到此示例的代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter11/1.decomposing_time_series/trend.py。让我们开始吧:
-
我们将首先生成一个日期范围,并为每个月生成相应的销售数据,覆盖 10 年:
date_rng = pd.date_range(start='2010-01-01', end='2020-12-31', freq='M') sales_data = pd.Series(range(1, len(date_rng) + 1), index=date_rng) -
接下来,我们必须绘制数据以可视化上升趋势:
plt.figure(figsize=(10, 5)) plt.plot(sales_data, label='Sales Data') plt.title('Time Series Data with Trend') plt.xlabel('Time') plt.ylabel('Sales') plt.legend() plt.show()这将产生以下图表:

图 11.1 – 具有上升趋势的月度销售数据
图 11.1 显示了十年来销售额的持续上升趋势。这表明业务一直在稳步增长。
在我们的初步分析中,我们集中在理解销售数据中十年间的总体上升趋势。这为我们提供了有关企业长期增长的宝贵洞察。
通常,企业会经历在特定时间段内定期出现的波动,如月份或季度。这被称为季节性。识别这些季节性模式和理解整体趋势同样重要,因为它可以帮助企业预测并为高需求或低需求时期做好准备。为了说明这一点,我们将扩展我们的分析,加入销售数据中的季节性因素。
季节性
季节性 是指在时间序列中定期出现的重复且可预测的模式。这些模式通常对应于特定的时间段,如天、月或季节,并且可能受外部因素如天气、假期或文化事件的影响。
与长期趋势不同,季节性跨越较短的时间框架,对数据产生短期影响。季节性的这种周期性特征使得企业能够预测并规划需求波动,从而优化其运营和战略。
重要提示
了解季节性有助于识别重复出现的模式,并预测某些行为或事件可能发生的时间。这些信息对于准确的预测和规划至关重要。
在接下来的部分,我们将扩展之前提出的销售案例,同时关注季节性因素。
分析带有季节性的长期销售趋势
在这个用例的这一部分,我们旨在分析包括季节性变化的十年销售趋势。你可以在这里找到完整的代码示例:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter11/1.decomposing_time_series/seasonality.py。让我们开始吧:
-
我们将从为每个月生成一个日期范围,并相应地生成销售数据开始,覆盖 10 年:
date_rng = pd.date_range(start='2010-01-01', end='2020-12-31', freq='M') seasonal_data = pd.Series([10, 12, 15, 22, 30, 35, 40, 38, 30, 22, 15, 12] * 11, index=date_rng) -
然后,我们必须绘制数据,以可视化季节性成分:

图 11.2 – 带有季节性因素的月度销售数据
图 11.2 显示了每 12 个月重复的模式,表明销售中存在明显的季节性。销售在年中达到高峰,并在年末和年初下降,暗示着夏季的销售较高,冬季的销售较低。这一模式在多年中的一致性有助于预测未来的销售周期。了解这些季节性趋势对库存管理、营销活动和在销售高峰期的资源分配非常有价值,帮助企业相应优化策略。
虽然识别趋势和季节性提供了对销售模式的宝贵见解,但现实世界的数据通常还包含另一个关键成分:噪声。在接下来的部分,我们将深入探讨噪声,并扩展销售用例,以探索噪声如何影响销售。
噪声
噪声,也称为残差或误差,代表时间序列数据中无法归因于趋势或季节性的随机波动或不规则性。它反映了数据中的变化性,这些变化性无法通过基础模式来解释。
重要说明
虽然噪声通常被认为是不需要的,但它是任何现实世界数据的自然组成部分。识别并隔离噪声对于构建准确的模型和理解时间序列中固有的不确定性至关重要。
在接下来的部分,我们将扩展前面介绍的销售用例,并重点关注噪声。
分析带噪声的销售数据
在这个用例中,我们旨在分析包含噪声的销售数据,除了趋势和季节性因素外。这将帮助我们理解随机波动如何影响我们识别潜在模式的能力。要跟随这个示例,请查看以下代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter11/1.decomposing_time_series/noise.py。让我们开始吧:
-
让我们导入所需的库:
import pandas as pd import matplotlib.pyplot as plt -
我们将从生成一个覆盖 10 年的每月日期范围开始:
date_rng = pd.date_range(start='2010-01-01', end='2020-12-31', freq='M') -
然后,我们必须创建带噪声的销售数据:
np.random.seed(42) noise_data = pd.Series(np.random.normal(0, 2, len(date_rng)), index=date_rng) -
现在,我们必须绘制数据以可视化噪声:

图 11.3 – 带噪声的每月销售数据
图 11.3 显示了随机、不可预测的变化,这些变化没有遵循任何特定模式。这些波动发生在短时间内,导致数据的不稳定,使得更难看出任何模式。
现在我们可以识别不同的时间序列组件,让我们来看看不同类型的时间序列。
时间序列数据的类型
在本节中,我们将简要回顾时间序列数据的类型——单变量和多变量——同时阐明它们的区别,并展示它们的应用。
单变量时间序列数据
单变量时间序列数据由单个变量或观察值在时间上记录而成。它是一个一维的按时间顺序排列的序列,相较于多变量时间序列数据,它更易于分析。
考虑一个单变量时间序列,表示一个城市多年来每月的平均温度。你可以在这里找到完整的代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter11/2.types/univariate.py。
让我们生成我们的单变量时间序列数据:
-
首先,我们将创建我们需要的数据范围,在这个例子中是从
2010-01-01到2020-12-31:date_rng = pd.date_range(start='2010-01-01', end='2020-12-31', freq='M') -
然后,我们必须通过使用正态分布(也称为高斯分布)添加噪声,来创建温度的相应值:
temperature_data = pd.Series(np.random.normal(20, 5, len(date_rng)), index=date_rng)让我们理解值参数:
-
20:这是5:这是正态分布的标准差。噪声值通常会围绕均值波动约±5 个单位。较大的标准差意味着噪声会更分散,而较小的标准差意味着噪声值更接近均值。 -
我们之前创建的日期范围被作为索引传递给数据框。
-
-
现在,让我们绘制单变量时间序列数据:
plt.figure(figsize=(10, 5)) plt.plot(temperature_data, label='Temperature Data') plt.title('Univariate Time Series Data') plt.xlabel('Time') plt.ylabel('Temperature (°C)') plt.legend() plt.show()这将输出以下图表:

图 11.4 – 单变量温度数据
在这个例子中,单变量时间序列代表了每月的平均温度。由于数据是随机生成的,均值为 20°C,并有一定的波动(标准差为 5°C),因此图表将表现出围绕该平均温度的随机波动。
理解单变量时间序列数据的复杂性为深入研究多变量时间序列分析打下了坚实的基础。与单变量数据只观察单一变量随时间的变化不同,多变量时间序列数据涉及同时监测多个相互关联的变量。
多元时间序列数据
多元时间序列数据涉及多个变量或观察值,这些变量或观察值是随着时间记录的。每个变量都是一个按时间顺序排列的序列,并且这些变量可能是相互依赖的,从而捕捉到更复杂的关系。
考虑一个多元时间序列,表示一个城市在多年中的月平均温度和月降水量。你可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter11/2.types/multivariate.py找到这个示例的代码。让我们开始吧:
-
让我们为这个示例添加所需的库:
import pandas as pd import matplotlib.pyplot as plt import numpy as np -
现在,让我们通过使用之前创建的温度数据并在同一个 DataFrame 中添加一条新的时间序列数据(表示降水量数据,具有不同的均值和标准差值),来生成一个多元时间序列数据示例:
date_rng = pd.date_range(start='2010-01-01', end='2020-12-31', freq='M') temperature_data = pd.Series(np.random.normal(20, 5, len(date_rng)), index=date_rng) rainfall_data = pd.Series(np.random.normal(50, 20, len(date_rng)), index=date_rng) -
将所有时间序列合并到同一个 DataFrame 中,确保包含温度和降水量数据:
multivariate_data = pd.DataFrame({'Temperature': temperature_data, 'Rainfall': rainfall_data}) print(multivariate_data.head()) -
合并后的时间序列 DataFrame 如下所示:
Temperature Rainfall 2010-01-31 19.132623 56.621393 2010-02-28 18.551274 51.249927 2010-03-31 24.502358 65.679049 2010-04-30 27.069077 73.044307 2010-05-31 21.176376 41.317497 -
最后,让我们绘制多元时间序列数据:

图 11.5 – 多元数据
在这个示例中,多元时间序列包括温度和降水量数据,提供了一个更全面的环境条件视角。
总体而言,单变量数据较易处理,而多元数据使我们能够捕捉到随时间变化的变量之间更复杂的关系和依赖性。多元分析在解决经济学、金融、环境科学和医疗健康等各个领域的现实挑战时至关重要,在这些领域中,理解变量之间的多方面关系至关重要。
现在我们对时间序列数据有了较强的理解,可以探索有效清理和管理这种数据的方法。
识别时间序列数据中的缺失值
识别时间序列数据中的缺失值有点类似于识别其他类型数据中的缺失值,但由于时间序列的时间性特征,存在一些特定的注意事项。由于我们在第八章《检测和处理缺失值和异常值》中讨论过其中的一些技术,让我们在这里总结它们,并重点说明这些技术在分析时间序列数据时的具体应用,使用股票市场分析作为示例。
假设我们有某公司多年来每日的股价数据(开盘价、最高价、最低价和收盘价)。我们的目标是识别这些时间序列中的缺失数据,以确保数据集的完整性。你可以在这里找到该示例的代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter11/3.missing_values/1.identify_missing_values.py。
让我们从生成数据开始:
-
首先,我们将生成从 2020 年 1 月 1 日到 2023 年 12 月 31 日的工作日日期范围。这里,
freq='B'用于生成仅包括工作日(即排除周末)的日期范围:date_range = pd.date_range(start='2020-01-01', end='2023-12-31', freq='B') # Business days -
接下来,我们必须为日期范围生成随机股价,长度为n:
n = len(date_range) data = { 'open': np.random.uniform(100, 200, n), 'high': np.random.uniform(200, 300, n), 'low': np.random.uniform(50, 100, n), 'close': np.random.uniform(100, 200, n) } -
接下来,我们必须通过传递在上一步创建的所有单独数据点来创建一个 DataFrame:
df = pd.DataFrame(data, index=date_range) -
现在,让我们引入随机的 NaN 值,以模拟数据中的一些缺失值:
nan_indices = np.random.choice(n, size=100, replace=False) df.iloc[nan_indices] = np.nan -
然后,随机丢弃一些日期,以模拟缺失的时间戳:
missing_dates = np.random.choice(date_range, size=50, replace=False) -
最后,显示 DataFrame 的前几行:
open high low close 2020-01-01 137.454012 262.589138 55.273685 183.849183 2020-01-02 195.071431 288.597775 82.839005 180.509032 2020-01-03 173.199394 261.586319 91.105158 182.298381 2020-01-06 159.865848 223.295947 69.021000 193.271051 2020-01-07 NaN NaN NaN NaN
这里需要注意的关键点是,我们有两种缺失数据:
-
完整的行缺失,因此没有完整的日期索引可用
-
当前日期的某些列中的部分观测值缺失
我们将在这里主要处理第一种情况,因为第二种情况在*第八章**《检测和处理缺失值与异常值》中已经讲过了。让我们从简单而有效的isnull()方法开始。
检查 NaN 或空值
与常规数据集不同,时间序列数据点按时间顺序排列。缺失的值可能会破坏数据的连续性,影响趋势和季节模式的分析。我们可以使用isnull()方法来识别缺失的时间戳。这里,我们要查找数据集中缺失的完整行:
-
要检查时间序列 DataFrame 中哪些日期缺失,我们需要创建一个完整的日期范围(没有缺失值),并且该日期范围的频率与当前 DataFrame 索引的频率一致,然后将其与当前 DataFrame 中的日期范围进行对比。这里,我们正在为工作日创建一个完整的日期范围:
complete_index = pd.date_range(start=df.index.min(), end=df.index.max(), freq='B') -
为了快速查看缺失的索引点,必须将 DataFrame 重新索引到这个完整的日期范围,以便识别任何缺失的时间戳:
df_reindexed = df.reindex(complete_index) -
现在,我们可以使用
isnull()方法来识别任何缺失的时间戳:missing_timestamps = df_reindexed[df_reindexed.isnull().any(axis=1)]
在这里,我们可以看到数据中有一些缺失的时间戳:
print(f"\nPercentage of Missing Timestamps: {missing_timestamps_percentage:.2f}%")
Percentage of Missing Timestamps: 14.09%
到目前为止的分析告诉我们,我们的数据集中缺失了完整的日期。现在,让我们添加一些可视化图表,帮助我们更好地看到数据中的空白。
注意
如在第八章**,检测与处理缺失值和异常值中所述,你可以使用isnull()方法查看每列中缺失的数量——例如,missing_values = df.isnull().sum()。
目视检查
可视化数据有助于我们识别缺失值及缺失模式。图表能够揭示数据中的缺口,而这些缺口在表格检查中可能不易察觉。
继续前一部分的例子,让我们绘制时间序列数据并在图表上标出任何缺失值:
-
绘制闭盘价格图:
plt.figure(figsize=(14, 7)) plt.plot(df.index, df['close'], linestyle='-', label='Closing Price', color='blue') -
用垂直线标记缺失的时间戳:
for date in missing_dates: plt.axvline(x=date, color='red', linestyle='--', linewidth=1) plt.title('Daily Closing Prices with Missing Timestamps and NaN Values Highlighted') plt.xlabel('Date') plt.ylabel('Closing Price') plt.legend() plt.grid(True) plt.show()这将生成以下图表:

图 11.6 – 日闭盘价及缺失时间戳高亮显示
在图 11.6中,闭盘价格用蓝色标记显示,而缺失的时间戳用虚线高亮,便于识别数据中的缺口。现在,让我们探讨最后一种方法,称为滞后分析。在这种方法中,我们创建一个滞后的序列版本,并与原始数据进行比较,以检测不一致之处。
注意
在第三章,数据剖析 – 理解数据结构、质量和分布中,我们演示了多种数据剖析方法。你可以通过使用内建的缺口分析功能,将类似的方法应用于时间序列数据。只需在创建报告时传递tsmode=True即可——例如,profile = ProfileReport(df, tsmode=True)。
随着我们深入,探索有效的时间序列缺失数据处理策略变得至关重要。
处理时间序列数据中的缺失值
缺失值是时间序列数据中常见的问题,可能由于多种原因产生,比如传感器故障、数据传输问题,或只是记录观察值的缺失。如我们所讨论的,通常会出现两种主要情况:
-
某些特征中的空值:想象一下股票市场分析,其中收集了每日交易数据。虽然所有交易日都已记录,但某些日子的成交量可能由于报告错误而缺失。这种情况带来了一个挑战:如何在确保分析保持稳健的同时,保持数据集的完整性?
-
完整行缺失:相反,考虑一个天气监测系统,它记录每日气温。如果某些完整天的数据缺失——可能是由于传感器故障——这就构成了一个重大问题。缺失的时间戳意味着你不能简单地填充数据;这些天的数据缺失会打乱整个时间序列。
在下一部分中,我们将重点解决第一种情况,考虑某些特征中缺失值的存在。完成这一步后,我们可以调整方法来处理第二种情况。
删除缺失数据
删除缺失数据是一个直接的方法,但应该谨慎操作,并考虑其对整体数据集的影响。以下是一些可能适合删除数据的场景:
-
如果缺失值占数据集的比例很小(例如,少于 5%),删除它们可能是可行的。如果数据丢失不会显著影响分析结果或从数据集得出的结论,这种方法效果较好。例如,在一个包含 10,000 个时间点的数据集中,如果有 50 个时间点缺失,删除这 50 个数据点(占数据的 0.5%)可能不会显著影响整体分析。
-
如果插补缺失值会引入过多的不确定性,特别是当这些值非常重要且无法准确估计时。这种情况通常出现在缺失值是高度不可预测的数据时,插补结果不可靠。
-
如果缺失值完全是随机发生的,并且没有遵循任何系统的模式。例如,传感器数据中偶尔出现的随机故障导致缺失读数,但这些故障没有任何潜在的规律。
让我们重新审视股票市场的使用案例,看看如何删除 null 值,并观察这对数据集的影响。
删除缺失数据在股票市场使用案例中的应用
在我们的股票价格数据场景中,我们将添加一些 NaN 值,并评估删除这些值的影响。你可以在这里找到完整的代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter11/3.missing_values/2.remove_missing_values.py。让我们开始吧:
-
继续使用上一节的示例,我们将创建具有不同特征的股票数据。然后,我们将从特定特征(例如,
close和open)中随机选择一些索引,以便将该索引的每个特征的值映射为 NaN 值:nan_indices_close = np.random.choice(df.index, size=50, replace=False) nan_indices_open = np.random.choice(df.index, size=50, replace=False) -
然后,我们将之前随机选择的索引映射为 NaN 值:
df.loc[nan_indices_close, 'close'] = np.nan df.loc[nan_indices_open, 'open'] = np.nan -
让我们检查数据中有多少 NaN 或 null 值:
missing_values = df.isnull().sum() Percentage of Missing Values in Each Column: open 4.793864 high 0.000000 low 0.000000 close 4.793864正如预期的那样,
open和close特征中引入了一些 null 值。在删除数据集中的任何行之前,让我们先检查一下包含 null 值的数据行数:
print(f"\nNumber of rows before dropping NaN values: {len(df)}") Number of rows before dropping NaN values: 1043 -
在这个阶段,我们将删除在
close或low列中包含 NaN 值的任何行:df_cleaned = df.dropna() print(f"\nNumber of rows after dropping NaN values: {len(df_cleaned)}") ---- Number of rows after dropping NaN values: 945 -
让我们绘制时间序列数据:

图 11.7 – 删除/标记缺失数据后的每日收盘价格
如图 11.7所示,原始的收盘价已被绘制,因缺失值被丢弃的点通过红色“x”标记突出显示。请记住,即使是选择性丢弃,删除行也可能会导致有用信息的丧失,因为它减少了样本大小,从而可能降低分析的统计功效并影响结果的普适性。
在需要保留每个时间戳,但需要解决特征中的缺失值的场景中,前向和后向填充提供了实用的解决方案。这些方法允许我们保持时间序列数据的时间顺序完整性,同时根据相邻的观测值高效地填补缺失值。让我们探索一下前向和后向填充如何有效地处理时间序列分析中的缺失数据。
前向填充和后向填充
Forward fill(ffill)和backward fill(bfill)是通过将最后已知值向前传播或将下一个已知值向后传播来填补缺失值的两种方法,分别用于时间序列中的缺失数据。
在处理时间序列反向填充时,选择ffill和bfill之间的方式取决于多个因素和使用场景。以下是何时使用每种方法的概述,以及做出这些决策时的思考过程:
-
Ffill:前向填充,也称为最后观测值向前填充(LOCF),是将最后已知值向前传播以填补缺失的数据点。
这是你应该使用它的情况:
-
当你认为最近已知的值是预测未来缺失值的最佳依据时
-
在金融时间序列中,将最后已知价格向前传播通常是一个合理的假设
-
在处理缓慢变化的变量时,如果假设其持续性较好
-
在你希望保持最近状态,直到新的信息变得可用时
如果你仍然不确定,或者在思考该使用哪种方法时,回答以下三个问题中的至少两个“是”将帮助你做出正确的决策:
-
该变量是否可能在短时间内保持相对稳定?
-
使用最后已知值作为缺失数据的合理假设吗?
-
是不是更重要的是反映最近已知的状态,而不是潜在的未来变化?
-
-
Bfill:与此相反,后向填充是将下一个已知值向后传播以填补缺失的数据点。
这是你应该使用它的情况:
-
当你对未来的值比过去的值更有信心时
-
在你希望将已知的结果追溯性地应用于之前缺失的时间段时
-
当你处理滞后效应时,未来的事件会影响过去的缺失数据
-
在你希望将数据与下一个已知状态对齐,而不是与之前的状态对齐时
如果你仍然不确定,或者在思考该使用哪种方法时,回答以下问题中的“是”将帮助你做出正确的决策:
-
下一个已知值是否更有可能代表缺失数据而不是前一个已知值?
-
您是否处理一种情况,即未来信息应该通知过去的缺失值?
-
是否与下一个已知状态对齐能为您的分析提供更有意义的见解?
-
在实践中,选择 ffill 和 bfill 通常需要结合领域专业知识、对数据生成过程的理解以及考虑特定分析目标。同时,值得尝试两种方法并比较结果,看哪一种为您的特定用例提供更有意义和准确的见解。
使用 ffill 和 bfill 处理时间序列数据时,始终有一些重要的考虑因素。让我们扩展一下:
-
顺序性质:时间序列数据的顺序性质对于 ffill 和 bfill 方法确实至关重要。这两种方法都依赖于相邻数据点相关的假设,这是时间序列分析的基础。
-
Ffill 和上升趋势:Ffill 可适用于上升趋势,因为它向前延续最后已知的值,可能在上升趋势中低估真实值。然而,在强烈上升趋势中可能会导致“阶梯”效应,可能低估增长率。
-
Bfill 和下降趋势:Bfill 可适用于下降趋势,因为它会拉回未来更低的值,可能在下降趋势中高估真实值。在强烈下降趋势中可能会产生类似的“阶梯”效应,可能会夸大下降率。
-
在选择 ffill 和 bfill 时,应考虑趋势的方向以及缺失数据期间的强度和长度。对于微妙的趋势,任一方法都可能适用,选择可能更多地取决于其他因素,如数据的性质或具体的分析目标。
-
如果缺失值与周围数据点不一致,这两种方法确实可能传播错误。对于长时间的缺失数据,填充值可能会显著偏离真实的基础模式。
-
处理异常值:如果异常值在一段缺失数据之前或之后,ffill 或 bfill 可能会传播这种异常值,扭曲系列。
-
数据连续性的假设:这两种方法都假设缺失的数据可以通过相邻的已知值合理地逼近,但这并不总是正确的。对于可能突然改变或存在不连续性的变量,这些方法可能不适用。
让我们重新看一下股票价格的例子,并看看如何填补空值列。
在股市使用案例中填充空值
在这个示例中,我们将不关注缺失的索引,只关注一些特征中缺失的数据。让我们深入研究代码——和往常一样,你可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter11/3.missing_values/3.back_forward_fill.py找到完整的端到端代码:
-
这段代码将随机缺失值引入 DataFrame(
df)的close和open列。它首先使用np.random.choice从 DataFrame 的索引中随机选择 50 个索引。选中的索引存储在两个变量nan_indices_close和nan_indices_open中,这些变量对应于缺失值将被插入的行:nan_indices_close = np.random.choice(df.index, size=50, replace=False) nan_indices_open = np.random.choice(df.index, size=50, replace=False) -
以下代码使用
.loc访问器在nan_indices_close指定的索引处将NaN赋值给close列,类似地,在nan_indices_open指定的索引处将NaN赋值给open列。实际上,这将在这两列中创建 50 个随机的缺失值,这对于模拟真实世界数据场景或测试数据处理技术非常有用:df.loc[nan_indices_close, 'close'] = np.nan df.loc[nan_indices_open, 'open'] = np.nan -
使用 ffill 和 bfill 填充 NaN 值:
df['close_ffill'] = df['close'].ffill() # Forward Fill df['close_bfill'] = df['close'].bfill() # Backward Fill -
让我们来看一下结果:
print(df[['open', 'close', 'close_ffill', 'close_bfill']].head(20)) # Show first 20 rows这将显示以下输出:
open close close_ffill close_bfill 2020-01-01 137.454012 183.849183 183.849183 183.849183 2020-01-02 195.071431 180.509032 180.509032 180.509032 2020-01-03 173.199394 182.298381 182.298381 182.298381 2020-01-06 159.865848 193.271051 193.271051 193.271051 2020-01-07 115.601864 NaN 193.271051 120.028202 2020-01-08 115.599452 120.028202 120.028202 120.028202 2020-01-09 105.808361 161.678361 161.678361 161.678361 2020-01-10 186.617615 174.288149 174.288149 174.288149 2020-01-13 160.111501 173.791739 173.791739 173.791739 2020-01-14 170.807258 152.144902 152.144902 152.144902 2020-01-15 102.058449 NaN 152.144902 137.111294 2020-01-16 196.990985 137.111294 137.111294 137.111294
正如我们所见,在2020-01-07和2020-01-15,close列中有缺失值(NaN)。这表示这两个日期的收盘价没有被记录或无法获取。
如我们所学,ffill 方法(close_ffill)通过最后一个有效观测值填充缺失值:
-
对于
2020-01-07,收盘价使用来自2020-01-06的最后一个已知值(193.27)进行填充。 -
对于
2020-01-15,缺失值使用来自2020-01-14的最后一个有效价格(152.14)进行填充。
另一方面,bfill 方法(close_bfill)通过下一个有效观测值填充缺失值:
-
对于
2020-01-07,由于没有立即记录下一个有效价格,它使用2020-01-08的收盘价(120.03)进行填充。 -
对于
2020-01-15,该值使用来自2020-01-16的下一个已知价格进行填充。
让我们仔细看看在执行不同填充方法后,数据发生了什么变化:
-
在
2020-01-07,ffill 方法相比 bfill 方法高估了缺失值,bfill 则与下一个已知值更为接近。 -
在
2020-01-15,ffill 和 bfill 提供了不同的估算结果,ffill 可能高估了该值,而 bfill 则较为准确。
一般建议,我们需要调查缺失值的模式。如果缺失值是随机且稀疏的,任一方法可能都合适。然而,如果存在系统性的模式,可能需要更复杂的插值方法,例如插值。插值允许我们通过利用数据集中的现有值来估算缺失的数据点,提供了一种更为细致的方式,能够捕捉随时间变化的趋势和模式。接下来我们将更详细地讨论这一点。
插值
插值是一种通过根据周围数据点填补缺口来估算缺失值的方法。与前向填充(ffill)和后向填充(bfill)不同,后者是复制现有值,插值使用数学技术来估算缺失值。插值有不同的技术和应用。所以,让我们看一下可用的选项及其考虑因素:
-
线性插值:线性插值通过一条直线连接两个相邻的已知数据点,并沿此线估算缺失值。它是最简单的插值形式,假设数据点之间存在线性关系。它适用于数据点之间的变化预计为线性或近似线性的情况。常用于金融数据、温度读数和其他预期逐渐变化的环境数据中。
-
多项式插值:多项式插值将一个多项式函数拟合到已知的数据点,并使用这个函数来估计缺失值。更高阶的多项式可以捕捉数据点之间更复杂的关系。它适用于具有非线性趋势的数据集,通常用于科学和工程应用中,其中数据遵循多项式趋势。
-
样条插值:样条插值使用分段多项式,通常是三次样条,来拟合数据点,确保数据点的平滑性,并通过数据提供平滑的曲线。它适用于需要数据点之间平滑过渡的数据集,常用于计算机图形学、信号处理和环境数据中。
让我们在我们的用例中使用插值。
股票市场用例中的插值
考虑到之前提到的同一时间序列数据集,其中存在缺失值。在这种情况下,我们希望使用不同的插值方法来填补这些缺失值。你可以在本书的 GitHub 仓库中找到完整的代码示例:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter11/3.missing_values/4.interpolation.py。让我们开始吧:
-
以下代码将随机缺失值引入我们的数据框(
df)中的close和open列,就像在上一节中所做的那样:nan_indices_close = np.random.choice(df.index, size=50, replace=False) nan_indices_open = np.random.choice(df.index, size=50, replace=False) df.loc[nan_indices_close, 'close'] = np.nan df.loc[nan_indices_open, 'open'] = np.nan -
以下代码行用于通过线性插值填充我们的 DataFrame(
df)中close列的缺失值。该代码特别使用线性插值,其中通过在缺失值前后最近的已知数据点之间画一条直线来估算缺失值:df['close_linear'] = df['close'].interpolate(method='linear') -
我们可以通过将方法参数更改为
method='polynomial'来使用多项式插值填充缺失值。这指定插值应使用order=3的多项式函数。order参数表示要使用的多项式的次数。在这种情况下,使用三次多项式(三次方),意味着估算缺失值的函数将是一个曲线,可能比简单的直线(如线性插值)提供更好的拟合,以适应更复杂的数据趋势:df['close_poly'] = df['close'].interpolate(method='polynomial', order=3) -
我们可以通过将方法更改为
method='spline'来使用样条插值填充缺失值。这指定插值应使用样条插值,这是一种分段的多项式函数,确保数据点处的平滑性。order=3参数表示每段样条使用的多项式的次数。在这种情况下,使用三次样条(第三次多项式),意味着插值将涉及拟合三次多项式到数据的各个段落:df['close_spline'] = df['close'].interpolate(method='spline', order=3) -
现在,让我们绘制插值后的数据:

图 11.8 – 日闭盘价插值
在图 11.8中,我们可以看到不同插值方法下数据的变化。为了更好地理解这些差异,让我们看看插值后的实际数据,如图 11.9所示:

图 11.9 – 日闭盘价插值表
让我们比较不同的插值方法并得出一些结论:
在 2020-01-07,我们有以下数据:
-
线性插值:156.649626
-
多项式插值:142.704592
-
样条插值:143.173016
在 2020-01-15,我们有以下数据:
-
线性插值:144.628098
-
多项式插值:127.403857
-
样条插值:128.666028
根据这些数据,线性插值似乎提供了比多项式和样条插值更高的估计值。它假设数据点之间存在线性趋势,这对于非线性数据可能并不准确。多项式插值似乎提供了较低的估计值并能够捕捉更复杂的关系,但也容易过拟合。最后,样条插值提供了平滑的估计值,介于线性插值和多项式插值之间,提供了简单性与准确性之间的平衡。在这种具体情况下,我们会选择样条插值,因为它提供了一条平滑的曲线,避免了突变,结果更现实,更接近数据中预期的趋势。虽然基于提供的数据推荐使用样条插值,但验证插值结果与已知数据点或领域知识的符合性仍然是至关重要的。
注意
插值方法,如线性插值、多项式插值和样条插值,也可以用来处理时间序列数据中的异常值。
选择和调整插值参数来填充缺失值,需要理解数据的特征和分析的具体需求。对于具有线性趋势的简单数据,线性插值既高效又有效。然而,如果数据表现出非线性模式,多项式插值可能提供更好的拟合,且多项式的阶数(order)会影响曲线的复杂度;较低的阶数适用于简单趋势,而较高的阶数可能能捕捉更多的细节,但也有过拟合的风险。样条插值提供了一种平滑而灵活的方法,立方样条(order=3)因其平滑性和灵活性而被广泛使用。调优这些方法时,可以从较简单的方法开始,逐步测试更复杂的方法,同时监控过拟合现象,并确保拟合与数据的潜在趋势一致。采用交叉验证、视觉检查和统计指标来评估和优化插值选择。
现在我们已经探讨了时间序列中处理缺失数据的各种技术,接下来总结不同的填充方法是非常重要的,以便理解它们独特的应用和有效性。
比较不同的缺失值处理方法
处理时间序列数据中的缺失值是一个复杂的过程,需要仔细考虑数据集的具体背景和特征。决定是丢弃值、使用 bfill,还是应用插值,应根据对后续分析影响的仔细评估以及保留时间序列中关键信息的需要来指导。下表总结了不同的技术,并可作为指导:
| 方法 | 使用时机 | 优点 | 缺点 |
|---|---|---|---|
| 填充缺失值 | 小比例的缺失值 | - 简单性- 避免插值不确定性 | - 信息丢失- 潜在偏差 |
| 向后填充(Bfill) | 缺失值预计之前有一致的值 | - 保留总体趋势- 适用于递增趋势 | - 如果缺失值与随后的值不同,可能会传播错误 |
| 向前填充(Ffill) | 缺失值预计遵循一致的值 | - 实现简单- 保持最近状态直到新数据可用 | - 如果趋势变化,可能会误导数据- 如果缺失值与之前的值不同,则会传播错误 |
| 线性插值 | 缺失值需要根据相邻的数据点进行估算 | - 实现简单易懂- 保留整体趋势 | - 可能无法捕捉非线性趋势- 对离群值敏感 |
| 多项式插值 | 缺失值需要通过更复杂的关系进行估算 | - 捕捉复杂关系- 对多项式阶数具有灵活性 | - 可能导致过拟合和振荡- 计算量大 |
| 样条插值 | 缺失值需要通过平滑过渡来估算 | - 提供平滑曲线- 避免高阶多项式的振荡 | - 实现较为复杂- 计算量大 |
表 11.1 – 不同时间序列缺失数据处理方法的比较
在研究了填充时间序列数据缺失值的各种方法之后,另一个同样重要的方面是:时间序列与其自身滞后值的相关性。
时间序列数据分析
自相关和偏自相关是时间序列分析中的关键工具,它们提供了数据模式的洞察并指导模型选择。在离群值检测中,它们有助于区分真实的异常和预期的变动,从而实现更准确、更具上下文感知的离群值识别。
自相关和偏自相关
自相关是指将时间序列与其自身滞后值进行相关分析。简而言之,它衡量时间序列中每个观察值与其过去观察值的关系。自相关是理解时间序列数据中存在的时间依赖性和模式的关键概念。
偏自相关函数(PACF)是时间序列分析中的一种统计工具,用于衡量在去除中间滞后效应后,时间序列与其滞后值之间的相关性。它提供了一个更直接的衡量不同时间点观察值之间关系的方式,排除了较短滞后效应的间接影响。
自相关和偏自相关在以下情况中有帮助:
-
时间模式:它们有助于识别随时间重复的模式。这对于理解时间序列数据的固有结构至关重要。
-
平稳性评估:它们有助于评估时间序列的平稳性。缺乏平稳性可能会影响统计分析的可靠性以及模型预测的准确性。
-
模型的滞后选择:它们指导时间序列模型中适当滞后的选择,如自回归(AR)分量在自回归滑动平均(ARIMA)模型中的应用。
-
季节性检测:在特定滞后期的自相关函数(ACF)图中出现显著峰值,表明存在季节性,为进一步分析提供了线索。
-
异常检测:自相关函数中出现不寻常的模式可能表明数据中存在异常值或离群点,需要进一步调查和清理。
现在,让我们对来自股票价格数据集的close_filled序列进行 ACF 和 PACF 分析。此分析将帮助我们确定适当的参数(p和q),以便在接下来的部分中进行 ARIMA 建模。
股票市场案例中的 ACF 和 PACF
我们将继续使用到目前为止的示例,并添加 ACT 和 PACF 图表。像往常一样,您可以查看完整代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter11/4.analisis/autocorrelation.py。让我们开始吧:
-
创建自相关图:
plot_acf(df['close'].dropna(), lags=40, ax=plt.gca()) -
创建部分自相关图:
plot_pacf(df['close'].dropna(), lags=40, ax=plt.gca())结果图如下所示:

图 11.10 – ACF 和 PACF 图
让我们解释一下前面图表中可以看到的内容。对于自相关函数(ACF),我们可以看到以下内容:
-
ACF 图显示了序列与其滞后值在不同滞后期(本例中
lags=40)的相关性。ACF 图的X轴表示滞后期的数量,指示计算相关性时回溯的时间点数。 -
ACF 图的Y轴表示原始时间序列与其滞后值之间的相关系数。相关值的范围从-1 到 1。
-
蓝色阴影区域表示置信区间。超出阴影区域的柱状条被认为具有统计显著性,表明在这些滞后期存在强烈的自相关性,并可能为ARIMA 模型中的 q 参数(MA 阶数)提供潜在的值,正如我们在接下来的部分中将看到的。
-
在固定间隔处出现显著峰值表明时间序列数据中存在季节性。
-
如果 ACF 图在滞后 1 期存在显著的自相关(如我们案例中的蓝色阴影区域之外的尖峰),则表明该序列与其即时前值之间有很强的相关性。这可能意味着该序列是非平稳的,可能需要差分(d > 0)。
对于 PACF,我们可以看到以下内容:
-
PACF 图显示了时间序列与其滞后值之间的相关性,去除了由较短滞后解释的效应。
-
PACF 图中的显著峰值表明滞后 1 和潜在的滞后 2 可能是 ARIMA 模型中p 参数(AR 阶数)的良好候选项。
注意
当我们在 ACF 和 PACF 图中指定lags=40时,我们是在检查时间序列在 40 个不同滞后区间的自相关和偏自相关。这意味着我们将看到序列如何与自身在滞后 1 到 滞后 40之间的相关性。
ACF 和 PACF 图对于识别时间序列中的基本结构至关重要。在接下来的部分中,我们将把 ACF 和 PACF 分析与异常值检测和处理联系起来,确保我们的时间序列模型准确捕捉到潜在模式。
处理异常值
时间序列数据通常表现出季节性模式(例如,假期期间的销售峰值)和趋势(例如,多年来的逐步增长)。在这种情况下,异常值可能并不是异常现象;它可能反映了正常的季节性效应或基础趋势的变化。例如,黑色星期五期间零售销售的突然激增是可以预期的,不应视为异常值。诸如时间序列季节性分解(STL)、自相关和季节性指数等技术可以帮助理解数据的预期行为,从而为识别异常值提供更清晰的基础。
使用季节性分解识别异常值
识别时间序列中的异常值的一种方法是将序列分解为趋势、季节性和残差组件,因为异常值通常出现在残差组件中。要将序列分解为趋势、季节性和残差组件,我们可以使用 STL 方法。该方法通过分析残差组件(理想情况下应该是白噪声)来帮助识别和处理异常值。让我们看看如何使用股市数据来实现这一点。你可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter11/5.outliers/1.seasonal_decomposition.py找到完整的代码示例:
result = seasonal_decompose(df['close'], model='additive', period=252)
在这段代码中,我们在假设每年有 252 个工作日的情况下对时间序列进行分解。我们还将计算残差的 Z 值,以便使用以下代码识别异常值:
df['resid_z'] = zscore(df['residual'].dropna())
最后,让我们绘制分解后的序列:

图 11.11 – 分解后的时间序列
可以通过分析残差组件来检测异常值。残差组件中显著偏离零的值或突增表示潜在的异常值:

图 11.12 – 分解值表
基于图 11.11中的分解时间序列,我们可以通过检查残差和resid_z列来分析异常值。通常,绝对值大于 2 或 3 的 Z 分数被视为潜在的异常值。在该数据集中,最大正残差出现在2020-01-06(Z 分数:1.468043)、2020-01-17(Z 分数:1.300488)和2020-01-27(Z 分数:1.172529)上,而最大负残差出现在2020-01-15(Z 分数:-1.721474)和2020-01-22(Z 分数:-1.082559)上。尽管这些数值显示出与趋势和季节性成分的某些偏差,但没有一个 Z 分数超过典型的±2 或±3 阈值,表明该数据集没有极端异常值。残差似乎相对均匀地分布在零周围,表明分解模型拟合良好。然而,具有最大偏差的日期(2020-01-06、2020-01-15和2020-01-17)可能值得进一步调查,看看是否有任何异常事件或因素可以解释它们偏离预期值的原因。
深入挖掘这些数据以了解波动背后的原因,并仔细检查后,我们可以看到这些日期的偏差是由特定事件和系统问题引起的:
免责声明!
以下事件对应的是虚构事件!
-
2020-01-06:股市交易系统的技术故障导致价格暂时激增 -
2020-01-15:错误的交易输入导致价格突然下跌,随后被修正 -
2020-01-17:一次重大经济公告导致波动性增加,并使股价短暂上涨 -
2020-01-22:关于季度财报结果的误传引发了暂时的恐慌性抛售 -
2020-01-27:关于并购的谣言引发了投机性购买,暂时抬高了价格
这些发现帮助我们理解到,残差的偏差并非随机发生,而是由于特定的、可识别的事件所致。虽然这些事件在统计上不符合显著异常值的标准,但它们突显了股价数据中固有的波动性和噪声。鉴于股价的噪声特性,即使没有显著的异常值,平滑技术仍然变得至关重要!
处理异常值 – 基于模型的方法 – ARIMA
ARIMA 模型广泛用于时间序列数据的预测。它们根据过去的观测值预测未来的数值,使得通过将实际值与预测值进行比较,从而有效地识别异常值。ARIMA 模型由三个主要部分组成:
-
自回归(AR):利用观测值与多个滞后观测值之间的依赖关系(p)
-
集成(I):通过对观测值的差分处理,使时间序列平稳化(d)
-
移动平均(MA):利用观测值与应用于滞后观测值的移动平均模型的残差误差之间的依赖关系(q)
ARIMA 模型在处理以下异常值时有效:
-
加性异常值(AO):时间序列中的突升或突降
-
创新异常值(IO):影响整个序列的变化,从发生点开始向后延伸
让我们讨论一下 ARIMA 模型如何在我们一直在处理的股票价格数据示例中用于异常值检测和平滑。您可以在 github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter11/5.outliers/3.arima.py 找到完整示例:
-
对
close_filled序列拟合 ARIMA 模型:model = ARIMA(df['close_filled'], order=(2,1,1)) results = model.fit() -
计算残差和 Z 分数:
df['residuals'] = results.resid df['residuals_z'] = zscore(df['residuals'].dropna()) -
基于 Z 分数阈值(例如,±3)识别任何异常值:
outliers_arima = df[np.abs(df['residuals_z']) > 3] -
可视化原始
close_filled序列和从 ARIMA 模型获得的平滑序列:df['arima_smooth'] = results.fittedvalues以下是输出结果:

图 11.13 – ARIMA 平滑和异常值检测
-
生成诊断图以评估模型拟合,包括残差分析、分位数-分位数(Q-Q)图和标准化残差:
results.plot_diagnostics(figsize=(14,8)) -
结果图如下:

图 11.14 – 残差分析、Q-Q 图和标准化残差
让我们深入探讨一下 图 11.14 中显示的诊断图:
-
标准化残差:标准化残差是通过其标准差对 ARIMA 模型的残差进行缩放得到的。为了使 ARIMA 模型被认为是一个良好的拟合,标准化残差应当像白噪声一样,意味着它们不应显示出明显的模式。这意味着残差是随机分布的,均值为零,方差恒定。如果残差中出现模式,则表明模型未能捕捉到数据中的某些潜在结构,可能需要进一步的调整。在我们的案例中,残差看起来像白噪声。
-
直方图加核密度估计(KDE):结合残差的核密度估计(KDE)图,提供了对残差分布的可视化评估。对于拟合良好的 ARIMA 模型,残差应遵循正态分布。直方图应呈现典型的钟形曲线,KDE 图应叠加一条与之匹配的平滑曲线。若与正态分布存在偏差,如偏态或重尾,表明残差不是正态分布,这暗示模型可能存在问题。在我们的案例中,我们没有看到残差中有显著的偏态或尾部。
-
正态 Q-Q 图:Q-Q 图将残差的分位数与正态分布的分位数进行比较。如果残差服从正态分布,Q-Q 图上的点将沿着 45 度线排列。显著偏离这条线的点表示偏离正态分布。在我们的案例中,我们没有看到任何显著的偏差。
-
自相关图(残差的自相关函数(ACF)):自相关图展示了残差的自相关函数(ACF)。对于一个合理指定的 ARIMA 模型,残差应该没有显著的自相关。这意味着没有任何滞后项的自相关系数应该具有统计学上的显著性。ACF 图中的显著峰值表明残差仍然与其过去的值相关,暗示模型尚未完全捕捉到时间序列的结构。这可以指导进一步的模型优化,比如增加 AR 或 MA 组件的阶数。在我们的案例中,一切看起来都很好。
什么是滞后 0?
在自相关图(ACF 图)中,滞后 0 是指时间序列与自身在滞后 0 时的自相关,实际上是时间序列与自身在相同时间点的相关性。根据定义,这个相关性总是 1,因为任何时间序列在滞后 0 时与自身完全相关。这意味着滞后 0 时的自相关值总是 1,这就是为什么在 ACF 图中滞后 0 处会看到一个峰值的原因。
玩弄不同的设置并观察它们对 ARIMA 模型和残差的影响是一个好主意。
在探索使用 ARIMA 方法检测和处理我们股票价格数据集中的异常值后,我们发现异常值会显著影响我们时间序列模型的准确性和可靠性。虽然 ARIMA 方法有助于识别和调整这些突变,但考虑其他稳健的异常值检测和处理方法也非常重要。接下来的部分我们将介绍其中一种方法,即使用移动窗口技术。
移动窗口技术
移动窗口技术,也称为滚动或滑动窗口方法,涉及分析一个固定大小的数据子集或“窗口”,该窗口会在较大的数据集上顺序移动。在窗口的每个位置,都会应用特定的计算或函数,例如计算均值、中位数、总和或更复杂的统计量。当窗口通过一个或多个数据点滑动时,计算会使用新的数据子集进行更新。该方法在时间序列分析中尤其稳健,通常用于平滑数据、识别趋势或检测随时间变化的异常值。
移动窗口技术的优势在于它能够提供局部分析,同时与更广泛的数据集保持联系。例如,在平滑时间序列时,移动平均可以减少噪声并突出底层趋势,而不会扭曲整体信号。类似地,在金融数据中,移动窗口可以用来计算滚动平均值或波动性,提供市场条件的实时视图。
在本节中,我们将重点介绍两种主要方法:简单移动平均(SMA)和指数加权移动平均(EMA)。这两者都可以作为基础,稍后可以通过其他统计量(如中位数)进行调整。
SMA
SMA是常用的统计计算,表示一组数据点在指定时间内的平均值。它是一种移动平均,通过平滑数据中的波动,更容易识别趋势。SMA 通过将一组值相加,并将总和除以数据点的数量来计算。更先进的方法,如卡尔曼平滑,可以通过建模底层过程来估计缺失值:
SMAt = (Xt + Xt–1 + Xt–2 + ...+ Xt–n+1)/n
在这里,我们有如下公式:
-
SMAt 是时刻t的 SMA。
-
Xt + Xt–1 + Xt–2 + ...+ Xt–n+1 是该时间段的数据值。
-
n是参与计算的周期数。
现在,让我们介绍指数加权移动平均(EMA),以便我们可以对比这两者。
EMA
EMA对最近的数据点赋予更多的权重,对较早的数据点赋予较少的权重。它使用指数衰减公式:
EMAt = α • Xt + (1 – α) • EMAt–1
其中,α是平滑因子。
现在,让我们讨论一下 SMA 和 EMA 如何在我们一直在使用的股票价格数据示例中进行异常值检测和平滑。
使用 SMA 和 EMA 对股票价格进行平滑
继续使用我们之前呈现的股票价格数据示例,让我们看看 SMA 和 EMA 对数据的影响:
首先,让我们计算 12 个月窗口的 SMA:
-
定义 SMA 的
窗口大小和 EMA 的跨度大小:window_size = 20 span = 20 -
计算 SMA:
df['SMA'] = df['close'].rolling(window=window_size, min_periods=1).mean() -
计算 EMA:
df['EMA'] = df['close'].ewm(span=span, adjust=False).mean() -
计算 SMA 和 EMA 的残差:
df['SMA_residuals'] = df['close'] - df['SMA'] df['EMA_residuals'] = df['close'] - df['EMA'] sma_window = 12 data['SMA'] = data['Passengers'].rolling(window=sma_window).mean() -
绘制原始时间序列和 SMA:

图 11.15 – SMA 和 EMA
在这个例子中,我们使用 20 的窗口大小和 20 的跨度分别计算了 SMA 和 EMA。SMA 的窗口大小决定了在每个时间点计算平均值时包含多少个之前的数据点。和 SMA 一样,你数据点的频率会影响跨度的选择。如果你的数据是按日计的,跨度 20 大约代表过去 20 天的历史数据。
让我们再多讨论一下生成的图表:
-
SMA:
-
平滑效果:SMA 通过对窗口内的值进行平均来平滑时间序列数据,减少噪声,突出底层趋势。
-
异常值影响:虽然 SMA 减少了异常值的影响,但它仍可能会受到异常值的影响,因为它对窗口内的所有值赋予相同的权重。
-
-
EMA:
-
平滑效果:EMA 也对数据进行了平滑处理,但对最近的观察值赋予更多的权重,使其对近期变化更具响应性。
-
异常值影响:EMA 不太受较旧异常值的影响,但由于其加权机制,可能更容易受到近期异常值的影响。
-
在平滑度和响应性之间找到平衡
较大的窗口大小会导致更平滑的移动平均,但可能会滞后于数据的变化。较小的窗口大小使得移动平均对短期波动更具响应性,但可能引入更多噪声。
记得我们在图 11.10中创建的自相关图吗?我们可以利用该分析,根据观察到的自相关模式来调整跨度或窗口大小。以下几点将帮助你选择窗口大小和跨度:
-
考虑数据点的频率(每日、每周、每月)。
-
如果自相关图显示在较短滞后期存在显著的自相关,EMA 采用较小跨度或 SMA 采用较小窗口大小可以帮助保持对近期变化的响应,同时减少短期噪声的影响。
-
如果你的数据呈现季节性模式,你可能会选择与季节周期相符的窗口大小或跨度。例如,如果存在每周季节性,可能考虑使用 5 或 7 的窗口大小。可以使用自相关图来帮助确定这一点。
为了评估窗口模型的表现,我们可以使用平均绝对误差(MAE),以及均方误差(MSE)和均方根误差(RMSE)。我们可以比较原始数据和这些模型生成的平滑值之间的误差,如下图所示:

图 11.16 – SMA 和 EMA 的性能指标
为了确保我们清楚理解图 11.16中呈现的不同指标,让我们更详细地看一下:
-
MAE:这表示一组预测中的平均误差幅度,提供了预测值和实际值之间绝对差异的简单平均值。
-
MSE:该指标衡量预测值和实际值之间的平均平方差,比 MAE 更重视较大的误差。
-
RMSE:RMSE 是 MSE 的平方根,提供了一个可解释的平均误差幅度度量,与原始数据的尺度一致。
现在我们知道这些术语的含义,让我们来解读它们在股票价格案例中的应用。较低的 MAE、MSE 和 RMSE 值表示平滑方法的表现更好。虽然 SMA 和 EMA 的 MAE 和 RMSE 值非常接近,但指数加权法(EMA)的 MSE 值较低。
下表对何时使用 SMA 和 EMA 进行了比较和总结:
| 标准 | SMA | EMA |
|---|---|---|
| 平滑类型 | 对数据点进行简单和均匀的平滑处理 | 更敏感和适应性强,对近期数据点赋予更大权重 |
| 数据点加权 | 对窗口中的所有数据点赋予相等权重 | 对近期观察值赋予更多权重;较老的观察值获得指数递减的权重 |
| 对变化的响应性 | 滞后指标;对近期变化响应较慢 | 对近期变化更敏感;快速适应数据的变化 |
| 稳定性适应性 | 适合稳定且波动较小的时间序列 | 适合波动性较大或变化迅速的时间序列 |
| 对趋势的适应性 | 平滑长期趋势,适合识别整体模式 | 对变化趋势的适应较快,适合捕捉近期变化 |
| 使用场景示例 | 分析长期趋势和识别季节性模式 | 捕捉短期波动并对市场波动做出反应 |
| 计算复杂性 | 计算较简单,易于理解和实现 | 更复杂的计算涉及平滑因子 |
表 11.2 – SMA 与 EMA 的比较
除了移动平均技术之外,探索高级特征工程步骤,如滞后和差分,可以显著丰富我们对数据的理解和预测能力。我们将在下一节中进行探讨。
时间序列数据的特征工程
有效的特征工程在时间序列分析中至关重要,可以揭示有意义的模式并提高预测准确性。它涉及将原始数据转化为能够捕捉时间依赖性、季节性变化以及时间序列其他相关方面的信息特征。我们要探索的第一个技术是创建特征的滞后。
滞后特征及其重要性
滞后特征是时间序列特征工程中的关键部分,因为它们允许我们将时间序列数据转换为适合监督学习模型的格式。滞后特征涉及创建代表目标变量过去观察值的新变量:
-
Lag 1:来自上一个时间步的值
-
Lag 2:来自两个时间步之前的值
-
Lag k:来自 k 个时间步之前的值
通过将时间序列数据按指定的时间步数(称为滞后)进行平移,这些过去的值会作为当前时间戳的特征包含在模型中。正如我们所知道的,时间序列数据通常表现出时间依赖性,即当前值与过去的观察值相关。滞后特征有助于捕捉这些依赖关系,使模型能够从历史模式中学习。
现在,让我们讨论如何在我们一直在使用的股价数据示例中应用滞后特征。
在股价使用案例中创建滞后特征
继续我们之前提出的股价数据示例,让我们看看滞后特征对数据的影响:
-
首先,在
close列中引入更多激进的离群值:outlier_indices = np.random.choice(df.index, size=10, replace=False) df.loc[outlier_indices[:5], 'close'] = df['close'] * 1.5 # Increase by 50% df.loc[outlier_indices[5:], 'close'] = df['close'] * 0.5 # Decrease by 50% -
使用以下函数创建滞后特征:
def create_lagged_features(df, column, lags): for lag in lags: df[f'{column}_lag_{lag}'] =df[column].shift(lag) return df # Define the lags to create lags = [1, 5, 10, 20] -
为
close列创建滞后特征:df = create_lagged_features(df, 'close', lags) -
绘制原始时间序列和滞后数据集:

图 11.17 – 原始特征与滞后特征
正如我们在图 11.17中所看到的,滞后 1(close_lag_1)表示前一天的收盘价,滞后 5(close_lag_5)表示 5 天前的收盘价,依此类推。你可以观察每个滞后值如何捕捉目标变量的历史值。添加滞后特征到时间序列时,数据的起始日期会向前移动,因为在指定的滞后期结束之前,前几个数据点不能使用。这种偏移意味着,如果你添加更多的滞后,缺乏完整滞后数据的初始数据点数量会增加,从而有效地将起始日期向前推移。
可以自由尝试不同的滞后值,查看其对数据集的影响。调整滞后值可以帮助你捕捉数据中的不同时间依赖关系和趋势。
时间序列差分
在第四章《清理杂乱数据与数据操作》中,我们讨论了如何使用diff()函数计算两个日期时间对象之间的时间差,这有助于我们测量连续事件之间经过的时间。这个技巧有助于理解时间戳序列中的时间间隔。类似地,在时间序列分析中,差分是一种强大的技术,通过去除时间序列的水平变化,稳定时间序列的均值,从而消除趋势和季节性。正如我们在上一章中计算了时间差一样,我们可以将差分应用于股市数据,以突出随时间变化的变化。然而,我们还将引入一个新术语——季节性差分。
季节性差分
季节性差分是一种用于去除时间序列数据中的季节性模式的技术,使其更加平稳,适合分析和预测。季节性差分通过将某个观测值与相隔季节性周期的前一个观测值相减来实现。因此,我们需要借助之前提供的工具识别季节性周期,然后取该季节性周期对数据进行差分。
对于具有年度季节性模式的月度数据,我们可以使用以下公式:
y't = yt – yt–12
对于季度数据,我们可以使用以下公式:
y't = yt – yt–4
这里是季节性差分后的序列,和原始序列。
现在,让我们讨论如何在我们一直在处理的股价数据示例中使用差分。
对股价数据进行差分
为了展示季节性差分,我们将在股票市场数据中引入一些季节性。根据我们目前的分析,数据中并没有明显的季节性成分。让我们开始吧:
-
创建一个季节性成分(每周季节性,幅度较大):
seasonal_component = 50 * np.sin(2 * np.pi * np.arange(n) / 5) # 5-day seasonality -
生成加入季节性的随机股票价格:
data = { 'open': np.random.uniform(100, 200, n) + seasonal_component, 'high': np.random.uniform(200, 300, n) + seasonal_component, 'low': np.random.uniform(50, 100, n) + seasonal_component, 'close': np.random.uniform(100, 200, n) + seasonal_component } df = pd.DataFrame(data, index=date_range) -
计算第一次差分:
df['First Difference'] = df['close'].diff() -
计算第二次差分:
df['Second Difference'] = df['First Difference'].diff() -
最后,计算季节性差分(每周季节性):
df['Seasonal Difference'] = df['close'].diff(5)
让我们通过绘制第一次、第二次和季节性差分来演示差分操作:

图 11.18 – 原始序列与差分序列
在图 11.18中,我们可以观察到第一次、第二次和季节性差分。我们可以看到在原始图中,存在一些季节性,但在第一次差分后,季节性成分被最小化了。但我们如何从统计学角度评估这一点呢?让我们进行一些统计检验,检查时间序列的平稳性。
增广的迪基-富勒(ADF)检验
ADF检验是一种用于确定时间序列是否平稳的统计检验。ADF 检验检验原假设:时间序列样本中存在单位根。单位根的存在表示时间序列是非平稳的。备择假设是时间序列是平稳的。对于 ADF 检验,数值越负,表明反对原假设的证据越强。
p 值表示假设原假设为真时,获得至少与观察结果一样极端的检验结果的概率。在 ADF 检验中,我们希望看到一个小的 p 值来拒绝原假设 即非平稳性。
要得出一个时间序列是平稳的结论,我们通常需要看到以下几点:
-
p 值 < 0.05:这是统计检验中最常用的阈值。如果 p < 0.05,我们会在 5%的显著性水平上拒绝原假设。这意味着我们有足够的证据得出该序列是平稳的结论。
-
更小的 p 值:p < 0.01(1%显著性水平)和 p < 0.001(0.1%显著性水平)提供了更强的平稳性证据。
让我们编写代码进行这个检验:
def adf_test(series, title=''):
result = adfuller(series.dropna(), autolag='AIC')
print(f'Augmented Dickey-Fuller Test: {title}')
print(f'ADF Statistic: {result[0]}')
print(f'p-value: {result[1]}')
for key, value in result[4].items():
print(f' {key}: {value}')
print('\n')
现在是时候查看结果了!我们将对原始时间序列进行检验(检查它是否平稳),然后对每个差分后的时间序列进行检验。让我们解释一下这些结果:
Augmented Dickey-Fuller Test: Original Series
ADF Statistic: -3.5898552445987595
p-value: 0.005957961883734467
1%: -3.4367333690404767
5%: -2.8643583648001925
10%: -2.568270618452702
ADF 统计量-3.5899 小于 5%的临界值-2.8644,且 p 值低于 0.05。这表明我们可以拒绝单位根存在的原假设,暗示原始序列可能是平稳的。然而,结果相对接近临界值,表明平稳性接近临界:
Augmented Dickey-Fuller Test: First Difference
ADF Statistic: -11.786384523171499
p-value: 1.0064914317100746e-21
1%: -3.4367709764382024
5%: -2.8643749513463637
10%: -2.568279452717228
ADF 统计量为-11.7864,远低于 5%的临界值-2.8644,且 p 值极小。这强烈表明第一次差分后的序列是平稳的。与原始序列相比,ADF 统计量的显著下降表明第一次差分有效去除了剩余的趋势或单位根:
Augmented Dickey-Fuller Test: Second Difference
ADF Statistic: -14.95687341689794
p-value: 1.2562905072914351e-27
1%: -3.4367899468008916
5%: -2.8643833180472744
10%: -2.5682839089705536
ADF 统计量为-14.9569,远低于 5%的临界值,且 p 值极小。该结果表明第二次差分后的序列也是平稳的。然而,过度差分可能导致有意义的模式丧失并增加噪声,因此,在实现平稳性和保持序列的完整性之间必须保持平衡:
Augmented Dickey-Fuller Test: Seasonal Differencing
ADF Statistic: -11.48334880444129
p-value: 4.933051350797084e-21
1%: -3.4367899468008916
5%: -2.8643833180472744
10%: -2.5682839089705536
最后,ADF 统计量为-11.4833,远低于 5%的临界值,且 p 值非常小。这表明季节性差分成功地使得序列平稳。如果序列在特定的时间间隔内表现出周期性模式,季节性差分特别有效。
根据这些结果,第一次差分似乎是最合适的选择,原因如下:
-
原始序列在 1%的显著性水平下已经是平稳的,但第一次差分显著提高了平稳性。
-
第一次差分产生了一个非常显著的结果(p 值:1.006e-21),且不会导致过度差分的风险。
-
虽然第二次差分显示出更为显著的结果,但它可能导致过度差分,从而引入不必要的复杂性,并可能移除序列中的重要信息。
-
季节性差分也显示出强劲的结果,但除非数据中有明确的季节性模式,否则通常更倾向于使用较为简单的第一次差分方法。
总结来说,第一次差分在实现平稳性和避免过度差分之间取得了良好的平衡。现在,接下来我们讨论一些在时间序列领域最常见的应用场景。
在不同的行业中应用时间序列技术
能够分析时间模式为各行各业提供了竞争优势,尤其是在今天数据驱动的世界中。以下是一些不同行业中的常见应用场景:
| 领域 | 应用场景 | 解释 |
|---|---|---|
| 金融 | 股票市场分析 | 分析历史股价和交易量,以做出明智的投资决策 |
| 投资组合管理 | 评估投资组合的表现,以优化资产配置 | |
| 风险评估 | 建模和预测金融风险,如市场波动和信用违约 | |
| 医疗健康 | 患者监测 | 持续跟踪生命体征和健康指标,及早发现异常 |
| 流行病学 | 分析疾病传播的时间模式并预测疫情爆发 | |
| 治疗效果 | 评估医学干预措施随时间的效果 | |
| 气象学 | 天气预报 | 分析历史天气模式以预测未来气候 |
| 气候变化研究 | 监测气候数据中的长期趋势和变化 | |
| 自然灾害预测 | 早期检测潜在灾害,如飓风、洪水和干旱 | |
| 制造业 | 生产计划 | 预测需求并优化生产计划 |
| 质量控制 | 监控并确保产品质量 | |
| 设备维护 | 基于机械性能历史的预测性维护 | |
| 营销 | 销售预测 | 基于历史数据预测未来销售 |
| 客户参与度 | 分析客户与产品和服务的互动模式 | |
| 活动优化 | 评估营销活动随时间的影响 | |
| 领域 | 用例 | 说明 |
| 交通运输 | 交通流量分析 | 监控并优化城市地区的交通模式 |
| 车辆追踪 | 追踪运输车队的移动和效率 | |
| 供应链优化 | 预测需求并优化商品在时间中的流动 |
表 11.3 – 时间序列技术应用场景
有了这些,我们可以总结这一章的内容。
总结
时间序列分析在从各种行业中提取有意义的见解并做出明智决策中起着至关重要的作用。随着技术的发展,复杂的时间序列技术将变得越来越重要,用于理解复杂的时间模式和趋势。无论是在金融、医疗保健还是交通运输中,分析和预测时间依赖数据的能力使组织能够适应、优化并在不断变化的环境中做出战略决策。
在这一章中,我们介绍了处理缺失值和异常值的技术、差分方法,以及时间序列分析中的特征工程。我们学习了如何使用 ffill 和 bfill 处理缺失值,并比较了它们对股票价格数据的影响。我们还应用了包括一阶、二阶和季节性差分在内的差分技术,以实现平稳性,并通过 ADF 检验进行评估。我们还探索了滞后特征以捕捉时间依赖关系,并使用 MAE、MSE 和 RMSE 等指标评估了模型性能。这些技能将使你能够有效地管理和分析时间序列数据。
在下一章中,我们将转向另一种类型的数据——文本。分析文本数据涉及独特的挑战和方法,这些方法与用于数字时间序列的数据分析不同。我们将深入探讨文本预处理,涵盖文本清理技术、分词策略和拼写修正方法,这些对于任何自然语言处理(NLP)任务都是至关重要的。
第三部分:下游数据清洗——消费非结构化数据
本部分聚焦于处理非结构化数据(如文本、图像和音频)时面临的挑战和技术,特别是在现代机器学习环境下,尤其是大型语言模型(LLMs)。它全面概述了如何为机器学习应用准备非结构化数据类型,确保数据经过适当预处理以便分析和模型训练。各章节涵盖了文本、图像和音频数据的基本预处理方法,为读者提供了在当今由 AI 驱动的环境中处理更复杂和多样化数据集的工具。
本部分包含以下章节:
-
第十二章**,LLMs 时代的文本预处理
-
第十三章**,使用 LLMs 进行图像和音频预处理
第十二章:大规模语言模型时代的文本预处理
在大规模语言模型(LLMs)时代,掌握文本预处理比以往任何时候都更加重要。随着 LLMs 在复杂性和能力上的不断提升,成功的自然语言处理(NLP)任务的基础依然在于文本数据的准备工作。在本章中,我们将讨论文本预处理,这是任何 NLP 任务的基础。我们还将探讨重要的预处理技术,并重点研究如何调整这些技术以最大化 LLMs 的潜力。
在本章中,我们将覆盖以下主题:
-
在大规模语言模型时代重新学习文本预处理
-
文本清洗技术
-
处理稀有词汇和拼写变体
-
词块划分
-
分词策略
-
将词元转化为嵌入
技术要求
本章的完整代码可以在以下 GitHub 仓库中找到:
github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/tree/main/chapter12
让我们安装本章中将使用的必要库:
pip install transformers==4.42.4
pip install beautifulsoup4==4.12.3
pip install langchain-text-splitters==0.2.2
pip install tiktoken==0.7.0
pip install langchain==0.2.10
pip install langchain-experimental==0.0.62
pip install langchain-huggingface==0.0.3
pip install presidio_analyzer==2.2.355
pip install presidio_anonymizer==2.2.355
pip install rapidfuzz-3.9.4 thefuzz-0.22.1
pip install stanza==1.8.2
pip install tf-keras-2.17.0
在大规模语言模型时代重新学习文本预处理
文本预处理是指对原始文本数据应用各种技术,目的是清理、组织并将其转化为适合分析或建模的格式。其主要目标是通过解决与非结构化文本相关的常见挑战来提高数据的质量。这包括清理无关字符、处理变体以及为后续的自然语言处理(NLP)任务准备数据等任务。
随着大规模语言模型(LLMs)的快速发展,自然语言处理(NLP)的格局发生了显著变化。然而,基础的预处理技术,如文本清洗和分词,依然至关重要,尽管它们在方法和重要性上有所变化。
从文本清洗开始,尽管大规模语言模型(LLMs)在处理输入文本噪声方面表现出显著的鲁棒性,但清洗后的数据仍然能带来更好的结果,尤其在微调任务中尤为重要。基础清洗技术,如去除 HTML 标签、处理特殊字符以及文本标准化,依然是相关的。然而,像拼写纠正这样的高级技术对于 LLMs 的必要性可能较低,因为它们通常能处理轻微的拼写错误。领域特定的清洗仍然非常重要,尤其是在处理专业词汇或术语时。
随着子词标记化方法的出现,Tokenization 也得到了发展,现代大多数 LLM(大规模语言模型)都使用如字节对编码(BPE)或 WordPiece 等方法。传统的基于词的标记化在 LLM 的背景下不再常见。一些传统的 NLP 预处理步骤,如停用词去除、词干提取和词形还原,变得不那么重要。停用词去除,即去除常见词汇,如“and”或“the”,变得不那么必要,因为 LLM 能够理解这些词在上下文中的重要性以及它们如何贡献于句子的意义。类似地,词干提取和词形还原(如将“running”还原为“run”)也不常使用,因为 LLM 能够准确理解不同词形,并理解它们在文本中的关系。这一转变使得对语言的理解更加细致,能够捕捉到一些严格预处理可能遗漏的细微差别。
关键的信息是,虽然 LLM 能够令人印象深刻地处理原始文本,但在某些情境下,预处理仍然至关重要,因为它可以提高模型在特定任务上的表现。记住:垃圾进,垃圾出。清洗和标准化文本也可以减少 LLM 处理的 token 数量,从而可能降低计算成本。新的方法正在出现,它们将传统的预处理与 LLM 的能力相结合,利用 LLM 本身来进行数据清洗和预处理任务。
总之,尽管 LLM 在许多 NLP 任务中减少了对广泛预处理的需求,但理解并谨慎应用这些基础技术仍然具有价值。在接下来的章节中,我们将重点介绍仍然相关的文本预处理技术。
文本清洗
文本清洗的主要目标是将非结构化的文本信息转化为标准化且更易处理的形式。在清洗文本时,常见的操作包括去除 HTML 标签、特殊字符和数字值,标准化字母大小写,处理空格和格式问题。这些操作共同有助于提升文本数据的质量并减少其歧义性。让我们深入探讨这些技术。
去除 HTML 标签和特殊字符
HTML 标签通常会出现在从网页中提取内容的过程中。这些标签,如<p>、<a>或<div>,在 NLP 的上下文中没有语义意义,必须被移除。清洗过程包括识别并去除 HTML 标签,保留实际的文本内容。
对于这个示例,让我们假设我们有一个产品的用户评论数据集,并希望为情感分析准备文本数据。你可以在 GitHub 代码库中找到这一部分的代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter12/1.text_cleaning.py。在这个脚本中,数据生成也已提供,你可以一步一步跟着示例走。
重要提示
在本章中,我们包含了关键的代码片段,以说明最重要的概念。然而,要查看完整的代码,包括使用的库,并运行完整的端到端示例,请访问代码库。
我们将执行的第一个文本预处理步骤是移除 HTML 标签。让我们一步步查看代码:
-
让我们为这个示例导入所需的库:
from bs4 import BeautifulSoup from transformers import BertTokenizer -
这里展示了示例用户评论:
reviews = [ "<html>This product is <b>amazing!</b></html>", "The product is good, but it could be better!!!", "I've never seen such a terrible product. 0/10", "The product is AWESOME!!! Highly recommended!", ] -
接下来,我们创建一个使用
BeautifulSoup来解析 HTML 内容并提取文本的函数,移除所有 HTML 标签:def clean_html_tags(text): soup = BeautifulSoup(text, "html.parser") return soup.get_text() -
然后,我们对所有评论进行预处理:
def preprocess_text(text): text = clean_html_tags(text) return text preprocessed_reviews = [preprocess_text(review) for review in reviews] -
最后,我们得到以下的预处理评论:
- This product is amazing! - The product is good, but it could be better!!! - I've never seen such a terrible product. 0/10 - The product is AWESOME!!! Highly recommended!
如我们所见,所有 HTML 标签已经被移除,文本变得干净整洁。我们将继续通过添加另一个常见的预处理步骤来增强此示例:处理文本的大小写。
处理大小写
文本数据通常有各种大小写——大写、小写或两者的混合。不一致的大小写可能会导致语言处理任务中的歧义。因此,一种常见的文本清理做法是统一整个语料库中的字母大小写。这不仅有助于保持一致性,还能确保模型在不同大小写之间具有良好的泛化能力。
基于前面的示例,我们将扩展预处理函数,增加一个额外步骤:字母标准化:
-
让我们首先回顾一下在上一步移除 HTML 标签后的评论是怎样的:
- This product is amazing! - The product is good, but it could be better!!! - I've never seen such a terrible product. 0/10 - The product is AWESOME!!! Highly recommended! -
以下函数将把所有字符转换为小写字母:
def standardize_case(text): return text.lower() -
我们将扩展在前一个示例中介绍的
preprocess_text函数,将文本中的所有字符转换为小写字母,使得文本对大小写不敏感:def preprocess_text(text): text = clean_html_tags(text) text = standardize_case(text) return text -
让我们打印出预处理后的评论:
for preprocessed_review in preprocessed_reviews: print(f"- {preprocessed_review}")这里展示了小写处理后的评论:
- this product is amazing! - the product is good, but it could be better!!! - i've never seen such a terrible product. 0/10 - the product is awesome!!! highly recommended!
注意所有字母都变成小写了!请继续更新大小写函数,按照以下方式将所有内容转换为大写:
def standardize_case(text):
return text.upper()
这里展示了大写字母的评论:
- THIS PRODUCT IS AMAZING!
- THE PRODUCT IS GOOD, BUT IT COULD BE BETTER!!!
- I'VE NEVER SEEN SUCH A TERRIBLE PRODUCT. 0/10
- THE PRODUCT IS AWESOME!!! HIGHLY RECOMMENDED!
如果你在犹豫是否应该使用小写或大写,我们已经为你准备好了答案。
小写还是大写?
使用小写或大写文本的选择取决于 NLP 任务的具体要求。例如,情感分析等任务通常更适合小写处理,因为这能简化文本并减少变异性。相反,像命名实体识别(NER)这样的任务可能需要保留大小写信息,以便准确识别和区分实体。
例如,在德语中,所有名词都需要大写,因此保持大小写对于正确的语言表现至关重要。相比之下,英语通常不使用大小写来表达意义,因此对于一般文本分析来说,转换为小写可能更为合适。
在处理来自用户输入的文本数据时,如社交媒体帖子或评论,考虑大小写变化的作用非常重要。例如,一条推文可能会使用混合大小写来强调或表达语气,这对于情感分析可能是相关的。
现代大型语言模型(LLMs),如双向编码器表示(BERT)和 GPT-3,都是在混合大小写文本上训练的,能够有效处理大写和小写。这些模型利用大小写信息来增强上下文理解。它们的分词器本身设计能处理大小写敏感性,无需显式转换。
如果你的任务需要区分不同的大小写(例如,识别专有名词或首字母缩略词),最好保留原始的大小写。然而,始终参考你所使用的模型的文档和最佳实践。有些模型可能已优化为适应小写输入,如果文本转换为小写,可能会表现得更好。
下一步是学习如何处理文本中的数字值和符号。
处理数字值和符号
数字值、符号和数学表达式可能会出现在文本数据中,但并不总是对上下文产生有意义的贡献。清理它们需要根据任务的具体要求决定是保留、替换还是删除这些元素。
例如,在情感分析中,数字值可能不太相关,它们的存在可能会分散注意力。相反,对于与定量分析或金融情感相关的任务,保留数字信息变得至关重要。
在前面的示例基础上,我们将删除文本中的所有数字和符号:
-
让我们回顾一下上一步预处理后的数据样貌:
- This product is amazing! - The product is good, but it could be better!!! - I've never seen such a terrible product. 0/10 - The product is AWESOME!!! Highly recommended! -
现在,让我们添加一个函数,删除文本中所有除字母字符外 和空格以外的字符:
def remove_numbers_and_symbols(text): return ''.join(e for e in text if e.isalpha() or e.isspace()) -
应用文本预处理流程:
def preprocess_text(text): text = clean_html_tags(text) text = standardize_case(text) text = remove_numbers_and_symbols(text) return text -
让我们来看看预处理后的评论:
- this product is amazing - the product is good but it could be better - ive never seen such a terrible product - the product is awesome highly recommended
如你所见,在这个预处理步骤之后,文本中的所有标点符号和符号都已被移除。在文本预处理过程中,是否保留、替换或删除符号和标点符号,取决于你 NLP 任务的具体目标和数据集的特征。
保留符号和标点符号
随着大规模语言模型(LLMs)的发展,处理标点符号和符号的预处理方法已经发生了显著变化。现代大规模语言模型通过保留标点符号和符号受益,因为它们在多样化数据集上的广泛训练帮助模型更准确地理解上下文。保留这些符号有助于模型捕捉情感、强调和句子边界等细微差别。例如,感叹号和问号等标点符号在情感分析中发挥着重要作用,通过传达强烈的情感,提升了模型的表现。同样,在文本生成任务中,标点符号维持了可读性和结构,而在命名实体识别(NER)和翻译中,它有助于识别专有名词和句子边界。
另一方面,有些情况下,移除标点符号和符号可能会带来优势。现代大规模语言模型(LLMs)足够强大,能够处理噪声数据,但在某些应用中,通过移除标点符号简化文本可以优化预处理并减少独特标记的数量。这种方法对于主题建模和聚类等任务尤为有用,因为这些任务更侧重于内容而非结构元素。例如,移除标点符号有助于通过消除句子结构中的干扰来识别核心主题,而在文本分类中,当标点符号没有提供显著价值时,它可以帮助标准化输入数据。
另一种方法是用空格或特定标记替换标点符号和符号,这有助于在规范化文本时保持标记之间的某种分隔。这种方法对于自定义分词策略特别有用。在专业的自然语言处理(NLP)管道中,将标点符号替换为特定标记可以保留重要的区别,而不会给文本添加不必要的杂乱,从而促进更有效的分词和下游任务的预处理。
让我们通过一个简单的例子来看看如何移除或替换符号和标点符号。你可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter12/2.punctuation.py找到本节的代码:
-
创建示例文本:
text = "I love this product!!! It's amazing!!!" -
选项 1:用空格替换符号和标点符号:
replaced_text = text.translate(str.maketrans(string.punctuation, " " * len(string.punctuation))) print("Replaced Text:", replaced_text)这将打印以下输出:
I love this product It s amazing -
选项 2:移除符号和标点符号:
removed_text = "".join(char for char in text if char.isalnum() or char.isspace()) print("Removed Text:", removed_text)这将打印以下输出:
I love this product Its amazing
移除符号和数字是文本分析中的一个重要预处理步骤,它通过消除非字母数字字符简化了文本。在本节的最后,我们将讨论解决空格问题,以提高文本的可读性并确保一致的格式。
处理空格和格式问题
空格和格式不一致在文本数据中是常见的,尤其是当数据来源多样时。清理过程涉及解决多个连续空格、前后空格以及格式样式差异等问题。空格的规范化确保了文本表示的一致性,减少了下游模型误解的风险。
解决空格和格式化问题在大语言模型(LLMs)的世界中依然至关重要。尽管现代 LLMs 对各种格式不一致表现出较强的鲁棒性,但有效管理空格和格式仍能提升模型表现并确保数据一致性。
规范化空格和格式化可以创建统一的数据集,这通过最小化噪声并将注意力集中在内容上而非格式差异,有助于模型训练和分析。通过适当的空格管理提高可读性,有助于人类和机器学习的解读,清晰地划定文本元素。此外,一致的空格处理对于准确的分词非常重要——这是许多 NLP 任务中的基础过程——它确保了单词和短语的精确识别和处理。
所以,让我们回到评论示例,并在流程中添加另一步骤来去除空格:
-
让我们先从解决空格和格式化问题开始。此函数移除多余的空格,并确保单词之间只有一个空格:
def remove_extra_whitespace(text): return ' '.join(text.split()) -
接下来,我们将在文本预处理管道中添加这一步骤:
def preprocess_text(text): text = clean_html_tags(text) text = standardize_case(text) text = remove_numbers_and_symbols(text) text = remove_extra_whitespace(text) return text
让我们在应用新步骤之前,先看一下评论,并集中注意力于这里标记的空格:
- this productis amazing
- the product is good but it could be better
- ive never seen such a terribleproduct
- the product is awesome highly recommended
最后,在应用了空格移除之后,让我们检查清理后的数据集:
- this product is amazing
- the product is good but it could be better
- ive never seen such a terrible product
- the product is awesome highly recommended
让我们从纯文本清理过渡到专注于保护数据。
去除个人身份信息
在预处理文本数据时,去除个人身份信息(PII)对于维护隐私、确保符合规定以及提高数据质量至关重要。例如,考虑一个包含用户名、电子邮件地址和电话号码的用户评论数据集。如果这些敏感信息未被匿名化或移除,将会带来诸如隐私侵犯和潜在滥用等重大风险。通用数据保护条例(GDPR)、加利福尼亚消费者隐私法案(CCPA)和健康保险流动性与责任法案(HIPAA)等法规要求对个人数据进行小心处理。未能移除 PII 可能会导致法律处罚和信任丧失。此外,包含可识别的细节可能会给机器学习模型引入偏差,影响其泛化能力。去除 PII 对于负责任的人工智能开发至关重要,因为这可以在保持个人隐私的同时创建和使用数据集,为研究和分析提供有价值的见解。
以下代码片段演示了如何使用presidio-analyzer和presidio-anonymizer库来检测和匿名化 PII(个人身份信息)。我们一步步来看一下代码。完整代码可以通过以下链接访问:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter12/3.pii_detection.py:
-
让我们首先导入本示例所需的库:
import pandas as pd from presidio_analyzer import AnalyzerEngine from presidio_anonymizer import AnonymizerEngine from presidio_anonymizer.entities import OperatorConfig -
我们创建一个示例 DataFrame,其中有一列名为
text,包含包含不同类型 PII(例如,姓名、电子邮件地址和电话号码)的句子:data = { 'text': [ "Hello, my name is John Doe. My email is john.doe@example.com", "Contact Jane Smith at jane.smith@work.com", "Call her at 987-654-3210.", "This is a test message without PII." ] } df = pd.DataFrame(data) -
我们初始化
AnalyzerEngine来检测PII 实体,并初始化AnonymizerEngine来匿名化检测到的 PII 实体:analyzer = AnalyzerEngine() anonymizer = AnonymizerEngine() -
接下来,我们将定义一个匿名化函数,该函数在文本中检测 PII 并根据实体类型应用掩码规则:
def anonymize_text(text): analyzer_results = analyzer.analyze(text=text, entities=["PERSON", "EMAIL_ADDRESS", "PHONE_NUMBER"], language="en") operators = { "PERSON": OperatorConfig("mask", {"masking_char": "*", "chars_to_mask": 4, "from_end": True}), "EMAIL_ADDRESS": OperatorConfig("mask", {"masking_char": "*", "chars_to_mask": 5, "from_end": True}), "PHONE_NUMBER": OperatorConfig("mask", {"masking_char": "*", "chars_to_mask": 6, "from_end": True}) } anonymized_result = anonymizer.anonymize( text=text, analyzer_results=analyzer_results, operators=operators) return anonymized_result.textanonymize_text函数旨在通过匿名化特定类型的实体来保护给定文本中的敏感信息。它首先分析文本,识别出姓名(PERSON)、电子邮件地址(EMAIL_ADDRESS)和电话号码(PHONE_NUMBER)等实体。对于每种实体类型,它应用掩码操作来隐藏部分信息。具体来说,它会掩盖人名的最后四个字符、电子邮件地址的最后五个字符和电话号码的最后六个字符。该函数返回匿名化后的文本,确保个人信息被隐藏,同时保留文本的整体结构。 -
将匿名化函数应用于 DataFrame:
df['anonymized_text'] = df['text'].apply(anonymize_text) -
显示 DataFrame:
0 Hello, my name is John. My email is john.d... 1 Contact Jane S at jane.smith@wor* 2 Call her at 987-65. 3 This is a test message without PII.
通过使用这些配置,您可以根据特定需求定制匿名化过程,确保敏感信息得到适当保护。这种方法有助于您遵守隐私法规,并保护数据集中的敏感信息。
虽然删除 PII 对于保护隐私和确保数据合规性至关重要,但文本预处理的另一个关键方面是处理稀有词汇和拼写变体。
处理稀有词汇和拼写变体
大型语言模型(LLMs)的崛起彻底改变了我们与技术互动和处理信息的方式,特别是在处理拼写变化和罕见词汇的领域。在 LLMs 出现之前,管理这些语言挑战需要大量的人工努力,通常涉及专业知识和精心设计的算法。传统的拼写检查器和语言处理工具在处理罕见词和变化时常常力不从心,导致频繁的错误和低效。今天,像 GPT-4、Llama3 等 LLMs 通过利用庞大的数据集和复杂的机器学习技术,已经彻底改变了这一局面,它们能够理解并生成适应各种拼写变化和罕见术语的文本。这些模型能够识别并修正拼写错误,提供上下文适当的建议,并准确解释罕见词汇,从而提高文本处理的精准度和可靠性。
处理罕见词
在像 GPT-3 和 GPT-4 这样的 LLMs 时代,处理罕见词相比传统的自然语言处理(NLP)方法已经不再是一个大问题。这些模型在庞大而多样的数据集上进行了训练,使它们能够理解并生成带有罕见甚至未见过的词汇的文本。然而,在文本预处理和有效处理罕见词方面仍然需要一些注意事项。
那么,如何使用 LLMs 处理罕见词呢?我们需要理解一些关键概念,从分词开始。我们这里不深入探讨分词,因为稍后会有专门的部分进行讨论;现在,假设 LLMs 使用子词分词方法,将罕见词拆解成更常见的子词单元。这有助于通过将罕见词拆解成熟悉的组件来管理词汇外(OOV)词汇。关于 LLMs 的另一个有趣之处是,即使它们本身不认识某个词,它们也具备上下文理解能力,这意味着 LLMs 能够通过上下文推测罕见词的含义。
在下面的代码示例中,我们将测试 GPT-2 是否能处理罕见词。您可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter12/4.rare_words.py找到代码:
-
让我们导入所需的库:
from transformers import GPT2LMHeadModel, GPT2Tokenizer -
初始化 GPT-2 的分词器和模型:
tokenizer = GPT2Tokenizer.from_pretrained("gpt2") model = GPT2LMHeadModel.from_pretrained("gpt2") -
使用罕见词定义文本提示:
text = "The quokka, a rare marsupial," -
将输入文本编码为标记:
indexed_tokens = tokenizer.encode(text, return_tensors='pt') -
生成文本直到输出长度达到 50 个标记。模型根据输入提示生成文本,利用其对上下文的理解来处理罕见词:
output_text = model.generate(indexed_tokens, max_length=50, num_beams=5, no_repeat_ngram_size=2, early_stopping=True)给定代码片段中的
generate函数用于根据提供的输入标记生成模型的文本输出。此函数调用中的参数控制了文本生成过程中的各个方面:-
indexed_tokens:这表示模型将用来开始生成文本的输入序列。它由令牌化的文本组成,作为生成的起点。 -
max_length=50:此参数设置生成文本的最大长度。模型将生成多达 50 个令牌,包括输入令牌,确保输出不超过此长度。 -
num_beams=5:这控制梁搜索过程,模型在生成过程中跟踪最有可能的五个序列。梁搜索通过同时探索多个可能的结果并选择最可能的结果来提高生成文本的质量。 -
no_repeat_ngram_size=2:这防止模型在生成文本中重复任何两个令牌(二元组)。通过确保相同的短语不会多次出现,它有助于生成更连贯和少重复的输出。 -
early_stopping=True:此参数允许生成过程在所有梁都到达文本序列末端(例如,句子结束令牌)时提前停止。通过在已经生成了完整且合理的输出时避免不必要的继续,这可以使生成过程更高效。
-
这些参数可以根据所需的输出进行调整。例如,增加max_length会生成更长的文本,而修改num_beams可以在质量和计算成本之间进行平衡。调整no_repeat_ngram_size可以改变重复预防的严格性,而切换early_stopping可能会影响生成文本的效率和长度。我建议你去尝试这些配置,看看它们的输出 会如何受到影响:
-
生成的令牌被解码成可读的文本:
output_text_decoded = tokenizer.decode(output_text[0], skip_special_tokens=True) -
打印解码后的文本:
The quokka, a rare marsupial, is one of the world's most endangered species.
正如我们所见,模型理解了短尾树袋鼠的含义,并创建了一个单词序列,这是从提示中继续的额外文本,展示了 LLM 的语言生成能力。这是可能的,因为 LLM 将令牌转换为称为嵌入的数字表示,我们将在稍后看到,它捕捉了单词的含义。
我们讨论了在文本预处理中使用罕见词。现在让我们转向另一个挑战——拼写错误和拼写错误。
处理拼写变体和拼写错误
拼写变体和拼写错误的挑战在于它可能导致相似单词的不同标记化方式。在 LLM 时代,处理拼写和拼写错误已变得更加复杂。LLM 可以理解上下文并生成文本,通常会隐式地纠正这些错误。然而,显式预处理以纠正拼写错误仍然可以提高这些模型的性能,特别是在准确性至关重要的应用中。有多种方法可以解决拼写变体和错误,我们将在接下来的部分中看到,从拼写校正开始。
拼写校正
让我们通过使用 Hugging Face Transformers 的大语言模型(LLM)创建一个修正拼写错误的示例。我们将使用实验性的oliverguhr/spelling-correction-english-base拼写校正模型进行演示。你可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter12/5.spelling_checker.py找到完整的代码:
-
定义拼写校正函数管道。在这个函数内部,我们使用
oliverguhr/spelling-correction-english-base模型初始化拼写校正管道。这个模型是专门为拼写校正任务训练的:def fix_spelling(text): spell_check = pipeline("text2text-generation", model="oliverguhr/spelling-correction-english-base") -
我们使用管道生成校正后的文本。
max_length参数设置为2048,以便处理较长的输入文本:corrected = spell_check(text, max_length=2048)[0]['generated_text'] return corrected -
使用包含拼写错误的示例文本测试该函数:
sample_text = "My name si from Grece." corrected_text = fix_spelling(sample_text) Corrected text: My name is from Greece.
需要注意的是,这是一个实验性模型,它的表现可能会因输入文本的复杂性和上下文而有所不同。为了更稳健的拼写和语法校正,你可以考虑使用更高级的模型;然而,其中一些模型需要认证才能下载或签署协议。因此,为了简便起见,我们在这里使用了一个实验性模型。你可以将其替换为你能够访问的任何模型,从 Llama3 到 GPT4 等等。
拼写校正对于文本预处理任务的重要性将我们引入了模糊匹配的概念,这是一种通过容忍输入文本中的小错误和变化,进一步提高生成内容的准确性和相关性的技术。
模糊匹配
模糊匹配是一种用于比较字符串相似性的技术,即使它们并不完全相同。它就像是在寻找“有点相似”或“足够接近”的单词。因此,我们可以使用模糊匹配算法来识别和映射相似的单词,以及解决变体和小的拼写错误。我们可以通过添加使用TheFuzz库的模糊匹配来增强拼写校正功能。
让我们浏览一下你可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter12/6.fuzzy_matching.py找到的代码:
-
我们将从安装库开始:
pip install thefuzz==0.22.1 -
让我们导入所需的库:
from transformers import pipeline from thefuzz import process, fuzz -
初始化拼写校正管道:
def fix_spelling(text, threshold=80): spell_check = pipeline("text2text-generation", model="oliverguhr/spelling-correction-english-base")oliverguhr/spelling-correction-english-base模型专门为拼写修正任务进行了精细调整,使其成为一个高效且有效的拼写修正工具。该模型已经经过训练,能够识别并修正英语文本中的常见拼写错误,从而提高准确性。它经过优化,适用于文本到文本的生成,使其能够高效地生成输入文本的修正版本,并且计算开销最小。此外,模型的训练可能涉及了包含拼写错误及其修正的语料库,使其能够做出有根据且符合语境的修正。 -
生成与上一节中相同的修正文本:
corrected = spell_check(text, max_length=2048)[0]['generated_text'] -
将原始文本和修正后的文本分解为单词:
original_words = text.split() corrected_words = corrected.split() -
创建一个常见英语单词的词典(你可以扩展这个列表):
common_words = set(['the', 'be', 'to', 'of', 'and', 'a', 'in', 'that', 'have', 'I', 'it', 'for', 'not', 'on', 'with', 'he', 'as', 'you', 'do', 'at']) -
模糊匹配每个单词:
final_words = [] for orig, corr in zip(original_words, corrected_words): if orig.lower() in common_words: final_words.append(orig) else: matches = process.extractOne(orig, [corr], scorer=fuzz.ratio) if matches[1] >= threshold: final_words.append(matches[0]) else: final_words.append(orig) return ' '.join(final_words) -
用包含拼写错误的一些示例文本测试函数:
sample_text = "Lets do a copmarsion of speling mistaks in this sentense." corrected_text = fix_spelling(sample_text) -
打印结果:
Original text: Lets do a copmarsion of speling mistaks in this sentense. Corrected text: Let's do a comparison of speling mistaks in this sentence.
如你所见,并非所有拼写错误都已被修正。通过针对模型常常遗漏的例子进行微调,我们可以获得更好的表现。然而,好消息是!大语言模型(LLM)的兴起使得拼写错误的修正变得不那么重要,因为这些模型设计上是为了理解和处理文本的上下文。即使单词拼写错误,LLM 也能通过分析周围的单词和整体句子结构推断出意图的含义。这种能力减少了对拼写完美的需求,因为焦点转向了传达信息,而不是确保每个单词的拼写都正确。
完成初步的文本预处理步骤后,下一步至关重要的是分块。这一过程涉及将清理过的文本分解成更小、更有意义的单元。我们将在接下来的部分讨论这一点。
分块
分块是自然语言处理(NLP)中的一个基本预处理步骤,它涉及将文本拆分成更小、更易管理的单元,或称“块”。这一过程对于多种应用至关重要,包括文本摘要、情感分析、信息提取等。
为什么分块变得越来越重要?通过将大型文档分解,分块提高了可管理性和效率,尤其是对于具有令牌限制的模型,防止过载并实现更平稳的处理。它还通过允许模型专注于更小、更连贯的文本片段来提高准确性,相较于分析整个文档,这样可以减少噪音和复杂性。此外,分块有助于在每个片段中保持上下文,这对于机器翻译和文本生成等任务至关重要,确保模型能够有效理解和处理文本。
分块可以通过多种方式实现;例如,摘要任务可能更适合段落级分块,而情感分析可能使用句子级分块来捕捉细微的情感变化。在接下来的部分中,我们将专注于固定长度分块、递归分块和语义分块,因为它们在数据领域中更为常见。
实现固定长度分块
固定长度分块涉及将文本分成预定义长度的块,可以按字符数或标记数来划分。通常更为优选,因为它实现简单且确保块的大小一致。然而,由于划分是随机的,它可能会把句子或语义单元拆开,导致上下文的丧失。它适用于需要统一块大小的任务,如某些类型的文本分类。
为了展示固定长度分块,我们将再次使用评论数据,但这次会包括一些较长的评论。你可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter12/7.fixed_chunking.py查看完整示例:
-
让我们先加载示例数据:
reviews = [ "This smartphone has an excellent camera. The photos are sharp and the colors are vibrant. Overall, very satisfied with my purchase.", "I was disappointed with the laptop's performance. It frequently lags and the battery life is shorter than expected.", "The blender works great for making smoothies. It's powerful and easy to clean. Definitely worth the price.", "Customer support was unresponsive. I had to wait a long time for a reply, and my issue was not resolved satisfactorily.", "The book is a fascinating read. The storyline is engaging and the characters are well-developed. Highly recommend to all readers." ] -
导入
TokenTextSplitter类:from langchain_text_splitters import TokenTextSplitter -
初始化
TokenTextSplitter类,设置块大小为50个标记,并且没有重叠:text_splitter = TokenTextSplitter(chunk_size=50, chunk_overlap=0) -
将评论合并成一个文本块进行分块:
text_block = " ".join(reviews) -
将文本拆分为基于标记的块:
chunks = text_splitter.split_text(text_block) -
打印分块:
Chunk 1: This smartphone has an excellent camera. The photos are sharp and the colors are vibrant. Overall, very satisfied with my purchase. I was disappointed with the laptop's performance. It frequently lags and the battery life is shorter than expected. The blender works Chunk 2: great for making smoothies. It's powerful and easy to clean. Definitely worth the price. Customer support was unresponsive. I had to wait a long time for a reply, and my issue was not resolved satisfactorily. The book is a Chunk 3: fascinating read. The storyline is engaging and the characters are well-developed. Highly recommend to all readers.
为了了解不同块大小如何影响输出,你可以修改chunk_size参数。例如,你可以尝试20、70和150标记大小。这里,你可以看到如何调整代码来测试不同的块大小:
chunk_sizes = [20, 70, 150]
for size in chunk_sizes:
print(f"Chunk Size: {size}")
text_splitter = TokenTextSplitter(chunk_size=size, chunk_overlap=0)
chunks = text_splitter.split_text(text_block)
for i, chunk in enumerate(chunks):
print(f"Chunk {i + 1}:")
print(chunk)
print("\n")
我们成功地将评论划分为所需的块,但在继续之前,理解chunk_overlap=0参数的重要性是至关重要的。
块重叠
块重叠是指在拆分文本时,相邻块之间共享的字符或标记数。它是两个块之间“重叠”的文本量。
块重叠非常重要,因为它有助于保持上下文并增强文本的连贯性。通过确保相邻的块共享一些共同的内容,重叠保持连续性,防止重要信息在边界处丢失。例如,如果文档被分割成没有重叠的块,可能会有关键信息被分割成两个块,导致无法访问或丧失意义。在检索任务中,如搜索或问答,重叠确保即使相关细节跨越块边界,也能被捕捉到,从而提高检索过程的效果。例如,如果一个块在句子中间结束,重叠确保整个句子都会被考虑到,这是准确理解和生成回答所必需的。
让我们考虑一个简单的例子来说明块重叠:
Original text:
One of the most important things I didn't understand about the world when I was a child is the degree to which the returns for performance are superlinear.
使用五个单词的块大小和一个单词的重叠,我们将得到以下结果:
Chunk 1: "One of the most important"
Chunk 2: "important things I didn't understand"
Chunk 3: "understand about the world when"
Chunk 4: "when I was a child"
Chunk 5: "child is the degree to"
Chunk 6: "to which the returns for"
Chunk 7: "for performance are superlinear."
正如你所看到的,每个块与下一个块之间有两个单词的重叠,这有助于保持上下文并防止在块边界丢失意义。固定长度的分块将文本分割成大小均匀的段落,但这种方法有时会无法捕捉到有意义的文本单元,特别是在处理自然语言固有的变动性时。另一方面,转向段落分块,通过根据文本的自然结构进行分割,提供了一种更具上下文连贯性的方法。
实现递归字符分块
RecursiveCharacterTextSplitter 是一个复杂的文本分割工具,专为处理更复杂的文本分割任务而设计,特别是当处理需要分解成更小、更有意义的块的长文档时。与简单的文本分割器不同,后者只是将文本切割成固定或可变大小的块,RecursiveCharacterTextSplitter 使用递归方法来分割文本,确保每个块在上下文上既连贯又适合自然语言模型处理。从回顾示例开始,我们将演示如何使用 RecursiveCharacterTextSplitter 将文档分割成段落。完整的代码可以在 github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter12/8.paragraph_chunking.py 中找到:
-
我们创建一个
RecursiveCharacterTextSplitter实例:text_splitter = RecursiveCharacterTextSplitter( separators=["\n\n", "\n", " ", ""], chunk_size=200, chunk_overlap=0, length_function=len )RecursiveCharacterTextSplitter实例是通过特定参数实例化的:-
separators:这是一个分隔符列表,用于分割文本。在这里,它包括双换行符(\n\n)、单换行符(\n)、空格()、以及空字符串("")。这有助于分割器使用自然的文本边界和空白来进行分块。 -
chunk_size:这是每个块的最大大小,设置为 200 个字符。这意味着每个块将最多包含 200 个字符。 -
chunk_overlap:这是相邻块之间重叠的字符数,设置为 0。也就是说,块之间没有重叠。 -
length_function:这是一个用于衡量文本长度的函数,设置为len,它计算字符串中的字符数。
-
-
将文本拆分成块:
chunks = text_splitter.split_text(text_block) -
打印这些块。在第一个块中,用户对智能手机的相机非常满意,赞扬了照片的清晰度和生动的色彩。然而,用户对笔记本电脑的性能感到失望,提到了频繁的卡顿问题:
Chunk 1: This smartphone has an excellent camera. The photos are sharp and the colors are vibrant. Overall, very satisfied with my purchase. I was disappointed with the laptop's performance. It frequently lags用户对搅拌机很满意,指出其在制作果昔方面的高效性、强大的功率和易于清洁的特点。他们认为其性价比很高:
Chunk 2: and the battery life is shorter than expected. The blender works great for making smoothies. It's powerful and easy to clean. Definitely worth the price. Customer support was unresponsive. I had to用户在与客户支持的互动中有不好的体验,提到了长时间等待和未解决的问题。用户认为这本书非常吸引人,情节引人入胜,人物刻画深入,他们强烈推荐给读者:
Chunk 3: wait a long time for a reply, and my issue was not resolved satisfactorily. The book is a fascinating read. The storyline is engaging and the characters are well-developed. Highly recommend to all我们剩下一个单词:
Chunk 4: Readers.
现在,这些块并不完美,但让我们了解 RecursiveCharacterTextSplitter 的工作原理,这样你就可以根据自己的使用场景进行调整:
-
块大小目标:分割器的目标是生成大约 200 个字符的块,但这只是一个最大值,而不是严格要求。它将尽量创建接近 200 个字符的块,但不会超过这个限制。
-
递归方法:递归性质意味着它会重复应用这些规则,通过分隔符列表逐步找到合适的分割点。
-
保持语义意义:通过使用这种方法,分割器尝试将语义相关的内容保持在一起。例如,它会尽量避免在段落或句子中间进行分割。
-
chunk_overlap设置为0,这意味着块之间没有内容重复,每个块都是独立的。 -
len函数用于衡量块的大小,即它计算的是字符而不是词元。
length_function 参数
RecursiveCharacterTextSplitter 中的 length_function 参数是一个灵活的选项,允许你定义如何衡量文本块的长度。虽然 len 是默认且最常见的选择,但也有很多其他选项,从基于词元的到基于单词的,再到自定义实现。
递归切块法专注于根据固定大小和自然分隔符创建块,而语义切块法则更进一步,通过根据文本的意义和上下文对其进行分组。这种方法确保了块不仅在长度上连贯,而且在语义上具有意义,从而提高了后续自然语言处理任务的相关性和准确性。
实现语义切块
语义分块涉及根据语义意义而非仅仅是句法规则或固定长度来拆分文本。在幕后,使用嵌入将相关的句子聚集在一起(我们将在第十三章中深入探讨嵌入,章节标题为《图像与音频预处理与 LLMs》)。我们通常使用语义分块处理需要深度理解上下文的任务,例如问答系统和主题分析。让我们深入了解语义分块背后的过程:
-
文本输入:过程从文本输入开始,可以是一个文档、一组句子或任何需要处理的文本数据。
-
嵌入生成:文本的每个片段(通常是句子或小组句子(块))都被转换为高维向量表示,使用嵌入生成。这些嵌入是由预训练语言模型生成的,关键是要理解这些嵌入捕捉了文本的语义含义。换句话说,我们将文本转化为一个数值表示,它编码了 其意义!
-
相似度测量:然后将这些嵌入进行比较,以衡量文本不同部分之间的语义相似度。常用的技术如余弦相似度,用于量化不同片段之间的相关性。
-
聚类:根据相似度评分,将句子或文本片段聚集在一起。聚类算法将语义相似的句子归为同一组,这样可以确保每个组内的内容保持语义一致性和上下文连贯性。
-
块创建:聚类后的句子被组合成块。这些块被设计为语义上有意义的文本单元,可以更有效地被 NLP 模型处理。
让我们回到产品评论的示例,看看通过语义分块生成了什么样的块。你可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter12/9.semantic_chunking.py找到代码:
-
使用
HuggingFaceEmbeddings初始化SemanticChunker:text_splitter = SemanticChunker(HuggingFaceEmbeddings()) -
将文本拆分成块:
docs = text_splitter.create_documents([text_block]) -
打印块:
Chunk 1: This smartphone has an excellent camera. The photos are sharp and the colors are vibrant. Overall, very satisfied with my purchase. I was disappointed with the laptop's performance. It frequently lags and the battery life is shorter than expected. The blender works great for making smoothies. It's powerful and easy to clean. Chunk 2: Definitely worth the price. Customer support was unresponsive. I had to wait a long time for a reply, and my issue was not resolved satisfactorily. The book is a fascinating read. The storyline is engaging and the characters are well-developed. Highly recommend to all readers.
每个块包含相关的句子,这些句子构成一个连贯的段落。例如,第一个块讨论了各种产品的性能,而第二个块则包括了客户支持体验和书评。这些块在每个段落内保持上下文一致,确保相关信息被组合在一起。需要改进的一点是,第一个块包含了不同产品(智能手机、笔记本电脑和搅拌机)的评价,而第二个块则将客户支持体验与书评混合,这可能被视为语义上不相关。在这种情况下,我们可以进一步将文本拆分成更小、更集中的块,以提高其连贯性,或/和调整语义切分器的参数。
text_splitter = SemanticChunker(
embeddings=embedding_model,
buffer_size=200,
add_start_index=True,
breakpoint_threshold_type='percentile',
breakpoint_threshold_amount=0.9,
number_of_chunks=4,
sentence_split_regex=r'\.|\n|\s'
)
你可以在文档中找到更多关于这些参数的细节:
然而,在我们这个案例中,改进切分的步骤可能是这样的:
-
使用不同的嵌入模型,看看哪一种为你的文本提供了最佳的嵌入。
-
调整缓冲区大小,以找到块大小和连贯性之间的最佳平衡
-
调整阈值类型和数量,以优化基于语义断点的块切分位置
-
自定义句子分割的正则表达式,以更好地适应文本的结构
从切分(chunking)到分词(tokenization)的过渡,意味着从一个将文本划分为更大、更具语法意义的段落(块)的过程,转向将文本划分为更小、更细粒度单元(词元)的过程。让我们来看一下分词是如何工作的。
分词
分词是将一段文本拆分成更小的单元或词元的过程,词元可以是单词、子词或字符。这个过程对于将文本转化为适合计算处理的格式至关重要,使得模型能够在更精细的粒度上学习模式。
在分词阶段,一些关键术语包括 [CLS] 用于分类,[SEP] 用于分隔等。词汇表中的每个词项都会被分配一个 ID,模型内部使用这个 ID 来表示该词项。这些 ID 是整数,通常范围从 0 到词汇表大小减一。
世界上所有的词汇都能放进一个词汇表里吗?答案是不行! OOV(Out-Of-Vocabulary)词是指模型词汇表中没有的词。
现在我们知道了常用的术语,让我们来探讨不同类型的分词以及与之相关的挑战。
单词分词
单词分词是将文本拆分为单个单词的过程。
例如,句子“Tokenization is crucial in NLP!”会被分词成["Tokenization", "is", "crucial", "in", "NLP", "!"]。
词语分词保留了完整的词语,这对于需要词语级理解的任务是有益的。它在词语边界明确的语言中效果良好。这是一种简单的解决方案,但可能导致 OOV 词汇的问题,特别是在医学文本和包含许多拼写错误的文本等专业领域中。你可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter12/10.word_tokenisation.py找到完整的代码。
让我们看一个代码示例:
-
下载必要的 NLTK 数据(只需运行一次):
nltk.download('punkt') -
以以下文本作为示例:
text = "The quick brown fox jumps over the lazy dog. It's unaffordable!" -
执行词语分词:
word_tokens = word_tokenize(text) -
打印输出:
Tokens: ['The', 'quick', 'brown', 'fox', 'jumps', 'over', 'the', 'lazy', 'dog', '.', 'It', "'s", 'unaffordable', '!']
这种词语分词方法适用于处理简单、结构良好的文本,其中每个词语都通过空格和标点符号清晰分隔。它是一种简单的方法,与人类感知词语的方式非常契合。然而,相同词语的不同形式(例如,“run”、“running”、“ran”)被视为不同的标记,这可能会削弱模型的理解能力。它还可能导致词汇量过大,特别是在形态丰富或具有许多独特词汇的语言中。最后,这些模型在训练过程中没有出现过的词语会成为 OOV 标记。
鉴于词语分词的局限性,子词分词方法变得越来越流行。子词分词在词语级别分词和字符级别分词之间取得了平衡,解决了两者的许多不足之处。
子词分词
子词分词将文本拆分成比词语更小的单位,通常是子词。通过将词语拆分成已知的子词,它可以处理 OOV(未登录词)问题。它显著减少了词汇量和参数数量。接下来的部分将介绍子词分词的不同选项。
字节对编码(BPE)
BPE 从单个字符开始,迭代地合并最频繁的标记对以创建子词。它最初作为一种数据压缩算法开发,但已经被改编用于 NLP 任务中的分词。过程如下:
-
从单个字符的词汇表开始。
-
计算文本中所有字符对的频率。
-
合并最频繁的字符对形成新的标记。
-
重复该过程直到达到所需的词汇量。
这种基于频率的合并策略对于具有简单形态结构的语言(例如英语)或需要直接且稳健的分词时非常有用。由于基于频率的合并,它简单且计算高效。我们来演示一个如何实现 BPE 分词的例子。你可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter12/11.bpe_tokeniser.py找到完整示例:
-
加载预训练的分词器:
tokenizer = GPT2Tokenizer.from_pretrained('gpt2') -
加载示例文本:
text = "Tokenization in medical texts can include words like hyperlipidemia." -
对文本进行分词:
tokens = tokenizer.tokenize(text) -
将符号转换为输入 ID:
input_ids = tokenizer.convert_tokens_to_ids(tokens) -
打印符号和 ID,如下所示:
Tokens: ['Token', 'ization', 'Ġin', 'Ġmedical', 'Ġtexts', 'Ġcan', 'Ġinclude', 'Ġwords', 'Ġlike', 'Ġhyper', 'lip', 'idem', 'ia', '.'] Input IDs: [21920, 3666, 287, 1400, 1562, 649, 4551, 3545, 588, 20424, 3182, 1069, 257, 13]
分词输出中的特殊字符“Ġ”在 BPE 分词的上下文中具有特定含义。它表示紧随其后的符号原本前面有空格或位于文本的开头,因此它允许保留有关原始文本中单词边界和空格的信息。
让我们解释一下我们看到的输出:
-
Token和ization:这些是“Tokenization”的子词,分割时没有“Ġ”,因为它们是同一个单词的一部分。 -
in、medical、texts等:这些符号以Ġ开头,表示它们在原文中是独立的单词。 -
hyper、lip、id和emia:这些是hyperlipidemia(高脂血症)的子词。hyper表示这是一个新词,而后续的子词没有hyperlipidemia被分解成hyper(表示过量的前缀)、lip(与脂肪相关)、id(连接元素)和emia(表示血液状况的后缀)。
在探讨了 BPE 分词及其对文本处理的影响后,我们现在将注意力转向 WordPiece 分词,这是一种进一步优化 NLP 任务中子词单元处理的强大方法。
WordPiece 分词
WordPiece,BERT 使用的分词方法,从一个字符的基本词汇表开始,迭代地添加最频繁的子词单元。过程如下所示:
-
从单个字符的基本词汇表和一个用于未知词汇的特殊符号开始。
-
迭代地合并最频繁的符号对(从字符开始)以形成新符号,直到达到预定义的词汇表大小。
-
对于任何给定的词,使用词汇表中最长的匹配子词单元。这个过程称为最大匹配。
WordPiece 分词对于结构复杂的语言(例如韩语和日语)和高效处理多样化词汇至关重要。其有效性源于根据最大化可能性选择合并,因此可能会生成更有意义的子词。然而,一切都有代价,在这种情况下,由于可能性最大化步骤,它的计算密集性更高。让我们看一个使用 BERT 分词器执行 WordPiece 分词的代码示例。您可以在 github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter12/12.tokenisation_wordpiece.py 找到完整的代码。
-
加载预训练分词器:
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased') -
取一些样本文本:
text = "Tokenization in medical texts can include words like hyperlipidemia." -
对文本进行分词。该方法将输入文本分割成 WordPiece 标记。例如,
unaffordable被分解为un,##afford,##able:tokens = tokenizer.tokenize(text) -
将标记转换为输入 ID:
input_ids = tokenizer.convert_tokens_to_ids(tokens) Tokens: ['token', '##ization', 'in', 'medical', 'texts', 'can', 'include', 'words', 'like', 'hyper', '##lip', '##idem', '##ia'] Input IDs: [19204, 10859, 1999, 2966, 4524, 2064, 2421, 2540, 2066, 15088, 17750, 28285, 3676]
## 前缀用于表示该标记是前一个标记的延续。因此,它有助于通过指示应将标记附加到前一个标记而不加空格来重构原始单词。
在审查了诸如 BPE 和 WordPiece 等分词方法之后,关键是考虑如何调整分词器以处理专门领域的数据,例如医学文本,以确保在这些特定领域中进行精确和上下文相关的处理。
领域特定数据
当处理诸如医学文本之类的领域特定数据时,确保分词器能够有效处理专门词汇至关重要。当领域具有高频率的独特术语或专门词汇时,标准分词器可能无法达到最佳性能。在这种情况下,领域特定分词器可以更好地捕捉领域的细微差别和术语,从而提高模型性能。当面临这一挑战时,有一些可选方案:
-
在领域特定文本语料库上训练一个分词器,创建包含专门术语的词汇表
-
考虑通过领域特定标记扩展现有的分词器,而不是从头开始训练
然而,如何确定您需要更进一步地调整数据集上的分词器呢?让我们找找答案。
评估是否需要专门的分词器
正如我们所解释的,当处理诸如医学文本之类的领域特定数据时,评估是否需要专门的分词器是至关重要的。让我们看看几个考虑的关键因素:
-
分析 OOV 率:确定您领域特定语料库中不包含在标准分词器词汇表中的单词的百分比。高 OOV 率表明您领域中许多重要术语未被识别,突显出需要专门的分词器来更好地处理独特的词汇。
-
检查分词质量:通过手动检查样本分词,查看标准分词器如何分割领域特定术语。如果关键术语(如医学术语)经常被分解为无意义的子词,这表明分词器不适合该领域,可能需要定制化。
-
压缩比:使用标准和领域特定分词器测量每个句子的平均词汇数量。领域特定分词器显示出显著较低的比率,表明它在压缩和表示领域知识方面更为高效,减少冗余,提高性能。
例如,在医学语料库中,术语如 心肌梗死 可能被标准分词器分成 myo、cardial 和 infarction,导致意义丧失。然而,专门的医学分词器可能将 心肌梗死 识别为一个单独的术语,保留其含义并提升下游任务如实体识别和文本生成的质量。类似地,如果标准分词器的 OOV 率为 15%,而专门分词器仅为 3%,这清楚地表明需要定制化的需求。最后,如果使用标准分词器的压缩比为每句话 1.8 个词汇,而专门分词器为 1.2 个词汇,则表明专门分词器在捕捉领域特定细微差别方面更为高效。
让我们实现一个小应用程序,以评估不同医疗数据的分词器。示例的代码可在 github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter12/13.specialised_tokenisers.py 上找到:
-
初始化用于生物医学文本的 Stanza:
stanza.download('en', package='mimic', processors='tokenize') nlp = stanza.Pipeline('en', package='mimic', processors='tokenize') -
初始化标准 GPT-2 分词器:
standard_tokenizer = GPT2Tokenizer.from_pretrained("gpt2") -
将
pad_token设置为eos_token:standard_tokenizer.pad_token = standard_tokenizer.eos_token model = GPT2LMHeadModel.from_pretrained("gpt2") -
为模型设置
pad_token_id:model.config.pad_token_id = model.config.eos_token_id -
定义一个包含与心肌梗死和心脏病相关的句子的样本医学语料库:
corpus = [ "The patient suffered a myocardial infarction.", "Early detection of heart attack is crucial.", "Treatment for myocardial infarction includes medication.", "Patients with heart conditions require regular check-ups.", "Myocardial infarction can lead to severe complications." ] -
下面的
stanza_tokenize函数使用 Stanza 流水线对文本进行分词并返回一个词汇列表:def stanza_tokenize(text): doc = nlp(text) tokens = [word.text for sent in doc.sentences for word in sent.words] return tokens -
calculate_oov_and_compression函数对语料库中的每个句子进行分词并计算 OOV 率以及平均每句话的词汇数,并返回所有的词汇。对于标准分词器,它检查词汇表中是否存在这些标记,而对于 Stanza,则不会显式检查 OOV 标记:def calculate_oov_and_compression(corpus, tokenizer): oov_count = 0 total_tokens = 0 all_tokens = [] for sentence in corpus: tokens = tokenizer.tokenize(sentence) if hasattr(tokenizer, 'tokenize') else stanza_tokenize(sentence) all_tokens.extend(tokens) total_tokens += len(tokens) oov_count += tokens.count(tokenizer.oov_token) if hasattr(tokenizer, 'oov_token') else 0 oov_rate = (oov_count / total_tokens) * 100 if total_tokens > 0 else 0 avg_tokens_per_sentence = total_tokens / len(corpus) return oov_rate, avg_tokens_per_sentence, all_tokens -
analyze_token_utilization函数计算语料库中每个标记的频率,并返回一个标记利用率百分比的字典:def analyze_token_utilization(tokens): token_counts = Counter(tokens) total_tokens = len(tokens) utilization = {token: count / total_tokens for token, count in token_counts.items()} return utilization -
calculate_perplexity函数计算给定文本的困惑度,这是衡量模型预测样本能力的指标:def calculate_perplexity(tokenizer, model, text): inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True) with torch.no_grad(): outputs = model(inputs, labels=inputs["input_ids"]) return torch.exp(outputs.loss).item() -
以下脚本通过计算 OOV 率、每句平均标记数、标记利用率和困惑度,评估了标准 GPT-2 分词器和 Stanza 医学分词器的性能。最后,它打印每个分词器的结果并比较它们在
myocardialinfarction术语上的表现:for tokenizer_name, tokenizer in [("Standard GPT-2", standard_tokenizer), ("Stanza Medical", stanza_tokenize)]: oov_rate, avg_tokens, all_tokens = calculate_oov_and_compression(corpus, tokenizer) utilization = analyze_token_utilization(all_tokens) print(f"\n{tokenizer_name} Tokenizer:") print(f"OOV Rate: {oov_rate:.2f}%") print(f"Average Tokens per Sentence: {avg_tokens:.2f}") print("Top 5 Most Used Tokens:") for token, freq in sorted(utilization.items(), key=lambda x: x[1], reverse=True)[:5]: print(f" {token}: {freq:.2%}")
我们来看一下下表中展示的两个分词器的结果:
| 指标 | 标准 GPT-2 分词器 | Stanza 医学分词器 |
|---|---|---|
| OOV 率 | 0.00% | 0.00% |
| 每句平均标记数 | 10.80 | 7.60 |
| 最常用的五个 标记 | . : 9.26% | . : 13.16% |
| ocard : 5.56% | infarction : 7.89% | |
| ial : 5.56% | myocardial : 5.26% | |
| Ġinf : 5.56% | heart : 5.26% | |
| ar : 5.56% | The : 2.63% |
表 12.1 – GPT-2 分词器与专用医学分词器的比较
如表中所示,两个分词器的 OOV 率均为 0.00%,这意味着语料库中的所有标记都被两个分词器识别。Stanza Medical 分词器的每句平均标记数(7.60)低于标准 GPT-2 分词器(10.80)。这表明 Stanza Medical 分词器在将领域特定术语压缩成更少的标记方面更为高效。标准 GPT-2 分词器将有意义的医学术语拆分成更小的子词,从而导致标记利用效率较低(例如,ocard,ial,inf,和ar)。然而,Stanza Medical 分词器保持了医学术语的完整性(例如,infarction和myocardial),使标记更加有意义且与上下文相关。基于分析,Stanza Medical 分词器应该更适用于医学文本处理,原因如下:
-
它将领域特定术语高效地分词成更少的标记
-
它保持医学术语的完整性和意义
-
它提供了更多有意义且与上下文相关的标记,这对于医学领域中的实体识别和文本生成等任务至关重要
标准 GPT-2 分词器在处理一般文本时很有用,但它将医学术语拆分成子词,这可能导致上下文和意义的丧失,因此不太适合专门的医学文本。
词汇大小权衡
更大的词汇表可以捕捉到更多的领域特定术语,但会增加模型的大小和计算需求。找到一个平衡点,既能充分覆盖领域术语,又不至于过度膨胀。
在评估了不同的分词方法的表现后,包括它们如何处理 OOV 词汇以及在压缩领域特定知识时的效率,下一步的逻辑是探讨这些分词输出是如何通过嵌入技术转化为有意义的数值表示的。这一过渡非常关键,因为嵌入构成了模型理解和处理分词文本的基础。
将 tokens 转换为嵌入
嵌入是词语、短语或整个文档在高维向量空间中的数值表示。实质上,我们将词语表示为数字数组,以捕捉它们的语义意义。这些数值数组旨在编码词语和句子的潜在意义,使得模型能够以有意义的方式理解和处理文本。让我们从分词到嵌入的过程进行探索。
该过程从分词开始,将文本拆分为可管理的单位,称为 tokens。例如,句子“The cat sat on the mat”可能被分词为单个词或子词单元,如 [“The”, cat, “sat”, “on”, “the”, “mat”]。一旦文本被分词,每个 token 会通过嵌入层或查找表映射到一个嵌入向量。这个查找表通常会用随机值初始化,然后进行训练,以捕捉词语之间的有意义关系。例如,cat 可能被表示为一个 300 维的向量。
像 BERT 或 GPT 这样的高级模型会生成上下文化的嵌入,其中一个词的向量表示会受到其周围词语的影响。这使得模型能够理解细微差别和上下文,比如区分“bank”在“river bank”和“financial bank”中的不同含义。
让我们更详细地了解这些模型。
BERT – 上下文化嵌入模型
BERT 是谷歌开发的强大 NLP 模型。它属于基于 transformer 的模型家族,并且在大量文本数据上进行预训练,以学习词语的上下文化表示。
BERT 嵌入模型是 BERT 架构的一个组件,用于生成上下文化的词嵌入。与传统的词嵌入不同,传统词嵌入为每个词分配一个固定的向量表示,而 BERT 嵌入是上下文相关的,能够捕捉词语在整个句子中的意义。以下是如何使用 BERT 嵌入模型的解释 github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter12/14.embedding_bert.py:
-
加载预训练的 BERT 模型和分词器:
tokenizer = BertTokenizer.from_pretrained("bert-base-uncased") model = BertModel.from_pretrained("bert-base-uncased") -
编码输入文本:
input_text = "BERT embeddings capture contextual information." inputs= tokenizer(input_text, return_tensors="pt") -
获取 BERT 嵌入:
with torch.no_grad(): outputs = model(inputs) -
打印嵌入:
print("Shape of the embeddings tensor:", last_hidden_states.shape) Shape of the embeddings tensor: torch.Size([1, 14, 768])
嵌入张量的形状将是(1,sequence_length,hidden_size)。其中,sequence_length是输入句子中的标记数,hidden_size是 BERT 模型中的隐藏状态大小(bert-base-uncased为768)。
[CLS] 标记嵌入表示整个输入句子,通常用于分类任务。它是输出张量中的第一个标记:
CLS token embedding: [ 0.23148441 -0.32737488 ... 0.02315655]
句子中第一个实际单词的嵌入表示该特定单词的上下文化嵌入。句子中第一个实际单词的嵌入不仅仅是该单词的静态或孤立表示。相反,它是一个“上下文感知”或“上下文化”的嵌入,意味着它反映了该单词的含义如何受到句子中周围单词的影响。简单来说,这个嵌入不仅捕捉了单词的内在含义,还反映了基于周围单词提供的上下文,单词含义如何变化。这是像 BERT 这样的模型的一个关键特性,它根据单词在不同上下文中的使用,生成不同的嵌入。
First word embedding: [ 0.00773875 0.24699381 ... -0.09120814]
这里需要理解的关键点是,我们从文本开始,输出是向量或嵌入。使用transformers库提供的分词器时,分词步骤是在幕后进行的。分词器将输入句子转换为标记及其对应的标记 ID,然后传递给 BERT 模型。请记住,句子中的每个单词都有自己的嵌入,反映了该单词在句子上下文中的含义。
BERT 的多功能性使其在各种 NLP 任务中表现出色。然而,随着对更高效和任务特定的嵌入需求的增加,出现了像BAAI 通用嵌入(BGE)这样的模型。BGE 设计为更小、更快,提供高质量的嵌入,优化了语义相似性和信息检索等任务。
BGE
BAAI/bge-small-en 模型是北京人工智能研究院(BAAI)开发的一系列 BGE 模型的一部分。这些模型旨在生成高质量的文本嵌入,通常用于文本分类、语义搜索等各种 NLP 任务。
这些模型为文本生成嵌入(向量表示)。嵌入捕捉文本的语义含义,使其在诸如相似性搜索、聚类和分类等任务中非常有用。bge-small-en模型是该系列中的一个较小的、专为英语设计的模型。我们来看一个例子。此示例的完整代码可在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter12/15.embedding_bge.py查看:
-
定义模型名称和参数:
model_name = "BAAI/bge-small-en" model_kwargs = {"device": "cpu"} encode_kwargs = {"normalize_embeddings": True} -
初始化嵌入模型:
bge_embeddings = HuggingFaceBgeEmbeddings( model_name=model_name, model_kwargs=model_kwargs, encode_kwargs=encode_kwargs ) -
我们随机选取几句话进行嵌入:
sentences = [ "The quick brown fox jumps over the lazy dog.", "I love machine learning and natural language processing." ] -
为每个句子生成嵌入:
embeddings = [bge_embeddings.embed_query(sentence) for sentence in sentences] -
打印嵌入:
[-0.07455343008041382, -0.004580824635922909, 0.021685084328055382, 0.06458176672458649, 0.020278634503483772]... Length of embedding: 384 Embedding for sentence 2: [-0.025911744683980942, 0.0050039878115057945, -0.011821565218269825, -0.020849423483014107, 0.06114110350608826]...
像 bge-small-en 这样的 BGE 模型,设计上比 BERT 等较大、通用的模型更小、更高效,适合用于嵌入生成任务。这种高效性转化为更低的内存使用和更快的推理时间,使得 BGE 模型特别适合计算资源有限或实时处理至关重要的应用。尽管 BERT 是一个多功能的通用模型,能够处理广泛的 NLP 任务,但 BGE 模型特别针对生成高质量的嵌入进行了优化。这种优化使得 BGE 模型能够在特定任务中提供可比或甚至更优的性能,如语义搜索和信息检索,在这些任务中,嵌入的质量至关重要。通过专注于嵌入的精确度和语义丰富性,BGE 模型利用了先进的技术,如学习稀疏嵌入,结合了稠密和稀疏表示的优势。这种有针对性的优化使得 BGE 模型在需要细致文本表示和高效处理的场景中表现出色,相比更通用的 BERT 模型,它们在以嵌入为核心的应用中是更好的选择。
在 BERT 和 BGE 成功的基础上,通用文本嵌入(GTEs)的引入标志着又一步重要的进展。GTE 模型专门针对各种文本相关应用进行了精细调优,以提供强大且高效的嵌入。
GTE
GTE 代表了下一代嵌入模型,旨在应对对专业化和高效文本表示日益增长的需求。GTE 模型在为特定任务(如语义相似性、聚类和信息检索)提供高质量嵌入方面表现出色。让我们看看它们的实际应用。完整的代码可在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter12/16.embedding_gte.py找到:
-
加载 GTE-base 模型:
model = SentenceTransformer('thenlper/gte-base') -
我们随机选取一些文本进行嵌入:
texts = [ "The quick brown fox jumps over the lazy dog.", "I love machine learning and natural language processing.", "Embeddings are useful for many NLP tasks." ] -
生成嵌入:
embeddings = model.encode(texts -
打印嵌入的形状:
print(f"Shape of embeddings: {embeddings.shape}") Shape of embeddings: (3, 768) -
打印第一个嵌入的前几个值:
[-0.02376037 -0.04635307 0.02570779 0.01606994 0.05594607]
GTE 的一大亮点是其高效性。通过保持较小的模型尺寸和更快的推理时间,GTE 非常适合实时应用和计算资源受限的环境。这种高效性并不以牺牲性能为代价;GTE 模型在各种文本处理任务中依然能交付出色的结果。然而,它们减少的复杂性处理可能成为一个限制,因为较小的模型尺寸可能妨碍它们有效处理高度复杂或细致的文本。这可能导致对微妙上下文细节的捕捉不够准确,从而影响更复杂场景下的表现。此外,GTE 对高效性的专注可能导致其泛化能力下降;尽管在特定任务中表现优秀,但它可能在适应多样化或不常见的语言输入时遇到困难。而且,模型较小的尺寸可能限制其微调的灵活性,由于学习和存储特定领域复杂模式的能力较弱,可能无法很好地适应专业化任务或领域。
选择合适的嵌入模型
在为你的应用选择模型时,首先确定你的具体使用场景和领域。你是否需要一个用于分类、聚类、检索或摘要的模型,且你的领域是法律、医学还是通用文本,将显著影响你的选择。
接下来,评估模型的大小和内存使用情况。较大的模型通常能提供更好的性能,但也伴随着更高的计算要求和较高的延迟。在初期原型开发阶段,可以选择较小的模型,随着需求的发展,再考虑过渡到更大的模型。注意嵌入维度,因为更大的维度能够提供更丰富的数据表示,但也更具计算强度。在捕捉详细信息与保持操作效率之间找到平衡非常重要。
仔细评估推理时间,特别是如果你有实时应用需求;延迟较高的模型可能需要 GPU 加速才能达到性能标准。最后,使用像Massive Text Embedding Benchmark(MTEB)这样的基准测试来评估模型的性能,以便在不同的度量标准之间进行比较。考虑到内在评估,它考察模型对语义和句法关系的理解,以及外在评估,它则评估模型在特定下游任务上的表现。
利用嵌入解决实际问题
随着 BERT、BGE 和 GTE 等嵌入模型的进展,我们可以应对各个领域的广泛挑战。这些模型使我们能够解决不同的问题,具体如下:
-
语义搜索:嵌入通过捕捉查询和文档的上下文含义来提高搜索相关性,从而提升搜索结果的准确性。
-
推荐系统:它们根据用户的偏好和行为,提供个性化的内容推荐,量身定制推荐内容以满足个人需求。
-
文本分类:嵌入使得文档能够准确地归类到预定义的类别中,例如情感分析或主题识别。
-
信息检索:它们提高了从庞大数据集中检索相关文档的准确性,提升了信息检索系统的效率。
-
自然语言理解:嵌入支持如命名实体识别(NER)等任务,帮助系统识别和分类文本中的关键实体。
-
聚类技术:它们能够改善大型数据集中相似文档或主题的组织结构,帮助实现更好的聚类和数据管理。
-
多模态数据处理:嵌入对于整合和分析文本、图像和音频数据至关重要,有助于提供更全面的洞察和增强的决策能力。
让我们总结一下本章的学习内容。
总结
在本章中,我们回顾了文本预处理,这是自然语言处理中的一个关键步骤。我们介绍了不同的文本清理技术,从处理 HTML 标签和大小写到解决数字值和空格问题。我们深入探讨了分词,分析了词汇分词和子词分词,并提供了实用的 Python 示例。最后,我们介绍了多种文档嵌入方法,并介绍了当前最受欢迎的嵌入模型。
在下一章中,我们将继续探索非结构化数据,深入研究图像和音频的预处理。
第十三章:使用 LLMs 进行图像和音频预处理
在本章中,我们深入探讨了非结构化数据的预处理,特别关注图像和音频。我们探讨了从这些类型的媒体中提取有意义信息的各种技术和模型。讨论包括对图像预处理方法的详细审查,使用光学字符识别(OCR)从图像中提取文本,BLIP 模型生成图像标题的能力,以及 Whisper 模型将音频转换为文本的应用。
在本章中,我们将涵盖以下主题:
-
图像预处理的当前时代
-
从图像中提取文本
-
处理音频数据
技术要求
本章的完整代码可以在以下 GitHub 存储库中找到:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/tree/main/chapter13。
让我们安装本章中将使用的必要库:
pip install torchvision
pip install keras==3.4.1
pip install tensorflow==2.17.0
pip install opencv-python==4.10.0.84
pip install opencv-python==4.10.0.84
pip install paddleocr==2.8.1
pip install paddlepaddle==2.6.1
图像预处理的当前时代
在先进的视觉模型时代,如扩散模型和 OpenAI 的 CLIP 等模型,预处理变得至关重要,以确保图像的质量、一致性和适用性,用于训练和推断。这些模型要求图像以最大化它们学习复杂模式和生成高质量结果的能力的格式。在本节中,我们将逐步进行所有预处理步骤,使您的图像准备好进行后续任务。
在本节中,我们将使用一个常见的用例,即为训练扩散模型准备图像。您可以在 GitHub 存储库中找到此练习的代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter13/1.image_prerpocessing.py。
让我们开始加载一些图像。
加载图像
执行以下步骤加载图像:
-
首先,我们加载本练习所需的包:
from PIL import Image import numpy as np import cv2 import requests from io import BytesIO import matplotlib.pyplot as plt import tensorflow as tf from tensorflow.keras.preprocessing.image import ImageDataGenerator然后,我们将图像加载到我们的环境中。我们将使用 Python Pillow 库来处理加载图像。
-
接下来,我们创建一个函数从 URL 加载图像。这个函数从给定的 URL 获取图像,并使用
BytesIO加载到PIL图像对象中处理字节数据:def load_image_from_url(url): response = requests.get(url) img = Image.open(BytesIO(response.content)) return img -
然后,我们将创建一个辅助函数来显示我们的图像。我们将在整个章节中使用这个函数:
def show_image(image, title="Image"): plt.imshow(image) plt.title(title) plt.axis('off') plt.show() -
现在,我们将图像 URL 传递给我们的
load_image_from_url函数。在这里,我们使用了来自 Unsplash 的随机图像 URL,但您可以使用您可以访问的任何图像:image_url = "https://images.unsplash.com/photo-1593642532871-8b12e02d091c" image = load_image_from_url(image_url) -
让我们显示刚刚使用我们创建的函数加载的原始图像:
show_image(image, "Original Image")这将显示以下输出图像:

图 13.1 – 预处理前的原始图像
图像预处理对于将视觉数据输入到机器学习(ML)模型中至关重要。让我们深入探讨每种技术,解释其概念并通过 Python 代码展示其应用。
调整大小和裁剪
有效的预处理可以显著提高 AI 和机器学习(ML)模型的性能,通过确保图像中最相关的特征得到突出并易于检测。裁剪是一种帮助模型聚焦于相关特征的技术。其主要思路是修剪或切除图像的外边缘,以改善画面构图,聚焦于主要对象,或去除不需要的元素。裁剪的大小取决于任务的具体要求。例如,在目标检测中,裁剪应聚焦于感兴趣的目标,而在图像分类中,裁剪应确保主要对象居中并占据大部分画面。
裁剪图像有许多不同的技术,从简单的固定大小裁剪到更复杂的对象感知裁剪。固定大小裁剪涉及将所有图像调整为预定的大小,确保数据集的一致性,这对于需要标准化输入大小的应用非常有用,比如训练某些类型的神经网络。然而,如果主要对象未居中,这可能会导致重要信息的丢失。保持纵横比通过在裁剪时保持原图像的纵横比,避免了失真,通常通过填充(给图像添加边框以达到所需尺寸)或缩放(在保持纵横比的同时调整图像大小,然后裁剪到目标尺寸)来实现。中心裁剪是在图像的中心进行裁剪,假设主要对象通常位于中间,这在图像分类任务中很常见,其中主要对象应占据画面的大部分。对象感知裁剪使用算法检测图像中的主要对象,并围绕其进行裁剪,确保无论对象在原始图像中的位置如何,始终突出显示主要对象。这种技术在目标检测和识别任务中尤为有用。
调整大小是 AI 和机器学习任务中图像预处理的基本步骤,主要集中在将图像的尺寸调整为模型所需的标准大小。此过程对于确保输入数据的一致性并适应各种 AI 和机器学习算法的具体要求至关重要。
让我们在前一节开始的图像预处理流程中添加一些步骤,以查看裁剪和调整大小的效果。以下函数将图像调整大小到指定的目标尺寸(在本例中为 256x256 像素)。我们期望图像以统一的尺寸缩小以适应目标尺寸:
def resize_and_crop(image, target_size):
image = image.resize((target_size, target_size),
Image.LANCZOS)
return image
target_size = 256
processed_image = resize_and_crop(image, target_size)
让我们使用以下代码打印结果图像:
show_image(processed_image, "Resized and Cropped Image")
这将显示以下输出:

图 13.2 – 调整大小和裁剪后的图像
正如我们从图 13**.2中看到的那样,图像被调整为一个 256x256 像素的正方形,改变了原始图像不是正方形的长宽比。因此,调整大小确保所有数据具有统一的输入尺寸,从而便于批处理和将数据传递给模型进行训练。
接下来,我们将讨论图像的标准化,这与前几章讨论的特征标准化并无太大差异。
对数据集进行标准化和归一化
为了确保数据一致性并帮助模型训练更快收敛,我们可以强制输入数据处于一个共同的数值范围内。这种调整涉及将输入数据缩放到0和1之间,也被称为标准化或使用数据集的均值和标准差进行归一化。
对于大多数深度学习模型,将像素值强制限制在范围[0, 1]或[-1, 1]是标准做法。这可以通过将像素值除以 255(对于[0, 1])或减去均值并除以标准差(对于[-1, 1])来实现。在图像分类任务中,这种策略确保输入图像具有一致的像素值。例如,在一个手写数字数据集(如 MNIST)中,标准化或归一化像素值有助于模型更有效地学习数字的模式。在目标检测任务中,它有助于准确检测和分类图像中的对象。然而,标准化和归一化不仅限于图像预处理;它们是为任何机器学习问题准备数据的基本步骤。
让我们通过添加标准化和归一化步骤来扩展前面的示例。第一个函数执行标准化,以确保像素值在一个共同的尺度内,即在范围[0, 1]之间,我们通过除以 255 来实现这一点:
def normalize(image):
image_array = np.array(image)
normalized_array = image_array / 255.0
return normalized_array
normalized_image = normalize(processed_image)
可以在以下图中看到标准化后的图像:

图 13.3 – 标准化后的图片
正如我们从图 13**.3中看到的那样,从视觉上看,图像保持不变,至少对于人眼来说是这样的。标准化不会改变像素的相对强度;它只是将它们缩放到不同的范围,因此内容和细节应该保持不变。
让我们继续进行标准化练习。在标准化之前,像素值在范围[0, 255]内,并遵循图像强度的自然分布。标准化的想法是将所有像素值转换为具有均值0和标准差1。让我们看看如何在以下代码中实现:
def standardize(image):
image_array = np.array(image)
mean = np.mean(image_array, axis=(0, 1), keepdims=True)
std = np.std(image_array, axis=(0, 1), keepdims=True)
standardized_array = (image_array - mean) / std
return standardized_array
standardized_image = standardize(processed_image)
在这种情况下,由于标准化将均值转移到0并缩放值,图像的外观可能会发生变化。这可能使图像看起来不同,可能更加对比或亮度改变。但是,图像内容仍应可识别。

图 13.4 – 标准化后的图像
除了在图 13**.4中显示的变换后的图像外,这里还打印了数值的均值和标准差:
Mean after standardization: 0.0
Standard deviation after standardization: 1.000000000000416
这证实了标准化已正确缩放像素值。现在让我们继续进行数据增强部分。
数据增强
数据增强旨在通过应用随机变换(如旋转、翻转、平移、颜色抖动和对比度调整)在数据集中创建更多的变化。这通过修改现有图像的版本人为扩展数据集,有助于模型的泛化和性能,特别是在使用有限数据时。
常见的增强技术包括几何变换,如旋转、翻转和缩放,这些变换改变了图像的空间方向和大小。例如,将图像旋转 15 度或水平翻转可以为模型创造新的视角。调整亮度、对比度或色调等颜色空间变化可以模拟不同的光照条件,提高模型在不同环境中识别对象的能力。添加噪声或模糊可以帮助模型更好地适应真实数据中的缺陷和失真。
让我们回到我们的例子,看看如何创建图像变化:
-
首先,我们将定义要应用于图像的变换:
-
旋转范围:在 40 度范围内随机旋转图像。
-
宽度偏移范围:随机水平偏移图像的宽度的 20%。
-
高度偏移范围:随机垂直偏移图像的高度的 20%。
-
剪切范围:随机应用剪切变换。
-
缩放范围:随机缩放图像 20%。
-
水平翻转:随机水平翻转图像。
-
填充模式:定义在变换后如何填充新创建的像素。(这里使用“最近”像素值。)
-
-
让我们创建一个函数来应用这些变换:
datagen = ImageDataGenerator( rotation_range=40, width_shift_range=0.2, height_shift_range=0.2, shear_range=0.2, zoom_range=0.2, horizontal_flip=True, fill_mode='nearest' ) -
然后,我们将应用刚刚定义的变换到图像上:
def augment_image(image): image = np.expand_dims(image, axis=0) # Add batch dimension augmented_iter = datagen.flow(image, batch_size=1) augmented_image = next(augmented_iter)[0] return augmented_image augmented_image = augment_image(normalized_image) show_image(augmented_image, "Augmented Image")这将显示以下图像:

图 13.5 – 图像增强
从图 13.5中可以看到,图像有一些显著的变化;然而,图像依然保持可识别性,图片中的概念没有变化。
注意
由于我们在数据增强阶段使用了一些随机参数,因此此时生成的图像可能会有所不同。
数据增强的重要性在于它能增加数据集的多样性,进而帮助防止过拟合,因为模型通过识别更广泛的示例中的模式和特征,而不是仅仅记住训练数据。让我们进入下一部分,深入探讨噪声减少选项。
噪声减少
图像中的噪声指的是像素值中的随机变化,这些变化可能会扭曲图像的视觉质量,从而影响模型在训练过程中的表现。这些变化通常表现为微小、不规则的斑点或纹理,如随机的点、块或颗粒状纹理,破坏图像的平滑性和清晰度。它们往往使图像看起来不那么锐利,可能会遮挡重要细节,这对视觉解读和依赖清晰、准确数据进行训练的模型都是一个问题。
噪声减少试图减少随机变化,使数据变得更加简洁。像素值的这些随机变化的最小化有助于提高图像质量和模型准确性,因为这些噪声在训练过程中可能会误导模型。在以下小节中,我们将扩展讨论数据领域中一些常见的去噪技术,包括高斯平滑、非局部均值去噪和小波去噪。
高斯平滑
高斯模糊(或高斯平滑)对图像应用高斯滤波器,通过在每个像素周围的指定邻域内取像素值并计算平均值来实现。该滤波器对靠近邻域中心的像素赋予更高的权重,而对远离中心的像素赋予较低的权重,遵循高斯分布。去噪后的图像会显得更平滑,但边缘略微模糊,因此在一些允许或希望轻微模糊的应用中非常有用,比如艺术效果,或在边缘检测算法之前减少噪声。我们来看一下应用高斯平滑的代码:
def gaussian_blur(image):
blurred_image = cv2.GaussianBlur(image, (5, 5), 0)
return blurred_image
让我们展示去噪后的图像:
blurred_image = gaussian_blur(noisy_image)
show_image(blurred_image, "Gaussian Blur")
去噪后的图像可以在图 13.6中看到:

图 13.6 – 去噪图像 – 右侧是高斯模糊和中值模糊的结合
在下一部分,我们将讨论双边滤波器。
双边滤波器
双边滤波器通过同时考虑空间和强度差异来平滑图像。它根据像素之间的空间接近度和颜色相似度来计算平均值。我们来看一下代码:
def bilateral_filter(image):
image_uint8 = (image * 255).astype(np.uint8)
filtered_image = cv2.bilateralFilter(
image_uint8, 9, 75, 75)
filtered_image = filtered_image / 255.0
return filtered_image
bilateralFilter函数接受一些参数,我们需要对其进行解释:
-
9:这是在滤波过程中使用的每个像素邻域的直径。较大的值意味着在计算过程中将考虑更多的像素,导致更强的平滑效果。 -
75:这是颜色空间中的滤波器 sigma 值。较大的值意味着像素邻域内的更远颜色将被混合,导致更大的半相同颜色区域。 -
75:这是坐标空间中的滤波器 sigma 值。较大的值意味着,如果颜色足够接近,更远的像素将相互影响。它控制平滑的程度。
让我们使用这个函数,看看结果输出:
bilateral_filtered_image = bilateral_filter(noisy_image)
show_image(bilateral_filtered_image, "Bilateral Filter")
去噪后的图像可以在图 13.7中看到。

图 13.7 – 去噪图像 – 左侧是双边滤波,右侧是非局部均值去噪
在接下来的章节中,我们将讨论非局部均值去噪。
非局部均值去噪
非局部均值去噪通过比较图像块并平均相似的图像块来减少噪声,即使它们相隔较远。此方法通过比较图像中整个图像的小块像素,而不仅仅是邻近像素。与只考虑附近像素的简单方法不同,非局部均值去噪在图像中查找相似的图像块,即使它们相隔较远。当找到匹配时,该方法将这些相似的图像块平均在一起,以确定最终的像素值。
这种方法特别有效于保留细节和纹理,因为它可以识别并保留图像中始终如一的模式,而不是盲目地对所有内容进行平滑处理。通过仅对真正相似的图像块进行平均,它减少了噪声,同时保持了重要图像特征的完整性,这使得它在需要保留细节的应用中成为一个极好的选择。
让我们看一下代码:
def remove_noise(image):
image_uint8 = (image * 255).astype(np.uint8)
denoised_image = cv2.fastNlMeansDenoisingColored(
image_uint8, None, h=10, templateWindowSize=7,
searchWindowSize=21)
denoised_image = denoised_image / 255.0
return denoised_image
denoised_image = remove_noise(noisy_image)
show_image(denoised_image, "Non-Local Means Denoising")
fastNlMeansDenoisingColored函数将非局部均值去噪算法应用于图像。h=10参数表示滤波强度。较高的值可以去除更多噪声,但也可能去除一些图像细节。用于计算权重的模板块的像素大小由templateWindowSize变量反映。该值应该是奇数。较大的值意味着更强的平滑效果。最后,searchWindowSize=21表示用于计算给定像素加权平均的窗口大小应该是奇数。较大的值意味着更强的平滑效果。
为什么要使用奇数作为窗口大小,例如templateWindowSize和searchWindowSize?
使用奇数的主要原因是确保窗口内有一个明确的中心像素。例如,在一个 3x3 的窗口中(3 是奇数),中心像素是位于位置“(2,2)”的像素。这个中心像素至关重要,因为算法通常会计算周围像素与这个中心像素的相似度。如果使用偶数大小的窗口,就不会有单一的中央像素,如图 13.8所示。

图 13.8 – 使用奇数作为窗口大小
使用奇数简化了中央像素与邻近像素之间的权重和距离计算。这种简化在像非局部均值等算法中非常重要,因为像素之间的距离会影响在平均过程中每个像素所赋予的权重。奇数大小的窗口自然允许简单的索引和较少的计算复杂度。
关于searchWindowSize参数,它定义了算法在当前处理的图像块周围寻找相似图块的区域。为此搜索区域使用奇数大小的窗口,确保了有一个中央像素,搜索会围绕这个像素进行。这有助于准确地识别相似图块,并在整个图像上均匀地应用去噪效果。
去噪后的图像可以在上一节中的图 13.7看到。
在下一节中,我们将讨论最后一种方法——中值模糊。
中值模糊
中值模糊用邻近像素的中值替换每个像素的值。这种方法对于去除“椒盐噪声”非常有效,后者是指像素随机变为黑色或白色,正如我们稍后将看到的那样。我们首先用中值模糊方法对图像进行去噪,然后会看到这种方法如何解决椒盐效应。
以下函数执行medianBlur功能,它要求输入图像是 8 位无符号整数格式(uint8),其中像素值的范围是0到255。通过将图像乘以 255,像素值被缩放到[0, 255]的范围内:
def perform_median_blur(image):
image_uint8 = (image * 255).astype(np.uint8)
#The parameter below specifies the size of the kernel (5x5).
blurred_image = cv2.medianBlur(image_uint8, 5)
blurred_image = blurred_image / 255.0
return blurred_image
让我们使用以下代码展示去噪后的图像:
median_blurred_image = median_blur(noisy_image)
show_image(median_blurred_image, "Median Blur")
去噪后的图像可以在图 13.9中看到:

图 13.9 – 去噪图像 – 中值模糊
如前所述,现在让我们讨论“椒盐噪声”效应。
椒盐噪声
盐与胡椒噪声是一种脉冲噪声,特点是在图像中存在随机分布的黑白像素。此类噪声可能由多种因素引起,如数据传输错误、相机传感器故障或图像获取过程中的环境条件。黑色像素被称为“胡椒噪声”,而白色像素被称为“盐噪声”。这种噪声类型对图像质量影响尤其严重,因为它可能遮挡重要细节,并使得边缘检测和图像修复变得困难。
为了展示这一点,我们创建了一个函数,将这种噪声效果添加到原始图像中,以便我们可以进行去噪处理:
def add_salt_and_pepper_noise(image, salt_prob=0.02, pepper_prob=0.02):
noisy_image = np.copy(image)
num_salt = np.ceil(salt_prob * image.size)
coords = [np.random.randint(0, i - 1, int(num_salt)) for i in image.shape]
noisy_image[coords[0], coords[1], :] = 1
num_pepper = np.ceil(pepper_prob * image.size)
coords = [np.random.randint(0, i - 1, int(num_pepper)) for i in image.shape]
noisy_image[coords[0], coords[1], :] = 0
return noisy_image
这个函数接受三个参数:
-
image:将添加噪声的输入图像 -
salt_prob:将像素变为盐噪声(白色)的概率 -
pepper_prob:将像素变为胡椒噪声(黑色)的概率
这个函数向图像添加盐与胡椒噪声。它首先创建输入图像的副本,以避免修改原始图像。为了引入盐噪声(白色像素),它根据salt_prob参数计算受影响的像素数量,为这些像素生成随机坐标,并将它们设置为白色。同样,对于胡椒噪声(黑色像素),它使用pepper_prob参数计算受影响的像素数量,生成随机坐标,并将这些像素设置为黑色。然后返回带有噪声的图像。
要在数据上应用这一效果,你需要将以下标志设置为True。这个标志可以在add_salt_and_pepper_noise函数定义后找到:
use_salt_and_pepper_noise = True
if use_salt_and_pepper_noise:
noisy_image = add_salt_and_pepper_noise(tensor_to_image(tensor_image))
show_image(noisy_image, "Salt-and-Pepper Noisy Image")
带有噪声的图像可以在图 13.10中看到:

图 13.10 – 盐与胡椒噪声
现在,让我们将到目前为止学习的不同去噪技术应用到前面的图像中。不同的去噪效果可以在图 13.11和图 13.12中看到。

图 13.11 – 左:高斯模糊,右:中值模糊

图 13.12 – 左:双边滤波器,右:非局部均值去噪
如我们所见,中值模糊方法在去除这种噪声方面表现非常优秀,而其他方法则很难去除它。在本章的下一部分,我们将讨论一些在数据领域中变得越来越流行的图像应用场景,比如生成图像标题和从图像中提取文本。
从图像中提取文本
在讨论如何从图像中提取文本时,OCR 技术是最常提到的。OCR 技术使我们能够处理嵌入在图像中的文本信息,从而实现印刷文档的数字化、数据录入自动化,并提高可访问性。
当前 OCR 技术的主要优势之一是显著减少了人工数据输入的需求。例如,企业可以使用 OCR 将纸质文件转换为数字格式,这不仅节省了物理存储空间,还提升了文件管理流程。此转换使得文件的搜索、检索和共享变得更容易,从而简化了操作并提高了生产力。
在交通领域,尤其是自动驾驶汽车中,OCR 技术用于读取道路标志和车牌。这一功能对于导航和确保遵守交通法规至关重要。通过准确解读标识和车辆识别,OCR 有助于自动驾驶汽车的安全和高效运作。
此外,OCR 技术还被应用于社交媒体监控,以检测图像中的品牌标志和文本。这一应用对营销和品牌管理特别有益,因为它使企业能够追踪品牌的可见性和社交平台上的互动。例如,品牌可以利用 OCR 识别未经授权使用其标志的情况,或监控促销材料的传播,从而增强其营销策略并保护品牌身份。
让我们看看如何在数据领域中应用 OCR,使用开源解决方案。
PaddleOCR
PaddleOCR是由 PaddlePaddle 开发的开源 OCR 工具,PaddlePaddle 是百度的深度学习平台。该仓库提供端到端的 OCR 功能,包括文本检测、文本识别和多语言支持(github.com/PaddlePaddle/PaddleOCR)。
PaddleOCR 过程有许多步骤,详见下面的图 13.13:

图 13.13 – OCR 过程逐步解析
让我们一步一步地分解这个过程:
-
该过程从包含文本的输入图像开始。
-
图像预处理:图像可能会经过各种预处理步骤,如调整大小、转为灰度图、降噪,以增强文本的可见性。
-
文本检测:该模型检测图像中包含文本的区域。这可能涉及诸如高效准确场景文本(EAST)或可微分二值化(DB)等算法,用于找到围绕文本的边界框。
-
文本识别:检测到的文本区域会输入到一个识别模型(通常是卷积神经网络(CNN),接着是长短期记忆模型(LSTM)或变换器),将视觉文本转换为数字文本。
-
后处理:识别出的文本可能会通过拼写检查、语法修正或上下文分析进一步优化,以提高准确性。
-
提取文本:最终输出由提取的数字文本组成,准备进一步使用。
-
注释图像:可选地,可以生成原始图像的注释版本,显示检测到的文本区域及其识别出的文本。
起初看起来可能有些复杂,但幸运的是,这些步骤中的大部分都已被抽象化,用户无需操作,由 PaddleOCR 包自动处理。让我们引入一个 OCR 用例,从 YouTube 视频缩略图中提取文本。
YouTube 缩略图
YouTube 缩略图是平台上表示视频的小型可点击图像。它们作为用户在点击观看视频前看到的视觉预览。缩略图对于吸引观众至关重要,因为它们通常在影响观众是否决定观看内容方面发挥重要作用。
通过分析缩略图中的文本,如视频标题和推广语,利益相关者可以洞察观众的参与度和内容趋势。例如,营销团队可以收集一系列视频的缩略图,并使用 OCR 提取在高效内容中频繁出现的关键词和短语。这项分析可以揭示哪些词汇最能引起观众共鸣,帮助创作者优化未来的缩略图,并使信息传达与流行主题对接。此外,提取的文本还可以为搜索引擎优化(SEO)策略提供支持,识别趋势关键词并将其整合到视频标题、描述和标签中,从而提升视频的可发现性。在我们的案例中,我们在 GitHub 仓库中提供了一个文件夹,里面有来自我共同主持的频道Vector Lab的 YouTube 缩略图,讨论的是 Gen AI 和 ML 概念。以下是 GitHub 上图像文件夹的链接:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/tree/main/chapter13/images。
文件夹中的图像如下图所示,目标是传递所有这些图像并提取图像中显示的文本。

图 13.14 – 示例 YouTube 缩略图
让我们看看如何实现这个目标:
-
我们将通过初始化 PaddleOCR 开始:
ocr = PaddleOCR(use_angle_cls=True, lang='en')use_angle_cls=True标志启用 OCR 过程中的角度分类器。角度分类器通过确定图像中文本的方向来提高文本识别的准确性。这对于文本可能未水平对齐(例如,旋转或倾斜文本)的图像尤其有用。lang='en'参数指定了 OCR 的语言。在此情况下,'en'表示要识别的文本是英文。PaddleOCR 支持多种语言,并在你希望进行非英语语言的 OCR 时设置适当的语言。 -
接下来,我们定义提取文本的图像文件夹路径:
folder_path = 'chapter13/images' -
然后,我们指定支持的图片扩展名。在我们的案例中,我们只有
.png,但你可以在文件夹中添加任何图片类型:supported_extensions = ('.png', '.jpg', '.jpeg') -
接下来,我们获取文件夹中所有图片的路径,用于加载图片:
image_paths = [os.path.join(folder_path, file) for file in os.listdir(folder_path) if file.lower().endswith(supported_extensions)] -
然后,我们创建一个空的 DataFrame 来存储结果:
df = pd.DataFrame(columns=['Image Path', 'Extracted Text']) -
我们使用以下代码来检查是否没有返回图片路径,这意味着文件夹中要么没有图片,要么文件夹中的图片没有支持的扩展名:
if not image_paths: print("No images found in the specified folder.") else: for image_path in image_paths: process_image function. This function processes images and extracts text. For each thumbnail image, the function will extract any visible text, such as titles, keywords, or promotional phrases:def process_image(image_path):
result = ocr.ocr(image_path, cls=True)
extracted_text = ""
for line in result[0]:
extracted_text += line[1][0] + " "
print(f"从 {os.path.basename(image_path)} 提取的文本:\n{extracted_text}\n")
df.loc[len(df)] = [image_path, extracted_text]
The `process_image` function performs OCR on an image specified by `image_path`. It starts by invoking the `ocr` method from the `PaddleOCR` library, which processes the image and returns the recognized text along with other details. The function initializes an empty string, `extracted_text`, to accumulate the recognized text. It then iterates through each line of text detected by the OCR process, appending each line to `extracted_text` along with a space for separation. After processing the entire image, it prints the accumulated text along with the filename of the image. Finally, the function adds a new entry to a DataFrame called `df`, storing `image_path` and the corresponding `extracted_text` in a new row, thus updating the DataFrame with the latest OCR results. -
可选地,我们可以使用以下代码将 DataFrame 保存为 CSV 文件:
df.to_csv('extracted_texts.csv', index=False)
结果可以在 Figure 13**.15 中看到,左侧是图片路径,右侧是提取的文本。

图 13.15 – OCR 输出
结果很好;然而,我们可以看到在某些情况下存在拼写错误,可能是由于图片中文字的字体问题。这里的关键是要理解,我们不再需要处理图片,只需要处理文本,从而大大简化了我们的挑战。基于我们在 第十二章 中学到的内容,LLM 时代的文本预处理,我们现在可以通过各种方式操作和清理文本,例如分块、嵌入或通过 大型语言模型(LLMs)处理文本,正如我们在下一部分将看到的那样。
使用 LLM 与 OCR
尽管 OCR 技术取得了进展,但它经常会产生错误,特别是在复杂布局、低质量图片或不常见字体的情况下。这些错误包括识别错误的字符和错误的断词。因此,解决方案是将 OCR 提取的文本通过 LLM 来修正这些错误,因为 LLM 能理解上下文,能够改善语法和可读性。此外,原始 OCR 输出可能格式不一致且难以阅读;LLM 可以重新格式化和重构文本,确保内容连贯且结构良好。这种自动化的校对减少了人工干预的需求,节省了时间并最小化了人为错误。LLM 还能够标准化文本,使其一致,便于与其他系统(如数据库和分析工具)集成。
在这一部分,我们将扩展缩略图示例,将提取的文本通过 LLM 进行清理。要运行此示例,你需要首先完成以下设置。
Hugging Face 设置
为了运行这个示例,你需要在 Hugging Face 上有一个账户,并且需要一个令牌来进行身份验证。按照以下步骤操作:
-
访问
huggingface.co。 -
如果你没有账户,可以创建一个。
-
进入 Settings。
-
然后,点击 Access Tokens。你应该会看到以下页面:

图 13.16 – 在 Hugging Face 中创建新的访问令牌
-
点击创建新令牌按钮生成一个新的个人令牌。
-
请记住复制并保留此令牌,因为我们将在代码文件中粘贴它以进行身份验证!
现在,我们准备深入代码部分。
使用 LLM 清理文本
让我们看一下你可以在 GitHub 仓库中找到的代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter13/3.ocr_with_llms.py:
-
我们首先读取在上一步中提取的 OCR 文本:
df = pd.read_csv('extracted_texts.csv') -
然后,我们初始化 Hugging Face 模型。在这种情况下,我们使用的是
Mistral-Nemo-Instruct-2407,但你可以将其替换为任何你可以访问的 LLM:model_name = "mistralai/Mistral-Nemo-Instruct-2407"Hugging Face 是一个提供多样化预训练模型库的平台,用户可以通过友好的 API 轻松访问和集成这些模型。Hugging Face 的模型附带详细的文档,并且通过一个协作社区推动持续创新。我认为它类似于 GitHub 作为代码库的作用;而 Hugging Face 则作为机器学习模型的库。值得注意的是,Hugging Face 上的许多模型都是免费的,这使它成为个人和研究人员的一个具成本效益的选择。
相比之下,还有许多其他付费模型可供使用,例如 Azure OpenAI,提供访问 GPT-3 和 GPT-4 等模型。这些模型可以通过付费方式访问,并且你需要管理身份验证过程,这与 Hugging Face 的身份验证有所不同。
-
添加在之前设置部分中创建的 Hugging Face API 令牌:
api_token = PromptTemplate class, which helps create a prompt for the model:prompt_template = PromptTemplate(
input_variables=["text"],
template='''
修正以下文本中的拼写错误,并仅返回修正后的文本(小写形式)。响应应使用 JSON 格式,严格按照以下模式:
{{"corrected_text": "修正后的文本(小写形式)"}}
示例:提供了三个示例以指导模型:
输入:显示需要修正的输入文本。
输出:提供修正文本的预期 JSON 格式。这帮助模型学习所需内容,并鼓励它在生成响应时遵循相同的格式。
示例:
输入:"开放与专有 LLM"
输出:{{"corrected_text": "开放与专有 LLM"}}
输入:"如何减轻 AI 和 ML 系统向量实验室中的安全风险"
输出:{{"corrected_text": "如何减轻 AI 和 ML 系统向量实验室中的安全风险"}}
输入:"构建 DBRX 类自定义 LLM 与 Mosaic A1 训练向量实验室"
输出:{{"corrected_text": "构建 DBRX 类自定义 LLM 与 Mosaic A1 训练向量实验室"}}
要修正的文本:占位符{text},在调用模型时将被实际输入文本替换。
要修正的文本:
{text}
最终指令:指定输出应仅为 JSON 格式,这进一步强调了模型应避免不必要的解释或附加文本。
输出(仅 JSON 格式):
'''
)
PromptTemplate 类用于与语言模型一起修正文本中的拼写错误。PromptTemplate 类通过两个关键参数进行初始化:input_variables 和 template。input_variables 参数指定输入变量为["text"],表示将被修正的文本。template 参数包含发送给模型的提示结构。该结构包括明确的指令,要求模型修正拼写错误并以小写格式返回输出,格式为 JSON。JSON 模式指定了期望的输出格式,确保响应的一致性。模板还提供了三个输入文本及其相应修正后的输出示例,指导模型如何处理类似的请求。模板中的{text}占位符将在调用模型时被实际输入文本替换。最终指令强调输出应严格为 JSON 格式,避免任何附加的文本或解释。
-
我们从 Hugging Face 初始化模型,使用我们之前指定的模型名称和 API 密钥:
huggingface_llm = HuggingFaceHub(repo_id=model_name, huggingfacehub_api_token=api_token, model_kwargs={"task": "text-generation"}) -
我们接着将提示模板和模型结合,创建一个链条,该链条将接受输入文本、应用提示并生成输出:
llm_chain = LLMChain(prompt=prompt_template, llm=huggingface_llm) -
我们使用
llm_chain来生成响应:response = llm_chain.invoke(text) -
最后,我们对
ExtractedText列应用文本修正:df['Corrected Text'] = df['Extracted Text'].apply(correct_text)
让我们展示一些结果:
Original: HOW TO MITIGATE SaCURITY RISKS IN AI AND ML SYSTEM VECTOR LAB
Corrected: how to mitigate security risks in ai and ml system vector lab
Original: BUILDING DBRX-CLASS CUSTOM LLMS WITH MOSAIC A1 TRAINING VECTOR LAB
Corrected: building dbrx-class custom llms with mosaic a1 training vector lab
Original: MPROVING TeXT2SO L PeRFORMANCe WITH EASE ON DATABRICKS 7 VECTOR LAB
Corrected: improving text2so l means text2sql, which is challenging for the model to fix unless it is fine-tuned on this type of correction data. Another approach you could try is to include these very technical cases that the model seems to miss in the few-shot examples in the prompt to “teach” the model how to interpret these words.
In the code file on the GitHub repository, you’ll see that we have added error handling and parsing for the JSON output. This is necessary because we are asking the model to return the output in a specific format, but LLMs do not always follow these instructions precisely. There is currently ongoing work on enforcing the output of LLMs in a specific format, but at this point, it is experimental. You can find more information here: [`python.langchain.com/v0.1/docs/integrations/llms/lmformatenforcer_experimental/`](https://python.langchain.com/v0.1/docs/integrations/llms/lmformatenforcer_experimental/).
As of now, we have seen how we can use OCR to extract text from images and then fix the extracted text by passing it to an LLM. In the case of a thumbnail image, this extracted text can also be used as an image caption, as the video’s title is usually depicted on the image. However, there are cases where the image doesn’t contain any text, and we need to ask the model to infer the caption based on what it has seen and understood from the image. This will be the point of discussion for the next part.
Creating image captions
Creating accurate and meaningful captions for images involves not only recognizing and interpreting visual content but also understanding context and generating descriptive text that accurately reflects the image’s content. The complexity arises from the need for models to process various elements in an image, such as objects, scenes, and activities, and then translate these elements into coherent and relevant language. This challenge is further compounded by the diverse and often subtle nature of visual information, which can include different lighting conditions, angles, and contexts.
To showcase the difference between the technique we demonstrated earlier, and the captioning based on image understanding, we will use the same images from the thumbnails and attempt to create captions for them based on the image understanding process instead of the text extraction process. You can find the code for this part here: [`github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter13/4.image_captioning.py`](https://github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter13/4.image_captioning.py).
Now, let’s dive into the code:
1. Let’s start by importing the libraries we’ll need for this example:
```
import os
import pandas as pd
from PIL import Image
import matplotlib.pyplot as plt
from transformers import BlipProcessor, BlipForConditionalGeneration, AutoTokenizer, AutoModelForSeq2SeqLM
from langchain import PromptTemplate, LLMChain
from langchain.llms import HuggingFaceHub
```py
2. We then define the folder containing the images:
```
folder_path = 'chapter13/images'
```py
3. Next, we make a list of supported image extensions:
```
supported_extensions = ('.png', '.jpg', '.jpeg')
```py
4. We then get all image paths for each image in the folder:
```
image_paths = [os.path.join(folder_path, file) for file in os.listdir(folder_path) if file.lower().endswith(supported_extensions)]
```py
5. We create an empty DataFrame to store the results:
```
df = pd.DataFrame(columns=['图片路径', '生成的字幕', '优化后的字幕'])
```py
6. Then, we initialize the BLIP model and processor for image captioning:
```
blip_model = BlipForConditionalGeneration.from_pretrained("Salesforce/blip-image-captioning-base")
blip_processor = BlipProcessor.from_pretrained("Salesforce/blip-image-captioning-base")
```py
The `BlipForConditionalGeneration` model is a pretrained model designed for image captioning. It generates descriptive text (captions) for given images by understanding the visual content and producing coherent and relevant descriptions. The model is based on the BLIP architecture, which is optimized for linking visual and textual information. `BlipProcessor` is responsible for preparing images and text inputs in a format suitable for the BLIP model. It handles the preprocessing of images (such as resizing and normalization) and any required text formatting to ensure that the data fed into the model is in the correct format.
7. Now, we initialize the LLM for text refinement. Once we create the caption with the BLIP model, we will then pass it to an LLM again to clean and optimize the caption:
```
llm_model_name = "google/flan-t5-small" # 你也可以使用 Hugging Face 上的其他模型
tokenizer = AutoTokenizer.from_pretrained(llm_model_name)
model = AutoModelForSeq2SeqLM.from_pretrained(llm_model_name)
```py
This piece of code specifies the name of the pretrained model to be used. Here, `"google/flan-t5-small"` refers to a specific version of the T5 model, called `FLAN-T5 Small`, developed by Google. The `AutoTokenizer` class from Hugging Face’s Transformers library is used to load the tokenizer associated with the specified model. As we learned in *Chapter 12*, *Text Preprocessing in the Era of LLMs*, tokenizers are responsible for converting raw text into token IDs that the model can understand. They handle tasks such as tokenizing (splitting text into manageable units), adding special tokens, and encoding the text in a format suitable for model input. Finally, it loads the `google/flan-t5-small sequence-to-sequence` language model, which is suitable for tasks such as translation, summarization, or any task where the model needs to generate text based on some input text. The model has been pretrained on a large dataset, enabling it to understand and generate human-like text, and it is perfect for our use case of caption generation.
8. Next, we need to chain all our steps together and we will use functionality from LangChain to do so:
```
api_token = "add_your_hugging_face_token"
prompt_template = PromptTemplate(input_variables=["text"], template="修正并优化以下字幕:{text}")
huggingface_llm = HuggingFaceHub(repo_id=llm_model_name, huggingfacehub_api_token=api_token)
llm_chain = LLMChain(prompt=prompt_template, llm=huggingface_llm)
```py
The `PromptTemplate` object, which is used to define how prompts (input requests) are structured for the language model is created here. Here, we need a much simpler prompt than the one in the previous example as the task is simpler to explain to the model. This instructs the model to refine and correct the provided caption. The `{text}` placeholder will be replaced with the actual text that needs refinement. Then, an instance of `HuggingFaceHub` is created and finally, we create the LLMChain to connect the prompt with the language model.
9. We create a `refine_caption` function that accepts a generated caption as input and creates a prompt by formatting `prompt_template` with the input caption. It then uses `llm_chain` to run the prompt through the LLM, generating a refined caption, and it returns the refined caption:
```
def refine_caption(caption):
prompt = prompt_template.format(text=caption)
refined_caption = llm_chain.run(prompt)
return refined_caption
```py
10. We then create the `generate_caption` function, which accepts an image path as input:
```
def generate_caption(image_path):
image = Image.open(image_path).convert("RGB")
inputs = blip_processor(images=image, return_tensors="pt")
outputs = blip_model.generate(inputs)
caption = blip_processor.decode(outputs[0], skip_special_tokens=True)
return caption
```py
This function performs the following:
* The function opens the image file and converts it to RGB format.
* It then processes the image using `blip_processor`, returning a tensor suitable for the BLIP model.
* The function generates a caption by passing the processed image to the BLIP model. It finally decodes the model’s output into a human-readable caption, skipping special tokens, and returns the caption. 11. Finally, we process each image in the folder, generate an image caption, refine it, and append the final result to a DataFrame:
```
如果没有 image_paths:
print("在指定文件夹中未找到图像。")
否则:
对于 image_path in image_paths:
caption = generate_caption(image_path)
print(f"为{os.path.basename(image_path)}生成的字幕:\n{caption}\n")
refined_caption = refine_caption(caption)
print(f"精炼后的字幕:\n{refined_caption}\n")
df.loc[len(df)] = [image_path, caption, refined_caption]
```py
Let’s have a look at the captions generated by this process:

Figure 13.17 – Image caption creation
As we can see, this caption is poor compared to the previous method we demonstrated. The model attempts to understand what is happening in the image and grasp the context, but since the context is derived from a thumbnail, it ends up being quite inadequate. We need to understand that thumbnails often provide limited context about the video content; while they are designed to attract clicks, they may not convey enough information for the model to generate informative captions. The lack of context in combination with the fact that thumbnails are frequently visually cluttered with various images, graphics, and text elements makes it challenging for the model to discern the main subject or context. This complexity can lead to captions that are less coherent or relevant than we have experienced. So, in the case of dealing with thumbnails, the OCR process is best.
However, in cases where images do not contain text, unlike thumbnails that are often filled with written elements, the image understanding process becomes the primary method for generating captions. Since these images lack textual information, relying on the model’s visual understanding is essential for creating accurate and meaningful descriptions. Here is some homework for you: Pass through the BLIP process an image that has no text and see what you get!
But what about videos?
To handle videos, the process involves reading the video file and capturing frames at specified intervals, allowing us to analyze each frame, so, *each image*, independently. Once we have the frames, we can apply techniques like those used for images, such as OCR for text extraction, or image understanding models, such as BLIP, for caption generation.
Next, we will move from image to audio data and discuss how we can simplify the audio processing.
Handling audio data
A lot of work is happening in the audio processing space with the most significant advancements happening in **automatic speech recognition** (**ASR**) models. These models transform spoken language into written text, allowing the seamless integration of voice inputs into text-based workflows, thereby making it easier to analyze, search, and interact with. For instance, voice assistants, such as Siri and Google Assistant, rely on ASR to understand and respond to user commands, while transcription services convert meeting recordings into searchable text documents.
This conversion allows the passing of text input to LLMs to unlock powerful capabilities, such as sentiment analysis, topic modeling, automated summarization, and even supporting chat applications. For example, customer service call centers can use ASR to transcribe conversations, which can then be analyzed for customer sentiment or common issues, improving service quality and efficiency.
Handling audio data as text not only enhances accessibility and usability but also facilitates more efficient data storage and retrieval. Text data takes up less space than audio files and is easier to index and search. Moreover, it bridges the gap between spoken and written communication, enabling more natural and intuitive user interactions across various platforms and devices. For instance, integrating ASR in educational apps can help students with disabilities access spoken content in a text format, making learning more inclusive.
As ASR technologies continue to improve, the ability to accurately and efficiently convert audio to text will become increasingly important, driving innovation and expanding the potential of AI-driven solutions. Enhanced ASR models will further benefit areas such as real-time translation services, automated note-taking in professional settings, and accessibility tools for individuals with hearing impairments, showcasing the broad and transformative impact of this technology.
In the next section, we will discuss the Whisper model, which is effective for transforming audio into text and performing a range of audio processing tasks.
Using Whisper for audio-to-text conversion
The **Whisper model** from OpenAI is a powerful tool for transforming audio to text and serves as a base for many modern AI and ML applications. The applications range from real-time transcription and customer service to healthcare and education, showcasing its versatility and importance in the evolving landscape of audio processing technology:
* Whisper can be integrated into voice assistant systems, such as Siri, Google Assistant, and Alexa, to accurately transcribe user commands and queries.
* Call centers can use Whisper to transcribe customer interactions, allowing for sentiment analysis, quality assurance, and topic detection, thereby enhancing service quality.
* Platforms such as YouTube and podcast services can use Whisper to generate subtitles and transcriptions, improving accessibility and content indexing.
* Whisper can be used in real-time transcription services for meetings, lectures, and live events. This helps create accurate text records that are easy to search and analyze later.
* In telemedicine, Whisper can transcribe doctor-patient conversations accurately, facilitating better record-keeping and analysis. Moreover, it can assist in creating automated medical notes from audio recordings.
* Educational platforms can use Whisper to transcribe lectures and tutorials, providing students with written records of spoken content, enhancing learning and accessibility.
* Security systems use direct audio processing to verify identity based on unique vocal characteristics, offering a more secure and non-intrusive method of authentication.
As a pretrained model, Whisper can be used out of the box for many tasks, reducing the need for extensive fine-tuning and allowing for quick integration into various applications. The model supports multiple languages, making it versatile for global applications and diverse user bases. While Whisper primarily focuses on transforming audio to text, it also benefits from advancements in handling audio signals, potentially capturing nuances, such as tone and emotion. Although direct audio processing (such as emotion detection or music analysis) might require additional specialized models, Whisper’s robust transcription capability is foundational for many applications.
Using some audio from the `@VectorLab`) videos, we will parse the audio through Whisper to get the extracted text.
Extracting text from audio
The following code demonstrates how to use the Whisper model from Hugging Face to transcribe audio files into text. It covers loading necessary libraries, processing an audio file, generating a transcription using the model, and finally decoding and printing the transcribed text. Let’s have a look at the code, which you can also find here: [`github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter13/5.whisper.py`](https://github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter13/5.whisper.py).
Let’s begin:
1. We’ll start by importing the required libraries:
```
导入 torch
从 transformers 导入 WhisperProcessor, WhisperForConditionalGeneration
导入 librosa
```py
2. We start by loading the Whisper processor and model from Hugging Face:
```
processor = WhisperProcessor.from_pretrained("openai/whisper-large-v2")
model = WhisperForConditionalGeneration.from_pretrained("openai/whisper-large-v2")
```py
3. Next, we define the path to your audio file:
```
audio_path = "chapter13/audio/3.chain orchestrator.mp3"
```py
You can replace this file with any other audio you want.
4. Then, we load the audio file:
```
audio, rate = librosa.load(audio 将是一个包含音频样本的 NumPy 数组。
```py
5. `rate` is the sampling rate of the audio file. The `sr=16000` argument resamples the audio to a sampling rate of 16 kHz, which is the required input sampling rate for the Whisper mode. 6. Now, we preprocess the audio file for the Whisper model:
```
input_features = processor(audio, sampling_rate=rate, return_tensors="pt").input_features
```py
7. We then generate the transcription:
```
with torch.no_grad():
predicted_ids = model.generate(input_features)
```py
This line passes the preprocessed audio features to the model to generate transcription IDs. The model produces token IDs that correspond to the transcribed text.
8. Now, we decode the generated transcription:
```
transcription = processor.batch_decode(predicted_ids, skip_special_tokens=True)[0]
```py
This line decodes the predicted token IDs back into readable text. The `[0]` value at the end extracts the first (and only) transcription from the resulting list.
9. Finally, we print the transcribed text:
```
"正如你所看到的,你需要一个我们称之为链协调器的东西来协调所有步骤。所以从提出问题到得到回答的所有步骤。而最受欢迎的开源包是 Lama Index 和 LangChain,我们可以推荐。非常不错。所以这些链,这些步骤进入 RAG 应用或任何其他 LLM 应用,你可以有许多步骤在进行,对吧?所以你需要这个链来帮助它们进行协调"
```py
Is the transcription slow?
Depending on the model size and your hardware capabilities, the transcription process might take some time.
As we can see, the transcription is excellent. Now, in the use case we are dealing with, after transcribing the YouTube video, there are several valuable actions you can take on this project. First, you can create captions or subtitles to improve accessibility for viewers who are deaf or hard of hearing. Additionally, writing a summary or extracting key points can help viewers grasp the main ideas without watching the entire video. The transcription can also be transformed into a blog post or article, providing more context on the topic discussed. Extracting quotes or highlights from the transcription allows you to create engaging social media posts that promote the video. Utilizing the transcription for SEO purposes can improve the video’s search engine ranking by including relevant keywords in the description. You can also develop FAQs or discussion questions based on the video to encourage viewer engagement. Additionally, the transcription can serve as a reference for research, and you might consider adapting it into a script for an audiobook or podcast. Incorporating the transcription into educational materials, such as lesson plans, is another effective way to utilize the content. Lastly, you can create visual summaries or infographics based on the key points to present the main ideas visually. How cool is that?
In the following section, we will expand the use case and do some emotion detection from the transcribed text.
Detecting emotions
Emotion detection from text, often referred to as sentiment analysis or emotion recognition, is a subfield of **natural language processing** (**NLP**) that focuses on identifying and classifying emotions conveyed in written content. This area of study has gained significant traction due to the growing amount of textual data generated across social media, customer feedback, and other platforms.
In our case, we will use the `j-hartmann/emotion-english-distilroberta-base` model, built upon the DistilRoBERTa architecture. The DistilRoBERTa model is a smaller and faster variant of the RoBERTa model, which itself is based on the Transformer architecture. This model is specifically fine-tuned for emotion detection tasks. It has been trained on a dataset designed to recognize various emotions expressed in text, making it adept at identifying and classifying emotions from written content. It is designed to detect the following emotions from text:
* **Joy**: This represents happiness and positivity
* **Sadness**: This reflects feelings of sorrow and unhappiness
* **Anger**: This indicates feelings of frustration, annoyance, or rage
* **Fear**: This conveys feelings of anxiety or apprehension
* **Surprise**: This represents astonishment or unexpectedness
* **Disgust**: This reflects feelings of aversion or distaste
* **Neutral**: This indicates a lack of strong emotion or feeling
These emotions are typically derived from various datasets that categorize text based on emotional expressions, allowing the model to classify input text into these predefined categories.
Let’s have a look at the code, which is also available here: [`github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter13/6.emotion_detection.py`](https://github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter13/6.emotion_detection.py).
Memory check
The following code is memory intensive, so you may need to allocate more memory if working on virtual machines or Google Collab. The code was tested on Mac M1, 16 GB memory.
Let’s start coding:
1. We first import the libraries required for this example:
```
导入 torch
导入 pandas 为 pd
从 transformers 导入 WhisperProcessor, WhisperForConditionalGeneration, AutoModelForSequenceClassification, AutoTokenizer
导入 librosa
导入 numpy 为 np
```py
2. We then load the Whisper processor and model from Hugging Face:
```
whisper_processor = WhisperProcessor.from_pretrained("openai/whisper-large-v2")
whisper_model = WhisperForConditionalGeneration.from_pretrained("openai/whisper-large-v2")
```py
3. Then, we load the emotion detection processor and model from Hugging Face:
```
emotion_model_name = "j-hartmann/emotion-english-distilroberta-base"
emotion_tokenizer = AutoTokenizer.from_pretrained(emotion_model_name)
emotion_model = AutoModelForSequenceClassification.from_pretrained(emotion_model_name)
```py
4. We define the path to your audio file:
```
audio_path = "chapter13/audio/3.chain orchestrator.mp3" # 用实际的音频文件路径替换
```py
5. Once the path is defined, we load the audio file:
```
audio, rate = librosa.load(audio_path, sr=16000)
```py
6. We create a function called `split_audio` to split audio into chunks:
```
def split_audio(audio, rate, chunk_duration=30):
chunk_length = int(rate * chunk_duration)
num_chunks = int(np.ceil(len(audio)/chunk_length))
return [audio[i*chunk_length:(i+1)*chunk_length] for i in range(num_chunks)]
```py
7. We also create a function to transcribe audio using Whisper:
```
def transcribe_audio(audio_chunk, rate):
input_features = whisper_processor(audio_chunk, sampling_rate=rate, return_tensors="pt").input_features
with torch.no_grad():
predicted_ids = whisper_model.generate(input_features)
transcription = whisper_processor.batch_decode(predicted_ids, skip_special_tokens=True)[0]
return transcription
```py
The function preprocesses the audio file for the Whisper model and generates the transcription. Once it’s generated, the function decodes the generated transcription.
8. We then create a function to detect emotions from text using the emotion detection model:
```
def detect_emotion(text):
inputs = emotion_tokenizer(text, return_tensors="pt", truncation=True, padding=True, max_length=512)
outputs = emotion_model(inputs)
predicted_class_id = torch.argmax(outputs.logits, dim=-1).item()
emotions = emotion_model.config.id2label
return emotions[predicted_class_id]
```py
This function begins by tokenizing the input text with `emotion_tokenizer`, converting it into PyTorch tensors while handling padding, truncation, and maximum length constraints. The tokenized input is then fed into `emotion_model`, which generates raw prediction scores (logits) for various emotion classes. The function identifies the emotion with the highest score using `torch.argmax` to determine the class ID. This ID is then mapped to the corresponding emotion label through the `id2label` dictionary provided by the model’s configuration. Finally, the function returns the detected emotion as a readable label!
9. Then, we split the audio into chunks:
```
audio_chunks = split_audio(audio, rate, chunk_duration=30) # 30 秒一块
```py
10. We also create a DataFrame to store the results:
```
df = pd.DataFrame(columns=['块索引', '转录内容', '情感'])
```py
11. Finally, we process each audio chunk:
```
for i, audio_chunk in enumerate(audio_chunks):
transcription = transcribe_audio(audio_chunk,rate)
emotion = detect_emotion(transcription)
# 将结果追加到 DataFrame
df.loc[i] = [i, transcription, emotion]
```py
The output emotions are shown for each chunk of transcribed text, and in our case, all are neutral, as the video is just a teaching concept video:
块索引 情感
0 0 中立
1 1 中立
2 2 中立
Now, we will expand our use case a bit further to demonstrate how you can take the transcribed text and pass it through an LLM to create highlights for the YouTube video.
Automatically creating video highlights
In the era of digital content consumption, viewers often seek concise and engaging summaries of longer videos. Automatically creating video highlights involves analyzing video content and extracting key moments that capture the essence of the material. This process saves time and improves content accessibility, making it a valuable tool for educators, marketers, and entertainment providers alike.
Let’s have a look at the code. You can find it at the following link: [`github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter13/7.write_highlights.py`](https://github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter13/7.write_highlights.py).
In this code, we will expand the Whisper example. We will transcribe the text, then join all the transcribed chunks together, and finally, we will pass all the transcriptions to the LLM to create the highlights for the entire video. Let’s continue the previous example:
1. We start by initializing the Hugging Face model:
```
model_name = "mistralai/Mistral-Nemo-Instruct-2407" # 使用 Mistral 进行指令跟随
```py
2. Then, we add your Hugging Face API token:
```
api_token = "" # 请替换为您的实际 API 令牌
```py
3. Here’s the LangChain setup that we’ll be using in this use case. Notice the new prompt that we added:
```
prompt_template = PromptTemplate(
input_variables=["text"],
template='''这是来自 YouTube 视频的转录文本。请以项目符号的形式写出此视频的关键亮点。
{text}
输出:
'''
)
huggingface_llm = HuggingFaceHub(repo_id=model_name, huggingfacehub_api_token=api_token, model_kwargs={"task": "text-generation"})
llm_chain = LLMChain(prompt=prompt_template, llm=huggingface_llm)
```py
4. Next, we generate the transcription:
```
def transcribe_audio(audio_chunk, rate):
input_features = whisper_processor(audio_chunk,
sampling_rate=rate,
return_tensors="pt").input_features
with torch.no_grad():
predicted_ids = \
whisper_model.generate(input_features)
transcription = whisper_processor.batch_decode(
predicted_ids, skip_special_tokens=True)[0]
return transcription
```py
5. Then, we create a function to generate the key highlights from text using the LLM:
```
def generate_highlights(text):
try:
response = llm_chain.run(text)
return response.strip() # 清理响应周围的空格
except Exception as e:
print(f"生成高亮时出错: {e}")
return "error" # 优雅地处理错误
```py
6. Next, we split the audio into chunks:
```
audio_chunks = split_audio(audio, rate, chunk_duration=30) # 30 秒一块
```py
7. We then transcribe each audio chunk:
```
transcriptions = [transcribe_audio(chunk, rate) for chunk in audio_chunks]
```py
8. Then, we join all transcriptions into a single text:
```
full_transcription = " ".join(transcriptions)
```py
9. Finally, we generate highlights from the full transcription:
```
highlights = generate_highlights(full_transcription)
```py
Let’s see the automatically created highlights:
链式协调器:用于协调 LLM(大语言模型)应用中的所有步骤,如 RAG(检索增强生成)。
推荐的流行开源包:Lama Index 和 LangChain。
模块化:链式结构允许对流程进行模块化,使得更新或更改组件(如语言模型或向量存储)变得更加容易,而无需重建整个应用。
JNNIA 的快速发展
As we can see, there are some minor mistakes, mainly coming from the Whisper process, but other than that, it is actually pretty good.
In the next part, we will quickly review the research happening in the audio space, as it is a rapidly evolving field.
Future research in audio preprocessing
There is a growing trend toward the development of multimodal LLMs capable of processing various types of data, including audio. Currently, many language models are primarily text-based, but we anticipate the emergence of models that can handle text, images, and audio simultaneously. These multimodal LLMs have diverse applications, such as generating image captions and providing medical diagnoses based on patient reports. Research is underway to extend LLMs to support direct speech inputs. As noted, “Several studies have attempted to extend LLMs to support direct speech inputs with a connection module” ([`arxiv.org/html/2406.07914v2`](https://arxiv.org/html/2406.07914v2)), indicating ongoing efforts to incorporate audio processing capabilities into LLMs. Although not only relevant to audio, LLMs face several challenges with other data types, including the following:
* High computational resources required for processing
* Data privacy and security concerns
Researchers are actively exploring various strategies to overcome these challenges. To address the high computational demands, there is a focus on developing more efficient algorithms and architectures, such as transformer models with reduced parameter sizes and optimized training techniques. Techniques such as model compression, quantization, and distillation are being employed to make these models more resource-efficient without sacrificing performance ([`arxiv.org/abs/2401.13601`](https://arxiv.org/abs/2401.13601), [`arxiv.org/html/2408.04275v1`](https://arxiv.org/html/2408.04275v1), [`arxiv.org/html/2408.01319v1`](https://arxiv.org/html/2408.01319v1)). In terms of data privacy and security, researchers are investigating privacy-preserving ML techniques, including federated learning and differential privacy. These approaches aim to protect sensitive data by allowing models to learn from decentralized data sources without exposing individual data points. Additionally, advancements in encryption and secure multi-party computation are being integrated to ensure that data remains confidential throughout the processing pipeline. These efforts are crucial for enabling the widespread adoption of multimodal LLMs across various domains while ensuring they remain efficient and secure ([`towardsdatascience.com/differential-privacy-and-federated-learning-for-medical-data-0f2437d6ece9`](https://towardsdatascience.com/differential-privacy-and-federated-learning-for-medical-data-0f2437d6ece9), [`arxiv.org/pdf/2403.05156`](https://arxiv.org/pdf/2403.05156), [`pair.withgoogle.com/explorables/federated-learning/`](https://pair.withgoogle.com/explorables/federated-learning/)).
Let’s now summarize the learnings from this chapter.
Summary
In this chapter, we covered various image processing techniques, such as loading, resizing, normalizing, and standardizing images to prepare them for ML applications. We implemented augmentation to generate diverse variations for improved model generalization and applied noise removal to enhance image quality. We also examined the use of OCR for text extraction from images, particularly addressing the challenges presented by thumbnails. Additionally, we explored the BLIP model’s capability to generate captions based on visual content. Furthermore, we discussed video processing techniques involving frame extraction and key moment analysis.
Finally, we introduced the Whisper model, highlighting its effectiveness in converting audio to text and its automatic speech recognition capabilities across multiple languages.
This concludes the book! You did it!
I want to express my sincere gratitude for your dedication to finishing this book. I’ve aimed to share the insights from my experience, with a focus on ML and AI, as these fields have been central to my career. I find them incredibly fascinating and transformative, though I might be a bit biased.
As you’ve seen in the later chapters, I believe LLMs are poised to revolutionize the field and the way we process data. That’s why I dedicated the last chapters to building a foundation and showcasing how effortlessly different types of data can be transformed and manipulated using LLMs.
Please take my advice and spend some time diving into the code examples provided. Implement these techniques in your daily tasks and projects. If there’s anything you find yourself doing manually or redoing frequently, *code it up* to streamline your process. This hands-on practice will help reinforce your learning. Experiment with the techniques and concepts from this book on your own projects, as real growth occurs when you adapt and innovate with these tools in practical scenarios.
I’m traveling the world speaking at conferences and running workshops. If you see me at one of these events, don’t hesitate to say hello and talk to me! Who knows, our paths might cross at one of these events! In any case, I’d also love to hear about your progress and see what you’ve learned and built. Feel free to share your experiences with me—your insights and developments are always exciting to see. So, let’s stay connected! Connect with me on LinkedIn ([`www.linkedin.com/in/maria-zervou-533222107/`](https://www.linkedin.com/in/maria-zervou-533222107/)) and you can subscribe to my YouTube channel ([`www.youtube.com/channel/UCY2Z8Sc2L0wQnTOQPlLzUQw`](https://www.youtube.com/channel/UCY2Z8Sc2L0wQnTOQPlLzUQw)) for ongoing tutorials and content to keep you informed about new developments and techniques in ML and AI.
Thank you once again for your time and effort. Remember, learning is just the beginning; real growth comes from practicing and applying your knowledge. I’m excited to see where your journey leads you and hope our paths cross again in the future.


浙公网安备 33010602011771号