Python-数据整理-全-

Python 数据整理(全)

原文:zh.annas-archive.org/md5/23dc001d0992d683d7b7f8a65b290a27

译者:飞龙

协议:CC BY-NC-SA 4.0

第一章:前言

关于

本节简要介绍了作者、本书涵盖的内容、开始学习所需的技术技能,以及完成所有包含的活动和练习所需的硬件和软件要求。

关于本书

为了使数据有用和有意义,它必须经过整理和精炼。“使用 Python 进行数据整理”教你这些过程背后的所有核心思想,并为你提供该领域最受欢迎的工具和技术知识。

本书从 Python 的绝对基础开始,主要关注数据结构,然后迅速跳入 NumPy 和 pandas 库,作为数据整理的基本工具。我们强调为什么你应该远离其他语言中传统的数据清洗方式,并利用 Python 中专门预构建的例程。之后,你将学习如何使用相同的 Python 后端,从互联网、大型数据库仓库或 Excel 财务表格等多样化的来源中提取和转换数据。然后,你还将学习如何处理缺失或不正确的数据,并根据下游分析工具的要求重新格式化。你将通过实际案例和数据集来了解这些概念。

在阅读完本书之后,你将足够自信地处理各种来源的数据,高效地进行数据的提取、清洗、转换和格式化。

关于作者

Tirthajyoti Sarkar 博士在半导体技术领域担任高级首席工程师,在那里他将尖端的数据科学/机器学习技术应用于自动化设计和预测分析。他经常撰写关于 Python 编程和数据科学主题的文章。他拥有伊利诺伊大学的博士学位,以及来自斯坦福和麻省理工学院的人工智能和机器学习认证。

Shubhadeep Roychowdhury在一家位于巴黎的网络安全初创公司担任高级软件工程师,在那里他应用最先进的计算机视觉和数据工程算法和工具来开发尖端产品。他经常撰写关于 Python 中的算法实现和类似主题的文章。他拥有西孟加拉技术大学的计算机科学硕士学位,以及来自斯坦福的机器学习认证。

学习目标

  • 使用和操作复杂和简单的数据结构

  • 充分利用 DataFrames 和 numpy.array 在运行时的全部潜力

  • 使用 BeautifulSoup4 和 html5lib 进行网络爬取

  • 使用 RegEX 执行高级字符串搜索和操作

  • 使用 Pandas 处理异常值并进行数据插补

  • 使用描述性统计和绘图技术

  • 使用数据生成技术练习数据整理和建模

方法

使用 Python 进行数据处理采用实用方法,旨在在最短的时间内为初学者提供最基本的数据分析工具。它包含多个活动,使用真实业务场景让你练习并应用你的新技能,在高度相关的环境中。

读者对象

使用 Python 进行数据处理旨在为开发者、数据分析师和商业分析师提供,他们热衷于追求成为全职数据科学家或分析专家的职业。尽管这本书是为初学者准备的,但了解 Python 的相关知识对于轻松掌握这里涵盖的概念是必要的。对关系数据库和 SQL 的基本了解也将有所帮助。

最小硬件要求

为了获得最佳的学生体验,我们推荐以下硬件配置:

  • 处理器:Intel Core i5 或同等性能

  • 内存:8 GB RAM

  • 存储:35 GB 可用空间

软件要求

你还需要预先安装以下软件:

  • 操作系统:Windows 7 SP1 64 位、Windows 8.1 64 位或 Windows 10 64 位、Ubuntu Linux 或最新版本的 macOS

  • OS X 版本

  • 处理器:Intel Core i5 或同等性能

  • 内存:4 GB RAM(推荐 8 GB)

  • 存储:35 GB 可用空间

习惯用法

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:“这将返回与之关联的值- ["list_element1", 34]

代码块设置如下:

list_1 = []
    for x in range(0, 10):
    list_1.append(x)
list_1

新术语和重要单词以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:“点击新建并选择Python 3。”

安装和设置

每一段伟大的旅程都始于一个谦卑的步伐。我们即将在数据处理领域的冒险也不例外。在我们能够用数据做些令人惊叹的事情之前,我们需要准备好最富有成效的环境。在本节中,我们将了解如何做到这一点。

本书对环境的要求只有一个,那就是安装 Docker。如果你从未听说过 Docker,或者你对它的了解非常有限,那么请不要担心。为了本书的目的,你需要了解的关于 Docker 的信息如下:Docker 是一个轻量级的容器化引擎,可以在所有三个主要平台(Linux、Windows 和 macOS)上运行。Docker 背后的主要思想是在你的原生操作系统之上提供安全、简单和轻量级的虚拟化。

安装 Docker

  1. 要在 Mac 或 Windows 机器上安装 Docker,请创建一个 Docker 账户并下载最新版本。安装和设置都很简单。

  2. 一旦你设置了 Docker,打开一个 shell(如果你是 Mac 用户,则为 Terminal)并输入以下命令以验证安装是否成功:

    docker version
    

    如果输出显示了 Docker 的服务器和客户端版本,那么你已经设置好了。

拉取镜像

  1. 拉取镜像后,你将拥有所有必要的软件包(包括 Python 3.6.6)安装并准备好供你开始工作。在 shell 中输入以下命令:

    docker pull rcshubhadeep/packt-data-wrangling-base
    
  2. 如果你想查看包含在这个镜像中的所有软件包及其版本的完整列表,你可以检查这本书源代码仓库中的setup文件夹下的requirements.txt文件。一旦镜像准备好了,你就可以开始使用了。下载可能需要时间,这取决于你的连接速度。

运行环境

  1. 使用以下命令运行镜像:

    docker run -p 8888:8888 -v 'pwd':/notebooks -it rcshubhadeep/packt-data-wrangling-base
    

    这将为你提供一个即用环境。

  2. 在 Chrome 或 Firefox 中打开一个浏览器标签页,并转到http://localhost:8888。你将被提示输入一个令牌。令牌是dw_4_all

  3. 在运行镜像之前,创建一个新的文件夹,并使用cd命令从 shell 导航到该文件夹。

    一旦你创建了一个笔记本并将其保存为ipynb文件,你可以使用Ctrl +C来停止运行镜像。

Jupyter 笔记本简介

Project Jupyter 是一个开源的免费软件,它允许你从特殊的笔记本中交互式地运行 Python 和其他语言的代码,类似于浏览器界面。它于 2014 年从IPython项目诞生,并已成为整个数据科学工作者的默认选择。

  1. 一旦你运行了 Jupyter 服务器,点击新建并选择Python 3。一个新标签页将打开,显示一个新且空的笔记本。重命名 Jupyter 文件:

    图 0.1:Jupyter 服务器界面

    Jupyter 笔记本的主要构建块是单元格。有两种类型的单元格:In(代表输入)和Out(代表输出)。你可以在In单元格中编写代码、普通文本和 Markdown,按Shift + Enter(或Shift + Return),该特定In单元格中的代码将被执行。结果将显示在Out单元格中,你将进入一个新的In单元格,准备编写下一块代码。一旦你习惯了这种界面,你将逐渐发现它提供的强大功能和灵活性。

  2. 关于 Jupyter 单元格的最后一件事是,当你开始一个新的单元格时,默认情况下,它假定你将在其中编写代码。然而,如果你想写文本,你必须更改类型。你可以使用以下键序列来完成:Escape->m->Enter:

    图 0.2:Jupyter 笔记本
  3. 当你完成文本编写后,使用Shift + Enter来执行它。与代码单元格不同,编译后的 Markdown 的结果将显示在同一个位置,即“In”单元格中。

    注意

    要获得 Jupyter 中所有便捷快捷键的“速查表”,你可以将此 Gist 添加到书签:gist.github.com/kidpixo/f4318f8c8143adee5b40。有了这个基本的介绍和准备好的图像,我们就可以开始期待中的激动人心且富有启发的旅程了!

安装代码包

将课程代码包复制到C:/Code文件夹。

其他资源

本书代码包也托管在 GitHub 上:github.com/TrainingByPackt/Data-Wrangling-with-Python

我们还有来自我们丰富图书和视频目录的其他代码包,可在github.com/PacktPublishing/找到。去看看吧!

第二章:第一章

Python 数据整理简介

学习目标

到本章结束时,你将能够做到以下几件事情:

  • 定义数据整理在数据科学中的重要性

  • 操作 Python 中可用的数据结构

  • 比较内置 Python 数据结构的不同实现

本章描述了数据整理的重要性,确定了数据整理中要执行的重要任务,并介绍了基本的 Python 数据结构。

简介

数据科学和分析正在接管整个世界,数据科学家的职位通常被称为 21 世纪最酷的工作。但尽管对数据的强调,是科学使你——实践者——真正有价值。

要用高质量的科学数据进行实践,你需要确保数据来源正确、清洗、格式化和预处理。这本书教你这个数据科学流程中无价组件的最基本基础知识:数据整理。简而言之,数据整理是确保数据以干净、准确、格式化且准备好用于数据分析的格式的流程。

数据整理的一个显著例子是在加州大学圣地亚哥分校(UCSD)超级计算中心进行的。在加利福尼亚,野火非常常见,主要是因为干旱的天气和极端的高温,尤其是在夏季。UCSD 超级计算中心的数据科学家收集数据来预测火灾的性质和蔓延方向。来自气象站、森林中的传感器、消防站、卫星图像和 Twitter 流的数据可能仍然是不完整或缺失的。这些数据需要被清洗和格式化,以便用于预测未来野火的发生。

这是一个数据整理和数据科学如何证明是有帮助和相关的例子。

数据整理的重要性

石油不是从钻井平台直接以最终形式出现的;它必须经过提炼。同样,数据必须经过整理、按摩和提炼,才能用于智能算法和消费产品。这被称为整理。大多数数据科学家将大部分时间花在数据整理上。

数据整理通常是在数据科学/分析流程的第一个阶段进行的。在数据科学家确定了用于解决业务问题的有用数据源(例如,内部数据库存储或互联网或流式传感器数据)之后,他们接着从这些来源提取、清洗和格式化必要的数据。

通常,数据整理的任务包括以下步骤:

  • 从多个来源(包括网页和数据库表)抓取原始数据

  • 补充、格式化和转换——基本上是使其准备好用于建模过程(如高级机器学习)

  • 处理读写错误

  • 检测异常值

  • 执行快速的可视化(绘图)和基本统计分析,以判断您整理后的数据质量

这是对数据整理在典型数据科学流程中的定位和基本功能角色的说明性表示:

图 1.1:数据整理过程

图 1.1:数据整理过程

数据整理的过程首先是要找到分析所需的数据。这些数据可能来自一个或多个来源,例如推文、关系型数据库中的银行交易报表、传感器数据等等。这些数据需要被清理。如果有缺失数据,我们将使用几种技术中的任何一种来删除或替换它。如果有异常值,我们首先需要检测它们,然后适当地处理它们。如果数据来自多个来源,我们将不得不执行连接操作来合并它们。

在极其罕见的情况下,可能不需要进行数据整理。例如,如果机器学习任务所需的数据已经以可接受格式存储在内部数据库中,那么一个简单的 SQL 查询可能就足够将数据提取到表格中,以便传递到建模阶段。

用于数据整理的 Python

总是有人争论是否应该使用企业工具还是使用编程语言及其相关框架来进行数据整理过程。有许多商业级的企业工具用于数据格式化和预处理,用户不需要编写太多代码。以下是一些例子:

  • 通用数据分析平台,如 Microsoft Excel(带插件)

  • 统计发现包,如 JMP(来自 SAS)

  • 建模平台,如 RapidMiner

  • 专注于数据整理的利基玩家提供的分析平台,如 TrifactaPaxataAlteryx

然而,与这些现成的工具相比,像 Python 这样的编程语言提供了更多的灵活性、控制和功能。

随着数据量、速度和多样性(大数据的三个 V)的快速变化,始终发展并培养大量内部数据整理专业知识,使用基本的编程框架,这样组织就不会对任何企业平台的基本任务——数据整理——产生依赖,这是一个很好的主意:

图 1.2:过去 5 年全球 Google 趋势

图 1.2:过去五年全球 Google 趋势

使用开源、免费编程范式(如 Python)进行数据整理的一些明显优势如下:

  • 通用开源范式不对您为特定问题开发的方法施加任何限制

  • 快速、优化、开源库的卓越生态系统,专注于数据分析

  • 越来越多的支持将 Python 连接到所有可能的数据源类型

  • 易于使用的基本统计测试和快速可视化库来检查数据质量

  • 数据处理输出与高级机器学习模型的无缝接口

Python 是目前机器学习和人工智能最受欢迎的选择语言。

列表、集合、字符串、元组和字典

现在我们已经学习了 Python 的重要性,我们将从探索 Python 中的各种基本数据结构开始。我们将学习处理数据的技术。这对数据从业者来说是无价的。

我们可以通过在命令提示符窗口中输入以下命令来启动一个新的 Jupyter 服务器:

docker run -p 8888:8888 -v 'pwd':/notebooks -it rcshubhadeep/packt-data-wrangling-base:latest ipython

这将启动一个 jupyter 服务器,您可以在[http://localhost:8888](http://localhost:8888)访问它,并使用密码dw_4_all`访问主界面。

列表

列表是具有连续内存位置的 Python 基本数据结构,可以容纳不同的数据类型,并且可以通过索引进行访问。

我们将从列表和列表推导式开始。我们将生成一个数字列表,然后检查其中哪些是偶数。我们将进行排序、反转和检查重复项。我们还将看到我们可以以多少种不同的方式访问列表元素,遍历它们并检查元素的成员资格。

以下是一个简单列表的例子:

list_example = [51, 27, 34, 46, 90, 45, -19]

以下也是一个列表的例子:

list_example2 = [15, "Yellow car", True, 9.456, [12, "Hello"]]

如您所见,列表可以包含任何数量的允许的数据类型,如intfloatstringBoolean,列表也可以是不同数据类型的混合(包括嵌套列表)。

如果你来自强类型语言,如 C、C++或 Java,那么这可能会让你感到奇怪,因为在那些语言中不允许在单个数组中混合不同的数据类型。列表在某种程度上类似于数组,因为它们都是基于连续内存位置的,并且可以使用索引进行访问。但 Python 列表的力量在于它们可以容纳不同的数据类型,并且允许你操作数据。

注意

虽然如此,但列表的强大功能和你可以混合不同数据类型的事实,实际上可能会创建微妙的错误,这些错误可能非常难以追踪。

练习 1:访问列表成员

在以下练习中,我们将创建一个列表,然后观察访问元素的不同方式:

  1. 使用以下命令定义一个名为list_1的列表,包含四个整数成员:

    list_1 = [34, 12, 89, 1]
    

    索引将自动分配,如下所示:

    图 1.3:显示正向和反向索引的列表

    图 1.3:显示正向和反向索引的列表
  2. 使用正向索引从list_1中访问第一个元素:

    list_1[0] #34
    
  3. 使用正向索引从list_1中访问最后一个元素:

    list_1[3] #1
    
  4. 使用len函数从list_1中访问最后一个元素:

    list_1[len(list_1) - 1] #1
    

    Python 中的len函数返回指定列表的长度。

  5. 使用其反向索引从list_1中访问最后一个元素:

    list_1[-1] #1
    
  6. 使用正向索引从list_1中访问前三个元素:

    list_1[1:3] # [12, 89]
    

    这也被称为列表切片,因为它通过仅提取列表的一部分来从原始列表中返回一个较小的列表。要切片一个列表,我们需要两个整数。第一个整数将表示切片的开始,第二个整数将表示结束-1 的元素。

    注意

    注意切片没有包括第三个索引或结束元素。这就是列表切片的工作方式。

  7. 通过切片从list_1中访问最后两个元素:

    list_1[-2:] # [89, 1]
    
  8. 使用反向索引访问前两个元素:

    list_1[:-2] # [34, 12]
    

    当我们在冒号(:)的一侧留空时,我们基本上是在告诉 Python 要么走到列表的末尾,要么从列表的开始处开始。它将自动应用我们刚刚学到的列表切片规则。

  9. 反转字符串中的元素:

    list_1[-1::-1] # [1, 89, 12, 34]
    

    注意

    最后一行代码的可读性不是很好,这意味着仅通过查看它并不能明显知道它在做什么。这与 Python 的哲学相悖。因此,尽管这种代码可能看起来很聪明,但我们应该抵制写这种代码的诱惑。

练习 2:生成列表

我们将检查生成列表的各种方法:

  1. 使用append方法创建一个列表:

    list_1 = []
    for x in range(0, 10):
        list_1.append(x)
    list_1
    

    输出将如下所示:

    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    

    在这里,我们首先声明了一个空列表,然后使用一个for循环向其中添加值。append方法是由 Python 列表数据类型提供给我们的一种方法。

  2. 使用以下命令生成一个列表:

    list_2 = [x for x in range(0, 100)]
    list_2
    

    部分输出如下:

    图 1.4:列表推导式

    图 1.4:列表推导式

    这是列表推导式,这是一种非常强大的工具,我们需要掌握。列表推导式的强大之处在于我们可以在推导式内部使用条件语句。

  3. 使用while循环遍历一个列表以理解while循环和for循环之间的区别:

    i = 0
    while i < len(list_1) :
        print(list_1[i]) 
        i += 1
    

    部分输出将如下所示:

    图 1.5:使用循环显示内容的输出

    图 1.5:使用while循环显示list_1内容的输出
  4. 创建包含能被5整除的数字的list_3

    list_3 = [x for x in range(0, 100) if x % 5 == 0]
    list_3
    

    输出将是一个包含从 1 到 100,增量为 5 的数字的列表:

    [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]
    
  5. 通过添加两个列表来生成一个列表:

    list_1 = [1, 4, 56, -1]
    list_2 = [1, 39, 245, -23, 0, 45]
    list_3 = list_1 + list_2
    list_3
    

    输出如下所示:

    [1, 4, 56, -1, 1, 39, 245, -23, 0, 45]
    
  6. 使用extend关键字扩展字符串:

    list_1.extend(list_2)
    list_1
    

    部分输出如下所示:

图 1.6:list_1的内容

第二个操作会改变原始列表(list_1),并将list_2的所有元素追加到它上面。所以使用时要小心。

练习 3:遍历列表并检查成员资格

我们将遍历一个列表并测试某个值是否存在于其中:

  1. 遍历一个列表:

    list_1 = [x for x in range(0, 100)]
    for i in range(0, len(list_1)):
        print(list_1[i])
    

    输出如下所示:

    图 1.7:的部分

    图 1.7:list_1的部分
  2. 然而,这并不太符合 Python 风格。符合 Python 风格意味着遵循和遵守多年来由数千名非常能干的开发者创建的一系列最佳实践和约定,在这种情况下意味着使用 in 关键字,因为 Python 没有索引初始化、边界检查或索引递增,这与传统语言不同。在 Python 中遍历列表的方式如下:

    for i in list_1:
        print(i)
    

    输出如下:

    图 1.8:list_1 的部分

    图 1.8:list_1 的一个部分

    注意,在第二种方法中,我们不再需要计数器来访问列表索引;相反,Python 的 in 操作符直接给出第 i 个位置的元素。

  3. 使用 in 操作符检查整数 25 和 -45 是否在列表中:

     25 in list_1
    

    输出为 True

     -45 in list_1
    

    输出为 False

练习 4:排序列表

在上一个练习中,我们生成了一个名为 list_1 的列表。我们现在将对其进行排序:

  1. 由于原始列表是一个从 099 的数字列表,我们将按相反方向对其进行排序。为此,我们将使用带有 reverse=Truesort 方法:

    list_1.sort(reverse=True)
    list_1
    

    部分输出如下:

    图 1.9.jpg

    图 1.9:显示反转列表的输出部分
  2. 我们可以直接使用 reverse 方法来达到这个结果:

    list_1.reverse()
    list_1
    

    输出如下:

图 1.10:反转字符串后的输出部分

图 1.10:反转字符串后的输出部分

注意

sort 函数和 reverse 函数之间的区别在于,我们可以使用 sort 与自定义排序函数一起进行自定义排序,而 reverse 只能用来反转列表。在这里,这两个函数都是在原地工作的,所以使用时请注意这一点。

练习 5:生成随机列表

在这个练习中,我们将生成一个包含随机数字的 list

  1. 导入 random 库:

    import random
    
  2. 使用 randint 函数生成随机整数并将它们添加到列表中:

    list_1 = [random.randint(0, 30) for x in range (0, 100)]
    
  3. 使用 print(list_1) 打印列表。注意,list_1 中将有重复的值:

    list_1
    

    样本输出如下:

图 1.11.jpg

图 1.11:样本输出中 list_1 的部分

获取唯一数字列表的方法有很多,虽然你可能能够通过使用循环和另一个列表写几行代码来实现(你应该实际尝试一下!),但让我们看看如何在不使用循环和单行代码的情况下完成这个任务。这将带我们到下一个数据结构,集合。

活动 1:处理列表

在这个活动中,我们将生成一个随机数字的 list,然后从这个 list 中生成另一个 list,它只包含能被三整除的数字。重复实验三次。然后,我们将计算两个列表长度平均差异。

完成此活动的步骤如下:

  1. 创建一个包含 100 个随机数字的 list

  2. 从这个随机 list 中创建一个新的 list,包含能被 3 整除的数字。

  3. 计算这两个列表的长度,并将差值存储在一个新变量中。

  4. 使用循环,执行步骤 2 和 3,并找到差值变量三次。

  5. 计算这三个差值的算术平均值。

    注意

    本活动的解决方案可以在第 282 页找到。

集合

从数学的角度讲,集合仅仅是一组定义明确的、不同的对象集合。Python 通过其 set 数据类型为我们提供了一种直接处理它们的方法。

集合简介

使用我们生成的最后一个列表,我们将重新审视从其中去除重复元素的问题。我们可以通过以下代码行来实现:

list_12 = list(set(list_1))

如果我们打印出来,我们会看到它只包含唯一的数字。我们使用了 set 数据类型将第一个列表转换成集合,从而去除所有重复的元素,然后我们再次使用 list 函数将其从集合转换回列表:

list_12

输出将如下:

图 1.12:list_21 输出的部分

图 1.12:list_21 输出的部分

集合的并集和交集

这就是两个集合之间的并集看起来:

图 1.13:展示两个集合并集的维恩图

图 1.13:展示两个集合并集的维恩图

这仅仅意味着从两个集合中取出所有元素,但只取共有元素一次。

我们可以使用以下代码创建它:

set1 = {"Apple", "Orange", "Banana"}
set2 = {"Pear", "Peach", "Mango", "Banana"}

要找到两个集合的并集,应使用以下指令:

set1 | set2

输出如下:

{'Apple', 'Banana', 'Mango', 'Orange', 'Peach', 'Pear'}

注意,结果集中共有的元素,香蕉,只出现一次。两个集合之间的共有元素可以通过获取两个集合的交集来识别,如下所示:

图 1.14:展示两个集合交集的维恩图

图 1.14:展示两个集合交集的维恩图

在 Python 中,我们通过以下方式获取两个集合的交集:

set1 & set2

这将给我们一个只包含一个元素的集合。输出如下:

{'Banana'}

注意

你也可以计算集合之间的差(也称为补集)。要了解更多信息,请参阅此链接:docs.python.org/3/tutorial/datastructures.html#sets

创建空集

你可以通过创建一个不包含任何元素的集合来创建一个空集。你可以通过以下代码来完成:

null_set_1 = set({})
null_set_1

输出如下:

set()

然而,要创建字典,请使用以下命令:

null_set_2 = {}
null_set_2

输出如下:

{}

我们将在下一个主题中详细介绍这一点。

字典

字典就像一个列表,这意味着它是一个包含几个元素的集合。然而,在字典中,它是一个包含键值对的集合,其中键可以是任何可以散列的东西。通常,我们使用数字或字符串作为键。

要创建字典,请使用以下代码:

dict_1 = {"key1": "value1", "key2": "value2"}
dict_1

输出如下:

{'key1': 'value1', 'key2': 'value2'}

这也是一个有效的字典:

dict_2 = {"key1": 1, "key2": ["list_element1", 34], "key3": "value3",
          "key4": {"subkey1": "v1"}, "key5": 4.5}
dict_2

输出如下:

{'key1': 1,
 'key2': ['list_element1', 34],
 'key3': 'value3',
 'key4': {'subkey1': 'v1'},
 'key5': 4.5}

字典中的键必须是唯一的。

练习 6:在字典中访问和设置值

在这个练习中,我们将访问和设置字典中的值:

  1. 访问字典中的特定键:

    dict_2["key2"]
    

    这将返回与之关联的值,如下所示:

    ['list_element1', 34]
    
  2. 为键分配新值:

    dict_2["key2"] = "My new value"
    
  3. 定义一个空字典,然后使用键记法向它赋值:

    dict_3 = {}  # Not a null set. It is a dict
    dict_3["key1"] = "Value1"
    dict_3
    

    输出如下:

    {'key1': 'Value1'}
    

练习 7:遍历字典

在这个练习中,我们将遍历一个字典:

  1. 创建 dict_1

    dict_1 = {"key1": 1, "key2": ["list_element1", 34], "key3": "value3", "key4": {"subkey1": "v1"}, "key5": 4.5}
    
  2. 使用循环变量 kv

    for k, v in dict_1.items():
        print("{} - {}".format(k, v))
    

    输出如下:

    key1 - 1
    key2 - ['list_element1', 34]
    key3 - value3
    key4 - {'subkey1': 'v1'}
    key5 - 4.5
    

    注意

    注意我们在列表上迭代的方式和这里的方式之间的区别。

练习 8:重新审视唯一值列表问题

我们将利用字典键不能重复的事实来生成具有唯一值的列表:

  1. 首先,生成一个具有重复值的随机列表:

    list_1 = [random.randint(0, 30) for x in range (0, 100)]
    
  2. list_1 创建一个具有唯一值的列表:

    list(dict.fromkeys(list_1).keys())
    

    样本输出如下:

图片

图 1.15:显示唯一值列表的输出

在这里,我们使用了 Python 中 dict 数据类型的两个有用函数,fromkeyskeysfromkeys 创建一个字典,其中键来自 iterable(在这种情况下,是一个列表),值默认为 None,而 keys 给我们字典的键。

练习 9:从字典中删除值

在这个练习中,我们将从一个 dict 中删除一个值:

  1. 创建包含五个元素的 list_1

    dict_1 = {"key1": 1, "key2": ["list_element1", 34], "key3": "value3",
              "key4": {"subkey1": "v1"}, "key5": 4.5}
    dict_1
    

    输出如下:

    {'key1': 1,
     'key2': ['list_element', 34],
     'key3': 'value3',
     'key4': {'subkey1': 'v1'},
     'key5': 4.5}
    
  2. 我们将使用 del 函数并指定元素:

    del dict_1["key2"]
    

    输出如下:

    {'key3': 'value3', 'key4': {'subkey1': 'v1'}, 'key5': 4.5}
    

    注意

    del 操作符也可以用来从列表中删除特定索引。

练习 10:字典理解

在这个关于 dict 的最后练习中,我们将回顾比列表理解更少使用的理解方式:字典理解。我们还将检查创建 dict 的两种其他方法,这些方法在将来会很有用。

字典理解与列表理解的工作方式完全相同,但我们需要指定键和值:

  1. 生成一个字典,其中 09 作为键,键的平方作为值:

    list_1 = [x for x in range(0, 10)]
    dict_1 = {x : x**2 for x in list_1}
    dict_1
    

    输出如下:

    {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}
    

    你能否使用 dict 理解生成一个 dict,其中键从 09,值是键的平方根?这次,我们不会使用列表。

  2. 使用 dict 函数生成 dictionary

    dict_2 = dict([('Tom', 100), ('Dick', 200), ('Harry', 300)])
    dict_2
    

    输出如下:

    {'Tom': 100, 'Dick': 200, 'Harry': 300}
    

    你也可以使用 dict 函数生成 dictionary,如下所示:

    dict_3 = dict(Tom=100, Dick=200, Harry=300)
    dict_3
    

    输出如下:

    {'Tom': 100, 'Dick': 200, 'Harry': 300}
    

    它非常灵活。因此,前面的两个命令都将生成有效的字典。

    我们刚才注意到的看起来奇怪的值对 ('Harry', 300) 被称为 tuple。这是 Python 中的另一个重要基本数据类型。我们将在下一个主题中学习元组。

元组

元组是 Python 中的另一种数据类型。它具有顺序性,类似于列表。

元组由逗号分隔的值组成,如下所示:

tuple_1 = 24, 42, 2.3456, "Hello"

注意,与列表不同,这里我们没有打开和关闭方括号。

创建具有不同基数(cardinalities)的元组

这是我们创建空元组的方式:

tuple_1 = ()

而且这就是我们创建只有一个值的元组的方式:

tuple_1 = "Hello",

注意这里的尾随逗号。

我们可以嵌套元组,类似于列表和字典,如下所示:

tuple_1 = "hello", "there"
tuple_12 = tuple_1, 45, "Sam"

元组的一个特殊之处在于它们是一个不可变的数据类型。因此,一旦创建,我们无法更改它们的值。我们只能访问它们,如下所示:

tuple_1 = "Hello", "World!"
tuple_1[1] = "Universe!"

最后一行代码将导致TypeError,因为元组不允许修改。

这使得元组的使用案例在某种程度上与列表不同,尽管它们在几个方面看起来和行为非常相似。

解包元组

解包元组的术语简单地说就是获取元组中包含的值,并将它们放入不同的变量中:

tuple_1 = "Hello", "World"
hello, world = tuple_1
print(hello)
print(world)

输出如下:

Hello
World

当然,一旦我们这样做,我们就可以修改这些变量中包含的值。

练习 11:处理元组

  1. 创建一个元组来演示元组是不可变的。解包它以读取所有元素,如下所示:

    tupleE = "1", "3", "5"
    tupleE
    

    输出如下:

    ('1', '3', '5')
    
  2. 尝试从tupleE元组中覆盖一个变量:

    tupleE[1] = "5"
    

    这一步将导致TypeError,因为元组不允许修改。

  3. 尝试将一个序列赋值给tupleE元组:

    1, 3, 5 = tupleE
    
  4. 打印输出:

    print(1)
    print(3)
    

    输出如下:

    1
    3
    

到目前为止,我们主要看到了两种不同的数据类型。一种由数字表示;另一种由文本数据表示。虽然数字有其自己的技巧,我们将在后面看到,现在是时候更详细地研究文本数据了。

字符串

在本节的最后部分,我们将学习字符串。Python 中的字符串与其他编程语言中的字符串类似。

这是一个字符串:

string1 = 'Hello World!' 

一个字符串也可以用这种方式声明:

string2 = "Hello World 2!"

您可以使用单引号和双引号来定义字符串。

练习 12:访问字符串

Python 中的字符串行为类似于列表,除了一个大的注意事项。字符串是不可变的,而列表是可变的数据结构:

  1. 创建一个名为str_1的字符串:

    str_1 = "Hello World!"
    

    通过指定元素的位置来访问字符串的元素,就像我们在列表中做的那样。

  2. 访问字符串的第一个成员:

    str_1[0]
    

    输出如下:

    'H'
    
  3. 访问字符串的第四个成员:

    str_1[4]
    

    输出如下:

    'o'
    
  4. 访问字符串的最后一个成员:

    str_1[len(str_1) - 1]
    

    输出如下:

    '!'
    
  5. 访问字符串的最后一个成员:

    str_1[-1]
    

    输出如下:

    '!'
    

    上述每个操作都会给你特定索引处的字符。

    注意

    访问字符串元素的方法类似于访问列表。

练习 13:字符串切片

就像列表一样,我们可以切片字符串:

  1. 创建一个字符串,str_1

    str_1 = "Hello World! I am learning data wrangling"
    
  2. 指定切片值并切片字符串:

    str_1[2:10]
    

    输出如下:

    'llo Worl'
    
  3. 通过跳过切片值来切片字符串:

    str_1[-31:]
    

    输出如下:

    'd! I am learning data wrangling'
    
  4. 使用负数来切片字符串:

    str_1[-10:-5]
    

    输出如下:

    ' wran'
    

字符串函数

要找出字符串的长度,我们只需使用len函数:

str_1 = "Hello World! I am learning data wrangling"
len(str_1)

字符串的长度是 41。要转换字符串的大小写,我们可以使用lowerupper方法:

str_1 = "A COMPLETE UPPER CASE STRING"
str_1.lower()
str_1.upper()

输出如下:

'A COMPLETE UPPER CASE STRING'

要在字符串中搜索字符串,我们可以使用find方法:

str_1 = "A complicated string looks like this"
str_1.find("complicated")
str_1.find("hello")# This will return -1

输出是-1。你能弄清楚find方法是否区分大小写吗?你认为find方法在找到字符串时返回什么?

要用一个字符串替换另一个字符串,我们可以使用replace方法。由于我们知道字符串是一个不可变的数据结构,replace实际上返回一个新的字符串,而不是替换并返回实际的字符串:

str_1 = "A complicated string looks like this"
str_1.replace("complicated", "simple")

输出如下:

'A simple string looks like this'

你应该在 Python 3 的标准文档中查找字符串方法,以了解更多关于这些方法的信息。

练习 14:分割和连接

这两个字符串方法需要分别介绍,因为它们允许你将字符串转换为列表,反之亦然:

  1. 创建一个字符串,并使用split方法将其转换为列表:

    str_1 = "Name, Age, Sex, Address"
    list_1 = str_1.split(",")
    list_1
    

    上述代码将给出一个类似以下列表:

    ['Name', ' Age', ' Sex', ' Address']
    
  2. 使用join方法将这个列表组合成另一个字符串:

    " | ".join(list_1) 
    

    这段代码将给你一个类似这样的字符串:

    'Name |  Age |  Sex |  Address'
    

通过这些,我们结束了本章的第二个主题。我们现在有了学习数据整理的动力,并使用 Python 对数据结构的基本原理有了坚实的介绍。这个主题还有更多内容,将在未来的章节中介绍。

我们为你设计了一个活动,让你可以练习你刚刚学到的所有技能。这个小活动应该需要大约 30 到 45 分钟来完成。

活动二:分析多行字符串并生成唯一单词计数

本节将确保你已经理解了各种基本数据结构和它们的操作。我们将通过一个专门为此目的设计的活动来实现这一点。

在这个活动中,我们将做以下事情:

  • 获取多行文本并将其保存到 Python 变量中

  • 使用字符串方法去除其中的所有换行符

  • 从字符串中获取所有唯一的单词及其出现次数。

  • 重复步骤以找到所有唯一的单词和出现次数,不考虑大小写。

    注意

    为了简化这个活动,原始文本(可以在www.gutenberg.org/files/1342/1342-h/1342-h.htm找到)已经进行了一些预处理。

这些步骤将指导你完成这个活动的解决过程:

  1. 通过复制《傲慢与偏见》的第一章文本来创建一个mutliline_text变量。

    注意

    简·奥斯汀的《傲慢与偏见》的第一章已经在 GitHub 仓库github.com/TrainingByPackt/Data-Wrangling-with-Python/blob/master/Chapter01/Activity02/上提供。

  2. 使用typelen命令获取multiline_text字符串的类型和长度。

  3. 使用replace函数移除所有换行符和符号。

  4. 使用split函数在multiline_text中找到所有的单词。

  5. 从这个列表中创建一个只包含唯一单词的列表。

  6. 使用dict中的keyvalue计算列表中唯一单词出现的次数。

  7. 使用slice函数从你找到的唯一单词中找出前 25 个单词。

    你已经逐步创建了一个使用本章所学到的所有巧妙技巧的唯一单词计数器。

    注意

    这个活动的解决方案可以在第 285 页找到。

摘要

在本章中,我们学习了数据清洗(data wrangling)这个术语的含义。我们还从各种实际的数据科学场景中获得了例子,在这些场景中,数据清洗非常有用,并且在工业界中得到应用。我们继续学习 Python 提供的不同内置数据结构。我们通过探索列表(lists)、集合(sets)、字典(dictionaries)、元组(tuples)和字符串(strings)来亲自动手实践。它们是 Python 数据结构的基本构建块,我们在使用 Python 处理和操作数据时需要它们。我们进行了几个小型的动手练习,以了解更多关于它们的信息。我们以一个精心设计的活动结束本章,这个活动让我们将所有不同数据结构中的许多不同技巧结合到实际场景中,并让我们观察它们之间的相互作用。

在下一章中,我们将学习 Python 中的数据结构,并利用它们来解决现实世界的问题。

第三章:第二章

高级数据结构和文件处理

学习目标

到本章结束时,你将能够:

  • 比较 Python 的高级数据结构

  • 利用数据结构解决现实世界问题

  • 利用操作系统文件处理操作

本章强调 Python 中的数据结构和本书基础的操作系统的函数。

引言

在上一章中,我们介绍了不同基本数据结构的基本概念。我们学习了列表、集合、字典、元组和字符串。它们是未来章节的基石,对于数据科学至关重要。

然而,到目前为止我们所涵盖的只是它们的基本操作。一旦你学会了如何有效地利用它们,它们将提供更多。在本章中,我们将进一步探索数据结构的世界。我们将学习高级操作和操作,并使用这些基本数据结构来表示更复杂和更高级的数据结构;这在现实生活中处理数据时非常有用。

在现实生活中,我们处理来自不同来源的数据,通常从文件或数据库中读取数据。我们将介绍与文件相关的操作。我们将看到如何打开文件,以及有多少种方法可以做到这一点,如何从其中读取数据,如何写入数据,以及完成工作后如何安全地关闭它。最后一部分,许多人往往忽略,但非常重要。我们经常在现实世界的系统中遇到非常奇怪且难以追踪的 bug,仅仅是因为一个进程打开了一个文件而没有正确关闭它。无需多言,让我们开始我们的旅程。

高级数据结构

我们将首先讨论高级数据结构。我们将通过回顾列表来实现这一点。我们将构建一个栈和一个队列,探索多个元素成员资格检查,并加入一些函数式编程来增加趣味。如果这一切听起来令人生畏,那么请不要担心。我们将一步一步地完成,就像上一章一样,一旦你完成了这一章,你将感到自信。

要开始本章,你需要打开一个空白的笔记本。为此,你可以在 shell 中简单地输入以下命令。建议你在输入命令之前,首先使用cd命令导航到一个空目录:

docker run -p 8888:8888 -v 'pwd':/notebooks -it rcshubhadeep/packt-data-wrangling-base:latest

一旦 Docker 容器启动,将你的浏览器指向localhost:8888,并使用dw_4_all作为密码来访问笔记本界面。

迭代器

我们将从这个主题开始,即列表。然而,在我们进入列表之前,我们将介绍迭代器的概念。迭代器是一个实现了next方法的对象,这意味着迭代器是一个可以遍历集合(列表、元组、字典等)的对象。它是状态化的,这意味着每次我们调用next方法时,它都会从集合中给出下一个元素。如果没有更多的元素,那么它将引发StopIteration异常。

注意

当没有更多的值可以迭代时,迭代器的next方法会引发StopIteration异常。

如果你熟悉像 C、C++、Java、JavaScript 或 PHP 这样的编程语言,你可能已经注意到了这些语言中for循环实现的差异,它由三个不同的部分组成,即初始化、递增和终止条件,以及 Python 中的for循环。在 Python 中,我们不使用那种for循环。我们在 Python 中使用的是更类似于foreach循环的:for i in list_1。这是因为,在底层,for循环使用的是迭代器,所以我们不需要做所有额外的步骤。迭代器会为我们完成这些。

练习 15:迭代器简介

要生成数字列表,我们可以使用不同的方法:

  1. 生成一个包含 10000000 个 1 的列表:

    big_list_of_numbers = [1 for x in range(0, 10000000)]
    
  2. 检查这个变量的大小:

    from sys import getsizeof
    getsizeof(big_list_of_numbers)
    

    它将显示的值将大约是81528056(它在字节中)。这很多内存!而且big_list_of_numbers变量只有在列表推导完成后才可用。如果你尝试太大的数字,它也可能超出可用系统内存。

  3. 使用迭代器来减少内存使用:

    from itertools import repeat
    small_list_of_numbers = repeat(1, times=10000000)
    getsizeof(small_list_of_numbers)
    

    最后一行显示我们的small_list_of_numbers大小仅为56字节。此外,它是一种懒惰的方法,因为它没有生成所有元素。当需要时,它会逐个生成元素,从而节省我们的时间。实际上,如果你省略了times关键字参数,那么你可以实际生成无限数量的 1。

  4. 遍历新生成的迭代器:

    for i, x in enumerate(small_list_of_numbers): 
        print(x)
        if i > 10:
            break
    

    我们使用enumerate函数,以便我们得到循环计数器以及值。这有助于我们在达到计数器的某个特定数量时(例如 10)退出循环。

    输出将是一个包含 10 个 1 的列表。

  5. 要查找任何函数的定义,在 Jupyter 笔记本中输入函数名,然后输入一个?并按Shift + Enter。运行以下代码以了解我们如何使用 itertools 中的排列和组合:

    from itertools import (permutations, combinations, dropwhile, repeat,
    zip_longest)
    permutations?
    combinations?
    dropwhile?
    repeat?
    zip_longest?
    

栈是一个非常有用的数据结构。如果你对 CPU 内部结构和程序执行方式有些了解,那么你会意识到栈在很多这种情况下都存在。它只是一个具有一个限制条件的列表,即后进先出(LIFO),这意味着当从栈中读取值时,最后进入的元素会首先出来。以下插图将使这一点更加清晰:

图 2.1:进行两次插入和一次弹出操作的栈

图 2.1:进行两次插入元素和一次弹出操作的栈

如您所见,我们有一个后进先出(LIFO)策略来从栈中读取值。我们将使用 Python 列表来实现栈。Python 的列表有一个名为 pop 的方法,它执行与前面插图中所见相同的弹出操作。我们将使用它来实现栈。

练习 16:在 Python 中实现栈

  1. 首先,定义一个空栈:

    stack = []
    
  2. 使用 append 方法向栈中添加一个元素。多亏了 append,元素将始终追加到列表的末尾:

    stack.append(25)
    stack
    

    输出如下:

    [25]
    
  3. 向栈中追加另一个值:

    stack.append(-12)
    stack
    

    输出如下:

    [25, -12]
    
  4. 使用 pop 方法从我们的栈中读取一个值。此方法读取当前列表的最后一个索引,并将其返回给我们。读取完成后,它也会删除该索引:

    tos = stack.pop()tos
    

    输出如下:

    -12
    

    执行前面的代码后,tos 中将会有 -12,栈中只有一个元素,即 25

  5. 向栈中追加 "hello":

    stack.append("Hello")
    stack
    

    输出如下:

    [25, 'Hello']
    

想象你正在抓取一个网页,并且想要跟随其中存在的每个 URL。如果你在阅读网页时逐个将它们(追加)插入栈中,然后逐个弹出并跟随链接,那么你就有了一个干净且可扩展的解决方案。我们将在下一个练习中检查这个任务的这部分。

练习 17:使用用户定义的方法实现栈

我们将继续从上一个练习中关于栈的话题。但这次,我们将自己实现 appendpop 函数。这个练习的目标有两个。一方面,我们将实现栈,这次是一个真实世界的例子,这也涉及到对字符串方法的知识,因此也作为对上一章和活动的提醒。另一方面,它将向我们展示 Python 的一个微妙特性以及它是如何处理将列表变量传递给函数的,并将我们带到下一个练习,即函数式编程:

  1. 首先,我们将定义两个函数,stack_pushstack_pop。我们将其重命名,以避免命名空间冲突。同时,创建一个名为 url_stack 的栈以供以后使用:

    def stack_push(s, value):
        return s + [value]
    def stack_pop(s):
        tos = s[-1]
        del s[-1]
        return tos
    url_stack = []
    
  2. 第一个函数接受已经存在的栈,并将值添加到其末尾。

    注意

    注意值周围的方括号,这是为了将其转换为单元素列表,以便进行 + 操作。

  3. 第二个函数读取栈中当前 -1 索引处的值,然后使用 del 操作符删除该索引,并最终返回它之前读取的值。

  4. 现在,我们将有一个包含几个 URL 的字符串。我们的任务是分析这个字符串,当我们遇到 URL 时,逐个将它们推入栈中,然后最终使用 for 循环逐个弹出。让我们从关于数据科学的维基百科文章的第一行开始:

    wikipedia_datascience = "Data science is an interdisciplinary field that uses scientific methods, processes, algorithms and systems to extract knowledge [https://en.wikipedia.org/wiki/Knowledge] and insights from data [https://en.wikipedia.org/wiki/Data] in various forms, both structured and unstructured,similar to data mining [https://en.wikipedia.org/wiki/Data_mining]"
    
  5. 为了简化这个练习,我们在目标词旁边保留了方括号中的链接。

  6. 查找字符串的长度:

    len(wikipedia_datascience)
    

    输出如下:

    347
    
  7. 使用字符串的 split 方法将此字符串转换为列表,然后计算其长度:

    wd_list = wikipedia_datascience.split()
    len(wd_list)
    

    输出如下:

    34
    
  8. 使用 for 循环遍历每个单词并检查它是否是 URL。为此,我们将使用字符串的 startswith 方法,如果是 URL,则将其推入栈中:

    for word in wd_list:
        if word.startswith("[https://"):
            url_stack = stack_push(url_stack, word[1:-1])  
    # Notice the clever use of string slicing
    
  9. 打印 url_stack 中的值:

    url_stack
    

    输出如下:

    ['https://en.wikipedia.org/wiki/Knowledge',
     'https://en.wikipedia.org/wiki/Data',
     'https://en.wikipedia.org/wiki/Data_mining']
    
  10. 遍历列表并使用 stack_pop 函数逐个打印 URL:

    for i in range(0, len(url_stack)):
        print(stack_pop(url_stack))
    

    输出如下:

    图 2.2:使用栈打印的 URL 输出

    图 2.2:使用栈打印的 URL 输出
  11. 再次打印以确保在最后的 for 循环之后栈为空:

    print(url_stack)
    

    输出如下:

    []
    

我们在 stack_pop 方法中注意到一个奇怪的现象。我们传递了列表变量到那里,并在函数内部使用了 del 操作符,但每次调用函数时都会通过删除最后一个索引来改变原始变量。如果你来自像 C、C++ 和 Java 这样的语言,那么这完全是一个意外的行为,因为在那些语言中,这只能通过引用传递变量来实现,这可能导致 Python 代码中的微妙错误。所以请小心。一般来说,在函数内部更改变量的值不是一个好主意,这意味着在函数内部。传递给函数的任何变量都应被视为不可变的。这接近函数式编程的原则。Python 中的 lambda 表达式是一种构建单行、无名的函数的方法,按照惯例,这些函数是副作用免费的。

练习 18:Lambda 表达式

在这个练习中,我们将使用 lambda 表达式来证明著名的三角恒等式:

图 2.3 三角恒等式
  1. 导入 math 包:

    import math
    
  2. 定义两个函数,my_sinemy_cosine。我们声明这些函数的原因是,来自 math 包的原始 sincos 函数以弧度为输入,但我们更熟悉度。因此,我们将使用 lambda 表达式定义一个无名的单行函数并使用它。这个 lambda 函数将自动将我们的度数输入转换为弧度,然后对其应用 sincos 并返回值:

    def my_sine():
        return lambda x: math.sin(math.radians(x))
    def my_cosine():
        return lambda x: math.cos(math.radians(x))
    
  3. 定义 sinecosine 以满足我们的目的:

    sine = my_sine()
    cosine = my_cosine()
    math.pow(sine(30), 2) + math.pow(cosine(30), 2)
    

    输出如下:

    1.0
    

    注意,我们将 my_sinemy_cosine 的返回值分配给两个变量,然后直接使用它们作为函数。这比显式使用它们的方法要干净得多。注意,我们并没有在 lambda 函数中显式地写一个 return 语句。这是默认的。

练习 19:排序的 Lambda 表达式

Lambda 表达式将接受一个输入并根据元组中的值对其进行排序。Lambda 可以接受一个或多个输入。Lambda 表达式也可以通过使用 reverse 参数为 True 来实现逆序排序:

  1. 想象你在一个数据清洗工作中,你面临以下元组列表:

    capitals = [("USA", "Washington"), ("India", "Delhi"), ("France", "Paris"), ("UK", "London")]
    capitals
    

    输出如下:

    [('USA', 'Washington'),
     ('India', 'Delhi'),
     ('France', 'Paris'),
     ('UK', 'London')]
    
  2. 使用简单的 lambda 表达式按每个国家的首都名称对列表进行排序。使用以下代码:

    capitals.sort(key=lambda item: item[1])
    capitals
    

    输出如下:

    [('India', 'Delhi'),
     ('UK', 'London'),
     ('France', 'Paris'),
     ('USA', 'Washington')]
    

如我们所见,如果我们掌握了 lambda 表达式并在我们数据清洗工作中使用它们,它们是非常强大的。它们也是无副作用的,这意味着它们不会改变传递给它们的变量的值。

练习 20:多元素成员资格检查

这里有一个有趣的问题。让我们想象一下,你从你正在处理的一个文本语料库中抓取的一些单词列表:

  1. 创建一个 list_of_words 列表,其中包含从文本语料库中抓取的单词:

    list_of_words = ["Hello", "there.", "How", "are", "you", "doing?"]
    
  2. 查找此列表是否包含另一个列表的所有元素:

    check_for = ["How", "are"]
    

    存在一个复杂的解决方案,它涉及一个 for 循环和几个 if-else 条件(你应该尝试编写它!),但还有一个优雅的 Pythonic 解决方案来解决这个问题,它只需要一行代码并使用 all 函数。all 函数在可迭代对象的所有元素都为真时返回 True

  3. 使用 in 关键字检查列表 list_of_words 中的成员资格:

    all(w in list_of_words for w in check_for)
    

    输出如下:

    True
    

    它确实既优雅又简单,这个巧妙的小技巧在处理列表时非常重要。

队列

除了栈之外,我们感兴趣的另一个高级数据结构是队列。队列就像栈一样,意味着你一个接一个地添加元素。使用队列时,元素的读取遵循 FIFO(先进先出)策略。查看以下图表以更好地理解这一点:

图 2.4:队列的示意图

图 2.4:队列的示意图

我们将首先使用列表方法来完成这个任务,然后我们会向你展示这样做是不高效的。然后,我们将从 Python 的集合模块中学习 dequeue 数据结构。

练习 21:在 Python 中实现队列

  1. 使用纯列表方法创建一个 Python 队列:

    %%time
    queue = []
    for i in range(0, 100000):
        queue.append(i)
    print("Queue created")
    

    输出如下:

    Queue created
    Wall time: 11 ms
    
  2. 使用 pop 函数清空队列并检查其中的项目:

    for i in range(0, 100000):
        queue.pop(0)
    print("Queue emptied")
    

    输出如下:

    Queue emptied
    

    如果我们在执行前面的代码时使用 %%time 魔法命令,我们会看到它需要一段时间才能完成。在一个现代 MacBook 上,具有四核处理器和 8 GB RAM,它大约需要 1.20 秒才能完成。这种时间是因为 pop(0) 操作,这意味着每次我们从列表的左侧(当前 0 索引)弹出值时,Python 都必须通过将其他所有元素向左移动一个位置来重新排列列表的所有其他元素。确实,这不是一个非常优化的实现。

  3. 使用 Python 集合包中的deque数据结构实现相同的队列:

    %%time
    from collections import deque
    queue2 = deque()
    for i in range(0, 100000):
        queue2.append(i)
    print("Queue created")
    for i in range(0, 100000):
        queue2.popleft()
    print("Queue emptied")
    

    输出如下:

    Queue created
    Queue emptied
    Wall time: 23 ms
    
  4. 使用 Python 标准库中的专业和优化队列实现,这个操作所需的时间仅为 28 毫秒!这比之前有了巨大的改进。

队列是一个非常重要的数据结构。为了举一个现实生活中的例子,我们可以考虑一个生产者-消费者系统设计。在进行数据处理时,你经常会遇到必须处理非常大的文件的问题。处理这个问题的方法之一是将文件内容分成更小的部分,然后在创建小的、专门的工人进程时将它们推入队列,这些进程一次读取队列中的一小部分进行处理。这是一个非常强大的设计,你甚至可以有效地使用它来设计巨大的多节点数据处理管道。

我们在这里结束对数据结构的讨论。我们在这里讨论的只是冰山一角。数据结构是一个迷人的主题。还有许多其他的数据结构我们没有涉及,而且当它们被有效地使用时,可以提供巨大的附加价值。我们强烈鼓励你更多地探索数据结构。尽量了解链表、树、图、字典树以及它们的所有不同变体。它们不仅提供了学习的乐趣,而且也是数据从业者武器库中的秘密超级武器,你可以在每次面对困难的数据处理任务时使用它们。

活动 3:排列、迭代器、Lambda、列表

在这个活动中,我们将使用permutations生成所有可能的由 0、1 和 2 组成的三位数。然后遍历这个迭代器,并使用isinstanceassert确保返回类型是元组。同时,使用涉及dropwhilelambda表达式的单行代码将所有元组转换为列表,同时删除任何前导零(例如,(0, 1, 2)变为[1, 2])。最后,编写一个函数,它接受一个列表作为输入,并返回其中包含的实际数字。

这些步骤将指导你解决这个活动:

  1. 查找permutationsdropwhile的定义,来自itertools

  2. 编写一个表达式,使用012生成所有可能的三位数。

  3. 遍历之前生成的迭代器表达式。打印迭代器返回的每个元素。使用assertisinstance确保元素是元组类型。

  4. 再次使用dropwhile和 Lambda 表达式编写循环,以删除元组中的任何前导零。例如,(0, 1, 2)将变为[0, 2]。同时,将dropwhile的输出转换为列表。

  5. 检查dropwhile实际返回的类型。

  6. 将前面的代码合并到一个块中,这次编写一个单独的函数,你将传递由dropwhile生成的列表,该函数将返回列表中的整个数字。例如,如果你将[1, 2]传递给函数,它将返回12。确保返回类型确实是一个数字而不是字符串。尽管这个任务可以使用其他技巧完成,但我们要求你在函数中将传入的列表视为一个栈,并通过读取栈中的各个数字来生成数字。

通过这个活动,我们已经完成了这个主题,我们将转向下一个主题,该主题涉及基本的文件级操作。但在我们离开这个主题之前,我们鼓励你考虑一个解决方案来解决这个问题,而不使用我们在这里使用的所有高级操作和数据结构。你很快就会意识到原始解决方案是多么复杂,以及这些数据结构和操作带来了多大的价值。

注意

这个活动的解决方案可以在第 289 页找到。

Python 中的基本文件操作

在上一个主题中,我们研究了一些高级数据结构,并且学习了整洁且有用的函数式编程方法来无副作用地操作它们。在本主题中,我们将学习 Python 中的一些操作系统(OS)级函数。我们将主要关注与文件相关的函数,学习如何打开文件,逐行读取数据或一次性读取所有数据,最后如何干净地关闭我们打开的文件。我们将应用我们学到的一些技术,在一个我们将要读取的文件上进一步练习我们的数据处理技能。

练习 22:文件操作

在这个练习中,我们将学习 Python 的 OS 模块,我们还将看到两种非常实用的方法来写入和读取环境变量。在设计和开发数据处理管道时,写入和读取环境变量的能力通常非常重要。

注意

事实上,著名 12 因子应用程序设计的因素之一就是将配置存储在环境中的想法。你可以在以下 URL 查看:https://12factor.net/config。

OS 模块的目的是为你提供与操作系统依赖功能交互的方法。一般来说,它是相当底层的,而且大多数来自那里的函数在日常使用中并不有用,然而,其中一些是值得学习的。os.environ是 Python 维护的包含你操作系统中的所有当前环境变量的集合。它赋予你创建新环境变量的能力。os.getenv函数让你能够读取一个环境变量:

  1. 导入os模块。

    import os
    
  2. 设置一些环境变量:

    os.environ['MY_KEY'] = "MY_VAL"
    os.getenv('MY_KEY')
    

    输出如下:

    'MY_VAL'
    

    当环境变量未设置时打印环境变量:

    print(os.getenv('MY_KEY_NOT_SET'))
    

    输出如下:

    None
    
  3. 打印os环境:

    print(os.environ)
    

    注意

    由于安全原因,输出尚未添加。

    执行前面的代码后,你会看到你已成功打印了 MY_KEY 的值,当你尝试打印 MY_KEY_NOT_SET 时,它打印了 None

文件处理

在这个练习中,我们将了解如何在 Python 中打开文件。我们将了解我们可以使用的不同模式以及它们代表的意义。Python 有一个内置的 open 函数,我们将使用它来打开文件。open 函数接受一些输入参数。其中,第一个参数代表你想要打开的文件名,是唯一必须的。其他所有内容都有默认值。当你调用 open 时,Python 使用底层系统级调用打开文件句柄,并将其返回给调用者。

通常,文件可以以读取或写入模式打开。如果我们以某种模式打开一个文件,则不支持其他操作。读取通常意味着我们从现有文件的开始处开始读取,而写入可以意味着从文件开始处创建新文件并写入,或者打开现有文件并附加到它。以下是一个表格,展示了 Python 支持的所有不同文件打开模式:

图 2.5 读取文件的模式

图 2.5 读取文件的模式

还存在一个已弃用的模式 U,在 Python3 环境中不起作用。我们必须记住的一件事是,Python 总是会区分 tb 模式,即使底层操作系统不会。这是因为 b 模式下,Python 不会尝试解码它所读取的内容,而是返回字节对象,而在 t 模式下,它会尝试解码流并返回字符串表示。

你可以这样打开一个文件进行读取:

fd = open("Alice’s Adventures in Wonderland, by Lewis Carroll")

这是以 rt 模式打开的。如果你想以二进制模式打开相同的文件,也可以。要以二进制模式打开文件,请使用 rb 模式:

fd = open("Alice’s Adventures in Wonderland, by Lewis Carroll",
          "rb")
fd

输出如下:

<_io.BufferedReader name='Alice's Adventures in Wonderland, by Lewis Carroll'>

这就是我们以写入模式打开文件的方式:

fd = open("interesting_data.txt", "w")
fd

输出如下:

<_io.TextIOWrapper name='interesting_data.txt' mode='w' encoding='cp1252'>

练习 23:打开和关闭文件

在这个练习中,我们将了解如何关闭打开的文件。一旦我们打开文件,关闭它非常重要。由于悬挂的文件句柄,可能会发生许多系统级错误。一旦我们关闭文件,就无法使用该特定的文件句柄在该文件上执行任何进一步的操作:

  1. 以二进制模式打开文件:

    fd = open("Alice's Adventures in Wonderland, by Lewis Carroll",
              "rb")
    
  2. 使用 close() 关闭文件:

    fd.close()
    
  3. Python 还为我们提供了一个与文件句柄一起的 closed 标志。如果我们关闭前打印它,那么我们会看到 False,而如果我们关闭后打印它,那么我们会看到 True。如果我们逻辑上检查文件是否已正确关闭,那么这就是我们想要使用的标志。

with 语句

在这个练习中,我们将了解 Python 中的 with 语句以及我们如何在打开和关闭文件的情况下有效地使用它。

Python 中的 with 命令是一个复合语句。像任何复合语句一样,with 也会影响其内部代码的执行。在 with 的情况下,它用于将一段代码包裹在所谓的 Python 中的 上下文管理器 的作用域内。关于上下文管理器的详细讨论超出了本练习和这个主题的范围,但可以说,多亏了在 Python 中打开文件时 open 调用内部实现的上下文管理器,当我们用 with 语句包裹它时,可以保证会自动发生关闭调用。

注意

关于 with 的整个 PEP 可以在 www.python.org/dev/peps/pep-0343/ 找到。我们鼓励您去查看。

使用 with 语句打开文件

使用 with 语句打开一个文件:

with open("Alice’s Adventures in Wonderland, by Lewis Carroll")as fd:
    print(fd.closed)
print(fd.closed)

输出如下:

False
True

如果我们执行前面的代码,我们会看到第一个打印将结束打印 False,而第二个将打印 True。这意味着一旦控制流出 with 块,文件描述符将自动关闭。

注意

这无疑是打开文件并获得文件描述符的最干净、最 Pythonic 的方式。我们鼓励您在需要自己打开文件时始终使用此模式。

练习 24:逐行读取文件

  1. 打开一个文件,然后逐行读取文件并打印出来,就像我们读取它一样:

    with open("Alice’s Adventures in Wonderland, by Lewis Carroll",
              encoding="utf8") as fd:
        for line in fd:
                print(line)
    

    输出如下:

    图 2.6:Jupyter 笔记本截图

    图 2.6:Jupyter 笔记本截图
  2. 通过查看前面的代码,我们可以真正地看到为什么这很重要。通过这段简短的代码,你甚至可以逐行打开和读取大小为许多 GB 的文件,而不会淹没或超出系统内存!

    文件描述符对象中还有一个名为 readline 的显式方法,它一次从文件中读取一行。

  3. 在第一个循环之后立即复制相同的 for 循环:

    with open("Alice’s Adventures in Wonderland, by Lewis Carroll",
              encoding="utf8") as fd:
        for line in fd:
            print(line)
        print("Ended first loop")
        for line in fd:
            print(line)
    

    输出如下:

图 2.7:打开文件的部分

图 2.7:打开文件的部分

练习 25:写入文件

我们将通过向您展示如何写入文件来结束关于文件操作的主题。我们将写入几行到文件中,并读取该文件:

  1. 使用文件描述符对象的 write 函数:

    data_dict = {"India": "Delhi", "France": "Paris", "UK": "London",
                 "USA": "Washington"}
    with open("data_temporary_files.txt", "w") as fd:
        for country, capital in data_dict.items():
            fd.write("The capital of {} is {}\n".format(
                country, capital))
    
  2. 使用以下命令读取文件:

    with open("data_temporary_files.txt", "r") as fd:
        for line in fd:
            print(line)
    

    输出如下:

    The capital of India is Delhi
    The capital of France is Paris
    The capital of UK is London
    The capital of USA is Washington
    
  3. 使用以下命令使用打印函数写入文件:

    data_dict_2 = {"China": "Beijing", "Japan": "Tokyo"}
    with open("data_temporary_files.txt", "a") as fd:
        for country, capital in data_dict_2.items():
            print("The capital of {} is {}".format(
                country, capital), file=fd)
    
  4. 使用以下命令读取文件:

    with open("data_temporary_files.txt", "r") as fd:
        for line in fd:
            print(line)
    

    输出如下:

    The capital of India is Delhi
    The capital of France is Paris
    The capital of UK is London
    The capital of USA is Washington
    The capital of China is Beijing
    The capital of Japan is Tokyo
    

    注意:

    在第二种情况下,我们没有在要写入的字符串末尾添加额外的换行符,\n,打印函数会自动为我们完成这个操作。

通过这种方式,我们将结束这个主题。就像前面的主题一样,我们为您设计了一个活动来练习您新获得的知识。

活动 4:设计您自己的 CSV 解析器

在作为数据从业者的一生中,你将经常遇到 CSV 文件。CSV 是一个以逗号分隔的文件,其中通常存储和分隔表格格式的数据,尽管也可以使用其他字符。

在这个活动中,我们将被要求构建自己的 CSV 读取器和解析器。虽然如果我们试图涵盖所有用例和边缘情况,包括转义字符等,这将是一个大任务,但为了这个小型活动,我们将保持我们的要求简单。我们将假设没有转义字符,这意味着如果你在行中的任何位置使用逗号,那么它意味着你正在开始一个新列。我们还将假设我们感兴趣的唯一功能是能够逐行读取 CSV 文件,其中每次读取都会生成一个新字典,列名作为键,行名作为值。

这里有一个例子:

图 2.8 示例数据表

图 2.8 示例数据表

我们可以将前表中的数据转换为 Python 字典,其外观如下:{"Name": "Bob", "Age": "24", "Location": "California"}

  1. itertools 导入 zip_longest。创建一个函数来压缩 headerlinefillvalue=None

  2. 通过使用 with 块内的 r 模式,从 GitHub 链接打开附带的 sales_record.csv 文件,并首先检查它是否已打开。

  3. 读取第一行,并使用字符串方法生成所有列名的列表。

  4. 开始读取文件。逐行读取。

  5. 读取每一行,并将该行传递给一个函数,同时带有标题列表。该函数的工作是从这两个中构建一个字典,并填充 键:值。请记住,缺失的值应导致 None

    注意

    本活动的解决方案可以在第 291 页找到。

摘要

在本章中,我们学习了高级数据结构(如栈和队列)的工作原理。我们实现了栈和队列,然后专注于函数式编程的不同方法,包括迭代器,并将列表和函数结合起来。之后,我们探讨了 OS 级别的函数和环境变量以及文件的管理。我们还检查了处理文件的一种干净方式,并在最后一个活动中创建了自己的 CSV 解析器。

在下一章中,我们将处理三个最重要的库,即 NumPy、pandas 和 matplotlib。

第四章:第三章

NumPy、Pandas 和 Matplotlib 简介

学习目标

到本章结束时,您将能够:

  • 创建并操作一维和多维数组

  • 创建并操作 pandas DataFrame 和序列对象

  • 使用 Matplotlib 库绘制和可视化数值数据

  • 将 matplotlib、NumPy 和 pandas 应用于从 DataFrame/矩阵中计算描述性统计量

在本章中,您将了解 NumPy、pandas 和 matplotlib 库的基础知识。

简介

在前面的章节中,我们已经介绍了 Python 中的一些高级数据结构,例如栈、队列、迭代器和文件操作。在本节中,我们将介绍三个基本库,即 NumPy、pandas 和 matplotlib。

NumPy 数组

在数据科学家的生活中,读取和操作数组是至关重要的,这也是最常遇到的任务。这些数组可能是一个一维列表、一个多维表格或一个充满数字的矩阵。

数组可以填充整数、浮点数、布尔值、字符串,甚至混合类型。然而,在大多数情况下,数值数据类型占主导地位。

一些需要处理数字数组的示例场景如下:

  • 读取电话号码和邮政编码列表并提取特定模式

  • 创建一个随机数字矩阵以运行某些统计过程的蒙特卡洛模拟

  • 缩放和归一化销售数据表,包含大量财务和交易数据

  • 从大型原始数据表中创建一个包含关键描述性统计量(例如,平均值、中位数、最小/最大范围、方差、四分位数范围)的小型表格

  • 读取并分析一维数组中的时间序列数据,例如一个组织一年内的股价或气象站每日温度数据

简而言之,数组和数值数据表无处不在。作为一名数据整理专业人士,能够读取和处理数值数组的能力的重要性不容小觑。在这方面,NumPy 数组将是您在 Python 中需要了解的最重要对象。

NumPy 数组和功能

NumPySciPy是 Python 的开源附加模块,它们提供了预编译的快速函数,以提供常见的数学和数值例程。这些库已经发展成为高度成熟的库,它们提供的功能满足或可能超过与常见商业软件(如MATLABMathematica)相关联的功能。

NumPy 模块的主要优势是处理或创建一维或多维数组。这种高级数据结构/类是 NumPy 包的核心,它作为更高级类(如pandasDataFrame)的基本构建块,我们将在本章稍后介绍。

NumPy 数组与常见的 Python 列表不同,因为 Python 列表可以被视为简单的数组。NumPy 数组是为向量化操作而构建的,这些操作只需一行代码就能处理大量的数值数据。NumPy 数组中的许多内置数学函数是用 C 或 Fortran 等低级语言编写的,并预先编译以实现真正的快速执行。

注意

NumPy 数组是针对数值分析优化的数据结构,这就是为什么它们对数据科学家来说如此重要的原因。

练习 26:从列表创建 NumPy 数组

在这个练习中,我们将从列表创建一个 NumPy 数组:

  1. 要使用 NumPy,我们必须导入它。按照惯例,我们在导入时给它一个简短的名字,np

    import numpy as np
    
  2. 创建一个包含三个元素 1、2 和 3 的列表:

    list_1 = [1,2,3]
    
  3. 使用array函数将其转换为数组:

    array_1 = np.array(list_1)
    

    我们刚刚从常规 Python 列表对象list_1创建了一个名为array_1的 NumPy 数组对象。

  4. 创建一个包含浮点类型元素 1.2、3.4 和 5.6 的数组:

    import array as arr
    a = arr.array('d', [1.2, 3.4, 5.6])
    print(a)
    

    输出如下:

    array('d', [1.2, 3.4, 5.6])
    
  5. 让我们使用type函数检查新创建的对象的类型:

    type(array_1)
    

    输出如下:

    numpy.ndarray
    
  6. list_1上使用type

    type (list_1)
    

    输出如下:

    list
    

因此,这确实与常规的list对象不同。

练习 27:添加两个 NumPy 数组

这个简单的练习将演示两个 NumPy 数组的相加,并因此展示常规 Python 列表/数组与 NumPy 数组之间的关键区别:

  1. 考虑前一个练习中的list_1array_1。如果你已经更改了 Jupyter 笔记本,你将需要再次声明它们。

  2. 使用+符号将两个list_1对象相加,并将结果保存到list_2中:

    list_2 = list_1 + list_1
    print(list_2)
    

    输出如下:

     [1, 2, 3, 1, 2, 3]
    
  3. 使用相同的+符号将两个array_1对象相加,并将结果保存到array_2中:

    array_2 = array_1 + array_1
    print(array_2)
    

    输出如下:

    [2, ,4, 6]
    

你注意到区别了吗?第一个打印显示了一个包含 6 个元素的列表[1, 2, 3, 1, 2, 3]。但第二个打印显示了一个包含元素[2, 4, 6]的另一个 NumPy 数组(或向量),这些元素只是array_1各个元素的求和。

NumPy 数组类似于数学对象——向量。它们是为元素级操作而构建的,也就是说,当我们添加两个 NumPy 数组时,我们将第一个数组的第一个元素添加到第二个数组的第一个元素——在这个操作中有元素到元素的对应关系。这与 Python 列表形成对比,在 Python 列表中,元素只是简单追加,没有元素到元素的关系。这正是 NumPy 数组的真正力量:它们可以被当作数学向量来处理。

向量是一组数字,可以表示,例如,三维空间中点的坐标或图片中数字的颜色(RGB)。自然地,相对顺序对这样的集合很重要,正如我们之前讨论的,NumPy 数组可以保持这种顺序关系。这就是为什么它们非常适合用于数值计算。

练习 28:NumPy 数组上的数学运算

现在你已经知道这些数组就像向量一样,我们将尝试在数组上进行一些数学运算。

NumPy 数组甚至支持逐元素指数运算。例如,假设有两个数组——第一个数组的元素将被提升到第二个数组元素的幂:

  1. 使用以下命令乘以两个数组:

    print("array_1 multiplied by array_1: ",array_1*array_1)
    

    输出如下:

    array_1 multiplied by array_1:  [1 4 9]
    
  2. 使用以下命令除以两个数组:

    print("array_1 divided by array_1: ",array_1/array_1)
    

    输出如下:

    array_1 divided by array_1:  [1\. 1\. 1.]
    
  3. 使用以下命令将一个数组提升到第二个数组的幂:

    print("array_1 raised to the power of array_1: ",array_1**array_1)
    

    输出如下:

    array_1 raised to the power of array_1:  [ 1  4 27]
    

练习 29:NumPy 数组的高级数学运算

NumPy 拥有你所能想到的所有内置数学函数。在这里,我们将创建一个列表并将其转换为 NumPy 数组。然后,我们将对该数组执行一些高级数学运算。

在这里,我们正在创建一个列表并将其转换为 NumPy 数组。然后,我们将展示如何在该数组上执行一些高级数学运算:

  1. 创建一个包含五个元素的列表:

    list_5=[i for i in range(1,6)]
    print(list_5)
    

    输出如下:

    [1, 2, 3, 4, 5]
    
  2. 使用以下命令将列表转换为 NumPy 数组:

    array_5=np.array(list_5)
    array_5
    

    输出如下:

    array([1, 2, 3, 4, 5])
    
  3. 使用以下命令计算数组的正弦值:

    # sine function
    print("Sine: ",np.sin(array_5))
    

    输出如下:

    Sine:  [ 0.84147098  0.90929743  0.14112001 -0.7568025  -0.95892427]
    
  4. 使用以下命令计算数组的对数值:

    # logarithm
    print("Natural logarithm: ",np.log(array_5))
    print("Base-10 logarithm: ",np.log10(array_5))
    print("Base-2 logarithm: ",np.log2(array_5))
    

    输出如下:

    Natural logarithm:  [0\.         0.69314718 1.09861229 1.38629436 1.60943791]
    Base-10 logarithm:  [0\.         0.30103    0.47712125 0.60205999 0.69897   ]
    Base-2 logarithm:  [0\.         1\.         1.5849625  2\.         2.32192809]
    
  5. 使用以下命令计算数组的指数值:

    # Exponential
    print("Exponential: ",np.exp(array_5))
    

    输出如下:

    Exponential:  [  2.71828183   7.3890561   20.08553692  54.59815003 148.4131591 ]
    

练习 30:使用 arange 和 linspace 生成数组

数值数组的生成是一个相当常见的任务。到目前为止,我们一直是通过创建 Python 列表对象并将其转换为 NumPy 数组来做到这一点。然而,我们可以绕过这一步,直接使用原生 NumPy 方法进行操作。

arange函数根据你给出的最小和最大界限以及指定的步长创建一系列数字。另一个函数linspace在两个极端之间创建一系列固定数量的中间点:

  1. 使用以下命令创建一系列数字,使用arange方法:

    print("A series of numbers:",np.arange(5,16))
    

    输出如下:

    A series of numbers: [ 5  6  7  8  9 10 11 12 13 14 15]
    
  2. 使用arange函数打印数字,使用以下命令:

    print("Numbers spaced apart by 2: ",np.arange(0,11,2))
    print("Numbers spaced apart by a floating point number: ",np.arange(0,11,2.5))
    print("Every 5th number from 30 in reverse order\n",np.arange(30,-1,-5))
    

    输出如下:

    Numbers spaced apart by 2:  [ 0  2  4  6  8 10]
    Numbers spaced apart by a floating point number:  [ 0\.   2.5  5\.   7.5 10\. ]
    Every 5th number from 30 in reverse order
     [30 25 20 15 10  5  0]
    
  3. 对于线性间隔的数字,我们可以使用linspace方法,如下所示:

    print("11 linearly spaced numbers between 1 and 5: ",np.linspace(1,5,11))
    

    输出如下:

    11 linearly spaced numbers between 1 and 5:  [1\.  1.4 1.8 2.2 2.6 3\.  3.4 3.8 4.2 4.6 5\. ]
    

练习 31:创建多维数组

到目前为止,我们只创建了单维数组。现在,让我们创建一些多维数组(例如线性代数中的矩阵)。就像我们从简单的扁平列表创建一维数组一样,我们可以从列表的列表中创建一个二维数组:

  1. 使用以下命令创建一个列表的列表并将其转换为二维 NumPy 数组:

    list_2D = [[1,2,3],[4,5,6],[7,8,9]]
    mat1 = np.array(list_2D)
    print("Type/Class of this object:",type(mat1))
    print("Here is the matrix\n----------\n",mat1,"\n----------")
    

    输出如下:

    Type/Class of this object: <class 'numpy.ndarray'>
    Here is the matrix
    ---------- 
    [[1 2 3] 
    [4 5 6] 
    [7 8 9]] 
    ----------
    
  2. 可以使用以下代码将元组转换为多维数组:

    tuple_2D = np.array([(1.5,2,3), (4,5,6)])
    mat_tuple = np.array(tuple_2D)
    print (mat_tuple)
    

    输出如下:

    [[1.5 2\.  3\. ]
     [4\.  5\.  6\. ]]
    

因此,我们已经使用 Python 列表和元组创建了多维数组。

练习 32:二维数组的维度、形状、大小和数据类型

以下方法可让您检查数组的维度、形状和大小。注意,如果它是一个 3x2 矩阵,即它有 3 行和 2 列,那么形状将是(3,2),但大小将是 6,因为 6 = 3x2:

  1. 使用ndim通过以下命令打印矩阵的维度:

    print("Dimension of this matrix: ",mat1.ndim,sep='')
    

    输出如下:

    Dimension of this matrix: 2
    
  2. 使用size打印大小:

    print("Size of this matrix: ", mat1.size,sep='') 
    

    输出如下:

    Size of this matrix: 9
    
  3. 使用shape打印矩阵的形状:

    print("Shape of this matrix: ", mat1.shape,sep='')
    

    输出如下:

    Shape of this matrix: (3, 3)
    
  4. 使用dtype打印维度类型:

    print("Data type of this matrix: ", mat1.dtype,sep='')
    

    输出如下:

    Data type of this matrix: int32
    

练习 33:零矩阵、单位矩阵、随机矩阵和向量

现在我们已经熟悉了 NumPy 中的基本向量(一维)和矩阵数据结构,我们将看看如何轻松创建特殊矩阵。通常,您可能需要创建填充有零、一、随机数或对角线上的单位矩阵的矩阵:

  1. 使用以下命令打印零向量:

    print("Vector of zeros: ",np.zeros(5))
    

    输出如下:

    Vector of zeros:  [0\. 0\. 0\. 0\. 0.]
    
  2. 使用以下命令打印零矩阵:

    print("Matrix of zeros: ",np.zeros((3,4)))
    

    输出如下:

    Matrix of zeros:  [[0\. 0\. 0\. 0.]
     [0\. 0\. 0\. 0.]
     [0\. 0\. 0\. 0.]]
    
  3. 使用以下命令打印五矩阵:

    print("Matrix of 5's: ",5*np.ones((3,3)))
    

    输出如下:

    Matrix of 5's:  [[5\. 5\. 5.]
     [5\. 5\. 5.]
     [5\. 5\. 5.]]
    
  4. 使用以下命令打印单位矩阵:

    print("Identity matrix of dimension 2:",np.eye(2))
    

    输出如下:

    Identity matrix of dimension 2: [[1\. 0.]
     [0\. 1.]]
    
  5. 使用以下命令打印 4x4 的单位矩阵:

    print("Identity matrix of dimension 4:",np.eye(4))
    

    输出如下:

    Identity matrix of dimension 4: [[1\. 0\. 0\. 0.]
     [0\. 1\. 0\. 0.]
     [0\. 0\. 1\. 0.]
     [0\. 0\. 0\. 1.]]
    
  6. 使用randint函数打印具有随机形状的矩阵:

    print("Random matrix of shape (4,3):\n",np.random.randint(low=1,high=10,size=(4,3)))
    

    样本输出如下:

    Random matrix of shape (4,3):
     [[6 7 6]
     [5 6 7]
     [5 3 6]
     [2 9 4]]
    

    注意

    在创建矩阵时,您需要传递整数元组作为参数。

随机数生成是一个非常有用的工具,需要在数据科学/数据处理任务中掌握。我们将在统计部分再次探讨随机变量和分布的主题,并了解 NumPy 和 pandas 如何内置随机数和序列生成,以及操作函数。

练习 34:重塑、展平、最小值、最大值和排序

reshaperavel函数,它将任何给定的数组展平为一维数组。这在许多机器学习和数据分析任务中是一个非常有用的操作。

以下函数可以重塑函数。我们首先生成一个两位数的随机一维向量,然后将向量重塑为多维向量:

  1. 使用以下代码创建一个包含 30 个随机整数(从 1 到 99 中采样)的数组,并将其重塑成两种不同的形式:

    a = np.random.randint(1,100,30)
    b = a.reshape(2,3,5)
    c = a.reshape(6,5)
    
  2. 使用以下代码通过shape函数打印形状:

    print ("Shape of a:", a.shape)
    print ("Shape of b:", b.shape)
    print ("Shape of c:", c.shape)
    

    输出如下:

    Shape of a: (30,)
    Shape of b: (2, 3, 5)
    Shape of c: (6, 5)
    
  3. 使用以下代码打印数组 a、b 和 c:

    print("\na looks like\n",a)
    print("\nb looks like\n",b)
    print("\nc looks like\n",c)
    

    样本输出如下:

    a looks like
     [ 7 82  9 29 50 50 71 65 33 84 55 78 40 68 50 15 65 55 98 38 23 75 50 57
     32 69 34 59 98 48]
    b looks like
     [[[ 7 82  9 29 50]
      [50 71 65 33 84]
      [55 78 40 68 50]]
     [[15 65 55 98 38]
      [23 75 50 57 32]
      [69 34 59 98 48]]]
    c looks like
     [[ 7 82  9 29 50]
     [50 71 65 33 84]
     [55 78 40 68 50]
     [15 65 55 98 38]
     [23 75 50 57 32]
     [69 34 59 98 48]]
    

    注意

    "b"是一个三维数组——一种列表的列表的列表。

  4. 使用以下代码将文件 b 展平:

    b_flat = b.ravel()
    print(b_flat)
    

    样本输出如下:

    [ 7 82  9 29 50 50 71 65 33 84 55 78 40 68 50 15 65 55 98 38 23 75 50 57
     32 69 34 59 98 48]
    

练习 35:索引和切片

索引切片的 NumPy 数组与常规列表索引非常相似。我们甚至可以通过提供一个格式为(开始,步长,结束)的额外参数来以确定的步长遍历元素向量。此外,我们可以传递一个列表作为参数来选择特定元素。

在这个练习中,我们将学习一维和多维数组上的索引和切片操作:

注意

在多维数组中,你可以使用两个数字来表示一个元素的位置。例如,如果元素位于第三行第二列,其索引为 2 和 1(因为 Python 的零基索引)。

  1. 创建一个包含 10 个元素的数组,并通过使用不同的语法进行切片和索引来检查其各种元素。使用以下命令完成此操作:

    array_1 = np.arange(0,11)
    print("Array:",array_1)
    

    输出如下:

    Array: [ 0  1  2  3  4  5  6  7  8  9 10]
    
  2. 使用以下命令打印第七位置的元素:

    print("Element at 7th index is:", array_1[7])
    

    输出如下:

    Element at 7th index is: 7
    
  3. 使用以下命令打印第三和第六位置之间的元素:

    print("Elements from 3rd to 5th index are:", array_1[3:6])
    

    输出如下:

    Elements from 3rd to 5th index are: [3 4 5]
    
  4. 使用以下命令打印直到第四个位置的元素:

    print("Elements up to 4th index are:", array_1[:4])
    

    输出如下:

    Elements up to 4th index are: [0 1 2 3]
    
  5. 使用以下命令反向打印元素:

    print("Elements from last backwards are:", array_1[-1::-1])
    

    输出如下:

    Elements from last backwards are: [10  9  8  7  6  5  4  3  2  1  0]
    
  6. 使用以下命令按反向索引打印元素,跳过三个值:

    print("3 Elements from last backwards are:", array_1[-1:-6:-2])
    

    输出如下:

    3 Elements from last backwards are: [10  8  6]
    
  7. 使用以下命令创建一个名为array_2的新数组:

    array_2 = np.arange(0,21,2)
    print("New array:",array_2)
    

    输出如下:

    New array: [ 0  2  4  6  8 10 12 14 16 18 20]
    
  8. 打印数组的第二个、第四个和第九个元素:

    print("Elements at 2nd, 4th, and 9th index are:", array_2[[2,4,9]])
    

    输出如下:

    Elements at 2nd, 4th, and 9th index are: [ 4  8 18]
    
  9. 使用以下命令创建一个多维数组:

    matrix_1 = np.random.randint(10,100,15).reshape(3,5)
    print("Matrix of random 2-digit numbers\n ",matrix_1)
    

    样本输出如下:

    Matrix of random 2-digit numbers
      [[21 57 60 24 15]
     [53 20 44 72 68]
     [39 12 99 99 33]]
    
  10. 使用双括号索引访问值,使用以下命令:

    print("\nDouble bracket indexing\n")
    print("Element in row index 1 and column index 2:", matrix_1[1][2])
    

    样本输出如下:

    Double bracket indexing
    Element in row index 1 and column index 2: 44
    
  11. 使用单括号索引访问值,使用以下命令:

    print("\nSingle bracket with comma indexing\n")
    print("Element in row index 1 and column index 2:", matrix_1[1,2])
    

    样本输出如下:

    Single bracket with comma indexing
    Element in row index 1 and column index 2: 44
    
  12. 使用以下命令通过行或列访问多维数组中的值:

    print("\nRow or column extract\n")
    print("Entire row at index 2:", matrix_1[2])
    print("Entire column at index 3:", matrix_1[:,3])
    

    样本输出如下:

    Row or column extract
    Entire row at index 2: [39 12 99 99 33]
    Entire column at index 3: [24 72 99]
    
  13. 使用以下命令按指定的行和列索引打印矩阵:

    print("\nSubsetting sub-matrices\n")
    print("Matrix with row indices 1 and 2 and column indices 3 and 4\n", matrix_1[1:3,3:5])
    

    样本输出如下:

    Subsetting sub-matrices
    Matrix with row indices 1 and 2 and column indices 3 and 4
     [[72 68]
     [99 33]]
    
  14. 使用以下命令按指定的行和列索引打印矩阵:

    print("Matrix with row indices 0 and 1 and column indices 1 and 3\n", matrix_1[0:2,[1,3]])
    

    样本输出如下:

    Matrix with row indices 0 and 1 and column indices 1 and 3
     [[57 24]
     [20 72]]
    

条件子集

条件子集是一种根据某些数值条件选择特定元素的方法。它几乎就像是一个简化的 SQL 查询,用于子集元素。请看以下示例:

matrix_1 = np.array(np.random.randint(10,100,15)).reshape(3,5)
print("Matrix of random 2-digit numbers\n",matrix_1)
print ("\nElements greater than 50\n", matrix_1[matrix_1>50])

样本输出如下(请注意,由于是随机的,您的确切输出将不同):

Matrix of random 2-digit numbers
 [[71 89 66 99 54]
 [28 17 66 35 85]
 [82 35 38 15 47]]
Elements greater than 50
 [71 89 66 99 54 66 85 82]

练习 36:数组操作(数组-数组、数组-标量和通用函数)

NumPy 数组操作就像数学矩阵一样,操作是逐元素进行的。

创建两个矩阵(多维数组)并使用随机整数,演示逐元素数学运算,如加法、减法、乘法和除法。展示指数运算(将一个数提高到某个幂)如下:

注意

由于随机数生成,您的具体输出可能与这里显示的不同。

  1. 创建两个矩阵:

    matrix_1 = np.random.randint(1,10,9).reshape(3,3)
    matrix_2 = np.random.randint(1,10,9).reshape(3,3)
    print("\n1st Matrix of random single-digit numbers\n",matrix_1)
    print("\n2nd Matrix of random single-digit numbers\n",matrix_2)
    

    样本输出如下(请注意,由于是随机的,所以您的确切输出将与您不同):

    1st Matrix of random single-digit numbers
     [[6 5 9]
     [4 7 1]
     [3 2 7]]
    2nd Matrix of random single-digit numbers
     [[2 3 1]
     [9 9 9]
     [9 9 6]]
    
  2. 在矩阵上执行加法、减法、除法和线性组合:

    print("\nAddition\n", matrix_1+matrix_2)
    print("\nMultiplication\n", matrix_1*matrix_2)
    print("\nDivision\n", matrix_1/matrix_2)
    print("\nLinear combination: 3*A - 2*B\n", 3*matrix_1-2*matrix_2)
    

    样本输出如下(请注意,由于是随机的,所以您的确切输出将与您不同):

    Addition
     [[ 8  8 10]
     [13 16 10]
     [12 11 13]] ^
    Multiplication
     [[12 15  9]
     [36 63  9]
     [27 18 42]]
    Division
     [[3\.         1.66666667 9\.        ]
     [0.44444444 0.77777778 0.11111111]
     [0.33333333 0.22222222 1.16666667]]
    Linear combination: 3*A - 2*B
     [[ 14   9  25]
     [ -6   3 -15]
     [ -9 -12   9]]
    
  3. 执行标量、指数矩阵立方和指数平方根的加法:

    print("\nAddition of a scalar (100)\n", 100+matrix_1)
    print("\nExponentiation, matrix cubed here\n", matrix_1**3)
    print("\nExponentiation, square root using 'pow' function\n",pow(matrix_1,0.5))
    

    样本输出如下(请注意,由于是随机的,所以您的确切输出将与您不同):

    Addition of a scalar (100)
     [[106 105 109]
     [104 107 101]
     [103 102 107]]
    Exponentiation, matrix cubed here
     [[216 125 729]
     [ 64 343   1]
     [ 27   8 343]]
    Exponentiation, square root using 'pow' function
     [[2.44948974 2.23606798 3\.        ]
     [2\.         2.64575131 1\.        ]
     [1.73205081 1.41421356 2.64575131]]
    

数组堆叠

在数组上方(或旁边)堆叠是一个有用的数据整理操作。以下是代码:

a = np.array([[1,2],[3,4]])
b = np.array([[5,6],[7,8]])
print("Matrix a\n",a)
print("Matrix b\n",b)
print("Vertical stacking\n",np.vstack((a,b)))
print("Horizontal stacking\n",np.hstack((a,b)))

输出如下:

Matrix a
 [[1 2]
 [3 4]]
Matrix b
 [[5 6]
 [7 8]]
Vertical stacking
 [[1 2]
 [3 4]
 [5 6]
 [7 8]]
Horizontal stacking
 [[1 2 5 6]
 [3 4 7 8]]

NumPy 有许多其他高级功能,主要与统计和线性代数函数相关,这些函数在机器学习和数据科学任务中得到了广泛的应用。然而,并非所有这些对初学者级别的数据处理直接有用,所以我们在这里不会涵盖它。

Pandas 数据框

Pandas 库是一个 Python 包,它提供了快速、灵活且表达性强的数据结构,旨在使处理关系型或标记数据变得既简单又直观。它的目标是成为在 Python 中进行实际、现实世界数据分析的基本高级构建块。此外,它还有更广泛的目标,即成为任何语言中最强大和最灵活的开源数据分析/操作工具。

Pandas 的两种主要数据结构,Series(一维)和DataFrame(二维),可以处理绝大多数典型用例。Pandas 是建立在 NumPy 之上的,旨在与许多第三方库很好地集成到一个科学计算环境中。

练习 37:创建 Pandas 序列

在这个练习中,我们将学习如何从我们之前创建的数据结构中创建 pandas 序列对象。如果您已将 pandas 导入为 pd,则创建序列的函数简单为 pd.Series

  1. 初始化标签、列表和字典:

    labels = ['a','b','c']
    my_data = [10,20,30]
    array_1 = np.array(my_data)
    d = {'a':10,'b':20,'c':30}
    print ("Labels:", labels)
    print("My data:", my_data)
    print("Dictionary:", d)
    

    输出如下:

    Labels: ['a', 'b', 'c']
    My data: [10, 20, 30]
    Dictionary: {'a': 10, 'b': 20, 'c': 30}
    
  2. 使用以下命令将 pandas 导入为 pd

    import pandas as pd
    
  3. 使用以下命令从my_data列表创建一个序列:

    series_1=pd.Series(data=my_data)
    print(series_1)
    

    输出如下:

    0    10
    1    20
    2    30
    dtype: int64
    
  4. my_data列表创建一个序列,并使用以下命令添加labels

    series_2=pd.Series(data=my_data, index = labels)
    print(series_2)
    

    输出如下:

    a    10
    b    20
    c    30
    dtype: int64
    
  5. 然后,从 NumPy 数组创建一个序列,如下所示:

    series_3=pd.Series(array_1,labels)
    print(series_3)
    

    输出如下:

    a    10
    b    20
    c    30
    dtype: int32
    
  6. 从字典创建一个序列,如下所示:

    series_4=pd.Series(d)
    print(series_4)
    

    输出如下:

    a    10
    b    20
    c    30
    dtype: int64
    

练习 38:Pandas 序列和数据处理

pandas 系列对象可以存储多种类型的数据。这是构建更大表格的关键,其中多个系列对象堆叠在一起以创建类似数据库的实体:

  1. 使用以下命令使用数值数据创建 pandas 系列:

    print ("\nHolding numerical data\n",'-'*25, sep='')
    print(pd.Series(array_1))
    

    输出如下:

    Holding numerical data
    -------------------------
    0    10
    1    20
    2    30
    dtype: int32
    
  2. 使用以下命令使用标签创建 pandas 系列:

    print ("\nHolding text labels\n",'-'*20, sep='')
    print(pd.Series(labels))
    

    输出如下:

    Holding text labels
    --------------------
    0    a
    1    b
    2    c
    dtype: object
    
  3. 使用以下命令创建一个带有函数的 pandas 系列:

    print ("\nHolding functions\n",'-'*20, sep='')
    print(pd.Series(data=[sum,print,len]))
    

    输出如下:

    Holding functions
    --------------------
    0      <built-in function sum>
    1    <built-in function print>
    2      <built-in function len>
    dtype: object
    
  4. 使用以下命令使用字典创建 pandas 系列:

    print ("\nHolding objects from a dictionary\n",'-'*40, sep='')
    print(pd.Series(data=[d.keys, d.items, d.values]))
    

    输出如下:

    Holding objects from a dictionary
    ----------------------------------------
    0    <built-in method keys of dict object at 0x0000...
    1    <built-in method items of dict object at 0x000...
    2    <built-in method values of dict object at 0x00...
    dtype: object
    

练习 39:创建 Pandas DataFrame

pandas DataFrame 类似于 Excel 表格或关系数据库(SQL)表,它由三个主要组件组成:数据、索引(或行)和列。在底层,它是一堆 pandas 系列,这些系列本身又建立在 NumPy 数组之上。因此,我们之前关于 NumPy 数组的所有知识都适用于这里:

  1. 从一个二维数字矩阵创建一个简单的 DataFrame。首先,代码从均匀分布中抽取 20 个随机整数。然后,我们需要将其重塑为 (5,4) 的 NumPy 数组——5 行和 4 列:

    matrix_data = np.random.randint(1,10,size=20).reshape(5,4)
    
  2. 将行标签定义为 ('A','B','C','D','E'),列标签定义为 ('W','X','Y','Z')

    row_labels = ['A','B','C','D','E']
    column_headings = ['W','X','Y','Z']
    df = pd.DataFrame(data=matrix_data, index=row_labels,
                      columns=column_headings)
    
  3. 创建 DataFrame 的函数是 pd.DataFrame,它将在下面调用:

    print("\nThe data frame looks like\n",'-'*45, sep='')
    print(df) 
    

    样本输出如下:

    The data frame looks like
    ---------------------------------------------
       W  X  Y  Z
    A  6  3  3  3
    B  1  9  9  4
    C  4  3  6  9
    D  4  8  6  7
    E  6  6  9  1
    
  4. 使用以下命令通过一些整数列表的 Python 字典创建 DataFrame:

    d={'a':[10,20],'b':[30,40],'c':[50,60]}
    
  5. 将此字典作为数据参数传递给 pd.DataFrame 函数。传递一个行或索引列表。注意字典键变成了列名,而值被分配到多行中:

    df2=pd.DataFrame(data=d,index=['X','Y'])
    print(df2)
    

    输出如下:

        a   b   c
    X  10  30  50
    Y  20  40  60
    

    注意

    你最常遇到创建 pandas DataFrame 的方式是从本地磁盘或互联网上的文件中读取表格数据——CSV、文本、JSON、HTML、Excel 等等。我们将在下一章中介绍其中的一些。

练习 40:部分查看 DataFrame

在上一节中,我们使用 print(df) 来打印整个 DataFrame。对于大型数据集,我们只想打印数据的一部分。在这个练习中,我们将读取 DataFrame 的一部分:

  1. 执行以下代码以创建一个包含 25 行并填充随机数的 DataFrame:

    # 25 rows and 4 columns
    matrix_data = np.random.randint(1,100,100).reshape(25,4)
    column_headings = ['W','X','Y','Z']
    df = pd.DataFrame(data=matrix_data,columns=column_headings)
    
  2. 运行以下代码以仅查看 DataFrame 的前五行:

    df.head()
    

    样本输出如下(请注意,由于随机性,你的输出可能不同):

    图 3.1:DataFrame 的前五行

    图 3.1:DataFrame 的前五行

    默认情况下,head 只显示五行。如果你想看到任何特定数量的行,只需将其作为参数传递。

  3. 使用以下命令打印前八行:

    df.head(8)
    

    样本输出如下:

    图 3.2:DataFrame 的前八行

    图 3.2:DataFrame 的前八行

    就像 head 显示前几行一样,tail 显示最后几行。

  4. 使用 tail 命令打印 DataFrame,如下所示:

    df.tail(10)
    

    样本输出如下:

图 3.3:DataFrame 的最后十行

图 3.3:DataFrame 的最后十行

索引和切片列

从 DataFrame 中索引和切片列有两种方法。如下所示:

  • DOT 方法

  • 括号方法

DOT 方法适用于查找特定元素。括号方法直观且易于理解。在此方法中,您可以通过列的通用名称/标题访问数据。

以下代码说明了这些概念。在您的 Jupyter 笔记本中执行它们:

print("\nThe 'X' column\n",'-'*25, sep='')
print(df['X'])
print("\nType of the column: ", type(df['X']), sep='')
print("\nThe 'X' and 'Z' columns indexed by passing a list\n",'-'*55, sep='')
print(df[['X','Z']])
print("\nType of the pair of columns: ", type(df[['X','Z']]), sep='')

输出如下(此处显示截图,因为实际列很长):

图 3.4:'X' 列的行

这是显示列类型的输出:

图 3.5:'X' 列的类型

图 3.5:'X' 列的类型

这是通过传递列表索引 X 和 Z 列的输出:

图 3.6:'Y' 列的行

图 3.6:'Y' 列的行

这是显示列对类型的输出:

图 3.7:'Y' 列的类型

图 3.7:'Y' 列的类型

注意

对于多个列,对象变为 DataFrame。但对于单个列,它是一个 pandas 系列对象。

索引和切片行

在 DataFrame 中,可以使用以下方法进行行索引和切片:

  • 基于标签的 'loc' 方法

  • 基于索引的 'iloc' 方法

loc 方法直观且易于理解。在此方法中,您可以通过行的通用名称访问数据。另一方面,iloc 方法允许您通过它们的数值索引访问行。对于具有数千行的大表,这非常有用,尤其是在您想使用数值计数器在循环中遍历表时。以下代码说明了 iloc 的概念:

matrix_data = np.random.randint(1,10,size=20).reshape(5,4)
row_labels = ['A','B','C','D','E']
column_headings = ['W','X','Y','Z']
df = pd.DataFrame(data=matrix_data, index=row_labels,
                  columns=column_headings)
print("\nLabel-based 'loc' method for selecting row(s)\n",'-'*60, sep='')
print("\nSingle row\n")
print(df.loc['C'])
print("\nMultiple rows\n")
print(df.loc[['B','C']])
print("\nIndex position based 'iloc' method for selecting row(s)\n",'-'*70, sep='')
print("\nSingle row\n")
print(df.iloc[2])
print("\nMultiple rows\n")
print(df.iloc[[1,2]])

样本输出如下:

图 3.8:loc 和 iloc 方法的输出

图 3.8:loc 和 iloc 方法的输出

练习 41:创建和删除新列或行

数据清洗中最常见的任务之一是从 DataFrame 中创建或删除列或行。有时,您可能希望根据某些涉及现有列的数学运算或转换创建新列。这与操作数据库记录并在简单转换的基础上插入新列类似。我们在以下代码块中展示了这些概念:

  1. 使用以下代码片段创建新列:

    print("\nA column is created by assigning it in relation\n",'-'*75, sep='')
    df['New'] = df['X']+df['Z']
    df['New (Sum of X and Z)'] = df['X']+df['Z']
    print(df)
    

    样本输出如下:

    图 3.9:添加新列后的输出

    图 3.9:添加新列后的输出
  2. 使用 df.drop 方法删除列:

    print("\nA column is dropped by using df.drop() method\n",'-'*55, sep='')
    df = df.drop('New', axis=1) # Notice the axis=1 option, axis = 0 is #default, so one has to change it to 1
    print(df)
    

    样本输出如下:

    图 3.10:删除列后的输出

    图 3.10:删除列后的输出
  3. 使用 df.drop 方法删除特定行:

    df1=df.drop('A')
    print("\nA row is dropped by using df.drop method and axis=0\n",'-'*65, sep='')
    print(df1)
    

    以下是一个示例输出:

    图 3.11:删除行后的输出

    图 3.11:删除行后的输出

    放弃方法会创建 DataFrame 的副本,而不会更改原始 DataFrame。

  4. 通过将 inplace 参数设置为 True 来更改原始 DataFrame:

    print("\nAn in-place change can be done by making inplace=True in the drop method\n",'-'*75, sep='')
    df.drop('New (Sum of X and Z)', axis=1, inplace=True)
    print(df)
    

    以下是一个示例输出:

图 3.12:使用 inplace 参数后的输出

图 3.12:使用 inplace 参数后的输出

注意

所有常规操作都不是就地进行的,也就是说,它们不会影响原始 DataFrame 对象,而是返回原始数据的副本(或删除)。最后一段代码展示了如何使用 inplace=True 参数在现有 DataFrame 中进行更改。请注意,这种更改是不可逆的,应谨慎使用。

使用 NumPy 和 Pandas 进行统计和可视化

使用诸如 NumPy 和 pandas 这样的库的一个巨大优势是,有大量的内置统计和可视化方法可用,我们不需要搜索和编写新代码。此外,这些子程序大多数是用 C 或 Fortran 代码编写的(并且预编译),这使得它们执行速度极快。

复习基本描述性统计(以及用于可视化的 Matplotlib 库)

对于任何数据处理任务,从数据中提取基本描述性统计并创建一些简单的可视化/图表非常有用。这些图表通常是识别数据中的基本模式以及异常(如果有的话)的第一步。在任何统计分析中,描述性统计是第一步,随后是推断统计,它试图推断出可能生成数据的潜在分布或过程。

由于推断统计与数据科学管道的机器学习/预测建模阶段紧密相连,描述性统计自然与数据处理方面相关联。

描述性统计分析有两种主要方法:

  • 图形技术:条形图、散点图、折线图、箱线图、直方图等

  • 计算中心趋势和离散程度:平均值、中位数、众数、方差、标准差、范围等

在这个主题中,我们将展示如何使用 Python 完成这两个任务。除了 NumPy 和 pandas,我们还需要学习另一个优秀包的基础知识——matplotlib,这是 Python 中功能最强大且最灵活的可视化库。

练习 42:通过散点图介绍 Matplotlib

在这个练习中,我们将通过创建一些关于几个人的年龄、体重和身高数据的一些简单散点图来展示 matplotlib 的强大和简单性:

  1. 首先,我们定义简单的姓名、年龄、体重(以千克为单位)和身高(以厘米为单位)列表:

    people = ['Ann','Brandon','Chen','David','Emily','Farook',
              'Gagan','Hamish','Imran','Joseph','Katherine','Lily']
    age = [21,12,32,45,37,18,28,52,5,40,48,15]
    weight = [55,35,77,68,70,60,72,69,18,65,82,48]
    height = [160,135,170,165,173,168,175,159,105,171,155,158]
    
  2. 导入 matplotlib 中最重要的模块,称为 pyplot

    import matplotlib.pyplot as plt
    
  3. 创建年龄与体重之间的简单散点图:

    plt.scatter(age,weight)
    plt.show()
    

    输出如下:

    图 3.13:包含年龄和体重的散点图截图

    图 3.13:包含年龄和体重的散点图截图

    可以通过放大图形大小、自定义纵横比、添加带有适当字体大小的标题、添加带有自定义字体大小的 X 轴和 Y 轴标签、添加网格线、将 Y 轴限制在 0 到 100 之间、添加 X 和 Y 刻度标记、自定义散点图的颜色以及改变散点的大小来改进图表。

  4. 改进图表的代码如下:

    plt.figure(figsize=(8,6))
    plt.title("Plot of Age vs. Weight (in kgs)",fontsize=20)
    plt.xlabel("Age (years)",fontsize=16)
    plt.ylabel("Weight (kgs)",fontsize=16)
    plt.grid (True)
    plt.ylim(0,100)
    plt.xticks([i*5 for i in range(12)],fontsize=15)
    plt.yticks(fontsize=15)
    plt.scatter(x=age,y=weight,c='orange',s=150,edgecolors='k')
    plt.text(x=20,y=85,s="Weights after 18-20 years of age",fontsize=15)
    plt.vlines(x=20,ymin=0,ymax=80,linestyles='dashed',color='blue',lw=3)
    plt.legend(['Weight in kgs'],loc=2,fontsize=12)
    plt.show()
    

    输出如下:

图 3.14:显示年龄与体重关系的散点图截图

图 3.14:显示年龄与体重关系的散点图截图

观察以下内容:

  • 将一个元组(8,6)作为参数传递给图形大小。

  • Xticks内部使用列表推导式创建一个自定义的 5-10-15-…-55 列表。

  • plt.text()函数内部使用换行符(\n)来分割和分布文本为两行。

  • 在最后使用plt.show()函数。想法是持续添加各种图形属性(字体、颜色、坐标轴限制、文本、图例、网格等),直到你满意,然后使用一个函数显示图表。如果没有这个最后的函数调用,图表将不会显示。

统计度量定义 – 中心趋势和分散

中心趋势的度量是一个单一值,它试图通过确定数据集中数据的中心位置来描述这组数据。它们也被归类为汇总统计量:

  • 均值:均值是所有值的总和除以值的总数。

  • 中位数:中位数是中间值。它是将数据集分成两半的值。要找到中位数,将你的数据从小到大排序,然后找到上方和下方值数量相等的那个数据点。

  • 众数:众数是数据集中出现频率最高的值。在条形图中,众数是最高条。

通常,对于对称数据,均值是一个更好的度量,而对于具有偏斜(左重或右重)分布的数据,中位数是一个更好的度量。对于分类数据,你必须使用众数:

图 3.15:显示均值、中位数和众数的曲线截图

图 3.15:显示均值、中位数和众数的曲线截图

数据的分散程度是衡量数据集中的值可能偏离值平均值的程度。如果所有值都彼此接近,则分散度低;另一方面,如果某些或所有值与平均值(以及彼此)相差很大,则数据中存在很大的分散度:

  • 方差:这是最常见的分散度度量。方差是平均偏差平方的平均值。平方偏差确保了负偏差和正偏差不会相互抵消。

  • 标准差:由于方差是通过平方距离来产生的,其单位与原始数据不匹配。标准差是一种数学技巧,用于恢复等价性。它是方差的正平方根。

随机变量和概率分布

随机变量被定义为表示统计实验或过程结果的给定变量的值。

虽然听起来非常正式,但我们周围几乎所有可以测量的东西都可以被视为随机变量。

原因在于几乎所有自然、社会、生物和物理过程都是大量复杂过程的最终结果,我们无法了解这些基本过程的细节。我们能做的只是观察和测量最终结果。

我们周围典型的随机变量例子如下:

  • 一个国家的经济产出

  • 患者的血压

  • 工厂中化学过程的温度

  • 一个人在 Facebook 上的朋友数量

  • 一家公司的股票市场价格

这些值可以取任何离散或连续的值,并且遵循特定的模式(尽管模式可能会随时间变化)。因此,它们都可以被归类为随机变量。

什么是概率分布?

概率分布是一个函数,描述了随机变量可以假设的可能值的可能性。换句话说,变量的值基于潜在的概率分布而变化。

假设你前往一所学校,并测量随机选择的学生身高。身高在这里是一个随机变量的例子。当你测量身高时,你可以创建一个身高分布。这种类型的分布在你需要知道哪些结果最有可能、潜在值的分布范围以及不同结果的可能性时非常有用。

中心趋势和分散的概念适用于分布,并用于描述分布的性质和行为。

统计学家通常将所有分布分为两大类:

  • 离散分布

  • 连续分布

离散分布

离散概率函数也称为概率质量函数,可以假设离散的数值。例如,抛硬币和事件计数是离散函数。在抛硬币中,你只能得到正面或反面。同样,如果你每小时计算到达车站的火车数量,你可以计算 11 或 12 辆火车,但不能在两者之间。

一些显著的离散分布如下:

  • 二项分布用于模拟二元数据,如抛硬币

  • 泊松分布用于模拟计数数据,例如每小时图书馆图书借阅的数量

  • 均匀分布用于模拟具有相同概率的多个事件,例如掷骰子

连续分布

连续概率函数也被称为概率密度函数。如果一个变量可以在任意两个值之间取无限多个值,那么你就有了一个连续分布。连续变量通常是实数尺度上的测量,例如身高、体重和温度。

最著名的连续分布是正态分布,也称为高斯分布钟形曲线。这种对称分布适合广泛的现象,例如人类身高和智商分数。

正态分布与著名的68-95-99.7 规则相联系,该规则描述了如果数据遵循正态分布,那么数据落在平均值的 1、2 或 3 个标准差范围内的百分比。这意味着你可以快速查看一些样本数据,计算平均值和标准差,并可以有一个信心(不确定性的统计度量)认为任何未来的数据都将落在那些68%-95%-99.7%边界内。这个规则在工业、医学、经济学和社会科学中得到广泛应用:

图片

图 3.16:显示著名 68-95-99.7 规则正态分布的曲线

统计学和可视化中的数据处理

一个优秀的数据处理专业人员每天都会遇到令人眼花缭乱的各种数据源。正如我们之前解释的,由于众多复杂的子过程和相互作用的组合产生了这样的数据,它们都属于离散或连续随机变量的范畴。

如果所有这些数据继续被当作完全随机且没有任何形状或模式来处理,这将使数据处理人员或数据科学团队感到极其困难和困惑。必须为这些随机数据流提供一个正式的统计基础,而开始这一过程的最简单方法之一就是测量它们的描述性统计数据。

将数据流分配给特定的分布函数(或许多分布的组合)实际上是推断统计的一部分。然而,推断统计只有在描述性统计与测量数据模式的全部重要参数同时进行时才开始。

因此,作为数据科学流程的前线,数据处理必须处理测量和量化这些描述性统计数据。除了格式化和清理后的数据,数据处理人员的主要任务是向下一个分析团队成员移交这些度量(有时还有相应的图表)。

绘图可视化 帮助数据整理团队识别传入数据流中的潜在异常值和不符合的数据,并帮助他们采取适当的行动。我们将在下一章中看到一些这样的示例,我们将通过创建散点图或直方图来识别异常数据点,并对其进行插补或删除。

使用 NumPy 和 Pandas 在 DataFrame 上计算基本描述性统计

现在我们已经对 NumPy、pandas 和 matplotlib 有了一些基本了解,我们可以探索一些与这些库相关的附加主题,例如如何将它们结合起来进行高级数据生成、分析和可视化。

使用 NumPy 生成随机数

NumPy 提供了一系列令人眼花缭乱的随机数生成实用函数,所有这些函数都对应于各种统计分布,如均匀分布、二项分布、高斯正态分布、Beta/Gamma 分布和卡方分布。这些函数中的大多数都非常有用,在高级统计数据挖掘和机器学习任务中出现了无数次。强烈建议所有学习这本书的学生都要掌握它们。

在这里,我们将讨论三个可能在数据整理任务中派上用场的最重要分布——均匀分布、二项分布和高斯正态分布。我们的目标是展示一些简单的函数调用示例,用户需要时可以生成一个或多个随机数/数组。

注意

当学生使用这些函数时,每个学生的结果都可能不同,因为它们应该是随机的。

练习 43:从均匀分布生成随机数

在这个练习中,我们将从均匀分布生成随机数:

  1. 生成一个介于 110 之间的随机整数:

    x = np.random.randint(1,10)
    print(x)
    

    样本输出如下(你的输出可能不同):

    1
    
  2. 使用 size=1 作为参数生成介于 1 和 10 之间的随机整数。它生成一个大小为 1 的 NumPy 数组:

    x = np.random.randint(1,10,size=1)
    print(x)
    

    样本输出如下(你的输出可能因随机抽取而不同):

    [8]
    

    因此,我们可以轻松编写代码来生成掷骰子(一个正常的六面骰子)10 次的结果。

    如果我们脱离整数,生成一些实数呢?比如说,我们想要生成 20 名成年人的体重(以千克为单位)的人工数据,并且我们可以测量精确到小数点后两位的体重。

  3. 使用以下命令生成小数数据:

    x = 50+50*np.random.random(size=15)
    x= x.round(decimals=2)
    print(x)
    

    样本输出如下:

    [56.24 94.67 50.66 94.36 77.37 53.81 61.47 71.13 59.3  65.3  63.02 65.
     58.21 81.21 91.62]
    

    我们不仅限于使用一维数组。

  4. 生成并显示一个介于 01 之间的随机数的 3x3 矩阵:

    x = np.random.rand(3,3)
    print(x)
    

    样本输出如下(请注意,你的具体输出可能因随机性而不同):

    [[0.99240105 0.9149215  0.04853315]
     [0.8425871  0.11617792 0.77983995]
     [0.82769081 0.57579771 0.11358125]]
    

练习 44:从二项分布生成随机数并绘制条形图

二项分布是在特定次数的试验中,以预定的概率或机会获得特定成功次数的概率分布。

最明显的例子是抛硬币。一个公平的硬币可能有相等的机会出现正面或反面,但一个不公平的硬币可能有更多机会出现正面或反面。我们可以在 NumPy 中以以下方式模拟抛硬币。

假设我们有一个偏心的硬币,其正面朝上的概率为0.6。我们抛掷这个硬币十次,并记录每次出现的正面次数。那是一次试验或实验。现在,我们可以重复这个实验(10 次抛硬币)任意多次,比如 8 次。每次,我们记录正面次数:

  1. 可以使用以下代码来模拟实验:

    x = np.random.binomial(10,0.6,size=8)
    print(x)
    

    样本输出如下(注意,由于随机性,您的具体输出可能不同):

    [6 6 5 6 5 8 4 5]
    
  2. 使用柱状图绘制结果:

    plt.figure(figsize=(7,4))
    plt.title("Number of successes in coin toss",fontsize=16)
    plt.bar(left=np.arange(1,9),height=x)
    plt.xlabel("Experiment number",fontsize=15)
    plt.ylabel("Number of successes",fontsize=15)
    plt.show()
    

    样本输出如下:

图 3.17:显示二项分布和柱状图的图形截图

图 3.17:显示二项分布和柱状图的图形截图

练习 45:从正态分布生成随机数和直方图

我们在上一个主题中讨论了正态分布,并提到它是最重要的概率分布,因为当样本数量大时,许多自然、社会和生物数据都紧密遵循这种模式。NumPy 提供了一个简单的方法来生成与这种分布相对应的随机数:

  1. 使用以下命令从一个正态分布中抽取一个样本:

    x = np.random.normal()
    print(x)
    

    样本输出如下(注意,由于随机性,您的具体输出可能不同):

    -1.2423774071573694
    

    我们知道正态分布由两个参数定义——均值(µ)和标准差(σ)。事实上,这个特定函数的默认值是µ = 0.0 和σ = 1.0。

    假设我们知道某所学校青少年(12-16 岁)学生的身高呈正态分布,平均身高为 155 厘米,标准差为 10 厘米。

  2. 使用以下命令生成 100 名学生的直方图:

    # Code to generate the 100 samples (heights)
    heights = np.random.normal(loc=155,scale=10,size=100)
    # Plotting code
    #-----------------------
    plt.figure(figsize=(7,5))
    plt.hist(heights,color='orange',edgecolor='k')
    plt.title("Histogram of teen aged students's height",fontsize=18)
    plt.xlabel("Height in cm",fontsize=15)
    plt.xticks(fontsize=15)
    plt.yticks(fontsize=15)
    plt.show()
    

    样本输出如下:

图 3.18:青少年学生身高的直方图

注意使用loc参数来表示平均值(=155)和scale参数来表示标准差(=10)。大小参数设置为 100,以便生成可能样本。

练习 46:从 DataFrame 计算描述性统计

回想一下我们为绘图练习定义的ageweightheight参数。让我们将这些数据放入 DataFrame 中,以计算它们的各种描述性统计。

与 pandas DataFrame 一起工作的最好部分是它有一个内置的实用函数,可以单行代码显示所有这些描述性统计。它是通过使用describe方法来做到这一点的:

  1. 使用以下命令使用可用的序列数据构建一个字典:

    people_dict={'People':people,'Age':age,'Weight':weight,'Height':height}
    people_df=pd.DataFrame(data=people_dict)
    people_df
    

    输出如下:

    图 3.19:创建的字典输出
  2. 通过执行以下命令找出 DataFrame 的行数和列数:

    print(people_df.shape)
    

    输出结果如下:

    (12, 4)
    
  3. 通过执行以下命令获取简单的count(任何列都可以用于此目的):

    print(people_df['Age'].count())
    

    输出结果如下:

    12
    
  4. 使用以下命令计算年龄的总和:

    print(people_df['Age'].sum())
    

    输出结果如下:

    353
    
  5. 使用以下命令计算平均年龄:

    print(people_df['Age'].mean())
    

    输出结果如下:

    29.416666666666668
    
  6. 使用以下命令计算中值重量:

    print(people_df['Weight'].median())
    

    输出结果如下:

    66.5
    
  7. 使用以下命令计算最大高度:

    print(people_df['Height'].max())
    

    输出结果如下:

    175
    
  8. 使用以下命令计算重量的标准差:

    print(people_df['Weight'].std())
    

    输出结果如下:

    18.45120510148239
    

    注意我们是如何直接从 DataFrame 对象调用统计函数的。

  9. 要计算百分位数,我们可以从 NumPy 中调用一个函数并将特定的列(一个 pandas 序列)传递给它。例如,要计算年龄分布的 75 百分位数和 25 百分位数及其差(称为四分位距),请使用以下代码:

    pcnt_75 = np.percentile(people_df['Age'],75)
    pcnt_25 = np.percentile(people_df['Age'],25)
    print("Inter-quartile range: ",pcnt_75-pcnt_25)
    

    输出结果如下:

    Inter-quartile range:  24.0
    
  10. 使用describe命令来查找 DataFrame 的详细描述:

    print(people_df.describe())
    

    输出结果如下:

图 3.20:使用 describe 方法得到的 DataFrame 输出

图 3.20:使用 describe 方法得到的 DataFrame 输出

注意

此函数仅适用于包含数值数据的列。它对非数值列没有影响,例如,DataFrame 中的 People。

练习 47:内置绘图实用工具

DataFrame 还包含内置的绘图实用工具,这些实用工具围绕 matplotlib 函数创建,并创建基本数值数据的图表:

  1. 使用hist函数找出重量的直方图:

    people_df['Weight'].hist()
    plt.show()
    

    输出结果如下:

    图 3.21:重量的直方图

    图 3.21:重量的直方图
  2. 使用以下命令直接从 DataFrame 创建简单的散点图,以绘制重量与身高之间的关系:

    people_df.plot.scatter('Weight','Height',s=150,
    c='orange',edgecolor='k')
    plt.grid(True)
    plt.title("Weight vs. Height scatter plot",fontsize=18)
    plt.xlabel("Weight (in kg)",fontsize=15)
    plt.ylabel("Height (in cm)",fontsize=15)
    plt.show()
    

    输出结果如下:

图 3.22:重量与身高的散点图

注意

你可以尝试在这个函数调用周围使用 matplotlib 的常规方法来使你的图表更美观。

活动 5:从 CSV 文件生成统计数据

假设你正在处理著名的波士顿房价(从 1960 年)数据集。这个数据集在机器学习社区中很有名。可以制定许多回归问题,并且可以在该数据集上运行机器学习算法。你将通过将其作为 pandas DataFrame 读取来执行基本的数据整理活动(包括绘制一些趋势)。

注意

pandas 读取 CSV 文件的函数是read_csv

这些步骤将帮助你完成此活动:

  1. 加载必要的库。

  2. 从本地目录中读取 Boston 住房数据集(以.csv文件形式给出)。

  3. 检查前 10 条记录。找出记录总数。

  4. 创建一个较小的 DataFrame,其中不包含CHASNOXBLSTAT列。

  5. 检查你刚刚创建的新 DataFrame 的最后七条记录。

  6. 绘制新 DataFrame 中所有变量(列)的直方图。

  7. 使用for循环一次性绘制所有图表。尝试为每个图表添加一个独特的标题。

  8. 创建犯罪率与价格之间的散点图。

  9. 使用log10(crime)price进行绘图。

  10. 计算一些有用的统计数据,例如每套住宅的平均房间数、中位数年龄、平均距离五个波士顿就业中心以及价格低于$20,000 的房屋百分比。

    注意

    这个活动的解决方案可以在第 292 页找到。

摘要

在本章中,我们首先介绍了 NumPy 数组的基础知识,包括如何创建它们及其基本属性。我们讨论并展示了 NumPy 数组是如何优化向量化的元素级操作的,以及它与常规 Python 列表的不同之处。然后,我们转向练习 NumPy 数组的各种操作,如索引、切片、过滤和重塑。我们还涵盖了特殊的一维和二维数组,例如零数组、一数组、单位矩阵和随机数组。

在本章的第二大主题中,我们首先介绍了 pandas 系列对象,然后迅速转向一个至关重要的对象——pandas DataFrame。它与 Excel、MATLAB 或数据库标签页类似,但具有许多用于数据处理的有用属性。我们演示了 DataFrame 的一些基本操作,例如索引、子集、行和列的添加和删除。

接下来,我们介绍了使用 matplotlib 进行绘图的基础知识,matplotlib 是最广泛使用和最受欢迎的 Python 可视化库。除了绘图练习,我们还回顾了描述性统计(如集中趋势和离散程度度量)和概率分布(如均匀分布、二项分布和正态分布)的概念。

在下一章中,我们将介绍 pandas DataFrame 的更多高级操作,这些操作在日常数据处理工作中非常有用。

第五章:第四章

深入学习使用 Python 进行数据整理

学习目标

到本章结束时,你将能够:

  • 在 pandas DataFrame 上执行子集、过滤和分组操作

  • 从 DataFrame 应用布尔过滤和索引以选择特定元素

  • 在 pandas 中执行与 SQL 命令类似的 JOIN 操作

  • 识别缺失或损坏的数据,并选择删除或应用插补技术处理缺失或损坏的数据

在本章中,我们将详细了解 pandas DataFrame。

简介

在本章中,我们将学习涉及 pandas DataFrame 和 NumPy 数组的几个高级操作。完成本章的详细活动后,你将处理真实数据集并理解数据整理的过程。

子集、过滤和分组

数据整理最重要的方面之一是从各种来源涌入组织或商业实体的数据洪流中精心整理数据。大量的数据并不总是好事;相反,数据需要是有用且高质量的,才能在数据科学管道的下游活动中有效使用,例如机器学习和预测模型构建。此外,一个数据源可以用于多个目的,这通常需要数据整理模块处理不同的数据子集。然后这些数据被传递到单独的分析模块。

例如,假设你正在对美国州级经济产出进行数据整理。这是一个相当常见的场景,一个机器学习模型可能需要大型和人口众多的州(如加利福尼亚州、德克萨斯州等)的数据,而另一个模型则要求为小型和人口稀少的州(如蒙大拿州或北达科他州)处理的数据。作为数据科学流程的前线,数据整理模块有责任满足这两个机器学习模型的要求。因此,作为一名数据整理工程师,你必须在处理并生成最终输出之前,根据州的(人口)过滤和分组数据。

此外,在某些情况下,数据源可能存在偏差,或者测量偶尔会损坏传入的数据。尝试仅过滤无错误、高质量的数据用于下游建模是个好主意。从这些例子和讨论中可以看出,过滤和分组/分桶数据是任何从事数据整理任务的工程师必备的技能。让我们继续学习 pandas 中的一些这些技能。

练习 48:从 Excel 文件中加载和检查超市的销售数据

在这个练习中,我们将加载并检查一个 Excel 文件。

  1. 要将 Excel 文件读入 pandas,你需要在你的系统上安装一个名为 xlrd 的小型包。如果你在这个书的 Docker 容器内部工作,那么这个包可能在你下次启动容器时不可用,你必须遵循相同的步骤。使用以下代码安装 xlrd 包:

    !pip install xlrd
    
  2. 使用简单的 pandas 方法 read_excel 从 GitHub 加载 Excel 文件:

    import numpy as np
    import pandas as pd
    import matplotlib.pyplot as plt
    df = pd.read_excel("Sample - Superstore.xls")
    df.head()
    

    检查所有列,看它们是否对分析有用:

    图 4.1 Excel 文件在 DataFrame 中的输出

    图 4.1 Excel 文件在 DataFrame 中的输出

    检查文件时,我们可以看到第一列,称为 行 ID,并不是很有用。

  3. 使用 drop 方法从 DataFrame 中完全删除此列:

    df.drop('Row ID',axis=1,inplace=True)
    
  4. 检查新创建数据集的行数和列数。我们将在这里使用 shape 函数:

    df.shape
    

    输出如下:

    (9994, 20)
    

    我们可以看到数据集有 9,994 行和 20 列。

子集 DataFrame

子集涉及根据特定列和行提取部分数据,以满足业务需求。假设我们只对以下信息感兴趣:客户 ID、客户姓名、城市、邮政编码和销售额。为了演示目的,让我们假设我们只对 5 条记录感兴趣 - 第 5-9 行。我们可以使用一行 Python 代码来子集 DataFrame,只提取这么多信息。

使用 loc 方法通过列名和行索引来索引数据集:

df_subset = df.loc[
    [i for i in range(5,10)],
    ['Customer ID','Customer Name','City','Postal Code',
     'Sales']]
df_subset

输出如下:

图 4.2:按列名索引的 DataFrame

图 4.2:按列名索引的 DataFrame

我们需要传递两个参数给 loc 方法 - 一个用于指示行,另一个用于指示列。这些应该是 Python 列表。

对于行,我们必须传递一个列表 [5,6,7,8,9],但不是明确地写出,我们使用列表推导式,即 [i for i in range(5,10)]

因为我们所感兴趣的列不是连续的,我们不能仅仅放置一个连续的范围,需要传递一个包含特定名称的列表。所以,第二个参数只是一个包含特定列名的简单列表。

该数据集展示了根据业务需求对 DataFrame 进行子集的基本概念。

一个示例用例:确定销售额和利润的统计数据

这一小节展示了子集的典型用例。假设我们想要计算记录 100-199 的销售额和利润的描述性统计(均值、中位数、标准差等)。这就是子集如何帮助我们实现这一点的方式:

df_subset = df.loc[[i for i in range(100,200)],['Sales','Profit']]
df_subset.describe()

输出如下:

图 4.3 数据的描述性统计输出

图 4.3 数据的描述性统计输出

此外,我们可以从最终数据中创建销售额和利润数字的箱线图。

我们简单地提取了 100-199 条记录,并对其运行describe函数,因为我们不想处理所有数据!对于这个特定的问题,我们只对销售额和利润数字感兴趣,因此我们不应该走捷径,对全部数据进行描述。对于现实生活中的数据集,行和列的数量可能经常达到数百万,我们不想计算数据整理任务中未要求的数据。我们总是旨在子集化需要处理的确切数据,并在该部分数据上运行统计或绘图函数:

图 4.4:销售额和利润的箱线图

图 4.4:销售额和利润的箱线图

练习 49:唯一函数

在继续使用过滤方法之前,让我们快速偏离一下,探索一个超级有用的函数,称为unique。正如其名所示,此函数用于快速扫描数据并提取列或行中的唯一值。

在加载超级商店销售数据后,你会注意到有一些像“国家”、“州”和“城市”这样的列。一个自然的问题将是询问数据集中有多少个国家/州/城市:

  1. 使用一行简单的代码提取数据库中包含信息的国家/州/城市,如下所示:

    df['State'].unique()
    

    输出如下:

    图 4.5:数据集中存在的不同状态

    图 4.5:数据集中存在的不同状态

    你将看到所有在数据集中存在的州的列表。

  2. 使用nunique方法来计数唯一值的数量,如下所示:

    df['State'].nunique()
    

    输出如下:

    49
    

    这对于此数据集返回 49。所以,在美国的 50 个州中,有一个州没有出现在这个数据集中。

同样,如果我们对国家列运行此函数,我们将得到一个只有一个元素的数组,United States。立即,我们可以看到我们根本不需要保留国家列,因为该列中除了所有条目都相同之外,没有有用的信息。这就是一个简单的函数如何帮助我们决定删除整个列——也就是说,删除 9,994 条不必要的数据!

条件选择和布尔过滤

通常,我们不想处理整个数据集,而只想选择满足特定条件的部分数据集。这可能是任何数据整理任务中最常见的用例。

在我们的超级商店销售数据集的背景下,考虑以下可能从业务分析团队的日常活动中出现的一些常见问题:

  • 加利福尼亚的平均销售额和利润数字是多少?

  • 哪些州的销售额最高和最低?

  • 哪个消费者销售/利润的变异性最大?

  • 在销售额最高的前 5 个州中,哪种运输方式和产品类别最受欢迎?

可以给出无数个例子,其中业务分析团队或高管团队希望从满足某些特定标准的数据子集中提取洞察。

如果你有任何 SQL 的先验经验,你会知道这类问题需要相当复杂的 SQL 查询编写。还记得 WHERE 子句吗?

我们将向您展示如何使用条件子集和布尔过滤来回答这类问题。

首先,我们需要理解布尔索引的关键概念。这个过程本质上接受一个条件表达式作为参数,并返回一个布尔数据集,其中TRUE值出现在条件满足的地方。以下代码展示了简单示例。为了演示目的,我们对一个包含 10 条记录和 3 个列的小数据集进行了子集操作:

df_subset = df.loc[[i for i in range (10)],['Ship Mode','State','Sales']]
df_subset

输出如下:

图 4.6:样本数据集

图 4.6:样本数据集

现在,如果我们只想知道销售额高于$100 的记录,我们可以编写以下代码:

df_subset>100

这产生了以下布尔 DataFrame:

图 4.7:销售额高于 100 美元的记录

图 4.7:销售额高于 100 美元的记录

注意Sales列中的 True 和 False 条目。Ship ModeState列的值没有受到影响,因为比较的是数值量,而原始 DataFrame 中唯一的数值列是Sales

现在,让我们看看如果我们将这个布尔 DataFrame 作为索引传递给原始 DataFrame 会发生什么:

df_subset[df_subset>100]

输出如下:

图 4.8:将布尔 DataFrame 作为索引传递给原始 DataFrame 后的结果

图 4.8:将布尔 DataFrame 作为索引传递给原始 DataFrame 后的结果

NaN 值来自前面的代码尝试仅使用 TRUE 索引(在布尔 DataFrame 中)创建 DataFrame 的事实。

在布尔 DataFrame 中为 TRUE 的值被保留在最终输出 DataFrame 中。

程序在数据不可用(因为它们由于销售额小于$100 而被丢弃)的行中插入了NaN值。

现在,我们可能不想与这个结果 DataFrame Sales > $100一起工作。我们可以通过仅传递Sales列来实现这一点:

df_subset[df_subset['Sales']>100]

这产生了预期的结果:

图 4.9:移除 NaN 值后的结果

图 4.9:移除 NaN 值后的结果

我们不仅限于只涉及数字量的条件表达式。让我们尝试提取不涉及科罗拉多州的销售额较高的值(> $100)。

我们可以编写以下代码来完成这项任务:

df_subset[(df_subset['State']!='Colorado') & (df_subset['Sales']>100)]

注意字符串条件的使用。在这个表达式中,我们通过&运算符连接了两个条件。两个条件都必须用括号括起来。

第一个条件表达式简单地匹配 State 列中的条目与字符串 Colorado,并相应地分配 TRUE/FALSE。第二个条件与之前相同。通过 & 运算符连接在一起,它们仅提取 State 不是 ColoradoSales 大于 $100 的行。我们得到以下结果:

图片

图 4.10:State 不是加利福尼亚州且 Sales 大于 $100 的结果

注意

虽然理论上,你可以使用单个表达式和 &(逻辑与)和 |(逻辑或)运算符构建复杂的条件,但建议创建具有有限条件表达式的中间布尔 DataFrame,并逐步构建最终的 DataFrame。这使代码易于阅读和扩展。

练习 50:设置和重置索引

有时,我们可能需要重置或消除 DataFrame 的默认索引并分配新列作为索引:

  1. 使用以下命令创建 matrix_datarow_labelscolumn_headings 函数:

    matrix_data = np.matrix(
        '22,66,140;42,70,148;30,62,125;35,68,160;25,62,152')
    row_labels = ['A','B','C','D','E']
    column_headings = ['Age', 'Height', 'Weight']
    
  2. 使用 matrix_datarow_labelscolumn_headings 函数创建一个 DataFrame:

    df1 = pd.DataFrame(data=matrix_data, 
                       index=row_labels,
                       columns=column_headings)
    print("\nThe DataFrame\n",'-'*25, sep='')
    print(df1)
    

    输出如下:

    图片

    图 4.11:原始 DataFrame
  3. 如此重置索引:

    print("\nAfter resetting index\n",'-'*35, sep='')
    print(df1.reset_index())
    

    图片

    图 4.12:重置索引后的 DataFrame
  4. drop 设置为 True 来重置索引,如下所示:

    print("\nAfter resetting index with 'drop' option TRUE\n",'-'*45, sep='')
    print(df1.reset_index(drop=True))
    

    图 4.13:设置 drop 选项为 true 后重置索引的 DataFrame

    图 4.13:设置 drop 选项为 true 后重置索引的 DataFrame
  5. 使用以下命令添加新列:

    print("\nAdding a new column 'Profession'\n",'-'*45, sep='')
    df1['Profession'] = "Student Teacher Engineer Doctor Nurse".split()
    print(df1)
    

    输出如下:

    图片

    图 4.14:添加名为 Profession 的新列后的 DataFrame
  6. 现在,使用以下代码将 Profession 列设置为索引:

    print("\nSetting 'Profession' column as index\n",'-'*45, sep='')
    print (df1.set_index('Profession'))
    

    输出如下:

图片

图 4.15:将职业设置为索引后的 DataFrame

练习 51:分组方法

分组涉及以下步骤之一或多个:

  • 根据某些标准将数据拆分到组中

  • 对每个组独立应用函数

  • 将结果组合到数据结构中

在许多情况下,我们可以将数据集分成组并对这些组进行操作。在应用步骤中,我们可能希望执行以下操作之一:

  • 聚合:对每个组计算汇总统计量(或统计量) - 总和、平均值等

  • 转换:执行特定组的计算并返回类似索引的对象 - z 转换或用值填充缺失数据

  • 过滤:根据组内计算评估 TRUE 或 FALSE 来丢弃少量组

当然,这个 GroupBy 对象有一个描述方法,它以 DataFrame 的形式生成汇总统计信息。

GroupBy不仅限于单个变量。如果你传递多个变量(作为一个列表),那么你将得到一个本质上类似于 Excel 中的数据透视表的结构。以下是一个例子,我们将整个数据集(快照仅显示部分视图)中的所有州和城市分组在一起。

注意

对于那些之前使用过基于 SQL 的工具的人来说,名称GroupBy应该相当熟悉。

  1. 使用以下命令创建一个 10 条记录的子集:

    df_subset = df.loc[[i for i in range (10)],['Ship Mode','State','Sales']]
    
  2. 使用以下命令创建一个 pandas DataFrame,如下所示:

    byState = df_subset.groupby('State')
    
  3. 使用以下命令按州计算平均销售额:

    print("\nGrouping by 'State' column and listing mean sales\n",'-'*50, sep='')
    print(byState.mean())
    

    输出如下:

    图 4.16:按列表平均销售额分组州后的输出

    图 4.16:按列表平均销售额分组州后的输出
  4. 使用以下命令计算按州的总销售额:

    print("\nGrouping by 'State' column and listing total sum of sales\n",'-'*50, sep='')
    print(byState.sum())
    

    输出如下:

    图 4.17:按列表销售额总和分组州后的输出

    图 4.17:按列表销售额总和分组州后的输出
  5. 对 DataFrame 进行特定州的子集处理并显示统计信息:

    pd.DataFrame(byState.describe().loc['California'])
    

    输出如下:

    图 4.18:检查特定州的统计信息

    图 4.18:检查特定州的统计信息
  6. 使用Ship Mode属性执行类似的汇总:

    df_subset.groupby('Ship Mode').describe().loc[['Second Class','Standard Class']]
    

    输出如下:

    图 4.19:通过汇总 Ship Mode 属性检查销售

    图 4.19:通过汇总 Ship Mode 属性检查销售

    注意 pandas 是如何首先按State分组,然后按每个州下的城市进行分组的。

  7. 使用以下命令显示每个州每个城市的销售完整汇总统计信息——全部通过两行代码完成:

    byStateCity=df.groupby(['State','City'])
    byStateCity.describe()['Sales']
    

    输出如下:

图 4.20:检查销售汇总统计信息

图 4.20:检查销售汇总统计信息

检测异常值和处理缺失值

异常值检测和处理缺失值属于数据质量检查的微妙艺术。建模或数据挖掘过程本质上是一系列复杂的计算,其输出质量很大程度上取决于输入数据的质量和一致性。维护和监控这种质量的责任通常落在数据整理团队的肩上。

除了明显的数据质量问题外,缺失数据有时会对下游的机器学习(ML)模型造成破坏。一些 ML 模型,如贝叶斯学习,对异常值和缺失数据具有内在的鲁棒性,但像决策树和随机森林这样的常用技术由于这些技术的基本分割策略依赖于单个数据点而不是数据簇,因此存在处理缺失数据的问题。因此,在将数据传递给这样的 ML 模型之前,几乎总是必须对缺失数据进行插补。

异常值检测是一门微妙的艺术。通常,没有关于异常值的普遍认同的定义。从统计学的角度来看,一个落在某个范围之外的数据点可能经常被归类为异常值,但为了应用这个定义,你需要对数据内在统计分布的性质和参数有一个相当高的确定性。这需要大量的数据来建立这种统计确定性,即使如此,异常值可能不仅仅是不重要的噪声,而是更深层次线索的提示。让我们以一家美国快餐连锁餐厅的一些虚构销售数据为例。如果我们想将每日销售数据建模为时间序列,我们会观察到数据在四月中旬某处出现异常峰值:

图 4.21:一家美国快餐连锁餐厅的虚构销售数据

一个优秀的数据科学家或数据整理员应该对这一数据点产生好奇心,而不仅仅是由于它超出了统计范围就拒绝它。在实际情况中,当天的销售额之所以大幅上升,是因为一个不寻常的原因。因此,数据是真实的。但仅仅因为数据是真实的,并不意味着它是有用的。在最终目标是构建一个平滑变化的时间序列模型的情况下,这个数据点不应该产生影响,应该被拒绝。但这里的关键是我们不能不关注这些异常值就拒绝它们。

因此,关键在于在数百万数据流中系统及时地检测异常值,或者在从基于云的存储中读取数据时。在这个主题中,我们将快速浏览一些用于检测异常值的基本统计测试和一些用于填充缺失数据的基本插补技术。

Pandas 中的缺失值

用于检测缺失值的最有用函数之一是isnull。在这里,我们有一个名为df_missingDataFrame的快照(部分从我们正在处理的大型超市 DataFrame 中采样)并包含一些缺失值:

图 4.22:包含缺失值的 DataFrame

图 4.22:包含缺失值的 DataFrame

现在,如果我们简单地运行以下代码,我们将得到一个与原始数据集大小相同的 DataFrame,其中布尔值为 TRUE 表示遇到NaN的地方。因此,测试 DataFrame 的任何行或列中是否存在任何NaN/缺失值是简单的。你只需要添加这个布尔 DataFrame 的特定行和列。如果结果大于零,那么你就知道有一些 TRUE 值(因为这里的 FALSE 表示为 0,TRUE 表示为 1),相应地也有一些缺失值。尝试以下代码片段:

df_missing=pd.read_excel("Sample - Superstore.xls",sheet_name="Missing")
df_missing

输出如下:

图 4.23:包含 Excel 值的 DataFrame

图 4.23:包含 Excel 值的 DataFrame

在 DataFrame 上使用isnull函数并观察结果:

df_missing.isnull()

图 4.24:突出显示的缺失值输出

图 4.24:突出显示的缺失值输出

这是一个检测、计数和打印 DataFrame 每一列中缺失值的简单代码示例:

for c in df_missing.columns:
    miss = df_missing[c].isnull().sum()
    if miss>0:
        print("{} has {} missing value(s)".format(c,miss))
    else:
        print("{} has NO missing value!".format(c))

此代码扫描 DataFrame 的每一列,调用 isnull 函数,并将返回的对象(在这种情况下是一个 pandas Series 对象)求和以计算缺失值的数量。如果缺失值大于零,则相应地打印出消息。输出如下所示:

图 4.25:计数缺失值的输出

图 4.25:计数缺失值的输出

练习 52:使用 fillna 填充缺失值

要处理缺失值,您应该首先寻找方法不是完全删除它们,而是以某种方式填充它们。fillna 方法是执行此任务在 pandas DataFrame 上的一个有用函数。fillna 方法可能适用于字符串数据,但不适用于销售或利润等数值列。因此,我们应该将固定字符串替换限制在仅基于文本的非数值列上。Padffill 函数用于向前填充数据,即从序列的前一个数据复制。

可以使用 mean 函数使用两个值的平均值进行填充:

  1. 使用以下命令使用字符串 FILL 填充所有缺失值:

    df_missing.fillna('FILL')
    

    输出如下:

    图 4.26:缺失值被替换为 FILL

    图 4.26:缺失值被替换为 FILL
  2. 使用以下命令使用字符串 FILL 填充指定的列:

    df_missing[['Customer','Product']].fillna('FILL')
    

    输出如下:

    图 4.27:指定的列被替换为 FILL

    注意

    在所有这些情况下,函数都在原始 DataFrame 的副本上工作。因此,如果您想使更改永久,必须将这些函数返回的 DataFrame 赋值给原始 DataFrame 对象。

  3. 使用以下命令通过 pad 或 backfill 填充值:

    df_missing['Sales'].fillna(method='ffill')
    
  4. 使用 backfillbfill 向后填充,即从序列中的下一个数据复制:

    df_missing['Sales'].fillna(method='bfill')
    

    图 4.28:使用前向填充和后向填充填充缺失数据

    图 4.28:使用前向填充和后向填充填充缺失数据
  5. 您也可以使用 DataFrame 的平均值函数进行填充。例如,我们可能希望使用平均销售额填充销售中的缺失值。以下是我们可以这样做的方式:

    df_missing['Sales'].fillna(df_missing.mean()['Sales'])
    

图 4.29:使用平均值填充缺失数据

练习 53:使用 dropna 删除缺失值

此函数用于简单地删除包含 NaN/缺失值的行或列。然而,这里涉及一些选择。

如果轴参数设置为 0,则删除包含缺失值的行;如果轴参数设置为 1,则删除包含缺失值的列。如果 NaN 值不超过一定百分比,这些参数对于我们不希望删除特定行/列是有用的。

对于 dropna() 方法有用的两个参数如下:

  • how 参数确定当我们至少有一个 NaN 或所有 NaN 时,是否从 DataFrame 中删除行或列

  • thresh 参数要求保留许多非 NaN 值以保留行/列

  1. 要将轴参数设置为 0 并删除所有缺失行,请使用以下命令:

    df_missing.dropna(axis=0)
    
  2. 要将轴参数设置为 1 并删除所有缺失行,请使用以下命令:

    df_missing.dropna(axis=1)
    

    图 4.30:删除行或列以处理缺失数据

    图 4.30:删除行或列以处理缺失数据
  3. 将 axis 设置为 1 和 thresh 设置为 10 的值删除:

    df_missing.dropna(axis=1,thresh=10)
    

    输出如下:

图 4.31:使用 axis=1 和 thresh=10 删除值的 DataFrame

图 4.31:使用 axis=1 和 thresh=10 删除值的 DataFrame

所有这些方法都在临时副本上工作。要永久更改,您必须设置 inplace=True 或将结果分配给原始 DataFrame,即覆盖它。

使用简单统计测试进行异常值检测

正如我们已经讨论过的,数据集中的异常值可能由许多因素以多种方式产生:

  • 数据输入错误

  • 实验误差(与数据提取相关的)

  • 由于噪声或仪器故障导致的测量误差

  • 数据处理错误(由于编码错误导致的数据操作或突变)

  • 抽样误差(从错误或各种来源提取或混合数据)

无法确定一个通用的异常值检测方法。在这里,我们将向您展示一些使用标准统计测试对数值数据进行的一些简单技巧。

箱线图可能显示异常值。通过以下方式将两个销售额值设置为负数:

df_sample = df[['Customer Name','State','Sales','Profit']].sample(n=50).copy()
df_sample['Sales'].iloc[5]=-1000.0
df_sample['Sales'].iloc[15]=-500.0

要绘制箱线图,请使用以下代码:

df_sample.plot.box()
plt.title("Boxplot of sales and profit", fontsize=15)
plt.xticks(fontsize=15)
plt.yticks(fontsize=15)
plt.grid(True)

输出如下:

图 4.32:销售额和利润的箱线图

我们可以创建简单的箱线图来检查任何异常/不合逻辑的值。例如,在上面的例子中,我们故意将两个销售额值设置为负数,它们在箱线图中很容易被发现。

注意,利润可能是负数,所以这些负数点通常并不可疑。但一般来说,销售额不能是负数,所以它们被检测为异常值。

我们可以创建一个数值量的分布,并检查位于极端值的位置,以查看它们是否真正是数据的一部分或异常值。例如,如果一个分布几乎是正态的,那么任何超过 4 或 5 个标准差的价值可能是有嫌疑的:

图 4.33:远离主要异常值的价值

图 4.33:远离主要异常值的价值

连接、合并和连接

将表或数据集合并或连接是数据整理专业人士日常工作中非常常见的操作。这些操作类似于关系数据库表中的 JOIN 查询。通常,关键数据分布在多个表中,这些记录需要被合并到一个匹配该公共键的单一表中。这在任何类型的销售或交易数据中都是一个极其常见的操作,因此数据整理者必须掌握这一技能。pandas 库提供了方便且直观的内置方法来执行涉及多个 DataFrame 对象的各种类型的 JOIN 查询。

练习 54:连接

我们将首先学习沿各个轴(行或列)连接 DataFrame 的方法。这是一个非常有用的操作,因为它允许你在新数据到来或需要在表中插入新特征列时扩展 DataFrame:

  1. 从我们正在处理的原销售数据集中随机创建三个 DataFrame,每个 DataFrame 包含 4 条记录:

    df_1 = df[['Customer Name','State','Sales','Profit']].sample(n=4)
    df_2 = df[['Customer Name','State','Sales','Profit']].sample(n=4)
    df_3 = df[['Customer Name','State','Sales','Profit']].sample(n=4)
    
  2. 使用以下代码创建一个包含所有行连接的合并 DataFrame:

    df_cat1 = pd.concat([df_1,df_2,df_3], axis=0)
    df_cat1
    

    图 4.34:将 DataFrame 连接在一起

    图 4.34:将 DataFrame 连接在一起

    图 4.34:将 DataFrame 连接在一起
  3. 你也可以尝试沿列进行连接,尽管对于这个特定的例子来说,这没有任何实际意义。然而,pandas 在该操作中用 NaN 填充不可用的值:

    df_cat2 = pd.concat([df_1,df_2,df_3], axis=1)
    df_cat2
    

图 4.35:连接 DataFrame 后的输出

图 4.35:连接 DataFrame 后的输出

练习 55:通过公共键合并

通过公共键合并是数据表的一个极其常见的操作,因为它允许你在主数据库中合理化多个数据源——即如果它们有一些公共特征/键。

这通常是构建用于机器学习任务的大型数据库的第一步,其中每日传入的数据可能被放入单独的表中。然而,最终,最新的表需要与主数据表合并,以便输入到后端机器学习服务器中,然后更新模型及其预测能力。

在这里,我们将展示一个以客户名称为键的内部连接的简单示例:

  1. 一个 DataFrame,df_1,与客户名称相关的运输信息相关联,另一个表,df_2,有产品信息表格。我们的目标是根据公共客户名称将这些表合并到一个 DataFrame 中:

    df_1=df[['Ship Date','Ship Mode','Customer Name']][0:4]
    df_1
    

    输出如下所示:

    图 4.36:df_1 表中的条目

    图 4.36:df_1 表中的条目

    第二个 DataFrame 如下所示:

    df_2=df[['Customer Name','Product Name','Quantity']][0:4]
    df_2
    

    输出如下所示:

    图 4.37:df_2 表中的条目

    图 4.37:df_2 表中的条目
  2. 使用以下命令通过内部连接将这两个表连接起来:

    pd.merge(df_1,df_2,on='Customer Name',how='inner')
    

    输出如下所示:

    图 4.38:df_1 和 df_2 表的内部连接

    图 4.38:df_1 和 df_2 表的内部连接
  3. 使用以下命令删除重复项。

    pd.merge(df_1,df_2,on='Customer Name',how='inner').drop_duplicates()
    

    输出如下:

    图 4.39:删除重复项后,在表 df_1 和表 df_2 上进行内连接

    图 4.39:删除重复项后,在表 df_1 和表 df_2 上进行内连接
  4. 提取另一个名为 df_3 的小表来展示外连接的概念:

    df_3=df[['Customer Name','Product Name','Quantity']][2:6]
    df_3
    

    输出如下:

    图 4.40:创建表 df_3
  5. 使用以下命令在 df_1df_3 上执行内连接:

    pd.merge(df_1,df_3,on='Customer Name',how='inner').drop_duplicates()
    

    输出如下:

    图 4.41:合并表 df_1 和表 df_3 并删除重复项

    图 4.41:合并表 df_1 和表 df_3 并删除重复项
  6. 使用以下命令在 df_1df_3 上执行外连接:

    pd.merge(df_1,df_3,on='Customer Name',how='outer').drop_duplicates()
    

    输出如下:

图 4.42:在删除重复项后,在表 df_1 和表 df_2 上进行外连接

图 4.42:在删除重复项后,在表 df_1 和表 df_2 上进行外连接

注意,由于找不到与这些记录对应的条目,因此自动插入了某些 NaNNaT 值,因为这些条目是各自表中具有唯一客户名称的条目。NaT 代表“不是一个时间”对象,因为“发货日期”列中的对象是时间戳对象。

练习 56:连接方法

连接操作基于 索引 进行,通过将两个可能具有不同索引的 DataFrame 的列合并成一个单一来完成。它提供了一种通过行索引完成合并的更快方式。如果不同表中的记录索引不同但代表相同的基本数据,并且您想将它们合并到一个表中,这很有用:

  1. 使用以下命令创建以下表格,以客户名称作为索引:

    df_1=df[['Customer Name','Ship Date','Ship Mode']][0:4]
    df_1.set_index(['Customer Name'],inplace=True)
    df_1
    df_2=df[['Customer Name','Product Name','Quantity']][2:6]
    df_2.set_index(['Customer Name'],inplace=True)
    df_2
    

    输出如下:

    图 4.43:DataFrame df_1 和 df_2
  2. 使用以下命令在 df_1df_2 上执行左连接:

    df_1.join(df_2,how='left').drop_duplicates()
    

    输出如下:

    图 4.44:删除重复项后,在表 df_1 和表 df_2 上进行左连接
  3. 使用以下命令在 df_1df_2 上执行右连接:

    df_1.join(df_2,how='right').drop_duplicates()
    

    输出如下:

    图 4.45:删除重复项后,在表 df_1 和表 df_2 上进行右连接
  4. 使用以下命令在 df_1df_2 上执行内连接:

    df_1.join(df_2,how='inner').drop_duplicates()
    

    输出如下:

    图 4.46:删除重复项后,在表 df_1 和表 df_2 上进行内连接
  5. 使用以下命令在 df_1df_2 上执行外连接:

    df_1.join(df_2,how='outer').drop_duplicates()
    

    输出如下:

图 4.47:删除重复项后,在表 df_1 和表 df_2 上进行外连接

有用的 Pandas 方法

在这个主题中,我们将讨论 pandas 提供的一些小型实用函数,以便我们能够高效地与 DataFrame 一起工作。它们不属于任何特定的函数组,因此它们在这里在杂项类别下被提及。

练习 57:随机抽样

从大型 DataFrame 中随机采样一个随机分数通常非常有用,这样我们就可以在它们上练习其他方法并测试我们的想法。如果你有一个包含 100 万条记录的数据库表,那么在完整表上运行你的测试脚本在计算上可能不是有效的。

然而,你可能也不希望只提取前 100 个元素,因为数据可能已经按特定键排序,你可能会得到一个无趣的表格,这可能不会代表父数据库的完整统计多样性。

在这些情况下,sample方法非常有用,这样我们就可以随机选择 DataFrame 的一个受控分数:

  1. 使用以下命令指定从 DataFrame 中所需的样本数量:

    df.sample(n=5)
    

    输出如下:

    图 4.48:包含 5 个样本的 DataFrame
  2. 使用以下命令指定要采样的数据的确切分数(百分比):

    df.sample(frac=0.1)
    

    输出如下:

    图 4.49:包含 0.1%数据采样的 DataFrame

    您也可以选择是否进行有放回的抽样,即是否可以选择相同的记录多次。默认的 replace 选择是 FALSE,即无重复,抽样将尝试只选择新元素。

  3. 使用以下命令选择抽样:

    df.sample(frac=0.1, replace=True)
    

    输出如下:

图 4.50:包含 0.1%数据且启用重复的 DataFrame

value_counts方法

我们之前讨论了unique方法,该方法从 DataFrame 中查找并计数唯一记录。在类似方面的另一个有用函数是value_counts。此函数返回一个包含唯一值计数的对象。在返回的对象中,第一个元素是最频繁使用的对象。元素按降序排列。

让我们考虑这个方法的一个实际应用来展示其效用。假设你的经理要求你列出从你拥有的大型销售数据库中排名前 10 的客户。因此,业务问题是:哪些 10 个客户的名称在销售表中出现频率最高?如果数据在 RDBMS 中,你可以使用 SQL 查询实现相同的功能,但在 pandas 中,可以通过使用一个简单的函数来完成:

df['Customer Name'].value_counts()[:10]

输出如下:

图 4.51:前 10 名客户列表

value_counts 方法返回一个按计数频率排序的所有唯一客户名称计数的序列。通过只请求该列表的前 10 个元素,此代码返回出现频率最高的前 10 个客户名称的序列。

交叉表功能

与 group by 类似,pandas 还提供了交叉表功能,这与 MS Excel 等电子表格程序中的交叉表功能相同。例如,在这个销售数据库中,您想了解按地区和州(两个索引级别)的平均销售额、利润和销售数量。

我们可以通过一段简单的代码提取此信息(我们首先随机抽取 100 条记录以保持计算快速,然后应用此代码):

df_sample = df.sample(n=100)
df_sample.pivot_table(values=['Sales','Quantity','Profit'],index=['Region','State'],aggfunc='mean')

输出如下(请注意,由于随机抽样,您的具体输出可能不同):

图片

图 4.52:100 条记录的样本

练习 58:按列值排序 – sort_values 方法

按特定列对表格进行排序是分析师日常工作中最常用的操作之一。不出所料,pandas 提供了一个简单直观的排序方法,称为 sort_values 方法:

  1. 随机抽取 15 条记录,然后展示如何按 Sales 列排序,然后按 Sales 和 State 列一起排序:

    df_sample=df[['Customer Name','State','Sales','Quantity']].sample(n=15)
    df_sample
    

    输出如下:

    图 4.53:15 条记录的样本

    图 4.53:15 条记录的样本
  2. 使用以下命令按 Sales 排序值:

    df_sample.sort_values(by='Sales')
    

    输出如下:

    图 4.54:按 Sales 值排序的 DataFrame

    图 4.54:按 Sales 值排序的 DataFrame
  3. 按照 Sales 和 State 排序值:

    df_sample.sort_values(by=['State','Sales'])
    

    输出如下:

图片

图 4.55:按 Sales 和 State 排序的 DataFrame

练习 59:使用 apply 方法对用户定义函数的灵活性

pandas 库通过 apply 方法提供了极大的灵活性,用于处理任意复杂性的用户定义函数。与原生的 Python apply 函数类似,此方法接受用户定义的函数和额外的参数,并在对特定列的每个元素应用函数后返回一个新列。

例如,假设我们想创建一个基于销售价格列的类别特征列,如高/中/低。请注意,这是根据某些条件(销售阈值)将数值值转换为类别因子(字符串)的转换:

  1. 创建一个用户定义的函数,如下所示:

    def categorize_sales(price):
        if price < 50:
            return "Low"
        elif price < 200:
            return "Medium"
        else:
            return "High"
    
  2. 从数据库中随机抽取 100 条记录:

    df_sample=df[['Customer Name','State','Sales']].sample(n=100)
    df_sample.head(10)
    

    输出如下:

    图 4.56:数据库中的 100 条样本记录

    图 4.56:数据库中的 100 条样本记录
  3. 使用 apply 方法将分类函数应用于 Sales 列:

    注意

    df_sample['Sales Price Category']=df_sample['Sales'].apply(categorize_sales)
    df_sample.head(10)
    

    输出如下:

    图片

    图 4.57:在 Sales 列上使用 apply 函数后的 10 行 DataFrame
  4. apply 方法也适用于内置的 Python 原生函数。为了练习,让我们创建另一个用于存储客户名称长度的列。我们可以使用熟悉的 len 函数来完成此操作:

    df_sample['Customer Name Length']=df_sample['Customer Name'].apply(len)
    df_sample.head(10)
    

    输出如下:

    图片

    图 4.58:包含新列的 DataFrame
  5. 我们甚至可以直接将 lambda 表达式插入到 apply 方法中,而不是编写一个单独的函数。例如,假设我们正在推广我们的产品,并且想要显示原始价格大于 > $200 的折扣销售价格。我们可以使用 lambda 函数和 apply 方法来完成此操作:

    df_sample['Discounted Price']=df_sample['Sales'].apply(lambda x:0.85*x if x>200 else x)
    df_sample.head(10)
    

    输出如下:

img/C11065_04_59.jpg

图 4.59:Lambda 函数

注意

Lambda 函数包含一个条件,并且对原始销售价格大于 $200 的记录应用折扣。

活动六:处理 Adult Income 数据集(UCI)

在这个活动中,您将使用来自 UCI 机器学习门户的 Adult Income 数据集。Adult Income 数据集已被许多解决分类问题的机器学习论文所使用。您将从 CSV 文件中读取数据到 pandas DataFrame,并在此章节中学习的高级数据处理上进行一些练习。

本活动的目的是练习各种高级 pandas DataFrame 操作,例如,对于子集选择、应用用户定义的函数、汇总统计、可视化、布尔索引、分组和异常值检测在一个真实数据集上。我们已经在磁盘上下载了 CSV 文件以供您方便使用。然而,建议您自己练习数据下载,以便您熟悉这个过程。

这是数据集的 URL:archive.ics.uci.edu/ml/machine-learning-databases/adult/

这是数据集描述和变量的 URL:archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.names

这些步骤将帮助您解决此活动:

  1. 加载必要的库。

  2. 从以下 URL 读取 adult income 数据集:github.com/TrainingByPackt/Data-Wrangling-with-Python/blob/master/Chapter04/Activity06/

  3. 创建一个脚本,该脚本将逐行读取文本文件。

  4. Income 名称添加到响应变量中。

  5. 找出缺失值。

  6. 使用子集选择创建只包含年龄、教育和职业的 DataFrame。

  7. 以 20 为 bin 大小绘制年龄直方图。

  8. 创建一个函数来删除空白字符。

  9. 使用 apply 方法将此函数应用于所有具有字符串值的列,创建一个新列,将新列的值复制到旧列中,然后删除新列。

  10. 找出年龄在 30 到 50 岁之间的人数。

  11. 根据年龄和教育分组记录,以找出平均年龄的分布情况。

  12. 按职业分组并显示年龄的汇总统计。找出平均年龄最大的职业,以及在其劳动力中占最大份额的 75 分位数以上的职业。

  13. 使用子集和分组方法查找异常值。

  14. 在柱状图上绘制值。

  15. 使用公共键合并数据。

    注意

    该活动的解决方案可以在第 297 页找到。

摘要

在本章中,我们深入研究了 pandas 库,学习高级数据处理技术。我们从 DataFrame 的高级子集和过滤开始,通过学习布尔索引和数据子集的条件选择来总结这一部分。我们还介绍了如何设置和重置 DataFrame 的索引,尤其是在初始化时。

接下来,我们学习了一个与传统的数据库系统有深刻联系的主题——分组方法。然后,我们深入探讨了数据处理的重要技能——检查和处理缺失数据。我们展示了 pandas 如何使用各种插补技术来处理缺失数据。我们还讨论了删除缺失值的方法。此外,还展示了 DataFrame 对象的连接和合并的方法及其使用示例。我们看到了连接方法,以及它与 SQL 中类似操作的比较。

最后,我们介绍了 DataFrame 上的各种有用方法,例如随机抽样、uniquevalue_countsort_values 和交叉表功能。我们还展示了使用 apply 方法在 DataFrame 上运行任意用户定义函数的示例。

在学习了 NumPy 和 pandas 库的基本和高级数据处理技术之后,数据获取的自然问题随之而来。在下一章中,我们将向您展示如何处理各种数据源,也就是说,您将学习如何在 pandas 中从不同来源读取表格格式的数据。

第六章:第五章

适应不同类型的数据源

学习目标

到本章结束时,你将能够:

  • 将 CSV、Excel 和 JSON 文件读入 pandas DataFrame

  • 将 PDF 文档和 HTML 表格读入 pandas DataFrame

  • 使用如 Beautiful Soup 等强大且易于使用的库执行基本的网络爬取

  • 从门户网站提取结构和文本信息

在本章中,你将接触到应用于网络爬取的实际数据整理技术。

简介

到目前为止,在这本书中,我们一直专注于学习 pandas DataFrame 对象作为应用整理技术的主体数据结构。现在,我们将学习各种技术,通过这些技术我们可以从外部来源将数据读入 DataFrame。其中一些来源可能是基于文本的(CSV、HTML、JSON 等),而另一些则可能是二进制的(Excel、PDF 等),即不是 ASCII 格式。在本章中,我们将学习如何处理存在于网页或 HTML 文档中的数据。这对于数据从业者的工作非常重要。

注意

由于我们已经详细介绍了使用 NumPy 和 pandas 的基本操作示例,在本章中,我们将经常跳过诸如查看表格、选择列和绘图等琐碎的代码片段。相反,我们将专注于展示我们在此处旨在学习的新主题的代码示例。

从不同的基于文本(和非基于文本)来源读取数据

数据整理专业人士最宝贵且最广泛使用的技能之一是能够从各种来源提取和读取数据到结构化格式。现代分析管道依赖于它们扫描和吸收各种数据源的能力,以构建和分析一个富模式模型。这样一个功能丰富、多维度的模型将具有高度的预测和泛化准确性。它将被利益相关者和最终用户同样重视,对于任何数据驱动的产品。

在本章的第一个主题中,我们将探讨各种数据源以及它们如何导入到 pandas DataFrame 中,从而为整理专业人士提供极其宝贵的数据摄取知识。

本章提供的数据文件

因为这个主题是关于从各种数据源读取的,所以接下来的练习中我们将使用各种类型的小文件。所有数据文件都随 Jupyter 笔记本一起提供在代码仓库中。

本章需要安装的库

因为本章涉及读取各种文件格式,我们需要额外的库和软件平台的支持来实现我们的目标。

在你的 Jupyter 笔记本单元格中执行以下代码(不要忘记每行代码前的!)来安装必要的库:

!apt-get update !apt-get install -y default-jdk
!pip install tabula-py xlrd lxml

练习 60:从缺少标题的 CSV 文件中读取数据

pandas 库提供了一个简单的直接方法,称为 read_csv,用于从逗号分隔的文本文件或 CSV 中读取表格格式的数据。这特别有用,因为 CSV 是一种轻量级但极其方便的数据交换格式,适用于许多应用,包括机器生成数据等域。它不是专有格式,因此被各种数据生成源普遍使用。

有时,CSV 文件可能缺少标题,你可能需要添加自己的正确标题/列名。让我们看看如何做到这一点:

  1. 使用以下代码读取示例 CSV 文件(带有合适的标题)并检查生成的 DataFrame,如下所示:

    import numpy as np
    import pandas as pd
    df1 = pd.read_csv("CSV_EX_1.csv")
    df1
    

    输出如下:

    图 5.1:示例 CSV 文件的输出

    图 5.1:示例 CSV 文件的输出
  2. 使用 pandas DataFrame 读取没有标题的 .csv 文件:

    df2 = pd.read_csv("CSV_EX_2.csv")
    df2
    

    输出如下:

    图 5.2:使用 DataFrame 读取的 .csv 的输出

    图 5.2:使用 DataFrame 读取的 .csv 的输出

    当然,顶部数据行被错误地读取为列标题。你可以指定 header=None 来避免这种情况。

  3. 通过指定 header=None 来读取 .csv 文件,如下所示:

    df2 = pd.read_csv("CSV_EX_2.csv",header=None)
    df2
    

    然而,如果没有标题信息,你将得到以下输出。默认标题将是一些从 0 开始的默认数值索引:

    图 5.3:具有数值列标题的 CSV 文件

    图 5.3:具有数值列标题的 CSV 文件

    这可能适合数据分析目的,但如果你想使 DataFrame 真正反映正确的标题,那么你必须使用 names 参数添加它们。

  4. 添加 names 参数以获取正确的标题:

    df2 = pd.read_csv("CSV_EX_2.csv",header=None, names=['Bedroom','Sq.ft','Locality','Price($)'])
    df2
    

    最后,你将得到如下所示的 DataFrame:

图 5.4:具有正确列标题的 CSV 文件

图 5.4:具有正确列标题的 CSV 文件

练习 61:从非逗号分隔符的 CSV 文件中读取

虽然 CSV 代表逗号分隔值,但遇到分隔符/定界符不是逗号的原始数据文件相当常见:

  1. 使用 pandas DataFrame 读取 .csv 文件:

    df3 = pd.read_csv("CSV_EX_3.csv")
    df3
    
  2. 输出将如下所示:图 5.5:以分号作为分隔符的 DataFrame

    图 5.5:以分号作为分隔符的 DataFrame
  3. 显然,; 分隔符不是预期的,读取有误。一个简单的解决方案是在读取函数中明确指定分隔符/定界符:

    df3 = pd.read_csv("CSV_EX_3.csv",sep=';')
    df3
    

    输出如下:

图 5.6:从 DataFrame 中移除的分号

图 5.6:从 DataFrame 中移除的分号

练习 62:跳过 CSV 文件的标题

如果你的 CSV 文件已经包含标题,但你想跳过它们并添加自己的标题,你必须特别设置 header = 0 以实现这一点。如果你尝试将名称变量设置为你的标题列表,可能会发生意外的情况:

  1. 为具有标题的 .csv 文件添加名称,如下所示:

    df4 = pd.read_csv("CSV_EX_1.csv",names=['A','B','C','D'])
    df4
    

    输出如下:

    图 5.7:标题重叠的 CSV 文件

    图 5.7:标题重叠的 CSV 文件
  2. 为了避免这种情况,将 header 设置为零并提供名称列表:

    df4 = pd.read_csv("CSV_EX_1.csv",header=0,names=['A','B','C','D'])
    df4
    

    输出如下:

图 5.8:具有定义标题的 CSV 文件

图 5.8:具有定义标题的 CSV 文件

练习 63:读取 CSV 文件时跳过初始行和页脚

跳过初始行是一种广泛使用的方法,因为大多数情况下,CSV 数据文件的前几行是关于数据源或类似信息的元数据,这些信息不会被读入表格:

图 5.9:CSV 文件的内容

图 5.9:CSV 文件的内容

注意

CSV 文件的前两行是不相关的数据。

  1. 读取 CSV 文件并检查结果:

    df5 = pd.read_csv("CSV_EX_skiprows.csv")
    df5
    

    输出如下:

    图 5.10:带有意外错误的 DataFrame

    图 5.10:带有意外错误的 DataFrame
  2. 跳过前两行并读取文件:

    df5 = pd.read_csv("CSV_EX_skiprows.csv",skiprows=2)
    df5
    

    输出如下:

    图 5.11:跳过两行后的预期 DataFrame

    图 5.11:跳过两行后的预期 DataFrame
  3. 与跳过初始行类似,可能需要跳过文件的页脚。例如,我们不想读取以下文件末尾的数据:图 5.12:CSV 文件的内容

    图 5.12:CSV 文件的内容

    我们必须使用 skipfooterengine='python' 选项来启用此功能。这些 CSV 读取函数有两个引擎——基于 C 或 Python,其中只有 Python 引擎支持 skipfooter 选项。

  4. 在 Python 中使用 skipfooter 选项:

    df6 = pd.read_csv("CSV_EX_skipfooter.csv",skiprows=2,
    skipfooter=1,engine='python')
    df6
    

    输出如下:

图 5.13:没有页脚的 DataFrame

图 5.13:没有页脚的 DataFrame

只读取前 N 行(特别是对于大文件非常有用)

在许多情况下,我们可能不想读取整个数据文件,而只想读取前几行。这对于非常大的数据文件尤其有用,我们可能只想读取前几百行来检查初始模式,然后决定稍后读取整个数据。读取整个文件可能需要很长时间,并减慢整个数据处理流程。

read_csv 函数中的一个简单选项,称为 nrows,使我们能够做到这一点:

df7 = pd.read_csv("CSV_EX_1.csv",nrows=2)
df7

输出如下:

图 5.14:CSV 文件的前几行 DataFrame

练习 64:结合 Skiprows 和 Nrows 以小块读取数据

在继续讨论读取非常大的数据文件时,我们可以巧妙地结合 skiprowsnrows 来以预定的较小块读取这样的大文件。以下代码演示了这一点:

  1. 创建一个列表来存储 DataFrames:

    list_of_dataframe = []
    
  2. 将要读取的行数存储到变量中:

    rows_in_a_chunk = 10
    
  3. 创建一个变量来存储要读取的块数:

    num_chunks = 5
    
  4. 创建一个虚拟的 DataFrame 以获取列名:

    df_dummy = pd.read_csv("Boston_housing.csv",nrows=2)
    colnames = df_dummy.columns
    
  5. 遍历 CSV 文件以一次读取固定数量的行:

    for i in range(0,num_chunks*rows_in_a_chunk,rows_in_a_chunk):
        df = pd.read_csv("Boston_housing.csv",header=0,skiprows=i,nrows=rows_in_a_chunk,names=colnames)
        list_of_dataframe.append(df)
    

注意 iterator 变量如何在 range 函数内部设置以将其分成块。假设块的数量是 5,每个块中的行数是 10。那么,迭代器将有一个范围 (0,5*10,10),其中最后的 10 是步长,即它将以索引 (0,9,19,29,39,49) 迭代。

设置 skip_blank_lines 选项

默认情况下,read_csv 会忽略空白行。但有时,您可能希望将它们读取为 NaN,以便您可以计算原始数据文件中存在多少这样的空白条目。在某些情况下,这是默认数据流质量一致性的指标。为此,您必须禁用 skip_blank_lines 选项:

df9 = pd.read_csv("CSV_EX_blankline.csv",skip_blank_lines=False)
df9

输出如下:

图 5.15:具有空白行的 .csv 文件的 DataFrame

图 5.15:具有空白行的 .csv 文件的 DataFrame

从 Zip 文件中读取 CSV

这是以 pandas 为例的一个很棒的功能,因为它允许您直接从压缩文件(如 .zip.gz.bz2.xz)读取。唯一的要求是,目标数据文件(CSV)应该是压缩文件中唯一的文件。

在这个例子中,我们使用 7-Zip 程序压缩了示例 CSV 文件,并直接使用 read_csv 方法读取:

df10 = pd.read_csv('CSV_EX_1.zip')
df10

输出如下:

图片

图 5.16:压缩 CSV 的 DataFrame

使用 sheet_name 读取 Excel 文件并处理不同的 sheet_name

接下来,我们将关注 Microsoft Excel 文件。事实证明,我们在之前的 CSV 文件练习中学习的许多选项和方法也直接适用于 Excel 文件的读取。因此,我们在这里不会重复它们。相反,我们将关注它们之间的差异。Excel 文件可以由多个工作表组成,我们可以通过传递特定的参数来读取特定的工作表,即 sheet_name

例如,在相关的数据文件 Housing_data.xlsx 中,我们有三个标签页,以下代码将它们逐个读取到三个单独的 DataFrame 中:

df11_1 = pd.read_excel("Housing_data.xlsx",sheet_name='Data_Tab_1')
df11_2 = pd.read_excel("Housing_data.xlsx",sheet_name='Data_Tab_2')
df11_3 = pd.read_excel("Housing_data.xlsx",sheet_name='Data_Tab_3')

如果 Excel 文件有多个不同的工作表,但 sheet_name 参数设置为 None,那么 read_excel 函数将返回一个有序字典。之后,我们可以简单地遍历该字典或其键来检索单个 DataFrame。

让我们考虑以下示例:

dict_df = pd.read_excel("Housing_data.xlsx",sheet_name=None)
dict_df.keys()

输出如下:

odict_keys(['Data_Tab_1', 'Data_Tab_2', 'Data_Tab_3'])

练习 65:读取通用分隔文本文件

通用文本文件可以像读取 CSV 文件一样轻松读取。然而,如果您使用的是除空格或制表符之外的分隔符,您必须传递正确的分隔符:

  1. 以逗号分隔的文件,保存为 .txt 扩展名,如果未显式设置分隔符读取,将生成以下 DataFrame:

    df13 = pd.read_table("Table_EX_1.txt")
    df13
    

    输出如下:

    图片

    图 5.17:具有逗号分隔的 CSV 文件的 DataFrame
  2. 在这种情况下,我们必须显式设置分隔符,如下所示:

    df13 = pd.read_table("Table_EX_1.txt",sep=',')
    df13
    

    输出结果如下:

图 5.18:使用逗号分隔符读取的 DataFrame

图 5.18:使用逗号分隔符读取的 DataFrame

直接从 URL 读取 HTML 表格

Pandas 库允许我们直接从 URL 读取 HTML 表格。这意味着它们已经内置了一些 HTML 解析器,可以处理给定页面的 HTML 内容,并尝试提取页面中的各种表格。

注意

read_html 方法返回一个 DataFrame 列表(即使页面只有一个 DataFrame),你必须从列表中提取相关的表格。

考虑以下示例:

url = 'http://www.fdic.gov/bank/individual/failed/banklist.html'
list_of_df = pd.read_html(url)
df14 = list_of_df[0]
df14.head()

这些结果如下所示:

图 5.19:读取 HTML 表格的结果

图 5.19:读取 HTML 表格的结果

练习 66:进一步整理以获取所需数据

如前所述的练习中讨论的,这个 HTML 读取函数几乎总是为给定的 HTML 页面返回多个表格,我们必须进一步解析列表以提取我们感兴趣的特定表格:

  1. 例如,如果我们想获取 2016 年夏季奥运会奖牌榜(按国家)的表格,我们可以轻松地搜索到一个页面,并将其传递给 Pandas。我们可以通过以下命令来完成:

    list_of_df = pd.read_html("https://en.wikipedia.org/wiki/2016_Summer_Olympics_medal_table",header=0)
    
  2. 如果我们检查返回列表的长度,我们会看到它是 6:

    len(list_of_df)
    

    输出结果如下:

     6
    
  3. 为了查找表格,我们可以运行一个简单的循环:

    for t in list_of_df:
        print(t.shape)
    

    输出结果如下:

    图 5.20:表格的形状
  4. 看起来这个列表中的第二个元素就是我们正在寻找的表格:

    df15=list_of_df[1]
    df15.head()
    
  5. 输出结果如下:

图 5.21:第二张表格中的数据输出

图 5.21:第二张表格中的数据输出

练习 67:从 JSON 文件中读取

在过去的 15 年里,JSON 已经成为网络数据交换的通用选择。如今,它几乎成为所有公开可用的网络 API 的首选格式,同时也常用于私有网络 API。它是一种基于键值对和有序列表的无模式、基于文本的结构化数据表示。

Pandas 库提供了将数据直接从 JSON 文件读取到 DataFrame 中的出色支持。为了练习本章内容,我们包含了一个名为 movies.json 的文件。该文件包含自 1900 年以来几乎所有主要电影的演员、类型、标题和发行年份信息:

  1. 提取 2012 年复仇者联盟电影的演员列表(来自漫威漫画):

    df16 = pd.read_json("movies.json")
    df16.head()
    

    输出结果如下:

    图 5.22:显示复仇者联盟电影演员的 DataFrame

    图 5.22:显示复仇者联盟电影演员的 DataFrame
  2. 为了查找标题为 "复仇者联盟" 的演员列表,我们可以使用过滤功能:

    cast_of_avengers=df16[(df16['title']=="The Avengers") & (df16['year']==2012)]['cast']
    print(list(cast_of_avengers))
    

    输出结果将如下所示:

     [['Robert Downey, Jr.', 'Chris Evans', 'Mark Ruffalo', 'Chris Hemsworth', 'Scarlett Johansson', 'Jeremy Renner', 'Tom Hiddleston', 'Clark Gregg', 'Cobie Smulders', 'Stellan SkarsgÃyrd', 'Samuel L. Jackson']]
    

读取 Stata 文件

pandas 库还提供了直接读取 Stata 文件的函数。Stata 是一个流行的统计建模平台,被许多政府机构和研究组织使用,尤其是经济学家和社会科学家。

读取 Stata 文件(.dta 格式)的简单代码如下:

df17 = pd.read_stata("wu-data.dta")

练习 68:从 PDF 文件中读取表格数据

在各种数据源类型中,PDF 格式可能是最难以解析的。虽然有一些流行的 Python 包用于处理 PDF 文件的一般页面格式,但用于从 PDF 文件中提取表格的最佳库是 tabula-py

从这个包的 GitHub 页面来看,tabula-pytabula-java 的简单 Python 封装,可以从 PDF 中读取表格。您可以从 PDF 中读取表格并将它们转换为 pandas DataFrame。tabula-py 库还允许您将 PDF 文件转换为 CSV/TSV/JSON 文件。

在您运行此代码之前,需要在您的系统上安装以下包,但它们是免费的且易于安装:

  • urllib3

  • pandas

  • pytest

  • flake8

  • distro

  • pathlib

  1. 在以下链接中找到 PDF 文件:https://github.com/TrainingByPackt/Data-Wrangling-with-Python/blob/master/Chapter05/Exercise60-68/Housing_data.xlsx。以下代码从两页中检索表格并将它们连接成一个表格:

    from tabula import read_pdf
    df18_1 = read_pdf('Housing_data.pdf',pages=[1],pandas_options={'header':None})
    df18_1
    

    输出如下:

    图 5.23:通过合并 PDF 中跨越两页的表格得到的 DataFrame

    图 5.23:通过合并 PDF 中跨越两页的表格得到的 DataFrame
  2. 使用以下命令从同一 PDF 的另一页检索表格:

    df18_2 = read_pdf('Housing_data.pdf',pages=[2],pandas_options={'header':None})
    df18_2
    

    输出如下:

    图 5.24:显示来自另一页的表格的 DataFrame

    图 5.24:显示来自另一页的表格的 DataFrame
  3. 要连接从前两个步骤中得到的表格,请执行以下代码:

    df18=pd.concat([df18_1,df18_2],axis=1)
    df18
    

    输出如下:

    图 5.25:通过连接两个表格得到的 DataFrame

    图 5.25:通过连接两个表格得到的 DataFrame
  4. 在 PDF 提取过程中,大多数情况下,标题将难以自动提取。您必须通过 read-pdf 函数中的 names 参数作为 pandas_option 传递标题列表,如下所示:

    names=['CRIM','ZN','INDUS','CHAS','NOX','RM','AGE','DIS','RAD','TAX','PTRATIO','B','LSTAT','PRICE']
    df18_1 = read_pdf('Housing_data.pdf',pages=[1],pandas_options={'header':None,'names':names[:10]})
    df18_2 = read_pdf('Housing_data.pdf',pages=[2],pandas_options={'header':None,'names':names[10:]})
    df18=pd.concat([df18_1,df18_2],axis=1)
    df18
    

    输出如下:

图 5.26:具有正确列标题的 PDF 数据 DataFrame

图 5.26:具有正确列标题的 PDF 数据 DataFrame

在本章末尾,我们将进行一个完整的活动,阅读 PDF 报告中的表格并进行处理。

Beautiful Soup 4 和网页解析简介

读取和理解网页的能力对于收集和格式化数据的人来说是至关重要的。例如,考虑收集有关电影数据并将其格式化以供下游系统使用的任务。电影数据最好通过像 IMDB 这样的网站获得,而这些数据并不是以预包装的格式(CSV、JSON 等)提供的,因此你需要知道如何下载和读取网页。

此外,你还需要具备网页结构的知识,这样你才能设计一个系统,可以从整个网页中搜索(查询)特定信息并获取其值。这涉及到理解标记语言语法,并能够编写可以解析它们的代码。做这件事,同时考虑到所有边缘情况,对于像 HTML 这样的东西已经非常复杂,如果你将定制标记语言的范围扩展到包括 XML,那么这将成为一个团队的全职工作。

幸运的是,我们正在使用 Python,Python 有一个非常成熟和稳定的库来为我们完成所有复杂的任务。这个库叫做BeautifulSoup(目前处于第 4 版,因此从现在起我们将简称为bs4)。bs4是一个从 HTML 或 XML 文档中获取数据的库,它为你提供了一种优雅、规范、惯用的方式来导航和查询文档。它不包含解析器,但它支持不同的解析器。

HTML 结构

在我们深入研究bs4并开始使用它之前,我们需要检查 HTML 文档的结构。语**言是一种向网页浏览器告知网页组织结构的方式,意味着哪种类型的元素(文本、图像、视频等)来自哪里,它们在页面内部的哪个位置出现,它们的样式是什么,它们包含什么,以及它们如何响应用户输入。HTML5 是 HTML 的最新版本。一个 HTML 文档可以被视为一棵树,正如我们可以在以下图中看到的那样:

图 5.27:HTML 结构

图 5.27:HTML 结构

树中的每个节点代表文档中的一个元素。元素是以<开头并以>结尾的任何东西。例如,<html><head><p><br><img>等等都是各种 HTML 元素。一些元素有起始和结束元素,其中结束元素以</开头,并且与起始元素具有相同的名称,例如<p></p>,它们可以包含任意数量的其他类型的元素。一些元素没有结束部分,例如<br />元素,它们不能包含任何内容。

目前我们还需要了解关于元素的其他唯一事实是,元素可以有属性,这些属性用于修改元素的默认行为。一个 <a> 元素需要一个 href 属性来告诉浏览器当点击特定的 <a> 时应该导航到哪个网站,例如:<a href="http://cnn.com">。点击 CNN 新闻频道 <a> 将带你到 cnn.com:

图 5.28:CNN 新闻频道超链接

图 5.28:CNN 新闻频道超链接

因此,当你处于树中的特定元素时,你可以访问该元素的子元素以获取它们的内容和属性。

带着这些知识,让我们看看我们如何从 HTML 文档中读取和查询数据。

在这个主题中,我们将介绍网页的读取和解析,但我们不会从实时网站请求它们。相反,我们从磁盘读取它们。关于从互联网读取它们的章节将在未来的章节中介绍。

练习 69:使用 BeautifulSoup 读取 HTML 文件并提取其内容

在这个练习中,我们将做最简单的事情。我们将导入 BeautifulSoup 库,然后使用它来读取一个 HTML 文档。然后,我们将检查它返回的不同类型的对象。在练习这个主题时,你应该始终在文本编辑器中打开示例 HTML 文件,以便你可以检查不同的标签及其属性和内容:

  1. 导入 bs4 库:

    from bs4 import BeautifulSoup
    
  2. 请下载以下测试 HTML 文件并将其保存到您的磁盘上,然后使用 bs4 从磁盘读取它:

    with open("test.html", "r") as fd:    soup = BeautifulSoup(fd)    print(type(soup))
    

    输出如下:

    <class 'bs4.BeautifulSoup'>
    

    你可以直接将文件句柄传递给 BeautifulSoup 对象的构造函数,它将从句柄附加的文件中读取内容。我们将看到返回类型是 bs4.BeautifulSoup 的一个实例。这个类包含了我们需要导航文档表示的 DOM 树的所有方法。

  3. 通过使用类中的 prettify 方法以这种方式打印文件的漂亮内容:

    print(soup.prettify())
    

    输出如下:

    图 5.29:HTML 文件内容

    图 5.29:HTML 文件内容

    同样的信息也可以通过使用 soup.contents 成员变量来获取。区别在于:首先,它不会打印出任何美观的内容,其次,它本质上是一个列表。

    如果我们仔细查看单独的文本编辑器中的 HTML 文件内容,我们会看到有许多段落标签,或 <p> 标签。让我们从一个这样的 <p> 标签中读取内容。我们可以使用简单的 . 访问修饰符来完成,就像我们会对类的普通成员变量做的那样。

  4. bs4 的魔力在于它给我们提供了这样一种优秀的方式来取消引用标签,作为 BeautifulSoup 类实例的成员变量:

    with open("test.html", "r") as fd:
        soup = BeautifulSoup(fd)
        print(soup.p)
    

    输出如下:

    图 5.30: 标签中的文本

    图 5.30:

    标签中的文本

    如我们所见,这是 <p> 标签的内容。

    在上一个练习中,我们看到了如何读取一个标签,但我们可以很容易地看到这种方法的问题。当我们查看我们的 HTML 文档时,我们可以看到那里有多个 <p> 标签。我们如何访问所有的 <p> 标签?结果证明这是很容易的。

  5. 使用 findall 方法从标签中提取内容:

    with open("test.html", "r") as fd:
        soup = BeautifulSoup(fd)
        all_ps = soup.find_all('p')
        print("Total number of <p>  --- {}".format(len(all_ps)))
    

    输出如下:

    Total number of <p>  --- 6
    

    这将打印出 6,这正是文档中 <p> 标签的数量。

    我们已经看到了如何访问相同类型的所有标签。我们也看到了如何获取整个 HTML 文档的内容。

  6. 现在,我们将看到如何获取特定 HTML 标签下的内容,如下所示:

    with open("test.html", "r") as fd:
        soup = BeautifulSoup(fd)
        table = soup.table
        print(table.contents)
    

    输出如下:

    图 5.31: 标签下的内容

    图 5.31:<table> 标签下的内容

    在这里,我们正在从文档中获取(第一个)表格,然后使用相同的 "." 表示法,来获取该标签下的内容。

    在上一个练习中,我们看到了如何访问特定标签下的全部内容。然而,HTML 被表示为树,我们能够遍历特定节点的子标签。有几种方法可以做到这一点。

  7. 第一种方法是通过使用任何 bs4 实例的 children 生成器,如下所示:

    with open("test.html", "r") as fd:
        soup = BeautifulSoup(fd)
        table = soup.table
        for child in table.children:
            print(child)
            print("*****")
    

    当我们执行代码时,我们将看到如下所示的内容:

    图 5.32:遍历表格节点的子标签

    图 5.32:遍历表格节点的子标签

    看起来循环只执行了两次!嗯,"children" 生成器的问题在于它只考虑了标签的直接子标签。我们在 <table> 下有 <tbody>,并且整个表格结构都被它包裹着。这就是为什么它被视为 <table> 标签的单个子标签。

    我们探讨了如何浏览一个标签的直接子标签。我们将看到如何浏览一个标签的所有可能子标签,而不仅仅是直接子标签。

  8. 为了做到这一点,我们使用 bs4 实例中的 descendants 生成器,如下所示:

    with open("test.html", "r") as fd:
        soup = BeautifulSoup(fd)
        table = soup.table
        children = table.children
        des = table.descendants
        print(len(list(children)), len(list(des)))
    

    输出如下:

    9 61
    

代码块末尾的比较打印将显示 childrendescendants 之间的差异。我们从 children 获取的列表长度仅为 9,而从 descendants 获取的列表长度为 61。

练习 70:DataFrames 和 BeautifulSoup

到目前为止,我们已经看到了一些使用 bs4 在 HTML 文档内部导航标签的基本方法。现在,我们将更进一步,利用 bs4 的力量与 pandas 的力量相结合,从纯 HTML 表格生成一个 DataFrame。这种特定的知识对我们非常有用。通过我们现在将获得的知识,我们将能够轻松地准备一个 pandas DataFrame 来执行 EDA(探索性数据分析)或建模。我们将在测试 HTML 文件的一个简单小表格上展示这个过程,但这个概念同样适用于任何任意大的表格:

  1. 导入pandas并按以下方式读取文档:

    import pandas as pd
    fd = open("test.html", "r")
    soup = BeautifulSoup(fd)
    data = soup.findAll('tr')
    print("Data is a {} and {} items long".format(type(data), len(data)))
    

    输出如下:

    Data is a <class 'bs4.element.ResultSet'> and 4 items long
    
  2. 检查 HTML 源中的原始表格结构。你会看到第一行是列标题,所有后续的行都是数据。我们为两个部分分配了两个不同的变量,如下所示:

    data_without_header = data[1:]
    headers = data[0]
    header
    

    输出如下:

    <tr>
    <th>Entry Header 1</th>
    <th>Entry Header 2</th>
    <th>Entry Header 3</th>
    <th>Entry Header 4</th>
    </tr>
    

    注意

    请记住,抓取 HTML 页面的艺术与理解源 HTML 结构密不可分。因此,每次你想抓取一个页面时,你首先需要做的就是右键点击它,然后从浏览器中使用“查看源代码”来查看源 HTML。

  3. 一旦我们分离了这两个部分,我们需要两个列表推导式来使它们准备好进入 DataFrame。对于标题,这很简单:

    col_headers = [th.getText() for th in headers.findAll('th')]
    col_headers
    

    输出如下:

    ['Entry Header 1', 'Entry Header 2', 'Entry Header 3', 'Entry Header 4']
    
  4. 对于 pandas DataFrame 的数据准备有点棘手。你需要有一个二维列表,即列表的列表。我们以下这种方式完成:

    df_data = [[td.getText() for td in tr.findAll('td')] for tr in data_without_header]
    df_data
    

    输出如下:

    图片

    图 5.33:以二维列表的形式输出
  5. 调用pd.DataFrame方法,并使用以下代码提供正确的参数:

    df = pd.DataFrame(df_data, columns=col_headers)
    df.head()
    

图片

图 5.34:带有列标题的表格格式输出

练习 71:将 DataFrame 导出为 Excel 文件

在这个练习中,我们将看到如何将 DataFrame 保存为 Excel 文件。Pandas 可以原生地做到这一点,但需要openpyxl库的帮助来实现这个目标:

  1. 使用以下命令安装openpyxl库:

    !pip install openpyxl
    
  2. 要将 DataFrame 保存为 Excel 文件,请在 Jupyter 笔记本中使用以下命令:

    writer = pd.ExcelWriter('test_output.xlsx')df.to_excel(writer, "Sheet1")writer.save()
    writer
    

    输出如下:

    <pandas.io.excel._XlsxWriter at 0x24feb2939b0>
    

练习 72:使用 bs4 从文档中堆叠 URL

在之前(讨论堆栈时),我们解释了拥有一个堆栈的重要性,我们可以将网页中的 URL 推入堆栈,以便稍后弹出以跟踪每个 URL。在这里,在这个练习中,我们将看到它是如何工作的。

在给定的测试中,HTML 文件链接或<a>标签位于<ul>标签下,并且每个都包含在</li>标签内:

  1. 使用以下命令查找所有<a>标签:

    d = open("test.html", "r")
    soup = BeautifulSoup(fd)
    lis = soup.find('ul').findAll('li')
    stack = []
    for li in lis:    a = li.find('a', href=True)
    
  2. 在循环开始之前定义一个堆栈。然后在循环内部,使用append方法将链接推入堆栈:

    stack.append(a['href'])
    
  3. 打印堆栈:

图片

图 5.35:堆栈的输出

活动七:从网页中读取表格数据并创建 DataFrame

在这个活动中,你已经得到了一个包含所有国家 GDP 的维基百科页面。你被要求从页面中提到的三个来源创建三个DataFrameen.wikipedia.org/wiki/List_of_countries_by_GDP_(nominal)):

你必须做以下事情:

  1. 在单独的 Chrome/Firefox 标签页中打开页面,并使用类似检查元素的工具来查看源 HTML 并理解其结构

  2. 使用 bs4 读取页面

  3. 找到你需要处理的表格结构(有多少个表格?)

  4. 使用 bs4 找到正确的表格

  5. 将源名称及其对应的数据分开

  6. 从你创建的源列表中获取源名称

  7. 仅对第一个源将标题和数据从之前分开的数据中分离出来,然后使用这些数据创建一个 DataFrame

  8. 对其他两个数据源重复最后一个任务

    注意

    该活动的解决方案可以在第 308 页找到。

摘要

在这个主题中,我们研究了 HTML 文档的结构。HTML 文档是万维网的基础,考虑到其包含的数据量,我们可以轻易推断出 HTML 作为数据源的重要性。

我们学习了 bs4(BeautifulSoup4),这是一个 Python 库,它为我们提供了以 Python 方式读取和查询 HTML 文档的方法。我们使用 bs4 加载了一个 HTML 文档,并探索了多种不同的导航加载文档的方法。我们还获得了关于这些方法之间差异的必要信息。

我们探讨了如何从 HTML 文档(其中包含一个表格)创建 pandas DataFrame。尽管 pandas 中有一些内置的方法来完成这项工作,但一旦目标表格被编码在一个复杂的元素层次结构中,它们就会失败。因此,我们在本主题中通过逐步将 HTML 表格转换为 pandas DataFrame 所获得的知识是无价的。

最后,我们探讨了如何在代码中创建一个栈,将我们在读取 HTML 文件时遇到的全部 URL 推入栈中,然后在稍后时间使用它们。在下一章中,我们将讨论列表推导式、zip、格式化和异常值检测与清理。

第七章:第六章

学习数据清洗的隐藏秘密

学习目标

到本章结束时,你将能够:

  • 清洗和处理现实生活中的杂乱数据

  • 通过格式化数据以符合下游系统所需格式来准备数据分析

  • 从数据中识别并移除异常值

在本章中,你将了解现实生活中发生的数据问题。你还将学习如何解决这些问题。

简介

在本章中,我们将了解创建成功数据清洗管道背后的秘诀。在前几章中,我们介绍了数据清洗的基本数据结构和构建块,例如 pandas 和 NumPy。在本章中,我们将查看数据清洗的数据处理部分。

假设你有一个包含患有心脏病患者的数据库,就像任何调查一样,数据可能缺失、不正确或存在异常值。异常值是异常值,通常远离中心趋势,因此将其包含到你的复杂机器学习模型中可能会引入我们希望避免的严重偏差。通常,这些问题在金钱、人力和其他组织资源方面会造成巨大的差异。不可否认,具备解决这些问题的技能的人将对组织证明是一笔财富。

本节所需额外软件

这个练习的代码依赖于两个额外的库。我们需要安装 SciPypython-Levenshtein,并且我们将在运行的 Docker 容器中安装它们。请注意这一点,因为我们不在容器中。

要安装库,请在运行的 Jupyter 笔记本中输入以下命令:

!pip install scipy python-Levenshtein

高级列表推导式和 zip 函数

在这个主题中,我们将深入探讨列表推导式的核心。我们已经看到了它的基本形式,包括像 a = [i for i in range(0, 30)] 这样简单的东西,到涉及一个条件语句的稍微复杂一些的形式。然而,正如我们之前提到的,列表推导式是一个非常强大的工具,在本主题中,我们将进一步探索这个神奇工具的力量。我们将研究列表推导式的另一个近亲,称为 generators,并使用 zip 及其相关函数和方法。在本主题结束时,你将能够自信地处理复杂的逻辑问题。

生成器表达式的介绍

在之前讨论高级数据结构时,我们见证了 repeat 等函数。我们说它们代表一种特殊类型的函数,称为迭代器。我们还展示了迭代器的懒加载如何导致巨大的空间节省和时间效率。

迭代器是 Python 提供的函数式编程结构中的一个组成部分。函数式编程确实是一种非常高效且安全的方法来解决问题。它提供了比其他方法更多的优势,如模块化、易于调试和测试、可组合性、形式可证明性(一个理论计算机科学概念)等等。

练习 73:生成器表达式

在这个练习中,我们将介绍生成器表达式,它们被认为是函数式编程的另一个基石(实际上,它们受到了纯函数式语言 Haskell 的启发)。由于我们已经看到了一些列表推导的例子,生成器表达式对我们来说将很熟悉。然而,它们也提供了一些比列表推导更多的优势:

  1. 使用列表推导编写以下代码以生成介于 0 和 100,000 之间的所有奇数列表:

    odd_numbers2 = [x for x in range(100000) if x % 2 != 0]
    
  2. 使用以下代码通过 sys 模块中的 getsizeof 函数:

    from sys import getsizeof
    getsizeof(odd_numbers2)
    

    输出如下:

    406496
    

    我们将看到,这样做需要相当多的内存。它也不是非常高效。我们如何改变这一点?使用类似 repeat 的方法在这里不适用,因为我们需要列表推导的逻辑。幸运的是,我们可以将任何列表推导转换为生成器表达式。

  3. 为上述列表推导编写等效的生成器表达式:

    odd_numbers = (x for x in range(100000) if x % 2 != 0)
    

    注意,我们做的唯一改变是将列表推导语句用圆括号而不是方括号包围。这使得它缩小到大约 100 字节!这使得它成为一个惰性评估,因此更高效。

  4. 打印前 10 个奇数,如下所示:

    for i, number in enumerate(odd_numbers):
        print(number)
        if i > 10:
            break
    

    输出如下:

    1
    3
    5
    7
    9
    11
    13
    15
    17
    19
    21
    23
    

练习 74:一行生成器表达式

在这个练习中,我们将利用我们对生成器表达式的知识来生成一个表达式,该表达式将逐个从单词列表中读取一个单词,并移除它们末尾的换行符并将它们转换为小写。这当然可以使用显式的 for 循环来完成:

  1. 创建一个单词字符串,如下所示:

    words = ["Hello\n", "My name", "is\n", "Bob", "How are you", "doing\n"]
    
  2. 编写以下生成器表达式以完成任务,如下所示:

    modified_words = (word.strip().lower() for word in words)
    
  3. 创建一个列表推导,从生成器表达式中逐个获取单词,并最终打印出列表,如下所示:

    final_list_of_word = [word for word in modified_words]
    final_list_of_word
    

    输出如下:

图 6.1:单词列表推导

图 6.1:单词列表推导

练习 75:提取单字列表

如果我们查看上一个练习的输出,我们会注意到,由于源数据的杂乱性质(这在现实世界中是正常的),我们最终得到了一个列表,在某些情况下,我们有一个以上的单词连在一起,由空格分隔。为了改进这一点,并获取单字列表,我们不得不修改生成器表达式:

  1. 编写生成器表达式,然后编写等效的嵌套循环,以便我们可以比较结果:

    words = ["Hello\n", "My name", "is\n", "Bob", "How are you", "doing\n"]
    modified_words2 = (w.strip().lower() for word in words for w in word.split(" "))
    final_list_of_word = [word for word in modified_words2]
    final_list_of_word
    

    输出如下:

    图 6.2:从字符串中提取的单词列表
  2. 按照嵌套for循环的方式编写一个等效的代码,如下所示:

    modified_words3 = []
    for word in words:
        for w in word.split(" "):
            modified_words3.append(w.strip().lower())
    modified_words3
    

    输出如下:

图 6.3:使用嵌套循环从字符串中提取的单词列表

我们必须承认生成器表达式不仅节省了空间和时间,而且是一种更优雅的方式来编写相同的逻辑。

要记住生成器表达式中的嵌套循环是如何工作的,请记住循环是从左到右评估的,并且最终的循环变量(在我们的例子中,用单个字母“w”表示)被返回(因此我们可以对它调用striplower)。

以下图表将帮助您记住列表推导式或生成器表达式中嵌套for循环的技巧:

图 6.4:嵌套循环说明

图 6.4:嵌套循环说明

我们之前已经学习了生成器表达式中的嵌套for循环,但现在我们将学习生成器表达式中的独立for循环。我们将从两个for循环中获得两个输出变量,并且它们必须被当作一个元组来处理,这样它们在 Python 中就不会有语法上的歧义。

创建以下两个列表:

marbles = ["RED", "BLUE", "GREEN"]
counts = [1, 5, 13]

您被要求在给出前两个列表后生成所有可能的弹珠和计数的组合。您将如何做到这一点?当然,使用嵌套for循环和列表的append方法可以完成这个任务。那么生成器表达式呢?一个更优雅、更简单的解决方案如下:

marble_with_count = ((m, c) for m in marbles for c in counts)

这个生成器表达式在同时for循环的每次迭代中创建一个元组。这段代码等同于以下显式代码:

marble_with_count_as_list_2 = []
for m in marbles:
    for c in counts:
        marble_with_count_as_list_2.append((m, c))
marble_with_count_as_list_2

输出如下:

图 6.5:添加弹珠和计数

图 6.5:添加弹珠和计数

这个生成器表达式在同时for循环的每次迭代中创建一个元组。再次强调,生成器表达式简单、优雅且高效。

练习 76:zip 函数

在这个练习中,我们将检查zip函数,并将其与我们之前练习中编写的生成器表达式进行比较。之前生成器表达式的缺点是它产生了所有可能的组合。例如,如果我们需要将国家与其首都关联起来,使用生成器表达式来做这个会比较困难。幸运的是,Python 为我们提供了一个内置函数zip,专门用于这个目的:

  1. 创建以下两个列表:

    countries = ["India", "USA", "France", "UK"]
    capitals = ["Delhi", "Washington", "Paris", "London"]
    
  2. 使用以下命令生成一个包含国家名称作为第一个元素和首都名称作为第二个元素的元组列表:

    countries_and_capitals = [t for t in zip(countries, capitals)]
    
  3. 这表示得不是很好。我们可以使用dict,其中键是国家的名称,而值是首都的名称,可以使用以下命令:

    countries_and_capitals_as_dict = dict(zip(countries, capitals))
    

    输出如下:

图 6.6:包含国家和首都的字典

图 6.6:包含国家和首都的字典

练习 77:处理混乱的数据

如往常一样,在现实生活中,数据是杂乱的。因此,我们刚才看到的那些国家与首都的长度相等的漂亮列表并不存在。

zip函数不能与长度不等的长列表一起使用,因为一旦其中一个列表到达末尾,zip就会停止工作。为了在这种情况下帮助我们,itertools模块中提供了ziplongest

  1. 创建两个长度不等的长列表,如下所示:

    countries = ["India", "USA", "France", "UK", "Brasil", "Japan"]
    capitals = ["Delhi", "Washington", "Paris", "London"]
    
  2. 创建最终的dict并将None作为没有首都的国家在首都列表中的值:

    from itertools import zip_longest
    countries_and_capitals_as_dict_2 = dict(zip_longest(countries, capitals))
    countries_and_capitals_as_dict_2
    

    输出如下:

图 6.7:使用 ziplongest 的输出

图 6.7:使用 ziplongest 的输出

我们应该在这里暂停一下,思考一下通过调用一个函数并仅给出两个源数据列表,我们节省了多少行显式代码和难以理解的if-else条件逻辑。这确实令人惊叹!

通过这些练习,我们结束了本章的第一个主题。高级列表解析、生成器表达式以及zipziplongest等函数是一些我们必须掌握的重要技巧,如果我们想要编写干净、高效且易于维护的代码。不具备这三个品质的代码在业界被认为是次品,我们当然不希望编写这样的代码。

然而,我们没有涵盖这里的一个重要对象,那就是生成器。生成器是一种特殊类型的函数,它具有与生成器表达式相同的特性。然而,作为一个函数,它具有更广泛的范围,并且更加灵活。我们强烈建议您了解它们。

数据格式化

在本主题中,我们将格式化给定的数据集。正确格式化数据的动机主要包括以下几点:

  • 它有助于所有下游系统对每个数据点有一个单一且预先约定的数据格式,从而避免意外,实际上也避免了破坏。

  • 从主要用于机器消费的底层数据生成可读性强的报告。

  • 为了查找数据中的错误。

在 Python 中,有几种方法可以进行数据格式化。我们将从模运算符开始。

%运算符

Python 为我们提供了%运算符来对数据进行基本格式化。为了演示这一点,我们首先通过读取 CSV 文件加载数据,然后我们将对它应用一些基本的格式化。

使用以下命令从 CSV 文件加载数据:

from csv import DictReader
raw_data = []
with open("combinded_data.csv", "rt") as fd:
    data_rows = DictReader(fd)
    for data in data_rows:
        raw_data.append(dict(data))

现在,我们有一个名为raw_data的列表,它包含了 CSV 文件的所有行。您可以随意打印它来查看其外观。

输出如下:

图 6.8:原始数据

图 6.8:原始数据

我们将生成一份关于这些数据的报告。这份报告将包含每个数据点的一个部分,并报告个人的姓名、年龄、体重、身高、家族病史以及最终的心脏状况。这些点必须是清晰且易于理解的英文句子。

我们是这样做的:

for data in raw_data:
    report_str = """%s is %s years old and is %s meter tall weighing about %s kg.\n
    Has a history of family illness: %s.\n
    Presently suffering from a heart disease: %s
    """ % (data["Name"], data["Age"], data["Height"], data["Weight"], data["Disease_history"], data["Heart_problem"])
    print(report_str)

输出如下:

图 6.9:以可展示格式呈现的原始数据

图 6.9:以可展示格式呈现的原始数据

% 操作符有两种不同的用法:

  • 当在引号内使用时,它表示这里期望的数据类型。%s 代表字符串,而 %d 代表整数。如果我们指定了错误的数据类型,它将引发错误。因此,我们可以有效地使用这种格式化作为输入数据的错误过滤器。

  • 当我们在引号外使用 % 操作符时,它基本上告诉 Python 开始用外部提供的值替换所有内部数据。

使用格式化函数

在本节中,我们将探讨完全相同的格式化问题,但这次我们将使用更高级的方法。我们将使用 Python 的 format 函数。

要使用 format 函数,我们执行以下操作:

for data in raw_data:
    report_str = """{} is {} years old and is {} meter tall weighing about {} kg.\n
    Has a history of family illness: {}.\n
    Presently suffering from a heart disease: {}
    """.format(data["Name"], data["Age"], data["Height"], data["Weight"], data["Disease_history"], data["Heart_problem"])
    print(report_str)

输出如下:

图 6.10:使用字符串的格式化函数进行格式化的数据

注意,我们将 %s 替换为 {},并且不再使用引号外的 %,而是调用了 format 函数。

我们将看到强大的 format 函数如何使之前的代码更加易读和易懂。我们不再使用简单的空白 {},而是在其中提及键名,然后使用特殊的 Python ** 操作对 dict 进行解包,并将其提供给格式化函数。它足够智能,可以通过以下命令确定如何使用实际的 dict 中的值替换引号内的键名:

for data in raw_data:
    report_str = """{Name} is {Age} years old and is {Height} meter tall weighing about {Weight} kg.\n
    Has a history of family illness: {Disease_history}.\n
    Presently suffering from a heart disease: {Heart_problem}
    """.format(**data)
    print(report_str)

输出如下:

图 6.11:使用**操作的可读文件

图 6.11:使用**操作的可读文件

这种方法确实更加简洁且易于维护。

练习 78:使用{}进行数据表示

引号内的 {} 符号功能强大,我们可以通过它显著改变数据的表现形式:

  1. 使用以下命令将十进制数字转换为二进制形式:

    original_number = 42
    print("The binary representation of 42 is - {0:b}".format(original_number))
    

    输出如下:

    图 6.12:其二进制表示形式的数字
  2. 打印居中对齐的字符串:

    print("{:⁴²}".format("I am at the center"))
    

    输出如下:

    图 6.13:使用居中对齐格式化的字符串
  3. 打印居中对齐的字符串,但这次在两侧都有填充:

    print("{:=⁴²}".format("I am at the center"))
    

    输出如下:

图 6.14:使用填充居中对齐的字符串

图 6.14:使用填充居中对齐的字符串

如我们之前提到的,格式说明是一个强大的功能。

格式化日期很重要,因为日期的格式取决于数据来源,并且在数据清洗管道中可能需要多次转换。

我们可以使用熟悉的日期格式化符号,格式如下:

from datetime import datetime
print("The present datetime is {:%Y-%m-%d %H:%M:%S}".format(datetime.utcnow()))

输出如下:

图 6.15:格式化后的数据

图 6.15:格式化后的数据

将其与datetime.utcnow的实际输出进行比较,你将很容易看到这个表达式的力量。

识别和清除异常值

面对现实世界数据时,我们经常在记录集中看到特定的事情:有一些数据点与其余记录不匹配。它们有一些值太大,或太小,或完全缺失。这类记录被称为异常值

统计学上,有一个关于异常值含义的适当定义和概念。通常,你需要深厚的领域专业知识来理解何时将某个特定记录称为异常值。然而,在这个当前练习中,我们将探讨一些在现实世界数据中标记和过滤异常值的基本技术,这些技术在日常工作中很常见。

练习 79:数值数据中的异常值

在这个练习中,我们将首先基于数值数据构建异常值的概念。想象一个余弦曲线。如果你还记得高中数学中的这个概念,那么余弦曲线是在 [1, -1] 范围内一个非常平滑的曲线:

  1. 要构建余弦曲线,请执行以下命令:

    from math import cos, pi
    ys = [cos(i*(pi/4)) for i in range(50)]
    
  2. 使用以下代码绘制数据:

    import matplotlib.pyplot as plt
    plt.plot(ys)
    

    输出如下:

    图 6.16:余弦波

    如我们所见,它是一个非常平滑的曲线,没有异常值。我们现在要引入一些。

  3. 使用以下命令引入一些异常值:

    ys[4] = ys[4] + 5.0
    ys[20] = ys[20] + 8.0
    
  4. 绘制曲线:

    plt.plot(ys)
    

图 6.17:带有异常值的波

图 6.17:带有异常值的波

我们可以看到,我们已经成功地在曲线上引入了两个值,打破了平滑性,因此可以被认为是异常值。

检测我们的数据集是否有异常值的一个好方法是创建一个箱线图。箱线图是一种基于数据的中心趋势和一些(实际上,我们称之为四分位数)来绘制数值数据的方式。在箱线图中,异常值通常被绘制为单独的点。matplotlib库帮助从一系列数值数据中绘制箱线图,这并不困难。这就是我们这样做的方式:

plt.boxplot(ys)

一旦执行上述代码,你将能够看到有一个很好的箱线图,其中我们创建的两个异常值清晰地显示出来,就像以下图表所示:

图 6.18:带有异常值的箱线图

图 6.18:带有异常值的箱线图

Z-score

Z-score 是在数据集上的一种度量,它为每个数据点提供一个值,说明该数据点相对于数据集的标准差和平均值分布的程度。我们可以使用 z-score 在数据集中数值检测异常值。通常,任何 z-score 大于+3 或小于-3 的数据点都被认为是异常值。我们可以借助优秀的 SciPy 和pandas库的概念来过滤异常值。

使用 SciPy,使用以下命令计算 z-score:

from scipy import stats
cos_arr_z_score = stats.zscore(ys)
Cos_arr_z_score

输出如下:

图 6.19:z-score 值

图 6.19:z-score 值

练习 80:用于去除异常值的 Z 分数

在这个练习中,我们将讨论如何从一组数据中去除异常值。在上一个练习中,我们计算了每个数据点的 z 分数。在这个练习中,我们将利用这个结果来从我们的数据中移除异常值:

  1. 导入pandas并创建一个 DataFrame:

    import pandas as pd
    df_original = pd.DataFrame(ys)
    
  2. 将 z 分数小于 3 的异常值分配给:

    cos_arr_without_outliers = df_original[(cos_arr_z_score < 3)]
    
  3. 使用print函数打印新的和旧的形状:

    print(cos_arr_without_outliers.shape)
    print(df_original.shape)
    

    从两个打印结果(48, 1 和 50, 1)中,我们可以清楚地看到派生的 DataFrame 少了两行。这些就是我们的异常值。如果我们绘制cos_arr_without_outliers DataFrame,那么我们将看到以下输出:

图 6.20:无异常值的余弦波

如预期,我们得到了平滑的曲线并去除了异常值。

在任何数据清洗流程中,检测和去除异常值是一个复杂且关键的过程。这需要深厚的领域知识、描述性统计学的专业知识、对编程语言(以及所有有用的库)的掌握,以及大量的谨慎。我们建议在数据集上执行此操作时要非常小心。

练习 81:字符串的模糊匹配

在这个练习中,我们将探讨一个稍微不同的问题,乍一看可能看起来像是一个异常值。然而,经过仔细检查,我们会发现它确实不是,我们将了解一个有时被称为字符串模糊匹配的有用概念。

Levenshtein 距离是一个高级概念。我们可以将其视为将一个字符串转换为另一个字符串所需的最小单字符编辑次数。当两个字符串相同的时候,它们之间的距离是 0 - 差异越大,数字越高。我们可以考虑一个距离阈值,低于这个阈值我们将认为两个字符串是相同的。因此,我们不仅可以纠正人为错误,还可以设置一个安全网,以确保我们不会通过所有候选者。

Levenshtein 距离计算是一个复杂的过程,我们不会在这里从头开始实现它。幸运的是,像很多其他事情一样,有一个库可供我们使用来完成这个任务。它被称为python-Levenshtein

  1. 创建三艘船在不同日期的负载数据!

    图 6.21:初始化 ship_data 变量

    如果你仔细观察,你会注意到船名在三种不同情况下拼写不同。让我们假设船的实际名称是"Sea Princess"。从正常的角度来看,这确实看起来像有人犯了错误,数据点确实描述了同一艘船。在严格基于异常值的基础上移除其中两个可能不是最好的做法。

  2. 然后,我们只需从其中导入距离函数,并传递两个字符串给它来计算它们之间的距离:

    from Levenshtein import distance
    name_of_ship = "Sea Princess"
    for k, v in ship_data.items():
        print("{} {} {}".format(k, name_of_ship, distance(name_of_ship, k)))
    

    输出如下:

图 6.22:字符串之间的距离

我们会注意到字符串之间的距离是不同的。当它们相同的时候,距离是 0,当它们不同的时候,距离是一个正整数。我们可以在我们的数据处理工作中使用这个概念,并说距离小于或等于某个数字的字符串是相同的字符串。

在这里,我们再次需要谨慎考虑何时以及如何使用这种模糊字符串匹配。有时,它们是必需的,而其他时候它们可能会导致非常糟糕的错误。

活动 8:处理异常值和缺失数据

在这个活动中,我们将识别并去除异常值。这里,我们有一个 CSV 文件。我们的目标是利用我们迄今为止学到的知识来清理数据,并创建一个格式良好的 DataFrame。识别异常值的类型及其对数据的影响,并清理混乱的数据。

帮助你解决此活动的步骤如下:

  1. 读取 visit_data.csv 文件。

  2. 检查重复项。

  3. 检查是否有任何重要列包含 NaN。

  4. 去除异常值。

  5. 报告大小差异。

  6. 创建一个箱线图来检查异常值。

  7. 去除任何异常值。

    注意

    此活动的解决方案可以在第 312 页找到。

摘要

在本章中,我们学习了使用生成器表达式处理列表数据的一些有趣方法。它们既简单又优雅,一旦掌握,就能给我们一个强大的技巧,我们可以反复使用它来简化几个常见的数据处理任务。我们还考察了不同的数据格式化方法。数据的格式化不仅对准备漂亮的报告有用,而且对于保证下游系统的数据完整性通常非常重要。

我们通过检查一些识别和删除异常值的方法来结束本章。这对我们来说很重要,因为我们希望我们的数据得到适当的准备,并准备好进行所有复杂的数据分析工作。我们还观察到,花时间并利用领域专业知识来制定识别异常值的规则是多么重要,因为这样做可能会造成比好处更大的伤害。

在下一章中,我们将介绍如何读取网页、XML 文件和 API。

第八章:第七章

高级网络爬虫和数据收集

学习目标

到本章结束时,你将能够:

  • 利用 requestsBeautifulSoup 读取各种网页并从中收集数据

  • 使用应用程序程序接口 (API) 对 XML 文件和网页执行读取操作

  • 利用正则表达式技术从大量杂乱无章的文本语料库中抓取有用信息

在本章中,你将学习如何从网页、XML 文件和 API 中收集数据。

简介

上一章介绍了如何创建一个成功的数据整理管道。在本章中,我们将使用我们迄今为止所学到的所有技术构建一个实际的网络爬虫。本章建立在 BeautifulSoup 的基础上,并介绍了各种抓取网页和使用 API 收集数据的方法。

网络爬虫和 Beautiful Soup 库的基础

在当今这个互联互通的世界里,对于数据整理专业人士来说,最宝贵和最广泛使用的技能之一是能够从托管在网上的网页和数据库中提取和读取数据。大多数组织将数据托管在云端(公共或私有),而如今的大多数网络微服务都为外部用户提供了一些类型的 API 以访问数据:

图 7.1:数据整理 HTTP 请求和 XML/JSON 响应

图 7.1:数据整理 HTTP 请求和 XML/JSON 响应

作为数据整理工程师,了解网页结构和 Python 库的结构是必要的,这样你才能从网页中提取数据。万维网是一个不断增长、不断变化的宇宙,其中使用了不同的数据交换协议和格式。其中一些被广泛使用并已成为标准。

Python 库

Python 配备了内置模块,如 urllib 3,这些模块可以在互联网上放置 HTTP 请求并从云端接收数据。然而,这些模块在较低级别运行,需要更深入地了解 HTTP 协议、编码和请求。

在本章中,我们将利用两个 Python 库:RequestsBeautifulSoup。为了避免在较低级别处理 HTTP 方法,我们将使用 Requests 库。它是一个建立在纯 Python 网络实用库之上的 API,这使得放置 HTTP 请求变得简单直观。

BeautifulSoup 是最受欢迎的 HTML 解析包之一。它解析你传递的 HTML 内容,并在页面内构建一个详细的标签和标记树,以便于直观地遍历。这个树可以被程序员用来查找特定的标记元素(例如,一个表格、一个超链接或特定 div ID 内的文本块)以抓取有用的数据。

练习 81:使用 Requests 库从维基百科主页获取响应

维基百科主页由许多元素和脚本组成,这些都是 HTML、CSS 和 JavaScript 代码块的混合。为了读取维基百科的主页并提取一些有用的文本信息,我们需要逐步进行,因为我们不感兴趣的是所有代码或标记标签;只对文本的某些选定部分感兴趣。

在这个练习中,我们将剥离 HTML/CSS/JavaScript 层,以获取我们感兴趣的信息。

  1. 导入requests库:

    import requests
    
  2. 将主页 URL 分配给一个变量,wiki_home

    # First assign the URL of Wikipedia home page to a strings
    wiki_home = "https://en.wikipedia.org/wiki/Main_Page"
    
  3. 使用requests库的get方法从该页面获取响应:

    response = requests.get(wiki_home)
    
  4. 要获取响应对象的信息,请输入以下代码:

    type(response)
    

    输出如下:

    requests.models.Response
    

这是一个在requests库中定义的模型数据结构。

互联网是一个极其动态的地方。有可能在某人使用你的代码时,维基百科的主页已经改变,或者特定的 Web 服务器可能已经关闭,你的请求将基本上失败。如果你在未检查请求状态的情况下继续编写更复杂和详尽的代码,那么所有后续的工作都将徒劳无功。

Web 页面请求通常会返回各种代码。以下是一些你可能遇到的常见代码:

图 7.2:Web 请求及其描述

图 7.2:Web 请求及其描述

因此,我们编写一个函数来检查代码并根据需要打印出消息。这类小型辅助/实用函数对于复杂项目来说非常有用。

练习 82:检查 Web 请求的状态

接下来,我们将编写一个小型实用函数来检查响应的状态。

我们将首先养成编写小型函数来完成小型模块化任务的习惯,而不是编写长脚本,因为长脚本难以调试和跟踪:

  1. 使用以下命令创建status_check函数:

    def status_check(r):
        if r.status_code==200:
            print("Success!")
            return 1
        else:
            print("Failed!")
            return -1
    

    注意,除了打印适当的消息外,我们从这个函数返回 1 或-1。这很重要。

  2. 使用status_check命令检查响应:

    status_check(response)
    

    输出如下:

图 7.3:状态检查的输出

图 7.3:状态检查的输出

在本章中,我们不会使用这些返回值,但在以后的更复杂的编程活动中,你只有在获得此函数的返回值时才会继续进行,也就是说,你将编写一个条件语句来检查返回值,然后根据该值执行后续代码。

检查网页编码

我们还可以编写一个实用函数来检查网页的编码。任何 HTML 文档都有可能的编码,尽管最流行的是 UTF-8。一些最流行的编码包括 ASCII、Unicode 和 UTF-8。ASCII 是最简单的,但它无法捕获世界上各种口语和书面语言中使用的复杂符号,因此 UTF-8 已经成为当今网络开发的几乎通用标准。

当我们在维基百科主页上运行此函数时,我们得到该页使用的特定编码类型。这个函数与前面的函数类似,它接受requests响应对象作为参数并返回一个值:

def encoding_check(r):
    return (r.encoding)

检查响应:

encoding_check(response)

输出如下:

'UTF-8'

在这里,UTF-8 表示目前数字媒体和互联网上最流行的字符编码方案。它采用 1-4 字节的可变长度编码,从而可以表示世界上各种语言的 Unicode 字符。

练习 83:创建一个函数来解码响应内容并检查其长度

这一系列步骤的最终目的是获取一个页面的内容作为一个文本块或字符串对象,以便 Python 之后进行处理。在互联网上,数据流以编码格式移动。因此,我们需要解码响应对象的内容。为此,我们需要执行以下步骤:

  1. 编写一个实用函数来解码响应的内容:

    def decode_content(r,encoding):
        return (r.content.decode(encoding))
    contents = decode_content(response,encoding_check(response))
    
  2. 检查解码对象的类型:

    type(contents)
    

    输出如下:

    str
    

    我们最终通过读取 HTML 页面得到了一个字符串对象!

    注意

    注意,本章和 Jupyter 笔记本中的练习答案可能因维基百科页面的更新而有所不同。

  3. 检查对象的长度并尝试打印其中的一部分:

    len(contents)
    

    输出如下:

    74182
    

    如果你打印这个字符串的前 10,000 个字符,它看起来会类似于这样:

图 7.4:显示混合的 HTML 标记标签、文本和元素名称以及属性的输出

图 7.4:显示混合的 HTML 标记标签、文本和元素名称以及属性的输出

显然,这是一个由各种 HTML 标记标签、文本和元素名称/属性组成的混合体。如果不使用复杂的函数或方法,我们无法从中提取有意义的信息。幸运的是,BeautifulSoup库提供了这样的方法,我们将在下一节中看到如何使用它们。

练习 84:从 BeautifulSoup 对象中提取可读文本

结果表明,BeautifulSoup对象有一个text方法,可以用来提取文本:

  1. 导入包,然后将整个字符串(HTML 内容)传递给一个用于解析的方法:

    from bs4 import BeautifulSoup
    soup = BeautifulSoup(contents, 'html.parser')
    
  2. 在你的笔记本中执行以下代码:

    txt_dump=soup.text
    
  3. 查找txt_dmp:的类型

    type(txt_dump)
    

    输出如下:

    str
    
  4. 查找txt_dmp:的长度

    len(txt_dump)
    

    输出如下:

    15326
    
  5. 现在,文本输出的长度比原始 HTML 字符串的长度小得多。这是因为bs4已经解析了 HTML 并提取了仅用于进一步处理的人读文本。

  6. 打印这个文本的初始部分。

    print(txt_dump[10000:11000])
    

    你会看到类似以下的内容:

图 7.5:显示文本初始部分的输出

图 7.5:显示文本初始部分的输出

从一个部分提取文本

现在,让我们继续进行一个更有趣的数据处理任务。如果你打开维基百科首页,你可能会看到一个名为今日特色文章的部分。这是当天突出文章的摘录,该文章是随机选择并在首页推广的。实际上,这篇文章在一天中也可能发生变化:

图 7.6:示例维基百科页面突出显示“今日特色文章”部分

图 7.6:示例维基百科页面突出显示“今日特色文章”部分

你需要从这个部分提取文本。有几种方法可以完成这个任务。在这里,我们将介绍一种简单直观的方法。

首先,我们尝试识别两个索引——字符串的开始索引和结束索引,它们标志着我们感兴趣的文本的开始和结束。在下一张屏幕截图中,索引如下所示:

图 7.7:维基百科页面突出显示要提取的文本

图 7.7:维基百科页面突出显示要提取的文本

以下代码完成了提取:

idx1=txt_dump.find("From today's featured article")
idx2=txt_dump.find("Recently featured")
print(txt_dump[idx1+len("From today's featured article"):idx2])

注意,我们必须将“今日特色文章”字符串的长度添加到idx1中,然后将它作为起始索引传递。这是因为idx1找到的是“今日特色文章”字符串的开始位置,而不是结束位置。

它会打印出类似以下的内容(这是一个示例输出):

图 7.8:提取的文本

图 7.8:提取的文本

提取今天日期发生的重要历史事件

接下来,我们将尝试提取与今天日期发生的重要历史事件的文本。这通常可以在以下屏幕截图的右下角找到:

图 7.9:维基百科页面突出显示“历史上的今天”部分

图 7.9:维基百科页面突出显示“历史上的今天”部分

那么,我们能否应用与之前对“今日特色文章”所用的相同技术?显然不行,因为在我们想要提取结束的地方下面有文本,这与之前的情况不同。注意,在前面的练习中,固定的字符串“最近推荐”正好出现在我们想要提取停止的地方。因此,我们可以在代码中使用它。然而,在这种情况下,我们不能这样做,原因如下面的屏幕截图所示:

图 7.10:维基百科页面突出显示要提取的文本

图 7.10:突出显示要提取的维基百科页面文本

因此,在本节中,我们只想找出围绕我们感兴趣的主要内容的文本看起来是什么样子。为此,我们必须找出字符串“On this day”的起始位置,并使用以下命令打印出接下来的 1,000 个字符:

idx3=txt_dump.find("On this day")
print(txt_dump[idx3+len("On this day"):idx3+len("On this day")+1000])

它看起来是这样的:

图 7.11:维基百科“On this day”部分的输出

图 7.11:维基百科“On this day”部分的输出

为了解决这个问题,我们需要换一种思维方式,并使用一些 BeautifulSoup(以及编写另一个实用函数)的其他方法。

练习 85:使用高级 BS4 技术提取相关文本

HTML 页面由许多标记标签组成,例如

,表示文本/图像的分区,或者
    ,表示列表。我们可以利用这种结构来查看包含我们感兴趣文本的元素。在 Mozilla Firefox 浏览器中,我们可以通过右键单击并选择“检查元素”选项来轻松完成此操作:

    图 7.12:在维基百科上检查元素

    图 7.12:在维基百科上检查元素

    当你用鼠标悬停在这个上面时,你会看到页面上的不同部分被突出显示。通过这样做,可以轻松地发现负责我们感兴趣文本信息的精确标记文本块。在这里,我们可以看到某个<ul>块包含以下文本:

    图 7.13:识别包含文本的 HTML 块

    现在,找到包含这个<ul>块的<div>标签是明智的。通过查看之前的相同屏幕,我们找到了<div>及其 ID:

    图 7.14:包含文本的 ul 标签

    图 7.14:包含文本的
      标签
    1. 使用 BeautifulSoup 的find_all方法,该方法扫描 HTML 页面的所有标签(及其子元素)以找到并提取与特定<div>元素相关的文本。

      注意

      注意我们如何利用mp-otd ID 的<div>来在数十个其他<div>元素中识别它。

      find_all方法返回一个NavigableString类,该类与其关联一个有用的text方法,用于提取。

    2. 为了将这些想法结合起来,我们将创建一个空列表,并在遍历页面时将NavigableString类的文本追加到这个列表中:

      text_list=[] #Empty list
      for d in soup.find_all('div'):
              if (d.get('id')=='mp-otd'):
                  for i in d.find_all('ul'):
                      text_list.append(i.text)
      
    3. 现在,如果我们检查text_list列表,我们会看到它有三个元素。如果我们按标记打印这些元素,我们会看到我们感兴趣的文本作为第一个元素出现!

      for i in text_list:
          print(i)
          print('-'*100)
      

      注意

      在这个例子中,我们感兴趣的是列表的第一个元素。然而,确切的位置将取决于网页。

      输出如下:

    图 7.15:高亮的文本

    图 7.15:高亮的文本

    练习 86:创建一个紧凑的函数从维基百科主页提取“On this Day”文本

    正如我们之前讨论的,尝试将特定任务功能化总是好的,尤其是在网络爬虫应用程序中:

    1. 创建一个函数,其唯一任务是接受 URL(作为字符串)并返回与On this day部分对应的文本。这种功能方法的优点是你可以从任何 Python 脚本中调用这个函数,并在另一个程序中的任何地方将其用作独立模块:

      def wiki_on_this_day(url="https://en.wikipedia.org/wiki/Main_Page"):
          """
      
    2. 从维基百科主页的“On this day”部分提取文本。接受维基百科主页 URL 作为字符串。提供了一个默认的 URL:

          """
          import requests
          from bs4 import BeautifulSoup
      
          wiki_home = str(url)
          response = requests.get(wiki_home)
      
          def status_check(r):
              if r.status_code==200:
                  return 1
              else:
                  return -1
      
          status = status_check(response)
          if status==1:
              contents = decode_content(response,encoding_check(response))
          else:
              print("Sorry could not reach the web page!")
              return -1
      
          soup = BeautifulSoup(contents, 'html.parser')
          text_list=[]
      
          for d in soup.find_all('div'):
                  if (d.get('id')=='mp-otd'):
                      for i in d.find_all('ul'):
                          text_list.append(i.text)
      
          return (text_list[0])
      
    3. 注意这个函数如何利用状态检查,如果请求失败,则打印出错误信息。当我们用故意错误的 URL 测试这个函数时,它表现得如预期:

      print(wiki_on_this_day("https://en.wikipedia.org/wiki/Main_Page1"))
       Sorry could not reach the web page!
      

    从 XML 中读取数据

    XML,或可扩展标记语言,是一种类似于 HTML 但具有显著灵活性的网络标记语言(对于用户而言),例如能够定义自己的标签。它是 20 世纪 90 年代和 21 世纪初最被炒作的技术之一。它是一种元语言,也就是说,一种允许我们使用其机制定义其他语言的语言,例如 RSS、MathML(一种广泛用于网络发布和显示数学密集型技术信息的数学标记语言),等等。XML 在互联网上的常规数据交换中也得到了广泛使用,作为一名数据整理专业人士,你应该对其基本特性有足够的了解,以便在需要为项目提取数据时随时能够访问数据流管道。

    练习 87:创建 XML 文件和读取 XML 元素对象

    让我们创建一些随机数据,以便更好地理解 XML 数据格式。输入以下代码片段:

    1. 使用以下命令创建 XML 文件:

      data = '''
      <person>
        <name>Dave</name>
        <surname>Piccardo</surname>
        <phone type="intl">
           +1 742 101 4456
         </phone>
         <email hide="yes">
         dave.p@gmail.com</email>
      </person>'''
      
    2. 这是一个三引号字符串或多行字符串。如果你打印这个对象,你会得到以下输出。这是一个以树结构格式化的 XML 数据字符串,正如我们很快就会看到的,当我们解析结构并分离出各个部分时:图 7.16:XML 文件输出

      图 7.16:XML 文件输出
    3. 为了处理和整理数据,我们必须使用 Python XML 解析器引擎将其读取为Element对象:

      import xml.etree.ElementTree as ET
      tree = ET.fromstring(data)
      type (tree)
      

      输出结果如下:

       xml.etree.ElementTree.Element
      

    练习 88:在树(元素)中查找各种数据元素

    我们可以使用find方法在 XML 元素对象中搜索各种有用的数据,并使用text方法打印它们(或在我们想要的任何处理代码中使用它们)。我们还可以使用get方法提取我们想要的特定属性:

    1. 使用find方法查找Name

      # Print the name of the person
      print('Name:', tree.find('name').text)
       Dave
      
    2. 使用find方法查找Surname

      # Print the surname
      print('Surname:', tree.find('surname').text)
       Piccardo
      
    3. 使用find方法查找Phone。注意使用strip方法去除任何尾随空格/空白:

      # Print the phone number
      print('Phone:', tree.find('phone').text.strip())
      

      输出结果如下:

      +1 742 101 4456
      
    4. 使用find方法查找email statusactual email。注意使用get方法提取状态:

      # Print email status and the actual email
      print('Email hidden:', tree.find('email').get('hide'))
      print('Email:', tree.find('email').text.strip())
      

      输出结果如下:

       Email hidden: yes
       Email: dave.p@gmail.com
      

    将本地 XML 文件读取到 ElementTree 对象中

    我们也可以从 XML 文件(保存在磁盘上)中读取。

    这是一个相当常见的情况,其中前端网络抓取模块已经通过读取网页上的数据表下载了大量的 XML 文件,现在数据整理者需要解析这个 XML 文件以提取有意义的数值和文本数据。

    我们有一个与本章相关的文件,称为 "xml1.xml"。请确保您有该文件,并且它与您从 Jupyter Notebook 运行的目录相同:

    tree2=ET.parse('xml1.xml')
    type(tree2)
    The output will be as follows: xml.etree.ElementTree.ElementTree
    

    注意我们如何使用 parse 方法来读取这个 XML 文件。这与之前练习中使用的 fromstring 方法略有不同,当时我们是直接从一个字符串对象中读取。这会产生一个 ElementTree 对象而不是一个简单的 Element

    构建树形对象的想法与计算机科学和编程领域相同:

    • 有一个根节点

    • 根节点附加有子对象

    • 可能存在多个级别,即子节点的子节点递归向下

    • 树中的所有节点(根节点和子节点)都附加有包含数据的属性

    • 可以使用树遍历算法来搜索特定的属性

    • 如果提供,可以使用特殊方法来深入探测一个节点

    练习 89:遍历树,找到根节点,并探索所有子节点及其标签和属性

    XML 树中的每个节点都有标签和属性。其思路如下:

    图 7.17:找到 XML 标签的根节点和子节点
    1. 使用以下代码探索这些标签和属性:

      root=tree2.getroot()
      for child in root:
          print ("Child:",child.tag, "| Child attribute:",child.attrib)
      

      输出将如下所示:

    图 7.18:显示提取的 XML 标签的输出

    图 7.18:显示提取的 XML 标签的输出

    注意

    记住,每个 XML 数据文件可能遵循不同的命名或结构格式,但使用元素树方法将数据放入一种结构化的流程中,可以系统地探索。然而,最好在尝试自动提取之前先检查原始 XML 文件结构一次,并理解(即使是在高层次上)数据格式。

    练习 90:使用 text 方法提取有意义的数据

    我们几乎可以将 XML 树视为一个列表的列表,并相应地进行索引:

    1. 使用以下代码通过访问 root[0][2] 来获取元素:

      root[0][2]
      

      输出将如下所示:

      <Element 'gdppc' at 0x00000000051FF278>
      

      因此,这指向了 'gdppc' 这部分数据。在这里,"gdppc" 是标签,实际的人均 GDP 数据附着在这个标签上。

    2. 使用 text 方法来访问数据:

      root[0][2].text
      

      输出将如下所示:

       '70617'
      
    3. 使用 tag 方法来访问 gdppc

      root[0][2].tag
      

      输出将如下所示:

       'gdppc'
      
    4. 检查 root[0]

      root[0]
      

      输出将如下所示:

       <Element 'country1' at 0x00000000050298B8>
      
    5. 检查标签:

      root[0].tag
      

      输出将如下所示:

        'country1'
      

      我们可以使用 attrib 方法来访问它:

      root[0].attrib
      

      输出将如下所示:

       {'name': 'Norway'}
      

      因此,root[0] 仍然是一个元素,但它与 root[0][2] 具有不同的标签和属性集。这是预期的,因为它们都是树的一部分,作为节点,但每个节点都与不同级别的数据相关联。

    这段代码的最后输出很有趣,因为它返回一个字典对象。因此,我们可以通过其键来索引它。我们将在下一个练习中这样做。

    使用循环提取和打印 GDP/人均信息

    现在我们知道了如何读取 GDP/人均数据,以及如何从树中获取字典,我们可以通过在树上运行循环轻松构建一个简单的数据集:

    for c in root:
        country_name=c.attrib['name']
        gdppc = int(c[2].text)
        print("{}: {}".format(country_name,gdppc))
    

    输出如下:

     Norway: 70617
     Austria: 44857
     Israel: 38788
    

    我们可以将这些数据放入 DataFrame 或 CSV 文件中,以便保存到本地磁盘或进行进一步处理,例如简单的绘图!

    练习 91:为每个国家找到所有邻近国家并打印它们

    如我们之前提到的,对于树结构有高效的搜索算法,对于 XML 树的一种方法就是 findall。我们可以使用这个方法,在这个例子中,找到国家所有的邻近国家并将它们打印出来。

    为什么我们需要使用 findall 而不是 find 呢?因为不是所有国家都有相同数量的邻近国家,而 findall 会搜索与特定节点相关联的具有该标签的所有数据,而我们想要遍历所有这些数据:

    for c in root:
        ne=c.findall('neighbor') # Find all the neighbors
        print("Neighbors\n"+"-"*25)
        for i in ne: # Iterate over the neighbors and print their 'name' attribute
            print(i.attrib['name'])
        print('\n')
    

    输出看起来像这样:

    图 7.19:使用 findall 生成的输出

    练习 92:使用通过网络抓取获得的 XML 数据的一个简单演示

    在本章的最后主题中,我们学习了使用 requests 库进行简单的网络抓取。到目前为止,我们一直在处理静态 XML 数据,即来自本地文件或我们编写的字符串对象的数据。现在,是时候将我们的学习结合起来,直接从互联网上读取 XML 数据了(正如你预期的那样,你几乎总是这样做):

    1. 我们将尝试从一个名为 www.recipepuppy.com/ 的网站上读取烹饪食谱,该网站聚合了各种其他网站的食谱链接:

      import urllib.request, urllib.parse, urllib.error
      serviceurl = 'http://www.recipepuppy.com/api/?'
      item = str(input('Enter the name of a food item (enter \'quit\' to quit): '))
      url = serviceurl + urllib.parse.urlencode({'q':item})+'&p=1&format=xml'
      uh = urllib.request.urlopen(url)
      data = uh.read().decode()
      print('Retrieved', len(data), 'characters')
      tree3 = ET.fromstring(data)
      
    2. 这段代码将要求用户输入。你必须输入一个食品项目的名称。例如,'chicken tikka':图 7.20:从 XML 数据中抓取的演示

      图 7.20:从 XML 数据中抓取的演示
    3. 我们得到的是 XML 格式的数据,在创建 XML 树之前,我们需要读取和解析它:

      data = uh.read().decode()
      print('Retrieved', len(data), 'characters')
      tree3 = ET.fromstring(data)
      
    4. 现在,我们可以使用另一个有用的方法,称为 iter,它基本上遍历一个元素下的节点。如果我们遍历树并提取文本,我们得到以下输出:

      for elem in tree3.iter():
          print(elem.text)
      

      输出如下:

      图 7.21:使用 iter 生成的输出

      图 7.21:使用 iter 生成的输出
    5. 我们可以使用 find 方法搜索适当的属性并提取其内容。这就是为什么手动扫描 XML 数据并检查使用了哪些属性很重要的原因。记住,这意味着扫描原始字符串数据,而不是树结构。

    6. 打印原始字符串数据:图 7.22:显示提取的 href 标签的输出结果

      图 7.22:显示提取的 href 标签的输出结果

      现在我们知道要搜索哪些标签。

    7. 打印 XML 数据中的所有超链接:

      for e in tree3.iter():
          h=e.find('href')
          t=e.find('title')
          if h!=None and t!=None:
              print("Receipe Link for:",t.text)
              print(h.text)
              print("-"*100)
      

      注意h!=Nonet!=None的使用。当你第一次运行这类代码时,这些可能难以预料。你可能会遇到错误,因为一些标签可能返回一个None对象,即它们在这个 XML 数据流中由于某种原因而为空。这种情况相当常见,无法事先预料。如果你收到这样的错误,你必须使用你的 Python 知识和编程直觉来解决这个问题。在这里,我们只是在检查对象的类型,如果它不是None,那么我们需要提取与它相关的文本。

      最终输出如下:

      图 7.23:显示最终输出的输出结果

    图 7.23:显示最终输出的输出结果

    从 API 读取数据

    基本上,API 或应用程序编程接口是一种对计算资源(例如,操作系统或数据库表)的接口,它提供了一套公开的方法(函数调用),允许程序员访问该资源的特定数据或内部功能。

    网络 API,正如其名所示,是在网络上的 API。请注意,它不是一种特定的技术或编程框架,而是一种架构概念。想象一下 API 就像快餐店的客户服务中心。内部有许多食品、原材料、烹饪资源和食谱管理系统,但你所能看到的是固定的菜单项,而你只能通过这些项进行交互。它就像一个可以通过 HTTP 协议访问的端口,如果使用得当,能够提供数据和服务。

    网络 API 在当今各种数据服务中极为流行。在第一章中,我们讨论了加州大学圣地亚哥分校的数据科学团队如何从 Twitter 动态中提取数据来分析森林火灾的发生情况。为此,他们不去 twitter.com,通过查看 HTML 页面和文本来抓取数据。相反,他们使用 Twitter API,该 API 以流式格式连续发送这些数据。

    因此,对于数据整理专业人员来说,了解从网络 API 中提取数据的基本知识非常重要,因为你极有可能发现自己处于必须通过 API 接口读取大量数据进行处理和整理的情况。如今,大多数 API 以 JSON 格式流式传输数据。在本章中,我们将使用一个免费的 API 以 JSON 格式读取有关世界各地各种国家的信息,并进行处理。

    我们将使用 Python 的内置urllib模块以及 pandas 来创建 DataFrame。因此,我们现在可以导入它们。我们还将导入 Python 的JSON模块:

    import urllib.request, urllib.parse
    from urllib.error import HTTPError,URLError
    import json
    import pandas as pd
    

    定义基本 URL(或 API 端点)

    首先,我们需要设置基本 URL。当我们处理 API 微服务时,这通常被称为API 端点。因此,在你感兴趣的网站服务门户中寻找这样的短语,并使用他们提供的端点 URL:

    serviceurl = 'https://restcountries.eu/rest/v2/name/'
    

    基于 API 的微服务在提供服务和数据方面具有极高的动态性。它可以随时改变。在规划本章内容时,我们发现这个特定的 API 是一个很好的选择,可以轻松地提取数据,而且无需使用授权密钥(登录或特殊 API 密钥)。

    然而,对于大多数 API,你需要有自己的 API 密钥。通过注册他们的服务来获取。基本使用(直到固定数量的请求或数据流限制)通常是免费的,但之后你将需要付费。为了注册 API 密钥,你通常需要输入信用卡信息。

    我们希望避免所有这些麻烦来教你们基础知识,这就是为什么我们选择了这个例子,它不需要这样的授权。但是,根据你工作中会遇到的数据类型,请准备好学习如何使用 API 密钥。

    练习 93:定义和测试从 API 中提取国家数据的函数

    这个特定的 API 提供了关于世界各地国家的基本信息:

    1. 定义一个函数,当我们传递一个国家的名称作为参数时,从中提取数据。操作的核心包含在以下两行代码中:

      url = serviceurl + country_name
      uh = urllib.request.urlopen(url)
      
    2. 第一行代码将国家名称作为字符串追加到基本 URL 上,第二行向 API 端点发送一个get请求。如果一切顺利,我们会收到数据,对其进行解码,并将其作为 JSON 文件读取。整个练习的代码如下,包括围绕我们之前讨论的基本操作的一些错误处理代码:

      def get_country_data(country):
          """
          Function to get data about country from "https://restcountries.eu" API
          """
          country_name=str(country)
          url = serviceurl + country_name
      
          try: 
              uh = urllib.request.urlopen(url)
          except HTTPError as e:
              print("Sorry! Could not retrieve anything on {}".format(country_name))
              return None
          except URLError as e:
              print('Failed to reach a server.')
              print('Reason: ', e.reason)
              return None
          else:
              data = uh.read().decode()
              print("Retrieved data on {}. Total {} characters read.".format(country_name,len(data)))
              return data
      
    3. 通过传递一些参数来测试这个函数。我们传递一个正确的名称和一个错误的名称。响应如下:

      注意

      这是一个基本的错误处理示例。你必须考虑各种可能性,并在构建实际的 Web 或企业应用程序时编写这样的代码来捕获并优雅地响应用户输入。

    图 7.24:输入参数

    图 7.24:输入参数

    使用内置 JSON 库读取和检查数据

    正如我们已经提到的,JSON 看起来很像 Python 字典。

    在这个练习中,我们将使用 Python 的json模块来读取该格式的原始数据,并查看我们可以进一步处理的内容:

    x=json.loads(data)
    y=x[0]
    type(y)
    

    输出将如下所示:

     dict
    

    因此,当我们使用json模块的loads方法时,我们得到一个列表。它将字符串数据类型读入字典列表。在这种情况下,我们列表中只有一个元素,所以我们提取它并检查其类型,以确保它是一个字典。

    我们可以快速检查字典的键,即 JSON 数据(注意这里没有显示完整的截图)。我们可以看到相关的国家数据,例如电话区号、人口、面积、时区、边界等:

    图 7.25:dict_keys 的输出

    图 7.25:dict_keys 的输出

    打印所有数据元素

    在我们有字典可用的情况下,这项任务极其简单!我们只需要遍历字典,逐个打印键/项对:

    for k,v in y.items():
        print("{}: {}".format(k,v))
    

    输出如下:

    图 7.26:使用 dict 的输出

    图 7.26:使用 dict 的输出

    注意到字典中的项不是同一类型的,也就是说,它们不是相似的对象。有些是浮点数,例如面积,许多是简单的字符串,但有些是列表或甚至是列表的列表!

    这在 JSON 数据中相当常见。JSON 的内部数据结构可以是任意复杂和多层级的,也就是说,你可以有一个字典,其中包含列表的字典的字典的列表的列表……等等。

    注意

    因此,很明显,没有针对 JSON 数据格式的通用方法或处理函数,你必须根据你的特定需求编写自定义循环和函数来从这种字典对象中提取数据。

    现在,我们将编写一个小循环来提取瑞士使用的语言。首先,让我们仔细检查字典,看看语言数据在哪里:

    图 7.27:标签

    因此,数据嵌套在一个字典的列表中,通过主字典的特定键来访问。

    我们可以编写简单的两行代码来提取这些数据:

    for lang in y['languages']:
        print(lang['name'])
    

    输出如下:

    图 7.28:显示语言的输出

    图 7.28:显示语言的输出

    使用提取包含关键信息的 DataFrame 的函数

    在这里,我们感兴趣的是编写一个函数,该函数可以接受一个国家列表,并返回一个包含一些关键信息的 pandas DataFrame:

    • 首都

    • 地区

    • 子区域

    • 人口

    • 纬度/经度

    • 面积

    • 基尼系数

    • 时区

    • 货币

    • 语言

      注意

      这是在实际数据处理任务中通常期望你编写的包装函数,即一个实用函数,它可以接受用户参数,并输出一个包含从互联网上提取的关键信息的有用数据结构(或小型数据库类型对象)。

    我们首先展示整个函数,然后讨论一些关于它的关键点。这是一段稍微复杂且较长的代码。然而,基于你基于 Python 的数据处理知识,你应该能够仔细检查这个函数并理解它在做什么:

    import pandas as pd
    import json
    def build_country_database(list_country):
        """
        Takes a list of country names.
        Output a DataFrame with key information about those countries.
        """
        # Define an empty dictionary with keys
        country_dict={'Country':[],'Capital':[],'Region':[],'Sub-region':[],'Population':[],
                      'Lattitude':[],'Longitude':[],'Area':[],'Gini':[],'Timezones':[],
                      'Currencies':[],'Languages':[]}
    

    注意

    代码在这里被截断。请在此 GitHub 链接和代码包文件夹链接中找到整个代码 github.com/TrainingByPackt/Data-Wrangling-with-Python/blob/master/Chapter07/Exercise93-94/Chapter%207%20Topic%203%20Exercises.ipynb

    下面是这个函数的一些关键点:

    • 它首先构建一个空的列表字典。这是最终传递给 pandas DataFrame 方法的格式,该方法可以接受这种格式并返回一个带有列名设置为字典键名的漂亮 DataFrame。

    • 我们使用之前定义的 get_country_data 函数来提取用户定义列表中每个国家的数据。为此,我们只需遍历列表并调用此函数。

    • 我们检查 get_country_data 函数的输出。如果由于某种原因它返回一个 None 对象,我们将知道 API 读取没有成功,并且我们将打印出一条合适的消息。再次强调,这是一个错误处理机制的例子,你必须在代码中包含它们。没有这样的小错误检查代码,你的应用程序将不足以应对偶尔的错误输入或 API 故障!

    • 对于许多数据类型,我们只需从主 JSON 字典中提取数据并将其追加到我们数据字典中相应的列表中。

    • 然而,对于特殊的数据类型,例如时区、货币和语言,我们编写一个特殊的循环来提取数据而不会出错。

    • 我们还注意这些特殊数据类型可能有可变长度,也就是说,一些国家可能有多种官方语言,但大多数只有一个条目。因此,我们检查列表的长度是否大于一个,并相应地处理数据。

    练习 94:通过构建国家信息的小型数据库来测试函数

    最后,我们通过传递一个国家名称列表来测试这个函数:

    1. 为了测试其鲁棒性,我们传入一个错误的名字——例如,在这种情况下是“姜黄”!

      看看输出……它检测到对于错误的条目没有返回任何数据,并打印出一条合适的消息。关键是,如果你在函数中没有错误检查和处理代码,那么它将在该条目上停止执行,并且不会返回预期的迷你数据库。为了避免这种行为,这种错误处理代码是无价的:

      图 7.29:错误的条目被突出显示
    2. 最后,输出是一个 pandas DataFrame,如下所示:

    图 7.30:正确提取的数据

    图 7.30:正确提取的数据

    正则表达式(RegEx)基础

    正则表达式或regex用于确定给定字符序列 a(字符串)中是否存在模式。它们有助于操作文本数据,这对于涉及文本挖掘的数据科学项目通常是先决条件。

    正则表达式在网页抓取中的应用

    网页通常充满了文本,虽然BeautifulSoup或 XML 解析器中有些方法可以提取原始文本,但没有用于智能分析这些文本的方法。如果你作为一个数据整理员,正在寻找特定的数据(例如,特殊格式的电子邮件 ID 或电话号码),你必须在大规模语料库上进行大量字符串操作来提取电子邮件 ID 或电话号码。正则表达式非常强大,通过它们可以搜索任意长度的通配符复杂文本模式,因此可以节省数据整理专业人员大量时间和精力。

    正则表达式本身就像是一种小程序设计语言,其常见思想不仅用于 Python,还用于所有广泛使用的 Web 应用程序语言,如 JavaScript、PHP、Perl 等。Python 中的正则表达式模块是内置的,你可以使用以下代码导入它:

    import re
    

    练习 95:使用 match 方法检查模式是否与字符串/序列匹配

    最常见的正则表达式方法之一是match。这用于检查字符串开头是否存在精确或部分匹配(默认情况下):

    1. 导入正则表达式模块:

      import re
      
    2. 定义一个字符串和一个模式:

      string1 = 'Python'
      pattern = r"Python"
      
    3. 编写一个条件表达式来检查匹配:

      if re.match(pattern,string1):
          print("Matches!")
      else:
          print("Doesn't match.")
      

      前面的代码应该给出肯定回答,即“匹配!”。

    4. 使用仅第一个字母不同且转换为小写的字符串进行测试:

      string2 = 'python'
      if re.match(pattern,string2):
          print("Matches!")
      else:
          print("Doesn't match.")
      

      输出如下:

       Doesn't match.
      

    使用编译方法创建正则表达式程序

    在程序或模块中,如果我们正在大量使用特定的模式,那么使用compile方法创建正则表达式程序并调用该程序的方法会更好。

    下面是如何编译正则表达式程序的方法:

    prog = re.compile(pattern)
    prog.match(string1)
    

    输出如下:

     <_sre.SRE_Match object; span=(0, 6), match='Python'>
    

    此代码生成一个SRE.Match对象,其span为(0,6)和匹配的字符串为'Python'。这里的 span 简单地表示匹配模式的起始和结束索引。这些索引在文本挖掘程序中可能很有用,后续代码可以使用这些索引进行进一步搜索或决策。我们稍后会看到一些例子。

    练习 96:将程序编译以匹配对象

    编译对象的行为类似于函数,如果模式不匹配,则返回 None。在这里,我们将通过编写一个简单的条件来检查这一点。这个概念在稍后编写一个小型实用函数来检查正则表达式编译程序返回的对象类型并相应地处理时会很有用。我们无法确定一个模式是否会匹配给定的字符串,或者它是否会在文本的语料库中(如果我们正在搜索文本中的任何位置)出现。根据情况,我们可能会遇到 Match 对象或 None 作为返回值,我们必须优雅地处理这种情况:

    #string1 = 'Python'
    #string2 = 'python'
    #pattern = r"Python"
    
    1. 使用正则表达式的 compile 函数:

      prog = re.compile(pattern)
      
    2. 与第一个字符串进行匹配:

      if prog.match(string1)!=None:
          print("Matches!")
      else:
          print("Doesn't match.")
      

      输出如下:

       Matches!
      
    3. 与第二个字符串进行匹配:

      if prog.match(string2)!=None:
          print("Matches!")
      else:
          print("Doesn't match.")
      

      输出如下:

       Doesn't match.
      

    练习 97:在匹配中使用附加参数以检查位置匹配

    默认情况下,match 在给定字符串的开始处查找模式匹配。但有时,我们需要检查字符串中的特定位置的匹配:

    1. 将第二个位置匹配为 y

      prog = re.compile(r'y')
      prog.match('Python',pos=1)
      

      输出如下:

       <_sre.SRE_Match object; span=(1, 2), match='y'>
      
    2. pos=2 开始检查名为 thon 的模式,即第三个字符:

      prog = re.compile(r'thon')
      prog.match('Python',pos=2)
      

      输出如下:

       <_sre.SRE_Match object; span=(2, 6), match='thon'>
      
    3. 使用以下命令在另一个字符串中查找匹配项:

      prog.match('Marathon',pos=4)
      

      输出如下:

      <_sre.SRE_Match object; span=(4, 8), match='thon'>
      

    查找列表中以 "ing" 结尾的单词数量

    假设我们想找出一个给定的字符串是否有最后三个字母:'ing'。这种查询可能会出现在文本分析/文本挖掘程序中,有人对查找现在进行时态单词的实例感兴趣,这些单词很可能以 'ing' 结尾。然而,其他名词也可能以 'ing' 结尾(正如我们将在本例中看到的那样):

    prog = re.compile(r'ing')
    words = ['Spring','Cycling','Ringtone']
    

    创建一个 for 循环以查找以 'ing' 结尾的单词:

    for w in words:
        if prog.match(w,pos=len(w)-3)!=None:
            print("{} has last three letters 'ing'".format(w))
        else:
            print("{} does not have last three letter as 'ing'".format(w))
    

    输出如下:

     Spring has last three letters 'ing'
     Cycling has last three letters 'ing'
     Ringtone does not have last three letter as 'ing'
    

    注意

    它看起来很简单,你可能想知道为什么需要使用特殊的正则表达式模块来做这件事。简单的字符串方法应该就足够了。是的,对于这个特定的例子来说,这样做是可以的,但使用正则表达式的全部意义在于能够使用非常复杂的字符串模式,这些模式在用简单的字符串方法编写时并不明显。我们很快就会看到正则表达式与字符串方法相比的真正威力。但在那之前,让我们探索另一个最常用的方法,称为 search

    练习 98:正则表达式中的搜索方法

    Searchmatch 是相关概念,它们都返回相同的 Match 对象。它们之间的真正区别在于 match 只对第一个匹配项有效(要么在字符串的开始处,要么在指定的位置,就像我们在前面的练习中看到的那样),而 search 在字符串的任何位置查找模式,如果找到匹配项,则返回相应的位置:

    1. 使用 compile 方法查找匹配的字符串:

      prog = re.compile('ing')
      if prog.match('Spring')==None:
          print("None")
      
    2. 输出如下:

       None
      
    3. 使用以下命令搜索字符串:

      prog.search('Spring')
      <_sre.SRE_Match object; span=(3, 6), match='ing'>
      prog.search('Ringtone')
      <_sre.SRE_Match object; span=(1, 4), match='ing'>
      

      如您所见,match 方法对于输入 spring 返回 None,因此我们必须编写代码来显式地打印出来(因为在 Jupyter 笔记本中,对于 None 对象将不会显示任何内容)。但 search 方法返回一个 Match 对象,其 span=(3,6) 表示它找到了跨越这些位置的 ing 模式。

    类似地,对于 Ringtone 字符串,它找到匹配的正确位置并返回 span=(1,4)

    练习 99:使用 Match 对象的 span 方法定位匹配模式的起始位置

    通过现在您应该理解,Match 对象中包含的 span 对于定位模式在字符串中出现的确切位置非常有用。

    1. 使用模式 ing 初始化 prog

      prog = re.compile(r'ing')
      words = ['Spring','Cycling','Ringtone']
      
    2. 创建一个函数来返回匹配的起始和结束位置的元组。

      for w in words:
          mt = prog.search(w)
          # Span returns a tuple of start and end positions of the match
          start_pos = mt.span()[0] # Starting position of the match
          end_pos = mt.span()[1] # Ending position of the match
      
    3. 打印以 ing 结尾的单词在起始或结束位置。

          print("The word '{}' contains 'ing' in the position {}-{}".format(w,start_pos,end_pos))
      

    输出如下:

     The word 'Spring' contains 'ing' in the position 3-6
     The word 'Cycling' contains 'ing' in the position 4-7
     The word 'Ringtone' contains 'ing' in the position 1-4
    

    练习 100:使用 search 进行单字符模式匹配的示例

    现在,我们将通过各种有用模式的示例来真正开始学习正则表达式的使用。首先,我们将探索单字符匹配。我们还将使用 group 方法,它本质上以字符串格式返回匹配的模式,这样我们就可以轻松地打印和处理它:

    1. 点(.)匹配任何单个字符,除了换行符:

      prog = re.compile(r'py.')
      print(prog.search('pygmy').group())
      print(prog.search('Jupyter').group())
      

      输出如下:

       pyg
       pyt
      
    2. \w(小写 w)匹配任何单个字母、数字或下划线:

      prog = re.compile(r'c\wm')
      print(prog.search('comedy').group())
      print(prog.search('camera').group())
      print(prog.search('pac_man').group())
      print(prog.search('pac2man').group())
      

      输出如下:

       com
       cam
       c_m
       c2m
      
    3. \W(大写 W)匹配任何未被 \w 覆盖的内容:

      prog = re.compile(r'4\W1')
      print(prog.search('4/1 was a wonderful day!').group())
      print(prog.search('4-1 was a wonderful day!').group())
      print(prog.search('4.1 was a wonderful day!').group())
      print(prog.search('Remember the wonderful day 04/1?').group())
      

      输出如下:

       4/1
       4-1
       4.1
       4/1
      
    4. \s(小写 s)匹配单个空白字符,例如空格、换行符、制表符或回车:

      prog = re.compile(r'Data\swrangling')
      print(prog.search("Data wrangling is cool").group())
      print("-"*80)
      print("Data\twrangling is the full string")
      print(prog.search("Data\twrangling is the full string").group())
      print("-"*80)
      print("Data\nwrangling is the full string")
      print(prog.search("Data\nwrangling").group())
      

      输出如下:

      Data wrangling
      ----------------------------------------------------------------------
      Data	wrangling is the full string
      Data	wrangling
      ----------------------------------------------------------------------
      Data
      wrangling is the full string
      Data
      wrangling
      
    5. \d 匹配数字 0 – 9:

      prog = re.compile(r"score was \d\d")
      print(prog.search("My score was 67").group())
      print(prog.search("Your score was 73").group())
      

      输出如下:

       score was 67
       score was 73
      

    练习 101:字符串开头或结尾模式匹配的示例

    在这个练习中,我们将使用字符串匹配模式。重点是找出模式是否存在于字符串的开头或结尾:

    1. 编写一个函数来处理找不到匹配的情况,即处理返回的 None 对象:

      def print_match(s):
          if prog.search(s)==None:
              print("No match")
          else:
              print(prog.search(s).group())
      
    2. 使用 ^(上标)来匹配字符串开头的模式:

      prog = re.compile(r'^India')
      print_match("Russia implemented this law")
      print_match("India implemented that law")
      print_match("This law was implemented by India")
      The output is as follows: No match
       India
       No match
      
    3. 使用 $(美元符号)来匹配字符串结尾的模式:

      prog = re.compile(r'Apple$')
      print_match("Patent no 123456 belongs to Apple")
      print_match("Patent no 345672 belongs to Samsung")
      print_match("Patent no 987654 belongs to Apple")
      

      输出如下:

       Apple
       No match
       Apple
      

    练习 102:多字符模式匹配的示例

    现在,我们将转向更令人兴奋和有用的多字符匹配模式,通过示例来展示。现在,您应该开始看到并欣赏正则表达式的真正力量。

    注意:

    对于这些示例和练习,也要尝试思考如果没有正则表达式,您会如何实现它们,即通过使用简单的字符串方法和您能想到的任何其他逻辑。然后,将那种解决方案与使用正则表达式实现的解决方案进行比较,以体现简洁性和效率。

    1. 使用 * 来匹配前面 RE 的 0 或更多重复:

      prog = re.compile(r'ab*')
      print_match("a")
      print_match("ab")
      print_match("abbb")
      print_match("b")
      print_match("bbab")
      print_match("something_abb_something")
      

      输出如下:

       a
       ab
       abbb
       No match
       ab
       abb
      
    2. 使用 + 会导致结果正则表达式匹配前面正则表达式的 1 或更多重复:

      prog = re.compile(r'ab+')
      print_match("a")
      print_match("ab")
      print_match("abbb")
      print_match("b")
      print_match("bbab")
      print_match("something_abb_something")
      

      输出如下:

       No match
       ab
       abbb
       No match
       ab
       abb
      
    3. ? 使得结果 RE 精确匹配前面 RE 的 0 或 1 次重复:

      prog = re.compile(r'ab?')
      print_match("a")
      print_match("ab")
      print_match("abbb")
      print_match("b")
      print_match("bbab")
      print_match("something_abb_something")
      

      输出如下:

       a
       ab
       ab
       No match
       ab
       ab
      

    练习 103:贪婪与非贪婪匹配

    regex 中模式匹配的标准(默认)模式是贪婪的,也就是说,程序尝试尽可能多地匹配。有时,这种行为是自然的,但在某些情况下,你可能希望最小化匹配:

    1. 匹配字符串的贪婪方式如下:

      prog = re.compile(r'<.*>')
      print_match('<a> b <c>')
      

      输出如下:

       <a> b <c>
      
    2. 因此,前面找到的 regex 匹配了具有 <> 模式的两个标签,但如果我们只想匹配第一个标签并停止,我们可以通过在任意 regex 表达式后插入 ? 来使其非贪婪:

      prog = re.compile(r'<.*?>')
      print_match('<a> b <c>')
      

      输出如下:

       <a>
      

    练习 104:控制重复以匹配

    在许多情况下,我们希望精确控制文本中要匹配的模式重复次数。这可以通过几种方式完成,以下我们将展示一些示例:

    1. {m} 指定精确匹配 m 次的 RE。更少的匹配会导致不匹配并返回 None

      prog = re.compile(r'A{3}')
      print_match("ccAAAdd")
      print_match("ccAAAAdd")
      print_match("ccAAdd")
      

      输出如下:

       AAA
       AAA
       No match
      
    2. {m,n} 指定精确匹配 mn 次的 RE

      prog = re.compile(r'A{2,4}B')
      print_match("ccAAABdd")
      print_match("ccABdd")
      print_match("ccAABBBdd")
      print_match("ccAAAAAAABdd")
      

      输出如下:

       AAAB
       No match
       AAB
       AAAAB
      
    3. 省略 m 指定下界为零:

      prog = re.compile(r'A{,3}B')
      print_match("ccAAABdd")
      print_match("ccABdd")
      print_match("ccAABBBdd")
      print_match("ccAAAAAAABdd")
      

      输出如下:

       AAAB
       AB
       AAB
       AAAB
      
    4. 省略 n 指定无限上界:

      prog = re.compile(r'A{3,}B')
      print_match("ccAAABdd")
      print_match("ccABdd")
      print_match("ccAABBBdd")
      print_match("ccAAAAAAABdd")
      

      输出如下:

       AAAB
       No match
       No match
       AAAAAAAB
      
    5. {m,n}? 指定非贪婪方式匹配 RE 的 mn 次复制:

      prog = re.compile(r'A{2,4}')
      print_match("AAAAAAA")
      prog = re.compile(r'A{2,4}?')
      print_match("AAAAAAA")
      

      输出如下:

       AAAA
       AA
      

    练习 105:匹配字符集

    为了匹配任意复杂的模式,我们需要能够将字符的逻辑组合作为一个整体包括进来。Regex 给我们提供了这种能力:

    1. 以下示例展示了 regex 的这种用法。[x,y,z] 匹配 x、y 或 z:

      prog = re.compile(r'[A,B]')
      print_match("ccAd")
      print_match("ccABd")
      print_match("ccXdB")
      print_match("ccXdZ")
      

      输出将如下:

       A
       A
       B
       No match
      

      可以使用 - 在集合内匹配字符范围。这是最广泛使用的 regex 技术之一!

    2. 假设我们想要从文本中提取电子邮件地址。电子邮件地址通常具有以下形式 <some name>@<some domain name>.<some domain identifier>

      prog = re.compile(r'[a-zA-Z]+@+[a-zA-Z]+\.com')
      print_match("My email is coolguy@xyz.com")
      print_match("My email is coolguy12@xyz.com")
      

      输出如下:

       coolguy@xyz.com
       No match
      

      看看 [ … ] 内部的 regex 模式。它是 'a-zA-Z'。这涵盖了所有字母,包括小写和大写!通过这个简单的 regex,你能够匹配任何(纯)字母字符串作为电子邮件该部分的匹配。接下来,下一个模式是 '@',它通过一个 '+' 字符添加到前面的 regex 中。这是构建复杂 regex 的方法:通过添加/堆叠单个 regex 模式。我们也使用相同的 [a-zA-Z]作为电子邮件域名,并在末尾添加'.com' 以完成模式,使其成为一个有效的电子邮件地址。为什么是 \.?因为,单独的 DOT (.) 在 regex 中用作特殊修饰符,但在这里我们只想使用 DOT (.) 作为 DOT (.),而不是修饰符。因此,我们需要在它之前加上一个 \

    3. 因此,使用这个 regex,我们可以完美地提取第一个电子邮件地址,但第二个却得到了 'No match'

    4. 第二个电子邮件 ID 发生了什么?

    5. 正则表达式无法捕获它,因为它在名称中包含了数字 '12'!这个模式没有被表达式 [a-zA-Z] 捕获。

    6. 让我们改变它并添加数字:

      prog = re.compile(r'[a-zA-Z0-9]+@+[a-zA-Z]+\.com')
      print_match("My email is coolguy12@xyz.com")
      print_match("My email is coolguy12@xyz.org")
      

      输出如下:

       coolguy12@xyz.com
       No match
      

      现在,我们完美地捕获了第一个电子邮件 ID。但第二个电子邮件 ID 发生了什么?再次,我们得到了一个不匹配。原因是我们在那个电子邮件中将 .com 改为了 .org,而在我们的正则表达式表达式中,这部分被硬编码为 .com,因此它没有找到匹配项。

    7. 让我们在下面的正则表达式中尝试解决这个问题:

      prog = re.compile(r'[a-zA-Z0-9]+@+[a-zA-Z]+\.+[a-zA-Z]{2,3}')
      print_match("My email is coolguy12@xyz.org")
      print_match("My email is coolguy12[AT]xyz[DOT]org")
      

      输出如下:

       coolguy12@xyz.org
       No match
      
    8. 在这个正则表达式中,我们使用了这样一个事实,即大多数域名标识符有 2 到 3 个字符,所以我们使用了 [a-zA-Z]{2,3} 来捕获它。

    第二个电子邮件 ID 发生了什么?这是一个示例,说明你可以进行一些小的调整,以领先于想要从在线论坛或其他文本库中抓取并提取你的电子邮件 ID 的推销员。如果你不希望你的电子邮件被找到,你可以将 @ 改为 [AT],将 . 改为 [DOT],并希望这可以击败一些正则表达式技术(但不是所有技术)!

    练习 106:使用 OR 操作符在正则表达式中的使用

    因为正则表达式模式本身就像复杂而紧凑的逻辑构造器,所以当我们需要构建更复杂的程序时,我们想要将它们组合起来,这是完全有道理的。我们可以通过使用 | 操作符来实现这一点:

    1. 以下示例演示了使用 OR 操作符的使用:

      prog = re.compile(r'[0-9]{10}')
      print_match("3124567897")
      print_match("312-456-7897")
      

      输出如下:

       3124567897
       No match
      

      因此,在这里,我们试图提取可能是电话号码的 10 位数字的模式。注意模式中使用 {10} 来表示正好 10 位数字。但第二个数字由于明显的原因无法匹配——它在数字组之间插入了破折号。

    2. 使用多个较小的正则表达式,并通过以下命令逻辑组合它们:

      prog = re.compile(r'[0-9]{10}|[0-9]{3}-[0-9]{3}-[0-9]{4}')
      print_match("3124567897")
      print_match("312-456-7897")
      

      输出如下:

       3124567897
       312-456-7897
      

      电话号码以无数种方式书写,如果你在网上搜索,你会看到非常复杂的正则表达式示例(不仅是在 Python 中,还在其他广泛使用的语言中,如 JavaScript、C++、PHP、Perl 等,用于捕获电话号码)。

    3. 创建四个字符串并在它们上执行 print_match

      p1= r'[0-9]{10}'
      p2=r'[0-9]{3}-[0-9]{3}-[0-9]{4}'
      p3 = r'\([0-9]{3}\)[0-9]{3}-[0-9]{4}'
      p4 = r'[0-9]{3}\.[0-9]{3}\.[0-9]{4}'
      pattern= p1+'|'+p2+'|'+p3+'|'+p4
      prog = re.compile(pattern)
      print_match("3124567897")
      print_match("312-456-7897")
      print_match("(312)456-7897")
      print_match("312.456.7897")
      

      输出如下:

       3124567897
       312-456-7897
       (312)456-7897
       312.456.7897
      

    findall 方法

    本章我们将学习的最后一个正则表达式方法是 findall。本质上,它是一个搜索和聚合方法,也就是说,它将所有与正则表达式模式匹配的实例放入给定的文本中,并以列表的形式返回它们。这非常有用,因为我们只需计算返回列表的长度就可以计算出现的次数,或者我们可以根据需要逐个选择并使用返回的模式匹配词。

    注意,尽管在本章中我们给出了单个句子的简短示例,但使用正则表达式时,你通常会处理大量的文本。

    在这些情况下,你可能会从单个正则表达式模式搜索中获得许多匹配项。对于所有这些情况,findall 方法将是最有用的:

    ph_numbers = """Here are some phone numbers.
    Pick out the numbers with 312 area code: 
    312-423-3456, 456-334-6721, 312-5478-9999, 
    312-Not-a-Number,777.345.2317, 312.331.6789"""
    print(ph_numbers)
    re.findall('312+[-\.][0-9-\.]+',ph_numbers)
    

    输出如下:

     Here are some phone numbers.
    Pick out the numbers with 312 area code: 
    312-423-3456, 456-334-6721, 312-5478-9999, 
    312-Not-a-Number,777.345.2317, 312.331.6789
     ['312-423-3456', '312-5478-9999', '312.331.6789']
    

    活动 9:从古腾堡提取前 100 本电子书

    古腾堡项目通过鼓励志愿者努力数字化和归档文化作品来鼓励电子书创作和分发。本活动的目的是抓取古腾堡前 100 本电子书的 URL 以识别电子书的链接。它使用 BeautifulSoup4 解析 HTML 并使用正则表达式代码来识别前 100 本电子书的文件编号。

    如果您想将书籍下载到本地驱动器,可以使用那些书籍 ID 号。

    前往提供的 Jupyter 笔记本(在 GitHub 仓库中)进行此活动的操作。

    这些步骤将帮助您解决此活动:

    1. 导入必要的库,包括 regexbeautifulsoup

    2. 检查 SSL 证书。

    3. 从 URL 读取 HTML。

    4. 编写一个小函数来检查网络请求的状态。

    5. 解码响应并将此传递给 BeautifulSoup 进行 HTML 解析。

    6. 找到所有的 href 标签并将它们存储在链接列表中。检查列表看起来像什么——打印前 30 个元素。

    7. 使用正则表达式在这些链接中找到数字。这些是前 100 本电子书的文件编号。

    8. 初始化一个空列表来存储文件编号,在适当的范围内,并使用 regexhref 字符串中找到数字,使用 findall 方法。

    9. soup 对象的文本看起来是什么样子?使用 .text 方法并仅打印前 2,000 个字符(不要打印整个内容,因为它太长了)。

    10. 在从 soup 对象提取的文本(使用正则表达式)中搜索以找到前 100 本电子书的名称(昨天的排名)。

    11. 创建一个起始索引。它应该指向文本 Top 100 Ebooks yesterday。使用 soup.text 的 splitlines 方法。它将 soup 对象的文本行分割成行。

    12. 循环 1-100,将下一 100 行的字符串添加到这个临时列表中。提示:使用 splitlines 方法。

    13. 使用正则表达式从名称字符串中提取仅文本并将其附加到空列表中。使用 matchspan 找到索引并使用它们。

      注意

      此活动的解决方案可以在第 315 页找到。

    活动 10:通过读取 API 构建自己的电影数据库

    在这个活动中,您将通过与免费 API 进行通信和接口来构建一个完整的电影数据库。您将了解在程序尝试访问 API 时必须使用的唯一用户密钥。此活动将教授您有关使用 API 的一般章节,这对于其他非常流行的 API 服务(如 Google 或 Twitter)来说相当常见。因此,完成此练习后,您将自信地编写更复杂的程序来从这些服务中抓取数据。

    本活动的目标如下:

    • 从网络(OMDb 数据库)检索并打印关于一部电影(标题由用户输入)的基本数据

    • 如果找到电影的海报,它将下载文件并保存在用户指定的位置

    这些步骤将帮助您解决这个活动:

    1. 导入 urllib.requesturllib.parseurllib.errorjson

    2. 从同一文件夹中存储的 JSON 文件中加载秘密 API 密钥(您必须从 OMDb 网站获取一个并使用它;它有每日 1,000 次的限制),通过使用 json.loads() 将其存储在一个变量中。

    3. 获取一个密钥并将其存储在 JSON 中的 APIkeys.json

    4. 打开 APIkeys.json 文件。

    5. 将 OMDb 站点(www.omdbapi.com/?)作为一个字符串赋值给一个变量。

    6. 创建一个名为 apikey 的变量,其值为 URL 的最后一部分(&apikey=secretapikey),其中 secretapikey 是您自己的 API 密钥。

    7. 编写一个名为 print_json 的实用函数,用于从 JSON 文件(我们将从该门户获取)打印电影数据。

    8. 编写一个实用函数,根据 JSON 数据集中的信息下载电影的海报,并将其保存在您的本地文件夹中。使用 os 模块。海报数据存储在 JSON 键 Poster 中。使用 Python 命令打开文件并写入海报数据。完成后关闭文件。此函数将海报数据保存为图像文件。

    9. 编写一个名为 search_movie 的实用函数,通过电影名称搜索电影,打印下载的 JSON 数据,并将电影海报保存在本地文件夹中。使用 try-except 循环。使用之前创建的 serviceurlapikey 变量。您必须将包含键 t 和电影名称作为相应值的字典传递给 urllib.parse.urlencode() 函数,然后将 serviceurlapikey 添加到函数的输出中,以构造完整的 URL。此 URL 将用于访问数据。JSON 数据有一个名为 Response 的键。如果是 True,则表示读取成功。在处理数据之前检查这一点。如果不成功,则打印 JSONError,其中将包含电影数据库返回的适当错误消息。

    10. 通过输入 Titanic 测试 search_movie 函数。

    11. 通过输入 "Random_error"(显然,这将找不到,你应该能够检查你的错误捕获代码是否正常工作)来测试 search_movie 函数。

      注意:

      这个活动的解决方案可以在第 320 页找到。

    摘要

    在本章中,我们探讨了与高级数据收集和网络抓取相关的重要概念和学习模块。我们首先使用两个最流行的 Python 库——requestsBeautifulSoup 从网页中读取数据。在这个任务中,我们利用了上一章关于 HTML 页面的一般结构和它们与 Python 代码交互的知识。在这个过程中,我们从维基百科主页中提取了有意义的资料。

    然后,我们学习了如何从 XML 和 JSON 文件中读取数据,这两种格式是网络中最广泛使用的两种数据流/交换格式。对于 XML 部分,我们向您展示了如何高效地遍历树形结构数据字符串以提取关键信息。对于 JSON 部分,我们将它与使用 API(应用程序接口)从网络中读取数据结合起来。我们使用的 API 是 RESTful 的,这是 Web API 的主要标准之一。

    在本章的结尾,我们详细练习了使用正则表达式技术在复杂的字符串匹配问题中提取有用信息,这些信息是从 HTML 解析的大规模杂乱文本语料库中获得的。这一章对于你在数据处理职业生涯中的字符串和文本处理任务将非常有用。

    在下一章中,我们将学习如何使用 Python 进行数据库操作。

    第九章:第八章

    RDBMS 和 SQL

    学习目标

    到本章结束时,你将能够:

    • 使用 Python 将 RDBMS 的基本知识应用于查询数据库

    • 将 SQL 中的数据转换为 pandas DataFrame

    本章解释了数据库的概念,包括它们的创建、操作和控制,以及将表转换为 pandas DataFrame。

    简介

    我们数据之旅的本章专注于RDBMS(关系数据库管理系统)和SQL(结构化查询语言)。在前一章中,我们从文件中存储和读取数据。在本章中,我们将读取结构化数据,设计数据访问,并为数据库创建查询接口。

    数据已经以 RDBMS 格式存储多年。背后的原因如下:

    • RDBMS 是存储、管理和检索数据的一种非常安全的方式。

    • 它们有一个坚实的数学基础(关系代数和关系演算),并且提供了一个高效直观的声明性语言——SQL,以便轻松交互。

    • 几乎每种语言都有丰富的库来与不同的 RDBMS 交互,并且使用它们的技巧和方法已经过良好的测试和理解。

    • 扩展关系型数据库管理系统(RDBMS)是一个相当熟悉的任务,并且有许多受过良好训练、经验丰富的专业人士(数据库管理员或数据库管理员)来做这项工作。

    如以下图表所示,DBMS 市场很大。此图表是基于Gartner, Inc.2016年进行的市调产生的:

    图 8.1 2016 年商业数据库市场份额

    图 8.1 2016 年商业数据库市场份额

    在本章中,我们将学习和探索数据库和数据库管理系统的一些基本和基本概念。

    RDBMS 和 SQL 的复习

    RDBMS 是一种管理数据(对于最终用户以表格形式表示)的软件,它使用 Codd 的关系模型构建,并存储在物理硬盘上。我们今天遇到的数据库大多数都是 RDBMS。近年来,整个行业向一种新型的数据库管理系统转变,称为NoSQLMongoDBCouchDBRiak等)。这些系统虽然在某些方面遵循 RDBMS 的一些规则,但在大多数情况下拒绝或修改了它们。

    RDBMS 是如何结构的?

    RDBMS 结构由三个主要元素组成,即存储引擎、查询引擎和日志管理。以下是显示 RDBMS 结构的图:

    图 8.2 RDBMS 结构

    图 8.2 RDBMS 结构

    以下是一些 RDBMS 结构的主要概念:

    • 存储引擎:这是 RDBMS 中负责以高效方式存储数据并在请求时以高效方式返回数据的部分。作为 RDBMS 系统的最终用户(应用程序开发人员被视为 RDBMS 的最终用户),我们永远不会需要直接与这一层交互。

    • 查询引擎:这是 RDBMS 的部分,允许我们创建数据对象(表、视图等),操作它们(创建和删除列,创建/删除/更新行等),并使用简单而强大的语言查询它们(读取行)。

    • 日志管理:RDBMS 的这部分负责创建和维护日志。如果你想知道为什么日志如此重要,那么你应该了解一下在现代 RDBMS(如 PostgreSQL)中如何使用所谓的 Write Ahead Log(或简称为 WAL)来处理复制和分区。

    我们将在本章中关注查询引擎。

    SQL

    结构化查询语言Structured Query Language 或 SQL,通常发音为 sequel)是一种领域特定语言,最初是基于 E.F. Codd 的关系模型设计的,并且在当今的数据库中广泛用于定义、插入、操作和从数据库中检索数据。它可以进一步细分为四个较小的子语言,即 DDL(数据定义语言)、DML(数据操作语言)、DQL(数据查询语言)和 DCL(数据控制语言)。使用 SQL 有几个优点,其中一些如下:

    • 它基于一个坚实的数学框架,因此很容易理解。

    • 它是一种声明性语言,这意味着我们实际上从未告诉它如何完成其工作。我们几乎总是告诉它要做什么。这使我们从编写数据管理自定义代码的大负担中解放出来。我们可以更专注于我们试图解决的查询问题,而不是烦恼于如何创建和维护数据存储。

    • 它为你提供了一个快速且易于阅读的方式来处理数据。

    • SQL 提供了单次查询获取多个数据片段的现成方法。

    下一个主题的主要关注领域将是 DDL、DML 和 DQL。DCL 部分更多是针对数据库管理员的。

    • CREATE TABLEDROP TABLEALTER TABLE

      注意

      注意大写字母的使用。这并不是一个规范,你可以使用小写字母,但这是一个广泛遵循的约定,我们将在本书中使用它。

    • INSERT INTODELETE FROMUPDATE

    • SELECT 命令。我们还将看到并使用主键、外键、索引、连接等概念。

    一旦你在数据库中定义并插入数据,它可以表示如下:

    图 8.3 显示示例数据的表

    图 8.3 显示示例数据的表

    关于 RDBMS 另一件需要记住的事情是关系。通常,在一个表中,我们有一列或多列,每行在表中都有唯一值。我们称它们为该表的 主键。我们应该意识到,我们将在行之间遇到唯一值,这些值不是主键。它们与主键之间的主要区别是主键不能为空。

    通过使用一个表的主键,并在另一个表中将其作为外键提及,我们可以在两个表之间建立关系。某个表可以与任何有限数量的表相关联。关系可以是 1:1,这意味着第二个表的每一行都与第一个表的唯一一行相关联,或者 1:N,N:1,或者 N:M。关系的一个例子如下:

    图 8.4 显示关系的图

    图 8.4 显示关系的图

    通过这个简短的复习,我们现在可以开始动手练习,并编写一些 SQL 来存储和检索数据。

    使用 RDBMS(MySQL/PostgreSQL/SQLite)

    在这个主题中,我们将关注如何编写一些基本的 SQL 命令,以及如何从 Python 连接到数据库并在 Python 中有效地使用它。我们将选择 SQLite 作为数据库。还有其他数据库,例如 OracleMySQLPostgresqlDB2。你将要学习的主要技巧不会根据你使用的数据库而改变。但是,对于不同的数据库,你需要安装不同的第三方 Python 库(例如,PostgresqlPsycopg2 等)。它们之所以都以相同的方式(除了某些小细节外)运行,是因为它们都遵循 PEP249(通常称为 Python DB API 2)。

    这是一个很好的标准化,在从一种 RDBMS 转移到另一种时,它能节省我们很多麻烦。

    注意

    大多数用 Python 编写并使用某种 RDBMS 作为数据存储的行业标准项目,通常依赖于一个 ORM 或对象关系映射器。ORM 是一个高级库,在处理 RDBMS 时使许多任务变得更容易。它还提供了一个比在 Python 代码中编写原始 SQL 更 Pythonic 的 API。

    练习 107:在 SQLite 中连接到数据库

    在这个练习中,我们将探讨使用 Python 代码中的 RDBMS 的第一步。我们即将要做的是连接到数据库,然后关闭连接。我们还将了解如何最好地完成这项任务:

    1. 使用以下命令导入 Python 的 sqlite3 库:

      import sqlite3
      
    2. 使用 connect 函数连接到数据库。如果你对数据库有一些经验,你会注意到我们没有使用任何 服务器地址用户名密码 或其他凭证来连接到数据库。这是因为这些字段在 sqlite3 中不是强制性的,而在 PostgresqlMySQL 中则是。SQLite 的主要数据库引擎是嵌入式的:

      conn = sqlite3.connect("chapter.db")
      
    3. 关闭连接,如下所示:

      conn.close()
      

      这个 conn 对象是主要的连接对象,一旦我们想要与数据库交互,我们将来需要获取第二种类型的对象。我们需要小心关闭对数据库的任何打开连接。

    4. 使用与文件相同的 with 语句,连接到数据库,如下所示:

      with sqlite3.connect("chapter.db") as conn:
          pass
      

    在这个练习中,我们已经使用 Python 连接到数据库。

    练习 108:SQLite 中的 DDL 和 DML 命令

    在这个练习中,我们将查看如何创建一个表,并且我们还将向其中插入数据。

    如其名所示,DDL(数据定义语言)是提前与数据库引擎通信以定义数据外观的方式。数据库引擎根据提供的定义创建一个表对象,并对其进行准备。

    要在 SQL 中创建一个表,使用 CREATE TABLE SQL 子句。这需要表名和表定义。表名是数据库引擎用于查找和在未来所有事务中使用表的唯一标识符。它可以是一切(任何字母数字字符串),只要它是唯一的。我们将以(column_name_1 数据类型,column_name_2 数据类型,……)的形式添加表定义。出于我们的目的,我们将使用 textinteger 数据类型,但通常标准数据库引擎支持更多的数据类型,例如 float、double、日期时间、布尔值等。我们还需要指定一个主键。主键是一个唯一、非空的标识符,用于在表中唯一标识一行。在我们的例子中,我们使用电子邮件作为主键。主键可以是整数或文本。

    你需要知道的是,除非你对你刚刚执行的一系列操作(我们正式称之为“事务”)调用 commit,否则实际上不会执行任何操作并在数据库中反映出来。这个特性被称为原子性。实际上,为了使数据库成为行业标准(在实际生活中可用),它需要遵循 ACID(原子性、一致性、隔离性、持久性)特性:

    1. 使用 SQLite 的 connect 函数连接到 chapter.db 数据库,如下所示:

      with sqlite3.connect("chapter.db") as conn:
      

      注意

      一旦你添加了第 3 步中的代码片段,此代码就会生效。

    2. 通过调用 conn.cursor() 创建一个游标对象。游标对象作为与数据库通信的媒介。在 Python 中创建一个表,如下所示:

          cursor = conn.cursor()
          cursor.execute("CREATE TABLE IF NOT EXISTS user (email text, first_name text, last_name text, address text, age integer, PRIMARY KEY (email))")
      
    3. 按如下方式将行插入你创建的数据库中:

      cursor.execute("INSERT INTO user VALUES ('bob@example.com', 'Bob', 'Codd', '123 Fantasy lane, Fantasy City', 31)")
      cursor.execute("INSERT INTO user VALUES ('tom@web.com', 'Tom', 'Fake', '456 Fantasy lane, Fantasu City', 39)")
      
    4. 提交到数据库:

      conn.commit()
      

    这将创建一个表并将两行数据写入其中。

    从 SQLite 数据库中读取数据

    在前面的练习中,我们创建了一个表并在其中存储了数据。现在,我们将学习如何读取存储在此数据库中的数据。

    SELECT 子句非常强大,对于数据从业者来说,掌握 SELECT 以及与之相关的所有内容(例如条件、连接、分组等)非常重要。

    SELECT 后的 *** 告诉引擎从表中选择所有列。这是一个有用的缩写。我们还没有提到任何选择条件(例如年龄超过某个值、名字以某个字母序列开头等)。我们实际上是在告诉数据库引擎从表中选择所有行和所有列。如果我们有一个非常大的表,这将非常耗时且效率低下。因此,我们希望使用 LIMIT 子句来限制我们想要的行数。

    您可以使用 SQL 中的 SELECT 子句来检索数据,如下所示:

    with sqlite3.connect("chapter.db") as conn:
        cursor = conn.cursor()
        rows = cursor.execute('SELECT * FROM user')
        for row in rows:
            print(row)
    

    输出如下:

    图 8.5:SELECT 子句的输出

    使用 LIMITSELECT 子句的语法如下:

    SELECT * FROM <table_name> LIMIT 50;
    

    注意

    此语法是示例代码,在 Jupyter notebook 中无法工作。

    这将选择所有列,但只从表中选取前 50 行。

    练习 109:对数据库中存在的值进行排序

    在这个练习中,我们将使用 ORDER BY 子句按年龄对用户表的行进行排序:

    1. 按降序对 chapter.db 中的 age 进行排序,如下所示:

      with sqlite3.connect("chapter.db") as conn:
          cursor = conn.cursor()
          rows = cursor.execute('SELECT * FROM user ORDER BY age DESC')
          for row in rows:
              print(row)
      

      输出如下:

      图 8.6:按降序显示年龄的数据输出
    2. 按升序对 chapter.db 中的 age 进行排序,如下所示:

      with sqlite3.connect("chapter.db") as conn:
          cursor = conn.cursor()
          rows = cursor.execute('SELECT * FROM user ORDER BY age')
          for row in rows:
              print(row)
      
    3. 输出如下:

    图 8.7:按升序显示年龄的数据输出

    注意,我们不需要指定 ASC 来按升序排序。

    练习 110:修改表结构并更新新字段

    在这个练习中,我们将使用 ALTER 添加一个列,并使用 UPDATE 更新新添加列中的值。

    UPDATE 命令用于在插入后编辑/更新任何行。使用时请小心,因为没有选择性子句(如 WHERE)的 UPDATE 会影响整个表:

    1. 使用以下命令建立与数据库的连接:

      with sqlite3.connect("chapter.db") as conn:
          cursor = conn.cursor()
      
    2. user 表中添加另一列,并使用以下命令填充 null 值:

      cursor.execute("ALTER TABLE user ADD COLUMN gender text")
      
    3. 使用以下命令更新所有 gender 的值,使它们为 M

      cursor.execute("UPDATE user SET gender='M'")
      conn.commit()
      
    4. 要检查修改后的表,请执行以下命令:

      rows = cursor.execute('SELECT * FROM user')
      for row in rows:
              print(row)
      

    图 8.8:修改表后的输出

    我们已通过将所有用户的性别设置为 M(代表男性)来更新整个表。

    练习 111:在表中分组值

    在这个练习中,我们将学习一个我们已经在 pandas 中学习过的概念。这就是 GROUP BY 子句。GROUP BY 子句是一种用于从数据库中检索不同值并将它们放入单独桶的技术。

    以下图解说明了 GROUP BY 子句的工作原理:

    图 8.9:GROUP BY 子句在表上的说明

    在前面的图中,我们可以看到 Col3 列在所有行中只有两个唯一值,A 和 B。

    用于检查每个组所属行总数的命令如下:

    SELECT count(*), col3 FROM table1 GROUP BY col3
    

    向表中添加女性用户并根据性别进行分组:

    1. 向表中添加一个女性用户:

      cursor.execute("INSERT INTO user VALUES ('shelly@www.com', 'Shelly', 'Milar', '123, Ocean View Lane', 39, 'F')")
      
    2. 运行以下代码以查看按性别划分的计数:

      rows = cursor.execute("SELECT COUNT(*), gender FROM user GROUP BY gender")
      for row in rows:
              print(row)
      

      输出如下:

    图 8.10:GROUP BY 子句的输出

    数据库中的关系映射

    我们一直在使用单个表,对其进行修改,以及读取数据。然而,关系型数据库管理系统(RDBMS)的真正力量来自于处理不同对象(表)之间的关系。在本节中,我们将创建一个名为comments的新表,并以 1: N 的关系将其与用户表关联起来。这意味着一个用户可以有多个评论。我们将通过在comments表中添加user表的主键作为外键来实现这一点。这将创建一个 1: N 的关系。

    当我们将两个表进行关联时,我们需要指定数据库引擎在父表中的行被删除,且在另一表中存在许多子行时应该执行什么操作。正如我们可以在以下图中看到的那样,当我们删除用户表中的行 1 时,我们想知道问号所在位置会发生什么:

    图片

    图 8.11:关系说明

    在非 RDBMS 的情况下,这种情况可能会迅速变得难以管理和维护。然而,在使用 RDBMS 的情况下,我们只需要非常精确地告诉数据库引擎在这种情况下应该做什么。数据库引擎会为我们完成剩下的工作。我们使用ON DELETE来告诉引擎当父行被删除时,我们应该如何处理表中的所有行。以下代码说明了这些概念:

    with sqlite3.connect("chapter.db") as conn:
        cursor = conn.cursor()
        cursor.execute("PRAGMA foreign_keys = 1")
        sql = """
            CREATE TABLE comments (
                user_id text,
                comments text,
                FOREIGN KEY (user_id) REFERENCES user (email) 
                ON DELETE CASCADE ON UPDATE NO ACTION
            )
        """
        cursor.execute(sql)
        conn.commit()
    

    ON DELETE CASCADE行通知数据库引擎,当父行被删除时,我们希望删除所有子行。我们也可以为UPDATE定义操作。在这种情况下,对于UPDATE没有要执行的操作。

    FOREIGN KEY修饰符修改一个列定义(在这种情况下为user_id),并将其标记为外键,它与另一个表的主键(在这种情况下为email)相关联。

    你可能会注意到代码中看起来奇怪的cursor.execute("PRAGMA foreign_keys = 1")行。它之所以存在,仅仅是因为 SQLite 默认不使用正常的键外键功能。正是这一行启用了该功能。这是 SQLite 的典型做法,我们不需要为任何其他数据库使用它。

    comments表中添加行

    我们已经创建了一个名为comments的表。在本节中,我们将动态生成一个插入查询,如下所示:

    with sqlite3.connect("chapter.db") as conn:
        cursor = conn.cursor()
        cursor.execute("PRAGMA foreign_keys = 1")
        sql = "INSERT INTO comments VALUES ('{}', '{}')"
        rows = cursor.execute('SELECT * FROM user ORDER BY age')
        for row in rows:
            email = row[0]
            print("Going to create rows for {}".format(email))
            name = row[1] + " " + row[2]
            for i in range(10):
                comment = "This is comment {} by {}".format(i, name)
                conn.cursor().execute(sql.format(email, comment))
        conn.commit()
    

    注意我们如何动态生成插入查询,以便我们可以为每个用户插入 10 条评论。

    连接

    在这个练习中,我们将学习如何利用我们刚刚建立的关系。这意味着如果我们有一个表的键,我们可以从该表恢复所需的所有数据,以及从子表中的所有关联行。为了实现这一点,我们将使用一个称为连接的东西。

    连接基本上是一种使用任何类型的键-外键关系从两个表中检索相关行的方法。有许多类型的连接,如INNERLEFT OUTERRIGHT OUTERFULL OUTERCROSS。它们用于不同的场景。然而,在简单的 1: N 关系中,我们通常使用INNER连接。在第一章:使用 Python 进行数据整理的介绍中,我们学习了集合,然后我们可以将INNER连接视为两个集合的交集。以下图表说明了这些概念:

    图 8.12:交集连接

    在这里,A 代表一个表,B 代表另一个表。拥有共同成员的意义是它们之间存在关系。它将 A 的所有行与 B 的所有行进行比较,以找到满足连接谓词的匹配行。这可能会迅速变成一个复杂且耗时的操作。连接可以是昂贵的操作。通常,我们在指定连接后使用某种WHERE子句,以缩短从表 A 或 B 中检索的行的范围以执行匹配。

    在我们的例子中,我们的第一个表user有三个条目,主键是email。我们可以在查询中使用这一点来获取来自Bob的评论:

    with sqlite3.connect("chapter.db") as conn:
        cursor = conn.cursor()s
        cursor.execute("PRAGMA foreign_keys = 1")
        sql = """
            SELECT * FROM comments 
            JOIN user ON comments.user_id = user.email
            WHERE user.email='bob@example.com'
        """
        rows = cursor.execute(sql)
        for row in rows:
            print(row)
    

    输出如下:

    ('bob@example.com', 'This is comment 0 by Bob Codd', 'bob@example.com', 'Bob', 'Codd', '123 Fantasy lane, Fantasu City', 31, None)
    ('bob@example.com', 'This is comment 1 by Bob Codd', 'bob@example.com', 'Bob', 'Codd', '123 Fantasy lane, Fantasu City', 31, None)
    ('bob@example.com', 'This is comment 2 by Bob Codd', 'bob@example.com', 'Bob', 'Codd', '123 Fantasy lane, Fantasu City', 31, None)
    ('bob@example.com', 'This is comment 3 by Bob Codd', 'bob@example.com', 'Bob', 'Codd', '123 Fantasy lane, Fantasu City', 31, None)
    ('bob@example.com', 'This is comment 4 by Bob Codd', 'bob@example.com', 'Bob', 'Codd', '123 Fantasy lane, Fantasu City', 31, None)
    ('bob@example.com', 'This is comment 5 by Bob Codd', 'bob@example.com', 'Bob', 'Codd', '123 Fantasy lane, Fantasu City', 31, None)
    ('bob@example.com', 'This is comment 6 by Bob Codd', 'bob@example.com', 'Bob', 'Codd', '123 Fantasy lane, Fantasu City', 31, None)
    ('bob@example.com', 'This is comment 7 by Bob Codd', 'bob@example.com', 'Bob', 'Codd', '123 Fantasy lane, Fantasu City', 31, None)
    ('bob@example.com', 'This is comment 8 by Bob Codd', 'bob@example.com', 'Bob', 'Codd', '123 Fantasy lane, Fantasu City', 31, None)
    ('bob@example.com', 'This is comment 9 by Bob Codd', 'bob@example.com', 'Bob', 'Codd', '123 Fantasy lane, Fantasu City', 31, None)
    
    图 8.13:连接查询的输出

    从 JOIN 查询中检索特定列

    在上一个练习中,我们看到了我们可以使用连接来从两个表中检索相关行。然而,如果我们查看结果,我们会看到它返回了所有列,因此结合了两个表。这并不非常简洁。如果我们只想看到电子邮件和相关的评论,而不是所有数据呢?

    有一些简洁的代码可以让我们做到这一点:

    with sqlite3.connect("chapter.db") as conn:
        cursor = conn.cursor()
        cursor.execute("PRAGMA foreign_keys = 1")
        sql = """
            SELECT comments.* FROM comments
            JOIN user ON comments.user_id = user.email
            WHERE user.email='bob@example.com'
        """
        rows = cursor.execute(sql)
        for row in rows:
            print(row)
    

    只需更改SELECT语句,我们就可以使最终结果看起来如下:

    ('bob@example.com', 'This is comment 0 by Bob Codd')
    ('bob@example.com', 'This is comment 1 by Bob Codd')
    ('bob@example.com', 'This is comment 2 by Bob Codd')
    ('bob@example.com', 'This is comment 3 by Bob Codd')
    ('bob@example.com', 'This is comment 4 by Bob Codd')
    ('bob@example.com', 'This is comment 5 by Bob Codd')
    ('bob@example.com', 'This is comment 6 by Bob Codd')
    ('bob@example.com', 'This is comment 7 by Bob Codd')
    ('bob@example.com', 'This is comment 8 by Bob Codd')
    ('bob@example.com', 'This is comment 9 by Bob Codd')
    

    练习 112:删除行

    在这个练习中,我们将从用户表中删除一行,并观察它对comments表产生的影响。运行此命令时要非常小心,因为它可能会对数据产生破坏性影响。请记住,它几乎总是需要与WHERE子句一起运行,以便我们只删除部分数据而不是全部:

    1. 要从表中删除一行,我们使用SQL中的DELETE子句。要运行user表的删除操作,我们将使用以下代码:

      with sqlite3.connect("chapter.db") as conn:
          cursor = conn.cursor()
          cursor.execute("PRAGMA foreign_keys = 1")
          cursor.execute("DELETE FROM user WHERE email='bob@example.com'")
          conn.commit()
      
    2. 在用户表上执行SELECT操作:

      with sqlite3.connect("chapter.db") as conn:
          cursor = conn.cursor()
          cursor.execute("PRAGMA foreign_keys = 1")
          rows = cursor.execute("SELECT * FROM user")
          for row in rows:
              print(row)
      

      观察到用户 Bob 已被删除。

      现在,让我们转到comments表,我们必须记住我们在创建表时提到了ON DELETE CASCADE。数据库引擎知道,如果从父表(user)中删除一行,所有相关的子表(comments)中的行都必须被删除。

    3. 使用以下命令在评论表上执行选择操作:

      with sqlite3.connect("chapter.db") as conn:
          cursor = conn.cursor()
          cursor.execute("PRAGMA foreign_keys = 1")
          rows = cursor.execute("SELECT * FROM comments")
          for row in rows:
              print(row)
      

      输出如下:

      ('tom@web.com', 'This is comment 0 by Tom Fake')
      ('tom@web.com', 'This is comment 1 by Tom Fake')
      ('tom@web.com', 'This is comment 2 by Tom Fake')
      ('tom@web.com', 'This is comment 3 by Tom Fake')
      ('tom@web.com', 'This is comment 4 by Tom Fake')
      ('tom@web.com', 'This is comment 5 by Tom Fake')
      ('tom@web.com', 'This is comment 6 by Tom Fake')
      ('tom@web.com', 'This is comment 7 by Tom Fake')
      ('tom@web.com', 'This is comment 8 by Tom Fake')
      ('tom@web.com', 'This is comment 9 by Tom Fake')
      

      我们可以看到与 Bob 相关的所有行都被删除了。

    在表中更新特定值

    在这个练习中,我们将看到如何更新表中的行。我们以前已经看过这个,但正如我们提到的,只是在表级别。没有 WHERE 子句,更新通常不是一个好主意。

    将 UPDATE 与 WHERE 结合使用,以选择性地更新具有电子邮件地址tom@web.com的用户的名字:`

    with sqlite3.connect("chapter.db") as conn:
        cursor = conn.cursor()
        cursor.execute("PRAGMA foreign_keys = 1")
        cursor.execute("UPDATE user set first_name='Chris' where email='tom@web.com'")
        conn.commit()
        rows = cursor.execute("SELECT * FROM user")
        for row in rows:
            print(row)
    

    输出如下:

    图 8.14:更新查询的输出

    练习 113:关系数据库管理系统和 DataFrame

    我们已经探讨了从数据库存储和查询数据的许多基本方面,但作为一个数据整理专家,我们需要我们的数据打包并呈现为 DataFrame,这样我们就可以快速方便地对它们进行操作:

    1. 使用以下代码导入pandas

      import pandas as pd
      
    2. 创建一个包含emailfirst namelast nameagegendercomments作为列名的列列表。同时创建一个空的数据列表:

      columns = ["Email", "First Name", "Last Name", "Age", "Gender", "Comments"]
      data = []
      
    3. 使用SQLite连接到chapter.db并获取游标,如下所示:

      with sqlite3.connect("chapter.db") as conn:
          cursor = conn.cursor()
      Use the execute method from the cursor to set "PRAGMA foreign_keys = 1"
          cursor.execute("PRAGMA foreign_keys = 1")
      
    4. 创建一个包含SELECT命令的sql变量,并使用join命令连接数据库:

      sql = """
              SELECT user.email, user.first_name, user.last_name, user.age, user.gender, comments.comments FROM comments
              JOIN user ON comments.user_id = user.email
              WHERE user.email = 'tom@web.com'
          """
      
    5. 使用游标的execute方法执行sql命令:

          rows = cursor.execute(sql)
      
    6. 将行追加到数据列表中:

          for row in rows:
              data.append(row)
      
    7. 使用数据列表创建 DataFrame:

      df = pd.DataFrame(data, columns=columns)
      
    8. 我们已经使用数据列表创建了 DataFrame。您可以使用df.head将值打印到 DataFrame 中。

    活动 11:正确从数据库检索数据

    在这个活动中,我们有人员表:

    图 8.15:人员表

    我们有宠物表:

    图 8.16:宠物表

    如我们所见,人员表中的id列(它是一个整数)是该表的主键,也是宠物表的外键,通过owner_id列进行链接。

    人员表有以下列:

    • first_name: 人员的名字

    • last_name: 人员的姓氏(可以是“null”)

    • age: 人员的年龄

    • city: 他/她来自的城市

    • zip_code: 城市的邮政编码

    宠物表有以下列:

    • pet_name: 宠物的名字。

    • pet_type: 宠物的类型,例如,猫、狗等。由于缺乏更多信息,我们不知道哪个数字代表什么,但它是一个整数,可以是空值。

    • treatment_done: 它也是一个整数列,这里的 0 代表“否”,而 1 代表“是”。

    SQLite 数据库的名称是petsdb,它随活动笔记本一起提供。

    这些步骤将帮助您完成此活动:

    1. 连接到petsDB并检查连接是否成功。

    2. 查找人员数据库中的不同年龄段。

    3. 查找人数最多的年龄段。

    4. 找出没有姓氏的人。

    5. 查询有多少人拥有不止一只宠物。

    6. 查询已接受治疗的宠物数量。

    7. 查询已接受治疗且已知宠物类型的宠物数量。

    8. 查询有多少宠物来自名为east port的城市。

    9. 查询有多少宠物来自名为east port的城市,并且接受了治疗。

      注意

      本活动的解决方案可以在第 324 页找到。

    摘要

    我们已经到达了数据库章节的结尾。我们学习了如何使用 Python 连接到 SQLite 数据库。我们复习了关系型数据库的基础知识,并学习了如何打开和关闭数据库。然后我们学习了如何将这个关系型数据库导出到 Python 的 DataFrames 中。

    在下一章中,我们将对现实世界的数据集进行数据整理。

    第十章:第九章

    数据处理在现实生活中的应用

    学习目标

    到本章结束时,你将能够:

    • 对多个来自知名来源的完整数据集进行数据处理

    • 创建一个统一的数据库,可以传递给数据科学团队进行机器学习和预测分析

    • 将数据处理与版本控制、容器化、数据分析云服务以及大数据技术(如 Apache Spark 和 Hadoop)联系起来

    在本章中,你将应用你在现实生活中的数据集上所积累的知识,并调查其各个方面。

    简介

    我们在上一章学习了数据库,所以现在是将数据处理和 Python 知识与实际场景相结合的时候了。在现实世界中,来自单一来源的数据通常不足以进行分析。通常,数据处理员必须区分相关数据和非相关数据,并从不同来源组合数据。

    数据处理专家的主要工作是从多个来源提取数据,格式化和清理它(如果数据缺失,则进行数据插补),并最终以连贯的方式将其组合起来,为数据科学家或机器学习工程师准备进一步分析的数据集。

    在这个主题中,我们将尝试通过下载和使用来自知名网络门户的两个不同数据集来模拟这样一个典型的任务流程。每个数据集都包含与被询问的关键问题相关的部分数据。让我们更仔细地检查一下。

    将你的知识应用于实际生活中的数据处理任务

    假设你被问到这个问题:在过去 15 年里,印度的初等/中等/高等教育入学率是否随着人均 GDP 的提高而增加? 实际的建模和分析将由一些资深数据科学家完成,他们将使用机器学习和数据可视化进行分析。作为数据处理专家,你的工作将是获取并提供一个包含教育入学率和 GDP 数据的干净数据集,这些数据并排排列

    假设你有一个来自联合国的数据集链接,你可以下载包含全球所有国家教育数据的数据集。但这个数据集有一些缺失值,而且它没有包含任何 GDP 信息。还有人给了你另一个单独的 CSV 文件(从世界银行网站下载),它包含 GDP 数据,但格式很混乱。

    在这个活动中,我们将检查如何处理这两个不同的来源,清理数据以准备一个包含所需数据的简单最终数据集,并将其保存为本地驱动器上的 SQL 数据库文件:

    图 9.1:教育和经济数据合并的图示表示

    图 9.1:教育和经济数据合并的图示表示

    鼓励您跟随笔记本中的代码和结果,尝试理解和内化数据处理流程的本质。您还被鼓励尝试从这些文件中提取各种数据,并回答您自己对一个国家的社会经济因素及其相互关系的问题。

    注意

    提出关于社会、经济、技术和地缘政治主题的有趣问题,然后使用免费数据和一点编程知识来回答这些问题,这是学习任何数据科学主题最有趣的方式之一。您将在本章中了解到这个过程的一些内容。

    数据插补

    显然,我们缺少一些数据。假设我们决定通过在可用数据点之间进行简单线性插值来插补这些数据点。我们可以拿出笔和纸或计算器来计算这些值,并手动创建一个数据集。但作为一个数据处理员,我们当然会利用 Python 编程,并使用 pandas 插补方法来完成这项任务。

    但要这样做,我们首先需要创建一个包含缺失值的 DataFrame,也就是说,我们需要将另一个包含缺失值的 DataFrame 附加到当前 DataFrame 上。

    活动十二:数据处理任务 - 修复联合国数据

    假设数据分析的议程是找出在过去 15 年中,人均 GDP 的提高是否导致了小学、中学或高等教育入学率的增加。为此任务,我们首先需要清理或整理两个数据集,即教育入学率和 GDP 数据。

    联合国数据可在以下链接中找到:github.com/TrainingByPackt/Data-Wrangling-with-Python/blob/master/Chapter09/Activity12-15/SYB61_T07_Education.csv

    注意

    如果您下载 CSV 文件并使用 Excel 打开它,您将看到“脚注”列有时包含有用的注释。我们可能不想一开始就删除它。如果我们对特定国家或地区的数据感兴趣(就像在这个任务中一样),那么“脚注”可能就是NaN,即空白。在这种情况下,我们可以在最后删除它。但对于某些国家或地区,它可能包含信息。

    这些步骤将指导您找到解决方案:

    1. 从以下链接下载联合国数据集:github.com/TrainingByPackt/Data-Wrangling-with-Python/blob/master/Chapter09/Activity13/India_World_Bank_Info.csv

      联合国数据存在缺失值。清理数据,准备一个包含所需数据的简单最终数据集,并将其保存到本地驱动器上的 SQL 数据库文件中。

    2. 使用 pandas 的pd.read_csv方法创建一个 DataFrame。

    3. 由于第一行不包含有用的信息,使用skiprows参数跳过它。

    4. 删除区域/国家/地区和来源列。

    5. 将以下名称分配为 DataFrame 的列:区域/县/地区、年份、数据、值和脚注。

    6. 检查“脚注”列中有多少个独特的值。

    7. 检查值列的类型。

    8. 创建一个函数将值列转换为浮点数。

    9. 使用apply方法将此函数应用于一个值。

    10. 打印数据列中的独特值。

      注意:

      本活动的解决方案可在第 338 页找到。

    活动十三:数据整理任务 - 清理 GDP 数据

    GDP 数据可在data.worldbank.org/找到,并在 GitHub 上可用,地址为github.com/TrainingByPackt/Data-Wrangling-with-Python/blob/master/Chapter09/Activity12-15/India_World_Bank_Info.csv

    在本活动中,我们将清理 GDP 数据。

    1. 从原始数据帧中通过筛选创建三个数据帧:df_primarydf_secondarydf_tertiary,分别对应小学、中学和大学的学生人数(单位为千)。

    2. 绘制印度这类低收入国家和美国这类高收入国家小学学生入学人数的条形图。

    3. 由于存在缺失数据,使用 pandas 的插补方法通过简单线性插值在数据点之间插补这些数据点。为此,创建一个包含缺失值的 DataFrame,并将一个包含缺失值的新的 DataFrame 附加到当前 DataFrame 中。

    4. (针对印度)添加对应缺失年份的行 - 2004 - 20092011 – 2013

    5. 使用np.nan创建一个包含值的字典。请注意,有 9 个缺失数据点,因此我们需要创建一个包含相同值重复 9 次的列表。

    6. 创建一个包含缺失值的 DataFrame(来自前面的字典),我们可以将其附加。

    7. 将数据帧拼接在一起。

    8. 按年份排序并使用reset_index重置索引。使用inplace=True在 DataFrame 本身上执行更改。

    9. 使用线性插值方法进行线性插值。它通过线性插值值填充所有 NaN。有关此方法的更多详细信息,请参阅以下链接:pandas.pydata.org/pandas-docs/version/0.17/generated/pandas.DataFrame.interpolate.html

    10. 对美国(或其他国家)重复相同的步骤。

    11. 如果有未填写的值,使用limitlimit_direction参数与插值方法一起填充它们。

    12. 使用新数据绘制最终图形。

    13. 使用 pandas 的read_csv方法读取 GDP 数据。它通常会引发错误。

    14. 为了避免错误,尝试使用error_bad_lines = False选项。

    15. 由于文件中没有分隔符,添加\t分隔符。

    16. 使用skiprows函数删除无用的行。

    17. 检查数据集。使用表示与先前教育数据集相似的信息来过滤数据集。

    18. 为这个新数据集重置索引。

    19. 删除无用的行并重新索引数据集。

    20. 正确重命名列。这对于合并两个数据集是必要的。

    21. 我们将只关注 2003 年至 2016 年的数据。消除剩余的数据。

    22. 创建一个新的 DataFrame,名为df_gdp,包含第 43 行到第 56 行的数据。

      注意

      该活动的解决方案可以在第 338 页找到。

    活动十四:数据处理任务 – 合并联合国数据和 GDP 数据

    合并数据库的步骤如下:

    1. 重置合并的索引。

    2. 在年份列上合并两个 DataFrame,primary_enrollment_indiadf_gdp

    3. 删除数据、脚注以及地区/县/区域。

    4. 重新排列列以进行适当的查看和展示。

      注意

      该活动的解决方案可以在第 345 页找到。

    活动十五:数据处理任务 – 将新数据连接到数据库

    将数据连接到数据库的步骤如下:

    1. 导入 Python 的sqlite3模块,并使用connect函数连接到数据库。主要数据库引擎是嵌入式的。但对于像PostgresqlMySQL这样的不同数据库,我们需要使用那些凭据来连接它们。我们将Year指定为该表的PRIMARY KEY

    2. 然后,运行一个循环,逐行将数据集的行插入到表中。

    3. 如果我们查看当前文件夹,我们应该看到一个名为Education_GDP.db的文件,如果我们使用数据库查看程序检查它,我们可以看到数据已传输到那里。

      注意

      该活动的解决方案可以在第 347 页找到。

    在这个笔记本中,我们检查了一个完整的数据处理流程,包括从网络和本地驱动器读取数据,过滤、清洗、快速可视化、插补、索引、合并,并将数据写回数据库表。我们还编写了自定义函数来转换一些数据,并展示了在读取文件时可能遇到错误的情况。

    数据处理的扩展

    这是本书的最后一章,我们希望向您提供一个广泛的概述,介绍一些您可能需要学习的令人兴奋的技术和框架,以便在数据处理之外工作,成为一名全栈数据科学家。数据处理是整个数据科学和数据分析流程的一个基本部分,但它不是全部。您在这本书中学到了宝贵的技能和技术,但总是好的,拓宽视野,看看其他工具可以在这个竞争激烈且不断变化的世界中给您带来优势。

    成为数据科学家所需的其他技能

    要成为一名合格的数据科学家/分析师,你应该在你的技能库中拥有一些基本技能,无论你选择专注于哪种特定的编程语言。这些技能和知识是语言无关的,可以根据你的组织和企业需求,在任何你必须接受的框架中使用。我们在这里简要描述它们:

    • Git 和版本控制:Git 版本控制对于代码管理来说,就像 RDBMS 对于数据存储和查询一样。这意味着在 Git 时代之前和之后,代码版本控制之间存在巨大的差距。正如你可能已经注意到的,这本书的所有笔记本都托管在 GitHub 上,这是为了利用强大的 Git VCS。它为你提供了开箱即用的版本控制、历史记录、不同代码的分支功能、合并不同代码分支以及 cherry picking、diff 等高级操作。这是一个非常必要的工具,你几乎可以肯定,在你的旅途中你会在某个时刻遇到它。Packt 有关于它的非常好的书籍。你可以查看以获取更多信息。

    • Linux 命令行:来自 Windows 背景(或者甚至 Mac,如果你之前没有进行过任何开发)的人通常不太熟悉命令行。那些操作系统的优越 UI 隐藏了使用命令行与操作系统交互的底层细节。然而,作为一名数据专业人士,了解命令行是非常重要的。你可以通过命令行执行的操作如此之多,以至于令人惊讶。

    • SQL 和基本的关系数据库概念:我们专门用了一整章来介绍 SQL 和 RDBMS。然而,正如我们之前提到的,这远远不够。这是一个庞大的主题,需要多年的学习才能掌握。尝试从书籍和在线资源中了解更多关于它的内容(包括理论和实践)。不要忘记,尽管现在使用了所有其他数据来源,我们仍然有数亿字节的结构化数据存储在传统的数据库系统中。你可以确信,迟早你会遇到这样的系统。

    • Docker 和容器化:自从 2013 年首次发布以来,Docker 已经改变了我们在基于服务器的应用程序中分发和部署软件的方式。它为底层操作系统提供了一个干净且轻量级的抽象,让你能够快速迭代开发,而无需为创建和维护适当的环境而烦恼。它在开发和生产阶段都非常有用。由于几乎没有竞争对手,它们正在迅速成为行业中的默认选择。我们强烈建议你深入了解它。

    对大数据和云计算技术的基本了解

    大数据和云平台是当前最新的趋势。我们将用一两句话来介绍它们,并鼓励你尽可能多地了解它们。如果你计划成为一名数据专业人士,那么你可以确信,没有这些必要的技能,你将很难过渡到下一个层次:

    • 大数据的基本特征: 大数据仅仅是非常大规模的数据。这里的“规模”一词有点模糊。它可以指一个静态的数据块(如像印度或美国这样的大国的详细人口普查数据)或者随着时间的推移动态生成的大量数据。为了举例说明第二类,我们可以想想 Facebook 每天生成多少数据。大约是每天 500+ 太字节。你可以很容易地想象,我们将需要专门的工具来处理这么多的数据。大数据有三种不同的类别,即结构化、非结构化和半结构化。定义大数据的主要特征是体积、种类、速度和可变性。

    • Hadoop 生态系统: Apache Hadoop(及其相关生态系统)是一个旨在使用 Map-Reduce 编程模型简化大数据存储和处理的软件框架。它已经成为行业大数据处理的主要支柱之一。Hadoop 的模块设计时考虑到硬件故障是常见现象,并且应该由框架自动处理。Hadoop 的四个基础模块是 Common、HDFS、YARN 和 MapReduce。Hadoop 生态系统包括 Apache Pig、Apache Hive、Apache Impala、Apache Zookeeper、Apache HBase 等。它们是许多高需求和前沿数据管道中非常重要的基石。我们鼓励你更多地了解它们。它们在旨在利用数据的任何行业中都是必不可少的。

    • Apache Spark: Apache Spark 是一个通用的集群计算框架,最初由加州大学伯克利分校开发,并于 2014 年发布。它为你提供了一个接口,可以编程整个计算机集群,内置数据并行性和容错性。它包含 Spark Core、Spark SQL、Spark Streaming、MLib(用于机器学习)和 GraphX。现在,它是工业界用于基于流数据的实时处理大量数据的主要框架之一。如果你想要走向实时数据工程,我们鼓励你阅读并掌握它。

    • 亚马逊网络服务(AWS):亚马逊网络服务(通常缩写为 AWS)是由亚马逊提供的一系列托管服务,从基础设施即服务(IaaS)、数据库即服务(DBaaS)、机器学习即服务(MLaaS)、缓存、负载均衡器、NoSQL 数据库,到消息队列等多种类型。它们对各种应用都非常有用。它可以是一个简单的 Web 应用,也可以是一个多集群数据管道。许多知名公司都在 AWS 上运行其整个基础设施(如 Netflix)。他们提供按需提供、易于扩展、管理环境、流畅的用户界面来控制一切,以及一个非常强大的命令行客户端。他们还公开了一系列丰富的 API,我们几乎可以在任何编程语言中找到 AWS API 客户端。Python 的一个叫做 Boto3。如果你计划成为一名数据专业人士,那么几乎可以肯定地说,你最终会在某个时候使用他们的许多服务。

    数据处理需要什么?

    第一章使用 Python 进行数据处理入门中,我们了解到数据处理过程位于数据收集和高级分析(包括可视化和机器学习)之间。然而,存在于这些过程之间的边界可能并不总是严格和固定的。这很大程度上取决于组织文化和团队构成。

    因此,我们不仅需要了解数据处理,还需要了解数据科学平台的其他组件,以有效地处理数据。即使你正在执行纯粹的数据处理任务,对数据来源和利用的良好掌握也将为你提供优势,以便提出独特且高效的解决方案来解决复杂的数据处理问题,并提高这些解决方案对机器学习科学家或业务领域专家的价值:

    图 9.2:数据处理过程

    图 9.2:数据处理过程

    现在,实际上,我们已经在这本书中为数据处理平台部分打下了坚实的基础,假设它是数据处理工作流程的一个组成部分。例如,我们详细介绍了网络爬取、使用 RESTful API 以及使用 Python 库进行数据库访问和操作。

    我们还简要介绍了使用 matplotlib 在 Python 中的基本可视化技术和绘图函数。然而,还有其他高级统计绘图库,如Seaborn,你可以掌握它来进行更复杂的数据科学任务的可视化。

    商业逻辑和领域专业知识是最多样化的主题,而且只能在工作中学习,然而随着经验的积累,它最终会到来。如果你在金融、医学和医疗保健、工程等任何领域有学术背景和/或工作经验,那么这些知识将在你的数据科学职业生涯中派上用场。

    数据清洗的辛勤工作在机器学习领域得到了充分的体现。它是使机器从数据中学习模式和洞察力,以进行预测分析和智能、自动决策的科学和工程,这些数据量巨大,人类无法有效分析。机器学习已成为现代技术景观中最受欢迎的技能之一。它确实已成为最具激动人心和最有希望的智力领域之一,其应用范围从电子商务到医疗保健,几乎涵盖所有领域。数据清洗与机器学习内在相关,因为它准备数据,使其适合智能算法处理。即使你从数据清洗开始你的职业生涯,也可能自然过渡到机器学习。

    Packt 已经出版了大量的书籍,你应该探索这些书籍。在下一节中,我们将讨论一些可以采用的方法和 Python 库,以帮助你提高学习效率。

    掌握机器学习的技巧和窍门

    机器学习入门有一定难度。我们列出了一些结构化的 MOOCs 和令人难以置信的免费资源,以便你可以开始你的旅程:

    • 理解人工智能、机器学习、深度学习和数据科学等术语的定义和区别。培养阅读优秀帖子或聆听专家关于这些主题的演讲的习惯,并了解它们在解决某些商业问题中的真正影响力和适用性。

    • 通过观看视频、阅读像《终极学习机:对终极学习机的追求将如何重塑我们的世界》这样的书籍,以及阅读文章和关注像 KDnuggets、Brandon Rohrer 的博客、Open AI 关于他们研究的博客、Medium 上的《数据科学》出版物等有影响力的博客,来保持对最新趋势的了解。

    • 当你学习新的算法或概念时,请暂停并分析如何将这些机器学习概念或算法应用到你的日常工作中。这是学习和扩展你的知识库的最佳方法。

    • 如果你选择 Python 作为机器学习任务的优先语言,你将拥有一个出色的 ML 库scikit-learn。它是 Python 生态系统中使用最广泛的通用机器学习包。scikit-learn 拥有各种监督和非监督学习算法,这些算法通过一个稳定一致的接口公开。此外,它专门设计用于与其他流行的数据清洗和数值库无缝接口,如 NumPy 和 pandas。

    • 在当今的就业市场上,深度学习是另一种热门技能。Packt 有许多关于这个主题的书籍,Bookra 也有关于深度学习的优秀 MOOC 书籍,你可以学习并练习。对于 Python 库,你可以学习并使用TensorFlowKerasPyTorch进行深度学习。

    摘要

    数据无处不在,它环绕着我们。在这九章中,我们学习了如何清理、纠正和合并来自不同类型和来源的数据。利用 Python 的力量以及你在本书中学到的数据处理知识和技巧,你已准备好成为一名数据整理师。

    第十一章:附录

    关于

    本节包含帮助学生执行书中活动的详细步骤,学生需要执行这些步骤以达到活动的目标。

    活动一:处理列表的解决方案

    这些是完成此活动的步骤:

    1. 导入 random 库:

      import random
      
    2. 设置随机数的最大数量:

      LIMIT = 100
      
    3. 使用 random 库中的 randint 函数创建 100 个随机数。提示:尝试获取具有最少重复数的列表:

      random_number_list = [random.randint(0, LIMIT) for x in range(0, LIMIT)]
      
    4. 打印 random_number_list

      random_number_list
      

      样本输出如下:

      图 1.16:random_number_list 的输出部分

      图 1.16:random_number_list 的输出部分
    5. random_number_list 创建一个名为 list_with_divisible_by_3 的列表,其中只包含能被 3 整除的数字:

      list_with_divisible_by_3 = [a for a in random_number_list if a % 3 == 0]
      list_with_divisible_by_3
      

      样本输出如下:

      图 1.17:random_number_list 能被 3 整除的输出部分

      图 1.17:random_number_list 能被 3 整除的输出部分
    6. 使用 len 函数测量第一个列表和第二个列表的长度,并将它们存储在两个不同的变量中,length_of_random_listlength_of_3_divisible_list。在名为 difference 的变量中计算长度差异:

      length_of_random_list = len(random_number_list)
      length_of_3_divisible_list = len(list_with_divisible_by_3)
      difference = length_of_random_list - length_of_3_divisible_list
      difference
      

      样本输出如下:

      62
      
    7. 将我们迄今为止执行的任务组合起来,并添加一个 while 循环。循环运行 10 次,并将差异变量的值添加到一个列表中:

      NUMBER_OF_EXPERIMENTS = 10
      difference_list = []
      for i in range(0, NUMBER_OF_EXPERIEMENTS):
          random_number_list = [random.randint(0, LIMIT) for x in range(0, LIMIT)]
          list_with_divisible_by_3 = [a for a in random_number_list if a % 3 == 0]
      
          length_of_random_list = len(random_number_list)
          length_of_3_divisible_list = len(list_with_divisible_by_3)
          difference = length_of_random_list - length_of_3_divisible_list
      
          difference_list.append(difference)
      difference_list
      

      样本输出如下:

      [64, 61, 67, 60, 73, 66, 66, 75, 70, 61]
      
    8. 然后,计算你拥有的长度差异的算术平均值(普通平均值):

      avg_diff = sum(difference_list) / float(len(difference_list))
      avg_diff
      

      样本输出如下:

      66.3
      

    活动二:分析多行字符串并生成唯一单词计数的解决方案

    这些是完成此活动的步骤:

    1. 创建一个名为 multiline_text 的字符串,并将《傲慢与偏见》第一章中的文本复制到其中。使用 Ctrl + A 选择整个文本,然后使用 Ctrl + C 复制它,并将你刚刚复制的文本粘贴进去:图 1.18:初始化 mutliline_text 字符串

      图 1.18:初始化 mutliline_text 字符串
    2. 使用 type 函数查找字符串的类型:

      type(multiline_text)
      

      输出如下:

      str
      
    3. 现在,使用 len 函数找到字符串的长度:

      len(multiline_text)
      

      输出如下:

      4475
      
    4. 使用字符串方法去除所有换行符(\n\r)和符号。通过替换它们来移除所有换行符:

      multiline_text = multiline_text.replace('\n', "")
      

      然后,我们将打印并检查输出:

      multiline_text
      

      输出如下:

      图 1.19:移除换行符后的 multiline_text 字符串

      图 1.19:移除换行符后的 multiline_text 字符串
    5. 移除特殊字符和标点符号:

      # remove special chars, punctuation etc.
      cleaned_multiline_text = ""
      for char in multiline_text:
          if char == " ":
              cleaned_multiline_text += char
          elif char.isalnum():  # using the isalnum() method of strings.
              cleaned_multiline_text += char
          else:
              cleaned_multiline_text += " "
      
    6. 检查 cleaned_multiline_text 的内容:

      cleaned_multiline_text
      

      输出如下:

      图 1.20:cleaned_multiline_text 字符串

      图 1.20:cleaned_multiline_text 字符串
    7. 使用以下命令从清洗后的字符串生成所有单词的列表:

      list_of_words = cleaned_multiline_text.split()
      list_of_words
      

      输出如下:

      图 1.21:显示单词列表的输出部分

      图 1.21:显示单词列表的输出部分
    8. 找出单词的数量:

      len(list_of_words)
      

      输出为852

    9. 从你刚刚创建的列表中创建一个列表,其中只包含唯一单词:

      unique_words_as_dict = dict.fromkeys(list_of_words)
      len(list(unique_words_as_dict.keys()))
      

      输出为340

    10. 计算每个唯一单词在清洗后的文本中出现的次数:

      for word in list_of_words:
          if unique_words_as_dict[word] is None:
              unique_words_as_dict[word] = 1
          else:
              unique_words_as_dict[word] += 1
      unique_words_as_dict
      

      输出如下:

      图 1.22:显示唯一单词字典的输出部分

      图 1.22:显示唯一单词字典的输出部分

      你已经一步一步地创建了一个唯一单词计数器,使用了你刚刚学到的所有巧妙技巧。

    11. unique_words_as_dict中找出前 25 个单词。

      top_words = sorted(unique_words_as_dict.items(), key=lambda key_val_tuple: key_val_tuple[1], reverse=True)
      top_words[:25]
      

      完成此活动的步骤如下:

    图 1.23:多行文本中的前 25 个唯一单词

    图 1.23:多行文本中的前 25 个唯一单词

    活动三的解决方案:排列、迭代器、Lambda、列表

    解决这个活动的步骤如下:

    1. itertools中查找permutationsdropwhile的定义。在 Jupyter 中查找函数定义的方法是:输入函数名,后跟?,然后按Shift + Enter

      from itertools import permutations, dropwhile
      permutations?
      dropwhile?
      

      在每个?之后,你会看到一个长列表的定义。这里我们将跳过它。

    2. 编写一个表达式,使用 1、2 和 3 生成所有可能的三位数:

      permutations(range(3))
      

      输出如下:

      <itertools.permutations at 0x7f6c6c077af0>
      
    3. 遍历你之前生成的迭代器表达式。使用print打印迭代器返回的每个元素。使用assertisinstance确保元素是元组:

      for number_tuple in permutations(range(3)):
          print(number_tuple)
          assert isinstance(number_tuple, tuple)
      

      输出如下:

      (0, 1, 2)
      (0, 2, 1)
      (1, 0, 2)
      (1, 2, 0)
      (2, 0, 1)
      (2, 1, 0)
      
    4. 再次编写循环。但这次,使用带有 Lambda 表达式的dropwhile来删除元组中的任何前导零。例如,(0, 1, 2)将变成[0, 2]。同时,将dropwhile的输出转换为列表。

      可以作为一个额外任务来检查dropwhile实际返回的类型而不进行类型转换:

      for number_tuple in permutations(range(3)):
          print(list(dropwhile(lambda x: x <= 0, number_tuple)))
      

      输出如下:

      [1, 2]
      [2, 1]
      [1, 0, 2]
      [1, 2, 0]
      [2, 0, 1]
      [2, 1, 0]
      
    5. 将你之前编写的所有逻辑写出来,但这次写一个单独的函数,你将传递由dropwhile生成的列表,该函数将返回列表中的整个数字。例如,如果你将[1, 2]传递给函数,它将返回12。确保返回类型确实是一个数字而不是字符串。尽管可以使用其他技巧完成此任务,但我们要求你在函数中将传入的列表作为栈处理并生成数字:

      import math
      def convert_to_number(number_stack):
          final_number = 0
          for i in range(0, len(number_stack)):
              final_number += (number_stack.pop() * (math.pow(10, i)))
          return final_number
      for number_tuple in permutations(range(3)):
          number_stack = list(dropwhile(lambda x: x <= 0, number_tuple))
          print(convert_to_number(number_stack))
      

      输出如下:

      12.0
      21.0
      102.0
      120.0
      201.0
      210.0
      

    活动四的解决方案:设计你自己的 CSV 解析器

    完成此活动的步骤如下:

    1. itertools导入zip_longest

      from itertools import zip_longest
      
    2. 定义return_dict_from_csv_line函数,使其包含headerlinefillvalue作为None,并将其添加到dict中:

      def return_dict_from_csv_line(header, line):
          # Zip them
          zipped_line = zip_longest(header, line, fillvalue=None)
          # Use dict comprehension to generate the final dict
          ret_dict = {kv[0]: kv[1] for kv in zipped_line}
          return ret_dict
      
    3. 使用with块中的r模式打开随附的sales_record.csv文件。首先,检查它是否已打开,读取第一行,并使用字符串方法通过open("sales_record.csv", "r") as fd生成所有列名列表。当你读取每一行时,将那一行和标题列表传递给一个函数。该函数的工作是从这些中构建一个字典并填充key:values。请注意,缺失值应导致None

          first_line = fd.readline()
          header = first_line.replace("\n", "").split(",")
          for i, line in enumerate(fd):
              line = line.replace("\n", "").split(",")
              d = return_dict_from_csv_line(header, line)
              print(d)
              if i > 10:
                  break
      

      输出如下:

    图 2.10:代码的一部分

    图 2.10:输出的一部分

    活动五的解决方案:从 CSV 文件生成统计数据

    完成此活动的步骤如下:

    1. 加载必要的库:

      import numpy as np
      import pandas as pd
      import matplotlib.pyplot as plt
      
    2. 从本地目录读取波士顿住房数据集(以.csv文件形式给出):

      # Hint: The Pandas function for reading a CSV file is 'read_csv'.
      # Don't forget that all functions in Pandas can be accessed by syntax like pd.{function_name} 
      df=pd.read_csv("Boston_housing.csv")
      
    3. 检查前 10 条记录:

      df.head(10)
      

      输出如下:

      图 3.23:显示前 10 条记录的输出

      图 3.23:显示前 10 条记录的输出
    4. 查找记录总数:

      df.shape
      

      输出如下:

      (506, 14)
      
    5. 创建一个包含不包括CHASNOXBLSTAT列的小型 DataFrame:

      df1=df[['CRIM','ZN','INDUS','RM','AGE','DIS','RAD','TAX','PTRATIO','PRICE']]
      
    6. 检查你刚刚创建的新 DataFrame 的最后 7 条记录:

      df1.tail(7)
      

      输出如下:

      图 3.24:DataFrame 的最后七条记录

      图 3.24:DataFrame 的最后七条记录
    7. 使用for循环通过绘制新 DataFrame 中所有变量(列)的直方图:

      for c in df1.columns:
          plt.title("Plot of "+c,fontsize=15)
          plt.hist(df1[c],bins=20)
          plt.show()
      

      输出如下:

      图 3.25:使用 for 循环绘制的所有变量图

      图 3.25:使用 for 循环绘制所有变量的图
    8. 犯罪率可能是房价的指标(人们不愿意住在高犯罪率地区)。创建犯罪率与价格的散点图:

      plt.scatter(df1['CRIM'],df1['PRICE'])
      plt.show()
      

      输出如下:

      图 3.26:犯罪率与价格的散点图

      图 3.26:犯罪率与价格的散点图

      如果我们绘制 log10(crime)与价格的关系图,我们可以更好地理解这种关系。

    9. 创建 log10(crime)与价格的关系图:

      plt.scatter(np.log10(df1['CRIM']),df1['PRICE'],c='red')
      plt.title("Crime rate (Log) vs. Price plot", fontsize=18)
      plt.xlabel("Log of Crime rate",fontsize=15)
      plt.ylabel("Price",fontsize=15)
      plt.grid(True)
      plt.show()
      

      输出如下:

      图 3.27:犯罪率(Log)与价格的散点图

      图 3.27:犯罪率(Log)与价格的散点图
    10. 计算每户平均房间数:

      df1['RM'].mean()
      

      输出为6.284634387351788

    11. 计算中位数年龄:

      df1['AGE'].median()
      

      输出为77.5

    12. 计算平均(均值)距离五个波士顿就业中心:

      df1['DIS'].mean()
      

      输出为3.795042687747034

    13. 计算低价房屋(< $20,000)的百分比:

      # Create a Pandas series and directly compare it with 20
      # You can do this because Pandas series is basically NumPy array and you have seen how to filter NumPy array
      low_price=df1['PRICE']<20
      # This creates a Boolean array of True, False
      print(low_price)
      # True = 1, False = 0, so now if you take an average of this NumPy array, you will know how many 1's are there.
      # That many houses are priced below 20,000\. So that is the answer. 
      # You can convert that into percentage by multiplying with 100
      pcnt=low_price.mean()*100
      print("\nPercentage of house with <20,000 price is: ",pcnt)
      

      输出如下:

      0      False
      1      False
      2      False
      3      False
      4      False
      5      False
      6      False
      7      False
      8       True
      9       True
      10      True
      …
      500     True
      501    False
      502    False
      503    False
      504    False
      505     True
      Name: PRICE, Length: 506, dtype: bool
      Percentage of house with <20,000 price is:  41.50197628458498
      

    活动六的解决方案:处理 UCI 成人收入数据集

    完成此活动的步骤如下:

    1. 加载必要的库:

      import numpy as np
      import pandas as pd
      import matplotlib.pyplot as plt
      
    2. 从本地目录读取成人收入数据集(以.csv文件形式给出)并检查前 5 条记录:

      df = pd.read_csv("adult_income_data.csv")
      df.head()
      

      输出如下:

      图 4.61:显示.csv 文件前五条记录的 DataFrame

      图 4.61:显示 .csv 文件前五条记录的 DataFrame
    3. 创建一个脚本,逐行读取文本文件并提取第一行,这是 .csv 文件的标题:

      names = []
      with open('adult_income_names.txt','r') as f:
          for line in f:
              f.readline()
              var=line.split(":")[0]
              names.append(var)
      names
      

      输出如下:

      图 4.62:数据库中列的名称

      图 4.62:数据库中列的名称
    4. 使用 append 命令将响应变量(最后一列)的名称 Income 添加到数据集中:

      names.append('Income')
      
    5. 使用以下命令再次读取新文件:

      df = pd.read_csv("adult_income_data.csv",names=names)
      df.head()
      

      输出如下:

      图 4.63:添加了收入列的 DataFrame
    6. 使用 describe 命令获取数据集的统计摘要:

      df.describe()
      

      输出如下:

      图 4.64:数据集的统计摘要

      注意,只包含少量列。数据集中许多变量具有多个因素或类别。

    7. 使用以下命令列出类中所有变量的列表:

      # Make a list of all variables with classes
      vars_class = ['workclass','education','marital-status',
                    'occupation','relationship','sex','native-country']
      
    8. 使用以下命令创建循环以计数并打印它们:

      for v in vars_class:
          classes=df[v].unique()
          num_classes = df[v].nunique()
          print("There are {} classes in the \"{}\" column. They are: {}".format(num_classes,v,classes))
          print("-"*100)
      

      输出如下:

      图 4.65:不同因素或类别的输出

      图 4.65:不同因素或类别的输出
    9. 使用以下命令查找缺失值:

      df.isnull().sum()
      

      输出如下:

      图 4.66:查找缺失值

      图 4.66:查找缺失值
    10. 使用子集选择创建只包含年龄、教育和职业的 DataFrame:

      df_subset = df[['age','education','occupation']]
      df_subset.head()
      

      输出如下:

      图 4.67:子集 DataFrame

      图 4.67:子集 DataFrame
    11. 以 20 为 bin 大小绘制年龄直方图:

      df_subset['age'].hist(bins=20)
      

      输出如下:

      <matplotlib.axes._subplots.AxesSubplot at 0x19dea8d0>
      

      图 4.68:20 个 bin 大小的年龄直方图

      图 4.68:20 个 bin 大小的年龄直方图
    12. 以 25x10 的长图尺寸绘制按 education 分组的 age 的箱线图,并使 x 轴刻度字体大小为 15:

      df_subset.boxplot(column='age',by='education',figsize=(25,10))
      plt.xticks(fontsize=15)
      plt.xlabel("Education",fontsize=20)
      plt.show()
      

      输出如下:

      图 4.69:按教育分组年龄的箱线图

      图 4.69:按教育分组的年龄箱线图

      在进行任何进一步的操作之前,我们需要使用本章学到的 apply 方法。结果发现,当我们从 CSV 文件中读取数据集时,所有字符串前都带有空格字符。因此,我们需要从所有字符串中删除该空格。

    13. 创建一个函数来删除空格字符:

      def strip_whitespace(s):
          return s.strip()
      
    14. 使用 apply 方法将此函数应用于所有具有字符串值的列,创建一个新列,将新列的值复制到旧列中,然后删除新列。这是首选方法,以免意外删除有价值的数据。大多数时候,您需要创建一个具有所需操作的新列,并在必要时将其复制回旧列。忽略打印出的任何警告信息:

      # Education column
      df_subset['education_stripped']=df['education'].apply(strip_whitespace)
      df_subset['education']=df_subset['education_stripped']
      df_subset.drop(labels=['education_stripped'],axis=1,inplace=True)
      # Occupation column
      df_subset['occupation_stripped']=df['occupation'].apply(strip_whitespace)
      df_subset['occupation']=df_subset['occupation_stripped']
      df_subset.drop(labels=['occupation_stripped'],axis=1,inplace=True)
      

      这是您应该忽略的示例警告信息:

      图 4.70:可忽略的警告信息

      图 4.70:忽略的警告信息
    15. 使用以下命令找出 30 至 50 岁之间(包括)的人数:

      # Conditional clauses and join them by & (AND) 
      df_filtered=df_subset[(df_subset['age']>=30) & (df_subset['age']<=50)]
      

      检查新数据集的内容:

      df_filtered.head()
      

      输出如下:

      图 4.71:新 DataFrame 的内容

      图 4.71:新 DataFrame 的内容
    16. 查找过滤后的 DataFrame 的 shape,并将元组的索引指定为 0 以返回第一个元素:

      answer_1=df_filtered.shape[0]
      answer_1
      

      输出如下:

      1630
      
    17. 使用以下命令打印 30 至 50 岁之间黑人的数量:

      print("There are {} people of age between 30 and 50 in this dataset.".format(answer_1))
      

      输出如下:

      There are 1630 black of age between 30 and 50 in this dataset.
      
    18. 根据职业对记录进行分组,以找出平均年龄的分布情况:

      df_subset.groupby('occupation').describe()['age']
      

      输出如下:

      图 4.72:按年龄和教育分组的数据 DataFrame

      图 4.72:按年龄和教育分组的数据 DataFrame

      代码返回 79 rows × 1 columns.(79 行 × 1 列。)

    19. 按职业分组并显示年龄的汇总统计。找出平均年龄最大的职业以及在其劳动力中占比最大的 75 分位数以上的职业:

      df_subset.groupby('occupation').describe()['age']
      

      输出如下:

      图 4.73:显示年龄汇总统计的 DataFrame

      图 4.73:显示年龄汇总统计的 DataFrame

      是否有某个职业群体代表性非常低?也许我们应该删除这些数据,因为数据非常低,该群体在分析中不会很有用。实际上,仅通过查看前面的表格,你应该能够看到 barh 函数是 DataFrame 的索引,它是职业群体的汇总统计。我们可以看到 武装部队 群组几乎没有数据。这个练习教你,有时,异常值不仅仅是一个值,而可能是一个整个群体。这个群体的数据是好的,但太小,无法用于任何分析。因此,在这种情况下,它可以被视为异常值。但始终使用你的业务知识和工程判断来进行此类异常值检测以及如何处理它们。

    20. 使用子集和分组来查找异常值:

      occupation_stats= df_subset.groupby(
          'occupation').describe()['age']
      
    21. 在条形图上绘制值:

      plt.figure(figsize=(15,8))
      plt.barh(y=occupation_stats.index,
               width=occupation_stats['count'])
      plt.yticks(fontsize=13)
      plt.show()
      

      输出如下:

      图 4.74:显示职业统计的条形图

      图 4.74:显示职业统计的条形图
    22. 通过公共键进行合并练习。假设你被给出了两个数据集,其中公共键是 occupation。首先,通过从完整数据集中随机抽取样本创建两个这样的非交集数据集,然后尝试合并。包括至少两个其他列,以及每个数据集的公共键列。注意,如果公共键不是唯一的,合并后的数据集可能比两个起始数据集中的任何一个都有更多的数据点:

      df_1 = df[['age',
                 'workclass',
                 'occupation']].sample(5,random_state=101)
      df_1.head()
      

      输出如下:

    图 4.75:合并公共键后的输出

    第二个数据集如下:

    df_2 = df[['education',
               'occupation']].sample(5,random_state=101)
    df_2.head()
    

    输出如下:

    图 4.76:合并公共键后的输出

    将两个数据集合并在一起:

    df_merged = pd.merge(df_1,df_2,
                         on='occupation',
                         how='inner').drop_duplicates()
    df_merged
    

    输出如下:

    图片 5.37:DataFrame

    图 4.77:不同职业值的输出

    活动第七部分的解决方案:从网页中读取表格数据并创建 DataFrame

    完成此活动的步骤如下:

    1. 使用以下命令通过以下命令导入 BeautifulSoup 并加载数据:

      from bs4 import BeautifulSoup
      import pandas as pd
      
    2. 使用以下命令打开 Wikipedia 文件:

      fd = open("List of countries by GDP (nominal) - Wikipedia.htm", "r")
      soup = BeautifulSoup(fd)
      fd.close()
      
    3. 使用以下命令计算表格:

      all_tables = soup.find_all("table")
      print("Total number of tables are {} ".format(len(all_tables)))
      

      总共有 9 个表格。

    4. 使用类属性通过以下命令查找正确的表格:

      data_table = soup.find("table", {"class": '"wikitable"|}'})
      print(type(data_table))
      

      输出如下:

      <class 'bs4.element.Tag'>
      
    5. 使用以下命令通过以下命令分离来源和实际数据:

      sources = data_table.tbody.findAll('tr', recursive=False)[0]
      sources_list = [td for td in sources.findAll('td')]
      print(len(sources_list))
      

      输出如下:

      Total number of tables are 3.
      
    6. 使用 findAll 函数通过以下命令从 data_tablebody 标签中查找数据:

      data = data_table.tbody.findAll('tr', recursive=False)[1].findAll('td', recursive=False)
      
    7. 使用以下命令通过 findAll 函数从 data_tabletd 标签中查找数据:

      data_tables = []
      for td in data:
          data_tables.append(td.findAll('table'))
      
    8. 使用以下命令查找 data_tables 的长度:

      len(data_tables)
      

      输出如下:

      3
      
    9. 使用以下命令检查如何获取来源名称:

      source_names = [source.findAll('a')[0].getText() for source in sources_list]
      print(source_names)
      

      输出如下:

       ['International Monetary Fund', 'World Bank', 'United Nations']
      
    10. 分离第一个来源的标题和数据:

      header1 = [th.getText().strip() for th in data_tables[0][0].findAll('thead')[0].findAll('th')]
      header1
      

      输出如下:

       ['Rank', 'Country', 'GDP(US$MM)']
      
    11. 使用 findAll 通过以下命令从 data_tables 中查找行:

      rows1 = data_tables[0][0].findAll('tbody')[0].findAll('tr')[1:]
      
    12. 使用 strip 函数对每个 td 标签进行 rows1 中的数据查找:

      data_rows1 = [[td.get_text().strip() for td in tr.findAll('td')] for tr in rows1]
      
    13. 查找 DataFrame:

      df1 = pd.DataFrame(data_rows1, columns=header1)
      df1.head()
      

      输出如下:

      图 5.35:DataFrame

      图 5.35:从网页创建的 DataFrame
    14. 使用以下命令对其他两个来源执行相同的操作:

      header2 = [th.getText().strip() for th in data_tables[1][0].findAll('thead')[0].findAll('th')]
      header2
      

      输出如下:

       ['Rank', 'Country', 'GDP(US$MM)']
      
    15. 使用 findAll 通过以下命令从 data_tables 中查找行:

      rows2 = data_tables[1][0].findAll('tbody')[0].findAll('tr')[1:]
      
    16. 使用以下命令通过 strip 函数定义 find_right_text

      def find_right_text(i, td):
          if i == 0:
              return td.getText().strip()
          elif i == 1:
              return td.getText().strip()
          else:
              index = td.text.find("♠")
              return td.text[index+1:].strip()
      
    17. 使用以下命令通过 find_right_textdata_rows 中查找行:

      data_rows2 = [[find_right_text(i, td) for i, td in enumerate(tr.findAll('td'))] for tr in rows2]
      
    18. 使用以下命令计算 df2 DataFrame:

      df2 = pd.DataFrame(data_rows2, columns=header2)
      df2.head()
      

      输出如下:

      ![图 5.36:DataFrame 输出]

      图片 5.36:DataFrame 输出

      图 5.36:DataFrame 输出
    19. 现在,使用以下命令对第三个 DataFrame 执行相同的操作:

      header3 = [th.getText().strip() for th in data_tables[2][0].findAll('thead')[0].findAll('th')]
      header3
      

      输出如下:

      ['Rank', 'Country', 'GDP(US$MM)']
      
    20. 使用以下命令通过 findAlldata_tables 中查找行:

      rows3 = data_tables[2][0].findAll('tbody')[0].findAll('tr')[1:]
      
    21. 使用 find_right_text 通过以下命令从 data_rows3 中查找行:

      data_rows3 = [[find_right_text(i, td) for i, td in enumerate(tr.findAll('td'))] for tr in rows2]
      
    22. 使用以下命令计算 df3 DataFrame:

      df3 = pd.DataFrame(data_rows3, columns=header3)
      df3.head()
      

      输出如下:

    图 5.37:第三个 DataFrame

    图片 5.55:

    图 5.37:第三个 DataFrame

    活动第八部分的解决方案:处理异常值和缺失数据

    完成此活动的步骤如下:

    1. 加载数据:

      import pandas as pd
      import numpy as np
      import matplotlib.pyplot as plt
      %matplotlib inline
      
    2. 读取 .csv 文件:

      df = pd.read_csv("visit_data.csv")
      
    3. 打印 DataFrame 中的数据:

      df.head()
      

      输出如下:

      图 6.10:CSV 文件的内容

      图 6.10:CSV 文件的内容

      如我们所见,有一些数据值缺失,如果我们检查这些数据,我们会看到一些异常值。

    4. 使用以下命令检查重复项:

      print("First name is duplicated - {}".format(any(df.first_name.duplicated())))
      print("Last name is duplicated - {}".format(any(df.last_name.duplicated())))
      print("Email is duplicated - {}".format(any(df.email.duplicated())))
      

      输出如下:

      First name is duplicated - True
      Last name is duplicated - True
      Email is duplicated - False
      

      在名字的第一和最后部分都有重复,这是正常的。然而,正如我们所看到的,电子邮件没有重复,这是好的。

    5. 检查是否有任何重要列包含NaN

      # Notice that we have different ways to format boolean values for the % operator
      print("The column Email contains NaN - %r " % df.email.isnull().values.any())
      print("The column IP Address contains NaN - %s " % df.ip_address.isnull().values.any())
      print("The column Visit contains NaN - %s " % df.visit.isnull().values.any())
      

      输出如下:

      The column Email contains NaN - False 
      The column IP Address contains NaN - False 
      The column Visit contains NaN - True 
      

      访问列包含一些None值。鉴于手头的最终任务可能是预测访问次数,我们无法处理没有该信息的行。它们是一种异常值。让我们去除它们。

    6. 去除异常值:

      # There are various ways to do this. This is just one way. We encourage you to explore other ways.
      # But before that we need to store the previous size of the data set and we will compare it with the new size
      size_prev = df.shape
      df = df[np.isfinite(df['visit'])] #This is an inplace operation. After this operation the original DataFrame is lost.
      size_after = df.shape
      
    7. 报告大小差异:

      # Notice how parameterized format is used and then the indexing is working inside the quote marks
      print("The size of previous data was - {prev[0]} rows and the size of the new one is - {after[0]} rows".
      format(prev=size_prev, after=size_after))
      

      输出如下:

      The size of previous data was - 1000 rows and the size of the new one is - 974 rows
      
    8. 绘制箱线图以检查数据是否有异常值。

      plt.boxplot(df.visit, notch=True)
      

      输出如下:

      {'whiskers': [<matplotlib.lines.Line2D at 0x7fa04cc08668>,
        <matplotlib.lines.Line2D at 0x7fa04cc08b00>],
       'caps': [<matplotlib.lines.Line2D at 0x7fa04cc08f28>,
        <matplotlib.lines.Line2D at 0x7fa04cc11390>],
       'boxes': [<matplotlib.lines.Line2D at 0x7fa04cc08518>],
       'medians': [<matplotlib.lines.Line2D at 0x7fa04cc117b8>],
       'fliers': [<matplotlib.lines.Line2D at 0x7fa04cc11be0>],
       'means': []}
      

      箱线图如下:

      图 6.43:使用数据的箱线图

      图 6.43:使用数据的箱线图

      如我们所见,我们在这个列中有数据,在区间(0,3000)内。然而,数据的主要集中在大约 700 到大约 2300 之间。

    9. 去除超过 2900 和低于 100 的值——这些对我们来说是异常值。我们需要去除它们:

      df1 = df[(df['visit'] <= 2900) & (df['visit'] >= 100)]  # Notice the powerful & operator
      # Here we abuse the fact the number of variable can be greater than the number of replacement targets
      print("After getting rid of outliers the new size of the data is - {}".format(*df1.shape))
      

      去除异常值后,新数据的大小是923

      这是本章活动的结束。

    活动九的解决方案:从古腾堡提取前 100 本电子书

    完成此活动的步骤如下:

    1. 导入必要的库,包括regexbeautifulsoup

      import urllib.request, urllib.parse, urllib.error
      import requests
      from bs4 import BeautifulSoup
      import ssl
      import re
      
    2. 检查 SSL 证书:

      # Ignore SSL certificate errors
      ctx = ssl.create_default_context()
      ctx.check_hostname = False
      ctx.verify_mode = ssl.CERT_NONE
      
    3. 从 URL 读取 HTML:

      # Read the HTML from the URL and pass on to BeautifulSoup
      top100url = 'https://www.gutenberg.org/browse/scores/top'
      response = requests.get(top100url)
      
    4. 编写一个小函数来检查网络请求的状态:

      def status_check(r):
          if r.status_code==200:
              print("Success!")
              return 1
          else:
              print("Failed!")
              return -1
      
    5. 检查response的状态:

      status_check(response)
      

      输出如下:

      Success!
      1
      
    6. 解码响应并将其传递给BeautifulSoup进行 HTML 解析:

      contents = response.content.decode(response.encoding)
      soup = BeautifulSoup(contents, 'html.parser')
      
    7. 找到所有的href标签并将它们存储在链接列表中。检查列表的外观——打印前 30 个元素:

      # Empty list to hold all the http links in the HTML page
      lst_links=[]
      # Find all the href tags and store them in the list of links
      for link in soup.find_all('a'):
          #print(link.get('href'))
          lst_links.append(link.get('href'))
      
    8. 使用以下命令打印链接:

      lst_links[:30]
      

      输出如下:

      ['/wiki/Main_Page',
       '/catalog/',
       '/ebooks/',
       '/browse/recent/last1',
       '/browse/scores/top',
       '/wiki/Gutenberg:Offline_Catalogs',
       '/catalog/world/mybookmarks',
       '/wiki/Main_Page',
      'https://www.paypal.com/xclick/business=donate%40gutenberg.org&item_name=Donation+to+Project+Gutenberg',
       '/wiki/Gutenberg:Project_Gutenberg_Needs_Your_Donation',
       'http://www.ibiblio.org',
       'http://www.pgdp.net/',
       'pretty-pictures',
       '#books-last1',
       '#authors-last1',
       '#books-last7',
       '#authors-last7',
       '#books-last30',
       '#authors-last30',
       '/ebooks/1342',
       '/ebooks/84',
       '/ebooks/1080',
       '/ebooks/46',
       '/ebooks/219',
       '/ebooks/2542',
       '/ebooks/98',
       '/ebooks/345',
       '/ebooks/2701',
       '/ebooks/844',
       '/ebooks/11']
      
    9. 使用正则表达式在这些链接中查找数字。这些是前 100 本书的文件编号。初始化一个空列表来保存文件编号:

      booknum=[]
      
    10. 原始链接列表中的第 19 到 118 号数字是前 100 本电子书的编号。遍历适当的范围并使用正则表达式在链接(href)字符串中查找数字。使用findall()方法:

      for i in range(19,119):
          link=lst_links[i]
          link=link.strip()
          # Regular expression to find the numeric digits in the link (href) string
          n=re.findall('[0-9]+',link)
          if len(n)==1:
              # Append the filenumber casted as integer
              booknum.append(int(n[0]))
      
    11. 打印文件编号:

      print ("\nThe file numbers for the top 100 ebooks on Gutenberg are shown below\n"+"-"*70)
      print(booknum)
      

      输出如下:

      The file numbers for the top 100 ebooks on Gutenberg are shown below
      ----------------------------------------------------------------------
      [1342, 84, 1080, 46, 219, 2542, 98, 345, 2701, 844, 11, 5200, 43, 16328, 76, 74, 1952, 6130, 2591, 1661, 41, 174, 23, 1260, 1497, 408, 3207, 1400, 30254, 58271, 1232, 25344, 58269, 158, 44881, 1322, 205, 2554, 1184, 2600, 120, 16, 58276, 5740, 34901, 28054, 829, 33, 2814, 4300, 100, 55, 160, 1404, 786, 58267, 3600, 19942, 8800, 514, 244, 2500, 2852, 135, 768, 58263, 1251, 3825, 779, 58262, 203, 730, 20203, 35, 1250, 45, 161, 30360, 7370, 58274, 209, 27827, 58256, 33283, 4363, 375, 996, 58270, 521, 58268, 36, 815, 1934, 3296, 58279, 105, 2148, 932, 1064, 13415]
      
    12. soup 对象的文本看起来是什么样子?使用.text`方法并仅打印前 2,000 个字符(不要打印整个内容,因为它太长了)。

      你会注意到这里和那里有很多空格/空白。忽略它们。它们是 HTML 页面标记和其随意性质的一部分:

      print(soup.text[:2000])
      if (top != self) {
              top.location.replace (http://www.gutenberg.org);
              alert ('Project Gutenberg is a FREE service with NO membership required. If you paid somebody else to get here, make them give you your money back!');
            }
      
      

      输出如下:

      Top 100 - Project Gutenberg
      Online Book Catalog
       Book  Search
      -- Recent  Books
      -- Top  100
      -- Offline Catalogs
      -- My Bookmarks
      Main Page
      …
      Pretty Pictures
      Top 100 EBooks yesterday —
        Top 100 Authors yesterday —
        Top 100 EBooks last 7 days —
        Top 100 Authors last 7 days —
        Top 100 EBooks last 30 days —
        Top 100 Authors last 30 days
      Top 100 EBooks yesterday
      Pride and Prejudice by Jane Austen (1826)
      Frankenstein; Or, The Modern Prometheus by Mary Wollstonecraft Shelley (1367)
      A Modest Proposal by Jonathan Swift (1020)
      A Christmas Carol in Prose; Being a Ghost Story of Christmas by Charles Dickens (953)
      Heart of Darkness by Joseph Conrad (887)
      Et dukkehjem. English by Henrik Ibsen (761)
      A Tale of Two Cities by Charles Dickens (741)
      Dracula by Bram Stoker (732)
      Moby Dick; Or, The Whale by Herman Melville (651)
      The Importance of Being Earnest: A Trivial Comedy for Serious People by Oscar Wilde (646)
      Alice's Adventures in Wonderland by Lewis Carrol
      
    13. 使用正则表达式从 soup 对象中搜索提取的文本,以找到前 100 本电子书的名称(昨日的排名):

      # Temp empty list of Ebook names
      lst_titles_temp=[]
      
    14. 创建一个起始索引。它应该指向soup.textsplitlines方法。它将 soup 对象的文本行分割成行:

      start_idx=soup.text.splitlines().index('Top 100 EBooks yesterday')
      
    15. 循环 1-100,将下一 100 行的字符串添加到这个临时列表中。提示:使用splitlines方法:

      for i in range(100):
          lst_titles_temp.append(soup.text.splitlines()[start_idx+2+i])
      
    16. 使用正则表达式从名称字符串中提取文本并将其追加到空列表中。使用 match 和 span 找到索引并使用它们:

      lst_titles=[]
      for i in range(100):
          id1,id2=re.match('^[a-zA-Z ]*',lst_titles_temp[i]).span()
          lst_titles.append(lst_titles_temp[i][id1:id2])
      
    17. 打印标题列表:

      for l in lst_titles:
          print(l)
      

      输出如下:

      Pride and Prejudice by Jane Austen 
      Frankenstein
      A Modest Proposal by Jonathan Swift 
      A Christmas Carol in Prose
      Heart of Darkness by Joseph Conrad 
      Et dukkehjem
      A Tale of Two Cities by Charles Dickens 
      Dracula by Bram Stoker 
      Moby Dick
      The Importance of Being Earnest
      Alice
      Metamorphosis by Franz Kafka 
      The Strange Case of Dr
      Beowulf
      …
      The Russian Army and the Japanese War
      Calculus Made Easy by Silvanus P
      Beyond Good and Evil by Friedrich Wilhelm Nietzsche 
      An Occurrence at Owl Creek Bridge by Ambrose Bierce 
      Don Quixote by Miguel de Cervantes Saavedra 
      Blue Jackets by Edward Greey 
      The Life and Adventures of Robinson Crusoe by Daniel Defoe 
      The Waterloo Campaign 
      The War of the Worlds by H
      Democracy in America 
      Songs of Innocence
      The Confessions of St
      Modern French Masters by Marie Van Vorst 
      Persuasion by Jane Austen 
      The Works of Edgar Allan Poe 
      The Fall of the House of Usher by Edgar Allan Poe 
      The Masque of the Red Death by Edgar Allan Poe 
      The Lady with the Dog and Other Stories by Anton Pavlovich Chekhov
      

    活动第 10 项的解决方案:从 Gutenberg.org 提取前 100 本电子书

    完成此活动的步骤如下:

    1. 导入 urllib.requesturllib.parseurllib.errorjson

      import urllib.request, urllib.parse, urllib.error
      import json
      
    2. 使用 json.loads() 从存储在同一文件夹中的 JSON 文件中加载秘密 API 密钥(你必须从 OMDB 网站获取一个并使用它;它有每天 1,000 次的限制),并将其存储在一个变量中:

      注意

      以下单元格在解决方案笔记本中不会执行,因为作者无法提供他们的私人 API 密钥。

    3. 学生/用户/讲师需要获取一个密钥并将其存储在一个名为 APIkeys.json 的 JSON 文件中。我们称此文件为 APIkeys.json

    4. 使用以下命令打开 APIkeys.json 文件:

      with open('APIkeys.json') as f:
          keys = json.load(f)
          omdbapi = keys['OMDBapi']
      

      要传递的最终 URL 应该看起来像这样:www.omdbapi.com/?t=movie_name&apikey=secretapikey

    5. 使用以下命令将 OMDB 站点(www.omdbapi.com/?)作为字符串分配给名为 serviceurl 的变量:

      serviceurl = 'http://www.omdbapi.com/?'
      
    6. 创建一个名为 apikey 的变量,使用 URL 的最后一部分(&apikey=secretapikey),其中 secretapikey 是你自己的 API 密钥。电影名称部分是 t=movie_name,稍后将会说明:

      apikey = '&apikey='+omdbapi
      
    7. 编写一个名为 print_json 的实用函数,用于从 JSON 文件(我们将从该站点获取)中打印电影数据。以下是 JSON 文件的键:'Title'(标题)、'Year'(年份)、'Rated'(评级)、'Released'(上映日期)、'Runtime'(时长)、'Genre'(类型)、'Director'(导演)、'Writer'(编剧)、'Actors'(演员)、'Plot'(剧情)、'Language'(语言)、'Country'(国家)、'Awards'(奖项)、'Ratings'(评分)、'Metascore'(评分)、'imdbRating'(imdb 评分)、'imdbVotes'(imdb 投票数)和 'imdbID'(imdb ID):

      def print_json(json_data):
          list_keys=['Title', 'Year', 'Rated', 'Released', 'Runtime', 'Genre', 'Director', 'Writer', 
                     'Actors', 'Plot', 'Language', 'Country', 'Awards', 'Ratings', 
                     'Metascore', 'imdbRating', 'imdbVotes', 'imdbID']
          print("-"*50)
          for k in list_keys:
              if k in list(json_data.keys()):
                  print(f"{k}: {json_data[k]}")
          print("-"*50)
      
    8. 编写一个实用函数,根据 JSON 数据集中的信息下载电影的海报并将其保存在你的本地文件夹中。使用 os 模块。海报数据存储在 JSON 键 Poster 中。你可能需要拆分 Poster 文件名并提取文件扩展名。比如说,扩展名是 jpg。我们稍后会把这个扩展名和电影名称连接起来创建一个文件名,例如 movie.jpg。使用 open Python 命令打开文件并写入海报数据。完成后关闭文件。此函数可能不会返回任何内容,它只是将海报数据保存为图像文件:

      def save_poster(json_data):
          import os
          title = json_data['Title']
          poster_url = json_data['Poster']
          # Splits the poster url by '.' and picks up the last string as file extension
          poster_file_extension=poster_url.split('.')[-1]
          # Reads the image file from web
          poster_data = urllib.request.urlopen(poster_url).read()
      
          savelocation=os.getcwd()+'\\'+'Posters'+'\\'
          # Creates new directory if the directory does not exist. Otherwise, just use the existing path.
          if not os.path.isdir(savelocation):
              os.mkdir(savelocation)
      
          filename=savelocation+str(title)+'.'+poster_file_extension
          f=open(filename,'wb')
          f.write(poster_data)
          f.close()
      
    9. 编写一个名为 search_movie 的实用函数,通过电影名称搜索电影,打印下载的 JSON 数据(使用 print_json 函数进行此操作),并将电影海报保存在本地文件夹中(使用 save_poster 函数进行此操作)。使用 try-except 循环进行此操作,即尝试连接到网络门户。如果成功,则继续进行,如果不成功(即,如果引发异常),则仅打印错误消息。使用先前创建的变量 serviceurlapikey。你必须传递一个包含键 t 和电影名称作为相应值的字典到 urllib.parse.urlencode 函数,然后将 serviceurlapikey 添加到函数的输出以构造完整的 URL。此 URL 将用于访问数据。JSON 数据有一个名为 Response 的键。如果它是 True,则表示读取成功。在处理数据之前检查这一点。如果不成功,则打印 JSON 键 Error,它将包含电影数据库返回的适当错误消息:

      def search_movie(title):
          try:
              url = serviceurl + urllib.parse.urlencode({'t': str(title)})+apikey
              print(f'Retrieving the data of "{title}" now... ')
              print(url)
              uh = urllib.request.urlopen(url)
              data = uh.read()
              json_data=json.loads(data)
      
              if json_data['Response']=='True':
                  print_json(json_data)
                  # Asks user whether to download the poster of the movie
                  if json_data['Poster']!='N/A':
                      save_poster(json_data)
              else:
                  print("Error encountered: ",json_data['Error'])
      
          except urllib.error.URLError as e:
              print(f"ERROR: {e.reason}"
      
    10. 通过输入 Titanic 测试 search_movie 函数:

      search_movie("Titanic")
      

      以下是为 Titanic 检索到的数据:

      http://www.omdbapi.com/?t=Titanic&apikey=17cdc959
      --------------------------------------------------
      Title: Titanic
      Year: 1997
      Rated: PG-13
      Released: 19 Dec 1997
      Runtime: 194 min
      Genre: Drama, Romance
      Director: James Cameron
      Writer: James Cameron
      Actors: Leonardo DiCaprio, Kate Winslet, Billy Zane, Kathy Bates
      Plot: A seventeen-year-old aristocrat falls in love with a kind but poor artist aboard the luxurious, ill-fated R.M.S. Titanic.
      Language: English, Swedish
      Country: USA
      Awards: Won 11 Oscars. Another 111 wins & 77 nominations.
      Ratings: [{'Source': 'Internet Movie Database', 'Value': '7.8/10'}, {'Source': 'Rotten Tomatoes', 'Value': '89%'}, {'Source': 'Metacritic', 'Value': '75/100'}]
      Metascore: 75
      imdbRating: 7.8
      imdbVotes: 913,780
      imdbID: tt0120338
      --------------------------------------------------
      
    11. 通过输入 "Random_error"(显然,这将找不到,你应该能够检查你的错误捕获代码是否正常工作)来测试 search_movie 函数:

      search_movie("Random_error")
      

      检索 "Random_error" 的数据:

      http://www.omdbapi.com/?t=Random_error&apikey=17cdc959
      Error encountered:  Movie not found!
      

    在你工作的同一目录中查找名为 Posters 的文件夹。它应该包含一个名为 Titanic.jpg 的文件。检查该文件。

    活动第 11 项的解决方案:正确从数据库检索数据

    完成此活动的步骤如下:

    1. 连接到提供的 petsDB 数据库:

      import sqlite3
      conn = sqlite3.connect("petsdb")
      
    2. 编写一个函数来检查连接是否成功:

      # a tiny function to make sure the connection is successful
      def is_opened(conn):
          try:
              conn.execute("SELECT * FROM persons LIMIT 1")
              return True
          except sqlite3.ProgrammingError as e:
              print("Connection closed {}".format(e))
              return False
      print(is_opened(conn))
      

      输出如下:

      True
      
    3. 关闭连接:

      conn.close()
      
    4. 检查连接是否打开或关闭:

      print(is_opened(conn))
      

      输出如下:

      False
      
    5. 查找 persons 数据库中的不同年龄组。连接到提供的 petsDB 数据库:

      conn = sqlite3.connect("petsdb")
      c = conn.cursor()
      
    6. 执行以下命令:

      for ppl, age in c.execute("SELECT count(*), age FROM persons GROUP BY age"):
          print("We have {} people aged {}".format(ppl, age))
      

      输出如下:

      图 8.17:按年龄分组的输出部分

      图 8.17:按年龄分组的输出部分
    7. 要找出哪个年龄组的人数最多,请执行以下命令:

      sfor ppl, age in c.execute(
          "SELECT count(*), age FROM persons GROUP BY age ORDER BY count(*) DESC"):
          print("Highest number of people is {} and came from {} age group".format(ppl, age))
          break
      

      输出如下:

      Highest number of people is 5 and came from 73 age group
      
    8. 要找出有多少人没有全名(姓氏为空/空值),请执行以下命令:

      res = c.execute("SELECT count(*) FROM persons WHERE last_name IS null")
      for row in res:
          print(row)
      

      输出如下:

      (60,)
      
    9. 要找出有多少人有多于一只宠物,请执行以下命令:

      res = c.execute("SELECT count(*) FROM (SELECT count(owner_id) FROM pets GROUP BY owner_id HAVING count(owner_id) >1)")
      for row in res:
          print("{} People has more than one pets".format(row[0]))
      

      输出如下:

      43 People has more than one pets
      
    10. 要找出接受过治疗的有多少宠物,请执行以下命令:

      res = c.execute("SELECT count(*) FROM pets WHERE treatment_done=1")
      for row in res:
          print(row)
      

      输出如下:

      (36,)
      
    11. 要找出接受过治疗且已知宠物类型的宠物有多少,请执行以下命令:

      res = c.execute("SELECT count(*) FROM pets WHERE treatment_done=1 AND pet_type IS NOT null")
      for row in res:
          print(row)
      

      输出如下:

      (16,)
      
    12. 要找出来自名为 "east port" 的城市的宠物有多少,请执行以下命令:

      res = c.execute("SELECT count(*) FROM pets JOIN persons ON pets.owner_id = persons.id WHERE persons.city='east port'")
      for row in res:
          print(row)
      

      输出如下:

      (49,)
      
    13. 要找出有多少宠物来自名为“东港”的城市并且接受了治疗,执行以下命令:

      res = c.execute("SELECT count(*) FROM pets JOIN persons ON pets.owner_id = persons.id WHERE persons.city='east port' AND pets.treatment_done=1")
      for row in res:
          print(row)
      

      输出结果如下:

      (11,)
      

    活动十二的解决方案:数据整理任务 - 修复联合国数据

    完成此活动的步骤如下:

    1. 导入所需的库:

      import numpy as np
      import pandas as pd
      import matplotlib.pyplot as plt
      import warnings
      warnings.filterwarnings('ignore')s
      
    2. 保存数据集的 URL 并使用 pandas 的read_csv方法直接传递此链接以创建 DataFrame:

      education_data_link="http://data.un.org/_Docs/SYB/CSV/SYB61_T07_Education.csv"
      df1 = pd.read_csv(education_data_link)
      
    3. 打印 DataFrame 中的数据:

      df1.head()
      

      输出结果如下:

      图 9.4:删除第一行后的 DataFrame

      图 9.3:联合国数据的 DataFrame
    4. 由于第一行不包含有用的信息,使用skiprows参数删除第一行:

      df1 = pd.read_csv(education_data_link,skiprows=1)
      
    5. 打印 DataFrame 中的数据:

      df1.head()
      

      输出结果如下:

      图 9.4:删除第一行后的 DataFrame

      图 9.4:删除第一行后的 DataFrame
    6. 删除 Region/Country/Area 和 Source 列,因为它们不太有帮助:

      df2 = df1.drop(['Region/Country/Area','Source'],axis=1)
      
    7. 将以下名称分配为 DataFrame 的列:['Region/Country/Area','Year','Data','Value','Footnotes']

      df2.columns=['Region/Country/Area','Year','Data','Enrollments (Thousands)','Footnotes']
      
    8. 打印 DataFrame 中的数据:

      df1.head()
      

      输出结果如下:

      图 9.5:删除 Region/Country/Area 和 Source 列后的 DataFrame

      图 9.5:删除 Region/Country/Area 和 Source 列后的 DataFrame
    9. 检查Footnotes列包含多少唯一值:

      df2['Footnotes'].unique()
      

      输出结果如下:

      图 9.6:脚注列的唯一值

      图 9.6:脚注列的唯一值
    10. Value列数据转换为数值型,以便进一步处理:

      type(df2['Enrollments (Thousands)'][0])
      

      输出结果如下:

      str
      
    11. 创建一个实用函数,将Value列中的字符串转换为浮点数:

      def to_numeric(val):
          """
          Converts a given string (with one or more commas) to a numeric value
          """
          if ',' not in str(val):
              result = float(val)
          else:
              val=str(val)
              val=''.join(str(val).split(','))
              result=float(val)
          return result
      
    12. 使用apply方法将此函数应用于Value列数据:

      df2['Enrollments (Thousands)']=df2['Enrollments (Thousands)'].apply(to_numeric)
      
    13. 打印Data列中数据的唯一类型:

      df2['Data'].unique()
      

      输出结果如下:

      图 9.7:列中的唯一值
    14. 通过过滤和选择从原始 DataFrame 中创建三个 DataFrame:

      • df_primary:仅包含接受基础教育的学生(千)

      • df_secondary:仅包含接受中等教育的学生(千)

      • df_tertiary:仅包含接受高等教育的学生(千):

        df_primary = df2[df2['Data']=='Students enrolled in primary education (thousands)']
        df_secondary = df2[df2['Data']=='Students enrolled in secondary education (thousands)']
        df_tertiary = df2[df2['Data']=='Students enrolled in tertiary education (thousands)']
        
    15. 使用条形图比较低收入国家和高收入国家的初级学生入学率:

      primary_enrollment_india = df_primary[df_primary['Region/Country/Area']=='India']
      primary_enrollment_USA = df_primary[df_primary['Region/Country/Area']=='United States of America']
      
    16. 打印primary_enrollment_india数据:

      primary_enrollment_india
      

      输出结果如下:

      图 9.8:印度基础教育入学率的数据

      图 9.8:印度基础教育入学率的数据
    17. 打印primary_enrollment_USA数据:

      primary_enrollment_USA
      

      输出结果如下:

      图 9.9:美国基础教育入学率的数据

      图 9.9:美国基础教育入学率的数据
    18. 绘制印度数据:

      plt.figure(figsize=(8,4))
      plt.bar(primary_enrollment_india['Year'],primary_enrollment_india['Enrollments (Thousands)'])
      plt.title("Enrollment in primary education\nin India (in thousands)",fontsize=16)
      plt.grid(True)
      plt.xticks(fontsize=14)
      plt.yticks(fontsize=14)
      plt.xlabel("Year", fontsize=15)
      plt.show()
      

      输出结果如下:

      图 9.10:印度基础教育入学率的条形图

      图 9.10:印度小学入学柱状图
    19. 绘制美国的数据:

      plt.figure(figsize=(8,4))
      plt.bar(primary_enrollment_USA['Year'],primary_enrollment_USA['Enrollments (Thousands)'])
      plt.title("Enrollment in primary education\nin the United States of America (in thousands)",fontsize=16)
      plt.grid(True)
      plt.xticks(fontsize=14)
      plt.yticks(fontsize=14)
      plt.xlabel("Year", fontsize=15)
      plt.show()
      

      输出结果如下:

      图 9.11:美国小学入学柱状图

      图 9.11:美国小学入学柱状图

      数据插补:显然,我们缺少一些数据。假设我们决定通过在可用数据点之间进行简单线性插值来插补这些数据点。我们可以拿出笔和纸或计算器来计算这些值,并手动创建一个数据集。但作为一个数据处理员,我们当然会利用 Python 编程,并使用 pandas 插补方法来完成这项任务。但要做到这一点,我们首先需要创建一个包含缺失值的 DataFrame – 也就是说,我们需要将另一个包含缺失值的 DataFrame 附加到当前 DataFrame 中。

      (针对印度)附加对应缺失年份的行2004 - 2009, 2011 – 2013

    20. 找出缺失的年份:

      missing_years = [y for y in range(2004,2010)]+[y for y in range(2011,2014)]
      
    21. 打印missing_years变量中的值:

      missing_years
      

      输出结果如下:

      [2004, 2005, 2006, 2007, 2008, 2009, 2011, 2012, 2013]
      
    22. 使用np.nan创建一个包含值的字典。注意,有 9 个缺失数据点,因此我们需要创建一个包含相同值重复 9 次的列表:

      dict_missing = {'Region/Country/Area':['India']*9,'Year':missing_years,
                      'Data':'Students enrolled in primary education (thousands)'*9,
                      'Enrollments (Thousands)':[np.nan]*9,'Footnotes':[np.nan]*9}
      
    23. 创建一个包含缺失值的 DataFrame(来自前面的字典),我们可以append

      df_missing = pd.DataFrame(data=dict_missing)
      
    24. 将新的 DataFrames 附加到之前存在的 DataFrame 中:

      primary_enrollment_india=primary_enrollment_india.append(df_missing,ignore_index=True,sort=True)
      
    25. 打印primary_enrollment_india中的数据:

      primary_enrollment_india
      

      输出结果如下:

      图 9.12:对印度小学入学数据进行附加数据后的数据

      图 9.12:对印度小学入学数据进行附加数据后的数据
    26. 按照年份排序并使用reset_index重置索引。使用inplace=True在 DataFrame 本身上执行更改:

      primary_enrollment_india.sort_values(by='Year',inplace=True)
      primary_enrollment_india.reset_index(inplace=True,drop=True)
      
    27. 打印primary_enrollment_india中的数据:

      primary_enrollment_india
      

      输出结果如下:

      图 9.13:对印度小学入学数据进行排序后的数据

      图 9.13:对印度小学入学数据进行排序后的数据
    28. 使用interpolate方法进行线性插值。它通过线性插值值填充所有的NaN。有关此方法的更多详细信息,请查看此链接:pandas.pydata.org/pandas-docs/version/0.17/generated/pandas.DataFrame.interpolate.html

      primary_enrollment_india.interpolate(inplace=True)
      
    29. 打印primary_enrollment_india中的数据:

      primary_enrollment_india
      

      输出结果如下:

      图 9.14:对印度小学入学数据进行插值后的数据
    30. 绘制数据:

      plt.figure(figsize=(8,4))
      plt.bar(primary_enrollment_india['Year'],primary_enrollment_india['Enrollments (Thousands)'])
      plt.title("Enrollment in primary education\nin India (in thousands)",fontsize=16)
      plt.grid(True)
      plt.xticks(fontsize=14)
      plt.yticks(fontsize=14)
      plt.xlabel("Year", fontsize=15)
      plt.show()
      

      输出结果如下:

      图 9.15:印度小学入学柱状图

      图 9.15:印度小学入学柱状图
    31. 对美国重复相同的步骤:

      missing_years = [2004]+[y for y in range(2006,2010)]+[y for y in range(2011,2014)]+[2016]
      
    32. 打印missing_years中的值。

      missing_years
      

      输出结果如下:

      [2004, 2006, 2007, 2008, 2009, 2011, 2012, 2013, 2016]
      
    33. 创建dict_missing,如下所示:

      dict_missing = {'Region/Country/Area':['United States of America']*9,'Year':missing_years, 'Data':'Students enrolled in primary education (thousands)'*9, 'Value':[np.nan]*9,'Footnotes':[np.nan]*9}
      
    34. 创建df_missing的 DataFrame,如下所示:

      df_missing = pd.DataFrame(data=dict_missing)
      
    35. 将此追加到primary_enrollment_USA变量,如下所示:

      primary_enrollment_USA=primary_enrollment_USA.append(df_missing,ignore_index=True,sort=True)
      
    36. primary_enrollment_USA变量中的值进行排序,如下所示:

      primary_enrollment_USA.sort_values(by='Year',inplace=True)
      
    37. 重置primary_enrollment_USA变量的索引,如下所示:

      primary_enrollment_USA.reset_index(inplace=True,drop=True)
      
    38. 按如下方式插值primary_enrollment_USA变量:

      primary_enrollment_USA.interpolate(inplace=True)
      
    39. 打印primary_enrollment_USA变量:

      primary_enrollment_USA
      

      输出如下:

      图 9.16:完成所有操作后美国小学入学数据

      图 9.16:完成所有操作后美国小学入学数据
    40. 尽管如此,第一个值是空的。我们可以使用limitlimit_direction参数与插值方法来填充它。我们是如何知道这个的?通过在 Google 上搜索并查看这个 StackOverflow 页面。始终搜索你问题的解决方案,寻找已经完成的工作并尝试实现它:

      primary_enrollment_USA.interpolate(method='linear',limit_direction='backward',limit=1)
      

      输出如下:

      图 9.17:限制数据后美国小学入学数据

      图 9.17:限制数据后美国小学入学数据
    41. 打印primary_enrollment_USA中的数据:

      primary_enrollment_USA
      

      输出如下:

      图 9.18:美国小学入学数据

      图 9.18:美国小学入学数据
    42. 绘制数据:

      plt.figure(figsize=(8,4))
      plt.bar(primary_enrollment_USA['Year'],primary_enrollment_USA['Enrollments (Thousands)'])
      plt.title("Enrollment in primary education\nin the United States of America (in thousands)",fontsize=16)
      plt.grid(True)
      plt.xticks(fontsize=14)
      plt.yticks(fontsize=14)
      plt.xlabel("Year", fontsize=15)
      plt.show()
      

      输出如下:

    图 9.19:美国小学入学条形图

    图 9.19:美国小学入学条形图

    活动 13:数据处理任务 – 清洗 GDP 数据

    完成此活动的步骤如下:

    1. 印度 GDP 数据:我们将尝试从在世界银行门户中找到的 CSV 文件中读取印度的 GDP 数据。它已经提供给你,并且托管在 Packt GitHub 仓库上。但是,如果我们尝试正常读取它,Pandas 的read_csv方法将抛出错误。让我们看看如何一步一步地从这个文件中读取有用信息的指南:

      df3=pd.read_csv("India_World_Bank_Info.csv")
      

      输出如下:

      ---------------------------------------------------------------------------
      ParserError                               Traceback (most recent call last)
      <ipython-input-45-9239cae67df7> in <module>()
      …..
      ParserError: Error tokenizing data. C error: Expected 1 fields in line 6, saw 3
      

      我们可以尝试使用error_bad_lines=False选项在这种情况下。

    2. 读取印度世界银行信息.csv文件:

      df3=pd.read_csv("India_World_Bank_Info.csv",error_bad_lines=False)
      df3.head(10)
      

      输出如下:

      图 9.20:来自印度世界银行信息的 DataFrame

      图 9.20:来自印度世界银行信息的 DataFrame

      注意:

      有时,输出可能找不到,因为有三行而不是预期的单行。

    3. 显然,此文件的分隔符是制表符(\t):

      df3=pd.read_csv("India_World_Bank_Info.csv",error_bad_lines=False,delimiter='\t')
      df3.head(10)
      

      输出如下:

      图 9.21:使用分隔符后的印度世界银行信息 DataFrame

      图 9.21:使用分隔符后的印度世界银行信息 DataFrame
    4. 使用skiprows参数跳过前 4 行:

      df3=pd.read_csv("India_World_Bank_Info.csv",error_bad_lines=False,delimiter='\t',skiprows=4)
      df3.head(10)
      

      输出如下:

      图 9.22:使用 skiprows 后的印度世界银行信息 DataFrame

      图 9.22:使用 skiprows 后的印度世界银行信息 DataFrame
    5. 仔细检查数据集:在这个文件中,列是年度数据,行是各种类型的信息。通过用 Excel 检查文件,我们发现Indicator Name列是特定数据类型的名称所在的列。我们使用感兴趣的信息过滤数据集,并将其转置(行和列互换)以使其格式与先前的教育数据集相似:

      df4=df3[df3['Indicator Name']=='GDP per capita (current US$)'].T
      df4.head(10)
      

      输出如下:

      图 9.23:关注人均 GDP 的 DataFrame

      图 9.23:关注人均 GDP 的 DataFrame
    6. 没有索引,所以让我们再次使用reset_index

      df4.reset_index(inplace=True)
      df4.head(10)
      

      输出如下:

      图 9.24:使用 reset_index 从印度世界银行信息中创建的 DataFrame

      图 9.24:使用 reset_index 从印度世界银行信息中创建的 DataFrame
    7. 前三行没有用。我们可以重新定义 DataFrame 而不包括它们。然后,我们再次重新索引:

      df4.drop([0,1,2],inplace=True)
      df4.reset_index(inplace=True,drop=True)
      df4.head(10)
      

      输出如下:

      图 9.25:删除和重置索引后的印度世界银行信息 DataFrame

      在删除和重置索引后,从印度世界银行信息中创建的 DataFrame
    8. 让我们适当地重命名列(这对于合并是必要的,我们将在稍后查看):

      df4.columns=['Year','GDP']
      df4.head(10)
      

      输出如下:

      图 9.26:关注年份和 GDP 的 DataFrame

      图 9.26:关注年份和 GDP 的 DataFrame
    9. 看起来我们有从 1960 年以来的 GDP 数据。但我们感兴趣的是 2003-2016 年。让我们检查最后 20 行:

      df4.tail(20)
      

      输出如下:

      图 9.27:来自印度世界银行信息的 DataFrame

      图 9.27:来自印度世界银行信息的 DataFrame
    10. 因此,我们应该对 43-56 行感到满意。让我们创建一个名为df_gdp的 DataFrame:

      df_gdp=df4.iloc[[i for i in range(43,57)]]
      df_gdp
      

      输出如下:

      图 9.28:来自印度世界银行信息的 DataFrame

      图 9.28:来自印度世界银行信息的 DataFrame
    11. 我们需要再次重置索引(为了合并):

      df_gdp.reset_index(inplace=True,drop=True)
      df_gdp
      

      输出如下:

      图 9.29:来自印度世界银行信息的 DataFrame

      图 9.29:来自印度世界银行信息的 DataFrame
    12. 此 DataFrame 中的年份不是int类型。因此,它将与教育 DataFrame 合并时出现问题:

      df_gdp['Year']
      

      输出如下:

      图 9.30:关注年份的 DataFrame
    13. 使用 Python 内置的int函数的apply方法。忽略任何抛出的警告:

      df_gdp['Year']=df_gdp['Year'].apply(int)
      

    活动第 14 题的解决方案:数据整理任务 – 合并联合国数据和 GDP 数据

    完成此活动的步骤如下:

    1. 现在,将两个 DataFrame,即primary_enrollment_indiadf_gdp,在Year列上合并:

      primary_enrollment_with_gdp=primary_enrollment_india.merge(df_gdp,on='Year')
      primary_enrollment_with_gdp
      

      输出如下:

      图 9.31:合并后的数据
    2. 现在,我们可以删除DataFootnotesRegion/Country/Area列:

      primary_enrollment_with_gdp.drop(['Data','Footnotes','Region/Country/Area'],axis=1,inplace=True)
      primary_enrollment_with_gdp
      

      输出如下:

      图 9.32:删除数据、脚注和地区/国家/区域列后的合并数据

      图 9.32:删除数据、脚注和地区/国家/区域列后的合并数据
    3. 对列进行重新排列,以便数据科学家能够正确查看和展示:

      primary_enrollment_with_gdp = primary_enrollment_with_gdp[['Year','Enrollments (Thousands)','GDP']]
      primary_enrollment_with_gdp
      

      输出如下:

      图 9.33:重新排列列后的合并数据

      图 9.33:重新排列列后的合并数据
    4. 绘制数据:

      plt.figure(figsize=(8,5))
      plt.title("India's GDP per capita vs primary education enrollment",fontsize=16)
      plt.scatter(primary_enrollment_with_gdp['GDP'],
                  primary_enrollment_with_gdp['Enrollments (Thousands)'],
                 edgecolor='k',color='orange',s=200)
      plt.xlabel("GDP per capita (US $)",fontsize=15)
      plt.ylabel("Primary enrollment (thousands)",fontsize=15)
      plt.xticks(fontsize=14)
      plt.yticks(fontsize=14)
      plt.grid(True)
      plt.show()
      

      输出如下:

    图 9.34:合并数据的散点图

    图 9.34:合并数据的散点图

    活动 15:数据处理任务 – 将新数据连接到数据库

    完成此活动的步骤如下:

    1. 连接到数据库并写入值。我们首先导入 Python 的sqlite3模块,然后使用connect函数连接到数据库。将Year指定为该表的PRIMARY KEY

      import sqlite3
      with sqlite3.connect("Education_GDP.db") as conn:
          cursor = conn.cursor()
          cursor.execute("CREATE TABLE IF NOT EXISTS \
                         education_gdp(Year INT, Enrollment FLOAT, GDP FLOAT, PRIMARY KEY (Year))")
      
    2. 对数据集的每一行运行循环,将它们逐个插入到表中:

      with sqlite3.connect("Education_GDP.db") as conn:
          cursor = conn.cursor()
          for i in range(14):
              year = int(primary_enrollment_with_gdp.iloc[i]['Year'])
              enrollment = primary_enrollment_with_gdp.iloc[i]['Enrollments (Thousands)']
              gdp = primary_enrollment_with_gdp.iloc[i]['GDP']
              #print(year,enrollment,gdp)
              cursor.execute("INSERT INTO education_gdp (Year,Enrollment,GDP) VALUES(?,?,?)",(year,enrollment,gdp))
      

      如果我们查看当前文件夹,应该会看到一个名为Education_GDP.db的文件,如果我们使用数据库查看程序检查它,我们可以看到数据已传输到那里。

    在这些活动中,我们检查了一个完整的数据处理流程,包括从网络和本地驱动器读取数据,过滤、清洗、快速可视化、插补、索引、合并,并将数据写回数据库表。我们还编写了自定义函数来转换一些数据,并了解了在读取文件时可能遇到错误的情况如何处理。

posted @ 2025-10-24 09:50  绝不原创的飞龙  阅读(2)  评论(0)    收藏  举报