《利用Python进行数据分析》笔记

《利用Python进行数据分析》是一本非常不错的 Cookbook。早在去年年底的时候我就一边看一边记录一遍敲代码,阅读完比较重要的章节,整理了这篇笔记。有时候有些 API 忘记了,就翻看查找,比直接看 pdf 要方便。现在为了查找更方便,将这篇笔记迁移到博客上来。

准备工作

Python 的优点

  • 动态编程(解释型语言)
  • 科学计算社区活跃,有不断改良的库
  • 胶水语言,轻松集成 C、C++ 以及 Fortran 代码
  • 同时适用于研究和原型构建及构件生产系统

缺点

  • 运行速度慢
  • Python 有全局解释器锁(GIL),防止解释器同时执行多条 Python 字节码指令的机制。因此,多线程并行代码不能在单个 Python 进程中执行。

重要的 Python 库

NumPy

科学计算基础包,提供快速的数组处理能力,并作为在算法之间传递数据的容器(ndarray)。

SciPy

一组专门解决科学计算中各种标准问题域的包的集合。

pandas

针对数据分析和预处理,提供快速处理结构化数据的大量数据结构和函数。

matplotlib

提供好用的交互式数据绘图环境,绘制交互式图表

IPython

一个增强的 Python shell,以提高编写、测试、调试 Python 代码的速度。主要用于交互式数据处理利用 matplotlib 对数据进行可视化处理

除开标准的基于终端的 IPython shell 外,还提供了:

  • 一个 HTML 笔记本;
  • 一个基于 Qt 框架的 GUI 控制台;
  • 用于交互式并行和分布式计算的基础架构。

引入惯例

1
2
3
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

np.arange引用的即是 NumPy 中的 arange 函数。在 Python 软件开发过程中,不建议直接引入类似 NumPy 这种大型库的全部内容(from numpy import *)。

IPython:一种交互式计算和开发环境

IPython 本身并没有提供任何的计算或数据分析功能,其设计目的是在交互式计算和软件开发这两个方面最大化地提高生产力。由于大部分的数据分析代码都含有探索式操作(试误法和迭代法),因此 IPython 将有助于提高工作效率。

Tab 键自动完成

Tab 键自动完成功能不只可以用于搜索命名空间和自动完成对象和模块属性,还可以匹配路径(输入正斜杠/后)。

内省

在变量的前面或后面加上一个问号(?)可以显示有关该对象的一些通用信息,这叫做对象内省(object introspection)

如果该对象是一个函数或实例方法,则其 docstring(如果有的话)也会被显示出来。使用??还将显示出该函数的源代码(如果可能的话)。

另外,?还可以用于搜索 IPython 命名空间,一些字符再配以通配符(*)即可显示出所有与该通配符表达式相匹配的名称:

1
2
3
4
5
6
In [12]: np.*load*?
np.__loader__
np.load
np.loads
np.loadtxt
np.pkgload

%run 命令

在 IPython 环境中,可以通过 %run命令将文件当作 Python 程序来运行。

1
In [13]: %run ipython_script_test.py

脚本是在一个空的命名空间中运行的(没有任何 import,也没有定义任何其他的变量),所以其行为应该跟在标准命令行环境中一样。

如果希望脚本能够访问在交互式 IPython 命名空间中定义的变量,应该使用%run -i

键盘快捷键

挑选一些不太熟又有些实用的:

命令 说明
Ctrl + A 将光标移动到行首
Ctrl + E 将光标移动到行尾
Ctrl + U 删除从光标开始至行首的文本
Ctrl + K 删除从光标开始至行尾的文本
Ctrl + L 清屏

魔术命令(Magic Command)

魔术命令是以百分号%为前缀的命令,为常见任务提供便利,或者让用户轻松控制 IPython 系统的行为。

例如,通过%timeit这个魔术命令可以检测任意 Python 语句(如矩阵乘法)的执行时间(多次执行计算平均执行时间,用于评估执行时间非常小的代码):

1
2
3
4
5
6
7
In [1]: import numpy as np
In [2]: a = np.random.randn(100, 100)
In [3]: %timeit np.dot(a, a)
The slowest run took 2714.09 times longer than the fastest. This could mean that an intermediate result is being cached.
10000 loops, best of 3: 41.9 µs per loop

魔术命令可以看作运行于 IPython 系统中的命令行程序。它们大都还有一些“命令行选项”,使用?即可查看其选项。例如:%reset?

魔术命令默认是可以不带百分号使用的,只要没有定义与其同名的变量即可。这个技术叫做 automagic,可以通过%automagic打开或关闭。

输入%quickref%magic可以直接访问文档。

更多常用的 IPython 魔术命令可见原书 p58(pdf p69)。

基于 Qt 的富 GUI 控制台

IPython 团队开发了一个基于 Qt 框架(目的是为终端应用程序提供诸如内嵌图片、多行编辑、语法高亮等富文本编辑功能)的 GUI 控制台。

1
ipython qtconsole --pylab=inline

matplotlib 集成与 pylab 模式

IPython 对各个 GUI 框架进行了专门的处理以使其能够和 shell 配合得天衣无缝。通常我们通过以下命令来在启动 IPython 时集成 matplotlib:

1
ipython --pylab

交互式调试器

IPython 紧密集成并加强了 Python 内置的 pdb 调试器。发生异常后马上输入%debug命令将会调用那个“事后”调试器,并直接跳转到引发异常的那个栈帧(stack frame)。

在这个调试器中,可以执行任意 Python 代码并查看各个栈帧中的一切对象和数据。默认是从最低级开始的(即错误发生的地方)。输入ud即可在栈跟踪的各级别之间切换。

执行%pdb命令可以让 IPython 在出现异常之后自动调用调试器。

更多请查看原书 p67(pdf p78)。

基本性能分析:%prun%run -p

代码的性能分析跟代码执行时间密切相关,但更关注耗费时间的位置。主要的 Python 性能分析工具是 cProfile 模块,它在执行一个程序或代码块时,会记录各函数所耗费的时间。

1
python -m cProfile cprof_example.py

这样执行输出的结果会按照函数名排列。通常会再用-s标记指定一个排序规则:

1
python -m cProfile -s cprof_example.py

除命令行用法之外,cProfile 还可以编程的方式分析人意代码块的性能。%prun的格式跟 cProfile 差不多,但它分析的是 Python 语句而不是整个 .py 文件:

1
In [4]: %prun -l 7 -s cumulative run_expriment()

执行%run -p -s cumulative cprof_example.py也能达到上面那条系统命令行命令一样的效果,但无需退出 IPython。

关于逐行分析函数性能,查看原书 p74(pdf p85)。

IPython HTML Notebook

IPython Notebook 是一种基于 Web 技术的交互式计算文档格式。通过下面这条命令即可启动:

1
ipython notebook --pylab=inline

NumPy 基础:数组和矢量计算

NumPy 是高性能科学计算和数据分析的基础包,其部分功能如下:

  • ndarray,一个具有矢量算数运算和复杂广播能力的快速且节省空间的多位数组。
  • 用于对整组数据进行快速运算的标准数学函数(无需编写循环)。
  • 用于读写磁盘数据的工具以及用于操作内存映射文件的工具。
  • 线性代数、随机数生成以及傅立叶变换功能。
  • 用于集成由 C、C++、Fortran 等语言编写的代码的工具。

ndarray:一种多维数组对象

ndarray 是一个通用的同构数据多维容器,也就是说,其中的所有元素必须是相同类型的。

几种属性

  • array.ndim # 维度
  • array.shape # 行数和列数
  • array.size # 元素个数

创建 ndarray

函数 说明
array 将输入数据(列表、元组、数组和其他序列类型)转换为 ndarray。默认直接复制输入数据
asarray 将输入转换为 ndarray,如果输入本身就是一个 ndarray 就不进行复制
arange 类似于内置的 range,但返回一个 ndarray 而非列表
ones 根据指定的形状和 dtype 创建一个全 1 数组
ones_like 以另一个数组为参数,并根据其形状和 dtyoe 创建一个全 1 数组
zeros、zeros_like 类似于 ones 和 ones_like,产生全 0 数组
empty、empty_like 创建新数组,只分配内存空间但不填充任何值
eye、identity 创建一个正方的 N*N 单位矩阵(对角线为 1,其余为 0)

基本的索引和切片

将一个标量值赋值给一个切片时,该值会自动传播到整个选区。跟列表最重要的区别在于,数组切片是原始数组的视图,这意味着数据不会被复制,视图上的任何修改都会直接反映到源数组上。如果想要得到 ndarray 切片的一份副本而非视图,就需要显式地进行复制操作,例如arr[5:8].copy()

布尔型索引

跟算术运算一样,数组的比较运算(如==)也是矢量化的。因此有:

1
2
3
4
In [61]: names = np.array(['Bob','Joe','Will','Bob','Will','Joe','Joe'])
In [63]: names == 'Bob'
Out[63]: array([ True, False, False, True, False, False, False], dtype=bool)

生成的布尔型数组可用于数组索引,并和切片、整数等混合使用。

要选择除某值以外的其他值,既可以使用不等于符号(!=),也可以通过负号(-)对条件进行否定。选取多个名字需要组合应用多个布尔条件,使用&(和)、|(或)之类的布尔算术运算符即可。Python 关键字 and 和 or 在布尔型数组中无效

通过布尔型索引选取数组中的数据,将总是创建数据的副本

花式索引

花式索引(Fancy indexing)是一个 NumPy 术语,指利用整数数组进行索引。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
In [64]: arr = np.empty((8,4))
In [65]: for i in range(8):
...: arr[i] = i
...:
In [66]: arr
Out[66]:
array([[ 0., 0., 0., 0.],
[ 1., 1., 1., 1.],
[ 2., 2., 2., 2.],
[ 3., 3., 3., 3.],
[ 4., 4., 4., 4.],
[ 5., 5., 5., 5.],
[ 6., 6., 6., 6.],
[ 7., 7., 7., 7.]])
In [67]: arr[[4,3,0,6]]
Out[67]:
array([[ 4., 4., 4., 4.],
[ 3., 3., 3., 3.],
[ 0., 0., 0., 0.],
[ 6., 6., 6., 6.]])
In [68]: arr[[-3,-5,-7]]
Out[68]:
array([[ 5., 5., 5., 5.],
[ 3., 3., 3., 3.],
[ 1., 1., 1., 1.]])

花式索引和切片不一样,它总是将数据复制到新数组中。

数组转置和轴对换

简单的转置可以使用.T,其实就是进行轴对换:

1
2
3
4
5
6
In [71]: arr.T
Out[71]:
array([[ 0, 4, 8, 12, 16, 20, 24, 28],
[ 1, 5, 9, 13, 17, 21, 25, 29],
[ 2, 6, 10, 14, 18, 22, 26, 30],
[ 3, 7, 11, 15, 19, 23, 27, 31]])

对于高维数组,transpose需要得到一个由轴编号组成的元祖才能对这些轴进行转置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
In [74]: arr = np.arange(16).reshape((2, 2, 4))
In [75]: arr
Out[75]:
array([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7]],
[[ 8, 9, 10, 11],
[12, 13, 14, 15]]])
In [76]: arr.transpose((1, 0, 2))
Out[76]:
array([[[ 0, 1, 2, 3],
[ 8, 9, 10, 11]],
[[ 4, 5, 6, 7],
[12, 13, 14, 15]]])

ndarray 还有一个swapaxes方法,需要接受一对轴编号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
In [77]: arr
Out[77]:
array([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7]],
[[ 8, 9, 10, 11],
[12, 13, 14, 15]]])
In [78]: arr.swapaxes(1,2)
Out[78]:
array([[[ 0, 4],
[ 1, 5],
[ 2, 6],
[ 3, 7]],
[[ 8, 12],
[ 9, 13],
[10, 14],
[11, 15]]])

对于一维数组(序列),普通的转置操作可能无法对其进行转置,此时需要借助其他的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
In [10]: A
Out[10]: array([1, 1, 3])
In [11]: A.T
Out[11]: array([1, 1, 3])
In [13]: A[np.newaxis,:]
Out[13]: array([[1, 1, 3]])
In [14]: A[:,np.newaxis]
Out[14]:
array([[1],
[1],
[3]])

转置返回的都是源数据的视图(不会进行任何复制操作)。

合并

  • np.vstack((A, B)):上下合并;
  • np.hstack((A, B)):左右合并;
  • np.concatenate():合并操作针对多个矩阵或序列,用np.concatenate()会更加方便。可以用一个axis参数控制合并方向。

通用函数

通用函数(即 ufunc)是一种对 ndarray 中的数据执行元素级运算的函数。

1
2
3
4
5
6
In [2]: arr = np.arange(10)
In [3]: np.sqrt(arr)
Out[3]:
array([ 0. , 1. , 1.41421356, 1.73205081, 2. ,
2.23606798, 2.44948974, 2.64575131, 2.82842712, 3. ])

一元 ufunc

函数 说明
abs 计算整数、浮点数或复数的绝对值
fabs 更快地计算非复数的绝对值
sqrt 计算个元素的平方根
square 计算各元素的平方
exp 计算各元素的指数 e ** x
log、log10、log2、log1p 分别为自然对数(底数为 e)、底数为 10 的 log、底数为 2 的 log、log(1 + x)
sign 计算各元素的正负号:1(正数)、0(零)、-1(负数)
ceil 计算各元素的 ceiling 值,即大于等于该值的最小整数
floor 计算各元素的 floor 值,即小于等于该值的最大整数
rint 将各元素四舍五入到最接近的整数,保留 dtype
modf 将数组的小数和整数部分以两个独立数组的形式返回
logical_not 计算各元素 not x 的真值。相当于 -arr

二元 ufunc

函数 说明
add 将数组中对应的元素相加
subtract 从第一个数组中减去第二个数组中的元素
multiply 数组元素相乘
divide、floor_divide 除法或向下圆整除法(丢弃余数)
power 对第一个数组中的元素 A,根据第二个数组中的相应元素 B,计算 A ** B
mod 元素级的求模计算(除法的余数)
greater、greater_equal、less、less_equal、equal、not_equal 执行元素级的比较运算,最终产生布尔型数组
logical_and、logical_or、logical_xor 执行元素级的真值逻辑运算。相当于中缀运算符&、` ^`

利用数组进行数据处理

NumPy 数组可以将许多种数据处理任务表述为简洁的数组表达式,使运算速度快上一两个数量级。这种做法通常被称为矢量化

将条件逻辑表述为数组运算

1
2
3
4
5
6
7
8
9
10
In [12]: xarr = np.array([1.1, 1.2, 1.3, 1.4, 1.5])
In [13]: yarr = np.array([2.1, 2.2, 2.3, 2.4, 2.5])
In [14]: cond = np.array([True, False, True, True, False])
In [17]: result = np.where(cond, xarr, yarr)
In [18]: result
Out[18]: array([ 1.1, 2.2, 1.3, 1.4, 2.5])

np.where的第二个和第三个参数不必是数组,它们都可以是标量值。

数学和统计方法

可以通过数组上的一组数学函数对整个数组或某个轴向的数据进行统计计算。summean以及标准差std等聚合计算既可以当做数组的实例方法(arr.mean())调用,也可以当做顶级 NumPy 函数使用(np.mean(arr))。

meansum这类的函数可以接受一个 axis 参数(用于计算该轴向上的统计值),最终结果是一个少一维的数组。

方法 说明
sum 对数组中全部或某轴向的元素求和。零长度的数组的 sum 为 0
mean 算术平均数。零长度的数组的 mean 为 NaN
std、var 分别为标准差和方差,自由度可调(默认为 n)
min、max 最大值和最小值
argmin、argmax 分别为最大和最小元素的索引
cumsum 所有元素的累积和
cumprod 所有元素的累积值

用于布尔型数组的方法

  • any:用于测试数组中是否存在一个或多个 True;
  • all:检查数组中所有值是否都是 True。

排序

多维数组可以通过sort()在任何一个轴向上进行排序,只需将轴编号传入即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
In [4]: arr
Out[4]:
array([[-1.40298298, -0.94302353, -1.2592719 , 1.15911978],
[-0.53758748, 1.44386652, -1.8916146 , 0.66364269],
[-0.29874721, -0.10316202, 0.21261665, -2.47289258],
[-0.34280455, 0.43060591, -0.89851967, -0.73198573],
[-0.9066756 , 1.28177992, 0.35708335, 0.2792639 ]])
In [7]: arr.sort(1)
In [8]: arr
Out[8]:
array([[-1.40298298, -1.2592719 , -0.94302353, 1.15911978],
[-1.8916146 , -0.53758748, 0.66364269, 1.44386652],
[-2.47289258, -0.29874721, -0.10316202, 0.21261665],
[-0.89851967, -0.73198573, -0.34280455, 0.43060591],
[-0.9066756 , 0.2792639 , 0.35708335, 1.28177992]])

顶级方法np.sort返回的是数组的已排序副本,而就地排序则会修改数组本身。

唯一化以及其他的集合逻辑

方法 说明
unique(x) 计算 x 中的唯一元素,并返回有序结果
intersect1d(x, y) 计算 x 和 y 中的公共元素,并返回有序结果
union1d(x, y) 计算 x 和 y 的并集,并返回有序结果
in1d(x, y) 得到一个表示”x 的元素是否包含于 y“的布尔型数组
setdiff1d(x, y) 集合的差,即元素在 x 中且不在 y 中
setxor1d(x, y) 集合的异或,即存在于一个数组中但不同时存在于两个数组中的元素

线性代数

NumPy 提供了一个用于矩阵乘法的dot函数(既是一个数组方法也是 numpy 命名空间中的一个函数):

1
2
3
4
5
6
7
8
In [9]: x = np.array([[1.,2.,3.],[4.,5.,6.]])
In [10]: y = np.array([[6., 23.], [-1, 7], [8, 9]])
In [11]: x.dot(y)
Out[11]:
array([[ 28., 64.],
[ 67., 181.]])

numpy.linalg 中有一组标准的矩阵分解运算以及诸如求逆和行列式之类的东西。

常用的 numpy.linalg 函数

函数 说明
diag 以一维数组的形式返回方阵的对角线(或非对角线)元素,或将一维数组转换为方阵(非对角线元素为 0)
dot 矩阵乘法
trace 计算对角线元素的和
det 计算矩阵行列式
eig 计算方阵的本征值和本征向量
inv 计算方阵的逆
svd 计算奇异值分解(SVD)
solve 解线性方程组 Ax = b,其中 A 为一个方阵
lstsq 计算 Ax = b 的最小二乘解

随机数生成

numpy.random 模块对 Python 内置的random进行了补充,增加了一些用于高效生成多种概率分布的样本值的函数。例如用normal来得到一个标准正态分布的多维数组:

1
2
3
4
5
6
7
8
In [16]: samples = np.random.normal(size=(4, 4))
In [17]: samples
Out[17]:
array([[ 0.58779653, 0.69406318, 0.28244585, 0.25620597],
[ 0.95854832, 0.55800493, 0.17702977, -0.06442143],
[-1.60361823, -0.00796763, -0.48375863, -1.59447872],
[ 0.80866648, -1.75706417, 0.03793649, 0.18152227]])

部分 numpy.random 函数

函数 说明
seed 确定随机数生成器的种子
permutation 返回一个序列的随机排列或返回一个随机排列的范围
shuffle 对一个序列就地随机排列
rand 产生均匀分布的样本值
randint 从给定的上下限范围内随机选取整数
randn 产生正态分布(平均值为 0,标准差为 1)的样本值
binomial 产生二项分布的样本值
normal 产生正态(高斯)分布的样本值
beta 产生 Beta 分布的样本值
chisquare 产生卡方分布的样本值
gamma 产生 Gamma 分布的样本值
uniform 产生在 [0,1) 中均匀分布的样本值

pandas 入门

pandas 基于 NumPy 构建,含有使数据分析工作变得更快更简单的高级数据结构和操作工具。

pandas 引入约定:

1
2
from pandas import Series, DataFrame
import pandas as pd

Series

Series 是一种类似于一维数组的对象,它由一组数据(各种 NumPy 数据类型)以及一组与之相关的数据标签(即索引)组成:

1
2
3
4
5
6
7
8
9
In [3]: obj = Series([4, 7, -5, 3])
In [4]: obj
Out[4]:
0 4
1 7
2 -5
3 3
dtype: int64

Series 的字符串表现形式为:索引在左边,值在右边。可以通过 Series 的 values 和 index 属性获取其数组表示形式和索引对象:

1
2
3
4
5
In [5]: obj.values
Out[5]: array([ 4, 7, -5, 3])
In [6]: obj.index
Out[6]: RangeIndex(start=0, stop=4, step=1)

在创建 Series 时可以指定各个数据点的索引:

1
2
3
4
5
6
7
8
9
10
11
12
In [7]: obj2 = Series([4, 2, 3, 5], index=['d', 'a', 'b', 'c'])
In [8]: obj2
Out[8]:
d 4
a 2
b 3
c 5
dtype: int64
In [9]: obj2.index
Out[9]: Index(['d', 'a', 'b', 'c'], dtype='object')

与普通 NumPy 数组相比,可以通过索引来选取 Series 中的单个或一组值:

1
2
3
4
5
6
7
8
9
10
In [10]: obj2['a']
Out[10]: 2
In [11]: obj2['d'] = 6
In [12]: obj2[['d', 'a']]
Out[12]:
d 6
a 2
dtype: int64

NumPy 数组运算(根据布尔型数组进行过滤、标量乘法、应用数学函数等)都会保留索引和值之间的链接。

还可以将 Series 看成一个定长的有序字典,因为它是索引值到数据值的一个映射。它可以用在许多原本需要字典参数的函数中。

如果数据被存放在一个 Python 字典中,也可以直接通过这个字典来创建 Series。可以只传入一个字典(结果 Series 中的索引(有序排列)就是原字典的键),也可以同时传入一个 index 参数,字典中与索引相匹配的值被放到相应的位置上,找不到的值则为 NaN(数据缺失)。pandas 的isnullnotnull函数可用于检测缺失数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
In [13]: sdata = {'hello': 100, 'python': 200, 'pandas': 300}
In [14]: states = ['hello', 'pandas', 'numpy']
In [15]: obj3 = Series(sdata, index=states)
In [16]: obj3
Out[16]:
hello 100.0
pandas 300.0
numpy NaN
dtype: float64
In [17]: pd.isnull(obj3)
Out[17]:
hello False
pandas False
numpy True
dtype: bool
In [18]: obj3.isnull()
Out[18]:
hello False
pandas False
numpy True
dtype: bool

Series 很重要的一个功能是:它在算数运算中会自动对齐不同索引的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
In [19]: obj4 = Series(sdata)
In [20]: obj4
Out[20]:
hello 100
pandas 300
python 200
dtype: int64
In [21]: obj3 + obj4
Out[21]:
hello 200.0
numpy NaN
pandas 600.0
python NaN
dtype: float64

Series 对象本身及其索引都带有一个 name 属性,该属性跟 pandas 其他的关键功能关系非常密切:

1
2
3
4
5
6
7
8
9
10
11
In [23]: obj4.name = 'dict'
In [24]: obj4.index.name = 'words'
In [25]: obj4
Out[25]:
words
hello 100
pandas 300
python 200
Name: dict, dtype: int64

Series 的索引可以通过赋值的方式就地修改:

1
2
3
4
5
6
7
8
In [26]: obj4.index = ['bye', 'little', 'girl']
In [27]: obj4
Out[27]:
bye 100
little 300
girl 200
Name: dict, dtype: int64

DataFrame

DataFrame 是一个表格型的数据结构,它含有一组有序的列,每列可以是不同的值类型(数值、字符串、布尔型等)。DataFrame 既有行索引也有列索引,可以被看作由 Series 组成的字典(共用同一个索引)。

DataFrame 是以二维结构保存数据的,但可以被表示为层次化索引的表格型结构,这是 pandas 中许多高级数据处理功能的关键要素。

构建 DataFrame 的办法有很多,最常用的一种是直接传入一个由等长列表或 NumPy 数组组成的字典,DataFrame 会自动加上索引,且全部列会被有序排列:

1
2
3
4
5
6
7
8
9
10
11
12
In [4]: data = {'state': ['well', 'good', 'soso', 'bad'], 'year': [2000, 2001, 2002, 2003], 'nums':
...: [1.1, 2.3, 4.2, 5.4]}
In [5]: frame = DataFrame(data)
In [6]: frame
Out[6]:
nums state year
0 1.1 well 2000
1 2.3 good 2001
2 4.2 soso 2002
3 5.4 bad 2003

如果指定了列序列,则 DataFrame 的列就会按照指定顺序进行排列。如果传入的列找不到,会和 Series 一样产生 NaN 值:

1
2
3
4
5
6
7
8
9
10
11
12
13
In [10]: frames2 = DataFrame(data, columns=['year', 'state', 'nums', 'debt'], index=['one', 'two', '
...: three', 'four'])
In [11]: frames2
Out[11]:
year state nums debt
one 2000 well 1.1 NaN
two 2001 good 2.3 NaN
three 2002 soso 4.2 NaN
four 2003 bad 5.4 NaN
In [12]: frames2.columns
Out[12]: Index(['year', 'state', 'nums', 'debt'], dtype='object')

通过类似字典标记的方式或属性的方式,可以将 DataFrame 的列获取为一个Series:

1
2
3
4
5
6
7
In [13]: frames2['state'] # = frames2.state
Out[13]:
one well
two good
three soso
four bad
Name: state, dtype: object

行也可以通过位置或名称的方式进行获取,比如用索引字段ix

1
2
3
4
5
6
7
In [15]: frames2.ix['one']
Out[15]:
year 2000
state well
nums 1.1
debt NaN
Name: one, dtype: object

列可以通过赋值的方式进行修改。将列表或数组赋值给某个列时,其长度必须跟 DataFrame 的长度相匹配。如果赋值的是一个 Series,就会精确匹配 DataFrame 的索引,所有的空位都将被填上缺失值。为不存在的列赋值会创建出一个新列。关键字 del 用于删除列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
In [16]: frames2['eastern'] = frames2.state == 'Ohio'
In [17]: frames2
Out[17]:
year state nums debt eastern
one 2000 well 1.1 NaN False
two 2001 good 2.3 NaN False
three 2002 soso 4.2 NaN False
four 2003 bad 5.4 NaN False
In [18]: del frames2['eastern']
In [19]: frames2.columns
Out[19]: Index(['year', 'state', 'nums', 'debt'], dtype='object')

注意,通过索引方式返回的列只是相应数据的视图而非副本。因此,对返回的 Series 所做的任何就地修改全都会反映到源 DataFrame 上。通过 Series 的 copy 方法即可显式地复制列。

另一种常见的数据类型是嵌套字典。将其传给 DataFrame,会被解释为:外层字典的键作为列,内层键则作为行索引:

1
2
3
4
5
6
7
8
9
10
In [20]: pop = {'niboer': {2024: 2.4, 2027: 4.3}, 'huacun': {2024: 1.4, 2025:3.2, 2027: 6.4}}
In [21]: frames = DataFrame(pop)
In [22]: frames
Out[22]:
huacun niboer
2024 1.4 2.4
2025 3.2 NaN
2027 6.4 4.3

也可以对该结果进行转置:

1
2
3
4
5
In [23]: frames.T
Out[23]:
2024 2025 2027
huacun 1.4 3.2 6.4
niboer 2.4 NaN 4.3

如果 DataFrame 各列的数据类型不同,则值数组的数据类型就会选用能兼容所有列的数据类型。

索引对象

pandas 的索引对象负责管理轴标签和其他元数据(比如轴名称等)。构建 Series 或 DataFrame 时,所用到的任何数组或其他序列的标签都会被转换成一个 Index 对象:

1
2
3
4
5
6
7
8
9
In [24]: obj = Series(range(3), index=['a','b','c'])
In [25]: index = obj.index
In [26]: index
Out[26]: Index(['a', 'b', 'c'], dtype='object')
In [27]: index[1:]
Out[27]: Index(['b', 'c'], dtype='object')

Index 对象是不可修改的。这种设计使 Index 对象在多个数据结构之间安全共享:

1
2
3
4
5
6
In [31]: index = pd.Index(np.arange(3))
In [32]: obj2 = Series([1.5, -2.5, 0], index=index)
In [33]: obj2.index is index
Out[33]: True

Index 的方法和属性见原书 p126(pdf p137)。

基本功能

重新索引(reindex

调用某个 Series 的reindex方法将会根据新索引进行重排。如果某个索引值当前不存在,就引入缺失值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
In [2]: obj = Series([4.5, 7.2, -5.3, 3.6], index=['d','b','a','c'])
In [4]: obj = Series([4.5, 7.2, -5.3, 3.6], index=['d','b','a','c'])
In [5]: obj
Out[5]:
d 4.5
b 7.2
a -5.3
c 3.6
dtype: float64
In [6]: obj2 = obj.reindex(['a', 'b', 'c', 'd', 'e'])
In [7]: obj2
Out[7]:
a -5.3
b 7.2
c 3.6
d 4.5
e NaN
dtype: float64
In [8]: obj.reindex(['a', 'b', 'c', 'd', 'e'], fill_value=0)
Out[8]:
a -5.3
b 7.2
c 3.6
d 4.5
e 0.0
dtype: float64

对于时间序列这样的有序数据,重新索引时可能需要做一些插值处理。method选项即可到达此目的,例如使用ffill可以实现前向值填充(bfill实现向后插值):

1
2
3
4
5
6
7
8
9
10
11
In [9]: obj3 = Series(['blue', 'purple', 'yellow'], index=[0, 2, 4])
In [10]: obj3.reindex(range(6), method='ffill')
Out[10]:
0 blue
1 blue
2 purple
3 purple
4 yellow
5 yellow
dtype: object

对于 DataFramereindex可以修改(行)索引、列,或两个都修改。如果仅传入一个序列,则会重新索引行。使用columns关键字即可重新索引列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
In [13]: frame = DataFrame(np.arange(9).reshape((3,3)), index=['a', 'c', 'd'],columns=['Ohio', 'Texa
...: s', 'California'])
In [14]: frame
Out[14]:
Ohio Texas California
a 0 1 2
c 3 4 5
d 6 7 8
In [18]: frame.reindex(columns=['Texas', 'California', 'Ohio'])
Out[18]:
Texas California Ohio
a 1 2 0
c 4 5 3
d 7 8 6

也可以同时对行和列进行重新索引,而插值则只能按行应用(即轴 0):

1
2
3
4
5
6
7
8
In [19]: frame.reindex(index=['a', 'b', 'c', 'd'], method='ffill', columns=['Texas', 'California', '
...: Ohio'])
Out[19]:
Texas California Ohio
a 1 2 0
b 1 2 0
c 4 5 3
d 7 8 6

利用ix的标签索引功能,重新索引任务可以变得更简洁。

reindex函数的各参数及说明见原书 p129(pdf p140)。

丢弃指定轴上的项

对于 Series,drop方法返回一个在指定轴上删除了指定值的新对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
In [4]: obj = Series(np.arange(5.), index=['a', 'b', 'c', 'd', 'e'])
In [5]: new_obj = obj.drop('c')
In [6]: new_obj
Out[6]:
a 0.0
b 1.0
d 3.0
e 4.0
dtype: float64
In [7]: obj.drop(['d', 'c'])
Out[7]:
a 0.0
b 1.0
e 4.0
dtype: float64

对于 DataFrame,可以删除任意轴上的索引值:

1
2
3
4
5
6
7
8
9
10
11
12
13
In [10]: frames
Out[10]:
huacun niboer
2024 1.4 2.4
2025 3.2 NaN
2027 6.4 4.3
In [13]: frames.drop(['huacun'], axis=1)
Out[13]:
niboer
2024 2.4
2025 NaN
2027 4.3

索引、选取和过滤

Series 索引的工作方式类似于 NumPy 数组的索引,只不过不只是整数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
In [4]: obj = Series(np.arange(4.), index=['a', 'b', 'c', 'd'])
In [5]: obj['b']
Out[5]: 1.0
In [6]: obj[1]
Out[6]: 1.0
In [7]: obj[2:4]
Out[7]:
c 2.0
d 3.0
dtype: float64
In [8]: obj[['b', 'a', 'c']]
Out[8]:
b 1.0
a 0.0
c 2.0
dtype: float64

利用标签的切片运算与普通的 Python 切片运算不同,其末端是包含的

1
2
3
4
5
In [9]: obj['b':'c']
Out[9]:
b 1.0
c 2.0
dtype: float64

而对于 DataFrame,进行索引是在获取一个或多个

1
2
3
4
5
6
7
8
9
10
In [10]: data = DataFrame(np.arange(16).reshape((4, 4)), index=['Obio', 'Colorado', 'Utah', 'New Yor
...: k'], columns=['one', 'two', 'three', 'four'])
In [11]: data['two']
Out[11]:
Obio 1
Colorado 5
Utah 9
New York 13
Name: two, dtype: int64

想选取有以下两种方式:

  • 通过切片或布尔型数组选取行:
1
2
3
4
5
6
7
8
9
10
11
12
In [12]: data[:2]
Out[12]:
one two three four
Obio 0 1 2 3
Colorado 4 5 6 7
In [13]: data[data['three'] > 5]
Out[13]:
one two three four
Colorado 4 5 6 7
Utah 8 9 10 11
New York 12 13 14 15
  • 通过布尔型 DataFrame 进行索引:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
In [14]: data < 5
Out[14]:
one two three four
Obio True True True True
Colorado True False False False
Utah False False False False
New York False False False False
In [15]: data[data < 5]
Out[15]:
one two three four
Obio 0.0 1.0 2.0 3.0
Colorado 4.0 NaN NaN NaN
Utah NaN NaN NaN NaN
New York NaN NaN NaN NaN
In [16]: data[data < 5] = 0
In [17]: data
Out[17]:
one two three four
Obio 0 0 0 0
Colorado 0 5 6 7
Utah 8 9 10 11
New York 12 13 14 15

为了在 DataFrame 的行上进行标签索引,可以使用专门的索引字段ix,以通过 NumPy 式的标记法以及轴标签从 DataFrame 中选取行和列的子集。这也是一种重新索引的简单手段。

1
2
3
4
5
In [18]: data.ix['Colorado', ['two', 'three']]
Out[18]:
two 5
three 6
Name: Colorado, dtype: int64
1
2
3
4
5
In [20]: data.ix[['Colorado', 'Utah'], [3, 0, 1]]
Out[20]:
four one two
Colorado 7 0 5
Utah 11 8 9
1
2
3
4
5
6
7
In [21]: data.ix[2]
Out[21]:
one 8
two 9
three 10
four 11
Name: Utah, dtype: int64
1
2
3
4
5
6
In [22]: data.ix[:'Utah', 'two']
Out[22]:
Obio 0
Colorado 5
Utah 9
Name: two, dtype: int64
1
2
3
4
5
6
In [23]: data.ix[data.three > 5, :3]
Out[23]:
one two three
Colorado 0 5 6
Utah 8 9 10
New York 12 13 14

DataFrame 的索引选项见原书 p132(pdf p143)。

算术运算和数据对齐

pandas 最重要的一个功能是对不同索引的对象进行算数运算。在将对象相加时,如果存在不同的索引对,则结果的索引就是该索引对的并集。自动的数据对齐操作在不重叠的索引处引入了 NaN 值。

1
2
3
4
5
6
7
8
9
10
11
12
13
In [24]: s1 = Series([7.3, -2.5, 3.4, 1.5], index=['a', 'c', 'd', 'e'])
In [25]: s2 = Series([-2.1, 3.6, -1.5, 4, 3.1], index=['a', 'c', 'e', 'f', 'g'])
In [26]: s1+s2
Out[26]:
a 5.2
c 1.1
d NaN
e 0.0
f NaN
g NaN
dtype: float64

对于 DataFrame,对齐操作会同时发生在行和列上。

在算术方法中填充值

在使用add或者reindex等方法时,可以通过fill_value参数指定一个填充值.

DataFrame 和 Series 之间的运算

默认情况下,DataFrame 和 Series 之间的算术运算会将 Series 的索引匹配到 DataFrame 的,然后沿着行一直向下广播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
In [27]: frame = DataFrame(np.arange(12.).reshape((4,3)), columns=list('bde'), index=['Obio', 'Colorado', 'Utah', 'New York'])
In [28]: series = frame.ix[0]
In [29]: frame
Out[29]:
b d e
Obio 0.0 1.0 2.0
Colorado 3.0 4.0 5.0
Utah 6.0 7.0 8.0
New York 9.0 10.0 11.0
In [30]: series
Out[30]:
b 0.0
d 1.0
e 2.0
Name: Obio, dtype: float64
In [31]: frame - series
Out[31]:
b d e
Obio 0.0 0.0 0.0
Colorado 3.0 3.0 3.0
Utah 6.0 6.0 6.0
New York 9.0 9.0 9.0

如果某个索引值在 DataFrame 的列或 Series 的索引中找不到,则参与运算的两个对象就会被重新索引以形成并集。

如果希望匹配行且在列上广播,则必须使用算数运算方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
In [32]: series3 = frame['d']
In [33]: series3
Out[33]:
Obio 1.0
Colorado 4.0
Utah 7.0
New York 10.0
Name: d, dtype: float64
In [35]: frame.sub(series3, axis=0)
Out[35]:
b d e
Obio -1.0 0.0 1.0
Colorado -1.0 0.0 1.0
Utah -1.0 0.0 1.0
New York -1.0 0.0 1.0

传入的轴号就是希望匹配的轴。

函数应用和映射

NumPy 的ufuncs(元素级数组方法)也可以用于操作 pandas 对象:np.abs(frame)

另一个常见的操作是,将函数应用到由各列或行所形成的一维数组上,DataFrame 的apply方法即可实现此功能。通过applymap也可以使用元素级的 Python 函数(Series 有一个用于应用元素级函数的map方法)。

排序和排名

要对行或列索引进行排序(字典顺序),可使用sort_index方法来返回一个已排序的新对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
In [36]: from pandas import DataFrame, Series
In [37]: obj = Series(range(4), index=['d', 'a', 'b', 'c'])
In [38]: obj
Out[38]:
d 0
a 1
b 2
c 3
dtype: int64
In [39]: obj.sort_index()
Out[39]:
a 1
b 2
c 3
d 0
dtype: int64

处理缺失数据

pandas 使用浮点值 NaN 表示浮点和非浮点数组中的缺失数据,可以通过np.nan或者None得到。

滤除缺失数据

对于一个 Series,dropna函数返回一个仅含非空数据和索引值的 Series。

对于 DataFrame,dropna函数默认丢弃任何含有缺失值的行。传入how='all'将只丢弃全为 NaN 的行。丢弃列则传入axis=1即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
In [1]: from pandas import DataFrame, Series
In [2]: from numpy import nan as NA
In [3]: data = Series([1, NA, 3.5, NA, 7])
In [4]: data.dropna()
Out[4]:
0 1.0
2 3.5
4 7.0
dtype: float64
In [5]: data = DataFrame([[1., 6.5, 3.], [1., NA, NA], [NA, NA, NA], [NA, 6.5, 3.]])
In [6]: data
Out[6]:
0 1 2
0 1.0 6.5 3.0
1 1.0 NaN NaN
2 NaN NaN NaN
3 NaN 6.5 3.0
In [7]: data.dropna(axis=1)
Out[7]:
Empty DataFrame
Columns: []
Index: [0, 1, 2, 3]
In [8]: data.dropna()
Out[8]:
0 1 2
0 1.0 6.5 3.0
In [9]: data.dropna(how='all')
Out[9]:
0 1 2
0 1.0 6.5 3.0
1 1.0 NaN NaN
3 NaN 6.5 3.0

填充缺失数据

通过一个常数调用fillna将缺失值替换为常数值。而通过一个字典调用fillna可以实现对不同的列填充不同的值。fillna默认返回新对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
In [3]: df = DataFrame(np.random.randn(7, 3))
In [4]: df.loc[:4, 1] = np.nan
In [6]: df.loc[:2, 2] = np.nan
In [7]: df
Out[7]:
0 1 2
0 -0.048462 NaN NaN
1 -1.114710 NaN NaN
2 1.290745 NaN NaN
3 -0.129481 NaN -2.151261
4 -1.030350 NaN -1.330047
5 -2.431158 -0.340187 -0.246772
6 -1.478923 0.232547 1.959353
In [8]: df.fillna(0)
Out[8]:
0 1 2
0 -0.048462 0.000000 0.000000
1 -1.114710 0.000000 0.000000
2 1.290745 0.000000 0.000000
3 -0.129481 0.000000 -2.151261
4 -1.030350 0.000000 -1.330047
5 -2.431158 -0.340187 -0.246772
6 -1.478923 0.232547 1.959353
In [9]: df.fillna({1: 0.5, 3: -1})
Out[9]:
0 1 2
0 -0.048462 0.500000 NaN
1 -1.114710 0.500000 NaN
2 1.290745 0.500000 NaN
3 -0.129481 0.500000 -2.151261
4 -1.030350 0.500000 -1.330047
5 -2.431158 -0.340187 -0.246772
6 -1.478923 0.232547 1.959353

数据加载、存储与文件格式

NumPy 提供了一个低级但异常高效的二进制数据加载和存储机制,包括对内存映射数组的支持等。

本章着重介绍 pandas 的输入输出对象。输入输出通常划分为几个大类:

  • 读取文本文件和其他更高效的磁盘存储格式;
  • 加载数据库中的数据;
  • 利用 Web API 操作网络资源。

读写文本格式的数据

pandas 提供了一些用于将表格型数据读取为 DataFrame 对象的函数,其中read_csvread_table可能使用较多。

函数 说明
read_csv 从文件、URL、文件型对象中加载带分隔符的数据。默认分隔符为逗号
read_table 从文件、URL、文件型对象中加载带分隔符的数据。默认分隔符为制表符(\t),可以用sep参数指定分隔符
read_fwf 读取定宽列格式数据(即没有分隔符)
read_clipboard 读取剪贴板中的数据,可以看作是 read_table 的剪贴板版。在将网页转换为表格时很有用

在将文本数据转换为 DataFrame 时,用到了如下技术:

  • 索引:将一个或多个列当作返回的 DataFrame 处理,以及是否从文件、用户获取列名。
  • 类型推断和数据转换:包括用户定义值的转换、缺失值标记列表等。
  • 日期解析:包括组合功能,比如将分散在多个列的日期时间信息组合成结果中的单个列。
  • 迭代:支持对大文件进行逐块迭代。
  • 不规整数据问题:跳过一些行、页脚、注释或其他一些不重要的东西。

读入一个以逗号分隔的 csv 文本文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
In [5]: data = pd.read_csv('/Users/huang/Desktop/score.csv')
In [6]: data
Out[6]:
team score
0 1 45.6
1 2 65.4
2 3 67.7
3 4 76.7
4 5 56.3
5 6 87.5
6 7 45.6
7 8 77.0
8 9 54.6

对于没有标题行的文件,可以让 pandas 为其分配默认的列名:

1
2
3
4
5
6
7
8
9
10
11
12
In [9]: pd.read_csv('/Users/huang/Desktop/score.csv', header=None)
Out[9]:
0 1
0 1 45.6
1 2 65.4
2 3 67.7
3 4 76.7
4 5 56.3
5 6 87.5
6 7 45.6
7 8 77.0
8 9 54.6

也可以自定义列名:

1
2
3
4
5
6
7
8
9
10
11
12
In [10]: pd.read_csv('/Users/huang/Desktop/score.csv', names=['a', 'b'])
Out[10]:
a b
0 1 45.6
1 2 65.4
2 3 67.7
3 4 76.7
4 5 56.3
5 6 87.5
6 7 45.6
7 8 77.0
8 9 54.6

可以通过index_col参数来将某列指定为 DataFrame 的索引:

1
2
3
4
5
6
7
8
9
10
11
12
13
In [11]: pd.read_csv('/Users/huang/Desktop/score.csv', names=['a', 'b'], index_col='b')
Out[11]:
a
b
45.6 1
65.4 2
67.7 3
76.7 4
56.3 5
87.5 6
45.6 7
77.0 8
54.6 9

如果希望将多个列做成一个层次化索引,只需通过index_col参数传入由列编号或列名组成的列表即可。

对于用不固定的分隔符分隔字段的文本,可以通过sep参数传入一个正则表达式来作为read_table的分隔符。

可以通过skiprows跳过文件的某几行:

1
2
3
4
5
6
7
8
9
In [12]: pd.read_csv('/Users/huang/Desktop/score.csv', names=['a', 'b'], skiprows=[0, 2, 3])
Out[12]:
a b
0 2 65.4
1 5 56.3
2 6 87.5
3 7 45.6
4 8 77.0
5 9 54.6

更多的read_csv/read_table函数的参数选项见原书 p167(pdf p178)。

逐块读取文本文件

在处理很大的文件时,或找出大文件中的参数集以便后续处理时,可以只读取几行(避免读取整个文件),通过nrows进行指定即可:

1
2
3
4
5
6
7
In [13]: pd.read_csv('/Users/huang/Desktop/score.csv', names=['a', 'b'], nrows=4)
Out[13]:
a b
0 1 45.6
1 2 65.4
2 3 67.7
3 4 76.7

也可以通过设置chunksize(行数)来逐块读取文件:

1
2
3
4
In [17]: chunker = pd.read_csv('/Users/huang/Desktop/score.csv', names=['a', 'b'], chunksize=2)
In [23]: chunker
Out[23]: <pandas.io.parsers.TextFileReader at 0x1134a7470>

read_csv所返回的这个 TextParser 对象使你可以根据 chunksize 对文件进行逐块迭代。比如将值计数聚合到 “a” 列中:

1
2
3
4
5
6
In [18]: tot = Series([])
In [19]: for piece in chunker:
...: tot = tot.add(piece['a'].value_counts(), fill_value=0)
In [20]: tot = tot.order(ascending=False)

于是有:

1
2
3
4
5
In [22]: tot[:2]
Out[22]:
1 1.0
2 1.0
dtype: float64

TextParser 还有一个get_chunk方法,可以读取任意大小的块。

将数据写出到文本格式

利用 DataFrame(或 Series)的to_csv方法,可以将数据写到一个以逗号分隔的文件中。也可以通过sep参数指定使用其他分隔符,或者输出到sys.stdout来打印文本结果:

1
data.to_csv(sys.stdout, sep='|')

缺失值在输出结果中会被表示为空字符串,可以通过na_rep参数指定其为别的标记值。

如果没有设置其他选项,则会写出行和列的标签,也可以将它们禁用:

1
data.to_csv(sys.stdout, index=False, header=False)

此外,也可以只按指定的顺序写出一部分的列:

1
data.to_csv(sys.stdout, index=False, cols=['a', 'b', 'c'])

手工处理分隔符格式

对于含有畸形行的文件需要做一些额外处理。对于任何单字符分隔符文件,可以直接使用 python 内置的 csv 模块,将人意已打开的文件或文件型对象传给csv.reader

1
2
3
4
5
In [26]: import csv
In [27]: f = open('/Users/huang/Desktop/score.csv')
In [28]: reader = csv.reader(f)

对这个 reader 进行迭代将会为每行产生一个列表(并移除了所有的引号):

1
2
3
4
5
6
7
8
9
10
11
12
In [29]: for line in reader:
...: print(line)
...:
['1', '45.6']
['2', '65.4']
['3', '67.7']
['4', '76.7']
['5', '56.3']
['6', '87.5']
['7', '45.6']
['8', '77']
['9', '54.6']

JSON 数据

json 库是构建于 Python 标准库的。通过json.loads即可将 JSON 字符串转换为 Python 形式,而json.dumps则将 Python 对象转换为 JSON 格式(对象中所有的键都必须是字符串)。

XML 和 HTML:Web 信息收集

相关内容参见原书 p174(psf p185)。

二进制数据格式

二进制格式的 I/O 高速且高效,但不宜人来阅读。相关内容参见原书 p179(psf p190)。

使用 HTML 和 Web API

通过 requests 包可以方位公共 API。

1
2
3
4
5
6
7
8
In [36]: import requests
In [37]: url = 'https://www.easy-mock.com/mock/5a1fb5a85bc39d4cf3b7a778/robot/ask'
In [38]: resp = requests.get(url)
In [39]: resp
Out[39]: <Response [200]>

Response 对象的 text 属性含有 GET 请求的内容。许多 Web API 返回的都是 JSON 字符串,必须将其加载到一个 Python 对象中:

1
2
3
4
In [40]: data = json.loads(resp.text)
In [41]: data.keys()
Out[41]: dict_keys(['answer', 'id'])

使用数据库

对于基于 SQL 的关系型数据库,以 SQLite 为例:

1
2
3
4
import sqlite3
con = sqlite3.connect(':memory:')
con.execute(query)
con.commit()

pandas 有一个可以简化数据规整操作过程read_frame函数(位于 pandas.io.sql 模块),只需要传入 select 语句和连接对象即可:

1
2
3
import pandas.io.sql as sql
sql.read_frame('select * from test', con)

存取 MongoDB 中的数据

先在电脑上启动一个 MongoDB 实例,然后用 pymongo(MongoDB 的官方驱动器)通过默认端口进行连接:

1
2
import pymongo
con = pymongo.Connection('localhost', port=27017)

存储在 MongoDB 中的文档被组织在数据库的集合(collection)中。MongoDB 服务器的每个运行实例可以有多个数据库,而每个数据库又可以有多个集合。想保存之前通过 APi 获取的数据,首先访问响应集合:

1
answers = con.db.answers

然后可以通过answers.save将 Python 字典逐个存入 MongoDb 的相应集合中。

可以通过下列代码对集合进行查询:

1
cursor = answers.find({'answer': '提问已收到,正在寻找回答...', 'id': 1111})

返回的游标是一个迭代器,可以为每个文档产生一个字典。可以将其转换为一个 DataFrame,并只获取部分字段:

1
result = DataFrame(list(cursor), columns='answer')

数据规整化:清理、转换、合并、重塑

pandas 和 Python 标准库提供了一组高级的、灵活的、高效的核心函数和算法,以轻松地将数据规整化为正确的形式。

合并数据集

  • pandas.merge可根据一个或多个键将不同 DataFrame 中的行连接起来(相当于 SQL 中的连接操作)。
  • pandas.concat可以沿一条轴将多个对象堆叠到一起。
  • 实例方法combine_first可以将重复数据编连在一起,用一个对象中的值填充另一个对象中的缺失值。

数据库风格的 DataFrame 合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
In [1]: from pandas import Series, DataFrame
In [2]: df1 = DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'a', 'b'], 'data1': range(7)})
In [3]: df2 = DataFrame({'key': ['a', 'b', 'd'], 'data2': range(3)})
In [4]: df1
Out[4]:
data1 key
0 0 b
1 1 b
2 2 a
3 3 c
4 4 a
5 5 a
6 6 b

如果没有指定用哪个列进行连接,merge就会将重叠列的列名当作键。不过最好用on选项显式指定(如果两个对象的列名不同,也可以用left_onright_on分别进行指定):

1
2
3
4
5
6
7
8
9
10
11
In [6]: import pandas as pd
In [7]: pd.merge(df1, df2, on='key')
Out[7]:
data1 key data2
0 0 b 1
1 1 b 1
2 6 b 1
3 2 a 0
4 4 a 0
5 5 a 0

默认情况下,merge做的是“inner”连接,结果中的键是交集。其他方式还有”left”、”right”以及”outer”。外连接求取的是键的并集,组合了左连接和右连接的效果:

1
2
3
4
5
6
7
8
9
10
11
In [8]: pd.merge(df1, df2, how='outer')
Out[8]:
data1 key data2
0 0.0 b 1.0
1 1.0 b 1.0
2 6.0 b 1.0
3 2.0 a 0.0
4 4.0 a 0.0
5 5.0 a 0.0
6 3.0 c NaN
7 NaN d 2.0

以上是多对一的合并。多对多的合并操作非常简单,无需额外的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
In [9]: df1 = DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'b'], 'data1': range(6)})
In [10]: df2 = DataFrame({'key': ['a', 'b', 'a', 'b', 'd'], 'data1': range(5)})
In [11]: pd.merge(df1, df2, on='key', how='left')
Out[11]:
data1_x key data1_y
0 0 b 1.0
1 0 b 3.0
2 1 b 1.0
3 1 b 3.0
4 2 a 0.0
5 2 a 2.0
6 3 c NaN
7 4 a 0.0
8 4 a 2.0
9 5 b 1.0
10 5 b 3.0

多对多连接产生的是行的笛卡尔积。由于左边的 DataFrame 有 3 个 “b” 行,右边有 2 个,所以最终结果中就有 6 个 “b” 行。连接方式只影响出现在结果中的键:

1
2
3
4
5
6
7
8
9
10
11
12
13
In [12]: pd.merge(df1, df2, on='key', how='inner')
Out[12]:
data1_x key data1_y
0 0 b 1
1 0 b 3
2 1 b 1
3 1 b 3
4 5 b 1
5 5 b 3
6 2 a 0
7 2 a 2
8 4 a 0
9 4 a 2

要根据多个键进行合并,传入一个由列名组成的列表即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
In [17]: left = DataFrame({'key1': ['foo', 'foo', 'bar'],
...: 'key2': ['one', 'two', 'one'],
...: 'lval': [1, 2, 3]})
In [18]: right = DataFrame({'key1': ['foo', 'foo', 'bar', 'bar'],
...: 'key2': ['one', 'one', 'one', 'two'],
...: 'lval': [4, 5, 6, 7]})
In [19]: pd.merge(left, right, on=['key1', 'key2'], how='outer')
Out[19]:
key1 key2 lval_x lval_y
0 foo one 1.0 4.0
1 foo one 1.0 5.0
2 foo two 2.0 NaN
3 bar one 3.0 6.0
4 bar two NaN 7.0

在进行列-列连接时,DataFrame 对象中的索引会被丢弃。

mergesuffixes选项可用于指定附加到左右两个 DataFrame 对象的重叠列名上的字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
In [20]: pd.merge(left, right, on='key1')
Out[20]:
key1 key2_x lval_x key2_y lval_y
0 foo one 1 one 4
1 foo one 1 one 5
2 foo two 2 one 4
3 foo two 2 one 5
4 bar one 3 one 6
5 bar one 3 two 7
In [21]: pd.merge(left, right, on='key1', suffixes=('_left', '_right'))
Out[21]:
key1 key2_left lval_left key2_right lval_right
0 foo one 1 one 4
1 foo one 1 one 5
2 foo two 2 one 4
3 foo two 2 one 5
4 bar one 3 one 6
5 bar one 3 two 7

mergecopy选项默认为 True,以根据连接键对合并后的数据进行排序。有时在处理大数据集时,禁用该选项可获得更好的性能。

在 DataFrame 中的连接键位于其索引中时,可以传入left_index=Trueright_index=True以说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
In [4]: left1 = DataFrame({'key': ['a', 'b', 'a', 'a', 'b', 'c'], 'value': range(6)})
In [5]: right1 = DataFrame({'group_val': [3.5, 7]}, index=['a', 'b'])
In [6]: right1
Out[6]:
group_val
a 3.5
b 7.0
In [7]: pd.merge(left1, right1, left_on='key', right_index=True)
Out[7]:
key value group_val
0 a 0 3.5
2 a 2 3.5
3 a 3 3.5
1 b 1 7.0
4 b 4 7.0

轴向连接

另一种数据合并预算也被称作连接(concatenation)、绑定(binding)或堆叠(stacking):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
In [8]: s1 = Series([0, 1], index=['a', 'b'])
In [9]: s2 = Series([2, 3, 4], index=['c', 'd', 'e'])
In [10]: s3 = Series([5, 6], index=['f', 'g'])
In [11]: pd.concat([s1, s2, s3])
Out[11]:
a 0
b 1
c 2
d 3
e 4
f 5
g 6
dtype: int64

默认情况下,concataxis=0上工作,最终产生一个新的 Series(纵向叠加)。如果传入axis=1(列),则结果就会变成一个 DataFrame(横向叠加):

1
2
3
4
5
6
7
8
9
10
In [12]: pd.concat([s1, s2, s3], axis=1)
Out[12]:
0 1 2
a 0.0 NaN NaN
b 1.0 NaN NaN
c NaN 2.0 NaN
d NaN 3.0 NaN
e NaN 4.0 NaN
f NaN NaN 5.0
g NaN NaN 6.0

绘图与可视化

Python 有许多可视化工具,例如 Chaco、mayavi 等。而作为 Python 领域中使用最广泛的绘图工具,matplotlib 是一个用于创建出版质量图表的桌面绘图包。

有多种方法使用 matplotlib,最常用的方式是 Pylab 模式的 IPython(ipython --pylab),这样会将 IPython 配置为使用用户所指定(一般为默认)的 matplotlib GUI 后端。

可以绘制一张简单的图标来测试一切是否准备就绪:plot(np.arange(10))。如果没有问题就会弹出新窗口,可以用鼠标或者输入close()将其关闭。

matplotlib API 函数都位于 matplotlib.pyplot 模块中,通常的引入约定是:

1
import matplotlib.pyplot as plt

pandas 的绘图函数能够处理许多普通的绘图任务(书上介绍的相关函数可能比较老旧,需要学习官方文档),但如果需要自定义一些高级功能的话就必须学习 matplotlib API。推荐查阅 matplotlib 的示例库和文档进行学习。

Figure 和 Subplot

matplotlib 的图像都位于 Figure 对象中。可以用plt.figure()创建一个新的 Figure:

1
In [7]: fig = plt.figure()

会弹出一个新窗口,但是不能通过空 Figure 绘图,必须用add_subplot创建一个或多个 subplot:

1
In [8]: ax1 = fig.add_subplot(2, 2, 1)

上面这一行代码的意思是,图像有 2 * 2 个 subplot,且当前选中的是 4 个 subplot 中的第一个(编号从 1 开始)。可以更改第三个参数来创建剩余的 subplot。

如果直接发出绘图命令(plt.plot()),matplotlib 会在最后一个用过的 subplot(如果没有则创建一个)上进行绘制。

fig.add_subplot所返回的对象是 AxesSubplot 对象,直接调用它们的实例方法就可以在其他空着的 subplot 里画图:

1
2
3
In [33]: ax1.hist(randn(100), bins=20, color='k', alpha=0.3)
In [35]: ax3.scatter(np.arange(30), np.arange(30) + 3 * randn(30))

由于根据特定布局创建 Figure 和 subplot 是一件非常常见的任务,因此可以用更方便的plt.subplots创建一个 Figure,并返回一个含有已创建的 subplot 对象的 NumPy 数组:

1
2
3
In [36]: fig, axes = plt.subplots(2, 3)
In [37]: axes

之后可以通过和二维数组一样的索引方式来指定 subplot,例如axes[0, 1]

关于 pyplot.subplots 的选项以及调整 subplot 周围的间距的更多内容参见原书 p235(pdf p246)。

颜色、标记和线型

matplot 的 plot 函数接受一组 X 和 Y 坐标,还可以接受一个表示颜色和线型的字符串缩写。例如根据 x 和 y 绘制绿色虚线:

1
ax.plot(x, y, 'g--')

这种写法会更明确:

1
ax.plot(x, y, linestyle='--', color='g')

常见的颜色都有一个缩写词,也可以通过指定 RGB 值的形式。

可以通过指定marker='o'来给实际数据点加上标记(marker):

1
plot(randn(30).cumsum(), color='k', linestyle='dashed', marker='o')

标记也可以放到格式字符串中,但标记类型和线型必须放在颜色后面:

1
plt.plot(randn(30).cumsum(), 'ko--')

在线型图中,非实际数据点默认是按线性方式插值的。可以通过drawstyle选项修改:

1
2
3
4
5
In [47]: fig.add_subplot(2, 3, 1).plot(data, 'k-', label='Default')
In [48]: plt.plot(data, 'k-', drawstyle='steps-post', label='steps-post')
In [49]: plt.legend(loc='best')

刻度、标签和图例

对于大多数的图表装饰项,其主要实现方式有二:

  • 使用过程型的 pyplot 接口;
  • 使用更为面向对象的原生 matplotlib API。

设置标题、轴标签、刻度以及刻度标签

1
2
3
4
5
In [55]: fig = plt.figure()
In [56]: ax = fig.add_subplot(1, 1, 1)
In [57]: ax.plot(randn(1000).cumsum())

修改 X 轴的刻度时,set_xticks可以确定将刻度放在数据范围中的哪些位置:

1
In [58]: ticks = ax.set_xticks([0, 250, 500, 750, 1000])

默认情况下,这些位置也就是刻度标签,但可以通过set_xticklabels将任何其他的值用作标签:

1
In [59]: labels = ax.set_xticklabels(['one', 'two', 'three', 'four', 'five'], rotation=30, fontsize='small')

再用set_xlabel为 X 轴设置一个名称,并用set_title设置一个标题:

1
2
3
In [61]: ax.set_title('My first matplotlib plot')
In [62]: ax.set_xlabels('Stages')

结果如图:

figure_1

添加图例

图例(legend)是另一种用于标识图表元素的重要工具。添加图例最简单的是在添加 subplot 的时候传入 label 参数:

1
2
3
4
5
In [67]: ax.plot(randn(1000).cumsum(), 'k', label='one')
In [68]: ax.plot(randn(1000).cumsum(), 'k--', label='two')
In [69]: ax.plot(randn(1000).cumsum(), 'k.', label='three')

如果有不想添加图例的元素,不传入label或传入label='_nolegend_'即可。

之后,可以调用ax.legend()plt.legend()来自动创建图例:

1
ax.legend(loc='best')

注解以及在 Subplot 上绘图

可以绘制一些自定义的注解(例如文本、箭头或其他图形等)。

文本:

1
In [71]: ax.text(10, 10, 'Hello world!', family='monospace', fontsize=10)

其他参见官方文档。

将图表保存到文件

1
In [72]: plt.savefig('./Desktop/figpath.svg')

文件类型通过文件扩展名推断。

可以通过dpi控制每英寸点数分辨率,以及通过bbox_inches剪除当前图表周围的空白部分:

1
In [74]: plt.savefig('./Desktop/figpath.png', dpi=400, bbox_inches='tight')

更多 Figure.savefig 的选项见原书 p244(pdf p255)。

matplotlib 配置

几乎所有默认行为都能通过一组全局参数进行自定义。操作 matplotlib 配置系统的方式主要有两种,第一种是 Python 编程方式,即利用rc方法。

rc的第一个参数是希望自定义的对象。最简单的办法是将这些选项写成一个字典:

1
2
3
4
5
In [76]: font_options = {'family': 'monospace',
...: 'weight': 'bold',
...: 'size' : 'small'}
In [77]: plt.rc('font', **font_options)

要了解全部的自定义选项,请查阅 matplotlib 的配置文件 matplotlibrc(位于 matplotlib/mpl-data 目录)。如果对该文件进行了自定义,并将其放在你自己的 .matplotlibrc 目录中,则每次使用 matplotlib 时就会加载该文件。