Python深度学习入门-day02-第三方库篇
ZealSinger 发布于 阅读:25 Python
学习此章之前请有一定的Python基础,如果无,可以查看深度学习入门-Base篇
这一篇主要是学习Numpy,Pandas和Matplotlib
Numpy
NumPy是为了克服Python原生的列表List的一些缺点而被创建的
要能在Python程序中使用NumPy,首先就需要记住要导包
import numpy as np
数据类型
NumPy的每一个列表中只能包含一种数据类型以节约内存
NumPy.array一般由np.array(list)创建,即传入Python原生列表对象得到NumPy的array对象
用NumPy创建常见的数据类型的数组,同时通过输出语句可以观察到,NumPy.array对象的打印结果之间使用空格分隔而不会出现逗号(,)而Python原生List打印时会使用逗号进行分隔
import numpy as np
intNumber_arr = np.array([1,2,3,4,5]) #整数型数组
floatNumber_arr = np.array([1.0,2.2,3.3,4.4,5.5]) #浮点型数组
str_arr = np.array(["A","AB","哈哈"]) #字符型数组
print(intNumber_arr) # [1 2 3 4 5]
print(floatNumber_arr)
print(str_arr) #[A AB 哈哈]
因为类型是确定的,所以如果往一个已经确定类型的array中插入别的类型的元素,就会自动的转换/强转
import numpy as np
intNumber_arr = np.array([1,2,3,4,5])
floatNumber_arr = np.array([1.9,2.2,3.3,4.4,5.5])
intNumber_arr[0]=11.99 #往整型中插入浮点型
floatNumber_arr[0]=100 #往浮点型中插入整型
print(intNumber_arr[0]) # 11 小数部分被截断,精度缺失
print(floatNumber_arr[0]) # 100.0
如果需要整体转化,即将一个本来为整型类型的array转化为浮点数类型的array,可以使用.astype()方法
import numpy as np
intNumber_arr = np.array([1,2,3,4,5])
int2Float_arr = intNumber_arr.astype(float)
print(int2Float_arr) # [1. 2. 3. 4. 5.]
数组的创建
NumPy中常见的array创建方式有如下几种
-
np.array(list)和np.array(list,dtype),都是根据元组/列表进行创建,后者的dtype可以指定类型,假如你的list都是整型,但是你想得到一个浮点型的array,就可以让dtype=float -
np.zero()创建一个空array -
np.ones(shape)创建一个shape形状的整型array,元素全部默认为1,拓展,如果我们需要一个全x的数组,可以 x*np.ones(shape) 实现 -
np.full(shape,x)创建一个shape形状的,内容全部为x的array -
np.eye(n,k=0)创建一个n * n的矩阵,对角线为k,k可指定,默认为0 -
np.arange(start=0,stop,step=1)和np.arange(start=0,stop,step=1,dtype),注意区间左开右闭 -
# 语法:np.arange(start, stop, step, dtype) arr_arange1 = np.arange(10) # 从0开始,步长1,到10终止(不包含10) arr_arange2 = np.arange(2, 10, 2) # 从2开始,步长2,到10终止 arr_arange3 = np.arange(0, 1, 0.2) # 支持浮点数步长 print("默认序列:", arr_arange1) print("步长2序列:", arr_arange2) print("浮点步长序列:", arr_arange3) -
np.linspcae(start,stop,num=50,endpoint=True)在start-stop的范围内生成number个数据,会尽力保证等差,endpoint参数决定是否包含stop,也就是说该函数默认是包含终止值,但是如果不需要,可以通过参数控制 -
import numpy as np # 语法:np.linspace(start, stop, num=50, endpoint=True) arr_lin1 = np.linspace(0, 10, 5) # 0到10之间,生成5个均匀分布的数 arr_lin2 = np.linspace(0, np.pi, 10, endpoint=False) # 不包含终止值π print("5个元素的等差序列:", arr_lin1) print("10个元素(不含π)的序列:", arr_lin2) -
np.logspcae(start,stop,num=50)生成从10^start 到 10^stop 范围内的 number个数,可以理解为生成一个等比序列 -
np.random.rand(...)创建一个元素内容在[0,1]内的arr,可变入参,每个参数作为一个维度 -
import numpy as np # 1. 不传参数:生成单个随机数(标量) d1 = np.random.rand() print("不传参数:", d1, type(d1)) # 2. 传单个整数:生成一维数组 d2 = np.random.rand(5) print("\n传单个整数(一维数组):", d2) # 3. 传多个整数:生成多维数组(2行3列) d3 = np.random.rand(2, 3) print("\n传多个整数(二维数组):\n", d3) # 4. 不推荐:传元组(不会报错,但生成的是 1行2列数组,不符合预期) d4 = np.random.rand((2, 3)) # 不报错,但结果非预期 print("\n传元组(非预期结果):\n", d4, d4.shape) -
np.random.random(shape),功能和rand一样,主要区别在于传参,这里需要传入shap,而不能传入多个参数 -
import numpy as np # 1. 不传参数:生成单个随机数(不是数组,是标量) r1 = np.random.random() print("不传参数:", r1, type(r1)) # 2. 传单个整数:生成一维数组 r2 = np.random.random(5) print("\n传单个整数(一维数组):", r2) # 3. 传元组:生成多维数组(2行3列) r3 = np.random.random((2, 3)) print("\n传元组(二维数组):\n", r3) # 4. 错误用法:直接传多个整数(会抛出 TypeError) # r4 = np.random.random(2, 3) # 报错! -
np.random.randn生成均值为0,方差为1的标准正态分布(高斯分布)随机数,语法同rand() -
import numpy as np randn1 = np.random.randn(5) #一行五列 randn2 = np.random.randn(2, 3) # 两行三列 print("一维标准正态分布随机数:", randn1) print("\n二维标准正态分布随机数:\n", randn2) -
np.random.randint(start,stop,shape)生成一个元素范围在start-stop之间,shape形状的array,其实也可以通过random函数变化得到,例如10-100: 90*np.random.random(shape)+10 -
np.random.normal(均值,标准差,样本数量/shape)生成符合指定均值、指定标准差的正态分布随机数,返回 NumPy 数组(ndarray)或单个标量。randn()方法其实就是normal(0,1)的一种特里
多维数组
多维数组本身还是很好理解的,Numpy中稍微有点稀奇的一个是shape形状这个概念,有些函数例如np.ones(shape)需要传入这个来创建数组
arr1 = [1,2,3] # 形状为 3 or (3,) 表示三列,一维数组
arr2 = [[1,2,3]] # 形状为 (1,3) 表示是一个二维数组,目前是一行三列
arr3 = [[[1,2,3]]] # 形状为 (1,1,3) 表示三维数组
来看一下np.ones(shape)创建多维数组,同时,我们可以利用array.shape来获取一个数组的形状
import numpy as np
arr1 = np.ones(4)
arr2 = np.ones((2,4))
# 三维数组 最外层2个元素 每个元素中1个二维元素 每个二维元素中一个长度为4的一维数组
arr3 = np.ones((2,1,4))
print(arr1,arr1.shape)
print("========================")
print(arr2)
print("========================")
print(arr3)
输入如下
[1. 1. 1. 1.] (4,)
========================
[[1. 1. 1. 1.]
[1. 1. 1. 1.]]
========================
[[[1. 1. 1. 1.]]
[[1. 1. 1. 1.]]]
在很多时候,我们会需要进行数字的运算,运算的时候我们可能会需要将一维数组转化为二维或者将二维数组转化为一维数组,这个时候就需要借助reshape(shape),这个方法在传递参数的时候,可以只传入一个维度,剩下的维度传入-1可以函数底层进行计算
import numpy as np
arr1 = np.ones(4) # 一行四列
arr2 = np.ones((2,4)) # 两行四列
arr3 = arr1.reshape((4,1))
print(arr3)
print("========================")
arr4 = arr2.reshape((1,-1)) # 重新塑形 但是我只指塑性为一行,列的参数-1让函数底层自行计算
print(arr4)
输出
[[1.]
[1.]
[1.]
[1.]]
========================
[[1. 1. 1. 1. 1. 1. 1. 1.]]
元素的访问和修改
访问array的元素和访问List元素一样,可以采用下标索引访问,同样支持负数;如果是矩阵/多维,则使用[index1,index2]的方式
import numpy as np
arr = np.array([[1,2,3],[4,5,6]])
print(arr)
print(arr[1,1]) # 5
arr[1,1] = 99
print(arr[1,1]) # 99 其实也可以和别的语言一样使用 arr[index1][index2]的形式
还有一种花式索引,其形式为[ [ index1,index2,index3.... ] ],适合让我们批量挑取元素进行访问
import numpy as np
# 假设这里是一个100长度的一维数组,我们可以利用花式索引批量拿出0,10,15,10位置的元素
arr1 = np.arange(100)
temp = arr1[[0,10,15,10]]
print(temp)
print("=============================")
# 这里有个多维数组
arr2 = np.random.randint(0,10,(5,5))
print(arr2)
print("=============================")
# 批量将(0,0),(1,1),(2,2),(3,3)位置的元素赋值为100
arr2[ [0,1,2,3],[0,1,2,3] ] = 100
print(arr2)
数组的切片
array中也可以和List一样具有切片的特性
import numpy as np
# 假设这里是一个100长度的一维数组,我们可以利用花式索引批量拿出0,10,15,10位置的元素
arr1 = np.random.randint(0,100,10)
# [29 13 15 98 49 54 83 80 56 95]
print(arr1)
print("===========================")
# [13 15 98 49 54 83]
print(arr1[1:-3]) # 拿出1到倒数第三个元素
print("===========================")
# [29 15 49 83 56]
print(arr1[::2]) # 拿出所有元素,步长为2
print("===========================")
arr2 = np.random.randint(0,100,(5,5))
'''
[[93 80 8 97 58]
[25 0 73 54 23] [56 66 34 70 33] [ 4 80 8 6 77] [62 44 33 12 49]]'''
print(arr2)
print("===========================")
print(arr2[2,:]) #提取第二行 其实也可以arr2[2] 只是这么写没那么规范
'''
[[ 0 73 54]
[66 34 70]]'''
print(arr2[1:3,1:-1]) # 切除1到3行;每行为1到倒数第二个元素
利用切片来提取矩阵的列
import numpy as np
arr = np.arange(1,21).reshape((4,5))
# 提取整个第二列 但是需要注意,提取出单个列的话返回的是向量而不是矩阵
print(arr[:,2]) # [ 3 8 13 18]
print(arr[:,2:4]) # 多个列就是矩阵
# 如果想把 arr[:,2]得到的向量转化为矩阵
# 方法一
print("==================")
print(arr[:,2:3])
# 方法二
print("==================")
temp = arr[:,2].reshape((-1,1))
print(temp)
# 方法三
print("==================")
temp2 = arr[:,2].reshape((1,-1))
print(temp2.T) # T可以得到矩阵的倒置,只对矩阵有效
然后对于切片,之前也提到过,在NumPy中切片是数组的视图,而不是一份新数据,所以修改也会影响原来的数组,这个和List的特点不一样,并且赋值也不会创建新的变量(例如 arr2 = arr1,修改arr2也会导致arr1被修改)。如果真的需要拷贝一份新的,则使用copy()方法
数组的变形
数组的变形主要是针对于矩阵,向量是不存在什么变形的说法的,如果一定要说,向量应该也就一个reshape()操作了
-
arr.T矩阵的倒置 -
np.flipud(arr)和np.fliplr(arr)前者为上下翻转(flip-up-down),后者为左右翻转(flip-left-right),向量只能使用flipud(arr)(这里可能会感觉,向量不是一行么,为何只能上下反倒不能左右,其实这个是和数学上一致的,数学上向量默认为列向量) -
np.reshape()不多介绍了,很熟悉,矩阵的重塑 -
np.concatenate(arrays,axis=0),默认按照行进行拼接(行数会增加),axis=1则按照列进行拼接(列数会增加),第一个参数arrays是要参与拼接的所有arr组成的arr -
import numpy as np arr1 = np.arange(1,7).reshape((2,3)) arr2 = np.arange(10,16).reshape((2,3)) print(np.concatenate((arr1,arr2))) print(np.concatenate((arr1,arr2),1)) -
np.split(arr,[index1,index2...])在arr的index1,index2...的索引位置前进行切分 -
import numpy as np arr = np.arange(10,90,10) print(arr) tempA,tempB = np.split(arr,[5]) print(tempA) # [10 20 30 40 50] print(tempB) # [60 70 80] temp1,temp2,temp3 = np.split(arr,[3,6]) print(temp1) # [10 20 30] print(temp2) # [40 50 60] print(temp3) # [70 80]如果是对矩阵进行切分
-
import numpy as np arr = np.arange(10,90,10).reshape(4,2) ''' [[10 20] [30 40] [50 60] [70 80]]''' print(arr) temp1,temp2 = np.split(arr,[2]) # 默认按行,那就从第二行开始切分 ''' [[10 20] [30 40]]''' print(temp1) ''' [[50 60] [70 80]]''' print(temp2) temp1,temp2 = np.split(arr,[1],axis=1) # 按列切分,从第一列开始竖着切分 ''' [[10] [30] [50] [70]]''' print(temp1) ''' [[20] [40] [60] [80]]''' print(temp2)
数组的运算
直接看代码案例,首先是单个矩阵的基本运算,记住一个点所有的元素都会被操作和计算
import numpy as np
baseArr = np.arange(1,13).reshape(3,4)
print(baseArr)
# 加法-作用到每个元素上,每个元素都加
print(baseArr + 10)
# 减法-作用到每个元素上,每个元素都减
print(baseArr - 10)
# 乘法-作用到每个元素上,每个元素都乘
print(baseArr * 10)
# 除法-作用到每个元素上,每个元素都除
print(baseArr / 10)
# 取模-作用到每个元素上,每个元素都取模
print(baseArr % 10)
# 相反数-每个元素都取反
print(-baseArr)
然后是两个矩阵之间的计算
import numpy as np
arr1 = np.arange(10,20).reshape(2,5)
arr2 = np.arange(1,11).reshape(2,5)
# 矩阵间的减法-对应位置的元素相减
print(arr1-arr2)
# 矩阵间的加法-对应位置的元素相加
print(arr1+arr2)
# 矩阵间的乘法-对应位置的元素相乘,也就是逐元素乘积,线性代数中的那种乘法形式之后会有别的函数负责
print(arr1*arr2)
# 矩阵间的除法-对应位置的元素相除
print(arr1/arr2)
# 矩阵间的取模-对应位置的元素相取模
print(arr1%arr2)
上面的都是必须要满足两个矩阵维度相匹配的前提,对于和不同形状的之间的计算,涉及到了广播。有如下两个规则
如果向量和矩阵之间计算,那么向量会被广播成满足矩阵计算要求的行矩阵
如果某个矩阵是行矩阵/列矩阵,则其会被广播从而适配另外一个矩阵
如下案例代码,展示向量和矩阵之间的计算,当一个(x,y)的矩阵和一个向量进行计算的时候,向量必须满足格式即一行y列,该向量会被升级为(1,y)的行距阵,然后再广播为(x,y)的矩阵以适配另外一个矩阵
import numpy as np
arr2 = np.arange(1,7).reshape(2,3)
arr1 = np.array([-100,0,100])
# arr1是向量 需要去主动适配arr2
# 所以arr1在计算的时候就会变为
'''
[
[-100,0,100],
[-100,0,100],
[-100,0,100]
]
'''
# 然后每一个元素和arr2进行运算
print(arr1*arr2)
当一个(x,y)的矩阵和一个列矩阵进行计算的时候,列矩阵必须满足(x,1)即X行1列,该列矩阵会被广播为(x,y)的矩阵从而适配
import numpy as np
arr2 = np.arange(1,7).reshape(3,2)
arr1 = np.array([-100,0,100]).reshape(3,1)
# arr1是向量 需要去主动适配arr2
# 所以arr1在计算的时候就会变为
'''
[
[-100 -100]
[ 0 0] [ 100 100]]
'''
# 然后每一个元素和arr2进行运算
print(arr1*arr2)
输出
[[-100 -200]
[ 0 0]
[ 500 600]]
如果是一个行矩阵(1,y)和一个列矩阵(x,1),双方都会广播为(x,y)的矩阵后进行操作
import numpy as np
arr2 = np.arange(1,4).reshape(1,3)
arr1 = np.array([-100,0,100]).reshape(3,1)
print(arr1)
print(arr2)
print(arr1*arr2)
输出
[[-100]
[ 0]
[ 100]]
[[1 2 3]]
[[-100 -200 -300]
[ 0 0 0]
[ 100 200 300]]
数组的函数
矩阵乘积-np.dot
我们在之前讲的数组的一些运算,可以发现都是一些逐元计算。那么线性代数上的矩阵的乘积怎么实现呢?这里就需要借助np.dot(arr1,arr2)函数了
向量和向量的乘积,就和线代上的向量乘法一致,利用(x,y) * (y,x)然后相加即可
import numpy as np
arr1 = np.arange(1,4)
arr2 = np.arange(11,14)
print(arr1) # [1 2 3 ]
print(arr2) # [11 12 13]
# 1*11+2*12+3*13=74
print(np.dot(arr1,arr2))
同理,如果是向量和矩阵相乘,向量依次和矩阵的每一列每个元素相乘且求和
import numpy as np
arr1 = np.arange(5)
arr2 = np.arange(15).reshape(5,3)
print(arr1)
print(arr2)
'''
按照(1,5)*(5,3)=(1,3)
[[ 0 1 2]
[ 3 4 5] [[0*0+1*3+2*6+3*9+4*12=90] [0 1 2 3 4]* [ 6 7 8] = [0*1+1*4+2*7+3*10+4*13=100]]
[ 9 10 11] [0*2+1*5+2*8+3*11+4*14=110]] [12 13 14]]
'''
print(np.dot(arr1,arr2))
需要知道啊,线代中ab和ba是不一定相等的,所以同理,我们矩阵和向量相乘结果不一定和向量和矩阵相乘是一致的
import numpy as np
arr1 = np.arange(5)
arr2 = np.arange(15).reshape(3,5)
print(arr1)
print(arr2)
print(np.dot(arr2,arr1)) # [30 80 130]
矩阵和矩阵相乘
import numpy as np
arr1 = np.arange(10).reshape((5,2))
arr2 = np.arange(16).reshape(2,8)
print(arr1)
print(arr2)
print(np.dot(arr1,arr2))
数学函数
NumPy中提供了不少数学相关的函数,这里主要来介绍几个
np.abs()求绝对值np.sin();np.cos();np.tan()三角函数相关np.exp(x)求e^xnp.log(x)求ln(x);利用换底公式可以得到log2(x) = np.log(x)/np.log(2);log10(x) = np.log(x)/np.log(10)
聚合函数
矩阵和向量的聚合函数使用方式基本一致,只是向量使用的时候没有axis这个参数,常用的聚合函数有六个
np.max(arr,axis)求最大值,如果axis=0,则结果和行数相等,那么也是取出每列中最大值然后组成新的arr;同理,如果axis=1则结果和列数相等,也就是取出每一行的最大值然后组合;如果不写axis,则返回整个arr中最大的值np.sum(arr,axis)求和,如果axis=0,则对每列求和;axis=1则对每行求和;不写axis则整体求和np.mean()均值函数,axis的规则是一样的,求得是平均值np.std()求标准差,axis规则一样np.nan..()考虑到矩阵/向量中会存在缺失值,如果使用以上聚合函数遇到缺失会报错,那么我们可以在函数名前面加nan使用其安全的版本,计算的时候会自动忽略缺失值
布尔数组
布尔数组在NumPy中主要是通过整型数组/浮点数数组通过比较得方式之后得到得,和常数进行比较或者两个数组之间比较都会产生布尔数组
import numpy as np
arr1 = np.arange(5)
bool_arr = arr1 > 3
print(bool_arr) #[False False False False True]
复杂一点的就是多条件比较,需要注意,在Python中是使用and or not,在NumPy中是使用& | ~
import numpy as np
arr1 = np.arange(1,11)
bool_arr = (arr1 > 3)&(arr1 < 7)
print(bool_arr) # [False False False True True True False False False False]
对于布尔数组而言,主要的应用在于聚合函数np.sum()中传入的是布尔数组的时候,返回True的个数;np.any()只有布尔数组中有一个或者多于一个元素为True则返回True(例如判断两个arr中是否含有相同的元素);np.all()只有当布尔数组中所有的元素都为True的时候才会返回True
布尔数组也可以作为数字数字的掩码,即利用布尔数组的结果来从原本的数组中进行快速的筛选。这个怎么理解呢,看案例代码
import numpy as np
arr1 = np.arange(1,11).reshape(5,2)
print(arr1[(arr1>7)|(arr1<2)]) # [ 1 8 9 10]
# 在两个arr比较筛选出arr2>arr3的部分 和 arr3大于arr2的部分 这种需求下也可以使用
arr2 = np.random.randint(1,100, (3,4))
arr3 = np.random.randint(1,100, (3,4))
print(arr2[ arr2>arr3 ])
print(arr3[ arr3>arr2 ])
arr4 = np.random.randint(300,900,(10,10))
print(np.where( arr4>600 )) # 得到所有大于600的索引位置
print(np.where( arr4>600 )[0]) # 得到大于600的第一索引位置 类似findFirst的效果
print(np.where( arr4==np.max(arr4) )) # 找到最大元素所在索引位置
数组与张量
这部分内容是对于PyTorch的,因为NumPy很流行,PyTorch很大程度上吸收了NumPy的语法,我们可以将np=>torch;数组array=>张量tensor;n维数组/数组的维度=>n阶张量/张量的阶数这三个概念直接进行替换,从而能帮助我们很方便的上手PyTorch
并且张量和数组之间可以直接转换ts = torch.tensor(arr) ; array = np.array(ts)
当然,也没有说完全100%一样,但是不一样的地方是极少数,下图这几个方法是两者的不同之处,其余的方法是一致的
Pandas
Pands是一个标签库,其主要的作用就是在NumPy数组基础上基于其行列标签。Pandas中数组所有的特性依旧存在
一样的,我们在使用Pands得时候需要先导入库
import pandas as pd
对象的创建
一维对象
方法一:借助字典创建
Numpy中我们可以通过np.array(list)将列表转化为数组,在Pandas中我们可以利用pd.Series(dict)将Python的dict字典对象转化为Series对象
可以看到,Series的输出格式和dict的是不一样,而且在最后会告诉你其value的是什么对象类型,当然只是value的类型和Key无关
import pandas as pd
dict_v = {"A":1, "B":2, "C":3}
print(dict_v)
series = pd.Series(dict_v)
print(series)
输出
{'A': 1, 'B': 2, 'C': 3}
A 1
B 2
C 3
dtype: int64
方法二:借助数组创建
考虑到字典的创建本来就比较麻烦,我们可以利用两个数组创建,即pd.Series(values,keys),传入两个参数,第一个为value序列,第二个为Key序列(序列可以是列表,元组,数组,张量都可以)
keys可以省略,省略后key序列将会从0开始递增
import pandas as pd
values = (1,2,3)
keys = ("a","b","c")
series = pd.Series(values,keys)
print(series)
Series对象可以通过.values和.index两个属性分别得到value序列和key序列,需要注意的是,无论构建的时候你传入的是张量还是元组还是列表还是其他的,最后用values得到的都是数组的数据类型
所以可以知道,Pandas其实是基于NumPy创建的库
二维对象
把一维Series对象当作一列,二维对象就是多个一列合在一起,那么就是多列,所以二维对象一定是基于多个Series对象的,每一个Series就是一列数据
创建Series的时候,字典的Key是index,竖直方向延展;当需要创建二维DataFrame对象的时候,肯定还需要水平方向向的键,也就是colums
(1)我们可以利用多个Series创建DataFrame对象pd.DataFrame({key1:series1,key2:series2})
import pandas as pd
value = [1,2,3,4,5,6]
key_age = [27,45,33,36,27,43]
key_name = ["张三","李四","王五","赵六","孙七","周八"]
#s1理解为病人序号和年龄的字典表
s1 = pd.Series(key_age,value)
#s2理解为病人序号和姓名的字典表
s2 = pd.Series(key_name,value)
# 如果要合并成二维,那么就是序号-年龄-姓名
df = pd.DataFrame( {"年龄":s1,"姓名":s2} )
print(s1)
print(s2)
print(df)
输出
1 27
2 45
3 33
4 36
5 27
6 43
dtype: int64
1 张三
2 李四
3 王五
4 赵六
5 孙七
6 周八
dtype: object
年龄 姓名
1 27 张三
2 45 李四
3 33 王五
4 36 赵六
5 27 孙七
6 43 周八
从结果看到,行标签就是每个Series的Key序列,那如果两者的Keys序列不一样会怎么样?那么最终的DataFrame会去两个Key序列的并集,那么必然出现数据缺失,确实的地方会使用NaN
(2)直接利用三个数组创建二维DataFramepd.DataFrame(values,indexs,columns)第一个为value序列(序列中的每个元素就是对应结果表中的每一行的内容),第二个行标签序列,第三个为列标签序列,后两个参数可以不写,会默认从0开始递增作为序列
import pandas as pd
values = [["女",18,"发烧"],["男",19,"咽喉炎"],["未知",29,"腱鞘炎"],["男",35,"急性肠胃炎"],["女",33,"鼻炎"] ]
index = [1,2,3,4,5]
colums = ["性别","年龄","病因"]
df = pd.DataFrame(values, index=index, columns=colums)
print(df)
对于二维DataFrame,因为多了一个columns属性,所以可以通过df.columns获取列标签序列
对象索引
Pandas中的索引分为了显式索引和隐式索引。显式索引是Pandas对象提供的索引,隐式索引是数组本身自带的从0开始的索引。那么如果说,我们此时显式索引也是整型的话,就会导致sr[0]你不知道是按照显式访问还是隐式访问的
所以Pandas作者提供了索引器,可以通过loc访问显式;iloc访问隐式
import pandas as pd
sr = pd.Series(["x", "a", "b", "c"],[2,4,3,1])
print(sr)
# 分别通过显式索引和隐式索引访问元素b
print(sr.loc[3]) # 显式索引 b的key为3 所以用3访问
print(sr.iloc[2])# 隐式索引 b的key2 所以用2访问
当然,如上是基于隐式和显式会冲突的前提下,如果不会冲突,则可以不要使用loc,再者,因为之前也提到了Pandas基于NumPy的,所以花式索引也能使用来着;同样,也具备切片的特点,且切片也只是视图,共享内存地址
在二维对象DataFrame中,即使显式索引和隐式索引从理论上来讲不会冲突,但是你不使用索引器就会报错,所以在使用Pandas的时候我们得习惯使用索引器访问
import pandas as pd
# 字典创建法
i=["1号","2号","3号","4号"]
v1 =[53,64,72,82 ]
v2=["女",'男','男','女']
sr1 =pd.Series(v1,index=i)
sr2 = pd.Series(v2,index=i)
df=pd.DataFrame({"年龄":sr1,"性别":sr2})
# 访问指定元素
print(df.loc["1号","年龄"]) #索引
print(df.loc[["1号","3号"],["年龄","性别"]])#花式索引
print(df.loc["1号":"4号","性别"])#切片
print(df.loc[:,"年龄"])#切片 获取第一列
提取某一行或者某一列的时候,可以简写df.loc["3号"];df["年龄"]
对象变形
有时候的数据是畸形的,比如说行是特性,列是个体,那么我们必须要进行转置,此时使用df.T
import pandas as pd
# 先构建畸形数据
arr1 = ["1号", "2号", "3号", "4号"]
arr2 = ["年龄", "性别"]
arr3 = [[19,20,21,22],["男","女","男","女"]]
'''
1号 2号 3号 4号
年龄 19 20 21 22
性别 男 女 男 女
'''
df = pd.DataFrame(arr3, index=arr2, columns=arr1)
print(df)
print("===================")
'''
年龄 性别
1号 19 男
2号 20 女
3号 21 男
4号 22 女
'''
print(df.T)
除此之外,我们还可能会需要交换两行/两列的顺序,即上下翻转或者左右翻转,这个需要借助切片中step=-1即倒着采样的特性
import pandas as pd
arr1 = ["1号", "2号", "3号", "4号"]
arr2 = ["年龄", "性别"]
arr3 = [[19,20,21,22],["男","女","男","女"]]
df = pd.DataFrame(arr3, index=arr2, columns=arr1)
print(df)
'''
年龄 性别
1号 19 男
2号 20 女
3号 21 男
4号 22 女
'''
print(df.T)
df = df.T
print("========接着上面的案例===========")
print(df.iloc[:,::-1])#左右翻转 即性别和年龄列翻过来
print(df.iloc[::-1,:])#上下翻转 1234变为4321
对象的重塑
因为对象比数组多了行列标签,所以原本的reshape()方法其实已经不适用。Pandas对象的重塑没那么灵活,但是在实际场景中用的也少。
我们可以直接通过df[newKey]=newSr的方式,给当前df对象直接新增一列,列的标签为newKey,列下面的values即newSr;通过df.loc["一号"]=[...]的方式新增一行
我们还可以进行Serise对象的相加,使用pd.concat([sr1,sr2])
结果的key为两个key的并集,并且由于Pandas舍弃了字典和集合的不可重复性,所以concat后的结果是会有重复的
可以看到,这个方法是默认在行的基础上进行合并的(增加行数),如果需要在列的基础上进行合并(增加列数),则使用pd.concat([sr1,sr2],axis=1)
如果需要不重复性,可以利用.index.is_unique或.colomns.is_unique返回是否为True来判断,如果为True则说明没有重复,如果为False则说明有重复需要去重
对象的计算
如果我们现在需要对某一列的数据全部加10,那么可以直接df[列名]=df[列名]+10,其余计算操作一样的
如果我们需要进行对象和对象之间的计算,例如df1+df2
那么需要保证双方都是数字对象,两者可以维度不同,没有的数据NaN(相加的双方只要有一方没有那行,计算结果就会为NaN),有的数据相互加
并且,NumPy中的数学函数例如np.cos()等都可以直接作用于Pandas的对象中
import numpy as np
import pandas as pd
v1 = [ [10, '女'], [20, '男'], [30, '男'], [40, '女'] ]
v2 = [ 1, 2 ,3, 6 ]
i1 = [ '1 号', '2 号', '3 号', '4 号' ]; c1 = [ '年龄', '性别' ]
i2 = [ '1 号', '2 号', '3 号', '6 号' ]; c2 = [ '牌照' ]
df1 = pd.DataFrame( v1, index=i1, columns=c1 )
df2 = pd.DataFrame( v2, index=i2, columns=c2 )
df1['年龄'] = np.log(df1['年龄'])
print(df1)
缺失值
Serise和DataFrame对象都能使用.isnull()方法检查是否为缺失值,如果要检测是否不为缺失值,可以使用.notnull() 或者 ~(.isnull())取反操作
对于Serise对象,我们可以直接使用.dropna()方法去除缺失值,对于DataFrame对象,要么去除含有NaN的那一行或者那一列,使用df.dropna()默认是剔除那一行,如果需要去除那一列,需要指定axis参数df.dropna(axis="columns"),一般我们都是去一行,因为一行才是个体,列是特征
当然,如果因为一个就删除一行可能过于激进,我们可以使用df.dropna(how='all')这种方式,只有当这一行/列全部为NaN的时候才会被去除
如果需要填充缺失值,可以使用sr.fillna(x)和df.fillna(x),会使用X将NaN进行填充;
搭配np的一些聚合函数,例如.fillna(np.mean(sr/df))则可以实现均值填充;.fillna(method='ffill')则可以实现用上一值/前值进行填充.fillna(method='bfill')则可以实现用下一值/后值进行填充
操作Excel/CSV和数据分析
我们一般还是使用CSV,需要注意的是,如果你的文档数据里面没有行列标签,那么你需要手动补上行标签和列标签
当我们需要导入CSV的时候,可以使用pd.read_csv(path,index_col=0),第二个参数啥意思暂且不需要在意,需要注意的是,Pandas可能会要求CSV文件为utf-8的格式才能解析
当我们读取到的CSV比较大,得到df对象也比较大,我们可以使用df.head()只打印前五行的内容,然后就能用NumPy的方法,因为Pandas将其变为了对象的方法,所以也可以直接用df对象直接调用一些聚合函数,从而不需要导入Numpy包,且这些函数都属于安全版本,即都会忽略NaN
可以看到,如果每次数据分析我们都需要来回调用df.max;df.min;df.mean这些聚合函数,显得十分繁琐和冗余,所以有一个.describe()方法能直接查看所有相关的聚合信息,可以看到这个函数会忽略字符串列(因为字符串没有计算意义)
数据透视
数据透视对于数据分析极其的重要
如下 我们这里有一个泰坦尼克号的人员信息和生还信息名单,我现在需要考察前面的其他特征 和 考察核心即是否生还 之间的关系(这个可以类比神经网络,是否生还即为输出,前面的为输入)
对上述进行数据透视,需要借助.pivot_table方法,如下,传入两个参数第一个为考察核心,第二个为参考特征,整个方法就是考察性别如何影响生还率的,看到输出知道,女性生还率70%+
除此之外,还可以指定一个columns参数作为第二个参考特征
在这个函数的入参中,有一个aggfunc参数,默认为mean,除此之外,所有的聚合函数都可以作为其入参,会影响其计算,例如我们可以df.pivot_table('是否生还',index='性别',aggfunc='max'),当然,在我们目前的案例中,mean平均值是最佳选择
多特征的数据透视
上述的操作我们只引用了性别和船舱两个参考特征,如果我们还需要引入更多的特征,例如表格中的年龄和费用。
可以观察到,这两个列的数据都十分的分散,如果需要加入分类,会导致dt对象横向和纵向索引都会很大,那么这个时候就不能再按类别分了,此时需要借助pd.cut()和pd.qcut()
例如年龄,我们可以不一岁一岁的进行分类,而是按照范围进行分
age = pd.cut(df['年龄'],[0,18,120])
上述代码利用cut方法就将年龄分为了0-18;18-120两个区间,此时我们再将其加入到pivot_table函数中进行计算
我们还可以加入费用,可以看到,pd.cut可以让我们自己决定如何分隔数据,pd.qcut()只需要给出希望分成多少份,函数能自己帮我们分
Matplotlib
简单使用
Matplotlib本身库比较大,我们一般也只需要用到核心模块matplotlib.pyplot,
import matplotlib.pyplot as plt
我们先来尝试一下最简单的画图,这里使用.plot(x,y)函数
import matplotlib.pyplot as plt
fig1 = plt.figure() #创建新图窗
x = [1,2,3,4,5] #数据的X值
y = [1,4,9,16,25] #数据的Y值
plt.plot(x,y) #.plot(x,y)函数,先描点再画线
plt.show()

可以看到这个图虽然出来了,但是不是很高清,我们可以通过添加另外一个库让图片更加清晰
import matplotlib.pyplot as plt
from matplotlib_inline import backend_inline
backend_inline.set_matplotlib_formats('svg')
fig1 = plt.figure()
x = [1,2,3,4,5]
y = [1,4,9,16,25]
plt.plot(x,y)
plt.show()
保存图片
保存图片需要使用.savefig(),入参里面填入要保存的绝对路径和图形名以及后缀,需要注意这里要有个r字符串,即r'绝对路径\图形名.后缀 ,这个后缀,为了保存清晰的图片,我们基本都推荐保存svg矢量格式的图片
import matplotlib.pyplot as plt
# 移除 matplotlib_inline 相关代码,因为它们仅适用于 Jupyter 环境
fig1 = plt.figure()
x = [1,2,3,4,5]
y = [1,4,9,16,25]
plt.plot(x,y)
plt.show()
fig1.savefig('fig1.png')
两种画图方式
Matplotlib中有两种画图方式:Matlab方式和面向对象的方式,我们上面展示的是前者
两种方式都可以完成需求并且大部分时候两者能互相转换,一般建议自己使用Matlab,但是面向对象的方式也要知道,遇到别人的工程这么写的时候能知道
如下对比知道,面向对象的方式先调用了一次axes()方法,fig是整个窗口,axes是窗口内画图的部分,也就是坐标轴
图窗与坐标轴
图形窗口(figure)在 Matlab 中会单独弹出,该窗口中可容纳元素,也可以是空的窗口。画图之前记得手动创建一下Fig对象,虽然例如使用Jupyter中不一定看得到,但是如果有保存图片的需求,这个Fig对象是必不可少的
坐标轴(axes)是一个矩形,其下方是 x 轴的数值与刻度,左侧是 y 轴的数值与刻度。
多图形的绘制
如上图,我们只是划了一条线,我们想要在一个窗口内画多个该如何做?很简单,就是利用plt对象多调用几次plot函数即可
import matplotlib.pyplot as plt
fig1 = plt.figure()
x = [ 1, 2, 3, 4, 5 ] # 数据的 x 值
y1 = [ 1, 2, 3, 4, 5 ] # 数据的 y1 值
y2 = [ 0, 0, 0, 0, 0 ] # 数据的 y2 值
y3 = [ -1, -2, -3, -4, -5 ] # 数据的 y3 值
# 使用Matlab的方式绘制
plt.plot(x,y1)
plt.plot(x,y2)
plt.plot(x,y3)
plt.show()
# 使用面向对象的方式绘制
ax1 = plt.axes()
ax1.plot(x,y1)
ax1.plot(x,y2)
ax1.plot(x,y3)

绘制多个子图
在绘制多个子图上,两种方式可能区别会大一点
首先,两种方式都是借助plt.subplot()方法
可以看到,使用Matlab的方式绘制,就是依次交替使用subplot()方法和plot()方法,且需要传入三个参数 ;
而使用面向对象的方式,需要向plt.subplot(x) 传入一个参数x代表需要几个子图,然后会返回Fig对象和ax数组对象,需要注意,这里会返回Fig对象,就不需要再次使用plt.figure()了,然后因为ax反参是一个数组,数组的长度就是子图的个数,代表每个坐标系,所以我们只需要遍历并且都调用plot方法即可,我们之后都可以利用plt.subplot()代替plt.figure()和plt.axes()
import matplotlib.pyplot as plt
fig1 = plt.figure()
x = [ 1, 2, 3, 4, 5 ] # 数据的 x 值
y1 = [ 1, 2, 3, 4, 5 ] # 数据的 y1 值
y2 = [ 0, 0, 0, 0, 0 ] # 数据的 y2 值
y3 = [ -1, -2, -3, -4, -5 ] # 数据的 y3 值
# 使用Matlab的方式绘制
# 传入三个参数 分别代表 需要 3行1列个子图,当前是子图的第一个
plt.subplot(3,1,1)
plt.plot(x,y1)
plt.subplot(3,1,2)
plt.plot(x,y2)
plt.subplot(3,1,3)
plt.plot(x,y3)
# 使用面向对象的方式绘制
fig2,ax2 = plt.subplot(3)
ax2[0].plot(x,y1)
ax2[1].plot(x,y2)
ax2[2].plot(x,y3)

图表类型
plt提供了五类基本的图表类型二维图;网络图;统计图;轮廓图;三维图
我们这里只看深度学习可能会要用到的,如果遇到了不会的,可以直接去官方现场学
Plot types — Matplotlib 3.10.8 documentation
作者也还准备了许多提前画好的图,我们可以借鉴和学习Examples — Matplotlib 3.10.8 documentation
二维图
二维图,只需要给定两个向量即可画图,其中线型图其实可以代替其余大部分的二维图,画对应图就使用名字旁对应的函数,我们这里只会介绍线型图Plot
我们这里只学习plot()
首先对于plot()的基本使用我们已经了解,最基本的需要两个入参plot(x,y)代表横坐标和纵坐标
如果需要使用颜色,可以添加入参color,例如plt.plot(x,y,color="#7CB5EC"),需要注意,这个color是十六进制的方式进行获取和输入,可以通过其余网站或者QQ截图的方式获取
#绘制不同颜色的二维图
import matplotlib.pyplot as plt
fig1 = plt.figure()
x = [ 1, 2, 3, 4, 5 ] # 数据的 x 值
y1 = [ 0, 1, 2, 3, 4 ] # 数据的 y1 值
y2 = [ 1, 2, 3, 4, 5 ] # 数据的 y2 值
y3 = [ 2, 3, 4, 5, 6 ] # 数据的 y3 值
y4 = [ 3, 4, 5, 6, 7 ] # 数据的 y4 值
y5 = [ 4, 5, 6, 7, 8 ] # 数据的 y5 值
plt.plot(x, y1, color='#7CB5EC')
plt.plot(x, y2, color='#F7A35C')
plt.plot(x, y3, color='#A2A2D0')
plt.plot(x, y4, color='#F6675D')
plt.plot(x, y5, color='#47ADC7')
plt.show()
# 使用面向对象的方式
Fig2, ax2 = plt.subplots()
ax2.plot(x, y1, color='#7CB5EC')
ax2.plot(x, y2, color='#F7A35C')
ax2.plot(x, y3, color='#A2A2D0')
ax2.plot(x, y4, color='#F6675D')
ax2.plot(x, y5, color='#47ADC7')

如果需要设置线条样式,可以使用linestyle入参,例如plt.plot(x, y1, linestyle='-'),这个参数默认就是-表示实线,除此之外我们一般还可以设置:表示点线, -.表示虚线,--表示虚线, 表示隐藏该线条等等
import matplotlib.pyplot as plt
fig1 = plt.figure()
x = [ 1, 2, 3, 4, 5 ] # 数据的 x 值
y1 = [ 0, 1, 2, 3, 4 ] # 数据的 y1 值
y2 = [ 1, 2, 3, 4, 5 ] # 数据的 y2 值
y3 = [ 2, 3, 4, 5, 6 ] # 数据的 y3 值
y4 = [ 3, 4, 5, 6, 7 ] # 数据的 y4 值
y5 = [ 4, 5, 6, 7, 8 ] # 数据的 y5 值
plt.plot(x, y1, linestyle='-')
plt.plot(x, y2, linestyle='--')
plt.plot(x, y3, linestyle='-.')
plt.plot(x, y4, linestyle=':')
plt.plot(x, y5, linestyle=' ')
plt.show()
# 面向对象方式
Fig2, ax2 = plt.subplots()
ax2.plot(x, y1, linestyle='-')
ax2.plot(x, y2, linestyle='--')
ax2.plot(x, y3, linestyle='-.')
ax2.plot(x, y4, linestyle=':')
ax2.plot(x, y5, linestyle=' ')

如果需要设置粗细,可以使用linewidth,例如plt.plot(x, y1, linewidth=0.5),一般我们设置0.5-3的范围就差不多了
如果需要标记节点,可以使用marker,例如plt.plot(x, y1, marker='.'),如果还需要修改标记的大小,可以使用markersize,我们一般设置为3-9的大小差不多
那么综上,我们可以一起用起来,如下图所示
此时可以关注一下线条五,可以看到线条五linestyle=''并且marker='o' ,这个就是隐藏线条的最大作用,可以模拟出二维图中的散点图即plt.scatter()并且效率更高,scatter是一个一个点单独渲染,plot是整体渲染且格式整齐
还可以将上述属性综合起来写在第三个参数上,例如
# 如下第三个参数bs- b表示颜色为blue s表示使用正方形标记节点 -表示线条样式
plt.plot(train_process['epoch'], train_process.val_acc_all, "bs-", label="Val acc")
网格图
网格图我们只介绍imshow()函数,其余两个在深度学习中用处不大
imshow()的功能是绘制图像,可以接受二维数组(灰度图)或者三维数组(彩色图)等将其作为图像数据渲染成可视化图像,在绘制图片、热力图、卷积神经网络的特征图等基于像素 / 数组的可视化中都有应用,执行之后不会有任何窗口弹出,如下案例代码弹窗是show()函数的功能
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0,10,1000)
# 1000 行 ×1 列的彩色竖条热力图,数据基于 `sin(x)*cos(x)` 的周期性波动
l = np.sin(x)*np.cos(x).reshape(-1,1)
fig1 = plt.figure()
plt.imshow(l) # imshow函数负责将l这个二维数组绘画出来
plt.show() # 图片展示功能是show()的作用 如果没有这一行 不会弹窗的
# 面向对象的方式
plt.subplots()

除此之外,所有的网格图都可以配置加一个颜色条,但是这种功能在面向对象的方式中是缺失的,所以我们只能通过Matlab的方式中进行操作
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0,10,1000)
l = np.sin(x)*np.cos(x).reshape(-1,1)
fig1 = plt.figure()
plt.imshow(l)
plt.colorbar()
plt.show()

统计图
在深度学习领域,我们一般只要用到直方图,其余的都是数据分析领域的,我们这里也只学习hist()函数
需要注意的是,条形图bar和直方图hist不要弄混,条型图bar可以被plot替代,hist则是统计学中的函数,为了看清楚分布的均值与标准差
先来看看其简单的使用,传入样本数组,即可进行绘制,如下图,利用NumPy生成符合正态分布的100个样本data,然后将其传入到hist(data)中
import matplotlib.pyplot as plt
import numpy as np
arr = np.random.randn(100)
print(arr)
fig1 = plt.figure()
plt.hist(arr)
plt.show()

可以通过bins参数设置区间划分的数量,默认是10,也就是10条柱,可以修改为20试试plt.hist(data,bins=20)或者ax.hist(data,bins=20)
可以通过alpha参数设置透明度,alpha参数的默认为1,我们可以修改为0.5试试plt.hist(data,alpha=0.5)/ax.hist(data,alpha=0.5)
可以通过histtype设置图表类型,默认为bar,可以从["bar", "barstacked", "step", "stepfilled"]中进行选择,例如plt.hist(data,histtype="step")/ax.hist(data,histtype="step")就是设置为无填充的样式
可以通过color设置直方图颜色,和plot()的颜色设置一样,采用十六进制颜色设置plt.hist(arr,bins=20,alpha=0.5,histtype="step",color="#14C468")设置为绿色
通过edgecolor设置直方图边缘颜色,即两两直方柱之间的边缘部分的颜色。例如plt.hist(data,edgecolor="#191A1C")
图床属性
坐标轴上下限设置
尽管 Matplotlib 会自动调整图窗为最佳的坐标轴上下限,但叛逆的我们知道,很多时候仍需手动设置,才能适应当时的情况。
这里有两种方式lim和axis
lim法
lim法分为了xlim(start,end)和ylim(start,end),分别用于设置X轴和Y轴的上下限,我们可以看到,如下是没有指定的时候,自动给我们配置的XY轴情况
我们自己指定后展示如下,需要注意的是,plt和ax对于这个方法相对调用稍微不太一样plt.xlim()/ax.set_xlim()
import matplotlib.pyplot as plt
fig = plt.figure()
x = [1,2,3,4,5,6]
y = [1,16,81,100,125,180]
plt.plot(x,y)
plt.xlim(1,7)
plt.ylim(1,180)
plt.show()
# 面向对象方式(lim 法)
Fig2, ax2 = plt.subplots()
ax2.plot(x,y)
ax2.set_xlim(1,5)
ax2.set_ylim(1,125)

axis法
axis主要是入参上和lim法不一样,传入一个lenth=4的列表,分别为[x.start , x.end , y.start , y.end ],除此之外,还可以使用axis('equal')让x轴和y轴比例达到1:1
import matplotlib.pyplot as plt
fig = plt.figure()
x = [1,2,3,4,5,6]
y = [1,16,81,100,125,180]
plt.plot(x,y)
plt.axis([1,7,1,180])
plt.show()

标题与轴名称
这里也是Matlab方式和面向对象的方式最后的一个区别
我们会需要给X轴还有Y轴还有整个图像命名,这个时候就需要借助plt.title设置标题;plt.xlabel设置X轴名字;plt.ylabel设置Y轴名字
这里顺便汇总一些Matlab和面向对象的一些方法区别汇总
import matplotlib.pyplot as plt
fig = plt.figure()
x = [1,2,3,4,5,6]
y = [1,16,81,100,125,180]
plt.plot(x,y)
plt.title('Plot Title')
plt.xlabel('X Axis')
plt.ylabel('Y Axis')
plt.show()

额外提一下,面向对象方式统一了五个函数,所以我们可以写成ax.set(xlim=(),ylim(),title="",xlabel="",ylabel="")
图例
图例一般只会出现在二维图和统计图中,网格图一般使用颜色条
图例主要就是使用label参数,如果你想要这个线条有图例就加入label参数,反之如果不想要就不加即可,默认展示在左上角
还有一种就是使用plt.legend()函数,给其传入一个数组,数组一一对应前面你的线条,同时legend函数还有三个常用的关键字参数
第一个 loc设置图例位置,可选upper;center;lower和left;center,right两组中分别选一个,那么就可使用legend(data,loc='upper right'),也可以直接使用legend(data,loc='best')程序自行调整为最佳
第二个 frameon图例边框,默认True,为显示;如果不想要边框则设置为False
第三个 ncol用于设置图例的列数,默认为1即所有图例都一列展示,可以根据需求设置为多列,例如设置为两列ncol=2
网格
可以给图形背景上网格,主要利用plt.grid()函数
该函数有两个参数color设置颜色以及linestyle设置线条格式
import matplotlib.pyplot as plt
fig = plt.figure()
x = [1,2,3,4,5,6]
y = [1,16,81,100,125,180]
plt.plot(x,y)
plt.title('Plot Title')
plt.xlabel('X Axis')
plt.ylabel('Y Axis')
plt.grid(linestyle='--')
plt.show()

文章标题:Python深度学习入门-day02-第三方库篇
文章链接:http://8.154.40.120:24180/?post=59
本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明来自ZealSinger !
如果觉得文章对您有用,请随意打赏。
您的支持是我们继续创作的动力!
微信扫一扫
支付宝扫一扫