pandas 实践手册

本篇博客参考自 Python Data Science Handbook 第三章,旨在对 pandas 库的使用方法进行详细介绍。

安装和使用

关于 pandas 的安装可以参考官方教程,官方推荐直接基于 Anaconda 进行安装。安装完成后,我们可以导入 pandas 并查看其版本:

In[1]: import pandas
pandas.__version__
Out[1]: '1.0.3'

与 Numpy 一样,为了使用方便我们会将 Pandas 以别名的形式导入:

In[2]: import pandas as pd

在接下来的介绍中我们都将使用该导入方式。值得一提的是,在 Jupyter lab 中我们可以通过 Tab 键来进行自动补全,使用问号来查看相关文档,如下所示:

In [3]: pd.<TAB> # 查看可以调用的方法
In [4]: pd? # 查看官方文档

Pandas 对象

本章节将介绍三种基本的 Pandas 对象(数据结构):SeriesDataFrameIndex。我们可以简单地将 Pandas 对象理解为 Numpy 数组的增强版本,其中行与列可以通过标签进行识别,而不仅是简单的数字索引。Pandas 为这些基本数据结构提供了一系列有用的工具与方法。为了小节之间的独立性,每节的最开始会先进行包导入(编号每节独立):

In[1]: import numpy as np
import pandas as pd

Series 对象

Series 对象是一个可索引数据的一维数组,我们可以基于列表或数组来创建该对象:

In[2]: data = pd.Series([0.25, 0.5, 0.75, 1.0])
data
Out[2]: 0 0.25
1 0.50
2 0.75
3 1.00
dtype: float64

如上所示,Series 对象包含了一个值序列和一个索引序列,我们可以分别通过 valuesindex 属性来进行访问。values 属性是一个 Numpy 数组:

In[3]: data.values
Out[3]: array([0.25, 0.5 , 0.75, 1. ]) # 输出的缩进就是这样,还蛮怪的

index 属性则是一个类数组的对象,类型为 pd.Index,将在之后进行介绍:

In[4]: data.index
Out[4]: RangeIndex(start=0, stop=4, step=1)

和 Numpy 数组类似,我们可以通过方括号输入对应的索引来访问数据:

In[5]: data[1]
Out[5]: 0.5

In[6]: data[1:3] # 支持切片
Out[6]: 1 0.50
2 0.75
dtype: float64

Series 作为广义 Numpy 数组

虽然看起来和一维 Numpy 数组很像,但 Series 对象要比其更加通用和灵活。两者的关键区别在于:Numpy 数组使用隐式定义的数值索引来访问值,而 Series 对象则使用明确定义的索引来访问值。这一明确的索引定义赋予了 Series 对象额外的能力,例如索引不一定是整数,也可以是任意类型的值:

In[7]: data = pd.Series([0.25, 0.5, 0.75, 1.0],
index=['a', 'b', 'c', 'd'])
data
Out[7]: a 0.25
b 0.50
c 0.75
d 1.00
dtype: float64

元素的访问也随之更改:

In[8]: data['b']
Out[8]: 0.5

我们甚至可以使用非连续的索引:

In[9]: data = pd.Series([0.25, 0.5, 0.75, 1.0],
index=[2, 5, 3, 7])
data
Out[9]: 2 0.25
5 0.50
3 0.75
7 1.00
dtype: float64

In[10]: data[5]
Out[10]: 0.5

Series 作为特殊的字典

我们还可以将 Series 看作一种特殊的 Python 字典。字典是一种将任意的键映射到任意的值上的数据结构,而 Series 则是将包含类型信息的键映射到包含类型信息的值上的数据结构。类型信息可以为 Series 提供比普通字典更高效的操作。

我们可以直接基于字典来构建 Series 对象:

In[11]: population_dict = {'California': 38332521,
'Texas': 26448193,
'New York': 19651127,
'Florida': 19552860,
'Illinois': 12882135}
population = pd.Series(population_dict)
population
Out[11]: California 38332521
Texas 26448193
New York 19651127
Florida 19552860
Illinois 12882135
dtype: int64

可以看到索引即为字典的键(新版 Pandas 中似乎不会对键进行排序以生成索引,而是保持原状)。我们可以像字典一样通过索引访问值,也可以使用字典不支持的切片操作(注意此处的切片会包含尾部):

In[12]: population['California']
Out[12]: 38332521

In[13]: population['California':'Texas']
Out[13]: California 38332521
Texas 26448193
dtype: int64

构建 Series 对象

如上所述,Series 对象的构建方式有很多种,其基本遵循如下形式:

>>> pd.Series(data, index=index)

其中 index 为可选参数,data 可以是很多数据结构之一,例如:

  • 列表或 Numpy 数组
In[14]: pd.Series([2, 4, 6])
Out[14]: 0 2
1 4
2 6
dtype: int64
  • 标量(重复填充):
In[15]: pd.Series(5, index=[100, 200, 300])
Out[15]: 100 5
200 5
300 5
dtype: int64
  • 字典(键即为索引):
In[16]: pd.Series({2:'a', 1:'b', 3:'c'})
Out[16]: 1 b
2 a
3 c
dtype: object

索引还可以特别设置为子集的形式,例如:

In[17]: pd.Series({2:'a', 1:'b', 3:'c'}, index=[3, 2])
Out[17]: 3 c
2 a
dtype: object

DataFrame 对象

Series 对象一样,DataFrame 对象也可以被认为是 Numpy 数组的推广,或是一种特殊的 Python 字典。下面我们将分别从这两个角度进行介绍。

DataFrame 作为广义 Numpy 数组

我们可以将 DataFrame 看做一个拥有灵活的行索引与列名的二维 Numpy 数组,其本质上就是一系列对齐(共享相同的索引)的 Series 对象。为了说明这一点,我们首先构建一个包含五大洲面积数据的 Series

In[18]: area_dict = {'California': 423967, 'Texas': 695662, 'New York': 141297,
'Florida': 170312, 'Illinois': 149995}
area = pd.Series(area_dict)
area
Out[18]: California 423967
Texas 695662
New York 141297
Florida 170312
Illinois 149995
dtype: int64

将该对象与之前的人口数据进行合并(使用字典),即可得到一个 DataFrame 对象:

In[19]: states = pd.DataFrame({'population': population,
'area': area})
states
Out[19]: population area
California 38332521 423967
Texas 26448193 695662
New York 19651127 141297
Florida 19552860 170312
Illinois 12882135 149995

Series 对象类似,我们可以通过 index 属性来获取 DataFrame 对象的索引标签:

In[20]: states.index
Out[20]: Index(['California', 'Texas', 'New York', 'Florida', 'Illinois'], dtype='object')

此外,DataFrame 对象还有一个 columns 属性,其为一个包含列标签的 Index 对象:

In[21]: states.columns
Out[21]: Index(['population', 'area'], dtype='object')

因此,DataFrame 对象可以看做是二维 Numpy 数组的推广,其行与列都拥有广义的索引以方便进行数据查询。

DataFrame 作为特殊的字典

我们也可以将 DataFrame 对象看作一种特殊的字典,其将一个列名映射到一个 Series 对象上。例如:

In[22]: states['area']
Out[22]: California 423967
Texas 695662
New York 141297
Florida 170312
Illinois 149995
Name: area, dtype: int64

注意如果直接访问行索引会报错,因此 DataFrame 对象需要首先通过列索引来找到列对象,再去通过行索引访问具体的值。而对于二维 Numpy 数组来说,data[0] 返回的是第一行,需要与 DataFrame 区分开来(其返回的是列)。

构建 DataFrame 对象

DataFrame 对象的构建方式同样有很多种,例如:

  • 基于单个 Series 对象构建
In[23]: pd.DataFrame(population, columns=['population'])
Out[23]: population
California 38332521
Texas 26448193
New York 19651127
Florida 19552860
Illinois 12882135
  • 基于字典的列表构建
In[24]: data = [{'a': i, 'b': 2 * i}
for i in range(3)]
pd.DataFrame(data)
Out[24]: a b
0 0 0
1 1 2
2 2 4

即使字典中缺少部分键,Pandas 也会自动填充为 NaN

In[25]: pd.DataFrame([{'a': 1, 'b': 2}, {'b': 3, 'c': 4}])
Out[25]: a b c
0 1.0 2 NaN
1 NaN 3 4.0
  • 基于 Series 对象的字典构建
In[26]: pd.DataFrame({'population': population,
'area': area})
Out[26]: population area
California 38332521 423967
Texas 26448193 695662
New York 19651127 141297
Florida 19552860 170312
Illinois 12882135 149995
  • 基于二维 Numpy 数组构建(不指定则为整数索引):
In[27]: pd.DataFrame(np.random.rand(3, 2),
columns=['foo', 'bar'],
index=['a', 'b', 'c'])
Out[27]: foo bar
a 0.165577 0.626570
b 0.799517 0.376820
c 0.614334 0.886437
  • 基于 Numpy 结构化数组构建(较为特殊):
In[28]: A = np.zeros(3, dtype=[('A', 'i8'), ('B', 'f8')])
A
Out[28]: array([(0, 0.0), (0, 0.0), (0, 0.0)], dtype=[('A', '<i8'), ('B', '<f8')])
In[29]: pd.DataFrame(A)
Out[29]: A B
0 0 0.0
1 0 0.0
2 0 0.0

以上是书中列举的常用构建方法,这里补充一个在使用过程中遇到的构建案例:

  • 基于嵌套列表(或元组)构建(可以混用):
In[extra1]: pd.DataFrame([[1,2],[2,3],[3,4]], columns=['A', 'B'])
Out[extra2]: A B
0 1 2
1 2 3
2 3 4

对于这种构建方式,在实践过程中我们可以对于每组数据构建一个列表,然后通过 list(zip(a_list, b_list)) 创建嵌套列表,再基于上述方式创建 DataFrame 即可(行索引为默认整数索引)。

Index 对象

Series 对象与 DataFrame 对象中,都包含由于查找与修改数据的索引(index),其结构为一个 Index 对象。我们可以将 Index 对象看做一个不可变数组或是一个有序集合(多重集,因为可能包含重复值)。下面将分别从这两个角度进行介绍。首先我们基于一个整数列表创建一个简单的 Index 对象:

In[30]: ind = pd.Index([2, 3, 5, 7, 11])
ind
Out[30]: Int64Index([2, 3, 5, 7, 11], dtype='int64')

Index 作为不可变数组

Index 对象可以执行很多与数组类似的操作,如通过索引访问:

In[31]: ind[1]
Out[31]: 3

In[32]: ind[::2]
Out[32]: Int64Index([2, 5, 11], dtype='int64')

其也拥有很多与 Numpy 数组相似的属性:

In[33]: print(ind.size, ind.shape, ind.ndim, ind.dtype)
Out[33]: 5 (5,) 1 int64

需要注意的是,Index 对象与 Numpy 数组的区别在于其是不可变的(类似列表与元组的区别),我们不能对索引进行修改:

In[34]: ind[1] = 0
TypeError: Index does not support mutable operations

这样可以保证在共享索引时更加安全。

Index 作为有序集合

Pandas 对象的设计初衷之一是便于执行数据集之间的连接这样的操作。Index 对象遵循 Python 内置的 set 数据结构的特性,可以方便地进行各种连接操作,例如:

In[35]: indA = pd.Index([1, 3, 5, 7, 9])
indB = pd.Index([2, 3, 5, 7, 11])
In[36]: indA & indB # 交集
Out[36]: Int64Index([3, 5, 7], dtype='int64')

In[37]: indA | indB # 并集
Out[37]: Int64Index([1, 2, 3, 5, 7, 9, 11], dtype='int64')

In[38]: indA ^ indB # 对称差集
Out[38]: Int64Index([1, 2, 9, 11], dtype='int64')

上述操作也可以通过对象方法执行,如 indA.intersection(indB)

未完待续