Python数据分析 笔记2(NumPy)

参考书籍 《利用Python进行数据分析(原书第2版)》第4章 NumPy基础:数组与向量化计算

NumPy => Numerical Python的简称

@多数情况下,数据分析应用关注的内容
· 在数据处理、清洗、构造子集、过滤、变换以及其他计算中进行快速的向量化计算。
· 常见的数组算法,比sort、unique以及set操作等。
· 高效的描述性统计和聚合/概述数据。
· 数据排列和相关数据操作,例如对异构数据进行merge和join。
· 使用数组表达式来表明条件逻辑,代替if-elif-else条件分支的循环。
· 分组数据的操作(聚合、变换以及函数式操作)。
 

@NumPy涉及的内容
· ndarray,一种高效多维数组
· 对所有数据进行快速的矩阵计算 => 无须编写循环程序 [1]
· 对硬盘中数组数据进行读写的工具,并对内存映射文件进行操作。
· 线性代数、随机数生成以及傅里叶变换功能。
· 用于连接NumPy到C、C++和FORTRAN语言类库的C语言API。

[1] NumPy可以针对全量数组进行复杂计算而不需要写Python循环。
【例】假设一个NumPy数组包含10万个整数,一个Python列表包含同样内容
           操作:同时对每个序列乘以2,看看花费的时间

In [1]: import numpy as np
In [2]: my_arr = np.arange(100000)
In [3]: my_list = list(range(100000))
In [4]: %time for i in range(10): my_arr2 = my_arr * 2
Wall time: 1.99 ms
In [5]: %time for i in range(10): my_list2 = [x * 2 for x in my_list]
Wall time: 126 ms
In [6]:

可以看出,NumPy的方法比Python方法要快,并且使用的内存也更少。
说明:系统时间(Wall clock time) 指一段程序从运行到终止,系统时钟走过的时间。
CPU time和Wall time 参考阅读 -> https://blog.csdn.net/filyouzicha/article/details/52447887
进程时间也称CPU时间,用以度量进程使用的中央处理器资源。CPU总时间(user + sys)= 用户时间(user)和系统时间(sys)。
实际时间指实际流逝的时间,是从进行开始执行到完成所经历的墙上时钟时间(wall clock)时间。

注意:Python列表直接乘以2的含义是list + list,即将两个列表拼在一起。

In [7]: my_arr = np.arange(10)
In [8]: my_arr
Out[8]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

In [9]: my_list = list(range(10))
In [10]: my_list
Out[10]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

In [11]: my_arr * 2
Out[11]: array([ 0,  2,  4,  6,  8, 10, 12, 14, 16, 18])

In [12]: my_list * 2
Out[12]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

In [13]: my_list2 = list(range(10))
In [15]: [x * 2 for x in my_list2]
Out[15]: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

In [16]:     

约定:以下所说的数组都是指ndarray对象

4.1 NumPy ndarray:多维数组对象

NumPy的核心特征之一就是N-维数组对象——ndarray。

@数组的三个属性:维度ndim,每一维度的数量shape,数据类型ntype

一个ndarray是一个多维同类数据容器,同类数据即它包含的每一个元素均为相同类型。每一个数组都有一个ndim属性,用来描述数组的维度;每一个数组都有一个shape属性,用来表征数组每一维度的数量;每一个数组都有一个dtype属性,用来描述数组的数据类型

数组之间的加减乘除是针对每一个位置的标量两两对应操作,即允许你进行批量操作而无须任何for循环。NumPy用户称这种特性为向量化。任何在两个等尺寸数组之间的算术操作都应用了逐元素操作的方式。

【例】生成一个2x3 随机数矩阵,进行加法和乘法操作。查看shape属性值和dtype属性值。

查看ndim属性值。

同尺寸数组之间的比较,会产生一个布尔值数组;不同尺寸的数组间的操作,将会用到广播特性(暂做了解)。

【例】两个2x2数组比较

In [16]: arr1 = np.array([[1,2],[3,4]])
In [17]: arr2 = np.array([[4,3],[2,1]])
In [18]: arr1 > arr2
Out[18]:
array([[False, False],
       [ True,  True]])
In [19]:    

@生成数组的方法

【例】以二维矩阵 2x3 为例

1)生成随机数矩阵 np.random.randn(2,3)

2)np.array() 接收二维列表two_d_list  => np.array(two_d_list) 

3)生成全0数组 np.zeros((2,3))

4)生成全1数组 np.ones((2,3))

@arange是Python内建函数range的数组版

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

@选择单个元素的两种方法

【例】以二维数组为例,选择第1行第2列,可以使用 arr[0][1] 或 arr[0,1] 

In [53]: arr = np.array([[11,12,13],[21,22,23]])
In [54]: arr
Out[54]:
array([[11, 12, 13],
       [21, 22, 23]])

In [55]: arr[0][1]
Out[55]: 12

In [56]: arr[0,1]
Out[56]: 12

@切片

如果你传入了一个数值给数组的切片,例如arr1[5:8] = 12,数值被传递给了整个切片

注意:区别于Python的内建列表,数组的切片是原数组的视图。这意味着数据并不是被复制了,任何对于视图的修改都会反映到原数组上。

【例】改变数组切片中的元素,原数组也会变化

In [38]: list1 = list(range(10))
In [39]: arr1 = np.arange(10)
In [40]: list1_slice = list1[5:8]
In [41]: arr1_slice = arr1[5:8]
In [42]: list1_slice[0] = 666
In [43]: arr1_slice[0] = 666

In [44]: list1_slice
Out[44]: [666, 6, 7]
In [45]: arr1_slice
Out[45]: array([666,   6,   7])

In [46]: list1
Out[46]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
In [47]: arr1
Out[47]: array([  0,   1,   2,   3,   4, 666,   6,   7,   8,   9])

如果你想要一份数组切片的拷贝而不是一份视图的话,必须显式地复制这个数组,例如 arr1[5:8].copy()

In [48]: arr1 = np.arange(10)
In [49]: arr1_slice = arr1[5:8].copy()
In [50]: arr1_slice[0] = 666

In [51]: arr1_slice
Out[51]: array([666,   6,   7])

In [52]: arr1
Out[52]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

@数组的切片索引 => 按照维度依次切片

我觉得这样比较好理解~ 想象有一个方形蛋糕

二维数组 => 俯视蛋糕,有横纵两种刀法切蛋糕

三位数组 => 在上述基础上,增加一种刀法——刀面平行于桌子,从蛋糕侧面横切


 

【例】3x3二维数组切片(以下描述从0开始计数)
arr2d[ :2,1:]  先切第一个维度 :2 => 选择第0~1行;再切第二个维度 1: => 选择第1~2列
arr2d[2] 等价于 arr2d[2,: ],即只切了第一个维度 => 选择第2行

@数组的转置换和换轴

我对转置矩阵的形象理解~ 把二维矩阵想象成一个矩形,沿着左上右下顶点连接的对角线翻折就得到转置矩阵啦~其实就是x轴变成y轴,y轴变成x轴。

转置可以理解为换轴的特殊情况。数组拥有transpose方法,也有特殊的T属性。

【例】创建一个3x5 矩阵,首先使用T属性转置
 

In [57]: arr = np.arange(15).reshape((3, 5))

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

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

使用transpose方法可以达到相同效果,如下第0个参数的值为1 => 0轴换成1轴;第1个参数的值为0 => 1轴换成0轴

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

应用:使用np.dot计算内积

In [64]: np.dot(arr.T,arr)
Out[64]:
array([[125, 140, 155, 170, 185],
       [140, 158, 176, 194, 212],
       [155, 176, 197, 218, 239],
       [170, 194, 218, 242, 266],
       [185, 212, 239, 266, 293]])

In [65]: np.dot(arr,arr.T)
Out[65]:
array([[ 30,  80, 130],
       [ 80, 255, 430],
       [130, 430, 730]])

对于更高维度的数组,transpose方法可以接收包含轴编号的元组,用于置换轴。

【例】三维数组置换轴

In [66]: arr = np.arange(16).reshape((2, 2, 4))

In [67]: arr
Out[67]:
array([[[ 0,  1,  2,  3],
        [ 4,  5,  6,  7]],

       [[ 8,  9, 10, 11],
        [12, 13, 14, 15]]])

In [68]: arr.transpose((1, 0, 2))
Out[68]:
array([[[ 0,  1,  2,  3],
        [ 8,  9, 10, 11]],

       [[ 4,  5,  6,  7],
        [12, 13, 14, 15]]])

In [70]: arr.shape
Out[70]: (2, 2, 4)

In [71]: arr.transpose((1, 0, 2)).shape
Out[71]: (2, 2, 4)

arr.transpose((1, 0, 2)) => 原0轴变为1轴,原1轴变成0轴,最后的2轴没有改变。

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

通用函数,也可以称为ufunc,是一种在ndarray数据中进行逐元素操作的函数。
有很多ufunc是简单的逐元素转换,比如square, sqrt或exp函数。接收一个数组 => 一元通用函数

In [78]: arr = np.arange(8)

In [79]: arr1 = np.square(arr)

In [80]: arr1
Out[80]: array([ 0,  1,  4,  9, 16, 25, 36, 49], dtype=int32)

In [81]: arr2 = np.sqrt(arr1)

In [82]: arr2
Out[82]: array([0., 1., 2., 3., 4., 5., 6., 7.])

In [86]: arr2_2 = np.array(np.sqrt(arr1),dtype=np.int32)

In [87]: arr2_2
Out[87]: array([0, 1, 2, 3, 4, 5, 6, 7])

还有一些通用函数,比如add或maximum,接收两个数组并返回一个数组作为结果 => 二元通用函数

也有一些通用函数返回多个数组。比如modf,它返回一个浮点值数组的小数部分和整数部分。

In [88]: arr = np.random.randn(2,3)

In [89]: arr
Out[89]:
array([[-0.91043845,  0.44003487,  1.26963933],
       [-0.08591442, -0.40683512,  1.11877103]])

In [90]: remainder, whole_part = np.modf(arr)

In [91]: remainder
Out[91]:
array([[-0.91043845,  0.44003487,  0.26963933],
       [-0.08591442, -0.40683512,  0.11877103]])

In [92]: whole_part
Out[92]:
array([[-0.,  0.,  1.],
       [-0., -0.,  1.]])

 

一元通用函数

 

 

 

 

 

二元通用函数

 

 

 

 

 

 

 

 

 

 

4.3 使用数组进行面向数组编程

numpy.where函数是三元表达式x if condition else y的向量化版本。
假设我们有一个布尔值数组cond和两个数值数组,假设cond中的元素为True时,取xarr中的对应元素值,否则取yarr中的元素。
我们可以通过列表推导式来完成,像下列代码这样:

In [93]: xarr = np.array([1.1, 1.2, 1.3, 1.4, 1.5])
In [94]: yarr = np.array([2.1, 2.2, 2.3, 2.4, 2.5])
In [95]: cond = np.array([True, False, True, True, False])
In [96]: result = [(x if c else y)
    ...: for x, y, c in zip(xarr, yarr, cond)]

In [97]: result
Out[97]: [1.1, 2.2, 1.3, 1.4, 2.5]

这样会产生的问题:
首先,如果数组很大的话,速度会很慢;
其次,当数组是多维时,就无法凑效了。而使用np.where时,就可以非常简单地完成:

In [98]: result = np.where(cond, xarr, yarr)
In [99]: result
Out[99]: array([1.1, 2.2, 1.3, 1.4, 2.5])

传递给np.where的数组既可以是同等大小的数组,也可以是标量。

【例】假设有一个随机生成的矩阵数据,想将其中的正值都替换为2,将所有的负值替换为-2,使用np.where会实现:

In [172]: arr = np.random.randn(4, 4)
In [173]: arr
Out[173]:
array([[-0.5031, -0.6223, -0.9212, -0.7262],
        [ 0.2229,  0.0513, -1.1577,  0.8167],
        [ 0.4336,  1.0107,  1.8249, -0.9975],
        [ 0.8506, -0.1316,  0.9124,  0.1882]])
In [174]: arr > 0
Out[174]:
array([[False, False, False, False],
        [ True,  True, False,  True],
        [ True,  True,  True, False],
        [ True, False,  True,  True]], dtype=bool)
In [175]: np.where(arr > 0, 2, -2)
Out[175]:
array([[-2, -2, -2, -2],
        [ 2,  2, -2,  2],
        [ 2,  2,  2, -2],
        [ 2, -2,  2,  2]])

可以使用np.where将标量和数组联合,例如,我可以使用 np.where(arr > 0, 2, arr) 将arr中的所有正值替换为常数2。

4.3.2 数学和统计方法

使用聚合函数(通常也叫缩减函数),比如sum、mean和std(标准差),可以接收一个可选参数axis,这个参数可以用于计算给定轴向上的统计值,形成一个下降一维度的数组 => 沿着选定轴拍扁(可以用二维、三维矩阵脑补一下)

arr.mean(1)表示“计算每一列的平均值”
arr.sum(0)表示“计算行轴向的累和”

这里理解了很久,特别是三维情况,于我画了图理解,如下

1)二维情况

【例】arr.mean(1)表示 以1轴求平均,这里的1轴就是下图的Y轴

2)三维情况

增加了一个维度,所以加上Z轴,特别注意,此时的0轴已经不是X轴了(这里卡壳很久= =)。如下是一个2x2x2的三维数组,可以把arr[0]理解为第一个面,注意元素的标识,会发现代表第几个面的是标识的第一个参数(而非第三个)。

形象化如下图:

四维可以想象不同时间有同大小立方体,沿着时间轴拍扁,更多维度就不纠结了,直接用纯数学思维理解更简单。

回到这一章节- -

其它的方法,例如cumsum和cumprod并不会聚合,它们会产生一个中间结果 => 沿着选定轴逐级拍扁
在多维数组中,像cumsum这样的累积函数返回相同长度的数组,但是可以在指定轴向上根据较低维度的切片进行部分聚合。

【例】sum,cumsum和cumprod

In [100]: arr = np.arange(6).reshape(2,3)

In [101]: arr
Out[101]:
array([[0, 1, 2],
       [3, 4, 5]])

In [107]: arr[0]
Out[107]: array([0, 1, 2])

In [108]: arr.cumsum(axis=0)
Out[108]:
array([[0, 1, 2],
       [3, 5, 7]], dtype=int32)

In [109]: arr.cumsum(axis=1)
Out[109]:
array([[ 0,  1,  3],
       [ 3,  7, 12]], dtype=int32)

In [110]: arr.cumprod(axis=0)
Out[110]:
array([[ 0,  1,  2],
       [ 0,  4, 10]], dtype=int32)

In [111]: arr.cumprod(axis=1)
Out[111]:
array([[ 0,  0,  0],
       [ 3, 12, 60]], dtype=int32)

In [112]: arr.sum(axis=0)
Out[112]: array([3, 5, 7])

In [113]: arr.sum(axis=1)
Out[113]: array([ 3, 12])

 

基础数组统计方法

 

 

 

 

 

 

4.3.3 布尔值数组的方法

布尔值会被强制为1(True)和0(False)。因此,sum通常可以用于计算布尔值数组中的True的个数:

【例】统计随机数矩阵中的正值个数

In [190]: arr = np.random.randn(100)
In [191]: (arr > 0).sum() 
Out[191]: 42

对于布尔值数组,有两个非常有用的方法any和all。
any检查数组中是否至少有一个True
all检查是否每个值都是True

In [192]: bools = np.array([False, False, True, False])
In [193]: bools.any()
Out[193]: True
In [194]: bools.all()
Out[194]: False

这些方法也可适用于非布尔值数组,所有的非0元素都会按True处理。

4.3.4 排序
和Python的内建列表类型相似,NumPy数组可以使用sort方法按位置排序:

In [195]: arr = np.random.randn(6)
In [196]: arr
Out[196]: array([ 0.6095, -0.4938,  1.24  , -0.1357,  1.43  , -0.8469])
In [197]: arr.sort()
In [198]: arr
Out[198]: array([-0.8469, -0.4938, -0.1357,  0.6095,  1.24  ,  1.43  ])

还可以在多维数组中根据传递的axis值,沿着轴向对每个一维数据段进行排序:

In [199]: arr = np.random.randn(5, 3)
In [200]: arr
Out[200]:
array([[ 0.6033,  1.2636, -0.2555],
        [-0.4457,  0.4684, -0.9616],
        [-1.8245,  0.6254,  1.0229],
        [ 1.1074,  0.0909, -0.3501],
        [ 0.218 , -0.8948, -1.7415]])
In [201]: arr.sort(1)
In [202]: arr
Out[202]:
array([[-0.2555,  0.6033,  1.2636],
        [-0.9616, -0.4457,  0.4684],
        [-1.8245,  0.6254,  1.0229],
        [-0.3501,  0.0909,  1.1074],
        [-1.7415, -0.8948,  0.218 ]])

4.3.5 唯一值与其他集合逻辑
NumPy包含一些针对一维ndarray的基础集合操作(可以与Python的set比较)。
@np.unique => 返回的是数组中唯一值排序后形成的数组

In [206]: names =np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
In [207]: np.unique(names)
Out[207]:
array(['Bob', 'Joe', 'Will'],
      dtype='<U4')
In [208]: ints = np.array([3, 3, 3, 2, 2, 1, 1, 4, 4])
In [209]: np.unique(ints)
Out[209]: array([1, 2, 3, 4])

@np.in1d,可以检查一个数组中的值是否在另外一个数组中,并返回一个布尔值数组

In [211]: values = np.array([6, 0, 0, 3, 2, 5, 6])
In [212]: np.in1d(values, [2, 3, 6])
Out[212]: array([ True, False, False,  True,  True, False,  True], dtype=bool)

 

数组的集合操作

 

 

 

 

 

 

4.4 使用数组进行文件输入和输出

np.save和np.load是高效存取硬盘数据的两大工具函数。
数组在默认情况下是以未压缩的格式进行存储的,后缀名是.npy(如果文件存放路径中没写.npy,会自动加上)。

In [213]: arr = np.arange(10)
In [214]: np.save('some_array', arr)

硬盘上的数组可以使用np.load进行载入:

In [215]: np.load('some_array.npy')
Out[215]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

你可以使用np.savez并将数组作为参数传递给该函数,用于在未压缩文件中保存多个数组,
当载入一个.npz文件的时候,你会获得一个字典型的对象,并通过该对象很方便地载入单个数组:

In [216]: np.savez('array_archive.npz', a=arr, b=arr*2)
In [217]: arch = np.load('array_archive.npz')
In [218]: arch['b']
Out[218]: array([ 0,  2,  4,  6,  8, 10, 12, 14, 16, 18])

4.5 线性代数

@点乘
NumPy中,*是矩阵的逐元素乘积;点乘可以使用函数dot或特殊符号@
x.dot(y)  <=> np.dot(x, y)   <=>  x @ y

In [142]: x = np.arange(6).reshape(2,3)

In [143]: x
Out[143]:
array([[0, 1, 2],
       [3, 4, 5]])

In [144]: y =np.arange(6).reshape(3,2)

In [145]: y
Out[145]:
array([[0, 1],
       [2, 3],
       [4, 5]])

In [146]: x.dot(y)
Out[146]:
array([[10, 13],
       [28, 40]])

In [148]: np.dot(x,y)
Out[148]:
array([[10, 13],
       [28, 40]])

In [149]: x@y
Out[149]:
array([[10, 13],
       [28, 40]])

numpy.linalg拥有一个矩阵分解的标准函数集,以及其它常用函数,例如求逆和行列式求解。

常用numpy.linalg函数

 

 

 

 

 

 

4.6 伪随机数生成

使用normal来获得一个4×4的正态分布样本数组=> np.random.normal(size=(4, 4))
我们可以生成的数据为伪随机数,因为它们是由具有确定性行为的算法根据随机数生成器中的随机数种子生成的。

numpy.random中的部分函数列表

 

4.7 示例:随机漫步

首先,让我们考虑一个简单的随机漫步,从0开始,步进为1和-1,且两种步进发生的概率相等。
以下是使用内建random模块利用纯Python实现的一个1000步的随机漫步:

In [247]: import random
    .....: position = 0
    .....: walk = [position]
    .....: steps = 1000
    .....: for i in range(steps):
    .....:      step = 1 if random.randint(0, 1) else -1
    .....:      position += step
    .....:      walk.append(position)
    .....:

上述walk只是对随机步进的累积,并且可以通过一个数组表达式实现。

现在使用np.random模块一次性抽取1000次投掷硬币的结果,每次投掷的结果为1或-1,然后计算累积值:

In [251]: nsteps = 1000
In [252]: draws = np.random.randint(0, 2, size=nsteps)
In [253]: steps = np.where(draws > 0, 1, -1)
In [254]: walk = steps.cumsum()

说明:numpy.random.randint(low, high=None, size=None, dtype='l')
函数返回一个随机整型数组,如果提供了high,每个数在范围[low, high);否则范围是[0,low)。
size可以是一个整数(一维数组)或者元组(多维数组)。

由此我们开始从漫步轨道上提取一些统计数据,比如最大值、最小值等:

In [255]: walk.min()
Out[255]: -3
In [256]: walk.max()
Out[256]: 31

@一次性模拟多次随机漫步
如果你的目标是模拟多次随机漫步,比如说5000次,每次1000步。
=> size传入一个2个元素的元组,numpy.random中的函数可以生成一个二维的抽取数组,并且可以使用cumsum一次性地跨行算出全部5000次随机步的累积和:

In [1]: nwalks = 5000  #进行5000次随机漫步
In [2]: nsteps = 1000  #每次随机漫步走1000步
In [5]: draws = np.random.randint(0, 2, size=(nwalks, nsteps))  #生成5000x1000矩阵,随机值为0或1
In [6]: steps = np.where(draws > 0, 1, -1)  #上述矩阵值为1的位置返回1,否则返回-1,从而得到一个5000x1000矩阵,元素值值为1或-1
In [7]: walks = steps.cumsum(1)  #每一行都是一次漫步,在行的方向上(沿着1轴)进行逐个累加,得到一个5000x1000矩阵
In [8]: walks
Out[8]:
array([[ -1,   0,   1, ...,  -6,  -7,  -6],
       [  1,   2,   3, ...,  18,  19,  18],
       [  1,   0,   1, ...,  -8,  -9,  -8],
       ...,
       [ -1,   0,   1, ...,  38,  37,  36],
       [  1,   2,   1, ..., -30, -29, -30],
       [  1,   2,   3, ...,  26,  27,  26]], dtype=int32)

In [9]: walks.max()
Out[9]: 140
In [10]: walks.min()
Out[10]: -107


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章