1.3.1. NumPy数组对象

1.3.1.1. 什么是NumPy和NumPy数组?

1.3.1.1.1. NumPy数组

Python对象
  • 高级数值对象:整数、浮点数
  • 容器:列表(插入和附加无成本),字典(快速查找)
NumPy提供:
  • 用于多维数组的Python扩展包
  • 更接近硬件(效率)
  • 设计用于科学计算(方便)
  • 也称为面向数组的计算
>>> import numpy as np
>>> a = np.array([0, 1, 2, 3])
>>> a
array([0, 1, 2, 3])

例如,一个数组包含:

  • 在离散时间步长的实验/模拟值
  • 由测量装置记录的信号,例如声波
  • 图像的像素、灰度或颜色
  • 在不同X-Y-Z位置测量的3-D数据,例如MRI扫描
  • ...

为什么有用:内存高效的容器,提供快速的数值操作。

In [1]: L = range(1000)
In [2]: %timeit [i**2 for i in L]
1000 loops, best of 3: 403 us per loop
In [3]: a = np.arange(1000)
In [4]: %timeit a**2
100000 loops, best of 3: 12.7 us per loop

1.3.1.1.2. NumPy参考文档

  • 在网上:http://docs.scipy.org/

  • 交互式帮助:

    In [5]: np.array?
    
    String Form:<built-in function array>
    Docstring:
    array(object, dtype=None, copy=True, order=None, subok=False, ndmin=0, ...
  • 查找某个东西:

    >>> np.lookfor('create array') 
    
    Search results for 'create array'
    ---------------------------------
    numpy.array
    Create an array.
    numpy.memmap
    Create a memory-map to an array stored in a *binary* file on disk.
    In [6]: np.con*?
    
    np.concatenate
    np.conj
    np.conjugate
    np.convolve

1.3.1.1.3. 导入约定

导入numpy的建议约定是:

>>> import numpy as np

1.3.1.2. 创建数组

1.3.1.2.1. 手动构建数组

  • 1-D

    >>> a = np.array([0, 1, 2, 3])
    
    >>> a
    array([0, 1, 2, 3])
    >>> a.ndim
    1
    >>> a.shape
    (4,)
    >>> len(a)
    4
  • 2-D,3-D,...

    >>> b = np.array([[0, 1, 2], [3, 4, 5]])    # 2 x 3 array
    
    >>> b
    array([[0, 1, 2],
    [3, 4, 5]])
    >>> b.ndim
    2
    >>> b.shape
    (2, 3)
    >>> len(b) # returns the size of the first dimension
    2
    >>> c = np.array([[[1], [2]], [[3], [4]]])
    >>> c
    array([[[1],
    [2]],
    [[3],
    [4]]])
    >>> c.shape
    (2, 2, 1)

练习:简单数组

  • 创建一个简单的二维数组。首先,重做上面的例子。然后创建自己的:第一行上向后计数的奇数,第二行上的偶数。
  • 在这些数组上使用函数len()numpy.shape()它们如何相互关联?以及与数组的ndim属性的关系?

1.3.1.2.2. 创建数组的函数

在实践中,我们很少一个一个地输入元素...

  • 均匀间隔:

    >>> a = np.arange(10) # 0 .. n-1  (!)
    
    >>> a
    array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
    >>> b = np.arange(1, 9, 2) # start, end (exclusive), step
    >>> b
    array([1, 3, 5, 7])
  • 或按点的个数:

    >>> c = np.linspace(0, 1, 6)   # start, end, num-points
    
    >>> c
    array([ 0. , 0.2, 0.4, 0.6, 0.8, 1. ])
    >>> d = np.linspace(0, 1, 5, endpoint=False)
    >>> d
    array([ 0. , 0.2, 0.4, 0.6, 0.8])
  • 常用数组:

    >>> a = np.ones((3, 3))  # reminder: (3, 3) is a tuple
    
    >>> a
    array([[ 1., 1., 1.],
    [ 1., 1., 1.],
    [ 1., 1., 1.]])
    >>> b = np.zeros((2, 2))
    >>> b
    array([[ 0., 0.],
    [ 0., 0.]])
    >>> c = np.eye(3)
    >>> c
    array([[ 1., 0., 0.],
    [ 0., 1., 0.],
    [ 0., 0., 1.]])
    >>> d = np.diag(np.array([1, 2, 3, 4]))
    >>> d
    array([[1, 0, 0, 0],
    [0, 2, 0, 0],
    [0, 0, 3, 0],
    [0, 0, 0, 4]])
  • np.random:随机数(Mersenne Twister PRNG):

    >>> a = np.random.rand(4)       # uniform in [0, 1]
    
    >>> a
    array([ 0.95799151, 0.14222247, 0.08777354, 0.51887998])
    >>> b = np.random.randn(4) # Gaussian
    >>> b
    array([ 0.37544699, -0.11425369, -0.47616538, 1.79664113])
    >>> np.random.seed(1234) # Setting the random seed

练习:使用函数创建数组

  • arangelinspaceoneszeroseyediag
  • 使用随机数创建不同类型的数组。
  • 在创建具有随机值的数组之前尝试设置种子。
  • 查看函数np.empty它有什么作用?什么时候这可能有用?

1.3.1.3. 基本数据类型

你可能已经注意到,在某些情况下,数组元素显示有后面的点(例如2. vs 2)。这是由于使用的数据类型的差异:

>>> a = np.array([1, 2, 3])
>>> a.dtype
dtype('int64')
>>> b = np.array([1., 2., 3.])
>>> b.dtype
dtype('float64')

不同的数据类型允许我们在存储器中更紧凑地存储数据,但大多数时候我们只使用浮点数。注意,在上面的例子中,NumPy从输入中自动检测数据类型。


你可以显式指定所需的数据类型:

>>> c = np.array([1, 2, 3], dtype=float)
>>> c.dtype
dtype('float64')

默认数据类型是浮点数:

>>> a = np.ones((3, 3))
>>> a.dtype
dtype('float64')

还有其他类型:

复数:
>>> d = np.array([1+2j, 3+4j, 5+6*1j])
>>> d.dtype
dtype('complex128')
布尔:
>>> e = np.array([True, False, False, True])
>>> e.dtype
dtype('bool')
字符串:
>>> f = np.array(['Bonjour', 'Hello', 'Hallo',])
>>> f.dtype # <--- strings containing max. 7 letters
dtype('S7')
更多类型:
  • int32
  • int64
  • uint32
  • uint64

1.3.1.4. 基本的可视化

现在我们有了第一个数据数组,我们将把它们可视化。

首先启动IPython:

$ ipython

或者notebook:

$ ipython notebook

IPython启动之后,启用交互式画图:

>>> %matplotlib  

或者,从notebook中,在notebook中启用画图:

>>> %matplotlib inline 

inline对notebook很重要,这样图表显示在notebook中,而不是显示在新窗口中。

Matplotlib是一个2D绘图软件包。我们可以如下导入其函数:

>>> import matplotlib.pyplot as plt  # the tidy way

然后使用(请注意,如果你未使用%matplotlib启用交互图,则必须显式使用show):

>>> plt.plot(x, y)       # line plot    
>>> plt.show() # <-- shows the plot (not needed with interactive plots)

或者,如果你已启用%matplotlib的交互式画图:

>>> plt.plot(x, y)       # line plot    
  • 1D绘图

    >>> x = np.linspace(0, 3, 20)
    
    >>> y = np.linspace(0, 9, 20)
    >>> plt.plot(x, y) # line plot
    [<matplotlib.lines.Line2D object at ...>]
    >>> plt.plot(x, y, 'o') # dot plot
    [<matplotlib.lines.Line2D object at ...>]

    [源代码hires.pngpdf]

    ../../_images/numpy_intro_1.png
  • 2D数组(如图片):

    >>> image = np.random.rand(30, 30)
    
    >>> plt.imshow(image, cmap=plt.cm.hot)
    >>> plt.colorbar()
    <matplotlib.colorbar.Colorbar instance at ...>

    [源代码hires.pngpdf]

    ../../_images/numpy_intro_2.png

另见

更多在:matplotlib一章

练习:简单的可视化

  • 绘制一些简单的数组:余弦作为时间的函数和2D矩阵。
  • 尝试使用2D矩阵上的gray颜色图。

1.3.1.5. 索引和切片

可以按照与其他Python序列(例如列表)相同的方式访问和赋值数组的元素:

>>> a = np.arange(10)
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> a[0], a[2], a[-1]
(0, 2, 9)

警告

指数从0开始,和其他Python序列(以及C/C++)一样。相反,在Fortran或Matlab中,索引从1开始。

支持反转python序列的通常写法:

>>> a[::-1]
array([9, 8, 7, 6, 5, 4, 3, 2, 1, 0])

对于多维数组,索引是整数组成的元组:

>>> a = np.diag(np.arange(3))
>>> a
array([[0, 0, 0],
[0, 1, 0],
[0, 0, 2]])
>>> a[1, 1]
1
>>> a[2, 1] = 10 # third line, second column
>>> a
array([[ 0, 0, 0],
[ 0, 1, 0],
[ 0, 10, 2]])
>>> a[1]
array([0, 1, 0])

注意

  • 在2D中,第一个维度对应于,第二个维度对应于
  • 对于多维数组aa[0]解释为获取未指定的维度中的所有元素。

切片:与其他Python序列一样,数组也可以切片:

>>> a = np.arange(10)
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> a[2:9:3] # [start:end:step]
array([2, 5, 8])

请注意,不包括最后一个索引!

>>> a[:4]
array([0, 1, 2, 3])

不需要所有三个切片分量:默认情况下,start为0,end是最后一个,step为1:

>>> a[1:3]
array([1, 2])
>>> a[::2]
array([0, 2, 4, 6, 8])
>>> a[3:]
array([3, 4, 5, 6, 7, 8, 9])

NumPy索引和切片摘要的小小图解...

../../_images/numpy_indexing.png

你还可以组合赋值和切片:

>>> a = np.arange(10)
>>> a[5:] = 10
>>> a
array([ 0, 1, 2, 3, 4, 10, 10, 10, 10, 10])
>>> b = np.arange(5)
>>> a[5:] = b[::-1]
>>> a
array([0, 1, 2, 3, 4, 4, 3, 2, 1, 0])

练习:索引和切片

  • 尝试不同的切片风格,使用startendstep:从一个linspace开始,尝试获得从前向后数的奇数,和向后向前数的偶数。

  • 重复上图中的切片。你可以使用以下表达式创建数组:

    >>> np.arange(6) + np.arange(0, 51, 10)[:, np.newaxis]
    
    array([[ 0, 1, 2, 3, 4, 5],
    [10, 11, 12, 13, 14, 15],
    [20, 21, 22, 23, 24, 25],
    [30, 31, 32, 33, 34, 35],
    [40, 41, 42, 43, 44, 45],
    [50, 51, 52, 53, 54, 55]])

练习:数组创建

创建以下数组(使用正确的数据类型):

[[1, 1, 1, 1],
[1, 1, 1, 1],
[1, 1, 1, 2],
[1, 6, 1, 1]]
[[0., 0., 0., 0., 0.],
[2., 0., 0., 0., 0.],
[0., 3., 0., 0., 0.],
[0., 0., 4., 0., 0.],
[0., 0., 0., 5., 0.],
[0., 0., 0., 0., 6.]]

课程标准:每个用3句话

提示:可以用类似于列表的方式访问各个数组元素,例如a[1]a[1, 2]

提示:查看diag的docstring。

练习:tile数组的创建

浏览np.tile的文档,并使用此函数构造数组:

[[4, 3, 4, 3, 4, 3],
[2, 1, 2, 1, 2, 1],
[4, 3, 4, 3, 4, 3],
[2, 1, 2, 1, 2, 1]]

1.3.1.6. 拷贝和视图

切片操作在原始数组上创建视图,这只是访问数组数据的一种方法。因此,原始数组不会复制到内存中。你可以使用np.may_share_memory()来检查两个数组是否共享同一个内存块。但请注意,这使用启发式算法,可能会给你假阳性。

修改视图时,原始数组也会被修改

>>> a = np.arange(10)
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> b = a[::2]
>>> b
array([0, 2, 4, 6, 8])
>>> np.may_share_memory(a, b)
True
>>> b[0] = 12
>>> b
array([12, 2, 4, 6, 8])
>>> a # (!)
array([12, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> a = np.arange(10)
>>> c = a[::2].copy() # force a copy
>>> c[0] = 12
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> np.may_share_memory(a, c)
False

这种行为第一次看到可能令人惊讶...但它节省内存和时间。

工作示例:素数筛

../../_images/prime-sieve.png

计算0-99之间的素数,用筛子

  • 构造一个形状为(100,) 的布尔数组is_prime,开始全部填充True:
>>> is_prime = np.ones((100,), dtype=bool)
  • 叉掉0和1,它们不是素数:
>>> is_prime[:2] = 0
  • 对于从2开始的每个整数j,叉掉它的倍数:
>>> N_max = int(np.sqrt(len(is_prime) - 1))
>>> for j in range(2, N_max + 1):
... is_prime[2*j::j] = False
  • 浏览help(np.nonzero),然后打印这些素数

  • 跟进:

    • 将上述代码移动到名为prime_sieve.py的脚本文件中
    • 运行它并检查它的工作
    • 使用Eratosthenes筛中建议的优化:
    1. 跳过j,它们已知不是素数
    2. 叉掉的第一个数字是j^2

1.3.1.7. 花式索引

NumPy数组不但可以用切片索引,而且可以用布尔数组或整数数组(掩码)索引。此方法称为花式索引它创建拷贝而不是视图

1.3.1.7.1. 使用布尔掩码

>>> np.random.seed(3)
>>> a = np.random.randint(0, 21, 15)
>>> a
array([10, 3, 8, 0, 19, 10, 11, 9, 10, 6, 0, 20, 12, 7, 14])
>>> (a % 3 == 0)
array([False, True, False, True, False, False, False, True, False,
True, True, False, True, False, False], dtype=bool)
>>> mask = (a % 3 == 0)
>>> extract_from_a = a[mask] # or, a[a%3==0]
>>> extract_from_a # extract a sub-array with the mask
array([ 3, 0, 9, 6, 0, 12])

使用掩码进行索引对于将新值分配给子数组非常有用:

>>> a[a % 3 == 0] = -1
>>> a
array([10, -1, 8, -1, 19, 10, 11, -1, 10, -1, -1, 20, -1, 7, 14])

1.3.1.7.2. 用整数数组索引

>>> a = np.arange(0, 100, 10)
>>> a
array([ 0, 10, 20, 30, 40, 50, 60, 70, 80, 90])

索引可以用整数数组来完成,其中相同的索引重复几次:

>>> a[[2, 3, 2, 4, 2]]  # note: [2, 3, 2, 4, 2] is a Python list
array([20, 30, 20, 40, 20])

可以使用此类索引来分配新值:

>>> a[[9, 7]] = -100
>>> a
array([ 0, 10, 20, 30, 40, 50, 60, -100, 80, -100])

当通过用整数数组索引创建新数组时,新数组的形状与整数数组相同:

>>> a = np.arange(10)
>>> idx = np.array([[3, 4], [9, 7]])
>>> idx.shape
(2, 2)
>>> a[idx]
array([[3, 4],
[9, 7]])

下面的图片说明了各种花式索引应用

../../_images/numpy_fancy_indexing.png

练习:花式索引

  • 再次,再现上图中所示的花式索引。
  • 在左边使用花式索引,右边使用数组创建将值赋值到数组,例如将上图中数组的一部分设置为零。