查看原文
其他

【翻译】《利用Python进行数据分析·第2版》第4章(中)NumPy基础:数组和矢量计算

SeanCheney Python爱好者社区 2019-04-07

作者:SeanCheney   Python爱好者社区专栏作者

简书专栏:https://www.jianshu.com/u/130f76596b02


前文传送门:

【翻译】《利用Python进行数据分析·第2版》第1章 准备工作

【翻译】《利用Python进行数据分析·第2版》第2章(上)Python语法基础,IPython和Jupyter

【翻译】《利用Python进行数据分析·第2版》第2章(中)Python语法基础,IPython和Jupyter

【翻译】《利用Python进行数据分析·第2版》第2章(下)Python语法基础,IPython和Jupyter

【翻译】《利用Python进行数据分析·第2版》第3章(上)Python的数据结构、函数和文件

【翻译】《利用Python进行数据分析·第2版》第3章(中)Python的数据结构、函数和文件

【翻译】《利用Python进行数据分析·第2版》第3章(下)Python的数据结构、函数和文件

【翻译】《利用Python进行数据分析·第2版》第4章(上)NumPy基础:数组和矢量计算


切片索引


ndarray的切片语法跟Python列表这样的一维对象差不多:


In [88]: arr Out[88]: array([ 0,  1,  2,  3,  4, 64, 64, 64,  8,  9]) In [89]: arr[1:6] Out[89]: array([ 1,  2,  3,  4, 64])


对于之前的二维数组arr2d,其切片方式稍显不同:


In [90]: arr2d Out[90]: array([[1, 2, 3],       [4, 5, 6],       [7, 8, 9]]) In [91]: arr2d[:2] Out[91]: array([[1, 2, 3],       [4, 5, 6]])


可以看出,它是沿着第0轴(即第一个轴)切片的。也就是说,切片是沿着一个轴向选取元素的。表达式arr2d[:2]可以被认为是“选取arr2d的前两行”。

你可以一次传入多个切片,就像传入多个索引那样:


In [92]: arr2d[:2, 1:] Out[92]: array([[2, 3],       [5, 6]])


像这样进行切片时,只能得到相同维数的数组视图。通过将整数索引和切片混合,可以得到低维度的切片。


例如,我可以选取第二行的前两列:


In [93]: arr2d[1, :2] Out[93]: array([4, 5])


相似的,还可以选择第三列的前两行:


In [94]: arr2d[:2, 2] Out[94]: array([3, 6])


图4-2对此进行了说明。注意,“只有冒号”表示选取整个轴,因此你可以像下面这样只对高维轴进行切片:


In [95]: arr2d[:, :1] Out[95]: array([[1],       [4],       [7]])


图4-2 二维数组切片


自然,对切片表达式的赋值操作也会被扩散到整个选区:


In [96]: arr2d[:2, 1:] = 0 In [97]: arr2d Out[97]: array([[1, 0, 0],       [4, 0, 0],       [7, 8, 9]])


布尔型索引


来看这样一个例子,假设我们有一个用于存储数据的数组以及一个存储姓名的数组(含有重复项)。在这里,我将使用numpy.random中的randn函数生成一些正态分布的随机数据:


In [98]: names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe']) In [99]: data = np.random.randn(7, 4) In [100]: names Out[100]: array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'],      dtype='<U4') In [101]: data Out[101]: array([[ 0.0929,  0.2817,  0.769 ,  1.2464],       [ 1.0072, -1.2962,  0.275 ,  0.2289],       [ 1.3529,  0.8864, -2.0016, -0.3718],       [ 1.669 , -0.4386, -0.5397,  0.477 ],       [ 3.2489, -1.0212, -0.5771,  0.1241],       [ 0.3026,  0.5238,  0.0009,  1.3438],       [-0.7135, -0.8312, -2.3702, -1.8608]])


假设每个名字都对应data数组中的一行,而我们想要选出对应于名字"Bob"的所有行。跟算术运算一样,数组的比较运算(如==)也是矢量化的。因此,对names和字符串"Bob"的比较运算将会产生一个布尔型数组:


In [102]: names == 'Bob' Out[102]: array([ True, False, False,  True, False, False, False], dtype=bool)


这个布尔型数组可用于数组索引:


In [103]: data[names == 'Bob'] Out[103]: array([[ 0.0929,  0.2817,  0.769 ,  1.2464],       [ 1.669 , -0.4386, -0.5397,  0.477 ]])


布尔型数组的长度必须跟被索引的轴长度一致。此外,还可以将布尔型数组跟切片、整数(或整数序列,稍后将对此进行详细讲解)混合使用:


In [103]: data[names == 'Bob'] Out[103]: array([[ 0.0929,  0.2817,  0.769 ,  1.2464],       [ 1.669 , -0.4386, -0.5397,  0.477 ]])


注意:如果布尔型数组的长度不对,布尔型选择就会出错,因此一定要小心。


下面的例子,我选取了names == 'Bob'的行,并索引了列:


In [104]: data[names == 'Bob', 2:] Out[104]: array([[ 0.769 ,  1.2464],       [-0.5397,  0.477 ]]) In [105]: data[names == 'Bob', 3] Out[105]: array([ 1.2464,  0.477 ])


要选择除"Bob"以外的其他值,既可以使用不等于符号(!=),也可以通过~对条件进行否定:


In [106]: names != 'Bob' Out[106]: array([False,  True,  True, False,  True,  True,  True], dtype=bool) In [107]: data[~(names == 'Bob')] Out[107]: array([[ 1.0072, -1.2962,  0.275 ,  0.2289],       [ 1.3529,  0.8864, -2.0016, -0.3718],       [ 3.2489, -1.0212, -0.5771,  0.1241],       [ 0.3026,  0.5238,  0.0009,  1.3438],       [-0.7135, -0.8312, -2.3702, -1.8608]])


~操作符用来反转条件很好用:


In [108]: cond = names == 'Bob' In [109]: data[~cond] Out[109]: array([[ 1.0072, -1.2962,  0.275 ,  0.2289],       [ 1.3529,  0.8864, -2.0016, -0.3718],       [ 3.2489, -1.0212, -0.5771,  0.1241],       [ 0.3026,  0.5238,  0.0009,  1.3438],       [-0.7135, -0.8312, -2.3702, -1.8608]])


选取这三个名字中的两个需要组合应用多个布尔条件,使用&(和)、|(或)之类的布尔算术运算符即可:


In [110]: mask = (names == 'Bob') | (names == 'Will') In [111]: mask Out[111]: array([ True, False,  True,  True,  True, False, False], dtype=bool) In [112]: data[mask] Out[112]: array([[ 0.0929,  0.2817,  0.769 ,  1.2464],       [ 1.3529,  0.8864, -2.0016, -0.3718],       [ 1.669 , -0.4386, -0.5397,  0.477 ],       [ 3.2489, -1.0212, -0.5771,  0.1241]])


通过布尔型索引选取数组中的数据,将总是创建数据的副本,即使返回一模一样的数组也是如此。


注意:Python关键字and和or在布尔型数组中无效。要是用&与|。


通过布尔型数组设置值是一种经常用到的手段。为了将data中的所有负值都设置为0,我们只需:


In [113]: data[data < 0] = 0 In [114]: data Out[114]: array([[ 0.0929,  0.2817,  0.769 ,  1.2464],       [ 1.0072,  0.    ,  0.275 ,  0.2289],       [ 1.3529,  0.8864,  0.    ,  0.    ],       [ 1.669 ,  0.    ,  0.    ,  0.477 ],       [ 3.2489,  0.    ,  0.    ,  0.1241],       [ 0.3026,  0.5238,  0.0009,  1.3438],       [ 0.    ,  0.    ,  0.    ,  0.    ]])


通过一维布尔数组设置整行或列的值也很简单:


In [115]: data[names != 'Joe'] = 7 In [116]: data Out[116]: array([[ 7.    ,  7.    ,  7.    ,  7.    ],       [ 1.0072,  0.    ,  0.275 ,  0.2289],       [ 7.    ,  7.    ,  7.    ,  7.    ],       [ 7.    ,  7.    ,  7.    ,  7.    ],       [ 7.    ,  7.    ,  7.    ,  7.    ],       [ 0.3026,  0.5238,  0.0009,  1.3438],       [ 0.    ,  0.    ,  0.    ,  0.    ]])


后面会看到,这类二维数据的操作也可以用pandas方便的来做。


花式索引


花式索引(Fancy indexing)是一个NumPy术语,它指的是利用整数数组进行索引。假设我们有一个8×4数组:


In [117]: arr = np.empty((8, 4)) In [118]: for i in range(8):   .....:     arr[i] = i In [119]: arr Out[119]: array([[ 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 [120]: arr[[4, 3, 0, 6]] Out[120]: array([[ 4.,  4.,  4.,  4.],       [ 3.,  3.,  3.,  3.],       [ 0.,  0.,  0.,  0.],       [ 6.,  6.,  6.,  6.]])


这段代码确实达到我们的要求了!使用负数索引将会从末尾开始选取行:


In [121]: arr[[-3, -5, -7]] Out[121]: array([[ 5.,  5.,  5.,  5.],       [ 3.,  3.,  3.,  3.],       [ 1.,  1.,  1.,  1.]])


一次传入多个索引数组会有一点特别。它返回的是一个一维数组,其中的元素对应各个索引元组:


In [122]: arr = np.arange(32).reshape((8, 4)) In [123]: arr Out[123]: array([[ 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]]) In [124]: arr[[1, 5, 7, 2], [0, 3, 1, 2]] Out[124]: array([ 4, 23, 29, 10])


附录A中会详细介绍reshape方法。


最终选出的是元素(1,0)、(5,3)、(7,1)和(2,2)。无论数组是多少维的,花式索引总是一维的。


这个花式索引的行为可能会跟某些用户的预期不一样(包括我在内),选取矩阵的行列子集应该是矩形区域的形式才对。下面是得到该结果的一个办法:


In [125]: arr[[1, 5, 7, 2]][:, [0, 3, 1, 2]] Out[125]: array([[ 4,  7,  5,  6],       [20, 23, 21, 22],       [28, 31, 29, 30],       [ 8, 11,  9, 10]])


记住,花式索引跟切片不一样,它总是将数据复制到新数组中。


数组转置和轴对换


转置是重塑的一种特殊形式,它返回的是源数据的视图(不会进行任何复制操作)。数组不仅有transpose方法,还有一个特殊的T属性:


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


在进行矩阵计算时,经常需要用到该操作,比如利用np.dot计算矩阵内积:


In [129]: arr = np.random.randn(6, 3) In [130]: arr Out[130]: array([[-0.8608,  0.5601, -1.2659],       [ 0.1198, -1.0635,  0.3329],       [-2.3594, -0.1995, -1.542 ],       [-0.9707, -1.307 ,  0.2863],       [ 0.378 , -0.7539,  0.3313],       [ 1.3497,  0.0699,  0.2467]]) In [131]: np.dot(arr.T, arr) Out[131]: array([[ 9.2291,  0.9394,  4.948 ],       [ 0.9394,  3.7662, -1.3622],       [ 4.948 , -1.3622,  4.3437]])


对于高维数组,transpose需要得到一个由轴编号组成的元组才能对这些轴进行转置(比较费脑子):


In [132]: arr = np.arange(16).reshape((2, 2, 4)) In [133]: arr Out[133]: array([[[ 0,  1,  2,  3],        [ 4,  5,  6,  7]],       [[ 8,  9, 10, 11],        [12, 13, 14, 15]]]) In [134]: arr.transpose((1, 0, 2)) Out[134]: array([[[ 0,  1,  2,  3],        [ 8,  9, 10, 11]],       [[ 4,  5,  6,  7],        [12, 13, 14, 15]]])


这里,第一个轴被换成了第二个,第二个轴被换成了第一个,最后一个轴不变。

简单的转置可以使用.T,它其实就是进行轴对换而已。ndarray还有一个swapaxes方法,它需要接受一对轴编号:


In [135]: arr Out[135]: array([[[ 0,  1,  2,  3],        [ 4,  5,  6,  7]],       [[ 8,  9, 10, 11],        [12, 13, 14, 15]]]) In [136]: arr.swapaxes(1, 2) Out[136]: array([[[ 0,  4],        [ 1,  5],        [ 2,  6],        [ 3,  7]],       [[ 8, 12],        [ 9, 13],        [10, 14],        [11, 15]]])


swapaxes也是返回源数据的视图(不会进行任何复制操作)。


4.2 通用函数:快速的元素级数组函数


通用函数(即ufunc)是一种对ndarray中的数据执行元素级运算的函数。你可以将其看做简单函数(接受一个或多个标量值,并产生一个或多个标量值)的矢量化包装器。


许多ufunc都是简单的元素级变体,如sqrt和exp:


In [137]: arr = np.arange(10) In [138]: arr Out[138]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) In [139]: np.sqrt(arr) Out[139]: array([ 0.    ,  1.    ,  1.4142,  1.7321,  2.    ,  2.2361,  2.4495,        2.6458,  2.8284,  3.    ]) In [140]: np.exp(arr) Out[140]: array([    1.    ,     2.7183,     7.3891,    20.0855,    54.5982,         148.4132,   403.4288,  1096.6332,  2980.958 ,  8103.0839])


这些都是一元(unary)ufunc。另外一些(如add或maximum)接受2个数组(因此也叫二元(binary)ufunc),并返回一个结果数组:


In [141]: x = np.random.randn(8) In [142]: y = np.random.randn(8) In [143]: x Out[143]: array([-0.0119,  1.0048,  1.3272, -0.9193, -1.5491,  0.0222,  0.7584,       -0.6605]) In [144]: y Out[144]: array([ 0.8626, -0.01  ,  0.05  ,  0.6702,  0.853 , -0.9559, -0.0235,       -2.3042]) In [145]: np.maximum(x, y) Out[145]: array([ 0.8626,  1.0048,  1.3272,  0.6702,  0.853 ,  0.0222,  0.7584,         -0.6605])


这里,numpy.maximum计算了x和y中元素级别最大的元素。


虽然并不常见,但有些ufunc的确可以返回多个数组。modf就是一个例子,它是Python内置函数divmod的矢量化版本,它会返回浮点数数组的小数和整数部分:


In [146]: arr = np.random.randn(7) * 5 In [147]: arr Out[147]: array([-3.2623, -6.0915, -6.663 ,  5.3731,  3.6182,  3.45  ,  5.0077]) In [148]: remainder, whole_part = np.modf(arr) In [149]: remainder Out[149]: array([-0.2623, -0.0915, -0.663 ,  0.3731, 0.6182,  0.45  ,  0.0077]) In [150]: whole_part Out[150]: array([-3., -6., -6.,  5.,  3.,  3.,  5.])


Ufuncs接受out选项参数,可以让它们在数组的原地进行操作:


In [151]: arr Out[151]: array([-3.2623, -6.0915, -6.663 ,  5.3731,  3.6182,  3.45  ,  5.0077]) In [152]: np.sqrt(arr) Out[152]: array([    nan,     nan,     nan,  2.318 ,  1.9022,  1.8574,  2.2378]) In [153]: np.sqrt(arr, arr) Out[153]: array([    nan,     nan,     nan,  2.318 ,  1.9022,  1.8574,  2.2378]) In [154]: arr Out[154]: array([    nan,     nan,     nan,  2.318 ,  1.9022,  1.8574,  2.2378])


表4-3和表4-4分别列出了一些一元和二元ufunc。



赞赏作者

Python爱好者社区历史文章大合集

Python爱好者社区历史文章列表(每周append更新一次)

福利:文末扫码立刻关注公众号,“Python爱好者社区”,开始学习Python课程:

关注后在公众号内回复“课程”即可获取:

小编的Python入门视频课程!!!

崔老师爬虫实战案例免费学习视频。

丘老师数据科学入门指导免费学习视频。

陈老师数据分析报告制作免费学习视频。

玩转大数据分析!Spark2.X+Python 精华实战课程免费学习视频。

丘老师Python网络爬虫实战免费学习视频。


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存