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 函数来使用聚合(有时称为缩减)如sum
、mean
和std
(标准差)。当您使用 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
像mean
和sum
这样的函数接受一个可选的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)
表示“计算沿着行的总和”。
像cumsum
和cumprod
这样的其他方法不进行聚合,而是产生中间结果的数组:
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()
。
另外两个方法,any
和all
,特别适用于布尔数组。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) |
计算x 和y 中排序的公共元素 |
union1d(x, y) |
计算元素的排序并集 |
in1d(x, y) |
计算一个布尔数组,指示x 的每个元素是否包含在y 中 |
setdiff1d(x, y) |
差集,x 中不在y 中的元素 |
setxor1d(x, y)
| 对称差集;在任一数组中但不在两个数组中的元素 |
4.5 使用数组进行文件输入和输出
NumPy 能够以一些文本或二进制格式将数据保存到磁盘并从磁盘加载数据。在本节中,我只讨论 NumPy 内置的二进制格式,因为大多数用户更倾向于使用 pandas 和其他工具来加载文本或表格数据(详见第六章:数据加载、存储和文件格式)。
numpy.save
和numpy.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
译者:飞龙
此开放访问网络版本的《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,您需要熟悉其两个主要数据结构:Series和DataFrame。虽然它们并非适用于每个问题的通用解决方案,但它们为各种数据任务提供了坚实的基础。
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 的交互式显示的字符串表示在左侧显示索引,右侧显示值。由于我们没有为数据指定索引,因此会创建一个默认索引,由整数0
到N-1
(其中N
是数据的长度)组成。您可以通过其array
和index
属性分别获取 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 中的isna
和notna
函数来检测缺失数据:
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
属性已经适当设置。
行也可以通过特殊的iloc
和loc
属性按位置或名称检索(稍后在使用 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 的index
和columns
有设置它们的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