Pandas 数据分析实用指南第二版(一)

原文:annas-archive.org/md5/ef72ddf5930d597094f1662f9e78e83e

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

数据科学通常被描述为一个跨学科的领域,其中编程技能、统计知识和领域知识相交。它已经迅速成为我们社会中最热门的领域之一,懂得如何处理数据已成为当今职业生涯中的必备技能。不论行业、角色或项目如何,数据技能的需求都很高,而学习数据分析是产生影响的关键。

数据科学领域涉及许多不同的方面:数据分析师更侧重于提取业务洞察,而数据科学家则更侧重于将机器学习技术应用于业务问题。数据工程师则专注于设计、构建和维护供数据分析师和数据科学家使用的数据管道。机器学习工程师与数据科学家的技能集有很多相似之处,并且像数据工程师一样,他们是熟练的软件工程师。数据科学领域涵盖了许多领域,但对于所有这些领域,数据分析都是基础构建模块。本书将为你提供入门所需的技能,无论你的旅程将带你走向何方。

传统的数据科学技能集包括了解如何从各种来源(如数据库和 API)收集数据并进行处理。Python 是一种流行的数据科学语言,提供了收集和处理数据以及构建生产级数据产品的手段。由于它是开源的,通过利用他人编写的库来解决常见的数据任务和问题,使得开始进行数据科学变得容易。

Pandas 是与 Python 中的数据科学同义的强大且流行的库。本书将通过使用 Pandas 在现实世界的数据集上进行数据分析,为你提供动手实践的入门,包括涉及股市、模拟黑客攻击、天气趋势、地震、葡萄酒和天文数据的实际案例。Pandas 通过使我们能够高效地处理表格数据,简化了数据处理和可视化的过程。

一旦我们掌握了如何进行数据分析,我们将探索许多应用。我们将构建 Python 包,并尝试进行股票分析、异常检测、回归、聚类和分类,同时借助常用于数据可视化、数据处理和机器学习的额外库,如 Matplotlib、Seaborn、NumPy 和 Scikit-learn。在你完成本书后,你将能充分准备好,开展自己的 Python 数据科学项目。

本书适用对象

本书面向那些有不同经验背景的人,旨在学习 Python 中的数据科学,可能是为了应用于项目、与数据科学家合作和/或与软件工程师一起进行机器学习生产代码的工作。如果你的背景与以下之一(或两个)相似,你将从本书中获得最大的收益:

  • 你在其他语言(如 R、SAS 或 MATLAB)中有数据科学的经验,并希望学习 pandas,将你的工作流程迁移到 Python。

  • 你有一定的 Python 经验,并希望学习如何使用 Python 进行数据科学。

本书内容

第一章数据分析简介,教你数据分析的基本原理,为你打下统计学基础,并指导你如何设置环境以便在 Python 中处理数据并使用 Jupyter Notebooks。

第二章操作 Pandas DataFrame,介绍了 pandas 库,并展示了如何处理 DataFrame 的基础知识。

第三章使用 Pandas 进行数据整理,讨论了数据操作的过程,展示了如何探索 API 获取数据,并引导你通过 pandas 进行数据清洗和重塑。

第四章聚合 Pandas DataFrame,教你如何查询和合并 DataFrame,如何对它们执行复杂的操作,包括滚动计算和聚合,以及如何有效处理时间序列数据。

第五章使用 Pandas 和 Matplotlib 可视化数据,展示了如何在 Python 中创建你自己的数据可视化,首先使用 matplotlib 库,然后直接从 pandas 对象中创建。

第六章使用 Seaborn 绘图及自定义技术,继续讨论数据可视化,教你如何使用 seaborn 库来可视化你的长格式数据,并为你提供自定义可视化的工具,使其达到可用于展示的效果。

第七章金融分析 – 比特币与股票市场,带你了解如何创建一个用于分析股票的 Python 包,并结合从 第一章数据分析简介,到 第六章使用 Seaborn 绘图及自定义技术,所学的所有内容,并将其应用于金融领域。

第八章基于规则的异常检测,介绍了如何模拟数据并应用从 第一章数据分析简介,到 第六章使用 Seaborn 绘图及自定义技术,所学的所有知识,通过基于规则的异常检测策略来捕捉试图认证进入网站的黑客。

第九章在 Python 中入门机器学习,介绍了机器学习以及如何使用scikit-learn库构建模型。

第十章更好的预测 - 优化模型,向你展示了调整和提高机器学习模型性能的策略。

第十一章机器学习异常检测,重新探讨了通过机器学习技术进行登录尝试数据的异常检测,同时让你了解实际工作流程的样子。

第十二章前路漫漫,讲解了提升技能和进一步探索的资源。

为了最大程度地从本书中受益

你应该熟悉 Python,特别是 Python 3 及以上版本。你还需要掌握如何编写函数和基本脚本,理解标准编程概念,如变量、数据类型和控制流程(if/else、for/while 循环),并能将 Python 用作函数式编程语言。具备一些面向对象编程的基础知识会有所帮助,但并不是必需的。如果你的 Python 水平尚未达到这一程度,Python 文档中有一个有用的教程,能帮助你迅速入门:docs.python.org/3/tutorial/index.html

本书的配套代码可以在 GitHub 上找到,地址为github.com/stefmolin/Hands-On-Data-Analysis-with-Pandas-2nd-edition。为了最大程度地从本书中受益,建议你在阅读每一章时,在 Jupyter 笔记本中进行跟随操作。我们将在第一章数据分析导论中介绍如何设置环境并获取这些文件。请注意,如果需要,还可以参考 Python 101 笔记本,它提供了一个速成课程/复习资料:github.com/stefmolin/Hands-On-Data-Analysis-with-Pandas-2nd-edition/blob/master/ch_01/python_101.ipynb

最后,务必完成每章末尾的练习。有些练习可能相当具有挑战性,但它们会让你对材料的理解更加深入。每章练习的解答可以在github.com/stefmolin/Hands-On-Data-Analysis-with-Pandas-2nd-edition/tree/master/solutions中找到,位于各自的文件夹内。

下载彩色图像

我们还提供了一份 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。你可以在这里下载:static.packt-cdn.com/downloads/9781800563452_ColorImages.pdf

使用的约定

本书中使用了一些文本约定。

文本中的代码:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL 和用户输入。以下是一个例子:“使用pip安装requirements.txt文件中的包。”

一段代码会如下所示。该行的开头将以>>>为前缀,接下来的行将以...为前缀:

>>> df = pd.read_csv(
...     'data/fb_2018.csv', index_col='date', parse_dates=True
... )
>>> df.head()

任何没有前缀>>>...的代码我们不会执行——它仅供参考:

try:
    del df['ones']
except KeyError:
    pass # handle the error here

当我们希望将你的注意力引导到代码块的某一部分时,相关的行或项会被加粗显示:

>>> df.price.plot(
...     title='Price over Time', ylim=(0, None)
... )

结果将显示在没有任何前缀的行中:

>>> pd.Series(np.random.rand(2), name='random')
0 0.235793
1 0.257935
Name: random, dtype: float64

任何命令行输入或输出都如下所示:

# Windows:
C:\path\of\your\choosing> mkdir pandas_exercises
# Linux, Mac, and shorthand:
$ mkdir pandas_exercises

加粗:表示新术语、重要词汇或屏幕上看到的词。例如,菜单或对话框中的词会以这种方式出现在文本中。以下是一个例子:“使用文件浏览器窗格,双击ch_01文件夹,该文件夹包含我们将用来验证安装的 Jupyter Notebook。”

提示或重要注意事项

以这种方式显示。

联系我们

我们始终欢迎读者的反馈。

customercare@packtpub.com

勘误表:尽管我们已尽力确保内容的准确性,但错误仍然会发生。如果你在本书中发现错误,我们将不胜感激,如果你能向我们报告。请访问www.packtpub.com/support/errata,选择你的书籍,点击“勘误提交表单”链接,并填写相关细节。

copyright@packt.com,并链接到该材料。

如果你有兴趣成为作者:如果你在某个领域有专业知识,并且有兴趣写作或为书籍做贡献,请访问authors.packtpub.com

评审

请留下评论。一旦你阅读并使用了本书,为什么不在你购买的站点上留下评论呢?潜在的读者可以看到并利用你的公正意见来做出购买决策,我们在 Packt 可以了解你对我们产品的看法,而我们的作者也能看到你对他们书籍的反馈。谢谢!

欲了解更多关于 Packt 的信息,请访问packt.com

第一部分:开始使用 Pandas

我们的旅程从数据分析和统计学的介绍开始,这将为我们在本书中覆盖的概念奠定坚实的基础。接下来,我们将设置我们的 Python 数据科学环境,这个环境包含了我们在完成示例时所需的一切,并开始学习 pandas 的基础知识。

本节包括以下章节:

  • 第一章数据分析介绍

  • 第二章使用 Pandas DataFrames

第一章:第一章:数据分析简介

在我们开始使用 pandas 进行数据分析的实践介绍之前,我们需要学习数据分析的基础知识。那些曾经查看过软件库文档的人都知道,如果你不知道自己在找什么,它可能会让人感到压倒性的复杂。因此,掌握不仅是编码方面的技能,还需要掌握分析数据所需的思维方式和工作流程,这将对未来提升我们的技能集非常有帮助。

与科学方法类似,数据科学也有一些常见的工作流程,当我们想进行分析并展示结果时,可以遵循这些流程。这个过程的核心是统计学,它为我们提供了描述数据、做出预测以及得出结论的方法。由于不要求具备统计学的先验知识,本章将让我们接触到在本书中将要使用的统计概念,以及可以进一步探索的领域。

在掌握基础知识之后,我们将为本书的剩余部分设置我们的 Python 环境。Python 是一门强大的语言,其用途远远超出了数据科学:例如构建 web 应用程序、软件开发和网页抓取等。为了在项目之间有效地工作,我们需要学习如何创建虚拟环境,这样可以将每个项目的依赖关系隔离开来。最后,我们将学习如何使用 Jupyter Notebooks,以便跟随书中的内容进行实践。

本章将涵盖以下主题:

  • 数据分析的基础

  • 统计基础

  • 设置虚拟环境

本章材料

本书的所有文件都可以在 GitHub 上找到:github.com/stefmolin/Hands-On-Data-Analysis-with-Pandas-2nd-edition。虽然不一定需要 GitHub 账户来完成本书中的内容,但创建一个账户是个好主意,因为它可以作为任何数据/编程项目的作品集。此外,使用 Git 将提供一个版本控制系统,并使协作变得更容易。

提示

阅读这篇文章,了解一些 Git 基础:www.freecodecamp.org/news/learn-the-basics-of-git-in-under-10-minutes-da548267cc91/

为了获取文件的本地副本,我们有几个选项(按从最不实用到最实用的顺序排列):

  • 下载 ZIP 文件并在本地解压文件。

  • 直接克隆仓库,而不是先 fork。

  • 先 fork 仓库然后克隆它。

本书为每一章都提供了练习;因此,建议那些希望将自己的解答与原始内容一起保存在 GitHub 上的读者fork仓库并克隆fork 后的版本。当我们 fork 一个仓库时,GitHub 会在我们自己的个人资料下创建一个包含原始仓库最新版本的仓库。然后,任何时候我们对自己的版本做出更改,都可以将更改推送回去。请注意,如果我们只是克隆仓库,将无法享受到这一点。

启动此过程的相关按钮在以下截图中已被圈出:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_1.1_B16834.jpg

](https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_1.1_B16834.jpg)

图 1.1 – 获取本地代码副本以便跟随

重要提示

克隆过程将把文件复制到当前工作目录中的一个名为Hands-On-Data-Analysis-with-Pandas-2nd-edition的文件夹中。为了创建一个文件夹来放置这个仓库,我们可以使用mkdir my_folder && cd my_folder。这将创建一个名为my_folder的新文件夹(目录),然后将当前目录更改为该文件夹,之后我们就可以克隆仓库。我们可以通过在命令之间添加&&来将这两个命令(以及任何数量的命令)连接起来。这可以理解为然后(前提是第一个命令成功执行)。

这个仓库为每一章提供了文件夹。本章的材料可以在github.com/stefmolin/Hands-On-Data-Analysis-with-Pandas-2nd-edition/tree/master/ch_01找到。虽然本章大部分内容不涉及编程,但你可以在 GitHub 网站上跟随introduction_to_data_analysis.ipynb笔记本,直到我们在本章末尾设置环境为止。设置完成后,我们将使用check_your_environment.ipynb笔记本来熟悉 Jupyter 笔记本并运行一些检查,确保一切都为本书的其余部分做好准备。

由于用于生成这些笔记本内容的代码并不是本章的主要内容,因此大部分代码已被分离到visual_aids包中,该包用于创建视觉效果以便在本书中解释概念,还有check_environment.py文件。如果你选择查看这些文件,不必感到不知所措;本书将涵盖所有与数据科学相关的内容。

每一章都有练习;然而,仅此一章中有一个exercises.ipynb笔记本,其中包含生成一些初始数据的代码。完成这些练习需要具备基本的 Python 知识。对于想复习基础的读者,请确保运行本章材料中附带的python_101.ipynb笔记本,进行快速入门。对于更正式的介绍,官方的 Python 教程是一个很好的起点:docs.python.org/3/tutorial/index.html

数据分析基础

数据分析是一个高度迭代的过程,包括收集、准备(清洗)、探索性数据分析EDA)和得出结论。在分析过程中,我们将频繁回顾这些步骤。下图展示了一个通用的工作流程:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_1.2_B16834.jpg

图 1.2 – 数据分析工作流程

在接下来的几节中,我们将概述每个步骤,首先是数据收集。实际上,这个过程通常偏向于数据准备阶段。调查发现,尽管数据科学家最不喜欢数据准备工作,但它占据了他们 80%的工作时间(www.forbes.com/sites/gilpress/2016/03/23/data-preparation-most-time-consuming-least-enjoyable-data-science-task-survey-says/)。这一数据准备步骤正是pandas大放异彩的地方。

数据收集

数据收集是任何数据分析的自然第一步——我们无法分析没有的数据。实际上,我们的分析可以在我们拥有数据之前就开始。当我们决定想要调查或分析什么时,我们必须考虑可以收集哪些对分析有用的数据。虽然数据可以来自任何地方,但在本书中我们将探讨以下几种数据来源:

  • 使用网页抓取从网站的 HTML 中提取数据(通常使用 Python 包,如seleniumrequestsscrapybeautifulsoup

  • cURLrequests Python 包

  • 数据库(可以通过 SQL 或其他数据库查询语言提取数据)

  • 提供数据下载的互联网资源,如政府网站或雅虎财经

  • 日志文件

    重要说明

    第二章使用 Pandas DataFrame,将教会我们如何处理上述数据来源。第十二章前方的道路,提供了许多寻找数据源的资源。

我们周围充满了数据,因此可能性是无限的。然而,重要的是要确保我们收集的数据有助于我们得出结论。例如,如果我们试图确定温度较低时热巧克力的销售量是否更高,我们应该收集每天售出的热巧克力数量和当天的温度数据。虽然了解人们为获取热巧克力旅行的距离可能很有趣,但这与我们的分析无关。

在开始分析之前,不要过于担心找到完美的数据。很可能,总会有一些我们想要从初始数据集中添加/删除、重新格式化、与其他数据合并或以某种方式更改的内容。这就是数据清理发挥作用的地方。

数据清理

数据清理是准备数据并将其转换为可以用于分析的格式的过程。数据的一个不幸现实是,它通常是“脏”的,这意味着在使用之前需要进行清理(准备)。以下是我们可能遇到的一些数据问题:

  • 100而不是1000,或是打字错误。此外,可能会记录多个相同条目的版本,比如New York CityNYCnyc

  • 计算机错误:也许我们有一段时间没有记录条目(缺失数据)。

  • 意外值:也许记录数据的人决定用问号表示数字列中的缺失值,这样该列中的所有条目就会被当作文本处理,而不是数字值。

  • 信息不完整:想象一下一个带有可选问题的调查;并不是每个人都会回答这些问题,因此我们会有缺失数据,但这并非由于计算机或人为错误。

  • 分辨率:数据可能是按秒收集的,而我们需要按小时的数据来进行分析。

  • 领域的相关性:通常,数据是作为某个过程的产物被收集或生成的,而不是专门为我们的分析而收集。为了将其转化为可用状态,我们需要对其进行清理。

  • 数据的格式:数据可能以不利于分析的格式记录,这需要我们对其进行重塑。

  • 数据记录过程中的配置错误:来自配置错误的跟踪器和/或 Web 钩子的数据可能会缺失字段或以错误的顺序传递。

大多数数据质量问题是可以解决的,但有些问题是无法解决的,比如当数据是按天收集的,而我们需要按小时的数据。这时,我们有责任仔细检查我们的数据,处理任何问题,以确保我们的分析不被扭曲。我们将在第三章《使用 Pandas 的数据清理》和第四章《Pandas 数据框架的聚合》中详细讨论这个过程。

一旦我们对数据进行了初始清理,我们就准备好进行 EDA 了。请注意,在 EDA 过程中,我们可能需要一些额外的数据整理:这两个步骤是高度交织在一起的。

探索性数据分析

在 EDA 过程中,我们使用可视化和总结统计来更好地理解数据。由于人脑擅长发现视觉模式,数据可视化对于任何分析都是至关重要的。事实上,某些数据的特征只能在图表中观察到。根据我们的数据,我们可能创建图表来查看感兴趣的变量随时间的变化情况,比较每个类别包含多少观察结果,查找异常值,查看连续和离散变量的分布等等。在第五章使用 Pandas 和 Matplotlib 可视化数据,以及第六章使用 Seaborn 和自定义技术绘图,我们将学习如何为 EDA 和演示创建这些图表。

重要提示

数据可视化非常强大;不幸的是,它们经常会误导。一个常见问题源于y轴的刻度,因为大多数绘图工具默认会放大以展示近距离的模式。软件很难知道每种可能图形的合适轴限制;因此,在展示结果之前,我们的工作是适当调整坐标轴。您可以阅读更多有关图表可能误导的方法,详见venngage.com/blog/misleading-graphs/

在我们之前看到的工作流程图(图 1.2)中,EDA 和数据整理共享一个框。这是因为它们密切相关:

  • 在进行 EDA 之前,数据需要准备好。

  • 在 EDA 过程中创建的可视化可能表明需要进行额外的数据清理。

  • 数据整理使用总结统计来查找潜在的数据问题,而 EDA 则用于理解数据。当我们进行 EDA 时,不正确的清理将扭曲研究结果。此外,需要数据整理技能来跨数据子集获取总结统计。

在计算总结统计时,我们必须牢记我们收集到的数据类型。数据可以是定量的(可测量的数量)或分类的(描述、分组或类别)。在这些数据类别中,我们有进一步的细分,可以让我们知道可以在其上执行哪些操作。

例如,分类数据可以是 on = 1 / off = 0。请注意,on 大于 off 的事实是没有意义的,因为我们任意选择这些数字来表示 onoff 的状态。当类别之间有排名时,它们是 low < medium < high

定量数据可以使用区间尺度比例尺度。区间尺度包括像温度这样的量。我们可以用摄氏度来测量温度,并比较两个城市的温度,但说一个城市的温度是另一个城市的两倍并没有意义。因此,区间尺度的值可以通过加法/减法进行有意义的比较,但不能通过乘法/除法进行比较。比例尺度则是那些可以通过比率(乘法和除法)进行有意义比较的值。例如,价格、大小和数量都属于比例尺度。

完成 EDA 后,我们可以通过得出结论来决定接下来的步骤。

得出结论

在我们收集了分析所需的数据、清理了数据并进行了深入的 EDA(探索性数据分析)之后,就到了得出结论的阶段。这时,我们总结 EDA 中的发现,并决定接下来的步骤:

  • 在可视化数据时,我们是否注意到了任何模式或关系?

  • 我们是否可以从数据中做出准确的预测?继续对数据进行建模是否有意义?

  • 我们是否需要处理缺失的数据点?如何处理?

  • 数据的分布情况如何?

  • 数据是否能帮助我们回答问题或为我们正在调查的问题提供洞察?

  • 我们是否需要收集新的或额外的数据?

如果我们决定对数据进行建模,这将涉及到机器学习和统计学。虽然严格来说,这不属于数据分析范畴,但通常是下一步,我们将在第九章Python 中的机器学习入门,和第十章做出更好的预测——优化模型中讨论。此外,我们将在第十一章机器学习异常检测中看到这个过程在实践中的应用。作为参考,附录中的机器学习工作流部分提供了一张完整的工作流图,展示了从数据分析到机器学习的全过程。第七章金融分析——比特币与股市,和第八章基于规则的异常检测,将集中讨论从数据分析中得出结论,而不是构建模型。

下一节将回顾统计学内容;有统计学基础的读者可以跳过并直接阅读设置虚拟环境部分。

统计学基础

当我们想要对所分析的数据做出观察时,我们常常(如果不是总是的话)以某种方式借助统计学。我们所拥有的数据被称为样本,它是从(并且是)总体中观察到的一个子集。统计学有两大类:描述性统计和推断性统计。通过描述性统计,顾名思义,我们的目的是描述样本。推断性统计则是利用样本统计量来推断或推测有关总体的某些信息,比如潜在的分布情况。

重要提示

样本统计量被用作总体参数的估计量,这意味着我们必须量化它们的偏差和方差。对此有多种方法;一些方法会对分布的形状做出假设(参数法),而另一些则不会(非参数法)。这些内容远远超出了本书的范围,但了解它们是有益的。

通常,分析的目标是为数据创造一个故事;不幸的是,统计数据非常容易被误用。这正是某句名言的主题:

“有三种谎言:谎言、可恶的谎言和统计数据。”

— 本杰明·迪斯雷利

这在推断性统计中尤为突出,推断性统计在许多科学研究和论文中用于展示研究者发现的显著性。这是一个更为高级的话题,因为本书并非统计学书籍,我们将仅简要介绍推断性统计背后的一些工具和原理,读者可以进一步深入学习。我们将专注于描述性统计,帮助解释我们正在分析的数据。

抽样

在我们尝试进行任何分析之前,有一个重要的事情需要记住:我们的样本必须是随机样本,且能够代表总体。这意味着数据必须是无偏采样的(例如,如果我们在询问人们是否喜欢某个体育队,我们不能只问该队的球迷),同时我们应该确保样本中包含(理想情况下)总体中所有不同群体的成员(在体育队的例子中,我们不能只问男性)。

当我们讨论在第九章中关于机器学习的内容时,在 Python 中入门机器学习,我们需要对数据进行抽样,而数据本身就是一个初步的样本。这称为重采样。根据数据的不同,我们需要选择不同的抽样方法。通常,我们最好的选择是简单随机抽样:我们使用随机数生成器随机挑选行。当数据中有不同的组时,我们希望我们的样本是分层随机抽样,这种方法会保持数据中各组的比例。在某些情况下,我们没有足够的数据来使用上述的抽样策略,因此我们可能会采用有放回的随机抽样(自助法);这称为自助样本。请注意,我们的基础样本需要是随机样本,否则我们可能会增加估计量的偏差(如果是便利样本,由于某些行在数据中出现的频率较高,我们可能会更频繁地选择这些行,但在真实的总体中,这些行的出现频率可能没有那么高)。我们将在第八章中看到自助法的一个例子,基于规则的异常检测

重要提示

讨论自助法背后的理论及其后果远远超出了本书的范围,但可以通过观看这个视频来了解基本概念:www.youtube.com/watch?v=gcPIyeqymOU

你可以在www.khanacademy.org/math/statistics-probability/designing-studies/sampling-methods-stats/a/sampling-methods-review阅读更多关于抽样方法的信息,以及它们的优缺点。

描述性统计

我们将从描述性统计的单变量统计开始讨论;单变量意味着这些统计量是从一个()变量计算出来的。本节中的所有内容都可以扩展到整个数据集,但统计量将是按我们记录的每个变量来计算的(意味着如果我们有 100 个速度和距离的配对观测值,我们可以计算整个数据集的平均值,这将给出平均速度和平均距离的统计数据)。

描述性统计用于描述和/或总结我们正在处理的数据。我们可以通过集中趋势的度量开始数据的总结,它描述了大多数数据集中在哪个区域,并且通过离散度分散度的度量来表示数据值的分布范围。

集中趋势的度量

集中趋势的度量描述了我们数据分布的中心位置。有三种常用的统计量作为中心的度量:均值、中位数和众数。每种方法都有其独特的优点,取决于我们处理的数据类型。

均值

也许最常见的用于总结数据的统计量是平均值,或(0 + 1 + 1 + 2 + 9)/5

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/B16834_01_002.jpg

我们用xi 来表示变量X的第i个观察值。注意,变量本身用大写字母表示,而具体的观察值则用小写字母表示。Σ(希腊大写字母Sigma)用于表示求和,在均值的公式中,它的求和范围从1n,其中n是观察值的数量。

关于均值,有一点需要注意,那就是它对异常值(由与我们分布不同的生成过程产生的值)非常敏感。在前面的例子中,我们只处理了五个数据值;然而,9 远大于其他数字,并把均值拉高了,几乎比除了 9 以外的所有值都要高。在我们怀疑数据中存在异常值时,可能更倾向于使用中位数作为我们的集中趋势度量。

中位数

与均值不同,中位数对异常值具有较强的鲁棒性。以美国的收入为例;最高的 1%收入远高于其他人群,这会使均值偏高,从而扭曲对平均收入的认知。然而,中位数能更好地代表平均收入,因为它是我们数据的第 50 百分位数;这意味着 50%的数据值大于中位数,50%的数据值小于中位数。

提示

i百分位数是指数据中有i%的观察值小于该值,因此 99 百分位数是X中的值,表示 99%的x小于它。

中位数是通过从一个有序数列中取中间值来计算的;如果数据的个数是偶数,则取中间两个值的平均值。如果我们再次使用数字 0、1、1、2 和 9,那么我们的中位数是 1。请注意,这个数据集的均值和中位数是不同的;然而,取决于数据的分布,它们可能是相同的。

众数

众数是数据中最常见的值(如果我们再次使用数字 0、1、1、2 和 9,那么 1 就是众数)。在实践中,我们常常会听到类似“分布是双峰的或多峰的”(与单峰分布相对)的说法,这表示数据的分布有两个或更多的最常见值。这并不一定意味着它们每个出现的次数相同,而是它们比其他值的出现次数显著多。如下面的图所示,单峰分布只有一个众数(位于0),双峰分布有两个众数(分别位于**-23**),而多峰分布有多个众数(分别位于**-2**、0.43):

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_1.3_B16834.jpg

图 1.3 – 可视化连续数据的众数

理解众数的概念在描述连续分布时非常有用;然而,在大多数情况下,当我们描述连续数据时,我们会使用均值或中位数作为中心趋势的度量。而在处理分类数据时,我们通常会使用众数。

分散度的度量

知道分布的中心在哪里,只是帮助我们部分总结数据分布——我们还需要知道数据如何围绕中心分布,以及它们之间的距离有多远。分散度的度量告诉我们数据的分布情况;这将指示我们的分布是狭窄(低分散)还是宽广(分布很广)。与中心趋势的度量一样,我们有多种方式来描述分布的分散度,选择哪种方式取决于情况和数据。

范围

范围是最小值(最小值)和最大值(最大值)之间的距离。范围的单位与数据的单位相同。因此,除非两个数据分布的单位相同且测量的是相同的事物,否则我们不能比较它们的范围并说一个比另一个更分散:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/B16834_01_003.jpg

从范围的定义中,我们可以看出,为什么它并不总是衡量数据分散度的最佳方式。它给出了数据的上下界限;然而,如果数据中存在异常值,范围就会变得没有意义。

另一个关于范围的问题是,它没有告诉我们数据如何在其中心周围分散;它实际上只是告诉我们整个数据集的分散程度。这就引出了方差的问题。

方差

方差描述了观察值与其平均值(均值)之间的分散程度。总体方差表示为σ²(读作西格玛平方),样本方差表示为s²。它是通过计算离均值的平均平方距离来得出的。注意,必须对这些距离进行平方,这样均值以下的距离就不会与均值以上的距离相互抵消。

如果我们希望样本方差成为总体方差的无偏估计量,我们需要除以n - 1而不是n,以弥补使用样本均值而非总体均值的偏差;这就是贝塞尔修正(en.wikipedia.org/wiki/Bessel%27s_correction)。大多数统计工具默认会给出样本方差,因为获取整个总体的数据是非常罕见的:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/B16834_01_004.jpg

方差给我们提供了一个带有平方单位的统计量。这意味着,如果我们从以美元( )表示的收入数据开始,那么我们的方差将是以美元平方( )表示的收入数据开始,那么我们的方差将是以美元平方( )表示的收入数据开始,那么我们的方差将是以美元平方(²)为单位的。当我们试图了解数据的分布时,这并不十分有用;我们可以使用幅度(大小)本身来查看某个事物的分布情况(大值 = 大范围),但除此之外,我们需要一个单位与数据相同的分布度量。为此,我们使用标准差。

标准差

我们可以使用标准差来查看数据点离均值有多远,平均而言。小的标准差意味着值接近均值,而大的标准差意味着值分散得更广。这与我们想象的分布曲线有关:标准差越小,曲线的峰值越窄(0.5);标准差越大,曲线的峰值越宽(2):

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_1.4_B16834.jpg

图 1.4 – 使用标准差来量化分布的扩散

标准差仅仅是方差的平方根。通过执行这个操作,我们得到的统计量使用的单位可以让我们再次理解(以收入为例,使用$作为单位):

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/B16834_01_005.jpg

请注意,人口标准差用σ表示,样本标准差用s表示。

变异系数

当我们从方差转到标准差时,我们的目的是得到一个更具意义的单位;然而,如果我们想将一个数据集的分散度与另一个数据集进行比较,我们需要再次使用相同的单位。解决这一问题的一种方法是计算变异系数CV),它是无单位的。变异系数是标准差与均值的比值:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/B16834_01_006.jpg

我们将在第七章《金融分析——比特币与股市》中使用这个指标;由于变异系数是无单位的,我们可以用它来比较不同资产的波动性。

四分位距

到目前为止,除了范围,我们讨论了基于均值的分散度量;现在,我们将探讨如何使用中位数作为集中趋势的度量来描述数据的扩散。如前所述,中位数是第 50 百分位数或第 2四分位数(Q2)。百分位数和四分位数都是分位数——将数据划分为包含相同比例数据的相等组的值。百分位数将数据分成 100 个部分,而四分位数将其分成四个部分(25%、50%、75%和 100%)。

由于分位数整齐地划分了数据,并且我们知道每个部分中有多少数据,它们是帮助我们量化数据分布的理想选择。一个常见的度量是四分位距IQR),即第三四分位数和第一四分位数之间的距离:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/B16834_01_007.jpg

IQR 给出了围绕中位数的数据显示的分布并且量化了分布中 50%数据的离散度。当检查数据是否存在异常值时,它也很有用,我们将在第八章中讨论,“基于规则的异常检测”。此外,IQR 可以用来计算一个无单位的离散度度量,我们接下来会讨论。

四分位数离散系数

就像当我们使用均值作为中心趋向度时会有变异系数一样,当我们使用中位数作为中心度量时,也有四分位离散系数。这个统计量也是无单位的,因此可以用来比较不同的数据集。它通过将半四分位距(IQR 的一半)除以中位数(第一四分位数和第三四分位数之间的中点)来计算:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/B16834_01_008.jpg

我们将在第七章,“金融分析——比特币与股市”中再次看到这个度量,当时我们会评估股票的波动性。现在,让我们看看如何使用中心趋向度和离散度度量来总结数据。

总结数据

我们已经看到了许多可以用来通过数据的中心和离散度来总结数据的描述性统计量;实际上,在深入一些其他前述的度量指标之前,查看5 数概括并可视化分布被证明是有帮助的第一步。正如其名称所示,5 数概括提供了五个描述性统计量来总结我们的数据:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_1.5_B16834.jpg

图 1.5 – 5 数概括

箱型图(或称箱线图)是 5 数概括的可视化表示。中位数用盒子中的粗线表示。盒子的顶部是 Q3,底部是 Q1。盒子两侧的线(胡须)延伸到最小值和最大值。根据我们绘图工具使用的约定,尽管如此,它们可能只延伸到某个统计量;任何超出这些统计量的值都被标记为异常值(使用点表示)。对于本书而言,胡须的下界为Q1 – 1.5 * IQR,上界为Q3 + 1.5 * IQR,这被称为Tukey 箱型图

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_1.6_B16834.jpg

图 1.6 – Tukey 箱型图

虽然箱型图是了解数据分布的一个很好的工具,但它不能显示每个四分位数内部的分布情况。为了这个目的,我们使用直方图来处理离散变量(例如:人数或书籍数量),使用核密度估计KDEs)来处理连续变量(例如:身高或时间)。虽然我们可以在离散变量上使用 KDE,但这容易让人产生混淆。直方图适用于离散和连续变量;然而,在这两种情况下,我们必须记住,选择的分箱数量会轻易改变我们看到的分布形状。

制作直方图时,会创建若干个等宽的分箱,并为每个分箱的值添加相应高度的条形。以下图为一个具有 10 个分箱的直方图,展示了与图 1.6中生成箱型图的数据相同的三个中心趋势度量:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_1.7_B16834.jpg

图 1.7 – 直方图示例

重要提示

实际操作中,我们需要调整分箱数量以找到最佳值。然而,我们必须小心,因为这可能会误导分布的形状。

KDE 类似于直方图,不同之处在于,KDE 不是为数据创建分箱,而是绘制一个平滑的曲线,它是分布概率密度函数PDF)的估计。PDF 适用于连续变量,并告诉我们概率在各个值之间的分布。PDF 的值越高,表示对应值的概率越大:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_1.8_B16834.jpg

图 1.8 – 带有标记中心位置的 KDE

当分布开始偏斜,且一侧尾部较长时,均值中心度量容易被拉向那一侧。非对称的分布会表现出一定的偏度左偏(负偏)分布具有左侧长尾;右偏(正偏)分布具有右侧长尾。在负偏的情况下,均值会小于中位数,而在正偏的情况下则相反。当没有偏度时,均值和中位数相等:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_1.9_B16834.jpg

图 1.9 – 可视化偏度

重要提示

另一个统计量是峰度,它比较分布中心的密度与尾部的密度。偏度和峰度都可以通过 SciPy 包进行计算。

我们数据中的每一列都是一个随机变量,因为每次观察时,我们会根据潜在的分布获得一个值——它不是静态的。当我们对获得X或更小值的概率感兴趣时,我们使用累积分布函数CDF),它是概率密度函数(PDF)的积分(曲线下的面积):

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Formula_01_009.jpghttps://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Formula_01_010.jpg

随机变量X小于或等于特定值x的概率记作P(X ≤ x)。对于连续变量,获得精确值x的概率是 0。这是因为该概率将是从xx的 PDF 积分(曲线下宽度为零的面积),即为 0:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Formula_01_011.jpg

为了实现可视化,我们可以从样本中估计累积分布函数(CDF),称为经验累积分布函数ECDF)。由于这是累积的,在X轴上的值等于x时,Y值表示的是累积概率P(X ≤ x)。让我们以P(X ≤ 50),**P(X = 50)P(X > 50)**为例进行可视化:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_1.10_B16834.jpg

图 1.10 – 可视化累积分布函数

除了检查数据的分布外,我们可能还需要使用概率分布进行模拟等用途(如在第八章中讨论的基于规则的异常检测)或假设检验(见推断统计*部分);让我们来看一些我们可能会遇到的分布。

常见分布

尽管有许多概率分布,每个分布都有特定的应用场景,但有一些我们会经常遇到。高斯分布正态分布呈钟形曲线,参数化由其均值(μ)和标准差(σ)。标准正态分布Z)的均值为 0,标准差为 1。许多自然现象遵循正态分布,如身高。请注意,测试一个分布是否符合正态分布并非易事——有关更多信息,请参见进一步阅读部分。

泊松分布是一种离散分布,通常用来模拟到达事件。到达之间的时间可以通过指数分布来建模。两者都由它们的均值λ(λ)定义。均匀分布在其区间内对每个值赋予相同的概率。我们经常使用它来生成随机数。当我们生成一个随机数以模拟一次成功/失败的结果时,这叫做伯努利试验。它通过成功概率(p)进行参数化。当我们多次进行同样的实验(n)时,总成功次数便是一个二项式随机变量。伯努利分布和二项式分布都是离散的。

我们可以可视化离散和连续分布;然而,离散分布给我们提供了一个概率质量函数PMF)而不是概率密度函数(PDF):

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_1.11_B16834.jpg

图 1.11 – 可视化一些常用的分布

我们将在第八章基于规则的异常检测中使用这些分布,当我们模拟一些登录尝试数据以进行异常检测时。

缩放数据

为了比较来自不同分布的变量,我们必须对数据进行缩放,我们可以通过使用最小-最大缩放来实现。我们取每个数据点,减去数据集的最小值,然后除以范围。这将标准化我们的数据(将其缩放到[0, 1]的范围内):

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Formula_01_012.jpg

这不是缩放数据的唯一方式;我们还可以使用均值和标准差。在这种情况下,我们将从每个观察值中减去均值,然后除以标准差来标准化数据。这给出了我们所知道的Z-分数

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Formula_01_013.jpg

我们得到了一个均值为 0 且标准差(和方差)为 1 的标准化分布。Z-分数告诉我们每个观察结果与均值相差了多少个标准差;均值的 Z-分数为 0,而一个比均值低 0.5 个标准差的观察结果的 Z-分数为-0.5。

当然,还有其他方式来缩放我们的数据,我们最终选择的方式将取决于我们的数据及其用途。通过牢记中心趋势和离散度的测量,您将能够确定如何在遇到的任何其他方法中进行数据缩放。

量化变量之间的关系

在前面的章节中,我们处理的是单变量统计,并且只能对我们关注的变量做出某些描述。通过多变量统计,我们试图量化变量之间的关系,并尝试预测未来的行为。

协方差是一种用于量化变量之间关系的统计量,它显示一个变量随着另一个变量的变化(也称为它们的联合方差):

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Formula_01_014.jpg

重要提示

E[X] 对于我们来说是一个新的符号。它被读作X 的期望值X 的期望,通过将X的所有可能值乘以它们的概率相加来计算——它是X的长期平均值。

协方差的大小不容易解释,但它的符号告诉我们变量是正相关还是负相关。然而,我们也希望量化变量之间关系的强度,这就引出了相关性。相关性告诉我们变量如何在方向(相同或相反)和大小(关系的强度)上共同变化。为了找到相关性,我们通过将协方差除以变量标准差的乘积来计算皮尔逊相关系数,其符号为ρ(希腊字母rho):

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Formula_01_015.jpg

这使得协方差标准化,并产生一个介于-1 和 1 之间的统计量,便于描述相关性的方向(符号)和强度(大小)。相关系数为 1 被称为完美正相关(线性相关),-1 则为完美负相关。接近 0 的值表示没有相关性。如果相关系数接近 1 的绝对值,那么变量被认为是强相关的;而接近 0.5 的相关系数则表示变量间的相关性较弱。

让我们通过散点图来看一些例子。在图 1.12的最左边的子图(ρ = 0.11)中,我们看到变量之间没有相关性:它们看起来像是没有模式的随机噪声。下一个图(ρ = -0.52)有弱的负相关性:我们可以看到,随着x变量的增加,y变量下降,尽管仍有一些随机性。第三个图(ρ = 0.87)有强的正相关性:xy一起增加。最右边的图(ρ = -0.99)有接近完美的负相关性:随着x的增加,y减少。我们还可以看到点如何形成一条直线:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_1.12_B16834.jpg

图 1.12 – 比较相关系数

为了快速估计两个变量之间关系的强度和方向(并判断是否存在关系),我们通常会使用散点图,而不是计算精确的相关系数。这是因为以下几个原因:

  • 在可视化中寻找模式更容易,但通过查看数字和表格得出相同的结论则需要更多的工作。

  • 我们可能会看到变量之间似乎有关联,但它们可能不是线性相关的。查看视觉表现可以很容易地判断我们的数据是否实际上是二次的、指数的、对数的或其他非线性函数。

以下两个图都显示了强正相关的数据,但通过观察散点图,显然这些数据并不是线性的。左边的是对数型的,而右边的是指数型的:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_1.13_B16834.jpg

图 1.13 – 相关系数可能会误导人

很重要的一点是,尽管我们可能发现XY之间存在相关性,但这并不意味着X 导致 Y,或者Y 导致 X。可能存在某个Z实际上同时引起了这两者;也许X导致某个中介事件,从而导致Y,或者这其实只是巧合。请记住,我们往往没有足够的信息来报告因果关系——相关性并不意味着因果关系

提示

一定要查看 Tyler Vigen 的虚假相关性博客(www.tylervigen.com/spurious-correlations),里面有一些有趣的相关性。

总结统计量的陷阱

有一个非常有趣的数据集,展示了当我们仅使用总结统计量和相关系数来描述数据时,我们必须多么小心。它还向我们展示了绘图并非可选项。安斯科姆四重奏是一个包含四个不同数据集的集合,它们具有相同的总结统计量和相关系数,但当被绘制出来时,很明显它们并不相似:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_1.14_B16834.jpg

图 1.14 – 总结统计量可能会误导人

请注意,图 1.14 中的每个图都具有相同的最佳拟合线,其方程为y = 0.50x + 3.00。在下一节中,我们将高层次地讨论这个直线是如何创建的,以及它代表了什么意义。

重要提示

总结统计量在我们了解数据时非常有帮助,但要小心仅仅依赖它们。记住,统计数据可能会误导人;在得出任何结论或继续分析之前,务必先绘制数据图表。你可以在en.wikipedia.org/wiki/Anscombe%27s_quartet了解更多关于安斯科姆四重奏的内容。此外,也要查看Datasaurus Dozen,这是 13 个数据集,它们具有相同的总结统计量,访问地址为www.autodeskresearch.com/publications/samestats

预测与预报

假设我们最喜欢的冰淇淋店请求我们帮助预测他们在某一天能卖出多少冰淇淋。他们相信外面的温度对他们的销售有很大的影响,因此他们收集了在不同温度下售出冰淇淋的数量。我们同意帮助他们,第一步就是绘制他们收集的数据的散点图:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_1.15_B16834.jpg

图 1.15 – 不同温度下冰淇淋销售的观察结果

我们可以在散点图中观察到一个上升趋势:在较高温度下,卖出的冰激凌更多。然而,为了帮助冰激凌店,我们需要找到一种方法来从这些数据中做出预测。我们可以使用一种叫做回归分析的技术,通过一个方程来描述温度和冰激凌销售量之间的关系。通过这个方程,我们将能够预测在某一温度下的冰激凌销售量。

重要说明

请记住,相关性并不意味着因果关系。人们可能会在气温升高时购买冰激凌,但气温升高并不一定导致人们购买冰激凌。

第九章《Python 中的机器学习入门》中,我们将深入讨论回归分析,因此本讨论将仅为概述。回归有许多种类型,会产生不同的方程,例如线性回归(我们将在这个例子中使用)和逻辑回归。我们的第一步是确定因变量,即我们想要预测的量(冰激凌销售量),以及我们将用来预测它的变量,这些被称为自变量。虽然我们可以有许多自变量,但我们的冰激凌销售例子只有一个自变量:温度。因此,我们将使用简单线性回归将温度和销售量之间的关系建模为一条直线:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_1.16_B16834.jpg

图 1.16 – 拟合冰激凌销售数据的直线

上一个散点图中的回归线得出了以下关系方程:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Formula_01_016.jpg

假设今天温度是 35°C——我们将把这个值代入方程中的温度。结果预测冰激凌店将销售 24.54 个冰激凌。这个预测值位于前图中的红线旁边。注意,冰激凌店实际上不能卖部分冰激凌。

在将模型交给冰激凌店之前,重要的是要讨论我们得到的回归线中的虚线和实线部分之间的区别。当我们使用回归线的实线部分进行预测时,我们正在使用插值,这意味着我们将预测回归所建立时的温度下的冰激凌销售量。另一方面,如果我们试图预测在 45°C 时会卖出多少个冰激凌,这就是外推(虚线部分),因为在我们进行回归时并没有包括这么高的温度。外推可能非常危险,因为许多趋势并不会无限延续。人们可能会决定由于温度过高而不外出,这意味着他们将不会销售预测的 39.54 个冰激凌,而是会销售零个。

在处理时间序列时,我们的术语略有不同:我们通常会根据过去的值来预测未来的值。预测是时间序列的一种预测类型。然而,在尝试对时间序列建模之前,我们通常会使用一个叫做时间序列分解的过程,将时间序列分解成多个组成部分,这些组成部分可以以加法或乘法的方式组合,并可作为模型的组成部分。

趋势组件描述了时间序列在长期内的行为,而不考虑季节性或周期性效应。通过趋势,我们可以对时间序列的长期变化做出广泛的判断,比如地球人口在增加某只股票的价值停滞不前季节性组件解释了时间序列中与季节相关的系统性变化。例如,纽约市街头的冰淇淋车在夏季数量较多,冬季则几乎消失;这种模式每年都会重复,不管每年夏天的实际数量是否相同。最后,周期性组件解释了时间序列中其他无法用季节性或趋势解释的异常或不规则波动;例如,飓风可能会导致冰淇淋车数量在短期内减少,因为在户外不安全。由于周期性成分的不可预测性,这一部分很难通过预测来预见。

我们可以使用 Python 来分解时间序列为趋势、季节性和噪声残差。周期性成分被包含在噪声中(随机且不可预测的数据);在我们去除时间序列中的趋势和季节性后,剩下的就是残差:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_1.17_B16834.jpg

图 1.17 – 时间序列分解的示例

在构建时间序列预测模型时,一些常见的方法包括指数平滑法和 ARIMA 模型。ARIMA代表自回归AR)、差分I)、移动平均MA)。自回归模型利用了一个观察值在时间t时与先前某个观察值之间的相关性,例如时间t - 1时的观察值。在第五章,《使用 Pandas 和 Matplotlib 可视化数据》中,我们将探讨一些技术来判断一个时间序列是否具有自回归性;需要注意的是,并不是所有的时间序列都有自回归性。差分部分涉及差分数据,即数据从一个时间点到另一个时间点的变化。例如,如果我们关心的是滞后(时间间隔)为 1 的情况,那么差分数据就是时间t的值减去时间t - 1的值。最后,移动平均部分使用滑动窗口计算最后x个观察值的平均值,其中x是滑动窗口的长度。例如,如果我们有一个 3 期的移动平均,那么当我们获得所有的数据直到时间 5 时,我们的移动平均计算只会使用时间 3、4 和 5 来预测时间 6 的值。在第七章,《金融分析——比特币与股市》中,我们将构建一个 ARIMA 模型。

移动平均给过去的每一个时间段赋予相等的权重。在实际操作中,这并不总是对我们的数据一个现实的期望。有时候,所有过去的数值都很重要,但它们对未来数据点的影响不同。对于这些情况,我们可以使用指数平滑法,它使我们能够对最近的数值赋予更多权重,对更远的数值赋予较少的权重,从而预测未来的数据。

请注意,我们不仅限于预测数字;事实上,根据数据的不同,我们的预测也可以是类别性的——例如预测某种口味的冰淇淋在某一天销售量最多,或者判断一封邮件是否为垃圾邮件。这类预测将在第九章,《Python 机器学习入门》中介绍。

推断统计学

如前所述,推断统计学处理的是从我们拥有的样本数据中推断或推导出关于总体的结论。在我们得出结论时,必须注意我们是进行的观察性研究还是实验。对于观察性研究,独立变量不受研究者控制,因此我们是观察参与研究的人(比如吸烟研究——我们不能强迫人们吸烟)。我们不能控制独立变量意味着我们不能得出因果关系的结论。

通过实验,我们能够直接影响自变量,并将受试者随机分配到对照组和实验组,例如 A/B 测试(适用于网站重设计到广告文案等各种场景)。请注意,对照组不接受治疗;他们可能会接受安慰剂(具体取决于研究的内容)。这种设置的理想方式是双盲,即负责施治的研究人员不知道哪个治疗是安慰剂,也不知道哪个受试者属于哪个组别。

重要提示

我们经常会看到贝叶斯推断和频率推断的相关内容。这两者基于两种不同的概率处理方式。频率学派统计学侧重于事件发生的频率,而贝叶斯统计学则在确定事件概率时使用信念的程度。在第十一章机器学习中的异常检测中,我们会看到贝叶斯统计学的一个例子。你可以在www.probabilisticworld.com/frequentist-bayesian-approaches-inferential-statistics/了解更多关于这两种方法的差异。

推论统计学为我们提供了将样本数据的理解转化为对总体的推断的工具。记住,我们之前讨论的样本统计量是总体参数的估计量。我们的估计量需要置信区间,它提供一个点估计以及围绕点估计的误差范围。这是一个范围,表示真实的总体参数在某个置信水平下的可能取值范围。在 95%的置信水平下,95%从总体中随机抽取的样本计算出的置信区间包含真实的总体参数。通常,统计学中会选择 95%作为置信水平,虽然 90%和 99%也很常见;置信水平越高,区间越宽。

假设检验允许我们测试真实总体参数是否小于、大于或不等于某个值在某个显著性水平(称为α)下。执行假设检验的过程始于陈述我们的初始假设或零假设:例如,真实总体均值为 0。我们选择一个统计显著性水平,通常为 5%,这是在零假设为真时拒绝零假设的概率。然后,我们计算测试统计量的临界值,这将取决于我们拥有的数据量以及我们正在测试的统计量类型(例如一个总体的平均值或候选人得票比例)。将临界值与来自我们数据的测试统计量进行比较,并根据结果,我们要么拒绝要么不拒绝零假设。假设检验与置信区间密切相关。显著性水平相当于 1 减去置信水平。这意味着如果零假设值不在置信区间内,则结果在统计上是显著的。

重要提示

在选择计算置信区间的方法或假设检验的适当检验统计量时,我们必须注意许多事项。这超出了本书的范围,请参阅本章末尾的Further reading部分的链接获取更多信息。此外,请务必查看一些假设检验中使用的 p 值的失误,例如 p-hacking,详见en.wikipedia.org/wiki/Misuse_of_p-values

现在我们已经概述了统计学和数据分析,准备开始本书的 Python 部分。让我们从设置虚拟环境开始。

设置虚拟环境

本书使用 Python 3.7.3 编写,但代码应适用于所有主要操作系统上的 Python 3.7.1+。在本节中,我们将讲解如何设置虚拟环境,以便跟随本书的内容。如果您的计算机尚未安装 Python,请首先阅读有关虚拟环境的以下部分,然后决定是否安装 Anaconda,因为它也会安装 Python。要安装不带 Anaconda 的 Python,请从www.python.org/downloads/下载,并按照venv部分而不是conda部分操作。

重要提示

要检查 Python 是否已安装,请在 Windows 命令行上运行where python3或在 Linux/macOS 上运行which python3。如果返回结果为空,请尝试仅使用python(而不是python3)运行。如果已安装 Python,请通过运行python3 --version来检查版本。请注意,如果python3可用,则应在整本书中使用它(反之亦然,如果python3不可用,则使用python)。

虚拟环境

大多数情况下,当我们想在电脑上安装软件时,我们只需下载它,但编程语言的特性要求包不断更新并依赖于其他特定版本,这可能会导致一些问题。比如,我们有一天在做一个项目时需要一个特定版本的 Python 包(比如 0.9.1),但第二天我们在做另一个分析时需要同一个包的最新版本(比如 1.1.0),以便访问一些更新的功能。听起来好像不会有什么问题,对吧?但是,如果这个更新导致了第一个项目或我们项目中依赖该包的其他包出现了兼容性问题怎么办呢?这是一个足够常见的问题,已经有了解决方案来防止这种情况:虚拟环境。

虚拟环境允许我们为每个项目创建独立的环境。每个环境只会安装它所需的包。这样可以方便地与他人共享我们的环境,安装多个版本的相同包用于不同的项目而不相互干扰,并避免安装更新包或有其他依赖关系的包时带来的意外副作用。为我们工作的任何项目创建一个专用的虚拟环境是一个好习惯。

我们将讨论两种常见的设置方式,你可以决定哪种最适合。注意,本节中的所有代码将在命令行中执行。

venv

Python 3 自带venv模块,它将根据我们选择的路径创建一个虚拟环境。设置和使用开发环境的过程如下(安装了 Python 之后):

  1. 为项目创建一个文件夹。

  2. 使用venv在此文件夹中创建环境。

  3. 激活环境。

  4. 使用pip在环境中安装 Python 包。

  5. 完成后停用环境。

实际上,我们将为每个项目创建独立的环境,因此我们的第一步是为所有项目文件创建一个目录。我们可以使用mkdir命令来完成这项工作。创建完成后,我们将使用cd命令切换到新创建的目录。由于我们已经获得了项目文件(从章节材料部分获得的指示),以下内容仅供参考。要创建一个新目录并进入该目录,我们可以使用以下命令:

$ mkdir my_project && cd my_project

提示

cd <path>将当前目录更改为<path>指定的路径,路径可以是绝对路径(完整路径)或相对路径(从当前目录到目标目录的路径)。

在继续之前,使用cd命令导航到包含本书仓库的目录。注意,路径将取决于它被克隆/下载的位置:

$ cd path/to/Hands-On-Data-Analysis-with-Pandas-2nd-edition

由于操作系统之间在剩余步骤上有所不同,我们将分别讲解 Windows 和 Linux/macOS。请注意,如果你的系统同时安装了 Python 2 和 Python 3,确保在以下命令中使用python3,而不是python

Windows

为了创建本书的虚拟环境,我们将使用标准库中的venv模块。请注意,我们必须为环境提供一个名称(book_env)。记住,如果你的 Windows 设置将python与 Python 3 相关联,那么在以下命令中使用python而不是python3

C:\...> python3 -m venv book_env

现在,我们在之前克隆/下载的仓库文件夹中,有一个名为book_env的虚拟环境文件夹。为了使用该环境,我们需要激活它:

C:\...> %cd%\book_env\Scripts\activate.bat

提示

Windows 用当前目录的路径替换%cd%,这使我们不必输入完整的路径直到book_env部分。

请注意,在我们激活虚拟环境后,可以在命令行提示符前看到(book_env),这表明我们已经进入该环境:

(book_env) C:\...> 

当我们使用完环境后,只需将其停用:

(book_env) C:\...> deactivate

在环境中安装的任何软件包在环境外部是不存在的。请注意,我们在命令行提示符前不再看到(book_env)。你可以在 Python 文档中阅读更多关于venv的信息:docs.python.org/3/library/venv.html

虚拟环境创建完成后,激活它,然后转到安装所需的 Python 包部分进行下一步操作。

Linux/macOS

为了创建本书的虚拟环境,我们将使用标准库中的venv模块。请注意,我们必须为环境提供一个名称(book_env):

$ python3 -m venv book_env

现在,我们在之前克隆/下载的仓库文件夹中,有一个名为book_env的虚拟环境文件夹。为了使用该环境,我们需要激活它:

$ source book_env/bin/activate

请注意,在我们激活虚拟环境后,可以在命令行提示符前看到(book_env),这表明我们已经进入该环境:

(book_env) $

当我们使用完环境后,只需将其停用:

(book_env) $ deactivate

在环境中安装的任何软件包在环境外部是不存在的。请注意,我们在命令行提示符前不再看到(book_env)。你可以在 Python 文档中阅读更多关于venv的信息:docs.python.org/3/library/venv.html

虚拟环境创建完成后,激活它,然后转到安装所需的 Python 包部分进行下一步操作。

conda

Anaconda 提供了一种专门为数据科学设置 Python 环境的方法。它包含了本书中将使用的一些包,以及一些可能对本书未涉及的任务有用的其他包(同时也解决了可能难以安装的 Python 外部依赖问题)。Anaconda 使用conda作为环境和包管理器,而不是pip,尽管仍然可以使用pip安装包(前提是使用 Anaconda 自带的pip)。需要注意的是,有些包可能无法通过conda获得,在这种情况下,我们需要使用pip。可以查阅conda文档中的这个页面,比较condapipvenv的命令:conda.io/projects/conda/en/latest/commands.html#conda-vs-pip-vs-virtualenv-commands

重要提示

请注意,Anaconda 的安装非常大(尽管 Miniconda 版本要轻得多)。那些用于数据科学以外目的的 Python 用户,可能更倾向于使用我们之前讨论的venv方法,以便更好地控制安装内容。

Anaconda 还可以与 Spyder 的venv选项一起打包使用。

你可以在 Anaconda 的官方文档中阅读更多关于 Anaconda 及其安装的内容:

一旦安装了 Anaconda 或 Miniconda,确认是否正确安装,可以通过在命令行运行conda -V来显示版本。请注意,在 Windows 上,所有conda命令必须在Anaconda Prompt中运行(而不是Command Prompt)。

为本书创建一个新的conda环境,命名为book_env,可以运行以下命令:

(base) $ conda create --name book_env

运行conda env list将显示系统上的所有conda环境,其中现在包括book_env。当前活动的环境将有一个星号(*)标记—默认情况下,base环境将处于活动状态,直到我们激活另一个环境:

(base) $ conda env list
# conda environments:
#
base                  *  /miniconda3
book_env                 /miniconda3/envs/book_env

要激活book_env环境,我们运行以下命令:

(base) $ conda activate book_env

请注意,在我们激活虚拟环境后,可以在命令行提示符前看到(book_env);这表明我们已经进入该环境:

(book_env) $

使用完环境后,我们可以停用它:

(book_env) $ conda deactivate

在环境中安装的任何包都只存在于该环境中。请注意,我们的命令行提示符前不再有(book_env)。你可以在www.freecodecamp.org/news/why-you-need-python-environments-and-how-to-manage-them-with-conda-85f155f4353c/阅读更多关于如何使用conda管理虚拟环境的内容。

在下一节中,我们将安装跟随本书所需的 Python 包,因此请现在确保激活虚拟环境。

安装所需的 Python 包

我们可以利用 Python 标准库做很多事情;然而,我们经常会发现需要安装并使用外部包来扩展功能。仓库中的requirements.txt文件包含了我们需要安装的所有包,以便跟随本书进行学习。该文件会位于当前目录中,也可以通过github.com/stefmolin/Hands-On-Data-Analysis-with-Pandas-2nd-edition/blob/master/requirements.txt找到。我们可以通过在调用pip3 install时使用-r标志,一次性安装多个包,这种方式的优势是方便共享。

在安装任何东西之前,请确保激活你用venvconda创建的虚拟环境。请注意,如果在运行以下命令之前没有激活虚拟环境,包将会安装到虚拟环境外部:

(book_env) $ pip3 install -r requirements.txt

提示

如果你遇到任何问题,可以在github.com/stefmolin/Hands-On-Data-Analysis-with-Pandas-2nd-edition/issues上报告。

为什么选择 pandas?

在 Python 数据科学领域,pandas库几乎是无处不在的。它建立在 NumPy 库之上,允许我们对单一类型数据的数组进行高效的数学运算。Pandas 将这一点扩展到了数据框(dataframes),可以将其视为数据表。我们将在第二章,《使用 Pandas 数据框》一章中正式介绍数据框。

除了高效的运算外,pandas还提供了matplotlib绘图库,使得我们无需编写大量matplotlib代码就能轻松创建各种图表。我们始终可以通过matplotlib调整图表,但对于快速可视化数据,我们只需要在pandas中写一行代码即可。我们将在第五章,《使用 Pandas 和 Matplotlib 可视化数据》一章,以及第六章,《使用 Seaborn 绘图和定制技巧》一章中进一步探索这一功能。

重要说明

封装函数是围绕另一个库的代码而编写的,它们隐藏了一些复杂性,并为重复该功能留下了更简单的接口。这是面向对象编程(OOP)的核心原则之一,称为抽象,它减少了代码的复杂性和重复。本书中我们将创建自己的封装函数。

除了pandas,这本书还使用了 Jupyter Notebooks。虽然你可以选择不使用它们,但熟悉 Jupyter Notebooks 非常重要,因为它们在数据领域非常常见。作为介绍,我们将在下一节使用 Jupyter Notebook 验证我们的设置。

Jupyter Notebooks

本书的每一章都包含用于跟随的 Jupyter Notebooks。Jupyter Notebooks 在 Python 数据科学中无处不在,因为它们使得在探索环境中编写和测试代码变得非常简单。我们可以逐块执行代码,并将生成的结果直接打印到笔记本中相应的代码下方。此外,我们可以使用Markdown为我们的工作添加文本说明。Jupyter Notebooks 可以轻松打包和共享;它们可以推送到 GitHub(在那里将被渲染),转换为 HTML 或 PDF,发送给其他人,或进行演示。

启动 JupyterLab

JupyterLab 是一个 IDE,允许我们创建 Jupyter Notebooks 和 Python 脚本,与终端交互,创建文本文档,引用文档等等,所有这些功能都可以在我们本地机器的清晰 Web 界面上完成。在真正成为高级用户之前,有很多键盘快捷键需要掌握,但界面非常直观。在创建环境时,我们已经安装了运行 JupyterLab 所需的一切,因此让我们快速浏览 IDE,确保我们的环境设置正确。首先,激活我们的环境,然后启动 JupyterLab:

(book_env) $ jupyter lab

然后会在默认浏览器中启动一个窗口,显示 JupyterLab。我们将看到启动器选项卡和左侧的文件浏览器面板:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_1.18_B16834.jpg

图 1.18 – 启动 JupyterLab

使用文件浏览器面板,在ch_01文件夹中双击,其中包含我们用来验证设置的 Jupyter Notebook。

验证虚拟环境设置

打开checking_your_setup.ipynb笔记本,位于ch_01文件夹中,如下截图所示:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_1.19_B16834.jpg

图 1.19 – 验证虚拟环境设置

重要说明

内核是在 Jupyter Notebook 中运行和检查我们代码的进程。请注意,我们不限于运行 Python 代码 —— 我们也可以运行 R、Julia、Scala 和其他语言的内核。默认情况下,我们将使用 IPython 内核来运行 Python。在本书中,我们将更深入地学习 IPython。

点击前面截图中指示的代码单元格,然后通过点击播放(▶)按钮来运行它。如果所有内容都显示为绿色,则环境已经设置好了。但是,如果情况不是这样,请从虚拟环境中运行以下命令,为 Jupyter 创建一个带有 book_env 虚拟环境的特殊核心:

(book_env) $ ipython kernel install --user --name=book_env

这在 Jupyter Notebook 中的 book_env 核心中添加了一个额外的选项:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_1.20_B16834.jpg

图 1.20 – 选择不同的核心

需要注意的是,当内核运行时,Jupyter Notebooks 将保留我们为变量分配的值,并且在我们保存文件时,Out[#] 单元格中的结果也将被保存。关闭文件并不会停止内核,关闭浏览器中的 JupyterLab 标签页也不会停止它。

关闭 JupyterLab

关闭浏览器中的 JupyterLab 不会停止 JupyterLab 或正在运行的内核(我们也不会重新获得命令行界面)。要完全关闭 JupyterLab,我们需要在终端中按下 Ctrl + C 几次(这是一个键盘中断信号,让 JupyterLab 知道我们要关闭它),直到我们重新获得提示符:

...
[I 17:36:53.166 LabApp] Interrupted...
[I 17:36:53.168 LabApp] Shutting down 1 kernel
[I 17:36:53.770 LabApp] Kernel shutdown: a38e1[...]b44f
(book_env) $

欲了解更多关于 Jupyter 的信息,包括教程,请访问 jupyter.org/。在 jupyterlab.readthedocs.io/en/stable/ 上了解更多关于 JupyterLab 的信息。

总结

在本章中,我们了解了数据分析的主要过程:数据收集、数据整理、探索性数据分析(EDA)和得出结论。接着我们概述了描述性统计,并学习了如何描述数据的中心趋势和分布;如何用五数总结、箱线图、直方图和核密度估计来数值和视觉上总结数据;如何缩放我们的数据;以及如何量化数据集中变量之间的关系。

我们初步介绍了预测和时间序列分析。然后,我们简要概述了推断统计学的一些核心主题,这些主题可以在掌握本书内容后进一步探索。请注意,本章中的所有示例都是关于一个或两个变量的,而现实生活中的数据往往是高维的。第十章做出更好的预测 – 优化模型,将涉及一些解决这个问题的方法。最后,我们为本书建立了虚拟环境,并学习了如何使用 Jupyter Notebooks。

现在我们已经打下了坚实的基础,下一章我们将开始在 Python 中处理数据。

练习

运行 introduction_to_data_analysis.ipynb 笔记本来复习本章内容,再复习 python_101.ipynb 笔记本(如果需要),然后完成以下练习,练习在 JupyterLab 中处理数据和计算汇总统计信息:

  1. 探索 JupyterLab 界面,了解一些可用的快捷键。现在不用担心记住它们(最终,它们会变成第二天性,节省你很多时间)——只要熟悉使用 Jupyter Notebooks。

  2. 所有数据都是正态分布的吗?请解释为什么或为什么不。

  3. 在什么情况下使用中位数而不是均值作为中心度量更有意义?

  4. 运行exercises.ipynb笔记本中第一个单元格的代码。它将给你一个包含 100 个值的列表,你将在本章的其他练习中使用这些值。确保将这些值视为总体的样本。

  5. 使用练习 4中的数据,在不导入任何statistics模块(标准库中的docs.python.org/3/library/statistics.html)的情况下计算以下统计数据,然后确认你的结果与使用statistics模块时得到的结果是否一致(在可能的情况下):

    a) 平均值

    b) 中位数

    c) 众数(提示:查看标准库中collections模块的Counter类,docs.python.org/3/library/collections.html#collections.Counter

    d) 样本方差

    e) 样本标准差

  6. 使用练习 4中的数据,适当使用statistics模块中的函数计算以下统计数据:

    a) 范围

    b) 变异系数

    c) 四分位数间距

    d) 四分位差系数

  7. 使用以下策略对练习 4中创建的数据进行缩放:

    a) 最小-最大缩放(归一化)

    b) 标准化

  8. 使用练习 7中的缩放数据,计算以下内容:

    a) 标准化和归一化数据之间的协方差

    b) 标准化和归一化数据之间的皮尔逊相关系数(这实际上是 1,但由于过程中四舍五入,结果会稍微小一点)

进一步阅读

以下是一些资源,可以帮助你更熟悉 Jupyter:

一些资源用于学习更多高级统计概念(我们这里不会涉及),并且仔细应用这些概念,如下所示:

第二章:第二章:使用 Pandas DataFrame

是时候开始我们的 pandas 之旅了。本章将让我们熟悉在进行数据分析时使用 pandas 执行一些基本但强大的操作。

我们将从介绍主要的 pandas 开始。数据结构为我们提供了一种组织、管理和存储数据的格式。了解 pandas 数据结构在解决问题或查找如何对数据执行某项操作时将无比有帮助。请记住,这些数据结构与标准 Python 数据结构不同,原因是它们是为特定的分析任务而创建的。我们必须记住,某个方法可能只能在特定的数据结构上使用,因此我们需要能够识别最适合我们要解决的问题的数据结构。

接下来,我们将把第一个数据集导入 Python。我们将学习如何从 API 获取数据、从其他 Python 数据结构创建 DataFrame 对象、读取文件并与数据库进行交互。起初,你可能会想,为什么我们需要从其他 Python 数据结构创建 DataFrame 对象;然而,如果我们想要快速测试某些内容、创建自己的数据、从 API 拉取数据,或者重新利用其他项目中的 Python 代码,那么我们会发现这些知识是不可或缺的。最后,我们将掌握检查、描述、过滤和总结数据的方法。

本章将涵盖以下主题:

  • Pandas 数据结构

  • 从文件、API 请求、SQL 查询和其他 Python 对象创建 DataFrame 对象

  • 检查 DataFrame 对象并计算总结统计量

  • 通过选择、切片、索引和过滤获取数据的子集

  • 添加和删除数据

本章内容

本章中我们将使用的文件可以在 GitHub 仓库中找到,地址是 github.com/stefmolin/Hands-On-Data-Analysis-with-Pandas-2nd-edition/tree/master/ch_02。我们将使用来自 data/ 目录的地震数据。

本章中会使用四个 CSV 文件和一个 SQLite 数据库文件,它们将在不同的时间点被使用。earthquakes.csv文件包含从 USGS API 拉取的 2018 年 9 月 18 日到 10 月 13 日的数据。对于数据结构的讨论,我们将使用example_data.csv文件,该文件包含五行数据,并且是earthquakes.csv文件中的列的子集。tsunamis.csv文件是earthquakes.csv文件中所有伴随海啸的地震数据的子集,时间范围为上述日期。quakes.db文件包含一个 SQLite 数据库,其中有一个表存储着海啸数据。我们将利用这个数据库学习如何使用pandas从数据库中读取和写入数据。最后,parsed.csv文件将用于本章结尾的练习,我们也将在本章中演示如何创建它。

本章的伴随代码已被分成六个 Jupyter Notebooks,按照使用顺序编号。它们包含了我们在本章中将运行的代码片段,以及任何需要为本文本进行裁剪的命令的完整输出。每次需要切换笔记本时,文本会指示进行切换。

1-pandas_data_structures.ipynb笔记本中,我们将开始学习主要的pandas数据结构。之后,我们将在2-creating_dataframes.ipynb笔记本中讨论创建DataFrame对象的各种方式。我们将在3-making_dataframes_from_api_requests.ipynb笔记本中继续讨论此话题,探索 USGS API 以收集数据供pandas使用。学习完如何收集数据后,我们将开始学习如何在4-inspecting_dataframes.ipynb笔记本中检查数据。然后,在5-subsetting_data.ipynb笔记本中,我们将讨论各种选择和过滤数据的方式。最后,我们将在6-adding_and_removing_data.ipynb笔记本中学习如何添加和删除数据。让我们开始吧。

Pandas 数据结构

Python 本身已经提供了几种数据结构,如元组、列表和字典。Pandas 提供了两种主要的数据结构来帮助处理数据:SeriesDataFrameSeriesDataFrame数据结构中各自包含了另一种pandas数据结构——Index,我们也需要了解它。然而,为了理解这些数据结构,我们首先需要了解 NumPy(numpy.org/doc/stable/),它提供了pandas所依赖的 n 维数组。

前述的数据结构以 Python CapWords风格实现,而对象则采用snake_case书写。(更多 Python 风格指南请参见www.python.org/dev/peps/pep-0008/。)

我们使用pandas函数将 CSV 文件读取为DataFrame类的对象,但我们使用DataFrame对象的方法对其执行操作,例如删除列或计算汇总统计数据。使用pandas时,我们通常希望访问pandas对象的属性,如维度、列名、数据类型以及是否为空。

重要提示

在本书的其余部分,我们将DataFrame对象称为 dataframe,Series对象称为 series,Index对象称为 index/indices,除非我们明确指的是类本身。

对于本节内容,我们将在1-pandas_data_structures.ipynb笔记本中进行操作。首先,我们将导入numpy并使用它读取example_data.csv文件的内容到一个numpy.array对象中。数据来自美国地质调查局(USGS)的地震 API(来源:earthquake.usgs.gov/fdsnws/event/1/)。请注意,这是我们唯一一次使用 NumPy 读取文件,并且这样做仅仅是为了演示;重要的是要查看 NumPy 表示数据的方式:

>>> import numpy as np
>>> data = np.genfromtxt(
...     'data/example_data.csv', delimiter=';', 
...     names=True, dtype=None, encoding='UTF'
... )
>>> data
array([('2018-10-13 11:10:23.560',
'262km NW of Ozernovskiy, Russia', 
        'mww', 6.7, 'green', 1),
('2018-10-13 04:34:15.580', 
        '25km E of Bitung, Indonesia', 'mww', 5.2, 'green', 0),
('2018-10-13 00:13:46.220', '42km WNW of Sola, Vanuatu', 
        'mww', 5.7, 'green', 0),
('2018-10-12 21:09:49.240', 
        '13km E of Nueva Concepcion, Guatemala',
        'mww', 5.7, 'green', 0),
('2018-10-12 02:52:03.620', 
        '128km SE of Kimbe, Papua New Guinea',
        'mww', 5.6, 'green', 1)],
      dtype=[('time', '<U23'), ('place', '<U37'),
             ('magType', '<U3'), ('mag', '<f8'),
             ('alert', '<U5'), ('tsunami', '<i8')])

现在我们将数据存储在一个 NumPy 数组中。通过使用shapedtype属性,我们可以分别获取数组的维度信息和其中包含的数据类型:

>>> data.shape
(5,)
>>> data.dtype
dtype([('time', '<U23'), ('place', '<U37'), ('magType', '<U3'), 
       ('mag', '<f8'), ('alert', '<U5'), ('tsunami', '<i8')])

数组中的每个条目都是 CSV 文件中的一行。NumPy 数组包含单一的数据类型(不同于允许混合类型的列表);这使得快速的矢量化操作成为可能。当我们读取数据时,我们得到了一个numpy.void对象的数组,它用于存储灵活的类型。这是因为 NumPy 必须为每一行存储多种不同的数据类型:四个字符串,一个浮点数和一个整数。不幸的是,这意味着我们不能利用 NumPy 为单一数据类型对象提供的性能提升。

假设我们想找出最大幅度——我们可以使用numpy.void对象。这会创建一个列表,意味着我们可以使用max()函数来找出最大值。我们还可以使用%%timeit %)来查看这个实现所花费的时间(时间会有所不同):

>>> %%timeit
>>> max([row[3] for row in data])
9.74 µs ± 177 ns per loop 
(mean ± std. dev. of 7 runs, 100000 loops each)

请注意,每当我们编写一个只有一行内容的for循环,或者想要对初始列表的成员执行某个操作时,应该使用列表推导式。这是一个相对简单的列表推导式,但我们可以通过添加if...else语句使其更加复杂。列表推导式是我们工具箱中一个非常强大的工具。更多信息可以参考 Python 文档:https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions。

提示

IPython (ipython.readthedocs.io/en/stable/index.html) 提供了一个 Python 的交互式 Shell。Jupyter 笔记本是建立在 IPython 之上的。虽然本书不要求掌握 IPython,但熟悉一些 IPython 的功能会有所帮助。IPython 在其文档中提供了一个教程,链接是 ipython.readthedocs.io/en/stable/interactive/

如果我们为每一列创建一个 NumPy 数组,那么这项操作将变得更加简单(且更高效)。为了实现这一点,我们将使用字典推导式 (www.python.org/dev/peps/pep-0274/) 来创建一个字典,其中键是列名,值是包含数据的 NumPy 数组。同样,重要的部分在于数据现在是如何使用 NumPy 表示的:

>>> array_dict = {
...     col: np.array([row[i] for row in data])
...     for i, col in enumerate(data.dtype.names)
... }
>>> array_dict
{'time': array(['2018-10-13 11:10:23.560',
        '2018-10-13 04:34:15.580', '2018-10-13 00:13:46.220',
        '2018-10-12 21:09:49.240', '2018-10-12 02:52:03.620'],
        dtype='<U23'),
 'place': array(['262km NW of Ozernovskiy, Russia', 
        '25km E of Bitung, Indonesia',
        '42km WNW of Sola, Vanuatu',
        '13km E of Nueva Concepcion, Guatemala',
        '128km SE of Kimbe, Papua New Guinea'], dtype='<U37'),
 'magType': array(['mww', 'mww', 'mww', 'mww', 'mww'], 
        dtype='<U3'),
 'mag': array([6.7, 5.2, 5.7, 5.7, 5.6]),
 'alert': array(['green', 'green', 'green', 'green', 'green'], 
        dtype='<U5'),
 'tsunami': array([1, 0, 0, 0, 1])}

现在,获取最大值的幅度仅仅是选择mag键并在 NumPy 数组上调用max()方法。这比列表推导式的实现速度快近两倍,尤其是处理仅有五个条目的数据时——想象一下,第一个尝试在大数据集上的表现将会有多糟糕:

>>> %%timeit
>>> array_dict['mag'].max()
5.22 µs ± 100 ns per loop 
(mean ± std. dev. of 7 runs, 100000 loops each)

然而,这种表示方式还有其他问题。假设我们想获取最大幅度的地震的所有信息;我们该如何操作呢?我们需要找到最大值的索引,然后对于字典中的每一个键,获取该索引。结果现在是一个包含字符串的 NumPy 数组(我们的数值已被转换),并且我们现在处于之前看到的格式:

>>> np.array([
...     value[array_dict['mag'].argmax()]
...     for key, value in array_dict.items()
... ])
array(['2018-10-13 11:10:23.560',
       '262km NW of Ozernovskiy, Russia',
       'mww', '6.7', 'green', '1'], dtype='<U31')

考虑如何按幅度从小到大排序数据。在第一种表示方式中,我们需要通过检查第三个索引来对行进行排序。而在第二种表示方式中,我们需要确定mag列的索引顺序,然后按照这些相同的索引排序所有其他数组。显然,同时操作多个包含不同数据类型的 NumPy 数组有些繁琐;然而,pandas是在 NumPy 数组之上构建的,可以让这一过程变得更加简单。让我们从Series数据结构的概述开始,探索pandas

Series

Series类提供了一种数据结构,用于存储单一类型的数组,就像 NumPy 数组一样。然而,它还提供了一些额外的功能。这个一维表示可以被看作是电子表格中的一列。我们为我们的列命名,而其中的数据是相同类型的(因为我们测量的是相同的变量):

>>> import pandas as pd
>>> place = pd.Series(array_dict['place'], name='place')
>>> place
0          262km NW of Ozernovskiy, Russia
1              25km E of Bitung, Indonesia
2                42km WNW of Sola, Vanuatu
3    13km E of Nueva Concepcion, Guatemala
4      128km SE of Kimbe, Papua New Guinea
Name: place, dtype: object

注意结果左侧的数字;这些数字对应于原始数据集中行号(由于 Python 中的计数是从 0 开始的,因此行号比实际行号少 1)。这些行号构成了索引,我们将在接下来的部分讨论。行号旁边是行的实际值,在本示例中,它是一个字符串,指示地震发生的地点。请注意,在 Series 对象的名称旁边,我们有 dtype: object;这表示 place 的数据类型是 object。在 pandas 中,字符串会被分类为 object

要访问 Series 对象的属性,我们使用 <object>.<attribute_name> 这种属性表示法。以下是我们将要访问的一些常用属性。注意,dtypeshape 是可用的,正如我们在 NumPy 数组中看到的那样:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.1_B16834.jpg

图 2.1 – 常用的系列属性

重要提示

大多数情况下,pandas 对象使用 NumPy 数组来表示其内部数据。然而,对于某些数据类型,pandas 在 NumPy 的基础上构建了自己的数组(https://pandas.pydata.org/pandas-docs/stable/reference/arrays.html)。因此,根据数据类型,values 方法返回的可能是 pandas.arraynumpy.array 对象。因此,如果我们需要确保获得特定类型的数据,建议使用 array 属性或 to_numpy() 方法,而不是 values

请务必将 pandas.Series 文档(pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.html)收藏以便以后参考。它包含有关如何创建 Series 对象、所有可用属性和方法的完整列表,以及源代码链接。在了解了 Series 类的高层次介绍后,我们可以继续学习 Index 类。

索引

Index 类的引入使得 Series 类比 NumPy 数组更为强大。Index 类为我们提供了行标签,使得我们可以通过行号选择数据。根据索引的类型,我们可以提供行号、日期,甚至字符串来选择行。它在数据条目的标识中起着关键作用,并在 pandas 中的多种操作中被使用,正如我们在本书中将要看到的那样。我们可以通过 index 属性访问索引:

>>> place_index = place.index
>>> place_index
RangeIndex(start=0, stop=5, step=1)

注意,这是一个 RangeIndex 对象。它的值从 0 开始,到 4 结束。步长为 1 表明索引值之间的差距为 1,意味着我们有该范围内的所有整数。默认的索引类是 RangeIndex;但是,我们可以更改索引,正如我们将在第三章 *《数据清理与 Pandas》*中讨论的那样。通常,我们要么使用行号的 Index 对象,要么使用日期(时间)的 Index 对象。

Series对象一样,我们可以通过values属性访问底层数据。请注意,这个Index对象是基于一个 NumPy 数组构建的:

>>> place_index.values
array([0, 1, 2, 3, 4], dtype=int64)

Index对象的一些有用属性包括:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.2_B16834.jpg

图 2.2 – 常用的索引属性

NumPy 和pandas都支持算术运算,这些运算将按元素逐一执行。NumPy 会使用数组中的位置来进行运算:

>>> np.array([1, 1, 1]) + np.array([-1, 0, 1])
array([0, 1, 2])

pandas中,这种按元素逐一执行的算术运算是基于匹配的索引值进行的。如果我们将一个索引从04Series对象(存储在x中)与另一个索引从15y对象相加,只有当索引对齐时,我们才会得到结果(14)。在第三章使用 Pandas 进行数据整理中,我们将讨论一些方法来改变和对齐索引,这样我们就可以执行这些类型的操作而不丢失数据:

>>> numbers = np.linspace(0, 10, num=5) # [0, 2.5, 5, 7.5, 10]
>>> x = pd.Series(numbers) # index is [0, 1, 2, 3, 4]
>>> y = pd.Series(numbers, index=pd.Index([1, 2, 3, 4, 5]))
>>> x + y
0     NaN
1     2.5
2     7.5
3    12.5
4    17.5
5     NaN
dtype: float64

现在我们已经了解了SeriesIndex类的基础知识,接下来我们可以学习DataFrame类。请注意,关于Index类的更多信息可以在相应的文档中找到:pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Index.html

数据框

Series类中,我们本质上处理的是电子表格的列,数据类型都是相同的。DataFrame类是在Series类基础上构建的,可以拥有多个列,每列都有其自己的数据类型;我们可以将其看作是代表整个电子表格。我们可以将我们从示例数据中构建的 NumPy 表示形式转化为DataFrame对象:

>>> df = pd.DataFrame(array_dict) 
>>> df

这给我们提供了一个由六个系列组成的数据框。请注意time列前面的那一列;它是行的Index对象。在创建DataFrame对象时,pandas会将所有的系列对齐到相同的索引。在这种情况下,它仅仅是行号,但我们也可以轻松地使用time列作为索引,这将启用一些额外的pandas功能,正如我们在第四章聚合 Pandas 数据框中将看到的那样:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.3_B16834.jpg

图 2.3 – 我们的第一个数据框

我们的列每一列都有单一的数据类型,但它们并非都具有相同的数据类型:

>>> df.dtypes
time        object
place       object
magType     object
mag        float64
alert       object
tsunami      int64
dtype: object

数据框的值看起来与我们最初的 NumPy 表示非常相似:

>>> df.values
array([['2018-10-13 11:10:23.560',
        '262km NW of Ozernovskiy, Russia',
        'mww', 6.7, 'green', 1],
['2018-10-13 04:34:15.580', 
        '25km E of Bitung, Indonesia', 'mww', 5.2, 'green', 0],
['2018-10-13 00:13:46.220', '42km WNW of Sola, Vanuatu', 
        'mww', 5.7, 'green', 0],
       ['2018-10-12 21:09:49.240',
        '13km E of Nueva Concepcion, Guatemala',
        'mww', 5.7, 'green', 0],
['2018-10-12 02:52:03.620','128 km SE of Kimbe, 
Papua New Guinea', 'mww', 5.6, 'green', 1]], 
      dtype=object)

我们可以通过columns属性访问列名。请注意,它们实际上也存储在一个Index对象中:

>>> df.columns
Index(['time', 'place', 'magType', 'mag', 'alert', 'tsunami'], 
      dtype='object')

以下是一些常用的数据框属性:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.4_B16834.jpg

图 2.4 – 常用的数据框属性

请注意,我们也可以对数据框执行算术运算。例如,我们可以将df加到它自己上,这将对数值列进行求和,并将字符串列进行连接:

>>> df + df

Pandas 只有在索引和列都匹配时才会执行操作。在这里,pandas将字符串类型的列(timeplacemagTypealert)在数据框之间进行了合并。而数值类型的列(magtsunami)则进行了求和:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.5_B16834.jpg

图 2.5 – 添加数据框

关于DataFrame对象以及可以直接对其执行的所有操作的更多信息,请参考官方文档:pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.html;请务必将其添加书签以备将来参考。现在,我们已经准备好开始学习如何从各种来源创建DataFrame对象。

创建 pandas DataFrame

现在我们已经了解了将要使用的数据结构,接下来可以讨论创建它们的不同方式。然而,在深入代码之前,了解如何直接从 Python 获取帮助是非常重要的。如果我们在使用 Python 时遇到不确定的地方,可以使用内置的help()函数。我们只需要运行help(),并传入我们想查看文档的包、模块、类、对象、方法或函数。当然,我们也可以在线查找文档;然而,在大多数情况下,help()与在线文档是等效的,因为它们用于生成文档。

假设我们首先运行了import pandas as pd,然后可以运行help(pd)来显示有关pandas包的信息;help(pd.DataFrame)来查看所有关于DataFrame对象的方法和属性(注意,我们也可以传入一个DataFrame对象);help(pd.read_csv)以了解有关pandas读取 CSV 文件到 Python 中的函数及其使用方法。我们还可以尝试使用dir()函数和__dict__属性,它们将分别为我们提供可用项的列表或字典;不过,它们可能没有help()函数那么有用。

此外,我们还可以使用???来获取帮助,这得益于 IPython,它是 Jupyter Notebooks 强大功能的一部分。与help()函数不同,我们可以在想要了解更多的内容后加上问号,就像在问 Python 一个问题一样;例如,pd.read_csv?pd.read_csv??。这三者会输出略有不同的信息:help()会提供文档字符串;?会提供文档字符串,并根据我们的查询增加一些附加信息;而??会提供更多信息,且在可能的情况下,还会显示源代码。

现在,让我们转到下一个笔记本文件2-creating_dataframes.ipynb,并导入我们即将使用的包。我们将使用 Python 标准库中的datetime,以及第三方包numpypandas

>>> import datetime as dt
>>> import numpy as np
>>> import pandas as pd

重要提示

我们通过将pandas包引入并为其指定别名pd,这是导入pandas最常见的方式。事实上,我们只能用pd来引用它,因为那是我们导入到命名空间中的别名。包需要在使用之前导入;安装将所需的文件放在我们的计算机上,但为了节省内存,Python 不会在启动时加载所有已安装的包——只有我们明确告诉它加载的包。

现在我们已经准备好开始使用pandas了。首先,我们将学习如何从其他 Python 对象创建pandas对象。接着,我们将学习如何从平面文件、数据库中的表格以及 API 请求的响应中创建pandas对象。

从 Python 对象

在讲解如何从 Python 对象创建DataFrame对象的所有方法之前,我们应该先了解如何创建Series对象。记住,Series对象本质上是DataFrame对象中的一列,因此,一旦我们掌握了这一点,理解如何创建DataFrame对象应该就不难了。假设我们想创建一个包含五个介于01之间的随机数的序列,我们可以使用 NumPy 生成随机数数组,并从中创建序列。

提示

NumPy 使得生成数值数据变得非常简单。除了生成随机数外,我们还可以使用np.linspace()函数在某个范围内生成均匀分布的数值;使用np.arange()函数获取一系列整数;使用np.random.normal()函数从标准正态分布中抽样;以及使用np.zeros()函数轻松创建全零数组,使用np.ones()函数创建全一数组。本书中我们将会一直使用 NumPy。

为了确保结果是可重复的,我们将在这里设置种子。任何具有类似列表结构的Series对象(例如 NumPy 数组):

>>> np.random.seed(0) # set a seed for reproducibility
>>> pd.Series(np.random.rand(5), name='random')
0    0.548814
1    0.715189
2    0.602763
3    0.544883
4    0.423655
Name: random, dtype: float64

创建DataFrame对象是创建Series对象的扩展;它由一个或多个系列组成,每个系列都会有不同的名称。这让我们联想到 Python 中的字典结构:键是列名,值是列的内容。注意,如果我们想将一个单独的Series对象转换为DataFrame对象,可以使用它的to_frame()方法。

提示

在计算机科学中,__init__()方法。当我们运行pd.Series()时,Python 会调用pd.Series.__init__(),该方法包含实例化新Series对象的指令。我们将在第七章中进一步了解__init__()方法,金融分析 – 比特币与股票市场

由于列可以是不同的数据类型,让我们通过这个例子来做一些有趣的事情。我们将创建一个包含三列、每列有五个观察值的DataFrame对象:

  • random:五个介于01之间的随机数,作为一个 NumPy 数组

  • text:一个包含五个字符串或None的列表

  • truth:一个包含五个随机布尔值的列表

我们还将使用pd.date_range()函数创建一个DatetimeIndex对象。该索引将包含五个日期(periods=5),日期之间相隔一天(freq='1D'),并以 2019 年 4 月 21 日(end)为结束日期,索引名称为date。请注意,关于pd.date_range()函数接受的频率值的更多信息,请参见pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases

我们所需要做的,就是将列打包成字典,使用所需的列名作为键,并在调用pd.DataFrame()构造函数时传入该字典。索引通过index参数传递:

>>> np.random.seed(0) # set seed so result is reproducible
>>> pd.DataFrame(
...     {
...         'random': np.random.rand(5),
...         'text': ['hot', 'warm', 'cool', 'cold', None],
...         'truth': [np.random.choice([True, False]) 
...                   for _ in range(5)]
...     }, 
...     index=pd.date_range(
...         end=dt.date(2019, 4, 21),
...         freq='1D', periods=5, name='date'
...     )
... )

重要提示

按照约定,我们使用_来存放在循环中我们不关心的变量。在这里,我们使用range()作为计数器,其值不重要。有关_在 Python 中作用的更多信息,请参见hackernoon.com/understanding-the-underscore-of-python-309d1a029edc

在索引中包含日期,使得通过日期(甚至日期范围)选择条目变得容易,正如我们在第三章《Pandas 数据处理》中将看到的那样:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.6_B16834.jpg

图 2.6 – 从字典创建数据框

在数据不是字典而是字典列表的情况下,我们仍然可以使用pd.DataFrame()。这种格式的数据通常来自 API。当数据以这种格式时,列表中的每个条目将是一个字典,字典的键是列名,字典的值是该索引处该列的值:

>>> pd.DataFrame([
...     {'mag': 5.2, 'place': 'California'},
...     {'mag': 1.2, 'place': 'Alaska'},
...     {'mag': 0.2, 'place': 'California'},
... ])

这将给我们一个包含三行(每个列表条目对应一行)和两列(每个字典的键对应一列)的数据框:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.7_B16834.jpg

图 2.7 – 从字典列表创建数据框

事实上,pd.DataFrame()也适用于元组列表。注意,我们还可以通过columns参数将列名作为列表传入:

>>> list_of_tuples = [(n, n**2, n**3) for n in range(5)]
>>> list_of_tuples
[(0, 0, 0), (1, 1, 1), (2, 4, 8), (3, 9, 27), (4, 16, 64)]
>>> pd.DataFrame(
...     list_of_tuples,
...     columns=['n', 'n_squared', 'n_cubed']
... )

每个元组被当作记录处理,并成为数据框中的一行:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.8_B16834.jpg

图 2.8 – 从元组列表创建数据框

我们还可以选择使用pd.DataFrame()与 NumPy 数组:

>>> pd.DataFrame(
...     np.array([
...         [0, 0, 0],
...         [1, 1, 1],
...         [2, 4, 8],
...         [3, 9, 27],
...         [4, 16, 64]
...     ]), columns=['n', 'n_squared', 'n_cubed']
... )

这样会将数组中的每个条目按行堆叠到数据框中,得到的结果与图 2.8完全相同。

从文件

我们想要分析的数据大多数来自 Python 之外。在很多情况下,我们可能会从数据库或网站获得一个数据转储,然后将其带入 Python 进行筛选。数据转储之所以得名,是因为它包含大量数据(可能是非常详细的层次),且最初往往不加区分;因此,它们可能显得笨重。

通常,这些数据转储会以文本文件(.txt)或 CSV 文件(.csv)的形式出现。Pandas 提供了许多读取不同类型文件的方法,因此我们只需查找匹配我们文件格式的方法即可。我们的地震数据是 CSV 文件,因此我们使用pd.read_csv()函数来读取它。然而,在尝试读取之前,我们应始终先进行初步检查;这将帮助我们确定是否需要传递其他参数,比如sep来指定分隔符,或names来在文件没有表头行的情况下手动提供列名。

重要提示

Windows 用户:根据您的设置,接下来的代码块中的命令可能无法正常工作。如果遇到问题,笔记本中有替代方法。

我们可以直接在 Jupyter Notebook 中进行尽职调查,得益于 IPython,只需在命令前加上!,表示这些命令将作为 Shell 命令执行。首先,我们应该检查文件的大小,既要检查行数,也要检查字节数。要检查行数,我们使用wc工具(单词计数)并加上-l标志来计算行数。我们文件中有 9,333 行:

>>> !wc -l data/earthquakes.csv
9333 data/earthquakes.csv

现在,让我们检查一下文件的大小。为此,我们将使用ls命令查看data目录中的文件列表。我们可以添加-lh标志,以便以易于阅读的格式获取文件信息。最后,我们将此输出发送到grep工具,它将帮助我们筛选出我们想要的文件。这告诉我们,earthquakes.csv文件的大小为 3.4 MB:

>>> !ls -lh data | grep earthquakes.csv
-rw-r--r-- 1 stefanie stefanie 3.4M ... earthquakes.csv

请注意,IPython 还允许我们将命令的结果捕获到 Python 变量中,因此,如果我们不熟悉管道符(|)或grep,我们可以这样做:

>>> files = !ls -lh data
>>> [file for file in files if 'earthquake' in file]
['-rw-r--r-- 1 stefanie stefanie 3.4M ... earthquakes.csv']

现在,让我们看一下文件的顶部几行,看看文件是否包含表头。我们将使用head工具,并通过-n标志指定行数。这告诉我们,第一行包含数据的表头,并且数据是以逗号分隔的(仅仅因为文件扩展名是.csv并不意味着它是逗号分隔的):

>>> !head -n 2 data/earthquakes.csv
alert,cdi,code,detail,dmin,felt,gap,ids,mag,magType,mmi,net,nst,place,rms,sig,sources,status,time,title,tsunami,type,types,tz,updated,url
,,37389218,https://earthquake.usgs.gov/[...],0.008693,,85.0,",ci37389218,",1.35,ml,,ci,26.0,"9km NE of Aguanga, CA",0.19,28,",ci,",automatic,1539475168010,"M 1.4 - 9km NE of Aguanga, CA",0,earthquake,",geoserve,nearby-cities,origin,phase-data,",-480.0,1539475395144,https://earthquake.usgs.gov/earthquakes/eventpage/ci37389218

请注意,我们还应该检查文件的底部几行,以确保没有多余的数据需要通过tail工具忽略。这个文件没有问题,因此结果不会在此处重复;不过,笔记本中包含了结果。

最后,我们可能对查看数据中的列数感兴趣。虽然我们可以仅通过计算head命令结果的第一行中的字段数来实现,但我们也可以选择使用awk工具(用于模式扫描和处理)来计算列数。-F标志允许我们指定分隔符(在这种情况下是逗号)。然后,我们指定对文件中的每个记录执行的操作。我们选择打印NF,这是一个预定义变量,其值是当前记录中字段的数量。在这里,我们在打印之后立即使用exit,以便只打印文件中第一行的字段数,然后停止。这看起来有点复杂,但这绝不是我们需要记住的内容:

>>> !awk -F',' '{print NF; exit}' data/earthquakes.csv
26

由于我们知道文件的第一行包含标题,并且该文件是逗号分隔的,我们也可以通过使用head获取标题并用 Python 解析它们来计算列数:

>>> headers = !head -n 1 data/earthquakes.csv
>>> len(headers[0].split(','))
26

重要说明

直接在 Jupyter Notebook 中运行 Shell 命令极大地简化了我们的工作流程。然而,如果我们没有命令行的经验,最初学习这些命令可能会很复杂。IPython 的文档提供了一些关于运行 Shell 命令的有用信息,您可以在ipython.readthedocs.io/en/stable/interactive/reference.html#system-shell-access找到。

总结一下,我们现在知道文件大小为 3.4MB,使用逗号分隔,共有 26 列和 9,333 行,第一行是标题。这意味着我们可以使用带有默认设置的pd.read_csv()函数:

>>> df = pd.read_csv('earthquakes.csv')

请注意,我们不仅仅局限于从本地机器上的文件读取数据;文件路径也可以是 URL。例如,我们可以从 GitHub 读取相同的 CSV 文件:

>>> df = pd.read_csv(
...     'https://github.com/stefmolin/'
...     'Hands-On-Data-Analysis-with-Pandas-2nd-edition'
...     '/blob/master/ch_02/data/earthquakes.csv?raw=True'
... )

Pandas 通常非常擅长根据输入数据自动判断需要使用的选项,因此我们通常不需要为此调用添加额外的参数;然而,若有需要,仍有许多选项可以使用,其中包括以下几种:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.9_B16834.jpg

图 2.9 – 读取文件时有用的参数

本书中,我们将处理 CSV 文件;但请注意,我们也可以使用read_excel()函数读取 Excel 文件,使用read_json()函数读取json文件,或者使用带有sep参数的read_csv()函数来处理不同的分隔符。

如果我们不学习如何将数据框保存到文件中,以便与他人分享,那将是失职。为了将数据框写入 CSV 文件,我们调用其to_csv()方法。在这里我们必须小心;如果数据框的索引只是行号,我们可能不想将其写入文件(对数据的使用者没有意义),但这是默认设置。我们可以通过传入index=False来写入不包含索引的数据:

>>> df.to_csv('output.csv', index=False)

与从文件中读取数据一样,SeriesDataFrame 对象也有方法将数据写入 Excel(to_excel())和 JSON 文件(to_json())。请注意,虽然我们使用 pandas 中的函数来读取数据,但我们必须使用方法来写入数据;读取函数创建了我们想要处理的 pandas 对象,而写入方法则是我们使用 pandas 对象执行的操作。

提示

上述读取和写入的文件路径是 /home/myuser/learning/hands_on_pandas/data.csv,而我们当前的工作目录是 /home/myuser/learning/hands_on_pandas,因此我们可以简单地使用 data.csv 的相对路径作为文件路径。

Pandas 提供了从许多其他数据源读取和写入的功能,包括数据库,我们接下来会讨论这些内容;pickle 文件(包含序列化的 Python 对象——有关更多信息,请参见 进一步阅读 部分);以及 HTML 页面。请务必查看 pandas 文档中的以下资源,以获取完整的功能列表:pandas.pydata.org/pandas-docs/stable/user_guide/io.html

从数据库中读取

Pandas 可以与 SQLite 数据库进行交互,而无需安装任何额外的软件包;不过,若要与其他类型的数据库进行交互,则需要安装 SQLAlchemy 包。与 SQLite 数据库的交互可以通过使用 Python 标准库中的 sqlite3 模块打开数据库连接来实现,然后使用 pd.read_sql() 函数查询数据库,或在 DataFrame 对象上使用 to_sql() 方法将数据写入数据库。

在我们从数据库中读取数据之前,先来写入数据。我们只需在我们的 DataFrame 上调用 to_sql(),并告诉它要写入哪个表,使用哪个数据库连接,以及如果表已存在该如何处理。本书 GitHub 仓库中的这一章节文件夹里已经有一个 SQLite 数据库:data/quakes.db。请注意,要创建一个新的数据库,我们可以将 'data/quakes.db' 更改为新数据库文件的路径。现在让我们把 data/tsunamis.csv 文件中的海啸数据写入名为 tsunamis 的数据库表中,如果表已存在,则替换它:

>>> import sqlite3
>>> with sqlite3.connect('data/quakes.db') as connection:
...     pd.read_csv('data/tsunamis.csv').to_sql(
...         'tsunamis', connection, index=False,
...         if_exists='replace'
...     )

查询数据库与写入数据库一样简单。请注意,这需要了解 pandas 与 SQL 的对比关系,并且可以参考 第四章聚合 Pandas DataFrames,了解一些 pandas 操作与 SQL 语句的关系示例。

让我们查询数据库中的完整tsunamis表。当我们编写 SQL 查询时,首先声明我们要选择的列,在本例中是所有列,因此我们写"SELECT *"。接下来,我们声明要从哪个表中选择数据,在我们这里是tsunamis,因此我们写"FROM tsunamis"。这就是我们完整的查询(当然,它可以比这更复杂)。要实际查询数据库,我们使用pd.read_sql(),传入查询和数据库连接:

>>> import sqlite3
>>> with sqlite3.connect('data/quakes.db') as connection:
...     tsunamis = \
...         pd.read_sql('SELECT * FROM tsunamis', connection)
>>> tsunamis.head()

我们现在在数据框中已经有了海啸数据:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.10_B16834.jpg

图 2.10 – 从数据库读取数据

重要说明

我们在两个代码块中创建的connection对象是with语句的一个示例,自动在代码块执行后进行清理(在本例中是关闭连接)。这使得清理工作变得简单,并确保我们不会留下任何未完成的工作。一定要查看标准库中的contextlib,它提供了使用with语句和上下文管理器的工具。文档请参考 docs.python.org/3/library/contextlib.html

来自 API

我们现在可以轻松地从 Python 中的数据或从获得的文件中创建SeriesDataFrame对象,但如何从在线资源(如 API)获取数据呢?无法保证每个数据源都会以相同的格式提供数据,因此我们必须在方法上保持灵活,并能够检查数据源以找到合适的导入方法。在本节中,我们将从 USGS API 请求一些地震数据,并查看如何从结果中创建数据框。在第三章《使用 Pandas 进行数据清理》中,我们将使用另一个 API 收集天气数据。

在本节中,我们将在3-making_dataframes_from_api_requests.ipynb笔记本中工作,因此我们需要再次导入所需的包。与之前的笔记本一样,我们需要pandasdatetime,但我们还需要requests包来发起 API 请求:

>>> import datetime as dt
>>> import pandas as pd
>>> import requests

接下来,我们将向 USGS API 发起GET请求,获取一个 JSON 负载(包含请求或响应数据的类似字典的响应),并指定geojson格式。我们将请求过去 30 天的地震数据(可以使用dt.timedeltadatetime对象进行运算)。请注意,我们将yesterday作为日期范围的结束日期,因为 API 尚未提供今天的完整数据:

>>> yesterday = dt.date.today() - dt.timedelta(days=1)
>>> api = 'https://earthquake.usgs.gov/fdsnws/event/1/query'
>>> payload = {
...     'format': 'geojson',
...     'starttime': yesterday - dt.timedelta(days=30),
...     'endtime': yesterday
... }
>>> response = requests.get(api, params=payload)

重要说明

GET 是一种 HTTP 方法。这个操作告诉服务器我们想要读取一些数据。不同的 API 可能要求我们使用不同的方法来获取数据;有些会要求我们发送 POST 请求,在其中进行身份验证。你可以在nordicapis.com/ultimate-guide-to-all-9-standard-http-methods/上了解更多关于 API 请求和 HTTP 方法的信息。

在我们尝试从中创建 dataframe 之前,应该先确认我们的请求是否成功。我们可以通过检查response对象的status_code属性来做到这一点。状态码及其含义的列表可以在en.wikipedia.org/wiki/List_of_HTTP_status_codes找到。200响应将表示一切正常:

>>> response.status_code
200

我们的请求成功了,接下来让我们看看我们得到的数据是什么样的。我们请求了一个 JSON 负载,它本质上是一个字典,因此我们可以使用字典方法来获取更多关于它结构的信息。这将是大量的数据;因此,我们不想只是将它打印到屏幕上进行检查。我们需要从 HTTP 响应(存储在response变量中)中提取 JSON 负载,然后查看键以查看结果数据的主要部分:

>>> earthquake_json = response.json()
>>> earthquake_json.keys()
dict_keys(['type', 'metadata', 'features', 'bbox'])

我们可以检查这些键对应的值是什么样的数据;其中一个将是我们需要的数据。metadata部分告诉我们一些关于请求的信息。虽然这些信息确实有用,但它不是我们现在需要的:

>>> earthquake_json['metadata']
{'generated': 1604267813000,
 'url': 'https://earthquake.usgs.gov/fdsnws/event/1/query?
format=geojson&starttime=2020-10-01&endtime=2020-10-31',
 'title': 'USGS Earthquakes',
 'status': 200,
 'api': '1.10.3',
 'count': 13706}

features 键看起来很有前景;如果它确实包含了我们所有的数据,我们应该检查它的数据类型,以避免试图将所有内容打印到屏幕上:

>>> type(earthquake_json['features'])
list

这个键包含一个列表,所以让我们查看第一个条目,看看这是不是我们想要的数据。请注意,USGS 数据可能会随着更多关于地震信息的披露而被修改或添加,因此查询相同的日期范围可能会得到不同数量的结果。基于这个原因,以下是一个条目的示例:

>>> earthquake_json['features'][0]
{'type': 'Feature',
 'properties': {'mag': 1,
  'place': '50 km ENE of Susitna North, Alaska',
  'time': 1604102395919, 'updated': 1604103325550, 'tz': None,
  'url': 'https://earthquake.usgs.gov/earthquakes/eventpage/ak020dz5f85a',
  'detail': 'https://earthquake.usgs.gov/fdsnws/event/1/query?eventid=ak020dz5f85a&format=geojson',
  'felt': None, 'cdi': None, 'mmi': None, 'alert': None,
  'status': 'reviewed', 'tsunami': 0, 'sig': 15, 'net': 'ak',
  'code': '020dz5f85a', 'ids': ',ak020dz5f85a,',
  'sources': ',ak,', 'types': ',origin,phase-data,',
  'nst': None, 'dmin': None, 'rms': 1.36, 'gap': None,
  'magType': 'ml', 'type': 'earthquake',
  'title': 'M 1.0 - 50 km ENE of Susitna North, Alaska'},
 'geometry': {'type': 'Point', 'coordinates': [-148.9807, 62.3533, 5]},
 'id': 'ak020dz5f85a'} 

这绝对是我们需要的数据,但我们需要全部数据吗?仔细检查后,我们只关心properties字典中的内容。现在,我们面临一个问题,因为我们有一个字典的列表,而我们只需要从中提取一个特定的键。我们该如何提取这些信息,以便构建我们的 dataframe 呢?我们可以使用列表推导式从features列表中的每个字典中隔离出properties部分:

>>> earthquake_properties_data = [
...     quake['properties'] 
...     for quake in earthquake_json['features']
... ]

最后,我们准备创建我们的 dataframe。Pandas 已经知道如何处理这种格式的数据(字典列表),因此我们只需要在调用pd.DataFrame()时传入数据:

>>> df = pd.DataFrame(earthquake_properties_data)

现在我们知道如何从各种数据源创建 dataframes,我们可以开始学习如何操作它们。

检查一个 DataFrame 对象

我们读取数据时应该做的第一件事就是检查它;我们需要确保数据框不为空,并且行数据符合预期。我们的主要目标是验证数据是否正确读取,并且所有数据都存在;然而,这次初步检查还会帮助我们了解应将数据处理工作重点放在哪里。在本节中,我们将探索如何在4-inspecting_dataframes.ipynb笔记本中检查数据框。

由于这是一个新笔记本,我们必须再次处理设置。此次,我们需要导入pandasnumpy,并读取包含地震数据的 CSV 文件:

>>> import numpy as np
>>> import pandas as pd
>>> df = pd.read_csv('data/earthquakes.csv')

检查数据

首先,我们要确保数据框中确实有数据。我们可以检查empty属性来了解情况:

>>> df.empty
False

到目前为止,一切顺利;我们有数据。接下来,我们应检查读取了多少数据;我们想知道观察数(行数)和变量数(列数)。为此,我们使用shape属性。我们的数据包含 9,332 个观察值和 26 个变量,这与我们最初检查文件时的结果一致:

>>> df.shape
(9332, 26)

现在,让我们使用columns属性查看数据集中列的名称:

>>> df.columns
Index(['alert', 'cdi', 'code', 'detail', 'dmin', 'felt', 'gap', 
       'ids', 'mag', 'magType', 'mmi', 'net', 'nst', 'place', 
       'rms', 'sig', 'sources', 'status', 'time', 'title', 
       'tsunami', 'type', 'types', 'tz', 'updated', 'url'],
      dtype='object')

重要提示

拥有列的列表并不意味着我们知道每一列的含义。特别是在数据来自互联网的情况下,在得出结论之前,务必查阅列的含义。有关geojson格式中字段的信息,包括每个字段在 JSON 负载中的含义(以及一些示例值),可以在美国地质调查局(USGS)网站上的earthquake.usgs.gov/earthquakes/feed/v1.0/geojson.php找到。

我们知道数据的维度,但它实际是什么样的呢?为此,我们可以使用head()tail()方法,分别查看顶部和底部的行。默认情况下,这将显示五行数据,但我们可以通过传入不同的数字来更改这一设置。让我们看看前几行数据:

>>> df.head()

以下是我们使用head()方法获得的前五行:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.11_B16834.jpg

图 2.11 – 检查数据框的前五行

要获取最后两行,我们使用tail()方法并传入2作为行数:

>>> df.tail(2)

以下是结果:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.12_B16834.jpg

图 2.12 – 检查数据框的底部两行

提示

默认情况下,当我们在 Jupyter Notebook 中打印包含许多列的数据框时,只有一部分列会显示出来。这是因为pandas有一个显示列数的限制。我们可以使用pd.set_option('display.max_columns', <new_value>)来修改此行为。有关更多信息,请查阅pandas.pydata.org/pandas-docs/stable/user_guide/options.html。该文档中还包含了一些示例命令。

我们可以使用dtypes属性查看各列的数据类型,这样可以轻松地发现哪些列被错误地存储为不正确的类型。(记住,字符串会被存储为object。)这里,time列被存储为整数,这是我们将在第三章《数据清洗与 Pandas》中学习如何修复的问题:

>>> df.dtypes
alert       object
...
mag        float64
magType     object
...
time         int64
title       object
tsunami      int64
...
tz         float64
updated      int64
url         object
dtype: object

最后,我们可以使用info()方法查看每列中有多少非空条目,并获取关于索引的信息。pandas通常会将对象类型的值表示为None,而NaNfloatinteger类型的列)表示缺失值:

>>> df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 9332 entries, 0 to 9331
Data columns (total 26 columns):
 #   Column   Non-Null Count  Dtype  
---  ------   --------------  -----  
 0   alert    59 non-null     object 
 ... 
 8   mag      9331 non-null   float64
 9   magType  9331 non-null   object 
 ... 
 18  time     9332 non-null   int64  
 19  title    9332 non-null   object 
 20  tsunami  9332 non-null   int64  
 ... 
 23  tz       9331 non-null   float64
 24  updated  9332 non-null   int64  
 25  url      9332 non-null   object 
dtypes: float64(9), int64(4), object(13)
memory usage: 1.9+ MB

在初步检查之后,我们已经了解了数据的结构,现在可以开始尝试理解数据的含义。

描述和总结数据

到目前为止,我们已经检查了从地震数据创建的DataFrame对象的结构,但除了几行数据的样子,我们对数据一无所知。接下来的步骤是计算总结统计数据,这将帮助我们更好地了解数据。Pandas 提供了几种方法来轻松实现这一点;其中一种方法是describe(),如果我们只对某一列感兴趣,它也适用于Series对象。让我们获取数据中数字列的总结:

>>> df.describe()

这会为我们提供 5 个数字总结,以及数字列的计数、均值和标准差:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.13_B16834.jpg

图 2.13 – 计算总结统计数据

提示

如果我们想要不同的百分位数,可以通过percentiles参数传递它们。例如,如果我们只想要 5%和 95%的百分位数,我们可以运行df.describe(percentiles=[0.05, 0.95])。请注意,我们仍然会得到第 50 个百分位数的结果,因为那是中位数。

默认情况下,describe()不会提供关于object类型列的任何信息,但我们可以提供include='all'作为参数,或者单独运行它来查看np.object类型的数据:

>>> df.describe(include=np.object)

当描述非数字数据时,我们仍然可以得到非空出现的计数(count);然而,除了其他总结统计数据外,我们会得到唯一值的数量(unique)、众数(top)以及众数出现的次数(freq):

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.14_B16834.jpg

图 2.14 – 类别列的总结统计数据

重要提示

describe() 方法只会为非空值提供摘要统计信息。这意味着,如果我们有 100 行数据,其中一半是空值,那么平均值将是 50 个非空行的总和除以 50。

使用 describe() 方法可以轻松获取数据的快照,但有时我们只想要某个特定的统计数据,不论是针对某一列还是所有列。Pandas 也使得这变得非常简单。下表列出了适用于 SeriesDataFrame 对象的方法:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.15_B16834.jpg

Figure 2.15 – 对系列和数据框架的有用计算方法

提示

Python 使得计算某个条件为 True 的次数变得容易。在底层,True 计算为 1False 计算为 0。因此,我们可以对布尔值序列运行 sum() 方法,得到 True 输出的计数。

对于 Series 对象,我们有一些额外的方法来描述我们的数据:

  • unique(): 返回列中的不同值。

  • value_counts(): 返回给定列中每个唯一值出现的频率表,或者,当传入normalize=True时,返回每个唯一值出现的百分比。

  • mode(): 返回列中最常见的值。

查阅 USGS API 文档中的 alert 字段(可以在 earthquake.usgs.gov/data/comcat/data-eventterms.php#alert 找到)告诉我们,alert 字段的值可以是 'green''yellow''orange''red'(当字段被填充时),并且 alert 列中的警报级别是两个唯一值的字符串,其中最常见的值是 'green',但也有许多空值。那么,另一个唯一值是什么呢?

>>> df.alert.unique()
array([nan, 'green', 'red'], dtype=object)

现在我们了解了该字段的含义以及数据中包含的值,我们预计 'green' 的数量会远远大于 'red';我们可以通过使用 value_counts() 来检查我们的直觉,得到一个频率表。注意,我们只会得到非空条目的计数:

>>> df.alert.value_counts()
green    58
red       1
Name: alert, dtype: int64

请注意,Index 对象也有多个方法,能够帮助我们描述和总结数据:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.16_B16834.jpg

Figure 2.16 – 对索引的有用方法

当我们使用 unique()value_counts() 时,我们已经预览了如何选择数据的子集。现在,让我们更详细地讨论选择、切片、索引和过滤。

获取数据的子集

到目前为止,我们已经学习了如何处理和总结整个数据;然而,我们通常会对对数据子集进行操作和/或分析感兴趣。我们可能希望从数据中提取许多类型的子集,比如选择特定的列或行,或者当满足特定条件时选择某些列或行。为了获取数据的子集,我们需要熟悉选择、切片、索引和过滤等操作。

在本节中,我们将在5-subsetting_data.ipynb笔记本中进行操作。我们的设置如下:

>>> import pandas as pd
>>> df = pd.read_csv('data/earthquakes.csv')

选择列

在前一部分,我们看到了列选择的例子,当时我们查看了alert列中的唯一值;我们作为数据框的属性访问了这个列。记住,列是一个Series对象,因此,例如,选择地震数据中的mag列将给我们返回一个包含地震震级的Series对象:

>>> df.mag
0       1.35
1       1.29
2       3.42
3       0.44
4       2.16
        ... 
9327    0.62
9328    1.00
9329    2.40
9330    1.10
9331    0.66
Name: mag, Length: 9332, dtype: float64

Pandas 为我们提供了几种选择列的方法。使用字典式的符号来选择列是替代属性符号选择列的一种方法:

>>> df['mag']
0       1.35
1       1.29
2       3.42
3       0.44
4       2.16
        ... 
9327    0.62
9328    1.00
9329    2.40
9330    1.10
9331    0.66
Name: mag, Length: 9332, dtype: float64

提示

我们还可以使用get()方法来选择列。这样做的好处是,如果列不存在,不会抛出错误,而且可以提供一个备选值,默认值是None。例如,如果我们调用df.get('event', False),它将返回False,因为我们没有event列。

请注意,我们并不局限于一次只选择一列。通过将列表传递给字典查找,我们可以选择多列,从而获得一个DataFrame对象,它是原始数据框的一个子集:

>>> df[['mag', 'title']]

这样我们就得到了来自原始数据框的完整magtitle列:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.17_B16834.jpg

图 2.17 – 选择数据框的多列

字符串方法是选择列的一种非常强大的方式。例如,如果我们想选择所有以mag开头的列,并同时选择titletime列,我们可以这样做:

>>> df[
...     ['title', 'time'] 
...     + [col for col in df.columns if col.startswith('mag')]
... ]

我们得到了一个由四列组成的数据框,这些列符合我们的筛选条件。注意,返回的列顺序是我们要求的顺序,而不是它们最初出现的顺序。这意味着如果我们想要重新排序列,所要做的就是按照希望的顺序选择它们:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.18_B16834.jpg

图 2.18 – 根据列名选择列

让我们来分析这个例子。我们使用列表推导式遍历数据框中的每一列,只保留那些列名以mag开头的列:

>>> [col for col in df.columns if col.startswith('mag')]
['mag', 'magType']

然后,我们将这个结果与另外两个我们想要保留的列(titletime)合并:

>>> ['title', 'time'] \
... + [col for col in df.columns if col.startswith('mag')]
['title', 'time', 'mag', 'magType']

最后,我们能够使用这个列表在数据框上执行实际的列选择操作,最终得到了图 2.18中的数据框:

>>> df[
...     ['title', 'time'] 
...     + [col for col in df.columns if col.startswith('mag')]
... ]

提示

字符串方法的完整列表可以在 Python 3 文档中找到:docs.python.org/3/library/stdtypes.html#string-methods

切片

当我们想要从数据框中提取特定的行(切片)时,我们使用DataFrame切片,切片的方式与其他 Python 对象(如列表和元组)类似,第一个索引是包含的,最后一个索引是不包含的:

>>> df[100:103]

当指定切片100:103时,我们会返回行100101102

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.19_B16834.jpg

](https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.19_B16834.jpg)

图 2.19 – 切片数据框以提取特定行

我们可以通过使用链式操作来结合行和列的选择:

>>> df[['title', 'time']][100:103]

首先,我们选择了所有行中的titletime列,然后提取了索引为100101102的行:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.20_B16834.jpg

](https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.20_B16834.jpg)

图 2.20 – 使用链式操作选择特定行和列

在前面的例子中,我们选择了列,然后切片了行,但顺序并不重要:

>>> df[100:103][['title', 'time']].equals(
...     df[['title', 'time']][100:103]
... )
True

提示

请注意,我们可以对索引中的任何内容进行切片;然而,确定我们想要的最后一个字符串或日期后面的内容会很困难,因此在使用pandas时,切片日期和字符串的方式与整数切片不同,并且包含两个端点。只要我们提供的字符串可以解析为datetime对象,日期切片就能正常工作。在第三章《使用 Pandas 进行数据清洗》中,我们将看到一些相关示例,并学习如何更改作为索引的内容,从而使这种类型的切片成为可能。

如果我们决定使用链式操作来更新数据中的值,我们会发现pandas会抱怨我们没有正确执行(即使它能正常工作)。这是在提醒我们,使用顺序选择来设置数据可能不会得到我们预期的结果。(更多信息请参见pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy。)

让我们触发这个警告,以便更好地理解它。我们将尝试更新一些地震事件的title列,使其变为小写:

>>> df[110:113]['title'] = df[110:113]['title'].str.lower()
/.../book_env/lib/python3.7/[...]:1: SettingWithCopyWarning:  
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead
See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  """Entry point for launching an IPython kernel.

正如警告所示,成为一个有效的pandas用户,不仅仅是知道如何选择和切片—我们还必须掌握索引。由于这只是一个警告,我们的值已经更新,但这并不总是如此:

>>> df[110:113]['title']
110               m 1.1 - 35km s of ester, alaska
111    m 1.9 - 93km wnw of arctic village, alaska
112      m 0.9 - 20km wsw of smith valley, nevada
Name: title, dtype: object

现在,让我们讨论如何使用索引正确设置值。

索引

Pandas 的索引操作为我们提供了一种单一方法,来选择我们想要的行和列。我们可以使用loc[]iloc[],分别通过标签或整数索引来选择数据子集。记住它们的区别的好方法是将它们想象为location(位置)与integer location(整数位置)。对于所有的索引方法,我们先提供行索引器,再提供列索引器,两者之间用逗号分隔:

df.loc[row_indexer, column_indexer]

注意,使用loc[]时,如警告信息所示,我们不再触发pandas的任何警告。我们还将结束索引从113改为112,因为loc[]是包含端点的:

>>> df.loc[110:112, 'title'] = \
...     df.loc[110:112, 'title'].str.lower()
>>> df.loc[110:112, 'title']
110               m 1.1 - 35km s of ester, alaska
111    m 1.9 - 93km wnw of arctic village, alaska
112      m 0.9 - 20km wsw of smith valley, nevada
Name: title, dtype: object

如果我们使用:作为行(列)索引器,就可以选择所有的行(列),就像普通的 Python 切片一样。让我们使用loc[]选择title列的所有行:

>>> df.loc[:,'title']
0                  M 1.4 - 9km NE of Aguanga, CA
1                  M 1.3 - 9km NE of Aguanga, CA
2                  M 3.4 - 8km NE of Aguanga, CA
3                  M 0.4 - 9km NE of Aguanga, CA
4                  M 2.2 - 10km NW of Avenal, CA
                          ...                   
9327        M 0.6 - 9km ENE of Mammoth Lakes, CA
9328                 M 1.0 - 3km W of Julian, CA
9329    M 2.4 - 35km NNE of Hatillo, Puerto Rico
9330               M 1.1 - 9km NE of Aguanga, CA
9331               M 0.7 - 9km NE of Aguanga, CA
Name: title, Length: 9332, dtype: object

我们可以同时选择多行和多列,使用loc[]

>>> df.loc[10:15, ['title', 'mag']]

这让我们仅选择1015行的titlemag列:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.21_B16834.jpg

图 2.21 – 使用索引选择特定的行和列

如我们所见,使用loc[]时,结束索引是包含的。但iloc[]则不是这样:

>>> df.iloc[10:15, [19, 8]]

观察我们如何需要提供一个整数列表来选择相同的列;这些是列的编号(从0开始)。使用iloc[]时,我们丢失了索引为15的行;这是因为iloc[]使用的整数切片在结束索引上是排除的,类似于 Python 切片语法:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.22_B16834.jpg

图 2.22 – 通过位置选择特定的行和列

然而,我们并不限于只对行使用切片语法;列同样适用:

>>> df.iloc[10:15, 6:10]

通过切片,我们可以轻松地抓取相邻的行和列:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.23_B16834.jpg

图 2.23 – 通过位置选择相邻行和列的范围

使用loc[]时,切片操作也可以在列名上进行。这给我们提供了多种实现相同结果的方式:

>>> df.iloc[10:15, 6:10].equals(df.loc[10:14, 'gap':'magType'])
True

要查找标量值,我们使用at[]iat[],它们更快。让我们选择记录在索引为10的行中的地震幅度(mag列):

>>> df.at[10, 'mag']
0.5

"幅度"列的列索引为8;因此,我们也可以通过iat[]查找幅度:

>>> df.iat[10, 8]
0.5

到目前为止,我们已经学习了如何使用行/列名称和范围来获取数据子集,但如何只获取符合某些条件的数据呢?为此,我们需要学习如何过滤数据。

过滤

Pandas 为我们提供了几种过滤数据的方式,包括True/False值;pandas可以使用这些值来为我们选择适当的行/列。创建布尔掩码的方式几乎是无限的——我们只需要一些返回每行布尔值的代码。例如,我们可以查看mag列中震级大于 2 的条目:

>>> df.mag > 2
0       False
1       False
2        True
3       False
        ...  
9328    False
9329     True
9330    False
9331    False
Name: mag, Length: 9332, dtype: bool

尽管我们可以在整个数据框上运行此操作,但由于我们的地震数据包含不同类型的列,这样做可能不太有用。然而,我们可以使用这种策略来获取一个子集,其中地震的震级大于或等于 7.0:

>>> df[df.mag >= 7.0]

我们得到的结果数据框只有两行:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.24_B16834.jpg

图 2.24 – 使用布尔掩码过滤

不过,我们得到了很多不需要的列。我们本可以将列选择附加到最后一个代码片段的末尾;然而,loc[]同样可以处理布尔掩码:

>>> df.loc[
...     df.mag >= 7.0, 
...     ['alert', 'mag', 'magType', 'title', 'tsunami', 'type']
... ]

以下数据框已经过滤,只包含相关列:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.25_B16834.jpg

图 2.25 – 使用布尔掩码进行索引

我们也不局限于只使用一个条件。让我们筛选出带有红色警报和海啸的地震。为了组合多个条件,我们需要将每个条件用括号括起来,并使用&来要求两个条件都为真:

>>> df.loc[
...     (df.tsunami == 1) & (df.alert == 'red'), 
...     ['alert', 'mag', 'magType', 'title', 'tsunami', 'type']
... ]

数据中只有一个地震满足我们的标准:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.26_B16834.jpg

图 2.26 – 使用 AND 组合过滤条件

如果我们想要至少一个条件为真,则可以使用|

>>> df.loc[
...     (df.tsunami == 1) | (df.alert == 'red'), 
...     ['alert', 'mag', 'magType', 'title', 'tsunami', 'type']
... ]

请注意,这个过滤器要宽松得多,因为虽然两个条件都可以为真,但我们只要求其中一个为真:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.27_B16834.jpg

图 2.27 – 使用 OR 组合过滤条件

重要提示

在创建布尔掩码时,我们必须使用位运算符(&|~)而不是逻辑运算符(andornot)。记住这一点的一个好方法是:我们希望对我们正在测试的系列中的每一项返回一个布尔值,而不是返回单一的布尔值。例如,在地震数据中,如果我们想选择震级大于 1.5 的行,那么我们希望每一行都有一个布尔值,表示该行是否应该被选中。如果我们只希望对数据得到一个单一的值,或许是为了总结它,我们可以使用any()/all()将布尔系列压缩成一个可以与逻辑运算符一起使用的布尔值。我们将在第四章《聚合 Pandas 数据框》中使用any()all()方法。

在前面两个示例中,我们的条件涉及到相等性;然而,我们并不局限于此。让我们选择所有在阿拉斯加的地震数据,其中alert列具有非空值:

>>> df.loc[
...     (df.place.str.contains('Alaska')) 
...     & (df.alert.notnull()), 
...     ['alert', 'mag', 'magType', 'title', 'tsunami', 'type']
... ]

所有阿拉斯加的地震,alert值为green,其中一些伴随有海啸,最大震级为 5.1:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.28_B16834.jpg

图 2.28 – 使用非数字列创建布尔掩码

让我们来分解一下我们是如何得到这个的。Series对象有一些字符串方法,可以通过str属性访问。利用这一点,我们可以创建一个布尔掩码,表示place列中包含单词Alaska的所有行:

df.place.str.contains('Alaska')

为了获取alert列不为 null 的所有行,我们使用了Series对象的notnull()方法(这同样适用于DataFrame对象),以创建一个布尔掩码,表示alert列不为 null 的所有行:

df.alert.notnull()

提示

我们可以使用~,也称为True值和False的反转。所以,df.alert.notnull()~df.alert.isnull()是等价的。

然后,像我们之前做的那样,我们使用&运算符将两个条件结合起来,完成我们的掩码:

(df.place.str.contains('Alaska')) & (df.alert.notnull())

请注意,我们不仅限于检查每一行是否包含文本;我们还可以使用正则表达式。r字符出现在引号外面;这样,Python 就知道这是一个\)字符,而不是在尝试转义紧随其后的字符(例如,当我们使用\n表示换行符时,而不是字母n)。这使得它非常适合与正则表达式一起使用。Python 标准库中的re模块(docs.python.org/3/library/re.html)处理正则表达式操作;然而,pandas允许我们直接使用正则表达式。

使用正则表达式,让我们选择所有震级至少为 3.8 的加利福尼亚地震。我们需要选择place列中以CACalifornia结尾的条目,因为数据不一致(我们将在下一节中学习如何解决这个问题)。$字符表示结束'CA$'给我们的是以CA结尾的条目,因此我们可以使用'CA|California$'来获取以任一项结尾的条目:

>>> df.loc[
...     (df.place.str.contains(r'CA|California$'))
...     & (df.mag > 3.8),         
...     ['alert', 'mag', 'magType', 'title', 'tsunami', 'type']
... ]

在我们研究的时间段内,加利福尼亚只有两次震级超过 3.8 的地震:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.29_B16834.jpg

图 2.29 – 使用正则表达式进行过滤

提示

正则表达式功能非常强大,但不幸的是,也很难正确编写。通常,抓取一些示例行进行解析并使用网站测试它们会很有帮助。请注意,正则表达式有很多种类型,因此务必选择 Python 类型。这个网站支持 Python 类型的正则表达式,并且还提供了一个不错的备忘单: https://regex101.com/。

如果我们想获取震级在 6.5 和 7.5 之间的所有地震怎么办?我们可以使用两个布尔掩码——一个检查震级是否大于或等于 6.5,另一个检查震级是否小于或等于 7.5——然后用 & 运算符将它们结合起来。幸运的是,pandas 使得创建这种类型的掩码变得更容易,它提供了 between() 方法:

>>> df.loc[
...     df.mag.between(6.5, 7.5), 
...     ['alert', 'mag', 'magType', 'title', 'tsunami', 'type']
... ]

结果包含所有震级在 [6.5, 7.5] 范围内的地震——默认情况下包括两个端点,但我们可以传入 inclusive=False 来更改这一点:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.30_B16834.jpg

图 2.30 – 使用数值范围进行过滤

我们可以使用 isin() 方法创建一个布尔掩码,用于匹配某个值是否出现在值列表中。这意味着我们不必为每个可能匹配的值编写一个掩码,然后使用 | 将它们连接起来。让我们利用这一点来过滤 magType 列,这一列表示用于量化地震震级的测量方法。我们将查看使用 mwmwb 震级类型测量的地震:

>>> df.loc[
...     df.magType.isin(['mw', 'mwb']), 
...     ['alert', 'mag', 'magType', 'title', 'tsunami', 'type']
... ]

我们有两个震级采用 mwb 测量类型的地震,四个震级采用 mw 测量类型的地震:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.31_B16834.jpg

图 2.31 – 使用列表中的成员关系进行过滤

到目前为止,我们一直在基于特定的值进行过滤,但假设我们想查看最低震级和最高震级地震的所有数据。与其先找到 mag 列的最小值和最大值,再创建布尔掩码,不如让 pandas 给我们这些值出现的索引,并轻松地过滤出完整的行。我们可以分别使用 idxmin()idxmax() 来获取最小值和最大值的索引。让我们抓取最低震级和最高震级地震的行号:

>>> [df.mag.idxmin(), df.mag.idxmax()]
[2409, 5263]

我们可以使用这些索引来抓取相应的行:

>>> df.loc[
...     [df.mag.idxmin(), df.mag.idxmax()], 
...     ['alert', 'mag', 'magType', 'title', 'tsunami', 'type']
... ]

最小震级的地震发生在阿拉斯加,最大震级的地震发生在印度尼西亚,并伴随海啸。我们将在 第五章,《使用 Pandas 和 Matplotlib 可视化数据》,以及 第六章,《使用 Seaborn 绘图与自定义技术》中讨论印度尼西亚的地震:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.32_B16834.jpg

图 2.32 – 过滤以隔离包含列的最小值和最大值的行

重要说明

请注意,filter() 方法并不是像我们在本节中所做的那样根据值来过滤数据;相反,它可以根据行或列的名称来子集化数据。有关 DataFrameSeries 对象的示例,请参见笔记本。

添加和移除数据

在前面的章节中,我们经常选择列的子集,但如果某些列/行对我们不有用,我们应该直接删除它们。我们也常常根据mag列的值来选择数据;然而,如果我们创建了一个新列,用于存储布尔值以便后续选择,那么我们只需要计算一次掩码。非常少情况下,我们会遇到既不想添加也不想删除数据的情况。

在我们开始添加和删除数据之前,理解一个重要概念非常关键:虽然大多数方法会返回一个新的DataFrame对象,但有些方法是就地修改数据的。如果我们编写一个函数,传入一个数据框并修改它,那么它也会改变原始的数据框。如果我们遇到这种情况,即不想改变原始数据,而是希望返回一个已经修改过的数据副本,那么我们必须在做任何修改之前确保复制我们的数据框:

df_to_modify = df.copy()

重要提示

默认情况下,df.copy()会创建一个deep=False浅拷贝,对浅拷贝的修改会影响原数据框,反之亦然。我们通常希望使用深拷贝,因为我们可以修改深拷贝而不影响原始数据。更多信息可以参考文档:pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.copy.html

现在,让我们转向最后一个笔记本6-adding_and_removing_data.ipynb,并为本章剩余部分做准备。我们将再次使用地震数据,但这次我们只读取一部分列:

>>> import pandas as pd
>>> df = pd.read_csv(
...     'data/earthquakes.csv', 
...     usecols=[
...         'time', 'title', 'place', 'magType', 
...         'mag', 'alert', 'tsunami'
...     ]
... )

创建新数据

创建新列可以通过与变量赋值相同的方式来实现。例如,我们可以创建一列来表示数据的来源;由于我们所有的数据都来自同一来源,我们可以利用广播将这一列的每一行都设置为相同的值:

>>> df['source'] = 'USGS API'
>>> df.head()

新列被创建在原始列的右侧,并且每一行的值都是USGS API

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.33_B16834.jpg

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.33_B16834.jpg

图 2.33 – 添加新列

重要提示

我们不能通过属性符号(df.source)创建新列,因为数据框还没有这个属性,因此必须使用字典符号(df['source'])。

我们不仅仅限于将一个值广播到整列;我们可以让这一列存储布尔逻辑结果或数学公式。例如,如果我们有关于距离和时间的数据,我们可以创建一列速度,它是通过将距离列除以时间列得到的结果。在我们的地震数据中,我们可以创建一列,告诉我们地震的震级是否为负数:

>>> df['mag_negative'] = df.mag < 0
>>> df.head()

请注意,新列已添加到右侧:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.34_B16834.jpg

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.33_B16834.jpg

图 2.34 – 在新列中存储布尔掩码

在前一部分中,我们看到place列存在一些数据一致性问题——同一个实体有多个名称。在某些情况下,加利福尼亚的地震标记为CA,而在其他情况下标记为California。不言而喻,这会引起混淆,如果我们没有仔细检查数据,可能会导致问题。例如,仅选择CA时,我们错过了 124 个标记为California的地震。这并不是唯一存在问题的地方(NevadaNV也都有)。通过使用正则表达式提取place列中逗号后的所有内容,我们可以亲眼看到一些问题:

>>> df.place.str.extract(r', (.*$)')[0].sort_values().unique()
array(['Afghanistan', 'Alaska', 'Argentina', 'Arizona',
       'Arkansas', 'Australia', 'Azerbaijan', 'B.C., MX',
       'Barbuda', 'Bolivia', ..., 'CA', 'California', 'Canada',
       'Chile', ..., 'East Timor', 'Ecuador', 'Ecuador region',
       ..., 'Mexico', 'Missouri', 'Montana', 'NV', 'Nevada', 
       ..., 'Yemen', nan], dtype=object)

如果我们想将国家及其附近的任何地方视为一个整体实体,我们还需要做一些额外的工作(参见EcuadorEcuador region)。此外,我们通过查看逗号后面的信息来解析位置的简单尝试显然失败了;这是因为在某些情况下,我们并没有逗号。我们需要改变解析的方式。

这是一个df.place.unique(),我们可以简单地查看并推断如何正确地匹配这些名称。然后,我们可以使用replace()方法根据需要替换place列中的模式:

>>> df['parsed_place'] = df.place.str.replace(
...     r'.* of ', '', regex=True # remove <x> of <x> 
... ).str.replace(
...     'the ', '' # remove "the "
... ).str.replace(
...     r'CA$', 'California', regex=True # fix California
... ).str.replace(
...     r'NV$', 'Nevada', regex=True # fix Nevada
... ).str.replace(
...     r'MX$', 'Mexico', regex=True # fix Mexico
... ).str.replace(
...     r' region$', '', regex=True # fix " region" endings
... ).str.replace(
...     'northern ', '' # remove "northern "
... ).str.replace(
...     'Fiji Islands', 'Fiji' # line up the Fiji places
... ).str.replace( # remove anything else extraneous from start 
...     r'^.*, ', '', regex=True 
... ).str.strip() # remove any extra spaces

现在,我们可以检查剩下的解析地点。请注意,关于South Georgia and South Sandwich IslandsSouth Sandwich Islands,可能还有更多需要修正的地方。我们可以通过另一次调用replace()来解决这个问题;然而,这表明实体识别确实可能相当具有挑战性:

>>> df.parsed_place.sort_values().unique()
array([..., 'California', 'Canada', 'Carlsberg Ridge', ...,
       'Dominican Republic', 'East Timor', 'Ecuador',
       'El Salvador', 'Fiji', 'Greece', ...,
       'Mexico', 'Mid-Indian Ridge', 'Missouri', 'Montana',
       'Nevada', 'New Caledonia', ...,
       'South Georgia and South Sandwich Islands', 
       'South Sandwich Islands', ..., 'Yemen'], dtype=object)

重要提示

在实践中,实体识别可能是一个极其困难的问题,我们可能会尝试使用自然语言处理NLP)算法来帮助我们。虽然这超出了本书的范围,但可以在 https://www.kdnuggets.com/2018/12/introduction-named-entity-recognition.html 上找到更多信息。

Pandas 还提供了一种通过一次方法调用创建多个新列的方式。使用assign()方法,参数是我们想要创建(或覆盖)的列名,而值是这些列的数据。我们将创建两个新列;一个列将告诉我们地震是否发生在加利福尼亚,另一个列将告诉我们地震是否发生在阿拉斯加。我们不仅仅展示前五行(这些地震都发生在加利福尼亚),我们将使用sample()随机选择五行:

>>> df.assign(
...     in_ca=df.parsed_place.str.endswith('California'), 
...     in_alaska=df.parsed_place.str.endswith('Alaska')
... ).sample(5, random_state=0)

请注意,assign()并不会改变我们的原始数据框;相反,它返回一个包含新列的DataFrame对象。如果我们想用这个新的数据框替换原来的数据框,我们只需使用变量赋值将assign()的结果存储在df中(例如,df = df.assign(...)):

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.35_B16834.jpg

图 2.35 – 一次创建多个新列

assign() 方法也接受 assign(),它会将数据框传递到 lambda 函数作为 x,然后我们可以在这里进行操作。这使得我们可以利用在 assign() 中创建的列来计算其他列。例如,让我们再次创建 in_cain_alaska 列,这次还会创建一个新列 neither,如果 in_cain_alaska 都是 False,那么 neither 就为 True

>>> df.assign(
...     in_ca=df.parsed_place == 'California', 
...     in_alaska=df.parsed_place == 'Alaska',
...     neither=lambda x: ~x.in_ca & ~x.in_alaska
... ).sample(5, random_state=0)

记住,~ 是按位取反运算符,所以这允许我们为每一行创建一个列,其结果是 NOT in_ca AND NOT in_alaska

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.36_B16834.jpg

图 2.36 – 使用 lambda 函数一次性创建多个新列

提示

在使用 pandas 时,熟悉 lambda 函数至关重要,因为它们可以与许多功能一起使用,并且会显著提高代码的质量和可读性。在本书中,我们将看到许多可以使用 lambda 函数的场景。

现在我们已经了解了如何添加新列,让我们来看一下如何添加新行。假设我们正在处理两个不同的数据框:一个包含地震和海啸的数据,另一个则是没有海啸的地震数据:

>>> tsunami = df[df.tsunami == 1]
>>> no_tsunami = df[df.tsunami == 0]
>>> tsunami.shape, no_tsunami.shape
((61, 10), (9271, 10))

如果我们想查看所有的地震数据,我们可能需要将两个数据框合并成一个。要将行追加到数据框的底部,我们可以使用 pd.concat() 或者数据框本身的 append() 方法。concat() 函数允许我们指定操作的轴——0 表示将行追加到底部,1 表示将数据追加到最后一列的右侧,依据的是连接列表中最左边的 pandas 对象。让我们使用 pd.concat() 并保持默认的 axis=0 来处理行:

>>> pd.concat([tsunami, no_tsunami]).shape
(9332, 10) # 61 rows + 9271 rows

请注意,之前的结果等同于在数据框上运行 append() 方法。它仍然返回一个新的 DataFrame 对象,但避免了我们需要记住哪个轴是哪个,因为 append() 实际上是 concat() 函数的一个包装器:

>>> tsunami.append(no_tsunami).shape
(9332, 10) # 61 rows + 9271 rows

到目前为止,我们一直在处理 CSV 文件中的部分列,但假设我们现在想处理读取数据时忽略的一些列。由于我们已经在这个笔记本中添加了新列,所以我们不想重新读取文件并再次执行这些操作。相反,我们将沿列方向(axis=1)进行合并,添加回我们缺失的内容:

>>> additional_columns = pd.read_csv(
...     'data/earthquakes.csv', usecols=['tz', 'felt', 'ids']
... )
>>> pd.concat([df.head(2), additional_columns.head(2)], axis=1)

由于数据框的索引对齐,附加的列被放置在原始列的右侧:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.37_B16834.jpg

图 2.37 – 按照匹配的索引连接列

concat()函数使用索引来确定如何连接值。如果它们不对齐,这将生成额外的行,因为pandas不知道如何对齐它们。假设我们忘记了原始 DataFrame 的索引是行号,并且我们通过将time列设置为索引来读取了其他列:

>>> additional_columns = pd.read_csv(
...     'data/earthquakes.csv',
...     usecols=['tz', 'felt', 'ids', 'time'], 
...     index_col='time'
... )
>>> pd.concat([df.head(2), additional_columns.head(2)], axis=1)

尽管额外的列包含了前两行的数据,pandas仍然为它们创建了一个新行,因为索引不匹配。在第三章使用 Pandas 进行数据清洗中,我们将看到如何重置索引和设置索引,这两种方法都可以解决这个问题:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.38_B16834.jpg

图 2.38 – 连接具有不匹配索引的列

重要提示

第四章聚合 Pandas DataFrame中,我们将讨论合并操作,这将处理一些在增加 DataFrame 列时遇到的问题。通常,我们会使用concat()append()来添加行,但会使用merge()join()来添加列。

假设我们想连接tsunamino_tsunami这两个 DataFrame,但no_tsunami DataFrame 多了一列(假设我们向其中添加了一个名为type的新列)。join参数指定了如何处理列名重叠(在底部添加时)或行名重叠(在右侧连接时)。默认情况下,这是outer,所以我们会保留所有内容;但是,如果使用inner,我们只会保留它们共有的部分:

>>> pd.concat(
...     [
...         tsunami.head(2),
...         no_tsunami.head(2).assign(type='earthquake')
...     ], 
...     join='inner'
... )

注意,no_tsunami DataFrame 中的type列没有出现,因为它在tsunami DataFrame 中不存在。不过,看看索引;这些是原始 DataFrame 的行号,在我们将其分为tsunamino_tsunami之前:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.39_B16834.jpg

图 2.39 – 添加行并仅保留共享列

如果索引没有实际意义,我们还可以传入ignore_index来获取连续的索引值:

>>> pd.concat(
...     [
...         tsunami.head(2), 
...         no_tsunami.head(2).assign(type='earthquake')
...     ],
...     join='inner', ignore_index=True
... )

现在索引是连续的,行号与原始 DataFrame 不再匹配:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.40_B16834.jpg

图 2.40 – 添加行并重置索引

确保查阅pandas文档以获取有关concat()函数和其他数据合并操作的更多信息,我们将在第四章聚合 Pandas DataFrame中讨论这些内容:http://pandas.pydata.org/pandas-docs/stable/user_guide/merging.html#concatenating-objects。

删除不需要的数据

在将数据添加到我们的数据框后,我们可以看到有删除不需要数据的需求。我们需要一种方法来撤销我们的错误并去除那些我们不打算使用的数据。和添加数据一样,我们可以使用字典语法删除不需要的列,就像从字典中删除键一样。del df['<column_name>']df.pop('<column_name>') 都可以工作,前提是确实有一个名为该列的列;否则,我们会得到一个 KeyError。这里的区别在于,虽然 del 会立即删除它,pop() 会返回我们正在删除的列。记住,这两个操作都会修改原始数据框,因此请小心使用它们。

让我们使用字典语法删除 source 列。注意,它不再出现在 df.columns 的结果中:

>>> del df['source']
>>> df.columns
Index(['alert', 'mag', 'magType', 'place', 'time', 'title', 
       'tsunami', 'mag_negative', 'parsed_place'],
      dtype='object')

注意,如果我们不确定列是否存在,应该将我们的列删除代码放在 try...except 块中:

try:
    del df['source']
except KeyError:
    pass # handle the error here

之前,我们创建了 mag_negative 列来过滤数据框;然而,我们现在不再希望将这个列包含在数据框中。我们可以使用 pop() 获取 mag_negative 列的系列,这样我们可以将它作为布尔掩码稍后使用,而不必将其保留在数据框中:

>>> mag_negative = df.pop('mag_negative')
>>> df.columns
Index(['alert', 'mag', 'magType', 'place', 'time', 'title', 
       'tsunami', 'parsed_place'],
      dtype='object')

我们现在在 mag_negative 变量中有一个布尔掩码,它曾经是 df 中的一列:

>>> mag_negative.value_counts()
False    8841
True      491
Name: mag_negative, dtype: int64

由于我们使用 pop() 移除了 mag_negative 系列而不是删除它,我们仍然可以使用它来过滤数据框:

>>> df[mag_negative].head()

这样我们就得到了具有负震级的地震数据。由于我们还调用了 head(),因此返回的是前五个这样的地震数据:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.41_B16834.jpg

图 2.41 – 使用弹出的列作为布尔掩码

DataFrame 对象有一个 drop() 方法,用于删除多行或多列,可以原地操作(覆盖原始数据框而不需要重新赋值)或返回一个新的 DataFrame 对象。要删除行,我们传入索引列表。让我们删除前两行:

>>> df.drop([0, 1]).head(2)

请注意,索引从 2 开始,因为我们删除了 01

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.42_B16834.jpg

图 2.42 – 删除特定的行

默认情况下,drop() 假设我们要删除的是行(axis=0)。如果我们想删除列,我们可以传入 axis=1,或者使用 columns 参数指定我们要删除的列名列表。让我们再删除一些列:

>>> cols_to_drop = [
...     col for col in df.columns
...     if col not in [
...         'alert', 'mag', 'title', 'time', 'tsunami'
...     ]
... ]
>>> df.drop(columns=cols_to_drop).head()

这会删除所有不在我们想保留的列表中的列:

https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/hsn-da-pd-2e/img/Figure_2.43_B16834.jpg

图 2.43 – 删除特定的列

无论我们决定将 axis=1 传递给 drop() 还是使用 columns 参数,我们的结果都是等效的:

>>> df.drop(columns=cols_to_drop).equals(
...     df.drop(cols_to_drop, axis=1)
... )
True

默认情况下,drop() 会返回一个新的 DataFrame 对象;然而,如果我们确实想从原始数据框中删除数据,我们可以传入 inplace=True,这将避免我们需要将结果重新赋值回数据框。结果与 图 2.43 中的相同:

>>> df.drop(columns=cols_to_drop, inplace=True)
>>> df.head()

使用就地操作时要始终小心。在某些情况下,可能可以撤销它们;然而,在其他情况下,可能需要从头开始并重新创建DataFrame

总结

在本章中,我们学习了如何使用pandas进行数据分析中的数据收集部分,并使用统计数据描述我们的数据,这将在得出结论阶段时派上用场。我们学习了pandas库的主要数据结构,以及我们可以对其执行的一些操作。接下来,我们学习了如何从多种来源创建DataFrame对象,包括平面文件和 API 请求。通过使用地震数据,我们讨论了如何总结我们的数据并从中计算统计数据。随后,我们讲解了如何通过选择、切片、索引和过滤来提取数据子集。最后,我们练习了如何添加和删除DataFrame中的列和行。

这些任务也是我们pandas工作流的核心,并为接下来几章关于数据清理、聚合和数据可视化的新主题奠定了基础。请确保在继续之前完成下一节提供的练习。

练习

使用data/parsed.csv文件和本章的材料,完成以下练习以练习你的pandas技能:

  1. 使用mb震级类型计算日本地震的 95 百分位数。

  2. 找出印度尼西亚与海啸相关的地震百分比。

  3. 计算内华达州地震的汇总统计。

  4. 添加一列,指示地震是否发生在环太平洋火山带上的国家或美国州。使用阿拉斯加、南极洲(查找 Antarctic)、玻利维亚、加利福尼亚、加拿大、智利、哥斯达黎加、厄瓜多尔、斐济、危地马拉、印度尼西亚、日本、克麦得岛、墨西哥(注意不要选择新墨西哥州)、新西兰、秘鲁、菲律宾、俄罗斯、台湾、汤加和华盛顿。

  5. 计算环太平洋火山带内外地震的数量。

  6. 计算环太平洋火山带上的海啸数量。

进一步阅读

具有 R 和/或 SQL 背景的人可能会发现查看pandas语法的比较会有所帮助:

  • 与 R / R 库的比较: https://pandas.pydata.org/pandas-docs/stable/getting_started/comparison/comparison_with_r.html

  • 与 SQL 的比较: https://pandas.pydata.org/pandas-docs/stable/comparison_with_sql.html

  • SQL 查询: https://pandas.pydata.org/pandas-docs/stable/getting_started/comparison/comparison_with_sql.html

以下是一些关于处理序列化数据的资源:

  • Python 中的 Pickle: 对象序列化: https://www.datacamp.com/community/tutorials/pickle-python-tutorial

  • 将 RData/RDS 文件读取到 pandas.DataFrame 对象中(pyreader): https://github.com/ofajardo/pyreadr

以下是一些关于使用 API 的附加资源:

  • requests 包文档: https://requests.readthedocs.io/en/master/

  • HTTP 方法: https://restfulapi.net/http-methods/

  • HTTP 状态码: https://restfulapi.net/http-status-codes/

要了解更多关于正则表达式的知识,请参考以下资源:

  • 《精通 Python 正则表达式》 作者:Félix López, Víctor Romero: https://www.packtpub.com/application-development/mastering-python-regular-expressions

  • 正则表达式教程 — 学习如何使用正则表达式: https://www.regular-expressions.info/tutorial.html

内容概要:本文介绍了种基于带通滤波后倒谱预白化技术的轴承故障检测方法,特别适用于变速工况下故障特征提取困难的问题。该方法通过对振动信号进行带通滤波,抑制噪声干扰,再利用倒谱预白化消除调制效应,提升周期性冲击特征的可辨识度,最后通过平方包络谱分析有效识别轴承故障频率。文中提供了完整的Matlab代码实现,便于读者复现算法并应用于实际故障诊断场景。该技术对于早期微弱故障信号的检测具有较强敏感性,能够显著提高变速条件下轴承故障诊断的准确性。; 适合人群:具备定信号处理基础,从事机械故障诊断、工业设备状态监测等相关领域的研究生、科研人员及工程技术人员。; 使用场景及目标:①解决变速工况下传统包络谱分析易受频率混叠和噪声干扰导致故障特征难以识别的问题;②实现对轴承早期故障微弱冲击信号的有效提取与增强;③为旋转机【轴承故障检测】【借助倒谱预白化技术在变速条件下诊断轴承故障的应用】带通滤波后的倒谱预白化的平方包络谱用于轴承故障检测(Matlab代码实现)械的智能运维与预测性维护提供技术支持。; 阅读建议:建议结合Matlab代码逐行理解算法流程,重点关注带通滤波器设计、倒谱预白化处理步骤及平方包络谱的生成过程,同时推荐使用公开数据集(如CWRU)进行验证与对比实验,以深入掌握方法优势与适用边界。
内容概要:本文系统介绍了嵌入式RTOS与Linux系统开发的核心技术,涵盖RTOS的任务管理、优先级设计、同步机制(信号量、互斥锁)、通信机制(消息队列、事件标志组)和定时器管理,以及嵌入式Linux的内核裁剪、设备驱动开发、文件系统与存储管理、网络协议栈应用和多线程开发。文章还探讨了RTOS与Linux混合开发场景下的跨系统通信、任务同步与资源调度策略,并强调了调试与性能优化的重要性,介绍了RTOS和Linux各自的调试工具与方法,旨在帮助开发者构建高性能、高可靠性的嵌入式系统。; 适合人群:具备嵌入式系统基础知识,从事工业控制、物联网、智能设备开发的1-5年经验的软硬件研发工程师;对实时系统与复杂功能集成有需求的技术人员。; 使用场景及目标:①在实时性要求高的场景中合理运用RTOS实现精准任务调度与低延迟响应;②在功能复杂的嵌入式设备中使用Linux进行驱动开发、网络通信与系统扩展;③在双系统架构中实现RTOS与Linux间的高效协同与数据交互;④通过性能分析工具优化系统响应、降低功耗与提升稳定性。; 阅读建议:建议结合实际项目背景阅读,重点关注任务划分、优先级设计、跨系统通信机制及调试工具的使用,配合实验平台动手实践各类驱动、通信接口与性能调优操作,深入理解理论与工程落地之间的联系。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值