第一章 Python 机器学习入门之Numpy(理论知识学习)

第一章 Python 机器学习入门之Numpy的使用


前言

提示:这里可以添加本文要记录的大概内容:

记录数据分析的学习


提示:以下是本篇文章正文内容,下面案例可供参考

一、Numpy是什么?

NumPy是Python科学计算的基础软件包,提供多了维数组对象,多种派生对象(掩码数组、矩阵等)以及用于快速操作数组的函数及API,它包括数学、逻辑、数组形状变换、排序、选择、I/O 、离散傅立叶变换、基本线性代数、基本统计运算、随机模拟等。

二、使用步骤

1.安装配置

代码如下(示例):

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
import  ssl
ssl._create_default_https_context = ssl._create_unverified_context

1.1、设置警告信息

使用seterr函数屏蔽产生无效数据的警告信息

>>> import numpy as np
>>> np.seterr(invalid='ignore')
{'divide': 'warn', 'over': 'warn', 'under': 'ignore', 'invalid': 'warn'}
>>> a = np.array([1,2,3])
>>> b = np.array([1,0,1])
>>> a/b
array([ 1., inf,  3.])

2.基本概念

2.1 NumPy数组的数据类型

在这里插入图片描述
主要有整型(integrate)、浮点型(float)、布尔型(bool)和复数型(complex)和自定义类型。
演示如何查看和指定数组的数据类型
代码如下(示例):

>>> a = np.array([0,1,2,3])
>>> a.dtype
dtype('int32')
>>> a = np.array([0,1,2,3.0])
>>> a.dtype
dtype('float64')
>>> a = np.array([0,1,2,3+0j])
>>> a.dtype
dtype('complex128')
>>> a = np.array([0,1,2,3], dtype=np.int16)
>>> a.dtype
dtype('int16')
>>> a = np.array([0,1,2,3], dtype=np.uint8)
>>> a.dtype
dtype('uint8')

2.2 NumPy数组的属性

主要的两个属性np.dtype(查看数组的数据类型) , np.shape(数组结构或形状)

属性					说明
ndarray.dtype		元素类型
ndarray.shape		数组结构或形状
ndarray.size		数组元素个数
ndarray.itemsize	数组元素的大小,以字节为单位
ndarray.ndim		数组的维度数,也叫秩
ndarray.flags		数组的内存信息
ndarray.real		元素的实部
ndarray.imag		元素的虚部
ndarray.data		元素数组的实际存储区

2.3 维、秩、轴

维,就是维度。我们说数组是几维的,就是指维度,3维的数组,其维度数自然就是3。维度数,有一个专用名字,叫做秩,也就是上一节提到的数组属性 ndim。秩这个名字感觉有些多余,不如维度数更容易理解。但是,(axis)的概念一定要建立起来,并且要理解,因为轴的概念很重要。简单来说,可以把数组的轴,和笛卡尔坐标系的轴对应一下。

一维数组,类比于一维空间,只有一个轴,那就是0轴。
在这里插入图片描述
二维数组,类比于二维平面,有两个轴,我们习惯表示成行、列,那么行的方向就是0轴,列的方向就是1轴。
在这里插入图片描述
三维数组,类比于三维空间,有三个轴,我们习惯表示成层、行、列,那么层的方向就是0轴,行的方向就是1轴,列的方向就是2轴。

用一个求和的例子来演示一下轴概念的重要性。以三维数组为例,求和的需求会比较复杂

>>> a = np.arange(18).reshape((3,2,3)) # 323列的结构
>>> a
array([[[ 0,  1,  2],
        [ 3,  4,  5]],
       [[ 6,  7,  8],
        [ 9, 10, 11]],
       [[12, 13, 14],
        [15, 16, 17]]])
>>> np.sum(a) # 全部元素求和
153 
>>> np.sum(a, axis=0) # 层(0轴)方向求和,即所有层的对应元素求和
array([[18, 21, 24],
       [27, 30, 33]])
>>> np.sum(a, axis=1) # 行(1轴)方向求和,即各层列求和
array([[ 3,  5,  7],
       [15, 17, 19],
       [27, 29, 31]])
>>> np.sum(a, axis=2) # 列(2轴)方向求和,即各层行求和
array([[ 3, 12],
       [21, 30],
       [39, 48]])
>>> np.sum(np.sum(a, axis=1), axis=1) # 分层求和方法1
array([15, 51, 87])
>>> np.sum(np.sum(a, axis=2), axis=1) # 分层求和方法2
array([15, 51, 87])

2.4 广播和矢量化

在讲两个概念之前,我们先思考两个问题:

1、整型数组各元素加1;
2、求两个等长整型数组对应元素之和组成的新数组;

若用python的列表实现的话,代码大约会这样写吧。

>>> x = list(range(5))
>>> for i in range(len(x)): # 遍历数组为每个元素加1
        x[i] += 1
>>> y = list(range(5,10))
>>> z = list()
>>> for i, j in zip(x, y): # 遍历两个数组,逐个元素求和
        z.append(i+j)

用NumPy数组实现的话,代码就简洁多了,无需循环

>>> a = np.arange(5)
>>> a += 1
>>> b = np.arange(5,10)
>>> c = a + b

广播(broadcast)和矢量化(vectorization),是NumPy最精髓的特性,是NumPy的灵魂。所谓广播,就是将对数组的操作映射到每个数组元素上矢量化可以理解为代码中没有显式的循环、索引等。NumPy数组最重要的特性是广播和矢量化,体现在性能上,就是接近C语言的运行效率,体现在代码上,则有这样的特点:

矢量化代码更简洁,更易于阅读
代码行越少意味着出错的几率越小
代码更接近于标准的数学符号
矢量化代码更pythonic
在这里插入图片描述

3. 创建数组

3.1构造简单数组

构造简单数组的方法可以分为四类,分别是蛮力构造法、特殊数值法、随机数值法和定长分割法。在这里插入图片描述

3.1.1 蛮力构造法

蛮力构造法使用 np.array() 来创建数组,其原型为:

numpy.array(object, dtype=None, copy=True, order=None, subok=False, ndmin=0)

蛮力构造法就是你想要什么结构,就直接用 Python 数组或元组写出来。这个方法看起来简单,但很容易出错,不适合构造体量较大的数组。

>>> a = np.array([[1,2,3],[4,5,6]]) # 创建23列数组
>>> a
array([[1, 2, 3],
       [4, 5, 6]])
>>> a.dtype
dtype('int32')

在创建数组时,指定元素的数据类型。

>>> a = np.array([[1,2,3],[4,5,6]], dtype=np.uint8) # 创建23列的单字节无符号整型数组
>>> a
array([[1, 2, 3],
       [4, 5, 6]], dtype=uint8)
3.1.2 特殊数值法

特殊值一般指0,1,空值等。特殊数值法适合构造全0、全1、空数组,或者由0、1组成的类似单位矩阵(主对角线为1,其余为0)的数组。特殊数值法使用的4个函数原型如下:

numpy.zeros(shape, dtype=float, order=‘C’)
numpy.ones(shape, dtype=float, order=‘C’)
numpy.empty(shape, dtype=float, order=‘C’)
numpy.eye(N, M=None, k=0, dtype=float, order='C’)

这几个函数配合 shape 和 dtype 参数,可以很方便的构造一些简单数组:

>>> np.zeros(6)
array([0., 0., 0., 0., 0., 0.])
>>> np.zeros((2,3))
array([[0., 0., 0.],
       [0., 0., 0.]])
>>> np.ones((2,3),dtype=np.float)
array([[1., 1., 1.],
       [1., 1., 1.]])
>>> np.empty((2,3))
array([[1., 1., 1.],
       [1., 1., 1.]])
>>> np.eye(2)
array([[1., 0.],
       [0., 1.]])

如果我们需要一个3行4列、初始值都是5的数组,该怎么做呢?填充函数 fill() 就是专门解决这个问题的:

>>> a = np.empty((3,4), dtype=np.int8)
>>> a.fill(5)
>>> a
array([[5, 5, 5, 5],
       [5, 5, 5, 5],
       [5, 5, 5, 5]], dtype=int8)
3.1.3 随机数值法

NumPy 有一个 random 子模块,功能hen强大。用随机数值法创建数组,主要就是使用 random 子模块。

numpy.random.random(size=None)
numpy.random.randint(low, high=None, size=None, dtype=‘l’)
numpy.random.normal(loc=0.0, scale=1.0, size=None)

random() 生成 [0,1) 区间内的随机浮点数数,randint() 生成 [low, high) 区间内的随机整数。左闭右开

>>> np.random.random(3)
array([0.4129063 , 0.94242888, 0.10129428])
>>> np.random.random((2,3))
array([[0.80530845, 0.96161533, 0.89166972],
       [0.22920038, 0.84989557, 0.46865645]])
>>> np.random.randint(5)
4
>>> np.random.randint(1,5,size=(2,3))
array([[3, 3, 1],
       [3, 3, 3]])

normal() 生成以 loc 为均值、以 scale 为标准差的正态分布数组。下面,我们用正态分布函数模拟生成1000位成年男性的身高数据(假定中国成年男性平均身高170厘米,标准差4厘米),并画出柱状图:

>>> import matplotlib.pyplot as plt
>>> tall = np.random.normal(170, 4, 1000)
>>> bins = np.arange(156, 190, 2)
>>> plt.hist(tall, bins)
(array([  3.,   4.,  15.,  31.,  94., 163., 185., 186., 157.,  92.,  43.,
        18.,   6.,   1.,   1.,   0.]), array([156, 158, 160, 162, 164, 166, 168, 170, 172, 174, 176, 178, 180,
       182, 184, 186, 188]), <a list of 16 Patch objects>)
>>> plt.show()
3.1.4 定长分割法

定长分割法最常用的函数是 arange(),非常类似于 Python 的 range() 函数,只是前面多了一个字母a。另一个定长分割函数 linspace() 类似于 arange(),但功能更强大。两个函数的原型如下:

numpy.arange(start, stop, step, dtype=None)
numpy.linspace(start, stop, num=50, endpoint=True, retstep=False, dtype=None)

arange() 和 Python 的 range() 函数用法相同,还可以接收浮点型参数:

>>> np.arange(5)
array([0, 1, 2, 3, 4])
>>> np.arange(5,11)
array([ 5,  6,  7,  8,  9, 10])
>>> np.arange(5,11,2)
array([5, 7, 9])
>>> np.arange(5.5,11,1.5)
array([ 5.5,  7. ,  8.5, 10. ])
>>> np.arange(3,15).reshape(3,4)
array([[ 3,  4,  5,  6],
       [ 7,  8,  9, 10],
       [11, 12, 13, 14]])

linspace() 函数需要3个参数:一个起点、一个终点,一个返回元素的个数。linspace() 返回的元素包括起点和终点,可以通过endpoint参数选择是否包含终点。

>>> np.linspace(0, 4.5, 5) # 04.5之间返回5个等距数值(包括04.5array([0., 1.125, 2.25 , 3.375, 4.5])
>>> np.linspace(0,4.5,5,endpoint=False) # 04.5之间返回5个等距数值(包括0,但不包括4.5array([0. , 0.9, 1.8, 2.7, 3.6])

3.2 构造复杂数组

3.2.1 重复构造法

重复构造法主要使用 repeat() 和 tile() 两个函数,repeat() 用来重复元素,tile() 用来重复数组。

>>> a = np.arange(5)
>>> a
array([0, 1, 2, 3, 4])
>>> np.repeat(a,3) # 重复元素3array([0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4])
>>> np.tile(a,3) # 重复数组3array([0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 0, 1, 2, 3, 4])
>>> np.tile(a,(3,2)) # 重复数组32array([[0, 1, 2, 3, 4, 0, 1, 2, 3, 4],
       [0, 1, 2, 3, 4, 0, 1, 2, 3, 4],
       [0, 1, 2, 3, 4, 0, 1, 2, 3, 4]])

对于多维数组a,repeat() 还有一个默认参数 axis,tile() 也有不同表现,请仔细揣摩。

>>> a = np.arange(6).reshape((2,3))
>>> a
array([[0, 1, 2],
       [3, 4, 5]])
>>> np.repeat(a,3)
array([0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5])
>>> np.repeat(a,3,axis=0)
array([[0, 1, 2],
       [0, 1, 2],
       [0, 1, 2],
       [3, 4, 5],
       [3, 4, 5],
       [3, 4, 5]])
>>> np.repeat(a,3,axis=1)
array([[0, 0, 0, 1, 1, 1, 2, 2, 2],
       [3, 3, 3, 4, 4, 4, 5, 5, 5]])
>>> np.tile(a,3)
array([[0, 1, 2, 0, 1, 2, 0, 1, 2],
       [3, 4, 5, 3, 4, 5, 3, 4, 5]])
>>> np.tile(a,(2,3))
array([[0, 1, 2, 0, 1, 2, 0, 1, 2],
       [3, 4, 5, 3, 4, 5, 3, 4, 5],
       [0, 1, 2, 0, 1, 2, 0, 1, 2],
       [3, 4, 5, 3, 4, 5, 3, 4, 5]])
3.2.2 网格构造法

经纬度网格是科学数据中常用的概念,通常,经度用Longitude表示,简写成lon,纬度用latitude表示,简写成lat。
用数组表示经纬度网格,一般有两种方式。第一种方式,用两个一维数组表示:

>>> lon = np.linspace(-180,180,37) # 网格精度为10°,共计37个经度点
>>> lat = np.linspace(-90,90,19) # 网格精度为10°,共计19个纬度点

第二种方式,则是用 np.meshgrid() 生成两个二维数组,分别表示经度网格、纬度网格。np.meshgrid() 以刚才的两个一维数组 lon 和 lat 为参数,生成的网格精度也是10°。

>>> lons,lats = np.meshgrid(lon,lat)
>>> lons.shape
(19, 37)
>>> lats.shape
(19, 37)

构造网格,除了 np.meshgrid() 之外,还有一个更牛的方法,可以直接生成纬度网格和经度网格(请注意,纬度在前,经度在后):

>>> lats, lons = np.mgrid[-90:91:5., -180:181:5.] # 网格精度为5°,网格shape为(37,73)
>>> lons.shape, lats.shape
((37, 73), (37, 73))
>>> lats, lons = np.mgrid[-90:90:37j, -180:180:73j] # 也可以用虚实指定分割点数,网格精度同样为5°
>>> lons.shape, lats.shape
((37, 73), (37, 73))

做一下延申,把这个网格用3D的方式画出来,展示一下 NumPy 和 matplotlib 的强大力量。

>>> lats, lons = np.mgrid[-90:91:5., -180:181:5.]
>>> lons = np.radians(lons) # 度转弧度
>>> lats = np.radians(lats) # 度转弧度
>>> z = np.sin(lats) # 网格上所有点的z坐标
>>> x = np.cos(lats)*np.cos(lons) # 网格上所有点的x坐标
>>> y = np.cos(lats)*np.sin(lons) # 网格上所有点的y坐标
>>> import matplotlib.pyplot as plt
>>> import mpl_toolkits.mplot3d
>>> ax = plt.subplot(111,projection='3d')
>>> ax.plot_surface(x,y,z,cmap=plt.cm.coolwarm,alpha=0.8)
>>> plt.show()

4.操作数组

4.1 索引和切片

NumPy数组对象的内容可以通过索引或切片来访问和修改。对于一维数组的索引和切片,NumPy数组和Python的列表一样灵活。

a = np.arange(9)
>>> a[-1]                            # 最后一个元素
8
>>> a[2:5]                           # 返回第2到第5个元素(左闭右开)
array([2, 3, 4])
>>> a[:7:3]                          # 返回第0到第7个元素,步长为3
array([0, 3, 6])
>>> a[::-1]                          # 返回逆序的数组 反转数组
array([8, 7, 6, 5, 4, 3, 2, 1, 0])

对于多维数组操作,NumPy数组比 Python的列表更加灵活、强大。假设有一栋2层楼,每层楼内的房间都是3行4列,那我们可以用一个三维数组来保存每个房间的居住人数(当然,也可以是房间面积等其他数值信息)。

>>> a = np.arange(24).reshape(2,3,4)    # 234>>> a
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]]])
>>> a[1][2][3]                          # 虽然可以这样
23
>>> a[1,2,3]                            # 但这才是规范的用法
23
>>> a[:,0,0]                            # 所有楼层的第1排第1array([ 0, 12])
>>> a[0,:,:]                            # 1楼的所有房间,等价与a[0]或a[0,...]
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
>>> a[:,:,1:3]                          # 所有楼层所有排的第24array([[[ 1,  2],
        [ 5,  6],
        [ 9, 10]],

       [[13, 14],
        [17, 18],
        [21, 22]]])
>>> a[1,:,-1]                           # 2层每一排的最后一个房间
array([15, 19, 23])

提示:

1、对多维数组切片或索引得到的结果,维度不是确定的;
2、切片返回的数组不是原始数据的副本,而是指向与原始数组相同的内存区域。数组切片不会复制内部数组数据,只是产生了原始数据的一个新视图。
>>> a = np.arange(12).reshape(3,4)
>>> a
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
>>> b = a[1:,2:] # 数组b是数组a的切片(不要忘记数组是从0索引开始)
>>> b
array([[ 6,  7],
       [10, 11]])
>>> b[:,:] = 99 # 改变数组b的值,也会同时影响数组a
>>> b
array([[99, 99],
       [99, 99]])
>>> a
array([[ 0,  1,  2,  3],
       [ 4,  5, 99, 99],
       [ 8,  9, 99, 99]])

4.2 改变结构

NumPy数组的存储顺序和数组的视图是相互独立的,因此改变数组的维度是非常便捷的操作,这一类操作不会改变所操作的数组本身的存储顺序, resize() 除外。

  • reshape() - 按照指定的结构(形状)返回数组的新视图,但不会改变数组
  • resize() - 按照指定的结构(形状)改变数组,无返回值
  • ravel() - 返回多维数组一维化的视图,但不会改变原数组
  • transpose() - 返回行变列的视图,但不会改变原数组
  • rollaxis() - 翻滚轴,返回新的视图
>>> a = np.arange(12)
>>> b = a.reshape((34)) # reshape()返回数组a的一个新视图,但不会改变数组a
>>>> a.shape
(12,)
>>> b.shape
(3, 4)
>>> b is a
False
>>> b.base is a
True
a.resize([4,3]) # resize()则真正改变了数组a的结构
>>> a.shape
(4, 3)
>>> a.ravel() # 返回多维数组一维化的视图,但不会改变原数组
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])
>>> a.transpose() # 返回行变列的视图,但不会改变原数组
array([[ 0,  3,  6,  9],
       [ 1,  4,  7, 10],
       [ 2,  5,  8, 11]])
>>> a.T  # 返回行变列的视图,等价于transpose()
array([[ 0,  3,  6,  9],
       [ 1,  4,  7, 10],
       [ 2,  5,  8, 11]])
>>> np.rollaxis(a, 1, 0) # 翻滚轴,1轴变0array([[ 0,  3,  6,  9],
       [ 1,  4,  7, 10],
       [ 2,  5,  8, 11]])

4.3 合并与拆分

NumPy数组一旦创建就不能再改变其元素数量了。如果要动态改变数组元素数量,只能通过合并或者拆分的方法,生成新的数组。对于刚刚上手NumPy的程序员来说,最大的困惑就是不能使用append() 方法向数组内添加元素,甚至连 append() 方法都找不到了。其实,NumPy仍然保留了append() 方法,只不过这个方法不再是NumPy数组的方法,而是是升级到最外层的NumPy命名空间,并且该方法的功能不再是追加元素,而是合并数组

>>> np.append([[1, 2, 3]], [[4, 5, 6]])
array([1, 2, 3, 4, 5, 6])
>>> np.append([[1, 2, 3]], [[4, 5, 6]], axis=0) 

array([[1, 2, 3],
       [4, 5, 6]])
>>> np.append([[1, 2, 3]], [[4, 5, 6]], axis=1)

array([[1, 2, 3, 4, 5, 6]])

不过,这个append()委实不够好用,我给大家推荐的是stack()方法
np.hstack() 水平合并 axis = 1
np.vstack() 垂直合并 axis = 0
np.dstack() 深度合并

>>> a = np.arange(4).reshape(2,2)
>>> b = np.arange(4,8).reshape(2,2)
>>> np.hstack((a,b)) # 水平合并
array([[0, 1, 4, 5],
       [2, 3, 6, 7]])
>>> np.vstack((a,b)) # 垂直合并
array([[0, 1],
       [2, 3],
       [4, 5],
       [6, 7]])
>>> np.dstack((a,b)) # 深度合并
array([[[0, 4],
        [1, 5]],

       [[2, 6],
        [3, 7]]])

stack 函数原型为 stack(arrays, axis=0),请注意体会下面例子中的 axis 的用法。

>>> a = np.arange(60).reshape(3,4,5)
>>> b = np.arange(60).reshape(3,4,5)
>>> a.shape, b.shape
>>> np.stack((a,b), axis=0).shape
(2, 3, 4, 5)
>>> np.stack((a,b), axis=1).shape
(3, 2, 4, 5)
>>> np.stack((a,b), axis=2).shape
(3, 4, 2, 5)
>>> np.stack((a,b), axis=3).shape
(3, 4, 5, 2)

因为数组切片非常简单,所以数组拆分应用较少。拆分是合并的逆过程,最常用的方法是split()。

>>> a = np.arange(8).reshape(2,4)
>>> np.vsplit(a, 2) # 垂直方向拆分成2部分
[array([[0, 1, 2, 3]]), array([[4, 5, 6, 7]])]
>>> np.hsplit(a, 2) # 水平方向拆分成2部分
[array([[0, 1],
       [4, 5]]), array([[2, 3],
       [6, 7]])]

4.4 复制

改变数组结构返回的是原元数据的一个新视图,而不是原元数据的副本。浅复制(view)和深复制(copy)则是创建原数据的副本,但二者之间也有细微差别:浅复制(view)是共享内存,深复制(copy)则是独享。

>>> a = np.arange(6).reshape((2,3))
>>> b = a.view()
>>> b is a
False
>>> b.base is a
False
>>> b.flags.owndata
False
>>> c = a.copy()
>>> c is a
False
>>> c.base is a
False
>>> c.flags.owndata
True

4.5 排序

NumPy 数组排序函数有两个**,一个是sort()**,一个是argsort()sort()返回输入数组的排序副本argsort()返回的是数组值从小到大的索引号。从函数原型看,这两个函数的参数是完全一样的,默认都是进行列排序及axis = 1。

numpy.sort(a, axis=-1, kind=‘quicksort’, order=None)
numpy.argsort(a, axis=-1, kind=‘quicksort’, order=None)
  • a - 要排序的数组
  • axis - 沿着它排序数组的轴,如果没有,则沿着最后的轴排序
  • kind - 排序方法,默认为’quicksort’(快速排序),其他选项还有 ‘mergesort’(归并排序)和‘heapsort’(堆排序)
  • order - 如果数组包含字段,则是要排序的字段
>>> a = np.random.random((2,3))
>>> a
array([[0.79658569, 0.14507096, 0.63016223],
       [0.24983103, 0.98368325, 0.71092079]])
>>> np.argsort(a) # 返回行内从小到大排序的索引序号(列排序),相当于axis=1(最后的轴)
array([[1, 2, 0],
       [0, 2, 1]], dtype=int64)
>>> np.sort(a) # 返回行内从小到大排序的一个新数组(列排序)
array([[0.14507096, 0.63016223, 0.79658569],
       [0.24983103, 0.71092079, 0.98368325]])
>>> np.sort(a,axis=0) # 返回列内每一行都是从小到大排序(行排序)
array([[0.24983103, 0.14507096, 0.63016223],
       [0.79658569, 0.98368325, 0.71092079]])

我们再看看排序字段的使用。先定义一个新的数据类型dt:dt类似于一个字典,有两个键值对,一个是姓名name,一个是年龄age,姓名长度10个字符,年龄是整型。

>>> dt = np.dtype([('name',  'S10'),('age',  int)])
>>> a = np.array([("zhang",21),("wang",25),("li",  17),  ("zhao",27)], dtype = dt)
>>> np.sort(a, order='name') # 如果指定姓名排序,结果是李王张赵
array([(b'li', 17), (b'wang', 25), (b'zhang', 21), (b'zhao', 27)],
      dtype=[('name', 'S10'), ('age', '<i4')])
>>> np.sort(a, order='age') # 如果指定年龄排序,结果则是李张王赵
array([(b'li', 17), (b'zhang', 21), (b'wang', 25), (b'zhao', 27)],
      dtype=[('name', 'S10'), ('age', '<i4')])

4.6 查找和筛选

这里,我们约定查找是返回符合条件的元素的索引号,筛选是返回符合条件的元素。查找和筛选,是 NumPy 数组最令人心动的功能,也是相对比较烧脑的操作。

4.6.1 查找

下面的代码演示了返回数组中最大值和最小值的索引(对于多维数组,这个索引是数组转成一维之后的索引):

>>> a = np.random.random((2,3))
>>> a
array([[0.47881615, 0.55682904, 0.29173085],
       [0.41107703, 0.91467593, 0.88852535]])
>>> np.argmax(a)
4
>>> np.argmin(a)
2

下面的代码演示了返回数组中非零元素的索引:

>>> a = np.random.randint(0, 2, (2,3))
>>> a
array([[0, 0, 0],
       [0, 1, 1]])
>>> np.nonzero(a)
(array([1, 1], dtype=int64), array([1, 2], dtype=int64))

numpy.where() 用于返回数组中满足给定条件的元素的索引,还可以用于替换符合条件的元素(也可在DataFrame中使用,筛选数据):

  • numpy.where(condition[, x, y])
>>> a = np.arange(10)
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> np.where(a < 5)
(array([0, 1, 2, 3, 4], dtype=int64),)
>>> a = a.reshape((2, -1))
>>> a
array([[0, 1, 2, 3, 4],
       [5, 6, 7, 8, 9]])
>>> np.where(a < 5)
(array([0, 0, 0, 0, 0], dtype=int64), array([0, 1, 2, 3, 4], dtype=int64))
>>> np.where(a < 5, a, 10*a) # 满足条件的元素不变,其他元素乘以10
array([[ 0,  1,  2,  3,  4],
       [50, 60, 70, 80, 90]])
4.6.2 筛选

筛选有3种方式,一是使用np.where()返回的python元组二是使用逻辑表达式返回的布尔型数组,三是使用整型数组。

>>> a = np.random.random((3,4))
>>> a
array([[0.41551063, 0.38984904, 0.01204226, 0.72323978],
       [0.82425869, 0.64216573, 0.41475495, 0.21351508],
       [0.30104819, 0.52046164, 0.58286043, 0.66749564]])
>>> a[np.where(a>0.5)] # 返回大于0.5的元素(使用np.where()返回的python元组)
array([0.72323978, 0.82425869, 0.64216573, 0.52046164, 0.58286043,
       0.66749564])
>>> a[(a>0.3)&(a<0.7)] # 返回大于0.3且小于0.7的元素(使用逻辑表达式返回的布尔型数组)
array([0.41551063, 0.38984904, 0.64216573, 0.41475495, 0.30104819,
       0.52046164, 0.58286043, 0.66749564])
>>> a[np.array([2,1])] # 返回整形数组指定的项(使用整型数组)
array([[0.30104819, 0.52046164, 0.58286043, 0.66749564],
       [0.82425869, 0.64216573, 0.41475495, 0.21351508]])
>>> a = a.ravel()
>>> a[np.array([3,5,7,11])] # 返回整形数组指定的项(使用整型数组)
array([0.72323978, 0.64216573, 0.21351508, 0.66749564])
>>> a[np.array([[3,5],[7,11]])] # 返回整形数组指定的项(使用整型数组)
array([[0.72323978, 0.64216573],
       [0.21351508, 0.66749564]])

4.7. 数组I/O

所谓数组I/O,就是讨论如何分发、交换数据。在机器学习算法模型的例子中,海量的训练数据通常都是从数据文件中读出来的,而数据文件一般是csv格式,NumPy 自带的csv文件读写函数,可以很方便的读写csv格式的数据文件。除了支持通用的csv格式的数据文件, NumPy 为数组对象引入了新的二进制文件格式,用于数据交换。后缀名为.npy 文件用于存储单个数组,后缀名为.npz 文件用于存取多个数组。
在这里插入图片描述

下面的代码演示了NumPy读写CSV格式的数据文件的方法。实际操作下面的代码时,请注意结合实际情况替换对应的文件路径和文件名。

# 1、直接将生成的数组进行保存
>>> a = np.random.random((15,5))	
>>> np.savetxt('demo.csv', a, delimiter=',') # 将数组a保存成CSV格式的数据文件
# 2、打开csv文件
>>> data = np.loadtxt('demo.csv', delimiter=',') # 打开CSV格式的数据文件
>>> data.shape, data.dtype 
((15, 5), dtype('float64'))

NumPy 自定义的数据交换格式也是一个非常好用的数据交换方式,使用它保存 NumPy 数组时不会丢失任何信息,特别是数据类型的信息。实际操作下面的代码时,请注意结合实际情况替换对应的文件路径和文件名。

>>> single_arr_fn = 'single_arr.npy' # 存储单个数组文件名
>>> multi_arr_fn = 'multi_arr.npz' # 存储多个数组文件名
>>> lon = np.linspace(10,90,9)
>>> lat = np.linspace(20,60,5)
>>> np.save(single_arr_fn, lon) # 用save()函数把经度数组保存成.npy文件
>>> lon = np.load(single_arr_fn) # 接着用load()函数读出来
>>> np.savez(multi_arr_fn, longitude=lon, latitude=lat) #保存两个数组到一个文件
>>> data = np.load(multi_arr_fn) # 用load()函数把这个.npz文件读成一个结构data
>>> data.files # 查看所有的数组名
>>> data['longitude'] # 使用data[数组名],就可以取得想要的数据
>>> data['latitude'] # 使用data[数组名],就可以取得想要的数据

5. 常用函数

5.1 特殊值

NumPy有两个很有趣的特殊值,np.nan 和 np.inf。nan 是 not a number 的简写,意为不是数字,inf 是 infinity 的简写,意为无穷大。np.nan 也可以写作 np.Nan、np.NaN 或者 np.NAN,np.inf 也可以写作 np.Inf 或 np.Infinity。

  • 特殊值主要用来表示
  • np.nan(缺失值,空值)
  • np.inf(无效值)
  • 基本方法
  • np.isnan()
  • np.isinf()
>>> a = np.array([1, 2, np.nan, np.inf])
>>> a.dtype
dtype('float64')
>>> a[0] = np.nan
>>> a[1] = np.inf
>>> a
array([nan, inf, nan, inf])
>>> a[0] == a[2] # 两个np.nan不相等
False
>>> a[1] == a[3] # 两个np.inf则相等
True
>>> np.isnan(a[0]) # 判断一个数字是否是np.nan
True
>>> np.isinf(a[1]) # 判断一个数字是否是np.inf
True

尽管数组元素包含 np.nan,但这并不影响数值计算。

>>> a = np.array([9, 3, np.nan, 5, 3])
>>> a = np.repeat(a,2)[:-1]
>>> a[1::2] += (a[2::2]-a[1::2])/2
>>> a
array([ 9.,  6.,  3., nan, nan, nan,  5.,  4.,  3.])

5.2 函数命名空间

刚开始使用NumPy函数的时候,你一定会有这样的困惑:

  • 都是求和、求极值,下面这两种写法有什么区别吗?
>>> a = np.random.random(10)
>>> a.max(), np.max(a)
(0.8975052328686041, 0.8975052328686041)
>>> a.sum(), np.sum(a)
(5.255303938070831, 5.255303938070831)
  • 同样是复制,为什么深复制 copy() 两种写法都行,而浅复制 view() 则只有数组的方法?
>>> a = np.random.random(10)
>>> a.copy()
array([0.14712593, 0.05692805, 0.41679214, 0.62755199, 0.58272166,
       0.88131178, 0.26184716, 0.30175671, 0.78588028, 0.50557561])
>>> np.copy(a)
array([0.14712593, 0.05692805, 0.41679214, 0.62755199, 0.58272166,
       0.88131178, 0.26184716, 0.30175671, 0.78588028, 0.50557561])
>>> a.view()
array([0.14712593, 0.05692805, 0.41679214, 0.62755199, 0.58272166,
       0.88131178, 0.26184716, 0.30175671, 0.78588028, 0.50557561])
>>> np.view(a)
Traceback (most recent call last):
  File "<pyshell#61>", line 1, in <module>
    np.view(a)
AttributeError: module 'numpy' has no attribute 'view'
  • 为什么 where() 不能作为数组 ndarray 的函数,而必须作为 NumPy 的函数?
>>> np.where(a>0.5)
(array([3, 4, 5, 8, 9], dtype=int64),)
>>> a.where(a>0.5)
Traceback (most recent call last):
  File "<pyshell#65>", line 1, in <module>
    a.where(a>0.5)
AttributeError: 'numpy.ndarray' object has no attribute 'where'

以上这些差异,取决于函数在不同的的命名空间是否有映射。数组的大部分函数在顶层命名空间有映射,因此可以有两种用法。数组的一小部分函数,没有映射到顶层命名空间,只能有一种用法。而顶层命名空间的大部分函数,也都只有一种用法。下表是我整理出来的常用方法的和命名空间的关系,仅供参考。
在这里插入图片描述

5.3 数学函数

如果不熟悉 NumPy,Python 程序员一般都选择使用 math 模块来应对数学问题。从现在开始,我们可以放弃 math 模块了,因为 NumPy 的数学函数比 math 的更方便。我把这两个模块的数学函数整理了一下,分成5类,汇总在这里。其他诸如求和、求差、求积的函数,我把它们归类到统计函数。

  • 数学常数
  • 舍入函数
  • 快速转换函数
  • 幂函数、指数函数和对数函数
  • 部分三角函数
    在这里插入图片描述
    试用一下这些常用数学函数。
>>> import numpy as np
>>> import math
>>> math.e == np.e # 两个模块的自然常数相等!
True
>>> math.pi == np.pi # 两个模块的圆周率相等!
True
>>> np.ceil(5.3)
6.0
>>> np.ceil(-5.3)
-5.0
>>> np.floor(5.8)
5.0
>>> np.floor(-5.8)
-6.0
>>> np.around(5.87, 1)
5.9
>>> np.rint(5.87)
6.0
>>> np.degrees(np.pi/2)
90.0
>>> np.radians(180)
3.141592653589793
>>> np.hypot(3,4) # 求平面上任意两点的距离
5.0
>>> np.power(3,1/2)
1.7320508075688772
>>> np.log2(1024)
10.0
>>> np.exp(1)
2.718281828459045
>>> np.sin(np.radians(30)) #正弦、余弦函数的周期是2pi
0.49999999999999994
>>> np.sin(np.radians(150))
0.49999999999999994
>>> np.degrees(np.arcsin(0.5)) # 反正弦、反余弦函数的周期则是pi
30.000000000000004

5.4 统计函数

NumPy 的统计函数有很多,我整理了一下,大约可以分成4类:

  • 查找特殊值
  • 求和差积
  • 均值和方差
  • 相关系数
    在这里插入图片描述
    统计学上,方差和标准差使用比较频繁,我们来演示一下:
>>> a = np.random.randint(0,50,(3,4))
>>> np.sum(np.square(a-a.mean()))/a.size # 用方差定义就方差
238.25
>>> np.var(a) # 用方差函数求方差,结果相同
238.25
>>> np.sqrt(a.var()) # 对方差开方,即是标准差
15.435349040433131
>>> a.std() # 用标准差函数求标准差,结果相同
15.435349040433131

综合运用统计函数,分析两只股票的关联关系和收益率。pa 和 pb 是两只股票连续30个交易日的股价数组。每日股价收益率定义为当日股价与前一个交易日股价差除以最后一个交易日的股价。

>>> pa = np.array([79.66, 81.29, 80.37, 79.31, 79.84, 78.53, 78.29, 78.51, 77.99, 79.82, 80.41, 79.27, 80.26, 81.61, 81.39, 80.29, 80.18, 78.38, 75.06, 76.15, 75.66, 73.90, 72.14, 74.27, 75.27, 76.15, 75.40, 76.51, 77.57, 77.06])
>>> pb = np.array([30.93, 31.61, 31.62, 31.77, 32.01, 31.52, 30.09, 30.54, 30.78, 30.84, 30.80, 30.38, 30.88, 31.38, 31.05, 29.90, 29.96, 29.59, 28.71, 28.95, 29.19, 28.71, 27.93, 28.35, 28.92, 29.17, 29.02, 29.43, 29.12, 29.11])
>>> np.corrcoef(pa, pb) # 两只股票的相关系数为0.867,关联比较密切
array([[1.        , 0.86746674],
       [0.86746674, 1.        ]])
>>> pa_re=np.diff(pa)/pa[:-1]
>>> pb_re=np.diff(pb)/pb[:-1]
>>> import matplotlib.pyplot as plt
>>> plt.plot(pa_re)
[<matplotlib.lines.Line2D object at 0x000002262AEBD9C8>]
>>> plt.plot(pb_re)
[<matplotlib.lines.Line2D object at 0x000002262BB96408>]
>>> plt.show()

5.5 插值函数

数据插值是数据处理过程中经常用到的技术,常用的插值有一维插值、二维插值、高阶插值等,常见的算法有线性插值、B样条插值、临近插值等。不过,NumPy只提供了一个简单的一维线性插值函数 np.interp(),其他更加复杂的插值功能放到了SciPy中。

下面用一个实例来演示NumPy一维线性插值函数的使用方法。假定_x和_y是原始的样本点x坐标和y坐标构成的数组,总数只有11个点。如果想在_x的值域范围内插值更多的点,如增加到33个点,就需要在_x的值域范围内生成33个点的 x坐标构成的数组x,再利用插值函数np.interp()得到对应的33个点的y坐标构成的数组y。

>>> import matplotlib.pyplot as plt
>>> _x = np.linspace(0, 2*np.pi, 11)
>>> _y = np.sin(_x)
>>> x = np.linspace(0, 2*np.pi, 33)
>>> y = np.interp(x, _x, _y)
>>> plt.plot(x, y, 'o')
[<matplotlib.lines.Line2D object at 0x0000020A2A8D1048>]
>>> plt.plot(_x, _y, 'o')
[<matplotlib.lines.Line2D object at 0x0000020A2A5ED148>]
>>> plt.show()

5.6 多项式拟合函数

拟合与插值看起来有一些相似,所以初学者比较容易混淆,实际上二者是完全不同的概念。拟合又称回归,是指已知某函数的若干离散函数值,通过调整该函数中若干待定系数,使得该函数与已知离散函数值的误差达到最小。

多项式拟合是最常见的拟合方法。对函数 f ( x ) f(x) f(x),我们可以使用一个 k k k阶多项式去近似。

f ( x ) ≈ g ( x ) = a 0 + a 1 x + a 2 x 2 + a 3 x 3 + . . . + a k x k f(x) \approx g(x) = a_0 + a_1x + a_2x^2 + a_3x^3 + … + a_kx^k f(x)≈g(x)=a0​+a1​x+a2​x2+a3​x3+…+ak​xk

通过选择合适的系数(最佳系数),可以使函数 f ( x ) f(x) f(x)和 g ( x ) g(x) g(x)之间的误差达到最小。最小二乘法被用于寻找多项式的最佳系数。NumPy提供了一个非常简单易用的多项式拟合函数np.polyfit(),只需要输入一组自变量的离散值,和一组对应的函数 f ( x ) f(x) f(x),并指定多项式次数 k k k,就可以返回一组最佳系数。函数np.poly1d(),则可以将一组最佳系数转成函数 g ( x ) g(x) g(x)。

下面的例子首先生成了原始数据点_x 和_y,然后分别用4次、5次、6次和7 次多项式去拟合原始数据,并计算出每次拟合的误差。

>>> import numpy as np
>>> import matplotlib.pyplot as plt
>>> plt.rcParams['font.sans-serif'] = ['FangSong'] # 指定字体以保证中文正常显示
>>> plt.rcParams['axes.unicode_minus'] = False # 正确显示连字符
>>> _x = np.linspace(-1, 1, 201)
>>> _y = ((_x**2-1)**3 + 0.5)*np.sin(2*_x) + np.random.random(201)/10 - 0.1
>>> plt.plot(_x, _y, ls='', marker='o', label="原始数据")
[<matplotlib.lines.Line2D object at 0x0000011D87DFFC08>]
>>> for k in range(4, 8):
		g = np.poly1d(np.polyfit(_x, _y, k)) # g是k次多项式
		loss = np.sum(np.square(g(_x)-_y)) # g(x)f(x)的误差
		plt.plot(_x, g(_x), label="%d次多项式,误差:%0.3f"%(k,loss))
 
[<matplotlib.lines.Line2D object at 0x0000011D87FDAA08>]
[<matplotlib.lines.Line2D object at 0x0000011D8A2A9A88>]
[<matplotlib.lines.Line2D object at 0x0000011D87FE0AC8>]
[<matplotlib.lines.Line2D object at 0x0000011D87FE5348>]
>>> plt.legend()
<matplotlib.legend.Legend object at 0x0000011D87FE00C8>
>>> plt.show()

5.7 自定义广播函数

广播是NumPy最具特色的特性之一,几乎所有的NumPy函数都可以通过广播特性将操作映射到数组的每一个元素上。然而NumPy函数并不能完成所有的工作,有些工作还需要我们自己来定义函数。如何让我们自己定义的函数也可以广播到数组的每一个元素上,就是自定义广播函数要做的事情。

假定a和b是两个结构相同的数组,数据类型为8位无符号整型。我们希望用a和b生成一个具有相同结构的新数组c,生成规则是:同一位置的元素,若a或b任何一方等于0,则c等于0;若a等于b,则c等于0;若a和b均等于2的整数次幂,则c取a和b中的较大者;若a或b只有一方等于2的整数幂,则c等于满足条件的一方;以上条件都不满足时,c取a和b中的较大者。

很显然,现有的NumPy函数都无法实现这个计算功能,因此需要自定义函数,其代码如下。

>>> def func_demo(x, y):
 			 if x == 0 or y == 0 or x == y:
 			     return 0
        	 elif x&(x-1) == 0 and y&(y-1) == 0: # x和y都是2的整数次幂
 			     return max(x, y)
 			 elif x&(x-1) == 0: # 仅有x等于2的整数次幂
 			     return x
 			 elif y&(y-1) == 0: # 仅有y等于2的整数次幂
 			     return y
 			 else:
 			     return max(x, y)
5.7.1 使用np.frompyfunc定义广播函数

使用 np.frompyfunc( ) 将数值函数转换成数组函数需要提供三个参数:数值函数、输入参数的个数和返回值的个数。另外,np.frompyfunc() 返回的广播函数,其返回值是object类型,最终需要根据实际情况显式地转换数据类型,其代码如下。

>>> uf = np.frompyfunc(func_demo, 2, 1)
>>> a = np.random.randint(0, 256, (2,5), dtype=np.uint8)
>>> b = np.random.randint(0, 256, (2,5), dtype=np.uint8)
>>> a
array([[118, 33, 164, 187, 48],
 [ 41, 128, 242, 225, 34]], dtype=uint8)
>>> b
array([[170, 207, 35, 61, 251],
 [251, 206, 70, 208, 85]], dtype=uint8)
>>> c = uf(a, b)
>>> c
array([[170, 207, 164, 187, 251],
 [251, 128, 242, 225, 85]], dtype=object)
>>> c = c.astype(np.uint8)
>>> c
array([[170, 207, 164, 187, 251],
 [251, 128, 242, 225, 85]], dtype=uint8)
5.7.2 使用np.vectorize定义广播函数

np.frompyfunc( ) 适用于多个返回值的函数。如果返回值只有一个,使用 np.vectorize( ) 定义广播函数更方便,并且还可以通过 otypes 参数指定返回数组的元素类型,其代码如下。

>>> uf = np.vectorize(func_demo, otypes=[np.uint8])
>>> a = np.array([[118, 33, 164, 187, 48],
 [ 41, 128, 242, 225, 34]], dtype=np.uint8)
>>> b = np.array([[170, 207, 35, 61, 251],
 [251, 206, 70, 208, 85]], dtype=np.uint8)
>>> c = uf(a, b)
>>> c
array([[170, 207, 164, 187, 251],
 [251, 128, 242, 225, 85]], dtype=uint8)

自定义广播函数并不是真正的广播函数,其运行效率和循环遍历几乎没有差别,因此除非确实必要,否则不应该滥用自定义广播函数。事实上,总有一些技巧可以不用遍历数组也能实现对数组元素的操作,如对数组元素分组操作等。

6.矩阵对象

6.1 矩阵对象

矩阵(Matrix)是一个按照矩形阵列排列的复数或实数集合,但在NumPy中,矩阵np.matrix是数组np.ndarray的派生类。这意味着矩阵本质上是一个数组,拥有数组的所有属性和方法;同时,矩阵又有一些不同于数组的特性和方法。

  • 首先,矩阵是二维的,不能像数组一样幻化成任意维度,即使展开或切片,返回也是二维的;
  • 其次,矩阵和矩阵、矩阵和数组都可以做加减乘除运算,运算结果总是返回矩阵;
  • 最后,矩阵的乘法不同于数组乘法。

6.2 创建矩阵

np.mat() 函数用于创建矩阵,它可以接受列表、数组甚至是字符串等形式的参数,还可以使用dtype参数指定数据类型,其代码如下。

>>> import numpy as np
>>> np.mat([[1,2,3],[4,5,6]], dtype=np.int32) # 使用列表创建矩阵
matrix([[1, 2, 3],
        [4, 5, 6]])
>>> np.mat(np.arange(6).reshape((2,3))) # 使用数组创建矩阵
matrix([[0, 1, 2],
        [3, 4, 5]])
>>> np.mat('1 4 7; 2 5 8; 3 6 9') # 使用Matlab风格的字符串创建矩阵
matrix([[1, 4, 7],
        [2, 5, 8],
        [3, 6, 9]])

此外,和生成特殊值数组类似,numpy.matlib 子模块()也提供了多个函数用于生成特殊值矩阵和随机数矩阵,其代码如下。

>>> import numpy.matlib as mat # 也可以不用单独导入,直接使用numpy.matlib.mat
>>> print(mat.zeros((2,3))) # 全0矩阵
[[0. 0. 0.]
 [0. 0. 0.]]
>>> print(mat.ones((2,3))) # 全1矩阵
[[1. 1. 1.]
 [1. 1. 1.]]
>>> print(mat.eye(3)) # 单位矩阵
[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]
>>> print(mat.empty((2,3))) # 空矩阵
[[1. 1. 1.]
 [1. 1. 1.]]
>>> print(mat.rand((2,3))) # [0,1)区间随机数矩阵
[[0.505 0.494 0.96 ]
 [0.681 0.002 0.281]]
>>> print(mat.randn((2,3))) # 均值0方差1的高斯(正态)分布矩阵
[[ 1.984  2.452  0.567]
 [ 0.646 -0.909  0.774]]

6.3 矩阵属性

矩阵有几个特有的属性,如转置矩阵、逆矩阵、共轭矩阵、共轭转置矩阵等。熟悉这些属性对矩阵计算会有很大的帮助。

>>> m = np.mat(np.arange(6).reshape((2,3)))
>>> print(m)
[[0 1 2]
 [3 4 5]]
>>> print(m.T) # 返回自身的转置矩阵
[[0 3]
 [1 4]
 [2 5]]
>>> print(m.H) # 返回自身的共轭转置矩阵
[[0 3]
 [1 4]
 [2 5]]
>>> print(m.I) # 返回自身的逆矩阵
[[-0.778  0.278]
 [-0.111  0.111]
 [ 0.556 -0.056]]
>>> print(m.A) # 返回自身数据的视图(ndarray类)
[[0 1 2]
 [3 4 5]]

6.4 矩阵乘法

矩阵运算和数组运算大致相同,只有乘法运算有较大差别。在讲广播和矢量化时,我们已经知道,两个数组相乘就是对应元素相乘,条件是两个数组的结构相同。事实上,即使两个数组的结构不同,只要满足特定条件,也能做乘法运算。
除了对应元素相乘,数组还可以使用 np.dot() 函数相乘,其代码如下。

对于数组而言,使用星号相乘和使用np.dot()函数相乘是完全不同的两种乘法;对于矩阵来说,不管是使用星号相乘还是使用np.dot()函数相乘,结果都是np.dot( )函数相乘的结果,因为矩阵没有对应元素相乘这个概念。np.dot()函数实现的乘法就是矩阵乘法。那么矩阵乘法究竟是怎么运算的呢?下图是矩阵相乘的具体算法示意图。
在这里插入图片描述

不是所有的矩阵都能相乘。我们来看,矩阵A乘以矩阵B,二者可以相乘的条件是:A的列数必须等于B的行数。比如,a是4行2列,b是2行3列,axb,4223,没问题,但是反过来,bxa,2342,就无法运算了。可见,矩阵乘法,不满足交换律。再来看看乘法规则。概括说,就是A的各行逐一去乘B的各列。比如,A的第1行和b的第2列,元素个数一定相等,对应元素相乘后求和,作为结果矩阵第1行第2列的值。再比如,a的第3行和b的第3列,对应元素相乘后求和,作为结果矩阵第3行第3列的值。以此类推,我们就得到了矩阵A乘以矩阵B的结果矩阵。
那么,这个眼花缭乱的矩阵乘法,有什么实用价值吗?答案是:有,不但有,而且有非常大的使用价值。对于程序员来说,矩阵乘法最常见的应用是图像的旋转、位移、透视等操作。下面,我们来推导一个平面直角坐标系的旋转矩阵。
在这里插入图片描述

下面,我们应用这个推导结果,定义一个函数,返回平面上的点围绕原点旋转给定角度后的坐标:

>>> def rotate(p,d):
	a = np.radians(d)
	m = np.array([[np.cos(a), np.sin(a)],[-np.sin(a), np.cos(a)]])
	return np.dot(np.array(p), m)

>>> rotate((5.7,2.8), 35) # 旋转35°
array([3.06315263, 5.56301141])
>>> rotate((5.7,2.8), 90) # 旋转90°
array([-2.8,  5.7])
>>> rotate((5.7,2.8), 180) # 旋转180°
array([-5.7, -2.8])
>>> rotate((5.7,2.8), 360) # 旋转360°
array([5.7, 2.8])

7 随机抽样子模块

7.1 随机数

np.random.random() 是最常用的随机数生成函数,该函数生成的随机数均匀分布于[0, 1) 区间(左闭右开)。默认np.random.random() 函数返回一个浮点型随机数,该函数还可以接受一个整型或元组参数,用于指定返回的浮点型随机数数组的结构(shape)。也有很多人习惯使用 np.random.rand() 函数生成随机数,其功能和 np.random.random() 函数一样,只是 np.random.rand() 函数不接受元组参数,必须要写成多个整型参数。

>>> import numpy as np
>>> print(np.random.random())
0.29306990716963177
>>> print(np.random.random(2))
[0.894 0.88 ]
>>> print(np.random.random((2,3)))
[[0.614 0.953 0.773]
 [0.383 0.909 0.988]]

np.random.randint() 是另一个常用的随机数生成函数,该函数生成的随机整数均匀分布于[low, high) 区间(左闭右开)。如果省略 low 参数,则默认 low 的值等于 0。np.random.randint()函数还有一个默认参数 size,用于指定返回的整型随机数数组的结构(shape),其代码如下。

>>> print(np.random.randint(10))
6
>>> print(np.random.randint(10, size=5))
[1 0 3 2 8]
>>> print(np.random.randint(10, size=(2,5)))
[[0 4 0 7 3]
 [9 6 9 1 9]]
>>> print(np.random.randint(10, 100, size=(2,5)))
[[92 99 74 50 21]
 [87 80 66 74 11]]

7.2 随机抽样

随机抽样是从指定的有序列表中随机抽取指定数量的元素。随机抽样的应用比较广泛,如产品抽检、抽签排序等。NumPy 的随机抽样函数是 np.random.choice(),其原型如下。

np.random.choice(a, size=None, replace=True, p=None)

参数 a 表示待抽样的全体样本,它只接受整数或一维的数组(列表)。参数 a 如果是整数,相当于将数组 np.arange(a) 作为全体样本。参数 size 用于指定返回抽样结果数组的结构(shape)。参数 replace 用于指定是否允许多次抽取同一个样本,默认为允许。参数 p 是和全体样本集合等长的权重数组,用于指定对应样本被抽中的概率

>>> print(np.random.choice(1,5)) # 抽签样本只有1个元素0,抽取5[0 0 0 0 0]
>>> print(np.random.choice(['a','b','c'], size=(3,5), p=[0.5,0.25,0.25])) # 指定权重
[['b' 'c' 'c' 'b' 'c']
 ['b' 'c' 'a' 'c' 'a']
 ['b' 'b' 'a' 'b' 'a']]
>>> print(np.random.choice(np.arange(100), size=(2,5), replace=False)) # 不允许重复
[[21 66 85 49 14]
 [16 29 46 99 84]]

7.3 正态分布

使用 np.random.randn() 函数是最简单的生成标准正态分布随机数的方法。np.random.randn() 函数用于生成均值为 0、标准差为 1 的正态分布(标准正态分布)随机数。该函数可以接受一个或两个整型参数,用来指定返回的符合标准正态分布的随机数数组的结构(shape)。

>>> print(np.random.randn()) # 标准正态分布,均值为0,标准差为1
1.7581590925779629
>>> print(np.random.randn(5))
[ 1.735 -1.502 -0.417 -1.22   0.653]
>>> print(np.random.randn(2,5))
[[ 0.008  1.556 -1.151  0.604  0.113]
 [-1.219  1.11  -1.264  0.638 -1.388]]

如果需要生成非标准正态分布随机数,则应该使用 np.random.normal() 函数。np.random.normal() 函数默认生成均值为 0、标准差为 1 的正态分布随机数。参数 loc 用于指定均值,参数scale 用于指定标准差,参数 size 用于指定返回的符合正态分布的随机数数组的结构(shape)。从下面的代码运行结果可以看出,和使用默认标准差相比,指定标准差为 0.2 时,数据分布更加靠近均值。

>>> print(np.random.normal()) # 默认均值为0,标准差为1
-0.06260301454820448
>>> print(np.random.normal(loc=2, size=5)) # 参数loc指定均值为2
[2.112 1.881 0.798 2.533 3.235]
>>> print(np.random.normal(loc=2, scale=0.2, size=(2,5))) # 参数loc指定均值为2,参数scale指定标准差为0.2
[[2.268 2.18  1.832 1.98  1.931]
 [1.585 2.304 1.914 2.054 1.637]]

7.4 伪随机数的深度思考

计算机程序或编程语言中的随机数都是伪随机数。因为计算机硬件是确定的,代码是固定的,算法是准确的,通过这些确定的、固定的、准确的东西不会产生真正的随机数,除非引入这个封闭系统以外的因素。计算机系统的随机数算法一般使用线性同余或平方取中的算法,通过一个种子(通常用时钟代替)产生。这意味着,如果知道了种子和已经产生的随机数,就可能获得接下来随机数序列的信息,这就是伪随机数的可预测性。

NumPy 随机数函数内部使用了一个伪随机数生成器,这个生成器每次实例化时都需要一个种子(整数)完成初始化。如果两次初始化的种子相同,则每次初始化后生成的随机数序列就完全一致。np.random.seed() 函数可以指定伪随机数生成器的初始化种子,其代码如下。

>>> np.random.seed(12345) # 使用'12345'随机种子初始化伪随机数生成器
>>> print(np.random.random(5))
[0.93  0.316 0.184 0.205 0.568]
>>> print(np.random.random((2,3)))
[[0.596 0.965 0.653]
 [0.749 0.654 0.748]]
>>> np.random.seed(12345) # 再次使用'12345'随机种子初始化伪随机数生成器
>>> print(np.random.random(5)) # 和上面完全一致
[0.93  0.316 0.184 0.205 0.568]
>>> print(np.random.random((2,3))) # 和上面完全一致
[[0.596 0.965 0.653]
 [0.749 0.654 0.748]]

从上述代码中可以看出,只要指定相同的种子,接下来的随机序列就完全一致。这意味着,只有从外部引入真正的随机因子(如天空云朵的形状、邻居家无线网络信号的强度等)作为种子,才可以得到真正的随机数。

此外,NumPy 还提供了随机数生成器,可以直接操作这个生成器来生成随机数,其代码如下。

>>> r = np.random.RandomState(12345) # 使用随机数生成器也同样
>>> print(r.random(5)) # 和上面完全一致
[0.93  0.316 0.184 0.205 0.568]
>>> print(r.random((2,3))) # 和上面完全一致
[[0.596 0.965 0.653]
 [0.749 0.654 0.748]]

总结

提示:这里对文章进行总结:

例如:以上仅仅简单介绍了Numpy的基础使用,主要的点:广播机制,bool类型筛选,随机数的构造,即这是后续使用较多的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值