Python 数据分析(PYDA)第三版(二)(2)

简介: Python 数据分析(PYDA)第三版(二)

Python 数据分析(PYDA)第三版(二)(1)https://developer.aliyun.com/article/1482373


4.4 数组导向编程与数组

使用 NumPy 数组使您能够将许多种类的数据处理任务表达为简洁的数组表达式,否则可能需要编写循环。用数组表达式替换显式循环的这种做法被一些人称为向量化。一般来说,向量化的数组操作通常比它们纯 Python 等效的要快得多,在任何类型的数值计算中影响最大。稍后,在附录 A:高级 NumPy 中,我将解释广播,这是一种用于向量化计算的强大方法。

举个简单的例子,假设我们希望在一组常规值的网格上评估函数sqrt(x² + y²)numpy.meshgrid函数接受两个一维数组,并产生两个对应于两个数组中所有(x, y)对的二维矩阵:

In [169]: points = np.arange(-5, 5, 0.01) # 100 equally spaced points
In [170]: xs, ys = np.meshgrid(points, points)
In [171]: ys
Out[171]: 
array([[-5.  , -5.  , -5.  , ..., -5.  , -5.  , -5.  ],
 [-4.99, -4.99, -4.99, ..., -4.99, -4.99, -4.99],
 [-4.98, -4.98, -4.98, ..., -4.98, -4.98, -4.98],
 ...,
 [ 4.97,  4.97,  4.97, ...,  4.97,  4.97,  4.97],
 [ 4.98,  4.98,  4.98, ...,  4.98,  4.98,  4.98],
 [ 4.99,  4.99,  4.99, ...,  4.99,  4.99,  4.99]])

现在,评估函数只是写出您将用两个点写出的相同表达式的问题:

In [172]: z = np.sqrt(xs ** 2 + ys ** 2)
In [173]: z
Out[173]: 
array([[7.0711, 7.064 , 7.0569, ..., 7.0499, 7.0569, 7.064 ],
 [7.064 , 7.0569, 7.0499, ..., 7.0428, 7.0499, 7.0569],
 [7.0569, 7.0499, 7.0428, ..., 7.0357, 7.0428, 7.0499],
 ...,
 [7.0499, 7.0428, 7.0357, ..., 7.0286, 7.0357, 7.0428],
 [7.0569, 7.0499, 7.0428, ..., 7.0357, 7.0428, 7.0499],
 [7.064 , 7.0569, 7.0499, ..., 7.0428, 7.0499, 7.0569]])

作为第九章:绘图和可视化的预览,我使用 matplotlib 来创建这个二维数组的可视化:

In [174]: import matplotlib.pyplot as plt
In [175]: plt.imshow(z, cmap=plt.cm.gray, extent=[-5, 5, -5, 5])
Out[175]: <matplotlib.image.AxesImage at 0x17f04b040>
In [176]: plt.colorbar()
Out[176]: <matplotlib.colorbar.Colorbar at 0x1810661a0>
In [177]: plt.title("Image plot of $\sqrt{x² + y²}$ for a grid of values")
Out[177]: Text(0.5, 1.0, 'Image plot of $\\sqrt{x² + y²}$ for a grid of values'
)

在在网格上评估函数的绘图中,我使用了 matplotlib 函数imshow来从函数值的二维数组创建图像图。

图 4.3:在网格上评估函数的绘图

如果您在 IPython 中工作,可以通过执行plt.close("all")关闭所有打开的绘图窗口:

In [179]: plt.close("all")

注意

术语矢量化用于描述其他计算机科学概念,但在本书中,我使用它来描述对整个数据数组进行操作,而不是逐个值使用 Python 的for循环。

将条件逻辑表达为数组操作

numpy.where函数是三元表达式x if condition else y的矢量化版本。假设我们有一个布尔数组和两个值数组:

In [180]: xarr = np.array([1.1, 1.2, 1.3, 1.4, 1.5])
In [181]: yarr = np.array([2.1, 2.2, 2.3, 2.4, 2.5])
In [182]: cond = np.array([True, False, True, True, False])

假设我们想要从cond中对应的值为True时从xarr中取一个值,否则从yarr中取一个值。一个做到这一点的列表推导可能如下所示:

In [183]: result = [(x if c else y)
 .....:           for x, y, c in zip(xarr, yarr, cond)]
In [184]: result
Out[184]: [1.1, 2.2, 1.3, 1.4, 2.5]

这有多个问题。首先,对于大数组来说速度不会很快(因为所有工作都是在解释的 Python 代码中完成的)。其次,它不适用于多维数组。使用numpy.where可以通过单个函数调用来实现这一点:

In [185]: result = np.where(cond, xarr, yarr)
In [186]: result
Out[186]: array([1.1, 2.2, 1.3, 1.4, 2.5])

numpy.where的第二个和第三个参数不需要是数组;它们中的一个或两个可以是标量。在数据分析中,where的典型用法是根据另一个数组生成一个新的值数组。假设你有一个随机生成数据的矩阵,并且你想用 2 替换所有正值和用-2 替换所有负值。这可以通过numpy.where来实现:

In [187]: arr = rng.standard_normal((4, 4))
In [188]: arr
Out[188]: 
array([[ 2.6182,  0.7774,  0.8286, -0.959 ],
 [-1.2094, -1.4123,  0.5415,  0.7519],
 [-0.6588, -1.2287,  0.2576,  0.3129],
 [-0.1308,  1.27  , -0.093 , -0.0662]])
In [189]: arr > 0
Out[189]: 
array([[ True,  True,  True, False],
 [False, False,  True,  True],
 [False, False,  True,  True],
 [False,  True, False, False]])
In [190]: np.where(arr > 0, 2, -2)
Out[190]: 
array([[ 2,  2,  2, -2],
 [-2, -2,  2,  2],
 [-2, -2,  2,  2],
 [-2,  2, -2, -2]])

在使用numpy.where时,可以将标量和数组组合在一起。例如,我可以用常数 2 替换arr中的所有正值,如下所示:

In [191]: np.where(arr > 0, 2, arr) # set only positive values to 2
Out[191]: 
array([[ 2.    ,  2.    ,  2.    , -0.959 ],
 [-1.2094, -1.4123,  2.    ,  2.    ],
 [-0.6588, -1.2287,  2.    ,  2.    ],
 [-0.1308,  2.    , -0.093 , -0.0662]])

数学和统计方法

一组数学函数,用于计算整个数组或沿轴的数据的统计信息,作为数组类的方法可访问。您可以通过调用数组实例方法或使用顶级 NumPy 函数来使用聚合(有时称为缩减)如summeanstd(标准差)。当您使用 NumPy 函数,如numpy.sum时,您必须将要聚合的数组作为第一个参数传递。

这里我生成一些正态分布的随机数据并计算一些聚合统计数据:

In [192]: arr = rng.standard_normal((5, 4))
In [193]: arr
Out[193]: 
array([[-1.1082,  0.136 ,  1.3471,  0.0611],
 [ 0.0709,  0.4337,  0.2775,  0.5303],
 [ 0.5367,  0.6184, -0.795 ,  0.3   ],
 [-1.6027,  0.2668, -1.2616, -0.0713],
 [ 0.474 , -0.4149,  0.0977, -1.6404]])
In [194]: arr.mean()
Out[194]: -0.08719744457434529
In [195]: np.mean(arr)
Out[195]: -0.08719744457434529
In [196]: arr.sum()
Out[196]: -1.743948891486906

meansum这样的函数接受一个可选的axis参数,该参数在给定轴上计算统计量,结果是一个维数少一的数组:

In [197]: arr.mean(axis=1)
Out[197]: array([ 0.109 ,  0.3281,  0.165 , -0.6672, -0.3709])
In [198]: arr.sum(axis=0)
Out[198]: array([-1.6292,  1.0399, -0.3344, -0.8203])

这里,arr.mean(axis=1)表示“计算沿着列的平均值”,而arr.sum(axis=0)表示“计算沿着行的总和”。

cumsumcumprod这样的其他方法不进行聚合,而是产生中间结果的数组:

In [199]: arr = np.array([0, 1, 2, 3, 4, 5, 6, 7])
In [200]: arr.cumsum()
Out[200]: array([ 0,  1,  3,  6, 10, 15, 21, 28])

在多维数组中,像cumsum这样的累积函数返回一个相同大小的数组,但是根据每个较低维度切片沿着指定轴计算部分累积:

In [201]: arr = np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
In [202]: arr
Out[202]: 
array([[0, 1, 2],
 [3, 4, 5],
 [6, 7, 8]])

表达式arr.cumsum(axis=0)计算沿着行的累积和,而arr.cumsum(axis=1)计算沿着列的和:

In [203]: arr.cumsum(axis=0)
Out[203]: 
array([[ 0,  1,  2],
 [ 3,  5,  7],
 [ 9, 12, 15]])
In [204]: arr.cumsum(axis=1)
Out[204]: 
array([[ 0,  1,  3],
 [ 3,  7, 12],
 [ 6, 13, 21]])

查看表 4.6 以获取完整列表。我们将在后面的章节中看到这些方法的许多示例。

表 4.6:基本数组统计方法

方法 描述
sum 数组或沿轴的所有元素的总和;长度为零的数组的总和为 0
mean 算术平均值;对于长度为零的数组无效(返回NaN
std, var 分别是标准差和方差
min, max 最小值和最大值
argmin, argmax 分别是最小和最大元素的索引
cumsum 从 0 开始的元素的累积和
cumprod 从 1 开始的元素的累积乘积

布尔数组的方法

在前面的方法中,布尔值被强制转换为 1(True)和 0(False)。因此,sum经常被用作计算布尔数组中True值的计数的手段:

In [205]: arr = rng.standard_normal(100)
In [206]: (arr > 0).sum() # Number of positive values
Out[206]: 48
In [207]: (arr <= 0).sum() # Number of non-positive values
Out[207]: 52

这里表达式(arr > 0).sum()中的括号是必要的,以便能够在arr > 0的临时结果上调用sum()

另外两个方法,anyall,特别适用于布尔数组。any测试数组中是否有一个或多个值为True,而all检查是否每个值都为True

In [208]: bools = np.array([False, False, True, False])
In [209]: bools.any()
Out[209]: True
In [210]: bools.all()
Out[210]: False

这些方法也适用于非布尔数组,其中非零元素被视为True

排序

与 Python 内置的列表类型类似,NumPy 数组可以使用sort方法原地排序:

In [211]: arr = rng.standard_normal(6)
In [212]: arr
Out[212]: array([ 0.0773, -0.6839, -0.7208,  1.1206, -0.0548, -0.0824])
In [213]: arr.sort()
In [214]: arr
Out[214]: array([-0.7208, -0.6839, -0.0824, -0.0548,  0.0773,  1.1206])

您可以通过将轴编号传递给sort方法,在多维数组中对每个一维部分的值沿着轴进行原地排序。在这个例子数据中:

In [215]: arr = rng.standard_normal((5, 3))
In [216]: arr
Out[216]: 
array([[ 0.936 ,  1.2385,  1.2728],
 [ 0.4059, -0.0503,  0.2893],
 [ 0.1793,  1.3975,  0.292 ],
 [ 0.6384, -0.0279,  1.3711],
 [-2.0528,  0.3805,  0.7554]])

arr.sort(axis=0)对每列内的值进行排序,而arr.sort(axis=1)对每行进行排序:

In [217]: arr.sort(axis=0)
In [218]: arr
Out[218]: 
array([[-2.0528, -0.0503,  0.2893],
 [ 0.1793, -0.0279,  0.292 ],
 [ 0.4059,  0.3805,  0.7554],
 [ 0.6384,  1.2385,  1.2728],
 [ 0.936 ,  1.3975,  1.3711]])
In [219]: arr.sort(axis=1)
In [220]: arr
Out[220]: 
array([[-2.0528, -0.0503,  0.2893],
 [-0.0279,  0.1793,  0.292 ],
 [ 0.3805,  0.4059,  0.7554],
 [ 0.6384,  1.2385,  1.2728],
 [ 0.936 ,  1.3711,  1.3975]])

顶层方法numpy.sort返回一个数组的排序副本(类似于 Python 内置函数sorted),而不是在原地修改数组。例如:

In [221]: arr2 = np.array([5, -10, 7, 1, 0, -3])
In [222]: sorted_arr2 = np.sort(arr2)
In [223]: sorted_arr2
Out[223]: array([-10,  -3,   0,   1,   5,   7])

有关使用 NumPy 的排序方法的更多详细信息,以及更高级的技术,如间接排序,请参见附录 A:高级 NumPy。还可以在 pandas 中找到与排序相关的其他数据操作(例如,按一个或多个列对数据表进行排序)。

唯一值和其他集合逻辑

NumPy 具有一些用于一维 ndarrays 的基本集合操作。一个常用的操作是numpy.unique,它返回数组中排序的唯一值:

In [224]: names = np.array(["Bob", "Will", "Joe", "Bob", "Will", "Joe", "Joe"])
In [225]: np.unique(names)
Out[225]: array(['Bob', 'Joe', 'Will'], dtype='<U4')
In [226]: ints = np.array([3, 3, 3, 2, 2, 1, 1, 4, 4])
In [227]: np.unique(ints)
Out[227]: array([1, 2, 3, 4])

numpy.unique与纯 Python 替代方案进行对比:

In [228]: sorted(set(names))
Out[228]: ['Bob', 'Joe', 'Will']

在许多情况下,NumPy 版本更快,并返回一个 NumPy 数组而不是 Python 列表。

另一个函数numpy.in1d测试一个数组中的值在另一个数组中的成员资格,返回一个布尔数组:

In [229]: values = np.array([6, 0, 0, 3, 2, 5, 6])
In [230]: np.in1d(values, [2, 3, 6])
Out[230]: array([ True, False, False,  True,  True, False,  True])

请参见表 4.7 以获取 NumPy 中数组集合操作的列表。

表 4.7:数组集合操作

方法 描述
unique(x) 计算x中排序的唯一元素
intersect1d(x, y) 计算xy中排序的公共元素
union1d(x, y) 计算元素的排序并集
in1d(x, y) 计算一个布尔数组,指示x的每个元素是否包含在y
setdiff1d(x, y) 差集,x中不在y中的元素

setxor1d(x, y) | 对称差集;在任一数组中但不在两个数组中的元素 |

4.5 使用数组进行文件输入和输出

NumPy 能够以一些文本或二进制格式将数据保存到磁盘并从磁盘加载数据。在本节中,我只讨论 NumPy 内置的二进制格式,因为大多数用户更倾向于使用 pandas 和其他工具来加载文本或表格数据(详见第六章:数据加载、存储和文件格式)。

numpy.savenumpy.load是在磁盘上高效保存和加载数组数据的两个主要函数。默认情况下,数组以未压缩的原始二进制格式保存,文件扩展名为*.npy*:

In [231]: arr = np.arange(10)
In [232]: np.save("some_array", arr)

如果文件路径尚未以*.npy*结尾,则会添加扩展名。然后可以使用numpy.load加载磁盘上的数组:

In [233]: np.load("some_array.npy")
Out[233]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

您可以使用numpy.savez并将数组作为关键字参数传递来保存多个数组到未压缩的存档中:

In [234]: np.savez("array_archive.npz", a=arr, b=arr)

当加载一个*.npz*文件时,您会得到一个类似字典的对象,它会延迟加载各个数组:

In [235]: arch = np.load("array_archive.npz")
In [236]: arch["b"]
Out[236]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

如果您的数据压缩效果很好,您可能希望使用numpy.savez_compressed

In [237]: np.savez_compressed("arrays_compressed.npz", a=arr, b=arr)

4.6 线性代数

线性代数运算,如矩阵乘法、分解、行列式和其他方阵数学,是许多数组库的重要组成部分。两个二维数组使用*进行元素级乘积,而矩阵乘法需要使用dot函数或@中缀运算符。dot既是一个数组方法,也是numpy命名空间中用于执行矩阵乘法的函数:

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

x.dot(y)等同于np.dot(x, y)

In [246]: np.dot(x, y)
Out[246]: 
array([[ 28.,  64.],
 [ 67., 181.]])

两个二维数组与适当大小的一维数组之间的矩阵乘积会得到一个一维数组:

In [247]: x @ np.ones(3)
Out[247]: array([ 6., 15.])

numpy.linalg具有一套标准的矩阵分解和逆矩阵、行列式等功能:

In [248]: from numpy.linalg import inv, qr
In [249]: X = rng.standard_normal((5, 5))
In [250]: mat = X.T @ X
In [251]: inv(mat)
Out[251]: 
array([[  3.4993,   2.8444,   3.5956, -16.5538,   4.4733],
 [  2.8444,   2.5667,   2.9002, -13.5774,   3.7678],
 [  3.5956,   2.9002,   4.4823, -18.3453,   4.7066],
 [-16.5538, -13.5774, -18.3453,  84.0102, -22.0484],
 [  4.4733,   3.7678,   4.7066, -22.0484,   6.0525]])
In [252]: mat @ inv(mat)
Out[252]: 
array([[ 1.,  0.,  0.,  0.,  0.],
 [ 0.,  1.,  0.,  0.,  0.],
 [ 0.,  0.,  1., -0.,  0.],
 [ 0.,  0.,  0.,  1.,  0.],
 [-0.,  0.,  0.,  0.,  1.]])

表达式X.T.dot(X)计算X与其转置X.T的点积。

请参见表 4.8 以获取一些最常用的线性代数函数的列表。

表 4.8:常用的numpy.linalg函数

函数 描述
diag 返回方阵的对角线(或非对角线)元素作为 1D 数组,或将 1D 数组转换为具有非对角线零的方阵
dot 矩阵乘法
trace 计算对角线元素的和
det 计算矩阵行列式
eig 计算方阵的特征值和特征向量
inv 计算方阵的逆
pinv 计算矩阵的 Moore-Penrose 伪逆
qr 计算 QR 分解
svd 计算奇异值分解(SVD)
solve 解线性方程组 Ax = b,其中 A 是方阵
lstsq 计算Ax = b的最小二乘解

4.7 示例:随机漫步

随机漫步的模拟提供了利用数组操作的说明性应用。让我们首先考虑一个简单的从 0 开始的随机漫步,步长为 1 和-1,发生概率相等。

这是一个使用内置的random模块实现一次包含 1,000 步的随机漫步的纯 Python 方法:

#! blockstart
import random
position = 0
walk = [position]
nsteps = 1000
for _ in range(nsteps):
 step = 1 if random.randint(0, 1) else -1
 position += step
 walk.append(position)
#! blockend

查看图 4.4 以查看这些随机漫步中前 100 个值的示例图:

In [255]: plt.plot(walk[:100])

图 4.4:一个简单的随机漫步

你可能会观察到walk是随机步数的累积和,可以被评估为一个数组表达式。因此,我使用numpy.random模块一次绘制 1,000 次硬币翻转,将这些设置为 1 和-1,并计算累积和:

In [256]: nsteps = 1000
In [257]: rng = np.random.default_rng(seed=12345)  # fresh random generator
In [258]: draws = rng.integers(0, 2, size=nsteps)
In [259]: steps = np.where(draws == 0, 1, -1)
In [260]: walk = steps.cumsum()

从中我们可以开始提取统计数据,比如沿着漫步轨迹的最小值和最大值:

In [261]: walk.min()
Out[261]: -8
In [262]: walk.max()
Out[262]: 50

一个更复杂的统计量是第一次穿越时间,即随机漫步达到特定值的步数。在这里,我们可能想知道随机漫步离原点 0 至少 10 步的时间。np.abs(walk) >= 10给出一个布尔数组,指示漫步已经达到或超过 10,但我们想要第一个 10 或-10 的索引。事实证明,我们可以使用argmax来计算这个,它返回布尔数组中最大值的第一个索引(True是最大值):

In [263]: (np.abs(walk) >= 10).argmax()
Out[263]: 155

请注意,在这里使用argmax并不总是高效的,因为它总是对数组进行完整扫描。在这种特殊情况下,一旦观察到True,我们就知道它是最大值。

一次模拟多个随机漫步

如果你的目标是模拟许多随机漫步,比如说五千次,你可以通过对前面的代码进行微小修改来生成所有的随机漫步。如果传递一个 2 元组,numpy.random函数将生成一个二维数组的抽样,我们可以为每一行计算累积和,以一次性计算所有五千次随机漫步:

In [264]: nwalks = 5000
In [265]: nsteps = 1000
In [266]: draws = rng.integers(0, 2, size=(nwalks, nsteps)) # 0 or 1
In [267]: steps = np.where(draws > 0, 1, -1)
In [268]: walks = steps.cumsum(axis=1)
In [269]: walks
Out[269]: 
array([[  1,   2,   3, ...,  22,  23,  22],
 [  1,   0,  -1, ..., -50, -49, -48],
 [  1,   2,   3, ...,  50,  49,  48],
 ...,
 [ -1,  -2,  -1, ..., -10,  -9, -10],
 [ -1,  -2,  -3, ...,   8,   9,   8],
 [ -1,   0,   1, ...,  -4,  -3,  -2]])

现在,我们可以计算所有漫步中获得的最大值和最小值:

In [270]: walks.max()
Out[270]: 114
In [271]: walks.min()
Out[271]: -120

在这些漫步中,让我们计算到达 30 或-30 的最小穿越时间。这有点棘手,因为并非所有的 5000 次都达到 30。我们可以使用any方法来检查:

In [272]: hits30 = (np.abs(walks) >= 30).any(axis=1)
In [273]: hits30
Out[273]: array([False,  True,  True, ...,  True, False,  True])
In [274]: hits30.sum() # Number that hit 30 or -30
Out[274]: 3395

我们可以使用这个布尔数组来选择实际穿越绝对值 30 水平的walks的行,并在轴 1 上调用argmax来获取穿越时间:

In [275]: crossing_times = (np.abs(walks[hits30]) >= 30).argmax(axis=1)
In [276]: crossing_times
Out[276]: array([201, 491, 283, ..., 219, 259, 541])

最后,我们计算平均最小穿越时间:

In [277]: crossing_times.mean()
Out[277]: 500.5699558173785

随意尝试使用与等大小硬币翻转不同的步骤分布。你只需要使用不同的随机生成器方法,比如standard_normal来生成具有一定均值和标准差的正态分布步数:

In [278]: draws = 0.25 * rng.standard_normal((nwalks, nsteps))

注意

请记住,这种矢量化方法需要创建一个具有nwalks * nsteps元素的数组,这可能会在大型模拟中使用大量内存。如果内存更受限制,则需要采用不同的方法。

4.8 结论

尽管本书的大部分内容将集中在使用 pandas 构建数据整理技能上,我们将继续以类似的基于数组的风格工作。在附录 A:高级 NumPy 中,我们将深入探讨 NumPy 的特性,帮助您进一步发展数组计算技能。

五、使用 pandas 入门

原文:wesmckinney.com/book/pandas-basics

译者:飞龙

协议:CC BY-NC-SA 4.0

此开放访问网络版本的《Python 数据分析第三版》现已作为印刷版和数字版的伴侣提供。如果您发现任何勘误,请在此处报告。请注意,由 Quarto 生成的本站点的某些方面与 O’Reilly 的印刷版和电子书版本的格式不同。

如果您发现本书的在线版本有用,请考虑订购纸质版无 DRM 的电子书以支持作者。本网站的内容不得复制或再生产。代码示例采用 MIT 许可,可在 GitHub 或 Gitee 上找到。

pandas 将是本书剩余部分中的一个主要工具。它包含了专为在 Python 中快速方便地进行数据清洗和分析而设计的数据结构和数据操作工具。pandas 经常与数值计算工具(如 NumPy 和 SciPy)、分析库(如 statsmodels 和 scikit-learn)以及数据可视化库(如 matplotlib)一起使用。pandas 采用了 NumPy 的很多习惯用法,特别是基于数组的计算和对数据处理的偏好,而不使用for循环。

虽然 pandas 采用了许多来自 NumPy 的编码习惯,但最大的区别在于 pandas 是为处理表格或异构数据而设计的。相比之下,NumPy 更适合处理同质类型的数值数组数据。

自 2010 年成为开源项目以来,pandas 已经发展成一个相当庞大的库,适用于广泛的实际用例。开发者社区已经发展到超过 2500 名不同的贡献者,他们在解决日常数据问题时一直在帮助构建这个项目。充满活力的 pandas 开发者和用户社区是其成功的关键部分。

注意

很多人不知道我自 2013 年以来并没有积极参与日常 pandas 的开发;从那时起,它一直是一个完全由社区管理的项目。请务必向核心开发人员和所有贡献者传达感谢他们的辛勤工作!

在本书的剩余部分中,我使用以下的 NumPy 和 pandas 的导入约定:

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

因此,每当在代码中看到pd.时,它指的是 pandas。您可能也会发现将 Series 和 DataFrame 导入到本地命名空间中更容易,因为它们经常被使用:

In [3]: from pandas import Series, DataFrame

5.1 pandas 数据结构简介

要开始使用 pandas,您需要熟悉其两个主要数据结构:SeriesDataFrame。虽然它们并非适用于每个问题的通用解决方案,但它们为各种数据任务提供了坚实的基础。

Series

Series 是一个一维数组样对象,包含一系列值(与 NumPy 类型相似的类型)和一个关联的数据标签数组,称为索引。最简单的 Series 是仅由数据数组形成的:

In [14]: obj = pd.Series([4, 7, -5, 3])
In [15]: obj
Out[15]: 
0    4
1    7
2   -5
3    3
dtype: int64

Series 的交互式显示的字符串表示在左侧显示索引,右侧显示值。由于我们没有为数据指定索引,因此会创建一个默认索引,由整数0N-1(其中N是数据的长度)组成。您可以通过其arrayindex属性分别获取 Series 的数组表示和索引对象:

In [16]: obj.array
Out[16]: 
<PandasArray>
[4, 7, -5, 3]
Length: 4, dtype: int64
In [17]: obj.index
Out[17]: RangeIndex(start=0, stop=4, step=1)

.array属性的结果是一个PandasArray,通常包装了一个 NumPy 数组,但也可以包含特殊的扩展数组类型,这将在 Ch 7.3:扩展数据类型中更详细讨论。

通常,您会希望创建一个带有标识每个数据点的索引的 Series:

In [18]: obj2 = pd.Series([4, 7, -5, 3], index=["d", "b", "a", "c"])
In [19]: obj2
Out[19]: 
d    4
b    7
a   -5
c    3
dtype: int64
In [20]: obj2.index
Out[20]: Index(['d', 'b', 'a', 'c'], dtype='object')

与 NumPy 数组相比,当选择单个值或一组值时,可以在索引中使用标签:

In [21]: obj2["a"]
Out[21]: -5
In [22]: obj2["d"] = 6
In [23]: obj2[["c", "a", "d"]]
Out[23]: 
c    3
a   -5
d    6
dtype: int64

这里["c", "a", "d"]被解释为索引列表,即使它包含字符串而不是整数。

使用 NumPy 函数或类似 NumPy 的操作,例如使用布尔数组进行过滤、标量乘法或应用数学函数,将保留索引值链接:

In [24]: obj2[obj2 > 0]
Out[24]: 
d    6
b    7
c    3
dtype: int64
In [25]: obj2 * 2
Out[25]: 
d    12
b    14
a   -10
c     6
dtype: int64
In [26]: import numpy as np
In [27]: np.exp(obj2)
Out[27]: 
d     403.428793
b    1096.633158
a       0.006738
c      20.085537
dtype: float64

将 Series 视为固定长度的有序字典的另一种方式,因为它是索引值到数据值的映射。它可以在许多上下文中使用,您可能会使用字典:

In [28]: "b" in obj2
Out[28]: True
In [29]: "e" in obj2
Out[29]: False

如果您的数据包含在 Python 字典中,可以通过传递字典来创建一个 Series:

In [30]: sdata = {"Ohio": 35000, "Texas": 71000, "Oregon": 16000, "Utah": 5000}
In [31]: obj3 = pd.Series(sdata)
In [32]: obj3
Out[32]: 
Ohio      35000
Texas     71000
Oregon    16000
Utah       5000
dtype: int64

Series 可以使用其to_dict方法转换回字典:

In [33]: obj3.to_dict()
Out[33]: {'Ohio': 35000, 'Texas': 71000, 'Oregon': 16000, 'Utah': 5000}

当您只传递一个字典时,生成的 Series 中的索引将遵循字典的keys方法的键的顺序,这取决于键插入顺序。您可以通过传递一个索引,其中包含字典键的顺序,以便它们出现在生成的 Series 中的顺序来覆盖这一点:

In [34]: states = ["California", "Ohio", "Oregon", "Texas"]
In [35]: obj4 = pd.Series(sdata, index=states)
In [36]: obj4
Out[36]: 
California        NaN
Ohio          35000.0
Oregon        16000.0
Texas         71000.0
dtype: float64

在这里,sdata中找到的三个值被放置在适当的位置,但由于没有找到"California"的值,它显示为NaN(不是一个数字),在 pandas 中被视为标记缺失或NA值。由于states中没有包含"Utah",因此它被排除在结果对象之外。

我将使用术语“missing”、“NA”或“null”来交替引用缺失数据。应该使用 pandas 中的isnanotna函数来检测缺失数据:

In [37]: pd.isna(obj4)
Out[37]: 
California     True
Ohio          False
Oregon        False
Texas         False
dtype: bool
In [38]: pd.notna(obj4)
Out[38]: 
California    False
Ohio           True
Oregon         True
Texas          True
dtype: bool

Series 还具有这些作为实例方法:

In [39]: obj4.isna()
Out[39]: 
California     True
Ohio          False
Oregon        False
Texas         False
dtype: bool

我将在第七章:数据清洗和准备中更详细地讨论处理缺失数据的工作。

对于许多应用程序来说,Series 的一个有用特性是它在算术运算中自动按索引标签对齐:

In [40]: obj3
Out[40]: 
Ohio      35000
Texas     71000
Oregon    16000
Utah       5000
dtype: int64
In [41]: obj4
Out[41]: 
California        NaN
Ohio          35000.0
Oregon        16000.0
Texas         71000.0
dtype: float64
In [42]: obj3 + obj4
Out[42]: 
California         NaN
Ohio           70000.0
Oregon         32000.0
Texas         142000.0
Utah               NaN
dtype: float64

数据对齐功能将在后面更详细地讨论。如果您有数据库经验,可以将其视为类似于连接操作。

Series 对象本身和其索引都有一个name属性,它与 pandas 功能的其他区域集成:

In [43]: obj4.name = "population"
In [44]: obj4.index.name = "state"
In [45]: obj4
Out[45]: 
state
California        NaN
Ohio          35000.0
Oregon        16000.0
Texas         71000.0
Name: population, dtype: float64

Series 的索引可以通过赋值来直接更改:

In [46]: obj
Out[46]: 
0    4
1    7
2   -5
3    3
dtype: int64
In [47]: obj.index = ["Bob", "Steve", "Jeff", "Ryan"]
In [48]: obj
Out[48]: 
Bob      4
Steve    7
Jeff    -5
Ryan     3
dtype: int64

DataFrame

DataFrame 表示数据的矩形表,并包含一个有序的、命名的列集合,每个列可以是不同的值类型(数值、字符串、布尔值等)。DataFrame 既有行索引又有列索引;它可以被视为共享相同索引的一系列 Series 的字典。

注意

虽然 DataFrame 在物理上是二维的,但您可以使用它来以分层索引的方式表示更高维度的数据,这是我们将在第八章:数据整理:连接、合并和重塑中讨论的一个主题,并且是 pandas 中一些更高级数据处理功能的一个组成部分。

有许多构建 DataFrame 的方法,尽管其中最常见的一种是从等长列表或 NumPy 数组的字典中构建:

data = {"state": ["Ohio", "Ohio", "Ohio", "Nevada", "Nevada", "Nevada"],
 "year": [2000, 2001, 2002, 2001, 2002, 2003],
 "pop": [1.5, 1.7, 3.6, 2.4, 2.9, 3.2]}
frame = pd.DataFrame(data)

生成的 DataFrame 将自动分配其索引,与 Series 一样,并且列根据data中键的顺序放置(取决于字典中的插入顺序):

In [50]: frame
Out[50]: 
 state  year  pop
0    Ohio  2000  1.5
1    Ohio  2001  1.7
2    Ohio  2002  3.6
3  Nevada  2001  2.4
4  Nevada  2002  2.9
5  Nevada  2003  3.2

注意

如果您正在使用 Jupyter 笔记本,pandas DataFrame 对象将显示为更适合浏览器的 HTML 表格。请参见图 5.1 作为示例。

图 5.1:Jupyter 中 pandas DataFrame 对象的外观

对于大型 DataFrame,head方法仅选择前五行:

In [51]: frame.head()
Out[51]: 
 state  year  pop
0    Ohio  2000  1.5
1    Ohio  2001  1.7
2    Ohio  2002  3.6
3  Nevada  2001  2.4
4  Nevada  2002  2.9

类似地,tail返回最后五行:

In [52]: frame.tail()
Out[52]: 
 state  year  pop
1    Ohio  2001  1.7
2    Ohio  2002  3.6
3  Nevada  2001  2.4
4  Nevada  2002  2.9
5  Nevada  2003  3.2

如果指定一系列列,DataFrame 的列将按照该顺序排列:

In [53]: pd.DataFrame(data, columns=["year", "state", "pop"])
Out[53]: 
 year   state  pop
0  2000    Ohio  1.5
1  2001    Ohio  1.7
2  2002    Ohio  3.6
3  2001  Nevada  2.4
4  2002  Nevada  2.9
5  2003  Nevada  3.2

如果传递一个字典中不包含的列,它将以缺失值的形式出现在结果中:

In [54]: frame2 = pd.DataFrame(data, columns=["year", "state", "pop", "debt"])
In [55]: frame2
Out[55]: 
 year   state  pop debt
0  2000    Ohio  1.5  NaN
1  2001    Ohio  1.7  NaN
2  2002    Ohio  3.6  NaN
3  2001  Nevada  2.4  NaN
4  2002  Nevada  2.9  NaN
5  2003  Nevada  3.2  NaN
In [56]: frame2.columns
Out[56]: Index(['year', 'state', 'pop', 'debt'], dtype='object')

DataFrame 中的列可以通过类似字典的表示法或使用点属性表示法检索为 Series:

In [57]: frame2["state"]
Out[57]: 
0      Ohio
1      Ohio
2      Ohio
3    Nevada
4    Nevada
5    Nevada
Name: state, dtype: object
In [58]: frame2.year
Out[58]: 
0    2000
1    2001
2    2002
3    2001
4    2002
5    2003
Name: year, dtype: int64

注意

提供类似属性访问(例如,frame2.year)和 IPython 中列名称的制表符补全作为便利。

frame2[column]适用于任何列名,但只有当列名是有效的 Python 变量名且不与 DataFrame 中的任何方法名冲突时,frame2.column才适用。例如,如果列名包含空格或下划线以外的其他符号,则无法使用点属性方法访问。

请注意,返回的 Series 具有与 DataFrame 相同的索引,并且它们的name属性已经适当设置。

行也可以通过特殊的ilocloc属性按位置或名称检索(稍后在使用 loc 和 iloc 在 DataFrame 上进行选择中详细介绍):

In [59]: frame2.loc[1]
Out[59]: 
year     2001
state    Ohio
pop       1.7
debt      NaN
Name: 1, dtype: object
In [60]: frame2.iloc[2]
Out[60]: 
year     2002
state    Ohio
pop       3.6
debt      NaN
Name: 2, dtype: object

列可以通过赋值进行修改。例如,可以为空的debt列分配一个标量值或一个值数组:

In [61]: frame2["debt"] = 16.5
In [62]: frame2
Out[62]: 
 year   state  pop  debt
0  2000    Ohio  1.5  16.5
1  2001    Ohio  1.7  16.5
2  2002    Ohio  3.6  16.5
3  2001  Nevada  2.4  16.5
4  2002  Nevada  2.9  16.5
5  2003  Nevada  3.2  16.5
In [63]: frame2["debt"] = np.arange(6.)
In [64]: frame2
Out[64]: 
 year   state  pop  debt
0  2000    Ohio  1.5   0.0
1  2001    Ohio  1.7   1.0
2  2002    Ohio  3.6   2.0
3  2001  Nevada  2.4   3.0
4  2002  Nevada  2.9   4.0
5  2003  Nevada  3.2   5.0

当将列表或数组分配给列时,值的长度必须与 DataFrame 的长度相匹配。如果分配一个 Series,其标签将被重新对齐到 DataFrame 的索引,插入任何不存在的索引值的缺失值:

In [65]: val = pd.Series([-1.2, -1.5, -1.7], index=[2, 4, 5])
In [66]: frame2["debt"] = val
In [67]: frame2
Out[67]: 
 year   state  pop  debt
0  2000    Ohio  1.5   NaN
1  2001    Ohio  1.7   NaN
2  2002    Ohio  3.6  -1.2
3  2001  Nevada  2.4   NaN
4  2002  Nevada  2.9  -1.5
5  2003  Nevada  3.2  -1.7

分配一个不存在的列将创建一个新列。

del关键字将像字典一样删除列。例如,首先添加一个新列,其中布尔值等于"Ohio"state列:

In [68]: frame2["eastern"] = frame2["state"] == "Ohio"
In [69]: frame2
Out[69]: 
 year   state  pop  debt  eastern
0  2000    Ohio  1.5   NaN     True
1  2001    Ohio  1.7   NaN     True
2  2002    Ohio  3.6  -1.2     True
3  2001  Nevada  2.4   NaN    False
4  2002  Nevada  2.9  -1.5    False
5  2003  Nevada  3.2  -1.7    False

警告:

不能使用frame2.eastern点属性表示法创建新列。

然后可以使用del方法删除此列:

In [70]: del frame2["eastern"]
In [71]: frame2.columns
Out[71]: Index(['year', 'state', 'pop', 'debt'], dtype='object')

注意

从 DataFrame 索引返回的列是基础数据的视图,而不是副本。因此,对 Series 的任何原地修改都将反映在 DataFrame 中。可以使用 Series 的copy方法显式复制列。

另一种常见的数据形式是嵌套字典的字典:

In [72]: populations = {"Ohio": {2000: 1.5, 2001: 1.7, 2002: 3.6},
 ....:                "Nevada": {2001: 2.4, 2002: 2.9}}

如果将嵌套字典传递给 DataFrame,pandas 将解释外部字典键为列,内部键为行索引:

In [73]: frame3 = pd.DataFrame(populations)
In [74]: frame3
Out[74]: 
 Ohio  Nevada
2000   1.5     NaN
2001   1.7     2.4
2002   3.6     2.9

您可以使用类似于 NumPy 数组的语法转置 DataFrame(交换行和列):

In [75]: frame3.T
Out[75]: 
 2000  2001  2002
Ohio     1.5   1.7   3.6
Nevada   NaN   2.4   2.9

警告:

请注意,如果列的数据类型不全都相同,则转置会丢弃列数据类型,因此转置然后再次转置可能会丢失先前的类型信息。在这种情况下,列变成了纯 Python 对象的数组。

内部字典中的键被组合以形成结果中的索引。如果指定了显式索引,则这种情况不成立:

In [76]: pd.DataFrame(populations, index=[2001, 2002, 2003])
Out[76]: 
 Ohio  Nevada
2001   1.7     2.4
2002   3.6     2.9
2003   NaN     NaN

Series 的字典以类似的方式处理:

In [77]: pdata = {"Ohio": frame3["Ohio"][:-1],
 ....:          "Nevada": frame3["Nevada"][:2]}
In [78]: pd.DataFrame(pdata)
Out[78]: 
 Ohio  Nevada
2000   1.5     NaN
2001   1.7     2.4

有关可以传递给 DataFrame 构造函数的许多内容,请参见表 5.1。

表 5.1:DataFrame 构造函数的可能数据输入

类型 注释
2D ndarray 一组数据的矩阵,传递可选的行和列标签
数组、列表或元组的字典 每个序列都变成了 DataFrame 中的一列;所有序列必须具有相同的长度
NumPy 结构化/记录数组 被视为“数组的字典”情况
Series 的字典 每个值都变成了一列;如果没有传递显式索引,则每个 Series 的索引被合并在一起以形成结果的行索引
字典的字典 每个内部字典都变成了一列;键被合并以形成行索引,就像“Series 的字典”情况一样
字典或 Series 的列表 每个项目都变成了 DataFrame 中的一行;字典键或 Series 索引的并集成为 DataFrame 的列标签
列表或元组的列表 被视为“2D ndarray”情况
另一个 DataFrame 除非传递了不同的索引,否则将使用 DataFrame 的索引
NumPy MaskedArray 与“2D ndarray”情况类似,只是在 DataFrame 结果中缺少掩码值

如果 DataFrame 的indexcolumns有设置它们的name属性,这些也会被显示出来:

In [79]: frame3.index.name = "year"
In [80]: frame3.columns.name = "state"
In [81]: frame3
Out[81]: 
state  Ohio  Nevada
year 
2000    1.5     NaN
2001    1.7     2.4
2002    3.6     2.9

与 Series 不同,DataFrame 没有name属性。DataFrame 的to_numpy方法将 DataFrame 中包含的数据作为二维 ndarray 返回:

In [82]: frame3.to_numpy()
Out[82]: 
array([[1.5, nan],
 [1.7, 2.4],
 [3.6, 2.9]])

如果 DataFrame 的列是不同的数据类型,则返回的数组的数据类型将被选择以容纳所有列:

In [83]: frame2.to_numpy()
Out[83]: 
array([[2000, 'Ohio', 1.5, nan],
 [2001, 'Ohio', 1.7, nan],
 [2002, 'Ohio', 3.6, -1.2],
 [2001, 'Nevada', 2.4, nan],
 [2002, 'Nevada', 2.9, -1.5],
 [2003, 'Nevada', 3.2, -1.7]], dtype=object)

索引对象

pandas 的 Index 对象负责保存轴标签(包括 DataFrame 的列名)和其他元数据(如轴名称)。在构建 Series 或 DataFrame 时使用的任何数组或其他标签序列都会在内部转换为 Index:

In [84]: obj = pd.Series(np.arange(3), index=["a", "b", "c"])
In [85]: index = obj.index
In [86]: index
Out[86]: Index(['a', 'b', 'c'], dtype='object')
In [87]: index[1:]
Out[87]: Index(['b', 'c'], dtype='object')

Index 对象是不可变的,因此用户无法修改它们:

index[1] = "d"  # TypeError

不可变性使得在数据结构之间共享 Index 对象更加安全:

In [88]: labels = pd.Index(np.arange(3))
In [89]: labels
Out[89]: Index([0, 1, 2], dtype='int64')
In [90]: obj2 = pd.Series([1.5, -2.5, 0], index=labels)
In [91]: obj2
Out[91]: 
0    1.5
1   -2.5
2    0.0
dtype: float64
In [92]: obj2.index is labels
Out[92]: True

注意

一些用户可能不经常利用 Index 提供的功能,但由于一些操作会产生包含索引数据的结果,因此了解它们的工作原理是很重要的。

除了类似数组,Index 还表现得像一个固定大小的集合:

In [93]: frame3
Out[93]: 
state  Ohio  Nevada
year 
2000    1.5     NaN
2001    1.7     2.4
2002    3.6     2.9
In [94]: frame3.columns
Out[94]: Index(['Ohio', 'Nevada'], dtype='object', name='state')
In [95]: "Ohio" in frame3.columns
Out[95]: True
In [96]: 2003 in frame3.index
Out[96]: False

与 Python 集合不同,pandas 的 Index 可以包含重复标签:

In [97]: pd.Index(["foo", "foo", "bar", "bar"])
Out[97]: Index(['foo', 'foo', 'bar', 'bar'], dtype='object')

具有重复标签的选择将选择该标签的所有出现。

每个 Index 都有一些用于集合逻辑的方法和属性,可以回答关于其包含的数据的其他常见问题。一些有用的方法总结在 Table 5.2 中。

Table 5.2: 一些索引方法和属性

方法/属性 描述
append() 与其他 Index 对象连接,生成一个新的 Index
difference() 计算索引的差集
intersection() 计算集合交集
union() 计算集合并
isin() 计算布尔数组,指示每个值是否包含在传递的集合中
delete() 通过删除索引i处的元素来计算新的索引
drop() 通过删除传递的值来计算新的索引
insert() 通过在索引i处插入元素来计算新的索引
is_monotonic 如果每个元素大于或等于前一个元素则返回True
is_unique 如果索引没有重复值则返回True

| unique() | 计算索引中唯一值的数组 |


Python 数据分析(PYDA)第三版(二)(3)https://developer.aliyun.com/article/1482376

相关文章
|
21天前
|
机器学习/深度学习 数据采集 数据可视化
Python 数据分析:从零开始构建你的数据科学项目
【10月更文挑战第9天】Python 数据分析:从零开始构建你的数据科学项目
45 2
|
21天前
|
机器学习/深度学习 数据可视化 算法
使用Python进行数据分析:从零开始的指南
【10月更文挑战第9天】使用Python进行数据分析:从零开始的指南
35 1
|
3天前
|
数据采集 存储 数据挖掘
Python数据分析:Pandas库的高效数据处理技巧
【10月更文挑战第27天】在数据分析领域,Python的Pandas库因其强大的数据处理能力而备受青睐。本文介绍了Pandas在数据导入、清洗、转换、聚合、时间序列分析和数据合并等方面的高效技巧,帮助数据分析师快速处理复杂数据集,提高工作效率。
16 0
|
1月前
|
数据采集 数据可视化 数据挖掘
Python中的数据分析:从零开始的指南
Python中的数据分析:从零开始的指南
46 2
|
29天前
|
数据采集 数据可视化 数据挖掘
基于Python的数据分析与可视化实战
本文将引导读者通过Python进行数据分析和可视化,从基础的数据操作到高级的数据可视化技巧。我们将使用Pandas库处理数据,并利用Matplotlib和Seaborn库创建直观的图表。文章不仅提供代码示例,还将解释每个步骤的重要性和目的,帮助读者理解背后的逻辑。无论你是初学者还是有一定基础的开发者,这篇文章都将为你提供有价值的见解和技能。
61 0
|
4天前
|
存储 数据挖掘 数据处理
Python数据分析:Pandas库的高效数据处理技巧
【10月更文挑战第26天】Python 是数据分析领域的热门语言,Pandas 库以其高效的数据处理功能成为数据科学家的利器。本文介绍 Pandas 在数据读取、筛选、分组、转换和合并等方面的高效技巧,并通过示例代码展示其实际应用。
15 1
|
9天前
|
数据采集 数据可视化 数据挖掘
R语言与Python:比较两种数据分析工具
R语言和Python是目前最流行的两种数据分析工具。本文将对这两种工具进行比较,包括它们的历史、特点、应用场景、社区支持、学习资源、性能等方面,以帮助读者更好地了解和选择适合自己的数据分析工具。
16 2
|
21天前
|
数据采集 数据可视化 数据挖掘
使用Python进行高效的数据分析
【10月更文挑战第9天】使用Python进行高效的数据分析
19 1
|
21天前
|
数据采集 机器学习/深度学习 数据挖掘
如何使用Python进行高效的数据分析
【10月更文挑战第9天】如何使用Python进行高效的数据分析
22 1
|
23天前
|
机器学习/深度学习 存储 数据挖掘
深入浅出:使用Python进行数据分析
【10月更文挑战第7天】本文将带你进入Python数据分析的世界,从基础的数据结构开始,逐步深入到数据处理、数据可视化以及机器学习等高级主题。我们将通过实际案例和代码示例,让你了解如何使用Python进行有效的数据分析。无论你是初学者还是有一定经验的开发者,都能从中获得新的知识和启发。

热门文章

最新文章