Pandas-秘籍-全-

Pandas 秘籍(全)

原文:Pandas Cookbook

协议:CC BY-NC-SA 4.0

零、前言

自从 2012 年被《哈佛评论》(Harvard Review)称为,“21 世纪最性感的工作”之后,数据科学的知名度迅速上升。在 2016 年和 2017 年,它被 Glassdoor 排名第一。 行业的需求助推了数据科学的飞速普及。 几个应用在新闻中引起了轰动,例如 Netflix 提出了更好的电影推荐,IBM 沃森在 Jeopardy 击败了人类,特斯拉制造了无人驾驶汽车,美国职棒大联盟的球队发现了被低估的前景以及谷歌学习识别互联网上的猫。

几乎每个行业都在寻找使用数据科学来构建新技术或提供更深刻见解的方法。 由于取得了如此显着的成功,炒作的光环似乎封装了数据科学。 支持这种炒作的大多数科学进展都来自机器学习领域,机器学习领域产生了使预测负责人工智能的算法。

所有机器学习算法的基本组成部分当然是数据。 由于公司已经意识到这一点,所以不乏它。 商业智能公司 Domo 估计,在最近两年中已创建了 90% 的世界数据。 尽管机器学习得到了所有关注,但它完全依赖于所馈送数据的质量。 在数据到达机器学习算法的输入层之前,必须对其进行准备,并且为了正确准备数据,需要对它进行彻底的探索,以进行基本理解并识别不正确之处。 在探索数据之前,必须先将其捕获。

总之,我们可以将数据科学流水线分为三个阶段-数据捕获,数据探索和机器学习。 有大量工具可用于完成管道的每个阶段。 Pandas 是科学 Python 生态系统中用于数据探索和分析的主要工具。 它具有检查,清理,整理,过滤,转换,聚合甚至可视化(在一些帮助下)所有类型数据的强大能力。 它不是最初捕获数据的工具,也不是构建机器学习模型的工具。

对于许多使用 Python 的数据分析人员和科学家来说,绝大多数工作将使用 Pandas 来完成。 这可能是因为最初的数据探索和准备往往花费最多的时间。 一些整个项目仅包含数据探索,没有机器学习组件。 数据科学家在此阶段花费了太多时间,以至于出现了永恒的传说 - 数据科学家花费了 80% 的时间来清理数据,另外 20% 的时间抱怨清理数据。

尽管有大量的开放源代码和免费编程语言可用于数据探索,但目前该领域仅由两个参与者(Python 和 R)主导。这两种语言的语法差异很大,但都具有进行数据分析和执行机器学习的能力。 受欢迎程度的一种度量是在受欢迎的 Q&A 站点 Stack Overflow 上提出的问题数量:

尽管这不是使用情况的真实衡量标准,但很显然,Python 和 R 都已变得越来越流行,这可能是由于它们的数据科学功能所致。 有趣的是,直到 2012 年数据科学开始兴起,Python 问题的比例一直保持不变。 这张图最令人惊讶的是,Pandas 问题现在占 Stack Overflow 上所有最新问题的百分之一。

Python 之所以成为数据科学首选语言的原因之一是,它是一种相当容易学习和开发的语言,因此入门门槛低。 它也是免费和开源的,能够在各种硬件和软件上运行,并且轻而易举地启动并运行。 它有一个非常活跃的社区,在线上有大量免费资源。 在我看来,Python 是用于开发程序的最有趣的语言之一。 语法非常清晰,简洁和直观,但像所有语言一样,需要花费相当长的时间才能掌握。

由于 Python 并非像 R 那样用于数据分析,因此语法可能不像其他 Python 库那样自然。 实际上,这可能是其中有太多栈溢出问题的原因的一部分。 尽管 Pandas 功能强大,但通常编写得很差。 本书的主要目的之一是展示高性能和惯用的 Pandas 代码。

不幸的是,Stack Overflow 发挥了其巨大的作用,使错误信息永久存在,并且是大量写得不好的 Pandas 的来源。 这实际上不是 Stack Overflow 或其社区的错。 Pandas 是一个开放源代码项目,即使在最近,它也进行了许多重大更改,因为它在 2018 年已接近成立十年。然而,开放源代码的优点在于,新功能一直在不断增加。

本书中的秘籍是根据我作为数据科学家工作,建立和托管数周的数据探索训练营,回答关于 Stack Overflow 的数百个问题以及为我的本地聚会小组创建教程而制定的。 这些秘籍不仅为常见数据问题提供惯用的解决方案,而且还带您穿越许多真实世界的数据集,在这些数据集中经常发现令人惊讶的见解。 这些秘籍还将帮助您掌握 Pandas 库,从而极大地提高生产力。 仅对 Pandas 有粗略了解的人与对 Pandas 有所了解的人之间存在巨大差异。 有很多有趣而有趣的技巧来解决您的数据问题,这些技巧只有在您真正了解库内外的情况下才会显现出来。 就个人而言,我发现 Pandas 是一种用于分析数据的令人愉悦且有趣的工具,希望您和我一起享受旅途。 如有疑问,请随时在 Twitter 上与我联系:@TedPetrou

本书涵盖的内容

第 1 章,“Pandas 基础”涵盖了用于识别两个主要 Pandas 数据结构(序列和数据帧)的组成部分的解剖结构和词汇表。 每一列必须仅具有一种数据类型,并且涵盖了每种数据类型。 您将学习如何通过调用和链接它们的方法来释放序列和数据帧的潜能。

第 2 章,“基本数据帧操作”着重介绍您将在数据分析期间执行的最关键和最常见的操作。

第 3 章,“开始数据分析”可帮助您开发例程,以在读取数据后开始使用。 其他有趣的发现将被发现。

第 4 章,“选择数据子集”涵盖了选择不同数据子集的许多不同且可能引起混淆的方式。

第 5 章,“布尔索引”涵盖了查询数据以根据布尔条件选择子集的过程。

第 6 章,“索引对齐”以非常重要且经常被误解的index对象为目标。 错误使用索引会导致许多错误的结果,这些秘籍向您展示了如何正确使用它来提供有力的结果。

第 7 章,“进行聚集,过滤和转换的分组”涵盖了强大的分组函数,这些函数几乎是数据分析期间始终需要的。 您将构建自定义函数以应用于您的组。

第 8 章,“将数据重组为整齐的表格”,解释了整洁的数据及其重要性,然后向您展示了如何将许多不同形式的杂乱数据集转换为整洁的数据集。

第 9 章,“组合 Pandas 对象”涵盖了许多可用于垂直或水平组合数据帧和序列的方法。 我们还将进行一些网上爬虫比较,以比较特朗普总统和奥巴马总统的支持率,并连接到 SQL 关系数据库。

第 10 章,“时间序列分析”涵盖了高级且强大的时间序列函数,可以按任何可能的时间维度进行剖析。

第 11 章,“使用 Matplotlib,Pandas 和 Seaborn 进行可视化”介绍了 matplotlib 库,该库负责 Pandas 中的所有绘图。 然后,我们将重点转移到 Pandas plot方法上,最后转移到seaborn库,该库能够产生在美学上令人愉悦的可视化效果,而这些效果在 Pandas 中不直接可用。

这本书需要什么

Pandas 是用于 Python 编程语言的第三方包,在本书出版时,它的版本为 0.20。 目前,Python 有两个主要受支持的版本,版本 2.7 和 3.6。 Python 3 是未来,现在强烈建议所有 Python 的科学计算用户都使用它,因为 2020 年将不再支持 Python2。本书中的所有示例均已在 Python 3.6 上以 pandas 0.20 运行和测试。

除了 Pandas,您还需要安装 matplotlib 2.0 版和 seaborn 0.8 版可视化库。 对 Pandas 的主要依赖是 NumPy 库,它构成了大多数流行的 Python 科学计算库的基础。

您可以通过多种方式来安装 Pandas 和计算机上提到的其余库,但是到目前为止,最简单的方法是安装 Anaconda 发行版。 它由 Continuum Analytics 创建,将所有流行的用于科学计算的库打包到一个可下载的文件中,该文件可在 Windows,Mac OSX 和 Linux 上使用。 访问下载页面以获取 Anaconda 发行版。

除了所有科学计算库之外,Anaconda 发行版还附带 Jupyter 笔记本,这是一个基于浏览器的程序,可使用 Python 和许多其他语言进行开发。 本书的所有秘籍都是在 Jupyter 笔记本内部开发的,每一章的所有单个笔记本都可以使用。

无需使用 Anaconda 发行版就可以安装本书所需的所有库。 对于感兴趣的用户,请访问 Pandas 安装页面

运行 Jupyter 笔记本

建议阅读本书的全部内容的方法是启动并运行 Jupyter 笔记本,以便您可以在阅读秘籍的同时运行代码。 与仅阅读本书相比,这使您可以自己进行探索并获得更深刻的理解。

假设您已经在计算机上安装了 Anaconda 发行版,则可以使用两个选项来启动 Jupyter 笔记本:

  • 使用程序 Anaconda Navigator
  • 从终端/命令提示符运行jupyter notebook命令

Anaconda Navigator 是基于 GUI 的工具,可让您轻松查找 Anaconda 提供的所有不同软件。 运行程序将为您提供如下屏幕:

如您所见,有许多可用的程序。 单击“启动”以打开 Jupyter 笔记本。 浏览器中将打开一个新标签,向您显示主目录中的文件夹和文件列表:

您可以通过打开终端/命令提示符并运行Jupyter 笔记本,命令来启动 Jupyter 笔记本,而不是使用 Anaconda Navigator,如下所示:

不必从主目录运行此命令。 您可以从任何位置运行它,浏览器中的内容将反映该位置。

尽管我们现在已经启动了 Jupyter 笔记本程序,但实际上并没有启动一个单独的笔记本就可以开始用 Python 开发。 为此,您可以单击页面右侧的“新建”按钮,该按钮将下拉列出所有可能使用的内核的列表。 如果您刚刚下载了 Anaconda,则只有一个可用的内核(Python 3)。 选择 Python 3 内核后,将在浏览器中打开一个新标签,您可以在其中开始编写 Python 代码:

当然,您可以打开以前创建的笔记本,而不用开始新的笔记本。 为此,只需在 Jupyter 笔记本浏览器主页中提供的文件系统中导航,然后选择要打开的笔记本即可。 所有 Jupyter 笔记本文件都以.ipynb结尾。 例如,当您导航到这本书的笔记本文件的位置时,您将看到所有这些文件,如下所示:

这本书是给谁的

本书包含近 100 种秘籍,从非常简单到高级。 所有秘籍都力求以清晰,简洁,现代的惯用 Pandas 代码编写。 “工作原理”部分包含对秘籍每个步骤的复杂性的非常详细的描述。 通常,在“更多”部分,您将获得似乎是一个全新的秘籍。 这本书密密麻麻地包装着大量的 Pandas 代码。

概括地说,与后五章相比,前六章中的秘籍更简单,更侧重于 Pandas 的基本和基本操作,后五章中的重点是更高级的操作并且更受项目驱动。 由于复杂性范围很广,因此这本书对于新手和日常用户都非常有用。 根据我的经验,即使定期使用 Pandas 的人也不会在没有惯用的 Pandas 密码的情况下掌握它。 Pandas 提供的宽度在一定程度上促进了这一点。 几乎总是有多种方法可以完成相同的操作,这些方法可以使用户以非常低效的方式获得所需的结果。 在同一问题的两组 Pandas 解决方案之间看到一个数量级或更多个性能差异并不少见。

本书唯一真正的先决条件是 Python 的基础知识。 假定读者熟悉 Python 中所有常见的内置数据容器,例如列表,集合,字典和元组。

如何充分利用这本书

您可以采取几项措施来充分利用本书。 首先,也是最重要的是,您应该下载所有代码,这些代码将存储在 Jupyter 笔记本中。 阅读每个秘籍时,请在笔记本中运行代码的每个步骤。 在运行代码时,请确保自己进行探索。 其次,在您的浏览器选项卡之一中打开 Pandas 官方文档。 Pandas 文档是一个很好的资源,其中包含超过 1000 页的材料。 文档中有大多数 Pandas 操作的示例,它们通常会直接链接到“另见”部分。 虽然涵盖了大多数操作的基础知识,但它提供了一些琐碎的示例和伪造的数据,这些伪造的数据并不能反映您在分析现实世界中的数据集时可能遇到的情况。

约定

在本书中,您将找到一些可以区分不同类型信息的文本样式。 最常见的是,您会在每个秘籍中看到如下所示的代码块:

>>> employee = pd.read_csv('data/employee')
>>> max_dept_salary = employee.groupby('DEPARTMENT')['BASE_SALARY'].max()

在笔记本中输出时,pandas 序列和数据帧的样式不同。 Pandas 序列没有特殊的格式,只是原始文本。 它们将直接出现在代码块本身中创建它们的代码行的前面,如下所示:

>>> max_dept_salary.head()
DEPARTMENT
Admn. & Regulatory Affairs      140416.0
City Controller's Office         64251.0
City Council                    100000.0
Convention and Entertainment     38397.0
Dept of Neighborhoods (DON)      89221.0
Name: BASE_SALARY, dtype: float64

另一方面,数据帧在笔记本中具有良好的风格,并显示为代码框外部的图像,如下所示:

>>> employee.pivot_table(index='DEPARTMENT', 
                         columns='GENDER', 
                         values='BASE_SALARY').round(0).head()

文本,数据库表名称,文件夹名称,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄中的代码字如下所示:为了通过GENDER查找平均BASE_SALARY,可以使用pivot_table方法。

新术语重要词以粗体显示。 您在屏幕上看到的字词,例如在菜单或对话框中,将像这样显示在文本中:在 Jupyter 笔记本电脑中,按住快捷键S + Tab + Tab,将光标置于对象中的某个位置,将弹出文档字符串的窗口,使得该方法更易于使用。

提示和技巧是这样的。警告或重要提示出现在这样的框中。

每个秘籍的假设

应该假设在每个秘籍的开始,都会将 pandas,NumPy 和 matplotlib 导入命名空间。 为了将绘图直接嵌入到笔记本中,还必须运行魔术命令%matplotlib inline。 同样,所有数据都存储在 data目录中,并且最通常存储为 CSV 文件,可以通过read_csv函数直接读取。

>>> import pandas as pd
>>> import numpy as np
>>> import matplotlib.pyplot as plt
>>> %matplotlib inline

>>> my_dataframe = pd.read_csv('data/dataset_name.csv')

数据集说明

本书共使用了大约十二个数据集。 完成秘籍中的步骤时,在每个数据集上具有背景信息可能会非常有帮助。 可以在 PacktPublishing/Pandas-Cookbookdataset_descriptions Jupyter 笔记本中找到每个数据集的详细说明。 对于每个数据存储,将有一个列列表,有关每个列的信息以及有关如何获取数据的注释。

标题

在本书中,您会发现经常出现的几个标题(准备,操作步骤,工作原理,更多以及另见)。

为了给出有关如何完成秘籍的明确说明,我们使用以下部分:

准备

本节将告诉您秘籍中的预期内容,并介绍如何设置秘籍所需的任何软件或任何初步设置。

操作步骤

本节包含遵循秘籍所需的步骤。

工作原理

本节通常包括对上一节中发生的情况的详细说明。

更多

本节包含有关秘籍的其他信息,以使读者对秘籍有更多的了解。

另见

本部分提供了指向该秘籍其他有用信息的有用链接。

一、Pandas 基础

在本章中,我们将介绍以下内容:

  • 剖析数据帧的结构
  • 访问主要的数据帧组件
  • 了解数据类型
  • 选择单列数据作为序列
  • 调用序列方法
  • 与运算符一起使用序列
  • 将序列方法链接在一起
  • 使索引有意义
  • 重命名行和列名称
  • 创建和删除列

介绍

本章的目的是通过彻底检查序列和数据帧数据结构来介绍 Pandas 的基础。 对于 Pandas 用户来说,了解序列和数据帧的每个组件,并了解 Pandas 中的每一列数据正好具有一种数据类型,这一点至关重要。

在本章中,您将学习如何从数据帧中选择一个数据列,该数据列将作为序列返回。 使用此一维对象可以轻松显示不同的方法和运算符如何工作。 许多序列方法返回另一个序列作为输出。 这导致有可能连续调用其他方法,这被称为方法链接

序列和数据帧的索引组件是将 Pandas 与其他大多数数据分析库区分开的组件,并且是了解执行多少操作的关键。 当我们将其用作序列值的有意义的标签时,我们将瞥见这个强大的对象。 最后两个秘籍包含在数据分析期间经常发生的简单任务。

剖析数据帧的结构

在深入研究 Pandas 之前,值得了解数据帧的组件。 在视觉上,Pandas 数据帧的输出显示(在 Jupyter 笔记本中)似乎只不过是由行和列组成的普通数据表。 隐藏在表面下方的是三个组成部分-您必须具备的索引数据(也称为)。 请注意,以便最大化数据帧的全部潜力。

准备

此秘籍将电影数据集读入 pandas 数据帧中,并提供其所有主要成分的标签图。

操作步骤

  1. 使用read_csv函数读取影片数据集,并使用head方法显示前五行:
>>> movie = pd.read_csv('data/movie.csv')
>>> movie.head()
  1. 分析数据帧的标记解剖结构:

工作原理

Pandas 首先使用出色且通用的read_csv函数将数据从磁盘读入内存,然后读入数据帧。 列和索引的输出均以粗体显示,这使它们易于识别。 按照惯例,术语索引标签列名分别是指索引和列的各个成员。 术语索引整体上指所有索引标签,正如术语整体上指所有列名称一样。

列和索引用于特定目的,即为数据帧的列和行提供标签。 这些标签允许直接轻松地访问不同的数据子集。 当多个序列或数据帧组合在一起时,索引将在进行任何计算之前首先对齐。 列和索引统称为

DataFrame具有两个轴:垂直轴(索引)和水平轴(列)。 Pandas 借鉴了 NumPy 的约定,并使用整数 0/1 作为引用垂直/水平轴的另一种方式。

数据帧的数据(值)始终为常规字体,并且是与列或索引完全独立的组件。 Pandas 使用NaN不是数字)来表示缺失值。 请注意,即使color列仅包含字符串值,它仍使用NaN表示缺少的值。

列中间的三个连续点表示存在至少一列,但由于列数超过了预定义的显示限制,因此未显示。

Python 标准库包含csv模块,可用于解析和读取数据。 Pandas 的read_csv函数比该模块提供了性能和功能上的强大提升。

更多

head方法接受单个参数n,该参数控制显示的行数。 同样,tail方法返回最后的n行。

另见

访问主要的数据帧组件

可以直接从数据帧访问三个数据帧组件(索引,列和数据)中的每一个。 每个组件本身都是一个 Python 对象,具有自己的独特属性和方法。 通常,您希望对单个组件而不是对整个数据帧进行操作。

准备

此秘籍将数据帧的索引,列和数据提取到单独的变量中,然后说明如何从同一对象继承列和索引。

操作步骤

  1. 使用数据帧属性indexcolumnsvalues将索引,列和数据分配给它们自己的变量:
>>> movie = pd.read_csv('data/movie.csv')
>>> index = movie.index
>>> columns = movie.columns
>>> data = movie.values
  1. 显示每个组件的值:
>>> index
RangeIndex(start=0, stop=5043, step=1)

>>> columns
Index(['color', 'director_name', 'num_critic_for_reviews',
       ...
       'imdb_score', 'aspect_ratio', 'movie_facebook_likes'],
       dtype='object')

>>> data
array([['Color', 'James Cameron', 723.0, ..., 7.9, 1.78, 33000],
       ..., 
       ['Color', 'Jon Gunn', 43.0, ..., 6.6, 1.85, 456]],
       dtype=object)
  1. 输出每个数据帧组件的类型。 类型的名称是输出最后一个点后面的单词:
>>> type(index)
pandas.core.indexes.range.RangeIndex

>>> type(columns)
pandas.core.indexes.base.Index

>>> type(data)
numpy.ndarray
  1. 有趣的是,索引和列的类型似乎都密切相关。 内置的issubclass方法检查RangeIndex是否确实是Index的子类:
>>> issubclass(pd.RangeIndex, pd.Index)
True

工作原理

您可以使用indexcolumnsvalues属性访问数据帧的三个主要组件。columns属性的输出似乎只是列名称的序列。 从技术上讲,此列名称序列是Index对象。 函数type的输出是对象的完全限定的类名

变量columns的对象的全限定类名称为pandas.core.indexes.base.Index。 它以包名称开头,后跟模块路径,并以类型名称结尾。 引用对象的常用方法是在包名称后加上对象类型的名称。 在这种情况下,我们将这些列称为 Pandas 的Index对象。

内置的subclass函数检查第一个参数是否从第二个参数继承。IndexRangeIndex对象非常相似,实际上,pandas 具有许多专门为索引或列保留的相似对象。 索引和列都必须都是某种Index对象。 本质上,索引和列表示同一事物,但沿不同的轴。 有时将它们称为行索引列索引

在这种情况下,Index对象是指可用于索引或列的所有可能的对象。 它们都是pd.Index的子类。 这是Index对象的完整列表:CategoricalIndexMultiIndexIntervalIndexInt64IndexUInt64IndexFloat64IndexRangeIndexTimedeltaIndexDatetimeIndexPeriodIndex

RangeIndexIndex对象的一种特殊类型,类似于 Python 的range对象。 直到必须将其整个值序列加载到内存中为止,从而节省了内存。 它完全由其开始,停止和步长值定义。

更多

尽可能使用哈希表实现Index对象,以实现非常快速的选择和数据对齐。 它们与 Python 集相似,因为它们支持诸如相交和并集之类的操作,但是由于它们的排序允许重复,因此它们是不同的。

Python 字典和集合也通过哈希表实现,无论对象的大小如何,都可以在恒定时间内非常快速地进行成员资格检查。

注意values数据帧属性如何返回 NumPy N 维数组或ndarray。 大部分 Pandas 都严重依赖ndarray。 在索引,列和数据之下是 NumPy ndarrays。 可以将它们视为构建许多其他对象的 Pandas 的基本对象。 要看到这一点,我们可以查看indexcolumns的值:

>>> index.values
array([   0,    1,    2, ..., 4913, 4914, 4915])

>>> columns.values
array(['color', 'director_name', 'num_critic_for_reviews',
 ...
 'imdb_score', 'aspect_ratio', 'movie_facebook_likes'],
 dtype=object)

另见

了解数据类型

用非常广泛的术语来说,数据可以分类为连续的或分类的。 连续数据始终是数字,代表某种度量,例如身高,工资或薪水。 连续数据可能具有无限数量的可能性。 另一方面,分类数据代表离散的有限数量的值,例如汽车颜色,扑克手类型或谷类食品品牌。

Pandas 没有将数据大致分为连续数据或分类数据。 相反,它对许多不同的数据类型都有精确的技术定义。 下表包含所有 pandas 数据类型,及其等效字符串,以及每种类型的一些注释:

通用数据类型名称 NumPy / Pandas 对象 Pandas 字符串名称 注释
布尔 np.bool bool 存储为单个字节。
整数 np.int int 默认为 64 位。 也可以使用无符号整数 - np.uint
浮点 np.float float 默认为 64 位。
复数 np.complex complex 在数据分析中很少见。
对象 np.object Oobject 通常为字符串,但是对于具有多种不同类型的列或其他 Python 对象(元组,列表,字典等)来说是万能的。
日期时间 np.datetime64, pd.Timestamp datetime64 具有纳秒精度的特定时间点。
时间增量 np.timedelta64, pd.Timedelta timedelta64 时间增量,从几天到纳秒。
类别 pd.Categorical Categorical 仅限于 Pandas。 对于唯一值相对较少的对象列很有用。

准备

在此秘籍中,我们将显示数据帧中每一列的数据类型。 了解每一列中保存的数据类型至关重要,因为它会从根本上改变可能进行的操作的类型。

操作步骤

  1. 使用dtypes属性显示每一列及其数据类型:
>>> movie = pd.read_csv('data/movie.csv')
>>> movie.dtypes
color                       object
director_name               object
num_critic_for_reviews     float64
duration                   float64
director_facebook_likes    float64
                            ...   
title_year                 float64
actor_2_facebook_likes     float64
imdb_score                 float64
aspect_ratio               float64
movie_facebook_likes         int64
Length: 28, dtype: object
  1. 使用get_dtype_counts方法返回每种数据类型的计数:
>>> movie.get_dtype_counts()
float64    13
int64       3
object     12

工作原理

每个数据帧的列必须恰好是一种类型。 例如,aspect_ratio列中的每个值都是 64 位浮点数,movie_facebook_likes列中的每个值都是 64 位整数。 Pandas 默认使用其核心数字类型,整数,并且浮点数为 64 位,而不管所有数据放入内存所需的大小如何。 即使列完全由整数值 0 组成,数据类型仍将为int64get_dtype_counts是一种方便的方法,用于直接返回数据帧中所有数据类型的计数。

同构数据是指所有具有相同类型的列的另一个术语。 整个数据帧可能包含不同列的不同数据类型的异构数据

对象数据类型是一种与其他数据类型不同的数据类型。 对象数据类型的列可以包含任何有效 Python 对象的值。 通常,当列属于对象数据类型时,它表示整个列都是字符串。 不一定是这种情况,因为这些列可能包含整数,布尔值,字符串或其他甚至更复杂的 Python 对象(例如列表或字典)的混合物。 对象数据类型是 Pandas 无法识别为其他任何特定类型的列的全部内容。

更多

几乎所有的 Pandas 数据类型都是直接从 NumPy 构建的。 这种紧密的集成使用户可以更轻松地集成 Pandas 和 NumPy 操作。 随着 Pandas 越来越大,越来越流行,事实证明,对象数据类型对于具有字符串值的所有列来说太通用了。 Pandas 创建了自己的分类数据类型,以处理具有固定数量的可能值的字符串(或数字)列。

另见

选择单列数据作为序列

序列是来自数据帧的单列数据。 它是数据的一个维度,仅由索引和数据组成。

准备

此秘籍检查了两种不同的语法以选择“序列”,一种使用索引运算符,另一种使用点符号。

操作步骤

  1. 将列名作为字符串传递给索引运算符以选择数据的序列:
>>> movie = pd.read_csv('data/movie.csv')
>>> movie['director_name']
  1. 或者,您可以使用点符号来完成相同的任务:
>>> movie.director_name
  1. 检查序列解剖结构:
  2. 验证输出是否为序列:
>>> type(movie['director_name'])
pandas.core.series.Series

工作原理

Python 有几个内置对象用于包含数据,例如列表,元组和字典。 所有这三个对象都使用索引运算符来选择其数据。数据帧是更强大,更复杂的数据容器,但它们也使用索引运算符作为选择数据的主要方式。 将单个字符串传递给数据帧索引运算符将返回一个序列。

序列的视觉输出风格比数据帧少。 它代表一列数据。 连同索引和值一起,输出显示序列的名称,长度和数据类型。

或者,虽然不建议这样做,但可能会出错,但是可以使用带有列名作为属性的点表示法来访问数据列。 尽管它适用于此特定示例,但这不是最佳实践,并且容易出错和误用。 不能以这种方式访问​​带有空格或特殊字符的列名称。 如果列名称为director name,则该操作将失败。 与数据帧方法冲突的列名,例如count,也无法使用点符号正确选择。 分配新值或删除带有点符号的列可能会导致意外的结果。 因此,在生产代码中应避免使用点表示法访问列。

更多

如果会引起麻烦,为什么有人会使用点符号语法呢? 程序员很懒,而且键入的字符更少。 但主要是,当您想使用自动完成智能功能时,它非常方便。 因此,在本书中有时会使用点标记进行列选择。 自动完成智能非常适合帮助您了解对象可用的所有可能的属性和方法。

在使用步骤 1 中的索引运算符后,尝试链接操作时,智能将无法工作,但将继续使用步骤 2 中的点符号。下面的屏幕快照显示了在选择了索引之后的弹出窗口。director_name带点符号。 在点后按选项卡后,所有可能的属性和方法将显示在列表中:

在 Jupyter 笔记本中,在按下Shift + Tab + Tab,并将光标放在对象中某处的情况下,将弹出文档字符串窗口,使该方法更易于使用。 如果您在使用索引运算符选择一列后尝试链接一个操作,则该智能再次消失。

注意点表示法的另一个原因是,它在流行的问答网站 Stack Overflow 上在线使用的数量激增。 另外,请注意,旧列名称现在是序列的name,实际上已经成为一个属性:

>>> director = movie['director_name']
>>> director.name
'director_name'

可以使用to_frame方法将此序列转换为单列数据帧。 此方法将使用序列名称作为新的列名称:

>>> director.to_frame()

另见

  • 要了解 Python 对象如何获得使用索引运算符的能力,请参见 Python 文档中的__getitem__特殊方法
  • 请参阅第 2 章,“基本数据帧操作”的“选择多个数据帧的列”秘籍

调用序列方法

利用一维序列是所有 Pandas 数据分析的组成部分。 典型的工作流程将使您在序列和数据帧上的执行语句之间来回切换。 调用序列方法是使用序列提供的功能的主要方法。

准备

序列和数据帧都具有强大的函数。 我们可以使用dir函数来揭示序列的所有属性和方法。 此外,我们可以找到序列和数据帧共有的属性和方法的数量。 这两个对象共享绝大多数的属性和方法名称:

>>> s_attr_methods = set(dir(pd.Series))
>>> len(s_attr_methods)
442

>>> df_attr_methods = set(dir(pd.DataFrame))
>>> len(df_attr_methods)
445

>>> len(s_attr_methods & df_attr_methods)
376

本秘籍涵盖了最常见且功能最强大的序列方法。 对于数据帧,许多方法几乎是等效的。

操作步骤

  1. 读完电影数据集后,让我们选择两个具有不同数据类型的序列。director_name列包含字符串,形式上是对象数据类型,列actor_1_facebook_likes包含数字数据,形式上是float64
>>> movie = pd.read_csv('data/movie.csv')
>>> director = movie['director_name']
>>> actor_1_fb_likes = movie['actor_1_facebook_likes']
  1. 检查每个序列的head
>>> director.head()
0        James Cameron
1       Gore Verbinski
2           Sam Mendes
3    Christopher Nolan
4          Doug Walker
Name: director_name, dtype: object

>>> actor_1_fb_likes.head()
0     1000.0
1    40000.0
2    11000.0
3    27000.0
4      131.0
Name: actor_1_facebook_likes, dtype: float64
  1. 序列的数据类型通常确定哪种方法最有用。 例如,对象数据类型序列最有用的方法之一是value_counts,它计算每个唯一值的所有出现次数:
>>> director.value_counts()
Steven Spielberg        26
Woody Allen             22
Martin Scorsese         20
Clint Eastwood          20
                        ..
Fatih Akin               1
Analeine Cal y Mayor     1
Andrew Douglas           1
Scott Speer              1
Name: director_name, Length: 2397, dtype: int64
  1. value_counts方法通常对于具有对象数据类型的序列更为有用,但有时也可以提供对数值序列的深入了解。 与actor_1_fb_likes一起使用时,似乎已将较高的数字四舍五入到最接近的千位,因为不太可能有那么多电影获得准确的 1,000 个赞:
>>> actor_1_fb_likes.value_counts()
1000.0     436
11000.0    206
2000.0     189
3000.0     150
          ... 
216.0        1
859.0        1
225.0        1
334.0        1
Name: actor_1_facebook_likes, Length: 877, dtype: int64
  1. 可以使用sizeshape参数或len函数对序列中的元素数进行计数:
>>> director.size
4916
>>> director.shape
(4916,)
>>> len(director)
4916
  1. 此外,还有一种有用但令人困惑的count方法,它返回非缺失值的数量:
>>> director.count()
4814
>>> actor_1_fb_likes.count()
4909
  1. 基本摘要统计信息可以通过minmaxmeanmedianstdsum方法得出:
>>> actor_1_fb_likes.min(), actor_1_fb_likes.max(), \
    actor_1_fb_likes.mean(), actor_1_fb_likes.median(), \
    actor_1_fb_likes.std(), actor_1_fb_likes.sum()
(0.0, 640000.0, 6494.488490527602, 982.0, 15106.98, 31881444.0)
  1. 为了简化步骤 7,您可以使用describe方法一次返回汇总统计信息和一些分位数。 当describe与对象数据类型列一起使用时,将返回完全不同的输出:
>>> actor_1_fb_likes.describe()
count      4909.000000
mean       6494.488491
std       15106.986884
min           0.000000
25%         607.000000
50%         982.000000
75%       11000.000000
max      640000.000000
Name: actor_1_facebook_likes, dtype: float64

>>> director.describe()
count                 4814
unique                2397
top       Steven Spielberg
freq                    26
Name: director_name, dtype: object
  1. quantile方法用于计算数字数据的精确分位数:
>>> actor_1_fb_likes.quantile(.2)
510

>>> actor_1_fb_likes.quantile([.1, .2, .3, .4, .5,
                               .6, .7, .8, .9])
0.1      240.0
0.2      510.0
0.3      694.0
0.4      854.0
        ...   
0.6     1000.0
0.7     8000.0
0.8    13000.0
0.9    18000.0
Name: actor_1_facebook_likes, Length: 9, dtype: float64
  1. 由于第 6 步中的count方法返回的值小于在第 5 步中找到的序列元素的总数,因此我们知道每个序列中都有缺失的值。isnull方法可用于确定每个单独的值是否丢失。 结果将是布尔序列,其长度与原始序列相同:
>>> director.isnull()
0       False
1       False
2       False
3       False
        ...  
4912     True
4913    False
4914    False
4915    False
Name: director_name, Length: 4916, dtype: bool
  1. 可以用fillna方法替换序列中的所有缺失值:
>>> actor_1_fb_likes_filled = actor_1_fb_likes.fillna(0)
>>> actor_1_fb_likes_filled.count()
4916
  1. 要删除缺少值的序列元素,请使用dropna
>>> actor_1_fb_likes_dropped = actor_1_fb_likes.dropna()
>>> actor_1_fb_likes_dropped.size
4909

工作原理

将字符串传递给数据帧的索引运算符会将单个列选择为序列。 选择本秘籍中使用的方法是因为它们在数据分析中的使用频率。

本秘籍中的步骤应简单明了,并具有易于解释的输出。 即使输出易于阅读,您也可能无法跟踪返回的对象。 它是标量值,元组,另一个序列还是其他 Python 对象? 花一点时间,看看每一步之后返回的输出。 您可以命名返回的对象吗?

步骤 1 中head方法的结果是另一个序列。value_counts方法也产生一个序列,但具有原始序列的唯一值作为索引,计数作为其值。 在步骤 5 中,sizecount返回标量值,但是shape返回单项元组。

形状属性返回一个单项元组似乎很奇怪,但这是从 NumPy 借来的约定,它允许任意数量的维度的数组。

在步骤 7 中,每个方法返回一个标量值,并作为元组输出。 这是因为 Python 将仅包含逗号分隔值且不带括号的表达式视为元组。

在步骤 8 中,describe返回一个序列,其所有摘要统计信息名称均作为索引,而实际统计信息则为值。

在步骤 9 中,quantile是灵活的,当传递单个值时返回标量值,但在给定列表时返回序列。

从步骤 10、11 和 12,isnullfillnadropna都返回一个序列。

更多

value_counts方法是最有用的序列方法之一,在探索性分析中特别是在分类列分析中被大量使用。 它默认返回计数,但是通过将normalize参数设置为True,则返回相对频率,这提供了另一种分布图:

>>> director.value_counts(normalize=True)
Steven Spielberg        0.005401
Woody Allen             0.004570
Martin Scorsese         0.004155
Clint Eastwood          0.004155
                          ...   
Fatih Akin              0.000208
Analeine Cal y Mayor    0.000208
Andrew Douglas          0.000208
Scott Speer             0.000208
Name: director_name, Length: 2397, dtype: float64

在此秘籍中,我们通过观察count方法的结果与size属性不匹配,确定该序列中缺少值。 一种更直接的方法是使用hasnans属性:

>>> director.hasnans
True

isnull存在一个补充:notnull方法,该方法为所有非缺失值返回True

>>> director.notnull()
0        True
1        True
2        True
3        True
        ...  
4912    False
4913     True
4914     True
4915     True
Name: director_name, Length: 4916, dtype: bool

另见

  • 要连续调用许多序列方法,请在本章中一起参考“链接序列方法”秘籍

与运算符一起使用序列

Python 中存在大量用于操作对象的运算符。 运算符本身不是对象,而是强制对对象执行操作的语法结构和关键字。 例如,将加法运算符放在两个整数之间时,Python 会将它们加在一起。 在以下代码中查看更多运算符示例:

>>> 5 + 9   # plus operator example adds 5 and 9
14

>>> 4 ** 2  # exponentiation operator raises 4 to the second power
16

>>> a = 10  # assignment operator assigns 10 to a

>>> 5 <= 9  # less than or equal to operator returns a boolean
True

运算符可以处理任何类型的对象,而不仅仅是数字数据。 这些示例显示了正在操作的不同对象:

>>> 'abcde' + 'fg' 
'abcdefg'

>>> not (5 <= 9)
False

>>> 7 in [1, 2, 6]
False

>>> set([1,2,3]) & set([2,3,4])
set([2,3])

访问 TutorialsPoint,以查看所有基本 Python 运算符的表。 并非对每个对象都实现所有运算符。 这些示例在使用运算符时都会产生错误:

>>> [1, 2, 3] - 3
TypeError: unsupported operand type(s) for -: 'list' and 'int'

>>> a = set([1,2,3])     
>>> a[0] TypeError: 'set' object does not support indexing

序列和数据帧对象可与大多数 Python 运算符一起使用。

准备

在此秘籍中,各种运算符将应用于不同的序列对象,以产生具有完全不同值的新序列。

操作步骤

  1. 选择imdb_score列作为序列:
>>> movie = pd.read_csv('data/movie.csv')
>>> imdb_score = movie['imdb_score']
>>> imdb_score
0       7.9
1       7.1
2       6.8
       ... 
4913    6.3
4914    6.3
4915    6.6
Name: imdb_score, Length: 4916, dtype: float64
  1. 使用加法运算符可向每个序列元素添加一个:
>>> imdb_score + 1
0       8.9
1       8.1
2       7.8
       ... 
4913    7.3
4914    7.3
4915    7.6
Name: imdb_score, Length: 4916, dtype: float64
  1. 其他基本算术运算符减号(-),乘法(*),除法(/)和幂(**)与标量值相似。 在这一步中,我们将序列乘以2.5
>>> imdb_score * 2.5
0       19.75
1       17.75
2       17.00
        ...  
4913    15.75
4914    15.75
4915    16.50
Name: imdb_score, Length: 4916, dtype: float64
  1. Python 使用两个连续的除法运算符(//)进行地板除法,并使用百分号(%)进行模数运算,这将在除法后返回余数。 序列使用这些相同的方式:
>>> imdb_score // 7
0       1.0
1       1.0
2       0.0
       ... 
4913    0.0
4914    0.0
4915    0.0
Name: imdb_score, Length: 4916, dtype: float64
  1. 存在六个比较运算符,它们大于(>),小于(<),大于或等于(>=,小于或等于(<=),等于(==),并且不等于(!=)。 每个比较运算符都会根据条件的结果将序列中的每个值转换为TrueFalse
>>> imdb_score > 7
0        True
1        True
2       False
        ...  
4913    False
4914    False
4915    False
Name: imdb_score, Length: 4916, dtype: bool

>>> director = movie['director_name']
>>> director == 'James Cameron'
0        True
1       False
2       False
        ...  
4913    False
4914    False
4915    False
Name: director_name, Length: 4916, dtype: bool

工作原理

本秘籍中使用的所有运算符都将相同的操作应用于序列中的每个元素。 在本机 Python 中,这将需要一个for循环在应用操作之前遍历序列中的每个项目。 Pandas 严重依赖 NumPy 库,该库允许进行向量化计算,也可以对整个数据序列进行操作而无需显式编写for循环。 每个操作都返回一个具有相同索引的序列,但其值已被运算符修改。

更多

此秘籍中使用的所有运算符都具有等效的方法,这些方法可产生完全相同的结果。 例如,在步骤 1 中,可以用add方法再现imdb_score + 1。 检查以下代码以查看秘籍中每个步骤的方法版本:

>>> imdb_score.add(1)              # imdb_score + 1
>>> imdb_score.mul(2.5)            # imdb_score * 2.5
>>> imdb_score.floordiv(7)         # imdb_score // 7
>>> imdb_score.gt(7)               # imdb_score > 7
>>> director.eq('James Cameron')   # director == 'James Cameron'

为什么 Pandas 提供与这些运算符等效的方法? 从本质上讲,运算符只能以一种方式进行操作。 另一方面,方法可以具有允许您更改其默认行为的参数:

运算符分组 运算符 序列方法名称
算术 +-*///%** addsubmuldivfloordivmodpow
比较 <><=>===!= ltgtlegeeqne

您可能对 Python 序列对象或与此相关的任何对象如何在遇到运算符时知道该怎么办感到好奇。 例如,表达式imdb_score * 2.5如何知道将序列中的每个元素乘以2.5? Python 使用特殊方法为对象与运算符通信提供了一种内置的标准化方法。

特殊方法是对象在遇到运算符时在内部调用的方法。 特殊方法在 Python 数据模型中定义,这是官方文档中非常重要的一部分,并且对于整个语言中的每个对象都是相同的。 特殊方法始终以两个下划线开头和结尾。 例如,每当使用乘法运算符时,就会调用特殊方法__mul__。 Python 将imdb_score * 2.5表达式解释为imdb_score.__mul__(2.5)

使用特殊方法和使用运算符之间没有什么区别,因为它们在做完全相同的事情。 运算符只是特殊方法的语法糖。

另见

将序列方法链接在一起

在 Python 中,每个变量都是一个对象,并且所有对象都具有引用或返回更多对象的属性和方法。 使用点符号的方法的顺序调用称为方法链接。 Pandas 是一个很适合进行方法链接的库,因为许多序列和数据帧方法返回更多的序列和数据帧,因此可以调用更多方法。

准备

为了激励方法链接,让我们用一个简单的英语句子将事件链转换为方法链。 考虑一下句子,“一个人开车去商店买食物,然后开车回家,在洗碗之前准备,做饭,上菜和吃食物”。

该句子的 Python 版本可能采用以下形式:

>>> person.drive('store')\
          .buy('food')\
          .drive('home')\
          .prepare('food')\
          .cook('food')\
          .serve('food')\
          .eat('food')\
          .cleanup('dishes')

在前面的代码中,person是调用每个方法的对象,就像人正在执行原始句子中的所有动作一样。 传递给每个方法的参数指定方法的操作方式。

尽管可以在单个连续的行中写入整个方法链,但更可取的是在每行中写入一个方法。 由于 Python 通常不允许将一个表达式写在多行上,因此您需要使用反斜杠行继续符。 或者,您可以将整个表达式用括号括起来。 为了进一步提高可读性,请将每种方法直接放在其上方的点下。 此秘籍显示了与 Pandas 序列相似的方法链接。

操作步骤

  1. 加载电影数据集,然后选择两列作为不同的序列:
>>> movie = pd.read_csv('data/movie.csv')
>>> actor_1_fb_likes = movie['actor_1_facebook_likes']
>>> director = movie['director_name']
  1. 附加到链上的最常见方法之一是head方法。 这抑制了长输出。 对于较短的链,则不需要将每种方法放在不同的行上:
>>> director.value_counts().head(3)
Steven Spielberg    26
Woody Allen         22
Clint Eastwood      20
Name: director_name, dtype: int64
  1. 计算缺失值数量的一种常见方法是在isnull之后链接sum方法:
>>> actor_1_fb_likes.isnull().sum()
7
  1. actor_1_fb_likes的所有非缺失值都应为整数,因为不可能有小数 Facebook 点赞。 缺少值的任何数字列的数据类型都必须为float。 如果我们用零填充actor_1_fb_likes中的缺失值,则可以使用astype方法将其转换为整数:
>>> actor_1_fb_likes.dtype
dtype('float64')

>>> actor_1_fb_likes.fillna(0)\
                    .astype(int)\
                    .head()
0     1000
1    40000
2    11000
3    27000
4      131
Name: actor_1_facebook_likes, dtype: int64

工作原理

所有的 Python 对象都可以进行方法链接,因为每个对象方法必须返回另一个自身将具有更多方法的对象。 该方法不必返回相同类型的对象。

步骤 2 首先使用value_counts返回一个序列,然后链接head方法以选择前三个元素。 最后返回的对象是一个序列,也可以在其上链接更多方法。

在步骤 3 中,isnull方法创建一个布尔序列。 Pandas 在数值上将False/True求值为 0/1,因此sum方法返回缺失值的数量。

步骤 4 中的三个链接方法中的每一个都返回一个序列。 似乎不直观,但是astype方法返回具有不同数据类型的全新序列。

更多

无需对第 3 步中的布尔值求和以找到缺失值的总数,我们可以采用序列的平均值来获取缺失值的百分比:

>>> actor_1_fb_likes.isnull().mean()
0.0014

如本秘籍开头所述,对于多行代码,可以使用括号而不是反斜杠。 第 4 步可以这样重写:

>>> (actor_1_fb_likes.fillna(0)
                     .astype(int)
                     .head())

并不是所有的程序员都喜欢使用方法链接,因为它有一些缺点。 这样的缺点之一是调试变得困难。 链中产生的中间对象都不存储在变量中,因此,如果出现意外结果,将很难跟踪链中发生它的确切位置。

秘籍开头的示例可以重写,以使每种方法的结果都保存为唯一变量。 这使跟踪错误更加容易,因为您可以在每个步骤检查对象:

>>> person1 = person.drive('store')
>>> person2 = person1.buy('food')
>>> person3 = person2.drive('home')
>>> person4 = person3.prepare('food')
>>> person5 = person4.cook('food')
>>> person6 = person5.serve('food')
>>> person7 = person6.eat('food')
>>> person8 = person7.cleanup('dishes')

使索引有意义

数据帧的索引为每行提供一个标签。 如果在创建数据帧时未显式提供索引,则默认情况下,将创建RangeIndex,其标签为从 0 到n-1的整数,其中 n 是行数。

准备

此秘籍将影片数据集的毫无意义的默认行索引替换为影片标题,这更有意义。

操作步骤

  1. 读取电影数据集,然后使用set_index方法将每部电影的标题设置为新索引:
>>> movie = pd.read_csv('data/movie.csv')
>>> movie2 = movie.set_index('movie_title')
>>> movie2
  1. 或者,可以通过read_csv函数的index_col参数在初始读取时选择一列作为索引:
>>> movie = pd.read_csv('data/movie.csv', index_col='movie_title')

工作原理

一个有意义的索引是清楚地标识每一行的索引。 默认的范围索引不是很有帮助。 由于每一行仅标识一部电影的数据,因此使用电影标题作为标签是有意义的。 如果您提前知道哪个列将是一个很好的索引,则可以在导入时使用read_csv函数的index_col参数指定该索引。

默认情况下,set_indexread_csv都将从数据帧中删除用作索引的列。 使用set_index,可以通过将drop参数设置为False将列保留在数据帧中。

更多

相反,可以使用reset_index方法将索引变成一列。 这将使movie_title再次成为一列,并将索引还原回RangeIndexreset_index始终将列作为数据帧中的第一个列,因此这些列可能未按其原始顺序排列:

>>> movie2.reset_index()

另见

重命名行和列名称

数据帧上最基本,最常见的操作之一是重命名行或列的名称。 好的列名是描述性的,简短的,并且在大小写,空格,下划线和其他功能方面遵循通用约定。

准备

在此秘籍中,行名和列名均被重命名。

操作步骤

  1. 读取电影数据集,并通过将其设置为电影标题来使索引有意义:
>>> movie = pd.read_csv('data/movie.csv', index_col='movie_title')
  1. 数据帧的rename方法接受将旧值映射到新值的字典。 让我们为行创建一个,为列创建另一个:
>>> idx_rename = {'Avatar':'Ratava', 'Spectre': 'Ertceps'} 
>>> col_rename = {'director_name':'Director Name', 
                  'num_critic_for_reviews': 'Critical Reviews'} 
  1. 将字典传递给rename方法,并将结果分配给新变量:
>>> movie_renamed = movie.rename(index=idx_rename, 
                                 columns=col_rename)
>>> movie_renamed.head()

工作原理

数据帧的rename方法允许使用indexcolumns参数同时重命名行标签和列标签。 这些参数中的每一个都可以设置为字典,该字典将旧标签映射到它们的新值。

更多

重命名行标签和列标签有多种方法。 可以直接将索引和列属性重新分配给 Python 列表。 当列表具有与行和列标签相同数量的元素时,此分配有效。 以下代码在每个索引对象上使用tolist方法来创建 Python 标签列表。 然后,它修改列表中的几个值,并将列表重新分配给属性indexcolumns

>>> movie = pd.read_csv('data/movie.csv', index_col='movie_title')
>>> index = movie.index
>>> columns = movie.columns

>>> index_list = index.tolist()
>>> column_list = columns.tolist()

## rename the row and column labels with list assignments
>>> index_list[0] = 'Ratava'
>>> index_list[2] = 'Ertceps'
>>> column_list[1] = 'Director Name'
>>> column_list[2] = 'Critical Reviews'

>>> print(index_list)
['Ratava', "Pirates of the Caribbean: At World's End", 'Ertceps', 'The Dark Knight Rises', ... ]

>>> print(column_list)
['color', 'Director Name', 'Critical Reviews', 'duration', ...]

## finally reassign the index and columns
>>> movie.index = index_list
>>> movie.columns = column_list

创建和删除列

在数据分析期间,极有可能需要创建新列来表示新变量。 通常,这些新列将从数据集中已有的先前列创建。 Pandas 有几种不同的方法可以向数据帧添加新列。

准备

在此秘籍中,我们通过使用赋值在影片数据集中创建新列,然后使用drop方法删除列。

操作步骤

  1. 创建新列的最简单方法是为其分配标量值。 将新列的名称作为字符串放入索引运算符。 让我们在电影数据集中创建has_seen列以指示我们是否看过电影。 我们将为每个值分配零。 默认情况下,新列将追加到末尾:
>>> movie = pd.read_csv('data/movie.csv')
>>> movie['has_seen'] = 0
  1. 有几列包含有关 Facebook 点赞次数的数据。 让我们将所有演员和导演的 Facebook 点赞数加起来,并将其分配到actor_director_facebook_likes列:
>>> movie['actor_director_facebook_likes'] =  \
        (movie['actor_1_facebook_likes'] + 
         movie['actor_2_facebook_likes'] + 
         movie['actor_3_facebook_likes'] + 
         movie['director_facebook_likes'])
  1. 从本章的“调用序列方法”秘籍中,我们知道此数据集包含缺失值。 当像上一步那样将数字列彼此相加时,pandas 将缺失值默认为零。 但是,如果缺少特定行的所有值,则 Pandas 也会将总数也保留为丢失。 让我们检查新列中是否缺少值,并用 0 填充它们:
>>> movie['actor_director_facebook_likes'].isnull().sum()
122
>>> movie['actor_director_facebook_likes'] = \
    movie['actor_director_facebook_likes'].fillna(0)
  1. 数据集中还有另一列cast_total_facebook_likes。 看到此列的百分比来自我们新创建的列actor_director_facebook_likes会很有趣。 在创建百分比列之前,我们先进行一些基本数据验证。 让我们确保cast_total_facebook_likes大于或等于actor_director_facebook_likes
>>> movie['is_cast_likes_more'] = \
         (movie['cast_total_facebook_likes'] >=             
          movie['actor_director_facebook_likes'])
  1. is_cast_likes_more现在是一列布尔值。 我们可以使用all序列方法检查此列的所有值是否均为True
>>> movie['is_cast_likes_more'].all()
False
  1. 事实证明,至少有一部电影的actor_director_facebook_likescast_total_facebook_likes多。 导演的 Facebook 点赞可能不是演员总点赞数的一部分。 让我们回溯并删除actor_director_facebook_likes列:
>>> movie = movie.drop('actor_director_facebook_likes',
                       axis='columns')
  1. 让我们重新创建一个仅包含演员总数的列:
>>> movie['actor_total_facebook_likes'] = \
         (movie['actor_1_facebook_likes'] + 
          movie['actor_2_facebook_likes'] + 
          movie['actor_3_facebook_likes'])

>>> movie['actor_total_facebook_likes'] = \
         movie['actor_total_facebook_likes'].fillna(0)
  1. 再次检查cast_total_facebook_likes中的所有值是否都大于actor_total_facebook_likes
>>> movie['is_cast_likes_more'] = \
         (movie['cast_total_facebook_likes'] >= 
          movie['actor_total_facebook_likes'])

>>> movie['is_cast_likes_more'].all()
True
  1. 最后,让我们计算来自actor_total_facebook_likescast_total_facebook_likes的百分比:
>>> movie['pct_actor_cast_like'] = \
         (movie['actor_total_facebook_likes'] / 
          movie['cast_total_facebook_likes'])
  1. 让我们验证此列的最小值和最大值在 0 到 1 之间:
>>> (movie['pct_actor_cast_like'].min(), 
     movie['pct_actor_cast_like'].max())
(0.0, 1.0)
  1. 然后,我们可以将该列输出为序列。 首先,我们需要将索引设置为电影标题,以便我们可以正确识别每个值。
>>> movie.set_index('movie_title')['pct_actor_cast_like'].head()
movie_title
Avatar                                        0.577369
Pirates of the Caribbean: At World's End      0.951396
Spectre                                       0.987521
The Dark Knight Rises                         0.683783
Star Wars: Episode VII - The Force Awakens    0.000000
Name: pct_actor_cast_like, dtype: float64

工作原理

许多 Pandas 操作都很灵活,列创建就是其中之一。 该秘籍既分配了标量值(如步骤 1 所示),又分配了序列(如步骤 2 所示),以创建新列。

步骤 2 将四个不同的序列使用加法运算符相加。 步骤 3 使用方法链来查找和填充缺失值。 步骤 4 使用大于或等于比较运算符返回布尔序列,然后在步骤 5 中使用all方法对其进行求值,以检查每个单个值是否为True

drop方法接受要删除的行或列的名称。 默认情况下是按索引名称删除行。 要删除列,必须将axis参数设置为 1 或column。 轴的默认值为 0 或字符串index

步骤 7 和 8 在没有director_facebook_likes列的情况下将步骤 3 的工作重做到步骤 5。 第 9 步最终计算出自第 4 步以来我们想要的期望列。第 10 步验证百分比在 0 到 1 之间。

更多

除了insert方法的末尾,还可以将新列插入数据帧中的特定位置。insert方法将新列的整数位置作为第一个参数,将新列的名称作为第二个参数,并将值作为第三个参数。 您将需要使用索引的get_loc方法来查找列名称的整数位置。

insert方法就地修改了调用的数据帧,因此不会有赋值语句。 可以通过从gross中减去budget并将其直接插入gross之后,来计算每部电影的利润:

>>> profit_index = movie.columns.get_loc('gross') + 1
>>> profit_index
9

>>> movie.insert(loc=profit_index,
                 column='profit',
                 value=movie['gross'] - movie['budget'])

使用drop方法删除列的另一种方法是使用del语句:

>>> del movie['actor_director_facebook_likes']

另见

  • 请参阅第 9 章,“组合 Pandas 对象”的“对数据帧添加新行”秘籍,来添加和删除行,这是一种较不常用的操作
  • 请参阅第 3 章,“开始数据分析”的“制定数据分析例程”秘籍。

二、数据帧基本操作

在本章中,我们将介绍以下主题:

  • 选择数据帧的多个列
  • 用方法选择列
  • 明智地排序列名称
  • 处理整个数据帧
  • 将数据帧方法链接在一起
  • 将运算符与数据帧一起使用
  • 比较缺失值
  • 转换数据帧操作的方向
  • 确定大学校园的多样性

介绍

本章介绍了数据帧的许多基本操作。 许多秘籍将与第 1 章,“Pandas 基础”中的内容类似,这些内容主要涵盖序列操作。

选择数据帧的多个列

选择单个列是通过将所需的列名作为字符串传递给数据帧的索引运算符来完成的。 在第 1 章,“Pandas 基础”的“选择序列”秘籍中对此进行了介绍。 通常需要关注当前工作数据集的一个子集,这是通过选择多个列来完成的。

准备

在此秘籍中,将从movie数据集中选择所有actordirector列。

操作步骤

  1. 读取电影数据集,并将所需列的列表传递给索引运算符:
>>> movie_actor_director = movie[['actor_1_name', 'actor_2_name',
                                  'actor_3_name', 'director_name']]
>>> movie_actor_director.head()

  1. 在某些情况下,需要选择数据帧的一列。 这是通过将单个元素列表传递给索引运算符来完成的:
>>> movie[['director_name']].head()

工作原理

数据帧的索引运算符非常灵活,可以接受许多不同的对象。 如果传递了字符串,它将返回一维序列。 如果将列表传递给索引运算符,它将以指定顺序返回列表中所有列的数据帧。

步骤 2 显示了如何选择单个列作为数据帧而不是序列。 最常见的是,使用字符串选择单个列,从而得到一个序列。 当数据帧是所需的输出时,只需将列名放在一个单元素列表中。

更多

在索引运算符内部传递长列表可能会导致可读性问题。 为了解决这个问题,您可以先将所有列名保存到列表变量中。 下面的代码获得与步骤 1 相同的结果:

>>> cols = ['actor_1_name', 'actor_2_name',
            'actor_3_name', 'director_name']
>>> movie_actor_director = movie[cols]

KeyError是处理 Pandas 的最常见例外之一。 此错误主要是由于列名或索引名的错误输入。 每当尝试不使用列表进行多列选择时,都会引发相同的错误:

>>> movie['actor_1_name', 'actor_2_name',
          'actor_3_name', 'director_name']
KeyError: ('actor_1_name', 'actor_2_name',
           'actor_3_name', 'director_name')

这是一个常见的错误,因为很容易忘记将所需的列放在列表中。 您可能想知道这里到底发生了什么。 技术上,用逗号分隔的四个字符串名称是一个元组对象。 通常,元组用开括号和闭括号括起来,但这不是必需的:

>>> tuple1 = 1, 2, 3, 'a', 'b'
>>> tuple2 = (1, 2, 3, 'a', 'b')
>>> tuple1 == tuple2
True

Pandas 正试图找到与元组('actor_1_name', 'actor_2_name', 'actor_3_name', 'director_name')完全相同的列名。 它失败并引发KeyError

用方法选择列

尽管列选择通常直接由索引运算符完成,但是有一些数据帧方法可以以替代方式方便其选择。select_dtypesfilter是执行此操作的两种有用方法。

准备

您需要熟悉所有 Pandas 数据类型以及如何访问它们。 第 1 章,“Pandas 基础”中的“了解数据类型”秘籍具有包含所有 Pandas 数据类型的表。

工作原理

  1. 读入电影数据集,并使用电影的标题标记每一行。 使用get_dtype_counts方法输出每种特定数据类型的列数:
>>> movie = pd.read_csv('data/movie.csv',
                        index_col='movie_title')
>>> movie.get_dtype_counts()
float64    13
int64       3
object     11
dtype: int64
  1. 使用select_dtypes方法仅选择整数列:
>>> movie.select_dtypes(include=['int']).head()

  1. 如果要选择所有数字列,则只需将字符串数字传递给include参数:
>>> movie.select_dtypes(include=['number']).head()

  1. 选择列的另一种方法是使用filter方法。 此方法很灵活,可以根据使用的参数搜索列名(或索引标签)。 在这里,我们使用like参数搜索包含确切字符串facebook的所有列名称:
>>> movie.filter(like='facebook').head()

  1. filter方法允许使用regex参数通过正则表达式搜索列。 在这里,我们搜索名称中某处有数字的所有列:
>>> movie.filter(regex='\d').head()

工作原理

步骤 1 列出了所有不同数据类型的频率。 或者,您可以使用dtypes属性来获取每一列的确切数据类型。select_dtypes方法在其include参数中获取数据类型的列表,并返回仅包含那些给定数据类型的列的数据帧。 列表值可以是数据类型的字符串名称,也可以是实际的 Python 对象。

filter方法仅通过检查列名而不是实际数据值来选择列。 它具有三个互斥的参数itemslikeregex,一次只能使用其中一个。like参数采用一个字符串,并尝试查找名称中某处包含该确切字符串的所有列名称。 为了获得更大的灵活性,您可以使用regex参数代替通过正则表达式选择列名称。 这个特定的正则表达式\d表示从零到九的所有数字,并且匹配其中至少包含一个数字的任何字符串。

正则表达式是代表搜索模式的字符序列,这些搜索模式用于选择文本的不同部分。 它们允许非常复杂和高度特定的模式匹配。

更多

filter方法带有另一个参数items,该参数采用一列确切的列名。 这几乎与索引运算符完全相同,只是如果其中一个字符串与列名不匹配,则不会引发KeyError。 例如,movie.filter(items=['actor_1_name', 'asdf'])运行无错误,并返回单列数据帧。

select_dtypes的一个令人困惑的方面是它同时接受字符串和 Python 对象的灵活性。 下表应阐明选择许多不同列数据类型的所有可能方法。 在 Pandas 中没有引用数据类型的标准或首选方法,因此最好同时了解两种方式:

Python 对象 字符串 注释
np.number number 选择整数和浮点数,而不考虑大小
np.float64, np.float_, float float64float_float 仅选择 64 位浮点数
np.float16, np.float32, np.float128 float16float32float128 分别选择精确的 16 位,32 位和 128 位浮点数
np.floating floating 选择所有浮点,而不管大小
np.int0, np.int64, np.int_, int int0int64int_int 仅选择 64 位整数
np.int8, np.int16, np.int32 int8int16int32 分别选择 8、16 和 32 位整数
np.integer integer 选择所有整数,而不考虑大小
np.object objectO 选择所有对象数据类型
np.datetime64 datetime64datetime 所有 64 位的日期时间
np.timedelta64 timedelta64timedelta 所有 64 位的时间增量
pd.Categorical category Pandas 特有的; NumPy 没有等效的东西

因为所有整数和浮点数默认为 64 位,所以可以通过使用字符串int,或float来选择它们,如上表所示。 如果要选择所有整数和浮点数,而不管它们的大小如何,请使用字符串number

另见

明智地排序列名称

最初将数据集导入为数据帧之后要考虑的首要任务之一是分析列的顺序。 这个基本任务经常被忽略,但是可以在分析进行中产生很大的不同。 计算机没有优先选择列顺序,计算也不受影响。 作为人类,我们自然地从左到右查看和阅读列,这直接影响我们对数据的解释。 杂物柱布置类似于壁橱中的杂物衣服布置。 在短裤顶部的衬衫和裤子旁边放西装是没有好处的。 考虑列顺序时,查找和解释信息要容易得多。

没有标准的规则集来规定应如何在数据集中组织列。 但是,优良作法是制定一组您始终遵循的准则以简化分析。 如果您与一组共享大量数据集的分析师合作,则尤其如此。

准备

以下是排序列的简单指南:

  • 将每列分为离散列或连续列
  • 在离散列和连续列中将公共列分组
  • 将最重要的列组首先放置在分类列之前,然后再放置连续列

本秘籍向您展示如何使用此指南排序各列。 有许多明智的可能排序。

操作步骤

  1. 读取电影数据集,然后扫描数据:
>>> movie = pd.read_csv('data/movie.csv')
>>> movie.head()

  1. 输出所有列名称并扫描相似的离散列和连续列:
>>> movie.columns
Index(['color', 'director_name', 'num_critic_for_reviews',
       'duration', 'director_facebook_likes',
       'actor_3_facebook_likes', 'actor_2_name',
       'actor_1_facebook_likes', 'gross', 'genres',
       'actor_1_name', 'movie_title', 'num_voted_users',
       'cast_total_facebook_likes', 'actor_3_name',
       'facenumber_in_poster', 'plot_keywords',
       'movie_imdb_link', 'num_user_for_reviews', 'language',
       'country', 'content_rating', 'budget', 'title_year',
       'actor_2_facebook_likes', 'imdb_score', 'aspect_ratio',
       'movie_facebook_likes'], dtype='object')
  1. 这些列似乎没有任何逻辑顺序。 将名称合理地组织到列表中,以便遵循上一部分的指南:
>>> disc_core = ['movie_title', 'title_year',
                 'content_rating', 'genres']
>>> disc_people = ['director_name', 'actor_1_name', 
                   'actor_2_name', 'actor_3_name']
>>> disc_other = ['color', 'country', 'language', 
                  'plot_keywords', 'movie_imdb_link']

>>> cont_fb = ['director_facebook_likes', 'actor_1_facebook_likes', 
               'actor_2_facebook_likes', 'actor_3_facebook_likes',
               'cast_total_facebook_likes', 'movie_facebook_likes']

>>> cont_finance = ['budget', 'gross']
>>> cont_num_reviews = ['num_voted_users', 'num_user_for_reviews',
                        'num_critic_for_reviews']
>>> cont_other = ['imdb_score', 'duration',
                  'aspect_ratio', 'facenumber_in_poster']
  1. 将所有列表连接在一起以获得最终的列顺序。 另外,请确保此列表包含原始文档中的所有列:
>>> new_col_order = disc_core + disc_people + \
                    disc_other + cont_fb + \
                    cont_finance + cont_num_reviews + \
                    cont_other
>>> set(movie.columns) == set(new_col_order)
True
  1. 将具有新列顺序的列表传递给数据帧的索引运算符以对列进行重新排序:
>>> movie2 = movie[new_col_order]
>>> movie2.head()

工作原理

要从数据帧中选择列的子集,请使用特定列名称的列表。 例如,movie[['movie_title', 'director_name']]仅使用movie_titledirector_name列创建一个新的数据帧。 通过名称选择列是 Pandas 数据帧的索引运算符的默认行为。

步骤 3 根据类型(离散或连续)以及它们的数据相似程度,将所有列名称整齐地组织到单独的列表中。 最重要的列(例如电影的标题)位于第一位。

步骤 4 连接所有列名称列表,并验证此新列表是否包含与原始列名称相同的值。 Python 集是无序的,并且相等语句检查一个集的每个成员是否是另一个集的成员。 手动排序此秘籍中的列容易受到人为错误的影响,因为很容易错误地忘记新列列表中的列。

步骤 5 通过将新的列顺序作为列表传递给索引运算符来完成重新排序。 现在,这个新顺序比原来的要明智得多。

更多

除了前面提到的简单建议外,还有其他排序列的准则。 Hadley Wickham 在有关整洁数据的开创性论文中建议将固定变量放在第一位,然后再放置测量变量。 由于此数据并非来自受控实验,因此可以灵活地确定哪些变量是固定的,哪些是测量的。 测量变量的良好候选者是我们希望预测的变量,例如gross,总收入或imdb_score。 例如,以这种顺序,我们可以混合离散变量和连续变量。 在该演员的名字之后直接放置 Facebook 点赞人数的列可能更有意义。 当然,由于计算部分不受列顺序的影响,因此您可以提出自己的列顺序准则。

通常,您将直接从关系数据库中提取数据。 关系数据库的一种非常常见的做法是将主键(如果存在)作为第一列,并在其后直接放置任何外键。

主键唯一地标识当前表中的行。 外键唯一地标识其他表中的行。

另见

处理整个数据帧

在第 1 章,“Pandas 基础”的“调用序列方法”秘籍中,对单列或序列数据进行操作的各种方法。 当从数据帧调用这些相同的方法时,它们会立即对每一列执行该操作。

准备

在本秘籍中,我们将对电影数据集探索各种最常见的数据帧属性和方法。

操作步骤

  1. 阅读电影数据集,并获取基本描述性属性shapesizendim,以及运行len函数:
>>> movie = pd.read_csv('data/movie.csv')
>>> movie.shape
(4916, 28)

>>> movie.size
137648

>>> movie.ndim
2

>>> len(movie)
4916
  1. 使用count方法查找每列的不丢失值的数量。 输出是一个序列,现在其旧列名称为,其索引为:
>>> movie.count()
color                     4897
director_name             4814
num_critic_for_reviews    4867
duration                  4901
                          ... 
actor_2_facebook_likes    4903
imdb_score                4916
aspect_ratio              4590
movie_facebook_likes      4916
Length: 28, dtype: int64
  1. 其他计算摘要统计信息的方法,例如minmaxmeanmedianstd都返回相似的序列,其索引中的列名称及其计算结果为值:
>>> movie.min()
num_critic_for_reviews     1.00
duration                   7.00
director_facebook_likes    0.00
actor_3_facebook_likes     0.00
                           ... 
actor_2_facebook_likes     0.00
imdb_score                 1.60
aspect_ratio               1.18
movie_facebook_likes       0.00
Length: 16, dtype: float64
  1. describe方法非常强大,可以一次计算前面步骤中的所有描述性统计数据和四分位数。 最终结果是一个数据帧,其描述性统计信息为,其索引为:
>>> movie.describe()

  1. 可以使用percentiles参数在describe方法中指定精确的分位数:
>>> movie.describe(percentiles=[.01, .3, .99])

工作原理

步骤 1 提供有关数据集大小的基本信息。shape属性返回行和列数的两个元素的元组。size属性返回数据帧中元素的总数,它只是行和列数的乘积。ndim属性返回维数,对于所有数据帧,维数均为 2。 Pandas 定义了内置的len函数以返回行数。

步骤 2 和步骤 3 中的方法将每一列汇总为一个数字。 现在,每个列名称都是序列中的索引标签,其汇总结果为相应的值。

如果仔细观察,您会发现步骤 3 的输出缺少步骤 2 的所有对象列。其原因是对象列中缺少值,而 pandas 不知道如何处理字符串值与缺失值。 它会静默删除无法为其计算最小值的所有列。

在这种情况下,静默意味着没有引发任何错误并且没有发出警告。 这有点危险,需要用户熟悉 Pandas。

数字列也缺少值,但返回了结果。 默认情况下,pandas 通过跳过数值列来处理缺失值。 通过将skipna参数设置为False可以更改此行为。 如果存在至少一个缺失值,这将导致所有这些聚合方法的 Pandas 返回NaN

describe方法可一次显示所有主要摘要,并且可以通过将 0 到 1 之间的数字列表传递给percentiles参数来扩展其摘要以包含更多分位数。 默认情况下,仅在数字列上显示信息。 有关describe方法的更多信息,请参见“开发数据分析例程”秘籍。

更多

要查看skipna参数如何影响结果,我们可以将其值设置为False,然后从前面的秘籍重新运行步骤 3。 只有没有缺失值的数字列将计算结果:

>>> movie.min(skipna=False)
num_critic_for_reviews     NaN
duration                   NaN
director_facebook_likes    NaN
actor_3_facebook_likes     NaN
                          ... 
actor_2_facebook_likes     NaN
imdb_score                 1.6
aspect_ratio               NaN
movie_facebook_likes       0.0
Length: 16, dtype: float64

将数据帧方法链接在一起

无论您相信方法链接是否是一种好的做法,在使用 Pandas 进行数据分析时都会遇到它是很普遍的。 第 1 章,“Pandas 基础”中的“将序列方法链接在一起”秘籍展示了链接序列方法一起的几个示例。 本章中的所有方法链都将从数据帧开始。 方法链接的关键之一是知道在链接的每个步骤中返回的确切对象。 在 Pandas 中,这几乎总是一个数据帧,序列或标量值。

准备

在此秘籍中,我们计算移动数据集每一列中的所有缺失值。

操作步骤

  1. 要获得缺失值的计数,必须首先调用isnull方法以将每个数据帧值更改为布尔值。 让我们在电影数据集上调用此方法:
>>> movie = pd.read_csv('data/movie.csv')
>>> movie.isnull().head()

  1. 我们将链接将True/False布尔值解释为 1/0 的sum方法。 请注意,返回了一个序列:
>>> movie.isnull().sum().head()
color                       19
director_name              102
num_critic_for_reviews      49
duration                    15
director_facebook_likes    102
dtype: int64
  1. 我们可以再走一步,取该序列的总和,然后将整个数据帧中缺失值总数的计数作为标量值返回:
>>> movie.isnull().sum().sum()
2654
  1. 略有偏差是为了确定数据帧中是否缺少任何值。 我们在此连续两次使用any方法来执行此操作:
>>> movie.isnull().any().any()
True

工作原理

isnull方法返回一个与调用数据帧相同大小的数据帧,但所有值都转换为布尔值。 请参阅以下数据类型的计数以验证这一点:

>>> movie.isnull().get_dtype_counts()
bool    28
dtype: int64

由于布尔值的数值求值为 0/1,因此可以按列对它们进行求和,如步骤 2 所示。所得的序列本身也具有sum方法,该方法可以使我们在数据帧中获得总计的缺失值。

在步骤 4 中,数据帧的any方法返回布尔值序列,指示每个列是否存在至少一个Trueany方法再次链接到该布尔结果序列上,以确定是否有任何列缺少值。 如果步骤 4 求值为True,则整个数据帧中至少存在一个缺失值。

更多

电影数据集中具有对象数据类型的大多数列都包含缺少的值。 默认情况下,聚合方法minmaxsum不返回任何内容,如以下代码片段所示,该代码片段选择三个对象列并尝试查找每个对象的最大值:

>>> movie[['color', 'movie_title', 'color']].max()
Series([], dtype: float64)

为了迫使 Pandas 为每一列返回值,我们必须填写缺失值。 在这里,我们选择一个空字符串:

>>> movie.select_dtypes(['object']).fillna('').min()
color                                                          Color
director_name                                          Etienne Faure
actor_2_name                                           Zubaida Sahar
genres                                                       Western
actor_1_name                                           Oscar Jaenada
movie_title                                                 Æon Flux
actor_3_name                                           Oscar Jaenada
plot_keywords                                    zombie|zombie spoof
movie_imdb_link    http://www.imdb.com/title/tt5574490/?ref_=fn_t...
language                                                        Zulu
country                                                 West Germany
content_rating                                                     X
dtype: object

出于可读性考虑,方法链通常被编写为每行一个方法调用,并在末尾使用反斜杠字符以避开新行。 这样可以更轻松地阅读和插入有关链的每个步骤返回的内容的注释:

>>> # rewrite the above chain on multiple lines
>>> movie.select_dtypes(['object']) \
         .fillna('') \
         .min()

由于未统一定义最小值和最大值,因此汇总所有字符串的列是无类型。 尝试调用明显没有字符串解释的方法,例如查找均值或方差,将无法正常工作。

另见

  • 参考第 1 章,“Pandas 基础”中的“将序列方法链接到一起”秘籍

将运算符与数据帧一起使用

它与第 1 章,“Pandas 基础”的秘籍有关,其中提供了关于运算符的入门知识。 这里。 Python 算术和比较运算符直接在数据帧上工作,就像在序列上一样。

准备

当数据帧直接使用算术运算符或比较运算符之一进行运算时,每列的每个值都会对其应用运算。 通常,当运算符与数据帧一起使用时,列要么全为数字,要么为所有对象(通常是字符串)。 如果数据帧不包含同类数据,则该操作很可能会失败。 让我们来看一个关于大学数据集失败的示例,该数据集同时包含数字和对象数据类型。 尝试将5添加到数据帧的每个值都会引发TypeError,因为不能将整数添加到字符串中:

>>> college = pd.read_csv('data/college.csv')
>>> college + 5
TypeError: Could not operate 5 with block values must be str, not int

若要成功将运算符与数据帧配合使用,请首先选择同类数据。 对于此秘籍,我们将选择以UGDS_开头的所有列。 这些栏代表按种族划分的大学生比例。 首先,我们导入数据并使用机构名称作为索引的标签,然后使用filter方法选择所需的列:

>>> college = pd.read_csv('data/college.csv', index_col='INSTNM')
>>> college_ugds_ = college.filter(like='UGDS_')
>>> college_ugds_.head()

此秘籍使用多个运算符和一个数据帧将本科生的列四舍五入到最接近的百分之一。 然后,我们将看到此结果如何等效于round方法。

操作步骤

  1. 为了与运算符开始四舍五入的冒险,我们首先将.00501添加到college_ugds_的每个值:
>>> college_ugds_ + .00501

  1. 使用楼层除法运算符//舍入到最接近的整数百分比:
>>> (college_ugds_ + .00501) // .01

  1. 要完成舍入练习,请除以 100:
>>> college_ugds_op_round = (college_ugds_ + .00501) // .01 / 100
>>> college_ugds_op_round.head()

  1. 现在,使用数据帧的round方法为我们自动进行舍入。 NumPy 四舍五入正好在两边到偶数边中间的数字。 因此,我们在舍入前添加一小部分:
>>> college_ugds_round = (college_ugds_ + .00001).round(2)
  1. 使用数据帧的equals方法测试两个数据帧的相等性:
>>> college_ugds_op_round.equals(college_ugds_round)
True

工作原理

步骤 1 使用加法运算符,该运算符尝试将标量值添加到数据帧的每一列的每个值。 由于列都是数字,因此此操作按预期进行。 每列中都有一些缺失值,但在操作后它们仍然缺失。

从数学上讲,添加.005应该足够,以便下一步的底数分割正确舍入到最接近的整数百分比。 由于浮点数的不精确性而出现问题:

>>> .045 + .005
0.049999999999999996

每个数字都有一个额外的.00001,以确保浮点表示的前四位数字与实际值相同。 之所以可行,是因为数据集中所有点的最大精度是四个小数位。

步骤 2 将楼层除法运算符//应用于数据帧中的所有值。 实际上,当我们除以小数时,它是将每个值乘以100并截断任何小数。 在表达式的第一部分周围需要括号,因为底数划分的优先级高于加法。 步骤 3 使用除法运算符将小数返回正确的位置。

在步骤 4 中,我们使用round方法重现了先前的步骤。 在执行此操作之前,由于与步骤 1 有所不同的原因,我们必须再次向每个数据帧值添加一个额外的.00001。NumPy 和 Python 3 的舍入数字恰好位于两边到偶数之间。 这种与偶数技术的联系通常不是学校正式教的。 它不会始终将数字偏向更高端

这里有必要四舍五入,以使两个数据帧值相等。equals方法确定两个数据帧之间的所有元素和索引是否完全相同,并返回一个布尔值。

更多

与序列一样,数据帧具有与运算符等效的方法。 您可以将运算符替换为其等效的方法:

>>> college_ugds_op_round_methods = college_ugds_.add(.00501) \
                                                 .floordiv(.01) \
                                                 .div(100)
>>> college_ugds_op_round_methods.equals(college_ugds_op_round)
True

另见

比较缺失值

Pandas 使用 NumPy NaN(np.nan)对象表示缺失值。 这是不寻常的对象,因为它不等于其自身。 与自身相比,甚至 Python 的None对象也将其求值为True

>>> np.nan == np.nan
False
>>> None == None
True

np.nan的所有其他比较也返回False,除了不等于:

>>> np.nan > 5
False
>>> 5 > np.nan
False
>>> np.nan != 5
True

准备

序列和数据帧使用等号运算符==进行逐元素比较,以返回相同大小的对象。 此秘籍向您展示如何使用相等运算符,该运算符与equals方法非常不同。

与前面的秘籍一样,将使用代表大学数据集中各种族学生的分数的列:

>>> college = pd.read_csv('data/college.csv', index_col='INSTNM')
>>> college_ugds_ = college.filter(like='UGDS_')

操作步骤

  1. 为了了解相等运算符的工作原理,我们将每个元素与一个标量值进行比较:
>>> college_ugds_ == .0019

  1. 这可以按预期工作,但是每当您尝试比较缺少值的数据帧时,就会出现问题。 该相同的等于运算符可用于在逐个元素的基础上将两个数据帧相互比较。 例如,将college_ugds_与自身进行比较,如下所示:
>>> college_self_compare = college_ugds_ == college_ugds_
>>> college_self_compare.head()

  1. 乍一看,所有值似乎都相等,就像您期望的那样。 但是,使用all方法确定每列是否仅包含True值会产生意外结果:
>>> college_self_compare.all()
UGDS_WHITE    False
UGDS_BLACK    False
UGDS_HISP     False
UGDS_ASIAN    False
UGDS_AIAN     False
UGDS_NHPI     False
UGDS_2MOR     False
UGDS_NRA      False
UGDS_UNKN     False
dtype: bool
  1. 发生这种情况是因为缺失值彼此之间没有相等的比较。 如果您尝试使用相等运算符对缺失值进行计数并对布尔列求和,则每个数字将得到零:
>>> (college_ugds_ == np.nan).sum()
UGDS_WHITE    0
UGDS_BLACK    0
UGDS_HISP     0
UGDS_ASIAN    0
UGDS_AIAN     0
UGDS_NHPI     0
UGDS_2MOR     0
UGDS_NRA      0
UGDS_UNKN     0
dtype: int64
  1. 计算缺失值的主要方法是使用isnull方法:
>>> college_ugds_.isnull().sum()
UGDS_WHITE    661
UGDS_BLACK    661
UGDS_HISP     661
UGDS_ASIAN    661
UGDS_AIAN     661
UGDS_NHPI     661
UGDS_2MOR     661
UGDS_NRA      661
UGDS_UNKN     661
dtype: int64
  1. 比较两个整个数据帧的正确方法不是使用相等运算符,而是使用equals方法:
>>> college_ugds_.equals(college_ugds_)
True

工作原理

步骤 1 将一个数据帧与一个标量值进行比较,而步骤 2 将一个数据帧与另一个数据帧进行比较。 乍看之下,这两种操作都非常简单直观。 第二个操作实际上是检查数据帧是否具有相同标签的索引,以及是否具有相同数量的元素。 如果不是这种情况,操作将失败。 有关更多信息,请参见第 6 章,“索引对齐”中的“生成笛卡尔积”秘籍。

步骤 3 验证数据帧中的列均不相等。 步骤 4 进一步显示了np.nan与它本身的不等价性。 步骤 5 验证数据帧中确实存在缺失值。 最后,第 6 步显示了将数据帧与equals方法进行比较的正确方法,该方法始终返回布尔型标量值。

更多

所有比较运算符都有对应的方法,可以使用更多功能。 有点令人困惑的是,数据帧的eq方法像相等运算符一样进行逐元素比较。eq方法与equals方法完全不同。 它仅执行与相等运算符相似的任务。 以下代码重复了步骤 1:

>>> college_ugds_.eq(.0019)    # same as college_ugds_ == .0019 

pandas.testing子包中,存在开发人员在创建单元测试时必须使用的函数。 如果两个数据帧不相等,则assert_frame_equal函数将引发AssertionError。 如果传递的两个帧相等,则返回None

>>> from pandas.testing import assert_frame_equal
>>> assert_frame_equal(college_ugds_, college_ugds_) 

单元测试是软件开发中非常重要的部分,并确保代码正确运行。 Pandas 包含成千上万的单元测试,可帮助确保其正常运行。 要了解有关 Pandas 如何运行其单元测试的更多信息,请参阅文档中的“对 Pandas 做贡献”部分。

转换数据帧操作的方向

许多数据帧方法都有一个axis参数。 这个重要的参数控制操作的方向。 轴参数只能是两个值之一(0 或 1),并且分别作为字符串indexcolumn的别名。

准备

几乎所有的数据帧方法都将axis参数默认为0/index。 此秘籍向您展示了如何调用相同的方法,但其操作方向已被调换。 为了简化练习,将仅使用引用大学数据集中每个学校的百分比种族的列。

操作步骤

  1. 读取大学数据集; 以UGDS_开头的列代表特定种族的本科生所占的百分比。 使用filter方法选择以下列:
>>> college = pd.read_csv('data/college.csv', index_col='INSTNM')
>>> college_ugds_ = college.filter(like='UGDS_')
>>> college_ugds_.head()

  1. 现在,数据帧包含均匀的列数据,可以在垂直和水平方向上合理地进行操作。count方法返回非缺失值的数量。 默认情况下,其axis参数设置为 0:
>>> college_ugds_.count()
UGDS_WHITE    6874
UGDS_BLACK    6874
UGDS_HISP     6874
UGDS_ASIAN    6874
UGDS_AIAN     6874
UGDS_NHPI     6874
UGDS_2MOR     6874
UGDS_NRA      6874
UGDS_UNKN     6874

由于axis参数几乎总是设置为 0,因此无需执行以下操作,但出于理解的目的,第 2 步等效于college_ugds_.count(axis=0)college_ugds_.count(axis='index')

  1. axis参数更改为 1 /列,将对操作进行转置,以使每行数据都有其非缺失值的计数:
>>> college_ugds_.count(axis='columns').head()
INSTNM
Alabama A & M University               9
University of Alabama at Birmingham    9
Amridge University                     9
University of Alabama in Huntsville    9
Alabama State University               9
  1. 代替计算非缺失值,我们可以对每一行中的所有值求和。 每行百分比应总计为 1。sum方法可用于验证这一点:
>>> college_ugds_.sum(axis='columns').head()
INSTNM
Alabama A & M University               1.0000
University of Alabama at Birmingham    0.9999
Amridge University                     1.0000
University of Alabama in Huntsville    1.0000
Alabama State University               1.0000
  1. 为了了解每列的分布,可以使用median方法:
>>> college_ugds_.median(axis='index')
UGDS_WHITE    0.55570
UGDS_BLACK    0.10005
UGDS_HISP     0.07140
UGDS_ASIAN    0.01290
UGDS_AIAN     0.00260
UGDS_NHPI     0.00000
UGDS_2MOR     0.01750
UGDS_NRA      0.00000
UGDS_UNKN     0.01430

工作原理

操作的方向是 Pandas 中比较混乱的方面之一,互联网上到处都有讨论它的解释的线程。 许多新手 Pandas 用户很难记住axis参数的含义。 幸运的是,在 Pandas 中,一项操作可以完成两个潜在的方向。 一种可能的方法是尝试双向尝试直到获得所需结果的简单蛮力解决方案。 我记得axis参数的含义,认为 1 看起来像一列,对axis=1的任何操作都会返回一个新的数据列(与该列具有相同数量的项)。

这在第 3 步中得到确认,在第 3 步中,结果(没有head方法)将返回新的数据列,并且可以根据需要轻松地将其作为列附加到数据帧中。axis等于1/index的其他步骤将返回新的数据行。

更多

使用axis=1cumsum方法累积了每一行的种族百分比。 它给出的数据视图略有不同。 例如,很容易看到每所学校的白人,黑人和西班牙裔美国人的确切百分比:

>> college_ugds_cumsum = college_ugds_.cumsum(axis=1)
>>> college_ugds_cumsum.head()

另见

确定大学校园的多样性

每年都会写很多文章,讨论多样性对大学校园的不同方面和影响。 各种组织已经开发出度量标准,以尝试测量多样性。 《美国新闻》是为许多不同类别的大学提供排名的领导者,其中之一就是多样性。 他们的多样性指数排名前十的学院如下:

>> pd.read_csv('data/college_diversity.csv', index_col='School')

准备

我们的大学数据集将种族分为九个不同类别。 当尝试量化没有明显定义的事物时,例如多样性,它有助于从非常简单的事物开始。 在此秘籍中,我们的多样性指标将等于学生人数超过 15% 的种族数。

操作步骤

  1. 读入大学数据集,并仅针对大学生种族栏进行过滤:
>>> college = pd.read_csv('data/college.csv', index_col='INSTNM')
>>> college_ugds_ = college.filter(like='UGDS_')
  1. 这些大学中许多都缺少其种族列的值。 我们可以计算每一行的所有缺失值,并对所得的序列从最高到最低进行排序。 这将向大学揭示缺少值:
>>> college_ugds_.isnull()\
                 .sum(axis=1)\
                 .sort_values(ascending=False)\
                 .head()
INSTNM
Excel Learning Center-San Antonio South         9
Philadelphia College of Osteopathic Medicine    9
Assemblies of God Theological Seminary          9
Episcopal Divinity School                       9
Phillips Graduate Institute                     9
dtype: int64
  1. 既然我们已经看到了缺少所有种族列的大学,我们可以使用dropna方法删除所有缺少 9 个种族百分比的所有行。 然后,我们可以计算剩余的缺失值:
>>> college_ugds_ = college_ugds_.dropna(how='all')
>>> college_ugds_.isnull().sum()
UGDS_WHITE    0
UGDS_BLACK    0
UGDS_HISP     0
UGDS_ASIAN    0
UGDS_AIAN     0
UGDS_NHPI     0
UGDS_2MOR     0
UGDS_NRA      0
UGDS_UNKN     0
dtype: int64
  1. 数据集中没有遗漏任何值。 现在,我们可以计算多样性指标。 首先,我们将使用大于或等于数据帧的方法ge将每个值转换为布尔值:
>>> college_ugds_.ge(.15)

  1. 从这里开始,我们可以使用sum方法对每个学院的True值进行计数。 请注意,返回了一个序列:
>>> diversity_metric = college_ugds_.ge(.15).sum(axis='columns')
>>> diversity_metric.head()
INSTNM
Alabama A & M University               1
University of Alabama at Birmingham    2
Amridge University                     3
University of Alabama in Huntsville    1
Alabama State University               1
dtype: int64
  1. 为了了解分布情况,让我们在本序列中使用value_counts方法:
>>> diversity_metric.value_counts()
1    3042
2    2884
3     876
4      63
0       7
5       2
dtype: int64
  1. 令人惊讶的是,两所学校在五个不同种族类别中的比例超过 15% 。 让我们对diversity_metric序列进行排序,以找出它们是哪些:
>>> diversity_metric.sort_values(ascending=False).head()
INSTNM
Regency Beauty Institute-Austin          5
Central Texas Beauty College-Temple      5
Sullivan and Cogliano Training Center    4
Ambria College of Nursing                4
Berkeley College-New York                4
dtype: int64
  1. 学校可以这么多样化似乎有点可疑。 让我们看一下这两家顶级学校的原始百分比。.loc索引器用于根据索引标签专门选择:
>>> college_ugds_.loc[['Regency Beauty Institute-Austin', 
                       'Central Texas Beauty College-Temple']]

  1. 似乎有几个类别被汇总到“未知”和“两个或多个种族”列中。 无论如何,它们似乎都非常不同。 我们可以看到美国新闻学院排名前 10 的学校在这一基本多样性指标方面的表现如何:
>>> us_news_top = ['Rutgers University-Newark',
                   'Andrews University', 
                   'Stanford University', 
                   'University of Houston',
                   'University of Nevada-Las Vegas']

>>> diversity_metric.loc[us_news_top]
INSTNM
Rutgers University-Newark         4
Andrews University                3
Stanford University               3
University of Houston             3
University of Nevada-Las Vegas    3
dtype: int64

工作原理

第 2 步进行计数,然后显示缺失值最多的学校。 由于数据帧中有九列,因此每所学校的缺失值最大数目为九。 许多学校缺少每一列的值。 步骤 3 删除所有值均缺失的行。 步骤 3 中的dropna方法具有how参数,该参数默认为字符串any,但也可以更改为all。 设置为any时,它将删除包含一个或多个缺失值的行。 设置为all时,它仅删除缺少所有值的行。

在这种情况下,我们保守地删除丢失所有值的行。 这是因为某些缺失值可能仅代表 0% 。 这不是碰巧的情况,因为执行dropna之后没有丢失值。 如果仍然缺少值,我们可以运行fillna(0)方法用 0 填充所有剩余值。

步骤 4 使用大于或等于方法ge开始我们的多样性指标计算。 这将导致所有布尔值的数据帧,通过设置axis='columns'将其水平求和。

在第 5 步中使用value_counts方法来生成我们的多样性指标的分布。 对于学校而言,很少有三个种族的大学生人数占总人数的 15% 或更多。 第 7 步和第 8 步根据我们的指标找到最多样化的两所学校。 尽管它们是多种多样的,但似乎很多种族并没有得到充分考虑,并且被默认为未知类别和两个或多个类别。

步骤 9 从“美国新闻”文章中选择排名前五的学校。 然后,从我们新创建的序列中选择其多样性指标。 事实证明,这些学校在我们的简单排名系统中也得分很高。

更多

另外,我们可以通过按最大种族百分比对它们进行排序来找到差异最小的学校:

>>> college_ugds_.max(axis=1).sort_values(ascending=False).head(10)
INSTNM
Dewey University-Manati                               1.0
Yeshiva and Kollel Harbotzas Torah                    1.0
Mr Leon's School of Hair Design-Lewiston              1.0
Dewey University-Bayamon                              1.0
Shepherds Theological Seminary                        1.0
Yeshiva Gedolah Kesser Torah                          1.0
Monteclaro Escuela de Hoteleria y Artes Culinarias    1.0
Yeshiva Shaar Hatorah                                 1.0
Bais Medrash Elyon                                    1.0
Yeshiva of Nitra Rabbinical College                   1.0
dtype: float64

我们还可以确定是否有任何一所学校的所有 9 个种族类别都超过 1% :

>>> (college_ugds_ > .01).all(axis=1).any()
True

另见

三、开始数据分析

在本章中,我们将介绍以下主题:

  • 制定数据分析计划
  • 通过更改数据类型减少内存
  • 从最大值中选择最小值
  • 通过排序选择每个组中最大的组
  • sort_values替代nlargest
  • 计算追踪止损单价格

介绍

重要的是,要考虑作为分析人员在将数据集作为数据帧导入工作区后首次遇到数据集时应采取的步骤。 您通常会首先执行一组任务来检查数据吗? 您是否了解所有可能的数据类型? 本章首先介绍您第一次遇到新的数据集时可能要执行的任务。 本章通过回答在 Pandas 中不常见的常见问题继续进行。

制定数据分析计划

尽管开始数据分析时没有标准方法,但是通常最好在首次检查数据集时为自己开发一个例程。 类似于我们用于起床,洗澡,上班,吃饭等的常规例程,开始的数据分析例程可帮助人们快速熟悉新的数据集。 该例程可以表现为动态任务清单,随着您对 Pandas 的熟悉和数据分析的扩展而不断发展。

探索性数据分析EDA)是一个术语,用于涵盖数据分析的整个过程,而无需正式使用统计测试程序。 EDA 的许多工作都涉及可视地显示数据之间的不同关系,以检测有趣的模式并提出假设。

准备

本秘籍涵盖了 EDA 的一小部分但又是基础部分:以常规方式和系统方式收集元数据单变量描述性统计信息。 它概述了在首次将任何数据集作为 pandas 数据帧导入时可以执行的一组常见任务。 此秘籍可能有助于形成您在首次检查数据集时可以实现的例程的基础。

元数据描述数据集,或更恰当地描述关于数据的数据。 元数据的示例包括列/行数,列名称,每列的数据类型,数据集的来源,收集日期,不同列的可接受值,等等。 单变量描述性统计信息是有关数据集的各个变量(列)的摘要统计信息,独立于所有其他变量。

操作步骤

首先,将收集college数据集上的一些元数据,然后是每列的基本摘要统计信息:

  1. 读取数据集,并使用head方法查看前五行:
>>> college = pd.read_college('data/college.csv')
>>> college.head()

  1. 使用shape属性获取数据帧的尺寸:
>>> college.shape
>>> (7535, 27)
  1. 使用info方法列出每一列的数据类型,非缺失值的数量以及内存使用情况:
>>> college.info()

  1. 获取数字列的摘要统计信息,并转置数据帧以获得更可读的输出:
>>> college.describe(include=[np.number]).T

  1. 获取对象和分类列的摘要统计信息:
>>> college.describe(include=[np.object, pd.Categorical]).T

工作原理

导入数据集后,常见的任务是打印出数据帧的前几行,以使用head方法进行手动检查。shape属性返回第一条元数据,即包含行数和列数的元组。

一次获取最多元数据的主要方法是info方法。 它提供每个列的名称,非缺失值的数量,每个列的数据类型以及数据帧的近似内存使用情况。 对于所有数据帧,列值始终是一种数据类型。 关系数据库也是如此。 总体而言,数据帧可能由具有不同数据类型的列组成。

在内部,Pandas 将相同数据类型的列一起存储在块中。 要深入了解 Pandas 的内部,请参阅 Jeff Tratner 的幻灯片

步骤 4 和步骤 5 在不同类型的列上生成单变量描述性统计信息。 强大的describe方法根据提供给include参数的数据类型产生不同的输出。 默认情况下,describe输出所有数字(主要是连续)列的摘要,并静默删除任何类别列。 您可以使用np.number或字符串number在摘要中包含整数和浮点数。 从技术上讲,数据类型是层次结构的一部分,其中数字位于整数和浮点上方。 查看下图,以更好地了解 NumPy 数据类型层次结构:

一般来说,我们可以将数据分类为连续数据或分类数据。 连续数据始终是数字,通常可以具有无限多种可能性,例如身高,体重和薪水。 分类数据代表离散值,这些离散值具有有限的可能性,例如种族,就业状况和汽车颜色。 分类数据可以用数字或字符表示。

分类列通常将是np.objectpd.Categorical类型。 步骤 5 确保同时代表这两种类型。 在第 4 步和第 5 步中,输出数据帧均带有T属性。 这简化了具有许多列的数据帧的可读性。

更多

当与数字列一起使用时,可以指定从describe方法返回的确切分位数:

>>> college.describe(include=[np.number], 
                     percentiles=[.01, .05, .10, .25, .5,
                                  .75, .9, .95, .99]).T

数据字典

数据分析的关键部分涉及创建和维护数据字典。 数据字典是元数据表和每列数据上的注释。 数据字典的主要目的之一是解释列名的含义。 高校数据集使用许多缩写,这对于首次检查它的分析师而言可能是陌生的。

以下college_data_dictionary.csv文件中提供了大学数据集的数据字典:

>>> pd.read_csv('data/collge_data_dictionaray.csv')

如您所见,它在解密缩写列名称方面非常有用。 实际上,数据帧不是存储数据字典的最佳位置。 诸如 Excel 或 Google 表格之类的平台具有易于编辑值和附加列的能力,是更好的选择。 至少,应在数据字典中包含一列以跟踪数据注释。 数据字典是您作为协作者的分析师可以共享的第一件事。

通常,您正在使用的数据集源自数据库,您必须联系该数据库的管理员才能获取更多信息。 正式的电子数据库通常具有更正式的数据表示形式,称为模式。 如果可能,请尝试与对设计有专业知识的人员一起调查您的数据集。

另见

通过更改数据类型减少内存

Pandas 并未将数据大致分为连续数据或分类数据,但对许多不同的数据类型都有精确的技术定义。

准备

此秘籍将大学数据集中的对象列之一的数据类型更改为特殊的 Pandas 分类数据类型,以大大减少其内存使用量。

操作步骤

  1. 阅读我们的大学数据集后,我们选择几列不同的数据类型,这些列将清楚地显示可以节省多少内存:
>>> college = pd.read_csv('data/college.csv')
>>> different_cols = ['RELAFFIL', 'SATMTMID', 'CURROPER',
 'INSTNM', 'STABBR']
>>> col2 = college.loc[:, different_cols]
>>> col2.head()

  1. 检查每一列的数据类型:
>>> col2.dtypes
RELAFFIL      int64
SATMTMID    float64
CURROPER      int64
INSTNM       object
STABBR       object
dtype: object
  1. 使用memory_usage方法查找每一列的内存使用情况:
>>> original_mem = col2.memory_usage(deep=True)
>>> original_mem
Index           80
RELAFFIL     60280
SATMTMID     60280
CURROPER     60280
INSTNM      660240
STABBR      444565
dtype: int64
  1. RELAFFIL列不需要使用 64 位,因为它仅包含 0/1 值。 让我们使用astype方法将此列转换为 8 位(1 字节)整数:
>>> col2['RELAFFIL'] = col2['RELAFFIL'].astype(np.int8)
  1. 使用dtypes属性来确认数据类型更改:
>>> col2.dtypes
RELAFFIL       int8
SATMTMID    float64
CURROPER      int64
INSTNM       object
STABBR       object
dtype: object
  1. 再次查找每一列的内存使用情况,并注意减少的地方:
>>> college[different_cols].memory_usage(deep=True)
Index           80 
RELAFFIL      7535 
SATMTMID     60280 
CURROPER     60280 
INSTNM      660240
STABBR      444565
  1. 为了节省更多内存,如果对象数据类型的基数相当低(唯一值数量),则将需要考虑将其更改为分类。 首先让我们检查两个对象列的唯一值数量:
>>> col2.select_dtypes(include=['object']).nunique()
INSTNM    7535
STABBR      59
dtype: int64
  1. STABBR列是转换为分类的很好的候选者,因为其值的唯一值少于百分之一:
>>> col2['STABBR'] = col2['STABBR'].astype('category')
>>> col2.dtypes
RELAFFIL        int8
SATMTMID     float64
CURROPER       int64
INSTNM        object
STABBR      category
dtype: object
  1. 再次计算内存使用情况:
>>> new_mem = col2.memory_usage(deep=True)
>>> new_mem
Index           80
RELAFFIL      7535
SATMTMID     60280
CURROPER     60280
INSTNM      660699
STABBR       13576
dtype: int64
  1. 最后,让我们比较原始内存使用情况和更新后的内存使用情况。 正如预期的那样,RELAFFIL列是其原始大小的八分之一,而STABBR列已缩小到其原始大小的百分之三:
>>> new_mem / original_mem
Index       1.000000
RELAFFIL    0.125000
SATMTMID    1.000000
CURROPER    1.000000
INSTNM      1.000695
STABBR      0.030538
dtype: float64

工作原理

Pandas 将integerfloat数据类型默认为 64 位,而不管特定数据帧的最大必要大小如何。 可以使用astype方法将整数,浮点数甚至是布尔值强制转换为其他数据类型,并将其作为字符串或特定对象的确切类型传递给它,如步骤 4 所示。

RELAFFIL列是转换为较小整数类型的好选择,因为数据字典说明其值必须为 0/1。 现在RELAFFIL的内存是CURROPER的八分之一,仍然是以前的类型。

显示的存储单位是字节而不是位。 1 个字节等于 8 位,因此当将RELAFFIL更改为 8 位整数时,它将使用 1 个 1 字节的内存,并且由于有 7,535 行,因此其内存占用量相当于 7,535 个字节。

对象数据类型的列(例如INSTNM)与其他 pandas 数据类型不同。 对于所有其他 Pandas 数据类型,该列中的每个值都是相同的数据类型。 例如,当列具有int64类型时,每个单独的列值也都是int64。 对于对象数据类型的列,情况并非如此。 每个单独的列值可以是任何类型。 对象数据类型可以混合使用字符串,数字,日期时间,甚至其他 Python 对象(例如列表或元组)。 因此,对于与任何其他数据类型都不匹配的数据列,有时将对象数据类型称为全部捕获。 但是,绝大多数时候,对象数据类型列都是字符串。

关系数据库管理系统(例如微软的 SQL Server 或 PostgreSQL)具有用于字符的特定数据类型,例如varchartextnchar,它们通常也指定最大字符数。 Pandas 对象数据类型是更广泛的数据类型。 对象列中的每个值可以是任何数据类型。

因此,对象数据类型列中每个单独值的存储都不一致。 像其他数据类型一样,每个值都没有预定义的内存量。 为了使 Pandas 提取对象数据类型列的确切内存量,必须在memory_usage方法中将deep参数设置为True

对象列是最大节省内存的目标。 Pandas 还有 NumPy 中不提供的其他分类数据类型。 当转换为category时,Pandas 内部会创建从整数到每个唯一字符串值的映射。 因此,每个字符串仅需要在内存中保留一次。 如您所见,这种简单的数据类型更改将内存使用量减少了 97% 。

您可能还已经注意到,索引使用的内存量极低。 如果在创建数据帧的过程中未指定索引(如本秘籍所述),pandas 会将索引默认为RangeIndexRangeIndex与内置范围函数非常相似。 它按需产生值,并且仅存储创建索引所需的最少信息量。

更多

为了更好地了解对象数据类型的列与整数和浮点数之间的区别,可以修改这些列中每个列的单个值,并显示结果的内存使用情况。CURROPERINSTNM列分别为int64和对象类型,:

>>> college.loc[0, 'CURROPER'] = 10000000
>>> college.loc[0, 'INSTNM'] = college.loc[0, 'INSTNM'] + 'a'
>>> college[['CURROPER', 'INSTNM']].memory_usage(deep=True)
Index           80
CURROPER     60280
INSTNM      660345

CURROPER的内存使用量保持不变,因为 64 位整数足以容纳更大的数字。 另一方面,仅将一个字母添加到一个值中,INSTNM的内存使用量增加了 105 个字节。

Python 3 使用 Unicode,这是一种标准的字符表示形式,旨在对世界上所有的书写系统进行编码。 Unicode 每个字符最多使用 4 个字节。 第一次对字符值进行修改时,Pandas 似乎有一些开销(100 字节)。 之后,每个字符增加 5 个字节。

并非所有列都可以强制转换为所需的类型。 看一下MENONLY列,在数据字典中似乎只包含 0/1 值。 导入时该列的实际数据类型意外地为float64。 这样做的原因是碰巧缺少值,用np.nan表示。 没有整数表示丢失的值。 甚至只有一个缺失值的任何数字列都必须是浮点数。 此外,如果其中一个值丢失,则integer数据类型的任何列将自动强制为浮点型:

>>> college['MENONLY'].dtype
dtype('float64')

>>> college['MENONLY'].astype(np.int8)
ValueError: Cannot convert non-finite values (NA or inf) to integer

此外,在引用数据类型时,可以用字符串名称代替 Python 对象。 例如,当在describe数据帧方法中使用include参数时,可以传递形式对象 NumPy / pandas 对象或其等效字符串表示形式的列表。 这些内容可在第 2 章,“基本数据帧操作”中的“用方法选择列”秘籍的开头的表格中找到。 例如,以下每个产生相同的结果:

>>> college.describe(include=['int64', 'float64']).T
>>> college.describe(include=[np.int64, np.float64]).T
>>> college.describe(include=['int', 'float']).T 
>>> college.describe(include=['number']).T      

在更改类型时,可以类似地使用以下字符串:

>>> college['MENONLY'] = college['MENONLY'].astype('float16')
>>> college['RELAFFIL'] = college['RELAFFIL'].astype('int8')

字符串与 Pandas 或 NumPy 纯对象的等价性出现在 Pandas 库中的其他位置,并且可能导致混乱,因为有访问同一事物的两种不同的方式。

最后,可以看到最小的RangeIndexInt64Index之间存在巨大的存储差异,后者将每个行索引存储在内存中:

>>> college.index = pd.Int64Index(college.index)
>>> college.index.memory_usage() # previously was just 80
60280 

另见

从最大值中选择最小值

此秘籍可用于创建吸引人的新闻头条,例如“在前 100 名最好的大学中,这 5 名学费最低”或“在前 50 个城市中,这 10 个是最便宜的”。 在分析期间,可能首先需要找到一个数据组,该数据组在单个列中包含最高的n值,然后从该子集中找到最低的m基于不同列的值。

准备

在本秘籍中,我们利用便利的方法nlargestnsmallest从前 100 名得分最高的电影中找到了前五部预算最低的电影。

操作步骤

  1. 读取电影数据集,然后选择movie_titleimdb_scorebudget列:
>>> movie = pd.read_csv('data/movie.csv')
>>> movie2 = movie[['movie_title', 'imdb_score', 'budget']]
>>> movie2.head()

  1. 使用nlargest方法通过imdb_score选择前 100 个电影:
>>> movie2.nlargest(100, 'imdb_score').head()

  1. 链接nsmallest方法可返回前 100 名得分最低的五部预算电影:
>>> movie2.nlargest(100, 'imdb_score').nsmallest(5, 'budget')

工作原理

nlargest方法的第一个参数n必须为整数,并选择要返回的行数。 第二个参数columns以列名作为字符串。 步骤 2 返回得分最高的 100 部电影。 我们可以将该中间结果另存为自己的变量,但是,我们在步骤 3 中将nsmallest方法链接到该变量,该方法恰好返回五行,按budget排序。

更多

可以将列名列表传递给nlargest/nsmallest方法的columns参数。 仅当在列表的第一列中存在重复的值共享第 n 个排名位的情况时,这才对打破关系有用。

通过排序选择每个组中的最大值

在数据分析期间执行的最基本,最常见的操作之一是选择包含组中某个列的最大值的行。 例如,这就像在内容分级中查找每年评分最高的电影或票房最高的电影。 要完成此任务,我们需要对组以及用于对组中每个成员进行排名的列进行排序,然后提取每个组的最高成员。

准备

在此秘籍中,我们将找到每年评分最高的电影。

操作步骤

  1. 读入电影数据集并将其缩小为我们关心的三列movie_titletitle_yearimdb_score
>>> movie = pd.read_csv('data/movie.csv')
>>> movie2 = movie[['movie_title', 'title_year', 'imdb_score']]
  1. 使用sort_values方法按title_year对数据帧进行排序。 默认行为从最小到最大。 通过将ascending参数设置为等于True,可以反转此行为:
>>> movie2.sort_values('title_year', ascending=False).head()

  1. 注意只有年份是如何排序的。 要一次对多列进行排序,请使用一个列表。 让我们看看如何对年份和分数进行排序:
>>> movie3 = movie2.sort_values(['title_year','imdb_score'],
                                 ascending=False)
>>> movie3.head()

  1. 现在,我们使用drop_duplicates方法仅保留每年的第一行:
>>> movie_top_year = movie3.drop_duplicates(subset='title_year')
>>> movie_top_year.head()

工作原理

在第 1 步中,我们将数据集精简为仅关注重要的列。 此秘籍将与整个数据帧相同。 第 2 步显示了如何按单个列对数据帧进行排序,这并不是我们想要的。 步骤 3 同时对多个列进行排序。 它首先通过对所有title_year排序,然后在title_year的每个不同值内按imdb_score排序来工作。

drop_duplicates方法的默认行为是保留每个唯一行的第一次出现,因为每一行都是唯一的,所以不会删除任何行。 但是,subset参数将其更改为仅考虑为其提供的列(或列列表)。 在此示例中,每年仅返回一行。 正如我们在最后一步中按年份和得分排序一样,我们获得的年度最高评分电影。

更多

可以按升序对一列进行排序,而同时按降序对另一列进行排序。 为此,请将布尔值列表传递给ascending参数,该参数与您希望对每一列进行排序的方式相对应。 以下title_yearcontent_rating降序排列,budget升序排列。 然后,它查找每年和内容分级组中预算最低的电影:

>>> movie4 = movie[['movie_title', 'title_year',
                    'content_rating', 'budget']]
>>> movie4_sorted = movie4.sort_values(['title_year', 
                                        'content_rating', 'budget'], 
                                        ascending=[False, False, True])
>>> movie4_sorted.drop_duplicates(subset=['title_year', 
                                          'content_rating']).head(10)

默认情况下,drop_duplicates保持最开始的外观,但是可以通过在最后传递keep参数来选择每个组的最后一行,或通过False完全删除所有重复项来修改此行为

sort_values替代nlargest

前两个秘籍的工作原理类似,它们以略有不同的方式对值进行排序。 查找一列数据的顶部n值等同于对整个列进行降序排序并获取第一个n值。 Pandas 有许多可以通过多种方式做到这一点的行动。

准备

在本秘籍中,我们将使用sort_values方法复制“从最大值中选择最小值”秘籍,并探讨两者之间的区别。

操作步骤

  1. 让我们从“从最大值中选择最小值”秘籍的最后步骤中重新创建结果:
>>> movie = pd.read_csv('data/movie.csv')
>>> movie2 = movie[['movie_title', 'imdb_score', 'budget']]
>>> movie_smallest_largest = movie2.nlargest(100, 'imdb_score') \
                                   .nsmallest(5, 'budget')
>>> movie_smallest_largest

  1. 使用sort_values复制表达式的第一部分,并使用head方法获取第一100行:
>>> movie2.sort_values('imdb_score', ascending=False).head(100)
  1. 现在,我们拥有得分最高的 100 部电影,我们可以再次将sort_valueshead结合使用,以budget来获得最低的五部电影:
>>> movie2.sort_values('imdb_score', ascending=False).head(100) \
          .sort_values('budget').head()

工作原理

如步骤 2 所示,通过在操作后链接head方法,sort_values方法几乎可以复制nlargest。步骤 3 通过链接另一个sort_values可以复制nsmallest,并且只需取前五个即可完成查询。head方法显示行。

查看步骤 1 中第一个数据帧的输出,并将其与步骤 3 中的输出进行比较。它们是否相同? 没有! 发生了什么? 要了解为什么两个结果不相等,让我们看一下每个秘籍的中间步骤的尾部:

>>> movie2.nlargest(100, 'imdb_score').tail()

>>> movie2.sort_values('imdb_score', ascending=False) \
          .head(100).tail()

由于存在超过 100 部评分至少为8.4的电影而引起问题。 每种方法nlargestsort_values的联系均不同,导致 100 行数据帧略有不同。

更多

如果查看nlargest文档,则会看到keep参数具有三个可能的值,firstlastFalse。 据我对其他 Pandas 方法的了解,keep=False应该允许所有纽带保留在结果中。 不幸的是,Pandas 在尝试执行此操作时会引发错误。 我在 GitHub 上给 Pandas 开发团队创建了一个问题,以进行此改进。

计算追踪止损单的价格

本质上,有无数种交易股票的策略。 许多投资者采用的一种基本交易类型是止损单。 止损单是投资者下达的买卖股票的命令,每当市场价格达到某个点时,该订单就会执行。 止损单对于防止巨大损失和保护收益都是有用的。

就本秘籍而言,我们将仅检查用于出售当前拥有股票的止损单。 在典型的止损订单中,价格在订单的整个生命周期内都不会改变。 例如,如果您以每股 100 美元的价格购买了股票,则可能希望将停止订单设置为每股 90 美元,以将下行空间限制为 10% 。

一种更高级的策略是,如果价值增加,则不断修改止损单的销售价格以跟踪股票的价值。 这称为追踪止损指令。 具体来说,如果相同的 100 美元股票增加到 120 美元,那么低于当前市场价格 10% 的追踪止损单将使销售价格上涨到 108 美元。

自购买之日起,追踪止损单永远不会向下移动,并始终与最大值挂钩。 如果股票价格从 120 美元跌至 110 美元,止损单仍将保持在 108 美元。 仅当价格升至 120 美元以上时,价格才会增加。

准备

此秘籍需要使用第三方包pandas-datareader来在线获取股市价格。 它没有预装在 Anaconda 发行版中。 要安装此包,只需访问命令行并运行conda install pandas-datareader。 如果没有 Anaconda,可以通过运行pip install pandas-datareader进行安装。 该秘籍确定给定任何股票的初始购买价格的追踪止损单价格。

操作步骤

  1. 首先,我们将与 Tesla Motors(TSLA)股票合作,并假设在 2017 年的第一个交易日进行购买:
>>> import pandas_datareader as pdr
>>> tsla = pdr.DataReader('tsla', data_source='google',
                          start='2017-1-1')
>>> tsla.head(8)

  1. 为简单起见,我们将使用每个交易日的收盘价:
>>> tsla_close = tsla['Close']
  1. 使用cummax方法跟踪直到当前日期的最高收盘价:
>>> tsla_cummax = tsla_close.cummax()
>>> tsla_cummax.head(8)
Date
2017-01-03    216.99
2017-01-04    226.99
2017-01-05    226.99
2017-01-06    229.01
2017-01-09    231.28
2017-01-10    231.28
2017-01-11    231.28
2017-01-12    231.28
Name: Close, dtype: float64
  1. 为了将下行限制为 10% ,我们将tsla_cummax乘以 0.9。 这将创建跟踪止损单:
>>> tsla_trailing_stop = tsla_cummax * .9
>>> tsla_trailing_stop.head(8)
Date
2017-01-03    195.291
2017-01-04    204.291
2017-01-05    204.291
2017-01-06    206.109
2017-01-09    208.152
2017-01-10    208.152
2017-01-11    208.152
2017-01-12    208.152
Name: Close, dtype: float64

工作原理

cummax方法通过保留遇到的最大值直到并包括当前值来工作。 将该序列乘以 0.9 或您要使用的任何缓冲,将创建跟踪止损单。 在此特定示例中,TSLA 的值增加了,因此其尾随止损也增加了。

更多

该秘籍仅介绍了如何使用有用的 Pandas 来交易证券,并且在计算止损单是否触发以及何时触发止损时停止了计算。 可以将此秘籍转换为接受代码,购买日期和止损百分比并返回尾随止损价格的函数:

>>> def set_trailing_loss(symbol, purchase_date, perc):
        close = pdr.DataReader(symbol, 'google',
                               start=purchase_date)['Close']
        return close.cummax() * perc

>>> msft_trailing_stop = set_trailing_loss('msft', '2017-6-1', .85)
>>> msft_trailing_stop.head()
Date
2017-06-01    59.585
2017-06-02    60.996
2017-06-05    61.438
2017-06-06    61.642
2017-06-07    61.642
Name: Close, dtype: float64

在减肥计划中可以使用非常相似的策略。 只要您偏离最小体重太远,都可以设置警告。 Pandas 为您提供了cummin方法来跟踪最小值。 如果您连续跟踪每天的体重,则以下代码可提供比迄今为止最低记录体重高出 5% 的尾随减肥:

>>> weight.cummin() * 1.05

另见

  • Pandas 的另外两种累积方法的官方文档,cumsumcumprod

四、选择数据子集

在本章中,我们将介绍以下主题:

  • 选择序列数据
  • 选择数据帧的行
  • 同时选择数据帧的行和列
  • 同时通过整数和标签和选择数据
  • 加速标量选择
  • 以延迟方式对行切片
  • 按词典顺序切片

介绍

序列或数据帧中数据的每个维度都通过索引对象标记。 正是这个索引将 Pandas 数据结构与 NumPy 的 n 维数组分开。 索引为数据的每一行和每一列提供了有意义的标签,而 Pandas 用户可以通过使用这些标签来选择数据。 此外,pandas 允许其用户通过行和列的整数位置选择数据。 这种双重选择功能(一种使用标签,另一种使用整数位置)使得强大而又令人困惑的语法可以选择数据子集。

通过使用标签或整数位置选择数据并非 Pandas 所独有。 Python 字典和列表是内置的数据结构,它们以下列其中一种方式选择其数据。 字典和列表都具有精确的说明,并且对于传递给索引运算符的内容都具有有限的用例。 字典的键(其标签)必须是不可变的对象,例如字符串,整数或元组。 列表必须使用整数或切片对象进行选择。 通过将键传递给索引运算符,词典一次只能选择一个对象。 从某种意义上说,Pandas 结合了使用整数(如列表)和标签(如字典)选择数据的能力。

选择序列数据

序列和数据帧是复杂的数据容器,具有多个属性,这些属性使用索引运算符以不同方式选择数据。 除了索引运算符本身之外,.iloc.loc属性也可用,并以其自己的独特方式使用索引运算符。 这些属性统称为索引器

索引术语可能会引起混淆。 这里使用术语“索引运算符”将其与其他索引器区分开。 它指代直接在序列或数据帧之后的括号[]。 例如,给定一个s序列,您可以通过以下方式选择数据:s[item]s.loc[item]。 第一个使用索引运算符。 第二个使用.loc索引器。

序列和数据帧索引器允许按整数位置(如 Python 列表)和标签(如 Python 字典)进行选择。.iloc索引器仅按整数位置选择,并且与 Python 列表类似。.loc索引器仅按索引标签进行选择,这与 Python 词典的工作方式类似。

准备

.loc和。iloc与序列和数据帧一起使用。 此秘籍展示了如何通过.iloc通过整数位置以及通过.loc通过标签选择序列数据。 这些索引器不仅获取标量值,还获取列表和切片。

操作步骤

  1. 读取以机构名称作为索引的大学数据集,并使用索引运算符选择一个列作为“序列”:
>>> college = pd.read_csv('data/college.csv', index_col='INSTNM')
>>> city = college['CITY']
>>> city.head()
INSTNM
Alabama A & M University                   Normal
University of Alabama at Birmingham    Birmingham
Amridge University                     Montgomery
University of Alabama in Huntsville    Huntsville
Alabama State University               Montgomery
Name: CITY, dtype: object
  1. .iloc索引器仅按整数位置进行选择。 向其传递整数将返回标量值:
>>> city.iloc[3]
Huntsville
  1. 要选择几个不同的整数位置,请将列表传递给.iloc。 这将返回一个序列:
>>> city.iloc[[10,20,30]]
INSTNM
Birmingham Southern College                            Birmingham
George C Wallace State Community College-Hanceville    Hanceville
Judson College                                             Marion
Name: CITY, dtype: object
  1. 要选择等间距的数据分区,请使用切片符号:
>>> city.iloc[4:50:10]
INSTNM
Alabama State University              Montgomery
Enterprise State Community College    Enterprise
Heritage Christian University           Florence
Marion Military Institute                 Marion
Reid State Technical College           Evergreen
Name: CITY, dtype: object
  1. 现在,我们转到.loc索引器,该索引器仅使用索引标签进行选择。 传递单个字符串将返回标量值:
>>> city.loc['Heritage Christian University']
Florence
  1. 要选择几个不相交的标签,请使用一个列表:
>>> np.random.seed(1)
>>> labels = list(np.random.choice(city.index, 4))
>>> labels
['Northwest HVAC/R Training Center',
 'California State University-Dominguez Hills',
 'Lower Columbia College',
 'Southwest Acupuncture College-Boulder']

>>> city.loc[labels]
INSTNM
Northwest HVAC/R Training Center                Spokane
California State University-Dominguez Hills      Carson
Lower Columbia College                         Longview
Southwest Acupuncture College-Boulder           Boulder
Name: CITY, dtype: object
  1. 要选择等间距的数据分区,请使用切片符号。 确保起始值和终止值是字符串。 您可以使用整数来指定切片的步长:
>>> city.loc['Alabama State University':
             'Reid State Technical College':10]
INSTNM
Alabama State University              Montgomery
Enterprise State Community College    Enterprise
Heritage Christian University           Florence
Marion Military Institute                 Marion
Reid State Technical College           Evergreen
Name: CITY, dtype: object

工作原理

序列中的值由从 0 开始的整数引用。步骤 2 使用.loc索引器选择序列的第四个元素。 步骤 3 将三个项目的整数列表传递给索引运算符,该运算符返回选择了那些整数位置的序列。 此功能是对 Python 列表的增强,它无法以这种方式选择多个不相交的项目。

在步骤 4 中,使用指定了startstopstep值的切片符号来选择序列的整个部分。

步骤 5 至 7 使用基于标签的索引器.loc复制步骤 2 至 4。 标签必须与索引中的值完全匹配。 为了确保标签正确,我们在步骤 6 中从索引中随机选择四个标签,并将它们存储到列表中,然后再将它们的值选择为序列。 使用.loc索引器的选择始终包含最后一个元素,如步骤 7 所示。

更多

与步骤 2 和步骤 5 一样,当将标量值传递给索引运算符时,将返回标量值。 与其他步骤一样,传递列表或切片时,将返回一个序列。 此返回值似乎不一致,但是如果我们将序列视为将标签映射到值的类似于字典的对象,则返回值是有意义的。 要选择单个项目并将其保留在序列中,请以单项列表而不是标量值的形式传递:

>>> city.iloc[[3]]
INSTNM
University of Alabama in Huntsville    Huntsville
Name: CITY, dtype: object

将切片符号与.loc一起使用时需要格外小心。 如果start索引出现在stop索引之后,则返回一个空序列,而不会引发异常:

>>> city.loc['Reid State Technical College':
             'Alabama State University':10]
Series([], Name: CITY, dtype: object)

另见

选择数据帧的行

选择[DataGate]行的最明确,最优选的方法是使用.iloc.loc索引器。 它们能够独立且同时选择行或列。

准备

此秘籍向您展示如何使用.iloc.loc索引器从数据帧中选择行。

操作步骤

  1. 读取大学数据集,并将索引设置为机构名称:
>>> college = pd.read_csv('data/college.csv', index_col='INSTNM')
>>> college.head()

  1. 将整数传递给.iloc索引器,以选择该位置的整个行:
>>> college.iloc[60]
CITY                  Anchorage
STABBR                       AK
HBCU                          0
                        ...    
UG25ABV                  0.4386
MD_EARN_WNE_P10           42500
GRAD_DEBT_MDN_SUPP      19449.5
Name: University of Alaska Anchorage, Length: 26, dtype: object
  1. 要获得与上一步相同的行,请将索引标签传递给.loc索引器:
>>> college.loc['University of Alaska Anchorage']
CITY                  Anchorage
STABBR                       AK
HBCU                          0
                        ...    
UG25ABV                  0.4386
MD_EARN_WNE_P10           42500
GRAD_DEBT_MDN_SUPP      19449.5
Name: University of Alaska Anchorage, Length: 26, dtype: object
  1. 要将不相交的一组行选择为数据帧,请将整数列表传递给.iloc索引器:
>>> college.iloc[[60, 99, 3]]

  1. 可以使用.loc通过将确切的机构名称列表传递给第 4 步中相同的数据帧:
>>> labels = ['University of Alaska Anchorage',
              'International Academy of Hair Design',
              'University of Alabama in Huntsville']
>>> college.loc[labels]
  1. 将切片符号与.iloc一起使用以选择整个数据段:
>>> college.iloc[99:102]

  1. 切片符号也可以与.loc索引器一起使用,并且包含最后一个标签:
>>> start = 'International Academy of Hair Design'
>>> stop = 'Mesa Community College'
>>> college.loc[start:stop]

工作原理

将标量值,标量列表或切片对象传递给.iloc.loc索引器,会使 Pandas 扫描索引标签中的适当行并返回它们。 如果传递单个标量值,则返回一个序列。 如果传递了列表或切片对象,则返回一个数据帧。

更多

在步骤 5 中,可以直接从步骤 4 中返回的数据帧中选择索引标签列表,而无需复制和粘贴:

>>> college.iloc[[60, 99, 3]].index.tolist()
['University of Alaska Anchorage',
 'International Academy of Hair Design',
 'University of Alabama in Huntsville']

另见

  • 请参阅第 6 章,“索引对齐”的“检查索引对象”秘籍。

同时选择数据帧的行和列

直接使用索引运算符是从数据帧中选择一列或多列的正确方法。 但是,它不允许您同时选择行和列。 要同时选择行和列,您将需要将有效的行和列选择都用逗号传递给.iloc.loc索引器。

准备

选择行和列的通用形式将类似于以下代码:

>>> df.iloc[rows, columns]
>>> df.loc[rows, columns]

rowscolumns变量可以是标量值,列表,切片对象或布尔序列。

第 5 章,“布尔索引”中介绍了将布尔序列传递给索引器。

在此秘籍中,每个步骤都显示使用.iloc同时选择行和列,以及使用.loc进行精确复制。

操作步骤

  1. 读入大学数据集,并将索引设置为机构名称。 用切片符号选择前三行和前四列:
>>> college = pd.read_csv('data/college.csv', index_col='INSTNM')
>>> college.iloc[:3, :4]
>>> college.loc[:'Amridge University', :'MENONLY']

  1. 选择两个不同列的所有行:
>>> college.iloc[:, [4,6]].head()
>>> college.loc[:, ['WOMENONLY', 'SATVRMID']].head()

  1. 选择不相交的行和列:
>>> college.iloc[[100, 200], [7, 15]]
>>> rows = ['GateWay Community College',
            'American Baptist Seminary of the West']
>>> columns = ['SATMTMID', 'UGDS_NHPI']
>>> college.loc[rows, columns]

  1. 选择一个标量值:
>>> college.iloc[5, -4]
>>> college.loc['The University of Alabama', 'PCTFLOAN']
-.401
  1. 切片行并选择单个列:
>>> college.iloc[90:80:-2, 5]
>>> start = 'Empire Beauty School-Flagstaff'
>>> stop = 'Arizona State University-Tempe'
>>> college.loc[start:stop:-2, 'RELAFFIL']
INSTNM
Empire Beauty School-Flagstaff     0
Charles of Italy Beauty College    0
Central Arizona College            0
University of Arizona              0
Arizona State University-Tempe     0
Name: RELAFFIL, dtype: int64

工作原理

同时选择行和列的关键之一是了解方括号中逗号的用法。 逗号左侧的选择始终根据行索引选择行。 逗号右边的选择始终根据列索引选择列。

不必同时选择行和列。 步骤 2 显示了如何选择所有行和列的子集。 冒号表示一个切片对象,该对象仅返回该维度的所有值。

更多

选择行的子集以及所有列时,不必在逗号后使用冒号。 如果没有逗号,则默认行为是选择所有列。 先前的秘籍正是以这种方式选择了行。 但是,您可以使用冒号表示所有列的一部分。 以下代码行是等效的:

>>> college.iloc[:10]
>>> college.iloc[:10, :]

同时通过整数和标签和选择数据

.iloc.loc索引器均通过整数或标签位置选择数据,但不能同时处理两种输入类型的组合。 在早期版本的 Pandas 中,可以使用另一个索引器.ix通过整数和标签位置选择数据。 尽管这在某些特定情况下很方便,但是它本质上是模棱两可的,并且使许多 Pandas 使用者感到困惑。.ix索引器随后被弃用,因此应避免使用。

准备

.ix弃用之前,可以使用college.ix[:5, 'UGDS_WHITE':'UGDS_UNKN']UGDS_WHITEUGDS_UNKN选择大学数据集的前五行和列。 现在不可能直接使用.loc.iloc来做到这一点。 以下秘籍显示了如何找到列的整数位置,然后使用.iloc完成选择。

操作步骤

  1. 读入大学数据集,并将机构名称(INSTNM)分配为索引:
>>> college = pd.read_csv('data/college.csv', index_col='INSTNM')
  1. 使用索引方法get_loc查找所需列的整数位置:
>>> col_start = college.columns.get_loc('UGDS_WHITE')
>>> col_end = college.columns.get_loc('UGDS_UNKN') + 1
>>> col_start, col_end
  1. 使用col_startcol_end使用.iloc按整数位置选择列:
>>> college.iloc[:5, col_start:col_end]

工作原理

步骤 2 首先通过columns属性检索列索引。 索引具有get_loc方法,该方法接受索引标签并返回其整数位置。 我们找到要切片的列的开始和结束整数位置。 我们添加一个是因为用.iloc切片不包括最后一项。 步骤 3 将切片符号与行和列一起使用。

更多

我们可以做一个非常相似的操作来使.loc与整数和位置的混合使用。 下面显示了如何选择第 10 至 15 行(包括第 10 行)以及UGDS_WHITEUGDS_UNKN列:

>>> row_start = df_college.index[10]
>>> row_end = df_college.index[15]
>>> college.loc[row_start:row_end, 'UGDS_WHITE':'UGDS_UNKN']

使用.ix进行相同的操作(已弃用,因此请勿执行此操作)如下所示:

>>> college.ix[10:16, 'UGDS_WHITE':'UGDS_UNKN']

通过将.loc.iloc链接在一起可以实现相同的结果,但是链接索引器通常不是一个好主意:

>>> college.iloc[10:16].loc[:, 'UGDS_WHITE':'UGDS_UNKN']

另见

  • 请参考第 2 章,“基本数据帧操作”的“用方法选择列”。

加速标量选择

.iloc.loc索引器都能够从序列或数据帧中选择单个元素(标量值)。 但是,存在分度器.iat.at,它们分别以更快的速度实现相同的功能。 与.iloc相似,.iat索引器使用整数位置进行选择,并且必须传递两个以逗号分隔的整数。 与.loc相似,.at索引使用标签进行选择,并且必须传递一个索引和由逗号分隔的列标签。

准备

如果计算时间至关重要,则此秘籍很有价值。 当使用标量选择时,它显示了.iat.at相对于.iloc.loc的性能提高。

操作步骤

  1. 以机构名称作为索引,读取college记分板数据集。 将大学名称和列名称传递给.loc,以便选择标量值:
>>> college = pd.read_csv('data/college.csv', index_col='INSTNM')
>>> cn = 'Texas A & M University-College Station'
>>> college.loc[cn, 'UGDS_WHITE']
.661
  1. 使用.at获得相同的结果:
>>> college.at[cn, 'UGDS_WHITE']
.661
  1. 使用%timeit魔术命令查找速度差异:
>>> %timeit college.loc[cn, 'UGDS_WHITE']
8.97 µs ± 617 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

>>> %timeit college.at[cn, 'UGDS_WHITE']
6.28 µs ± 214 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
  1. 找到前面选择的整数位置,然后计时.iloc.iat之间的差:
>>> row_num = college.index.get_loc(cn)
>>> col_num = college.columns.get_loc('UGDS_WHITE')
>>> row_num, col_num
(3765, 10)

>>> %timeit college.iloc[row_num, col_num]
9.74 µs ± 153 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

>>> %timeit college.iat[row_num, col_num]
7.29 µs ± 431 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

工作原理

标量索引器.iat.at仅接受标量值。 如果其他任何东西传递给他们,他们就会失败。 在进行标量选择时,它们是.iloc.loc的直接替代品。timeit魔术命令在以两个百分号开头时对整个代码块计时,而在以一个百分号开头时一次。 它表明,通过切换到标量索引器,平均可节省约 2.5 微秒。 这可能并不多,但是如果在程序中重复进行标量选择,则可能会很快加起来。

更多

.iat.at都可以与序列一起使用。 给它们传递一个标量值,它们将返回一个标量:

>>> state = college['STBBR']   # Select a Series
>>> state.iat[1000]
'IL'

>>> state.at['Stanford University']
'CA'

以延迟方式对行切片

本章前面的秘籍展示了如何使用.iloc.loc索引器选择任一维度中的序列和数据帧的子集。 选择行的快捷方式仅包含索引运算符本身。 这只是显示 Pandas 其他功能的捷径,但索引运算符的主要功能实际上是选择数据帧的列。 如果要选择行,则最好使用.iloc.loc,因为它们是明确的。

准备

在此秘籍中,我们将切片对象传递给序列和数据帧索引运算符。

操作步骤

  1. 读取以机构名称作为索引的大学数据集,然后从索引 10 到 20 选择每隔一行:
>>> college = pd.read_csv('data/college.csv', index_col='INSTNM')
>>> college[10:20:2]

  1. 序列也存在相同的切片:
>>> city = college['CITY']
>>> city[10:20:2]
INSTNM
Birmingham Southern College              Birmingham
Concordia College Alabama                     Selma
Enterprise State Community College       Enterprise
Faulkner University                      Montgomery
New Beginning College of Cosmetology    Albertville
Name: CITY, dtype: object
  1. 序列和数据帧都可以仅通过索引运算符按标签进行切片:
>>> start = 'Mesa Community College'
>>> stop = 'Spokane Community College'
>>> college[start:stop:1500]

  1. 这是带有序列的同一标签:
>>> city[start:stop:1500]
INSTNM
Mesa Community College                            Mesa
Hair Academy Inc-New Carrollton         New Carrollton
National College of Natural Medicine          Portland
Name: CITY, dtype: object

工作原理

索引运算符根据传递给它的对象类型来更改行为。 以下伪代码概述了数据帧索引运算符如何处理其传递的对象:

>>> df[item]  # Where `df` is a DataFrame and item is some object

If item is a string then
    Find a column name that matches the item exactly
    Raise KeyError if there is no match
    Return the column as a Series

If item is a list of strings then
    Raise KeyError if one or more strings in item don't match columns
    Return a DataFrame with just the columns in the list

If item is a slice object then
   Works with either integer or string slices
   Raise KeyError if label from label slice is not in index
   Return all ROWS that are selected by the slice

If item is a list, Series or ndarray of booleans then
   Raise ValueError if length of item not equal to length of DataFrame
   Use the booleans to return only the rows with True in same location

前面的逻辑涵盖了所有最常见的情况,但并不详尽。 序列的逻辑与数据帧的逻辑稍有不同,实际上更为复杂。 由于其复杂性,最好避免在序列上仅使用索引运算符本身,而应使用显式的.iloc.loc索引器。

序列的索引运算符的一种可接受的用例是在进行布尔索引时。 有关更多详细信息,请参见第 6 章“索引对齐”。

我在本节中将这种行切片称为惰性,因为它不使用更明确的.iloc.loc。 就个人而言,我总是在对行进行切片时使用这些索引器,因为从来没有确切地知道我在做什么。

更多

重要的是要知道,这种延迟切片不适用于列,仅适用于数据帧的行和序列,也不能同时选择行和列。 以下面的代码为例,该代码尝试选择前十行​​和两列:

>>> college[:10, ['CITY', 'STABBR']]
TypeError: unhashable type: 'slice' 

要以这种方式进行选择,您需要使用.loc.iloc。 这是一种可能的方法,该方法首先选择所有机构标签,然后使用基于标签的索引器.loc

>>> first_ten_instnm = college.index[:10]
>>> college.loc[first_ten_instnm, ['CITY', 'STABBR']]

按词典顺序切片

.loc索引器通常根据索引的确切字符串标签选择数据。 但是,它还允许您根据索引中值的字典顺序选择数据。 具体来说,.loc允许您使用切片符号按词典顺序选择带有索引的所有行。 仅在对索引排序时有效。

准备

在本秘籍中,您将首先对索引进行排序,然后在.loc索引器中使用切片符号选择两个字符串之间的所有行。

操作步骤

  1. 读取大学数据集,并将机构名称设置为索引:
>>> college = pd.read_csv('data/college.csv', index_col='INSTNM')
  1. 尝试选择所有词典名称在'Sp''Su'之间的大学:
>>> college.loc['Sp':'Su']
KeyError: 'Sp'
  1. 由于索引未排序,因此前面的命令失败。 让我们继续对索引进行排序:
>>> college = college.sort_index()

  1. 现在,让我们从步骤 2 重新运行相同的命令:
>>> college.loc['Sp':'Su']

工作原理

.loc的正常行为是根据传递给它的确切标签来选择数据。 在索引中找不到这些标签时,将引发KeyError。 但是,只要按字典顺序对索引进行排序并将切片传递给该索引,就会存在对此行为的一个特殊例外。 现在可以在切片的startstop标签之间进行选择,即使它们不是索引的精确值也是如此。

更多

使用此秘籍,可以轻松地在两个字母之间选择大学。 例如,要选择所有以字母DS开头的大学,则可以使用college.loc['D':'T']。 像这样的切片仍然包含最后一个索引,因此从技术上讲,这将返回一确切名称为T的大学。

当索引按相反方向排序时,这种切片方式也适用。 您可以使用索引属性is_monotonic_increasingis_monotonic_decreasing确定索引的排序方向。 为了使字典式切片能够正常工作,这些参数中的任何一个都必须为True。 例如,以下代码按字典顺序对索引从ZA进行排序:

>>> college = college.sort_index(ascending=False)
>>> college.index.is_monotonic_decreasing
True
>>> college.loc['E':'B']

Python 将所有大写字母排在小写字母之前,并将所有整数排在大写字母之前。

五、布尔索引

在本章中,我们将介绍以下主题:

  • 计算布尔统计量
  • 构造多个布尔条件
  • 使用布尔索引进行过滤
  • 使用索引选择来替代布尔索引
  • 使用唯一索引和排序索引进行选择
  • 了解股票价格
  • 翻译 SQL WHERE子句
  • 确定股票市场收益的正态性
  • 使用query方法提高布尔索引的可读性
  • 使用where方法保留序列
  • 屏蔽数据帧的行
  • 使用布尔值,整数位置和标签进行选择

介绍

从数据集中过滤数据是最常见的基本操作之一。 有许多方法可以使用布尔下标过滤(或子集)Pandas 中的数据。 布尔索引(也称为布尔选择)可能是一个令人困惑的术语,但出于 Pandas 的目的,它是指通过为每行提供布尔值(TrueFalse)来选择行 。 这些布尔值通常存储在序列或 NumPy ndarray中,通常是通过将布尔条件应用于数据帧中的一个或多个列来创建的。 我们首先创建布尔序列并计算它们的统计量,然后继续创建更复杂的条件,然后以多种方式使用布尔索引来过滤数据。

计算布尔统计量

首次引入布尔序列时,计算有关它们的基本摘要统计信息可能会很有帮助。 布尔序列的每个值的取值为 0 或 1,因此所有适用于数值的序列方法也适用于布尔值。

准备

在此秘籍中,我们通过将条件应用于数据列来创建布尔序列,然后从中计算汇总统计信息。

操作步骤

  1. 读取movie数据集,将索引设置为电影标题,然后检查前几行:
>>> movie = pd.read_csv('data/movie.csv', index_col='movie_title')
>>> movie.head()

  1. 通过对duration序列使用大于比较运算符,确定每部电影的时长是否大于两个小时:
>>> movie_2_hours = movie['duration'] > 120
>>> movie_2_hours.head(10)
movie_title
Avatar                                         True
Pirates of the Caribbean: At World's End       True
Spectre                                        True
The Dark Knight Rises                          True
Star Wars: Episode VII - The Force Awakens    False
John Carter                                    True
Spider-Man 3                                   True
Tangled                                       False
Avengers: Age of Ultron                        True
Harry Potter and the Half-Blood Prince         True
Name: duration, dtype: bool
  1. 现在,我们可以使用该序列来确定超过两个小时的电影数量:
>>> movie_2_hours.sum()
1039
  1. 要查找超过两个小时的数据集中的电影百分比,请使用mean方法:
>>> movie_2_hours.mean()
0.2114
  1. 不幸的是,步骤 4 的输出具有误导性。duration列缺少一些值。 如果回头看步骤 1 的数据帧输出,您将看到最后一行缺少duration的值。 为此,步骤 2 中的布尔条件返回False。 我们需要先删除丢失的值,然后求值条件并取均值:
>>> movie['duration'].dropna().gt(120).mean()
.2112
  1. 使用describe方法输出有关布尔序列的一些摘要统计信息:
>>> movie_2_hours.describe()
count      4916
unique        2
top       False
freq       3877
Name: duration, dtype: object

工作原理

大多数数据帧不会像我们的电影数据集那样具有布尔值列。 产生布尔序列的最直接方法是使用比较运算符之一将条件应用于列之一。 在步骤 2 中,我们使用大于号运算符来测试每部电影的时长是否超过两个小时(120 分钟)。 第 3 步和第 4 步从布尔序列计算两个重要量,即和和均值。 这些方法是可行的,因为 Python 将False/True求值为 0/1。

您可以自己证明布尔级数的均值代表True值的百分比。 为此,请使用value_counts方法,将normalize参数设置为True,以获取其分布:

>>> movie_2_hours.value_counts(normalize=True)
False    0.788649
True     0.211351
Name: duration, dtype: float64

步骤 5 提醒我们步骤 4 的错误结果。即使duration列缺少值,布尔条件也将所有这些比较与缺少的值求值为False。 删除这些缺失值使我们能够计算出正确的统计量。 通过方法链接,只需一步即可完成。

步骤 6 显示,Pandas 通过显示频率信息对待布尔列的方式类似于对待对象数据类型的方式。 这是考虑布尔序列的自然方法,而不是像对数字数据那样显示分位数。

更多

可以比较来自同一数据帧的两列以生成布尔序列。 例如,我们可以确定具有演员 1 的 Facebook 点赞数比演员 2 更多的电影的百分比。要做到这一点,我们将选择这两列,然后删除任何其中一部电影缺少值的行。 然后,我们将进行比较并计算均值:

>>> actors = movie[['actor_1_facebook_likes', 
                    'actor_2_facebook_likes']].dropna()
>>> (actors['actor_1_facebook_likes'] > 
     actors['actor_2_facebook_likes']).mean()
.978

另见

  • 参阅第 1 章,“Pandas 基础”的“将序列方法链接到一起”秘籍
  • 参阅第 1 章,“Pandas 基础”中的“使用运算符”秘籍

构造多个布尔条件

在 Python 中,布尔表达式使用内置的逻辑运算符andornot。 这些关键字不适用于 Pandas 中的布尔索引,而是分别用&|~代替。 此外,每个表达式必须用括号括起来,否则会产生错误。

准备

为数据集构造一个精确的过滤器可能会使您将多个布尔表达式组合在一起以提取一个精确的子集。 在此秘籍中,我们将构造多个布尔表达式,然后将它们组合在一起以查找title_year为 2000 之前或 2009 年之后,imdb_score大于 8,并且content_ratingPG-13的所有电影。

操作步骤

  1. 加载电影数据集并将索引设置为标题:
>>> movie = pd.read_csv('data/movie.csv', index_col='movie_title')
  1. 创建一个变量以将每个条件集作为布尔序列独立保存:
>>> criteria1 = movie.imdb_score > 8
>>> criteria2 = movie.content_rating == 'PG-13'
>>> criteria3 = ((movie.title_year < 2000) | 
                 (movie.title_year > 2009))

>>> criteria2.head()     # all criteria Series look similar
movie_title
Avatar                                         True
Pirates of the Caribbean: At World's End       True
Spectre                                        True
The Dark Knight Rises                          True
Star Wars: Episode VII - The Force Awakens    False
Name: content_rating, dtype: bool
  1. 将所有条件组合到一个布尔序列中:
>>> criteria_final = criteria1 & criteria2 & criteria3
>>> criteria_final.head()
movie_title
Avatar                                        False
Pirates of the Caribbean: At World's End      False
Spectre                                       False
The Dark Knight Rises                          True
Star Wars: Episode VII - The Force Awakens    False
dtype: bool

工作原理

可以使用标准比较运算符(<>==!=<=>=)将序列中的所有值与标量值进行比较。 表达式movie.imdb_score > 8产生布尔序列,其中所有超过 8 的imdb_score值价格均为True,而小于或等于 8 的价格为False。 此布尔序列的索引保留与原始索引相同的索引,在这种情况下,为电影的标题。

criteria3变量由两个独立的布尔表达式创建。 每个表达式必须用括号括起来才能正常运行。 管道字符|用于在两个序列的每个值之间创建逻辑or条件。

所有三个条件都必须为True以匹配秘籍要求。 它们每个都与和号字符&组合在一起,后者在每个序列值之间创建逻辑and条件。

更多

Pandas 对逻辑运算符使用不同语法的结果是运算符优先级不再相同。 比较运算符的优先级高于andornot。 但是,Pandas 的新运算符(按位运算符&|~)比比较运算符具有更高的优先级,因此需要括号。 一个例子可以帮助清除这一点。 采取以下表达式:

>>> 5 < 10 and 3 > 4
False 

在前面的表达式中,首先求值5 < 10,然后求值3 < 4,最后求值and。 Python 通过表达式进行如下操作:

>>> 5 < 10 and 3 > 4
>>> True and 3 > 4
>>> True and False
>>> False

让我们看一下如果criteria3中的表达式编写如下会发生什么:

>>> movie.title_year < 2000 | movie.title_year > 2009
TypeError: cannot compare a dtyped [float64] array with a scalar of type [bool]

由于按位运算符的优先级比比较运算符的优先级高,因此2000 | movie.title_year首先被求值,这是没有意义的,并且会产生错误。 因此,需要括号以正确的顺序求值操作。

为何 Pandas 不能使用andornot? 当求值这些关键字时,Python 尝试查找整个对象的真实性。 因为将整个序列而不是每个元素作为TrueFalse都没有意义,Pandas 都会引发错误。

Python 中的许多对象都具有布尔表示形式。 例如,除 0 以外的所有整数都被视为True。 除空字符串外,所有字符串均为True。 所有非空集,元组,字典和列表都是True。 空的数据帧或序列不会求值为TrueFalse,而是会引发错误。 通常,要检索 Python 对象的真实性,请将其传递给bool函数。

另见

使用布尔索引进行过滤

序列和数据帧对象的布尔选择实际上是相同的。 两者都通过将与要过滤的对象索引相同的布尔序列传递给索引运算符来工作。

准备

此秘籍为不同的电影组构造了两个复杂且独立的布尔标准。 第一组电影是根据之前的秘籍制作的,包括imdb_score大于 8,content_ratingPG-13title_year在 2000 年之前或 2009 年之后的电影。第二组电影包括imdb_score小于 5,content_rating的 R 和title_year在 2000 年至 2010 年之间的数据。

操作步骤

  1. 读取movie数据集,将索引设置为movie_title,并创建第一组条件:
>>> movie = pd.read_csv('data/movie.csv', index_col='movie_title')
>>> crit_a1 = movie.imdb_score > 8
>>> crit_a2 = movie.content_rating == 'PG-13'
>>> crit_a3 = (movie.title_year < 2000) | (movie.title_year > 2009)
>>> final_crit_a = crit_a1 & crit_a2 & crit_a3
  1. 为第二组电影创建条件:
>>> crit_b1 = movie.imdb_score < 5
>>> crit_b2 = movie.content_rating == 'R'
>>> crit_b3 = ((movie.title_year >= 2000) & 
               (movie.title_year <= 2010))
>>> final_crit_b = crit_b1 & crit_b2 & crit_b3
  1. 使用 pandas or运算符组合两组标准。 这将产生一个布尔序列,其中的任何一部电影都是这两组电影的成员:
>>> final_crit_all = final_crit_a | final_crit_b
>>> final_crit_all.head()
movie_title
Avatar                                        False
Pirates of the Caribbean: At World's End      False
Spectre                                       False
The Dark Knight Rises                          True
Star Wars: Episode VII - The Force Awakens    False
dtype: bool
  1. 拥有布尔序列后,只需将其传递给索引运算符即可过滤数据:
>>> movie[final_crit_all].head()

  1. 我们已经成功过滤了数据和数据帧的所有列。 我们无法轻松地执行手动检查来确定过滤器是否正常工作。 让我们使用.loc索引器过滤行和列:
>>> cols = ['imdb_score', 'content_rating', 'title_year']
>>> movie_filtered = movie.loc[final_crit_all, cols]
>>> movie_filtered.head(10)

工作原理

在步骤 1 和步骤 2 中,每组条件都是从更简单的布尔表达式构建的。 不必像此处所做的那样为每个布尔表达式创建一个不同的变量,但是这样做确实使读取和调试任何逻辑错误变得容易得多。 当我们需要两组电影时,步骤 3 使用 Pandas 逻辑or运算符将它们组合在一起。

步骤 4 显示了布尔索引工作原理的确切语法。 您只需将从第 3 步创建的布尔值序列直接传递给索引运算符即可。 仅选择final_crit_all中具有True值的电影。

如步骤 5 所示,布尔索引还可以与.loc索引器配合使用,同时执行布尔索引和单个列选择。 精简的数据帧易于手动检查 逻辑是否正确实现。

布尔索引与.iloc索引运算符不能完全兼容。 如果将布尔序列传递给它,则会引发异常。 但是,如果您传递布尔 N 维数组,则它将与其他索引器在此秘籍中的行为相同。

更多

如前所述,可以使用一个长布尔表达式代替其他几个短布尔表达式。 要使用一长行代码复制第 1 步中的final_crit_a变量,我们可以执行以下操作:

>>> final_crit_a2 = (movie.imdb_score > 8) & \
                    (movie.content_rating == 'PG-13') & \
                    ((movie.title_year < 2000) | 
                     (movie.title_year > 2009))
>>> final_crit_a2.equals(final_crit_a)
True

另见

使用索引选择来替代布尔索引

通过使用索引,可以复制布尔选择的特定情况。 通过索引进行选择更加直观,并提高了可读性。

准备

在此秘籍中,我们使用college数据集通过布尔索引和索引选择从特定状态中选择所有机构,然后将它们各自的性能相互比较。

操作步骤

  1. 读取college数据集,并使用布尔索引从德克萨斯(TX)州选择所有机构:
>>> college = pd.read_csv('data/college.csv')
>>> college[college['STABBR'] == 'TX'].head()

Pandas official documentation on

  1. 要使用索引选择复制此内容,我们需要将STABBR列移入索引。 然后,我们可以在.loc索引器中使用基于标签的选择:
>>> college2 = college.set_index('STABBR')
>>> college2.loc['TX'].head()

  1. 让我们比较两种方法的速度:
>>> %timeit college[college['STABBR'] == 'TX']
1.43 ms ± 53.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

>>> %timeit college2.loc['TX']
526 µs ± 6.67 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
  1. 布尔索引的时间是索引选择时间的三倍。 由于设置索引不是免费的,所以让我们也计时一下该操作:
>>> %timeit college2 = college.set_index('STABBR')
1.04 ms ± 5.37 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

工作原理

步骤 1 通过确定哪些数据行具有STABBR等于TX来创建布尔序列。 该序列传递给索引运算符,该运算符对数据进行子集化。 可以通过将同一列移到索引,并简单地将基本的基于标签的索引选择与.loc一起使用来复制此过程。 通过索引选择比布尔选择快得多。

更多

此秘籍仅选择一个状态。 可以使用布尔选择和索引选择来选择多个状态。 让我们选择德州TX),加利福尼亚CA)和纽约NY)。 使用布尔选择时,可以使用isin方法,但是使用索引时,只需将列表传递给.loc即可:

>>> states = ['TX', 'CA', 'NY']
>>> college[college['STABBR'].isin(states)]
>>> college2.loc[states]

故事的内容比该秘籍的解释要多得多。 Pandas 根据索引是唯一索引还是排序索引来不同地实现索引。 有关更多详细信息,请参见以下秘籍。

使用唯一索引和排序索引进行选择

当索引是唯一的或已排序时,索引选择性能会大大提高。 先前的秘籍使用了包含重复项的未排序索引,因此选择速度相对较慢。

准备

在此秘籍中,我们使用college数据集来形成唯一索引或排序索引,以提高索引选择的性能。 我们还将继续将性能与布尔索引进行比较。

操作步骤

  1. 读取大学数据集,以STABBR作为索引创建一个单独的数据帧,然后检查索引是否已排序:
>>> college = pd.read_csv('data/college.csv')
>>> college2 = college.set_index('STABBR')
>>> college2.index.is_monotonic
False
  1. college2中的索引进行排序,并将其存储为另一个对象:
>>> college3 = college2.sort_index()
>>> college3.index.is_monotonic
True
  1. 从所有三个数据帧中选择德克萨斯州(TX)的时间:
>>> %timeit college[college['STABBR'] == 'TX']
1.43 ms ± 53.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

>>> %timeit college2.loc['TX']
526 µs ± 6.67 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

>>> %timeit college3.loc['TX']
183 µs ± 3.67 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
  1. 排序后的索引执行速度比布尔选择快近一个数量级。 现在让我们转向唯一索引。 为此,我们使用机构名称作为索引:
>>> college_unique = college.set_index('INSTNM')
>>> college_unique.index.is_unique
True
  1. 让我们选择带有布尔索引的斯坦福大学:
>>> college[college['INSTNM'] == 'Stanford University']

  1. 让我们通过索引选择来选择斯坦福大学:
>>> college_unique.loc['Stanford University']
CITY                  Stanford
STABBR                      CA
HBCU                         0
...
UG25ABV                 0.0401
MD_EARN_WNE_P10          86000
GRAD_DEBT_MDN_SUPP       12782
Name: Stanford University, dtype: object
  1. 它们都产生相同的数据,只是对象不同。 让我们为每种方法计时:
>>> %timeit college[college['INSTNM'] == 'Stanford University']
1.3 ms ± 56.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

>>> %timeit college_unique.loc['Stanford University']
157 µs ± 682 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

工作原理

当索引未排序且包含重复项时(如college2一样),Pandas 将需要检查索引中的每个单个值以进行正确选择。 像college3一样对索引进行排序时,pandas 利用称为二分搜索的算法来大大提高性能。

在秘籍的后半部分,我们使用唯一列作为索引。 Pandas 通过哈希表实现唯一索引,从而使选择速度更快。 几乎可以在同一时间查找每个索引位置,而不管其长度如何。

更多

布尔选择比索引选择具有更大的灵活性,因为可以对任意数量的列进行条件调整。 在此秘籍中,我们使用单列作为索引。 可以将多个列连接在一起以形成索引。 例如,在以下代码中,我们将索引设置为等于citystate列的连接:

>>> college.index = college['CITY'] + ', ' + college['STABBR']
>>> college = college.sort_index()
>>> college.head()

从这里,我们可以从特定城市和州的组合中选择所有大学,而无需布尔索引。 让我们从Miami, FL中选择所有大学:

>>> college.loc['Miami, FL'].head()

我们可以将这种复合索引选择与布尔索引的速度进行比较。 有一个数量级以上的差异:

>>> %%timeit 
>>> crit1 = college['CITY'] == 'Miami' 
>>> crit2 = college['STABBR'] == 'FL'
>>> college[crit1 & crit2]
2.43 ms ± 80.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

>>> %timeit college.loc['Miami, FL']
197 µs ± 8.69 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

另见

了解股票价格

购买了多头股票的投资者显然希望以历史最高价或接近历史最高价的价格出售股票。 当然,这在实践中很难做到,尤其是当股价仅将其历史的一小部分花费在一定阈值之上时。 我们可以使用布尔索引来查找股票花费高于或低于某个特定值的所有时间点。 此练习可以帮助我们了解某些股票的交易范围。

准备

在此秘籍中,我们研究了从 2010 年初到 2017 年中期的斯伦贝谢股票。 我们使用布尔索引来提取这段时间内收盘价的最低和最高百分之十的序列。 然后,我们绘制所有点并突出显示上下百分之十的点。

操作步骤

  1. 读入斯伦贝谢股票数据,将Date列放入索引,并将其转换为DatetimeIndex
>>> slb = pd.read_csv('data/slb_stock.csv', index_col='Date', 
                     parse_dates=['Date'])
>>> slb.head()

  1. 选择收盘价作为序列,然后使用describe方法返回摘要统计信息作为序列:
>>> slb_close = slb['Close']
>>> slb_summary = slb_close.describe(percentiles=[.1, .9])
>>> slb_summary
count    1895.000000
mean       79.121905
std        11.767802
min        51.750000
10%        64.892000
50%        78.000000
90%        93.248000
max       117.950000
Name: Close, dtype: float64
  1. 使用布尔选择,选择最高或最低百分之十的所有收盘价:
>>> upper_10 = slb_summary.loc['90%']
>>> lower_10 = slb_summary.loc['10%']
>>> criteria = (slb_close < lower_10) | (slb_close > upper_10)
>>> slb_top_bottom_10 = slb_close[criteria]
  1. 在所有收盘价黑色上方,将生成的过滤后的序列以浅灰色绘制。 使用matplotlib库在第十和第九十个百分位处绘制水平线:
>>> slb_close.plot(color='black', figsize=(12,6))
>>> slb_top_bottom_10.plot(marker='o', style=' ',
                           ms=4, color='lightgray')

>>> xmin = criteria.index[0]
>>> xmax = criteria.index[-1]
>>> plt.hlines(y=[lower_10, upper_10], xmin=xmin,
               xmax=xmax, color='black')

工作原理

步骤 2 中describe方法的结果本身就是一个以识别摘要统计量作为其索引标签的序列。 该摘要序列用于将第十和九十个百分位存储为它们自己的变量。 步骤 3 使用布尔索引来仅选择分布的高和低十分之一的那些值。

序列和数据帧都具有通过plot方法的直接绘图函数。 对plot方法的第一个调用来自slb_close序列,其中包含所有 SLB 收盘价。 这是绘图中的黑线。 来自slb_filtered的点直接在收盘价上方绘制为灰色标记。style参数设置为单个空格,因此不会画线。ms参数设置标记大小。

Matplotlib 带有便利函数hlines,它可以绘制水平线。 它获取y值的列表,并将它们从xmin绘制到xmax

从我们创建的图块的新角度来看,很明显地看到,尽管 SLB 的历史最高价接近每股 120 美元,但在过去七年中只有 10% 的交易日超过了 93 美元。

更多

我们可以使用 matplotlib 的fill_between函数,而不是在收盘价上方绘制红点(黑点)以指示上下十分之一百分位。 此函数填充两行之间的所有区域。 它带有一个可选的where参数,该参数接受一个布尔序列,并警告其确切要填充的位置:

>>> slb_close.plot(color='black', figsize=(12,6))
>>> plt.hlines(y=[lower_10, upper_10], 
               xmin=xmin, xmax=xmax,color='lightgray')
>>> plt.fill_between(x=criteria.index, y1=lower_10,
                     y2=slb_close.values, color='black')
>>> plt.fill_between(x=criteria.index,y1=lower_10,
                     y2=slb_close.values, where=slb_close < lower_10,
                     color='lightgray')
>>> plt.fill_between(x=criteria.index, y1=upper_10, 
                     y2=slb_close.values, where=slb_close > upper_10,
                     color='lightgray')

另见

  • 请参阅第 11 章,“使用 Matplotlib,Pandas 和 Seaborn 进行可视化”

翻译 SQL WHERE 子句

许多 Pandas 用户将使用通用的结构化查询语言SQL)直接从数据库中处理数据。 SQL 是用于定义,操作和控制存储在数据库中的数据的标准化语言。SELECT语句是使用 SQL 选择,过滤,聚合和排序数据的最常用方法。 Pandas 可以连接数据库并向它们发送 SQL 语句。

SQL 是数据科学家要了解的非常重要的语言。 世界上许多数据都存储在数据库中,这需要 SQL 来检索,操作和执行分析。 SQL 语法非常简单易学。 Oracle,Microsoft,IBM 等公司提供了许多不同的 SQL 实现。 尽管语法在不同的实现之间不兼容,但其核心看起来几乎相同。

准备

在 SQL SELECT语句中,WHERE子句非常常见,并过滤数据。 此秘籍将编写与选择雇员数据集的特定子集的 SQL 查询等效的 Pandas 代码。

无需了解任何 SQL 语法即可使用此秘籍。

假设我们有一项任务是找到所有在警察或消防部门工作的女性雇员,其基本工资在 80 到 12 万美元之间。 以下 SQL 语句将为我们回答此查询:

SELECT
    UNIQUE_ID,
    DEPARTMENT,
    GENDER,
    BASE_SALARY
FROM
    EMPLOYEE
WHERE
    DEPARTMENT IN ('Houston Police Department-HPD', 
                   'Houston Fire Department (HFD)') AND
    GENDER = 'Female' AND 
    BASE_SALARY BETWEEN 80000 AND 120000;

操作步骤

  1. 读取employee数据集作为数据帧:
>>> employee = pd.read_csv('data/employee.csv')
  1. 在过滤出数据之前,对每个过滤后的列进行一些手动检查以了解将在过滤器中使用的确切值会有所帮助:
>>> employee.DEPARTMENT.value_counts().head()
Houston Police Department-HPD     638
Houston Fire Department (HFD)     384
Public Works & Engineering-PWE    343
Health & Human Services           110
Houston Airport System (HAS)      106
Name: DEPARTMENT, dtype: int64

>>> employee.GENDER.value_counts()
 Male 1397
 Female 603

>>> employee.BASE_SALARY.describe().astype(int)
count      1886
mean      55767
std       21693
min       24960
25%       40170
50%       54461
75%       66614
max      275000
Name: BASE_SALARY, dtype: int64
  1. 为每个条件写一个声明。 使用isin方法测试是否等于多个值之一:
>>> depts = ['Houston Police Department-HPD', 
             'Houston Fire Department (HFD)']
>>> criteria_dept = employee.DEPARTMENT.isin(depts)
>>> criteria_gender = employee.GENDER == 'Female'
>>> criteria_sal = (employee.BASE_SALARY >= 80000) & \
                   (employee.BASE_SALARY <= 120000)
  1. 将所有布尔序列结合在一起:
>>> criteria_final = (criteria_dept & 
                      criteria_gender & 
                      criteria_sal)
  1. 使用布尔索引来选择仅符合最终条件的行:
>>> select_columns = ['UNIQUE_ID', 'DEPARTMENT',
                     'GENDER', 'BASE_SALARY']
>>> employee.loc[criteria_final, select_columns].head()

工作原理

在实际进行任何过滤之前,您显然需要知道将使用的确切字符串名称。 序列value_counts方法是获取确切的字符串名称和该值的出现次数的极好方法。

isin序列方法等效于 SQL IN运算符,并接受要保留的所有可能值的列表。 可以使用OR条件序列来复制此表达式,但效率不高或惯用。

薪水标准criteria_sal是通过组合两个简单的不等式表达式形成的。 最后,所有条件都与 Pandasand运算符&结合在一起,以产生单个布尔序列作为过滤器。

更多

对于许多操作,Pandas 有多种方法来做同一件事。 在前面的秘籍中,薪水标准使用两个单独的布尔表达式。 与 SQL 相似,序列具有between方法,其工资标准等效编写如下:

>>> criteria_sal = employee.BASE_SALARY.between(80000, 120000)

isin的另一个有用的应用是提供由其他一些 pandas 语句自动生成的值序列。 这样可以避免进行任何手动调查来查找要存储在列表中的确切字符串名称。 相反,让我们尝试从最经常出现的五个部门中排除行:

>>> top_5_depts = employee.DEPARTMENT.value_counts().index[:5]
>>> criteria = ~employee.DEPARTMENT.isin(top_5_depts)
>>> employee[criteria]

SQL 的等效项如下:

SELECT 
    * 
FROM 
    EMPLOYEE 
WHERE 
    DEPARTMENT not in 
    (
      SELECT 
          DEPARTMENT 
     FROM (
           SELECT
               DEPARTMENT,
               COUNT(1) as CT
           FROM
               EMPLOYEE
           GROUP BY
               DEPARTMENT
           ORDER BY
               CT DESC
           LIMIT 5
          )
   );                          

注意,使用了 pandas 否定运算符~,它否定了序列的所有布尔值。

另见

确定股票市场收益的正态性

在基础统计教科书中,正态分布非常依赖于描述许多不同的数据种群。 尽管大多数时间里许多随机过程的确看起来像正态分布,但现实生活中往往更为复杂。 股市回报率是分布的主要示例,该分布看上去看起来很正常,但实际上相差很远。

准备

该秘籍描述了如何查找互联网零售巨头亚马逊的每日股市收益,并非正式地测试它们是否遵循正态分布。

操作步骤

  1. 加载亚马逊库存数据并将日期设置为索引:
>>> amzn = pd.read_csv('data/amzn_stock.csv', index_col='Date',
                      parse_dates=['Date'])
>>> amzn.head()

  1. 通过仅选择收盘价然后使用pct_change方法获得每日收益率来创建序列:
>>> amzn_daily_return = amzn.Close.pct_change()
>>> amzn_daily_return.head()
Date
2010-01-04         NaN
2010-01-05    0.005900
2010-01-06   -0.018116
2010-01-07   -0.017013
2010-01-08    0.027077
Name: Close, dtype: float64
  1. 删除缺失值并绘制收益的直方图,以目视检查分布:
>>> amzn_daily_return = amzn_daily_return.dropna()
>>> amzn_daily_return.hist(bins=20)

  1. 正态分布大致遵循 68-95-99.7 规则-这意味着 68% 的数据介于平均值的 1 个标准差之间,95% 介于 2 个平均值之间和 39.7% 介于 3 个平均值之间。我们现在将计算均值介于 1、2 和 3 个标准差之间的每日收益的百分比。 为此,我们需要均值和标准差:
>>> mean = amzn_daily_return.mean() 
>>> std = amzn_daily_return.std()
  1. 计算每个观察值的z-score的绝对值。z-score是偏离平均值的标准差数:
>>> abs_z_score = amzn_daily_return.sub(mean).abs().div(std)
  1. 查找在 1、2 和 3 个标准差内的收益百分比:
>>> pcts = [abs_z_score.lt(i).mean() for i in range(1,4)]
>>> print('{:.3f} fall within 1 standard deviation. '
          '{:.3f} within 2 and {:.3f} within 3'.format(*pcts))
0.787 fall within 1 standard deviation. 0.957 within 2 and 0.985 within 3

工作原理

默认情况下,pct_change序列方法计算当前元素和上一个元素之间的百分比变化。 这会将原始股票收盘价转换为每日百分比收益。 返回的序列的第一个元素是缺少值,因为没有先前的价格。

直方图是用于汇总和可视化一维数字数据的奇妙图。 从图中可以明显看出,分布是对称的,但仍然很难确定其是否为正态。 有正式的统计程序可以确定分布的正态性,但是我们仅会发现数据与 68-95-99.7 规则的匹配程度。

步骤 5 为每个观测值计算远离平均值的标准差数,称为z-score。 此步骤使用方法而不是符号(-/)进行减法和除法。 小于的方法也用于步骤 6 中的符号。

在步骤 6 中取平均值似乎有些奇怪。abs_z_score.lt(1)表达式的结果是布尔序列。 当布尔值求值为 0 或 1 时,取该序列的平均值将返回True元素的百分比,这就是我们所希望的。

现在,我们可以将结果数(78.7-95.7-98.5)与 68-95-99.7 规则进行比较,从而更轻松地确定收益的正态性。 百分比与 1 和 3 个标准差的规则有很大差异,我们可以得出结论,亚马逊的每日股票收益率不遵循正态分布。

更多

为了使这一过程自动化,我们可以编写一个函数,该函数在中接收股票数据,并输出日收益率的直方图以及与平均值相差 1、2 和 3 个标准差的百分比。 下面的函数执行此操作,并将方法替换为其对应的符号:

>>> def test_return_normality(stock_data):
        close = stock_data['Close']
        daily_return = close.pct_change().dropna()
        daily_return.hist(bins=20)
        mean = daily_return.mean() 
        std = daily_return.std()

        abs_z_score = abs(daily_return - mean) / std 
        pcts = [abs_z_score.lt(i).mean() for i in range(1,4)]

        print('{:.3f} fall within 1 standard deviation. '
              '{:.3f} within 2 and {:.3f} within 3'.format(*pcts))

>>> slb = pd.read_csv('data/slb_stock.csv', index_col='Date',
                      parse_dates=['Date'])
>>> test_return_normality(slb)
0.742 fall within 1 standard deviation. 0.946 within 2 and 0.986 within 3

另见

使用query方法提高布尔索引的可读性

布尔索引不一定是读取或写入的最令人愉快的语法,尤其是在使用单行编写复杂过滤器时。 Pandas 通过数据帧的query方法具有替代的基于字符串的语法,该语法可提供更高的清晰度。

数据帧的query方法是实验性的,不具备布尔索引功能,因此不应用于生产代码。

准备

此秘籍复制了本章中的较早秘籍“转换 SQL 数据帧的WHERE子句”,但是利用了query方法。 此处的目标是为来自警察局或消防局的,薪水在 80 至 12 万美元之间的女性雇员筛选雇员数据。

操作步骤

  1. 读入员工数据,分配所选部门,并将列导入变量:
>>> employee = pd.read_csv('data/employee.csv')
>>> depts = ['Houston Police Department-HPD',
             'Houston Fire Department (HFD)']
>>> select_columns = ['UNIQUE_ID', 'DEPARTMENT',
                      'GENDER', 'BASE_SALARY']
  1. 构建查询字符串并执行方法:
>>> qs = "DEPARTMENT in @depts " \
         "and GENDER == 'Female' " \
         "and 80000 <= BASE_SALARY <= 120000"

>>> emp_filtered = employee.query(qs)
>>> emp_filtered[select_columns].head()

工作原理

传递给query方法的字符串看起来比普通的 Pandas 代码更像普通的英语。 与depts一样,可以使用 at 符号(@)来引用 Python 变量。 通过简单地引用其名称而不用内引号,可在查询名称空间中使用所有数据帧的列名称。 如果需要一个字符串,例如Female,则需要用引号将其引起来。

query语法的另一个不错的功能是能够在单个表达式中编写双重不等式,并且能够理解冗长的逻辑运算符andornot,而不是像布尔值那样的按位等效索引。

更多

不用手动输入部门名称列表,我们可以以编程方式创建它。 例如,如果我们要按频率查找不是前十名部门成员的所有女性雇员,则可以运行以下代码:

>>> top10_depts = employee.DEPARTMENT.value_counts() \
                                     .index[:10].tolist()
>>> qs = "DEPARTMENT not in @top10_depts and GENDER == 'Female'"
>>> employee_filtered2 = employee.query(qs)
>>> employee_filtered2.head()

另见

使用where方法保留序列

布尔索引必须通过删除不符合条件的所有行来过滤数据集。 除了丢弃所有这些值外,还可以使用where方法保留它们。where方法将保留序列或数据帧的大小,并将不符合条件的值设置为缺失或将其替换为其他值。

准备

在此秘籍中,我们通过where方法布尔条件,在movie数据集中,针对演员 1 的 Facebook 点赞的最小和最大数目设置上下限。

操作步骤

  1. 读取movie数据集,将影片标题设置为索引,然后选择actor_1_facebook_likes列中所有不丢失的值:
>>> movie = pd.read_csv('data/movie.csv', index_col='movie_title')
>>> fb_likes = movie['actor_1_facebook_likes'].dropna()
>>> fb_likes.head()
movie_title
Avatar                                         1000.0
Pirates of the Caribbean: At World's End      40000.0
Spectre                                       11000.0
The Dark Knight Rises                         27000.0
Star Wars: Episode VII - The Force Awakens      131.0
Name: actor_1_facebook_likes, dtype: float64
  1. 让我们使用describe方法来了解分布情况:
>>> fb_likes.describe(percentiles=[.1, .25, .5, .75, .9]) \
            .astype(int)
count      4909
mean       6494
std       15106
min           0
10%         240
25%         607
50%         982
75%       11000
90%       18000
max      640000
Name: actor_1_facebook_likes, dtype: int64
  1. 此外,我们可以绘制此序列的直方图以直观地检查分布:
>>> fb_likes.hist()

  1. 这是非常糟糕的可视化,并且很难了解分布情况。 另一方面,第 2 步的汇总统计信息似乎在告诉我们,在很多观察中,该数据高度偏向右侧,比中位数大一个数量级。 让我们创建标准来测试点赞次数是否少于 20,000:
>>> criteria_high = fb_likes < 20000
>>> criteria_high.mean().round(2)
.91
  1. 大约 91% 的电影的演员 1 少于 20,000 个。 现在,我们将使用where方法,该方法接受布尔条件。 默认行为是返回与原始大小相同的序列,但将所有False位置替换为缺少的值:
>>> fb_likes.where(criteria_high).head()
movie_title
Avatar                                         1000.0
Pirates of the Caribbean: At World's End          NaN
Spectre                                       11000.0
The Dark Knight Rises                             NaN
Star Wars: Episode VII - The Force Awakens      131.0
Name: actor_1_facebook_likes, dtype: float64
  1. where方法的第二个参数other允许您控制替换值。 让我们将所有缺失值更改为 20,000:
>>> fb_likes.where(criteria_high, other=20000).head()
movie_title
Avatar                                         1000.0
Pirates of the Caribbean: At World's End      20000.0
Spectre                                       11000.0
The Dark Knight Rises                         20000.0
Star Wars: Episode VII - The Force Awakens      131.0
Name: actor_1_facebook_likes, dtype: float64
  1. 类似地,我们可以创建条件来为最少的点赞次数设置下限。 在这里,我们链接另一个where方法,并将不符合条件的值替换为300
>>> criteria_low = fb_likes > 300
>>> fb_likes_cap = fb_likes.where(criteria_high, other=20000)\
                           .where(criteria_low, 300)
>>> fb_likes_cap.head()
movie_title
Avatar                                         1000.0
Pirates of the Caribbean: At World's End      20000.0
Spectre                                       11000.0
The Dark Knight Rises                         20000.0
Star Wars: Episode VII - The Force Awakens      300.0
Name: actor_1_facebook_likes, dtype: float64
  1. 原始序列和修改后的序列的长度相同:
>>> len(fb_likes), len(fb_likes_cap)
(4909, 4909)
  1. 让我们使用修改后的序列创建直方图。 数据范围更窄时,应该可以绘制出更好的图:
>>> fb_likes_cap.hist()

工作原理

where方法再次保留调用对象的大小和形状,并且不修改传递的布尔值为True的值。 重要的是在步骤 1 中删除丢失的值,因为where方法最终将在以后的步骤中将其替换为有效数字。

第 2 步中的摘要统计信息为我们提供了一些直观的方法来限定数据上限。 另一方面,第 3 步中的直方图似乎会将所有数据聚集到一个桶中。 对于纯直方图,数据有太多离群值,因此无法绘制出正确的图。where方法允许我们在数据上放置一个上限和下限,这将导致带有更多可见条的直方图。

更多

Pandas 实际上具有复制此操作的内置方法clipclip_lowerclip_upperclip方法可以同时设置地板和天花板。 我们还检查此替代方法是否产生完全相同的序列,它会执行以下操作:

>>> fb_likes_cap2 = fb_likes.clip(lower=300, upper=20000)
>>> fb_likes_cap2.equals(fb_likes_cap)
True

另见

屏蔽数据帧的行

mask方法执行与where方法完全相反的操作。 默认情况下,无论布尔条件为True,它都会创建缺失值。 从本质上讲,它实际上是掩盖或掩盖数据集中的值。

准备

在此秘籍中,我们将屏蔽 2010 年之后制作的电影数据集的所有行,然后过滤所有缺少值的行。

操作步骤

  1. 读取movie数据集,将电影标题设置为索引,并创建条件:
>>> movie = pd.read_csv('data/movie.csv', index_col='movie_title')
>>> c1 = movie['title_year'] >= 2010
>>> c2 = movie['title_year'].isnull()
>>> criteria = c1 | c2
  1. 在数据帧上使用mask方法可以使从 2010 年开始制作的带有电影的行中的所有值都丢失。 最初具有title_year缺失值的所有电影也会被屏蔽:
>>> movie.mask(criteria).head()

  1. 请注意,前面的数据帧中的第三,第四和第五行中的所有值是如何丢失的。 链接dropna方法以删除所有值均缺失的行:
>>> movie_mask = movie.mask(criteria).dropna(how='all')
>>> movie_mask.head()

  1. 步骤 3 中的操作只是执行基本布尔索引的一种复杂方法。 我们可以检查两个方法是否产生相同的数据帧:
>>> movie_boolean = movie[movie['title_year'] < 2010]
>>> movie_mask.equals(movie_boolean)
False
  1. equals方法告诉我们它们不相等。 出了点问题。 让我们进行一些完整性检查,看看它们是否具有相同的形状:
>>> movie_mask.shape == movie_boolean.shape
True
  1. 当我们使用前面的mask方法时,它创建了许多缺失值。 缺少值是float数据类型,因此任何以前的整数列现在都是浮点数。 如果列的数据类型不同,即使值相同,equals方法也会返回False。 让我们检查数据类型的相等性,以查看是否发生了这种情况:
>>> movie_mask.dtypes == movie_boolean.dtypes
color                         True
director_name                 True
num_critic_for_reviews        True
duration                      True
director_facebook_likes       True
actor_3_facebook_likes        True
actor_2_name                  True
actor_1_facebook_likes        True
gross                         True
genres                        True
actor_1_name                  True
num_voted_users              False
cast_total_facebook_likes    False
.....
dtype: bool
  1. 事实证明,几列没有相同的数据类型。 Pandas 对于这些情况有另一种选择。 在其开发人员主要使用的测试模块中,有一个函数assert_frame_equal,您可以使用它检查序列和数据帧的相等性,而无需同时检查数据类型的相等性:
from pandas.testing import assert_frame_equal
>>> assert_frame_equal(movie_boolean, movie_mask, check_dtype=False)

工作原理

默认情况下,mask方法覆盖缺少值的数据。mask方法的第一个参数是条件,该条件通常是布尔级数,例如criteria。 因为mask方法是从数据帧调用的,所以条件为False的每一行中的所有值都将变为丢失。 步骤 3 使用此掩码的数据帧删除包含所有缺失值的行。 步骤 4 显示了如何使用布尔索引执行相同的过程。

在数据分析过程中,持续验证结果非常重要。 检查序列和数据帧的相等性是一种非常通用的验证方法。 我们在步骤 4 中的首次尝试产生了意外结果。 在深入研究之前,一些基本的健全性检查(例如确保行和列的数目相同或行和列的名称相同)是很好的检查。

步骤 6 将两个序列的数据类型一起比较。 在这里,我们揭示了数据帧不等效的原因。equals方法检查值和数据类型是否相同。 步骤 7 中的assert_frame_equal函数具有许多可用参数,可以通过各种方式测试相等性。 注意,调用assert_frame_equal后没有输出。 当两个传递的数据帧相等时,此方法返回None;否则,将引发错误。

更多

让我们比较掩盖和删除丢失的行与布尔索引之间的速度差异。 在这种情况下,布尔索引大约快一个数量级:

>>> %timeit movie.mask(criteria).dropna(how='all')
11.2 ms ± 144 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

>>> %timeit movie[movie['title_year'] < 2010]
1.07 ms ± 34.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

另见

使用布尔值,整数位置和标签进行选择

第 4 章,“选择数据子集”涵盖了有关通过.iloc.loc索引器选择不同数据子集的各种方法。 这两个索引器都通过整数位置或标签同时选择行和列。 这两个索引器都可以通过布尔索引进行数据选择,即使布尔不是整数也不是标签。

准备

在本秘籍中,我们将为.iloc.loc索引器使用布尔索引过滤行和列。

操作步骤

  1. 读入电影数据集,将索引设置为标题,然后创建一个布尔序列,匹配内容分级为G和 IMDB 得分小于4的所有电影:
>>> movie = pd.read_csv('data/movie.csv', index_col='movie_title')
>>> c1 = movie['content_rating'] == 'G'
>>> c2 = movie['imdb_score'] < 4
>>> criteria = c1 & c2
  1. 首先,将这些条件传递给.loc索引器以过滤行:
>>> movie_loc = movie.loc[criteria]
>>> movie_loc.head()

  1. 让我们检查一下此数据帧是否完全等于直接由索引运算符生成的数据帧:
>>> movie_loc.equals(movie[criteria])
True
  1. 现在,让我们尝试使用.iloc索引器进行相同的布尔索引:
>>> movie_iloc = movie.iloc[criteria]
ValueError: iLocation based boolean indexing cannot use an indexable as a mask
  1. 事实证明,由于存在索引,我们不能直接使用布尔序列。 但是,我们可以使用布尔值的 ndarray。 要提取数组,请使用values属性:
>>> movie_iloc = movie.iloc[criteria.values]
>>> movie_iloc.equals(movie_loc)
True
  1. 尽管不是很常见,但可以进行布尔索引来选择特定的列。 在这里,我们选择所有数据类型为 64 位整数的列:
>>> criteria_col = movie.dtypes == np.int64
>>> criteria_col.head()
color                      False
director_name              False
num_critic_for_reviews     False
duration                   False
director_facebook_likes    False
dtype: bool

>>> movie.loc[:, criteria_col].head()

  1. 由于criteria_col是一个序列,始终有一个索引,因此必须使用基础 ndarray 使其与.iloc一起使用。 以下产生与步骤 6 相同的结果。
>>> movie.iloc[:, criteria_col.values].head() 
  1. 布尔序列可以用于选择行,然后同时选择具有整数或标签的列。 请记住,您需要在行和列选择之间加上逗号。 让我们保留行标准,然后选择content_ratingimdb_scoretitle_yeargross
>>> cols = ['content_rating', 'imdb_score', 'title_year', 'gross']
>>> movie.loc[criteria, cols].sort_values('imdb_score')

  1. 可以使用.iloc复制相同的操作,但是您需要获取所有列的整数位置:
>>> col_index = [movie.columns.get_loc(col) for col in cols]
>>> col_index
[20, 24, 22, 8]

>>> movie.iloc[criteria.values, col_index] 

工作原理

布尔索引可以用.iloc.loc索引器完成,但要注意的是.iloc不能传递给序列而是基础ndarray。 让我们看一下条件序列的一维ndarray

>>> a = criteria.values
>>> a[:5]
array([False, False, False, False, False], dtype=bool)

>>> len(a), len(criteria)
(4916, 4916)

数组的长度与序列的长度相同,而序列与电影的数据帧长度相同。 布尔数组的整数位置与数据帧的整数位置对齐,并且过滤器按预期进行。 这些数组也可以与.loc运算符一起使用,但是它们对于.iloc是必需的。

步骤 6 和 7 显示了如何按列而不是按行进行过滤。 需要冒号:来指示所有行的选择。 冒号后面的逗号分隔行和列的选择。 实际上,通过select_dtypes方法可以更轻松地选择具有整数数据类型的列。

步骤 8 和 9 显示了一种同时对行和列选择进行布尔索引的非常通用和有用的方法。 您只需在行和列选择之间放置一个逗号。 第 9 步使用列表推导式遍历所有所需的列名,以使用索引方法get_loc查找其整数位置。

更多

实际上,可以将数组和布尔值列表传递给序列对象,这些对象的长度与您要建立索引的数据帧的长度不同。 让我们通过选择第一行和第三行以及第一列和第四列来查看一个示例:

>>> movie.loc[[True, False, True], [True, False, False, True]]

这两个布尔列表的长度与其所索引的轴的长度不同。 列表中未明确指定布尔值的其余行和列将被删除。

另见

  • 请参阅第 4 章,“选择数据子集”中的“同时使用整数和标签选择数据”秘籍。
  • 请参阅第 2 章,“基本数据帧操作”的“用方法选择列”。

六、索引对齐

在本章中,我们将介绍以下主题:

  • 检查索引对象
  • 生成笛卡尔积
  • 索引爆炸
  • 用不相等的索引填充值
  • 追加来自不同数据帧的列
  • 突出显示每一列的最大值
  • 用方法链复制idxmax
  • 寻找最常见的最大值

介绍

当以某种方式组合多个序列或数据帧时,在进行任何计算之前,数据的每个维度会首先自动在每个轴上对齐。 轴的这种无声且自动的对齐会给初学者造成极大的困惑,但它为超级用户提供了极大的灵活性。 本章将深入探讨索引对象,然后展示利用其自动对齐功能的各种秘籍。

检查索引对象

如第 1 章,“Pandas 基础”中所讨论的,序列和数据帧的每个轴都有一个索引对象,用于标记值。 有许多不同类型的索引对象,但是它们都具有相同的共同行为。 除特殊的多重索引之外,所有索引对象都是一维数据结构,结合了 Python 集和 NumPy ndarrays的功能和实现。

准备

在本秘籍中,我们将检查大学数据集的列索引并探索其许多功能。

操作步骤

  1. 读入大学数据集,为列索引分配一个变量,然后输出:
>>> college = pd.read_csv('data/college.csv')
>>> columns = college.columns
>>> columns
Index(['INSTNM', 'CITY', 'STABBR', 'HBCU', ...], dtype='object')
  1. 使用values属性访问基础的 NumPy 数组:
>>> columns.values
array(['INSTNM', 'CITY', 'STABBR', 'HBCU', ...], dtype=object)
  1. 通过带有标量,列表或切片的整数位置从索引中选择项目:
>>> columns[5]
'WOMENONLY'

>>> columns[[1,8,10]]
Index(['CITY', 'SATMTMID', 'UGDS'], dtype='object')

>>> columns[-7:-4]
Index(['PPTUG_EF', 'CURROPER', 'PCTPELL'], dtype='object')
  1. 索引与序列和数据帧共享许多相同的方法:
>>> columns.min(), columns.max(), columns.isnull().sum()
('CITY', 'WOMENONLY', 0)
  1. 直接在Index对象上使用基本算术和比较运算符:
>>> columns + '_A'
Index(['INSTNM_A', 'CITY_A', 'STABBR_A', 'HBCU_A', ...], dtype='object')

>>> columns > 'G'
array([ True, False,  True,  True, ...], dtype=bool)
  1. 创建索引后尝试直接更改索引值失败。 索引是不可变的对象:
>>> columns[1] = 'city'
TypeError: Index does not support mutable operations

工作原理

从许多索引对象操作中可以看到,它与序列和ndarrays似乎有很多共同点。 最大的差异之一来自第 6 步。索引是不可变的,创建后就无法更改它们的值。

更多

索引支持集合运算,并集,交集,差和对称差:

>>> c1 = columns[:4]
>>> c1
Index(['INSTNM', 'CITY', 'STABBR', 'HBCU'], dtype='object')

>>> c2 = columns[2:6]
>>> c2
Index(['STABBR', 'HBCU', 'MENONLY'], dtype='object')

>>> c1.union(c2) # or `c1 | c2`
Index(['CITY', 'HBCU', 'INSTNM', 'MENONLY', 'RELAFFIL', 'STABBR'], dtype='object')

>>> c1.symmetric_difference(c2) # or `c1 ^ c2`
Index(['CITY', 'INSTNM', 'MENONLY'], dtype='object')

索引与 Python 集共享一些相同的操作。 索引在另一重要方面类似于 Python 集。 它们(通常)是使用哈希表实现的,当从数据帧中选择行或列时,哈希表的访问速度非常快。 当使用哈希表实现它们时,索引对象的值必须是不可变的,例如字符串,整数或元组,就像 Python 字典中的键一样。

索引支持重复值,并且如果在任何索引中碰巧有重复项,则哈希表将无法再用于其实现,并且对象访问会变得很慢。

另见

生成笛卡尔积

每当两个序列或数据帧与另一个序列或数据帧一起操作时,每个对象的索引(行索引和列索引)都首先对齐,然后再开始任何操作。 这种索引对齐方式是无声的,对于那些刚接触 Pandas 的人来说可能是非常令人惊讶的。 除非索引相同,否则这种对齐方式总是在索引之间创建笛卡尔积。

笛卡尔积是一个数学术语,通常出现在集合论中。 两个集之间的笛卡尔积是两个集的偶对的所有组合。 例如,标准纸牌中的 52 张纸牌代表 13 个等级(A, 2, 3,..., Q, K)和四个花色之间的笛卡尔积。

准备

生成笛卡尔积并非总是预期的结果,但是了解发生的方式和时间以避免意外后果至关重要。 在此秘籍中,将具有重叠但不相同的索引的两个序列相加在一起,产生了令人惊讶的结果。

操作步骤

请按照以下步骤创建笛卡尔积:

  1. 构造两个具有不同索引但包含一些相同值的序列:
>>> s1 = pd.Series(index=list('aaab'), data=np.arange(4))
>>> s1
a    0
a    1
a    2
b    3
dtype: int64

>>> s2 = pd.Series(index=list('cababb'), data=np.arange(6))
>>> s2
c    0
a    1
b    2
a    3
b    4
b    5
dtype: int64
  1. 将两个序列加在一起以生成笛卡尔积:
>>> s1 + s2
a    1.0
a    3.0
a    2.0
a    4.0
a    3.0
a    5.0
b    5.0
b    7.0
b    8.0
c    NaN
dtype: float64

工作原理

每个序列都是使用类构造器创建的,该类构造器接受各种各样的输入,最简单的是每个参数index和数据的值序列。

笛卡尔数学乘积与对两个 Pandas 对象进行运算的结果略有不同。s1中的每个a标签与s2中的每个a标签配对。 该配对在所得序列中产生六个a标签,三个b标签和一个c标签。 笛卡尔积在所有相同的索引标签之间发生。

由于带有标签c的元素是序列s2所特有的,因此 pandas 默认将其值设置为 missing,因为s1中没有标签可以对齐。 每当索引标签对于一个对象唯一时,Pandas 默认为缺少值。 不幸的结果是,将序列的数据类型更改为float,而每个序列仅具有整数作为值。 发生这种情况是因为 NumPy 缺少值对象。np.nan仅对于浮点数存在,而对于整数不存在。序列和数据帧的列必须具有齐次数值数据类型; 因此,每个值都转换为浮点数。 对于这个小的数据集,这几乎没有什么区别,但是对于较大的数据集,这可能会对内存产生重大影响。

更多

当索引以相同顺序包含相同的完全相同的元素时,将发生上述示例的异常。 发生这种情况时,不会发生笛卡尔积,而是按其位置对齐索引。 请注意,每个元素均按位置精确对齐,并且数据类型仍为整数:

>>> s1 = pd.Series(index=list('aaabb'), data=np.arange(5))
>>> s2 = pd.Series(index=list('aaabb'), data=np.arange(5))
>>> s1 + s2
a    0
a    2
a    4
b    6
b    8
dtype: int64

如果索引的元素相同,但是序列之间的顺序不同,则会产生笛卡尔积。 让我们在s2中更改索引的顺序,然后重新运行相同的操作:

>>> s1 = pd.Series(index=list('aaabb'), data=np.arange(5))
>>> s2 = pd.Series(index=list('bbaaa'), data=np.arange(5))
>>> s1 + s2
a    2
a    3
a    4
a    3
a    4
a    5
a    4
a    5
a    6
b    3
b    4
b    4
b    5
dtype: int64

有趣的是,Pandas 在同一项操作中有两个截然不同的结果。 如果笛卡尔积是 Pandas 的唯一选择,那么将数据帧的列加在一起这样的简单操作将使返回的元素数量激增。

在此秘籍中,每个序列具有不同数量的元素。 通常,当操作维中不包含相同数量的元素时,Python 和其他语言中的类似数组的数据结构将不允许进行操作。 Pandas 可以通过在完成操作之前先对齐索引来实现此目的。

另见

  • 第 3 章,“开始数据分析”中的“通过更改数据类型来减少内存”秘籍

索引爆炸

先前的秘籍中有一个琐碎的示例,其中将两个小序列与不相等的索引一起添加。 处理较大的数据时,此问题可能会产生可笑的错误结果。

准备

在此秘籍中,我们添加了两个较大的序列,它们的索引只有几个唯一值,但顺序不同。 结果将使索引中的值数量爆炸。

操作步骤

  1. 读入员工数据并将索引设置为与race列相等:
>>> employee = pd.read_csv('data/employee.csv', index_col='RACE')
>>> employee.head()

  1. 选择BASE_SALARY列作为两个不同的序列。 检查此操作是否确实创建了两个新对象:
>>> salary1 = employee['BASE_SALARY']
>>> salary2 = employee['BASE_SALARY']
>>> salary1 is salary2
True
  1. salary1salary2变量实际上是指同一对象。 这意味着对一个的任何更改都会更改另一个。 为确保您收到数据的全新副本,请使用copy方法:
>>> salary1 = employee['BASE_SALARY'].copy()
>>> salary2 = employee['BASE_SALARY'].copy()
>>> salary1 is salary2
False
  1. 让我们通过对其中一个序列进行排序来更改其索引顺序:
>>> salary1 = salary1.sort_index()
>>> salary1.head()
RACE
American Indian or Alaskan Native    78355.0
American Indian or Alaskan Native    81239.0
American Indian or Alaskan Native    60347.0
American Indian or Alaskan Native    68299.0
American Indian or Alaskan Native    26125.0
Name: BASE_SALARY, dtype: float64

>>> salary2.head()
RACE
Hispanic/Latino    121862.0
Hispanic/Latino     26125.0
White               45279.0
White               63166.0
White               56347.0
Name: BASE_SALARY, dtype: float64
  1. 让我们将这些salary序列加在一起:
>>> salary_add = salary1 + salary2
>>> salary_add.head()
RACE
American Indian or Alaskan Native    138702.0
American Indian or Alaskan Native    156710.0
American Indian or Alaskan Native    176891.0
American Indian or Alaskan Native    159594.0
American Indian or Alaskan Native    127734.0
Name: BASE_SALARY, dtype: float64
  1. 操作成功完成。 让我们再添加一个salary1序列,然后输出每个序列的长度。 我们只是将该指数从 2,000 个值分解为超过 100 万个值:
>>> salary_add1 = salary1 + salary1
>>> len(salary1), len(salary2), len(salary_add), len(salary_add1)
(2000, 2000, 1175424, 2000)

工作原理

首先出现步骤 2,以创建两个唯一的对象,但实际上,它创建了一个由两个不同的变量名称引用的对象。 表达式employee['BASE_SALARY']从技术上讲创建的是视图,而不是全新的副本。 使用is运算符对此进行了验证。

在熊猫中,视图不是新对象,而只是对另一个对象的引用,通常是数据帧的某些子集。 此共享对象可能导致许多问题。

为了确保两个变量都引用完全不同的对象,我们使用copy序列方法,并再次使用is运算符验证它们是否是不同的对象。 步骤 4 使用sort_index方法按种族对序列进行排序。 第 5 步将这些不同的序列加在一起以产生一些结果。 仅检查头部,仍不清楚产生了什么。

步骤 6 向其自身添加salary1,以显示两个不同序列添加之间的比较。 此秘籍中所有序列的长度都已输出,我们清楚地看到series_add现已爆炸超过一百万个值。 索引中的每个唯一值都会产生笛卡尔积,因为索引并不完全相同。 此秘籍显着显示了将多个序列或数据帧组合在一起时索引可能产生的影响。

更多

通过做一些数学运算,我们可以验证salary_add的值的数量。 当笛卡尔积在所有相同的索引值之间发生时,我们可以求和它们各自计数的平方。 索引中甚至缺少的值也会与它们自身产生笛卡尔积:

>>> index_vc = salary1.index.value_counts(dropna=False)
>>> index_vc
Black or African American            700
White                                665
Hispanic/Latino                      480
Asian/Pacific Islander               107
NaN                                   35
American Indian or Alaskan Native     11
Others                                 2
Name: RACE, dtype: int64

>>> index_vc.pow(2).sum()
1175424

用不相等的索引填充值

当使用加法运算符将两个序列加在一起并且一个索引标签没有出现在另一个索引标签中时,结果值始终会丢失。 Pandas 提供了add方法,该方法提供了一种填充缺失值的选项。

准备

在本秘籍中,我们使用add方法的fill_value参数将baseball数据集中具有不等索引的多个序列合并在一起,以确保结果中没有缺失值。

操作步骤

  1. 读取三个baseball数据集,并将索引设置为playerID
>>> baseball_14 = pd.read_csv('data/baseball14.csv',
                              index_col='playerID')
>>> baseball_15 = pd.read_csv('data/baseball15.csv',
                              index_col='playerID')
>>> baseball_16 = pd.read_csv('data/baseball16.csv',
                              index_col='playerID')
>>> baseball_14.head()

  1. 使用索引方法difference发现baseball_14中而不是baseball_15中的索引标签,反之亦然:
>>> baseball_14.index.difference(baseball_15.index)
Index(['corpoca01', 'dominma01', 'fowlede01', 'grossro01',
       'guzmaje01', 'hoeslj01', 'krausma01', 'preslal01',
       'singljo02'], dtype='object', name='playerID')

>>> baseball_14.index.difference(baseball_16.index)
Index(['congeha01', 'correca01', 'gattiev01', 'gomezca01',
       'lowrije01', 'rasmuco01', 'tuckepr01', 'valbulu01'],
       dtype='object', name='playerID')
  1. 每个索引都有很多独特的参与者。 让我们找出三年内每个玩家的总点击数。H列包含匹配数:
>>> hits_14 = baseball_14['H']
>>> hits_15 = baseball_15['H']
>>> hits_16 = baseball_16['H']
>>> hits_14.head()
playerID
altuvjo01    225
cartech02    115
castrja01    103
corpoca01     40
dominma01    121
Name: H, dtype: int64
  1. 我们首先使用加法运算符将两个序列相加:
>>> (hits_14 + hits_15).head()
playerID
altuvjo01    425.0
cartech02    193.0
castrja01    174.0
congeha01      NaN
corpoca01      NaN
Name: H, dtype: float64
  1. 即使玩家congeha01corpoca01记录了 2015 年的热门歌曲,但他们的成绩仍然缺失。 让我们使用add方法及其参数fill_value来避免丢失值:
>>> hits_14.add(hits_15, fill_value=0).head()
playerID
altuvjo01    425.0
cartech02    193.0
castrja01    174.0
congeha01     46.0
corpoca01     40.0
Name: H, dtype: float64
  1. 我们通过再次链接add方法来添加 2016 年的匹配:
>>> hits_total = hits_14.add(hits_15, fill_value=0) \
                        .add(hits_16, fill_value=0)
>>> hits_total.head()
playerID
altuvjo01    641.0
bregmal01     53.0
cartech02    193.0
castrja01    243.0
congeha01     46.0
Name: H, dtype: float64
  1. 检查结果中是否缺少值:
>>> hits_total.hasnans
False

工作原理

add方法的工作方式与加法运算符相似,但通过提供fill_value参数代替不匹配的索引,可以提供更大的灵活性。 在此问题中,将不匹配的索引值默认设置为 0 是有意义的,但是您可以使用其他任何数字。

有时每个序列都包含与缺失值相对应的索引标签。 在此特定实例中,当添加两个序列时,无论是否使用fill_value参数,索引标签仍将对应于缺失值。 为了澄清这一点,请看以下示例,其中索引标签a对应于每个序列中的缺失值:

>>> s = pd.Series(index=['a', 'b', 'c', 'd'],
                  data=[np.nan, 3, np.nan, 1])
>>> s
a    NaN
b    3.0
c    NaN
d    1.0
dtype: float64

>>> s1 = pd.Series(index=['a', 'b', 'c'], data=[np.nan, 6, 10])
>>> s1
a    NaN 
b    6.0
c   10.0 
dtype: float64

>>> s.add(s1, fill_value=5)
a     NaN
b     9.0
c    15.0
d     6.0
dtype: float64

更多

此秘籍展示了如何仅将单个索引添加到序列中。 也完全可以将数据帧一起添加。 将数据帧加在一起将在计算之前对齐索引和列,并产生不匹配索引的缺失值。 首先,从 2014 年棒球数据集中选择一些列。

>>> df_14 = baseball_14[['G','AB', 'R', 'H']]
>>> df_14.head()

Let's also select a few of the same and a few different columns from the 2015 baseball dataset:

>>> df_15 = baseball_15[['AB', 'R', 'H', 'HR']]
>>> df_15.head()

如果行或列标签无法对齐,则将两个数据帧一起添加会丢失值。 使用style属性访问highlight_null方法可轻松查看缺失值的位置:

>>> (df_14 + df_15).head(10).style.highlight_null('yellow')

只有两个数据帧中都出现playerID的行才会丢失。 类似地,ABHR列是两个数据帧中唯一出现的列。 即使我们在指定fill_value参数的情况下使用add方法,我们仍然缺少值。 这是因为在我们的输入数据中从来没有行和列的某些组合。 例如,playerID congeha01和列G的交集。 他只出现在 2015 年没有G列的数据集中。 因此,没有任何值被填充:

>>> df_14.add(df_15, fill_value=0).head(10) \
         .style.highlight_null('yellow')

追加来自不同数据帧的列

所有数据帧都可以向自己添加新列。 但是,像往常一样,每当一个数据帧从另一个数据帧或序列添加一个新列时,索引都将在创建新列之前首先对齐。

准备

此秘籍使用employee数据集添加一个新列,其中包含该员工部门的最高薪水。

操作步骤

  1. 导入employee数据,然后在新的数据帧中选择DEPARTMENTBASE_SALARY列:
>>> employee = pd.read_csv('data/employee.csv')
>>> dept_sal = employee[['DEPARTMENT', 'BASE_SALARY']]
  1. 将此较小的数据帧按每个部门内的薪水排序:
>>> dept_sal = dept_sal.sort_values(['DEPARTMENT', 'BASE_SALARY'], 
                                      ascending=[True, False])
  1. 使用drop_duplicates方法保留每个DEPARTMENT的第一行:
>>> max_dept_sal = dept_sal.drop_duplicates(subset='DEPARTMENT')
>>> max_dept_sal.head()

  1. DEPARTMENT列放入每个数据帧的索引中:
>>> max_dept_sal = max_dept_sal.set_index('DEPARTMENT')
>>> employee = employee.set_index('DEPARTMENT')
  1. 现在索引包含匹配的值,我们可以将新列追加到employee数据帧:
>>> employee['MAX_DEPT_SALARY'] = max_dept_sal['BASE_SALARY']
>>> employee.head()

  1. 我们可以使用query方法验证结果,以检查是否存在BASE_SALARY大于MAX_DEPT_SALARY的行:
>>> employee.query('BASE_SALARY > MAX_DEPT_SALARY')

工作原理

步骤 2 和 3 找到每个部门的最高工资。 为了使索引自动对齐正常工作,我们将每个数据帧索引设置为部门。 步骤 5 之所以有效,是因为左侧的数据帧中的每行索引;employee与来自右侧数据帧max_dept_sal的一个且仅一个索引对齐。 如果max_dept_sal在其索引中重复了任何部门,则该操作将失败。

例如,让我们看看当我们在具有重复索引值的等式的右侧使用数据帧时会发生什么。 我们使用数据帧的sample方法随机选择十行而不进行替换:

>>> np.random.seed(1234)
>>> random_salary = dept_sal.sample(n=10).set_index('DEPARTMENT')
>>> random_salary

注意索引中有几个重复的部门。 现在,当我们尝试创建新列时,将引发一个错误,警告我们有重复项。employee数据帧中的至少一个索引标签与random_salary中的两个或多个索引标签结合在一起:

>>> employee['RANDOM_SALARY'] = random_salary['BASE_SALARY']
ValueError: cannot reindex from a duplicate axis

更多

并非等号左侧的所有索引都需要匹配,但最多只能有一个匹配。 如果左对齐的数据帧索引没有任何内容,则将缺少结果值。 让我们创建一个发生这种情况的示例。 我们将仅使用max_dept_sal序列的前三行来创建新列:

>>> employee['MAX_SALARY2'] = max_dept_sal['BASE_SALARY'].head(3)
>>> employee.MAX_SALARY2.value_counts()
140416.0    29
100000.0    11
64251.0      5
Name: MAX_SALARY2, dtype: int64

>>> employee.MAX_SALARY2.isnull().mean()
.9775

该操作成功完成,但仅为三个部门的填充了薪水。 没有出现在max_dept_sal序列的前三行中的所有其他部门导致值丢失。

另见

  • 第 3 章“开始数据分析”中的“从最大值中选择最小值”秘籍

突出显示每一列的最大值

college数据集有许多数字列,它们描述了有关每所学校的不同指标。 许多人都对在某些指标上表现最好的学校感兴趣。

准备

此秘籍发现每个数字列具有最大值的学校,并设置数据帧的样式以突出显示信息,以便用户轻松使用。

操作步骤

  1. 阅读以机构名称作为索引的大学数据集:
>>> college = pd.read_csv('data/college.csv', index_col='INSTNM')
>>> college.dtypes
CITY                   object
STABBR                 object
HBCU                  float64
MENONLY               float64
                       ...   
PCTFLOAN              float64
UG25ABV               float64
MD_EARN_WNE_P10        object
GRAD_DEBT_MDN_SUPP     object
Length: 26, dtype: object
  1. CITYSTABBR以外的所有其他列似乎都是数字。 检查上一步中的数据类型会意外显示MD_EARN_WNE_P10GRAD_DEBT_MDN_SUPP列属于对象类型,而不是数字类型。 为了更好地了解这些列中的值是什么,让我们检查它们的第一个值:
>>> college.MD_EARN_WNE_P10.iloc[0]
'30300'

>>> college.GRAD_DEBT_MDN_SUPP.iloc[0]
'33888'
  1. 这些值是字符串,但我们希望它们是数字。 这意味着在序列的其他地方可能会出现非数字字符。 一种检查方法是按降序对这些列进行排序并检查前几行:
>>> college.MD_EARN_WNE_P10.sort_values(ascending=False).head()
INSTNM
Sharon Regional Health System School of Nursing    PrivacySuppressed
Northcoast Medical Training Academy                PrivacySuppressed
Success Schools                                    PrivacySuppressed
Louisiana Culinary Institute                       PrivacySuppressed
Bais Medrash Toras Chesed                          PrivacySuppressed
Name: MD_EARN_WNE_P10, dtype: object
  1. 罪魁祸首似乎是一些学校对这两列数据存在隐私问题。 要将这些列强制为数字,请使用 pandas 函数to_numeric
>>> cols = ['MD_EARN_WNE_P10', 'GRAD_DEBT_MDN_SUPP']
>>> for col in cols:
        college[col] = pd.to_numeric(college[col], errors='coerce')

>>> college.dtypes.loc[cols]
MD_EARN_WNE_P10       float64
GRAD_DEBT_MDN_SUPP    float64
dtype: object
  1. 使用select_dtypes方法仅过滤数字列。 这将排除STABBRCITY列,列,其中最大值对于此问题没有意义:
>>> college_n = college.select_dtypes(include=[np.number])
>>> college_n.head()

  1. 通过利用数据字典,有几列仅具有二进制(0/1)值,不会提供有用的信息。 为了以编程方式找到这些列,我们可以创建布尔序列并使用nunique方法找到具有两个唯一值的所有列:
>>> criteria = college_n.nunique() == 2
>>> criteria.head()
HBCU          True
MENONLY       True
WOMENONLY     True
RELAFFIL      True
SATVRMID     False
dtype: bool
  1. 将此布尔序列传递给列索引对象的索引运算符,并创建二进制列的列表:
>>> binary_cols = college_n.columns[criteria].tolist()
>>> binary_cols
['HBCU', 'MENONLY', 'WOMENONLY', 'RELAFFIL', 'DISTANCEONLY', 'CURROPER']
  1. 使用drop方法删除二进制列:
>>> college_n2 = college_n.drop(labels=binary_cols, axis='columns')
>>> college_n2.head()

  1. 使用idxmax方法查找每一列的最大值的索引标签:
>>> max_cols = college_n2.idxmax()
>>> max_cols
SATVRMID                      California Institute of Technology
SATMTMID                      California Institute of Technology
UGDS                               University of Phoenix-Arizona
UGDS_WHITE                Mr Leon's School of Hair Design-Moscow
                                         ...                    
PCTFLOAN                                  ABC Beauty College Inc
UG25ABV                           Dongguk University-Los Angeles
MD_EARN_WNE_P10                     Medical College of Wisconsin
GRAD_DEBT_MDN_SUPP    Southwest University of Visual Arts-Tucson
Length: 18, dtype: object
  1. max_cols序列上调用unique方法。 这将返回唯一列名称的ndarray
>>> unique_max_cols = max_cols.unique()
>>> unique_max_cols[:5]
array(['California Institute of Technology',
       'University of Phoenix-Arizona',
       "Mr Leon's School of Hair Design-Moscow",
       'Velvatex College of Beauty Culture',
       'Thunderbird School of Global Management'], dtype=object)
  1. 使用max_cols的值选择仅具有最大值的学校的行,然后使用style属性突出显示这些值:
>>> college_n2.loc[unique_max_cols].style.highlight_max()

工作原理

idxmax方法非常强大,当索引被有意义地标记时,它变得非常有用。 出乎意料的是,MD_EARN_WNE_P10GRAD_DEBT_MDN_SUPP均为object数据类型。 导入时,如果列中至少包含一个字符串,则 pandas 将列的所有数值强制转换为字符串。

通过检查步骤 2 中的特定列值,我们可以清楚地看到 在这些列中有字符串。 在第 3 步中,我们以降序排序,因为数字字符首先出现。 这会将所有字母值提升到该序列的顶部。 我们发现PrivacySuppressed字符串造成严重破坏。 Pandas 可以使用to_numeric函数将仅包含数字字符的所有字符串强制转换为实际的数字数据类型。 要覆盖在to_numeric遇到无法转换的字符串时引发错误的默认行为,必须将coerce传递给errors参数。 这将强制所有非数字字符串变为缺失值(np.nan)。

几列没有有用或有意义的最大值。 在第 4 步到第 6 步中已将它们删除。select_dtypes对于具有许多列的非常宽的数据帧极为有用。

在步骤 7 中,idxmax遍历所有列以找到每个列的最大值的索引。 它将结果作为序列输出。 SAT 数学和口语成绩均最高的学校是加利福尼亚理工学院。 洛杉矶东国大学的 25 岁以上的学生人数最多。

尽管idxmax提供的信息很好,但它不会产生相应的最大值。 为此,我们从max_cols序列的值中收集所有唯一的学校名称。

最后,在步骤 8 中,我们使用.loc索引器根据索引标签选择行,在第一步中将其作为学校名称。 此过滤器仅适用于具有最大值的学校。数据帧具有实验性style属性,该属性本身具有一些方法来更改显示的数据帧的外观。 突出显示最大值可使结果更加清晰。

更多

默认情况下,highlight_max方法突出显示每列的最大值。 我们可以使用axis参数突出显示每行的最大值。 在这里,我们只选择college数据集的种族百分比列,并突出显示每所学校百分比最高的种族:

>>> college = pd.read_csv('data/college.csv', index_col='INSTNM')
>>> college_ugds = college.filter(like='UGDS_').head()
>>> college_ugds.style.highlight_max(axis='columns')

尝试在大型数据帧上应用样式会导致 Jupyter 崩溃,这就是为什么仅将样式应用于数据帧的头部的原因。

另见

使用方法链接复制idxmax

尝试自行实现内置数据帧方法可能是一个很好的练习。 这种复制可以使您对通常不会遇到的其他 Pandas 方法有更深入的了解。idxmax是仅使用本书到目前为止介绍的方法进行复制的一种挑战性方法。

准备

此秘籍将基本方法缓慢地链接在一起,以最终找到包含最大列值的所有行索引值。

操作步骤

  1. 加载大学数据集并执行与上一个秘籍相同的操作,以仅获取感兴趣的数字列:
>>> college = pd.read_csv('data/college.csv', index_col='INSTNM')
>>> cols = ['MD_EARN_WNE_P10', 'GRAD_DEBT_MDN_SUPP']

>>> for col in cols:
        college[col] = pd.to_numeric(college[col], errors='coerce')

>>> college_n = college.select_dtypes(include=[np.number])
>>> criteria = college_n.nunique() == 2
>>> binary_cols = college_n.columns[criteria].tolist()
>>> college_n = college_n.drop(labels=binary_cols, axis='columns')
  1. 使用max方法查找每列的最大值:
>>> college_n.max().head()
SATVRMID         765.0
SATMTMID         785.0
UGDS          151558.0
UGDS_WHITE         1.0
UGDS_BLACK         1.0
dtype: float64
  1. 使用数据帧的eq方法使用其max列测试每个值。 默认情况下,eq方法将列数据帧的列与传递的序列索引的标签对齐:
>>> college_n.eq(college_n.max()).head()

  1. 此数据帧中所有具有至少一个True值的行都必须包含最大列数。 让我们使用any方法查找具有至少一个True值的所有此类行:
>>> has_row_max = college_n.eq(college_n.max()).any(axis='columns')
>>> has_row_max.head()
INSTNM
Alabama A & M University               False
University of Alabama at Birmingham    False
Amridge University                     False
University of Alabama in Huntsville    False
Alabama State University               False
dtype: bool
  1. 只有 18 列,这意味着has_row_max中最多只能有 18 个True值。 让我们找出实际有多少个:
>>> college_n.shape
(7535, 18)

>>> has_row_max.sum()
401
  1. 这有点出乎意料,但是事实证明,有些列的许多行等于最大值。 这对于许多最大值为 1 的百分比列很常见。idxmax返回第一次出现的最大值。 让我们备份一下,删除any方法,然后看一下步骤 3 的输出。让我们运行cumsum方法来累积所有True值。 显示了前三行:
>>> college_n.eq(college_n.max()).cumsum()

  1. 有些列具有一个唯一的最大值,例如SATVRMIDSATMTMID,而另一些列则具有UGDS_WHITE。 109 所学校的本科生中有 100% 是白人。 如果我们再链接一次cumsum方法,则值 1 在每一列中只会出现一次,并且它将是最大值的第一次出现:
>>> college_n.eq(college_n.max()).cumsum().cumsum()

  1. 现在,我们可以使用eq方法测试每个值是否等于 1,然后使用any方法查找具有至少一个True值的行:
>>> has_row_max2 = college_n.eq(college_n.max()) \
                             .cumsum() \
                             .cumsum() \
                             .eq(1) \
                             .any(axis='columns')
>>> has_row_max2.head()
INSTNM
Alabama A & M University               False
University of Alabama at Birmingham    False
Amridge University                     False
University of Alabama in Huntsville    False
Alabama State University               False
dtype: bool
  1. 测试has_row_max2True值不超过列数:
>>> has_row_max2.sum()
16
  1. 我们需要has_row_max2True的所有机构。 我们可以简单地在序列本身上使用布尔索引:
>>> idxmax_cols = has_row_max2[has_row_max2].index
>>> idxmax_cols
Index(['Thunderbird School of Global Management',
       'Southwest University of Visual Arts-Tucson',
       'ABC Beauty College Inc',
       'Velvatex College of Beauty Culture',
       'California Institute of Technology',
       'Le Cordon Bleu College of Culinary Arts-San Francisco',
       'MTI Business College Inc', 'Dongguk University-Los Angeles',
       'Mr Leon's School of Hair Design-Moscow',
       'Haskell Indian Nations University', 'LIU Brentwood',
       'Medical College of Wisconsin', 'Palau Community College',
       'California University of Management and Sciences',
       'Cosmopolitan Beauty and Tech School',
       'University of Phoenix-Arizona'], dtype='object', name='INSTNM')
  1. 这些机构中的所有 16 个都是至少其中一列的第一个最大出现次数的索引。 我们可以检查它们是否与idxmax方法中找到的相同:
>>> set(college_n.idxmax().unique()) == set(idxmax_cols)
True

工作原理

第一步通过将两列转换为数字并消除二进制列来复制上一个秘籍的工作。 我们在步骤 2 中找到每列的最大值。在这里,需要谨慎,因为 Pandas 会默默地丢弃无法产生最大值的列。 如果发生这种情况,则第 3 步仍将完成,但将为每列生成所有False值,而没有可用的最大值。

步骤 4 使用any方法在每一行中进行扫描,以搜索至少一个True值。 具有至少一个True值的任何行都包含一列的最大值。 我们在步骤 5 中对所得的布尔序列求和,以确定多少行包含最大值。 出乎意料的是,行多于列。 步骤 6 深入说明了为什么会发生这种情况。 我们对步骤 3 的输出进行累计,并检测等于每列最大值的总行数。

许多大学只有一个种族就拥有 100% 的学生人数。 到目前为止,这是最大的多个行的最大贡献者。 如您所见,SAT 成绩栏和大学本科生只有一排具有最大值的行,但是某些种族栏有最大值。

我们的目标是找到具有最大值的第一行。 我们需要再次取累加总和,以使每一列只有一行等于 1。步骤 8 将代码格式化为每行只有一个方法,并完全按照步骤 4 的方式运行any方法。 此步骤成功后,则True值应不超过列数。 步骤 9 断言这是真的。

为了验证我们是否在前几列中找到与idxmax相同的列,我们对has_row_max2本身使用了布尔选择。 列将以不同的顺序排列,因此我们将列名称的顺序转换为集合,这些集合固有地无序比较相等性。

更多

可以用一长串代码将索引运算符与匿名函数链接起来,从而完成此秘籍。 这个小技巧使您无需执行第 10 步。在此秘籍中,我们可以估算直接idxmax方法与我们的手动工作之间的差异:

>>> %timeit college_n.idxmax().values
1.12 ms ± 28.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

>>> %timeit college_n.eq(college_n.max()) \
                                  .cumsum() \
                                  .cumsum() \
                                  .eq(1) \
                                  .any(axis='columns') \
                                  [lambda x: x].index
5.35 ms ± 55.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

不幸的是,我们的工作速度是 Pandas idxmax内置方法的五倍,但是不管其性能如何下降,许多创新且实用的解决方案都使用布尔序列和cumsum累积方法来查找条纹或一个轴的特定模式。

寻找最常见的最大值

大学数据集包含超过 7,500 所大学的 8 个不同种族的本科人口百分比。 找到每所学校本科生人数最多的种族,然后为整个数据集找到此结果的分布将是很有趣的。 我们将能够回答一个类似“哪个机构的白人学生比其他任何种族都要多”的问题。

准备

在此秘籍中,我们使用idxmax方法找到每所学校的本科生百分比最高的种族,然后找到这些最大值的分布。

操作步骤

  1. 阅读大学数据集,然后仅选择那些包含大学种族百分比信息的列:
>>> college = pd.read_csv('data/college.csv', index_col='INSTNM')
>>> college_ugds = college.filter(like='UGDS_')
>>> college_ugds.head()

  1. 使用idxmax方法获取每一行具有最高竞争百分比的列名称:
>>> highest_percentage_race = college_ugds.idxmax(axis='columns')
>>> highest_percentage_race.head()
INSTNM
Alabama A & M University               UGDS_BLACK
University of Alabama at Birmingham    UGDS_WHITE
Amridge University                     UGDS_BLACK
University of Alabama in Huntsville    UGDS_WHITE
Alabama State University               UGDS_BLACK
dtype: object
  1. 使用value_counts方法返回最大出现次数的分布:
>>> highest_percentage_race.value_counts(normalize=True)
UGDS_WHITE    0.670352
UGDS_BLACK    0.151586
UGDS_HISP     0.129473
UGDS_UNKN     0.023422
UGDS_ASIAN    0.012074
UGDS_AIAN     0.006110
UGDS_NRA      0.004073
UGDS_NHPI     0.001746
UGDS_2MOR     0.001164
dtype: float64

工作原理

此秘籍的关键是要认识到所有列都代表相同的信息单元。 我们可以将这些列相互比较,通常是而不是情况。 例如,直接将 SAT 口语成绩与大学生人数进行比较是没有意义的。 由于数据是以这种方式构造的,因此我们可以将idxmax方法应用于数据的每一行,以找到具有最大值的列。 我们需要使用axis参数更改其默认行为。

第 2 步完成此操作并返回一个序列,我们现在可以简单地对其应用value_counts方法以返回分布。 我们将True传递给normalize参数,因为我们对分布(相对频率)感兴趣,而不是原始计数。

更多

我们可能想探索更多并回答这个问题:对于黑人学生多于其他种族的学校,第二高种族百分比的分布是什么?

>>> college_black = college_ugds[highest_percentage_race == 'UGDS_BLACK']
>>> college_black = college_black.drop('UGDS_BLACK', axis='columns')
>>> college_black.idxmax(axis='columns').value_counts(normalize=True)
UGDS_WHITE    0.661228
UGDS_HISP     0.230326
UGDS_UNKN     0.071977
UGDS_NRA      0.018234
UGDS_ASIAN    0.009597
UGDS_2MOR     0.006718
UGDS_AIAN     0.000960
UGDS_NHPI     0.000960
dtype: float64

在此秘籍中应用相同的方法之前,我们需要删除UGDS_BLACK列。 有趣的是,这些黑人人口较多的学校似乎倾向于拥有较高的西班牙裔人口。

七、分组以进行汇总,过滤和转换

在本章中,我们将介绍以下主题:

  • 定义聚合
  • 使用函数对多个列执行分组和聚合
  • 分组后删除多重索引
  • 自定义聚合函数
  • 使用*args**kwargs自定义聚合函数
  • 检查groupby对象
  • 筛选少数人群居多的州
  • 转换减肥赌注
  • 计算每个州的 SAT 加权平均成绩
  • 按连续变量分组
  • 计算城市之间的航班总数
  • 寻找最长的准时航班

介绍

数据分析过程中最基本的任务之一是在对每个组执行计算之前将数据分成独立的组。 该方法已经存在了相当长的时间,但是最近被称为拆分应用组合。 本章介绍了功能强大的groupby方法,该方法可让您以可想象的任何方式对数据进行分组,并在返回单个数据集之前将任何类型的函数独立地应用于每个组。

Hadley Wickham 创造了术语“拆分应用组合”,用于描述将数据分为独立的可管理块,将函数独立应用于这些块,然后将结果组合在一起的通用数据分析模式。 可以在他的论文中找到更多详细信息。

在开始使用秘籍之前,我们只需要了解一些术语。 所有基本的分组操作都有分组列,这些列中值的每个唯一组合代表数据的独立分组。 语法如下所示:

>>> df.groupby(['list', 'of', 'grouping', 'columns'])
>>> df.groupby('single_column')  # when grouping by a single column

该操作的结果返回一个分组对象。 正是这个分组对象将成为驱动整个整章所有计算的引擎。 在通过对象创建此分组时,Pandas 实际上很少执行,仅验证了分组是可能的。 您必须在该分组对象上链接方法,以释放其潜能。

从技术上讲,该操作的结果将是DataFrameGroupBySeriesGroupBy,但为简单起见,在整章中将其称为分组对象。

定义聚合

groupby方法最常见的用途是执行聚合。 实际是什么聚合? 在我们的数据分析世界中,当许多输入的序列被汇总或组合为单个值输出时,就会发生汇总。 例如,对一列的所有值求和或求其最大值是应用于单个数据序列的常见聚合。 聚合仅获取许多值,然后将其转换为单个值。

除了介绍中定义的分组列外,大多数聚合还有两个其他组件,聚合列聚合函数。 汇总列是其值将被汇总的列。 聚合函数定义聚集的方式。 主要的聚合函数包括summinmaxmeancountvariancestd等。

准备

在此秘籍中,我们检查航班数据集,并执行最简单的可能的汇总,仅涉及单个分组列,单个汇总列和单个汇总函数。 我们将找到每家航空公司的平均到达延误时间。 Pandas 具有相当多种不同的语法来产生聚合,本秘籍涵盖了它们。

操作步骤

  1. 读取飞行数据集,并定义分组列(AIRLINE),聚合列(ARR_DELAY)和聚合函数(mean):
>>> flights = pd.read_csv('data/flights.csv')
>>> flights.head()

  1. 将分组列放在groupby方法中,然后通过字典在agg方法中将聚集列及其聚合函数配对:
>>> flights.groupby('AIRLINE').agg({'ARR_DELAY':'mean'}).head()

  1. 或者,您可以将汇总列放在索引运算符中,然后将汇总函数作为字符串传递给agg
>>> flights.groupby('AIRLINE')['ARR_DELAY'].agg('mean').head()
AIRLINE
AA     5.542661
AS    -0.833333
B6     8.692593
DL     0.339691
EV     7.034580
Name: ARR_DELAY, dtype: float64
  1. 上一步中使用的字符串名称是 Pandas 提供的一种便捷功能,可让您引用特定的聚合函数。 您可以将任何聚合函数直接传递给agg方法,例如 NumPy mean函数。 输出与上一步相同:
>>> flights.groupby('AIRLINE')['ARR_DELAY'].agg(np.mean).head()
  1. 在这种情况下,可以完全跳过agg方法,而直接使用mean方法。 此输出也与步骤 3 相同:
>>> flights.groupby('AIRLINE')['ARR_DELAY'].mean().head()

工作原理

groupby方法的语法不像其他方法那么简单。 让我们通过将groupby方法的结果存储为自己的变量来拦截步骤 2 中的方法链

>>> grouped = flights.groupby('AIRLINE')
>>> type(grouped)
pandas.core.groupby.DataFrameGroupBy

首先使用其自己独特的属性和方法来生产一个全新的中间对象。 在此阶段没有任何计算。 Pandas 仅验证分组列。 该分组对象具有agg方法来执行聚合。 使用此方法的一种方法是向其传递一个字典,该字典将聚合列映射到聚合函数,如步骤 2 所示。

有几种不同的语法产生相似的结果,而步骤 3 显示了另一种方法。 与其标识字典中的聚合列,不如将其放在索引运算符中,就如同您从数据帧中将其选择为列一样。 然后,将函数字符串名称作为标量传递给agg方法。

您可以将任何汇总函数传递给agg方法。 为了简单起见,Pandas 允许您使用字符串名称,但是您也可以像在步骤 4 中一样明确地调用一个聚合函数。NumPy 提供了许多聚合值的函数。

步骤 5 显示了最后一种语法风格。 如本例所示,当仅应用单个聚合函数时,通常可以直接将其作为对分组对象本身的方法进行调用,而无需使用agg。 并非所有聚合函数都具有等效的方法,但是许多基本函数都有。 以下是几个聚合函数的列表,这些函数可以作为字符串传递给agg或作为方法直接链接到分组对象:

min     max    mean    median    sum    count    std var size    describe    nunique     idxmin     idxmax

更多

如果您不对agg使用聚合函数,则 pandas 会引发异常。 例如,让我们看看将平方根函数应用于每个组会发生什么:

>>> flights.groupby('AIRLINE')['ARR_DELAY'].agg(np.sqrt)
ValueError: function does not reduce

另见

使用函数对多个列执行分组和聚合

可以对多列进行分组和聚合。 语法仅与使用单个列进行分组和聚合时稍有不同。 与任何分组操作一样,它有助于识别三个组成部分:分组列,聚合列和聚合函数。

准备

在本秘籍中,我们通过回答以下查询来展示数据帧的groupby方法的灵活性:

  • 查找每个工作日每个航空公司的已取消航班的数量
  • 查找每个航空公司在工作日内已取消和改航航班的数量和百分比
  • 对于每个始发地和目的地,查找航班总数,已取消航班的数量和百分比,以及通话时间的平均值和方差

操作步骤

  1. 读取航班数据集,并通过定义分组列(AIRLINE, WEEKDAY),聚合列(CANCELLED)和聚合函数(sum)回答第一个查询:
>>> flights.groupby(['AIRLINE', 'WEEKDAY'])['CANCELLED'] \
           .agg('sum').head(7)
AIRLINE  WEEKDAY
AA       1          41
         2           9
         3          16
         4          20
         5          18
         6          21
         7          29
Name: CANCELLED, dtype: int64
  1. 通过使用每对分组和聚集列的列表来回答第二个查询。 另外,对聚合函数使用列表:
>>> flights.groupby(['AIRLINE', 'WEEKDAY']) \
            ['CANCELLED', 'DIVERTED'].agg(['sum', 'mean']).head(7)

  1. 使用agg方法中的字典来回答第三个查询,以将特定的聚合列映射到特定的聚合函数:
>>> group_cols = ['ORG_AIR', 'DEST_AIR']
>>> agg_dict = {'CANCELLED':['sum', 'mean', 'size'], 
                'AIR_TIME':['mean', 'var']}
>>> flights.groupby(group_cols).agg(agg_dict).head()

工作原理

要像步骤 1 一样按多列分组,我们将字符串名称列表传递给groupby方法。AIRLINEWEEKDAY的每个唯一组合均形成一个独立的组。 在每个组中,找到已取消航班的总数,然后将其作为序列返回。

步骤 2,再次按AIRLINEWEEKDAY分组,但这一次汇总了两列。 它将两个聚合函数summean中的每一个应用于每个列,从而每组返回四个列。

步骤 3 进一步进行,并使用字典将特定的聚合列映射到不同的聚合函数。 请注意,size聚合函数返回每个组的总行数。 这与count汇总函数不同,后者会返回每组非缺失值的数量。

更多

执行聚合时,会遇到几种主要的语法。 以下四个伪代码块总结了使用groupby方法执行聚合的主要方式:

  1. agg与字典一起使用是最灵活的方法,它允许您为每一列指定聚合函数:
>>> df.groupby(['grouping', 'columns']) \
      .agg({'agg_cols1':['list', 'of', 'functions'], 
            'agg_cols2':['other', 'functions']})
  1. agg与聚合函数列表一起使用,会将每个函数应用于每个聚合列:
>>> df.groupby(['grouping', 'columns'])['aggregating', 'columns'] \
      .agg([aggregating, functions])
  1. 直接使用紧随汇总列之后的方法而不是agg,仅将该方法应用于每个汇总列。 这种方式不允许多种聚合函数:
>>> df.groupby(['grouping', 'columns'])['aggregating', 'columns'] \
      .aggregating_method()
  1. 如果您未指定汇总列,则汇总方法将应用于所有非分组列:
>>> df.groupby(['grouping', 'columns']).aggregating_method()

在前面的四个代码块中,当按单个列进行分组或聚合时,可以用字符串代替任何列表。

分组后删除多重索引

不可避免地,当使用groupby时,您可能会在列或行或两者中都创建多重索引。 具有多重索引的数据帧更加难以导航,并且有时列名称也令人困惑。

准备

在本秘籍中,我们使用groupby方法执行聚合,以创建具有行和列多重索引的数据帧,然后对其进行处理,以使索引为单个级别,并且列名具有描述性。

操作步骤

  1. 读取航班数据集; 编写声明以查找飞行的总里程和平均里程; 以及每个航空公司在每个工作日的最大和最小到达延误:
>>> flights = pd.read_csv('data/flights.csv')
>>> airline_info = flights.groupby(['AIRLINE', 'WEEKDAY'])\
                          .agg({'DIST':['sum', 'mean'], 
                                'ARR_DELAY':['min', 'max']}) \
                          .astype(int)
>>> airline_info.head(7)

  1. 行和列均由具有两个级别的多重索引标记。 让我们将其压缩到单个级别。 为了解决这些列,我们使用多重索引方法get_level_values。 让我们显示每个级别的输出,然后将两个级别连接起来,然后再将其设置为新的列值:
>>> level0 = airline_info.columns.get_level_values(0)
Index(['DIST', 'DIST', 'ARR_DELAY', 'ARR_DELAY'], dtype='object')

>>> level1 = airline_info.columns.get_level_values(1)
Index(['sum', 'mean', 'min', 'max'], dtype='object')

>>> airline_info.columns = level0 + '_' + level1
>>> airline_info.head(7)

  1. 使用reset_index将行标签返回到单个级别:
>>> airline_info.reset_index().head(7)

工作原理

当使用agg方法对多个列执行聚合时,pandas 将创建一个具有两个级别的索引对象。 聚合列变为顶层,聚合函数变为底层。 Pandas 显示的多重索引级别与单级别的列不同。 除了最里面的级别以外,屏幕上不会显示重复的索引值。 您可以检查第 1 步中的数据帧以进行验证。 例如,DIST列仅显示一次,但它引用了前两列。

最里面的多重索引级别是最接近数据的级别。 这将是最底部的列级别和最右边的索引级别。

步骤 2 通过首先使用多重索引方法get_level_values.检索每个级别的基础值来定义新列。此方法接受一个整数,该整数标识索引级别。 它们从顶部/左侧以零开始编号。 索引支持向量化操作,因此我们将两个级别与下划线分开。 我们将这些新值分配给columns属性。

在第 3 步中,我们将两个索引级别都设为reset_index作为列。 我们可以像在第 2 步中那样将级别连接在一起,但是将它们保留为单独的列更有意义。

更多

默认情况下,在分组操作结束时,pandas 将所有分组列放入索引中。 可以将groupby方法中的as_index参数设置为False,以避免此行为。 您可以在分组后将reset_index方法链接起来,以获得与步骤 3 中相同的效果。让我们看一下其中的一个示例,该示例通过查找每个航空公司从每个航班出发的平均距离来得出:

>>> flights.groupby(['AIRLINE'], as_index=False)['DIST'].agg('mean') \
                                                        .round(0)

看一下先前结果中航空公司的顺序。 默认情况下,pandas 对分组列进行排序。sort参数存在于groupby方法中,并且默认为True。 您可以将其设置为False,以使分组列的顺序与在数据集中遇到分组列的顺序相同。 通过不对数据进行排序,您还将获得较小的性能提升。

自定义聚合函数

Pandas 提供了许多最常见的聚合函数,供您与分组对象一起使用。 在某些时候,您将需要编写自己的自定义用户定义函数,而这些函数在 pandas 或 NumPy 中不存在。

准备

在此秘籍中,我们使用大学数据集来计算每个州的本科生人数的均值和标准差。 然后,我们使用此信息从每个状态的任何单一总体值的均值中找到最大标准差数。

操作步骤

  1. 读取大学数据集,并按州找到本科人口的均值和标准差:
>>> college = pd.read_csv('data/college.csv')
>>> college.groupby('STABBR')['UGDS'].agg(['mean', 'std']) \
                                     .round(0).head()

  1. 这个输出不是我们想要的。 我们不是在寻找整个组的均值和标准差,而是寻找任何一个机构的均值的最大标准差数。 为了计算这一点,我们需要从每个机构的本科生人数中减去各州的本科生平均人数,然后除以标准差。 这使每个群体的本科生人数标准化。 然后,我们可以利用这些分数的绝对值的最大值来找到距离均值最远的那个。 Pandas 不提供能够执行此操作的函数。 相反,我们将需要创建一个自定义函数:
>>> def max_deviation(s):
        std_score = (s - s.mean()) / s.std()
        return std_score.abs().max()
  1. 定义函数后,将其直接传递给agg方法以完成聚合:
>>> college.groupby('STABBR')['UGDS'].agg(max_deviation) \
                                     .round(1).head()
STABBR
AK    2.6
AL    5.8
AR    6.3
AS    NaN
AZ    9.9
Name: UGDS, dtype: float64

工作原理

不存在预定义的 Pandas 函数来计算偏离均值的最大标准差数。 我们被迫在步骤 2 中构造一个自定义函数。请注意,此自定义函数max_deviation接受单个参数s。 展望第 3 步,您会注意到函数名称位于agg方法内,而没有直接调用。 参数s没有明确传递给max_deviation的地方。 相反,Pandas 将UGDS列作为序列隐式传递给max_deviation

每个组都会调用一次max_deviation函数。 由于s是序列,因此所有常规的序列方法均可用。 在称为标准化的过程中,从组中的每个值中减去该特定组的平均值,然后再除以标准差。

标准化是一种常见的统计过程,用于了解各个值与平均值之间的差异。 对于正态分布,数据的 99.7% 位于平均值的三个标准差之内。

由于我们对均值的绝对偏差感兴趣,因此我们从所有标准化得分中获取绝对值并返回最大值。agg方法必须从我们的自定义函数中返回单个标量值,否则将引发异常。 Pandas 默认使用样本标准差,该样本标准差对于只有单个值的任何组均未定义。 例如,州缩写AS(美属萨摩亚)返回了缺失值,因为它在数据集中只有一个机构。

更多

可以将我们的自定义函数应用于多个聚合列。 我们只需将更多列名称添加到索引运算符。max_deviation函数仅适用于数字列:

>>> college.groupby('STABBR')['UGDS', 'SATVRMID', 'SATMTMID'] \
           .agg(max_deviation).round(1).head()

您还可以将自定义的聚合函数与预构建函数一起使用。 以下是按国家和宗教派别进行的分组:

>>> college.groupby(['STABBR', 'RELAFFIL']) \
            ['UGDS', 'SATVRMID', 'SATMTMID'] \
           .agg([max_deviation, 'mean', 'std']).round(1).head()

请注意,pandas 使用函数名称作为返回列的名称。 您可以使用重命名方法直接更改列名称,也可以修改特殊功能属性__name__

>>> max_deviation.__name__
'max_deviation'

>>> max_deviation.__name__ = 'Max Deviation'
>>> college.groupby(['STABBR', 'RELAFFIL']) \
            ['UGDS', 'SATVRMID', 'SATMTMID'] \
           .agg([max_deviation, 'mean', 'std']).round(1).head()

使用*args**kwargs自定义聚合函数

在编写自己的用户定义的自定义聚合函数时,pandas 隐式地将每个聚合列作为一个序列一次传递给它。 有时,您将需要向函数传递的参数不仅仅是序列本身。 为此,您需要了解 Python 将任意数量的参数传递给函数的能力。 在inspect模块的帮助下,让我们看一下分组对象的agg方法的签名:

>>> college = pd.read_csv('data/college.csv')
>>> grouped = college.groupby(['STABBR', 'RELAFFIL'])

>>> import inspect
>>> inspect.signature(grouped.agg)
<Signature (arg, *args, **kwargs)>

参数*args允许您将任意数量的非关键字参数传递给自定义的聚合函数。 同样,**kwargs允许您传递任意数量的关键字参数。

准备

在此秘籍中,我们为大学数据集构建了一个自定义函数,该函数可按州和宗教隶属关系找到本科生人口在两个值之间的学校所占的百分比。

操作步骤

  1. 定义一个函数,该函数返回大学人口在 1000 至 3,000 之间的学校的百分比:
>>> def pct_between_1_3k(s):
        return s.between(1000, 3000).mean()
  1. 计算按州和宗教归属分类的百分比:
>>> college.groupby(['STABBR', 'RELAFFIL'])['UGDS'] \
           .agg(pct_between_1_3k).head(9)
STABBR  RELAFFIL
AK      0           0.142857
        1           0.000000
AL      0           0.236111
        1           0.333333
AR      0           0.279412
        1           0.111111
AS      0           1.000000
AZ      0           0.096774
        1           0.000000
Name: UGDS, dtype: float64
  1. 该函数可以正常工作,但不能给用户提供选择上下限的灵活性。 让我们创建一个新函数,该函数允许用户定义以下范围:
>>> def pct_between(s, low, high):
        return s.between(low, high).mean()
  1. 将此上限和下限传递给agg方法:
>>> college.groupby(['STABBR', 'RELAFFIL'])['UGDS'] \
           .agg(pct_between, 1000, 10000).head(9)
STABBR  RELAFFIL
AK      0           0.428571
        1           0.000000
AL      0           0.458333
        1           0.375000
AR      0           0.397059
        1           0.166667
AS      0           1.000000
AZ      0           0.233871
        1           0.111111
Name: UGDS, dtype: float64

工作原理

步骤 1 创建一个不接受任何额外参数的函数。 上下限必须硬编码到函数本身中,这不是很灵活。 步骤 2 显示了此聚合的结果。

我们在第 3 步中创建了一个更加灵活的函数,该函数允许用户动态定义上下限。 步骤 4 是*args**kwargs的魔力发挥作用的地方。 在此特定示例中,我们将两个非关键字参数 1,000 和 10,000 传递给agg方法。 Pandas 分别将这两个参数传递给pct_betweenlowhigh参数。

在步骤 4 中,有几种方法可以达到相同的结果。我们可以在以下命令中明确使用参数名称来产生相同的结果:

>>> college.groupby(['STABBR', 'RELAFFIL'])['UGDS'] \
           .agg(pct_between, high=10000, low=1000).head(9)

关键字参数的顺序并不重要,只要它们位于函数名称之后即可。 更进一步,我们可以混合使用非关键字和关键字参数,只要关键字参数排在最后即可:

>>> college.groupby(['STABBR', 'RELAFFIL'])['UGDS'] \
           .agg(pct_between, 1000, high=10000).head(9)

为了便于理解,最好按函数签名中定义的顺序包含所有参数名称。

从技术上讲,当调用agg时,所有非关键字参数都收集到名为args的元组中,而所有关键字参数都收集到名为kwargs的字典中。

更多

不幸的是,当同时使用多个聚合函数时,Pandas 没有直接使用这些附加参数的方法。 例如,如果您希望使用pct_betweenmean函数进行汇总,则会出现以下异常:

>>> college.groupby(['STABBR', 'RELAFFIL'])['UGDS'] \
           .agg(['mean', pct_between], low=100, high=1000) 
TypeError: pct_between() missing 2 required positional arguments: 'low' and 'high'

Pandas 无法理解需要将额外的参数传递给pct_between。 为了将我们的自定义函数与其他内置函数甚至其他自定义函数一起使用,我们可以定义一种称为闭包的特殊类型的嵌套函数。 我们可以使用通用闭包来构建所有自定义函数:

>>> def make_agg_func(func, name, *args, **kwargs):
        def wrapper(x):
            return func(x, *args, **kwargs)
        wrapper.__name__ = name
        return wrapper

>>> my_agg1 = make_agg_func(pct_between, 'pct_1_3k', low=1000, high=3000)
>>> my_agg2 = make_agg_func(pct_between, 'pct_10_30k', 10000, 30000)

>>> college.groupby(['STABBR', 'RELAFFIL'])['UGDS'] \
           .agg(['mean', my_agg1, my_agg2]).head()

make_agg_func函数充当创建自定义聚合函数的工厂。 它接受您已经构建的自定义聚合函数(在这种情况下为pct_between),name参数以及任意数量的额外参数。 它返回一个已经设置了额外参数的函数。 例如,my_agg1是一个特定的定制聚合函数,可以找到大学人口在一千到三千之间的学校所占的百分比。 额外的参数(*args**kwargs)为您的自定义函数(pct_between)指定了一组精确的参数 )。name参数非常重要,每次调用make_agg_func时必须唯一。 它将最终用于重命名聚合列。

闭包是一个在其中包含一个函数(一个嵌套函数),并返回此嵌套函数的函数。 此嵌套函数必须引用外部函数范围内的变量才能成为闭包。 在此示例中,make_agg_func是外部函数,并返回嵌套函数wrapper,该函数从外部函数访问变量funcargskwargs

另见

检查分组对象

在数据帧上使用groupby方法的直接结果将是一个分组对象。 通常,我们将继续对该对象进行操作以进行聚合或转换,而无需将其保存到变量中。 在中,检查此分组对象的主要目的是检查单个组。

准备

在本秘籍中,我们通过直接在其上调用方法以及遍历其每个组来检查分组对象本身。

操作步骤

  1. 首先,将大学数据集中的州和宗教隶属关序列进行分组,然后将结果保存到变量中并确认其类型:
>>> college = pd.read_csv('data/college.csv')
>>> grouped = college.groupby(['STABBR', 'RELAFFIL'])
>>> type(grouped)
pandas.core.groupby.DataFrameGroupBy
  1. 使用dir函数发现其所有可用函数:
>>> print([attr for attr in dir(grouped) if not attr.startswith('_')])
['CITY', 'CURROPER', 'DISTANCEONLY', 'GRAD_DEBT_MDN_SUPP', 'HBCU', 'INSTNM', 'MD_EARN_WNE_P10', 'MENONLY', 'PCTFLOAN', 'PCTPELL', 'PPTUG_EF', 'RELAFFIL', 'SATMTMID', 'SATVRMID', 'STABBR', 'UG25ABV', 'UGDS', 'UGDS_2MOR', 'UGDS_AIAN', 'UGDS_ASIAN', 'UGDS_BLACK', 'UGDS_HISP', 'UGDS_NHPI', 'UGDS_NRA', 'UGDS_UNKN', 'UGDS_WHITE', 'WOMENONLY', 'agg', 'aggregate', 'all', 'any', 'apply', 'backfill', 'bfill', 'boxplot', 'corr', 'corrwith', 'count', 'cov', 'cumcount', 'cummax', 'cummin', 'cumprod', 'cumsum', 'describe', 'diff', 'dtypes', 'expanding', 'ffill', 'fillna', 'filter', 'first', 'get_group', 'groups', 'head', 'hist', 'idxmax', 'idxmin', 'indices', 'last', 'mad', 'max', 'mean', 'median', 'min', 'ndim', 'ngroup', 'ngroups', 'nth', 'nunique', 'ohlc', 'pad', 'pct_change', 'plot', 'prod', 'quantile', 'rank', 'resample', 'rolling', 'sem', 'shift', 'size', 'skew', 'std', 'sum', 'tail', 'take', 'transform', 'tshift', 'var']
  1. 查找具有ngroups属性的组数:
>>> grouped.ngroups
112
  1. 要查找每个组的唯一标识标签,请查看groups属性,该属性包含映射到该组的所有相应索引标签的每个唯一组的字典:
>>> groups = list(grouped.groups.keys())
>>> groups[:6]
[('AK', 0), ('AK', 1), ('AL', 0), ('AL', 1), ('AR', 0), ('AR', 1)]
  1. 通过将get_group方法传递给一个确切的组标签的元组来检索单个组。 例如,要获得佛罗里达州的所有宗教附属学校,请执行以下操作:
>>> grouped.get_group(('FL', 1)).head()

  1. 您可能想看看每个单独的组。 这是可能的,因为分组对象是可迭代的:
>>> from IPython.display import display
>>> for name, group in grouped:
        print(name)
        display(group.head(3))

  1. 您还可以在分组对象上调用head方法,以在单个数据帧中将每个组的第一行放在一起。
>>> grouped.head(2).head(6)

工作原理

步骤 1 正式创建了分组对象。 显示所有公共属性和方法以揭示所有可能的函数(如在步骤 2 中所做的那样)很有用。每个组由元组唯一标识,该元组包含分组列中值的唯一组合。 Pandas 允许您使用第 5 步中显示的get_group方法选择特定的组作为数据帧。

很少需要遍历整个组,通常,如果有必要,应避免这样做,因为这样做可能会很慢。 有时候,您别无选择。 当通过对象遍历分组时,将为您提供一个元组,其中包含组名和数据帧,而没有分组列。 在步骤 6 中,此元组在for循环中解包为变量namegroup

在遍历组时可以做的一件有趣的事情是直接在笔记本中显示每个组的几行。 为此,可以使用IPython.display模块中的打印函数或display函数。 使用print函数可得到纯文本格式的数据帧,而没有任何不错的 HTML 格式。 使用display函数将以其常规的易于阅读的格式生成数据帧。

更多

在步骤 2 的列表中没有探索几种有用的方法。例如nth方法,当给定一个整数列表时,该方法从每个组中选择那些特定的行。 例如,以下操作从每个组中选择第一行和最后一行:

>>> grouped.nth([1, -1]).head(8)

另见

筛选少数人群居多的州

在第 4 章,“选择数据子集”中,我们在过滤掉False行之前将每一行标记为TrueFalse。 以类似的方式,可以在过滤掉False组之前将整个数据组标记为TrueFalse。 为此,我们首先使用groupby方法形成组,然后应用filter方法。filter方法接受必须返回TrueFalse来指示是否保留组的函数。

在调用groupby方法之后应用的filter方法,与第 2 章“基本数据帧操作”中的数据帧filter方法完全不同。

准备

在此秘籍中,我们使用大学数据集查找非白人大学生比白人多的所有州。 由于这是来自美国的数据集,因此白人占多数,因此,我们正在寻找少数居多的州。

操作步骤

  1. 读取大学数据集,按州分组,并显示分组总数。 这应该等于从nunique序列方法检索的唯一状态数:
>>> college = pd.read_csv('data/college.csv', index_col='INSTNM')
>>> grouped = college.groupby('STABBR')
>>> grouped.ngroups
59

>>> college['STABBR'].nunique() # verifying the same number
59
  1. grouped变量具有filter方法,该方法接受一个自定义函数来确定是否保留组。 自定义函数将隐式传递给当前组的数据帧,并且需要返回一个布尔值。 我们定义一个函数来计算少数民族学生的总百分比,如果该百分比大于用户定义的阈值,则返回True
>>> def check_minority(df, threshold):
        minority_pct = 1 - df['UGDS_WHITE']
        total_minority = (df['UGDS'] * minority_pct).sum()
        total_ugds = df['UGDS'].sum()
        total_minority_pct = total_minority / total_ugds
        return total_minority_pct > threshold
  1. 使用check_minority函数传递的filter方法和 50% 的阈值来查找具有少数多数的所有状态:
>>> college_filtered = grouped.filter(check_minority, threshold=.5)
>>> college_filtered.head()

  1. 仅查看输出可能并不表示实际发生了什么。数据帧以状态亚利桑那(AZ)而不是阿拉斯加(AK)开头,因此我们可以从视觉上确认某些更改。 让我们将此过滤后的数据帧的shape与原始数据进行比较。 查看结果,大约 60% 的行已被过滤,仅剩下 20 个州占少数:
>>> college.shape
(7535, 26)

>>> college_filtered.shape
(3028, 26)

>>> college_filtered['STABBR'].nunique()
20

工作原理

此秘籍以州为单位查看所有机构的总人口。 目标是保留所有州中总体上占少数的所有行。 这要求我们按状态对数据进行分组,这是在步骤 1 中完成的。我们发现有 59 个独立的组。

filter分组方法将所有行保留在一个组中或将其过滤掉。 它不会更改列数。filter分组方法通过用户定义的函数(例如此秘籍中的check_minority)执行此关守。 要过滤的一个非常重要的方面是它将特定组的整个数据帧传递给用户定义的函数,并为每个组返回一个布尔值。

check_minority函数内部,首先计算每个机构的非白人学生的百分比和总数,然后找到所有学生的总数。 最后,根据给定的阈值检查整个州的非白人学生百分比,这会产生布尔值。

最终结果是一个数据帧,其列与原始列相同,但过滤掉了不符合阈值的状态中的行。 由于过滤后的数据帧的标题可能与原始标题相同,因此您需要进行一些检查以确保操作成功完成。 我们通过检查行数和唯一状态数来验证这一点。

更多

我们的函数check_minority是灵活的,并接受参数以降低或提高少数群体阈值的百分比。 让我们检查几个其他阈值的唯一状态的形状和数量:

>>> college_filtered_20 = grouped.filter(check_minority, threshold=.2)
>>> college_filtered_20.shape
(7461, 26)

>>> college_filtered_20['STABBR'].nunique()
57

>>> college_filtered_70 = grouped.filter(check_minority, threshold=.7)
>>> college_filtered_70.shape
(957, 26)

>>> college_filtered_70['STABBR'].nunique()
10

另见

转换减肥赌注

增加减肥动机的一种方法是与他人打赌。 此秘籍中的方案将跟踪四个月内两个人的减肥情况,并确定获胜者。

准备

在此秘籍中,我们使用来自两个人的模拟数据来跟踪四个月内减肥的百分比。 在每个月底,将根据当月体重百分比最高的个人宣布获胜者。 要跟踪减肥,我们将数据按月和人分组,然后调用transform方法以查找从月初起每周每周的减肥百分比。

操作步骤

  1. 读取原始weight_loss数据集,并检查两个人AmyBob的第一个月数据。 每月总共有四个称量:
>>> weight_loss = pd.read_csv('data/weight_loss.csv')
>>> weight_loss.query('Month == "Jan"')

  1. 要确定每个月的赢家,我们只需要比较每月第一周到最后一周的减肥效果即可。 但是,如果我们想每周更新一次,我们还可以计算从当前周到每月第一周的减肥。 让我们创建一个能够提供每周更新的函数:
>>> def find_perc_loss(s):
        return (s - s.iloc[0]) / s.iloc[0]
  1. 让我们在一月份为Bob测试此函数。
>>> bob_jan = weight_loss.query('Name=="Bob" and Month=="Jan"')
>>> find_perc_loss(bob_jan['Weight'])
0    0.000000
2   -0.010309
4   -0.027491
6   -0.027491
Name: Weight, dtype: float64

您应该忽略最后一个输出中的索引值。 0、2、4 和 6 只是引用数据帧的原始行标签,与星期无关。

  1. 第一周后,鲍勃减肥了 1% 。 他在第二周继续减肥,但在最后一周没有任何进展。 我们可以将此函数应用于人和周的每个单一组合,以获得相对于每月第一周的每周减肥。 为此,我们需要将数据按NameMonth分组,然后使用transform方法应用此自定义函数:
>>> pcnt_loss = weight_loss.groupby(['Name', 'Month'])['Weight'] \
                           .transform(find_perc_loss)
>>> pcnt_loss.head(8)
0    0.000000
1    0.000000
2   -0.010309
3   -0.040609
4   -0.027491
5   -0.040609
6   -0.027491
7   -0.035533
Name: Weight, dtype: float64
  1. transform方法必须返回与调用数据帧具有相同行数的对象。 让我们将此结果作为新列添加到原始数据帧中。 为了帮助缩短输出,我们将选择Bob的前两个月的数据:
>>> weight_loss['Perc Weight Loss'] = pcnt_loss.round(3)
>>> weight_loss.query('Name=="Bob" and Month in ["Jan", "Feb"]')

  1. 请注意,减肥百分比在新月后重新设置。 通过这个新的专栏,我们可以手动确定获胜者,但让我们看看是否可以找到一种自动执行此操作的方法。 由于唯一重要的一周是最后一周,所以我们选择第 4 周:
>>> week4 = weight_loss.query('Week == "Week 4"')
>>> week4 

  1. 这缩小了周数,但仍然不会自动找出每个月的赢家。 让我们使用pivot方法重塑此数据,以便BobAmy每月的减肥百分比并排:
>>> winner = week4.pivot(index='Month', columns='Name',
                         values='Perc Weight Loss')
>>> winner

  1. 此输出使每个月的获胜者更加清楚,但我们仍然可以走得更远。 NumPy 具有一个称为where的向量化if-then-else函数,该函数可以将序列或布尔数组映射到其他值。 让我们为获奖者的名字创建一个列,并突出显示每个月的获奖百分比:
>>> winner['Winner'] = np.where(winner['Amy'] < winner['Bob'],
                                'Amy', 'Bob')
>>> winner.style.highlight_min(axis=1)

  1. 使用value_counts方法以赢得的月份数返回最终分数:
>>> winner.Winner.value_counts()
Amy    3
Bob    1
Name: Winner, dtype: int64

工作原理

在整个秘籍中,query方法用于过滤数据,而不是布尔索引。 有关更多信息,请参阅第 5 章,“布尔索引”的“查询方法”秘籍,以提高布尔索引的可读性。

我们的目标是找到每个人每个月的减肥百分比。 一种完成此任务的方法是计算相对于每个月初的每周减肥。 此特定任务非常适合transform分组方法。transform方法接受一个函数作为其必需的参数。 该函数隐式地传递给每个非分组列(或仅使用在索引运算符中指定的列,如在此秘籍中使用Weight所做的那样)。 它必须返回与传递的组长度相同的值序列,否则将引发异常。 本质上,原始数据帧中的所有值都在转换。 没有聚集或过滤发生。

第 2 步创建一个函数,该函数从其所有值中减去传递的序列的第一个值,然后将该结果除以第一个值。 这将计算相对于第一个值的百分比损失(或收益)。 在第 3 步中,我们在一个月内对一个人测试了此函数。

在步骤 4 中,我们在人和周的每个组合上以相同的方式使用此函数。 从字面上看,我们正在将Weight列转换为当前一周的体重损失百分比。 为每个人输出第一个月的数据。 Pandas 将新数据作为序列返回。 该序列本身并没有什么用处,并且更有意义地作为新列附加到原始数据帧中。 我们在步骤 5 中完成此操作。

要确定获胜者,只需每月的第 4 周。 我们可以在这里停下来,手动确定获胜者,但 Pandas 提供了自动执行此功能的函数。 第 7 步中的pivot函数通过将一列的唯一值转换为新的列名称来重塑我们的数据集。index参数用于您不想旋转的列。 传递给values参数的列将平铺在indexcolumns参数中列的每个唯一组合上。

只有在indexcolumns参数中的列的每种唯一组合仅出现一次时,pivot方法才有效。 如果唯一的组合不止一个,则会引发异常。 在这种情况下,您可以使用pivot_table方法,该方法允许您将多个值聚合在一起。

枢纽化之后,我们利用高效且快速的 NumPy where函数,该函数的第一个参数是产生布尔序列的条件。True值映射到AmyFalse值映射到Bob。我们突出显示每个月的获胜者,并使用value_counts方法统计最终得分。

更多

看一下第 7 步中的数据帧输出。您是否注意到月份是按字母顺序而不是按时间顺序排列的? 不幸的是,至少在这种情况下,Pandas 按字母顺序为我们排序了几个月。 我们可以通过将Month的数据类型更改为分类变量来解决此问题。 分类变量将每列的所有值映射为一个整数。 我们可以选择此映射为月份的正常时间顺序。 Pandas 在pivot方法期间使用此基础整数映射按时间顺序排列月份:

>>> week4a = week4.copy()
>>> month_chron = week4a['Month'].unique() # or use drop_duplicates
>>> month_chron
array(['Jan', 'Feb', 'Mar', 'Apr'], dtype=object)

>>> week4a['Month'] = pd.Categorical(week4a['Month'],
                                     categories=month_chron,
                                     ordered=True)
>>> week4a.pivot(index='Month', columns='Name',
                 values='Perc Weight Loss')

要转换Month列,请使用Categorical构造器。 将原始列作为序列传递,并将所有类别的唯一序列按所需顺序传递给categories参数。 由于Month列已经按时间顺序排列,因此我们可以简单地使用unique方法,该方法保留了获取所需数组的顺序。 通常,要按字母顺序以外的其他方式对对象数据类型的列进行排序,请将其转换为类别。

另见

计算每个州的 SAT 加权平均成绩

分组对象具有四个接受一个或多个函数以对每个组执行计算的方法。 这四种方法是aggfiltertransformapply。 这些方法的前三个方法中的每个方法都有一个非常特定的输出,函数必须返回该输出。agg必须返回标量值,filter必须返回布尔值,transform必须返回与传递的组长度相同的序列。 但是,apply方法可能返回标量值,序列或什至任何形状的数据帧,因此使其非常灵活。 每个组也仅将其称为 ,这与对每个非分组列调用一次的transformagg形成对比。apply方法能够同时对多个列进行操作时返回单个对象的能力,使得此秘籍中的计算成为可能。

准备

在此秘籍中,我们从大学数据集中计算每个州的数学和口头 SAT 分数的加权平均值。 我们根据每个学校的本科生人数对分数进行加权。

操作步骤

  1. 读取大学数据集,并在UGDSSATMTMIDSATVRMID列中删除所有缺少值的行。 这三列中的每一列都必须具有非缺失值:
>>> college = pd.read_csv('data/college.csv')
>>> subset = ['UGDS', 'SATMTMID', 'SATVRMID']
>>> college2 = college.dropna(subset=subset)
>>> college.shape
(7535, 27)

>>> college2.shape
(1184, 27)
  1. 绝大多数机构没有我们三个必填列的数据,但这仍然足够继续。 接下来,创建一个用户定义的函数以仅计算 SAT 数学分数的加权平均值:
>>> def weighted_math_average(df):
        weighted_math = df['UGDS'] * df['SATMTMID']
        return int(weighted_math.sum() / df['UGDS'].sum())
  1. 按状态分组,然后将此函数传递给apply方法:
>>> college2.groupby('STABBR').apply(weighted_math_average).head()
STABBR
AK    503
AL    536
AR    529
AZ    569
CA    564
dtype: int64
  1. 我们成功为每个组返回了一个标量值。 让我们绕个小弯路,将相同的函数传递给agg方法,看看结果如何:
>>> college2.groupby('STABBR').agg(weighted_math_average).head()

  1. weighted_math_average函数将应用于数据帧中的每个非聚合列。 如果尝试将列限制为SATMTMID,则将出现错误,因为您将无法访问UGDS。 因此,完成对多列操作的最佳方法是使用apply
>>> college2.groupby('STABBR')['SATMTMID'] \
            .agg(weighted_math_average)
KeyError: 'UGDS'
  1. apply的一个不错的功能是您可以通过返回一个序列来创建多个新列。 此返回的序列的索引将是新的列名。 让我们修改一下函数,以计算两个 SAT 分数的加权平均值和算术平均值,以及每个组中机构数量的计数。 我们以序列返回以下五个值:
>>> from collections import OrderedDict
>>> def weighted_average(df):
        data = OrderedDict()
        weight_m = df['UGDS'] * df['SATMTMID']
        weight_v = df['UGDS'] * df['SATVRMID']

        wm_avg = weight_m.sum() / df['UGDS'].sum()
        wv_avg = weight_v.sum() / df['UGDS'].sum()

        data['weighted_math_avg'] = wm_avg
        data['weighted_verbal_avg'] = wv_avg
        data['math_avg'] = df['SATMTMID'].mean()
        data['verbal_avg'] = df['SATVRMID'].mean()
        data['count'] = len(df)
        return pd.Series(data, dtype='int')

>>> college2.groupby('STABBR').apply(weighted_average).head(10)

工作原理

为了正确完成此秘籍,我们需要首先过滤没有UGDSSATMTMIDSATVRMID值缺失的机构。 默认情况下,dropna方法删除具有一个或多个缺失值的行。 我们必须使用subset参数来限制其查找缺少值的列。

在第 2 步中,我们定义一个仅计算SATMTMID列的加权平均值的函数。 加权平均值与算术平均值的不同之处在于,每个值都乘以一定的权重。 然后将这个数量相加并除以权重之和。 在这种情况下,我们的体重就是在校学生人数。

在第 3 步中,我们将此函数传递给apply方法。 我们的函数weighted_math_average传递了每个组所有原始列的数据帧。 它返回单个标量值,即SATMTMID的加权平均值。 此时,您可能认为可以使用agg方法进行此计算。 用agg直接替换apply不起作用,因为agg返回其每个聚合列的值。

实际上,可以通过预先计算UGDSSATMTMID的乘法来间接使用agg

步骤 6 确实显示了apply的多功能性。 我们构建了一个新函数,该函数计算两个 SAT 列的加权平均值和算术平均值以及每个组的行数。 为了使apply创建多个列,您必须返回一个序列。 索引值用作结果数据帧中的列名。 您可以使用此方法返回任意多个值。

请注意,OrderedDict类是从collections模块导入的,该模块是标准库的一部分。 该有序字典用于存储数据。 普通的 Python 字典不能用来存储数据,因为它不保留插入顺序。

构造器pd.Series确实具有一个索引参数,您可以使用它来指定顺序,但是使用OrderedDict会更干净。

更多

在此秘籍中,我们为每个组返回一行作为序列。 通过返回数据帧,可以为每个组返回任意数量的行和列。 除了查找算术和加权均值之外,我们还查找两个 SAT 列的几何和谐波均值,然后将结果作为数据帧返回,其中数据行是均值类型的名称,列是 SAT 类型。 为了减轻我们的负担,我们使用 NumPy 函数average来计算加权平均值,并使用 SciPy 函数gmeanhmean来计算几何和调和平均值:

>>> from scipy.stats import gmean, hmean
>>> def calculate_means(df):
        df_means = pd.DataFrame(index=['Arithmetic', 'Weighted',
                                       'Geometric', 'Harmonic'])
        cols = ['SATMTMID', 'SATVRMID']
        for col in cols:
            arithmetic = df[col].mean()
            weighted = np.average(df[col], weights=df['UGDS'])
            geometric = gmean(df[col])
            harmonic = hmean(df[col])
            df_means[col] = [arithmetic, weighted,
                             geometric, harmonic]

        df_means['count'] = len(df)
        return df_means.astype(int)

>>> college2.groupby('STABBR').apply(calculate_means).head(12)

另见

按连续变量分组

在对 Pandas 进行分组时,通常使用具有离散重复值的列。 如果没有重复的值,则分组将毫无意义,因为每个组只有一行。 连续数字列通常具有很少的重复值,并且通常不用于形成组。 但是,如果我们可以将具有连续值的列转换为离散列,方法是将每个值放入一个桶中,四舍五入或使用其他映射,则将它们分组是有意义的。

准备

在此秘籍中,我们探索航班数据集以发现不同旅行距离的航空公司分布。 例如,这使我们能够找到在 500 到 1,000 英里之间飞行最多的航空公司。 为此,我们使用 Pandascut函数离散化每次飞行的距离。

操作步骤

  1. 读取航班数据集,并输出前五行:
>>> flights = pd.read_csv('data/flights.csv')
>>> flights.head()

  1. 如果要查找一定距离范围内的航空公司分布,则需要将DIST列的值放入离散的桶中。 让我们使用 pandas cut函数将数据分为五个桶:
>>> bins = [-np.inf, 200, 500, 1000, 2000, np.inf]
>>> cuts = pd.cut(flights['DIST'], bins=bins)
>>> cuts.head()
0     (500.0, 1000.0]
1    (1000.0, 2000.0]
2     (500.0, 1000.0]
3    (1000.0, 2000.0]
4    (1000.0, 2000.0]
Name: DIST, dtype: category
Categories (5, interval[float64]): [(-inf, 200.0] < (200.0, 500.0] < (500.0, 1000.0] < (1000.0, 2000.0] < (2000.0, inf]]
  1. 创建有序的分类序列。 为了帮助您了解发生了什么,让我们计算每个类别的值:
>>> cuts.value_counts()
(500.0, 1000.0]     20659
(200.0, 500.0]      15874
(1000.0, 2000.0]    14186
(2000.0, inf]        4054
(-inf, 200.0]        3719
Name: DIST, dtype: int64
  1. cuts序列现在可以用于形成组。 Pandas 允许您以任何希望的方式来分组。 将cuts序列传递到groupby方法,然后在AIRLINE列上调用value_counts方法以查找每个距离组的分布。 请注意,SkyWest(OO)组成了少于 200 英里的航班的 33%,但仅占 200 到 500 英里之间的航班的 16%:
>>> flights.groupby(cuts)['AIRLINE'].value_counts(normalize=True) \
                                    .round(3).head(15)
DIST            AIRLINE
(-inf, 200.0]   OO         0.326
                EV         0.289
                MQ         0.211
                DL         0.086
                AA         0.052
                UA         0.027
                WN         0.009
(200.0, 500.0]  WN         0.194
                DL         0.189
                OO         0.159
                EV         0.156
                MQ         0.100
                AA         0.071
                UA         0.062
                VX         0.028
Name: AIRLINE, dtype: float64

工作原理

在步骤 2 中,cut函数将DIST列的每个值放入五个仓位之一。 箱由定义边缘的六个数字序列创建。 您总是需要比容器数多一个边缘。 您可以为bins参数传递一个整数,该整数将自动创建该数目的等宽槽。 NumPy 中提供了负无穷大对象和正无穷大对象,并确保将所有值放置在桶中。 如果您的值在箱边缘之外,则将使它们丢失并且不会放置在箱中。

cuts变量现在是五个有序类别的序列。 它具有所有常规的序列方法,在步骤 3 中,使用value_counts方法来了解其分布。

非常有趣的是,pandas 允许您将groupby方法传递给任何对象。 这意味着您可以从与当前数据帧完全无关的内容中形成组。 在这里,我们将cuts变量中的值分组。 对于每个分组,我们通过将normalize设置为True,以value_counts查找每个航空公司的航班百分比。

从这个结果可以得出一些有趣的见解。 从全部结果来看,SkyWest 是领先的航空公司,飞行距离不到 200 英里,但没有超过 2,000 英里的航班。 相比之下,美国航空公司在 200 英里以下的航班中排名第五,但到目前为止,在 1,000 到 2,000 英里之间的航班最多。

更多

当按cuts变量分组时,我们可以找到更多结果。 例如,我们可以为每个距离分组找到第 25、50 和 75% 的通话时间。 由于通话时间以分钟为单位,因此我们可以除以 60 得到小时:

>>> flights.groupby(cuts)['AIR_TIME'].quantile(q=[.25, .5, .75]) \
                                     .div(60).round(2)
DIST                  
(-inf, 200.0]     0.25    0.43
                  0.50    0.50
                  0.75    0.57
(200.0, 500.0]    0.25    0.77
                  0.50    0.92
                  0.75    1.05
(500.0, 1000.0]   0.25    1.43
                  0.50    1.65
                  0.75    1.92
(1000.0, 2000.0]  0.25    2.50
                  0.50    2.93
                  0.75    3.40
(2000.0, inf]     0.25    4.30
                  0.50    4.70
                  0.75    5.03
Name: AIR_TIME, dtype: float64

当使用cut函数时,我们可以使用此信息来创建内容丰富的字符串标签。 这些标签代替了间隔符号。 我们还可以链接unstack方法,该方法将内部索引级别转换为列名称:

>>> labels=['Under an Hour', '1 Hour', '1-2 Hours',
            '2-4 Hours', '4+ Hours']
>>> cuts2 = pd.cut(flights['DIST'], bins=bins, labels=labels)
>>> flights.groupby(cuts2)['AIRLINE'].value_counts(normalize=True) \
                                     .round(3) \
                                     .unstack() \
                                     .style.highlight_max(axis=1)

另见

计算城市之间的航班总数

在航班数据集中,我们具有始发地和目的地机场的数据。 例如,计算从休斯敦出发并降落在亚特兰大的航班数量是微不足道的。 更困难的是计算两个城市之间的航班总数,而不管始发地或目的地是哪一个。

准备

在此秘籍中,我们计算两个城市之间的航班总数,而不管始发地或目的地是哪个。 为此,我们按字母顺序对始发和目的地机场进行排序,以使机场的每种组合始终以相同的顺序出现。 然后,我们可以使用这种新的列安排来形成组,然后进行计数。

操作步骤

  1. 读取航班数据集,并找到每个始发地与目的地机场之间的航班总数:
>>> flights = pd.read_csv('data/flights.csv')
>>> flights_ct = flights.groupby(['ORG_AIR', 'DEST_AIR']).size()
>>> flights_ct.head()
ORG_AIR  DEST_AIR
ATL      ABE         31
         ABQ         16
         ABY         19
         ACY          6
         AEX         40
dtype: int64
  1. 选择在两个方向上的休斯顿(IAH)和亚特兰大(ATL)之间的航班总数:
>>> flights_ct.loc[[('ATL', 'IAH'), ('IAH', 'ATL')]]
ORG_AIR  DEST_AIR
ATL      IAH         121
IAH      ATL         148
dtype: int64
  1. 我们可以简单地将这两个数字相加得出城市之间的总航班,但是有一种更有效,更自动化的解决方案可以适用于所有航班。 让我们按照字母顺序对每一行的起点和终点城市进行独立排序:
>>> flights_sort = flights[['ORG_AIR', 'DEST_AIR']] \
                          .apply(sorted, axis=1)
>>> flights_sort.head()

  1. 现在,每行都已独立排序,列名不正确。 让我们将其重命名为更通用的名称,然后再次找到所有城市之间的航班总数:
>>> rename_dict = {'ORG_AIR':'AIR1', 'DEST_AIR':'AIR2'}
>>> flights_sort = flights_sort.rename(columns=rename_dict)
>>> flights_ct2 = flights_sort.groupby(['AIR1', 'AIR2']).size()
>>> flights_ct2.head()
AIR1  AIR2
ABE   ATL     31
      ORD     24
ABI   DFW     74
ABQ   ATL     16
      DEN     46
dtype: int64
  1. 让我们选择亚特兰大和休斯顿之间的所有航班,并验证其是否与步骤 2 中的值之和匹配:
>>> flights_ct2.loc[('ATL', 'IAH')]
269
  1. 如果我们尝试选择休斯顿和亚特兰大的航班,则会出现错误:
>>> flights_ct2.loc[('IAH', 'ATL')]
IndexingError: Too many indexers

工作原理

在第 1 步中,我们按起点和目的地机场列形成分组,然后将size方法应用于分组对象,该对象仅返回每个组的总行数。 请注意,我们可以将字符串size传递给agg方法以获得相同的结果。 在步骤 2 中,选择了亚特兰大和休斯顿之间每个方向的航班总数。 序列flights_count具有两个级别的多重索引。 从多重索引中选择行的一种方法是将loc索引运算符传递给精确级别值的元组。 在这里,我们实际上选择了两个不同的行('ATL', 'HOU')('HOU', 'ATL')。 我们使用元组列表来正确执行此操作。

步骤 3 是秘籍中最相关的步骤。 对于亚特兰大和休斯顿之间的所有航班,我们只希望有一个标签,到目前为止,我们有两个标签。 如果我们按字母顺序对出发地和目的地机场的每种组合进行排序,那么我们将为机场之间的航班使用一个标签。 为此,我们使用数据帧的apply方法。 这与分组的apply方法不同。 在步骤 3 中没有形成组。

必须向数据帧的apply方法传递一个函数。 在这种情况下,它是内置的sorted函数。 默认情况下,此函数作为序列应用于每个列。 我们可以使用axis=1(或axis='index')来改变计算方向。sorted函数将每行数据隐式地作为序列传递给它。 它返回已排序的机场代码的列表。 这是将第一行作为序列传递给排序函数的示例:

>>> sorted(flights.loc[0, ['ORG_AIR', 'DEST_AIR']])
['LAX', 'SLC']

apply方法以这种确切的方式使用sorted遍历所有行。 完成此操作后,将对每一行进行独立排序。 列名现在已无意义。 我们在下一步中对列名称进行重命名,然后执行与步骤 2 中相同的分组和汇总。这次,亚特兰大和休斯顿之间的所有航班都属于同一标签。

更多

您可能想知道为什么我们不能使用更简单的sort_values序列方法。 此方法不是独立进行排序,而是将行或列保留为单个记录,就像在进行数据分析时所期望的那样。 步骤 3 是非常昂贵的操作,需要几秒钟才能完成。 只有大约 60,000 行,因此该解决方案无法很好地扩展到更大的数据。

步骤 3 是非常昂贵的操作,需要几秒钟才能完成。 只有大约 60,000 行,因此该解决方案无法很好地扩展到更大的数据。 在所有 Pandas 中,用axis=1调用apply方法是性能最低的操作之一。 在内部,Pandas 在每行上循环,不会因 NumPy 提供任何速度提升。 如果可能,请避免将applyaxis=1一起使用。

使用 NumPy sort函数可以大大提高速度。 让我们继续使用此函数并分析其输出。 默认情况下,它将对每一行进行独立排序:

>>> data_sorted = np.sort(flights[['ORG_AIR', 'DEST_AIR']])
>>> data_sorted[:10]
array([['LAX', 'SLC'],
       ['DEN', 'IAD'],
       ['DFW', 'VPS'],
       ['DCA', 'DFW'],
       ['LAX', 'MCI'],
       ['IAH', 'SAN'],
       ['DFW', 'MSY'],
       ['PHX', 'SFO'],
       ['ORD', 'STL'],
       ['IAH', 'SJC']], dtype=object)

返回二维 NumPy 数组。 NumPy 并不容易进行分组操作,因此让我们使用数据帧构造器创建一个新的数据帧并检查它是否等于步骤 3 中的flights_sorted数据帧:

>>> flights_sort2 = pd.DataFrame(data_sorted, columns=['AIR1', 'AIR2'])
>>> fs_orig = flights_sort.rename(columns={'ORG_AIR':'AIR1',
                                           'DEST_AIR':'AIR2'})
>>> flights_sort2.equals(fs_orig)
True

由于数据帧相同,因此您可以将第 3 步替换为先前的更快排序例程。 我们来计时一下每种不同的排序方法之间的区别:

>>> %%timeit 
>>> flights_sort = flights[['ORG_AIR', 'DEST_AIR']] \
                          .apply(sorted, axis=1)
7.41 s ± 189 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

>>> %%timeit
>>> data_sorted = np.sort(flights[['ORG_AIR', 'DEST_AIR']])
>>> flights_sort2 = pd.DataFrame(data_sorted,
                                 columns=['AIR1', 'AIR2'])
10.6 ms ± 453 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

NumPy 解决方案的速度比对 Pandas 使用apply快 700 倍。

另见

寻找最长的准时航班

对于航空公司而言,最重要的指标之一是其准时飞行表现。 美国联邦航空管理局认为,航班在比原定抵达时间至少晚 15 分钟后才抵达。 Pandas 有直接的方法来计算每个航空公司的准时航班总数和百分比。 尽管这些基本摘要统计数据是一个重要的指标,但是还有其他一些重要的计算很有趣,例如,找出每个航空公司在其始发机场的连续准点飞行时间。

准备

在此秘籍中,我们找到了每个始发机场的每家航空公司的最长连续航班准点率。 这要求列中的每个值都必须知道紧随其后的值。 为了将条纹应用到每个组之前,我们巧妙地使用了diffcumsum方法来发现条纹。

操作步骤

  1. 在开始实际的航班数据集之前,让我们练习计算带有少量样本序列的航班的条纹:
>>> s = pd.Series([0, 1, 1, 0, 1, 1, 1, 0])
>>> s
0    0
1    1
2    1
3    0
4    1
5    1
6    1
7    0
dtype: int64
  1. 我们对 1 的条纹的最终表示将是与原始序列相同长度的序列,每个条纹从 1 开始独立计数。 首先,我们使用cumsum方法:
>>> s1 = s.cumsum()
>>> s1
0    0
1    1
2    2
3    2
4    3
5    4
6    5
7    5
dtype: int64
  1. 现在,我们已经积累了序列中的所有值。 让我们将此序列乘以原始序列:
>>> s.mul(s1)
0    0
1    1
2    2
3    0
4    3
5    4
6    5
7    0
dtype: int64
  1. 我们最初只有一个非零值。 这个结果非常接近我们的期望。 我们只需要重新开始每个连胜,而不是从累加的总和开始。 让我们链接diff方法,该方法从当前值中减去前一个值:
>>> s.mul(s1).diff()
0    NaN
1    1.0
2    1.0
3   -2.0
4    3.0
5    1.0
6    1.0
7   -5.0
dtype: float64
  1. 负值表示条纹结束。 我们需要将负值向下传播到序列上,并使用它们减去步骤 2 中多余的累加。为此,我们将使用where方法使所有非负值都丢失:
>>> s.mul(s1).diff().where(lambda x: x < 0)
0    NaN
1    NaN
2    NaN
3   -2.0
4    NaN
5    NaN
6    NaN
7   -5.0
dtype: float64
  1. 现在,我们可以使用ffill方法向下传播这些值:
>>> s.mul(s1).diff().where(lambda x: x < 0).ffill()
0    NaN
1    NaN
2    NaN
3   -2.0
4   -2.0
5   -2.0
6   -2.0
7   -5.0
dtype: float64
  1. 最后,我们可以将此序列添加回s1,以清除多余的累积量:
>>> s.mul(s1).diff().where(lambda x: x < 0).ffill() \
     .add(s1, fill_value=0)
0    0.0
1    1.0
2    2.0
3    0.0
4    1.0
5    2.0
6    3.0
7    0.0
dtype: float64
  1. 现在我们有了一个连续工作的条纹查找器,我们可以找到每个航空公司和始发机场最长的条纹。 让我们读入航班数据集并创建一列以表示准时到达:
>>> flights = pd.read_csv('data/flights.csv')
>>> flights['ON_TIME'] = flights['ARR_DELAY'].lt(15).astype(int)
>>> flights[['AIRLINE', 'ORG_AIR', 'ON_TIME']].head(10)

  1. 使用前七个步骤中的逻辑来定义一个函数,该函数返回给定序列的最大连胜数:
>>> def max_streak(s):
        s1 = s.cumsum()
        return s.mul(s1).diff().where(lambda x: x < 0) \
                .ffill().add(s1, fill_value=0).max()
  1. 找出每个航空公司和始发机场的最大准点到达率,以及航班总数和准点到达率。 首先,对一年中的日期和预定的出发时间进行排序:
>>> flights.sort_values(['MONTH', 'DAY', 'SCHED_DEP']) \
           .groupby(['AIRLINE', 'ORG_AIR'])['ON_TIME'] \
           .agg(['mean', 'size', max_streak]).round(2).head()

工作原理

在 Pandas 中查找数据中的条纹并不是一项简单的操作,需要先行或后行的方法,例如diffshift,或记住当前状态的方法,例如cumsum。 前七个步骤的最终结果是序列的长度与原始序列的长度相同,可以跟踪所有连续的序列。 在这些步骤中,我们使用muladd方法代替它们的等效运算符(*)和(+)。 我认为,这样可以使计算从左到右的过程更加简洁。 您当然可以将它们替换为实际的运算符。

理想情况下,我们希望告诉 Pandas 在每个条纹开始时都应用cumsum方法,并在每个条纹结束后重新设置自身。 要将此信息传达给 Pandas,需要采取许多步骤。 第 2 步将整个序列中的所有结果累积起来。 其余步骤将慢慢清除所有多余的累积。 为了识别这种多余的累积,我们需要找到每个条纹的末尾并从下一个条纹的开始减去该值。

要找到每个条纹的结尾,请通过在步骤 3 中将s1乘以原始零序列和 1 来巧妙地使所有值不属于条纹零。 条纹。 很好,但是同样,我们需要消除多余的累积。 知道条纹结束的地方并不能使我们到达那里。

在第 4 步中,我们使用diff方法来查找此多余部分。diff方法获取当前值与位于距离其一定行数的任何值之间的差。 默认情况下,返回当前值与前一个值之间的差。

在步骤 4 中,只有负值才有意义。那些是连续结束后的值。 这些值需要向下传播,直到后续条纹结束。 为了消除(丢失)所有我们不关心的值,我们使用where方法,该方法采用与调用序列大小相同的条件序列。 默认情况下,所有True值保持不变,而False值丢失。where方法允许您通过将函数作为第一个参数来将调用序列用作条件的一部分。 使用一个匿名函数,该函数隐式传递给调用序列,并检查每个值是否小于零。 第 5 步的结果是一个序列,其中仅保留负值,其余更改为缺失值。

步骤 6 中的ffill方法将缺失值替换为在序列中前进/后退的最后一个非缺失值。 由于前三个值不跟随非缺失值,因此它们仍然丢失。 我们终于有了消除多余积蓄的序列。 我们将累加序列添加到步骤 6 的结果中,以使条纹全部从零开始。add方法允许我们用fill_value参数替换缺少的值。 这样就完成了在数据集中查找条纹的过程。 当执行这样的复杂逻辑时,最好使用一个小的数据集,在此您可以知道最终的输出是什么。 从第 8 步开始并在分组时建立这种寻路逻辑将是非常困难的任务。

在步骤 8 中,我们创建ON_TIME列。 值得注意的一项是,已取消的排期缺少ARR_DELAY的值,该值未通过布尔条件,因此ON_TIME列的值为零。 取消的航班与延迟的航班一样。

第 9 步将我们的逻辑从前七个步骤转变为一个函数,并链接max方法以返回最长的条纹。 由于我们的函数返回单个值,因此它正式是一个聚合函数,可以按照步骤 10 的操作传递给agg方法。为确保我们正在查看实际的连续航班,我们使用sort_values方法按日期和预定的出发时间进行排序。

更多

既然我们找到了准点到达时间最长的条纹,我们可以轻松地找到相反的地方-延迟到达的最长条纹。 以下函数为传递给它的每个组返回两行。 第一行是条纹的起点,最后一行是条纹的终点。 每行包含开始/结束条纹的月份和日期,以及条纹的总长度:

>>> def max_delay_streak(df):
        df = df.reset_index(drop=True)
        s = 1 - df['ON_TIME']
        s1 = s.cumsum()
        streak = s.mul(s1).diff().where(lambda x: x < 0) \
                  .ffill().add(s1, fill_value=0)
        last_idx = streak.idxmax()
        first_idx = last_idx - streak.max() + 1
        df_return = df.loc[[first_idx, last_idx], ['MONTH', 'DAY']]
        df_return['streak'] = streak.max()
        df_return.index = ['first', 'last']
        df_return.index.name='type'
        return df_return

>>> flights.sort_values(['MONTH', 'DAY', 'SCHED_DEP']) \
           .groupby(['AIRLINE', 'ORG_AIR']) \
           .apply(max_delay_streak) \
           .sort_values('streak', ascending=False).head(10)

当我们使用分组的apply方法时,每个组的数据帧都传递给max_delay_streak函数。 在此函数内部,删除了数据帧的索引并用RangeIndex代替,以便我们轻松找到条纹的第一行和最后一行。 反转ON_TIME列,然后使用相同的逻辑查找延迟飞行的条纹。 条纹的第一行和最后一行的索引存储为变量。 然后,这些索引用于选择条纹结束的月份和日期。 我们使用数据帧返回结果。 我们标记并命名索引以使最终结果更清晰。

我们的最终结果显示了最长的延迟条纹以及第一和最后一个日期。 让我们进行调查,看看是否可以找出导致这些延迟的原因。 天气恶劣是航班延误或取消的常见原因。 从第一行开始,美国航空(AA)从达拉斯沃思堡(DFW)机场开始拥有连续 38 班延误航班,从 2015 年 2 月 26 日至 2015 年 3 月 1 日。查看 2015 年 2 月 27 日的历史天气数据 ,降雪量为 2 英寸,这是当天的记录这是 DFW 的主要天气事件,并给整个城市造成了严重问题。 请注意,DFW 出现了第三次最长的连胜纪录,但这次是几天前,并且是另一家航空公司。

另见

八、将数据重组为整齐的表格

在本章中,我们将介绍以下主题:

  • 使用stack将变量值整理为列名
  • 使用melt将变量值整理为列名
  • 同时堆叠多组变量
  • 反转堆叠数据
  • groupby聚合后解除堆叠
  • 使用用groupby聚合复制pivot_table
  • 重命名轴级别以方便重塑
  • 将多个变量存储为列名时进行整理
  • 将多个变量存储为列值时进行整理
  • 在同一单元格中存储两个或多个值时进行整理
  • 在列名和值中存储变量时进行整理
  • 将多个观测单位存储在同一表中时进行整理

介绍

前几章中使用的所有数据集都没有做太多或做任何工作来更改其结构。 我们立即开始以原始形状处理数据集。 在开始更详细的分析之前,许多野外的数据集将需要大量的重组。 在某些情况下,整个项目可能只关心格式化数据,以便其他人可以轻松处理它。

有许多术语用于描述数据重组的过程,其中整齐的数据是数据科学家最常用的。 整洁的数据是 Hadley Wickham 创造的一个术语,用于描述使分析变得容易进行的数据形式。 本章将涵盖 Hadley 提出的许多想法以及如何用 Pandas 来实现它们。 要了解有关整理数据的更多信息,请阅读 Hadley 的论文

什么是整洁的数据? Hadley 提出了三个简单的指导原则来确定数据集是否整洁:

  • 每个变量组成一列
  • 每个观测结果排成一行
  • 每种观测单位组成一个表格

任何不符合这些准则的数据集都被认为是混乱的。 一旦开始将数据重组为整齐的格式,此定义将变得更有意义,但是现在,我们需要知道什么是变量,观测值和观测单位。

要获得关于变量实际含义的直觉,最好考虑一下变量名称和变量值之间的区别。 变量名称是标签,例如性别,种族,薪水和职位。 变量值是指每次观察都可能发生变化的事物,例如性别中的男性/女性或种族中的白色/黑色。 单个观测值是单个观测单位的所有变量值的集合。 为了帮助了解观察单位可能是什么,请考虑零售商店,该商店具有有关每个交易,员工,客户,物品和商店本身的数据。 这些中的每一个都可以视为观察单位,并且需要自己的表格。 将员工信息(例如,工作时间)与客户信息(例如,花费的金额)组合在同一张表中,将破坏这一整洁的原则。

解决杂乱数据的第一步是在存在杂乱数据时对其进行识别,并且存在无限可能。 Hadley 明确提到了五种最常见的混乱数据类型:

  • 列名是值,不是变量名
  • 多个变量存储在列名中
  • 变量存储在行和列中
  • 多种观测单位存储在同一表中
  • 一个观测单位存储在多个表中

重要的是要了解,整理数据通常不涉及更改数据集的值,填写缺失的值或进行任何类型的分析。 整理数据涉及更改数据的形状或结构以符合整理原则。 整洁的数据类似于将所有工具都放在工具箱中,而不是随机散布在整个房屋中。 在工具箱中正确放置工具可以轻松完成所有其他任务。 数据格式正确后,进行进一步分析变得容易得多。

一旦发现混乱的数据,您将使用 Pandas 工具来重组数据,使数据整洁。 Pandas 提供给您的主要整洁工具是数据帧方法stackmeltunstackpivot。 较复杂的整理工作涉及撕裂文本,这需要str访问器。 其他辅助方法,例如renamerename_axisreset_indexset_index,将有助于对整洁的数据进行最终处理。

使用stack将变量值整理为列名

为了帮助理解整洁数据和混乱数据之间的差异,让我们看一下一个简单的表格,该表格可能是也可能不是整齐的:

>>> state_fruit = pd.read_csv('data/state_fruit.csv', index_col=0)
>>> state_fruit

该表似乎没有任何混乱,并且该信息很容易消耗。 但是,按照整洁的原则,它实际上并不是整洁的。 每个列名称实际上是变量的值。 实际上,数据帧中甚至都没有变量名。 将凌乱的数据集转换为整洁的数据的第一步之一就是识别所有变量。 在此特定数据集中,我们具有水果的变量。 在问题的背景下,还没有找到任何数字数据。 我们可以将此变量标记为权重或其他任何明智的名称。

准备

这个特定的混乱数据集包含变量值作为列名。 我们将需要将这些列名称转换为列值。 在本秘籍中,我们使用stack方法将数据帧重组为整齐的形式。

操作步骤

  1. 首先,请注意,状态名称位于数据帧的索引中。 这些状态正确地垂直放置,不需要重组。 问题是列名。stack方法采用所有列名,并将其整形为垂直,作为单个索引级别:
>>> state_fruit.stack()
Texas    Apple      12
         Orange     10
         Banana     40
Arizona  Apple       9
         Orange      7
         Banana     12
Florida  Apple       0
         Orange     14
         Banana    190
dtype: int64
  1. 注意,我们现在有了一个带有多重索引的序列。 现在索引中有两个级别。 原始索引已被推到左侧,以便为旧的列名腾出空间。 使用这一命令,我们现在基本上有了整洁的数据。 每个变量,状态,水果和重量都是垂直的。 让我们使用reset_index方法将结果转换为数据帧:
>>> state_fruit_tidy = state_fruit.stack().reset_index()
>>> state_fruit_tidy

  1. 现在我们的结构是正确的,但是列名没有意义。 让我们用适当的标识符替换它们:
>>> state_fruit_tidy.columns = ['state', 'fruit', 'weight']
>>> state_fruit_tidy

  1. 可以直接使用鲜为人知的序列方法rename_axis来设置索引级别的名称,而不是直接更改columns属性,然后再使用reset_index
>>> state_fruit.stack()\
               .rename_axis(['state', 'fruit'])

state    fruit 
Texas    Apple      12
         Orange     10
         Banana     40
Arizona  Apple       9
         Orange      7
         Banana     12
Florida  Apple       0
         Orange     14
         Banana    190
dtype: int64
  1. 从这里,我们可以简单地将reset_index方法与name参数链接起来,以重现步骤 3 的输出:
>>> state_fruit.stack()\
               .rename_axis(['state', 'fruit'])\
               .reset_index(name='weight')

工作原理

stack方法功能强大,需要花费一些时间才能完全理解和欣赏。 它接受所有列名并转置它们,因此它们成为新的最里面的索引级别。 请注意,每个旧列名称仍如何通过与每个状态配对来标记其原始值。3 x 3数据帧中有 9 个原始值,这些值被转换为具有相同数量值的单个序列。 原始的第一行数据成为结果序列中的前三个值。

在步骤 2 中重置索引后,pandas 将我们的数据帧的列默认设置为level_0level_10。 这是因为调用此方法的序列具有两个未正式命名的索引级别。 Pandas 还从外部从零开始按整数引用索引。

步骤 3 显示了一种重命名列的简单直观的方法。 您可以通过将columns属性设置为等于列表来简单地为整个数据帧设置新列。

或者,可以通过链接rename_axis方法在一个步骤中设置列名称,该方法在将列表作为第一个参数传递时,将这些值用作索引级别名称。 重置索引时,Pandas 使用这些索引级别名称作为新的列名称。 此外,reset_index方法具有一个name参数,该参数对应于序列值的新列名称。

所有序列都有一个name属性,可以直接设置或使用rename方法设置。 当使用reset_index时,这个属性成为列名。

更多

使用stack的关键之一是将所有不希望转换的列都放在索引中。 最初使用索引中的状态读取此秘籍中的数据集。 让我们看一下如果不将状态读入索引,将会发生什么:

>>> state_fruit2 = pd.read_csv('data/state_fruit2.csv')
>>> state_fruit2

由于状态名称不在索引中,因此在此数据帧上使用stack可将所有值整形为一个长值序列:

>>> state_fruit2.stack()
0  State       Texas
   Apple          12
   Orange         10
   Banana         40
1  State     Arizona
   Apple           9
   Orange          7
   Banana         12
2  State     Florida
   Apple           0
   Orange         14
   Banana        190
dtype: object

这个命令将重塑所有列,这次包括状态,而这根本不是我们所需要的。 为了正确地重塑此数据,您需要首先使用set_index方法将所有未重塑的列放入索引中,然后使用stack。 下面的代码与步骤 1 产生相似的结果:

>>> state_fruit2.set_index('State').stack()

另见

使用melt将变量值​​整理为列名

像大多数大型 Python 库一样,Pandas 也有许多不同的方式来完成同一任务-区别通常是可读性和性能。 Pandas 包含一个名为melt的数据帧方法,该的工作原理与先前秘籍中介绍的stack方法相似,但灵活性更高。

在 Pandas 版本 0.20 之前,melt仅作为必须通过pd.melt访问的函数提供。 Pandas 仍然是一个不断发展的库,您需要期待每个新版本的变化。 Pandas 一直在推动将只能在数据帧上运行的所有函数移至方法上,例如它们对melt所做的一样。 这是使用melt的首选方法,也是本秘籍使用它的方式。 查看 Pandas 文档的“新增功能”部分,以了解所有更改的最新信息。

准备

在本秘籍中,我们使用melt方法来整理一个简单的数据帧,以变量值作为列名。

操作步骤

  1. 读取state_fruit2数据集,并确定哪些列需要转换,哪些列不需要转换:
>>> state_fruit2 = pd.read_csv('data/state_fruit2.csv')
>>> state_fruit2

  1. 通过将适当的列传递给id_varsvalue_vars参数来使用melt方法:
>>> state_fruit2.melt(id_vars=['State'],
                      value_vars=['Apple', 'Orange', 'Banana'])

  1. 这一步为我们创建了整洁的数据。 默认情况下,melt将转换后的前列名称称为变量,并将相应的值称为。 方便地,melt有两个附加参数var_namevalue_name,它们使您能够重命名这两列:
>>> state_fruit2.melt(id_vars=['State'],
                      value_vars=['Apple', 'Orange', 'Banana'],
                      var_name='Fruit',
                      value_name='Weight')

工作原理

melt方法功能强大,可以显着重塑您的数据帧。 它最多包含五个参数,其中两个参数对于理解如何正确重塑数据至关重要:

  • id_vars是您要保留为列且不重塑形状的列名列表
  • value_vars是您想要重整为单个列的列名列表

id_vars或标识变量保留在同一列中,但对于传递给value_vars的每列重复一次。melt的一个关键方面是它忽略索引中的值,实际上,它默默地删除了您的索引并用默认的RangeIndex代替了它。 这意味着,如果您确实希望保留索引中的值,那么在使用melt之前,需要先重置索引。

将水平列名称转换为垂直列值的某些通用术语是“融化”,“解除堆叠”或“取消旋转”。

更多

melt方法的所有参数都是可选的,并且如果您希望所有值都位于单个列中,而它们的旧列标签位于另一个列中,则可以使用其默认值调用melt

>>> state_fruit2.melt()

实际上,您可能有很多需要融合的变量,并且只想指定标识变量。 在这种情况下,以以下方式调用melt会产生与步骤 2 相同的结果。在融化单个列时,实际上甚至不需要列表,只需传递其字符串值即可:

>>> state_fruit2.melt(id_vars='State')

另见

同时堆叠多组变量

一些数据集包含多组变量作为列名,需要同时堆叠到自己的列中。 以movie数据集为例可以帮助阐明这一点。 首先,选择包含演员姓名及其对应的 Facebook 点赞的所有列:

>>> movie = pd.read_csv('data/movie.csv')
>>> actor = movie[['movie_title', 'actor_1_name', 
                   'actor_2_name', 'actor_3_name', 
                   'actor_1_facebook_likes',
                   'actor_2_facebook_likes',
                   'actor_3_facebook_likes']]
>>> actor.head()

如果我们将变量定义为电影的标题,演员名称和 Facebook 点赞数,那么我们将需要独立地堆叠两组列,而仅通过一次调用stackmelt

准备

在本秘籍中,我们将通过同时堆叠演员名称及其与wide_to_long函数相对应的 Facebook 点赞来整理actor数据帧。

操作步骤

  1. 我们将使用通用的wide_to_long函数将数据重整为整齐的形式。 要使用此函数,我们将需要更改要堆叠的列名,以使它们以数字结尾。 我们首先创建一个用户定义的函数来更改列名:
>>> def change_col_name(col_name):
        col_name = col_name.replace('_name', '')
        if 'facebook' in col_name:
            fb_idx = col_name.find('facebook')
            col_name = col_name[:5] + col_name[fb_idx - 1:] \
                                    + col_name[5:fb_idx-1]
        return col_name
  1. 将此函数传递给方法以转换所有列名:
>>> actor2 = actor.rename(columns=change_col_name)
>>> actor2.head()

  1. 使用wide_to_long函数可同时堆叠actoractor_facebook_likes列集:
>>> stubs = ['actor', 'actor_facebook_likes']
>>> actor2_tidy = pd.wide_to_long(actor2, 
                                  stubnames=stubs, 
                                  i=['movie_title'], 
                                  j='actor_num', 
                                  sep='_')
>>> actor2_tidy.head()

工作原理

wide_to_long函数以相当特定的方式工作。 它的主要参数是stubnames,它是一个字符串列表。 每个字符串代表一个列分组。 以该字符串开头的所有列都将被堆叠到一个列中。 在此秘籍中,有两列列:actoractor_facebook_likes。 默认情况下,这些列的每个组都需要以数字结尾。 此数字随后将用于标记整形数据。 每个列组都有一个下划线字符,将stubname与结尾数字分开。 为此,必须使用sep参数。

原始列名称与wide_to_long工作所需的模式不匹配。 可以通过使用列表精确指定列名称来手动更改列名称。 这很快就会成为很多类型的输入,因此,我们定义了一个函数,该函数自动将我们的列转换为有效的格式。change_col_name函数从参与者列中删除_name,并重新排列facebook列,以便现在它们都以数字结尾。

要实际完成列重命名,我们在步骤 2 中使用rename方法。它接受许多不同类型的参数,其中之一是函数。 将其传递给函数时,每个列名都会一次隐式传递给它。

现在,我们已经正确地创建了两个独立的列组,即以actoractor_facebook_likes开头的列,它们将被堆叠。 除此之外,wide_to_long还需要一个唯一列,即参数i,用作不会堆叠的标识变量。 还需要参数j,该参数仅重命名从原始列名的末尾去除的标识数字。 默认情况下,prefix参数包含搜索一个或多个数字的正则表达式\d+\d是与数字 0-9 匹配的特殊令牌。 加号+使表达式与这些数字中的一个或多个匹配。

要成为str方法的强大用户,您将需要熟悉正则表达式,这是与某些文本中的特定模式匹配的字符序列。 它们由具有特殊含义的“元字符”和“字面值”字符组成。 要使自己对正则表达式有用,请查看 Regular-Expressions.info 中的简短教程。

更多

当所有变量分组具有相同的数字结尾(如此秘籍中的数字)时,函数wide_to_long起作用。 当您的变量没有相同的结尾或不是以数字结尾时,您仍然可以使用wide_to_long同时进行列堆叠。 例如,让我们看一下以下数据集:

>>> df = pd.read_csv('data/stackme.csv')
>>> df

假设我们希望将a1b1列以及de列堆叠在一起。 另外,我们想使用a1b1作为行的标签。 要完成此任务,我们需要重命名列,以便它们以所需的标签结尾:

>>> df2 = df.rename(columns = {'a1':'group1_a1', 'b2':'group1_b2',
                               'd':'group2_a1', 'e':'group2_b2'})
>>> df2

然后,我们需要修改后缀参数,该参数通常默认为选择数字的正则表达式。 在这里,我们只是简单地告诉它找到任意数量的字符:

>>> pd.wide_to_long(df2, 
                    stubnames=['group1', 'group2'], 
                    i=['State', 'Country', 'Test'], 
                    j='Label', 
                    suffix='.+', 
                    sep='_')

另见

反转堆叠数据

数据帧具有两种相似的方法stackmelt,用于将水平列名称转换为垂直列值。数据帧分别具有分别通过unstackpivot方法直接反转这两个操作的能力。stack/unstack是更简单的方法,仅允许控制列/行索引,而melt/pivot提供更大的灵活性来选择要重塑的列。

准备

在此秘籍中,我们将stack/melt一个数据集,并立即将unstack/pivot的操作转换回其原始形式。

操作步骤

  1. 读取college数据集,以机构名称作为索引,并且仅包含大学生种族栏目:
>>> usecol_func = lambda x: 'UGDS_' in x or x == 'INSTNM'
>>> college = pd.read_csv('data/college.csv', 
                          index_col='INSTNM', 
                          usecols=usecol_func)
>>> college.head()

  1. 使用stack方法将每个水平列名称转换为垂直索引级别:
>>> college_stacked = college.stack()
>>> college_stacked.head(18)
INSTNM                                         
Alabama A &amp; M University         UGDS_WHITE    0.0333
                                     UGDS_BLACK    0.9353
                                     UGDS_HISP     0.0055
                                     UGDS_ASIAN    0.0019
                                     UGDS_AIAN     0.0024
                                     UGDS_NHPI     0.0019
                                     UGDS_2MOR     0.0000
                                     UGDS_NRA      0.0059
                                     UGDS_UNKN     0.0138
University of Alabama at Birmingham  UGDS_WHITE    0.5922
                                     UGDS_BLACK    0.2600
                                     UGDS_HISP     0.0283
                                     UGDS_ASIAN    0.0518
                                     UGDS_AIAN     0.0022
                                     UGDS_NHPI     0.0007
                                     UGDS_2MOR     0.0368
                                     UGDS_NRA      0.0179
                                     UGDS_UNKN     0.0100
dtype: float64
  1. 使用unstack序列方法将堆叠的数据转换回原始格式:
>>> college_stacked.unstack()
  1. 可以先执行melt,然后执行pivot,然后执行类似的操作序列。 首先,读入数据而不将机构名称放在索引中:
>>> college2 = pd.read_csv('data/college.csv', 
                          usecols=usecol_func)
>>> college2.head()

  1. 使用melt方法将所有竞速列转置为单列:
>>> college_melted = college2.melt(id_vars='INSTNM', 
                                   var_name='Race',
                                   value_name='Percentage')
>>> college_melted.head()

  1. 使用pivot方法来反转之前的结果:
>>> melted_inv = college_melted.pivot(index='INSTNM', 
                                      columns='Race',
                                      values='Percentage')
>>> melted_inv.head()

  1. 请注意,机构名称现在已转移到索引中,而不是按其原始顺序排列。 列名称不是按其原始顺序。 要从第 4 步中完全复制起始数据帧,请使用.loc索引运算符同时选择行和列,然后重置索引:
>>> college2_replication = melted_inv.loc[college2['INSTNM'],
                                          college2.columns[1:]]\
                                     .reset_index()
>>> college2.equals(college2_replication)
True

工作原理

在步骤 1 中,有多种方法可以完成相同的任务。在这里,我们展示read_csv函数的多功能性。usecols参数接受我们要导入的列的列表或动态确定它们的函数。 我们使用匿名函数来检查列名是否包含UGDS_或等于INSTNM。 该函数以字符串的形式传递给每个列名,并且必须返回一个布尔值。 通过这种方式可以节省大量的内存。

步骤 2 中的stack方法将所有列名称放入最里面的索引级别,并返回一个序列。 在步骤 3 中,unstack方法通过获取最里面的索引级别中的所有值将它们转换为列名来反转此操作。

步骤 3 的结果与步骤 1 不太完全相同。 整行都缺少值,默认情况下,stack方法在步骤 2 中将其删除。 为了保留这些丢失的值并创建精确的副本,请在stack方法中使用dropna=False

步骤 4 读取与步骤 1 相同的数据集,但没有将机构名称放入索引中,因为melt方法无法访问它。 步骤 5 使用melt方法转置所有Race列。 它通过将value_vars参数保留为其默认值None来执行此操作。 如果未指定,则id_vars参数中不存在的所有列都将转置。

步骤 6 用pivot方法反转了步骤 5 的操作,该方法接受三个参数。 每个参数都将一列作为字符串。index参数引用的列保持垂直并成为新索引。columns参数引用的列的值成为列名。values参数引用的值将平铺以对应于其先前索引和列标签的交集。

要使用pivot进行精确复制,我们需要按照与原始顺序完全相同的顺序对行和列进行排序。 由于机构名称在索引中,因此我们使用.loc索引运算符作为通过其原始索引对数据帧进行排序的方式。

更多

为了帮助进一步理解stack/unstack,让我们将它们用于转置college数据帧。

在这种情况下,我们使用矩阵转置的精确数学定义,其中新行是原始数据矩阵的旧列。

如果您看一下步骤 2 的输出,您会注意到有两个索引级别。 默认情况下,unstack方法使用最里面的索引级别作为新的列值。 索引级别从外部从零开始编号。 Pandas 默认将unstack方法的level参数设置为-1,这是指最里面的索引。 我们可以使用level=0代替unstack最外面的列:

>>> college.stack().unstack(0)

实际上,有一种非常简单的方法可以通过使用transpose方法或T属性来转置不需要stackunstack的数据帧:

>>> college.T
>>> college.transpose()

另见

  • 请参阅第 4 章,“选择数据子集”中的“同时选择数据帧的行和列”秘籍
  • Pandas unstackpivot方法的官方文档

groupby聚合后解除堆叠

按单个列对数据进行分组并在单个列上执行聚合将返回简单易用的结果,并且易于使用。 当按多个列进行分组时,可能不会以使消耗变得容易的方式来构造结果聚合。 由于默认情况下groupby操作将唯一的分组列放在索引中,因此unstack方法对于重新排列数据非常有用,以便以对解释更有用的方式显示数据。

准备

在此秘籍中,我们使用employee数据集执行聚合,并按多列分组。 然后,我们使用unstack方法将结果重塑为一种格式,以便于比较不同组。

操作步骤

  1. 读取员工数据集,并按种族找到平均工资:
>>> employee = pd.read_csv('data/employee.csv')
>>> employee.groupby('RACE')['BASE_SALARY'].mean().astype(int)
RACE
American Indian or Alaskan Native    60272
Asian/Pacific Islander               61660
Black or African American            50137
Hispanic/Latino                      52345
Others                               51278
White                                64419
Name: BASE_SALARY, dtype: int64
  1. 这是一个非常简单的groupby操作,可产生易于阅读且无需重塑的序列。 现在让我们按性别查找所有种族的平均工资:
>>> agg = employee.groupby(['RACE', 'GENDER'])['BASE_SALARY'] \
                  .mean().astype(int)
>>> agg
RACE                               GENDER
American Indian or Alaskan Native  Female    60238
                                   Male      60305
Asian/Pacific Islander             Female    63226
                                   Male      61033
Black or African American          Female    48915
                                   Male      51082
Hispanic/Latino                    Female    46503
                                   Male      54782
Others                             Female    63785
                                   Male      38771
White                              Female    66793
                                   Male      63940
Name: BASE_SALARY, dtype: int64
  1. 这种聚合更加复杂,可以进行重塑以简化不同的比较。 例如,如果每个种族并排而不是像现在这样垂直,则比较男性和女性的工资会更容易。 让我们解开性别索引级别:
>>> agg.unstack('GENDER')

  1. 同样,我们可以unstack竞赛索引级别:
>>> agg.unstack('RACE')

工作原理

第 1 步使用单个分组列(RACE),单个聚合列(BASE_SALARY)和单个聚合函数(mean)进行最简单的聚合。 此结果易于使用,不需要任何其他处理即可求值。 第 2 步通过将种族和性别分组在一起,稍微增加了复杂性。 生成的多重索引序列在一个维中包含所有值,这使得比较更加困难。 为了使信息更易于使用,我们使用unstack方法将一个(或多个)级别中的值转换为列。

默认情况下,unstack使用最里面的索引级别作为新列。 您可以使用level参数指定要取消堆叠的确切级别,该参数接受级别名称作为字符串或级别整数位置。 最好在整数位置上使用级别名称,以避免产生歧义。 第 3 步和第 4 步将每个级别拆栈,这将导致数据帧具有单级索引。 现在,按性别比较每个种族的薪水要容易得多。

更多

如果有多个分组和聚合列,则直接结果将是数据帧而不是序列。 例如,让我们计算除平均值以外的更多聚合,如步骤 2 所示:

>>> agg2 = employee.groupby(['RACE', 'GENDER'])['BASE_SALARY'] \
                   .agg(['mean', 'max', 'min']).astype(int)
>>> agg2

堆叠GENDER列将产生多重索引列。 从这里开始,您可以继续使用unstackstack方法交换行和列级别,直到获得所需的数据结构为止:

>>> agg2.unstack('GENDER')

另见

  • 请参阅第 7 章,“分组和多列聚合”的“进行聚集,过滤和转换的分组函数”秘籍

使用groupby聚合复制pivot_table

乍一看,pivot_table方法似乎提供了一种独特的数据分析方法。 但是,在进行少量按摩之后,可以使用groupby聚合完全复制其功能。 知道这种等效性可以帮助缩小 Pandas 功能的范围。

准备

在此秘籍中,我们使用flights数据集创建数据透视表,然后使用groupby操作重新创建它。

操作步骤

  1. 读取航班数据集,并使用pivot_table方法查找每个航空公司每个始发机场已取消航班的总数:
>>> flights = pd.read_csv('data/flights.csv')
>>> fp = flights.pivot_table(index='AIRLINE', 
                             columns='ORG_AIR', 
                             values='CANCELLED', 
                             aggfunc='sum',
                             fill_value=0).round(2)
>>> fp.head()

  1. groupby聚合无法直接复制此表。 诀窍是首先对indexcolumns参数中的所有列进行分组:
>>> fg = flights.groupby(['AIRLINE', 'ORG_AIR'])['CANCELLED'].sum()
>>> fg.head()
AIRLINE  ORG_AIR
AA       ATL         3
         DEN         4
         DFW        86
         IAH         3
         LAS         3
Name: CANCELLED, dtype: int64
  1. 使用unstack方法将ORG_AIR索引级别转换为列名称:
>>> fg_unstack = fg.unstack('ORG_AIR', fill_value=0)
>>> fp.equals(fg_unstack)
True

工作原理

pivot_table方法非常通用且灵活,但是执行与groupby聚合相当相似的操作,其中步骤 1 显示了一个简单示例。index参数采用一列(或多列),该列将不会被透视,并且其唯一值将放置在索引中。columns参数采用一列(或多列),该列将被透视,并且其唯一值将作为列名称。values参数采用将汇总的一列(或多列)。

还存在一个aggfunc参数,该参数带有一个或多个聚合函数,这些函数确定values参数中的列如何聚合。 它默认为均值,在此示例中,我们将其更改为计算总和。 此外,AIRLINEORG_AIR的某些唯一组合不存在。 这些缺失的组合将默认为结果数据帧中的缺失值。 在这里,我们使用fill_value参数将其更改为零。

步骤 2 使用indexcolumns参数中的所有列作为分组列开始复制过程。 这是使此秘籍生效的关键。 数据透视表只是分组列的所有唯一组合的交集。 步骤 3 通过使用unstack方法将最里面的索引级别转换为列名来完成复制。 就像pivot_table一样,并非AIRLINEORG_AIR的所有组合都存在。 我们再次使用fill_value参数将这些缺失的交集强制为零。

更多

可以使用groupby聚合复制更复杂的数据透视表。 例如,从pivot_table中获得以下结果:

>>> flights.pivot_table(index=['AIRLINE', 'MONTH'],
                        columns=['ORG_AIR', 'CANCELLED'],
                        values=['DEP_DELAY', 'DIST'],
                        aggfunc=[np.sum, np.mean],
                        fill_value=0)

要使用groupby聚合复制此代码,只需遵循秘籍中的相同模式,并将indexcolumns参数中的所有列放入groupby方法中,然后将unstack列中:

>>> flights.groupby(['AIRLINE', 'MONTH', 'ORG_AIR', 'CANCELLED']) \
           ['DEP_DELAY', 'DIST'] \
           .agg(['mean', 'sum']) \
           .unstack(['ORG_AIR', 'CANCELLED'], fill_value=0) \
           .swaplevel(0, 1, axis='columns')

有一些区别。 当像agg分组方法那样作为列表传递时,pivot_table方法不接受聚合函数作为字符串。 相反,您必须使用 NumPy 函数。 列级别的顺序也有所不同,其中pivot_table将聚合函数置于values参数中列之前的级别。 这与swaplevel方法相等,在这种情况下,该方法将切换前两个级别的顺序。

截至撰写本书时,将多个列堆叠在一起时存在一个错误,即忽略fill_value参数。 要解决此错误,请将.fillna(0)链接到代码末尾。

重命名轴级别以方便重塑

当每个轴(索引/列)级别具有名称时,使用stack/unstack方法进行重塑要容易得多。 Pandas 允许用户按整数位置或名称引用每个轴级别。 由于整数位置是隐式的而不是显式的,因此应尽可能考虑使用级别名称。 此建议来自“Python 之禅”,这是 Python 的指导原则的简短列表,一个是“显式优于隐式”。

准备

当用多列进行分组或聚合时,所得的 Pandas 对象将在一个或两个轴上具有多个级别。 在本秘籍中,我们将命名每个轴的每个级别,然后使用stack/unstack方法将数据显着重塑为所需的形式。

操作步骤

  1. 阅读大学数据集,并按机构和宗教信仰找到一些关于大学人口和 SAT 数学成绩的基本摘要统计数据:
>>> college = pd.read_csv('data/college.csv')
>>> cg = college.groupby(['STABBR', 'RELAFFIL']) \
                ['UGDS', 'SATMTMID'] \
                .agg(['size', 'min', 'max']).head(6)

  1. 请注意,两个索引级别都有名称,并且都是旧的列名称。 另一方面,列级别没有名称。 使用rename_axis方法为它们提供级别名称:
>>> cg = cg.rename_axis(['AGG_COLS', 'AGG_FUNCS'], axis='columns')
>>> cg

  1. 现在每个轴级别都有一个名称,重塑变得轻而易举。 使用stack方法将AGG_FUNCS列移至索引级别:
>>> cg.stack('AGG_FUNCS').head()

  1. 默认情况下,堆叠会将新的列级别放置在最里面的位置。 使用swaplevel方法切换电平的位置:
>>> cg.stack('AGG_FUNCS').swaplevel('AGG_FUNCS', 'STABBR',
                                    axis='index').head()

  1. 通过使用sort_index方法对级别进行排序,我们可以继续使用轴级别名称:
>>> cg.stack('AGG_FUNCS') \
      .swaplevel('AGG_FUNCS', 'STABBR', axis='index') \
      .sort_index(level='RELAFFIL', axis='index') \
      .sort_index(level='AGG_COLS', axis='columns').head(6)

  1. 为了完全重塑数据,您可能需要堆叠一些列,同时堆叠其他列。 在单个命令中将两个方法链接在一起:
>>> cg.stack('AGG_FUNCS').unstack(['RELAFFIL', 'STABBR'])

  1. 一次堆叠所有列以返回序列:
>>> cg.stack(['AGG_FUNCS', 'AGG_COLS']).head(12)
STABBR  RELAFFIL  AGG_FUNCS  AGG_COLS
AK      0         count      UGDS            7.0
                             SATMTMID        0.0
                  min        UGDS          109.0
                  max        UGDS        12865.0
        1         count      UGDS            3.0
                             SATMTMID        1.0
                  min        UGDS           27.0
                             SATMTMID      503.0
                  max        UGDS          275.0
                             SATMTMID      503.0
AL      0         count      UGDS           71.0
                             SATMTMID       13.0
dtype: float64

工作原理

groupby聚合的结果通常会产生具有多个轴级别的数据帧或序列。 步骤 1 中groupby操作的结果数据帧每个轴具有多个级别。 列级别未命名,这将要求我们仅按其整数位置引用它们。 为了大大简化我们引用列级别的能力,我们使用rename_axis方法对其进行了重命名。

rename_axis方法有点奇怪,因为它可以根据传递给它的第一个参数的类型来修改级别名称和级别值。 向其传递一个列表(如果只有一个级别,则为标量)会更改级别的名称。 向其传递字典或函数会更改级别的值。 在第 2 步中,我们向rename_axis方法传递一个列表,并返回一个具有所有轴级别命名的数据帧。

一旦所有轴级别都有名称,我们就可以轻松明确地控制数据的结构。 步骤 3 将AGG_FUNCS列堆叠到最里面的索引级别。 步骤 4 中的swaplevel方法接受要交换的级别的名称或位置作为前两个参数。sort_index方法被调用两次,并对每个级别的实际值进行排序。 请注意,列级别的值是列名SATMTMIDUGDS

通过步骤 6 进行堆叠和拆栈,我们可以得到截然不同的输出。也可以将每个单独的列级别堆叠到索引中以产生一个序列。

更多

如果您希望完全丢弃电平值,可以将它们设置为None。 当需要减少数据帧的可视输出中的混乱情况,或者很明显列级别代表什么并且不进行进一步处理时,可以采取这种措施:

>>> cg.rename_axis([None, None], axis='index') \
      .rename_axis([None, None], axis='columns')

将多个变量存储为列名时进行整理

每当列名称本身包含多个不同的变量时,就会出现一种特殊的混乱数据。 当年龄和性别连接在一起时,便会出现这种情况的常见示例。 要整理这样的数据集,我们必须使用 pandas str访问器来操作列,该访问器包含用于字符串处理的其他方法。

准备

在本秘籍中,我们将首先确定所有变量,其中一些变量将被连接在一起作为列名。 然后,我们对数据进行整形并解析文本以提取正确的变量值。

操作步骤

  1. 读取男士的weightlifting数据集,并标识变量:
>>> weightlifting = pd.read_csv('data/weightlifting_men.csv')
>>> weightlifting

  1. 变量是体重类别,性别/年龄类别和合格总数。 年龄和性别变量已合并为一个单元格。 在将它们分开之前,让我们使用melt方法将agesex列名称转置为单个垂直列:
>>> wl_melt = weightlifting.melt(id_vars='Weight Category', 
                                 var_name='sex_age', 
                                 value_name='Qual Total')
>>> wl_melt.head()

  1. 选择sex_age列,然后使用str访问器中可用的split方法将该列分为两个不同的列:
>>> sex_age = wl_melt['sex_age'].str.split(expand=True)
>>> sex_age.head()

  1. 此操作返回了一个完全独立的数据帧,具有无意义的列名。 让我们重命名列,以便我们可以显式访问它们:
>>> sex_age.columns = ['Sex', 'Age Group']
>>> sex_age.head()

  1. str访问器之后直接使用索引运算符从Sex列中选择第一个字符:
>>> sex_age['Sex'] = sex_age['Sex'].str[0]
>>> sex_age.head()

  1. 使用pd.concat函数将此数据帧与wl_melt连接在一起,以生成整洁的数据集:
>>> wl_cat_total = wl_melt[['Weight Category', 'Qual Total']]
>>> wl_tidy = pd.concat([sex_age, wl_cat_total], axis='columns')
>>> wl_tidy.head()

  1. 可以使用以下方法创建相同的结果:
>>> cols = ['Weight Category', 'Qual Total']
>>> sex_age[cols] = wl_melt[cols]

工作原理

weightlifting数据集与许多数据集一样,具有原始格式的易于消化的信息,但是从技术上讲,它很混乱,因为除一个列名之外,所有其他列都包含性别和年龄信息。 一旦确定了变量,就可以开始整理数据集。 只要列名称包含变量,就需要使用melt(或stack)方法。Weight Category变量已经在正确的位置,因此我们通过将其传递给id_vars参数来将其保留为标识变量。 请注意,我们不需要明确地命名要与value_vars融合的所有列。 默认情况下,id_vars中不存在的所有列都会融化。

sex_age列需要解析,并分为两个变量。 为此,我们转向str访问器提供的额外函数,该函数仅适用于序列(单个数据帧的列)。 在这种情况下,split方法是较常见的方法之一,因为它可以将字符串的不同部分分成各自的列。 默认情况下,它在空白处分割,但是您也可以使用pat参数指定字符串或正则表达式。 当expand参数设置为True时,将为每个独立的分割字符段形成一个新列。 当False时,返回单个列,其中包含所有段的列表。

在第 4 步中重命名列之后,我们需要再次使用str访问器。 有趣的是,索引运算符可用于选择或分割字符串段。 在这里,我们选择第一个字符,这是性别变量。 我们可以更进一步,将年龄分为最小年龄和最大年龄两个单独的列,但是通常以这种方式指代整个年龄组,因此我们将其保持不变。

步骤 6 显示了将所有数据连接在一起的两种不同方法之一。concat函数接受数据帧的集合,并将它们垂直(axis='index')或水平(axis='columns')连接。 由于两个数据帧的索引相同,因此可以像第 7 步中那样将一个数据帧的值分配给另一列中的新列。

更多

从步骤 2 开始,完成此秘籍的另一种方法是直接从sex_age列中分配新列,而无需使用split方法。assign方法可用于动态添加以下新列:

>>> age_group = wl_melt.sex_age.str.extract('(\d{2}[-+](?:\d{2})?)',
                                            expand=False)
>>> sex = wl_melt.sex_age.str[0]
>>> new_cols = {'Sex':sex, 
                'Age Group': age_group}
>>> wl_tidy2 = wl_melt.assign(**new_cols) \
                      .drop('sex_age',axis='columns')

>>> wl_tidy2.sort_index(axis=1).equals(wl_tidy.sort_index(axis=1))
True

以与步骤 5 完全相同的方式找到Sex列。由于我们没有使用split,因此必须以不同的方式提取Age Group列。extract方法使用复杂的正则表达式来提取字符串的非常特定的部分。 为了正确使用extract,您的图案必须包含捕获组。 通过将圆括号括在图案的一部分周围来形成捕获组。 在此示例中,整个表达式是一个大捕获组。 它以\d{2}开头,它精确地搜索两位数,然后是字面的正负号,或者是可选的后两位。 尽管表达式的最后部分(?:\d{2})?被括号括起来,但是?:表示它实际上不是捕获组。 从技术上讲,它是一个非捕获组,用于同时表示两个数字(可选)。 不再需要sex_age列,将其删除。 最后,将两个整洁的数据帧相互比较,发现它们是等效的。

另见

将多个变量存储为列值时进行整理

整洁的数据集每个变量必须有一个单独的列。 有时,多个变量名放在一列中,而其对应的值放在另一列中。 这种杂乱数据的一般格式如下:

在此示例中,前三行和后三行表示两个不同的观察值,每个观察值应为行。 需要对数据进行透视,使其最终如下所示:

准备

在此秘籍中,我们确定包含结构错误的变量的列,并将其旋转以创建整洁的数据。

操作步骤

  1. 读取餐厅inspections数据集,然后将Date列数据类型转换为datetime64
>>> inspections = pd.read_csv('data/restaurant_inspections.csv',
                              parse_dates=['Date'])
>>> inspections.head()

  1. 该数据集具有两个变量NameDate,它们分别正确地包含在单个列中。Info列本身具有五个不同的变量:BoroughCuisineDescriptionGradeScore。 让我们尝试使用pivot方法使NameDate列保持垂直,从Info列中的所有值中创建新列,并使用Value列作为它们的交集:
>>> inspections.pivot(index=['Name', 'Date'],
                      columns='Info', values='Value')
NotImplementedError: > 1 ndim Categorical are not supported at this time
  1. 不幸的是,Pandas 开发人员尚未为我们实现此功能。 将来,这行代码很有可能会起作用。 幸运的是,在大多数情况下,Pandas 有多种完成同一任务的方法。 让我们将NameDateInfo放入索引中:
>>> inspections.set_index(['Name','Date', 'Info']).head(10)

  1. 使用unstack方法来旋转Info列中的所有值:
>>> inspections.set_index(['Name','Date', 'Info']) \
               .unstack('Info').head()

  1. 使用reset_index方法将索引级别分为几列:
>>> insp_tidy = inspections.set_index(['Name','Date', 'Info']) \
                           .unstack('Info') \
                           .reset_index(col_level=-1)
>>> insp_tidy.head()

  1. 数据集很整齐,但是有一些烦人的剩余 Pandas 残骸需要清除。 让我们使用多重索引方法droplevel删除顶部的列级别,然后将索引级别重命名为None
>>> insp_tidy.columns = insp_tidy.columns.droplevel(0) \
                                         .rename(None)
>>> insp_tidy.head()

  1. 通过使用squeeze方法将该列数据帧转换为序列,可以避免在步骤 4 中创建多重索引列。 以下代码产生与上一步相同的结果:
>>> inspections.set_index(['Name','Date', 'Info']) \
               .squeeze() \
               .unstack('Info') \
               .reset_index() \
               .rename_axis(None, axis='columns')

工作原理

在第 1 步中,我们注意到在Info列中垂直放置了五个变量,在Value列中有相应的值。 因为我们需要将这五个变量中的每一个作为水平列名进行透视,所以pivot方法似乎可以工作。 不幸的是,当有多个非枢轴列时,Pandas 开发人员尚未实现这种特殊情况。 我们被迫使用另一种方法。

unstack方法还枢转垂直数据,但仅适用于索引中的数据。 第 3 步通过使用set_index方法移动将和不会旋转到索引中的两个列来开始此过程。 这些列进入索引后,即可像在步骤 3 中一样操作unstack

请注意,当我们拆开数据帧时,pandas 会保留原始的列名(在这里,它只是一个列Value),并创建一个以旧列名为上层的多重索引。 数据集现在基本上是整齐的,但是我们继续使用reset_index方法将无枢轴的列设置为普通列。 因为我们有多重索引列,所以我们可以使用col_level参数选择新列名称所属的级别。 默认情况下,名称会插入到最高级别(级别 0)。 我们使用-1表示最底层。

毕竟,我们还有一些多余的数据帧名称和索引需要丢弃。 不幸的是,没有可以删除级别的数据帧方法,因此我们必须进入索引并使用其droplevel方法。 在这里,我们用单级列覆盖了旧的多重索引列。 这些列仍具有无用的名称属性Info,该属性已重命名为None

通过将步骤 3 中的结果数据帧强制为序列,可以避免清理多重索引列。squeeze方法仅适用于单列数据帧,并将其转换为序列。

更多

实际上,可以使用pivot_table方法,该方法对允许多少个非透视列没有限制。pivot_table方法与pivot不同,它对与indexcolumns参数中的列之间的交点相对应的所有值执行汇总。 由于此交点中可能存在多个值,因此pivot_table要求用户向其传递一个汇总函数,以便输出单个值。 我们使用first汇总函数,该函数采用组中的第一个值。 在此特定示例中,每个交叉点都只有一个值,因此没有任何要累加的值。 默认的聚合函数是均值,在这里会产生错误,因为某些值是字符串:

>>> inspections.pivot_table(index=['Name', 'Date'], 
                            columns='Info', 
                            values='Value', 
                            aggfunc='first') \
               .reset_index() \
               .rename_axis(None, axis='columns')

另见

在同一单元格中存储两个或多个值时进行整理

表格数据本质上是二维的,因此,可以在单个单元格中显示的信息量有限。 解决方法是,您偶尔会看到在同一单元格中存储了多个值的数据集。 整洁的数据可为每个单元格精确地提供一个值。 为了纠正这些情况,通常需要使用str序列访问器中的方法将字符串数据解析为多列。

准备

在本秘籍中,我们检查一个数据集,该数据集的每个列中都有一个包含多个不同变量的列。 我们使用str访问器将这些字符串解析为单独的列以整理数据。

操作步骤

  1. 读取texas_cities数据集,并标识变量:
>>> cities = pd.read_csv('data/texas_cities.csv')
>>> cities

  1. City列看起来不错,并且仅包含一个值。 另一方面,Geolocation列包含四个变量:latitudelatitude directionlongitudelongitude direction。 让我们将Geolocation列分为四个单独的列:
>>> geolocations = cities.Geolocation.str.split(pat='. ',
                                                expand=True)
>>> geolocations.columns = ['latitude', 'latitude direction',
                            'longitude', 'longitude direction']
>>> geolocations

  1. 因为Geolocation的原始数据类型是对象,所以所有新列也是对象。 让我们将latitudelongitude更改为浮点数:
>>> geolocations = geolocations.astype({'latitude':'float',
                                        'longitude':'float'})
>>> geolocations.dtypes
latitude               float64
latitude direction      object
longitude              float64
longitude direction     object
dtype: object
  1. 将这些新列与原始的City列连接在一起:
>>> cities_tidy = pd.concat([cities['City'], geolocations],
                            axis='columns')
>>> cities_tidy

工作原理

读取数据后,我们决定数据集中有多少个变量。 在这里,我们选择将Geolocation列分为四个变量,但是我们可以只选择两个作为纬度和经度,并使用负号来区分西/东和南/北。

有几种方法可以使用str访问器中的方法来解析Geolocation列。 最简单的方法是使用split方法。 我们为它传递一个由任何字符(句点)和空格定义的简单正则表达式。 当空格跟随任何字符时,将进行分割,并形成一个新列。 该模式的首次出现在纬度的尽头。 空格紧跟度数字符,并形成分割。 分割字符将被丢弃,而不保留在结果列中。 下一个分割与逗号和空格匹配,紧跟在纬度方向之后。

总共进行了三个拆分,得到了四列。 步骤 2 的第二行为其提供了有意义的名称。 即使所得的latitudelongitude列似乎是浮点数,也并非如此。 它们最初是从对象列进行解析的,因此仍然是对象数据类型。 步骤 3 使用字典将列名称映射到其新类型。

您可以使用函数to_numeric尝试将每一列转换为整数或浮点数,而不是使用字典,如果字典有很多列名,则需要大量输入。 要在每列上迭代应用此函数,请对以下内容使用apply方法:

>>> geolocations.apply(pd.to_numeric, errors='ignore')

步骤 4 将城市连接到此新数据帧的前面,以完成整理数据的过程。

更多

split方法在此示例中使用简单的正则表达式非常有效。 对于其他示例,某些列可能会要求您根据几种不同的模式创建拆分。 要搜索多个正则表达式,请使用竖线字符|。 例如,如果我们只想分割度数符号和逗号,并在其后跟一个空格,则可以执行以下操作:

>>> cities.Geolocation.str.split(pat='° |, ', expand=True)

这将从步骤 2 返回相同的数据帧。可以使用管道字符将任意数量的其他拆分模式附加到前面的字符串模式。

extract方法是另一种出色的方法,它允许您提取每个单元格中的特定组。 这些捕获组必须用括号括起来。 结果中不存在任何括号外匹配的内容。 下一行产生与步骤 2 相同的输出:

>>> cities.Geolocation.str.extract('([0-9.]+). (N|S), ([0-9.]+). (E|W)',
                                   expand=True)

此正则表达式具有四个捕获组。 第一组和第三组至少搜索一个或多个带小数的连续数字。 第二和第四组搜索单个字符(方向)。 第一个和第三个捕获组由任何字符分隔,后跟一个空格。 第二个捕获组用逗号分隔,然后用空格隔开。

在列名和值中存储变量时进行整理

每当变量在列名称中水平存储并且在列值垂直向下存储时,就会出现一种特别难以诊断的混乱数据形式。 通常,您会遇到这种类型的数据集,而不是在数据库中,而是从其他人已经生成的汇总报告中遇到。

准备

在此秘籍中,变量在垂直和水平方向都可以识别,并通过meltpivot_table方法重新整理为整齐的数据。

操作步骤

  1. 读取sensors数据集并标识变量:
>>> sensors = pd.read_csv('data/sensors.csv')
>>> sensors

  1. 正确放置在垂直列中的唯一变量是GroupProperty列似乎具有三个唯一变量PressureTemperatureFlow20122016列的其余部分本身都是一个变量,我们可以明智地将其命名为Year。 用单个数据帧方法不可能重组这种混乱的数据。 让我们从melt方法开始,将年份分为自己的专栏:
>>> sensors.melt(id_vars=['Group', 'Property'], var_name='Year') \
           .head(6)

  1. 这解决了我们的问题之一。 让我们使用pivot_table方法将Property列转换为新的列名称:
>>> sensors.melt(id_vars=['Group', 'Property'], var_name='Year') \
           .pivot_table(index=['Group', 'Year'],
                        columns='Property', values='value') \
           .reset_index() \
           .rename_axis(None, axis='columns')

工作原理

一旦在步骤 1 中确定了变量,就可以开始重组。 Pandas 没有同时旋转,列的方法,因此我们必须一次完成这一任务。 我们通过将Property列传递给melt方法中的id_vars参数来保持年份垂直。

现在,结果中还有混乱的数据部分。如前面的秘籍“将多个变量存储为列值时进行整理”秘籍所述,当在index参数中使用多个列时,我们必须使用pivot_table来旋转数据帧。 旋转后,GroupYear变量卡在索引中。 我们将它们以列的形式推出。pivot_table方法将columns参数中使用的列名称保留为列索引的名称。 重置索引后,该名称变得毫无意义,我们使用rename_axis将其删除。

更多

每当涉及meltpivot_tablepivot的解决方案时,您都可以确定存在使用stackunstack的替代方法。 诀窍是首先将当前未旋转到索引中的列移动:

>>> sensors.set_index(['Group', 'Property']) \
           .stack() \
           .unstack('Property') \
           .rename_axis(['Group', 'Year'], axis='index') \
           .rename_axis(None, axis='columns') \
           .reset_index()

将多个观测单位存储在同一表中时进行整理

当每个表包含来自单个观察单位的信息时,通常更容易维护数据。 另一方面,当所有数据都在单个表中时,更容易发现见解;对于机器学习,所有数据都必须在单个表中。 整洁的数据的重点不是直接进行分析。 相反,它正在对数据进行结构化处理,以便更轻松地进行分析,并且在一个表中有多个观察单位时,可能需要将其分成各自的表。

准备

在本秘籍中,我们使用movie数据集来识别三个观察单位(电影,演员和导演),并分别为每个观察单位创建表格。 制定此秘籍的关键之一是了解演员和导演的 Facebook 点赞与电影无关。 每个演员和导演都映射到一个表示他们的 Facebook 点赞数的单一值。 由于这种独立性,我们可以将电影,导演和演员的数据分离到各自的表中。 数据库人员将此过程标准化,这可以提高数据完整性并减少冗余。

操作步骤

  1. 读入更改后的movie数据集,并输出前五行:
>>> movie = pd.read_csv('data/movie_altered.csv')
>>> movie.head()

  1. 该数据集包含有关电影本身,导演和演员的信息。 这三个实体可以视为观测单位。 在开始之前,让我们使用insert方法创建一列来唯一标识每个电影:
>>> movie.insert(0, 'id', np.arange(len(movie)))
>>> movie.head()

  1. 让我们尝试使用wide_to_long函数整理此数据集,以将所有演员放在一列中,并将其对应的 Facebook 点赞放在另一列中,并为导演做同样的事情,即使每部电影只有一个 :
>>> stubnames = ['director', 'director_fb_likes',
                 'actor', 'actor_fb_likes']
>>> movie_long = pd.wide_to_long(movie, 
                                 stubnames=stubnames, 
                                 i='id', 
                                 j='num', 
                                 sep='_').reset_index()

>>> movie_long['num'] = movie_long['num'].astype(int)
>>> movie_long.head(9)

  1. 现在可以将数据集拆分为多个较小的表:
>>> movie_table = movie_long[['id', 'year', 'duration', 'rating']]
>>> director_table = movie_long[['id', 'num',
                                 'director', 'director_fb_likes']]
>>> actor_table = movie_long[['id', 'num',
                              'actor', 'actor_fb_likes']]

   

  1. 这些表仍然存在几个问题。movie表将每个电影重复三遍,导演表的每个 ID 都有两行缺失,而一些电影的某些演员有缺失值。 让我们来照顾这​​些问题:
>>> movie_entity = movie_entity.drop_duplicates() \
                               .reset_index(drop=True)
>>> director_entity = director_entity.dropna() \
                                     .reset_index(drop=True)
>>> actor_table = actor_table.dropna() \
                             .reset_index(drop=True)

   

  1. 现在,我们已将观测单位分为各自的表,让我们将原始数据集的内存与这三个表进行比较:
>>> movie.memory_usage(deep=True).sum()
2318234

>>> movie_table.memory_usage(deep=True).sum() + \
    director_table.memory_usage(deep=True).sum() + \
    actor_table.memory_usage(deep=True).sum()
2627306
  1. 实际上,我们的新整理数据会占用更多的内存。 这是可以预期的,因为原始列中的所有数据都被简单地散布到新表中。 新表还每个都有索引,并且其中两个表都有一个额外的num列,这些列占了额外的内存。 但是,我们可以利用以下事实:Facebook 点赞数与电影无关,这意味着每个演员和导演在所有电影中都有一个 Facebook 点赞数。 在执行此操作之前,我们需要创建另一个表,将每个电影映射到每个演员/导演。 首先,创建特定于演员和导演表的id列,以唯一标识每个演员/导演:
>>> director_cat = pd.Categorical(director_table['director'])
>>> director_table.insert(1, 'director_id', director_cat.codes)

>>> actor_cat = pd.Categorical(actor_table['actor'])
>>> actor_table.insert(1, 'actor_id', actor_cat.codes)

  

  1. 我们可以使用这些表形成中间表和唯一的actor/director表。 我们首先使用director表执行此操作:
>>> director_associative = director_table[['id', 'director_id',
                                           'num']]
>>> dcols = ['director_id', 'director', 'director_fb_likes']
>>> director_unique = director_table[dcols].drop_duplicates() \
                                           .reset_index(drop=True)

    

  1. 让我们对actor表做同样的事情:
>>> actor_associative = actor_table[['id', 'actor_id', 'num']]
>>> acols = ['actor_id', 'actor', 'actor_fb_likes']
>>> actor_unique = actor_table[acols].drop_duplicates() \
                                     .reset_index(drop=True)

   

  1. 让我们找出我们的新表消耗了多少内存:
>>> movie_table.memory_usage(deep=True).sum() + \
    director_associative.memory_usage(deep=True).sum() + \
    director_unique.memory_usage(deep=True).sum() + \
    actor_associative.memory_usage(deep=True).sum() + \
    actor_unique.memory_usage(deep=True).sum()
1833402
  1. 现在我们已经标准化了表,我们可以构建一个实体关系图,显示所有表(实体),列和关系。 此图是使用易于使用的 ERDPlus 创建的:

工作原理

导入数据并识别这三个实体后,我们必须为每个观察创建一个唯一的标识符,以便在将电影,演员和导演分成不同的表格后,可以将它们链接在一起。 在第 2 步中,我们只需将 ID 列设置为从零开始的行号。 在第 3 步中,我们使用wide_to_long函数同时meltactordirector列。 它使用列的整数后缀垂直对齐数据,并将此整数后缀放置在索引中。 参数j用于控制其名称。 重复stubnames列表中不在列中的值以与已熔化的列对齐。

在第 4 步中,我们创建三个新表,并在每个表中保留id列。 我们还保留num列以标识确切的director/actor列。 步骤 5 通过删除重复项和缺失值来压缩每个表。

在第 5 步之后,这三个观测单位在各自的表中,但它们仍然包含与原始相同的数据量(还有更多),如步骤 6 所示。要返回memory_usage方法从object数据类型列中获得正确的字节数,必须将deep参数设置为True

每个演员/导演在其各自的表中仅需要一个条目。 我们不能简单地列出演员姓名和 Facebook 点赞的表格,因为无法将演员链接回原始电影。 电影和演员之间的关系称为多对多关系。 每个电影与多个演员相关联,每个演员可以出现在多个电影中。 为了解决此关系,创建了一个中间表或关联表,该表包含电影和演员的唯一标识符(主键)。

要创建关联表,我们必须唯一地标识每个演员/导演。 一种技巧是使用pd.Categorical从每个演员/导演姓名中创建一个分类数据类型。 分类数据类型具有从每个值到整数的内部映射。 在codes属性中可以找到该整数,该属性用作唯一 ID。 要设置关联表的创建,我们将此唯一 ID 添加到actor/director表中。

步骤 8 和步骤 9 通过选择两个唯一标识符来创建关联表。 现在,我们可以将actordirector表简化为唯一的名称和 Facebook 点赞的名称。 这种新的表安排使用的内存比原始表少 20% 。 正式的关系数据库具有实体关系图以可视化表格。 在第 10 步中,我们使用简单的 ERDPlus 工具进行可视化,这大大简化了对表之间关系的理解。

更多

通过将所有表重新结合在一起,可以重新创建原始的movie表。 首先,将关联表连接到actor/director表。 然后旋转num列,并向后添加列前缀:

>>> actors = actor_associative.merge(actor_unique, on='actor_id') \
                              .drop('actor_id', 1) \
                              .pivot_table(index='id', 
                                           columns='num',
                                           aggfunc='first')

>>> actors.columns = actors.columns.get_level_values(0) + '_' + \
                     actors.columns.get_level_values(1).astype(str)

>>> directors = director_associative.merge(director_unique,
                                           on='director_id') \
                                    .drop('director_id', 1) \
                                    .pivot_table(index='id',
                                                 columns='num',
                                                 aggfunc='first')

>>> directors.columns = directors.columns.get_level_values(0) + '_' + \
                        directors.columns.get_level_values(1) \
                                         .astype(str)

这些表现在可以与movie_table结合在一起:

>>> movie2 = movie_table.merge(directors.reset_index(),
                               on='id', how='left') \
                        .merge(actors.reset_index(),
                               on='id', how='left')
>>> movie.equals(movie2[movie.columns])
True

另见

九、组合 Pandas 对象

在本章中,我们将介绍以下主题:

  • 将新行追加到数据帧
  • 将多个数据帧连接在一起
  • 比较特朗普总统和奥巴马总统的支持率
  • 了解concatjoinmerge之间的区别
  • 连接到 SQL 数据库

介绍

可以使用多种选项将两个或多个数据帧或序列组合在一起。append方法最不灵活,仅允许将新行附加到数据帧。concat方法非常通用,可以在任一轴上组合任意数量的数据帧或序列。join方法通过将一个数据帧的列与其他数据帧的索引对齐来提供快速查找。merge方法提供了类似 SQL 的功能,可以将两个数据帧结合在一起。

将新行追加到数据帧

在执行数据分析时,创建新列比创建新行更为常见。 这是因为新的数据行通常代表新的观察结果,而作为分析人员,连续捕获新数据通常不是您的工作。 数据捕获通常留给其他平台,如关系数据库管理系统。 但是,这是一个必不可少的功能,因为它会不时出现。

准备

在本秘籍中,我们将首先使用.loc索引器将行追加到小型数据集,然后过渡到使用append方法。

操作步骤

  1. 读入名称数据集,并将其输出:
>>> names = pd.read_csv('data/names.csv')
>>> names

  1. 让我们创建一个包含一些新数据的列表,并使用.loc索引器设置一个等于该新数据的行标签:
>>> new_data_list = ['Aria', 1]
>>> names.loc[4] = new_data_list
>>> names

  1. .loc索引器使用标签来引用行。 在这种情况下,行标签与整数位置完全匹配。 可以使用非整数标签附加更多行:
>>> names.loc['five'] = ['Zach', 3]
>>> names

  1. 为了更明确地将变量与值相关联,可以使用字典。 同样,在这一步中,我们可以动态选择新的索引标签作为数据帧的长度:
>>> names.loc[len(names)] = {'Name':'Zayd', 'Age':2}
>>> names

  1. 序列还可以保存新数据,并且与字典完全相同:
>>> names.loc[len(names)] = pd.Series({'Age':32,
                                       'Name':'Dean'})
>>> names

  1. 前面的操作全部使用.loc索引运算符就地更改names数据帧。 没有返回的数据帧的单独副本。 在接下来的几个步骤中,我们将研究append方法,该方法不会修改调用数据帧的方法。 而是返回带有附加行的数据帧的新副本。 让我们从原始的names数据帧开始,并尝试追加一行。append的第一个参数必须是另一个数据帧,序列,字典或它们的列表,但不能是步骤 2 中的列表。让我们看看当尝试将字典与append一起使用时会发生什么:
>>> names = pd.read_csv('data/names.csv')
>>> names.append({'Name':'Aria', 'Age':1})
TypeError: Can only append a Series if ignore_index=True or if the Series has a name
  1. 此错误消息似乎有点不正确。 我们正在传递一个数据帧而不是一个序列,但是它为我们提供了如何更正它的说明:
>>> names.append({'Name':'Aria', 'Age':1}, ignore_index=True)

  1. 这有效,但是ignore_index是一个偷偷摸摸的参数。 当设置为True时,旧索引将被完全删除并替换为 0 至n-1之间的RangeIndex。 例如,让我们为names数据帧指定一个索引:
>>> names.index = ['Canada', 'Canada', 'USA', 'USA']
>>> names

  1. 重新运行步骤 7 中的代码,您将获得相同的结果。 原始索引被完全忽略。
  2. 让我们继续使用在索引中包含这些国家/地区字符串的names数据集,并通过append方法使用具有name属性的序列:
>>> s = pd.Series({'Name': 'Zach', 'Age': 3}, name=len(names))
>>> s
Age        3
Name    Zach
Name: 4, dtype: object

>>> names.append(s)

  1. append方法比.loc索引器更灵活。 它支持同时添加多行。 实现此目的的一种方法是使用序列的列表:
>>> s1 = pd.Series({'Name': 'Zach', 'Age': 3}, name=len(names))
>>> s2 = pd.Series({'Name': 'Zayd', 'Age': 2}, name='USA')
>>> names.append([s1, s2])

  1. 仅具有两列的小型数据帧非常简单,可以手动写出所有列名称和值。 当它们变大时,此过程将非常痛苦。 例如,让我们看一下 2016 年棒球数据集:
>>> bball_16 = pd.read_csv('data/baseball16.csv')
>>> bball_16.head()

  1. 该数据集包含 22 列,如果您手动输入新的数据行,则很容易输错列名称或完全忘记其中的一个。 为了帮助避免这些错误,让我们选择一行作为序列,并将to_dict方法链接到该行,以获取示例行作为字典:
>>> data_dict = bball_16.iloc[0].to_dict()
>>> print(data_dict)
{'playerID': 'altuvjo01', 'yearID': 2016, 'stint': 1, 'teamID': 'HOU', 'lgID': 'AL', 'G': 161, 'AB': 640, 'R': 108, 'H': 216, '2B': 42, '3B': 5, 'HR': 24, 'RBI': 96.0, 'SB': 30.0, 'CS': 10.0, 'BB': 60, 'SO': 70.0, 'IBB': 11.0, 'HBP': 7.0, 'SH': 3.0, 'SF': 7.0, 'GIDP': 15.0}
  1. 用字典理解清除旧值,将任何先前的字符串值分配为空字符串,将所有其他字符串值分配为缺失值。 现在,该词典可以用作您要输入的任何新数据的模板:
>>> new_data_dict = {k: '' if isinstance(v, str) else 
                        np.nan for k, v in data_dict.items()}
>>> print(new_data_dict)
{'playerID': '', 'yearID': nan, 'stint': nan, 'teamID': '', 'lgID': '', 'G': nan, 'AB': nan, 'R': nan, 'H': nan, '2B': nan, '3B': nan, 'HR': nan, 'RBI': nan, 'SB': nan, 'CS': nan, 'BB': nan, 'SO': nan, 'IBB': nan, 'HBP': nan, 'SH': nan, 'SF': nan, 'GIDP': nan}

工作原理

.loc索引运算符用于根据行和列标签选择和分配数据。 传递给它的第一个值表示行标签。 在步骤 2 中,names.loc[4]引用带有等于整数 4 的标签的行。此标签当前在数据帧中不存在。 赋值语句使用列表提供的数据创建新行。 如秘籍中所述,此操作将修改names数据帧本身。 如果以前存在标签等于整数 4 的行,则该命令将覆盖该行。 与append方法相比,就地进行此修改使此索引运算符的使用风险更高,该方法从未修改原始调用数据帧。

任何有效的标签都可以与.loc索引运算符一起使用,如步骤 3 所示。不管实际的新标签值是多少,新行始终将附加在最后。 即使使用列表分配也可以,但为清楚起见,最好使用字典,以便我们准确地知道与每个值关联的列,如步骤 4 所示。

步骤 5 显示了一个小技巧,可以动态地将新标签设置为数据帧中的当前行数。 只要索引标签与列名匹配,存储在序列中的数据也将得到正确分配。

其余步骤使用append方法,这是一种仅将新行追加到数据帧的简单方法。 大多数数据帧方法都允许通过axis参数进行行和列操作。append是一个例外,它只能将行追加到数据帧。

如步骤 6 中的错误消息所示,使用映射到值的列名字典不足以进行追加操作,如步骤 6 中的错误消息所示。要正确地追加没有行名的字典,您必须将ignore_index参数设置为True。 步骤 10 向您展示如何通过简单地将字典转换为序列来保持旧索引。 确保使用name参数,该参数随后将用作新的索引标签。 通过将序列列表作为第一个参数传递,可以用append方法添加任意数量的行。

当想要以更大的数据帧以这种方式附加行时,可以通过使用to_dict方法将单行转换为字典,然后使用字典推导式和一些默认值来清除所有旧值,从而避免大量键入和错误。

更多

将单行添加到数据帧是相当昂贵的操作,如果您发现自己编写了将单行数据附加到数据帧的循环,那么您做错了。 让我们首先创建 1,000 行新数据作为序列列表:

>>> random_data = []
>>> for i in range(1000):
        d = dict()
        for k, v in data_dict.items():
            if isinstance(v, str):
                d[k] = np.random.choice(list('abcde'))
            else:
                d[k] = np.random.randint(10)
        random_data.append(pd.Series(d, name=i + len(bball_16)))

>>> random_data[0].head()
2B    3
3B    9
AB    3
BB    9
CS    4
Name: 16, dtype: object

让我们花时间遍历每个项目一次添加一个附件需要花费多长时间:

>>> %%timeit
>>> bball_16_copy = bball_16.copy()
>>> for row in random_data:
        bball_16_copy = bball_16_copy.append(row)
4.88 s ± 190 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

仅花了 1,000 排就花了将近五秒钟。 如果我们改为通过整个序列列表,则速度会大大提高:

>>> %%timeit
>>> bball_16_copy = bball_16.copy()
>>> bball_16_copy = bball_16_copy.append(random_data)
78.4 ms ± 6.2 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

通过传递序列列表,时间已减少到十分之一秒以下。 在内部,pandas 将序列列表转换为单个数据帧,然后进行追加。

将多个数据帧连接在一起

通用的concat函数可将两个或多个数据帧(或序列)垂直和水平连接在一起。 通常,当同时处理多个 Pandas 对象时,连接并不是偶然发生的,而是通过它们的索引对齐每个对象。

准备

在此秘籍中,我们将水平和垂直方向的数据帧与concat函数结合在一起,然后更改参数值以产生不同的结果。

操作步骤

  1. 读取 2016 年和 2017 年的股票数据集,并将其股票代码作为索引:
>>> stocks_2016 = pd.read_csv('data/stocks_2016.csv', 
                              index_col='Symbol')
>>> stocks_2017 = pd.read_csv('data/stocks_2017.csv',
                              index_col='Symbol')

    

  1. 将所有stock数据集放在一个列表中,然后调用concat函数将它们连接在一起:
>>> s_list = [stocks_2016, stocks_2017]
>>> pd.concat(s_list)

  1. 默认情况下,concat函数垂直连接数据帧,一个接一个。 前面的数据帧的一个问题是无法识别每一行的年份。concat函数允许使用keys参数标记每个结果数据帧。 该标签将显示在级联框架的最外层索引级别中,并强制创建多重索引。 同样,为了清楚起见,names参数还可以重命名每个索引级别:
>>> pd.concat(s_list, keys=['2016', '2017'], 
              names=['Year', 'Symbol'])

  1. 也可以通过将axis参数更改为column或 1 来水平连接:
>>> pd.concat(s_list, keys=['2016', '2017'],
              axis='columns', names=['Year', None])

  1. 请注意,当一年中存在股票代号而另一年不存在时,会出现缺失值。 默认情况下,concat函数使用外连接,将列表中每个数据帧的所有行保留在列表中。 但是,它为我们提供了仅在两个数据帧中保留具有相同索引值的行的选项。 这称为内连接。 我们将join参数设置为inner,以更改行为:
>>> pd.concat(s_list, join='inner', keys=['2016', '2017'],
              axis='columns', names=['Year', None])

工作原理

第一个参数是concat函数所需的唯一参数,它必须是 Pandas 对象的列表,通常是数据帧或序列的列表或字典。 默认情况下,所有这些对象将垂直堆叠在另一个之上。 在此秘籍中,仅连接了两个数据帧,但是任何数量的 Pandas 对象都可以工作。 当我们垂直连接时,数据帧通过其列名称对齐。

在此数据集中,所有列名称均相同,因此 2017 年数据中的每个列均在 2016 年数据中的同一列名称下精确对齐。 但是,如步骤 4 所示,将它们水平连接时,只有两个年份的索引标签相匹配 - AAPLTSLA。 因此,这些股票代号在任何一年中都没有缺失值。 可以使用concat进行两种对齐方式,join参数引用的outer(默认)和inner

更多

append方法是concat的精简版本,只能将新行附加到数据帧。 在内部,append仅调用concat函数。 例如,此秘籍中的第 2 步可以复制以下内容:

>>> stocks_2016.append(stocks_2017)

比较特朗普总统和奥巴马总统的支持率

现任美国总统的公众支持是一个经常成为新闻头条的话题,并通过民意测验进行正式衡量。 近年来,这些民意调查的频率迅速增加,并且每周都有大量新的数据发布。 有许多不同的民意测验者都有各自的问题和方法来捕获其数据,因此,数据之间存在相当多的可变性。 来自加利福尼亚大学圣塔芭芭拉分校的美国总统职位项目每天提供的总批准评级低至单个数据点。

与本书中的大多数秘籍不同,该数据在 CSV 文件中不易获得。 通常,作为数据分析师,您将需要在 Web 上查找数据,并使用可以将其抓取为可通过本地工作站解析的格式的工具。

准备

在本秘籍中,我们将使用read_html函数,该函数功能强大,可以在线从表中抓取数据并将其转换为数据帧。 您还将学习如何检查网页以查找某些元素的基础 HTML。 我使用 Google Chrome 浏览器作为浏览器,建议您将其或 Firefox 用于基于 Web 的步骤。

操作步骤

  1. 导航至唐纳德·特朗普总统的美国总统职位批准页。 您应该获得一个包含时间序列图的页面,该页面紧随其后的是表格中的数据:

  1. read_html函数能够从网页上抓取表格并将其数据放入数据帧中。 它最适合简单的 HTML 表,并提供一些有用的参数来选择所需的确切表,以防同一页上有多个表。 让我们继续使用read_html作为其默认值,它将以列表形式将所有表作为数据帧返回:
>>> base_url = 'http://www.presidency.ucsb.edu/data/popularity.php?pres={}'
>>> trump_url = base_url.format(45)
>>> df_list = pd.read_html(trump_url)
>>> len(df_list)
14
  1. 该函数返回了 14 个表,乍一看似乎很荒谬,因为该网页似乎只显示了大多数人会识别为表的单个元素。read_html函数正式搜索以<table开头的 HTML 表元素。 我们通过右键单击批准数据表并选择查看或查看元素来检查 HTML 页面:

  1. 这将打开控制台,这是用于 Web 开发的非常强大的工具。 对于本秘籍,我们仅需要几个任务即可使用。 所有控制台都允许您在 HTML 中搜索特定的单词。 让我们搜索单词table。 我的浏览器找到 15 个不同的 HTML 表格,非常接近read_html返回的数字:

  1. 让我们开始检查df_list中的数据帧:
>>> df0 = df_list[0]
>>> df0.shape
(308, 1794)

>>> df0.head(7)

  1. 回顾网页,从 2017 年 1 月 22 日开始直到批准数据收集的那一天(即 2017 年 9 月 25 日),几乎每天都有批准表存在。这是八个多月或 250 行数据,该数据在某种程度上接近第一个表中的 308 行。 扫描其余的表,您会发现发现了许多空的,毫无意义的表,以及网页中实际上与表不相似的不同部分的表。 让我们使用read_html函数的一些参数来帮助我们选择所需的表。 我们可以使用match参数在表中搜索特定的字符串。 让我们搜索其中带有单词Start Date的表:
>>> df_list = pd.read_html(trump_url, match='Start Date')
>>> len(df_list)
3
  1. 通过在表中搜索特定的字符串,我们将表的数量减少到只有三个。 另一个有用的参数是attrs,它接受 HTML 属性及其值配对的字典。 我们想为我们的特定表找到一些独特的属性。 为此,让我们再次在数据表中单击鼠标右键。 这次,请确保单击表格标题之一的最上方。 例如,右键单击President,然后再次选择检查或检查元素:

  1. 您选择的元素应突出显示。 实际上,这不是我们感兴趣的元素。继续查看,直到遇到以<table开头的 HTML 标签。 等号左边的所有单词都是属性或attrs,右边的是值。 让我们在搜索中使用align属性及其值center
>>> df_list = pd.read_html(trump_url, match='Start Date',
                           attrs={'align':'center'})
>>> len(df_list)
1

>>> trump = df_list[0]
>>> trump.shape
(249, 19)

>>> trump.head(8)

  1. 我们仅与一个表匹配,并且行数非常接近起始日期和最后日期之间的总天数。 查看数据,似乎我们确实找到了要查找的表。 六个列的名称似乎在第 4 行。我们可以走得更远,更精确地选择要跳过的行以及要使用skiprowsheader参数的列名称。 我们还可以使用parse_dates参数确保将开始日期和结束日期正确地强制为正确的数据类型:
>>> df_list = pd.read_html(trump_url, match='Start Date',
                           attrs={'align':'center'}, 
                           header=0, skiprows=[0,1,2,3,5], 
                           parse_dates=['Start Date',
                                        'End Date'])
>>> trump = df_list[0]
>>> trump.head()

  1. 这几乎是我们想要的,除了缺少值的列。 让我们使用dropna方法删除缺少所有值的列:
>>> trump = trump.dropna(axis=1, how='all')
>>> trump.head()

  1. 让我们用ffill方法向前填充President列中的缺失值。 首先让我们检查其他列中是否缺少任何值:
>>> trump.isnull().sum()
President         242
Start Date          0
End Date            0
Approving           0
Disapproving        0
unsure/no data      0
dtype: int64

>>> trump = trump.ffill()
trump.head()

  1. 最后,检查数据类型以确保它们正确是很重要的:
>>> trump.dtypes
President                 object
Start Date        datetime64[ns]
End Date          datetime64[ns]
Approving                  int64
Disapproving               int64
unsure/no data             int64
dtype: object
  1. 让我们构建一个将所有步骤组合在一起的函数,以自动化检索任何总裁的批准数据的过程:
>>> def get_pres_appr(pres_num):
        base_url = 'http://www.presidency.ucsb.edu/data/popularity.php?pres={}'
        pres_url = base_url.format(pres_num)
        df_list = pd.read_html(pres_url, match='Start Date',
                               attrs={'align':'center'}, 
                               header=0, skiprows=[0,1,2,3,5], 
                               parse_dates=['Start Date',
                                            'End Date'])
        pres = df_list[0].copy()
        pres = pres.dropna(axis=1, how='all')
        pres['President'] = pres['President'].ffill()
        return pres.sort_values('End Date') \
                   .reset_index(drop=True)
  1. 唯一的参数pres_num表示每个总统的批准号。 巴拉克·奥巴马是美国第 44 任总统; 将 44 传递给get_pres_appr函数以获取其批准号:
>>> obama = get_pres_appr(44)
>>> obama.head()

  1. 在总统富兰克林·罗斯福第三任期期间,有总统支持率的评级数据可追溯到 1941 年。 通过我们的自定义函数以及concat函数,可以从该站点获取所有总统批准评级数据。 现在,让我们获取最后五位总统的支持率数据,并输出每位总统的前三行:
>>> pres_41_45 = pd.concat([get_pres_appr(x) for x in range(41,46)],
                            ignore_index=True)
>>> pres_41_45.groupby('President').head(3)

  1. 在继续之前,让我们确定是否有多个批准评级的日期:
>>> pres_41_45['End Date'].value_counts().head(8)
1990-08-26    2
1990-03-11    2
1999-02-09    2
2013-10-10    2
1990-08-12    2
1992-11-22    2
1990-05-22    2
1991-09-30    1
Name: End Date, dtype: int64
  1. 只有几天有重复的值。 为了简化分析,让我们仅保留重复日期存在的第一行:
>>> pres_41_45 = pres_41_45.drop_duplicates(subset='End Date')
  1. 让我们获得一些关于数据的摘要统计信息:
>>> pres_41_45.shape
(3679, 6)

>>> pres_41_45['President'].value_counts()
Barack Obama          2786
George W. Bush         270
Donald J. Trump        243
William J. Clinton     227
George Bush            153
Name: President, dtype: int64

>>> pres_41_45.groupby('President', sort=False) \
                       .median().round(1)

  1. 让我们在同一张图表上绘制每个总裁的支持率。 为此,我们将按每位总裁分组,遍历每组,并分别绘制每个日期的批准等级:
>>> from matplotlib import cm
>>> fig, ax = plt.subplots(figsize=(16,6))

>>> styles = ['-.', '-', ':', '-', ':']
>>> colors = [.9, .3, .7, .3, .9]
>>> groups = pres_41_45.groupby('President', sort=False)

>>> for style, color, (pres, df) in zip(styles, colors, groups):
        df.plot('End Date', 'Approving', ax=ax,
                label=pres, style=style, color=cm.Greys(color), 
                title='Presedential Approval Rating')

  1. 此图表将所有总统依次排列。 通过将批准等级与在职天数作图,我们可以更简单地比较它们。 让我们创建一个新变量来代表上班天数:
>>> days_func = lambda x: x - x.iloc[0]
>>> pres_41_45['Days in Office'] = pres_41_45.groupby('President') \
                                             ['End Date'] \
                                             .transform(days_func)
>>> pres_41_45.groupby('President').head(3)

  1. 自总统任期以来,我们已经成功地为每一行分配了相对天数。 有趣的是,新列Days in Office具有其值的字符串表示形式。 让我们检查其数据类型:
>>> pres_41_45.dtypes
...
Days in Office    timedelta64[ns]
dtype: object
  1. Days in Office列是具有纳秒精度的timedelta64对象。 这比所需的精度要高得多。 让我们通过仅获取日期将数据类型更改为整数:
>>> pres_41_45['Days in Office'] = pres_41_45['Days in Office'] \
                                             .dt.days
>>> pres_41_45['Days in Office'].head()
0     0
1    32
2    35
3    43
4    46
Name: Days in Office, dtype: int64
  1. 我们可以按照与步骤 19 中相似的方式来绘制此数据,但是存在一种完全不涉及任何循环的方法。 默认情况下,在数据帧上调用plot方法时,pandas 尝试将数据的每一列绘制为线图,并使用索引作为 x 轴。 知道了这一点之后,我们就来讨论一下数据,以便每位总裁都拥有自己的专栏以进行审批:
>>> pres_pivot = pres_41_45.pivot(index='Days in Office',
                                  columns='President',
                                  values='Approving')
>>> pres_pivot.head()

  1. 现在,每个总裁都有自己的批准等级列,我们可以直接对每个列进行绘制而无需分组。 为了减少剧情中的混乱情况,我们将仅绘制巴拉克·奥巴马和唐纳德·特朗普:
>>> plot_kwargs = dict(figsize=(16,6), color=cm.gray([.3, .7]), 
                       style=['-', '--'], title='Approval Rating')
>>> pres_pivot.loc[:250, ['Donald J. Trump', 'Barack Obama']] \
              .ffill().plot(**plot_kwargs)

工作原理

通常在到达所需的一个或多个表之前多次调用read_html。 您可以使用两个主要参数来指定表matchattrs。 提供给match的字符串用于查找表中实际文本的精确匹配。 这是将显示在网页本身上的文本。 另一方面,attrs参数搜索在表标记<table开始之后直接找到的 HTML 表属性。 要查看更多表格属性,请访问 W3Schools

在步骤 8 中找到表格后,我们仍然可以利用其他一些参数来简化操作。 HTML 表通常不会直接转换为漂亮的数据帧。 通常缺少列名,多余的行和未对齐的数据。 在此秘籍中,skiprows传递了行号列表,以便在读取文件时跳过。 它们对应于步骤 8 的数据帧输出中缺少值的行。header参数还用于指定列名称的位置。 请注意,header等于零,乍一看似乎是错误的。 每当header参数与skiprows结合使用时,将首先跳过各行,从而为每行产生一个新的整数标签。 正确的列名称位于第 4 行中,但是当我们跳过第 0 至 3 行时,其新的整数标签为 0。

在步骤 11 中,ffill方法垂直填充所有丢失的值,并向下填充最后一个非丢失的值。 该方法只是fillna(method='ffill')的快捷方式。

第 13 步构建了一个由所有先前步骤组成的函数,可以自动获得任何总裁的批准等级,前提是您拥有批准号。 功能上有一些差异。 并非将ffill方法应用于整个数据帧,我们仅将其应用于President列。 在 Trump 的数据帧中,其他列没有丢失数据,但这不能保证所有抓取的表在其他列中都不会丢失数据。 函数的最后一行以更自然的方式对日期进行排序,以便从最旧到最新进行数据分析。 这也改变了索引的顺序,因此我们将其与reset_index丢弃,以使其再次从零开始。

步骤 16 显示了一个常见的 Pandas 习惯用法,用于在将它们与concat函数组合在一起之前,将多个类似索引的数据帧收集到一个列表中。 连接到单个数据帧后,我们应该目视检查它以确保其准确性。 一种方法是通过对数据进行分组然后在每组上使用head方法来浏览每位总裁部分的前几行。

第 18 步的汇总统计数据很有趣,因为每位继任总统的中位数批准率均低于上一任总统。 推断数据会导致天真的预测未来几位总统的支持率为负面。

步骤 19 中的绘图代码相当复杂。 您可能想知道为什么我们首先需要遍历groupby对象。 在数据帧的当前结构中,它无法基于单个列中的值绘制不同的组。 但是,第 23 步显示了如何设置数据帧,以便 Pandas 可以直接绘制每个总统的数据,而不会像这样循环。

要了解步骤 19 中的绘图代码,您必须首先意识到groupby对象是可迭代的,并且在迭代过程中会产生一个包含当前组的元组(此处仅是总统的名字)和该组的子数据帧。 该groupby对象与控制绘图的颜色和线条样式的值一起压缩。 我们从 matplotlib 导入了调色板模块cm,该模块包含数十种不同的调色板。 在 0 到 1 之间传递一个float值会从该调色板中选择一种特定的颜色,我们在plot方法中将其与color参数一起使用。 同样重要的是要注意,我们必须创建图形fig和绘图表面ax,以确保将每个批准线放置在同一图形上。 在循环的每次迭代中,我们使用具有相同名称的参数ax的相同绘图表面。

为了更好地比较总统之间的差异,我们创建了一个新列,该列等于上任天数。 我们从每个主席组的其余日期中减去第一个日期。 当减去两个datetime64列时,结果是一个timedelta64对象,该对象表示一段时间,在这种情况下为几天。 如果我们将列的精度保留为纳秒,则通过使用特殊的dt访问器返回天数,x 轴将同样显示过多的精度。

至关重要的一步出现在步骤 23 中。我们对数据进行结构设计,以使每位总裁在其批准等级上都有一个唯一的列。 Pandas 为每一列单独一行。 最后,在第 24 步中,我们使用.loc索引器同时选择前 250 天(行)以及仅特朗普和奥巴马的列。ffill方法用于少数总统在特定日期缺少值的情况。 在 Python 中,可以通过在包含字典解压缩的过程中在它们前面加上**来将包含参数名称及其值的字典传递给函数。

更多

步骤 19 中的图显示了大量噪声,如果对其进行了平滑处理,则数据可能更易于解释。 一种常见的平滑方法称为滚动平均值。 Pandas 为数据帧和groupby对象提供了rolling方法。 它通过返回一个对象以等待对其执行附加操作,从而类似于groupby方法。 创建它时,必须将窗口的大小作为第一个参数传递,它可以是整数或日期偏移量字符串。

在此示例中,我们使用日期偏移字符串90D进行 90 天移动平均。on参数指定从中计算滚动窗口的列:

>>> pres_rm = pres_41_45.groupby('President', sort=False) \
                        .rolling('90D', on='End Date')['Approving'] \
                        .mean()
>>> pres_rm.head()
President    End Date   
George Bush  1989-01-26    51.000000
             1989-02-27    55.500000
             1989-03-02    57.666667
             1989-03-10    58.750000
             1989-03-13    58.200000
Name: Approving, dtype: float64

在这里,我们可以使用unstack方法重新构造数据,使其看起来与步骤 23 的输出类似,然后进行绘制:

>>> styles = ['-.', '-', ':', '-', ':']
>>> colors = [.9, .3, .7, .3, .9]
>>> color = cm.Greys(colors)
>>> title='90 Day Approval Rating Rolling Average'
>>> plot_kwargs = dict(figsize=(16,6), style=styles,
                       color = color, title=title)
>>> correct_col_order = pres_41_45.President.unique()

>>> pres_rm.unstack('President')[correct_col_order].plot(**plot_kwargs)

另见

了解concatjoinmerge之间的区别

mergejoin数据帧(而不是序列)方法以及concat函数都提供了非常相似的功能,可以将多个 Pandas 对象组合在一起。 由于它们是如此相似,并且它们在某些情况下可以相互复制,因此何时以及如何正确使用它们会变得非常混乱。 为了帮助弄清它们之间的差异,请查看以下概述:

  • concat
    • Pandas 函数
    • 垂直或水平组合两个或多个 Pandas 对象
    • 仅在索引上对齐
    • 每当索引中出现重复项时发生错误
    • 默认为外连接,带有内连接选项
  • join
    • 数据帧方法
    • 水平组合两个或多个 Pandas 对象
    • 将调用的数据帧的列或索引与其他对象的索引(而不是列)对齐
    • 通过执行笛卡尔积来处理连接列/索引上的重复值
    • 默认为左连接,带有内,外和右选项
  • merge
    • 数据帧方法
    • 准确地水平合并两个数据帧
    • 将调用的数据帧的列/索引与其他数据帧的列/索引对齐
    • 通过执行笛卡尔积来处理连接列/索引上的重复值
    • 默认为内连接,带有左,外和右选项

join方法的第一个参数是other,它可以是单个数据帧/序列,也可以是任意数量的数据帧/序列的列表。

准备

在此秘籍中,我们将执行组合数据帧所需的。 第一种情况使用concat更简单,而第二种情况使用merge更简单。

操作步骤

  1. 让我们使用循环而不是对read_csv函数的三个不同调用将 2016 年,2017 年和 2018 年的股票数据读入数据帧的列表中。 Jupyter 笔记本当前仅允许将一个数据帧显示在一行上。 但是,有一种方法可以在IPython库的帮助下自定义 HTML 输出。 用户定义的display_frames函数接受数据帧的列表并将它们全部输出到一行:
>>> from IPython.display import display_html

>>> years = 2016, 2017, 2018
>>> stock_tables = [pd.read_csv('data/stocks_{}.csv'.format(year),
                                index_col='Symbol') 
                    for year in years]

>>> def display_frames(frames, num_spaces=0):
        t_style = '<table style="display: inline;"'
        tables_html = [df.to_html().replace('<table', t_style) 
                       for df in frames]

        space = '&nbsp;' * num_spaces
        display_html(space.join(tables_html), raw=True)

>>> display_frames(stock_tables, 30)
>>> stocks_2016, stocks_2017, stocks_2018 = stock_tables

  1. concat函数是唯一能够垂直组合数据帧的函数。 让我们通过将列表stock_tables传递给它:
>>> pd.concat(stock_tables, keys=[2016, 2017, 2018])

  1. 通过将axis参数更改为columns,它也可以水平组合数据帧:
>>> pd.concat(dict(zip(years,stock_tables)), axis='columns')

  1. 现在我们已经开始水平组合数据帧了,我们可以使用joinmerge方法来复制concat的功能。 在这里,我们使用join方法来组合stock_2016stock_2017数据帧。 默认情况下,数据帧按其索引对齐。 如果任何一列具有相同的名称,则必须为lsuffixrsuffix参数提供一个值,以在结果中区分它们:
>>> stocks_2016.join(stocks_2017, lsuffix='_2016',
                     rsuffix='_2017', how='outer')

  1. 为了精确复制步骤 3 中concat函数的输出,我们可以将数据帧的列表传递给join方法:
>>> other = [stocks_2017.add_suffix('_2017'),
             stocks_2018.add_suffix('_2018')]
>>> stocks_2016.add_suffix('_2016').join(other, how='outer')

  1. 让我们检查一下它们是否实际上完全相等:
>>> stock_join = stocks_2016.add_suffix('_2016').join(other, 
                                                      how='outer')
>>> stock_concat = pd.concat(dict(zip(years,stock_tables)),
                             axis='columns')
>>> level_1 = stock_concat.columns.get_level_values(1)
>>> level_0 = stock_concat.columns.get_level_values(0).astype(str)
>>> stock_concat.columns = level_1 + '_' + level_0
>>> stock_join.equals(stock_concat)
True
  1. 现在,让我们转向merge,与concatjoin不同,它可以将两个数据帧恰好结合在一起。 默认情况下,merge尝试对齐每个数据帧中具有相同名称的列中的值。 但是,您可以通过将布尔参数left_indexright_index设置为True来选择使其与索引对齐。 让我们将 2016 年和 2017 年的股票数据合并在一起:
>>> stocks_2016.merge(stocks_2017, left_index=True, 
                      right_index=True)

  1. 默认情况下,合并使用内连接,并自动为名称相同的列提供后缀。 让我们更改为外连接,然后执行 2018 数据的另一个外连接以完全复制concat
>>> step1 = stocks_2016.merge(stocks_2017, left_index=True, 
                              right_index=True, how='outer',
                              suffixes=('_2016', '_2017'))

>>> stock_merge = step1.merge(stocks_2018.add_suffix('_2018'), 
                              left_index=True, right_index=True,
                              how='outer')

>>> stock_concat.equals(stock_merge)
True
  1. 现在,让我们将比较转到我们希望将列的值对齐而不是索引或列标签本身对齐的数据集。merge方法正是针对这种情况而构建的。 让我们看一下两个新的小型数据集food_pricesfood_transactions
>>> names = ['prices', 'transactions']
>>> food_tables = [pd.read_csv('data/food_{}.csv'.format(name)) 
                    for name in names]
>>> food_prices, food_transactions = food_tables
>>> display_frames(food_tables, 30)

  1. 如果我们想查找每笔交易的总金额,则需要在itemstore列上加入以下表格:
>>> food_transactions.merge(food_prices, on=['item', 'store'])

  1. 现在,价格已正确与其对应的物料和商店对齐,但是存在问题。 客户 2 共有四个steak项目。 由于steak项目在每个表中针对B的存储表都出现两次,因此在它们之间会产生笛卡尔积,导致四行。 此外,请注意缺少coconut项目,因为没有相应的价格。 让我们解决这两个问题:
>>> food_transactions.merge(food_prices.query('Date == 2017'),
                            how='left')

  1. 我们可以使用join方法复制它,但是我们必须首先将food_prices数据帧的连接列放入索引中:
>>> food_prices_join = food_prices.query('Date == 2017') \
                                  .set_index(['item', 'store'])
>>> food_prices_join

  1. join方法仅与传递的数据帧的索引对齐,但可以使用调用数据帧的索引或列。 要使用列在调用数据帧上对齐,您需要将它们传递给on参数:
>>> food_transactions.join(food_prices_join, on=['item', 'store'])
  1. 输出与步骤 11 的结果完全匹配。 要使用concat方法复制此内容,您需要将该项放置并存储列到两个数据帧的索引中。 但是,在此特定情况下,由于在至少一个数据帧(具有项steak和存储B中)出现重复的索引值,将产生错误:
>>> pd.concat([food_transactions.set_index(['item', 'store']), 
               food_prices.set_index(['item', 'store'])],
              axis='columns')
Exception: cannot handle a non-unique multi-index!

工作原理

同时导入多个数据帧时,重复编写read_csv函数可能很麻烦。 自动执行此过程的一种方法是将所有文件名放在列表中,并使用for循环遍历它们。 这是在步骤 1 中通过列表理解完成的。

此步骤的其余部分将构建一个函数,以在 Jupyter 笔记本的同一行输出中显示多个数据帧。 所有数据帧都有一个to_html方法,该方法返回表的原始 HTML 字符串表示形式。 通过将display属性更改为inline,可以更改每个表的 CSS(级联样式表),以便元素在水平方向上彼此相邻而不是垂直显示。 要在笔记本中正确呈现表格,您必须使用 IPython 库提供的辅助函数read_html

在第 1 步结束时,我们将数据帧的列表解压缩为它们自己的适当命名的变量,以便可以轻松,清晰地引用每个表。 关于数据帧的列表的好处是,它是concat函数的确切要求,如步骤 2 所示。请注意,步骤 2 如何使用keys参数命名每个数据块。 也可以通过将字典传递给concat来完成,如步骤 3 所示。

在步骤 4 中,我们必须将join的类型更改为outer,以包括所传递的数据帧中所有在调用数据帧中不存在索引的行。 在步骤 5 中,传递的数据帧的列表不能有任何共同的列。 尽管有rsuffix参数,但仅在传递单个数据帧而不是它们的列表时才起作用。 为了解决此限制,我们预先使用add_suffix方法更改列的名称,然后调用join方法。

在第 7 步中,我们使用merge,默认情况下,将对齐两个数据帧中相同的所有列名称。 要更改此默认行为,并对齐一个或两个的索引,请将left_indexright_index参数设置为True。 步骤 8 通过两个合并请求完成复制。 如您所见,当在其索引上对齐多个数据帧时,concat通常比合并好得多。

在第 9 步中,我们切换档位以关注merge具有优势的情况。merge方法是唯一能够按列值对齐调用和传递的数据帧的方法。 第 10 步向您展示了合并两个数据帧有多么容易。on参数不是必需的,但为清楚起见而提供。

不幸的是,如第 10 步所示,在合并数据帧时复制或删除数据非常容易。在合并数据后花一些时间进行健全性检查至关重要。 在这种情况下,food_prices数据集在商店B中具有steak的重复价格,因此我们通过在步骤 11 中仅查询当前年份来消除该行。我们还更改为左连接,以确保每笔交易无论是否存在价格,都会保留。

在这些实例中可以使用join,但是必须首先将传递的数据帧中的所有列移入索引。 最后,每当您打算按列中的值对齐数据时,concat都不是一个好的选择。

更多

可以在不知道文件名的情况下将所有文件从特定目录读取到数据帧中。 Python 提供了几种遍历目录的方法,其中glob模块是一种流行的选择。 汽油价格目录包含五个不同的 CSV 文件,每个文件具有从 2007 年开始的特定等级汽油的每周价格。每个文件只有两列-星期几和价格。 这是一种遍历所有文件,将它们读入数据帧并将它们全部与concat函数组合在一起的理想情况。glob模块具有glob函数,该函数采用一个参数-您要作为字符串迭代的目录的位置。 要获取目录中的所有文件,请使用字符串*。 在此示例中,*.csv仅返回以.csv结尾的文件。glob函数的结果是一个字符串文件名列表,可以直接将其传递给read_csv函数:

>>> import glob

>>> df_list = []
>>> for filename in glob.glob('data/gas prices/*.csv'):
        df_list.append(pd.read_csv(filename, index_col='Week',
                       parse_dates=['Week']))

>>> gas = pd.concat(df_list, axis='columns')
>>> gas.head()

另见

连接到 SQL 数据库

要成为一名认真的数据分析师,几乎可以肯定,您必须学习一些 SQL。 世界上许多数据都存储在接受 SQL 语句的数据库中。 关系数据库管理系统有许多种,其中 SQLite 是最受欢迎和易于使用的系统之一。

准备

我们将探索 SQLite 提供的 Chinook 示例数据库,其中包含音乐商店的 11 个数据表。 首先进入适当的关系数据库时,最好的事情之一就是研究数据库图(有时称为实体关系图) ,以更好地了解表之间的关系。 下图在导航此秘籍时将非常有帮助:

为了使此秘籍生效,您将需要安装sqlalchemy Python 包。 如果您安装了 Anaconda 发行版,则应该已经可以使用它。 与数据库建立连接时,SQLAlchemy 是首选的 Pandas 工具。 在本秘籍中,您将学习如何连接到 SQLite 数据库。 然后,您将问两个不同的查询,并通过使用merge方法将表连接在一起来回答它们。

操作步骤

  1. 在开始从chinook数据库中读取表之前,我们需要设置我们的 SQLAlchemy 引擎:
>>> from sqlalchemy import create_engine
>>> engine = create_engine('sqlite:///data/chinook.db')
  1. 现在,我们可以回到 Pandas 世界,并在剩下的秘籍中呆在那里。 让我们完成一个简单的命令,并使用read_sql_table函数读取tracks表。 表的名称是第一个参数,SQLAlchemy 引擎是第二个参数:
>>> tracks = pd.read_sql_table('tracks', engine)
>>> tracks.head()

  1. 对于本秘籍的其余部分,我们将在数据库图的帮助下回答几个不同的特定查询。 首先,让我们找到每种流派的平均歌曲长度:
>>> genre_track = genres.merge(tracks[['GenreId', 'Milliseconds']], 
                               on='GenreId', how='left') \
                        .drop('GenreId', axis='columns')

>>> genre_track.head()

  1. 现在我们可以轻松找到每种流派的每首歌曲的平均长度。 为了帮助简化解释,我们将Milliseconds列转换为timedelta数据类型:
>>> genre_time = genre_track.groupby('Name')['Milliseconds'].mean()
>>> pd.to_timedelta(genre_time, unit='ms').dt.floor('s')
                                             .sort_values()
Name
Rock And Roll        00:02:14
Opera                00:02:54
Hip Hop/Rap          00:02:58
...
Drama                00:42:55
Science Fiction      00:43:45
Sci Fi & Fantasy     00:48:31
Name: Milliseconds, dtype: timedelta64[ns]
  1. 现在,让我们找出每个客户花费的总金额。 我们需要将customersinvoicesinvoice_items表都相互连接:
>>> cust = pd.read_sql_table('customers', engine, 
                             columns=['CustomerId','FirstName',
                                      'LastName'])
>>> invoice = pd.read_sql_table('invoices', engine, 
                                 columns=['InvoiceId','CustomerId'])
>>> ii = pd.read_sql_table('invoice_items', engine, 
                            columns=['InvoiceId', 'UnitPrice',
                                     'Quantity'])

>>> cust_inv = cust.merge(invoice, on='CustomerId') \
                   .merge(ii, on='InvoiceId')
>>> cust_inv.head()

  1. 现在,我们可以将数量乘以单价,然后找到每个客户花费的总金额:
>>> total = cust_inv['Quantity'] * cust_inv['UnitPrice']
>>> cols = ['CustomerId', 'FirstName', 'LastName']
>>> cust_inv.assign(Total = total).groupby(cols)['Total'] \
                                  .sum() \
                                  .sort_values(ascending=False) \
                                  .head()
CustomerId  FirstName  LastName  
6           Helena     Holý          49.62
26          Richard    Cunningham    47.62
57          Luis       Rojas         46.62
46          Hugh       O'Reilly      45.62
45          Ladislav   Kovács        45.62
Name: Total, dtype: float64

工作原理

create_engine函数需要连接字符串才能正常工作。 SQLite 的连接字符串非常简单,它只是数据库的位置,位于数据目录中。 其他关系数据库管理系统具有更复杂的连接字符串。 您将需要提供用户名,密码,主机名,端口以及(可选)数据库。 您还需要提供 SQL 方言和驱动程序。 连接字符串的一般格式如下:dialect+driver://username:password@host:port/database。 您特定的关系数据库的驱动程序可能需要单独安装。

一旦创建了引擎,就可以使用步骤 2 中的read_sql_table函数将整个表选择到数据帧中非常容易。数据库中的每个表都有一个主键,该主键唯一地标识每一行。 在图中用图形符号标识它。 在第 3 步中,我们通过GenreId将流派链接到曲目。 因为我们只关心轨道长度,所以在执行合并之前,将轨道数据帧修剪为仅需要的列。 合并表格后,我们可以使用基本的groupby操作来回答查询。

我们进一步走了一步,将整数毫秒转换为更容易阅读的时间增量对象。 键以字符串形式传入正确的度量单位。 现在我们有了时间增量序列,我们可以使用dt属性访问floor方法,该方法将时间向下舍入到最接近的秒。

回答步骤 5 所需的查询涉及三个表。 通过将表传递给columns参数,可以将表显着减少到仅需要的列。 使用merge时,具有相同名称的连接列将不保留。 在第 6 步中,我们可以为价格乘以数量分配一列,内容如下:

cust_inv['Total'] = cust_inv['Quantity'] * cust_inv['UnitPrice']

以这种方式分配列没有错。 我们选择使用assign方法动态创建新列,以允许连续的方法链。

更多

如果您精通 SQL,则可以将 SQL 查询作为字符串编写,并将其传递给read_sql_query函数。 例如,以下将重现步骤 4 的输出:

>>> sql_string1 = '''
    select 
        Name, 
        time(avg(Milliseconds) / 1000, 'unixepoch') as avg_time
    from (
            select 
                g.Name, 
                t.Milliseconds
            from 
                genres as g 
            join
                tracks as t
                on 
                    g.genreid == t.genreid
         )
    group by 
        Name
    order by 
         avg_time
'''
>>> pd.read_sql_query(sql_string1, engine)

要重现步骤 6 的答案,请使用以下 SQL 查询:

>>> sql_string2 = '''
    select 
          c.customerid, 
          c.FirstName, 
          c.LastName, 
          sum(ii.quantity * ii.unitprice) as Total
    from
         customers as c
    join
         invoices as i
              on c.customerid = i.customerid
    join
        invoice_items as ii
              on i.invoiceid = ii.invoiceid
    group by
        c.customerid, c.FirstName, c.LastName
    order by
        Total desc
'''
>>> pd.read_sql_query(sql_string2, engine)

另见

十、时间序列分析

在本章中,我们将介绍以下主题:

  • 了解 Python 和 Pandas 日期工具之间的区别
  • 智能分割时间序列
  • 使用仅适用于日期时间索引的方法
  • 计算每周的犯罪数量
  • 分别汇总每周犯罪和交通事故
  • 按工作日和年份衡量犯罪
  • 使用日期时间索引和匿名函数进行分组
  • 按时间戳和另一列分组
  • 使用merge_asof,发现上次犯罪率降低了 20%

介绍

Pandas 的根源在于分析金融时间序列数据。 作者 Wes McKinney 当时对可用的 Python 工具并不满意,因此决定在他工作的对冲基金中建立 Pandas 来满足自己的需求。 从广义上讲,时间序列只是随时间推移收集的数据点。 最典型地,时间在每个数据点之间平均间隔。 Pandas 在处理日期,在不同时间段内进行汇总,对不同时间段进行采样等方面具有出色的功能。

了解 Python 和 Pandas 日期工具之间的区别

在介绍 Pandas 之前,了解并了解 Python 核心的日期和时间功能可能会有所帮助。datetime模块提供了三种不同的数据类型,datetimedatetime。 正式而言,date是一个由年,月和日组成的时刻。 例如,2013 年 6 月 7 日为日期。time由小时,分钟,秒和微秒(百万分之一秒)组成,并且未附加到任何日期。 时间的示例是 12 小时 30 分钟。datetime由日期和时间这两个元素共同组成。

另一方面,Pandas 有一个封装日期和时间的对象,称为Timestamp。 它具有纳秒级(十亿分之一秒)的精度,并且源自 NumPy 的datetime64数据类型。 Python 和 Pandas 都具有timedelta对象,在进行日期加/减时很有用。

准备

在本秘籍中,我们将首先探索 Python 的datetime模块,然后转向 Pandas 中相应的高级日期工具。

操作步骤

  1. 首先,将datetime模块导入我们的名称空间并创建datetimedatetime对象:
>>> import datetime

>>> date = datetime.date(year=2013, month=6, day=7)
>>> time = datetime.time(hour=12, minute=30, 
                         second=19, microsecond=463198)
>>> dt = datetime.datetime(year=2013, month=6, day=7, 
                           hour=12, minute=30, second=19, 
                           microsecond=463198)

>>> print("date is ", date)
>>> print("time is", time)
>>> print("datetime is", dt)

date is 2013-06-07 
time is 12:30:19.463198 
datetime is 2013-06-07 12:30:19.463198
  1. 让我们构造并打印出timedelta对象,这是datetime模块中的另一种主要数据类型:
>>> td = datetime.timedelta(weeks=2, days=5, hours=10,
                            minutes=20, seconds=6.73,
                            milliseconds=99, microseconds=8)
>>> print(td)
19 days, 10:20:06.829008
  1. 将此timedelta添加/减去到步骤 1 中的datedatetime对象中:
>>> print('new date is', date + td)
>>> print('new datetime is', dt + td)
new date is 2013-06-26
new datetime is 2013-06-26 22:50:26.292206
  1. 尝试将timedelta添加到time对象是不可能的:
>>> time + td
TypeError: unsupported operand type(s) for +: 'datetime.time' and 'datetime.timedelta'
  1. 让我们看一下 Pandas 及其Timestamp对象,这是具有纳秒精度的时间片刻。Timestamp构造器非常灵活,可以处理各种输入:
>>> pd.Timestamp(year=2012, month=12, day=21, hour=5,
                 minute=10, second=8, microsecond=99)
Timestamp('2012-12-21 05:10:08.000099')

>>> pd.Timestamp('2016/1/10') Timestamp('2016-01-10 00:00:00')

>>> pd.Timestamp('2014-5/10') Timestamp('2014-05-10 00:00:00')

>>> pd.Timestamp('Jan 3, 2019 20:45.56') Timestamp('2019-01-03 20:45:33')

>>> pd.Timestamp('2016-01-05T05:34:43.123456789') Timestamp('2016-01-05 05:34:43.123456789')
  1. 也可以将单个整数或浮点数传递给Timestamp构造器,该构造器返回的日期等于 Unix 纪元(即 1970 年 1 月 1 日)之后的纳秒数:
>>> pd.Timestamp(500)
Timestamp('1970-01-01 00:00:00.000000500')

>>> pd.Timestamp(5000, unit='D')
Timestamp('1983-09-10 00:00:00')
  1. Pandas 提供了to_datetime函数,其功能与Timestamp构造器非常相似,但在特殊情况下带有一些不同的参数。 请参阅以下示例:
>>> pd.to_datetime('2015-5-13')
Timestamp('2015-05-13 00:00:00')

>>> pd.to_datetime('2015-13-5', dayfirst=True)
Timestamp('2015-05-13 00:00:00')

>>> pd.to_datetime('Start Date: Sep 30, 2017 Start Time: 1:30 pm', 
               format='Start Date: %b %d, %Y Start Time: %I:%M %p')
Timestamp('2017-09-30 13:30:00')

>>> pd.to_datetime(100, unit='D', origin='2013-1-1')
Timestamp('2013-04-11 00:00:00')
  1. to_datetime函数具有更多功能。 它能够将整个列表或字符串序列或整数转换为时间戳。 由于我们更可能与序列或数据帧交互,而不是与单个标量值交互,因此您比Timestamp更可能使用to_datetime
>>> s = pd.Series([10, 100, 1000, 10000])
>>> pd.to_datetime(s, unit='D')
0   1970-01-11
1   1970-04-11
2   1972-09-27
3   1997-05-19
dtype: datetime64[ns]

>>> s = pd.Series(['12-5-2015', '14-1-2013',
                   '20/12/2017', '40/23/2017'])
>>> pd.to_datetime(s, dayfirst=True, errors='coerce')
0   2015-05-12
1   2013-01-14
2   2017-12-20
3          NaT
dtype: datetime64[ns]

>>> pd.to_datetime(['Aug 3 1999 3:45:56', '10/31/2017'])
DatetimeIndex(['1999-08-03 03:45:56', 
               '2017-10-31 00:00:00'], dtype='datetime64[ns]', freq=None)
  1. 类似于Timestamp构造器和to_datetime函数,pandas 具有Timedeltato_timedelta来表示时间量。Timedelta构造器和to_timedelta函数都可以创建一个Timedelta对象。 与to_datetime一样,to_timedelta具有更多功能,可以将整个列表或序列转换为Timedelta对象。
>>> pd.Timedelta('12 days 5 hours 3 minutes 123456789 nanoseconds')
Timedelta('12 days 05:03:00.123456')

>>> pd.Timedelta(days=5, minutes=7.34)
Timedelta('5 days 00:07:20.400000')

>>> pd.Timedelta(100, unit='W')
Timedelta('700 days 00:00:00')

>>> pd.to_timedelta('67:15:45.454')
Timedelta('2 days 19:15:45.454000')

>>> s = pd.Series([10, 100])
>>> pd.to_timedelta(s, unit='s')
0   00:00:10
1   00:01:40
dtype: timedelta64[ns]

>>> time_strings = ['2 days 24 minutes 89.67 seconds',
                    '00:45:23.6']
>>> pd.to_timedelta(time_strings)
TimedeltaIndex(['2 days 00:25:29.670000', 
                '0 days 00:45:23.600000'], dtype='timedelta64[ns]', freq=None)
  1. 可以将时间戳添加到时间戳中或从时间戳中减去。 它们甚至可以彼此分开以返回浮点数:
>>> pd.Timedelta('12 days 5 hours 3 minutes') * 2
Timedelta('24 days 10:06:00')

>>> pd.Timestamp('1/1/2017') + \
    pd.Timedelta('12 days 5 hours 3 minutes') * 2
Timestamp('2017-01-25 10:06:00')

>>> td1 = pd.to_timedelta([10, 100], unit='s')
>>> td2 = pd.to_timedelta(['3 hours', '4 hours'])
>>> td1 + td2
TimedeltaIndex(['03:00:10', '04:01:40'],
               dtype='timedelta64[ns]', freq=None)

>>> pd.Timedelta('12 days') / pd.Timedelta('3 days')
4.0
  1. 时间戳和时间增量都有大量可用作属性和方法的功能。 让我们采样其中的一些:
>>> ts = pd.Timestamp('2016-10-1 4:23:23.9')

>>> ts.ceil('h')
Timestamp('2016-10-01 05:00:00'

>>> ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second
(2016, 10, 1, 4, 23, 23)

>>> ts.dayofweek, ts.dayofyear, ts.daysinmonth
(5, 275, 31)

>>> ts.to_pydatetime()
datetime.datetime(2016, 10, 1, 4, 23, 23, 900000)

>>> td = pd.Timedelta(125.8723, unit='h')
>>> td
Timedelta('5 days 05:52:20.280000')

>>> td.round('min')
Timedelta('5 days 05:52:00')

>>> td.components
Components(days=5, hours=5, minutes=52, seconds=20, milliseconds=280, microseconds=0, nanoseconds=0)

>>> td.total_seconds()
453140.28

工作原理

datetime模块是 Python 标准库的一部分,非常流行并且被广泛使用。 因此,最好对它有所了解,因为您可能会跨过它。datetime模块实际上非常简单,总共只有六种类型的对象:datetimedatetimetimedelta以及时区上的其他两个对象。 Pandas TimestampTimedelta对象具有datetime模块对应物的所有功能以及更多功能。 在处理时间序列时,将有可能完全保留在 Pandas 中。

步骤 1 显示了如何使用datetime模块创建日期时间,日期,时间和时间增量。 只有整数可以用作日期或时间的每个组成部分,并作为单独的参数传递。 将此与第 5 步进行比较,在第 5 步中,pandas Timestamp构造器可以接受与参数相同的组件,以及各种日期字符串。 除了整数部分和字符串,第 6 步还显示了如何将单个数字标量用作日期。 此标量的单位默认为纳秒ns),但在第二条语句中将其更改为D),其他选项为小时h),分钟m),s),毫秒ms)和微秒µs)。

步骤 2 详细说明了datetime模块的timedelta对象及其所有参数的构造。 再次,将其与步骤 9 中显示的 pandas Timedelta构造器进行比较,该构造器接受这些相同的参数以及字符串和标量数字。

除了仅能创建单个对象的TimestampTimedelta构造器之外,to_datetimeto_timedelta函数还可以将整数或字符串的整个序列转换为所需的类型 。 这些函数还提供了构造器不可用的其他几个参数。 这些参数之一是errors,默认为字符串值raise,但也可以设置为ignorecoerce。 每当无法转换字符串日期时,errors参数都会确定要采取的措施。 当设置为raise时,引发异常并且程序执行停止。 当设置为ignore时,将返回原始序列,就像进入函数之前一样。 当设置为coerce时,NaT(不是时间)对象用于表示新值。 步骤 8 的第二条语句将所有值正确转换为Timestamp,最后一个被强制变为NaT

仅可用于to_datetime的这些参数中的另一个参数是format,当字符串包含 Pandas 无法自动识别的特定日期模式时,该参数特别有用。 在步骤 7 的第三条语句中,我们在其他一些字符中嵌入了日期时间。 我们用它们各自的格式指令替换字符串的日期和时间。

日期格式指令以单个百分号%开头,后跟单个字符。 每个指令都指定日期或时间的某些部分。 有关所有指令的表格,请参见 Python 官方文档

更多

当将大量字符串转换为时间戳时,日期格式指令实际上可以产生很大的不同。 每当 Pandas 使用to_datetime将字符串序列转换为时间戳时,它都会搜索代表日期的大量不同字符串组合。 即使所有字符串都具有相同的格式,也是如此。 通过format参数,我们可以指定确切的日期格式,这样 Pandas 不必每次都搜索正确的日期格式。 让我们创建一个日期列表作为字符串,并使用和不使用格式指令将它们转换为时间戳的时间:

>>> date_string_list = ['Sep 30 1984'] * 10000

>>> %timeit pd.to_datetime(date_string_list, format='%b %d %Y')
35.6 ms ± 1.47 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

>>> %timeit pd.to_datetime(date_string_list)
1.31 s ± 63.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

提供格式化指令可使性能提高 40 倍。

另见

智能分割时间序列

在第 4 章,“选择数据子集”中,彻底介绍了数据帧的选择和切片。 当数据帧具有DatetimeIndex时,将出现更多选择和切片的机会。

准备

在本秘籍中,我们将使用部分日期匹配来选择和切片带有DatetimeIndex的数据帧。

操作步骤

  1. hdf5文件crimes.h5读取丹佛crimes数据集,并输出列数据类型和前几行。hdf5文件格式允许有效地存储大量科学数据,并且与 CSV 文本文件完全不同。
>>> crime = pd.read_hdf('data/crime.h5', 'crime')
>>> crime.dtypes
OFFENSE_TYPE_ID              category
OFFENSE_CATEGORY_ID          category
REPORTED_DATE          datetime64[ns]
GEO_LON                       float64
GEO_LAT                       float64
NEIGHBORHOOD_ID              category
IS_CRIME                        int64
IS_TRAFFIC                      int64
dtype: object
  1. 请注意,有三个类别列和一个Timestamp(由 NumPy 的datetime64对象表示)。 这些数据类型是在创建数据文件时存储的,这与仅存储原始文本的 CSV 文件不同。 设置REPORTED_DATE列作为索引,以便进行智能时间戳切片:
>>> crime = crime.set_index('REPORTED_DATE')
>>> crime.head()

  1. 像往常一样,可以通过将值传递给.loc索引运算符来选择等于单个索引的所有行:
>>> crime.loc['2016-05-12 16:45:00']

  1. 在索引中使用Timestamp时,可以选择部分匹配索引值的所有行。 例如,如果我们要获取 2016 年 5 月 5 日以后的所有罪行,则只需选择以下内容:
>>> crime.loc['2016-05-12']

  1. 您不仅可以选择不正确的日期,而且可以选择整个月,一年甚至一天的小时:
>>> crime.loc['2016-05'].shape
(8012, 7)

>>> crime.loc['2016'].shape
(91076, 7)

>>> crime.loc['2016-05-12 03'].shape
(4, 7)
  1. 选择字符串还可以包含月份名称:
>>> crime.loc['Dec 2015'].sort_index()

  1. 包含月份名称的许多其他字符串模式也可以使用:
>>> crime.loc['2016 Sep, 15'].shape
(252, 7)

>>> crime.loc['21st October 2014 05'].shape
(4, 7)
  1. 除了选择之外,您还可以使用切片符号来选择精确的数据范围:
>>> crime.loc['2015-3-4':'2016-1-1'].sort_index()

  1. 请注意,无论何时何地,在结束日期实现的所有犯罪都包含在返回的结果中。 对于使用基于标签的.loc索引器的任何结果,都是如此。 您可以为切片的任何开始或结束部分提供尽可能多的精度(或缺乏精度):
>>> crime.loc['2015-3-4 22':'2016-1-1 11:45:00'].sort_index()

工作原理

hdf5文件的许多不错的功能之一是它们保留每一列的数据类型的能力,从而大大减少了所需的内存。 在这种情况下,这些列中的三列存储为 pandas 类别而不是对象。 将它们存储为对象将导致内存使用量增加四倍:

>>> mem_cat = crime.memory_usage().sum()
>>> mem_obj = crime.astype({'OFFENSE_TYPE_ID':'object', 
                            'OFFENSE_CATEGORY_ID':'object', 
                            'NEIGHBORHOOD_ID':'object'}) \
                   .memory_usage(deep=True).sum()
>>> mb = 2 ** 20
>>> round(mem_cat / mb, 1), round(mem_obj / mb, 1)
(29.4, 122.7)

为了使用索引运算符按日期智能地选择和切片行,索引必须包含日期值。 在步骤 2 中,我们将REPORTED_DATE列移到索引中,并正式创建DatetimeIndex作为新索引:

>>> crime.index[:2]
DatetimeIndex(['2014-06-29 02:01:00', '2014-06-29 01:54:00'],
dtype='datetime64[ns]', name='REPORTED_DATE', freq=None)

使用DatetimeIndex时,可以使用.loc索引器使用多种字符串选择行。 实际上,所有可以发送到 pandas Timestamp构造器的字符串都将在这里工作。 出乎意料的是,对于该秘籍中的任何选择或切片,实际上都没有必要使用.loc索引器。 索引运算符本身将以完全相同的方式工作。 例如,步骤 6 的第二条语句可以写为crime['21st October 2014 05']。 索引运算符通常为列保留,但只要存在DatetimeIndex,就可以灵活地使用时间戳。

就个人而言,我更喜欢在选择行时使用.loc索引器,并且始终将其本身用于索引运算符。.loc索引器是显式的,传递给它的第一个值始终用于选择行。

步骤 8 和 9 显示切片的工作方式与从先前步骤中选择的相同。 结果中将包括与片段的开始或结束值部分匹配的任何日期。

更多

我们原始的犯罪数据帧未排序,并且切片仍按预期工作。 对索引进行排序将导致性能大幅提高。 让我们看一下与第 8 步完成的切片的区别:

>>> %timeit crime.loc['2015-3-4':'2016-1-1']
39.6 ms ± 2.77 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

>>> crime_sort = crime.sort_index()
>>> %timeit crime_sort.loc['2015-3-4':'2016-1-1']
758 µs ± 42.1 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

排序后的数据帧与原始数据相比,性能提高了 50 倍。

另见

  • 请参阅第 4 章,“选择数据子集”

使用仅适用于日期时间索引的方法

有许多仅适用于日期时间索引的数据帧/序列方法。 如果索引为任何其他类型,则这些方法将失败。

准备

在本秘籍中,我们将首先使用方法按照时间成分选择数据行。 然后,我们将学习功能强大的日期偏移对象及其别名。

操作步骤

  1. 读取犯罪 HDF5 数据集,将索引设置为REPORTED_DATE,并确保我们具有日期时间索引:
>>> crime = pd.read_hdf('data/crime.h5', 'crime') \
              .set_index('REPORTED_DATE')
>>> print(type(crime.index))
<class 'pandas.core.indexes.datetimes.DatetimeIndex'>
  1. 使用between_time方法选择在凌晨 2 点到凌晨 5 点之间发生的所有犯罪,无论日期如何:
>>> crime.between_time('2:00', '5:00', include_end=False).head()

  1. 使用at_time选择特定时间的所有日期:
>>> crime.at_time('5:47').head()

  1. first方法提供了一种选择前n个时间段的优雅方法,其中n是整数。 这些时间段由可以在pd.offsets模块中的DateOffset对象正式表示。 必须按其索引对数据帧进行排序,以确保此方法可以工作。 让我们选择犯罪数据的前六个月:
>>> crime_sort = crime.sort_index()
>>> crime_sort.first(pd.offsets.MonthBegin(6))

  1. 这捕获了从 1 月到 6 月的数据,但令人惊讶的是,在 7 月选择了一行。 原因是 Pandas 实际上使用了索引中第一个元素的时间分量,在此示例中为6分钟。 让我们使用MonthEnd,这是一个稍微不同的偏移量:
>>> crime_sort.first(pd.offsets.MonthEnd(6))

  1. 这捕获了几乎相同数量的数据,但是如果仔细观察,仅捕获了 6 月 30 日以来的一行。 同样,这是因为保留了第一个索引的时间部分。 确切的搜索结果为2012-06-30 00:06:00。 那么,我们如何才能准确地获得六个月的数据呢? 有两种方法。 所有DateOffset都有一个normalize参数,当设置为True时,会将所有时间分量设置为零。 以下应该使我们非常接近我们想要的:
>>> crime_sort.first(pd.offsets.MonthBegin(6, normalize=True))

  1. 此方法已成功捕获了一年前六个月的所有数据。 在将normalize设置为True的情况下,搜索到2012-07-01 00:00:00,它实际上将包括该日期和时间确切报告的任何犯罪。 实际上,无法使用第一种方法来确保仅捕获从一月到六月的数据。 以下非常简单的切片将产生准确的结果:
>>> crime_sort.loc[:'2012-06']
  1. 有十二个日期偏移对象,可以非常精确地向前或向后移动到下一个最近的偏移量。 您可以使用名为偏移别名的字符串代替在pd.offsets中查找日期偏移对象。 例如,月末的字符串是M,月初的字符串是MS。 要表示这些偏移别名的数量,只需在其前面放置一个整数。 使用此表查找所有别名。 让我们看一下偏移别名的一些示例,其中包含对注释中所选内容的描述:
>>> crime_sort.first('5D') # 5 days
>>> crime_sort.first('5B') # 5 business days
>>> crime_sort.first('7W') # 7 weeks, with weeks ending on Sunday
>>> crime_sort.first('3QS') # 3rd quarter start
>>> crime_sort.first('A') # one year end

工作原理

一旦确保索引为日期时间索引,就可以利用本秘籍中的所有方法。 使用.loc索引器无法仅根据Timestamp的时间成分进行选择或切片。 要按时间范围选择所有日期,必须使用between_time方法,或者要选择确切的时间,请使用at_time。 确保为开始时间和结束时间传递的字符串至少包含小时和分钟。 也可以使用datetime模块中的time对象。 例如,以下命令将产生与步骤 2 相同的结果:

>>> import datetime
>>> crime.between_time(datetime.time(2,0), datetime.time(5,0),
                       include_end=False)

在第 4 步中,我们开始使用简单的first方法,但使用复杂的参数offset。 它必须是日期偏移对象,也可以是字符串的偏移别名。 为了帮助理解日期偏移对象,最好查看它们对单个Timestamp的作用。 例如,让我们采用索引的第一个元素,并以两种不同的方式为其添加六个月的时间:

>>> first_date = crime_sort.index[0]
>>> first_date
Timestamp('2012-01-02 00:06:00')

>>> first_date + pd.offsets.MonthBegin(6)
Timestamp('2012-07-01 00:06:00')

>>> first_date + pd.offsets.MonthEnd(6)
Timestamp('2012-06-30 00:06:00')

MonthBeginMonthEnd偏移量都不会增加或减少确切的时间量,而是有效地向上舍入到下个月的下一个月初或下个月,而不管它是在哪一天。 在内部,first方法使用数据帧的第一个索引元素,并添加传递给它的日期偏移。 然后切成片直到这个新日期。 例如,步骤 4 等效于以下内容:

>>> step4 = crime_sort.first(pd.offsets.MonthEnd(6))

>>> end_dt = crime_sort.index[0] + pd.offsets.MonthEnd(6)
>>> step4_internal = crime_sort[:end_dt]
>>> step4.equals(step4_internal)
True

步骤 5 至 7 直接从前面的等效操作开始。 在步骤 8 中,偏移别名使引用 DateOffsets 的方法更加紧凑。

first方法相对应的是last方法,该方法从给定日期偏移的数据帧中选择最后n个时间段。分组对象具有两个名称完全相同但功能完全不同的方法。 它们返回每个组的第一个或最后一个元素,与拥有日期时间索引无关。

更多

当可用的那些不能完全满足您的需求时,可以构建一个自定义的日期偏移:

>>> dt = pd.Timestamp('2012-1-16 13:40')
>>> dt + pd.DateOffset(months=1)
Timestamp('2012-02-16 13:40:00')

请注意,此自定义日期偏移使Timestamp精确增加了一个月。 让我们再看一个使用更多日期和时间组件的示例:

>>> do = pd.DateOffset(years=2, months=5, days=3,
                       hours=8, seconds=10)
>>> pd.Timestamp('2012-1-22 03:22') + do
Timestamp('2014-06-25 11:22:10')

另见

计算每周的犯罪数量

原始的丹佛犯罪数据集非常庞大,有 460,000 多行标记有报告日期。 计算每周犯罪的数量是可以通过根据一段时间进行分组来回答的许多查询之一。resample方法提供了一个简单的接口,可以按任何可能的时间跨度进行分组。

准备

在本秘籍中,我们将同时使用resamplegroupby方法来计算每周犯罪的数量。

操作步骤

  1. 读取犯罪 HDF5 数据集,将索引设置为REPORTED_DATE,然后对其进行排序以提高其余秘籍的性能:
>>> crime_sort = pd.read_hdf('data/crime.h5', 'crime') \
                   .set_index('REPORTED_DATE') \
                   .sort_index()
  1. 为了计算每周的犯罪数量,我们需要每周组成一个小组。resample方法采用日期偏移对象或别名,并返回准备对所有组执行操作的对象。 从resample方法返回的对象与调用groupby方法后产生的对象非常相似:
>>> crime_sort.resample('W')
DatetimeIndexResampler [freq=<Week: weekday=6>, axis=0, closed=right, label=right, convention=start, base=0]
  1. 偏移别名W, 用来通知 Pandas 我们要按周分组。 在上一步中没有发生太多事情。 Pandas 只是简单地验证了我们的偏移量,并返回了一个对象,该对象准备好每周作为一组执行操作。 调用resample返回一些数据后,可以链接几种方法。 让我们链接size方法以计算每周犯罪数量:
>>> weekly_crimes = crime_sort.resample('W').size()
>>> weekly_crimes.head()
REPORTED_DATE
2012-01-08     877
2012-01-15    1071
2012-01-22     991
2012-01-29     988
2012-02-05     888
Freq: W-SUN, dtype: int64
  1. 现在,我们将每周犯罪计数列为一个序列,而新索引一次增加一周。 默认情况下,有些事情是很重要的,要理解。 选择周日作为一周的最后一天,并且该日期也是用来标记所得序列中每个元素的日期。 例如,第一个索引值 2012 年 1 月 8 日是星期日。 在截至 8 日的那一周内,共发生了 877 起犯罪。 1 月 9 日星期一至 1 月 15 日星期日这周记录了 1,071 起犯罪。 让我们做一些健全性检查,并确保我们的重采样正是这样做的:
>>> len(crime_sort.loc[:'2012-1-8'])
877

>>> len(crime_sort.loc['2012-1-9':'2012-1-15'])
1071
  1. 让我们选择除周日之外的另一天,以固定偏移结束一周:
>>> crime_sort.resample('W-THU').size().head()
REPORTED_DATE
2012-01-05     462
2012-01-12    1116
2012-01-19     924
2012-01-26    1061
2012-02-02     926
Freq: W-THU, dtype: int64
  1. resample的几乎所有功能都可以通过groupby方法再现。 唯一的区别是必须在pd.Grouper对象中传递偏移量:
>>> weekly_crimes_gby = crime_sort.groupby(pd.Grouper(freq='W')) \
                                  .size()
>>> weekly_crimes_gby.head()
REPORTED_DATE
2012-01-08     877
2012-01-15    1071
2012-01-22     991
2012-01-29     988
2012-02-05     888
Freq: W-SUN, dtype: int64

>>> weekly_crimes.equal(weekly_crimes_gby)
True

工作原理

默认情况下,resample方法与日期时间索引隐式工作,这就是为什么我们在步骤 1 中将其设置为REPORTED_DATE的原因。在步骤 2 中,我们创建了一个中间对象,可帮助我们了解如何在数据内形成组。resample的第一个参数是rule,用于确定如何对索引中的时间戳进行分组。 在这种情况下,我们使用偏移别名W来形成长度为一周的组,该组在周日结束。 默认的结束日期是星期日,但可以通过在星期几的前面加上破折号和前三个字母来更改锚定的偏移量。

一旦我们与resample组成了小组,我们就必须链接一个方法以对每个小组采取行动。 在第 3 步中,我们使用size方法来计算每周的犯罪数量。 您可能想知道调用resample之后可以使用哪些所有可能的属性和方法。 下面检查resample对象并输出它们:

>>> r = crime_sort.resample('W')
>>> resample_methods = [attr for attr in dir(r) if attr[0].islower()]
>>> print(resample_methods)
['agg', 'aggregate', 'apply', 'asfreq', 'ax', 'backfill', 'bfill', 'count', 'ffill', 'fillna', 'first', 'get_group', 'groups', 'indices', 'interpolate', 'last', 'max', 'mean', 'median', 'min', 'ndim', 'ngroups', 'nunique', 'obj', 'ohlc', 'pad', 'plot', 'prod', 'sem', 'size', 'std', 'sum', 'transform', 'var']

步骤 4 通过按周手动切片数据并计算行数来验证步骤 3 中计数的准确性。 实际上,甚至不需要按Timestamp分组resample方法,因为该功能可以直接从groupby方法本身获得。 但是,必须使用freq参数将偏移量pd.Grouper的实例传递给groupby方法,如步骤 6 所示。

一个非常类似的名为pd.TimeGrouper的对象能够按照与pd.Grouper完全相同的方式按时间进行分组,但是从熊猫 0.21 版本开始,它已被弃用,不应使用。 不幸的是,在线上有很多使用pd.TimeGrouper的例子,但不要让它们诱惑您。

更多

即使索引不包含Timestamp,也可以使用resample。 您可以使用on参数选择带有时间戳的列,这些列将用于形成组:

>>> crime = pd.read_hdf('data/crime.h5', 'crime')
>>> weekly_crimes2 = crime.resample('W', on='REPORTED_DATE').size()
>>> weekly_crimes2.equals(weekly_crimes)
True

同样,通过选择key参数的Timestamp列,可以将groupbypd.Grouper结合使用:

>>> weekly_crimes_gby2 = crime.groupby(pd.Grouper(key='REPORTED_DATE', 
                                                  freq='W')).size()
>>> weekly_crimes_gby2.equals(weekly_crimes_gby)
True

通过调用每周犯罪序列中的plot方法,我们还可以轻松地绘制丹佛所有犯罪(包括交通事故)的线图:

>>> weekly_crimes.plot(figsize=(16, 4), title='All Denver Crimes')

另见

分别汇总每周犯罪和交通事故

丹佛犯罪数据集将所有犯罪和交通事故汇总在一个表格中,并通过二进制列IS_CRIMEIS_TRAFFIC将它们分开。resample方法允许您按一段时间分组并分别汇总特定的列。

准备

在本秘籍中,我们将使用resample方法对一年中的每个季度进行分组,然后分别汇总犯罪和交通事故的数量。

操作步骤

  1. 读取犯罪 HDF5 数据集,将索引设置为REPORTED_DATE,然后对其进行排序以提高其余秘籍的性能:
>>> crime_sort = pd.read_hdf('data/crime.h5', 'crime') \
                   .set_index('REPORTED_DATE') \
                   .sort_index()
  1. 使用resample方法按一年中的每个季度进行分组,然后将各组的IS_CRIMEIS_TRAFFIC列求和:
>>> crime_quarterly = crime_sort.resample('Q')['IS_CRIME',
                                               'IS_TRAFFIC'].sum()
>>> crime_quarterly.head()

  1. 请注意,所有日期均显示为该季度的最后一天。 这是因为偏移别名Q代表该季度末。 让我们使用偏移别名QS代表季度的开始:
>>> crime_sort.resample('QS')['IS_CRIME', 'IS_TRAFFIC'].sum().head()

  1. 让我们通过检查第二季度的数据是否正确来验证这些结果:
>>> crime_sort.loc['2012-4-1':'2012-6-30', 
                   ['IS_CRIME', 'IS_TRAFFIC']].sum()
IS_CRIME      9641
IS_TRAFFIC    5255
dtype: int64
  1. 可以使用groupby方法复制此操作:
>>> crime_quarterly2 = crime_sort.groupby(pd.Grouper(freq='Q')) \
                                 ['IS_CRIME', 'IS_TRAFFIC'].sum()
>>> crime_quarterly2.equals(crime_quarterly)
True
  1. 让我们作图以更好地分析一段时间内犯罪和交通事故的趋势:
>>> plot_kwargs = dict(figsize=(16,4), 
                       color=['black', 'lightgrey'], 
                       title='Denver Crimes and Traffic Accidents')
>>> crime_quarterly.plot(**plot_kwargs)

工作原理

在第 1 步中读取并准备好数据后,我们在第 2 步中开始分组和聚合。调用resample方法后,我们可以通过链接方法或选择一组要聚合的列来继续进行操作。 我们选择选择IS_CRIMEIS_TRAFFIC列进行汇总。 如果我们不只是选择这两个,那么所有数字列的总和将具有以下结果:

>>> crime_sort.resample('Q').sum().head()

默认情况下,偏移别名Q在技术上使用 12 月 31 日作为一年的最后一天。 代表一个季度的日期范围全部使用此结束日期计算。 汇总结果使用该季度的最后一天作为标签。 步骤 3 使用偏移别名QS,默认情况下,它使用 1 月 1 日作为一年的第一天来计算季度。

大多数公共企业都报告季度收入,但是从一月开始,它们都没有相同的日历年。 例如,如果我们希望季度开始于 3 月 1 日,则可以使用QS-MAR来锚定偏移别名:

>>> crime_sort.resample('QS-MAR')['IS_CRIME', 'IS_TRAFFIC'] \
              .sum().head()

与前面的秘籍一样,我们通过手动切片来验证结果,并使用pd.Grouper使用groupby方法复制结果以设置组长。 在第 6 步中,我们仅调用数据帧的plot方法。 默认情况下,为每列数据绘制一条线。 该图清楚地表明,在今年的前三个季度,报告的犯罪数量急剧增加。 犯罪和贩运似乎都是季节性因素,在较冷的月份数字较低,在较暖的月份数字较高。

更多

为了获得不同的视觉角度,我们可以绘制犯罪和交通增加百分比,而不是原始计数。 让我们将所有数据除以第一行并再次绘图:

>>> crime_begin = crime_quarterly.iloc[0]
>>> crime_begin
IS_CRIME      7882
IS_TRAFFIC    4726
Name: 2012-03-31 00:00:00, dtype: int64

>>> crime_quarterly.div(crime_begin) \
                   .sub(1) \
                   .round(2) \
                   .plot(**plot_kwargs)

按工作日和年份衡量犯罪

通过按工作日和按年衡量犯罪的同时,必须具有直接从时间戳中提取此信息的函数。 值得庆幸的是,此函数内置于任何包含dt访问器的时间戳组成的列中。

准备

在本秘籍中,我们将使用dt访问器为我们提供每个犯罪的工作日名称和年份(序列)。 我们通过使用这两个序列的小组来计算所有犯罪。 最后,我们在创建犯罪总量热图之前,调整数据以考虑部分年份和人口。

操作步骤

  1. 读入丹佛犯罪 HDF5 数据集,将REPORTED_DATE保留为一列:
>>> crime = pd.read_hdf('data/crime.h5', 'crime')
>>> crime.head()

  1. 所有“时间戳”列均具有称为dt访问器的特殊属性,该属性可访问为它们专门设计的各种其他属性和方法。 让我们找到每个REPORTED_DATE的工作日名称,然后计算这些值:
>>> wd_counts = crime['REPORTED_DATE'].dt.weekday_name \
                                         .value_counts()
>>> wd_counts
Monday       70024
Friday       69621
Wednesday    69538
Thursday     69287
Tuesday      68394
Saturday     58834
Sunday       55213
Name: REPORTED_DATE, dtype: int64
  1. 周末看来,犯罪和交通事故的发生率大大降低。 让我们按正确的工作日顺序排列此数据,并绘制水平条形图:
>>> days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 
            'Friday', 'Saturday', 'Sunday']
>>> title = 'Denver Crimes and Traffic Accidents per Weekday'
>>> wd_counts.reindex(days).plot(kind='barh', title=title)

  1. 我们可以执行非常类似的过程来按年份绘制计数:
>>> title = 'Denver Crimes and Traffic Accidents per Year' 
>>> crime['REPORTED_DATE'].dt.year.value_counts() \
                             .sort_index() \
                             .plot(kind='barh', title=title)

  1. 我们需要按工作日和年份分组。 一种方法是将工作日和年份序列保存为单独的变量,然后将这些变量与groupby方法一起使用:
>>> weekday = crime['REPORTED_DATE'].dt.weekday_name
>>> year = crime['REPORTED_DATE'].dt.year

>>> crime_wd_y = crime.groupby([year, weekday]).size()
>>> crime_wd_y.head(10)
REPORTED_DATE  REPORTED_DATE
2012           Friday            8549
               Monday            8786
               Saturday          7442
               Sunday            7189
               Thursday          8440
               Tuesday           8191
               Wednesday         8440
2013           Friday           10380
               Monday           10627
               Saturday          8875
dtype: int64
  1. 我们已经正确汇总了数据,但是结构并不完全有利于轻松进行比较。 让我们先重命名那些无意义的索引级别名称,然后再将unstack重命名为工作日级别,以使我们的表更具可读性:
>>> crime_table = crime_wd_y.rename_axis(['Year', 'Weekday']) \
                            .unstack('Weekday')
>>> crime_table

  1. 现在,我们有了更好的表示形式,更易于阅读,但值得注意的是,2017 年的数字并不完整。 为了更公平地进行比较,我们可以进行简单的线性外推法来估算犯罪的最终数量。 首先让我们找到 2017 年数据的最后一天:
>>> criteria = crime['REPORTED_DATE'].dt.year == 2017
>>> crime.loc[criteria, 'REPORTED_DATE'].dt.dayofyear.max()
272
  1. 天真的估计是假设全年犯罪率保持不变,并将 2017 年表中的所有值乘以 365/272。 但是,我们可以做得更好,查看历史数据并计算在一年的前 272 天中发生的犯罪的平均百分比:
>>> round(272 / 365, 3)
.745

>>> crime_pct = crime['REPORTED_DATE'].dt.dayofyear.le(272) \
                                      .groupby(year) \
                                      .mean() \
                                      .round(3)
>>> crime_pct
REPORTED_DATE
2012    0.748
2013    0.725
2014    0.751
2015    0.748
2016    0.752
2017    1.000
Name: REPORTED_DATE, dtype: float64

>>> crime_pct.loc[2012:2016].median()
.748
  1. 事实证明,也许非常巧合的是,在一年的前 272 天发生的犯罪百分比几乎与该年过去的天数百分比成正比。 现在让我们更新 2017 年的行,并更改列顺序以匹配工作日顺序:
>>> crime_table.loc[2017] = crime_table.loc[2017].div(.748) \
                                        .astype('int')
>>> crime_table = crime_table.reindex(columns=days)
>>> crime_table

  1. 我们可以绘制条形图或折线图,但这对于热图也是一个很好的情况,seaborn 库中提供了该图:
>>> import seaborn as sns
>>> sns.heatmap(crime_table, cmap='Greys')

  1. 犯罪似乎每年都在增加,但是该数据并未说明人口的增长。 让我们读一下有数据的每年丹佛人口的表格:
>>> denver_pop = pd.read_csv('data/denver_pop.csv',
                             index_col='Year')
>>> denver_pop

  1. 据报告,许多犯罪指标是每 100,000 名居民的比率。 让我们将人口除以 100,000,然后将原始犯罪计数除以该数字即可得出每 100,000 居民的犯罪率:
>>> den_100k = denver_pop.div(100000).squeeze()
>>> crime_table2 = crime_table.div(den_100k, axis='index') \
                              .astype('int')
>>> crime_table2

  1. 再一次,我们可以制作一个热图,即使在调整了人口增长之后,该热图看起来也几乎与第一个相同:
>>> sns.heatmap(crime_table2, cmap='Greys')

工作原理

所有包含时间戳的数据帧的列都可以使用dt访问器访问许多其他属性和方法。 实际上,从dt访问器可用的所有这些方法和属性也可以直接从单个时间戳对象获得。

在第 2 步中,我们使用仅适用于序列的dt访问器来提取工作日名称并简单地计算发生次数。 在执行步骤 3 之前,我们使用reindex方法手动重新排列索引的顺序,在最基本的使用情况下,该方法接受包含所需顺序的列表。 也可以使用.loc索引器完成此任务,如下所示:

>>> wd_counts.loc[days]
Monday       70024
Tuesday      68394
Wednesday    69538
Thursday     69287
Friday       69621
Saturday     58834
Sunday       55213
Name: REPORTED_DATE, dtype: int64

.loc相比,reindex方法实际上性能更高,并且在更多情况下具有许多参数。 然后,我们使用dt访问器的weekday_name属性检索一周中每一天的名称,并在制作水平条形图之前对出现的次数进行计数。

在第 4 步中,我们执行一个非常相似的过程,并再次使用dt访问器检索年份,然后使用value_counts方法对发生次数进行计数。 在这种情况下,我们使用sort_index而不是reindex,因为年份自然会按所需顺序排序。

秘籍的目标是将工作日和年份进行分组,因此这正是我们在第 5 步中所做的。groupby方法非常灵活,可以通过多种方式进行分组。 在此秘籍中,我们将两个序列yearweekday传递给它们,所有唯一的组合从中组成一个组。 然后,我们将size方法链接到该方法,该方法返回单个值,即每个组的长度。

在第 5 步之后,我们的序列很长,只有一列数据,这使得很难按年和工作日进行比较。 为了简化可读性,我们将工作日级别使用unstack旋转为水平列名称。

在步骤 7 中,我们使用布尔索引来仅选择 2017 年的犯罪,然后再次使用dt访问器中的dayofyear查找从年初开始经过的总天数。 该序列的最大值应告诉我们 2017 年有多少天的数据。

步骤 8 非常复杂。 我们首先通过使用crime['REPORTED_DATE'].dt.dayofyear.le(272)测试每个犯罪是否在每年的第 272 天或之前犯下来创建布尔值序列。 从这里开始,我们再次使用灵活的groupby方法按照先前计算的year序列来分组,然后使用mean方法来查找每年第 272 天或之前的犯罪百分比。

.loc索引器在步骤 9 中选择整个 2017 年数据行。我们用该行除以在步骤 8 中找到的中位数百分比来调整该行。

许多犯罪的可视化都是通过热图完成的,其中一个步骤是在第 10 步借助seaborn可视化库完成的。cmap参数采用几十个可用 matplotlib 调色板的字符串名称

在第 12 步中,我们将100k居民的犯罪率除以该年的人口。 这实际上是一个相当棘手的操作。 通常,将一个数据帧除以另一个时,它们在其列和索引上对齐。 但是,在此步骤中,crime_table没有公用的denver_pop列,因此,如果我们尝试对它们进行划分,则没有值会对齐。 要解决此问题,我们使用squeeze方法创建了den_100k序列。 我们仍然不能简单地划分这两个对象,因为默认情况下,数据帧和序列之间的划分会将数据帧的列与序列的索引对齐,如下所示:

>>> crime_table / den_100k

我们需要数据帧的索引与序列的索引对齐,并且为此,我们使用div方法,该方法允许我们使用axis参数更改对齐方向。 在步骤 13 中绘制已调整犯罪率的heatmap

更多

让我们通过编写一个函数来一次完成此秘籍的所有步骤并添加选择特定类型犯罪的功能来完成此分析的完成:

>>> ADJ_2017 = .748

>>> def count_crime(df, offense_cat):
        df = df[df['OFFENSE_CATEGORY_ID'] == offense_cat]
        weekday = df['REPORTED_DATE'].dt.weekday_name
        year = df['REPORTED_DATE'].dt.year

        ct = df.groupby([year, weekday]).size().unstack()
        ct.loc[2017] = ct.loc[2017].div(ADJ_2017).astype('int')

        pop = pd.read_csv('data/denver_pop.csv', index_col='Year')
        pop = pop.squeeze().div(100000)

        ct = ct.div(pop, axis=0).astype('int')
        ct = ct.reindex(columns=days)
        sns.heatmap(ct, cmap='Greys')
        return ct

>>> count_crime(crime, 'auto-theft')

另见

使用日期时间索引和匿名函数进行分组

将数据帧与DatetimeIndex一起使用将为许多新的和不同的操作打开一扇门,如本章中的几个秘籍所示。

准备

在本秘籍中,我们将展示对具有DatetimeIndex的数据帧使用groupby方法的多功能性。

操作步骤

  1. 读入丹佛crime hdf5文件,将REPORTED_DATE列放在索引中,然后对其进行排序:
>>> crime_sort = pd.read_hdf('data/crime.h5', 'crime') \
                   .set_index('REPORTED_DATE') \
                   .sort_index()
  1. DatetimeIndex本身具有许多与 Pandas Timestamp相同的属性和方法。 让我们看一下它们的共同点:
>>> common_attrs = set(dir(crime_sort.index)) & \
                   set(dir(pd.Timestamp))
>>> print([attr for attr in common_attrs if attr[0] != '_'])

['to_pydatetime', 'normalize', 'day', 'dayofyear', 'freq', 'ceil', 
'microsecond', 'tzinfo', 'weekday_name', 'min', 'quarter', 'month', 
'tz_convert', 'tz_localize', 'is_month_start', 'nanosecond', 'tz', 
'to_datetime', 'dayofweek', 'year', 'date', 'resolution', 'is_quarter_end', 
'weekofyear', 'is_quarter_start', 'max', 'is_year_end', 'week', 'round', 
'strftime', 'offset', 'second', 'is_leap_year', 'is_year_start', 
'is_month_end', 'to_period', 'minute', 'weekday', 'hour', 'freqstr', 
'floor', 'time', 'to_julian_date', 'days_in_month', 'daysinmonth']
  1. 然后,我们可以使用索引来查找工作日名称,类似于上一秘籍的步骤 2 中所做的操作:
>>> crime_sort.index.weekday_name.value_counts()
Monday       70024
Friday       69621
Wednesday    69538
Thursday     69287
Tuesday      68394
Saturday     58834
Sunday       55213
Name: REPORTED_DATE, dtype: int64
  1. 令人惊讶的是,groupby方法具有接受函数作为参数的能力。 该函数将隐式传递给索引,并且其返回值用于形成组。 让我们通过使用将索引转换为工作日名称的函数进行分组,然后分别计算犯罪和交通事故的数量,来了解这一点:
>>> crime_sort.groupby(lambda x: x.weekday_name) \
               ['IS_CRIME', 'IS_TRAFFIC'].sum()

  1. 您可以使用函数列表按年中的小时和年进行分组,然后对表进行整形以使其更具可读性:
>>> funcs = [lambda x: x.round('2h').hour, lambda x: x.year]
>>> cr_group = crime_sort.groupby(funcs) \
                          ['IS_CRIME', 'IS_TRAFFIC'].sum()
>>> cr_final = cr_group.unstack()
>>> cr_final.style.highlight_max(color='lightgrey')

工作原理

在第 1 步中,我们读入数据并将一列时间戳放入索引中以创建日期时间索引。 在第 2 步中,我们看到日期时间索引具有许多与单个时间戳对象相同的函数。 在第 3 步中,我们直接使用日期时间索引的这些额外函数提取工作日名称。

在步骤 4 中,我们利用groupby方法的特殊功能来接受通过日期时间索引传递的函数。 匿名函数中的x实际上是日期时间索引,我们使用它来检索工作日名称。 可以传递groupby任意数量的自定义函数的列表,如步骤 5 所示。这里,第一个函数使用日期时间索引的round方法将每个值四舍五入到最接近的第二小时。 第二个函数检索年份。 在分组和汇总之后,我们将unstack年作为列。 然后,我们突出显示每列的最大值。 犯罪率最高的报告时间是下午 3 点至 5 点。 大多数交通事故发生在下午 5 点之间。 晚上 7 点

更多

此秘籍的最终结果是带有多重索引列的数据帧。 使用此数据帧,可以仅选择犯罪或交通事故。xs方法允许您从任何索引级别中选择一个值。 让我们看一个示例,其中我们仅选择处理流量的数据部分:

>>> cr_final.xs('IS_TRAFFIC', axis='columns', level=0).head()

这称为在 Pandas 中截取的横截面。 我们必须使用axislevel参数专门表示我们的值所在的位置。 让我们再次使用xs仅选择 2016 年中处于不同级别的数据:

>>> cr_final.xs(2016, axis='columns', level=1).head()

另见

按时间戳和另一列分组

resample方法本身无法按时间段进行分组。 但是,groupby方法可以按时间段和其他列进行分组。

准备

在此秘籍中,我们将展示两种非常相似但不同的方法来按时间戳分组,并在另一列中进行。

操作步骤

  1. 读取employee数据集,并使用HIRE_DATE列创建日期时间索引:
>>> employee = pd.read_csv('data/employee.csv', 
                           parse_dates=['JOB_DATE', 'HIRE_DATE'], 
                           index_col='HIRE_DATE')
>>> employee.head()

  1. 首先,让我们按性别进行简单分组,然后找到每个分组的平均工资:
>>> employee.groupby('GENDER')['BASE_SALARY'].mean().round(-2)
GENDER
Female    52200.0
Male      57400.0
Name: BASE_SALARY, dtype: float64
  1. 让我们根据租用日期找到平均薪水,然后将每个人归类为 10 年:
>>> employee.resample('10AS')['BASE_SALARY'].mean().round(-2)
HIRE_DATE
1958-01-01     81200.0
1968-01-01    106500.0
1978-01-01     69600.0
1988-01-01     62300.0
1998-01-01     58200.0
2008-01-01     47200.0
Freq: 10AS-JAN, Name: BASE_SALARY, dtype: float64
  1. 如果我们想按性别和五年时间跨度分组,可以在致电groupby之后直接致电resample
>>> employee.groupby('GENDER').resample('10AS')['BASE_SALARY'] \
            .mean().round(-2)
GENDER  HIRE_DATE 
Female  1975-01-01     51600.0
        1985-01-01     57600.0
        1995-01-01     55500.0
        2005-01-01     51700.0
        2015-01-01     38600.0
Male    1958-01-01     81200.0
        1968-01-01    106500.0
        1978-01-01     72300.0
        1988-01-01     64600.0
        1998-01-01     59700.0
        2008-01-01     47200.0
Name: BASE_SALARY, dtype: float64
  1. 现在,这已经完成了我们打算要做的工作,但是每当我们要比较男性和女性的工资时,我们都会遇到一个小问题。 让我们unstack性别级别,看看会发生什么:
>>> sal_avg.unstack('GENDER')

  1. 男性和女性的 10 年期限不在同一日期开始。 发生这种情况的原因是,数据首先按性别分组,然后在每种性别内,根据雇用日期组成了更多的组。 让我们验证一下第一位雇用的男性是 1958 年,第一位雇用的女性是 1975 年:
>>> employee[employee['GENDER'] == 'Male'].index.min()
Timestamp('1958-12-29 00:00:00')

>>> employee[employee['GENDER'] == 'Female'].index.min()
Timestamp('1975-06-09 00:00:00')
  1. 要解决此问题,我们必须将日期与性别一起分组,并且只有通过groupby方法才能做到这一点:
>>> sal_avg2 = employee.groupby(['GENDER', 
                                 pd.Grouper(freq='10AS')]) \
                        ['BASE_SALARY'].mean().round(-2)
>>> sal_avg2
GENDER  HIRE_DATE 
Female  1968-01-01         NaN
        1978-01-01     57100.0
        1988-01-01     57100.0
        1998-01-01     54700.0
        2008-01-01     47300.0
Male    1958-01-01     81200.0
        1968-01-01    106500.0
        1978-01-01     72300.0
        1988-01-01     64600.0
        1998-01-01     59700.0
        2008-01-01     47200.0
Name: BASE_SALARY, dtype: float64
  1. 现在我们可以unstack性别,使行完美对齐:
>>> sal_final = sal_avg2.unstack('GENDER')
>>> sal_final

工作原理

步骤 1 中的read_csv函数允许将列都转换为时间戳,并同时将它们放入索引中,以创建日期时间索引。 第 2 步使用单个分组列GENDER执行简单的groupby操作。 步骤 3 使用resample方法和偏移别名10AS以 10 年的时间增量形成组。A是年份的别名,S通知我们该时期的开始用作标签。 例如,标签1988-01-01的数据跨越该日期,直到 1997 年 12 月 31 日为止。

有趣的是,从对groupby方法的调用返回的对象具有其自己的resample方法,但反之则不成立:

>>> 'resample' in dir(employee.groupby('GENDER'))
True

>>> 'groupby' in dir(employee.resample('10AS'))
False

在第 4 步中,根据最早雇用的员工,计算出男女的 10 年完全不同的开始日期。 步骤 6 验证每种性别最早雇用的雇员的年份与步骤 4 的输出相匹配。步骤 5 显示了当我们尝试将女性的工资与男性的工资进行比较时,这如何导致不一致。 他们没有相同的 10 年期限。

要缓解此问题,我们必须将“性别”和“时间戳”归为一组。resample方法仅能按单个时间戳分组。 我们只能使用groupby方法完成此操作。 使用pd.Grouper,我们可以复制resample的功能。 我们只需将偏移别名传递给freq参数,然后将对象与我们希望分组的所有其他列一起放在列表中,如步骤 7 所示。由于现在男性和女性的开始日期都相同 10 年期间,步骤 8 中的重塑数据将针对每种性别进行调整,从而使比较变得更加容易。 看起来,随着工作时间的延长,男性的工资往往会更高,尽管在 10 年以下的工作中,男性和女性的平均工资相同。

更多

从局外人的角度来看,步骤 8 中输出的行代表 10 年的间隔并不明显。 改善索引标签的一种方法是显示每个时间间隔的开始和结束。 我们可以通过将当前索引年份与自身添加的 9 连接来实现此目的:

>>> years = sal_final.index.year
>>> years_right = years + 9
>>> sal_final.index = years.astype(str) + '-' + years_right.astype(str)
>>> sal_final

实际上,有一种完全不同的方法来制作此秘籍。 我们可以使用cut函数根据每位员工的受聘年限并从中形成组来创建等宽间隔:

>>> cuts = pd.cut(employee.index.year, bins=5, precision=0)
>>> cuts.categories.values
array([Interval(1958.0, 1970.0, closed='right'),
       Interval(1970.0, 1981.0, closed='right'),
       Interval(1981.0, 1993.0, closed='right'),
       Interval(1993.0, 2004.0, closed='right'),
       Interval(2004.0, 2016.0, closed='right')], dtype=object)

>>> employee.groupby([cuts, 'GENDER'])['BASE_SALARY'] \
            .mean().unstack('GENDER').round(-2)

使用merge_asof查找相比上次降低了 20% 的犯罪率

很多时候,我们想知道上一次发生什么事情的时间。 例如,我们可能对上一次失业率低于 5% 或上一次股市连续五天上涨或上一次睡眠八个小时感兴趣。merge_asof函数为这些类型的问题提供答案。

准备

在此秘籍中,我们将找到每种犯罪类别当月的犯罪总数,然后找到上次发生率降低 20% 的时间。

操作步骤

  1. 读入丹佛犯罪数据集,将REPORTED_DATE放在索引中,然后对其进行排序:
>>> crime_sort = pd.read_hdf('data/crime.h5', 'crime') \
               .set_index('REPORTED_DATE') \
               .sort_index()
  1. 查找最近一个月的数据:
>>> crime_sort.index.max()
Timestamp('2017-09-29 06:16:00')
  1. 由于我们没有 9 月份的全部数据,因此将其从数据集中删除:
>>> crime_sort = crime_sort[:'2017-8']
>>> crime_sort.index.max()
Timestamp('2017-08-31 23:52:00')
  1. 让我们计算每个月的犯罪和交通事故数量:
>>> all_data = crime_sort.groupby([pd.Grouper(freq='M'),
                                   'OFFENSE_CATEGORY_ID']).size()
>>> all_data.head()
REPORTED_DATE  OFFENSE_CATEGORY_ID
2012-01-31     aggravated-assault     113
               all-other-crimes       124
               arson                    5
               auto-theft             275
               burglary               343
dtype: int64
  1. 尽管merge_asof函数可以使用索引,但重置它会更容易:
>>> all_data = all_data.sort_values().reset_index(name='Total')
>>> all_data.head()

  1. 让我们获取当前月份的犯罪计数,并新建一个列来表示目标:
>>> goal = all_data[all_data['REPORTED_DATE'] == '2017-8-31'] \
                   .reset_index(drop=True)
>>> goal['Total_Goal'] = goal['Total'].mul(.8).astype(int)
>>> goal.head()

  1. 现在使用merge_asof函数查找每个犯罪类别的每月犯罪总数上一次小于Total_Goal列的时间:
>>> pd.merge_asof(goal, all_data, left_on='Total_Goal',
                  right_on='Total', by='OFFENSE_CATEGORY_ID',
                  suffixes=('_Current', '_Last'))

工作原理

读完我们的数据后,我们决定不包括 2017 年 9 月的数据,因为它不是一个完整的月份。 我们使用部分日期字符串对直至 2017 年 8 月的所有犯罪进行分割,在第 4 步中,我们统计每月每个犯罪类别的所有犯罪,在第 5 步中,我们按此总数进行排序,这对于merge_asof是必需的。

在第 6 步中,我们将最新数据选择到单独的数据帧中。 我们将以 8 月的这个月为基准,并创建Total_Goal列,该列比当前少 20% 。 在第 7 步中,我们使用merge_asof查找上一次每月犯罪计数少于Total_Goal列的时间。

更多

除了时间戳和时间增量数据类型外,pandas 还提供了时间段类型来表示确切的时间段。 例如,2012-05代表 2012 年 5 月的整个月份。您可以通过以下方式手动构建时间段:

>>> pd.Period(year=2012, month=5, day=17, hour=14, minute=20, freq='T')
Period('2012-05-17 14:20', 'T')

该对象表示 2012 年 5 月 17 日下午 2:20 的整个分钟。 可以在步骤 4 中使用这些期间,而不用pd.Grouper按日期分组。 具有日期时间索引的数据帧具有to_period方法,可以将时间戳转换为期间。 它接受偏移别名来确定时间段的确切长度。

>>> ad_period = crime_sort.groupby([lambda x: x.to_period('M'), 
                                    'OFFENSE_CATEGORY_ID']).size()
>>> ad_period = ad_period.sort_values() \
                         .reset_index(name='Total') \
                         .rename(columns={'level_0':'REPORTED_DATE'})
>>> ad_period.head()

让我们验证此数据帧的最后两列是否等效于步骤 5 中的all_data

>>> cols = ['OFFENSE_CATEGORY_ID', 'Total']
>>> all_data[cols].equals(ad_period[cols])
True

现在,可以使用以下代码以几乎完全相同的方式复制步骤 6 和 7:

>>> aug_2018 = pd.Period('2017-8', freq='M')
>>> goal_period = ad_period[ad_period['REPORTED_DATE'] == aug_2018] \
                           .reset_index(drop=True)
>>> goal_period['Total_Goal'] = goal_period['Total'].mul(.8).astype(int)

>>> pd.merge_asof(goal_period, ad_period, left_on='Total_Goal',
                  right_on='Total', by='OFFENSE_CATEGORY_ID',
                  suffixes=('_Current', '_Last')).head()

十一、Pandas,Matplotlib 和 Seaborn 的可视化

在本章中,我们将介绍以下主题:

  • matplotlib 入门
  • 使用 matplotlib 可视化数据
  • Pandas 绘图的基础知识
  • 可视化航班数据集
  • 堆叠面积图以发现新兴趋势
  • 了解 Pandas 与 Pandas 的区别
  • 使用 Seaborn 网格进行多元分析
  • 在 Seaborn 钻石数据集中发现辛普森悖论

介绍

可视化是探索性数据分析以及演示和应用中的关键组成部分。 在探索性数据分析过程中,您通常是一个人或成小组工作,需要快速创建绘图以帮助您更好地理解数据。 它可以帮助您识别异常值和丢失的数据,也可以引发其他令人感兴趣的问题,这些问题将导致进一步的分析和更直观的显示。 通常不会在考虑最终用户的情况下完成这种类型的可视化。 严格来说是为了帮助您更好地了解当前情况。 绘图不一定是完美的。

在为报表或应用准备可视化文件时,必须使用其他方法。 注意小细节。 此外,通常您必须将所有可能的可视化范围缩小到仅最能代表您数据的少数几个。 良好的数据可视化使观看者享受提取信息的体验。 就像使观众迷失的电影一样,好的可视化效果将包含大量真正引起人们兴趣的信息。

Python 中主要的数据可视化库是 matplotlib,该项目始于 2000 年代初期,旨在模仿 Matlab 的绘图函数。 Matplotlib 具有极大的能力来绘制您可以想象的大多数事物,它为用户提供了强大的功能来控制绘制表面的各个方面。 也就是说,对于初学者来说,它并不是最友好的库。 值得庆幸的是,Pandas 使我们对数据的可视化变得非常容易,并且通常只需单击plot方法即可绘制出我们想要的内容。 Pandas 实际上并没有独自策划。 它在内部调用 matplotlib 函数来创建图。 我认为,Pandas 还添加了自己的样式,该样式比 matplotlib 中的默认样式好一些。

Seaborn 还是一个可视化库,它在内部调用 matplotlib 函数,并且自身不进行任何实际绘制。 Seaborn 可以轻松轻松地制作漂亮的绘图,并允许创建许多新类型的绘图,而这些新绘图无法直接从 matplotlib 或 Pandas 获得。 Seaborn 处理整洁(长)数据,而 Pandas 处理汇总(宽)数据效果最佳。 Seaborn 在其绘图函数中还接受了 Pandas 数据帧对象。

尽管可以在不直接运行任何 matplotlib 代码的情况下创建图,但有时仍需要使用它来手动调整更精细的图细节。 因此,前两个秘籍将介绍 matplotlib 的一些基础知识,如果您需要直接使用它,将非常有用。 除了前两个秘籍外,所有绘图示例都将使用 Pandas 或海生豆。

Python 中的可视化不一定必须依赖于 matplotlib。 Bokeh 迅速成为针对 Web 的非常流行的交互式可视化库。 它完全独立于 matplotlib,并且能够生成整个应用。

matplotlib 入门

对于许多数据科学家而言,他们绝大部分的绘图命令将直接来自 Pandas 或海生动物,它们都完全依赖于 matplotlib 进行实际的绘图。 但是,pandas 和 seaborn 都不提供 matplotlib 的完整替代品,有时您需要直接使用它。 因此,本秘籍将简要介绍 matplotlib 的最关键方面。

准备

让我们从下图中的 matplotlib 图的解剖开始我们的介绍:

Matplotlib 使用对象层次结构在输出中显示其所有绘图项。 该层次结构是了解有关 matplotlib 的一切的关键。 图形对象是层次结构的两个主要组成部分。 图形对象位于层次结构的顶部。 它是将要绘制的所有内容的容器。 图中包含一个或多个轴对象。 轴是使用 matplotlib 时将与之交互的主要对象,通常可以将其视为实际的绘图表面。 轴包含 x/y 轴,点,线,标记,标签,图例以及其他任何绘制的有用项目。

2017 年初,matplotlib 在发布版本 2.0 时进行了重大更改。 许多默认的绘图参数已更改。 解剖图实际上来自版本 1 的文档,但与版本 2 中更新的解剖图相比,在区分图形和轴方面做得更好

需要在轴对象和轴之间进行非常明显的区分。 它们是完全独立的对象。 使用 matplotlib 术语的轴域对象不是轴的复数,而是如前所述,该对象创建并控制了大多数有用的绘图元素。 轴仅指图的 xy (甚至 z)轴。

不幸的是,matplotlib 选择使用轴域(Axes,即单词轴的复数)来指代完全不同的对象,但是它对于库来说是至关重要的,因此目前不太可能更改。

由轴域对象创建的所有这些有用的绘图元素都称为艺术家。 甚至图形和轴域对象本身也是艺术家。 对艺术家的这种区分对本秘籍而言并不重要,但在进行更高级的 matplotlib 绘图时,尤其是在阅读文档时,将很有用。

Matplotlib 的面向对象指南

Matplotlib 为用户提供了两个不同的接口来进行绘图。 有状态接口直接通过pyplot模块进行所有调用。 此接口称为有状态,因为 matplotlib 隐式跟踪绘图环境的当前状态。 每当在有状态接口中创建图时,matplotlib 都会找到当前图形或当前轴并对其进行更改。 这种方法可以快速绘制一些东西,但是当处理多个图形和轴时可能变得笨拙。

Matplotlib 还提供了无状态或面向对象的接口,您可以在其中显式使用引用特定绘图对象的变量。 然后可以使用每个变量来更改绘图的某些属性。 面向对象的方法是显式的,您始终清楚地知道要修改的对象。

不幸的是,同时使用这两个选项会导致很多混乱,并且 matplotlib 以难以学习而著称。 该文档提供了使用这两种方法的示例。 教程,博客文章, Stack Overflow 文章在网络上比比皆是,这使这种混乱永久化。 本秘籍仅专注于面向对象的方法,因为它具有更多的 Python 风格,并且与我们与 Pandas 互动的方式更加相似。

如果您不熟悉 matplotlib,则可能不知道如何识别每种方法之间的差异。 通过有状态接口,所有命令将直接从pyplot发出,通常是别名plt。 制作简单的线图并在每个轴上添加一些标签如下所示:

>>> import matplotlib.pyplot as plt

>>> x = [-3, 5, 7]
>>> y = [10, 2, 5]

>>> plt.figure(figsize=(15,3))
>>> plt.plot(x, y)
>>> plt.xlim(0, 10)
>>> plt.ylim(-3, 8)
>>> plt.xlabel('X Axis')
>>> plt.ylabel('Y axis')
>>> plt.title('Line Plot')
>>> plt.suptitle('Figure Title', size=20, y=1.03)

面向对象的方法仍然使用pyplot,但是通常,它只是在第一步中创建图形和轴域对象。 创建后,将直接调用这些对象的方法来更改绘图。 以下代码使用面向对象的方法对上一个图进行精确复制:

>>> fig, ax = plt.subplots(figsize=(15,3))
>>> ax.plot(x, y)
>>> ax.set_xlim(0, 10)
>>> ax.set_ylim(-3, 8)
>>> ax.set_xlabel('X axis')
>>> ax.set_ylabel('Y axis')
>>> ax.set_title('Line Plot')
>>> fig.suptitle('Figure Title', size=20, y=1.03)

在这个简单的示例中,我们仅直接使用两个对象,即图形和轴,但是通常,图可以包含数百个对象; 可以使用每一种都以非常精细的方式进行修改,而使用状态接口则不容易做到。 在本章中,我们将构建一个空图并使用面向对象的接口修改其一些基本属性。

操作步骤

  1. 要使用面向对象的方法开始使用 matplotlib,您将需要导入pyplot模块和别名plt
>>> import matplotlib.pyplot as plt
  1. 通常,当使用面向对象的方法时,我们将创建一个图形和一个或多个轴域对象。 让我们使用subplots函数创建具有单个轴的图形:
>>> fig, ax = plt.subplots(nrows=1, ncols=1)

  1. subplots函数返回一个包含图形和一个或多个轴域对象(这里只是一个)的两个项目元组对象,这些对象被解包到变量figax中。 从现在开始,我们将通过常规的面向对象方法调用方法来直接使用这些对象。 让我们看一下每个对象的类型,以确保我们实际使用的是图形和轴域:
>>> type(fig)
matplotlib.figure.Figure

>>> type(ax)
matplotlib.axes._subplots.AxesSubplot
  1. 尽管您将调用比图形方法更多的轴域,但您可能仍需要与它们交互。 让我们找到图的大小,然后将其放大:
>>> fig.get_size_inches()
array([ 6.,  4.])

>>> fig.set_size_inches(14, 4)
>>> fig

  1. 在开始绘制之前,让我们检查一下 matplotlib 层次结构。 您可以使用axes属性收集图中的所有轴:
>>> fig.axes
[<matplotlib.axes._subplots.AxesSubplot at 0x112705ba8>]
  1. 此命令返回所有轴对象的列表。 但是,我们已经将轴对象存储在ax变量中。 让我们确认它们实际上是同一对象:
>>> fig.axes[0] is ax
True
  1. 为了明显地将图形与轴区分开,我们可以给每个图形一个唯一的facecolor。 Matplotlib 接受各种不同的颜色输入类型。 字符串名称支持大约 140 种 HTML 颜色(请参见此列表)。 您还可以使用包含从零到一的浮点数的字符串来表示灰色阴影:
>>> fig.set_facecolor('.9')
>>> ax.set_facecolor('.7')
>>> fig

  1. 现在我们已经区分了图形和轴域,让我们用get_children方法查看轴域的所有直接子代:
>>> ax_children = ax.get_children()
>>> ax_children
[<matplotlib.spines.Spine at 0x11145b358>,
 <matplotlib.spines.Spine at 0x11145b0f0>,
 <matplotlib.spines.Spine at 0x11145ae80>,
 <matplotlib.spines.Spine at 0x11145ac50>,
 <matplotlib.axis.XAxis at 0x11145aa90>,
 <matplotlib.axis.YAxis at 0x110fa8d30>,
 ...]
  1. 每个基本图都有四个刺和两个轴对象。 脊线代表数据边界,是您看到的与较深的灰色矩形(“轴”)接壤的四根物理线。 xy 轴对象包含更多的绘图对象,例如刻度和它们的标签以及整个轴的标签。 我们可以从该列表中选择刺,但这通常不是这样做的。 我们可以使用spines属性直接访问它们:
>>> spines = ax.spines
>>> spines
OrderedDict([('left', <matplotlib.spines.Spine at 0x11279e320>),
             ('right', <matplotlib.spines.Spine at 0x11279e0b8>),
             ('bottom', <matplotlib.spines.Spine at 0x11279e048>),
             ('top', <matplotlib.spines.Spine at 0x1127eb5c0>)])
  1. 刺包含在有序字典中。 让我们选择左侧的脊椎,并更改其位置和宽度,使其更加突出,并使底部的脊椎不可见:
>>> spine_left = spines['left']
>>> spine_left.set_position(('outward', -100))
>>> spine_left.set_linewidth(5)

>>> spine_bottom = spines['bottom']
>>> spine_bottom.set_visible(False)
>>> fig

  1. 现在,让我们集中讨论轴对象。 我们可以通过xaxisyaxis属性直接访问每个轴。Axes对象也可以直接使用某些轴属性。 在此步骤中,我们以两种方式更改每个轴的某些属性:
>>> ax.xaxis.grid(True, which='major', linewidth=2,
                  color='black', linestyle='--')
>>> ax.xaxis.set_ticks([.2, .4, .55, .93])
>>> ax.xaxis.set_label_text('X Axis', family='Verdana', fontsize=15)

>>> ax.set_ylabel('Y Axis', family='Calibri', fontsize=20)
>>> ax.set_yticks([.1, .9])
>>> ax.set_yticklabels(['point 1', 'point 9'], rotation=45)
>>> fig

工作原理

面向对象方法要掌握的关键思想之一是每个绘图元素都具有获取器设置器方法。 获取器方法均以get_开头,并检索特定属性或检索其他绘图对象。 例如,ax.get_yscale()检索绘制 y 轴以字符串形式绘制的比例类型(默认为linear),而ax.get_xticklabels()检索 matplotlib 文本对象列表,每个都有自己的获取器和设置器方法。 设置方法修改特定的属性或整个对象组。 许多 matplotlib 归结为锁存到特定的绘图元素上,然后通过获取器和设置器方法进行检查和修改。

把 matplotlib 层次结构类比为家可能是有用的。 家及其所有内容将是图形。 每个房间都是轴域,房间的内容是艺术家。

开始使用面向对象接口的最简单方法是使用pyplot模块,该模块通常是步骤 1 中的别名plt和。步骤 2 显示了面向对象的方法,是最常见的启动方法之一。plt.subplots函数创建一个图形,以及一个轴域对象网格。 前两个参数nrowsncols和定义了统一的轴对象网格。 例如,plt.subplots(2,4)在一个图形中创建了八个相同大小的轴对象。

plt.subplots函数有点奇怪,因为它返回一个两个项的元组。 第一个元素是图形,第二个元素是轴域对象。 该元组被解压缩为两个不同的变量figax。 如果您不习惯于拆开元组,则可能会看到步骤 2 如下所示:

>>> plot_objects = plt.subplots(nrows=1, ncols=1)
>>> type(plot_objects)
tuple

>>> fig = plot_objects[0]
>>> ax = plot_objects[1]

如果使用plt.subplots和创建多个轴,则元组中的第二项是包含所有轴的 NumPy 数组。 让我们在这里演示一下:

>>> plot_objects = plt.subplots(2, 4)

plot_objects变量是一个元组,其中包含一个数字作为其第一个元素,并包含一个 Numpy 数组作为其第二个元素:

>>> plot_objects[1]
array([[<matplotlib.axes._subplots.AxesSubplot object at 0x133b70a20>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x135d6f9e8>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x1310e4668>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x133565ac8>],
       [<matplotlib.axes._subplots.AxesSubplot object at 0x133f67898>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x1326d30b8>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x1335d5eb8>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x133f78f28>]], dtype=object)

步骤 3 验证我们确实有适当变量引用的图形和轴域对象。 在第 4 步中,我们遇到了获取器和设置器方法的第一个示例。 Matplotlib 将所有图形的默认宽度设置为 6 英寸乘以 4 英寸高,这不是屏幕上的实际大小,但是如果将图形保存到文件中,则将是确切大小。

步骤 5 显示,除了获取器方法之外,有时您还可以通过其属性直接访问另一个绘图对象。 通常,同时存在属性和获取方法来检索同一对象。 例如,查看以下示例:

>>> fig.axes == fig.get_axes()
True

>>> ax.xaxis == ax.get_xaxis()
True

>>> ax.yaxis == ax.get_yaxis()
True

许多美术师都具有facecolor属性,可以将其设置为覆盖一种特定颜色的整个表面,如步骤 7 所示。在步骤 8 中,可以使用get_children方法更好地了解对象层次。 返回轴正下方所有对象的列表。 可以从此列表中选择所有对象,然后开始使用设置器方法来修改属性,但这不是惯例。 通常,我们通常直接从属性或获取器方法中收集对象。

通常,在检索绘图对象时,它们会在列表或字典之类的容器中返回。 这就是在步骤 9 中收集刺时发生的情况。您必须从它们各自的容器中选择单个对象,以便在它们上使用获取器或设置器方法,如在步骤 10 中所做的那样。通常也使用for循环一次迭代一个。

步骤 11 以特殊方式添加网格线。 我们期望有get_gridset_grid方法,但是,只有grid方法,该方法接受布尔值作为打开/关闭网格线的第一个参数。 每个轴都有主刻度和次刻度,但默认情况下,副刻度是关闭的。which参数用于选择带有网格线的刻度线类型。

请注意,步骤 11 的前三行选择xaxis属性并从中调用方法,而后三行直接从轴域对象本身调用等效方法。 第二组方法是 matplotlib 提供的一种方便方式,可以节省一些击键。 通常,大多数对象只能设置自己的属性,而不能设置其子级的属性。 无法通过轴设置许多轴级属性,但是在此步骤中,可以设置一些属性。 两种方法都可以接受。

在步骤 11 中将网格线与第一行添加在一起时,我们设置属性linewidthcolor,,和linestyle。 这些都是 matplotlib 线(正式为Line2D对象)的所有属性。 您可以在此处查看所有可用属性。set_ticks方法接受一个浮点序列,并仅在那些位置绘制刻度线。 使用空列表将完全删除所有刻度。

每个轴可能都标有一些文本,为此 matplotlib 正式使用了Text对象。 所有可用文本属性中仅更改了几个。set_yticklabels轴方法接收一个字符串列表,用作每个刻度的标签。 您可以设置任意数量的文本属性。

更多

为了帮助找到每个绘图对象的所有可能的属性,只需调用properties方法,该方法会将所有它们显示为字典。 让我们看一下轴对象的属性的精选列表:

>>> ax.xaxis.properties()
{'alpha': None,
 'gridlines': <a list of 4 Line2D gridline objects>,
 'label': Text(0.5,22.2,'X Axis'),
 'label_position': 'bottom',
 'label_text': 'X Axis',
 'tick_padding': 3.5,
 'tick_space': 26,
 'ticklabels': <a list of 4 Text major ticklabel objects>,
 'ticklocs': array([ 0.2 , 0.4 , 0.55, 0.93]),
 'ticks_position': 'bottom',
 'visible': True}

另见

使用 matplotlib 可视化数据

Matplotlib 有几十种绘图方法,几乎​​可以想象任何一种绘图。 线,条,直方图,散点图,方格,小提琴,轮廓,饼图以及许多其他图都可以从“轴”对象中用作方法。 只有在 1.5 版(2015 年发布)中,matplotlib 才开始接受来自 Pandas 数据帧的数据。 在此之前,必须将数据从 NumPy 数组或 Python 列表传递给它。

准备

在本秘籍中,我们将通过将 Pandas 数据帧中的数据减少到 NumPy 数组来可视化电影预算随时间的趋势,然后将其传递给 matplotlib 绘图函数。

操作步骤

  1. 既然我们知道如何选择绘图元素并更改其属性,那么让我们实际创建数据可视化。 让我们阅读电影数据集,计算每年的预算中位数,然后找到五年滚动平均值以使数据平滑:
>>> movie = pd.read_csv('data/movie.csv')
>>> med_budget = movie.groupby('title_year')['budget'].median() / 1e6
>>> med_budget_roll = med_budget.rolling(5, min_periods=1).mean()
>>> med_budget_roll.tail()
title_year
2012.0    20.893
2013.0    19.893
2014.0    19.100
2015.0    17.980
2016.0    17.780
Name: budget, dtype: float64
  1. 让我们将数据放入 NumPy 数组中:
>>> years = med_budget_roll.index.values
>>> years[-5:]
array([ 2012.,  2013.,  2014.,  2015.,  2016.])

>>> budget = med_budget_roll.values
>>> budget[-5:]
array([ 20.893,  19.893,  19.1  ,  17.98 ,  17.78 ])
  1. plot方法用于创建折线图。 让我们用它在新图中绘制预算随时间推移的滚动中位数:
>>> fig, ax = plt.subplots(figsize=(14,4), linewidth=5,
                           edgecolor='.5')
>>> ax.plot(years, budget, linestyle='--', 
            linewidth=3, color='.2', label='All Movies')

>>> text_kwargs=dict(fontsize=20, family='cursive')
>>> ax.set_title('Median Movie Budget', **text_kwargs)
>>> ax.set_ylabel('Millions of Dollars', **text_kwargs)

  1. 有趣的是,电影预算中位数在 2000 年达到顶峰,随后又下降了。 也许这只是数据集的人工产物,其中近年来我们拥有的所有电影的数据都更多,而不仅仅是最受欢迎的电影。 让我们找出每年的电影数量:
>>> movie_count = movie.groupby('title_year')['budget'].count()
>>> movie_count.tail()
title_year
2012.0    191
2013.0    208
2014.0    221
2015.0    192
2016.0     86
Name: budget, dtype: int64
  1. 一个轴上可以放置任意数量的图,这些计数可以直接用中位数预算作为条形图绘制。 由于两个图的单位完全不同(美元与计数),因此我们可以创建辅助 y 轴,也可以将计数缩放到与预算相同的范围内。 我们选择后者,并在其前面直接将每个条的值标记为文本。 由于绝大多数数据都包含在最近几年中,因此我们也可以将数据限制为从 1970 年开始拍摄的电影:
>>> ct = movie_count.values
>>> ct_norm = ct / ct.max() * budget.max()

>>> fifth_year = (years % 5 == 0) & (years >= 1970)
>>> years_5 = years[fifth_year]
>>> ct_5 = ct[fifth_year]
>>> ct_norm_5 = ct_norm[fifth_year]

>>> ax.bar(years_5, ct_norm_5, 3, facecolor='.5', 
           alpha=.3, label='Movies per Year')
>>> ax.set_xlim(1968, 2017)
>>> for x, y, v in zip(years    _5, ct_norm_5, ct_5):
        ax.text(x, y + .5, str(v), ha='center')
>>> ax.legend()
>>> fig

  1. 如果仅查看每年预算最高的 10 部电影,这种趋势可能不会成立。 让我们找出每年仅前十部电影的五年滚动中位数:
>>> top10 = movie.sort_values('budget', ascending=False) \
                 .groupby('title_year')['budget'] \
                 .apply(lambda x: x.iloc[:10].median() / 1e6)

>>> top10_roll = top10.rolling(5, min_periods=1).mean()
>>> top10_roll.tail()
title_year
2012.0    192.9
2013.0    195.9
2014.0    191.7
2015.0    186.8
2016.0    189.1
Name: budget, dtype: float64
  1. 对于所有数据,这些数字表示一个比在步骤 13 中发现的数字高一个数量级。 以相同的比例绘制两条线看起来并不好。 让我们创建一个带有两个子图(轴)的全新图形,并在第二个轴中绘制上一步的数据:
>>> fig2, ax_array = plt.subplots(2, 1, figsize=(14,8), sharex=True)
>>> ax1 = ax_array[0]
>>> ax2 = ax_array[1]

>>> ax1.plot(years, budget, linestyle='--', linewidth=3, 
             color='.2', label='All Movies')
>>> ax1.bar(years_5, ct_norm_5, 3, facecolor='.5', 
            alpha=.3, label='Movies per Year')
>>> ax1.legend(loc='upper left')
>>> ax1.set_xlim(1968, 2017)
>>> plt.setp(ax1.get_xticklines(), visible=False)

>>> for x, y, v in zip(years_5, ct_norm_5, ct_5):
        ax1.text(x, y + .5, str(v), ha='center')

>>> ax2.plot(years, top10_roll.values, color='.2',
             label='Top 10 Movies')
>>> ax2.legend(loc='upper left')

>>> fig2.tight_layout()
>>> fig2.suptitle('Median Movie Budget', y=1.02, **text_kwargs)
>>> fig2.text(0, .6, 'Millions of Dollars', rotation='vertical', 
              ha='center', **text_kwargs)

>>> import os
>>> path = os.path.expanduser('~/Desktop/movie_budget.png')
>>> fig2.savefig(path, bbox_inches='tight')

工作原理

在第 1 步中,我们开始寻求分析电影预算的方法,方法是找出每年的预算中位数(百万美元)。 找到每年的预算中位数后,我们决定对其进行平滑处理,因为每年之间会有很大的差异。 我们选择对数据进行平滑处理是因为我们正在寻找一个总体趋势,而不必对任何一年的确切值感兴趣。

在此步骤中,我们使用rolling方法根据最近五年数据的平均值来计算每年的新值。 例如,将 2011 年至 2015 年的预算中位数进行分组并取平均值。 结果是 2015 年的新值。rolling方法唯一需要的参数是窗口的大小,默认情况下,窗口的大小将在当年结束。

rolling方法返回一个类似分组的对象,该对象必须使其组与另一个函数共同作用才能产生结果。 让我们手动验证rolling方法是否能像往年一样工作:

>>> med_budget.loc[2012:2016].mean()
17.78

>>> med_budget.loc[2011:2015].mean()
17.98

>>> med_budget.loc[2010:2014].mean()
19.1

这些值与步骤 1 的输出相同。在步骤 2 中,通过将数据放入 NumPy 数组中,我们准备使用 matplotlib。 在第 3 步中,我们创建图形和轴以设置面向对象的接口。plt.subplots方法支持大量输入。 请参阅此文档以查看此函数和figure函数的所有可能参数

plot方法中的前两个参数表示折线图的 x 和 y 值。 所有行属性都可以在plot的调用中进行更改。轴域的set_title方法提供标题,并可以在其调用内设置所有可用的文本属性。set_ylablel方法也是如此。 如果要为许多对象设置相同的属性,则可以将它们打包在一起作为字典,然后将该字典作为参数之一传递,如**text_kwargs一样。

在第 4 步中,我们注意到 2000 年左右开始的预算中值出现意外下降的趋势,并怀疑每年收集的电影数量可能起到解释作用。 我们选择通过从 1970 年开始每隔五年创建一个条形图来向图表添加此维度。我们对 NumPy 数据数组使用布尔选择的方式与在步骤 5 中对 Pandas 序列的处理方式相同。

bar方法将 x 值的高度和条形的宽度作为其前三个参数,并将条形的中心直接放在每个 x 值处。 条形高度是从电影计数中得出的,电影计数首先被缩小到零到一之间,然后乘以最大中位数预算。 这些钢筋高度存储在变量ct_norm_5中。 为了正确标记每个条形图,我们首先将条形图中心,其高度和实际影片数压缩在一起。 然后,我们遍历此压缩对象,并使用text方法将计数放在小节之前,该方法接受 x 值,y 值和字符串。 我们将 y 值略微向上调整,并使用水平对齐参数ha将文本居中。

回顾步骤 3,您会注意到label参数等于All Moviesplot方法。 这是为绘图创建图例时 matplotlib 使用的值。 调用legend Axes 方法会将所有带有指定标签的图放置在图例中。

为了调查预算中位数的意外下降,我们可以仅关注每年预算最高的 10 部电影。 在按年份分组后,第 6 步使用自定义聚合函数,然后以与以前相同的方式对结果进行平滑处理。 这些结果可以直接绘制在同一张图上,但是由于值要大得多,因此我们选择创建一个带有两个轴的全新图形。

我们通过在两个两行一列的网格中创建具有两个子图的图形来开始执行步骤 7。 请记住,当创建多个子图时,所有轴都存储在 NumPy 数组中。 步骤 5 的最终结果将在顶部轴中重新创建。 我们在底部的轴上绘制预算最高的 10 部电影。 请注意,年份与底部和顶部轴都对齐,因为在图形创建中sharex参数设置为True。 共享轴时,matplotlib 会删除所有刻度线的标签,但会保留每个刻度线的细小垂直线。 要删除这些刻度线,我们使用pyplotsetp函数。 尽管这不是直接面向对象的,但是当我们要为整个绘制对象序列设置属性时,它是显式的并且非常有用。 通过此有用的函数,我们将所有刻度线设置为不可见。

最后,我们然后多次调用图形方法。 这与我们通常调用的轴域方法不同。tight_layout方法通过删除多余的空间并确保不同的轴不会重叠来将子图调整为更好的外观。suptitle方法为整个图形创建标题,而set_title轴方法则为单个轴创建标题。 它接受 x 和 y 位置来表示图形坐标系中的位置,其中(0, 0)表示左下,而(1, 1)表示右上。 默认情况下,y 值为 0.98,但我们将其上移了几个点至 1.02。

每个轴域还具有一个坐标系,其中(0, 0)用于左下角,而(1, 1)用于右上角。 除了那些坐标系之外,每个轴还具有一个数据坐标系,这对于大多数人来说更自然,并表示 xy 轴的边界。 这些界限可以分别通过ax.get_xlim()ax.get_ylim()获取。 在此之前的所有绘图均使用数据坐标系。 请参阅“变换教程”以了解有关坐标系的更多信息。

由于两个轴的 y 轴使用相同的单位,因此我们使用图形的text方法使用图形坐标系将自定义 y 轴标签直接放置在每个轴之间。 最后,我们将图形保存到桌面。 路径中的波浪符号~代表主目录,但是savefig方法无法理解这意味着什么。 您必须使用os库中的expanduser函数来创建完整路径。 例如,path变量在我的机器上变为:

>>> os.path.expanduser('~/Desktop/movie_budget.png')
'/Users/Ted/Desktop/movie_budget.png'

savefig方法现在可以在正确的位置创建文件。 默认情况下,savefig将仅保存在图形坐标系的(0, 0))(1, 1)中绘制的内容。 由于我们的标题略微超出该区域,因此其中一些将被裁剪。 将bbox_inches参数设置为yight,matplotlib 将包含扩展到该区域之外的所有标题或标签。

更多

在 1.5 版发布之后,Matplotlib 开始接受其所有绘图函数的 pandas 数据帧。数据帧通过data参数传递给绘图方法。 这样做使您可以引用具有字符串名称的列。 以下脚本创建了从 2000 年开始随机选择的 100 部电影的 IMDB 分数与年份的散点图。 每个点的大小与预算成比例:

>>> cols = ['budget', 'title_year', 'imdb_score', 'movie_title']
>>> m = movie[cols].dropna()
>>> m['budget2'] = m['budget'] / 1e6
>>> np.random.seed(0)
>>> movie_samp = m.query('title_year >= 2000').sample(100)

>>> fig, ax = plt.subplots(figsize=(14,6))
>>> ax.scatter(x='title_year', y='imdb_score',
               s='budget2', data=movie_samp)

>>> idx_min = movie_samp['imdb_score'].idxmin()
>>> idx_max = movie_samp['imdb_score'].idxmax()
>>> for idx, offset in zip([idx_min, idx_max], [.5, -.5]):
        year = movie_samp.loc[idx, 'title_year']
        score = movie_samp.loc[idx, 'imdb_score']
        title = movie_samp.loc[idx, 'movie_title']
        ax.annotate(xy=(year, score), 
        xytext=(year + 1, score + offset), 
        s=title + ' ({})'.format(score),
        ha='center',
        size=16,
        arrowprops=dict(arrowstyle="fancy"))
>>> ax.set_title('IMDB Score by Year', size=25)
>>> ax.grid(True)

创建散点图后,最高得分的电影和最低得分的电影都用annotate方法标记。xy参数是我们要注释的点的元组。xytext参数是文本位置的另一个元组坐标。 由于ha设置为center,因此文本居中。

另见

Pandas 绘图的基础知识

Pandas 通过自动执行许多步骤使绘制过程变得非常容易。 所有 Pandas 绘图均由 matplotlib 内部处理,并通过数据帧或序列的plot方法公开访问。 我们说 Pandasplot方法是围绕 matplotlib 的包装器。 在 Pandas 中创建图时,将返回 matplotlib 轴或图。 您可以使用 matplotlib 的全部函数来修改该对象,直到获得所需的结果。

Pandas 仅能生成 matplotlib 可用的一小部分图,例如线图,条形图,方框图和散点图,以及核密度估计值KDE)和直方图。 Pandas 通过使过程变得非常简单和高效而擅长于其创建的绘图,通常只需要一行代码,从而节省了探索数据的大量时间。

准备

了解 Pandas 绘图的关键之一就是要知道绘图方法是否需要一个或两个变量来进行绘图。 例如,线图和散点图需要两个变量来绘制每个点。 对于条形图也是如此,后者需要一些 x 坐标来定位条形,并需要另一个变量来设置条形的高度。 箱线图,直方图和 KDE 仅使用一个变量进行绘制。

默认情况下,两变量线图和散点图使用索引作为 x 轴,将列的值用作 y 轴。 单变量图忽略索引,并对每个变量应用转换或聚合以制作其图。 在本秘籍中,我们将考察 Pandas 中两变量和一变量绘图之间的差异。

操作步骤

  1. 创建一个具有有意义索引的小型数据帧:
>>> df = pd.DataFrame(index=['Atiya', 'Abbas', 'Cornelia', 
                             'Stephanie', 'Monte'], 
                      data={'Apples':[20, 10, 40, 20, 50],
                            'Oranges':[35, 40, 25, 19, 33]})

  1. 条形图使用 x 轴的标签索引,并将列值用作条形高度。 在kind参数设置为bar的情况下,使用plot方法:
>>> color = ['.2', '.7']
>>> df.plot(kind='bar', color=color, figsize=(16,4))

  1. KDE 图忽略索引,并将每列的值用作 x 轴,并计算 y 值的概率密度:
>>> df.plot(kind='kde', color=color, figsize=(16,4))

  1. 让我们将所有两个变量图一起绘制在一个图中。 散点图是唯一需要您为 x 和 y 值指定列的散点图。 如果希望使用散点图的索引,则必须使用reset_index方法使其成为一列。 其他两个图使用 x 轴的索引,并为每个数字列创建一组新的线/条:
>>> fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(16,4))
>>> fig.suptitle('Two Variable Plots', size=20, y=1.02)
>>> df.plot(kind='line', color=color, ax=ax1, title='Line plot')
>>> df.plot(x='Apples', y='Oranges', kind='scatter', color=color, 
            ax=ax2, title='Scatterplot')
>>> df.plot(kind='bar', color=color, ax=ax3, title='Bar plot')

  1. 让我们也将所有一变量图放在同一张图中:
>>> fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(16,4))
>>> fig.suptitle('One Variable Plots', size=20, y=1.02)
>>> df.plot(kind='kde', color=color, ax=ax1, title='KDE plot')
>>> df.plot(kind='box', ax=ax2, title='Boxplot')
>>> df.plot(kind='hist', color=color, ax=ax3, title='Histogram')

工作原理

第 1 步创建了一个小的样本数据帧,它将帮助我们说明使用 Pandas 进行的两个变量绘制和一变量绘制之间的差异。 默认情况下,Pandas 将使用数据帧的每个数字列制作一组新的条形,线形,KDE,盒形图或直方图,并在将其作为两变量图时将索引用作 x 值。 散点图是例外之一,必须明确为 x 和 y 值指定一列。

pandas plot方法非常通用,并具有大量参数,可让您根据自己的喜好自定义结果。 例如,您可以设置图形大小,打开和关闭网格线,设置 xy 轴的范围,为图形着色,旋转刻度线,以及更多。

您还可以使用特定 matplotlib 绘图方法可用的任何参数。 多余的参数将由plot方法的**kwds参数收集,并正确传递给基础的 matplotlib 函数。 例如,在第 2 步中,我们创建一个条形图。 这意味着我们可以使用 matplotlib bar函数中可用的所有参数,以及 Pandas plot方法中可用的参数

在第 3 步中,我们创建一个单变量 KDE 图,该图将为数据帧中的每个数字列创建一个密度估计。 步骤 4 将所有两个变量图放置在同一图中。 同样,第 5 步将所有一变量图放置在一起。 第 4 步和第 5 步中的每个步骤都会创建一个具有三个轴对象的图形。 命令plt.subplots(1, 3)创建一个图形,该图形具有分布在一行和三列上的三个轴。 它返回一个由图和包含轴的一维 NumPy 数组组成的两元组。 元组的第一项被解包到变量fig中。 元组的第二个项目被解包为另外三个变量,每个变量一个。 Pandasplot方法方便地带有ax参数,使我们可以将绘图结果放入图中的特定轴中。

更多

除散点图外,所有图均未指定要使用的列。 Pandas 默认使用每一个数字列,并且在使用双变量图的情况下默认使用索引。 当然,您可以指定要用于每个 x 或 y 值的确切列:

>>> fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(16,4))
>>> df.sort_values('Apples').plot(x='Apples', y='Oranges', 
                                  kind='line', ax=ax1)
>>> df.plot(x='Apples', y='Oranges', kind='bar', ax=ax2)
>>> df.plot(x='Apples', kind='kde', ax=ax3)

另见

可视化航班数据集

探索性数据分析主要由可视化指导,而 Pandas 为快速,轻松地创建它们提供了一个很好的接口。 开始可视化任何数据集时的一种简单策略是仅关注单变量图。 最受欢迎的单变量图往往是用于分类数据(通常是字符串)的条形图,以及用于连续数据(总是数字)的直方图,箱形图或 KDE。 直接在项目开始时尝试同时分析多个变量可能会很困难。

准备

在本秘籍中,我们通过直接用 Pandas 创建单变量和多变量图来对航班数据集进行一些基本的探索性数据分析。

操作步骤

  1. 读取航班数据集,并输出前五行:
>>> flights = pd.read_csv('data/flights.csv')
>>> flights.head()

  1. 在开始绘制之前,让我们计算转向,取消,延迟和准时飞行的数量。 我们已经有用于转移和取消的二进制列。 只要航班到达时间晚于预定时间 15 分钟或更长时间,便视为航班延误。 让我们创建两个新的二进制列来跟踪延迟到达和准时到达:
>>> flights['DELAYED'] = flights['ARR_DELAY'].ge(15).astype(int)
>>> cols = ['DIVERTED', 'CANCELLED', 'DELAYED']
>>> flights['ON_TIME'] = 1 - flights[cols].any(axis=1)

>>> cols.append('ON_TIME')
>>> status = flights[cols].sum()
>>> status
DIVERTED       137
CANCELLED      881
DELAYED      11685
ON_TIME      45789
dtype: int64
  1. 现在,让我们在同一图上为分类列和连续列绘制几个图:
>>> fig, ax_array = plt.subplots(2, 3, figsize=(18,8))
>>> (ax1, ax2, ax3), (ax4, ax5, ax6) = ax_array
>>> fig.suptitle('2015 US Flights - Univariate Summary', size=20)

>>> ac = flights['AIRLINE'].value_counts()
>>> ac.plot(kind='barh', ax=ax1, title='Airline')

>>> oc = flights['ORG_AIR'].value_counts()
>>> oc.plot(kind='bar', ax=ax2, rot=0, title='Origin City')

>>> dc = flights['DEST_AIR'].value_counts().head(10)
>>> dc.plot(kind='bar', ax=ax3, rot=0, title='Destination City')

>>> status.plot(kind='bar', ax=ax4, rot=0, 
                log=True, title='Flight Status')
>>> flights['DIST'].plot(kind='kde', ax=ax5, xlim=(0, 3000),
                         title='Distance KDE')
>>> flights['ARR_DELAY'].plot(kind='hist', ax=ax6, 
                              title='Arrival Delay',
                              range=(0,200))

  1. 这不是对所有单变量统计信息的详尽研究,但为我们提供了一些变量的详细信息。 在继续进行多变量图绘制之前,让我们绘制出每周的飞行次数。 使用带有 x 轴上日期的时间序列图的正确情况。 不幸的是,我们在任何列中都没有 Pandas 时间戳,但确实有月和日。to_datetime函数有一个巧妙的技巧,可以识别与时间戳组件匹配的列名。 例如,如果您有一个数据帧架,其中的标题栏正好为三列yearmonth,day,,则将该数据帧传递给to_datetime函数将返回时间戳序列。 要准备我们当前的数据帧,我们需要为年份添加一列,并使用计划的出发时间来获取小时和分钟:
>>> hour = flights['SCHED_DEP'] // 100
>>> minute = flights['SCHED_DEP'] % 100
>>> df_date = flights[['MONTH', 'DAY']].assign(YEAR=2015, HOUR=hour,
                                               MINUTE=minute)
>>> df_date.head()

  1. 然后,几乎可以用to_datetime函数将这个数据帧转换为适当的时间戳序列:
>>> flight_dep = pd.to_datetime(df_date)
>>> flight_dep.head()
0   2015-01-01 16:25:00
1   2015-01-01 08:23:00
2   2015-01-01 13:05:00
3   2015-01-01 15:55:00
4   2015-01-01 17:20:00
dtype: datetime64[ns]
  1. 让我们将此结果用作新索引,然后使用resample方法查找每周的航班计数:
>>> flights.index = flight_dep
>>> fc = flights.resample('W').size()
>>> fc.plot(figsize=(12,3), title='Flights per Week', grid=True)

  1. 这个绘图很有启发性。 看来我们没有十月份的数据。 由于缺少这些数据,如果存在趋势,则很难通过视觉分析任何趋势。 前几周和后几周也低于正常水平,可能是因为没有整周的数据。 让我们每周进行一次缺少少于 1,000 个航班的数据。 然后,我们可以使用interpolate方法填写此丢失的数据:
>>> fc_miss = fc.where(fc > 1000)
>>> fc_intp = fc_miss.interpolate(limit_direction='both')

>>> ax = fc_intp.plot(color='black', figsize=(16,4))
>>> fc_intp[fc < 500].plot(linewidth=10, grid=True, 
                           color='.8', ax=ax)

>>> ax.annotate(xy=(.8, .55), xytext=(.8, .77), 
                xycoords='axes fraction', s='missing data', 
                ha='center', size=20, arrowprops=dict())
>>> ax.set_title('Flights per Week (Interpolated Missing Data)')

  1. 让我们改变方向,专注于多变量绘图。 让我们找到以下 10 个机场:
    • 入境航班旅行的平均距离最长
    • 至少有 100 个航班:
>>> flights.groupby('DEST_AIR')['DIST'] \
           .agg(['mean', 'count']) \
           .query('count > 100') \
           .sort_values('mean') \
           .tail(10) \
           .plot(kind='bar', y='mean', rot=0, legend=False,
                 title='Average Distance per Destination')

  1. 头两个目的地机场在夏威夷也就不足为奇了。 现在,让我们通过对 2,000 英里以下的所有航班的距离和通话时间进行散点图来同时分析两个变量:
>>> fs = flights.reset_index(drop=True)[['DIST', 'AIR_TIME']] \
                .query('DIST <= 2000').dropna()
>>> fs.plot(x='DIST', y='AIR_TIME', kind='scatter',
            s=1, figsize=(16,4))

  1. 正如预期的那样,距离和通话时间之间存在紧密的线性关系,尽管方差似乎随着里程数的增加而增加。 有一些航班不在趋势线之外。 让我们尝试识别它们。 可以使用线性回归模型来正式识别它们,但是由于 Pandas 不直接支持线性回归,因此我们将采用更为手动的方法。 让我们使用cut函数将飞行距离分为八组之一:
>>> fs['DIST_GROUP'] = pd.cut(fs['DIST'], bins=range(0, 2001, 250))
>>> fs['DIST_GROUP'].value_counts().sort_index()
(0, 250]         6529
(250, 500]      12631
(500, 750]      11506
(750, 1000]      8832
(1000, 1250]     5071
(1250, 1500]     3198
(1500, 1750]     3885
(1750, 2000]     1815
Name: DIST_GROUP, dtype: int64
  1. 我们将假设每个组中的所有航班应具有相似的飞行时间,因此,如果飞行时间偏离该组平均值,则为每个航班计算标准差的数量:
>>> normalize = lambda x: (x - x.mean()) / x.std()
>>> fs['TIME_SCORE'] = fs.groupby('DIST_GROUP')['AIR_TIME'] \
                         .transform(normalize)
>>> fs.head()

  1. 现在,我们需要一种发现异常值的方法。 箱形图为检测异常值提供了很好的视觉效果。 不幸的是,尝试使用plot方法绘制箱形图时存在一个错误,但是幸运的是,有一种数据帧的boxplot方法可以正常工作:
>>> ax = fs.boxplot(by='DIST_GROUP', column='TIME_SCORE',
                    figsize=(16,4))
>>> ax.set_title('Z-Scores for Distance Groups')
>>> ax.figure.suptitle('')

  1. 让我们任意选择检查距离均值大于六个标准差的点。 因为我们在步骤 9 中重置了fs数据帧中的索引,所以我们可以使用它来标识广告投放数据帧中的每个唯一行。 让我们创建一个仅包含异常值的单独的数据帧:
>>> outliers = flights.iloc[fs[fs['TIME_SCORE'] > 6].index]
>>> outliers = outliers[['AIRLINE','ORG_AIR', 'DEST_AIR', 'AIR_TIME',
                         'DIST', 'ARR_DELAY', 'DIVERTED']]
>>> outliers['PLOT_NUM'] = range(1, len(outliers) + 1)
>>> outliers

  1. 我们可以使用此表从步骤 9 识别出图中的离群值。Pandas 还提供了一种将表附加到图形底部的方法:
>>> ax = fs.plot(x='DIST', y='AIR_TIME', 
                 kind='scatter', s=1, 
                 figsize=(16,4), table=outliers)
>>> outliers.plot(x='DIST', y='AIR_TIME',
                  kind='scatter', s=25, ax=ax, grid=True)

>>> outs = outliers[['AIR_TIME', 'DIST', 'PLOT_NUM']]
>>> for t, d, n in outs.itertuples(index=False):
        ax.text(d + 5, t + 5, str(n))

>>> plt.setp(ax.get_xticklabels(), y=.1)
>>> plt.setp(ax.get_xticklines(), visible=False)
>>> ax.set_xlabel('')
>>> ax.set_title('Flight Time vs Distance with Outliers')

工作原理

在读取了步骤 1 中的数据并计算了延迟和按时航班的列之后,我们就可以开始制作单变量图了。 在第 3 步中对subplots函数的调用将创建一个大小相等的2 x 3轴网格。 我们将每个轴解压缩到其自己的变量中以进行引用。 对plot方法的每个调用都使用ax参数引用图中的特定轴。value_counts方法用于创建三个序列,这些序列构成了第一行中的绘图。rot参数将刻度标签旋转到给定角度。

左下角的绘图使用 y 轴的对数标度,因为准时航班的数量大约比取消航班的数量大两个数量级。 没有对数刻度,将很难看到左侧的两个条形图。 默认情况下,KDE 图可能会为不可能的值生成正数区域,例如底行中的负数英里。 因此,我们使用xlim参数限制 x 值的范围。

在到达延迟时,在右下角创建的直方图已传递range参数。 这不是 Pandas plot方法的方法签名的直接部分。 相反,此参数由**kwds参数收集,然后传递给 matplotlib hist函数。 在这种情况下,使用xlim不能如上图所示那样工作。可以仅裁剪图而不必重新计算图的该部分的新桶的宽度。 但是,range参数不仅限制了 x 轴,而且仅计算了该范围的箱宽。

第 4 步创建一个特殊的额外数据帧来容纳仅包含日期时间组件的列,以便我们可以在第 5 步中使用to_datetime函数将每一行立即转换为时间戳。resample方法默认情况下,基于传递的日期偏移量使用索引来形成组。 我们以序列返回每周航班数(W),然后在其上调用plot方法,该方法很好地将索引的格式设置为 x 轴。 十月份出现了一个明显的漏洞。

为了填补这个漏洞,我们使用where方法在步骤 7 的第一行中仅将小于 1,000 的值设置为丢失。然后,我们通过线性插值法填充丢失的数据。 默认情况下,interpolate方法仅在正向插值,因此,在数据帧开头的所有丢失值都将保留。 通过将limit_direction参数设置为both,我们确保没有缺失值。 绘制现在存储在fc_intp中的新数据。 为了更清楚地显示缺少的数据,我们选择原始数据中缺少的点,并在前一条线上方的相同轴上绘制线图。 通常,当我们注解绘图时,我们可以使用数据坐标,但是在这种情况下, x 轴的坐标是什么并不明显。 要使用轴坐标系(范围从(0, 0)(1, 1)的坐标系),请将xycoords参数设置为axes fraction。 现在,此新图将错误数据排除在外,这使得发现趋势变得容易得多。 夏季的空中交通流量比一年中其他任何时候都要多。

在第 8 步中,我们使用一长串方法对每个目标机场进行分组,并将meancount两个函数应用于距离列。query方法在方法链中使用时特别好,因为它可以清晰,简洁地选择给定条件的所需数据行。 进入plot方法时,数据帧中有两列,默认情况下,该方法将为每一列绘制条形图。 我们对count列不感兴趣,因此仅选择mean列来形成条形。 此外,在使用数据帧进行打印时,每个列名称都会出现在图例中。 这会将mean一词放在图例中,因此没有用,因此我们通过将legend参数设置为False将其删除。

步骤 9 通过查看行进距离与飞行时间之间的关系来开始新的分析。 由于点的数量众多,我们使用s参数缩小了它们的大小。 为了找到平均需要更长的时间到达目的地的航班,我们在步骤 10 中将每个航班分组为 250 英里,并在步骤 11 中找到与其组平均值的标准差数量。

在步骤 12 中,为by参数的每个唯一值在相同的轴中创建一个新的箱形图。 我们通过在调用boxplot之后将其保存到变量中来捕获轴域对象。 此方法会在图形上方创建不必要的标题,方法是先访问图形然后将suptitle设置为空字符串,然后将其删除。

在第 13 步中,当前数据帧fs包含我们找到最慢航班所需的信息,但它不具备我们可能需要进一步研究的所有原始数据。 因为我们在步骤 9 中重置了fs的索引,所以我们可以使用它来标识与原始行相同的行。 此步骤的第一行为我们做到了这一点。 我们还为每个异常行提供一个唯一的整数,以便以后在绘制时进行标识。

在第 14 步中,我们从与第 9 步中相同的散点图开始,但是使用table参数将离群值表附加到该图的底部。 然后,我们将离群值直接作为散点图绘制在顶部,并确保它们的点较大以轻松识别它们。itertuples方法循环遍历每个数据帧的行,并以元组的形式返回其值。 我们为绘图解压缩相应的 x 和 y 值,并用我们分配给它的编号标记它。

由于工作台直接放置在绘图的下方,因此会干扰 x 轴上的绘图对象。 我们将刻度线标签移动到轴的内部,并删除刻度线和轴标签。 该表向对这些外围事件感兴趣的任何人提供了一些不错的信息。

另见

堆叠面积图以发现新兴趋势

堆积面积图是发现新兴趋势的绝佳可视化工具,尤其是在市场中。 通常会显示诸如互联网浏览器,手机或车辆之类的产品的市场份额百分比。

准备

在本秘籍中,我们将使用从受欢迎的网站 metup.com 收集的数据。 使用堆叠的面积图,我们将显示五个与数据科学相关的聚会组之间的成员分布。

操作步骤

  1. 读取聚会组数据集,将join_date列转换为时间戳,将其放置在索引中,然后输出前五行:
>>> meetup = pd.read_csv('data/meetup_groups.csv', 
                          parse_dates=['join_date'], 
                          index_col='join_date')
>>> meetup.head()

  1. 让我们获取每周加入每个组的人数:
>>> group_count = meetup.groupby([pd.Grouper(freq='W'), 'group']) \
                        .size()
>>> group_count.head()
join_date   group   
2010-11-07  houstonr     5
2010-11-14  houstonr    11
2010-11-21  houstonr     2
2010-12-05  houstonr     1
2011-01-16  houstonr     2
dtype: int64
  1. 取消堆叠组级别,以便每个聚会组都有自己的数据列:
>>> gc2 = group_count.unstack('group', fill_value=0)
>>> gc2.tail()

  1. 此数据代表加入该特定星期的成员数量。 让我们取每一列的累加总和来获得成员的总数:
>>> group_total = gc2.cumsum()
>>> group_total.tail()

  1. 许多堆叠的面积图使用总数的百分比,因此每一行总是相加 100% 。 让我们将每一行除以总行数以得出该百分比:
>>> row_total = group_total.sum(axis='columns')
>>> group_cum_pct = group_total.div(row_total, axis='index')
>>> group_cum_pct.tail()

  1. 现在,我们可以创建堆积面积图,该图将不断累积列,一个列位于另一个列之上:
>>> ax = group_cum_pct.plot(kind='area', figsize=(18,4),
                            cmap='Greys', xlim=('2013-6', None), 
                            ylim=(0, 1), legend=False)
>>> ax.figure.suptitle('Houston Meetup Groups', size=25)
>>> ax.set_xlabel('')
>>> ax.yaxis.tick_right()

>>> plot_kwargs = dict(xycoords='axes fraction', size=15)
>>> ax.annotate(xy=(.1, .7), s='R Users', 
                color='w', **plot_kwargs)
>>> ax.annotate(xy=(.25, .16), s='Data Visualization', 
                color='k', **plot_kwargs)
>>> ax.annotate(xy=(.5, .55), s='Energy Data Science', 
                color='k', **plot_kwargs)
>>> ax.annotate(xy=(.83, .07), s='Data Science',
                color='k', **plot_kwargs)
>>> ax.annotate(xy=(.86, .78), s='Machine Learning',
                color='w', **plot_kwargs)

工作原理

我们的目标是确定休斯敦随时间推移在五个最大的数据科学聚会小组中的成员分布。 为此,我们需要找到自每个小组开始以来的每个时间点的成员总数。 我们有每个人加入每个小组的确切日期和时间。 在第 2 步中,我们按每周分组(偏移别名W)和聚会组,并使用size方法返回该周的签约数量。

所得的序列不适合与 Pandas 作图。 每个聚会组都需要自己的列,因此我们将group索引级别重塑为列。 我们将fill_value选项设置为零,以便在特定星期内没有成员资格的组不会缺少任何值。

我们需要每周的成员总数。 步骤 4 中的cumsum方法为我们提供了此功能。 我们可以在此步骤之后直接创建堆积面积图,这将是可视化原始总成员资格的好方法。 在第 5 步中,通过将每个值除以其行总数,可以找到每个组在所有组中占总数的百分比。 默认情况下,Pandas 会自动按对象的列对齐对象,因此我们不能使用除法运算符。 相反,我们必须使用div方法将对齐轴更改为索引

现在,该数据非常适合我们在步骤 6 中创建的堆积面积图。请注意,pandas 允许您使用日期时间字符串设置轴限制。 如果使用ax.set_xlim方法直接在 matplotlib 中完成此操作将不起作用。 该绘图的开始日期提前了几年,因为休斯顿 R 用户组的成立要早于其他任何组。

更多

尽管数据可视化专家通常对此并不满意,但 Pandas 可以创建饼图。 在这种情况下,我们使用它们来查看整个组随时间分布的快照。 首先,从数据收集结束前的 18 个月开始,每三个月选择一次数据。 我们使用asfreq方法,该方法仅适用于索引中具有日期时间值的数据帧。 偏移别名3MS用于表示每三个月的开始。 由于group_cum_pct是按周汇总的,因此并非总是存在月份的第一天。 我们将method参数设置为bfill,代表回填; 它将及时查看以查找其中包含数据的月份的第一天。 然后,我们使用to_period方法(也仅适用于索引中的日期时间)将索引中的值更改为 Pandas 时间段。 最后,我们对数据进行转置,以便每一列代表该月聚会组中成员的分布:

>>> pie_data = group_cum_pct.asfreq('3MS', method='bfill') \
                            .tail(6).to_period('M').T
>>> pie_data

从这里,我们可以使用plot方法创建饼图:

>>> from matplotlib.cm import Greys
>>> greys = Greys(np.arange(50,250,40))

>>> ax_array = pie_data.plot(kind='pie', subplots=True, 
                             layout=(2,3), labels=None,
                             autopct='%1.0f%%', pctdistance=1.22,
                             colors=greys)
>>> ax1 = ax_array[0, 0]
>>> ax1.figure.legend(ax1.patches, pie_data.index, ncol=3)
>>> for ax in ax_array.flatten():
        ax.xaxis.label.set_visible(True)
        ax.set_xlabel(ax.get_ylabel())
        ax.set_ylabel('')
>>> ax1.figure.subplots_adjust(hspace=.3)

了解 Pandas 与 Seaborn 的区别

在 Pandas 之外,Seaborn 是 Python 数据科学社区中创建可视化效果最广泛的库之一。 像 Pandas 一样,它本身不会进行任何实际的绘制,并且完全依赖于 matplotlib 进行繁重的工作。 Seaborn 绘图函数直接与 pandas 数据帧配合使用,以创建美观的可视化效果。

尽管 seaborn 和 panda 都减少了 matplotlib 的开销,但它们处理数据的方式却完全不同。 几乎所有的 Seaborn 绘图函数都需要整齐(或长)的数据。 当数据采用整齐的格式时,只有将某些函数应用到结果上后,才能准备使用或解释数据。 整洁的数据是使所有其他分析成为可能的原始构建块。 在数据分析过程中处理整洁的数据通常会创建聚合的数据或广泛的数据。 Pandas 使用这种格式的数据进行绘图。

准备

在此秘籍中,我们将使用 seaborn 和 matplotlib 构建相似的图,以明确表明它们接受整齐的数据还是广泛的数据。

操作步骤

  1. 读入员工数据集,并输出前五行:
>>> employee = pd.read_csv('data/employee.csv', 
                           parse_dates=['HIRE_DATE', 'JOB_DATE'])
>>> employee.head()

  1. 导入 seaborn 库,并为其命名为sns
>>> import seaborn as sns
  1. 让我们制作一个条形图,显示每个部门的 Seaborn:
>>> sns.countplot(y='DEPARTMENT', data=employee)

  1. 要使用 Pandas 重现该绘图,我们需要预先汇总数据:
>>> employee['DEPARTMENT'].value_counts().plot('barh')

  1. 现在,让我们使用 seaborn 找到每个种族的平均工资:
>>> ax = sns.barplot(x='RACE', y='BASE_SALARY', data=employee)
>>> ax.figure.set_size_inches(16, 4)

  1. 要用 Pandas 复制它,我们将需要按种族分组:
>>> avg_sal = employee.groupby('RACE', sort=False) \
                      ['BASE_SALARY'].mean()
>>> ax = avg_sal.plot(kind='bar', rot=0, figsize=(16,4), width=.8)
>>> ax.set_xlim(-.5, 5.5)
>>> ax.set_ylabel('Mean Salary')

  1. Seaborn 在大多数绘图函数中,还可以通过第三个变量hue来区分数据中的组。 让我们按种族和性别找到平均工资:
>>> ax = sns.barplot(x='RACE', y='BASE_SALARY', hue='GENDER', 
                     data=employee, palette='Greys')
>>> ax.figure.set_size_inches(16,4)

  1. 对于 Pandas,我们将必须按种族和性别进行分组,然后将性别作为列名称拆开:
>>> employee.groupby(['RACE', 'GENDER'], sort=False) \
            ['BASE_SALARY'].mean().unstack('GENDER') \
            .plot(kind='bar', figsize=(16,4), rot=0,
                  width=.8, cmap='Greys')

  1. 箱形图是 Seaborn 和 Pandas 共同的另一种图。 让我们使用 Seaborn 创建一个按种族和性别划分的薪金箱形图:
>>> sns.boxplot(x='GENDER', y='BASE_SALARY', data=employee,
                hue='RACE', palette='Greys')
>>> ax.figure.set_size_inches(14,4)

  1. Pandas 不容易为该箱形图产生精确的复制。 它可以为性别创建两个单独的轴,然后按种族绘制薪水箱形图:
>>> fig, ax_array = plt.subplots(1, 2, figsize=(14,4), sharey=True)
>>> for g, ax in zip(['Female', 'Male'], ax_array):
        employee.query('GENDER== @g') \
                .boxplot(by='RACE', column='BASE_SALARY',
                         ax=ax, rot=20)
        ax.set_title(g + ' Salary')
        ax.set_xlabel('')
>>> fig.suptitle('')

工作原理

在步骤 2 中导入 seaborn 会更改 matplotlib 的许多默认属性。 在类似字典的对象plt.rcParams中可以访问大约 300 个默认绘图参数。 要恢复 matplotlib 的默认设置,请不带任何参数调用plt.rcdefaults函数。 导入 seaborn 时,Pandas 绘图的样式也会受到影响。 我们的员工数据集满足了整洁数据的要求,因此非常适合用于几乎所有 Seaborn 的绘图函数。

Seaborn 将进行所有汇总; 您只需将数据帧提供给,,data,参数,并使用其字符串名称引用这些列。 例如,在步骤 3 中,countplot函数毫不费力地对DEPARTMENT的每次出现进行计数,以创建条形图。 所有 Seaborn 绘图函数均具有xy参数。 我们可以使用x而不是y绘制垂直条形图。 Pandas 会迫使您做更多的工作来获得相同的绘图。 在第 4 步中,我们必须使用value_counts方法预先计算垃圾箱的高度。

Seaborn 可以使用barplot函数进行更复杂的聚合,如步骤 5 和 7 所示。hue参数进一步在 x 轴上拆分每个组。 通过在步骤 6 和 8 中对xhue变量进行分组,Pandas 能够几乎复制这些图。

箱形图可在海生和 Pandas 中使用,并且可以直接用整洁的数据绘制,而无需任何汇总。 即使没有必要进行聚合,seaborn 仍然具有优势,因为它可以使用hue参数将数据整齐地拆分为单独的组。 如步骤 10 所示,Pandas 无法轻松地从 Seaborn 中复制此功能。每个组都需要使用query方法进行拆分,并绘制在其自己的轴上。 实际上,Pandas 可能会拆分多个变量,从而将列表传递给by参数,但结果却不尽人意:

>>> ax = employee.boxplot(by=['GENDER', 'RACE'], 
                      column='BASE_SALARY', 
                      figsize=(16,4), rot=15)
>>> ax.figure.suptitle('')

另见

使用 Seaborn 网格进行多元分析

要进一步了解 seaborn,了解作为海生网格返回多个轴的函数与返回单个轴的函数之间的层次结构会有所帮助:

网格类型 网格函数 轴函数 变量类型
FacetGrid factorplot stripplotswarmplotboxplotviolinplotlvplotpointplotbarplotcountplot 类别
FacetGrid lmplot regplot 连续
PairGrid pairplot regplotdistplotkdeplot 连续
JointGrid jointplot regplotkdeplotresidplot 连续
ClusterGrid clustermap heatmap 连续

Seaborn 轴函数可以全部独立调用以生成单个图。 在大多数情况下,网格函数使用轴函数来构建网格。 从网格函数返回的最终对象是网格类型,其中有四种不同的类型。 高级用例需要直接使用网格类型,但是在绝大多数情况下,您将调用基础网格函数来生成实际的网格而不是构造器本身。

准备

在本秘籍中,我们将研究性别和种族之间的经验年限与薪水之间的关系。 我们将首先使用 Seaborn 轴函数创建一个简单的回归图,然后使用网格函数为该图添加更多尺寸。

操作步骤

  1. 阅读员工数据集,并创建一个具有多年经验的列:
>>> employee = pd.read_csv('data/employee.csv', 
                       parse_dates=['HIRE_DATE', 'JOB_DATE'])
>>> days_hired = pd.to_datetime('12-1-2016') - employee['HIRE_DATE']

>>> one_year = pd.Timedelta(1, unit='Y')
>>> employee['YEARS_EXPERIENCE'] = days_hired / one_year
>>> employee[['HIRE_DATE', 'YEARS_EXPERIENCE']].head()

  1. 让我们用拟合的回归线创建一个基本的散点图,以表示经验年限和薪水之间的关系:
>>> ax = sns.regplot(x='YEARS_EXPERIENCE', y='BASE_SALARY',
                     data=employee)
>>> ax.figure.set_size_inches(14,4)

  1. regplot函数无法为第三个变量的不同级别绘制多条回归线。 让我们使用其父函数lmplot绘制一个 Seaborn 网格,为男性和女性添加相同的回归线:
>>> g = sns.lmplot('YEARS_EXPERIENCE', 'BASE_SALARY',
                    hue='GENDER', palette='Greys',
                    scatter_kws={'s':10}, data=employee)
>>> g.fig.set_size_inches(14, 4)
>>> type(g)
seaborn.axisgrid.FacetGrid

  1. Seaborn 网格函数的真正功能是它们能够基于另一个变量添加更多轴。 每个 Seaborn 网格都有colrow参数,可用于将数据进一步分为不同的组。 例如,我们可以为数据集中的每个唯一种族创建一个单独的图,并且仍然按性别拟合回归线:
>>> grid = sns.lmplot(x='YEARS_EXPERIENCE', y='BASE_SALARY',
                      hue='GENDER', col='RACE', col_wrap=3,
                      palette='Greys', sharex=False,
                      line_kws = {'linewidth':5},
                      data=employee)
>>> grid.set(ylim=(20000, 120000))

工作原理

在步骤 1 中,我们使用 Pandas 日期函数创建了另一个连续变量。 该数据是 2016 年 12 月 1 日从休斯敦市收集的。我们使用该日期来确定每个员工在该市工作了多长时间。 当减去日期时(如第二行代码所示),我们将返回一个时间增量对象,其最大单位为天。 我们可以简单地将该数字除以 365 来计算经验年限。 取而代之的是,我们使用Timedelta(1, unit='Y')进行更精确的测量,如果您在家数的话,恰好是 365 天,5 小时,42 分钟和 19 秒。

第 2 步使用 seaborn 轴函数regplot来创建带有估计回归线的散点图。 它返回一个轴,我们使用它来更改图形的大小。 为了为每种性别创建两条单独的回归线,我们必须使用其父函数lmplot。 它包含hue参数,该参数为该变量的每个唯一值创建一个新的回归线。 在第 3 步结束时,我们验证lmplot确实确实返回了 Seaborn 网格对象。

Seaborn 网格本质上是整个图形的包装,并提供了一些方便的方法来更改其元素。 所有 Seaborn 网格都可以使用其fig属性访问基础图形。 步骤 4 显示了 Seaborn 网格函数的常见用例,该用例是基于第三个甚至第四个变量创建多个图。 我们将col参数设置为RACE。 为数据集中的六个独特种族中的每个种族创建了六个回归图。 通常,这将返回由 1 行和 6 列组成的网格,但是我们使用col_wrap参数将列数限制为 3。

还有更多可用参数来控制网格的大多数重要方面。 可以从基础线和散点图 matplotlib 函数更改使用参数。 为此,请将scatter_kwsline_kws参数设置为等于具有 matplotlib 参数作为字符串的字典的字典,该字符串与您想要的值配对。

更多

具有分类特征时,我们可以进行类似类型的分析。 首先,让我们将类别变量racedepartment中的级别数分别减少到最常见的前两个和前三个:

>>> deps = employee['DEPARTMENT'].value_counts().index[:2]
>>> races = employee['RACE'].value_counts().index[:3]
>>> is_dep = employee['DEPARTMENT'].isin(deps)
>>> is_race = employee['RACE'].isin(races)
>>> emp2 = employee[is_dep & is_race].copy()
>>> emp2['DEPARTMENT'] = emp2['DEPARTMENT'].str.extract('(HPD|HFD)',
                                                        expand=True)
>>> emp2.shape
(968, 11)

>>> emp2['DEPARTMENT'].value_counts()
HPD    591
HFD    377
Name: DEPARTMENT, dtype: int64

>>> emp2['RACE'].value_counts()
White                        478
Hispanic/Latino              250
Black or African American    240
Name: RACE, dtype: int64

让我们使用一种更简单的轴级函数,例如小提琴图来查看按性别划分的多年工作经验分布:

>>> common_depts = employee.groupby('DEPARTMENT') \
                       .filter(lambda x: len(x) > 50)
>>> ax = sns.violinplot(x='YEARS_EXPERIENCE', y='GENDER',
                        data=common_depts)
>>> ax.figure.set_size_inches(10,4)

然后,我们可以使用网格函数factorplot为带有colrow参数的部门和种族的每个独特组合添加小提琴图:

>>> sns.factorplot(x='YEARS_EXPERIENCE', y='GENDER',
                   col='RACE', row='DEPARTMENT', 
                   size=3, aspect=2,
                   data=emp2, kind='violin')

从秘籍的开头查看表格。factorplot函数必须使用这八个固定轴函数之一。 为此,您可以将其名称作为字符串传递给kind参数。

在 Seaborn 钻石数据集中发现辛普森悖论

不幸的是,在进行数据分析时,报告错误的结果非常容易。 辛普森悖论是一种可以在数据分析中出现的较普遍现象。 当汇总所有数据时,当一组显示的结果高于另一组时,会发生这种情况,但是在将数据细分为不同的细分时,则显示相反的结果。 例如,假设我们有两个学生 A 和 B,他们分别接受了 100 个问题的测试。 学生 A 回答了 50% 正确的问题,而学生 B 回答了 80% 正确的问题。 这显然表明学生 B 具有更高的才能:

假设这两个测试非常不同。 学生 A 的测试包含 95 个困难的问题,只有五个容易解决的问题。 学生 B 接受了完全相反比例的测试。

这描绘了一个完全不同的画面。 现在,学生 A 在困难和容易解决的问题中所占的比例较高,但总体上所占的比例却低得多。 这是辛普森悖论的典型例子。 汇总的整体显示了每个单独部分的相反情况。

准备

在此秘籍中,我们将首先得出一个令人困惑的结果,该结果似乎表明,高品质的钻石比低品质的钻石有价值。 我们通过对数据进行更细粒度的瞥见来揭示辛普森的悖论,这表明事实恰恰相反。

操作步骤

  1. 读入钻石数据集,并输出前五行:
>>> diamonds = pd.read_csv('data/diamonds.csv')
>>> diamonds.head()

  1. 在开始分析之前,让我们将cutcolorclarity列更改为有序的分类变量:
>>> cut_cats = ['Fair', 'Good', 'Very Good', 'Premium', 'Ideal']
>>> color_cats = ['J', 'I', 'H', 'G', 'F', 'E', 'D']
>>> clarity_cats = ['I1', 'SI2', 'SI1', 'VS2',
                    'VS1', 'VVS2', 'VVS1', 'IF']
>>> diamonds['cut'] = pd.Categorical(diamonds['cut'],
                                     categories=cut_cats, 
                                     ordered=True)

>>> diamonds['color'] = pd.Categorical(diamonds['color'],
                                       categories=color_cats, 
                                       ordered=True)

>>> diamonds['clarity'] = pd.Categorical(diamonds['clarity'],
                                         categories=clarity_cats, 
                                         ordered=True)
  1. Seaborn 对其绘图使用类别顺序。 让我们用条形图来表示每个级别的切割,颜色和清晰度的平均价格:
>>> import seaborn as sns
>>> fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(14,4))
>>> sns.barplot(x='color', y='price', data=diamonds, ax=ax1)
>>> sns.barplot(x='cut', y='price', data=diamonds, ax=ax2)
>>> sns.barplot(x='clarity', y='price', data=diamonds, ax=ax3)
>>> fig.suptitle('Price Decreasing with Increasing Quality?') 

  1. 颜色和价格似乎呈下降趋势。 最高质量的切割和净度水平也价格低廉。 怎么会这样? 让我们更深入地挖掘并再次绘制每种钻石颜色的价格,但为每个透明度级别绘制一个新图:
>>> sns.factorplot(x='color', y='price', col='clarity',
                   col_wrap=4, data=diamonds, kind='bar')

  1. 这个绘图更具启发性。 尽管价格似乎随着颜色质量的提高而下降,但是当清晰度达到最高水平时,价格不会下降。 价格实际上有大幅上涨。 我们还没有仅仅关注钻石的价格,而没有关注其尺寸。 让我们从步骤 3 重新创建图,但使用克拉大小代替价格:
>>> fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(14,4))
>>> sns.barplot(x='color', y='carat', data=diamonds, ax=ax1)
>>> sns.barplot(x='cut', y='carat', data=diamonds, ax=ax2)
>>> sns.barplot(x='clarity', y='carat', data=diamonds, ax=ax3)
>>> fig.suptitle('Diamond size decreases with quality')

  1. 现在我们的故事开始变得更有意义了。 高质量的钻石似乎尺寸较小,这在直觉上是有道理的。 让我们创建一个新变量,将carat值分为五个不同的部分,然后创建一个点图。 准确地显示出以下事实表明,按尺寸细分质量更高的钻石确实会花费更多金钱:
>>> diamonds['carat_category'] = pd.qcut(diamonds.carat, 5)

>>> from matplotlib.cm import Greys
>>> greys = Greys(np.arange(50,250,40))

>>> g = sns.factorplot(x='clarity', y='price', data=diamonds,
                       hue='carat_category', col='color', 
                       col_wrap=4, kind='point', palette=greys)
>>> g.fig.suptitle('Diamond price by size, color and clarity',
                   y=1.02, size=20)

工作原理

在此秘籍中,创建分类列非常重要,因为可以对它们进行排序。 Seaborn 使用此顺序将标签放置在绘图上。 第 3 步和第 4 步显示了明显增加钻石质量的下降趋势。 这就是辛普森悖论成为中心焦点的地方。 整体的汇总结果与其他尚未检查的变量混淆。

揭示这一矛盾的关键在于关注克拉的大小。 第 5 步向我们揭示克拉的大小也随着质量的增加而减小。 考虑到这一事实,我们使用qcut函数将钻石尺寸切成五个相等大小的容器。 默认情况下,此函数根据给定的分位数将变量分为离散类别。 通过像此步骤一样将整数传递给它,可以创建等距的分位数。 您还可以选择将显式非规则分位数的序列传递给它。

使用此新变量,我们可以绘制第 6 步中每组每颗钻石尺寸的平均价格的图。seaborn 中的点图将创建一个连接每个类别均值的线图。 每个点的竖线是该组的标准差。 该图证实,只要我们将克拉的大小保持不变,钻石的确会随着质量的提高而变得更加昂贵。

更多

步骤 3 和 5 中的条形图可以使用更高级的 seaborn PairGrid构造器创建的,该构造器可以绘制双变量关系。 使用PairGrid分为两个步骤。 对PairGrid的第一次调用通过提醒网格哪些变量将为 x 和哪些变量将为 y 来准备网格。 第二步将图应用于 x 和 y 列的所有组合:

>>> g = sns.PairGrid(diamonds,size=5,
                 x_vars=["color", "cut", "clarity"],
                 y_vars=["price"])
>>> g.map(sns.barplot)
>>> g.fig.suptitle('Replication of Step 3 with PairGrid', y=1.02)

posted @ 2025-10-23 15:13  绝不原创的飞龙  阅读(0)  评论(0)    收藏  举报