Python-数据分析入门指南-全-

Python 数据分析入门指南(全)

原文:Get Started with Python Data Analysis

协议:CC BY-NC-SA 4.0

一、数据分析和库简介

数据是原始的信息,可以以任何形式存在,可用或不可用。我们可以很容易地在生活中的任何地方获得数据;例如,在撰写本文的当天,黄金价格为每盎司 1.158 美元。这没有任何意义,除了描述黄金的价格。这也表明基于上下文的数据是有用的。

有了关系数据连接,信息就出现了,并允许我们将知识扩展到感官范围之外。当我们拥有随着时间收集的黄金价格数据时,我们可能拥有的一条信息是,价格在三天内从 1.152 美元持续上涨到 1.158 美元。追踪黄金价格的人可以使用这个。

知识帮助人们在生活和工作中创造价值。该值基于组织、合成或总结的信息,以增强理解、意识或理解。它代表行动和决策的状态或潜力。当黄金价格连续三天上涨时,第二天可能会下降;这是有用的知识。

下图说明了从数据到知识的步骤;我们称这个过程为数据分析过程,我们将在下一节介绍它:

Introducing Data Analysis and Libraries

在本章中,我们将涵盖以下主题:

  • 数据分析和处理
  • 使用不同编程语言的数据分析中的库概述
  • 常见的 Python 数据分析库

数据分析和处理

数据每天都在变得越来越大,越来越多样化。因此,分析和处理数据以推进人类知识或创造价值是一大挑战。为了应对这些挑战,你将需要领域知识和各种技能,从计算机科学、人工智能 ( AI ) 和 机器学习 ( ML )、统计和数学以及知识领域等领域汲取知识,如下图所示:

Data analysis and processing

让我们来看看数据分析及其领域知识:

  • 计算机科学:我们需要这些知识来为高效的数据处理提供抽象。接下来的章节需要基本的 Python 编程经验。我们将介绍用于数据分析的 Python 库。
  • 人工智能与机器学习:如果说计算机科学知识帮助我们对数据分析工具进行编程,那么人工智能与机器学习则帮助我们对数据进行建模并从中学习,以构建智能产品。
  • 统计和数学:如果我们不使用统计技术或数学函数,我们就无法从原始数据中提取有用的信息。
  • 知识领域:除了技术和通用技术之外,洞察具体领域也很重要。数据字段是什么意思?我们需要收集哪些数据?基于专业知识,我们通过应用上述技术,一步一步地探索和分析原始数据。

数据分析是由以下步骤组成的过程:

  • 数据需求:我们有根据需求或者问题分析来定义收集什么样的数据。例如,如果我们想检测用户在互联网上阅读新闻时的行为,我们应该知道访问的文章链接、日期和时间、文章类别以及用户在不同页面上花费的时间。
  • 数据采集:数据可以从各种来源采集:手机、个人电脑、摄像头或录音设备。它也可以通过不同的方式获得:通信、事件以及人与人、人与设备或设备与设备之间的交互。数据随时随地出现在世界上。问题是我们如何找到并收集它来解决我们的问题?这就是这一步的使命。
  • 数据处理:最初获得的数据必须进行处理或组织分析。这个过程对性能很敏感。我们创建、插入、更新或查询数据的速度有多快?当构建一个必须处理大数据的真实产品时,我们应该仔细考虑这一步。我们应该使用什么样的数据库来存储数据?什么样的数据结构,如分析、统计或可视化,适合我们的目的?
  • 数据清理:经过处理整理后,数据仍可能存在重复或错误。因此,我们需要一个清洁步骤来减少这些情况,并提高以下步骤的结果质量。常见任务包括记录匹配、重复数据消除和列分段。根据数据的类型,我们可以应用几种类型的数据清理。例如,用户访问新闻网站的历史记录可能包含许多重复的行,因为用户可能已经多次刷新了某些页面。对于我们的具体问题,当我们探究用户的行为时,这些行可能没有任何意义,因此我们应该在将它们保存到数据库之前将其删除。我们可能遇到的另一种情况是新闻上的点击欺诈——有人只想提高自己的网站排名或破坏网站。在这种情况下,数据不会帮助我们探索用户的行为。我们可以使用阈值来检查访问页面事件是来自真人还是来自恶意软件。
  • 探索性数据分析:现在我们可以通过各种称为探索性数据分析的技术开始分析数据。我们可能会检测到数据清理中的其他问题,或者发现对进一步数据的请求。因此,在整个数据分析过程中,这些步骤可能是重复的。数据可视化技术也用于检查图形或图表中的数据。可视化通常有助于理解数据集,尤其是当数据集很大或很高维的时候。
  • 建模和算法:大量的数学公式和算法可以应用于从原始数据中检测或预测有用的知识。例如,我们可以使用相似性度量来聚集那些表现出相似新闻阅读行为的用户,并在下次推荐他们感兴趣的文章。或者,我们可以通过应用分类模型,如支持向量机 ( SVM )或线性回归,基于用户的新闻阅读行为来检测用户的性别。根据问题的不同,我们可能会使用不同的算法来获得可接受的结果。评估算法的准确性并为某个产品选择最佳的算法来实现可能需要很多时间。
  • 数据产品:这一步的目标是构建接收数据输入并根据问题需求生成输出的数据产品。我们将应用计算机科学知识来实现我们选择的算法以及管理数据存储。

数据分析中的库概述

有无数数据分析库帮助我们处理和分析数据。它们使用不同的编程语言,在解决各种数据分析问题时有不同的优缺点。现在,我们将介绍一些可能对您有用的常见库。他们应该给你一个该领域图书馆的概述。然而,本书的其余部分侧重于基于 Python 的库。

使用 Java 语言进行数据分析的一些库如下:

  • Weka :这是我第一次了解数据分析的熟悉的图书馆。它有一个图形用户界面,允许你在一个小数据集上运行实验。如果您想了解数据处理领域的可能性,这非常好。然而,如果你构建一个复杂的产品,我认为它不是最好的选择,因为它的性能,粗略的应用编程接口设计,非最佳算法,以及很少的文档(http://www.cs.waikato.ac.nz/ml/weka/)。
  • Mallet :这是的另一个 Java 库,用于统计自然语言处理、文档分类、聚类、主题建模、信息抽取以及其他关于文本的机器学习应用。Mallet 有一个名为 GRMM 的附加包,包含对一般推理、图形模型的支持,以及对具有任意图形结构的条件随机字段 ( CRF )的训练。以我的经验,库的性能和算法都比 Weka 好。然而,它只关注文本处理问题。参考页面位于http://mallet.cs.umass.edu/
  • Mahout :这个是 Apache 建立在 Hadoop 之上的机器学习框架;它的目标是建立一个可扩展的机器学习库。它看起来很有希望,但是伴随着所有的包袱和 Hadoop 的开销。主页在http://mahout.apache.org/
  • Spark :这是一个相对较新的 Apache 项目,据说比 Hadoop 快上百倍。它也是一个可扩展的库,由常见的机器学习算法和实用程序组成。开发可以用 Python 和任何 JVM 语言完成。参考页面位于https://spark.apache.org/docs/1.5.0/mllib-guide.html

下面是一些用 C++实现的库:

  • Vowpal Wabbit :这个库是一个快速的核心外学习系统,由微软研究院和之前的雅虎赞助!研究。它已用于在一小时内学习 1000 个节点上的万亿特征(1012)数据集。更多信息可在http://arxiv.org/abs/1110.4198的出版物中找到。
  • 多包:这个包是一个用 C++实现的多类、多标签、多任务分类助推软件。如果使用本软件,请参考 2012 年发表在《T4 机器学习研究》杂志上的论文、 MultiBoost:多用途助推包D.Benbouzidr . Busa-费科特N .卡萨格兰德f-D . CollinB. Kégl
  • MLpack :这是也是一个 C++机器-学习库,由佐治亚理工学院基础算法和统计工具实验室 ( FASTLab )开发。它关注可扩展性、速度和易用性,并在 NIPS 2011 的大学习研讨会上展示。其主页位于http://www.mlpack.org/about.html
  • Caffe :我们最后要提的 c++库就是 Caffe。这是一个深度学习框架,考虑了表达、速度和模块化。由 柏克莱视觉与学习中心 ( BVLC )和社区贡献者共同开发。你可以在http://caffe.berkeleyvision.org/找到更多关于它的信息。

用于数据处理和分析的其他库如下:

  • Statsmodels :这个是一个很棒的统计建模 Python 库,主要用于预测性和探索性分析。
  • 数据处理模块化工具包 ( MDP ):这个是有监督的和无监督的学习算法和其他数据处理单元的集合,可以组合成数据处理序列和更复杂的前馈网络架构(http://mdp-toolkit.sourceforge.net/index.html)。
  • Orange :这是一个面向新手和专家的开源数据可视化和分析。它充满了数据分析的功能,并有生物信息学和文本挖掘的插件。它包含一个自组织地图的实现,这使它与其他项目(http://orange.biolab.si/)有所不同。
  • Mirador :这个是一个对复杂数据集进行可视化探索的工具,支持 Mac 和 Windows。它使用户能够发现相关模式,并从数据中得出新的假设(http://orange.biolab.si/)。
  • RapidMiner :这个是另一个基于 GUI 的工具,用于数据挖掘、机器学习和预测性分析(https://rapidminer.com/)。
  • antao:这个架起了 Python 和低级语言之间的桥梁。该算法具有非常显著的性能提升,特别是对于大型矩阵运算,因此是深度学习模型的良好选择。然而,由于额外的编译层,调试并不容易。
  • 自然语言处理工具包 ( NLTK ):这个是用 Python 编写的,非常独特和突出。

这里,我无法列出所有用于数据分析的库。但是,我认为上述库足以占用您大量的时间来学习和构建数据分析应用程序。我希望你读完这本书后会喜欢它们。

数据分析中的 Python 库

Python 是一种多平台、通用的编程语言,可以在 Windows、Linux/Unix、Mac OS X 上运行,已经移植到 Java 和。NET 虚拟机。它有一个强大的标准库。此外,它还有许多用于数据分析的库:Pylearn2、Hebel、Pybrain、Pattern、MontePython 和 MILK。在本书中,我们将介绍一些常见的 Python 数据分析库,如 Numpy、Pandas、Matplotlib、PyMongo 和 scikit-learn。现在,为了帮助您入门,我将为那些不太熟悉科学 Python 堆栈的人简要介绍每个库的概述。

NumPy

Python 中用于科学计算的基础包之一是 Numpy。除其他外,它包含以下内容:

  • 一个强大的 N 维数组对象
  • 用于执行数组计算的复杂(广播)函数
  • 用于集成 C/C++和 Fortran 代码的工具
  • 有用的线性代数运算、傅立叶变换和随机数功能

除此之外,它还可以用作通用数据的高效多维容器。可以定义任意数据类型,并将其与各种数据库集成。

Pandas

Pandas 是一个 Python 包,支持丰富的数据结构和分析数据的功能,由 PyData 开发团队开发。它专注于 Python 的数据库的改进。Pandas 由以下东西组成:

  • 一组标记的数组数据结构;其中主要是系列、数据框和面板
  • 索引对象支持简单轴索引和多级/分级轴索引
  • 用于聚合和转换数据集的集成分组引擎
  • 日期范围生成和自定义日期偏移
  • 从平面文件或 PyTables/HDF5 格式加载和保存数据的输入/输出工具
  • 标准数据结构的最佳内存版本
  • 移动窗口统计和静动窗口线性/面板回归

由于的这些特性,Pandas 是需要复杂数据结构或高性能时间序列功能(如金融数据分析应用)的系统的理想工具。

Matplotlib

Matplotlib 是 2D 图形最常用的 Python 包。它提供了一种非常快速的方式来可视化来自 Python 的数据和多种格式的出版物质量图形:线图、等高线图、散点图和底图图。它带有一组默认设置,但允许自定义各种属性。然而,我们可以用 Matplotlib 中几乎每个属性的默认值轻松创建图表。

PyMongo

MongoDB 是 NoSQL 数据库的一种类型。它是高度可扩展、健壮,并且非常适合与基于 JavaScript 的 web 应用程序一起工作,因为我们可以将数据存储为 JSON 文档并使用灵活的模式。

PyMongo 是一个 Python 发行版,包含使用 MongoDB 的工具。许多工具也被编写用于与 PyMongo 一起工作,以添加更多功能,例如 MongoKit、Humongolus、MongoAlchemy 和 Ming。

科学知识文库

scikit-learn 是一个使用 Python 编程语言的开源机器学习库。它支持各种机器学习模型,如分类、回归和聚类算法,与 Python 数值和科学库 NumPy 和 SciPy 互操作。最新 scikit-learn 版本为 0.16.1,发布于 2015 年 4 月。

总结

在这一章中,我们提出了三个要点。首先,我们弄清了原始数据、信息和知识之间的关系。由于它对我们生活的贡献,我们在第二部分继续讨论数据分析和处理步骤的概述。最后,我们介绍了一些常见的受支持的库,它们对实际的数据分析应用程序很有用。其中,在接下来的章节中,我们将关注数据分析中的 Python 库。

练习练习

下表描述了用户对《白雪公主》电影的排名:

|

使用者辩证码

|

|

位置

|

等级

|
| --- | --- | --- | --- |
| A | 男性的 | 飞利浦 | four |
| B | 男性的 | 越南 | Two |
| C | 男性的 | 加拿大 | one |
| D | 男性的 | 加拿大 | Two |
| E | 女性的 | 越南 | five |
| F | 女性的 | 纽约州 | four |

练习 1 :在这个表格中我们可以找到哪些信息?我们能从中获得什么样的知识?

练习 2 :基于本章的数据分析过程,尝试定义预测用户 B 是否喜欢《玛琳菲森》电影所需的数据需求和分析步骤。

二、NumPy 数组和向量化计算

NumPy 是支持在 Python 中以高性能呈现和计算数据的基础包。它提供了如下一些有趣的特性:

  • Python 的扩展包,用于多维数组(ndarrays)、各种派生对象(如屏蔽数组)、提供向量化操作的矩阵和广播功能。通过利用现代 CPU 中的 【单指令多数据】(【SIMD】)指令集,向量化可以显著提高数组计算的性能。
  • 对数据数组进行快速便捷的操作,包括数学运算、基本统计运算、排序、选择、线性代数、随机数生成、离散傅里叶变换等。
  • 因为集成了 C/C++/Fortran 代码,所以更接近硬件的效率工具。

NumPy 是一个很好的入门包,让你熟悉数据分析中的数组和面向数组的计算。此外,这是学习其他更有效的工具(如 Pandas)的基本步骤,我们将在下一章中看到。我们将使用 NumPy 版本 1.9.1。

NumPy 数组

数组可用于包含实验或模拟步骤中数据对象的值、图像的像素或测量设备记录的信号。比如巴黎埃菲尔铁塔的纬度是 48.858598,经度是 2.294495。它可以在 NumPy 数组对象中表示为p:

>>> import numpy as np
>>> p = np.array([48.858598, 2.294495])
>>> p
array([48.858598, 2.294495])

这是一个使用np.array功能的数组的手动构造。导入 NumPy 的标准约定如下:

>>> import numpy as np

当然,您可以将from numpy import *放入代码中,以避免必须编写np。但是,由于潜在的代码冲突,您应该小心这个习惯(关于代码约定的更多信息可以在 Python 风格指南中找到,也称为 PEP8 ,位于https://www.python.org/dev/peps/pep-0008/)。

NumPy 数组有两个要求:创建时的固定大小和统一的固定数据类型,内存中的大小是固定的。以下功能帮助您获取p矩阵的信息:

>>> p.ndim    # getting dimension of array p
1
>>> p.shape   # getting size of each array dimension
(2,)
>>> len(p)    # getting dimension length of array p
2
>>> p.dtype    # getting data type of array p
dtype('float64')

数据类型

有五种基本的数值类型,包括布尔值(bool)、整数(int)、无符号整数(uint)、浮点(float)和复数。它们表示需要多少位来表示内存中的数组元素。除此之外,NumPy 还有一些类型,如intcintp,它们的位大小因平台而异。

下表列出了 NumPy 支持的数据类型:

|

类型

|

类型代码

|

描述

|

价值范围

|
| --- | --- | --- | --- |
| bool |   | 以字节形式存储的布尔值 | 真/假 |
| intc |   | 类似于 C int (int32 或 int 64) |   |
| intp |   | 用于索引的整数(与 C size_t 相同) |   |
| int8uint8 | i1,u1 | 有符号和无符号 8 位整数类型 | int8: (-128 到 127)uint8: (0 至 255) |
| int16uint16 | i2,u2 | 有符号和无符号 16 位整数类型 | int16: (-32768 到 32767)uint16: (0 至 65535) |
| int32uint32 | I4,u4 | 有符号和无符号 32 位整数类型 | int32: (-2147483648 到 2147483647uint32: (0 到 4294967295) |
| int64uinit64 | i8,u8 | 有符号和无符号 64 位整数类型 | Int64: (-9223372036854775808 到 9223372036854775807)uint64: (0 到 18446744073709551615) |
| float16 | 第二子代 | 半精度浮点:符号位、5 位指数和 10b 位尾数 |   |
| float32 | f4 / f | 单精度浮点:符号位、8 位指数和 23 位尾数 |   |
| float64 | f8 / d | 双精度浮点:符号位、11 位指数和 52 位尾数 |   |
| complex64complex128complex256 | c8、c16、c32 | 由两个 32 位、64 位和 128 位浮点数表示的复数 |   |
| object | Zero | Python 对象类型 |   |
| string_ | S | 固定长度字符串类型 | 使用S10声明长度为 10 的字符串dtype |
| unicode_ | U | 固定长度的 Unicode 类型 | 类似于 string_ example,我们有‘U10’ |

我们可以使用astype方法轻松地转换或将数组从一个dtype转换为另一个:

>>> a = np.array([1, 2, 3, 4])
>>> a.dtype
dtype('int64')
>>> float_b = a.astype(np.float64)
>>> float_b.dtype
dtype('float64')

astype函数将使用旧数组的数据副本创建一个新数组,即使新的dtype与旧的相似。

数组创建

提供了各种和功能来创建数组对象。它们对于我们在不同情况下创建和存储多维数组中的数据非常有用。

现在,在下表中,我们将通过示例总结 NumPy 的一些常见功能及其在数组创建中的使用:

|

功能

|

描述

|

例子

|
| --- | --- | --- |
| empty, empty_like | 创建给定形状和类型的新数组,不初始化元素 |

>>> np.empty([3,2], dtype=np.float64)
array([[0., 0.], [0., 0.], [0., 0.]])
>>> a = np.array([[1, 2], [4, 3]])
>>> np.empty_like(a)
array([[0, 0], [0, 0]])

|
| eyeidentity | 创建一个 NxN 单位矩阵,对角线上为 1,其他地方为零 |

>>> np.eye(2, dtype=np.int)
array([[1, 0], [0, 1]])

|
| onesones_like | 用给定的形状和类型创建一个新数组,所有元素用 1 填充 |

>>> np.ones(5)
array([1., 1., 1., 1., 1.])
>>> np.ones(4, dtype=np.int)
array([1, 1, 1, 1])
>>> x = np.array([[0,1,2], [3,4,5]])
>>> np.ones_like(x)
array([[1, 1, 1],[1, 1, 1]])

|
| zeroszeros_like | 这与onesones_like类似,但是用 0 来初始化元素 |

>>> np.zeros(5)
array([0., 0., 0., 0-, 0.])
>>> np.zeros(4, dtype=np.int)
array([0, 0, 0, 0])
>>> x = np.array([[0, 1, 2], [3, 4, 5]])
>>> np.zeros_like(x)
array([[0, 0, 0],[0, 0, 0]])

|
| arange | 创建一个在给定间隔内具有均匀间隔值的数组 |

>>> np.arange(2, 5)
array([2, 3, 4])
>>> np.arange(4, 12, 5)
array([4, 9])

|
| fullfull_like | 用给定的形状和类型创建一个新数组,并填充选定的值 |

>>> np.full((2,2), 3, dtype=np.int)
array([[3, 3], [3, 3]])
>>> x = np.ones(3)
>>> np.full_like(x, 2)
array([2., 2., 2.])

|
| array | 从现有数据创建数组 |

>>> np.array([[1.1, 2.2, 3.3], [4.4, 5.5, 6.6]])
array([1.1, 2.2, 3.3], [4.4, 5.5, 6.6]])

|
| asarray | 将输入转换为数组 |

>>> a = [3.14, 2.46]
>>> np.asarray(a)
array([3.14, 2.46])

|
| copy | 返回给定对象的数组副本 |

>>> a = np.array([[1, 2], [3, 4]])
>>> np.copy(a)
array([[1, 2], [3, 4]])

|
| fromstring | 从字符串或文本创建一维数组 |

>>> np.fromstring('3.14 2.17', dtype=np.float, sep=' ')
array([3.14, 2.17])

|

索引和切片

与列表等其他 Python 序列类型一样,很容易访问和分配每个数组元素的值:

>>> a = np.arange(7)
>>> a
array([0, 1, 2, 3, 4, 5, 6])
>>> a[1], a [4], a[-1]
(1, 4, 6)

在 Python 中,数组索引从 0 开始。这与 Fortran 或 Matlab 形成对比,后者的索引从 1 开始。

作为另一个例子,如果我们的数组是多维的,我们需要整数元组来索引一个项目:

>>> a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
>>> a[0, 2]      # first row, third column
3
>>> a[0, 2] = 10
>>> a
array([[1, 2, 10], [4, 5, 6], [7, 8, 9]])
>>> b = a[2]
>>> b
array([7, 8, 9])
>>> c = a[:2]
>>> c
array([[1, 2, 10], [4, 5, 6]])

我们称bc为阵片,是对原片的看法。这意味着数据不会复制到bc中,每当我们修改它们的值时,它也会反映在数组a中:

>>> b[-1] = 11
>>> a
array([[1, 2, 10], [4, 5, 6], [7, 8, 11]])

当我们省略索引号时,我们使用冒号(:)字符来取整个轴。

花式索引

除了用切片索引,NumPy 还支持用布尔或整数数组(掩码)索引。这个方法叫做 花式标引。它创建副本,而不是视图。

首先,我们看一个用布尔掩码数组进行索引的例子:

>>> a = np.array([3, 5, 1, 10])
>>> b = (a % 5 == 0)
>>> b
array([False, True, False, True], dtype=bool)
>>> c = np.array([[0, 1], [2, 3], [4, 5], [6, 7]])
>>> c[b]
array([[2, 3], [6, 7]])

第二个示例说明了如何在数组上使用整数掩码:

>>> a = np.array([[1, 2, 3, 4], 
 [5, 6, 7, 8], 
 [9, 10, 11, 12],
 [13, 14, 15, 16]])
>>> a[[2, 1]]
array([[9, 10, 11, 12], [5, 6, 7, 8]])
>>> a[[-2, -1]]          # select rows from the end
array([[ 9, 10, 11, 12], [13, 14, 15, 16]])
>>> a[[2, 3], [0, 1]]    # take elements at (2, 0) and (3, 1)
array([9, 14])

掩码数组的长度必须与其索引的轴的长度相同。

型式

下载示例代码

您可以从您在http://www.packtpub.com的账户中下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问http://www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

数组上的数值运算

我们正在熟悉创建和访问ndarrays。现在,我们继续下一步,对数组数据应用一些数学运算,而不写任何 for 循环,当然,性能更高。

标量操作将值传播到数组的每个元素:

>>> a = np.ones(4)
>>> a * 2
array([2., 2., 2., 2.])
>>> a + 3
array([4., 4., 4., 4.])

数组之间的所有算术运算都按元素应用运算:

>>> a = np.ones([2, 4])
>>> a * a
array([[1., 1., 1., 1.], [1., 1., 1., 1.]])
>>> a + a
array([[2., 2., 2., 2.], [2., 2., 2., 2.]])

此外,这里还有一些比较和逻辑运算的例子:

>>> a = np.array([1, 2, 3, 4])
>>> b = np.array([1, 1, 5, 3])
>>> a == b
array([True, False, False, False], dtype=bool)

>>> np.array_equal(a, b)      # array-wise comparison
False

>>> c = np.array([1, 0])
>>> d = np.array([1, 1])
>>> np.logical_and(c, d)      # logical operations
array([True, False])

数组函数

NumPy 中支持许多有用的数组函数来分析数据。我们将列出其中一些常用的部分。首先,置换函数是另一种不复制任何内容而返回原始数据数组视图的整形形式:

>>> a = np.array([[0, 5, 10], [20, 25, 30]])
>>> a.reshape(3, 2)
array([[0, 5], [10, 20], [25, 30]])
>>> a.T
array([[0, 20], [5, 25], [10, 30]])

一般来说,我们有swapaxes方法,该方法获取一对轴号并返回数据视图,而不进行复制:

>>> a = np.array([[[0, 1, 2], [3, 4, 5]], 
 [[6, 7, 8], [9, 10, 11]]])
>>> a.swapaxes(1, 2)
array([[[0, 3],
 [1, 4],
 [2, 5]],
 [[6, 9],
 [7, 10],
 [8, 11]]])

置换函数用于进行矩阵计算;例如,使用np.dot计算内矩阵乘积XT.X:

>>> a = np.array([[1, 2, 3],[4,5,6]])
>>> np.dot(a.T, a)
array([[17, 22, 27],
 [22, 29, 36],
 [27, 36, 45]])

对数组中的数据进行排序也是处理数据的一个重要需求。让我们来看看一些排序功能及其用途:

>>> a = np.array ([[6, 34, 1, 6], [0, 5, 2, -1]])

>>> np.sort(a)     # sort along the last axis
array([[1, 6, 6, 34], [-1, 0, 2, 5]])

>>> np.sort(a, axis=0)    # sort along the first axis
array([[0, 5, 1, -1], [6, 34, 2, 6]])

>>> b = np.argsort(a)    # fancy indexing of sorted array
>>> b
array([[2, 0, 3, 1], [3, 0, 2, 1]])
>>> a[0][b[0]]
array([1, 6, 6, 34])

>>> np.argmax(a)    # get index of maximum element
1

数组函数列表见下表:

|

功能

|

描述

|

例子

|
| --- | --- | --- |
| sincostancoshsinhtanharcosarctandeg2rad | 三角函数和双曲函数 |

>>> a = np.array([0.,30., 45.])
>>> np.sin(a * np.pi / 180)
array([0., 0.5, 0.7071678])

|
| aroundroundrintfixfloorceiltrunc | 将数组的元素舍入到给定或最接近的数字 |

>>> a = np.array([0.34, 1.65])
>>> np.round(a)
array([0., 2.])

|
| sqrtsquareexpexpm1exp2loglog10log1plogaddexp | 计算数组的指数和对数 |

>>> np.exp(np.array([2.25, 3.16]))
array([9.4877, 23.5705])

|
| addnegativemultiplydevidepowersubstractmodmodfremainder | 数组上的一组算术函数 |

>>> a = np.arange(6)
>>> x1 = a.reshape(2,3)
>>> x2 = np.arange(3)
>>> np.multiply(x1, x2)
array([[0,1,4],[0,4,10]])

|
| greatergreater_equallessless_equalequalnot_equal | 执行元素比较: >,> =, |

>>> np.greater(x1, x2)
array([[False, False, False], [True, True, True]], dtype = bool)

|

使用数组进行数据处理

借助 NumPy 包,我们可以轻松解决多种数据处理任务,而无需编写复杂的循环。这对我们控制代码以及程序的性能非常有帮助。在这一部分,我们想介绍一些数学和统计函数。

有关数学和统计函数的列表,请参见下表:

|

功能

|

描述

|

例子

|
| --- | --- | --- |
| sum | 计算数组中或沿轴的所有元素的总和 |

>>> a = np.array([[2,4], [3,5]])
>>> np.sum(a, axis=0)
array([5, 9])

|
| prod | 计算给定轴上数组元素的乘积 |

>>> np.prod(a, axis=1)
array([8, 15])

|
| diff | 计算沿给定轴的离散差值 |

>>> np.diff(a, axis=0)
array([[1,1]])

|
| gradient | 返回数组的梯度 |

>>> np.gradient(a)
[array([[1., 1.], [1., 1.]]), array([[2., 2.], [2., 2.]])]

|
| cross | 返回两个数组的叉积 |

>>> b = np.array([[1,2], [3,4]])
>>> np.cross(a,b)
array([0, -3])

|
| stdvar | 返回数组的标准差和方差 |

>>> np.std(a)
1.1180339
>>> np.var(a)
1.25

|
| mean | 计算数组的算术平均值 |

>>> np.mean(a)
3.5

|
| where | 从 x 或 y 返回满足条件的元素 |

>>> np.where([[True, True], [False, True]], [[1,2],[3,4]], [[5,6],[7,8]])
array([[1,2], [7, 4]])

|
| unique | 返回数组中已排序的唯一值 |

>>> id = np.array(['a', 'b', 'c', 'c', 'd'])
>>> np.unique(id)
array(['a', 'b', 'c', 'd'], dtype='|S1')

|
| intersect1d | 计算两个数组中已排序的公共元素 |

>>> a = np.array(['a', 'b', 'a', 'c', 'd', 'c'])
>>> b = np.array(['a', 'xyz', 'klm', 'd'])
>>> np.intersect1d(a,b)
array(['a', 'd'], dtype='|S3')

|

加载和保存数据

我们还可以通过使用 NumPy 包中支持的不同功能,以文本或二进制格式将数据保存到磁盘上或从磁盘上加载数据。

保存数组

默认情况下,数组是以未压缩的原始二进制格式保存的,文件扩展名由np.save函数指定:

>>> a = np.array([[0, 1, 2], [3, 4, 5]])
>>> np.save('test1.npy', a)

如果我们省略了.npy扩展名,库会自动分配它。

如果我们想以未压缩的.npz格式将多个数组存储到一个文件中,我们可以使用np.savez函数,如下例所示:

>>> a = np.arange(4)
>>> b = np.arange(7)
>>> np.savez('test2.npz', arr0=a, arr1=b)

.npz文件是一个压缩的文件档案,以它们包含的变量命名。当我们加载一个.npz文件时,我们得到一个类似字典的对象,可以查询它的数组列表:

>>> dic = np.load('test2.npz')
>>> dic['arr0']
array([0, 1, 2, 3])

将数组数据保存到文件中的另一种方法是使用np.savetxt功能,该功能允许我们在输出文件中设置格式属性:

>>> x = np.arange(4)
>>> # e.g., set comma as separator between elements
>>> np.savetxt('test3.out', x, delimiter=',')

加载数组

我们有两个常用函数如 np.loadnp.loadtxt,对应保存函数,用于加载数组:

>>> np.load('test1.npy')
array([[0, 1, 2], [3, 4, 5]])
>>> np.loadtxt('test3.out', delimiter=',')
array([0., 1., 2., 3.])

与的np.savetxt功能类似,np.loadtxt功能也有很多从文本文件加载数组的选项。

带 NumPy 的线性代数

线性代数是数学的一个分支,涉及向量空间和这些空间之间的映射。NumPy 有一个名为 linalg 的软件包,支持强大的线性代数功能。我们可以使用这些函数来寻找特征值和特征向量或者执行奇异值分解:

>>> A = np.array([[1, 4, 6],
 [5, 2, 2],
 [-1, 6, 8]])
>>> w, v = np.linalg.eig(A)
>>> w                           # eigenvalues
array([-0.111 + 1.5756j, -0.111 – 1.5756j, 11.222+0.j])
>>> v                           # eigenvector
array([[-0.0981 + 0.2726j, -0.0981 – 0.2726j, 0.5764+0.j],
 [0.7683+0.j, 0.7683-0.j, 0.4591+0.j],
 [-0.5656 – 0.0762j, -0.5656 + 0.00763j, 0.6759+0.j]])

该函数使用 geev Lapack 例程实现,该例程计算一般方阵的特征值和特征向量。

另一个常见的问题是求解线性系统,如以A为矩阵,以xb为向量的Ax = b。使用numpy.linalg.solve功能可以轻松解决问题:

>>> A = np.array([[1, 4, 6], [5, 2, 2], [-1, 6, 8]])
>>> b = np.array([[1], [2], [3]])
>>> x = np.linalg.solve(A, b)
>>> x
array([[-1.77635e-16], [2.5], [-1.5]])

下表将总结numpy.linalg包中的一些常用功能:

|

功能

|

描述

|

例子

|
| --- | --- | --- |
| dot | 计算两个数组的点积 |

>>> a = np.array([[1, 0],[0, 1]])
>>> b = np.array( [[4, 1],[2, 2]])
>>> np.dot(a,b)
array([[4, 1],[2, 2]])

|
| innerouter | 计算两个数组的内积和外积 |

>>> a = np.array([1, 1, 1])
>>> b = np.array([3, 5, 1])
>>> np.inner(a,b)
9

|
| linalg.norm | 求矩阵或向量范数 |

>>> a = np.arange(3)
>>> np.linalg.norm(a)
2.23606

|
| linalg.det | 计算一个数组的行列式 |

>>> a = np.array([[1,2],[3,4]])
>>> np.linalg.det(a)
-2.0

|
| linalg.inv | 计算矩阵的逆矩阵 |

>>> a = np.array([[1,2],[3,4]])
>>> np.linalg.inv(a)
array([[-2., 1.],[1.5, -0.5]])

|
| linalg.qr | 计算二维码分解 |

>>> a = np.array([[1,2],[3,4]])
>>> np.linalg.qr(a)
(array([[0.316, 0.948], [0.948, 0.316]]), array([[ 3.162, 4.427], [ 0., 0.632]]))

|
| linalg.cond | 计算矩阵的条件数 |

>>> a = np.array([[1,3],[2,4]])
>>> np.linalg.cond(a)
14.933034

|
| trace | 计算对角线元素的总和 |

>>> np.trace(np.arange(6)).
reshape(2,3))
4

|

NumPy 随机数

任何模拟的一个重要部分是生成随机数的能力。为此,NumPy 在子模块random中提供了各种例程。它使用一种特殊的算法,叫做默森扭转器,来产生伪随机数。

首先,我们需要定义一个种子,使随机数可以预测。重置该值时,每次都会出现相同的数字。如果我们不分配种子,NumPy 会根据系统的随机数发生器设备或时钟自动选择随机种子值:

>>> np.random.seed(20)

[0.0, 1.0]区间的随机数数组可以如下生成:

>>> np.random.rand(5)
array([0.5881308, 0.89771373, 0.89153073, 0.81583748, 
 0.03588959])
>>> np.random.rand(5)
array([0.69175758, 0.37868094, 0.51851095, 0.65795147, 
 0.19385022])

>>> np.random.seed(20)    # reset seed number
>>> np.random.rand(5)
array([0.5881308, 0.89771373, 0.89153073, 0.81583748, 
 0.03588959])

如果想在半开区间[min, max]生成随机整数,可以使用randint ( minmaxlength)功能:

>>> np.random.randint(10, 20, 5)
array([17, 12, 10, 16, 18])

NumPy 还提供了许多其他发行版,包括Betabionomialchi - squareDirichletexponentialFGammageometricGumbel

下表将列出一些分布函数,并给出生成随机数的示例:

|

功能

|

描述

|

例子

|
| --- | --- | --- |
| binomial | 从二项分布中抽取样本(n:试验次数,p:概率) |

>>> n, p = 100, 0.2
>>> np.random.binomial(n, p, 3)
array([17, 14, 23])

|
| dirichlet | 使用狄利克雷分布绘制样本 |

>>> np.random.dirichlet(alpha=(2,3), size=3)
array([[0.519, 0.480], [0.639, 0.36],
 [0.838, 0.161]])

|
| poisson | 从泊松分布中抽取样本 |

>>> np.random.poisson(lam=2, size= 2)
array([4,1])

|
| normal | 使用正态高斯分布绘制样本 |

>>> np.random.normal
(loc=2.5, scale=0.3, size=3)
array([2.4436, 2.849, 2.741)

|
| uniform | 使用均匀分布绘制样本 |

>>> np.random.uniform(
low=0.5, high=2.5, size=3)
array([1.38, 1.04, 2.19[)

|

我们还可以使用随机数生成来打乱列表中的项目。有时,当我们想要以随机顺序对列表进行排序时,这很有用:

>>> a = np.arange(10)
>>> np.random.shuffle(a)
>>> a
array([7, 6, 3, 1, 4, 2, 5, 0, 9, 8])

下图显示了两个分布binomialpoisson,并列显示了各种参数(可视化是用matplotlib创建的,将在第 4 章数据可视化中介绍):

NumPy random numbers

总结

在这一章中,我们介绍了很多与 NumPy 包相关的信息,尤其是常用的对ndarray中的数据处理和分析非常有帮助的函数。首先,我们学习了 NumPy 包中ndarray的属性和数据类型。其次,我们关注如何以不同的方式创建和操作ndarray,例如从其他结构转换,从磁盘读取数组,或者仅仅生成具有给定值的新数组。再次,我们研究了如何通过索引和切片来访问和控制ndarray中每个元素的值。

然后,我们开始熟悉ndarray上的一些常用功能和操作。

最后,我们继续讨论一些与统计、线性代数和采样数据相关的高级函数。这些函数在数据分析中起着重要的作用。

然而,尽管 NumPy 本身并不提供非常高级的数据分析功能,但了解它将帮助您更有效地使用 Pandas 等工具。这个工具将在下一章讨论。

练习练习

练习 1 :使用数组创建函数,让我们尝试在以下情况下创建数组变量:

  • 从现有数据创建ndarray
  • 初始化ndarray哪些元素用 1、0 或给定的间隔填充
  • 将数据从文件加载并保存到ndarray

练习 2:np.dot(a, b)(a*b)有什么区别?

练习 3 :考虑向量[1,2,3,4,5]构建一个新向量,在每个值之间插入四个连续的零。

练习 4 :以包含系统日志信息的数据示例文件chapter2-data.txt为例,解决以下任务:

  • 尝试从数据文件构建一个ndarray
  • 构建矩阵中每种设备类型的统计频率
  • 列出数据日志中出现的唯一操作系统
  • provinceID排序用户,统计各省用户数

三、Pandas 数据分析

在本章中,我们将探索另一个名为 Pandas 的数据分析库。本章的目标是给你一些基本知识和 Pandas 入门的具体例子。

Pandas 套餐概述

Pandas 是一个 Python 包,支持快速、灵活和富有表现力的数据结构,以及用于数据分析的计算功能。以下是 Pandas 支持的一些突出功能:

  • 带有标记轴的数据结构。这使得程序清晰明了,避免了因数据不一致而导致的常见错误。
  • 对缺失数据的灵活处理。
  • 大型数据集的智能基于标签的切片、花式索引和子集创建。
  • 通过轴标签在自定义轴上进行强大的算术运算和统计计算。
  • 对从文件、数据库或 HDF5 格式加载数据或将数据保存到文件、数据库或 HDF5 格式的强大输入和输出支持。

与 Pandas 安装相关,我们推荐一个简单的方法,那就是将其安装为 Anaconda 的一部分,Anaconda 是一个用于数据分析和科学计算的跨平台分发。可以参考http://docs.continuum.io/anaconda/的参考资料下载安装库。

安装后,我们可以像其他 Python 包一样使用它。首先,我们必须在程序开始时导入以下包:

>>> import pandas as pd
>>> import numpy as np

Pandas 的数据结构

让我们首先了解 Pandas 的两个主要数据结构:系列和数据框架。他们可以处理金融、统计、社会科学和许多工程领域的大多数用例。

系列

A 系列是类似于表格中的数组、列表或列的一维对象。系列中的每个项目都分配给索引中的一个条目:

>>> s1 = pd.Series(np.random.rand(4),
 index=['a', 'b', 'c', 'd'])
>>> s1
a    0.6122
b    0.98096
c    0.3350
d    0.7221
dtype: float64

默认情况下,如果没有通过索引,它将被创建为具有范围从0N-1的值,其中N是序列的长度:

>>> s2 = pd.Series(np.random.rand(4))
>>> s2
0    0.6913
1    0.8487
2    0.8627
3    0.7286
dtype: float64

我们可以通过使用索引来访问系列的值:

>>> s1['c']
0.3350
>>>s1['c'] = 3.14
>>> s1['c', 'a', 'b']
c    3.14
a    0.6122
b    0.98096

这种访问方法类似于 Python 字典。因此,Pandas 还允许我们直接从 Python 字典中初始化 Series 对象:

>>> s3 = pd.Series({'001': 'Nam', '002': 'Mary',
 '003': 'Peter'})
>>> s3
001    Nam
002    Mary
003    Peter
dtype: object

有时,我们希望过滤或重命名从 Python 字典创建的系列的索引。在这种情况下,我们可以将选定的索引列表直接传递给初始函数,类似于上面示例中的过程。只有索引列表中存在的元素才会出现在 Series 对象中。相反,字典中缺少的索引由 Pandas 初始化为默认的NaN值:

>>> s4 = pd.Series({'001': 'Nam', '002': 'Mary',
 '003': 'Peter'}, index=[
 '002', '001', '024', '065'])
>>> s4
002    Mary
001    Nam
024    NaN
065    NaN
dtype:   object
ect

该库还支持检测缺失数据的功能:

>>> pd.isnull(s4)
002    False
001    False
024    True
065    True
dtype: bool

同样,我们也可以从标量值初始化 Series:

>>> s5 = pd.Series(2.71, index=['x', 'y'])
>>> s5
x    2.71
y    2.71
dtype: float64

系列对象也可以用 NumPy 对象初始化,如ndarray。此外,Pandas 可以在算术运算中自动对齐以不同方式索引的数据:

>>> s6 = pd.Series(np.array([2.71, 3.14]), index=['z', 'y'])
>>> s6
z    2.71
y    3.14
dtype: float64
>>> s5 + s6
x    NaN
y    5.85
z    NaN
dtype: float64

数据框

数据框是一个表格数据结构,包括一组有序的列和行。它可以被认为是一组共享一个索引(列名)的 Series 对象。有许多方法可以初始化数据框对象。首先,让我们看一下从列表字典创建数据帧的常见示例:

>>> data = {'Year': [2000, 2005, 2010, 2014],
 'Median_Age': [24.2, 26.4, 28.5, 30.3],
 'Density': [244, 256, 268, 279]}
>>> df1 = pd.DataFrame(data)
>>> df1
 Density    Median_Age    Year
0  244        24.2        2000
1  256        26.4        2005
2  268        28.5        2010
3  279        30.3        2014

默认情况下,DataFrame 构造函数将按字母顺序排列列。我们可以通过将列的属性传递给初始化函数来编辑默认顺序:

>>> df2 = pd.DataFrame(data, columns=['Year', 'Density', 
 'Median_Age'])
>>> df2
 Year    Density    Median_Age
0    2000    244        24.2
1    2005    256        26.4
2    2010    268        28.5
3    2014    279        30.3
>>> df2.index
Int64Index([0, 1, 2, 3], dtype='int64')

我们可以提供类似于系列的数据帧的索引标签:

>>> df3 = pd.DataFrame(data, columns=['Year', 'Density', 
 'Median_Age'], index=['a', 'b', 'c', 'd'])
>>> df3.index
Index([u'a', u'b', u'c', u'd'], dtype='object')

我们也可以从嵌套列表中构建一个数据框架:

>>> df4 = pd.DataFrame([
 ['Peter', 16, 'pupil', 'TN', 'M', None],
 ['Mary', 21, 'student', 'SG', 'F', None],
 ['Nam', 22, 'student', 'HN', 'M', None],
 ['Mai', 31, 'nurse', 'SG', 'F', None],
 ['John', 28, 'laywer', 'SG', 'M', None]],
columns=['name', 'age', 'career', 'province', 'sex', 'award'])

列可以像系列一样通过列名来访问,或者通过类似字典的符号来访问,或者如果列名是语法上有效的属性名,则作为属性来访问:

>>> df4.name    # or df4['name'] 
0    Peter
1    Mary
2    Nam
3    Mai
4    John
Name: name, dtype: object

要在创建的数据框中修改或追加一个新列,我们需要指定列名和值:

>>> df4['award'] = None
>>> df4
 name age   career province  sex award
0  Peter  16    pupil       TN    M  None
1    Mary  21  student       SG    F  None
2    Nam   22  student       HN  M  None
3    Mai    31    nurse        SG    F    None
4    John    28    lawer        SG    M    None

使用两种方法,可以按位置或名称检索行:

>>> df4.ix[1]
name           Mary
age              21
career      student
province         SG
sex               F
award          None
Name: 1, dtype: object

数据框对象也可以从不同的数据结构中创建,如字典列表、系列字典或记录数组。初始化 DataFrame 对象的方法类似于上面的示例。

另一个常见的情况是从文本文件等位置向数据框提供数据。在这种情况下,我们使用read_csv函数,默认情况下,该函数期望列分隔符是逗号。但是,我们可以通过使用sep参数来改变这一点:

# person.csv file
name,age,career,province,sex
Peter,16,pupil,TN,M
Mary,21,student,SG,F
Nam,22,student,HN,M
Mai,31,nurse,SG,F
John,28,lawer,SG,M
# loading person.cvs into a DataFrame
>>> df4 = pd.read_csv('person.csv')
>>> df4
 name   age   career   province  sex
0    Peter    16    pupil       TN        M
1    Mary     21    student     SG       F
2    Nam      22    student     HN       M
3    Mai      31    nurse       SG       F
4    John     28    laywer      SG       M

在读取数据文件时,我们有时希望跳过一行或一个无效值。至于 Pandas0.16.2read_csv支持超过 50 个参数来控制加载过程。一些常见的有用参数如下:

  • sep:这是列之间的分隔符。默认为逗号符号。
  • dtype:这是数据或列的数据类型。
  • header:设置行号作为列名。
  • skiprows:这将跳过文件开头要跳过的行号。
  • error_bad_lines:显示无效行(字段太多),默认情况下会导致异常,因此不会返回数据帧。如果我们将该参数的值设置为false,将跳过不良行。

此外,Pandas 还支持直接从数据库读取数据帧或向数据库写入数据帧,如 Pandas 模块中的read_framewrite_frame功能。我们将在本章后面回到这些方法。

基本的基本功能

Pandas 支持许多对操纵 Pandas 数据结构有用的基本功能。在这本书里,我们将集中探讨和分析最重要的特征。

重新标记和更改标签

重新索引是 Pandas 数据结构中的一个关键方法。它确认新的或修改的数据是否满足沿着 Pandas 对象特定轴的给定标签集。

首先,让我们查看一个系列对象的reindex示例:

>>> s2.reindex([0, 2, 'b', 3])
0    0.6913
2    0.8627
b    NaN
3    0.7286
dtype: float64

当数据对象中不存在reindexed标签时,NaN的默认值将自动分配给该位置;数据框的情况也是如此:

>>> df1.reindex(index=[0, 2, 'b', 3],
 columns=['Density', 'Year', 'Median_Age','C'])
 Density  Year  Median_Age        C
0      244  2000        24.2      NaN
2      268  2010        28.5      NaN
b      NaN   NaN         NaN      NaN
3      279  2014        30.3      NaN

我们可以通过设置fill_value参数将缺失索引情况下的NaN值更改为自定义值。让我们看看reindex函数支持的参数,如下表所示:

|

争吵

|

描述

|
| --- | --- |
| index | 这是要符合的新标签/索引。 |
| method | 这是用于填充reindexed对象中的孔的方法。默认设置是未填充间隙。pad/ffill:向前填充值backfill / bfill:向后填充值nearest:使用最接近的值来填充间隙 |
| copy | 这将返回一个新对象。默认设置为true。 |
| level | 匹配传递的多索引级别上的索引值。 |
| fill_value | 这是用于缺失值的值。默认设置为NaN。 |
| limit | 这是forwardbackward方法中要填充的最大尺寸间隙。 |

头尾

在常见的数据分析情况下,我们的数据结构对象包含很多列和大量行。因此,我们无法查看或加载对象的所有信息。Pandas 支持允许我们检查小样本的功能。默认情况下,这些函数返回五个元素,但是我们也可以设置一个自定义数字。以下示例显示了如何显示较长序列的前五行和后三行:

>>> s7 = pd.Series(np.random.rand(10000))
>>> s7.head()
0    0.631059
1    0.766085
2    0.066891
3    0.867591
4    0.339678
dtype: float64
>>> s7.tail(3)
9997    0.412178
9998    0.800711
9999    0.438344
dtype: float64

我们也可以用同样的方式对数据框对象使用这些函数。

二进制运算

首先,我们将考虑对象之间的算术运算。在不同的索引对象情况下,预期的结果将是索引对的并集。我们将不再解释这一点,因为我们在上面的部分(s5 + s6)中有一个关于它的例子。这一次,我们将展示另一个带有数据帧的示例:

>>> df5 = pd.DataFrame(np.arange(9).reshape(3,3),0
 columns=['a','b','c'])
>>> df5
 a  b  c
0  0  1  2
1  3  4  5
2  6  7  8
>>> df6 = pd.DataFrame(np.arange(8).reshape(2,4), 
 columns=['a','b','c','d'])
>>> df6
 a  b  c  d
0  0  1  2  3
1  4  5  6  7
>>> df5 + df6
 a   b   c   d
0   0   2   4 NaN
1   7   9  11 NaN
2   NaN NaN NaN NaN

两种数据结构之间返回结果的机制相似。我们需要考虑的一个问题是对象之间缺少数据。在这种情况下,如果要填充一个固定值,如0,可以使用addsubdivmul等算术函数,以及函数支持的参数如fill_value:

>>> df7 = df5.add(df6, fill_value=0)
>>> df7
 a  b   c   d
0  0  2   4   3
1  7  9  11   7
2  6  7   8   NaN

接下来,我们将讨论数据对象之间的 comparison操作。我们有一些支持的功能,比如 等于 ( eq )、不等于 ( ne )、大于 ( gt )、小于 ( lt )、小于等于 ( le )和这里就是一个例子:

>>> df5.eq(df6)
 a      b      c      d
0   True   True   True  False
1  False  False  False  False
2  False  False  False  False

功能统计

图书馆的支持的统计方法在数据分析中真的很重要。要进入大数据对象,我们需要知道一些汇总信息,如平均值、总和或分位数。Pandas 支持大量计算它们的方法。让我们考虑一个计算df5sum信息的简单例子,它是一个数据框对象:

>>> df5.sum()
a     9
b    12
c    15
dtype: int64

当我们没有指定要计算 sum信息的轴时,默认情况下,该函数会在指数轴上计算,即轴0:

  • 系列:我们不需要指定轴。
  • 数据框:列(axis = 1)或索引(axis = 0)。默认设置为axis 0

我们还有skipna参数,允许我们决定是否排除丢失的数据。默认设置为true:

>>> df7.sum(skipna=False)
a    13
b    18
c    23
d   NaN
dtype: float64

我们要考虑的另一个功能是describe()。我们也可以非常方便地总结数据结构(如系列和数据框架)的大部分统计信息:

>>> df5.describe()
 a    b    c
count  3.0  3.0  3.0
mean   3.0  4.0  5.0
std    3.0  3.0  3.0
min    0.0  1.0  2.0
25%    1.5  2.5  3.5
50%    3.0  4.0  5.0
75%    4.5  5.5  6.5
max    6.0  7.0  8.0

我们可以使用percentiles参数指定输出中包含或排除的百分位数;例如,考虑以下情况:

>>> df5.describe(percentiles=[0.5, 0.8])
 a    b    c
count  3.0  3.0  3.0
mean   3.0  4.0  5.0
std    3.0  3.0  3.0
min    0.0  1.0  2.0
50%    3.0  4.0  5.0
80%    4.8  5.8  6.8
max    6.0  7.0  8.0

在这里,我们有一个 Pandas 中常见的支持统计函数的汇总表:

|

功能

|

描述

|
| --- | --- |
| idxmin(axis)idxmax(axis) | 这将计算具有最小或最大对应值的索引标签。 |
| value_counts() | 这将计算唯一值的频率。 |
| count() | 这将返回数据对象中非空值的数量。 |
| mean()median()min()max() | 这返回数据对象中轴的平均值、中值、最小值和最大值。 |
| std()var()sem() | 这些返回平均值的标准偏差、方差和标准误差。 |
| abs() | 这将获取数据对象的绝对值。 |

功能应用

Pandas 支持函数应用,允许我们在数据结构对象上应用其他包中支持的一些函数,比如 NumPy 或者我们自己的函数。这里我们举两个这种情况的例子,首先用apply执行std()函数,这是 NumPy 包的标准差计算函数:

>>> df5.apply(np.std, axis=1)    # default: axis=0
0    0.816497
1    0.816497
2    0.816497
dtype: float64

其次,如果我们想将公式应用于数据对象,我们也可以通过以下步骤使用 apply 函数:

  1. 定义要应用于数据对象的函数或公式。

  2. 通过apply调用定义的函数或公式。在这一步中,我们还需要计算出我们想要应用计算的轴:

    >>> f = lambda x: x.max() – x.min()    # step 1
    >>> df5.apply(f, axis=1)               # step 2
    0    2
    1    2
    2    2
    dtype: int64
    >>> def sigmoid(x):
     return 1/(1 + np.exp(x))
    >>> df5.apply(sigmoid)
     a           b         c
    0  0.500000  0.268941  0.119203
    1  0.047426  0.017986  0.006693
    2  0.002473  0.000911  0.000335
    
    

排序

有两种我们感兴趣的排序方式:按行或列索引排序和按数据值排序。

首先,我们将考虑按行和列索引排序的方法。在这种情况下,我们有sort_index ()功能。我们还有axis参数来设置函数是按行排序还是按列排序。带有truefalse值的ascending选项将允许我们按升序或降序对数据进行排序。该选项的默认设置为true:

>>> df7 = pd.DataFrame(np.arange(12).reshape(3,4), 
 columns=['b', 'd', 'a', 'c'],
 index=['x', 'y', 'z'])
>>> df7
 b  d   a   c
x  0  1   2   3
y  4  5   6   7
z  8  9  10  11
>>> df7.sort_index(axis=1)
 a  b   c  d
x   2  0   3  1
y   6  4   7  5
z  10  8  11  9

Series 有一个按值排序的方法顺序。对于对象中的NaN值,我们也可以通过na_position选项进行特殊处理:

>>> s4.order(na_position='first')
024     NaN
065     NaN
002    Mary
001     Nam
dtype: object
>>> s4
002    Mary
001     Nam
024     NaN
065     NaN
dtype: object

除此之外,Series 还有sort()功能,按数值对数据进行排序。但是,该函数不会返回排序数据的副本:

>>> s4.sort(na_position='first')
>>> s4
024     NaN
065     NaN
002    Mary
001     Nam
dtype: object

如果我们想对 DataFrame 对象应用排序函数,我们需要弄清楚哪些列或行将被排序:

>>> df7.sort(['b', 'd'], ascending=False)
 b  d   a   c
z  8  9  10  11
y  4  5   6   7
x  0  1   2   3

如果不想将排序结果自动保存到当前数据对象,可以将inplace参数的设置改为False

索引和选择数据

在本节中,我们将关注如何获取、设置或切片 Pandas 数据结构对象的子集。正如我们在前面几节中了解到的,系列或数据框对象具有轴标签信息。该信息可用于识别我们想要在对象中选择或分配新值的项目:

>>> s4[['024', '002']]    # selecting data of Series object
024     NaN
002    Mary
dtype: object
>>> s4[['024', '002']] = 'unknown' # assigning data
>>> s4
024    unknown
065        NaN
002    unknown
001        Nam
dtype: object

如果数据对象是数据帧结构,我们也可以用类似的方式进行:

>>> df5[['b', 'c']]
 b  c
0  1  2
1  4  5
2  7  8

对于数据框行的标签索引,我们使用ix函数,该函数使我们能够选择对象中的一组行和列。我们需要指定两个参数:我们想要获得的rowcolumn标签。默认情况下,如果我们不指定选定的列名,该函数将返回包含对象中所有列的选定行:

>>> df5.ix[0]
a    0
b    1
c    2
Name: 0, dtype: int64
>>> df5.ix[0, 1:3]
b    1
c    2
Name: 0, dtype: int64

此外,我们有许多方法来选择和编辑 Pandas 对象中包含的数据。下表总结了这些功能:

|

方法

|

描述

|
| --- | --- |
| icolirow | 这将按整数位置选择单行或单列。 |
| get_valueset_value | 这将按行或列标签选择或设置数据对象的单个值。 |
| xs | 这将按标签选择单个列或行作为系列。 |

型式

Pandas 数据对象可能包含重复的索引。在这种情况下,当我们通过索引标签获取或设置数据值时,它将影响具有相同选定索引名称的所有行或列。

计算工具

让我们从两个数据对象之间的相关性和协方差计算开始。系列和数据框都有一个cov方法。在数据框对象上,此方法将计算对象内部系列之间的协方差:

>>> s1 = pd.Series(np.random.rand(3))
>>> s1
0    0.460324
1    0.993279
2    0.032957
dtype: float64
>>> s2 = pd.Series(np.random.rand(3))
>>> s2
0    0.777509
1    0.573716
2    0.664212
dtype: float64
>>> s1.cov(s2)
-0.024516360159045424

>>> df8 = pd.DataFrame(np.random.rand(12).reshape(4,3), 
 columns=['a','b','c'])
>>> df8
 a         b         c
0  0.200049  0.070034  0.978615
1  0.293063  0.609812  0.788773
2  0.853431  0.243656  0.978057
0.985584  0.500765  0.481180
>>> df8.cov()
 a         b         c
a  0.155307  0.021273 -0.048449
b  0.021273  0.059925 -0.040029
c -0.048449 -0.040029  0.055067

相关方法的使用类似于协方差方法。在数据对象是数据帧的情况下,它计算数据对象中系列之间的相关性。然而,我们需要指定使用哪种方法来计算相关性。可用的方法有pearsonkendallspearman。默认情况下,该功能应用spearman方法:

>>> df8.corr(method = 'spearman')
 a    b    c
a  1.0  0.4 -0.8
b  0.4  1.0 -0.8
c -0.8 -0.8  1.0

我们还有corrwith功能,支持计算不同数据框对象中包含相同标签的系列之间的相关性:

>>> df9 = pd.DataFrame(np.arange(8).reshape(4,2), 
 columns=['a', 'b'])
>>> df9
 a  b
0  0  1
1  2  3
2  4  5
3  6  7
>>> df8.corrwith(df9)
a    0.955567
b    0.488370
c         NaN
dtype: float64

处理缺失的数据

在本节中,我们将讨论 Pandas 数据结构中的缺失、NaNnull值。一个对象中缺少数据是很常见的情况。产生缺失数据的一种情况是重新索引:

>>> df8 = pd.DataFrame(np.arange(12).reshape(4,3), 
 columns=['a', 'b', 'c'])
 a   b   c
0  0   1   2
1  3   4   5
2  6   7   8
3  9  10  11
>>> df9 = df8.reindex(columns = ['a', 'b', 'c', 'd'])
 a   b   c   d
0  0   1   2 NaN
1  3   4   5 NaN
2  6   7   8 NaN
4  9  10  11 NaN
>>> df10 = df8.reindex([3, 2, 'a', 0])
 a   b   c
3   9  10  11
2   6   7   8
a NaN NaN NaN
0   0   1   2

为了处理缺失值,我们可以使用isnull()notnull()函数来检测序列对象以及数据帧对象中的缺失值:

>>> df10.isnull()
 a      b      c
3  False  False  False
2  False  False  False
a   True   True   True
0  False  False  False

在系列中,我们可以使用dropna功能删除所有null数据和索引值:

>>> s4 = pd.Series({'001': 'Nam', '002': 'Mary',
 '003': 'Peter'},
 index=['002', '001', '024', '065'])
>>> s4
002    Mary
001     Nam
024     NaN
065     NaN
dtype: object
>>> s4.dropna()    # dropping all null value of Series object
002    Mary
001     Nam
dtype: object

使用 DataFrame 对象,它比 Series 稍微复杂一点。我们可以知道要删除哪些行或列,以及是否所有条目都必须是null或单个null值就足够了。默认情况下,该函数将删除任何包含缺失值的行:

>>> df9.dropna()    # all rows will be dropped
Empty DataFrame
Columns: [a, b, c, d]
Index: []
>>> df9.dropna(axis=1)
 a   b   c
0  0   1   2
1  3   4   5
2  6   7   8
3  9  10  11

控制缺失值的另一种方法是使用我们在上一节中介绍的函数的支持参数。它们对解决这个问题也非常有用。根据我们的经验,在创建数据对象时,我们应该在缺失的情况下分配一个固定值。这会让我们的物体在后面的加工步骤中更加干净。例如,考虑以下情况:

>>> df11 = df8.reindex([3, 2, 'a', 0], fill_value = 0)
>>> df11
 a   b   c
3  9  10  11
2  6   7   8
a  0   0   0
0  0   1   2

我们也可以使用fillna函数来填充缺失值中的自定义值:

>>> df9.fillna(-1)
 a   b   c  d
0  0   1   2 -1
1  3   4   5 -1
2  6   7   8 -1
3  9  10  11 -1

Pandas 用于数据分析的高级用途

在本节中,我们将考虑一些高级 Pandas 用例。

分级索引

分级索引通过将数据对象组织成轴上的多个索引级别,为我们提供了一种在较低维度上处理较高维度数据的方法:

>>> s8 = pd.Series(np.random.rand(8), index=[['a','a','b','b','c','c', 'd','d'], [0, 1, 0, 1, 0,1, 0, 1, ]])
>>> s8
a  0    0.721652
 1    0.297784
b  0    0.271995
 1    0.125342
c  0    0.444074
 1    0.948363
d  0    0.197565
 1    0.883776
dtype: float64

在前面的示例中,我们有一个具有两个索引级别的 Series 对象。可以使用unstack功能将对象重新排列成数据帧。在相反的情况下,可以使用stack功能:

>>> s8.unstack()
 0         1
a  0.549211  0.420874
b  0.051516  0.715021
c  0.503072  0.720772
d  0.373037  0.207026

我们还可以创建一个数据框,在两个轴上都有一个分层索引:

>>> df = pd.DataFrame(np.random.rand(12).reshape(4,3),
 index=[['a', 'a', 'b', 'b'],
 [0, 1, 0, 1]],
 columns=[['x', 'x', 'y'], [0, 1, 0]])
>>> df
 x                   y
 0         1         0
a 0  0.636893  0.729521  0.747230
 1  0.749002  0.323388  0.259496
b 0  0.214046  0.926961  0.679686
0.013258  0.416101  0.626927
>>> df.index
MultiIndex(levels=[['a', 'b'], [0, 1]],
 labels=[[0, 0, 1, 1], [0, 1, 0, 1]])
>>> df.columns
MultiIndex(levels=[['x', 'y'], [0, 1]],
 labels=[[0, 0, 1], [0, 1, 0]])

获取或设置具有多个索引级别的数据对象的值或子集的方法类似于非分层情况:

>>> df['x']
 0         1
a 0  0.636893  0.729521
 1  0.749002  0.323388
b 0  0.214046  0.926961
0.013258  0.416101
>>> df[[0]]
 x
 0
a 0  0.636893
 1  0.749002
b 0  0.214046
0.013258
>>> df.ix['a', 'x']
 0         1
0  0.636893  0.729521
0.749002  0.323388
>>> df.ix['a','x'].ix[1]
0    0.749002
1    0.323388
Name: 1, dtype: float64

在将数据分组为多个索引级别后,我们还可以使用大部分带有级别选项的描述和统计功能,这些功能可以用来指定我们要处理的级别:

>>> df.std(level=1)
 x                   y
 0         1         0
0  0.298998  0.139611  0.047761
0.520250  0.065558  0.259813
>>> df.std(level=0)
 x                   y
 0         1         0
a  0.079273  0.287180  0.344880
b  0.141979  0.361232  0.037306

面板数据

该面板是 Pandas 中三维数据的另一种数据结构。但是,它的使用频率低于系列或数据框。您可以将面板视为数据框对象的表格。我们可以从 3D ndarray或数据框对象的字典中创建面板对象:

# create a Panel from 3D ndarray
>>> panel = pd.Panel(np.random.rand(2, 4, 5),
 items = ['item1', 'item2'])
>>> panel
<class 'pandas.core.panel.Panel'>
Dimensions: 2 (items) x 4 (major_axis) x 5 (minor_axis)
Items axis: item1 to item2
Major_axis axis: 0 to 3
Minor_axis axis: 0 to 4

>>> df1 = pd.DataFrame(np.arange(12).reshape(4, 3), 
 columns=['a','b','c'])
>>> df1
 a   b   c
0  0   1   2
1  3   4   5
2  6   7   8
9  10  11
>>> df2 = pd.DataFrame(np.arange(9).reshape(3, 3), 
 columns=['a','b','c'])
>>> df2
 a  b  c
0  0  1  2
1  3  4  5
6  7  8
# create another Panel from a dict of DataFrame objects
>>> panel2 = pd.Panel({'item1': df1, 'item2': df2})
>>> panel2
<class 'pandas.core.panel.Panel'>
Dimensions: 2 (items) x 4 (major_axis) x 3 (minor_axis)
Items axis: item1 to item2
Major_axis axis: 0 to 3
Minor_axis axis: a to c

面板中的每个项目都是一个数据框。我们可以通过项目名称选择一个项目:

>>> panel2['item1']
 a   b   c
0  0   1   2
1  3   4   5
2  6   7   8
3  9  10  11

或者,如果我们想要通过轴或数据位置选择数据,我们可以使用ix方法,如在系列或数据框上:

>>> panel2.ix[:, 1:3, ['b', 'c']]
<class 'pandas.core.panel.Panel'>
Dimensions: 2 (items) x 3 (major_axis) x 2 (minor_axis)
Items axis: item1 to item2
Major_axis axis: 1 to 3
Minor_axis axis: b to c
>>> panel2.ix[:, 2, :]
 item1  item2
a      6      6
b      7      7
c      8      8

总结

我们已经完成了 Pandas 数据分析库的基础知识。每当您了解用于数据分析的库时,您都需要考虑我们在本章中解释的三个部分。数据结构:Pandas 库中有两种常见的数据对象类型;系列和数据帧。访问和操作数据对象的方法:Pandas 支持多种方法来选择、设置或切片数据对象的子集。但是,一般的机制是使用索引标签或项目的位置来标识值。功能和实用程序:它们是强大的库最重要的部分。在这一章中,我们介绍了 Pandas 的所有常见支持功能,这些功能允许我们轻松计算数据统计。该库还有许多其他有用的功能和实用程序,我们在本章中无法解释。如果你想扩展你对 Pandas 的经验,我们鼓励你开始自己的研究。它帮助我们以优化的方式处理大数据。在这本书的后面,你会看到更多的 Pandas 在行动。

到目前为止,我们已经了解了两个流行的 Python 库:NumPy 和 Pandas。Pandas 是建立在 NumPy 上的,因此它允许更方便的数据交互。然而,在某些情况下,我们可以灵活地将两者结合起来,以实现我们的目标。

练习练习

链接https://www.census.gov/2010census/csv/pop_change.csv包含美国人口普查数据集。它有 23 列,每个美国州有一行,还有几行用于宏观区域,如北部、南部和西部。

  • 将此数据集放入 Pandas 数据框。提示:跳过那些看起来没有帮助的行,例如注释或描述。
  • 虽然数据集包含每十年的变化指标,但我们对二十世纪下半叶(即 1950 年至 2000 年)的人口变化感兴趣。在这一时间跨度内,哪个地区的人口增长最大,哪个地区的人口增长最小?还有,美国哪个州?

高级开放式练习:

  • 在网上查找更多人口普查数据;不仅仅是美国,还有世界各国。也试着寻找同期的国内生产总值数据。尝试调整这些数据来探索模式。GDP 和人口增长是如何关联的?有什么特殊情况吗?比如 GDP 高但人口增长低的国家还是历史相反的国家?

四、数据可视化

数据可视化是指以图像或图形的形式呈现数据。这是数据分析中最重要的任务之一,因为它使我们能够看到分析结果,检测异常值,并为模型构建做出决策。有许多用于可视化的 Python 库,其中 matplotlib、seaborn、bokeh 和 ggplot 是最受欢迎的。然而,在本章中,我们主要关注 matplotlib 库,它被许多人在许多不同的上下文中使用。

Matplotlib 以各种格式和跨 Python 平台的交互环境生成出版物质量的数字。另一个优点是 Pandas 配备了一些 matplotlib 绘图例程的有用包装器,允许快速方便地绘制系列和数据帧对象。

IPython 包最初是作为标准交互式 Python 外壳的替代物,但后来发展成为数据探索、可视化和快速原型制作不可或缺的工具。可以通过各种选项使用 IPython matplotlib 提供的图形功能,其中最简单的开始是pylab标志:

$ ipython --pylab

该标志将预加载matplotlibnumpy,以便与默认 matplotlib 后端交互使用。IPython 可以在各种环境中运行:在终端中,作为Qt应用程序,或者在浏览器中。这些选项值得探索,因为 IPython 已经被许多用例所采用,例如原型制作、用于更吸引人的会议演讲或讲座的交互式幻灯片,以及作为共享研究的工具。

matplotlib 原料药引物

开始使用 matplotlib 绘图的最简单的方法通常是使用软件包支持的 MATLAB 应用编程接口:

>>> import matplotlib.pyplot as plt
>>> from numpy import *
>>> x = linspace(0, 3, 6)
>>> x
array([0., 0.6, 1.2, 1.8, 2.4, 3.])
>>> y = power(x,2)
>>> y
array([0., 0.36, 1.44, 3.24, 5.76, 9.])
>>> figure()
>>> plot(x, y, 'r')
>>> xlabel('x')
>>> ylabel('y')
>>> title('Data visualization in MATLAB-like API')
>>> plt.show()

前面命令的输出如下:

The matplotlib API primer

但是,除非有充分的理由,否则不应使用明星进口。在 matplotlib 的情况下,我们可以使用规范导入:

>>> import matplotlib.pyplot as plt

前面的例子可以写成如下:

>>> plt.plot(x, y)
>>> plt.xlabel('x')
>>> plt.ylabel('y')
>>> plt.title('Data visualization using Pyplot of Matplotlib')
>>> plt.show()

前面命令的输出如下:

The matplotlib API primer

如果我们只为绘图函数提供一个参数,它将自动将其用作y值,并生成从0N-1x值,其中N等于值的数量:

>>> plt.plot(y)
>>> plt.xlabel('x')
>>> plt.ylabel('y')
>>> plt.title('Plot y value without given x values')
>>> plt.show()

前面命令的输出如下:

The matplotlib API primer

默认情况下,轴的范围受输入xy数据的范围约束。如果我们想指定轴的viewport,我们可以使用axis()方法设置自定义范围。例如,在前面的可视化中,我们可以通过编写以下命令将x轴的范围从[0, 5]增加到[0, 6],将y轴的范围从[0, 9]增加到[0, 10]:

>>> plt.axis([0, 6, 0, 12])

线条属性

在 matplotlib 中绘制数据时,的默认线条格式是一条蓝色实线,缩写为b-。要更改此设置,我们只需将符号代码添加到plot功能中,符号代码包括作为颜色字符串的字母和作为线型字符串的符号。让我们考虑具有不同格式样式的几行的图:

>>> plt.plot(x*2, 'g^', x*3, 'rs', x**x, 'y-')
>>> plt.axis([0, 6, 0, 30])
>>> plt.show()

前面命令的输出如下:

Line properties

有许多线条样式和属性,例如颜色、线条宽度和虚线样式,我们可以从中进行选择,以控制地块的外观。以下示例说明了设置线属性的几种方法:

>>> line = plt.plot(y, color='red', linewidth=2.0)
>>> line.set_linestyle('--')
>>> plt.setp(line, marker='o')
>>> plt.show()

前面命令的输出如下:

Line properties

下表列出了line2d绘图的一些常见属性:

|

财产

|

值类型

|

描述

|
| --- | --- | --- |
| colorc | 任何 matplotlib 颜色 | 这将设置图中线条的颜色 |
| dashes | 开/关 | 这将设置点中油墨的顺序 |
| data | nparray``xdata``np.array``ydata | 这将设置用于可视化的数据 |
| linestylels | [ '-' | '—' | '-.' | ':' |...] | 这将设置图中的线条样式 |
| linewidthlw | 浮点值 | 这将设置图中线条的宽度 |
| marker | 任何符号 | 这将设置图中数据点的样式 |

人物和支线剧情

默认情况下,所有绘图命令适用于当前图形和轴。在某些情况下,我们希望在多个图形和轴中可视化数据,以比较不同的图或更有效地使用页面上的空间。需要两个步骤才能绘制数据。首先,我们必须定义我们要绘制哪个数字。其次,我们需要弄清楚我们的支线剧情在图中的位置:

>>> plt.figure('a')    # define a figure, named 'a'
>>> plt.subplot(221)    # the first position of 4 subplots in 2x2 figure
>>> plt.plot(y+y, 'r--')
>>> plt.subplot(222)    # the second position of 4 subplots
>>> plt.plot(y*3, 'ko')
>>> plt.subplot(223)    # the third position of 4 subplots
>>> plt.plot(y*y, 'b^')
>>> plt.subplot(224)
>>> plt.show()

前面命令的输出如下:

Figures and subplots

在这种情况下,我们目前有图a。如果要修改图a中的任何一个子图,首先调用命令选择图和子图,然后执行函数修改子图。例如,在这里,我们更改了我们的四情节图的第二情节的标题:

>>> plt.figure('a')
>>> plt.subplot(222)
>>> plt.title('visualization of y*3')
>>> plt.show()

前一命令的输出如下:

Figures and subplots

型式

如果我们不使用逗号分隔索引,整数子图规格必须是三位数。所以,plt.subplot(221)等于plt.subplot(2,2,1)命令。

有一种便利方法plt.subplots(),可以创建一个包含给定数量支线剧情的图形。就像前面的例子一样,我们可以使用plt.subplots(2,2)命令来创建一个由四个支线剧情组成的2x2人物。

我们也可以使用plt.axes([left, bottom, width, height])命令手动创建轴,而不是矩形网格,其中所有输入参数都在分数[0, 1]坐标中:

>>> plt.figure('b')    # create another figure, named 'b'
>>> ax1 = plt.axes([0.05, 0.1, 0.4, 0.32])
>>> ax2 = plt.axes([0.52, 0.1, 0.4, 0.32])
>>> ax3 = plt.axes([0.05, 0.53, 0.87, 0.44])
>>> plt.show()

前面命令的输出如下:

Figures and subplots

然而,当你手动创建轴时,需要更多的时间来平衡子情节之间的坐标和大小,以获得均匀的图形。

探索地块类型

到目前为止,我们已经了解了如何创建简单的线图。matplotlib 库支持更多对数据可视化有用的绘图类型。但是,我们的目标是提供基础知识,帮助您理解和使用库在最常见的情况下可视化数据。因此,我们只关注四种图型:散点图条形图等高线图直方图

散点图

一个散点图用于可视化在同一数据集中测量的变量之间的关系。使用plt.scatter()函数绘制简单的散点图很容易,它要求x轴和y轴都有数字列:

Scatter plots

让我们看看前面输出的命令:

>>> X = np.random.normal(0, 1, 1000)
>>> Y = np.random.normal(0, 1, 1000)
>>> plt.scatter(X, Y, c = ['b', 'g', 'k', 'r', 'c'])
>>> plt.show()

条形地块

一个条形图用于显示带有矩形条的分组数据,矩形条可以是垂直的也可以是水平的,条的长度对应于它们的值。我们使用plt.bar()命令来可视化一个竖条,使用plt.barh()命令来可视化另一个竖条:

Bar plots

前面输出的命令如下:

>>> X = np.arange(5)
>>> Y = 3.14 + 2.71 * np.random.rand(5)
>>> plt.subplots(2)
>>> # the first subplot
>>> plt.subplot(211)
>>> plt.bar(X, Y, align='center', alpha=0.4, color='y')
>>> plt.xlabel('x')
>>> plt.ylabel('y')
>>> plt.title('bar plot in vertical')
>>> # the second subplot
>>> plt.subplot(212)
>>> plt.barh(X, Y, align='center', alpha=0.4, color='c')
>>> plt.xlabel('x')
>>> plt.ylabel('y')
>>> plt.title('bar plot in horizontal')
>>> plt.show()

等高线图

我们使用等高线图来表示二维中三个数值变量之间的关系。两个变量沿xy轴绘制,第三个变量z用于绘制为不同颜色曲线的等高线高程:

>>> x = np.linspace(-1, 1, 255)
>>> y = np.linspace(-2, 2, 300)
>>> z = np.sin(y[:, np.newaxis]) * np.cos(x)
>>> plt.contour(x, y, z, 255, linewidth=2)
>>> plt.show()

让我们看看下图中的等高线图:

Contour plots

型式

如果要画等高线和填充等高线,可以用plt.contourf()法代替plt.contour()。与 MATLAB 不同,matplotlib 的contourf()不会画多边形的边。

直方图

一个直方图用图形表示数值数据的分布。通常,值的范围被划分为大小相等的箱,每个箱的高度对应于该箱内值的频率:

Histogram plots

前面输出的命令如下:

>>> mu, sigma = 100, 25
>>> fig, (ax0, ax1) = plt.subplots(ncols=2)
>>> x = mu + sigma * np.random.randn(1000)
>>> ax0.hist(x,20, normed=1, histtype='stepfilled', 
 facecolor='g', alpha=0.75)
>>> ax0.set_title('Stepfilled histogram')
>>> ax1.hist(x, bins=[100,150, 165, 170, 195] normed=1, 
 histtype='bar', rwidth=0.8)
>>> ax1.set_title('uniquel bins histogram')
>>> # automatically adjust subplot parameters to give specified padding
>>> plt.tight_layout()
>>> plt.show()

传说和注释

传说是用来识别人物中plot元素的重要元素。在图形中显示图例最简单的方法是使用plot函数的label参数,并通过调用plt.legend()方法来显示标签:

>>> x = np.linspace(0, 1, 20) 
>>> y1 = np.sin(x)
>>> y2 = np.cos(x)
>>> y3 = np.tan(x)
>>> plt.plot(x, y1, 'c', label='y=sin(x)')
>>> plt.plot(x, y2, 'y', label='y=cos(x)')
>>> plt.plot(x, y3, 'r', label='y=tan(x)')
>>> plt.lengend(loc='upper left')
>>> plt.show()

前面命令的输出如下:

Legends and annotations

图例命令中的loc参数用于计算标签框的位置。有几个有效的位置选项:lower leftrightupper leftlower centerupper rightcenterlower rightupper rightcenter rightbestupper centercenter left。默认位置设置为upper right。但是,当我们设置一个不存在于以上列表中的无效位置选项时,该功能会自动回落到最佳选项。

如果我们想要将图例拆分为图形中的多个框,我们可以手动设置地块线的预期标签,如下图所示:

Legends and annotations

前面命令的输出如下:

>>> p1 = plt.plot(x, y1, 'c', label='y=sin(x)')
>>> p2 = plt.plot(x, y2, 'y', label='y=cos(x)')
>>> p3 = plt.plot(x, y3, 'r', label='y=tan(x)')
>>> lsin = plt.legend(handles=p1, loc='lower right')
>>> lcos = plt.legend(handles=p2, loc='upper left')
>>> ltan = plt.legend(handles=p3, loc='upper right')
>>> # with above code, only 'y=tan(x)' legend appears in the figure
>>> # fix: add lsin, lcos as separate artists to the axes
>>> plt.gca().add_artist(lsin)
>>> plt.gca().add_artist(lcos)
>>> # automatically adjust subplot parameters to specified padding
>>> plt.tight_layout()
>>> plt.show()

我们要介绍的图形中的另一个元素是注释,它可以由文本、箭头或其他形状组成,以详细解释图形的各个部分,或者强调一些特殊的数据点。有不同的显示注释的方法,如textarrow、、annotation

  • text方法在图上给定坐标(x, y)处绘制文字;可选地具有自定义属性。函数中有一些常见的参数:xy、标签文本和字体相关的属性可以通过fontdict传入,如familyfontsizestyle
  • annotate方法可以绘制适当排列的文本和箭头。该函数的参数有s(标签文本)、xy(要标注的元素的位置)、xytext(标签的位置s)、xycoords(表示坐标类型的字符串xy)和arrowprops(连接标注的箭头的线属性字典)。

这里有一个简单的例子来说明annotatetext的功能:

>>> x = np.linspace(-2.4, 0.4, 20)
>>> y = x*x + 2*x + 1
>>> plt.plot(x, y, 'c', linewidth=2.0)
>>> plt.text(-1.5, 1.8, 'y=x^2 + 2*x + 1',
 fontsize=14, style='italic')
>>> plt.annotate('minima point', xy=(-1, 0),
 xytext=(-1, 0.3),
 horizontalalignment='center', 
 verticalalignment='top', 
 arrowprops=dict(arrowstyle='->', 
 connectionstyle='arc3'))
>>> plt.show()

前面命令的输出如下:

Legends and annotations

Pandas 绘图功能

我们已经使用 matplotlib 覆盖了一个绘图图形中的大部分重要组件。在本节中,我们将介绍另一种强大的绘图方法,用于从 Pandas 数据对象直接创建标准可视化,Pandas 数据对象通常用于操作数据。

对于 Pandas 中的系列或数据框对象,支持大多数绘图类型,如折线图、柱状图、柱状图和散点图以及饼图。要选择绘图类型,我们使用plot函数的kind参数。在没有指定任何类型的绘图的情况下,plot功能将默认生成线条样式可视化,如下例所示:

>>> s = pd.Series(np.random.normal(10, 8, 20))
>>> s.plot(style='ko—', alpha=0.4, label='Series plotting')
>>> plt.legend()
>>> plt.show()

前面命令的输出如下:

Plotting functions with Pandas

另一个例子将可视化由多列组成的数据框对象的数据:

>>> data = {'Median_Age': [24.2, 26.4, 28.5, 30.3],
 'Density': [244, 256, 268, 279]}
>>> index_label = ['2000', '2005', '2010', '2014'];
>>> df1 = pd.DataFrame(data, index=index_label)
>>> df1.plot(kind='bar', subplots=True, sharex=True)
>>> plt.tight_layout();
>>> plt.show()

前面命令的输出如下:

Plotting functions with Pandas

数据框的绘图方法有许多选项,允许我们处理列的绘图。例如,在上面的数据框可视化中,我们选择在单独的子图中绘制列。下表列出了更多选项:

|

争吵

|

价值

|

描述

|
| --- | --- | --- |
| subplots | True / False | 将每个数据列绘制在单独的子图中 |
| logy | True / False | 获得对数刻度y轴 |
| secondary_y | True / False | 在次级y轴上绘制数据 |
| sharexsharey | True / False | 共享相同的xy轴,链接棒和限制 |

其他 Python 数据可视化工具

除了 matplotlib,还有其他基于 Python 的强大数据可视化工具包。虽然我们无法深入探讨这些库,但我们希望在本次会议中至少简要介绍一下它们。

博克

Bokeh 是由王蒙杰、雨果·史和其他人在连续体分析公司的一个项目。它旨在以D3.js的风格提供优雅而迷人的视觉效果。该库可以快速轻松地创建交互式绘图、仪表板和数据应用程序。下面是 matplotlib 和 Bokeh 之间的一些区别:

  • Bokeh 通过 IPython 的浏览器内客户端渲染新模型实现了跨平台的无处不在
  • Bokeh 使用了 R 和 ggplot 用户熟悉的语法,而 matplotlib 则是 Matlab 用户更熟悉的语法
  • Bokeh 有一个连贯的愿景,即构建一个受 ggplot 启发的浏览器内交互式可视化工具,而 Matplotlib 有一个连贯的愿景,即专注于 2D 跨平台图形。

用 Bokeh 创建地块的基本步骤如下:

  • 在列表、系列和数据框中准备一些数据
  • 告诉 Bokeh 您想在哪里生成输出
  • 调用figure()创建带有一些整体选项的绘图,类似于前面讨论的 matplotlib 选项
  • 为数据添加渲染器,包括颜色、图例和宽度等视觉自定义
  • 询问博凯show()save()结果

玛雅维

MayaVi 是一个用于交互式科学数据可视化和 3D 绘图的库,建立在屡获殊荣的可视化工具包 ( VTK )之上,是开源可视化库的基于特征的包装器。它提供以下功能:

  • 通过对话框与可视化中的数据和对象进行交互的可能性。
  • Python 中用于脚本编写的接口。MayaVi 可以与 Numpy 和 scipy 一起开箱即用地进行 3D 绘图,并且可以在 IPython 笔记本内使用,这与 matplotlib 类似。
  • 对 VTK 的抽象,提供了一个更简单的编程模型。

让我们来看看一个完全使用 MayaVi 基于 VTK 的例子和他们提供的数据制作的插图:

MayaVi

总结

基于 matplotlib 库,我们已经完成了大部分基础知识,例如数据可视化的函数、参数和属性。我们希望,通过这些例子,你将能够理解它们并将其应用于你自己的问题。一般来说,为了可视化数据,我们需要考虑五个步骤——也就是说,将数据获取到合适的 Python 或 Pandas 数据结构中,例如列表、字典、系列或数据框架。我们在前面的章节中解释了如何完成这一步。第二步是为所讨论的数据对象定义图和子图。我们在人物和支线剧情部分讨论了这个问题。第三步是选择要在支线剧情中展现的剧情风格及其属性,如:linebarhistogramscatter plotlinestylecolor。第四步是给子剧情添加额外的组件,比如图例、注释和文本。第五步是显示或保存结果。

到目前为止,你可以用数据集做很多事情;例如,基于 Python 库(如 Numpy、Pandas 和 matplotlib)的操纵、清理、探索和可视化。现在,您可以将这些知识和实践与这些库结合起来,以越来越熟悉 Python 数据分析。

练习练习:

  • 说出两个真实或虚构的数据集,并解释哪种图最适合数据:折线图、条形图、散点图、等高线图或直方图。说出一个或两个应用程序,其中每种绘图类型都很常见(例如,直方图常用于图像编辑应用程序)。
  • 我们只关注 matplotlib 最常见的情节类型。经过一番研究,你能说出 matplotlib 中更多可用的绘图类型吗?
  • 第三章Pandas 数据分析中取一个 Pandas 数据结构,用合适的方式绘制数据。然后,将其作为 PNG 图像保存到磁盘。

五、时间序列

时间序列通常由一系列数据点组成,这些数据点来自随时间进行的测量。这种数据非常常见,出现在许多领域。

企业高管对股票价格、商品和服务价格或月销售额感兴趣。气象学家一天测量几次温度,并记录下降雨量、湿度、风向和风力。神经科医生可以使用脑电图来测量大脑沿头皮的电活动。一个社会学家可以利用竞选捐款数据来了解政党及其支持者,并将这些见解作为论证的辅助手段。时间序列数据的更多例子几乎可以无穷无尽地列举出来。

时间序列引物

一般来说,时间序列有两个目的。首先,它们帮助我们了解生成数据的底层过程。另一方面,我们希望能够使用现有数据预测相同或相关系列的未来值。当我们测量温度、降水或风时,我们希望更多地了解更复杂的事情,例如天气或一个地区的气候,以及各种因素如何相互作用。同时,我们可能对天气预报感兴趣。

在这一章中,我们将探索 Pandas 的时间序列能力。除了强大的核心数据结构——系列和数据框架——Pandas 还附带了处理时间相关数据的辅助功能。凭借其广泛的内置优化,Pandas 能够轻松处理包含数百万个数据点的大型时间序列。

我们将逐步接近时间序列,从日期和时间对象的基本构件开始。

处理日期和时间对象

Python 支持标准库中日期时间模块中的日期和时间处理:

>>> import datetime
>>> datetime.datetime(2000, 1, 1)
datetime.datetime(2000, 1, 1, 0, 0)

有时,日期是作为字符串给出或预期的,因此从字符串到字符串的转换是必要的,这分别通过两个函数实现:strptimestrftime:

>>> datetime.datetime.strptime("2000/1/1", "%Y/%m/%d")
datetime.datetime(2000, 1, 1, 0, 0)
>>> datetime.datetime(2000, 1, 1, 0, 0).strftime("%Y%m%d")
'20000101'

现实世界的数据通常有各种各样的形状,如果我们不需要记住解析时指定的确切日期格式,那就太好了。谢天谢地,Pandas 在处理代表日期或时间的字符串时,消除了很多摩擦。这些辅助功能之一是to_datetime:

>>> import pandas as pd
>>> import numpy as np
>>> pd.to_datetime("4th of July")
Timestamp('2015-07-04 
>>> pd.to_datetime("13.01.2000")
Timestamp('2000-01-13 00:00:00')
>>> pd.to_datetime("7/8/2000")
Timestamp('2000-07-08 00:00:00')

最后一个可以指 8 月 7 日或 7 月 8 日,视地区而定。为了消除这种情况的歧义,可以给to_datetime传递一个关键字参数dayfirst:

>>> pd.to_datetime("7/8/2000", dayfirst=True)
Timestamp('2000-08-07 00:00:00')

时间戳对象可以被视为 Pandas 版本的datetime对象,事实上,Timestamp类是datetime的子类:

>>> issubclass(pd.Timestamp, datetime.datetime)
True

这意味着它们在许多情况下可以互换使用:

>>> ts = pd.to_datetime(946684800000000000)
>>> ts.year, ts.month, ts.day, ts.weekday()
(2000, 1, 1, 5)

时间戳对象是 Pandas 时间序列功能的重要组成部分,因为时间戳是DateTimeIndex对象的构造块:

>>> index = [pd.Timestamp("2000-01-01"),
 pd.Timestamp("2000-01-02"),
 pd.Timestamp("2000-01-03")]
>>> ts = pd.Series(np.random.randn(len(index)), index=index)
>>> ts
2000-01-01    0.731897
2000-01-02    0.761540
2000-01-03   -1.316866
dtype: float64
>>> ts.indexDatetime
Index(['2000-01-01', '2000-01-02', '2000-01-03'],
dtype='datetime64[ns]', freq=None, tz=None)

这里有几点需要注意:我们创建一个时间戳对象列表,并将其作为索引传递给系列构造器。这个时间戳列表被动态转换成DatetimeIndex。如果我们只通过了日期字符串,我们就不会得到一个DatetimeIndex,只是一个index:

>>> ts = pd.Series(np.random.randn(len(index)), index=[
 "2000-01-01", "2000-01-02", "2000-01-03"])
>>> ts.index
Index([u'2000-01-01', u'2000-01-02', u'2000-01-03'], dtype='object')

然而,to_datetime函数足够灵活,如果我们只有一个日期字符串列表,那么它会有所帮助:

>>> index = pd.to_datetime(["2000-01-01", "2000-01-02", "2000-01-03"])
>>> ts = pd.Series(np.random.randn(len(index)), index=index)
>>> ts.index
DatetimeIndex(['2000-01-01', '2000-01-02', '2000-01-03'], dtype='datetime64[ns]', freq=None, tz=None))

另外需要注意的是,虽然我们有一个DatetimeIndex,但是freqtz属性都是None。我们将在本章后面学习这两种属性的效用。

借助to_datetime我们能够将各种字符串甚至字符串列表转换成时间戳或DatetimeIndex对象。有时,我们没有明确地得到关于一个序列的所有信息,我们必须自己生成固定间隔的时间戳序列。

Pandas 为这个任务提供了另一个巨大的实用功能:date_range

date_range功能有助于在开始日期和结束日期之间生成固定频率的datetime索引。还可以指定开始或结束日期以及要生成的时间戳数量。

频率可由freq参数指定,该参数支持多个偏移。您可以使用典型的时间间隔,如小时、分钟和秒钟:

>>> pd.date_range(start="2000-01-01", periods=3, freq='H')
DatetimeIndex(['2000-01-01 00:00:00', '2000-01-01 01:00:00', '2000-01-01 02:00:00'], dtype='datetime64[ns]', freq='H', tz=None)
>>> pd.date_range(start="2000-01-01", periods=3, freq='T')
DatetimeIndex(['2000-01-01 00:00:00', '2000-01-01 00:01:00', '2000-01-01 00:02:00'], dtype='datetime64[ns]', freq='T', tz=None)
>>> pd.date_range(start="2000-01-01", periods=3, freq='S')
DatetimeIndex(['2000-01-01 00:00:00', '2000-01-01 00:00:01', '2000-01-01 00:00:02'], dtype='datetime64[ns]', freq='S', tz=None)

freq属性允许我们指定多个选项。Pandas 已经成功地应用于金融和经济领域,不仅仅是因为处理商务约会非常简单。例如,要获得千禧年前三个工作日的指数,可以使用B偏移别名:

>>> pd.date_range(start="2000-01-01", periods=3, freq='B')
DatetimeIndex(['2000-01-03', '2000-01-04', '2000-01-05'], dtype='datetime64[ns]', freq='B', tz=None)

下表显示了可用的偏移别名,也可以在 Pandas 文档下的时间序列中查找:

|

别名

|

描述

|
| --- | --- |
| B | 工作日频率 |
| C | 自定义工作日频率 |
| D | 日历日频率 |
| W | 每周频率 |
| M | 月末频率 |
| 医学学士 | 营业月末频率 |
| 大陆弹道导弹(Continental Ballistic Missile) | 自定义业务月末频率 |
| 女士 | 月份开始频率 |
| BachelorofMarineScience 海洋科学学士 | 营业月开始频率 |
| CBMS | 自定义业务月开始频率 |
| Q | 四分之一结束频率 |
| 贝克勒尔 | 业务季度频率 |
| QS | 四分之一开始频率 |
| BQS | 业务季度开始频率 |
| A | 年终频率 |
| 钡 | 营业年度结束频率 |
| 如同 | 年度开始频率 |
| 停下 | 业务年度开始频率 |
| 钅波 | 营业时间频率 |
| H | 每小时频率 |
| T | 微小频率 |
| S | 其次是频率 |
| L | 毫秒 |
| U | 微秒 |
| 普通 | 纳秒 |

此外,偏移别名也可以组合使用。这里,我们生成一个datetime索引,包含五个元素,每一个相隔一天、一小时、一分钟和一秒钟:

>>> pd.date_range(start="2000-01-01", periods=5, freq='1D1h1min10s')
DatetimeIndex(['2000-01-01 00:00:00', '2000-01-02 01:01:10', '2000-01-03 02:02:20', '2000-01-04 03:03:30', '2000-01-05 04:04:40'], dtype='datetime64[ns]', freq='90070S', tz=None)

如果我们想在每 12 个小时的工作时间内索引数据,默认情况下从上午 9 点开始,下午 5 点结束,我们只需在BH别名前加上前缀:

>>> pd.date_range(start="2000-01-01", periods=5, freq='12BH')
DatetimeIndex(['2000-01-03 09:00:00', '2000-01-04 13:00:00', '2000-01-06 09:00:00', '2000-01-07 13:00:00', '2000-01-11 09:00:00'], dtype='datetime64[ns]', freq='12BH', tz=None)

业务时间的自定义定义也是可能的:

>>> ts.index
DatetimeIndex(['2000-01-01', '2000-01-02', '2000-01-03'], dtype='datetime64[ns]', freq=None, tz=None)

我们也可以使用这个定制的工作时间来构建索引:

>>> pd.date_range(start="2000-01-01", periods=5, freq=12 * bh)
DatetimeIndex(['2000-01-03 07:00:00', '2000-01-03 19:00:00', '2000-01-04 07:00:00', '2000-01-04 19:00:00', '2000-01-05 07:00:00', '2000-01-05 19:00:00', '2000-01-06 07:00:00'], dtype='datetime64[ns]', freq='12BH', tz=None)

一些频率允许我们指定锚定后缀,这允许我们表达间隔,例如每个星期五或每月的第二个星期二:

>>> pd.date_range(start="2000-01-01", periods=5, freq='W-FRI')
DatetimeIndex(['2000-01-07', '2000-01-14', '2000-01-21', '2000-01-28', '2000-02-04'], dtype='datetime64[ns]', freq='W-FRI', tz=None)
>>> pd.date_range(start="2000-01-01", periods=5, freq='WOM-2TUE')
DatetimeIndex(['2000-01-11', '2000-02-08', '2000-03-14', '2000-04-11', '2000-05-09'], dtype='datetime64[ns]', freq='WOM-2TUE', tz=None)

最后,我们可以合并不同频率的各种指标。可能性是无穷的。我们仅举一个例子,其中我们结合了两个指数——每个指数都超过十年——一个指向一年中的每个第一个工作日,一个指向二月的最后一天:

>>> s = pd.date_range(start="2000-01-01", periods=10, freq='BAS-JAN')
>>> t = pd.date_range(start="2000-01-01", periods=10, freq='A-FEB')
>>> s.union(t)
DatetimeIndex(['2000-01-03', '2000-02-29', '2001-01-01', '2001-02-28', '2002-01-01', '2002-02-28', '2003-01-01', '2003-02-28','2004-01-01', '2004-02-29', '2005-01-03', '2005-02-28', '2006-01-02', '2006-02-28', '2007-01-01', '2007-02-28','2008-01-01', '2008-02-29', '2009-01-01', '2009-02-28'], dtype='datetime64[ns]', freq=None, tz=None)

我们看到,2000 年和 2005 年不是从一个工作日开始的,2000 年、2004 年和 2008 年是闰年。

到目前为止,我们已经看到了两个强大的功能,to_datetimedate_range。现在,我们想深入时间序列,首先展示如何创建和绘制只有几行的时间序列数据。在本节的剩余部分,我们将展示访问和切片时间序列数据的各种方法。

Pandas 的时间序列数据很容易上手。可以创建一个随机游走,并绘制成几行:

>>> index = pd.date_range(start='2000-01-01', periods=200, freq='B')
>>> ts = pd.Series(np.random.randn(len(index)), index=index)
>>> walk = ts.cumsum()
>>> walk.plot()

下图显示了该图的可能输出:

Working with date and time objects

与通常的系列对象一样,您可以选择零件并分割索引:

>>> ts.head()
2000-01-03    1.464142
2000-01-04    0.103077
2000-01-05    0.762656
2000-01-06    1.157041
2000-01-07   -0.427284
Freq: B, dtype: float64
>>> ts[0]
1.4641415817112928
>>> ts[1:3]
2000-01-04    0.103077
2000-01-05    0.762656

我们可以使用日期字符串作为键,尽管我们的系列有一个DatetimeIndex:

>>> ts['2000-01-03']
1.4641415817112928

即使DatetimeIndex是由时间戳对象组成的,我们也可以使用datetime对象作为密钥:

>>> ts[datetime.datetime(2000, 1, 3)]
1.4641415817112928

访问类似于字典或列表中的查找,但功能更强大。例如,我们可以用字符串甚至混合对象进行切片:

>>> ts['2000-01-03':'2000-01-05']
2000-01-03    1.464142
2000-01-04    0.103077
2000-01-05    0.762656
Freq: B, dtype: float64
>>> ts['2000-01-03':datetime.datetime(2000, 1, 5)]
2000-01-03    1.464142
2000-01-04    0.103077
2000-01-05    0.762656
Freq: B, dtype: float64
>>> ts['2000-01-03':datetime.date(2000, 1, 5)]
2000-01-03   -0.807669
2000-01-04    0.029802
2000-01-05   -0.434855
Freq: B, dtype: float64 

甚至可以使用部分字符串来选择条目组。如果我们只对二月感兴趣,我们可以简单地写道:

>>> ts['2000-02']
2000-02-01    0.277544
2000-02-02   -0.844352
2000-02-03   -1.900688
2000-02-04   -0.120010
2000-02-07   -0.465916
2000-02-08   -0.575722
2000-02-09    0.426153
2000-02-10    0.720124
2000-02-11    0.213050
2000-02-14   -0.604096
2000-02-15   -1.275345
2000-02-16   -0.708486
2000-02-17   -0.262574
2000-02-18    1.898234
2000-02-21    0.772746
2000-02-22    1.142317
2000-02-23   -1.461767
2000-02-24   -2.746059
2000-02-25   -0.608201
2000-02-28    0.513832
2000-02-29   -0.132000

查看从 3 月到 5 月的所有条目,包括:

>>> ts['2000-03':'2000-05']
2000-03-01    0.528070
2000-03-02    0.200661
 ...
2000-05-30    1.206963
2000-05-31    0.230351
Freq: B, dtype: float64 

时间序列可以在时间上向前或向后移动。索引保持不变,值移动:

>>> small_ts = ts['2000-02-01':'2000-02-05']
>>> small_ts
2000-02-01    0.277544
2000-02-02   -0.844352
2000-02-03   -1.900688
2000-02-04   -0.120010
Freq: B, dtype: float64
>>> small_ts.shift(2)
2000-02-01         NaN
2000-02-02         NaN
2000-02-03    0.277544
2000-02-04   -0.844352
Freq: B, dtype: float64

为了在时间上向后移动,我们简单地使用负值:

>>> small_ts.shift(-2)
2000-02-01   -1.900688
2000-02-02   -0.120010
2000-02-03         NaN
2000-02-04         NaN
Freq: B, dtype: float64

重新采样时间序列

重采样描述时间序列数据的频率转换过程。在各种情况下,这是一种有用的技术,因为它通过将数据分组和聚合来促进理解。有可能根据每日温度数据创建一个新的时间序列,显示每周或每月的平均温度。另一方面,现实世界的数据可能不是以统一的时间间隔获取的,需要将观测值映射到统一的时间间隔,或者填充某些时间点的缺失值。这是重采样的两个主要使用方向:宁滨和聚集,以及填充缺失的数据。下采样和上采样也发生在其他领域,例如数字信号处理。在那里,下采样的过程通常被称为抽取,并执行采样率的降低。逆过程称为 插值,采样速率增加。我们将从数据分析的角度来看两个方向。

下采样时间序列数据

下采样减少数据中的样本数量。在这个缩减过程中,我们能够在数据点上应用聚合。让我们想象一个繁忙的机场,每小时有成千上万的人经过。机场管理局在主要区域安装了一个访客柜台,以便准确了解他们的机场有多繁忙。

他们每分钟都在从计数器接收数据。以下是一天的假设测量值,从 08:00 开始,600 分钟后的 18:00 结束:

>>> rng = pd.date_range('4/29/2015 8:00', periods=600, freq='T')
>>> ts = pd.Series(np.random.randint(0, 100, len(rng)), index=rng)
>>> ts.head()
2015-04-29 08:00:00     9
2015-04-29 08:01:00    60
2015-04-29 08:02:00    65
2015-04-29 08:03:00    25
2015-04-29 08:04:00    19

为了更好地了解一天的情况,我们可以将这个时间序列下采样到更大的间隔,例如 10 分钟。我们也可以选择聚合函数。默认聚合是取所有值并计算平均值:

>>> ts.resample('10min').head()
2015-04-29 08:00:00    49.1
2015-04-29 08:10:00    56.0
2015-04-29 08:20:00    42.0
2015-04-29 08:30:00    51.9
2015-04-29 08:40:00    59.0
Freq: 10T, dtype: float64

在我们的机场示例中,我们还对值的总和感兴趣,即给定时间范围内的游客总数。我们可以通过向how参数传递一个函数或函数名来选择聚合函数:

>>> ts.resample('10min', how='sum').head()
2015-04-29 08:00:00    442
2015-04-29 08:10:00    409
2015-04-29 08:20:00    532
2015-04-29 08:30:00    433
2015-04-29 08:40:00    470
Freq: 10T, dtype: int64

或者,我们可以通过重新采样到每小时一次的间隔来进一步缩短采样间隔:

>>> ts.resample('1h', how='sum').head()
2015-04-29 08:00:00    2745
2015-04-29 09:00:00    2897
2015-04-29 10:00:00    3088
2015-04-29 11:00:00    2616
2015-04-29 12:00:00    2691
Freq: H, dtype: int64

我们也可以要求其他东西。例如,一小时内通过我们机场的最大人数是多少:

>>> ts.resample('1h', how='max').head()
2015-04-29 08:00:00    97
2015-04-29 09:00:00    98
2015-04-29 10:00:00    99
2015-04-29 11:00:00    98
2015-04-29 12:00:00    99
Freq: H, dtype: int64

或者,如果我们对更不寻常的度量感兴趣,我们可以定义一个自定义函数。例如,我们可能有兴趣为每个小时选择一个随机样本:

>>> import random
>>> ts.resample('1h', how=lambda m: random.choice(m)).head()
2015-04-29 08:00:00    28
2015-04-29 09:00:00    14
2015-04-29 10:00:00    68
2015-04-29 11:00:00    31
2015-04-29 12:00:00     5 

如果通过字符串指定函数,Pandas 会使用高度优化的版本。

可以作为how参数的内置函数有:summeanstd, semmaxminmedianfirstlastohlcohlc指标在金融界很流行。它代表开-高-低-关。OHLC 图表是说明金融工具价格随时间变化的典型方法。

虽然在我们的机场,这个指标可能没有那么有价值,但我们仍然可以计算出来:

>>> ts.resample('1h', how='ohlc').head()
 open  high  low  close
2015-04-29 08:00:00     9    97    0     14
2015-04-29 09:00:00    68    98    3     12
2015-04-29 10:00:00    71    99    1      1
2015-04-29 11:00:00    59    98    0      4
2015-04-29 12:00:00    56    99    3
 55

对时间序列数据进行上采样

在上采样中,时间序列的频率增加。因此,我们的样本点比数据点多。其中一个主要问题是如何解释我们没有度量的系列中的条目。

让我们从一天中每小时的数据开始:

>>> rng = pd.date_range('4/29/2015 8:00', periods=10, freq='H')
>>> ts = pd.Series(np.random.randint(0, 100, len(rng)), index=rng)
>>> ts.head()
2015-04-29 08:00:00    30
2015-04-29 09:00:00    27
2015-04-29 10:00:00    54
2015-04-29 11:00:00     9
2015-04-29 12:00:00    48
Freq: H, dtype: int64

如果我们将向上采样到每 15 分钟采集一次的数据点,我们的时间序列将扩展为NaN值:

>>> ts.resample('15min')
>>> ts.head()
2015-04-29 08:00:00    30
2015-04-29 08:15:00   NaN
2015-04-29 08:30:00   NaN
2015-04-29 08:45:00   NaN
2015-04-29 09:00:00    27

处理缺失值的方法有多种,可以通过fill_method关键字参数控制重新采样。值可以向前或向后填充:

>>> ts.resample('15min', fill_method='ffill').head()
2015-04-29 08:00:00    30
2015-04-29 08:15:00    30
2015-04-29 08:30:00    30
2015-04-29 08:45:00    30
2015-04-29 09:00:00    27
Freq: 15T, dtype: int64
>>> ts.resample('15min', fill_method='bfill').head()
2015-04-29 08:00:00    30
2015-04-29 08:15:00    27
2015-04-29 08:30:00    27
2015-04-29 08:45:00    27
2015-04-29 09:00:00    27

使用limit参数,可以控制要填充的缺失值的数量:

>>> ts.resample('15min', fill_method='ffill', limit=2).head()
2015-04-29 08:00:00    30
2015-04-29 08:15:00    30
2015-04-29 08:30:00    30
2015-04-29 08:45:00   NaN
2015-04-29 09:00:00    27
Freq: 15T, dtype: float64

如果要在重采样过程中调整标签,可以使用loffset关键字参数:

>>> ts.resample('15min', fill_method='ffill', limit=2, loffset='5min').head()
2015-04-29 08:05:00    30
2015-04-29 08:20:00    30
2015-04-29 08:35:00    30
2015-04-29 08:50:00   NaN
2015-04-29 09:05:00    27
Freq: 15T, dtype: float64

还有另一种方式填写缺失值。我们可以使用一种算法来构建新的数据点,以某种方式适合现有的点,以某种方式定义。这个过程叫做插值。

我们可以让 Pandas 为我们插入一个时间序列:

>>> tsx = ts.resample('15min')
>>> tsx.interpolate().head()
2015-04-29 08:00:00    30.00
2015-04-29 08:15:00    29.25
2015-04-29 08:30:00    28.50
2015-04-29 08:45:00    27.75
2015-04-29 09:00:00    27.00
Freq: 15T, dtype: float64

我们看到默认的interpolate方法——线性插值——正在运行。Pandas 在两个现存点之间呈现线性关系。

Pandas 支持十几个interpolation功能,其中一些需要安装scipy库。我们不会在本章介绍interpolation方法,但我们鼓励您自己探索各种方法。正确的interpolation方法将取决于您的应用要求。

时区处理

虽然默认情况下,Pandas 对象不知道时区,但许多现实世界的应用程序会使用时区。和使用时间一样,时区也不是小事:你知道哪些国家有夏令时吗?你知道这些国家的时区是什么时候转换的吗?令人欣慰的是,Pandas 基于两个广受欢迎且经过验证的实用程序库的时区功能来处理时间和日期:pytzdateutil:

>>> t = pd.Timestamp('2000-01-01')
>>> t.tz is None
True

要提供时区信息,可以使用tz关键字参数:

>>> t = pd.Timestamp('2000-01-01', tz='Europe/Berlin')
>>> t.tz
<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>

这同样适用于ranges:

>>> rng = pd.date_range('1/1/2000 00:00', periods=10, freq='D', tz='Europe/London')
>>> rng
DatetimeIndex(['2000-01-01', '2000-01-02', '2000-01-03', '2000-01-04', '2000-01-05', '2000-01-06', '2000-01-07', '2000-01-08','2000-01-09', '2000-01-10'], dtype='datetime64[ns]', freq='D', tz='Europe/London')

时区对象也可以预先构建:

>>> import pytz
>>> tz = pytz.timezone('Europe/London')
>>> rng = pd.date_range('1/1/2000 00:00', periods=10, freq='D', tz=tz)
>>> rng
DatetimeIndex(['2000-01-01', '2000-01-02', '2000-01-03', '2000-01-04', '2000-01-05', '2000-01-06', '2000-01-07', '2000-01-08', '2000-01-09', '2000-01-10'], dtype='datetime64[ns]', freq='D', tz='Europe/London')

有时,您已经有一个时区不知道的时间序列对象,您想让时区知道。tz_localize功能有助于在时区感知对象和时区不感知对象之间切换:

>>> rng = pd.date_range('1/1/2000 00:00', periods=10, freq='D')
>>> ts = pd.Series(np.random.randn(len(rng)), rng)
>>> ts.index.tz is None
True
>>> ts_utc = ts.tz_localize('UTC')
>>> ts_utc.index.tz
<UTC>

要将时区感知对象移动到其他时区,可以使用tz_convert方法:

>>> ts_utc.tz_convert('Europe/Berlin').index.tz
<DstTzInfo 'Europe/Berlin' LMT+0:53:00 STD>

最后,要从对象中分离任何时区信息,可以将None传递给tz_converttz_localize:

>>> ts_utc.tz_convert(None).index.tz is None
True
>>> ts_utc.tz_localize(None).index.tz
 is None
True

超时

除了作为DatetimeIndex构建模块的强大的时间戳对象,Pandas 0.15 中还引入了另一个有用的数据结构——时间增量。时间增量也可以作为指数的基础,在本例中为TimedeltaIndex

时间增量是以不同单位表示的时间差异。Pandas 中的Timedelta类是 Python 标准库中datetime.timedelta的子类。与其他 Pandas 数据结构一样,时间增量可以由多种输入构建:

>>> pd.Timedelta('1 days')
Timedelta('1 days 00:00:00')
>>> pd.Timedelta('-1 days 2 min 10s 3us')
Timedelta('-2 days +23:57:49.999997')
>>> pd.Timedelta(days=1,seconds=1)
Timedelta('1 days 00:00:01')

如您所料,Timedeltas允许基本运算:

>>> pd.Timedelta(days=1) + pd.Timedelta(seconds=1)
Timedelta('1 days 00:00:01')

to_datetime类似,有一个to_timedelta函数可以将字符串或字符串列表解析成 Timedelta 结构或TimedeltaIndices:

>>> pd.to_timedelta('20.1s')
Timedelta('0 days 00:00:20.100000')

我们可以创建一个timedeltas的索引来代替绝对日期。例如,想象一下从火山测量。我们可能想进行测量,但从给定的日期开始索引,例如最后一次喷发的日期。我们可以创建一个以过去七天为条目的timedelta索引:

>>> pd.to_timedelta(np.arange(7), unit='D')
TimedeltaIndex(['0 days', '1 days', '2 days', '3 days', '4 days', '5 days', '6 days'], dtype='timedelta64[ns]', freq=None)

然后,我们可以使用从上次喷发开始编制索引的时间序列数据。如果我们有许多火山爆发的测量数据(可能来自多个火山),我们将有一个指数,使这些数据的比较和分析更加容易。例如,我们可以问在火山爆发后的第三天到第五天之间是否有典型的模式。这个问题用DatetimeIndex来回答不是不可能的,但是TimedeltaIndex让这种探索变得方便多了。

时间序列标绘

Pandas 对绘图有很大的支持,时间序列数据也是如此。

作为第一个示例,让我们获取一些月度数据并绘制出来:

>>> rng = pd.date_range(start='2000', periods=120, freq='MS')
>>> ts = pd.Series(np.random.randint(-10, 10, size=len(rng)), rng).cumsum()
>>> ts.head()
2000-01-01    -4
2000-02-01    -6
2000-03-01   -16
2000-04-01   -26
2000-05-01   -24
Freq: MS, dtype: int64

由于 matplotlib 是在引擎盖下使用的,我们可以传递一个熟悉的参数来绘图,例如 c 表示颜色,或者 title 表示图表标题:

>>> ts.plot(c='k', title='Example time series')
>>> plt.show()

下图显示了一个示例时间序列图:

Time series plotting

我们可以在 2 年和 5 年内叠加一个总图:

>>> ts.resample('2A').plot(c='0.75', ls='--')
>>> ts.resample('5A').plot(c='0.25', ls='-.')

下图显示了重新采样的 2 年图:

Time series plotting

下图显示了 5 年的重采样图:

Time series plotting

我们也可以将这种图表传递给plot方法。plot方法的返回值是一个AxesSubplot,可以让我们自定义剧情的很多方面。这里我们将X轴上的标签值设置为时间序列中的年份值:

>>> plt.clf()
>>> tsx = ts.resample('1A')
>>> ax = tsx.plot(kind='bar', color='k')
>>> ax.set_xticklabels(tsx.index.year)

Time series plotting

让我们想象一下我们有四个我们想同时绘制的时间序列。我们生成一个由 1000 × 4 个随机值组成的矩阵,并将每一列视为一个独立的时间序列:

>>> plt.clf()
>>> ts = pd.Series(np.random.randn(1000), index=pd.date_range('1/1/2000', periods=1000))
>>> df = pd.DataFrame(np.random.randn(1000, 4), index=ts.index, columns=['A', 'B', 'C', 'D'])
>>> df = df.cumsum()>>> df.plot(color=['k', '0.75', '0.5', '0.25'], ls='--')

Time series plotting

总结

在本章中,我们展示了如何在 Pandas 中处理时间序列。我们介绍了两种索引类型DatetimeIndexTimedeltaIndex,并深入探讨了它们的构建模块。Pandas 自带多功能助手功能,可以消除解析各种格式的日期或生成固定频率序列的痛苦。对数据进行重采样有助于获得更为精简的数据,或者有助于将不同频率的各种数据集相互对齐。Pandas 的明确目标之一是使处理缺失数据变得容易,这也与上采样相关。

最后,我们展示了时间序列是如何可视化的。由于 matplotlib 和 Pandas 是天然的伙伴,我们发现我们也可以将之前关于 matplotlib 的知识重用到时间序列数据中。

在下一章中,我们将探索在文本文件和数据库中加载和存储数据的方法。

练习练习

练习 1 :找到一两个数据集的真实例子,可以合理地分配给以下几组:

  • 固定频率数据
  • 可变频率数据
  • 频率通常以秒为单位的数据
  • 频率以纳秒为单位的数据
  • 数据,其中TimedeltaIndex更可取

创建各种固定频率范围:

  • 2000 年 1 月 1 日凌晨 1 时至 2 时每分钟
  • 从 2000-01-01 开始,每周两小时
  • 2000 年每周六和周日的条目
  • 2000 年、2001 年和 2002 年,如果是工作日,每个月的每个星期一都有一个条目

六、与数据库交互

数据分析从数据开始。因此,使用易于设置、操作且数据访问本身不会成为问题的数据存储系统是有益的。简而言之,我们希望拥有易于嵌入到我们的数据分析流程和工作流中的数据库系统。在本书中,我们主要关注数据库交互的 Python 方面,我们将学习如何将数据放入和取出 Pandas 数据结构。

有许多方法可以存储数据。在本章中,我们将学习与三个主要类别交互:文本格式、二进制格式和数据库。我们将重点介绍两种存储解决方案,MongoDB 和 Redis。MongoDB 是一个面向文档的数据库,这很容易开始,因为我们可以存储 JSON 文档,并且不需要预先定义模式。Redis 是一种流行的内存数据结构存储,在此基础上可以构建许多应用程序。使用 Redis 作为快速键值存储是可能的,但是 Redis 也支持列表、集合、散列、位数组,甚至像 HyperLogLog 这样的高级数据结构。

与文本格式的数据交互

文本是一种很好的媒介,也是一种简单的信息交流方式。以下陈述摘自道格·麦克洛伊的名言:编写程序来处理文本流,因为那是通用接口。

在本节中,我们将开始从文本文件读取数据和向文本文件写入数据。

从文本格式读取数据

通常,一个系统的原始数据日志存储在多个文本文件中,随着时间的推移,这些文本文件会积累大量信息。谢天谢地,在 Python 中与这类文件交互很简单。

Pandas 支持许多将数据从文本文件读入数据框对象的功能。最简单的一个就是read_csv()功能。让我们从一个小示例文件开始:

$ cat example_data/ex_06-01.txt
Name,age,major_id,sex,hometown
Nam,7,1,male,hcm
Mai,11,1,female,hcm
Lan,25,3,female,hn
Hung,42,3,male,tn
Nghia,26,3,male,dn
Vinh,39,3,male,vl
Hong,28,4,female,dn

型式

cat是 Unix shell 命令,可用于将文件内容打印到屏幕上。

在上面的示例文件中,每列由逗号分隔,第一行是标题行,包含列名。要将数据文件读入 DataFrame 对象,我们键入以下命令:

>>> df_ex1 = pd.read_csv('example_data/ex_06-01.txt')
>>> df_ex1
 Name  age  major_id     sex hometown
0    Nam    7         1    male      hcm
1    Mai   11         1  female      hcm
2    Lan   25         3  female       hn
3   Hung   42         3    male       tn
4  Nghia   26         3    male       dn
5   Vinh   39         3    male       vl
6   Hong   28         4  female       dn

我们看到read_csv函数使用逗号作为文本文件中列之间的默认分隔符,第一行自动用作列的标题。如果我们想更改此设置,我们可以使用sep参数更改分隔符号,并在示例文件没有标题行的情况下设置header=None

请参见下面的示例:

$ cat example_data/ex_06-02.txt
Nam     7       1       male    hcm
Mai     11      1       female  hcm
Lan     25      3       female  hn
Hung    42      3       male    tn
Nghia   26      3       male    dn
Vinh    39      3       male    vl
Hong    28      4       female  dn

>>> df_ex2 = pd.read_csv('example_data/ex_06-02.txt',
 sep = '\t', header=None)
>>> df_ex2
 0   1  2       3    4
0    Nam   7  1    male  hcm
1    Mai  11  1  female  hcm
2    Lan  25  3  female   hn
3   Hung  42  3    male   tn
4  Nghia  26  3    male   dn
5   Vinh  39  3    male   vl
6   Hong  28  4  female   dn

我们也可以使用等于所选行索引的header将特定行设置为标题行。同样,当我们想要使用数据文件中的任何一列作为 DataFrame 的列索引时,我们将index_col设置为该列的名称或索引。我们再用第二个数据文件example_data/ex_06-02.txt来说明这一点:

>>> df_ex3 = pd.read_csv('example_data/ex_06-02.txt',
 sep = '\t', header=None,
 index_col=0)
>>> df_ex3
 1  2       3    4
0
Nam     7  1    male  hcm
Mai    11  1  female  hcm
Lan    25  3  female   hn
Hung   42  3    male   tn
Nghia  26  3    male   dn
Vinh   39  3    male   vl
Hong   28  4  female   dn

除了这些参数,我们还有很多有用的参数可以帮助我们更有效地将数据文件加载到 Pandas 对象中。下表显示了一些常见参数:

|

参数

|

价值

|

描述

|
| --- | --- | --- |
| dtype | 列类型的类型名称或字典 | 设置数据或列的数据类型。默认情况下,它会尝试推断最合适的数据类型。 |
| skiprows | 列表式或整数式 | 要跳过的行数(从 0 开始)。 |
| na_values | 列表式或字典式,默认无 | 要识别为NA / NaN的值。如果 dict 被通过,这可以在每列的基础上设置。 |
| true_values | 目录 | 要转换为布尔真值的值列表。 |
| false_values | 目录 | 要转换为布尔假的值列表。 |
| keep_default_na | Booldefault True | 如果存在na_values参数并且keep_default_naFalse,则默认的 NaN 值被忽略,否则它们被附加到 |
| thousands | Strdefault None | 千位分隔符 |
| nrows | Intdefault None | 限制从文件中读取的行数。 |
| error_bad_lines | Booleandefault True | 如果设置为真,将返回一个数据帧,即使在解析过程中出现错误。 |

除了read_csv()功能,我们在 Pandas 中还有一些其他的解析功能:

|

功能

|

描述

|
| --- | --- |
| read_table | 将常规分隔文件读入数据框 |
| read_fwf | 将固定宽度格式化行的表格读入数据框 |
| read_clipboard | 从剪贴板中读取文本并传递到read_table。它对于从网页转换表格很有用 |

在某些情况下,我们无法使用这些函数自动解析磁盘中的数据文件。在这种情况下,我们也可以打开文件,通过阅读器进行迭代,标准库中的 CSV 模块支持:

$ cat example_data/ex_06-03.txt
Nam     7       1       male    hcm
Mai     11      1       female  hcm
Lan     25      3       female  hn
Hung    42      3       male    tn      single
Nghia   26      3       male    dn      single
Vinh    39      3       male    vl
Hong    28      4       female  dn

>>> import csv
>>> f = open('data/ex_06-03.txt')
>>> r = csv.reader(f, delimiter='\t')
>>> for line in r:
>>>    print(line)
['Nam', '7', '1', 'male', 'hcm']
['Mai', '11', '1', 'female', 'hcm']
['Lan', '25', '3', 'female', 'hn']
['Hung', '42', '3', 'male', 'tn', 'single']
['Nghia', '26', '3', 'male', 'dn', 'single']
['Vinh', '39', '3', 'male', 'vl']
['Hong', '28', '4', 'female', 'dn']

将数据写入文本格式

我们看到了如何将数据从文本文件加载到 Pandas 的数据结构中。现在,我们将学习如何将数据从程序的数据对象导出到文本文件。对应read_csv()功能,我们还有to_csv()功能,Pandas 支持。让我们看看下面的例子:

>>> df_ex3.to_csv('example_data/ex_06-02.out', sep = ';')

结果将如下所示:

$ cat example_data/ex_06-02.out
0;1;2;3;4
Nam;7;1;male;hcm
Mai;11;1;female;hcm
Lan;25;3;female;hn
Hung;42;3;male;tn
Nghia;26;3;male;dn
Vinh;39;3;male;vl
Hong;28;4;female;dn

如果我们想在将数据写入磁盘文件时跳过标题行或索引列,我们可以为标题和索引参数设置一个False值:

>>> import sys
>>> df_ex3.to_csv(sys.stdout, sep='\t',
 header=False, index=False)
7       1       male    hcm
11      1       female  hcm
25      3       female  hn
42      3       male    tn
26      3       male    dn
39      3       male    vl
28      4       female  dn

我们也可以通过在columns参数中指定数据帧的列的子集来将它们写入文件:

>>> df_ex3.to_csv(sys.stdout, columns=[3,1,4],
 header=False, sep='\t')
Nam     male    7       hcm
Mai     female  11      hcm
Lan     female  25      hn
Hung    male    42      tn
Nghia   male    26      dn
Vinh    male    39      vl
Hong    female  28      dn

有了 series 对象,我们可以使用相同的函数将数据写入文本文件,参数大多与上面相同。

与二进制格式的数据交互

我们可以用 pickle 模块读写 Python 对象的二进制序列化,可以在标准库中找到。如果您使用需要很长时间才能创建的对象,比如一些机器学习模型,对象序列化可能会很有用。通过酸洗这些对象,可以更快地访问该模型。它还允许您以标准化的方式分发 Python 对象。

Pandas 包括支持开箱腌制。相关的方法是read_pickle()to_pickle()功能,可以轻松地读写文件中的数据。这些方法将以 pickle 格式将数据写入磁盘,这是一种方便的短期存储格式:

>>> df_ex3.to_pickle('example_data/ex_06-03.out')
>>> pd.read_pickle('example_data/ex_06-03.out')
 1  2       3    4
0
Nam     7  1    male  hcm
Mai    11  1  female  hcm
Lan    25  3  female   hn
Hung   42  3    male   tn
Nghia  26  3    male   dn
Vinh   39  3    male   vl
Hong   28  4  female   dn

HDF5

HDF5 不是数据库,而是数据模型和文件格式。它适用于一写多读数据集。HDF5 文件包括两种对象:数据集和组,数据集是类似数组的数据集合,组是类似文件夹的容器,保存数据集和其他组。Python 中有一些与 HDF5 格式交互的接口,比如h5py使用大家熟悉的 NumPy 和 Python 构造,比如字典和 NumPy 数组语法。有了h5py,我们有了 HDF5 API 的高级接口,这有助于我们开始。但是,在本书中,我们将介绍另一个用于这种格式的库,称为 PyTables,它可以很好地处理 Pandas 对象:

>>> store = pd.HDFStore('hdf5_store.h5')
>>> store
<class 'pandas.io.pytables.HDFStore'>
File path: hdf5_store.h5
Empty

我们创建了一个空的 HDF5 文件,名为hdf5_store.h5。现在,我们可以向文件中写入数据,就像向dict添加键值对一样:

>>> store['ex3'] = df_ex3
>>> store['name'] = df_ex2[0]
>>> store['hometown'] = df_ex3[4]
>>> store
<class 'pandas.io.pytables.HDFStore'>
File path: hdf5_store.h5
/ex3                  frame        (shape->[7,4])
/hometown             series       (shape->[1])
/name                 series       (shape->[1])

存储在 HDF5 文件中的对象可以通过指定对象键来检索:

>>> store['name']
0      Nam
1      Mai
2      Lan
3     Hung
4    Nghia
5     Vinh
6     Hong
Name: 0, dtype: object

一旦我们完成了与 HDF5 文件的交互,我们就关闭它以释放文件句柄:

>>> store.close()
>>> store
<class 'pandas.io.pytables.HDFStore'>
File path: hdf5_store.h5
File is CLOSED

对于使用 HDF5 格式,还有其他受支持的有用功能。如果需要处理大量数据,您应该更详细地探索两个库-pytablesh5py

与 MongoDB 中的数据交互

许多应用程序需要比文本文件更强大的存储系统,这就是为什么许多应用程序使用数据库来存储数据。数据库有很多种,但有两大类:关系数据库,它支持一种称为 SQL 的标准声明性语言,以及所谓的 NoSQL 数据库,它们通常能够在没有预定义模式的情况下工作,并且数据实例更适合描述为文档,而不是行。

MongoDB 是一种 NoSQL 数据库,它将数据存储为文档,这些文档在集合中组合在一起。文档被表示为 JSON 对象。它存储数据的速度快、可扩展,查询数据也很灵活。要在 Python 中使用 MongoDB,我们需要导入pymongo包,并通过传递主机名和端口打开到数据库的连接。假设我们有一个 MongoDB 实例,运行在默认主机(localhost)和端口(27017)上:

>>> import pymongo
>>> conn = pymongo.MongoClient(host='localhost', port=27017)

如果我们不把任何参数放入pymongo.MongoClient()功能,它会自动使用默认的主机和端口。

在下一步中,我们将与 MongoDB 实例中的数据库进行交互。我们可以列出实例中可用的所有数据库:

>>> conn.database_names()
['local']
>>> lc = conn.local
>>> lc
Database(MongoClient('localhost', 27017), 'local')

上面的片段说我们的 MongoDB 实例只有一个数据库,名为‘local’。如果我们指向的数据库和集合不存在,MongoDB 将根据需要创建它们:

>>> db = conn.db
>>> db
Database(MongoClient('localhost', 27017), 'db')

每个数据库都包含文档组,称为集合。我们可以将它们理解为关系数据库中的表。要列出数据库中所有现有的集合,我们使用collection_names()函数:

>>> lc.collection_names()
['startup_log', 'system.indexes']
>>> db.collection_names()
[]

我们的db数据库还没有任何收藏。让我们创建一个名为person的集合,并将数据框对象中的数据插入其中:

>>> collection = db.person
>>> collection
Collection(Database(MongoClient('localhost', 27017), 'db'), 'person')
>>> # insert df_ex2 DataFrame into created collection
>>> import json
>>> records = json.load(df_ex2.T.to_json()).values()
>>> records
dict_values([{'2': 3, '3': 'male', '1': 39, '4': 'vl', '0': 'Vinh'}, {'2': 3, '3': 'male', '1': 26, '4': 'dn', '0': 'Nghia'}, {'2': 4, '3': 'female', '1': 28, '4': 'dn', '0': 'Hong'}, {'2': 3, '3': 'female', '1': 25, '4': 'hn', '0': 'Lan'}, {'2': 3, '3': 'male', '1': 42, '4': 'tn', '0': 'Hung'}, {'2': 1, '3':'male', '1': 7, '4': 'hcm', '0': 'Nam'}, {'2': 1, '3': 'female', '1': 11, '4': 'hcm', '0': 'Mai'}])
>>> collection.insert(records)
[ObjectId('557da218f21c761d7c176a40'),
 ObjectId('557da218f21c761d7c176a41'),
 ObjectId('557da218f21c761d7c176a42'),
 ObjectId('557da218f21c761d7c176a43'),
 ObjectId('557da218f21c761d7c176a44'),
 ObjectId('557da218f21c761d7c176a45'),
 ObjectId('557da218f21c761d7c176a46')]

df_ex2在加载到字典之前被转置并转换为 JSON 字符串。insert()函数从df_ex2接收我们创建的词典,并将其保存到集合中。

如果我们想要列出集合中的所有数据,我们可以执行以下命令:

>>> for cur in collection.find():
>>>     print(cur)
{'4': 'vl', '2': 3, '3': 'male', '1': 39, '_id': ObjectId('557da218f21c761d7c176
a40'), '0': 'Vinh'}
{'4': 'dn', '2': 3, '3': 'male', '1': 26, '_id': ObjectId('557da218f21c761d7c176
a41'), '0': 'Nghia'}
{'4': 'dn', '2': 4, '3': 'female', '1': 28, '_id': ObjectId('557da218f21c761d7c1
76a42'), '0': 'Hong'}
{'4': 'hn', '2': 3, '3': 'female', '1': 25, '_id': ObjectId('557da218f21c761d7c1
76a43'), '0': 'Lan'}
{'4': 'tn', '2': 3, '3': 'male', '1': 42, '_id': ObjectId('557da218f21c761d7c176
a44'), '0': 'Hung'}
{'4': 'hcm', '2': 1, '3': 'male', '1': 7, '_id': ObjectId('557da218f21c761d7c176
a45'), '0': 'Nam'}
{'4': 'hcm', '2': 1, '3': 'female', '1': 11, '_id': ObjectId('557da218f21c761d7c
176a46'), '0': 'Mai'}

如果我们想要在某些条件下从创建的集合中查询数据,我们可以使用find()函数,并传入描述我们想要检索的文档的字典。返回的结果是游标类型,它支持迭代器协议:

>>> cur = collection.find({'3' : 'male'})
>>> type(cur)
pymongo.cursor.Cursor
>>> result = pd.DataFrame(list(cur))
>>> result
 0   1  2     3    4                       _id
0   Vinh  39  3  male   vl  557da218f21c761d7c176a40
1  Nghia  26  3  male   dn  557da218f21c761d7c176a41
2   Hung  42  3  male   tn  557da218f21c761d7c176a44
3    Nam   7  1  male  hcm  557da218f21c761d7c176a45

有时候,我们希望删除 MongdoDB 中的数据。我们需要做的就是向集合上的remove()方法传递一个查询:

>>> # before removing data
>>> pd.DataFrame(list(collection.find()))
 0   1  2       3    4                       _id
0   Vinh  39  3    male   vl  557da218f21c761d7c176a40
1  Nghia  26  3    male   dn  557da218f21c761d7c176a41
2   Hong  28  4  female   dn  557da218f21c761d7c176a42
3    Lan  25  3  female   hn  557da218f21c761d7c176a43
4   Hung  42  3    male   tn  557da218f21c761d7c176a44
5    Nam   7  1    male  hcm  557da218f21c761d7c176a45
6    Mai  11  1  female  hcm  557da218f21c761d7c176a46

>>> # after removing records which have '2' column as 1 and '3' column as 'male'
>>> collection.remove({'2': 1, '3': 'male'})
{'n': 1, 'ok': 1}
>>> cur_all = collection.find();
>>> pd.DataFrame(list(cur_all))
 0   1  2       3    4                       _id
0   Vinh  39  3    male   vl  557da218f21c761d7c176a40
1  Nghia  26  3    male   dn  557da218f21c761d7c176a41
2   Hong  28  4  female   dn  557da218f21c761d7c176a42
3    Lan  25  3  female   hn  557da218f21c761d7c176a43
4   Hung  42  3    male   tn  557da218f21c761d7c176a44
5    Mai  11  1  female  hcm  557da218f21c761d7c176a46

我们逐步学习了如何在集合中插入、查询和删除数据。现在,我们将展示如何在 MongoDB 中更新集合中的现有数据:

>>> doc = collection.find_one({'1' : 42})
>>> doc['4'] = 'hcm'
>>> collection.save(doc)
ObjectId('557da218f21c761d7c176a44')
>>> pd.DataFrame(list(collection.find()))
 0   1  2       3    4                       _id
0   Vinh  39  3    male   vl  557da218f21c761d7c176a40
1  Nghia  26  3    male   dn  557da218f21c761d7c176a41
2   Hong  28  4  female   dn  557da218f21c761d7c176a42
3    Lan  25  3  female   hn  557da218f21c761d7c176a43
4   Hung  42  3    male  hcm  557da218f21c761d7c176a44
5    Mai  11  1  female  hcm  557da218f21c761d7c176a46

下表显示了为在 MongoDB 中操作文档提供快捷方式的方法:

|

更新方法

|

描述

|
| --- | --- |
| inc() | 递增数字字段 |
| set() | 将某些字段设置为新值 |
| unset() | 从文档中删除字段 |
| push() | 将值附加到文档中的数组上 |
| pushAll() | 将几个值追加到文档的数组中 |
| addToSet() | 仅当数组不存在时,才将值添加到数组中 |
| pop() | 移除数组的最后一个值 |
| pull() | 从数组中移除所有出现的值 |
| pullAll() | 从数组中移除任何值集的所有匹配项 |
| rename() | 重命名字段 |
| bit() | 通过按位运算更新值 |

与 Redis 中的数据交互

Redis 是一种高级的键值存储,其中的值可以是不同的类型:字符串、列表、集合、排序集合或散列。Redis 像 memcached 一样将数据存储在内存中,但它可以持久存储在磁盘上,不像 memcached 那样没有这样的选项。Redis 支持快速读写,大约每秒 100,000 次 set 或 get 操作。

要与 Redis 交互,我们需要将Redis-py模块安装到 Python 中,Python 在pypi上有,可以用pip安装:

$ pip install redis

现在,我们可以通过数据库服务器的主机和端口连接到 Redis。我们假设已经安装了一个 Redis 服务器,该服务器使用默认主机(localhost)和端口(6379)参数运行:

>>> import redis
>>> r = redis.StrictRedis(host='127.0.0.1', port=6379)
>>> r
StrictRedis<ConnectionPool<Connection<host=localhost,port=6379,db=0>>>

作为在 Redis 中存储数据的第一步,我们需要定义哪种数据结构适合我们的需求。在本节中,我们将介绍 Redis 中常用的四种数据结构:简单值、列表、集合和有序集。尽管数据以许多不同的数据结构存储在 Redis 中,但每个值都必须与一个键相关联。

简单值

这是 Redis 中最基本的一种价值。对于 Redis 中的每个键,我们还有一个可以有数据类型的值,比如字符串、整数或双精度。让我们从一个设置和获取 Redis 数据的例子开始:

>>> r.set('gender:An', 'male')
True
>>> r.get('gender:An')
b'male'

在这个例子中,我们希望将一个名为An的人的性别信息存储到 Redis 中。我们的关键是gender:An,我们的价值是male。它们都是一种字符串。

set()函数接收两个参数:键和值。第一个参数是键,第二个参数是值。如果我们想更新这个键的值,我们只需要再次调用函数并更改第二个参数的值。Redis 会自动更新它。

get()函数将检索我们的键的值,该值作为参数传递。在这种情况下,我们要获取关键gender:An的性别信息。

在第二个例子中,我们向您展示了另一种值类型,一个整数:

>>> r.set('visited_time:An', 12)
True
>>> r.get('visited_time:An')
b'12'
>>> r.incr('visited_time:An', 1)
13
>>> r.get('visited_time:An')
b'13'

我们看到了一个新的函数,incr(),用来将 key 的值增加一个给定量。如果我们的键不存在,RedisDB 将以给定的增量作为值创建该键。

列表

我们有一些方法来与 Redis 中的列表值进行交互。以下示例使用rpush()lrange()函数将列表数据放入数据库和从数据库获取列表数据:

>>> r.rpush('name_list', 'Tom')
1L
>>> r.rpush('name_list', 'John')
2L
>>> r.rpush('name_list', 'Mary')
3L
>>> r.rpush('name_list', 'Jan')
4L
>>> r.lrange('name_list', 0, -1)
[b'Tom', b'John', b'Mary', b'Jan']
>>> r.llen('name_list')
4
>>> r.lindex('name_list', 1)
b'John'

除了我们在示例中使用的rpush()lrange()函数之外,我们还想介绍另外两个函数。首先,llen()函数用于获取给定键在 Redis 中的列表长度。lindex()功能是检索列表项目的另一种方式。我们需要向函数传递两个参数:一个键和列表中项目的索引。下表列出了用 Redis 处理列表数据结构的其他一些强大功能:

|

功能

|

描述

|
| --- | --- |
| rpushx(name, value) | 如果名称存在,将值推到列表名称的尾部 |
| rpop(name) | 移除并返回列表名称的最后一项 |
| lset(name, index, value) | 将列表名称索引位置的项目设置为输入值 |
| lpushx(name,value) | 如果名称存在,将值推送到列表名称的开头 |
| lpop(name) | 移除并返回列表名称的第一项 |

设置

这个数据结构也类似于列表类型。但是,与列表相反,我们不能在集合中存储重复的值:

>>> r.sadd('country', 'USA')
1
>>> r.sadd('country', 'Italy')
1
>>> r.sadd('country', 'Singapore')
1
>>> r.sadd('country', 'Singapore')
0
>>> r.smembers('country')
{b'Italy', b'Singapore', b'USA'}
>>> r.srem('country', 'Singapore')
1
>>> r.smembers('country')
{b'Italy', b'USA'}

对应列表数据结构,我们还有很多功能可以获取、设置、更新或删除集合中的项目。它们列在支持的集合数据结构函数中,如下表所示:

|

功能

|

描述

|
| --- | --- |
| sadd(name, values) | 用键名向集合中添加值 |
| scard(name) | 用键名返回集合中的元素数 |
| smembers(name) | 用键名返回集合的所有成员 |
| srem(name, values) | 用键名从集合中移除值 |

有序集

当我们将数据添加到名为分数的集合中时,有序集合数据结构具有额外的属性。有序集合将使用分数来确定集合中元素的顺序:

>>> r.zadd('person:A', 10, 'sub:Math')
1
>>> r.zadd('person:A', 7, 'sub:Bio')
1
>>> r.zadd('person:A', 8, 'sub:Chem')
1
>>> r.zrange('person:A', 0, -1)
[b'sub:Bio', b'sub:Chem', b'sub:Math']
>>> r.zrange('person:A', 0, -1, withscores=True)
[(b'sub:Bio', 7.0), (b'sub:Chem', 8.0), (b'sub:Math', 10.0)]

通过使用zrange(name, start, end)函数,我们可以从排序的集合中获得一个范围内的值,该范围位于默认情况下按升序排序的开始和结束分数之间。如果要改变way的排序方式,可以将desc参数设置为Truewithscore参数用于我们想要获得分数和返回值的情况。返回类型是一个(值、分数)对的列表,如您在上面的示例中所见。

有关有序集合的更多可用功能,请参见下表:

|

功能

|

描述

|
| --- | --- |
| zcard(name) | 用关键字名称返回排序集中的元素数量 |
| zincrby(name, value, amount=1) | 将排序后的具有关键字名称的集合中的值的分数按数量递增 |
| zrangebyscore(name, min, max, withscores=False, start=None, num=None) | 从排序集中返回一个值范围,关键字名称的得分在最小值和最大值之间。如果withscorestrue,则返回分数和数值。如果给定了开始和num,返回范围的一部分 |
| zrank(name, value) | 返回一个从 0 开始的值,该值指示带键名的排序集中的值的等级 |
| zrem(name, values) | 从具有关键字名称的排序集中移除成员值 |

总结

我们已经完成了在不同的常用存储机制中与数据交互的基础知识,从简单的存储机制(如文本文件)到更结构化的存储机制(如 HDF5),再到更复杂的数据存储系统(如 MongoDB 和 Redis)。最合适的存储类型将取决于您的用例。数据存储层技术的选择在数据处理系统的总体设计中起着重要的作用。有时,我们需要组合各种数据库系统来存储我们的数据,例如数据的复杂性、系统的性能或计算需求。

练习练习

  • 选择一组您选择的数据,并为其设计存储选项。考虑文本文件、HDF5、文档数据库和数据结构存储作为可能的持久选项。还要评估更新或删除一个特定项目有多困难(例如,通过某种度量,多少行代码)。哪种存储类型最容易设置?哪种存储类型支持最灵活的查询?
  • 第 3 章Pandas 数据分析中,我们看到可以用 Pandas 创建分级索引。举个例子,假设你有超过 100 万居民的每个城市的数据,我们有一个两级指数,所以我们可以处理单个城市,也可以处理整个国家。您将如何用本章中介绍的各种存储选项来表示这种层次关系:文本文件、HDF5、MongoDB 和 Redis?从长远来看,你认为什么最方便?

七、数据分析应用示例

在本章中,我们希望让您熟悉典型的数据准备任务和分析技术,因为熟练准备、分组和重塑数据是成功数据分析的重要组成部分。

虽然准备数据似乎是一项平凡的任务——通常也是如此——但这是我们不能跳过的一步,尽管我们可以通过使用 Pandas 等工具来努力简化它。

为什么准备是必要的?因为大多数有用的数据将来自现实世界,并且会有缺陷、包含错误或者是零碎的。

数据准备之所以有用,还有更多原因:它让你与原材料密切接触。了解您的输入有助于您及早发现潜在的错误,并对结果建立信心。

以下是一些数据准备场景:

  • 一个客户交给你三个文件,每个文件包含一个单一地质现象的时间序列数据,但是观察到的数据被记录在不同的时间间隔,并使用不同的分隔符
  • 机器学习算法只能处理数字数据,但您的输入只包含文本标签
  • 你拿到了一个新兴服务的网络服务器的原始日志,你的任务是根据现有的访问者行为,对增长策略提出建议

数据收集

用于数据收集的工具库非常庞大,虽然我们将重点讨论 Python,但我们也想提一些有用的工具。如果它们在您的系统上可用,并且您希望大量处理数据,那么它们值得学习。

有一组工具属于 UNIX 传统,它强调文本处理,因此在过去的四十年中,开发了许多高性能和经过战斗考验的工具来处理文本。常见的工具有:sedgrepawksortuniqtrcuttailhead。它们做的是非常基础的事情,比如从文件中过滤掉行(grep)或列(cut),替换文本(sedtr)或只显示文件的一部分(headtail)。

我们只想用一个例子来证明这些工具的力量。

假设你拿到了一个网络服务器的日志文件,你对 IP 地址的分布感兴趣。

日志文件的每一行都包含一个常用日志服务器格式的条目(您可以从http://ita.ee.lbl.gov/html/contrib/EPA-HTTP.html下载该数据集):

$ cat epa-html.txt
wpbfl2-45.gate.net [29:23:56:12] "GET /Access/ HTTP/1.0" 200 2376ebaca.icsi.net [30:00:22:20] "GET /Info.html HTTP/1.0" 200 884

例如,我们想知道某些用户访问我们网站的频率。

我们只对第一列感兴趣,因为这是可以找到 IP 地址或主机名的地方。之后,我们需要统计每台主机出现的次数,最后以友好的方式显示结果。

sort | uniq -c节是我们这里的主力:它首先对数据进行排序,uniq -c将保存出现的次数以及值。sort -nr | head -15 是我们的格式化部分;我们按数字(-n)和反向(-r)排序,只保留前 15 个条目。

用管子把它们连在一起:

$ cut -d ' ' -f 1 epa-http.txt | sort | uniq -c | sort -nr | head -15
294 sandy.rtptok1.epa.gov
292 e659229.boeing.com
266 wicdgserv.wic.epa.gov
263 keyhole.es.dupont.com
248 dwilson.pr.mcs.net
176 oea4.r8stw56.epa.gov
174 macip26.nacion.co.cr
172 dcimsd23.dcimsd.epa.gov
167 www-b1.proxy.aol.com
158 piweba3y.prodigy.com
152 wictrn13.dcwictrn.epa.gov
151 nntp1.reach.com
151 inetg1.arco.com
149 canto04.nmsu.edu
146 weisman.metrokc.gov

只需一个命令,我们就可以将顺序服务器日志转换为访问我们站点的最常见主机的有序列表。我们还看到,在我们的顶级用户中,我们的访问量似乎没有很大的差异。

还有更多小的有用的工具,以下只是其中的一小部分:

  • csvkit:这是一套使用表格文件格式之王 CSV 的工具
  • jq:这是一款轻量级灵活的命令行 JSON 处理器
  • xmlstarlet:这是一个工具,支持带 XPath 的 XML 查询,等等
  • q:这在文本文件上运行 SQL

在 UNIX 命令行结束的地方,轻量级语言接管了。您可能只能从文本中获得印象,但您的同事可能会更喜欢 matplotlib 生成的视觉表示,如图表或漂亮的图形。

Python 及其数据工具生态系统比命令行要通用得多,但对于首次探索和简单操作来说,命令行的有效性往往是无与伦比的。

清洁数据

大多数真实世界的数据会有一些缺陷,因此需要先经过一个清理步骤。我们从一个小文件开始。尽管该文件仅包含四行,但它将允许我们演示清理数据集的过程:

$ cat small.csv
22,6.1
41,5.7
 18,5.3*
29,NA

请注意,该文件有一些问题。包含值的行都是逗号分隔的,但是我们有缺失(NA)和可能不干净(5.3*)的值。尽管如此,我们可以将该文件加载到数据框中:

>>> import pandas as pd
>>> df = pd.read_csv("small.csv")
>>> df
 22   6.1
0  41   5.7
1  18  5.3*
2  29   NaN

Pandas 用第一排作为header,但这不是我们想要的:

>>> df = pd.read_csv("small.csv", header=None)
>>> df
 0     1
0  22   6.1
1  41   5.7
2  18  5.3*
3  29   NaN

这样更好,但是我们希望提供自己的列名,而不是数值:

>>> df = pd.read_csv("small.csv", names=["age", "height"])
>>> df
 age height
0   22    6.1
1   41    5.7
2   18   5.3*
3   29    NaN

age列看起来不错,因为 Pandas 已经推断出了想要的类型,但是height还不能解析成数值:

>>> df.age.dtype
dtype('int64')
>>> df.height.dtype
dtype('O')

如果我们试图将height列强制转换为浮点值,Pandas 将报告一个异常:

>>> df.height.astype('float')
ValueError: invalid literal for float(): 5.3*

我们可以将任何可解析的值作为一个浮点数使用,并使用convert_objects方法丢弃其余的值:

>>> df.height.convert_objects(convert_numeric=True)
0    6.1
1    5.7
2    NaN
3    NaN
Name: height, dtype: float64

如果我们事先知道数据集中不需要的字符,我们可以用一个定制的转换器函数来扩充read_csv方法:

>>> remove_stars = lambda s: s.replace("*", "")
>>> df = pd.read_csv("small.csv", names=["age", "height"],
 converters={"height": remove_stars})
>>> df
 age height
0   22    6.1
1   41    5.7
2   18    5.3
3   29     NA

现在我们终于可以让高度栏更有用一点了。我们可以为其分配更新的版本,该版本具有喜欢的类型:

>>> df.height = df.height.convert_objects(convert_numeric=True)
>>> df
 age  height
0   22     6.1
1   41     5.7
2   18     5.3
3   29     NaN

如果我们只想保留完整的条目,我们可以删除任何包含未定义值的行:

>>> df.dropna()
 age  height
0   22     6.1
1   41     5.7
2   18     5.3

我们可以使用默认高度,也许是一个固定值:

>>> df.fillna(5.0)
 age  height
0   22     6.1
1   41     5.7
2   18     5.3
3   29     5.0

另一方面,我们也可以使用现有值的平均值:

>>> df.fillna(df.height.mean())
 age  height
0   22     6.1
1   41     5.7
2   18     5.3
3   29     5.7

最后三个数据框是完整和正确的,这取决于您在处理缺失值时对正确的定义。特别是,这些列具有所请求的类型,并准备好进行进一步的分析。哪个数据帧最适合将取决于手头的任务。

过滤

即使我们有干净且可能正确的数据,我们可能只想使用它的一部分,或者我们可能想检查异常值。离群点是由于可变性或测量误差而远离其他观察的观察点。在这两种情况下,我们都希望减少数据集中的元素数量,使其与进一步的处理更加相关。

在这个的例子中,我们将尝试寻找潜在的异常值。我们将使用美国能源信息管理局记录的欧洲布伦特原油现货价格。原始 Excel 数据可从http://www.eia.gov/dnav/pet/hist_xls/rbrted.xls获得(可在第二张工作表中找到)。我们稍微清理了一下数据(清理过程是本章末尾练习的一部分),并将使用以下数据框,包含 1987 年至 2015 年的 7160 个条目:

>>> df.head()
 date  price
0 1987-05-20  18.63
1 1987-05-21  18.45
2 1987-05-22  18.55
3 1987-05-25  18.60
4 1987-05-26  18.63
>>> df.tail()
 date  price
7155 2015-08-04  49.08
7156 2015-08-05  49.04
7157 2015-08-06  47.80
7158 2015-08-07  47.54
7159 2015-08-10  48.30

尽管许多人都知道油价——无论是从新闻还是加油站——但让我们暂时忘记我们所知道的一切。我们可以先问两个极端:

>>> df[df.price==df.price.min()]
 date  price
2937 1998-12-10    9.1
>>> df[df.price==df.price.max()]
 date   price
5373 2008-07-03  143.95

另一种发现潜在异常值的方法是要求最偏离平均值的值。我们可以首先使用np.abs函数计算与平均值的偏差:

>>> np.abs(df.price - df.price.mean())
0       26.17137
1       26.35137
...
7157     2.99863
7158     2.73863 
7159     3.49863

我们现在可以从标准偏差的倍数(我们选择 2.5)来比较这个偏差:

>>> import numpy as np
>>> df[np.abs(df.price - df.price.mean()) > 2.5 * df.price.std()]
 date   price
5354 2008-06-06  132.81
5355 2008-06-09  134.43
5356 2008-06-10  135.24
5357 2008-06-11  134.52
5358 2008-06-12  132.11
5359 2008-06-13  134.29
5360 2008-06-16  133.90
5361 2008-06-17  131.27
5363 2008-06-19  131.84
5364 2008-06-20  134.28
5365 2008-06-23  134.54
5366 2008-06-24  135.37
5367 2008-06-25  131.59
5368 2008-06-26  136.82
5369 2008-06-27  139.38
5370 2008-06-30  138.40
5371 2008-07-01  140.67
5372 2008-07-02  141.24
5373 2008-07-03  143.95
5374 2008-07-07  139.62
5375 2008-07-08  134.15
5376 2008-07-09  133.91
5377 2008-07-10  135.81
5378 2008-07-11  143.68
5379 2008-07-14  142.43
5380 2008-07-15  136.02
5381 2008-07-16  133.31
5382 2008-07-17  134.16

我们看到 2008 年夏天的那几天一定很特别。果然,不难找到标题为2007–08石油冲击的原因和后果的文章和随笔。我们仅仅通过查看数据就发现了这些事件的踪迹。

我们可以每十年分别问一次上面的问题。我们首先让我们的数据框架看起来更像一个时间序列:

>>> df.index = df.date
>>> del df["date"]
>>> df.head()
 price
date
1987-05-20  18.63
1987-05-21  18.45
1987-05-22  18.55
1987-05-25  18.60
1987-05-26  18.63

我们可以过滤掉八十年代:

>>> decade = df["1980":"1989"]
>>> decade[np.abs(decade.price - decade.price.mean()) > 2.5 * decade.price.std()]
 price
date
1988-10-03  11.60
1988-10-04  11.65
1988-10-05  11.20
1988-10-06  11.30
1988-10-07  11.35

我们观察到在可获得的数据(1987-1989)中,1988 年的秋天显示出石油价格的轻微上涨。同样,在九十年代,我们看到我们有一个更大的偏差,在 1990 年秋天:

>>> decade = df["1990":"1999"]
>>> decade[np.abs(decade.price - decade.price.mean()) > 5 * decade.price.std()]
 price
date
1990-09-24  40.75
1990-09-26  40.85
1990-09-27  41.45
1990-09-28  41.00
1990-10-09  40.90
1990-10-10  40.20
1990-10-11  41.15

过滤数据的用例还有很多。空间和时间是典型的单位:您可能希望按州或城市过滤人口普查数据,或者按季度过滤经济数据。可能性是无穷无尽的,将由你的项目驱动。

合并数据

的情况很常见:你有多个数据源,但是为了对内容做陈述,你宁愿把它们组合起来。幸运的是,Pandas 的连接和合并功能在组合、连接或对齐数据时,抽象掉了大部分痛苦。它也以高度优化的方式做到了这一点。

在两个数据帧具有相似形状的情况下,一个接一个地追加可能是有用的。也许AB是产品,一个数据框包含商店中每种产品售出的商品数量:

>>> df1 = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]})
>>> df1
 A  B
0  1  4
1  2  5
2  3  6
>>> df2 = pd.DataFrame({'A': [4, 5, 6], 'B': [7, 8, 9]})
>>> df2
 A  B
0  4  7
1  5  8
2  6  9
>>> df1.append(df2)
 A  B
0  1  4
1  2  5
2  3  6
0  4  7
1  5  8
2  6  9

有时,我们不会关心原始数据帧的索引:

>>> df1.append(df2, ignore_index=True)
 A  B
0  1  4
1  2  5
2  3  6
3  4  7
4  5  8
5  6  9

pd.concat函数提供了一种更灵活的组合对象的方法,它采用任意数量的序列、数据帧或面板作为输入。默认行为类似于追加:

>>> pd.concat([df1, df2])
 A  B
0  1  4
1  2  5
2  3  6
0  4  7
1  5  8
2  6  9

默认的concat操作沿着行或索引追加两个帧,对应于轴 0。要沿着列连接,我们可以传入 axis 关键字参数:

>>> pd.concat([df1, df2], axis=1)
 A  B  A  B
0  1  4  4  7
1  2  5  5  8
2  3  6  6  9

我们可以添加关键字来创建分层索引。

>>> pd.concat([df1, df2], keys=['UK', 'DE'])
 A  B
UK 0  1  4
 1  2  5
 2  3  6
DE 0  4  7
 1  5  8
 2  6  9

如果您希望稍后引用数据框的某些部分,这可能会很有用。我们使用ix索引器:

>>> df3 = pd.concat([df1, df2], keys=['UK', 'DE'])
>>> df3.ix["UK"]
 A  B
0  1  4
1  2  5
2  3  6

数据框类似于数据库表。因此,Pandas 在它们身上实现类似 SQL 的连接操作并不奇怪。令人惊讶的是,这些操作高度优化且速度极快:

>>> import numpy as np
>>> df1 = pd.DataFrame({'key': ['A', 'B', 'C', 'D'],
 'value': range(4)})
>>> df1
 key  value
0   A      0
1   B      1
2   C      2
3   D      3
>>> df2 = pd.DataFrame({'key': ['B', 'D', 'D', 'E'], 'value': range(10, 14)})
>>> df2
 key  value
0   B     10
1   D     11
2   D     12
3   E     13

如果我们在key上合并,我们得到一个内部连接。这通过基于连接谓词组合原始数据帧的列值来创建新的数据帧,这里使用key属性:

>>> df1.merge(df2, on='key')
 key  value_x  value_y
0   B        1       10
1   D        3       11
2   D        3       12

左、右和全连接可以由how参数指定:

>>> df1.merge(df2, on='key', how='left')
 key  value_x  value_y
0   A        0      NaN
1   B        1       10
2   C        2      NaN
3   D        3       11
4   D        3       12
>>> df1.merge(df2, on='key', how='right')
 key  value_x  value_y
0   B        1       10
1   D        3       11
2   D        3       12
3   E      NaN       13
>>> df1.merge(df2, on='key', how='outer')
 key  value_x  value_y
0   A        0      NaN
1   B        1       10
2   C        2      NaN
3   D        3       11
4   D        3       12
5   E      NaN       13

合并方法可以用 how 参数指定。下表显示了与 SQL 相比较的方法:

|

合并方法

|

SQL 联接名

|

描述

|
| --- | --- | --- |
| left | 左外连接 | 仅使用左侧框架中的按键。 |
| right | 右外连接 | 仅使用右侧框架中的关键点。 |
| outer | 完全外部连接 | 使用两个帧的联合密钥。 |
| inner | 内部连接 | 使用两个帧的关键点的交集。 |

重塑数据

我们看到了如何组合数据帧,但有时我们在单个数据结构中拥有所有正确的数据,但这种格式对于某些任务是不切实际的。我们再次从一些人工天气数据开始:

>>> df
 date    city  value
0   2000-01-03  London      6
1   2000-01-04  London      3
2   2000-01-05  London      4
3   2000-01-03  Mexico      3
4   2000-01-04  Mexico      9
5   2000-01-05  Mexico      8
6   2000-01-03  Mumbai     12
7   2000-01-04  Mumbai      9
8   2000-01-05  Mumbai      8
9   2000-01-03   Tokyo      5
10  2000-01-04   Tokyo      5
11  2000-01-05   Tokyo      6

如果我们想计算每个城市的最高温度,我们可以将数据按城市分组,然后使用max函数:

>>> df.groupby('city').max()
 date  value
city
London  2000-01-05      6
Mexico  2000-01-05      9
Mumbai  2000-01-05     12
Tokyo   2000-01-05      6

然而,如果我们每次都必须把数据整理成表格,我们可以更有效一点,首先创建一个重新整形的数据框架,把日期作为索引,城市作为列。

我们可以用pivot函数创建这样一个数据帧。参数是索引(我们使用日期)、列(我们使用城市)和值(存储在原始数据框的值列中):

>>> pv = df.pivot("date", "city", "value")
>>> pv
city date         London  Mexico  Mumbai  Tokyo
2000-01-03       6       3      12      5
2000-01-04       3       9       9      5
2000-01-05       4       8       8      6

我们可以直接在这个新的数据帧上使用max功能:

>>> pv.max()
city
London     6
Mexico     9
Mumbai    12
Tokyo      6
dtype: int64

有了更合适的形状,其他操作也变得更容易。例如,为了找到每天的最高温度,我们可以简单地提供一个额外的轴参数:

>>> pv.max(axis=1)
date
2000-01-03    12
2000-01-04     9
2000-01-05     8
dtype: int64

数据聚合

作为最后一个主题,我们将研究如何获得聚合数据的精简视图。Pandas 自带很多内置的聚合功能。我们已经在第三章Pandas 数据分析中看到了describe功能。这也适用于部分数据。我们再次从一些人工数据开始,包括每个城市日照时数的测量值和日期:

>>> df.head()
 country     city        date  hours
0  Germany  Hamburg  2015-06-01      8
1  Germany  Hamburg  2015-06-02     10
2  Germany  Hamburg  2015-06-03      9
3  Germany  Hamburg  2015-06-04      7
4  Germany  Hamburg  2015-06-05      3

要查看每个city的摘要,我们使用分组数据集上的describe功能:

>>> df.groupby("city").describe()
 hours
city
Berlin     count  10.000000
 mean    6.000000
 std     3.741657
 min     0.000000
 25%     4.000000
 50%     6.000000
 75%     9.750000
 max    10.000000
Birmingham count  10.000000
 mean    5.100000
 std     2.078995
 min     2.000000
 25%     4.000000
 50%     5.500000
 75%     6.750000
 max     8.000000

在某些data sets上,按多个属性分组会很有用。通过输入两个列名,我们可以大致了解每个国家和日期的日照时间:

>>> df.groupby(["country", "date"]).describe()
 hours country date
France  2015-06-01 count  5.000000
 mean   6.200000
 std    1.095445
 min    5.000000
 25%    5.000000
 50%    7.000000
 75%    7.000000
 max    7.000000
 2015-06-02 count  5.000000
 mean   3.600000
 std    3.577709
 min    0.000000
 25%    0.000000
 50%    4.000000
 75%    6.000000
 max    8.000000
UK      2015-06-07 std    3.872983
 min    0.000000
 25%    2.000000
 50%    6.000000
 75%    8.000000
 max    9.000000

我们也可以计算单个统计:

>>> df.groupby("city").mean()
 hours
city
Berlin        6.0
Birmingham    5.1
Bordeax       4.7
Edinburgh     7.5
Frankfurt     5.8
Glasgow       4.8
Hamburg       5.5
Leipzig       5.0
London        4.8
Lyon          5.0
Manchester    5.2
Marseille     6.2
Munich        6.6
Nice          3.9
Paris         6.3

最后,我们可以用agg方法定义要应用于组的任何函数。上面本来可以这样用agg来写:

>>> df.groupby("city").agg(np.mean)
hours
city
Berlin        6.0
Birmingham    5.1
Bordeax       4.7
Edinburgh     7.5
Frankfurt     5.8
Glasgow       4.8
...

但是任意函数都是可能的。作为最后一个例子,我们定义了一个custom函数,它接受一个序列对象的输入,并计算最小和最大元素之间的差值:

>>> df.groupby("city").agg(lambda s: abs(min(s) - max(s)))
 hours
city
Berlin         10
Birmingham      6
Bordeax        10
Edinburgh       8
Frankfurt       9
Glasgow        10
Hamburg        10
Leipzig         9
London         10
Lyon            8
Manchester     10
Marseille      10
Munich          9
Nice           10
Paris           9

分组数据

数据探索期间的一个典型工作流程如下:

  • 您可以找到一个用于对数据进行分组的标准。也许你有沿着大陆的每个国家的 GDP 数据,你想问关于大陆的问题。这些问题通常会导致一些函数应用——你可能想计算每个大陆的平均国内生产总值。最后,您希望将这些数据存储在新的数据结构中,以便进一步处理。

  • 这里我们用一个更简单的例子。想象一些关于每天晴天小时数和城市的虚构天气数据:

    >>> df
     date    city  value
    0   2000-01-03  London      6
    1   2000-01-04  London      3
    2   2000-01-05  London      4
    3   2000-01-03  Mexico      3
    4   2000-01-04  Mexico      9
    5   2000-01-05  Mexico      8
    6   2000-01-03  Mumbai     12
    7   2000-01-04  Mumbai      9
    8   2000-01-05  Mumbai      8
    9   2000-01-03   Tokyo      5
    10  2000-01-04   Tokyo      5
    11  2000-01-05   Tokyo      6
    
    
  • groups属性返回包含唯一组和相应值的字典作为轴标签:

    >>> df.groupby("city").groups
    {'London': [0, 1, 2],
    'Mexico': [3, 4, 5],
    'Mumbai': [6, 7, 8],
    'Tokyo': [9, 10, 11]}
    
    
  • 虽然的结果是一个 GroupBy 对象,而不是一个 DataFrame,但是我们可以使用通常的索引符号来引用列:

    >>> grouped = df.groupby(["city", "value"])
    >>> grouped["value"].max()
    city
    London     6
    Mexico     9
    Mumbai    12
    Tokyo      6
    Name: value, dtype: int64
    >>> grouped["value"].sum()
    city
    London    13
    Mexico    20
    Mumbai    29
    Tokyo     16
    Name: value, dtype: int64
    
    
  • 我们看到,根据我们的数据集,孟买似乎是一个阳光明媚的城市。实现上述目标的另一种更详细的方法是:

    >>> df['value'].groupby(df['city']).sum()
    city
    London    13
    Mexico    20
    Mumbai    29
    Tokyo     16
    Name: value, dtype: 
    int64
    
    

总结

在这一章中,我们已经看到了处理数据帧的方法,从清理和过滤,到分组、聚合和整形。Pandas 使许多常见的操作变得非常容易,而更复杂的操作,如按多个属性进行旋转或分组,也经常可以表示为一行。清理和准备数据是数据探索和分析的重要组成部分。

下一章将简要介绍机器学习算法,该算法正在应用数据分析结果来做出决策或构建有用的产品。

练习练习

练习 1: 清洗:在关于过滤的部分,我们使用了欧洲布伦特原油现货价格,在网上可以找到一个 Excel 文档。拿着这个 Excel 电子表格,试着把它转换成一个 CSV 文档,准备和 Pandas 一起导入。

提示:有很多方法可以做到。我们使用了一个名为xls2csv.py的小工具,并且我们能够用一个辅助方法加载生成的 CSV 文件:

import datetime
import pandas as pd
def convert_date(s):
    parts = s.replace("(", "").replace(")", "").split(",")
	if len(parts) < 6:
	return datetime.date(1970, 1, 1)
	return datetime.datetime(*[int(p) for p in parts])
	df = pd.read_csv("RBRTEd.csv", sep=',', names=["date", "price"], converters={"date": convert_date}).dropna()

拿一个对你的工作很重要的数据集来说,或者如果你手头没有,那就拿一个你感兴趣的并且可以在网上找到的数据集来说。提前问一两个关于数据的问题。然后使用清理、过滤、分组和绘图技术来回答你的问题。

八、基于 scikit-learn 的机器学习模型

在前一章中,我们看到了如何执行数据管理、数据聚合和分组。在这一章中,我们将简要地看到不同 scikit-learn 模块对不同模型的工作,scikit-learn 中的数据表示,使用示例理解监督和非监督学习,以及测量预测性能。

机器学习模型概述

机器学习是人工智能的一个子领域,探索机器如何从数据中学习来分析结构、帮助决策和做出预测。1959 年,阿瑟·塞缪尔将机器学习定义为“赋予计算机学习能力而无需明确编程的研究领域。”

大量应用采用机器学习方法,如垃圾邮件过滤、光学字符识别、计算机视觉、语音识别、信用审批、搜索引擎和推荐系统。

机器学习的一个重要驱动因素是,所有部门的数据生成速度都在加快;无论是网络流量、文本或图像,还是传感器数据或科学数据集。更大的数据量给存储和处理系统带来了许多新的挑战。另一方面,许多学习算法会产生更好的结果,有更多的数据可以学习。近年来,由于各种硬任务(如语音识别或图像中的对象检测)的性能显著提高,该领域受到了广泛关注。没有智能算法的帮助,理解大量数据似乎是不可能的。

学习问题通常使用一组样本(通常用 N 或 N 表示)来构建模型,然后对模型进行验证,并用于预测看不见的数据的属性。

每个样本可能由单个或多个值组成。在机器学习的背景下,数据的属性被称为特征。

机器学习可以根据输入数据的性质来安排:

  • 监督学习
  • 无监督学习

在监督学习中,输入数据(通常用 x 表示)与目标标签(y)相关联,而在无监督学习中,我们只有未标记的输入数据。

监督学习可以进一步分解为以下问题:

  • 分类问题
  • 回归问题

分类问题有一组固定的目标标签、类或类别,而回归问题有一个或多个连续的输出变量。将电子邮件分类为垃圾邮件还是非垃圾邮件是一项具有两个目标标签的分类任务。预测房价——考虑到房屋的数据,如大小、年龄和一氧化氮浓度——是一项回归任务,因为价格是连续的。

无监督学习处理不带标签的数据集。一个典型的例子是聚类或自动分类。目标是将相似的项目组合在一起。相似性意味着什么将取决于上下文,在这样的任务中有许多相似性度量可以使用。

不同型号的 scikit-learn 模块

scikit-learn 库被组织成子模块。每个子模块包含用于某类机器学习模型和方法的算法和辅助方法。

以下是这些子模块的示例,包括一些示例模型:

|

子模块

|

描述

|

示例模型

|
| --- | --- | --- |
| 串 | 这就是无监督聚类 | KMeans 和 Ward |
| 分解 | 这就是降维 | 常设仲裁法院和 NMF |
| 全体 | 这涉及到基于集成的方法 | AdaBoostClassifier,AdaBoostRegressor,随机应变分类器,随机森林回归器 |
| 皱胃向左移 | 这代表潜在鉴别分析 | 皱胃向左移 |
| 线性模型 | 这是广义线性模型 | 线性回归,物流回归,套索和感知器 |
| 混合 | 这是混合物模型 | GMM 和维也纳国际中心 |
| 朴素贝叶斯 | 这包括基于贝叶斯定理的监督学习 | BaseNB 和 BernoulliNB,GaussianNB |
| 邻居 | 这些是 k 最近的邻居 | KNeighborsClassifier,LSHForest |
| 神经网络 | 这包括基于神经网络的模型 | BernoulliRBM |
| 树 | 决策树 | 决定反对分类者,决定反对者 |

虽然这些方法是多种多样的,但 scikit-learn 库通过向大多数这些算法公开一个常规接口,抽象出了许多不同之处。表中列出的所有示例算法都实现了一种fit方法,并且大多数算法也实现了预测。这些方法代表了机器学习的两个阶段。首先,使用fit方法在现有数据上训练模型。一旦经过训练,就可以使用模型来预测未知数据的类别或值。在接下来的部分中,我们将看到这两种方法都在发挥作用。

scikit-learn 库是 PyData 生态系统的一部分。它的代码库在过去的六年里稳步增长,拥有超过 100 个贡献者,是 scikit 工具包中最活跃和最受欢迎的一个。

sci kit-learn 中的数据表示

与机器学习的异构领域和应用相反,scikit-learn 中的数据表示没有那么多样化,许多算法期望的基本格式很简单——样本和特征的矩阵。

底层数据结构是numpyndarray。矩阵中的每一行对应一个样本,每一列对应一个特征的值。

在机器学习数据集的世界中也有类似于Hello World的东西;例如,起源于 1936 年的 Iris 数据集。使用 scikit-learn 的标准安装,您已经可以访问几个数据集,包括由 150 个样本组成的鸢尾,每个样本由从三种不同鸢尾花物种获取的四个测量值组成:

>>> import numpy as np
>>> from sklearn import datasets
>>> iris = datasets.load_iris()

数据集被打包成一堆,它只是字典的一个薄薄的包装:

>>> type(iris)
sklearn.datasets.base.Bunch
>>> iris.keys()
['target_names', 'data', 'target', 'DESCR', 'feature_names']

data键下,我们可以找到样本和特征的矩阵,并可以确认其形状:

>>> type(iris.data)
numpy.ndarray
>>> iris.data.shape
(150, 4)

data矩阵中的每个条目都已被标记,这些标记可以在target属性中查找:

>>> type(iris.target)
numpy.ndarray
>>> iris.target.shape
(150,)
>>> iris.target[:10]
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
>>> np.unique(iris.target)
array([0, 1, 2])

目标名称已编码。我们可以在target_names属性中查找对应的名称:

>>> iris.target_names
>>> array(['setosa', 'versicolor', 'virginica'], dtype='|S10')

这是许多数据集的基本结构,例如示例数据、目标值和目标名称。

该数据集中的单个条目有哪些特征?:

>>> iris.data[0]
array([ 5.1,  3.5,  1.4,  0.2])

这四个特征是对真花的测量:它们的萼片长度和宽度,和花瓣长度和宽度。已经检查了三种不同的品种:鸢尾-濑户鸢尾鸢尾-云芝鸢尾-北美鸢尾

机器学习试图回答以下问题:如果只给定花的萼片和花瓣长度的测量值,我们能预测花的种类吗?

在下一节中,我们将看到如何用 scikit-learn 来回答这个问题。

除了有关花卉的数据之外,scikit-learn 发行版中还包括一些其他数据集,如下所示:

  • 波士顿房价数据集(506 个样本和 13 个属性)
  • 手写数字数据集的光学识别(5620 个样本和 64 个属性)
  • 鸢尾植物数据库(150 个样本和 4 个属性)
  • Linnerud 数据集(30 个样本和 3 个属性)

一些数据集没有包括在内,但是它们可以很容易地按需获取(因为这些数据集通常稍大一些)。在这些数据集中,您可以找到一个房地产数据集和一个新闻语料库:

>>> ds = datasets.fetch_california_housing()
downloading Cal. housing from http://lib.stat.cmu.edu/modules.php?op=...
>>> ds.data.shape
(20640, 8)
>>> ds = datasets.fetch_20newsgroups()
>>> len(ds.data)
11314
>>> ds.data[0][:50]
u"From: lerxst@wam.umd.edu (where's my thing)\nSubjec"
>>> sum([len([w for w in sample.split()]) for sample in ds.data])
3252437

这些数据集是开始使用 scikit-learn 库的好方法,它们还将帮助您测试自己的算法。最后,scikit-learn 还包括创建人工数据集的函数(前缀为datasets.make_)。

如果你使用自己的数据集,你将不得不把它们做成 scikit-learn 所期望的形状,这可能是它自己的任务。Pandas 之类的工具让这个任务变得简单多了,Pandas 数据帧可以通过数据帧上的as_matrix()方法轻松导出到numpy.ndarray

监督学习——分类和回归

在这个部分,我们将展示分类和回归的简短示例。

分类问题无处不在:文档分类、欺诈检测、商业智能中的市场细分、生物信息学中的蛋白质功能预测。

尽管手工规则可能会为新数据分配一个类别或标签,但使用算法从现有数据中学习和归纳会更快。

我们将继续使用 Iris 数据集。在我们应用学习算法之前,我们希望通过查看一些值和图来获得数据的直觉。

所有测量值共享相同的维度,这有助于可视化各种箱线图中的差异:

Supervised learning – classification and regression

我们看到花瓣长度(第三个特征)表现出最大的变化,这可以表明这个特征在分类过程中的重要性。用两个维度绘制数据点,每个轴使用一个特征,这也很有见地。而且,事实上,我们之前的观察强化了花瓣长度可能是区分不同物种的一个很好的指标。鸢尾似乎也比其他两个物种更容易分离:

Supervised learning – classification and regression

从的可视化中,我们获得了解决问题的直觉。我们将使用一种被称为支持向量机 ( SVM )的监督方法来学习虹膜数据的分类器。应用编程接口将模型和数据分开,因此,第一步是实例化模型。在这种情况下,我们传递一个可选的关键字参数,以便以后能够查询模型的概率:

>>> from sklearn.svm import SVC
>>> clf = SVC(probability=True)

下一步是根据我们的训练数据拟合模型:

>>> clf.fit(iris.data, iris.target)
SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0,
 degree=3, gamma=0.0, kernel='rbf', max_iter=-1,
 probability=True, random_state=None, shrinking=True,
 tol=0.001, verbose=False)

有了这条线,我们已经在数据集上训练了我们的第一个机器学习模型。这个模型现在可以用来预测未知数据的种类。如果给定一些我们以前从未见过的测量值,我们可以在模型上使用预测方法:

>>> unseen = [6.0, 2.0, 3.0, 2.0]
>>> clf.predict(unseen)
array([1])
>>> iris.target_names[clf.predict(unseen)]
array(['versicolor'],
 dtype='|S10')

我们看到分类器给测量赋予了versicolor标签。如果我们想象我们的情节中的未知点,我们会发现这似乎是一个明智的预测:

Supervised learning – classification and regression

其实分类器对这个标签是比较确定的,我们可以用分类器上的predict_proba方法来查询:

>>> clf.predict_proba(unseen)
array([[ 0.03314121,  0.90920125,  0.05765754]])

我们的示例由四个特征组成,但是许多问题涉及更高维的数据集,并且许多算法在这些数据集上也运行良好。

我们想展示监督学习问题的另一种算法:线性回归。在线性回归中,我们试图预测一个或多个连续的输出变量,称为回归和,给定一个三维输入向量。回归意味着输出是连续的。之所以称之为线性,是因为输出将以参数的线性函数建模。

我们首先创建一个示例数据集,如下所示:

>>> import matplotlib.pyplot as plt
>>> X = [[1], [2], [3], [4], [5], [6], [7], [8]]
>>> y = [1, 2.5, 3.5, 4.8, 3.9, 5.5, 7, 8]
>>> plt.scatter(X, y, c='0.25')
>>> plt.show()

给定这些数据,我们希望学习一个线性函数来逼近数据并最小化预测误差,预测误差定义为观察响应和预测响应之间的平方和:

>>> from sklearn.linear_model import LinearRegression
>>> clf = LinearRegression()
>>> clf.fit(X, y)

许多模型将在训练过程中学习参数。这些参数在属性名称的末尾用一个下划线标记。在该模型中,coef_属性将保存线性回归问题的估计系数:

>>> clf.coef_
array([ 0.91190476])

我们也可以根据我们的数据绘制预测:

>>> plt.plot(X, clf.predict(X), '--', color='0.10', linewidth=1)

该图的输出如下:

Supervised learning – classification and regression

上图是人工数据的简单例子,但是线性回归有广泛的应用。如果给定房地产对象的特征,我们就可以学会预测价格。如果给出星系的特征,如大小、颜色或亮度,就有可能预测它们的距离。如果给出家庭收入和父母受教育程度的数据,我们可以说说他们孩子的成绩。

线性回归的应用无处不在,一个或多个自变量可能与一个或多个因变量相关联。

无监督学习——聚类和降维

很多已有数据没有标注。用无监督模型从没有标签的数据中学习仍然是可能的。探索性数据分析期间的典型任务是找到相关的项目或集群。我们可以想象 Iris 数据集,但是没有标签:

Unsupervised learning – clustering and dimensionality reduction

虽然没有标签的任务看起来要困难得多,但是一组测量值(在左下方)似乎与分开了。聚类算法的目标是识别这些组。

我们将在虹膜数据集上使用 K-Means 聚类(没有标签)。该算法期望预先指定聚类的数量,这可能是一个缺点。K-Means 将尝试通过最小化聚类内的平方和来将数据集划分为组。

例如,我们实例化n_clusters等于3KMeans模型:

>>> from sklearn.cluster import KMeans
>>> km = KMeans(n_clusters=3)

类似于监督算法,我们可以使用fit方法训练模型,但我们只传递数据,不传递目标标签:

>>> km.fit(iris.data)
KMeans(copy_x=True, init='k-means++', max_iter=300, n_clusters=3, n_init=10, n_jobs=1, precompute_distances='auto', random_state=None, tol=0.0001, verbose=0)

我们已经看到属性以下划线结尾。在这种情况下,算法为训练数据分配了一个标签,可以用labels_属性进行检查:

>>> km.labels_
array([1, 1, 1, 1, 1, 1, ..., 0, 2, 0, 0, 2], dtype=int32)

我们已经可以将这些算法的结果与我们已知的目标标签进行比较:

>>> iris.target
array([0, 0, 0, 0, 0, 0, ..., 2, 2, 2, 2, 2])

我们快速relabel结果来简化预测误差计算:

>>> tr = {1: 0, 2: 1, 0: 2}
>>> predicted_labels = np.array([tr[i] for i in km.labels_])
>>> sum([p == t for (p, t) in zip(predicted_labels, iris.target)])
134

从 150 个样本中,K-Mean 为 134 个样本分配了正确的标签,准确率约为 90%。下图用灰色显示了正确预测的算法点,用红色显示了错误标记的点:

Unsupervised learning – clustering and dimensionality reduction

作为无监督算法的另一个例子,我们来看看主成分分析 ( 主成分分析)。主成分分析的目的是寻找高维数据中最大方差的方向。一个目标是通过将更高维度的空间投影到更低维的子空间,同时保留大部分信息,从而减少维度的数量。

问题出现在各个领域。您已经收集了许多样本,每个样本包含数百或数千个特征。并不是手头现象的所有属性都同样重要。在我们的 Iris 数据集中,我们看到花瓣长度本身似乎是各种物种的一个很好的鉴别器。主成分分析旨在找到解释数据中大多数变化的主成分。如果我们对我们的分量进行相应的排序(技术上,我们通过特征值对协方差矩阵的特征向量进行排序),我们可以保留解释大部分数据的特征向量,忽略剩余的特征向量,从而降低数据的维数。

用 scikit-learn 运行 PCA 很简单。我们将不讨论实现细节,而是尝试通过在 Iris 数据集上运行 PCA 来给你一个 PCA 的直觉,以便给你另一个角度。

流程与我们到目前为止实施的流程相似。首先,我们实例化我们的模型;这一次,PCA 来自分解子模块。我们还引入了一种标准化方法,称为StandardScaler,它将从我们的数据中移除平均值,并换算成单位方差。这一步是许多机器学习算法的共同要求:

>>> from sklearn.decomposition import PCA
>>> from sklearn.preprocessing import StandardScaler

首先,我们用一个参数实例化我们的模型(该参数指定要减少到的维数),标准化我们的输入,并运行fit_transform函数,该函数将处理主成分分析的机制:

>>> pca = PCA(n_components=2)
>>> X = StandardScaler().fit_transform(iris.data)
>>> Y = pca.fit_transform(X)

结果是虹膜数据集中的维度从四个维度(萼片和花瓣的宽度和长度)减少到两个维度。需要注意的是,这个投影并不在现有的两个维度上,所以我们的新数据集并不仅仅由花瓣的长度和宽度组成。相反,两个新的维度将代表现有特征的混合。

以下散点图显示了转换后的数据集;从图表上看,看起来我们仍然保留了数据集的本质,尽管我们将维度的数量减半:

Unsupervised learning – clustering and dimensionality reduction

降维只是处理高维数据集的一种方式,有时会受到所谓降维诅咒的影响。

测量预测性能

我们已经看到机器学习过程由以下步骤组成:

  • 模型选择:我们首先为我们的数据选择一个合适的模型。我们有标签吗?有多少样品?数据可分离吗?我们有几个维度?由于这一步不重要,选择将取决于实际问题。截至 2015 年秋季,scikit-learn 文档包含一个非常受欢迎的流程图,名为选择正确的评估者。它很短,但信息量很大,值得仔细看看。
  • 训练:我们要把模型和数据放在一起,这通常发生在 scikit-learn 中模型的拟合方法中。
  • 应用:一旦我们训练好我们的模型,我们就能够对看不见的数据做出预测。

到目前为止,我们忽略了在训练和应用之间发生的一个重要步骤:模型测试和验证。在这一步中,我们要评估我们的模型学习得有多好。

学习的一个目标,特别是机器学习,是泛化。有限的一组观察是否足以对任何可能的观察做出陈述,这是一个更深层次的理论问题,在机器学习的专用资源中有所回答。

一个模型概括的好不好也是可以检验的。然而,重要的是训练和测试输入是分开的。模型在训练输入上表现良好,但在看不见的测试输入上失败的情况称为过拟合,这种情况并不少见。

基本方法是将可用数据分割成一个训练和测试集,scikit-learn 通过train_test_split功能帮助创建这种分割。

我们回到 Iris 数据集,再次执行 SVC。这次我们将在训练集上评估算法的性能。我们留出 40%的数据用于测试:

>>> from sklearn.cross_validation import train_test_split
>>> X_train, X_test, y_train, y_test = train_test_split(iris.data, iris.target, test_size=0.4, random_state=0)
>>> clf = SVC()
>>> clf.fit(X_train, y_train)

score 函数返回给定数据和标签的平均精度。我们通过测试集进行评估:

>>> clf.score(X_test, y_test)
 0.94999999999999996

该模型似乎表现良好,对看不见的数据的准确率约为 94%。我们现在可以开始调整模型参数(也称为超参数)以提高预测性能。这个周期会带来过度拟合的问题。一种解决方案是将输入数据分成三组:一组用于训练、验证和测试。超参数调整的迭代模型将发生在训练集和验证集之间,而最终评估将在测试集上完成。将数据集分为三个也减少了我们可以学习的样本数量。

交叉验证 ( CV )是一种不需要验证集,但仍能抵消过度拟合的技术。数据集被分割成k部分(称为折叠)。对于每个折叠,模型在k-1折叠上训练,并在剩余折叠上测试。精度被视为折叠的平均值。

我们将在 Iris 数据集上展示五重交叉验证,再次使用 SVC:

>>> from sklearn.cross_validation import cross_val_score
>>> clf = SVC()
>>> scores = cross_val_score(clf, iris.data, iris.target, cv=5)
>>> scores
array([ 0.96666667,  1\.    ,  0.96666667,  0.96666667,  1\.    ])
>>> scores.mean()
0.98000000000000009

有不同类实现的各种策略来拆分数据集进行交叉验证:KFoldStratifiedKFoldLeaveOneOutLeavePOutLeaveOneLabelOutLeavePLableOutShuffleSplitStratifiedShuffleSplitPredefinedSplit

模型验证是一个重要的步骤,对于开发健壮的机器学习解决方案是必要的。

总结

在这一章中,我们旋风般地浏览了最流行的 Python 机器学习库之一:scikit-learn。我们看到了这个库期望什么样的数据。真实世界的数据很少会马上被输入到估计器中。有了强大的库,比如 Numpy,尤其是 Pandas,您已经看到了如何检索、组合和形成数据。可视化库,如 matplotlib,有助于直观地了解数据集、问题和解决方案。

在这一章中,我们看了一个规范数据集,Iris 数据集。我们也从不同的角度来看它:作为有监督和无监督学习中的一个问题,以及作为模型验证的一个例子。

总之,我们已经研究了四种不同的算法:支持向量机、线性回归、K-Means 聚类和主成分分析。其中的每一个都值得探索,尽管我们只用了几行 Python 就实现了所有的算法,但我们几乎没有触及表面。

有许多方法可以让您进一步了解数据分析过程。已经出版了数百本关于机器学习的书,所以我们在这里只想强调几本:用 Python 构建机器学习系统**里歇特科埃略将更深入地探讨 scikit-learn,这是我们在本章中无法做到的。从数据中学习阿布-穆斯塔法、马格东-伊斯梅尔组成,是学习一般问题的坚实理论基础的巨大资源。

最有趣的应用将会出现在你自己的领域。然而,如果你想获得一些灵感,我们建议你看看www.kaggle.com网站,该网站运行预测建模和分析竞赛,既有趣又有洞察力。

练习练习

以下问题是有人监督还是无人监督?回归还是分类问题?:

  • 识别自动售货机内的硬币
  • 识别手写数字
  • 如果给定一些关于人和经济的事实,我们想要估计消费者支出
  • 如果给定地理、政治和历史事件的数据,我们希望预测侵犯人权行为最终将在何时何地发生
  • 如果考虑到鲸鱼及其物种的声音,我们想给尚未标记的鲸鱼录音贴上标签

查找最早的机器学习模型和算法之一:感知器。在 Iris 数据集上尝试感知器,并估计模型的准确性。感知器与本章中的 SVC 相比如何?

posted @ 2025-10-23 15:22  绝不原创的飞龙  阅读(9)  评论(0)    收藏  举报