• A.2 高级数组操作
    • 数组重塑
    • C和Fortran顺序
    • 数组的合并和拆分
    • 堆叠辅助类:r和c
    • 元素的重复操作:tile和repeat
    • 花式索引的等价函数:take和put

    A.2 高级数组操作

    除花式索引、切片、布尔条件取子集等操作之外,数组的操作方式还有很多。虽然pandas中的高级函数可以处理数据分析工作中的许多重型任务,但有时你还是需要编写一些在现有库中找不到的数据算法。

    数组重塑

    多数情况下,你可以无需复制任何数据,就将数组从一个形状转换为另一个形状。只需向数组的实例方法reshape传入一个表示新形状的元组即可实现该目的。例如,假设有一个一维数组,我们希望将其重新排列为一个矩阵(结果见图A-3):

    1. In [18]: arr = np.arange(8)
    2. In [19]: arr
    3. Out[19]: array([0, 1, 2, 3, 4, 5, 6, 7])
    4. In [20]: arr.reshape((4, 2))
    5. Out[20]:
    6. array([[0, 1],
    7. [2, 3],
    8. [4, 5],
    9. [6, 7]])

    图A-3 按C顺序(按行)和按Fortran顺序(按列)进行重塑

    多维数组也能被重塑:

    1. In [21]: arr.reshape((4, 2)).reshape((2, 4))
    2. Out[21]:
    3. array([[0, 1, 2, 3],
    4. [4, 5, 6, 7]])

    作为参数的形状的其中一维可以是-1,它表示该维度的大小由数据本身推断而来:

    1. In [22]: arr = np.arange(15)
    2. In [23]: arr.reshape((5, -1))
    3. Out[23]:
    4. array([[ 0, 1, 2],
    5. [ 3, 4, 5],
    6. [ 6, 7, 8],
    7. [ 9, 10, 11],
    8. [12, 13, 14]])

    与reshape将一维数组转换为多维数组的运算过程相反的运算通常称为扁平化(flattening)或散开(raveling):

    1. In [27]: arr = np.arange(15).reshape((5, 3))
    2. In [28]: arr
    3. Out[28]:
    4. array([[ 0, 1, 2],
    5. [ 3, 4, 5],
    6. [ 6, 7, 8],
    7. [ 9, 10, 11],
    8. [12, 13, 14]])
    9. In [29]: arr.ravel()
    10. Out[29]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])

    如果结果中的值与原始数组相同,ravel不会产生源数据的副本。flatten方法的行为类似于ravel,只不过它总是返回数据的副本:

    1. In [30]: arr.flatten()
    2. Out[30]: 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的文档)。图A-3对此进行了说明:

    1. In [31]: arr = np.arange(12).reshape((3, 4))
    2. In [32]: arr
    3. Out[32]:
    4. array([[ 0, 1, 2, 3],
    5. [ 4, 5, 6, 7],
    6. [ 8, 9, 10, 11]])
    7. In [33]: arr.ravel()
    8. Out[33]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
    9. In [34]: arr.ravel('F')
    10. Out[34]: array([ 0, 4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11])

    图A-3 按C(行优先)或Fortran(列优先)顺序进行重塑

    二维或更高维数组的重塑过程比较令人费解(见图A-3)。C和Fortran顺序的关键区别就是维度的行进顺序:

    • C/行优先顺序:先经过更高的维度(例如,轴1会先于轴0被处理)。
    • Fortran/列优先顺序:后经过更高的维度(例如,轴0会先于轴1被处理)。

    数组的合并和拆分

    numpy.concatenate可以按指定轴将一个由数组组成的序列(如元组、列表等)连接到一起:

    1. In [35]: arr1 = np.array([[1, 2, 3], [4, 5, 6]])
    2. In [36]: arr2 = np.array([[7, 8, 9], [10, 11, 12]])
    3. In [37]: np.concatenate([arr1, arr2], axis=0)
    4. Out[37]:
    5. array([[ 1, 2, 3],
    6. [ 4, 5, 6],
    7. [ 7, 8, 9],
    8. [10, 11, 12]])
    9. In [38]: np.concatenate([arr1, arr2], axis=1)
    10. Out[38]:
    11. array([[ 1, 2, 3, 7, 8, 9],
    12. [ 4, 5, 6, 10, 11, 12]])

    对于常见的连接操作,NumPy提供了一些比较方便的方法(如vstack和hstack)。因此,上面的运算还可以表达为:

    1. In [39]: np.vstack((arr1, arr2))
    2. Out[39]:
    3. array([[ 1, 2, 3],
    4. [ 4, 5, 6],
    5. [ 7, 8, 9],
    6. [10, 11, 12]])
    7. In [40]: np.hstack((arr1, arr2))
    8. Out[40]:
    9. array([[ 1, 2, 3, 7, 8, 9],
    10. [ 4, 5, 6, 10, 11, 12]])

    与此相反,split用于将一个数组沿指定轴拆分为多个数组:

    1. In [41]: arr = np.random.randn(5, 2)
    2. In [42]: arr
    3. Out[42]:
    4. array([[-0.2047, 0.4789],
    5. [-0.5194, -0.5557],
    6. [ 1.9658, 1.3934],
    7. [ 0.0929, 0.2817],
    8. [ 0.769 , 1.2464]])
    9. In [43]: first, second, third = np.split(arr, [1, 3])
    10. In [44]: first
    11. Out[44]: array([[-0.2047, 0.4789]])
    12. In [45]: second
    13. Out[45]:
    14. array([[-0.5194, -0.5557],
    15. [ 1.9658, 1.3934]])
    16. In [46]: third
    17. Out[46]:
    18. array([[ 0.0929, 0.2817],
    19. [ 0.769 , 1.2464]])

    传入到np.split的值[1,3]指示在哪个索引处分割数组。

    表A-1中列出了所有关于数组连接和拆分的函数,其中有些是专门为了方便常见的连接运算而提供的。

    表A-1 数组连接函数

    堆叠辅助类:r和c

    NumPy命名空间中有两个特殊的对象——r和c,它们可以使数组的堆叠操作更为简洁:

    1. In [47]: arr = np.arange(6)
    2. In [48]: arr1 = arr.reshape((3, 2))
    3. In [49]: arr2 = np.random.randn(3, 2)
    4. In [50]: np.r_[arr1, arr2]
    5. Out[50]:
    6. array([[ 0. , 1. ],
    7. [ 2. , 3. ],
    8. [ 4. , 5. ],
    9. [ 1.0072, -1.2962],
    10. [ 0.275 , 0.2289],
    11. [ 1.3529, 0.8864]])
    12. In [51]: np.c_[np.r_[arr1, arr2], arr]
    13. Out[51]:
    14. array([[ 0. , 1. , 0. ],
    15. [ 2. , 3. , 1. ],
    16. [ 4. , 5. , 2. ],
    17. [ 1.0072, -1.2962, 3. ],
    18. [ 0.275 , 0.2289, 4. ],
    19. [ 1.3529, 0.8864, 5. ]])

    它还可以将切片转换成数组:

    1. In [52]: np.c_[1:6, -10:-5]
    2. Out[52]:
    3. array([[ 1, -10],
    4. [ 2, -9],
    5. [ 3, -8],
    6. [ 4, -7],
    7. [ 5, -6]])

    r和c的具体功能请参考其文档。

    元素的重复操作:tile和repeat

    对数组进行重复以产生更大数组的工具主要是repeat和tile这两个函数。repeat会将数组中的各个元素重复一定次数,从而产生一个更大的数组:

    1. In [53]: arr = np.arange(3)
    2. In [54]: arr
    3. Out[54]: array([0, 1, 2])
    4. In [55]: arr.repeat(3)
    5. Out[55]: array([0, 0, 0, 1, 1, 1, 2, 2, 2])

    笔记:跟其他流行的数组编程语言(如MATLAB)不同,NumPy中很少需要对数组进行重复(replicate)。这主要是因为广播(broadcasting,我们将在下一节中讲解该技术)能更好地满足该需求。

    默认情况下,如果传入的是一个整数,则各元素就都会重复那么多次。如果传入的是一组整数,则各元素就可以重复不同的次数:

    1. In [56]: arr.repeat([2, 3, 4])
    2. Out[56]: array([0, 0, 1, 1, 1, 2, 2, 2, 2])

    对于多维数组,还可以让它们的元素沿指定轴重复:

    1. In [57]: arr = np.random.randn(2, 2)
    2. In [58]: arr
    3. Out[58]:
    4. array([[-2.0016, -0.3718],
    5. [ 1.669 , -0.4386]])
    6. In [59]: arr.repeat(2, axis=0)
    7. Out[59]:
    8. array([[-2.0016, -0.3718],
    9. [-2.0016, -0.3718],
    10. [ 1.669 , -0.4386],
    11. [ 1.669 , -0.4386]])

    注意,如果没有设置轴向,则数组会被扁平化,这可能不会是你想要的结果。同样,在对多维进行重复时,也可以传入一组整数,这样就会使各切片重复不同的次数:

    1. In [60]: arr.repeat([2, 3], axis=0)
    2. Out[60]:
    3. array([[-2.0016, -0.3718],
    4. [-2.0016, -0.3718],
    5. [ 1.669 , -0.4386],
    6. [ 1.669 , -0.4386],
    7. [ 1.669 , -0.4386]])
    8. In [61]: arr.repeat([2, 3], axis=1)
    9. Out[61]:
    10. array([[-2.0016, -2.0016, -0.3718, -0.3718, -0.3718],
    11. [ 1.669 , 1.669 , -0.4386, -0.4386, -0.4386]])

    tile的功能是沿指定轴向堆叠数组的副本。你可以形象地将其想象成“铺瓷砖”:

    1. In [62]: arr
    2. Out[62]:
    3. array([[-2.0016, -0.3718],
    4. [ 1.669 , -0.4386]])
    5. In [63]: np.tile(arr, 2)
    6. Out[63]:
    7. array([[-2.0016, -0.3718, -2.0016, -0.3718],
    8. [ 1.669 , -0.4386, 1.669 , -0.4386]])

    第二个参数是瓷砖的数量。对于标量,瓷砖是水平铺设的,而不是垂直铺设。它可以是一个表示“铺设”布局的元组:

    1. In [64]: arr
    2. Out[64]:
    3. array([[-2.0016, -0.3718],
    4. [ 1.669 , -0.4386]])
    5. In [65]: np.tile(arr, (2, 1))
    6. Out[65]:
    7. array([[-2.0016, -0.3718],
    8. [ 1.669 , -0.4386],
    9. [-2.0016, -0.3718],
    10. [ 1.669 , -0.4386]])
    11. In [66]: np.tile(arr, (3, 2))
    12. Out[66]:
    13. array([[-2.0016, -0.3718, -2.0016, -0.3718],
    14. [ 1.669 , -0.4386, 1.669 , -0.4386],
    15. [-2.0016, -0.3718, -2.0016, -0.3718],
    16. [ 1.669 , -0.4386, 1.669 , -0.4386],
    17. [-2.0016, -0.3718, -2.0016, -0.3718],
    18. [ 1.669 , -0.4386, 1.669 , -0.4386]])

    花式索引的等价函数:take和put

    在第4章中我们讲过,获取和设置数组子集的一个办法是通过整数数组使用花式索引:

    1. In [67]: arr = np.arange(10) * 100
    2. In [68]: inds = [7, 1, 2, 6]
    3. In [69]: arr[inds]
    4. Out[69]: array([700, 100, 200, 600])

    ndarray还有其它方法用于获取单个轴向上的选区:

    1. In [70]: arr.take(inds)
    2. Out[70]: array([700, 100, 200, 600])
    3. In [71]: arr.put(inds, 42)
    4. In [72]: arr
    5. Out[72]: array([ 0, 42, 42, 300, 400, 500, 42, 42,800, 900])
    6. In [73]: arr.put(inds, [40, 41, 42, 43])
    7. In [74]: arr
    8. Out[74]: array([ 0, 41, 42, 300, 400, 500, 43, 40, 800, 900])

    要在其它轴上使用take,只需传入axis关键字即可:

    1. In [75]: inds = [2, 0, 2, 1]
    2. In [76]: arr = np.random.randn(2, 4)
    3. In [77]: arr
    4. Out[77]:
    5. array([[-0.5397, 0.477 , 3.2489, -1.0212],
    6. [-0.5771, 0.1241, 0.3026, 0.5238]])
    7. In [78]: arr.take(inds, axis=1)
    8. Out[78]:
    9. array([[ 3.2489, -0.5397, 3.2489, 0.477 ],
    10. [ 0.3026, -0.5771, 0.3026, 0.1241]])

    put不接受axis参数,它只会在数组的扁平化版本(一维,C顺序)上进行索引。因此,在需要用其他轴向的索引设置元素时,最好还是使用花式索引。