NumPy ndarray 提供了一种将同构类型数据块(连续或跨步)解释为多维数组对象的方法。数据类型或 dtype 决定了如何将数据解释为浮点、整数、布尔值或任何其他类型。
ndarray 之所以灵活,部分原因在于每个数组对象都是数据块上的跨行视图。例如,您可能想知道数组视图如何不复制任何数据。原因是 ndarray 不仅仅是一个内存块和一种数据类型;它还具有跨行信息,使数组能够以不同的步长在内存中移动。更准确地说,ndarray 内部由以下内容组成:arr[::2, ::-1]
- 指向数据的指针,即 RAM 或内存映射文件中的数据块
- 描述数组中固定大小的值单元格的数据类型或 dtype
- 显示数组形状 shape 的元组
- 跨度元组 strides ,表示沿某个维度推进一个元素需要“跨步”的字节数
例如,10×5阵列的形状为:(10,5)
In [12]: np.ones((10, 5)).shape
Out[12]: (10, 5)
典型的(C 阶)3 × 4 × 5 数组(8 字节)具有步幅(了解步幅可能很有用,因为通常,特定轴上的步幅越大,沿该轴执行计算的成本就越高):float64(160, 40, 8)
In [13]: np.ones((3, 4, 5), dtype=np.float64).strides
Out[13]: (160, 40, 8)
虽然 NumPy 用户很少会对数组步幅感兴趣,但需要它们来构建“零拷贝”数组视图。步幅甚至可以是负数,这使得数组能够在内存中“向后”移动(例如,在像这样的切片中就是这种情况)。obj[::-1]
obj[:, ::-1]
数据类型与层次结构
有时,可能需要检查数组是否包含整数、浮点数、字符串或 Python 对象的代码。由于浮点数有多种类型( 到 ),因此检查数据类型是否在类型列表中会非常冗长。幸运的是,数据类型具有超类,例如float16
float128
np.integer
np.floating
np.issubdtype
,它们可以与函数一起使用
In [14]: ints = np.ones(10, dtype=np.uint16)
In [15]: floats = np.ones(10, dtype=np.float32)
In [16]: np.issubdtype(ints.dtype, np.integer)
Out[16]: True
In [17]: np.issubdtype(floats.dtype, np.floating)
Out[17]: True
可以通过调用特定数据类型的方法查看该类型的所有父类:mro
In [18]: np.float64.mro()
Out[18]:
[numpy.float64,
numpy.floating,
numpy.inexact,
numpy.number,
numpy.generic,
float,
object]
因此,我们还有:
In [19]: np.issubdtype(ints.dtype, np.number)
Out[19]: True
数组高级算法与应用
除了花哨的索引、切片和布尔子集之外,还有很多方法可以处理数组。虽然数据分析应用程序的大部分繁重工作都是由 pandas 中的更高级别的函数处理的,但在某些时候,您可能需要编写一个现有库中找不到的数据算法。
数组重塑
重塑数组是指改变数组的维度和形状,而不改变数组中的元素。在许多情况下,可以将数组从一个形状转换为另一个形状,而无需复制任何数据。reshape
In [20]: arr = np.arange(8)
In [21]: arr
Out[21]: array([0, 1, 2, 3, 4, 5, 6, 7])
In [22]: arr.reshape((4, 2))
Out[22]:
array([[0, 1],
[2, 3],
[4, 5],
[6, 7]])
In [23]: arr.reshape((4, 2)).reshape((2, 4))
Out[23]:
array([[0, 1, 2, 3],
[4, 5, 6, 7]])
传递的形状尺寸之一可以是 –1,在这种情况下,将从数据推断出用于该尺寸的值:
In [24]: arr = np.arange(15)
In [25]: arr.reshape((5, -1))
Out[25]:
array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11],
[12, 13, 14]])
由于数组的属性是元组,因此也可以将其传递给:shape
reshape
In [26]: other_arr = np.ones((3, 5))
In [27]: other_arr.shape
Out[27]: (3, 5)
In [28]: arr.reshape(other_arr.shape)
Out[28]:
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
从一维到更高维度的相反操作通常称为扁平化 或 复杂化:
In [29]: arr = np.arange(15).reshape((5, 3))
In [30]: arr
Out[30]:
array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11],
[12, 13, 14]])
In [31]: arr.ravel()
Out[31]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])
如果结果中的值与原始数组相同,ravel
不会产生源数据的副本。flatten
方法的行为类似于ravel
,只不过它总是返回数据的副本:
In [32]: arr.flatten()
Out[32]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])
数据可以按不同的顺序进行重塑或分解。对于 NumPy 新手来说,这是一个稍微微妙的问题,所以在下一小节中我们将专门讲解这个问题。
C 与 FORTRAN 顺序
NumPy 能够适应内存中数据的许多不同布局。默认情况下,NumPy数组是按行优先顺序创建的。从空间上讲,这就意味着,对于一个二维数组,每行中的数据项是被存放在相邻内存位置上的。另一种顺序是列优先顺序,它意味着每列中的数据项是被存放在相邻内存位置上的。
由于一些历史原因,行和列优先顺序又分别称为C和Fortran顺序。在FORTRAN 77中,矩阵全都是列优先的。
像 reshape
和 reval
这样的函数,都可以接受一个表示数组数据存放顺序的order
参数。一般可以是‘C’
或‘F’
(还有‘A’
和‘K’
等不常用的选项,具体请参考NumPy的文档)
In [33]: arr = np.arange(12).reshape((3, 4))
In [34]: arr
Out[34]:
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
In [35]: arr.ravel()
Out[35]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
In [36]: arr.ravel('F')
Out[36]: array([ 0, 4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11])
重塑二维以上的数组可能有点令人费解,C和FORTRAN顺序之间的关键区别在于维度的遍历方式:
C/行优先顺序
首先遍历更高的维度(例如,轴1会先于轴0被处理)。
FORTRAN/列优先顺序
最后遍历更高的维度(例如,轴0会先于轴1被处理)。
数组的合并和拆分
numpy.concatenate
获取数组的序列(元组、列表等),并沿输入轴按顺序连接它们:
In [37]: arr1 = np.array([[1, 2, 3], [4, 5, 6]])
In [38]: arr2 = np.array([[7, 8, 9], [10, 11, 12]])
In [39]: np.concatenate([arr1, arr2], axis=0)
Out[39]:
array([[ 1, 2, 3],
[ 4, 5, 6],
[ 7, 8, 9],
[10, 11, 12]])
In [40]: np.concatenate([arr1, arr2], axis=1)
Out[40]:
array([[ 1, 2, 3, 7, 8, 9],
[ 4, 5, 6, 10, 11, 12]])
对于常见的级联类型,有一些方便的函数,如vstack
和hstack
。前面的操作可以表示为:
In [41]: np.vstack((arr1, arr2))
Out[41]:
array([[ 1, 2, 3],
[ 4, 5, 6],
[ 7, 8, 9],
[10, 11, 12]])
In [42]: np.hstack((arr1, arr2))
Out[42]:
array([[ 1, 2, 3, 7, 8, 9],
[ 4, 5, 6, 10, 11, 12]])
于此相反,split
将一个数组沿一个轴分割为多个数组:
In [43]: arr = rng.standard_normal((5, 2))
In [44]: arr
Out[44]:
array([[-1.4238, 1.2637],
[-0.8707, -0.2592],
[-0.0753, -0.7409],
[-1.3678, 0.6489],
[ 0.3611, -1.9529]])
In [45]: first, second, third = np.split(arr, [1, 3])
In [46]: first
Out[46]: array([[-1.4238, 1.2637]])
In [47]: second
Out[47]:
array([[-0.8707, -0.2592],
[-0.0753, -0.7409]])
In [48]: third
Out[48]:
array([[-1.3678, 0.6489],
[ 0.3611, -1.9529]])
传递给 np.split
的值 [1,3]
表示将数组拆分为多个部分的索引。
有关所有相关连接和拆分功能的列表,请参见表A.1,其中一些功能是为了方便通用的连接而提供的。
函数 | 英文注释 | 中文翻译 |
---|---|---|
concatenate | Join a sequence of arrays along an existing axis. | 沿现有轴连接一系列数组 |
stack | Join a sequence of arrays along a new axis. | 沿新轴连接一系列数组 |
vstack, row_stack | Stack arrays in sequence vertically (row wise). | 按顺序垂直堆叠数组(行方向) |
hstack | Stack arrays in sequence horizontally (column wise). | 按顺序水平堆叠数组(列方向) |
dstack | Stack arrays in sequence depth wise (along third axis). | 按顺序纵深堆叠数组(第三轴) |
column_stack | Stack 1-D arrays as columns into a 2-D array.1 | 将一维数组作为列堆叠到二维数组中 |
vsplit | Split an array into multiple sub-arrays vertically (row-wise). | 垂直(行方向)将数组拆分为多个子数组 |
hsplit | Split an array into multiple sub-arrays horizontally (column-wise). | 水平(列方向)将数组拆分为多个子数组 |
dsplit | Split array into multiple sub-arrays along the 3rd axis (depth). | 沿第三轴(深度)将数组拆分为多个子数组 |
堆叠辅助对象:r_ and c_🔗
NumPy命名空间中有两个特殊对象 r_
和 c_
,使堆叠数组更简洁:
In [49]: arr = np.arange(6)
In [50]: arr1 = arr.reshape((3, 2))
In [51]: arr2 = rng.standard_normal((3, 2))
In [52]: np.r_[arr1, arr2]
Out[52]:
array([[ 0. , 1. ],
[ 2. , 3. ],
[ 4. , 5. ],
[ 2.3474, 0.9685],
[-0.7594, 0.9022],
[-0.467 , -0.0607]])
In [53]: np.c_[np.r_[arr1, arr2], arr]
Out[53]:
array([[ 0. , 1. , 0. ],
[ 2. , 3. , 1. ],
[ 4. , 5. , 2. ],
[ 2.3474, 0.9685, 3. ],
[-0.7594, 0.9022, 4. ],
[-0.467 , -0.0607, 5. ]])
这些还可以将切片转换为数组:
In [54]: np.c_[1:6, -10:-5]
Out[54]:
array([[ 1, -10],
[ 2, -9],
[ 3, -8],
[ 4, -7],
[ 5, -6]])
有关如何处理c_
和r_
的更多信息,请参阅文档。
元素的重复操作:tile和repeat
重复或平铺数组以生成更大数组的两个有用的工具是 repeat
和 tile
函数。repeat
将数组中的每个元素复制几次,生成更大的数组:
In [55]: arr = np.arange(3)
In [56]: arr
Out[56]: array([0, 1, 2])
In [57]: arr.repeat(3)
Out[57]: array([0, 0, 0, 1, 1, 1, 2, 2, 2])
笔记:
跟其他流行的数组编程语言(如MATLAB)不同,NumPy中很少需要对数组进行重复(replicate)。这主要是因为广播(broadcasting,我们将在下一节中讲解该技术)能更好地满足该需求。
默认情况下,如果传递整数,则每个元素将重复该次数。如果传递整数数组,则每个元素可以重复不同的次数:
In [58]: arr.repeat([2, 3, 4])
Out[58]: array([0, 0, 1, 1, 1, 2, 2, 2, 2])
多维数组可以使其元素沿特定轴重复:
In [59]: arr = rng.standard_normal((2, 2))
In [60]: arr
Out[60]:
array([[ 0.7888, -1.2567],
[ 0.5759, 1.399 ]])
In [61]: arr.repeat(2, axis=0)
Out[61]:
array([[ 0.7888, -1.2567],
[ 0.7888, -1.2567],
[ 0.5759, 1.399 ],
[ 0.5759, 1.399 ]])
注意,如果没有设置轴向,则数组会被扁平化,这可能不是您想要的。类似地,当重复多维数组以将给定切片重复不同的次数时,可以传递整数数组:
In [62]: arr.repeat([2, 3], axis=0)
Out[62]:
array([[ 0.7888, -1.2567],
[ 0.7888, -1.2567],
[ 0.5759, 1.399 ],
[ 0.5759, 1.399 ],
[ 0.5759, 1.399 ]])
In [63]: arr.repeat([2, 3], axis=1)
Out[63]:
array([[ 0.7888, 0.7888, -1.2567, -1.2567, -1.2567],
[ 0.5759, 0.5759, 1.399 , 1.399 , 1.399 ]])
另一方面,tile
是沿轴堆叠阵列副本的快捷方式。您可以想象将其视为类似于“铺设瓷砖”:
In [64]: arr
Out[64]:
array([[ 0.7888, -1.2567],
[ 0.5759, 1.399 ]])
In [65]: np.tile(arr, 2)
Out[65]:
array([[ 0.7888, -1.2567, 0.7888, -1.2567],
[ 0.5759, 1.399 , 0.5759, 1.399 ]])
第二个参数是瓷砖的数量。对于标量,瓷砖是水平铺设的,而不是垂直铺设。它可以是一个表示“铺设”布局的元组:
In [66]: arr
Out[66]:
array([[ 0.7888, -1.2567],
[ 0.5759, 1.399 ]])
In [67]: np.tile(arr, (2, 1))
Out[67]:
array([[ 0.7888, -1.2567],
[ 0.5759, 1.399 ],
[ 0.7888, -1.2567],
[ 0.5759, 1.399 ]])
In [68]: np.tile(arr, (3, 2))
Out[68]:
array([[ 0.7888, -1.2567, 0.7888, -1.2567],
[ 0.5759, 1.399 , 0.5759, 1.399 ],
[ 0.7888, -1.2567, 0.7888, -1.2567],
[ 0.5759, 1.399 , 0.5759, 1.399 ],
[ 0.7888, -1.2567, 0.7888, -1.2567],
[ 0.5759, 1.399 , 0.5759, 1.399 ]])
花式索引的等价函数:take和put
我们讲过NumPy 基础知识:数组和矢量化计算,获取和设置数组子集的一种方法是使用整数数组进行花式索引:
In [69]: arr = np.arange(10) * 100
In [70]: inds = [7, 1, 2, 6]
In [71]: arr[inds]
Out[71]: array([700, 100, 200, 600])
ndarray 还有可供替代的方法,仅在单个轴上做出选择的特殊情况下是很有用的:
In [72]: arr.take(inds)
Out[72]: array([700, 100, 200, 600])
In [73]: arr.put(inds, 42)
In [74]: arr
Out[74]: array([ 0, 42, 42, 300, 400, 500, 42, 42, 800, 900])
In [75]: arr.put(inds, [40, 41, 42, 43])
In [76]: arr
Out[76]: array([ 0, 41, 42, 300, 400, 500, 43, 40, 800, 900])
要在其它轴上使用take
,只需传入axis
关键字即可:
In [77]: inds = [2, 0, 2, 1]
In [78]: arr = rng.standard_normal((2, 4))
In [79]: arr
Out[79]:
array([[ 1.3223, -0.2997, 0.9029, -1.6216],
[-0.1582, 0.4495, -1.3436, -0.0817]])
In [80]: arr.take(inds, axis=1)
Out[80]:
array([[ 0.9029, 1.3223, 0.9029, -0.2997],
[-1.3436, -0.1582, -1.3436, 0.4495]])
put
不接受 axis
参数,它只会在数组的扁平化版本(一维,C顺序)上进行索引。因此,在需要用其他轴向的索引设置元素时,最好还是使用花式索引。
广播(Broadcasting)
广播(broadcasting)指的是不同形状的数组之间的算术运算的执行方式。它是一种非常强大的功能,但也容易令人误解,即使是经验丰富的老手也是如此。将标量值跟数组合并时就会发生最简单的广播:
In [81]: arr = np.arange(5)
In [82]: arr
Out[82]: array([0, 1, 2, 3, 4])
In [83]: arr * 4
Out[83]: array([ 0, 4, 8, 12, 16])
这里我们说:在这个乘法运算中,标量值4被广播到了其他所有的元素上。
看一个例子,我们可以通过减去列平均值的方式对数组的每一列进行距平化处理。这个问题解决起来非常简单:
In [84]: arr = rng.standard_normal((4, 3))
In [85]: arr.mean(0)
Out[85]: array([0.1206, 0.243 , 0.1444])
In [86]: demeaned = arr - arr.mean(0)
In [87]: demeaned
Out[87]:
array([[ 1.6042, 2.3751, 0.633 ],
[ 0.7081, -1.202 , -1.3538],
[-1.5329, 0.2985, 0.6076],
[-0.7793, -1.4717, 0.1132]])
In [88]: demeaned.mean(0)
Out[88]: array([ 0., -0., 0.])
用广播的方式对行进行距平化处理会稍微麻烦一些。幸运的是,只要遵循一定的规则,低维度的值是可以被广播到数组的任意维度的(比如对二维数组各列减去行平均值)
这就引出了广播规则。
如果两个数组的后缘维度(trailing dimension,即从末尾开始算起的维度),轴长度相符,或者如果任一长度为1,则两个数组兼容于广播。然后广播会在缺失或长度为1的维度上执行。
由于arr.mean(0)
的长度为3,所以它可以在0轴向上进行广播:因为arr
的后缘维度是3,所以它们是兼容的。根据该原则,要在1轴向上做减法(即各行减去行平均值),较小的那个数组的形状必须是(4,1)
:
In [89]: arr
Out[89]:
array([[ 1.7247, 2.6182, 0.7774],
[ 0.8286, -0.959 , -1.2094],
[-1.4123, 0.5415, 0.7519],
[-0.6588, -1.2287, 0.2576]])
In [90]: row_means = arr.mean(1)
In [91]: row_means.shape
Out[91]: (4,)
In [92]: row_means.reshape((4, 1))
Out[92]:
array([[ 1.7068],
[-0.4466],
[-0.0396],
[-0.5433]])
In [93]: demeaned = arr - row_means.reshape((4, 1))
In [94]: demeaned.mean(1)
Out[94]: array([-0., 0., 0., 0.])
以下为用于此操作的图解。
另外一种情况,一个三维数组上沿0轴向加上一个二维数组。
在其他轴上广播
高维度数组的广播似乎更难以理解,而实际上它也是遵循广播原则的。如果不然,你就会得到下面这样一个错误:
In [95]: arr - arr.mean(1)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-95-8b8ada26fac0> in <module>
----> 1 arr - arr.mean(1)
ValueError: operands could not be broadcast together with shapes (4,3) (4,)
人们经常需要通过算术运算过程将较低维度的数组在除0轴以外的其他轴向上广播。根据广播的原则,较小数组的“广播维”必须为1。在上面那个行距平化的例子中,这就意味着要将行平均值的形状变成(4,1)
而不是(4,)
:
In [96]: arr - arr.mean(1).reshape((4, 1))
Out[96]:
array([[ 0.018 , 0.9114, -0.9294],
[ 1.2752, -0.5124, -0.7628],
[-1.3727, 0.5811, 0.7915],
[-0.1155, -0.6854, 0.8009]])
对于三维的情况,在三维中的任何一维上广播其实也就是将数据重塑为兼容的形状而已。图A-7说明了要在三维数组各维度上广播的形状需求。
于是就有了一个非常普遍的问题(尤其是在通用算法中),即专门为了广播而添加一个长度为1的新轴。虽然reshape
是一个办法,但插入轴需要构造一个表示新形状的元组。这是一个很郁闷的过程。因此,NumPy数组提供了一种通过索引机制插入轴的特殊语法。下面这段代码通过特殊的np.newaxis
属性以及“全”切片来插入新轴:
In [97]: arr = np.zeros((4, 4))
In [98]: arr_3d = arr[:, np.newaxis, :]
In [99]: arr_3d.shape
Out[99]: (4, 1, 4)
In [100]: arr_1d = rng.standard_normal(3)
In [101]: arr_1d[:, np.newaxis]
Out[101]:
array([[ 0.3129],
[-0.1308],
[ 1.27 ]])
In [102]: arr_1d[np.newaxis, :]
Out[102]: array([[ 0.3129, -0.1308, 1.27 ]])
因此,如果我们有一个三维数组,并希望对轴2进行距平化,那么只需要编写下面这样的代码就可以了:
In [103]: arr = rng.standard_normal((3, 4, 5))
In [104]: depth_means = arr.mean(2)
In [105]: depth_means
Out[105]:
array([[ 0.0431, 0.2747, -0.1885, -0.2014],
[-0.5732, -0.5467, 0.1183, -0.6301],
[ 0.0972, 0.5954, 0.0331, -0.6002]])
In [106]: depth_means.shape
Out[106]: (3, 4)
In [107]: demeaned = arr - depth_means[:, :, np.newaxis]
In [108]: demeaned.mean(2)
Out[108]:
array([[ 0., -0., 0., -0.],
[ 0., -0., -0., -0.],
[ 0., 0., 0., 0.]])
可能会想,在对指定轴进行距平化时,有没有一种既通用又不牺牲性能的方法呢?实际上是有的,但需要一些索引方面的技巧:
def demean_axis(arr, axis=0):
means = arr.mean(axis)
# This generalizes things like [:, :, np.newaxis] to N dimensions
indexer = [slice(None)] * arr.ndim
indexer[axis] = np.newaxis
return arr - means[indexer]
通过广播设置数组的值
算术运算所遵循的广播原则同样也适用于通过索引机制设置数组值的操作。对于最简单的情况,我们可以这样做:
In [109]: arr = np.zeros((4, 3))
In [110]: arr[:] = 5
In [111]: arr
Out[111]:
array([[5., 5., 5.],
[5., 5., 5.],
[5., 5., 5.],
[5., 5., 5.]])
但是,假设我们想要用一个一维数组来设置目标数组的各列,只要保证形状兼容就可以了:
In [112]: col = np.array([1.28, -0.42, 0.44, 1.6])
In [113]: arr[:] = col[:, np.newaxis]
In [114]: arr
Out[114]:
array([[ 1.28, 1.28, 1.28],
[-0.42, -0.42, -0.42],
[ 0.44, 0.44, 0.44],
[ 1.6 , 1.6 , 1.6 ]])
In [115]: arr[:2] = [[-1.37], [0.509]]
In [116]: arr
Out[116]:
array([[-1.37 , -1.37 , -1.37 ],
[ 0.509, 0.509, 0.509],
[ 0.44 , 0.44 , 0.44 ],
[ 1.6 , 1.6 , 1.6 ]])
ufunc高级应用
虽然许多NumPy用户只会用到通用函数所提供的快速的元素级运算,但通用函数实际上还有一些高级用法能使我们丢开循环而编写出更为简洁的代码。
ufunc实例方法
NumPy的各个二元ufunc都有一些用于执行特定矢量化运算的特殊方法。表A-2汇总了这些方法,下面我将通过几个具体的例子对它们进行说明。
reduce
接受一个数组参数,并通过一系列的二元运算对其值进行聚合(可指明轴向)。例如,我们可以用np.add.reduce
对数组中各个元素进行求和:
In [117]: arr = np.arange(10)
In [118]: np.add.reduce(arr)
Out[118]: 45
In [119]: arr.sum()
Out[119]: 45
起始值取决于ufunc(对于add
的情况,就是0)。如果设置了轴号,约简运算就会沿该轴向执行。这就使你能用一种比较简洁的方式得到某些问题的答案。在下面这个例子中,我们用np.logical_and检查数组各行中的值是否是有序的:
In [120]: my_rng = np.random.default_rng(12346) # for reproducibility
In [121]: arr = my_rng.standard_normal((5, 5))
In [122]: arr
Out[122]:
array([[-0.9039, 0.1571, 0.8976, -0.7622, -0.1763],
[ 0.053 , -1.6284, -0.1775, 1.9636, 1.7813],
[-0.8797, -1.6985, -1.8189, 0.119 , -0.4441],
[ 0.7691, -0.0343, 0.3925, 0.7589, -0.0705],
[ 1.0498, 1.0297, -0.4201, 0.7863, 0.9612]])
In [123]: arr[::2].sort(1) # sort a few rows
In [124]: arr[:, :-1] < arr[:, 1:]
Out[124]:
array([[ True, True, True, True],
[False, True, True, False],
[ True, True, True, True],
[False, True, True, False],
[ True, True, True, True]])
In [125]: np.logical_and.reduce(arr[:, :-1] < arr[:, 1:], axis=1)
Out[125]: array([ True, False, True, False, True])
注意,logical_and.reduce
跟all
方法是等价的。
ccumulate
跟reduce
的关系就像cumsum
跟sum
的关系那样。它产生一个跟原数组大小相同的中间“累计”值数组:
In [126]: arr = np.arange(15).reshape((3, 5))
In [127]: np.add.accumulate(arr, axis=1)
Out[127]:
array([[ 0, 1, 3, 6, 10],
[ 5, 11, 18, 26, 35],
[10, 21, 33, 46, 60]])
outer
用于计算两个数组的叉积:
In [128]: arr = np.arange(3).repeat([1, 2, 2])
In [129]: arr
Out[129]: array([0, 1, 1, 2, 2])
In [130]: np.multiply.outer(arr, np.arange(5))
Out[130]:
array([[0, 0, 0, 0, 0],
[0, 1, 2, 3, 4],
[0, 1, 2, 3, 4],
[0, 2, 4, 6, 8],
[0, 2, 4, 6, 8]])
outer
输出结果的维度是两个输入数据的维度之和:
In [131]: x, y = rng.standard_normal((3, 4)), rng.standard_normal(5)
In [132]: result = np.subtract.outer(x, y)
In [133]: result.shape
Out[133]: (3, 4, 5)
最后一个方法reduceat
用于计算“局部约简”,其实就是一个对数据各切片进行聚合的groupby
运算。它接受一组用于指示如何对值进行拆分和聚合的“面元边界”:
In [134]: arr = np.arange(10)
In [135]: np.add.reduceat(arr, [0, 5, 8])
Out[135]: array([10, 18, 17])
最终结果是在arr[0:5]
、arr[5:8]
以及arr[8:]
上执行的约简。跟其他方法一样,这里也可以传入一个axis
参数:
In [136]: arr = np.multiply.outer(np.arange(4), np.arange(5))
In [137]: arr
Out[137]:
array([[ 0, 0, 0, 0, 0],
[ 0, 1, 2, 3, 4],
[ 0, 2, 4, 6, 8],
[ 0, 3, 6, 9, 12]])
In [138]: np.add.reduceat(arr, [0, 2, 4], axis=1)
Out[138]:
array([[ 0, 0, 0],
[ 1, 5, 4],
[ 2, 10, 8],
[ 3, 15, 12]])
方法 | 描述 |
---|---|
accumulate(x) | 聚合值,保留所有局部聚合结果 |
at(x, indices, b=None) | 在指定索引处的 x 上执行操作。参数 b 是 ufuncs 的第二个输入,需要两个数组输入 |
reduce(x) | 通过连续执行原始的运算方式来聚合值 |
reduceat(x, bins) | 局部约简(也就是groupby)减少相连的数据片以生成聚合数组 |
outer(x, y) | 对x 和y 中的所有元素应用原始运算;得到的数组形状为x.shape+y.shape |
编写新的ufunc
有多种方法可以让你编写自己的NumPy ufuncs,最常见的是使用NumPy C API。
numpy.frompyfunc
接受一个Python函数以及两个分别表示输入输出参数数量的参数。例如,下面是一个能够实现元素级加法的简单函数:
In [139]: def add_elements(x, y):
.....: return x + y
In [140]: add_them = np.frompyfunc(add_elements, 2, 1)
In [141]: add_them(np.arange(8), np.arange(8))
Out[141]: array([0, 2, 4, 6, 8, 10, 12, 14], dtype=object)
用frompyfunc
创建的函数总是返回Python对象数组,这一点很不方便。幸运的是,还有另一个办法,即numpy.vectorize
。虽然没有frompyfunc
那么强大,但可以让你指定输出类型:
In [142]: add_them = np.vectorize(add_elements, otypes=[np.float64])
In [143]: add_them(np.arange(8), np.arange(8))
Out[143]: array([ 0., 2., 4., 6., 8., 10., 12., 14.])
虽然这两个函数提供了一种创建ufunc型函数的手段,但它们非常慢,因为它们在计算每个元素时都要执行一次Python函数调用,这就会比NumPy自带的基于C的ufunc慢很多:
In [144]: arr = rng.standard_normal(10000)
In [145]: %timeit add_them(arr, arr)
1.18 ms +- 14.8 us per loop (mean +- std. dev. of 7 runs, 1000 loops each)
In [146]: %timeit np.add(arr, arr)
2.8 us +- 64.1 ns per loop (mean +- std. dev. of 7 runs, 100000 loops each)
后面,会介绍使用 Numba,创建快速Python ufuncs
结构化和记录式数组
到目前为止我们所讨论的ndarray都是一种同质数据容器,也就是说,在它所表示的内存块中,各元素占用的字节数相同(具体根据dtype而定)。从表面上看,它似乎不能用于表示异质或表格型的数据。结构化数组是一种特殊的ndarray,其中的各个元素可以被看做C语言中的结构体(struct,这就是“结构化”的由来)或SQL表中带有多个命名字段的行:
In [147]: dtype = [('x', np.float64), ('y', np.int32)]
In [148]: sarr = np.array([(1.5, 6), (np.pi, -2)], dtype=dtype)
In [149]: sarr
Out[149]: array([(1.5 , 6), (3.1416, -2)], dtype=[('x', '<f8'), ('y', '<i4')])
定义结构化dtype(请参考NumPy的在线文档)的方式有很多。最典型的办法是元组列表,各元组的格式为(field_name,field_data_type
)。这样,数组的元素就成了元组式的对象,该对象中各个元素可以像字典那样进行访问:
In [150]: sarr[0]
Out[150]: (1.5, 6)
In [151]: sarr[0]['y']
Out[151]: 6
字段名保存在dtype.names
属性中。在访问结构化数组的某个字段时,返回的是该数据的视图,所以不会发生数据复制:
In [152]: sarr['x']
Out[152]: array([1.5 , 3.1416])
嵌套数据类型和多维字段
定义结构化数据类型时,你可以设置一个形状(可以是一个整数,也可以是一个元组):
In [153]: dtype = [('x', np.int64, 3), ('y', np.int32)]
In [154]: arr = np.zeros(4, dtype=dtype)
In [155]: arr
Out[155]:
array([([0, 0, 0], 0), ([0, 0, 0], 0), ([0, 0, 0], 0), ([0, 0, 0], 0)],
dtype=[('x', '<i8', (3,)), ('y', '<i4')])
在这种情况下,各个记录的x
字段所表示的是一个长度为3的数组:
In [156]: arr[0]['x']
Out[156]: array([0, 0, 0])
这样,访问arr[‘x’]
即可得到一个二维数组,而不是前面那个例子中的一维数组:
In [157]: arr['x']
Out[157]:
array([[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0]])
这就使你能用单个数组的内存块存放复杂的嵌套结构。你还可以嵌套dtype,作出更复杂的结构。下面是一个简单的例子:
In [158]: dtype = [('x', [('a', 'f8'), ('b', 'f4')]), ('y', np.int32)]
In [159]: data = np.array([((1, 2), 5), ((3, 4), 6)], dtype=dtype)
In [160]: data['x']
Out[160]: array([(1., 2.), (3., 4.)], dtype=[('a', '<f8'), ('b', '<f4')])
In [161]: data['y']
Out[161]: array([5, 6], dtype=int32)
In [162]: data['x']['a']
Out[162]: array([1., 3.])
pandas的DataFrame并不直接支持该功能,但它的分层索引机制跟这个差不多。
为什么要用结构化数组
跟pandas的DataFrame相比,NumPy的结构化数组是一种相对较低级的工具。它可以将单个内存块解释为带有任意复杂嵌套列的表格型结构。由于数组中的每个元素在内存中都被表示为固定的字节数,所以结构化数组能够提供非常快速高效的磁盘数据读写(包括内存映像)、网络传输等功能。
结构化数组的另一个常见用法是,将数据文件写成定长记录字节流,这是C和C++代码中常见的数据序列化手段(业界许多历史系统中都能找得到)。只要知道文件的格式(记录的大小、元素的顺序、字节数以及数据类型等),就可以用np.fromfile
将数据读入内存。这种用法超出了本书的范围,知道这点就可以了。
更多有关排序的话题
跟Python内置的列表一样,ndarray的sort
实例方法也是就地排序。也就是说,数组内容的重新排列是不会产生新数组的:
In [163]: arr = rng.standard_normal(6)
In [164]: arr.sort()
In [165]: arr
Out[165]: array([-1.1553, -0.9319, -0.5218, -0.4745, -0.1649, 0.03 ])
在对数组进行就地排序时要注意一点,如果目标数组只是一个视图,则原始数组将会被修改:
In [166]: arr = rng.standard_normal((3, 5))
In [167]: arr
Out[167]:
array([[-1.1956, 0.4691, -0.3598, 1.0359, 0.2267],
[-0.7448, -0.5931, -1.055 , -0.0683, 0.458 ],
[-0.07 , 0.1462, -0.9944, 1.1436, 0.5026]])
In [168]: arr[:, 0].sort() # Sort first column values in place
In [169]: arr
Out[169]:
array([[-1.1956, 0.4691, -0.3598, 1.0359, 0.2267],
[-0.7448, -0.5931, -1.055 , -0.0683, 0.458 ],
[-0.07 , 0.1462, -0.9944, 1.1436, 0.5026]])
相反,numpy.sort
会为原数组创建一个已排序副本。另外,它所接受的参数(比如kind)跟ndarray.sort
一样:
In [170]: arr = rng.standard_normal(5)
In [171]: arr
Out[171]: array([ 0.8981, -1.1704, -0.2686, -0.796 , 1.4522])
In [172]: np.sort(arr)
Out[172]: array([-1.1704, -0.796 , -0.2686, 0.8981, 1.4522])
In [173]: arr
Out[173]: array([ 0.8981, -1.1704, -0.2686, -0.796 , 1.4522])
这两个排序方法都可以接受一个axis参数,以便沿指定轴向对各块数据进行单独排序:
In [174]: arr = rng.standard_normal((3, 5))
In [175]: arr
Out[175]:
array([[-0.2535, 2.1183, 0.3634, -0.6245, 1.1279],
[ 1.6164, -0.2287, -0.6201, -0.1143, -1.2067],
[-1.0872, -2.1518, -0.6287, -1.3199, 0.083 ]])
In [176]: arr.sort(axis=1)
In [177]: arr
Out[177]:
array([[-0.6245, -0.2535, 0.3634, 1.1279, 2.1183],
[-1.2067, -0.6201, -0.2287, -0.1143, 1.6164],
[-2.1518, -1.3199, -1.0872, -0.6287, 0.083 ]])
你可能注意到了,这两个排序方法都不可以被设置为降序。其实这也无所谓,因为数组切片会产生视图(也就是说,不会产生副本,也不需要任何其他的计算工作)。许多Python用户都很熟悉一个有关列表的小技巧:values[::-1]
可以返回一个反序的列表。对ndarray也是如此:
In [178]: arr[:, ::-1]
Out[178]:
array([[ 2.1183, 1.1279, 0.3634, -0.2535, -0.6245],
[ 1.6164, -0.1143, -0.2287, -0.6201, -1.2067],
[ 0.083 , -0.6287, -1.0872, -1.3199, -2.1518]])
间接排序:argsort和lexsort
在数据分析工作中,常常需要根据一个或多个键对数据集进行排序。例如,一个有关学生信息的数据表可能需要以姓和名进行排序(先姓后名)。这就是间接排序的一个例子,如果你阅读过有关pandas的章节,那就已经见过不少高级例子了。给定一个或多个键,你就可以得到一个由整数组成的索引数组(我亲切地称之为索引器),其中的索引值说明了数据在新顺序下的位置。argsort
和numpy.lexsort
就是实现该功能的两个主要方法。下面是一个简单的例子:
In [179]: values = np.array([5, 0, 1, 3, 2])
In [180]: indexer = values.argsort()
In [181]: indexer
Out[181]: array([1, 2, 4, 3, 0])
In [182]: values[indexer]
Out[182]: array([0, 1, 2, 3, 5])
一个更复杂的例子,下面这段代码根据数组的第一行对其进行排序:
In [183]: arr = rng.standard_normal((3, 5))
In [184]: arr[0] = values
In [185]: arr
Out[185]:
array([[ 5. , 0. , 1. , 3. , 2. ],
[-0.7503, -2.1268, -1.391 , -0.4922, 0.4505],
[ 0.8926, -1.0479, 0.9553, 0.2936, 0.5379]])
In [186]: arr[:, arr[0].argsort()]
Out[186]:
array([[ 0. , 1. , 2. , 3. , 5. ],
[-2.1268, -1.391 , 0.4505, -0.4922, -0.7503],
[-1.0479, 0.9553, 0.5379, 0.2936, 0.8926]])
lexsort
跟argsort
差不多,只不过它可以一次性对多个键数组执行间接排序(字典序)。假设我们想对一些以姓和名标识的数据进行排序:
In [187]: first_name = np.array(['Bob', 'Jane', 'Steve', 'Bill', 'Barbara'])
In [188]: last_name = np.array(['Jones', 'Arnold', 'Arnold', 'Jones', 'Walters'])
In [189]: sorter = np.lexsort((first_name, last_name))
In [190]: sorter
Out[190]: array([1, 2, 3, 0, 4])
In [191]: list(zip(last_name[sorter], first_name[sorter]))
Out[191]:
[('Arnold', 'Jane'),
('Arnold', 'Steve'),
('Jones', 'Bill'),
('Jones', 'Bob'),
('Walters', 'Barbara')]
刚开始使用lexsort
的时候可能会比较容易头晕,这是因为键的应用顺序是从最后一个传入的算起的。不难看出,last_name
是先于first_name
被应用的。
替代排序算法
稳定的排序算法保留等价元素的相对位置。对于相对位置具有实际意义的那些间接排序而言,这一点非常重要:
In [192]: values = np.array(['2:first', '2:second', '1:first', '1:second',
.....: '1:third'])
In [193]: key = np.array([2, 2, 1, 1, 1])
In [194]: indexer = key.argsort(kind='mergesort')
In [195]: indexer
Out[195]: array([2, 3, 4, 0, 1])
In [196]: values.take(indexer)
Out[196]:
array(['1:first', '1:second', '1:third', '2:first', '2:second'],
dtype='<U8')
mergesort
(合并排序)是唯一的稳定排序,它保证有O(n log n)的性能(空间复杂度),但是其平均性能比默认的quicksort
(快速排序)要差。
部分排序数组
排序的目的之一可能是确定数组中最大或最小的元素。NumPy有两个优化方法,numpy.partition
和np.argpartition
,可以在第k
个最小元素划分的数组:
In [197]: rng = np.random.default_rng(12345)
In [198]: arr = rng.standard_normal(20)
In [199]: arr
Out[199]:
array([-1.4238, 1.2637, -0.8707, -0.2592, -0.0753, -0.7409, -1.3678,
0.6489, 0.3611, -1.9529, 2.3474, 0.9685, -0.7594, 0.9022,
-0.467 , -0.0607, 0.7888, -1.2567, 0.5759, 1.399 ])
In [200]: np.partition(arr, 3)
Out[200]:
array([-1.9529, -1.4238, -1.3678, -1.2567, -0.8707, -0.7594, -0.7409,
-0.0607, 0.3611, -0.0753, -0.2592, -0.467 , 0.5759, 0.9022,
0.9685, 0.6489, 0.7888, 1.2637, 1.399 , 2.3474])
当你调用partition(arr, 3)
,结果中的头三个元素是最小的三个,没有特定的顺序。numpy.argpartition
与numpy.argsort
相似,会返回索引,重排数据为等价的顺序:
In [201]: indices = np.argpartition(arr, 3)
In [202]: indices
Out[202]:
array([ 9, 0, 6, 17, 2, 12, 5, 15, 8, 4, 3, 14, 18, 13, 11, 7, 16,
1, 19, 10])
In [203]: arr.take(indices)
Out[203]:
array([-1.9529, -1.4238, -1.3678, -1.2567, -0.8707, -0.7594, -0.7409,
-0.0607, 0.3611, -0.0753, -0.2592, -0.467 , 0.5759, 0.9022,
0.9685, 0.6489, 0.7888, 1.2637, 1.399 , 2.3474])
numpy.searchsorted:在有序数组中查找元素
searchsorted
是一个在有序数组上执行二分查找的数组方法,只要将值插入到它返回的那个位置就能维持数组的有序性:
In [204]: arr = np.array([0, 1, 7, 12, 15])
In [205]: arr.searchsorted(9)
Out[205]: 3
你可以传入一组值就能得到一组索引:
In [206]: arr.searchsorted([0, 8, 11, 16])
Out[206]: array([0, 3, 3, 5])
从上面的结果中可以看出,对于元素0
,searchsorted
会返回0
。这是因为其默认行为是返回相等值组的左侧索引:
In [207]: arr = np.array([0, 0, 0, 1, 1, 1, 1])
In [208]: arr.searchsorted([0, 1])
Out[208]: array([0, 3])
In [209]: arr.searchsorted([0, 1], side='right')
Out[209]: array([3, 7])
再来看searchsorted
的另一个用法,假设我们有一个数据数组(其中的值在0到10000之间),还有一个表示“面元边界”的数组,我们希望用它将数据数组拆分开:
In [210]: data = np.floor(rng.uniform(0, 10000, size=50))
In [211]: bins = np.array([0, 100, 1000, 5000, 10000])
In [212]: data
Out[212]:
array([ 815., 1598., 3401., 4651., 2664., 8157., 1932., 1294., 916.,
5985., 8547., 6016., 9319., 7247., 8605., 9293., 5461., 9376.,
4949., 2737., 4517., 6650., 3308., 9034., 2570., 3398., 2588.,
3554., 50., 6286., 2823., 680., 6168., 1763., 3043., 4408.,
1502., 2179., 4743., 4763., 2552., 2975., 2790., 2605., 4827.,
2119., 4956., 2462., 8384., 1801.])
然后,为了得到各数据点所属区间的编号(其中1表示面元[0,100)
),我们可以直接使用searchsorted
:
In [213]: labels = bins.searchsorted(data)
In [214]: labels
Out[214]:
array([2, 3, 3, 3, 3, 4, 3, 3, 2, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 4,
3, 4, 3, 3, 3, 3, 1, 4, 3, 2, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 4, 3])
通过pandas的groupby
使用该结果即可非常轻松地对原数据集进行拆分:
In [215]: pd.Series(data).groupby(labels).mean()
Out[215]:
1 50.000000
2 803.666667
3 3079.741935
4 7635.200000
dtype: float64
用Numba编写快速NumPy函数
Numba是一个开源项目,它可以利用CPUs、GPUs或其它硬件为类似NumPy的数据创建快速函数。它使用了LLVM项目(http://llvm.org/),将Python代码转换为机器代码。
为了介绍Numba,来考虑一个纯粹的Python函数,它使用for
循环计算表达式(x - y).mean()
:
import numpy as np
def mean_distance(x, y):
nx = len(x)
result = 0.0
count = 0
for i in range(nx):
result += x[i] - y[i]
count += 1
return result / count
这个函数很慢:
In [209]: x = rng.standard_normal(10_000_000)
In [210]: y = rng.standard_normal(10_000_000)
In [211]: %timeit mean_distance(x, y)
1 loop, best of 3: 2 s per loop
In [212]: %timeit (x - y).mean()
100 loops, best of 3: 14.7 ms per loop
NumPy的版本要比它快过100倍。我们可以转换这个函数为编译的Numba函数,使用numba.jit
函数:
In [213]: import numba as nb
In [214]: numba_mean_distance = nb.jit(mean_distance)
也可以写成装饰器:
@nb.jit
def numba_mean_distance(x, y):
nx = len(x)
result = 0.0
count = 0
for i in range(nx):
result += x[i] - y[i]
count += 1
return result / count
它要比矢量化的NumPy快:
In [215]: %timeit numba_mean_distance(x, y)
100 loops, best of 3: 10.3 ms per loop
Numba不能编译Python代码,但它支持纯Python写的一个部分,可以编写数值算法。
Numba是一个深厚的库,支持多种硬件、编译模式和用户插件。它还可以编译NumPy Python API的一部分,而不用for
循环。Numba也可以识别可以便以为机器编码的结构体,但是若调用CPython API,它就不知道如何编译。Numba的jit
函数有一个选项,nopython=True
,它限制了可以被转换为Python代码的代码,这些代码可以编译为LLVM,但没有任何Python C API调用。jit(nopython=True)
有一个简短的别名numba.njit
。
前面的例子,我们还可以这样写:
from numba import float64, njit
@njit(float64(float64[:], float64[:]))
def mean_distance(x, y):
return (x - y).mean()
建议你学习 Numba 的线上文档
用Numba创建自定义numpy.ufunc对象
numba.vectorize
创建了一个编译的NumPy ufunc,它与内置的 ufunc 很像。考虑一个numpy.add
的Python例子:
from numba import vectorize
@vectorize
def nb_add(x, y):
return x + y
现在有:
In [13]: x = np.arange(10)
In [14]: nb_add(x, x)
Out[14]: array([ 0., 2., 4., 6., 8., 10., 12., 14., 16., 18.])
In [15]: nb_add.accumulate(x, 0)
Out[15]: array([ 0., 1., 3., 6., 10., 15., 21., 28., 36., 45.])
高级数组输入输出
np.save
和np.load
可用于读写磁盘上以二进制格式存储的数组。其实还有一些工具可用于更为复杂的场景。尤其是内存映像(memory map),它使你能处理在内存中放不下的数据集。
内存映像文件
内存映像文件是一种将磁盘上的非常大的二进制数据文件当做内存中的数组进行处理的方式。NumPy实现了一个类似于ndarray的memmap
对象,它允许将大文件分成小段进行读写,而不是一次性将整个数组读入内存。因此,基本上只要是能用于ndarray的算法就也能用于memmap
。
要创建一个内存映像,可以使用函数np.memmap
并传入一个文件路径、数据类型、形状以及文件模式:
In [217]: mmap = np.memmap('mymmap', dtype='float64', mode='w+',
.....: shape=(10000, 10000))
In [218]: mmap
Out[218]:
memmap([[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
...,
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.]])
对memmap
切片将会返回磁盘上的数据的视图:
In [219]: section = mmap[:5]
如果将数据赋值给这些视图:数据会先被缓存在内存中(就像是Python的文件对象),调用flush即可将其写入磁盘:
In [220]: section[:] = rng.standard_normal((5, 10000))
In [221]: mmap.flush()
In [222]: mmap
Out[222]:
memmap([[-0.9074, -1.0954, 0.0071, ..., 0.2753, -1.1641, 0.8521],
[-0.0103, -0.0646, -1.0615, ..., -1.1003, 0.2505, 0.5832],
[ 0.4583, 1.2992, 1.7137, ..., 0.8691, -0.7889, -0.2431],
...,
[ 0. , 0. , 0. , ..., 0. , 0. , 0. ],
[ 0. , 0. , 0. , ..., 0. , 0. , 0. ],
[ 0. , 0. , 0. , ..., 0. , 0. , 0. ]])
In [223]: del mmap
只要某个内存映像超出了作用域,它就会被垃圾回收器回收,之前对其所做的任何修改都会被写入磁盘。当打开一个已经存在的内存映像时,仍然需要指明数据类型和形状,因为磁盘上的那个文件只是一块二进制数据而已,没有任何元数据:
In [224]: mmap = np.memmap('mymmap', dtype='float64', shape=(10000, 10000))
In [225]: mmap
Out[225]:
memmap([[-0.9074, -1.0954, 0.0071, ..., 0.2753, -1.1641, 0.8521],
[-0.0103, -0.0646, -1.0615, ..., -1.1003, 0.2505, 0.5832],
[ 0.4583, 1.2992, 1.7137, ..., 0.8691, -0.7889, -0.2431],
...,
[ 0. , 0. , 0. , ..., 0. , 0. , 0. ],
[ 0. , 0. , 0. , ..., 0. , 0. , 0. ],
[ 0. , 0. , 0. , ..., 0. , 0. , 0. ]])
内存映像可以使用前面介绍的结构化或嵌套数据类型。
如果在计算机上运行此示例,则可能需要删除我们在上面创建的大文件:
In [226]: %xdel mmap
In [227]: !rm mymmap
HDF5及其他数组存储方式
PyTables和h5py这两个Python项目可以将NumPy的数组数据存储为高效且可压缩的HDF5格式(HDF意思是“层次化数据格式”)。你可以安全地将好几百GB甚至TB的数据存储为HDF5格式,请参考pandas线上文档。
取一系列一维数组,并将它们堆叠为列,以形成单个二维数组。二维数组按原样堆叠,就像使用
hstack
一样。首先将一维数组转换为二维列。 ↩︎