1. 为什么会有 Numpy ?
简单来讲,Python
内置的若干种数据类型,无法高效地应对计算密集型场景,比如矩阵运算。因此 Numpy
随之应运而生,并被认为是高性能科学计算和数据分析的基础包。
本专栏所介绍到几乎所有的高级工具,都是基于 Numpy
开发的。因为 NumPy
的大部分代码都是用 C 语言写的,其底层算法在设计时就有着极优异的性能,所以使得 NumPy
比纯 Python
代码高效得多。作为基础工具,其实玩转 Numpy
很简单,只要掌握三个关键知识点,即:数据类型的创建、数据层的索引切片、数组运算。下面我们分不同的篇幅一一展开。
对于新入门的同学,尤其需要注意的是,虽然大多数的数据分析工作并不会直接操作 Numpy 对象,但是深谙面向数组的编程方式和逻辑能力是成为 Python 数据分析大牛的关键,切记磨刀不误砍柴工。
面向数组的编程方式,最大的特点就是用数组表达式完成数据操作任务,无需写大量循环。向量化的数组操作会比纯 Python 的等价实现快一到两个数量级。在后续的学习中,我们会有机会细细品味其中的差别和优势。
2. ndarray 对象与创建
什么是数组?
简单说就是有序的元素序列。比如列表 [1,2,3,4]
,这个是简单的一维数组,只有 4个元素,并且不能被拆分为其他的数组组合;复杂一点呢, [[1,2,3],[4,5,6]]
是一个二维数组,由两个一维数组组成。
Numpy 最重要的一个特点就是它可以快速地创建一个 N 维数组对象(即 ndarray ,本专栏 ndarray 对象和 数组 并不做概念上的区分),然后你可以利用 ndarray 这种数据结构非常高效地执行一些数学运算,并且语法风格和 Python 基本一致。
- 通过 array 函数创建 ndarray 对象
创建 ndarray 最简单的方法就是使用 array 函数,它接受一个序列型对象(比如列表),并转化为 ndarray 对象:
In [1]: import numpy as np
In [2]: data0 = [2, 4, 6.5, 8]
In [3]: arr0 = np.array(data0)
In [4]: arr0
Out[4]: array([2. , 4. , 6.5, 8. ])
这里细心的朋友会发现一个有趣的现象,我们传入的列表中,存在 float 和 int 两种数据类型,但是在创建的 ndarray 对象中,默认转化为了 float 结构,这是因为 ndarray 是一个通用的同构数据多维容器,也就是说,其中的所有的元素必须是相同的类型, Numpy 会根据输入的实际情况进行转换。「也就是如果创建的时候,没有指定数据类型,那 Numpy 就会以数组中最小的数据类型为数据。」
In [5]: # 查看创建的arr0的变量类型
...: type(arr0)
Out[5]: numpy.ndarray
In [6]: # 可以用isinstance函数来判断是否是ndarray类型
...: isinstance(arr0, np.ndarray)
Out[6]: True
In [7]: # 创建多维数组
...: data1 = [[1, 2, 3, 4], [5, 6, 7, 8]]
...: arr1 = np.array(data1)
...: arr1
Out[7]:
array([[1, 2, 3, 4],
[5, 6, 7, 8]])
- 如何快速定义 ndarray 的数据类型
那我们是否可以在创建 ndarray 的过程中,直接显式的指定数据类型呢,答案是肯定的!
In [8]: # 利用 dtype 关键字,传入合适的数据类型,显式地定义
...: arr2 = np.array(data1, dtype=np.float32)
...: arr2
Out[8]:
array([[1., 2., 3., 4.],
[5., 6., 7., 8.]], dtype=float32)
In [9]: # 查看 arr2 的数据类型,这里实际上只查看的arr2的组成元素的数据类型
...: arr2.dtype
Out[9]: dtype('float32')
In [10]: # 查看 arr2 的各维度的大小,其结果用tuple表示。tuple的长度,表示数组的维
...: 数;具体值表示数组的大小
...: arr2.shape
Out[10]: (2, 4) # 表示该数组是2×4大小
Python支持基础的数据类型有 int,float,bool,string,complex 等,Numpy 则在此基础上进行了拓展,不过你并不需要进行死记硬背,记住常用的即可,用到的时候可以查表。Numpy 支持常用的数据类型如下:
类型 | 说明 |
---|---|
int8、uint8 | 分别表示有符号和无符号的8位整型,可表示的整数范围为 -128 ~ 127、0 ~ 255 |
int16、uint16 | 分别表示有符号和无符号的16位整型,可表示的整数范围为 -32768 ~ 32767、0 ~ 65535 |
int32、uint32 | 分别表示有符号和无符号的32位整型,可表示的整数范围为 -2147483648 ~ 2147483647、0 ~ 4294967295 |
int64、uint64 | 分别表示有符号和无符号的64位整型,可表示的整数范围为 -9223372036854775808 ~ 9223372036854775807、0 ~ 18446744073709551615 |
float16、float32、float64、float128 | 分别表示半精度浮点数、单精度浮点、双精度浮点、扩展精度浮点数 |
complex64、complex128、complex256 | 分别用两个32位、64位、128位的浮点数表示的复数 |
bool | 存储 True 和 False 值的布尔类型「不为零的皆变成 True,为零的变成 False」 |
Object | Python 对象类型 |
string_ | 类型代号 S,固定长度的字符串类型,每个字符 1 个字节 |
unicode_ | 类型代号 U,固定长度的 unicode 类型,跟字符串的定义方式一样,例如(U8) |
这里我们分别举一个布尔类型和字符串类型的例子,熟悉一下定义的规则:
In [12]: # 通过整形1和0,定义布尔类型的数组
...: data3 = [[1, 0], [0, 1]]
...: arr3 = np.array(data3, dtype=np.bool)
...: arr3
Out[12]:
array([[ True, False],
[False, True]])
In [13]: # 查看 arr3 的数据类型
...: arr3.dtype
Out[13]: dtype('bool')
尝试传入其它的数据类型,来定义布尔类型的数组,看一看会得到什么神奇的结果:
In [15]: data4 = [["a", "b"], ["c", ""]]
...: arr4 = np.array(data4, dtype=np.bool)
...: arr4
Out[15]:
array([[ True, True],
[ True, False]])
这里的定义规则非常 Pythonic,传入一个非空字符串,则 True,否则 False
定义字符串类型的数组:
In [16]: arr5 = np.array(data4, dtype=np.string_)
...: arr5
Out[16]:
array([[b'a', b'b'],
[b'c', b'']], dtype='|S1') # 这里的 1 表示的是 arr5 中各个元素的固定长度为 1
In [17]: # 查看arr5的数据类型
...: arr5.dtype
Out[17]: dtype('S1')
3. 数据类型的更改与格式化输出
在 Python 中,灵活即强大,这是一个深入人心的优势,也是无法抗拒的诱惑,确实,Numpy 也完美地集成了这个优点。对于我们已经定义好的数组,你可以方便地更改其数据类型,前提是数据类型之间的转化不会报错。
- 更改 ndarray 的数据类型
这里我们推荐用 astype 函数来对数组进行操作:
定义一个 np.float32 类型的数组
In [19]: data6 =[[1.230, 2.670], [1.450, 6.000]]
...: arr6 = np.array(data6, np.float32)
...: arr6
Out[19]:
array([[1.23, 2.67],
[1.45, 6. ]], dtype=float32)
变更数据类型为 np.float16
In [20]: arr6.astype(np.float16)
Out[20]:
array([[1.23, 2.67],
[1.45, 6. ]], dtype=float16)
变更数据类型为 np.int8
In [21]: arr6.astype(np.int8)
Out[21]:
array([[1, 2],
[1, 6]], dtype=int8)
虽然上面的操作中我们改变了 arr6 的数据类型,但是打印一下 arr6 你会发现,arr6 原始的的数据类型并没有发生变化:
In [22]: arr6
Out[22]:
array([[1.23, 2.67],
[1.45, 6. ]], dtype=float32)
如果只是按照上述方式,显式地更改数据类型,被操作对象 arr6 并没有发生更改。这是因为 astype 函数会返回更改数据类型后的新的数组。因此如果你想要保存更改后的结果,你需要将 astype 函数结果赋值给一个新命名的变量。
dtype 只接受 Numpy 能够识别的数据类型,因此,在数据类型之前需要加上 np. ,当然传入Python 原生的数据格式,比如 int 或 float ,也是可以的。
另外,在 np.float 与 np.int 之间的互相转化中, float 的小数部分会被直接抹去,仅仅保留整数部分,并非是四舍五入的方式。需要引起注意。
- 数组的格式化输出
在实际应用中,有这样一种场景:浮点数的小数位过多,我们希望格式化地 print 数组的结构,比如保留 3 位有效数字,但是并不想改变原数组的值。其实在 Numpy 中已经封装好了这种方法,一行代码就能搞定:
# precision: 默认保留8位位有效数字,后面不会补0;supress: 对很大/小的数不使用科学计数法 (True)
np.set_printoptions(precision=3, suppress=True)
举例如下:
In [23]: # 对浮点数数组,保留3位有效数字,并禁用科学计数法;小数位数不够,后面不会补 0
...: arr7 = np.array([[3.141592653], [9.8]], dtype=np.float16) # 定义一个 2 维数组
...: np.set_printoptions(precision=3, suppress=True)
...: arr7
Out[23]:
array([[3.14],
[9.8 ]], dtype=float16)
当然了,你也可以利用 Python 的语法规范,灵活地自定义数据的 print 效果:
In [25]: # 对浮点数,保留小数点后3位
...: np.set_printoptions(formatter={'float': '{: 0.3f}'.format})
...: arr7
Out[25]:
array([[ 3.141],
[ 9.797]], dtype=float16)
4. 创建数组的其它方式
用 array 函数可以非常便利地自定义简单的数组,但是想定义一些维度较大的数组的时候,则是有点黔驴技穷的感觉。其实创建数组有很多方式,array 函数只是最基本的用法。这里我们介绍如何创建一些特殊数组:
- zeros 函数,创建指定维度的全为 0 的数组
In [29]: # 创建一个大小为10的全0数组
...: np.zeros(10, dtype=np.int8)
Out[29]: array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0], dtype=int8)
In [30]: # 创建一个大小为2×3的全0数组
...: np.zeros((2,3), dtype=np.float16)
Out[30]:
array([[ 0.000, 0.000, 0.000],
[ 0.000, 0.000, 0.000]], dtype=float16)
zeros 函数接受的第一个参数为待创建数组的维度,若传入一个整数,则创建一个一维数组;若传入 tuple 类型的参数(a,b,c),则创建一个大小为 a×b×c 的数组;dtype 指定数据类型,缺省时 Numpy 默认指定。
- ones 函数,创建指定维度的全为 1 的数组
ones 函数的用法和 zeros 函数类似:
In [31]: # 创建一个大小为5的全1数组
...: np.ones(5, dtype=np.int8)
Out[31]: array([1, 1, 1, 1, 1], dtype=int8)
In [32]: # 创建一个大小为2×3的全1数组
...: np.ones((2,3), dtype=np.float16)
Out[32]:
array([[ 1.000, 1.000, 1.000],
[ 1.000, 1.000, 1.000]], dtype=float16)
- empty 函数,创建一个空数组,只分配内存空间,但是不填充任何值
empty 函数的用法也是类似:
In [33]: # empty 函数返回值为未经过初始化的垃圾值
...: np.empty((2,3), dtype=np.int8)
Out[33]:
array([[0, 0, 0],
[0, 0, 0]], dtype=int8)
- identity 函数,创建一个大小为 n×n 的单位矩阵(对角线为1,其余为0)
# identity函数原型如下:
np.identity(n, dtype=<type ‘float’>)
In [42]: # 创建一个大小为3×3的单位矩阵
...: np.identity(3, dtype=np.int8)
Out[42]:
array([[1, 0, 0],
[0, 1, 0],
[0, 0, 1]], dtype=int8)
- eye 函数,identity 的升级版本
# eye函数的原型如下:
np.eye(N, M=None, k=0, dtype=<type ‘float’>)
如果仅仅指定 N,则输出大小为 N×N 的方阵,功能与 identity 函数一致;如果同时指定 N 和 M ,则输出大小为 N×M 的矩形矩阵。K 为调节值,调节为 1 的对角线的位置偏离度。这里可以通过具体例子来体会一下:
In [44]: # 创建3×3的方阵
...: np.eye(N=3, dtype=np.int8)
Out[44]:
array([[1, 0, 0],
[0, 1, 0],
[0, 0, 1]], dtype=int8)
In [45]: # 创建3×4的矩形矩阵
...: np.eye(N=3, M=4, dtype=np.int8)
Out[45]:
array([[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0]], dtype=int8)
In [46]: # 创建3×4的矩形矩阵,并且为1的对角线向右偏移1个单位。
...: np.eye(N=3, M=4, k=1, dtype=np.int8)
Out[46]:
array([[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]], dtype=int8)
In [47]: # 创建3×4的矩形矩阵,并且为1的对角线向右偏移2个单位。
...: np.eye(N=3, M=4, k=2, dtype=np.int8)
Out[47]:
array([[0, 0, 1, 0],
[0, 0, 0, 1],
[0, 0, 0, 0]], dtype=int8)
需要注意的是,k 值可以为负数。比如 k=-2,则表示:为1的对角线向左偏移2个单位。
5. 总结
- 如何创建一个简单的 ndarray 想必朋友们都学会了。其实对于一个 ndarray 对象,我们可以把它认为是线性代数里的矩阵去加深理解;
- 数组数据类型的定义有2种方法,一个是在创建的时候用 dtype 直接定义,另一个是用 astype 方法进行变化,无论哪种方法,都是非常方便的。;
- Numpy 支持的数据类型相较 Python ,进行了很大的扩充, Numpy 支持的整型有8中,浮点型有4种。朋友们在实际使用中,可以根据业务需求,灵活地选择,以尽可能地节约内存资源,提高运算效率。
本章节列举了几种特殊数组的定义方式,其实我们还有用其他的方式来定义。例如在深度神经网络中,我们需要初始化一个符合高斯分布的高维数组以激活深度学习模型,这种情况下该如何定义呢?请等待下一篇!