IPython的开发者吸收了标准解释器的基本概念,在此基础上进行了大量的改进,创造出一个令人惊奇的工具。在它的主页上是这么说的:“这是一个增强的交互式Python shell。”具有tab补全,对象自省,强大的历史机制,内嵌的源代码编辑,集成Python调试器,%run机制,宏,创建多个环境以及调用系统shell的能力。
不过我们使用Pycharm,所以跳过学习。
简单来看看一个例子:
from numpy.random import randn
import numpy as np
data={i:randn() for i in range(7)}
print(data[])
注:from numpy.random import randn需要写在import numpy as np的前面
注:
{}
表示字典[]
表示数组()
表示元组
NumPy(Numerical Python的简称)是高性能科学计算和数据分析的基础包。它是本书所介绍的几乎所有高级工具的构建基础。其部分功能如下:
最后一点也是从生态系统角度来看最重要的一点。由于NumPy提供了一个简单易用的C API,因此很容易将数据传递给由低级语言编写的外部库,外部库也能以NumPy数组的形式将数据返回给Python。这个功能使Python成为一种包装C/C++/Fortran历史代码库的选择,并使被包装库拥有一个动态的、易用的接口。
NumPy本身并没有提供多么高级的数据分析功能,理解NumPy数组以及面向数组的计算将有助于你更加高效地使用诸如pandas之类的工具。
NumPy最重要的一个特点就是其N维数组对象(即ndarray),该对象是一个快速而灵活的大数据集容器。你可以利用这种数组对整数据执行一些数学运算,其语法跟标量元素之间的运算一样:
data=np.array([[0.9526, -0.246 , -0.8856],[0.5639, 0.2379, 0.9104]])
print(data)
print(data+data)
print(data*10)
output:
[[ 0.9526 -0.246 -0.8856]
[ 0.5639 0.2379 0.9104]]
[[ 1.9052 -0.492 -1.7712]
[ 1.1278 0.4758 1.8208]]
[[ 9.526 -2.46 -8.856]
[ 5.639 2.379 9.104]]
ndarray是一个通用的同构数据多维容器,也就是说,其中的所有元素必须是相同类型的。每个数组都有一个shape(一个表示各维度大小的元组)和一个dtype(一个用于说明数组数据类型的对象):
print(data.shape)
print(data.dtype)
output:
(2, 3)
float64
创建数组最简单的办法就是使用array函数。它接受一切序列型的对象(包括其他数组),然后产生一个新的含有传入数据的NumPy数组。以一个列表的转换为例:
import numpy as np
data1=[6,7,5,8,0,1]
arr1=np.array(data1)
print(arr1)
嵌套序列(比如由一组等长列表组成的列表)将会被转换为一个多维数组:
import numpy as np
data1=[[1, 2, 3, 4], [5, 6, 7, 8]]
arr1=np.array(data1)
print(arr1)
除非显式说明(稍后将会详细介绍),np.array会尝试为新建的这个数组推断出一个较为合适的数据类型。数据类型保存在一个特殊的dtype对象中。
除np.array之外,还有一些函数也可以新建数组。比如,zeros和ones分别可以创建指定长度或形状的全0或全1数组。empty可以创建一个没有任何具体值的数组。要用这些方法创建多维数组,只需传入一个表示形状的元组即可:
arr2=np.zeros((3,6),float)
print(arr2)
arr3=np.zeros(10)
print(arr3)
arr4=np.ones(1,int)
print(arr4)
arr5=np.empty((2,3,2)) #2个 3x2的数组
print(arr5)
output:
[[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.]
[1]
[[[6.23042070e-307 3.56043053e-307]
[1.37961641e-306 1.22382967e-307]
[8.01097889e-307 1.78020169e-306]]
[[7.56601165e-307 1.02359984e-306]
[1.33510679e-306 2.22522597e-306]
[6.23053614e-307 8.91253127e-313]]]
认为np.empty会返回全0数组的想法是不安全的。很多情况下(如前所示),它返回的都是一些未初始化的垃圾值。
.ndim :维度
.shape :各维度的尺度 (2,5)
.size :元素的个数 10
.dtype :元素的类型 dtype(‘int32’)
.itemsize :每个元素的大小,以字节为单位 ,每个元素占4个字节
ndarray数组的创建
np.arange(n) ; 元素从0到n-1的ndarray类型
np.ones(shape): 生成全1
np.zeros((shape), dtype = np.int32) : 生成int32型的全0
np.full(shape, val): 生成全为val
np.eye(n) : 生成单位矩阵np.ones_like(a) : 按数组a的形状生成全1的数组
np.zeros_like(a): 同理
np.full_like (a, val) : 同理np.linspace(1,10,4): 根据起止数据等间距地生成数组
np.linspace(1,10,4, endpoint = False):endpoint 表示10是否作为生成的元素
np.concatenate():
ndarray的数据类型
bool 用一位存储的布尔类型(值为TRUE或FALSE)
inti 由所在平台决定其精度的整数(一般为int32或int64)
int8 整数,范围为128至127
int16 整数,范围为32 768至32 767
int32 整数,范围为231至231 1
int64 整数,范围为263至263 1
uint8 无符号整数,范围为0至255
uint16 无符号整数,范围为0至65 535
uint32 无符号整数,范围为0至2321
uint64 无符号整数,范围为0至2641
float16 半精度浮点数(16位):其中用1位表示正负号,5位表示指数,10位表示尾数
float32 单精度浮点数(32位):其中用1位表示正负号,8位表示指数,23位表示尾数
float64或float 双精度浮点数(64位):其中用1位表示正负号,11位表示指数,52位表示尾数
complex64 复数,分别用两个32位浮点数表示实部和虚部
complex128或complex 复数,分别用两个64位浮点数表示实部和虚部
你可以通过ndarray的astype方法显式地转换其dtype:
import numpy as np
arr = np.array([1, 2, 3, 4, 5])
float_arr=arr.astype(np.float64)
print(float_arr.dtype)
在本例中,整数被转换成了浮点数。如果将浮点数转换成整数,则小数部分将会被截断。
如果某字符串数组表示的全是数字,也可以用astype将其转换为数值形式。如果转换过程因为某种原因而失败了(比如某个不能被转换为float64的字符串),就会引发一个TypeError。
numeric_strings = np.array(['1.2','2.4'], dtype=np.string_)
numeric_strings.astype(float)
数组和标量之间的运算
数组很重要,因为它使你不用编写循环即可对数据执行批量运算。这通常就叫做矢量化(vectorization)。大小相等的数组之间的任何算术运算都会将运算应用到元素级:
基本的索引和切片
NumPy数组的索引是一个内容丰富的主题,因为选取数据子集或单个元素的方式有很多。一维数组很简单。从表面上看,它们跟Python列表的功能差不多:
In [51]: arr = np.arange(10)
In [52]: arr
Out[52]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [53]: arr[5]
Out[53]: 5
In [54]: arr[5:8]
Out[54]: array([5, 6, 7])
In [55]: arr[5:8] = 12
In [56]: arr
Out[56]: array([ 0, 1, 2, 3, 4, 12, 12, 12, 8, 9])
如上所示,当你将一个标量值赋值给一个切片时(如arr[5:8]=12),该值会自动传播(也就说后面将会讲到的“广播”)到整个选区。跟列表最重要的区别在于,数组切片是原始数组的视图。这意味着数据不会被复制,视图上的任何修改都会直接反映到源数组上:
In [57]: arr_slice = arr[5:8]
In [58]: arr_slice[1] = 12345
In [59]: arr
Out[59]: array([ 0, 1, 2, 3, 4, 12, 12345, 12, 8, 9])
In [60]: arr_slice[:] = 64
In [61]: arr
Out[61]: array([ 0, 1, 2, 3, 4, 64, 64, 64, 8, 9])
如果你想要得到的是ndarray切片的一份副本而非视图,就需要显式地进行复制操作,例如arr[5:8].copy():
arr=np.array([ 0, 1, 2, 3, 4, 12, 12345, 12, 8, 9])
print(arr)
arr_slice=arr[5:8].copy()
arr_slice[:]=1
print(arr_slice)
print(arr)
output
[ 0 1 2 3 4 12 12345 12 8 9]
[1 1 1]
[ 0 1 2 3 4 12 12345 12 8 9]
对于高维度数组,能做的事情更多。在一个二维数组中,各索引位置上的元素不再是标量而是一维数组:
In [62]: arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
In [63]: arr2d[2]
Out[63]: array([7, 8, 9])
因此,可以对各个元素进行递归访问,但这样需要做的事情有点多。你可以传入一个以逗号隔开的索引列表来选取单个元素。也就是说,下面两种方式是等价的:
In [64]: arr2d[0][2]
Out[64]: 3
In [65]: arr2d[0, 2]
Out[65]: 3
切片索引
ndarray的切片语法跟Python列表这样的一维对象差不多:
In [75]: arr[1:6]
Out[75]: array([ 1, 2, 3, 4, 64])
高维度对象的花样更多,你可以在一个或多个轴上进行切片,也可以跟整数索引混合使用。对于上面那个二维数组arr2d,其切片方式稍显不同:
可以看出,它是沿着第0轴(即第一个轴)切片的。也就是说,切片是沿着一个轴向选取元素的。你可以一次传入多个切片,就像传入多个索引那样:
In [78]: arr2d[:2, 1:]
Out[78]:
array([[2, 3],
[5, 6]])
像这样进行切片时,只能得到相同维数的数组视图。通过将整数索引和切片混合,可以得到低维度的切片:
注意,只有单独一个:
的时候表示选取整个轴,因此你可以像下面这样只对高维轴进行切片:
In [81]: arr2d[:, :1]
Out[81]:
array([[1],
[4],
[7]])
布尔型索引
来看这样一个例子,假设我们有一个用于存储数据的数组以及一个存储姓名的数组(含有重复项)。在这里,我将使用numpy.random中的randn函数生成一些正态分布的随机数据。
假设每个名字都对应data数组中的一行,而我们想要选出对应于名字"Bob"的所有行。跟算术运算一样,数组的比较运算(如==)也是矢量化的。因此,对names和字符串"Bob"的比较运算将会产生一个布尔型数组:
import numpy as np
names=np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe','Joe'])
data=np.random.randn(7,4)
print(names)
print(data)
print(names=='Bob')
print(data[names=='Bob'])
output:
['Bob' 'Joe' 'Will' 'Bob' 'Will' 'Joe' 'Joe']
[[ 1.04800916 -2.43798919 -0.80548374 -0.22321222]
[-2.51647025 -0.85169887 -0.2588378 -0.37398974]
[ 1.19123641 0.62080487 0.85106681 -0.24398211]
[ 1.39354191 0.98470156 0.3815374 -0.08218244]
[-0.58025068 -1.6809215 0.77786144 0.48330294]
[ 0.276453 -1.33200014 -0.41610978 0.07494557]
[ 1.45946492 -1.75151171 0.32370049 -0.47419905]]
[ True False False True False False False]
[[ 1.04800916 -2.43798919 -0.80548374 -0.22321222]
[ 1.39354191 0.98470156 0.3815374 -0.08218244]]
布尔型数组的长度必须跟被索引的轴长度一致。此外,还可以将布尔型数组跟切片、整数(或整数序列,稍后将对此进行详细讲解)混合使用:
In [89]: data[names == 'Bob', 2:]
Out[89]:
array([[-0.2349, 1.2792],
[-0.0523, 0.0672]])
In [90]: data[names == 'Bob', 3]
Out[90]: array([ 1.2792, 0.0672])
要选择除"Bob"以外的其他值,既可以使用不等于符号(!=),也可以通过负号(-)对条件进行否定:
In [91]: names != 'Bob'
Out[91]: array([False, True, True, False, True, True, True], dtype=bool)
In [92]: data[-(names == 'Bob')]
Out[92]:
array([[-0.268 , 0.5465, 0.0939, -2.0445],
[-0.047 , -2.026 , 0.7719, 0.3103],
[-1.0023, -0.1698, 1.1503, 1.7289],
[ 0.1913, 0.4544, 0.4519, 0.5535],
[ 0.5994, 0.8174, -0.9297, -1.2564]])
选取这三个名字中的两个需要组合应用多个布尔条件,使用&(和)、|(或)之类的布尔算术运算符即可:
In [93]: mask = (names == 'Bob') | (names == 'Will')
In [94]: mask
Out[94]: array([True, False, True, True, True, False, False], dtype=bool)
In [95]: data[mask]
Out[95]:
array([[-0.048 , 0.5433, -0.2349, 1.2792],
[-0.047 , -2.026 , 0.7719, 0.3103],
[ 2.1452, 0.8799, -0.0523, 0.0672],
[-1.0023, -0.1698, 1.1503, 1.7289]])
通过布尔型数组设置值是一种经常用到的手段。为了将data中的所有负值都设置为0,我们只需:
In [96]: data[data < 0] = 0
In [97]: data
Out[97]:
array([[ 0. , 0.5433, 0. , 1.2792],
[ 0. , 0.5465, 0.0939, 0. ],
[ 0. , 0. , 0.7719, 0.3103],
[ 2.1452, 0.8799, 0. , 0.0672],
[ 0. , 0. , 1.1503, 1.7289],
[ 0.1913, 0.4544, 0.4519, 0.5535],
[ 0.5994, 0.8174, 0. , 0. ]])
通过一维布尔数组设置整行或列的值也很简单:
In [98]: data[names != 'Joe'] = 7
In [99]: data
Out[99]:
array([[ 7. , 7. , 7. , 7. ],
[ 0. , 0.5465, 0.0939, 0. ],
[ 7. , 7. , 7. , 7. ],
[ 7. , 7. , 7. , 7. ],
[ 7. , 7. , 7. , 7. ],
[ 0.1913, 0.4544, 0.4519, 0.5535],
[ 0.5994, 0.8174, 0. , 0. ]])
花式索引
花式索引(Fancy indexing)是一个NumPy术语,它指的是利用整数数组进行索引。假设我们有一个8×4数组:
import numpy as np
arr=np.empty((8,4),np.int32)
for i in range(8):
arr[i]=i
print(arr)
output
[[0 0 0 0]
[1 1 1 1]
[2 2 2 2]
[3 3 3 3]
[4 4 4 4]
[5 5 5 5]
[6 6 6 6]
[7 7 7 7]]
为了以特定顺序选取行子集,只需传入一个用于指定顺序的整数列表或ndarray即可:(这里是传入的顺序,并不是值)
In [103]: arr[[4, 3, 0, 6]] #这里是两个[]
Out[103]:
array([[ 4., 4., 4., 4.],
[ 3., 3., 3., 3.],
[ 0., 0., 0., 0.],
[ 6., 6., 6., 6.]])
使用负数索引将会从末尾开始选取行:
In [104]: arr[[-3, -5, -7]]
Out[104]:
array([[ 5., 5., 5., 5.],
[ 3., 3., 3., 3.],
[ 1., 1., 1., 1.]])
一次传入多个索引数组会有一点特别。它返回的是一个一维数组,其中的元素对应各个索引元组:
arr=np.arange(32).reshape((8, 4))
print(arr)
print(arr[[1,5,7,2],[0,3,1,2]])
output:
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]
[12 13 14 15]
[16 17 18 19]
[20 21 22 23]
[24 25 26 27]
[28 29 30 31]]
[ 4 23 29 10]
我们来看看具体是怎么一回事。最终选出的是元素(1,0)、(5,3)、(7,1)和(2,2)。这个花式索引的行为可能会跟某些用户的预期不一样(包括我在内),选取矩阵的行列子集应该是矩形区域的形式才对。下面是得到该结果的一个办法:
In [108]: arr[[1, 5, 7, 2]][:, [0, 3, 1, 2]]
Out[108]:
array([[ 4, 7, 5, 6],
[20, 23, 21, 22],
[28, 31, 29, 30],
[ 8, 11, 9, 10]])
另外一个办法是使用np.ix_函数,它可以将两个一维整数数组转换为一个用于选取方形区域的索引器:
In [109]: arr[np.ix_([1, 5, 7, 2], [0, 3, 1, 2])]
Out[109]:
array([[ 4, 7, 5, 6],
[20, 23, 21, 22],
[28, 31, 29, 30],
[ 8, 11, 9, 10]])
记住,花式索引跟切片不一样,它总是将数据复制到新数组中。
数组转置和轴对换
转置(transpose)是重塑的一种特殊形式,它返回的是源数据的视图(不会进行任何复制操作)。数组不仅有transpose方法,还有一个特殊的T属性:
In [110]: arr = np.arange(15).reshape((3, 5))
In [111]: arr
Out[111]:
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
In [112]: arr.T
Out[112]:
array([[ 0, 5, 10],
[ 1, 6, 11],
[ 2, 7, 12],
[ 3, 8, 13],
[ 4, 9, 14]])
在进行矩阵计算时,经常需要用到该操作,比如利用np.dot计算矩阵内积XTX:
In [113]: arr = np.random.randn(6, 3)
In [114]: np.dot(arr.T, arr)
Out[114]:
array([[ 2.584 , 1.8753, 0.8888],
[ 1.8753, 6.6636, 0.3884],
[ 0.8888, 0.3884, 3.9781]])
对于高维数组,transpose需要得到一个由轴编号组成的元组才能对这些轴进行转置(比较费脑子):
In [115]: arr = np.arange(16).reshape((2, 2, 4))
In [116]: arr
Out[116]:
array([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7]],
[[ 8, 9, 10, 11],
[12, 13, 14, 15]]])
In [117]: arr.transpose((1, 0, 2))
Out[117]:
array([[[ 0, 1, 2, 3],
[ 8, 9, 10, 11]],
[[ 4, 5, 6, 7],
[12, 13, 14, 15]]])
简单的转置可以使用.T,它其实就是进行轴对换而已。ndarray还有一个swapaxes方法,它需要接受一对轴编号:
In [118]: arr
Out[118]:
array([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7]],
[[ 8, 9, 10, 11],
[12, 13, 14, 15]]])
In [119]: arr.swapaxes(1, 2)
Out[119]:
array([[[ 0, 4],
[ 1, 5],
[ 2, 6],
[ 3, 7]],
[[ 8, 12],
[ 9, 13],
[10, 14],
[11, 15]]])
swapaxes也是返回源数据的视图(不会进行任何复制操作)。