第3章
CHAPTER 3


常用的Python工具








本章介绍Python数据类编程中常用的工具包,有相关基础的读者可以跳过本章。

3.1NumPy

NumPy是一款在Python中被广泛使用的开源科学计算库,它的特点是支持高维大型数组的运算,其图标如图31所示。



图31NumPy的图标


在金融领域应用中使用NumPy库进行运算,尤其是当涉及大型矩阵运算等时使用NumPy计算十分方便。除此之外,在图像相关的应用中也可以使用NumPy,输入的图像实际上是一个形状为(H,W,C)的高维数组,其中H、W、C分别为图像的高度、宽度与通道数(也有其他的图像表示形式,例如将通道数放在最前)。




3.1.1NumPy中的数据类型

NumPy中的数据类型众多,与C语言的数据类型较为相近。例如,其中的整型就分为int8、int16、int32、int64及它们对应的无符号形式,而Python中的整型则只有int进行表示,同样对于float也是类似的。

数据类型之间的转换使用np.[类型](待转换数组)或.as_type([类型])。如希望将int64类型的数组a转换为float32,则可以使用np.float32(a)或a.as_type(np.float32)完成。

3.1.2NumPy中数组的使用
1. 创建数组

在NumPy中多维数组被称作ndarray,使用NumPy创建多维数组十分方便,可以通过转换Python中的列表或元组得到,也能直接通过NumPy中的函数创建。NumPy中对数组进行操作的函数所返回的数据都是ndarray。例如要创建一个形状为(2,3,4)一共24个1的ndarray,可以通过以下的两种方式进行创建,代码如下: 


//ch3/test_numpy.py

import numpy as np



#方法1: 通过列表创建数组

a_list = [

[[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]],

[[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]

]



a_np1 = np.array(a_list)

print(a_np1)

#<class 'numpy.ndarray'>

print(type(a_np1))

#<class 'numpy.int32'>

print(type(a_np1[0][0][0]))



#方法2: 通过NumPy函数创建数组

a_np2 = np.ones(shape=[2, 3, 4])

print(a_np2)

#<class 'numpy.ndarray'>

print(type(a_np2))

#<class 'numpy.float64'>

print(type(a_np2[0][0][0]))




使用python test_numpy.py运行程序后,发现通过两种方式创建的数组都可以得到形状为(2, 3, 4)的数组,并且它们的类型都是<class 'numpy.ndarray'>。不同的是,a_np1与a_np2中的元素数据类型分别为<class 'numpy.int32'>与<class 'numpy.float64'>,这是因为a_list中的元素原本是Python中的int型,所以在转换为ndarray时是numpy.int32型。如果将a_list中的元素改为1.或1.0,此时a_np1与a_np2中的元素类型则都是<class 'numpy.float64'>。使用ndarray的astype方法进行类型转换,代码如下: 


a_np2_int = a_np2.astype(np.int32)

#<class 'numpy.int32'>

print(type(a_np2_int[0][0][0]))




NumPy除了提供了创建所有元素为1数组的方法np.ones,相似地,也可以使用np.zeros创建所有元素为0的数组,代码如下: 


b_np = np.zeros([2, 3, 4])

print(b_np)




除了可以用np.ones和np.zeros创建指定形状的数组,也可以使用np.ones_like与np.zeros_like创建和已知数组形状相同的全1或全0数组,其过程相当于先获得目标数组的形状,再使用np.ones或np.zeros进行创建,代码如下: 


#创建和b_np数组形状相同的数组,其中值全为1

one_like_b_np = np.ones_like(b_np)

print(one_like_b_np)

#(2, 3, 4)

print(one_like_b_np.shape)




2. 创建占位符

当不知道数组中的每个元素的具体值时,还可以使用np.empty来创建一个“空”数组作为占位符,这个“空”只是语义上而言的,其实数组中存在随机的数值。NumPy对使用empty方法创建的数组元素随机进行初始化,而后期需要做的是为数组中的元素进行赋值,代码如下: 


#empty1和empty2中的值都是随机初始化的,empty方法实际上是创建了占位符,运行效率高

empty1 = np.empty([2, 3])

print(empty1)

empty2 = np.empty([2, 3, 4])

print(empty2)




3. 数组的属性

所有的ndarray都有ndim、shape、size、dtype等属性,其中ndim用来查看数组的维度个数,如a_np1的形状为(2,3,4),那么它就是一个三维的数组,ndim的值为3,而shape是用来查看数组的形状的,即a_np1.shape是(2, 3, 4); size的意义则说明数组中总的元素个数,即a_np1.size=2×3×4=24,代码如下: 


#3

print(a_np1.ndim)

#(2, 3, 4)

print(a_np1.shape)

#24

print(a_np1.size)




4. 数组的转置

NumPy可以对高维数组进行转置(transpose),转置是指改变数组中元素的排列关系而不改变元素的数量。转置时需要指定axes参数,它指输出的数组的轴顺序与输入数组的轴顺序之间的对应关系。如新建一个形状为(2, 3, 4)的数组a,其在0、1、2轴上的长度分别为2、3、4,使用np.transpose(a, axes=[0, 2, 1])表示将a数组的2轴和1轴进行交换,而原数组的0轴保持不变,得到的新数组的形状则为(2,4,3)。可以这样理解: 原数组a是由2(0轴长度)个3×4(1轴和2轴)的矩阵组成的,0轴保持不变,而只有1轴与2轴进行转置,即两个3×4的矩阵分别进行矩阵转置即为最后的结果,故最终的形状为(2,4,3),具体转置结果的代码如下: 


//ch3/test_numpy.py

b_list = [






[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]],

[[13, 14, 15, 16], [17, 18, 19, 20], [21, 22, 23, 24]]

]



b_np = np.array(b_list)

print(b_np)

#(2, 3, 4)

print(b_np.shape)



b_np_t1 = np.transpose(b_np, axes=[0, 2, 1])

print(b_np_t1)

#(2, 4, 3)

print(b_np_t1.shape)



b_np_t2 = np.transpose(b_np, axes=[1, 0, 2])

print(b_np_t2)

#(3, 2, 4)

print(b_np_t2.shape)



b_np_t3 = np.transpose(b_np, axes=[1, 2, 0])

print(b_np_t3)

#(3, 4, 2)

print(b_np_t3.shape)



b_np_t4 = np.transpose(b_np, axes=[2, 0, 1])

print(b_np_t4)

#(4, 2, 3)

print(b_np_t4.shape)



b_np_t5 = np.transpose(b_np, axes=[2, 1, 0])

print(b_np_t5)

#(4, 3, 2)

print(b_np_t5.shape)




5. 数组的变形

NumPy支持对数组进行变形(reshape),变形和3.1.2节第4部分的转置一样,都能改变数组的形状,但是转置改变的是数组元素的排列,而变形改变的是数组元素的分组。换言之,转置前后数组元素的顺序会发生改变,而变形操作不会改变元素之间的顺序关系。比较转置操作和变形操作的异同的代码如下: 


//ch3/test_numpy.py

b_np_transpose = np.transpose(b_np, axes=[0, 2, 1])

print(b_np_transpose)

#(2, 4, 3)

print(b_np_transpose.shape)



b_np_reshape = np.reshape(b_np, newshape=[2, 4, 3])

print(b_np_reshape)






#(2, 4, 3)

print(b_np_reshape.shape)




从结果可以看出,b_np_transpose和b_np_reshape的形状都是(2, 3, 4),而数组内部元素的排列不同,b_np_transpose与原数组b_np中元素的排列顺序不同,而b_np_reshape的元素排列与原数组相同。

在实际操作中,常需要将数组变形为只有一行或者一列,此时将newshape指定为[1,-1]或[-1,1]即可,-1表示让程序自动求解该维度的长度。np.squeeze也是一个常用的函数,它可以对数组中长度为1的维度进行压缩,其本质也是reshape。

6. 数组的切分与合并

NumPy可以将大数组拆分为若干小数组,同时也能将若干小数组合并为一个大数组,切分通常使用split方法,而根据不同的需求,通常会使用stack或者concatenate方法进行数组的合并,下面分别介绍这几种方法的应用。

当使用split将大数组切分为小数组时,需要指定切分点的下标或切分的数量(indices_or_sections)及在哪个维度上切分(axis)。当指定切分下标时,需要为indices_or_sections参数传入一个切分下标的列表(list); 当指定切分数量时,为indices_or_sections参数传入一个整数k即可,表示需要将待切分数组沿指定轴平均切分为k部分,若指定的k无法完成均分,则此时split方法会抛出ValueError,分割的结果为含有若干分割结果ndarray的列表。如果需要非均等切分,读者则可以参考array_split方法,该方法在此不进行介绍。演示split方法的不同使用场景与方法的代码如下: 


//ch3/test_numpy.py

to_split_arr = np.arange(12).reshape(3, 4)



'''

[[ 0 1 2 3]

[ 4 5 6 7]

[ 8 9 10 11]]

'''

print(to_split_arr)

#形状为(3, 4)

print(to_split_arr.shape)



#[array([[0, 1, 2, 3]]), array([[4, 5, 6, 7]]), array([[ 8, 9, 10, 11]])]

axis_0_split_3_equal_parts = np.split(to_split_arr, 3)

print(axis_0_split_3_equal_parts)



'''

[array([[0, 1],

[4, 5],

[8, 9]]), 

array([[2, 3],

[6, 7],






[10, 11]])]

'''

axis_1_split_2_equal_parts = np.split(to_split_arr, 2, axis=1)

print(axis_1_split_2_equal_parts)



#ValueError,因为轴0的长度为3,所以无法被均分为2份

axis_0_split_2_equal_parts = np.split(to_split_arr, 2)



'''

[array([[0, 1, 2, 3],

[4, 5, 6, 7]]), 

array([[8, 9, 10, 11]])]

'''

axis_0_split_indices = np.split(to_split_arr, [2, ])

print(axis_0_split_indices)



'''

[array([[ 0, 1, 2],

[ 4, 5, 6],

[ 8, 9, 10]]), 

array([[ 3],

[ 7],

[11]])]

'''

axis_1_split_indices = np.split(to_split_arr, [3, ], axis=1)

print(axis_1_split_indices)




运行以上程序,从控制台打印的结果可以看出axis_0_split_3_equal_parts与axis_1_split_2_equal_parts分别将原数组在轴0(长度为3)和轴1(长度为4)平均切分为3份和2份,此时为split的indices_or_sections传入的值分别为3和2,代表需要切分的数量,而当尝试在0轴上切分为两部分时,程序会报错。当为split的indices_or_sections传入的值为[2,]和[3,]时会分别得到axis_0_split_indices和axis_1_split_indices,前者表示将原数组在0轴上分为两部分,第一部分是0轴下标小于2的部分,第二部分是下标大于或等于2的部分,即分为to_split_arr[:2,:]和to_split_arr[2:,:]; 类似地,axis_1_split_indices表示将原数组在1轴上分为两部分,分别为to_split_arr[:,:3]和to_split_arr[:,3:]。

在NumPy中合并数组通常有两种方式: stack和concatenate,两者有很多相似之处,也有明显的区别。这两个函数都需要传入待合并的数组列表及指定在哪个轴上进行合并; 区别是stack会为合并的数组新建一个轴,而concatenate直接在原始数组的轴上进行合并。假设现在需要对两个形状都为(3,4)的数组进行合并,使用stack函数在2轴进行合并时,由于原始数组只有0轴和1轴,并没有2轴,因此stack函数会为新数组新建一个2轴,得到的数组形状为(3,4,2),而如果使用concatenate在1轴上合并,则得到的新数组的形状为(3,4+4),即(3,8)。这两个函数在合并数组时的异同的代码如下: 


//ch3/test_numpy.py

#新建两个形状为(3, 4)的待合并数组






merge_arr1 = np.arange(12).reshape(3, 4)

merge_arr2 = np.arange(12, 24). reshape(3, 4)



print(merge_arr1)

print(merge_arr2)



#stack为新数组新建一个轴2

stack_arr1 = np.stack([merge_arr1, merge_arr2], axis=2)

print(stack_arr1)

#(3, 4, 2)

print(stack_arr1.shape)



#stack为新数组新建一个轴1,原始的轴1变为轴2

stack_arr2 = np.stack([merge_arr1, merge_arr2], axis=1)

print(stack_arr2)

#(3, 2, 4)

print(stack_arr2.shape)



#新数组在原始轴1上进行连接

concat_arr1 = np.concatenate([merge_arr1, merge_arr2], axis=1)

print(concat_arr1)

#(3, 8)

print(concat_arr1.shape)



#新数组在原始轴0上进行连接

concat_arr2 = np.concatenate([merge_arr1, merge_arr2], axis=0)

print(concat_arr2)

#(6, 4)

print(concat_arr2.shape)




运行以上程序可以得知,stack会在axis参数指定的轴上新建一个轴,改变合并后数组的维度,而concatenate函数仅会在原始数组的某一axis上进行合并,不会产生新的轴。

本书仅对NumPy基本的用法进行介绍,有兴趣了解其更多强大功能与用法的读者可以到NumPy的官方网站进一步学习。

3.2Matplotlib

Matplotlib是一个强大的Python开源图形库,其图标如图32所示。可以使用它轻松地绘制各种图形或者对数据进行可视化,如函数图、直方图、饼图等。Matplotlib常常和NumPy配合使用,NumPy提供绘图中的数据,Matplotlib对数据进行可视化。




图32Matplotlib的图标



3.2.1Matplotlib中的相关概念

在Matplotlib中,绘图主要通过两种方式,一种方式是使用pyplot直接绘图,使用方法较为简便,但是功能比较受限; 另一种方式则是通过pyplot.subplot返回的fig和axes对象进行绘图,这种方式的灵活性较强,本节主要对后者进行讲解。

首先需要了解Matplotlib中的一些概念,如图33所示。整个承载图像的画布称作Figure,Figure上可以有若干Axes,每个Axes(可以将Axes认为是子图)有自己独立的属性,如Title(标题)、Legend(图例)、图形(各种plot)等。




图33Matplotlib中的各种概念



在实际使用时,首先使用plt.subplot方法创建若干Axes,再依次对每个Axes进行绘图并设置它的Title与Legend等属性,最后使用plt.show或plt.savefig方法对图像进行显示或者保存。

3.2.2使用Matplotlib绘图
1. 绘制函数图像

本节将展示如何使用Matplotlib绘制函数图像,以使用正弦函数为例。首先,定义数据产生接口正弦函数并得到画图数据,接着创建figure与axes对象并使用axes.plot进行图像的绘制,代码如下: 


//ch3/test_matplotlib.py

import matplotlib.pyplot as plt






import numpy as np



#定义数据产生函数

def sin(start, end):

#使用np.linspace产生1000个等间隔的数据

x = np.linspace(start, end, num=1000)

return x, np.sin(x)



start = -10

end = 10



data_x, data_y = sin(start, end)



#得到figure与axes对象,使用subplots默认只生成一个axes

figure, axes = plt.subplots()

axes.plot(data_x, data_y, label='Sin(x)')

#显示plot中定义的label

axes.legend()

#在图中显示网格

axes.grid()

#设置图题

axes.set_title('Plot of Sin(x)')

#显示图像

plt.show()





运行程序可以得到如图34所示的函数图像。




图34sin(x)在[-10,10]上的图像



下面使用plt.subplots绘制多张子图,给plt.subplots方法以行列的形式(如给函数传入(m,n),则表示要画m行n列总共m×n张子图)传入需要绘制的子图数量。绘制2行3列一共6张正弦函数的图像,代码如下: 


//ch3/test_matplotlib.py

row = 2; col = 3

fig, axes = plt.subplots(row, col)

for i in range(row):

for j in range(col):

#以索引的形式取出每个axes

axes[i][j].plot(data_x, data_y, label='Sin(x)')

axes[i][j].set_title('Plot of Sin(x) at [{}, {}]'.format(i, j))

axes[i][j].legend()

#设置总图标题

plt.suptitle('All 2*3 plots')

plt.show()




运行以上程序可以得到如图35所示的图像。




图352×3张sin(x)在[-10,10]上的图像



2. 绘制散点图

当数据是杂乱无章的点时,常常需要绘制散点图以观察其在空间内的分布情况,此时可以使用scatter函数直接进行绘制,其用法与3.2.2节第1部分的plot函数基本类似。在正弦函数值上引入了随机噪声,并使用散点图呈现出来,代码如下: 


//ch3/test_matplotlib.py

#从均值为0、标准差为1的正态分布中引入小的噪声

noise_y = np.random.randn(*data_y.shape) / 2

noise_data_y = data_y + noise_y



figure, axes = plt.subplots()

#使用散点图进行绘制

axes.scatter(data_x, noise_data_y, label='sin(x) with noise scatter')

axes.grid()

axes.legend()

plt.show()




运行程序可以得到如图36所示的结果,从图中能看出引入小噪声后图像整体仍然维持正弦函数的基本形态,以散点图的形式绘制的结果十分直观。




图36引入小噪声后的正弦函数散点图



3. 绘制直方图

当需要查看数据的整体分布情况时,可以绘制直方图进行可视化,其用法与3.2.2节第1部分的plot函数基本类似,仅仅是可视化的图形表现上有所区别。绘制直方图的代码如下: 


//ch3/test_matplotlib.py

#生成10000个正态分布中的数组






norm_data = np.random.normal(size=[10000])

figure, axes = plt.subplots(1, 2)

#将数据分置于10个桶中

axes[0].hist(norm_data, bins=10, label='hist')

axes[0].set_title('Histogram with 10 bins')

axes[0].legend()

#将数据分置于1000个桶中

axes[1].hist(norm_data, bins=1000, label='hist')

axes[1].set_title('Histogram with 1000 bins')

axes[1].legend()

plt.show()




运行程序可以得到如图37所示的结果,可以看到桶的数量越多结果越细腻,越接近正态分布的结果。




图37以不同的区间间隔绘制直方图



4. 绘制条形图

使用Matplotlib绘制条形图十分方便,条形图常常也被称为柱状图,它的图形表现与直方图十分类似,但是条形图常被用于分类数据的可视化,而直方图则主要用于数值型数据的可视化,这就意味着在横轴上条形图的分隔不需要连续并且区间大小可以不相等,而直方图则需要区间连续并且间隔相等。使用bar进行绘图时,需要传入对应的x与y值,使用条形图绘制正弦函数的图像的代码如下: 


figure, axes = plt.subplots()

axes.bar(data_x, data_y, label='bar')

axes.legend()

axes.grid()

plt.show()




运行程序后可以得到如图38所示的结果,可以看出Matplotlib中的条形图可以绘制因变量为负值的图像,此时图像在x轴下方,使用bar绘制的条形图可以认为是散点图中所有的点向x轴作垂线而形成的图形。




图38以条形图的形式绘制正弦函数图像



5. 在同一张图中绘制多个图像

3.2.2节第1部分至第4部分都仅绘制了单个图像,本节将说明如何在同一张图中绘制多个图像。Matplotlib会维护一个当前处于活动状态的画布,此时直接在画布上使用绘图函数进行绘制即可,直到程序运行到plt.show显示图像时才会将整个画布清除,在一张图中同时绘制正弦函数曲线图与散点图,代码如下: 


//ch3/test_matplotlib.py

figure, axes = plt.subplots()

#绘制曲线图

axes.plot(data_x, data_y, label='Sin(x)', linewidth=5)

#绘制散点图,此时axes对象仍处于活动状态,直接绘制即可

axes.scatter(data_x, noise_data_y, label='scatter noise data', color='yellow')

axes.legend()

axes.grid()

plt.show()




程序的运行结果如图39所示,由于绘图函数默认使用蓝色,所以在绘制曲线图与散点图时本书额外使用linewidth与color参数指定线条和点的颜色与宽度,以便读者能看清图像。




图39在同一张图中同时绘制曲线图与散点图



6. 动态绘制图像

前面所绘制的图像都是静态图像,当数据随时间变化时,静态图像则不能表现出数据的变化规律,因此本部分将说明如何使用Matplotlib绘制实时的动态图像。首先需要对用到的函数进行一些说明。

(1) plt.ion(): 用于开启Matplotlib中的交互模式(interactive),开启交互模式后,只要程序遇到绘图指令,如plot、scatter等,就会直接显示绘图结果,而不需要再调用plt.show进行显示。

(2) plt.cla(): 表示清除当前活动的axes对象,清除后需要重新绘图以得到结果。相似的指令还有plt.clf(),这个函数表示清除当前figure对象。

(3) plt.pause(time): 延迟函数,由于交互模式下显示的图像会立即关闭,无法看清,所以需要使用plt.pause函数使绘制的图像暂停,以便观察。传入的参数time用于延迟时间,单位为秒。

(4) plt.ioff(): 表示退出交互模式。在绘图完成之后调用。


下面的代码定义了一个带系数的正弦函数,以传入不同的系数来模拟产生和时间相关的数据,并在交互模式下实时显示不同系数的正弦函数图像的变化情况,代码如下: 


//ch3/test_matplotlib.py

figure, axes = plt.subplots()

#定义时间的长度






num = 100



#定义带系数的正弦函数,以模拟不同时刻的数据

def sin_with_effi(start, end, effi):

x = np.linspace(start, end, num=1000)

return x, np.sin(effi * x)



#打开Matplotlib的交互绘图模式

plt.ion()



#对时间长度进行循环

for i in range(num):

#清除上一次绘图结果

plt.cla()

#取出当前时刻的数据

data_x, data_y = sin_with_effi(start, end, effi=i / 10)

axes.plot(data_x, data_y)

#暂停图像以显示最新结果

plt.pause(0.001)



#关闭交互模式

plt.ioff()

#显示最终结果

plt.show()




运行以上程序可以得到如图310所示的结果,可以看到随着时间的变化,由于正弦函数的系数越来越大,因此函数图像越来越紧密,能以十分直观的形式观察函数图像的变化情况。


7. 显示图像

Matplotlib也可以用于显示图像,代码如下: 


img_path = 'matplotlib_logo.png'

#读取图像

img = plt.imread(img_path)

#显示图像

plt.imshow(img)




运行程序后,能看到如图311所示的结果(前提是当前文件夹下有matplotlib_logo.png这张图像)。如果需要显示非PNG格式的图像,则需要使用pip install pillow命令额外安装pillow库以获得对更多图像的支持。



plt.imshow也能以热力图的形式显示矩阵,随机初始化形状为(256, 256)矩阵并使用Matplotlib进行显示的代码如下: 


//ch3/test_matplotlib.py

row = col = 256

#定义一个空的占位符

heatmap = np.empty(shape=[row, col])






图310在同一张图中同时绘制曲线图与散点图




图311使用Matplotlib显示图像





#初始化占位符中的每个像素

for i in range(row):

for j in range(col):

heatmap[i][j] = np.random.rand() * i + j

#imshow将输入的图像进行归一化并映射至0~255,较小值使用深色表示,较大值使用浅色表示

plt.imshow(heatmap)

plt.show()




运行程序后,得到如图312所示的结果,能看出图像从左上角到右下角的颜色逐渐变亮,说明其值从左上角到右下角逐渐增大,这符合代码中所写的矩阵初始化逻辑。




图312使用Matplotlib显示热力图



除了本节所展示的绘图方式与绘图类型,Matplotlib还有许多更广泛的应用。关于其更多用法可以查看Matplotlib官网。

3.3Pandas

Pandas是一个用于数据分析的开源Python库,其图标如图313所示。使用Pandas能高效地读取和处理类表格格式的数据,例如CSV、Excel、SQL数据等。




图313Pandas的图标



3.3.1Pandas中的数据结构

在Pandas中,有两种核心的数据结构,其中一维的数据称为Series,本书在此不对Series进行过多讲解,二维的数据结构被称作DataFrame,其结构如图314所示。从图中可以看出DataFrame是一种类表格的数据结构,其包含row和column,DataFrame中的每个row或者column都是一个Series。




图314DataFrame示意图



3.3.2使用Pandas读取数据
1. 读取CSV文件

CSV是逗号分隔值文件,使用纯文本来存储表格数据,前面讲过Pandas适用于读取类表格格式的数据,因此本节将展示如何使用Pandas便捷地读取CSV文件。

首先,打开记事本,在其中输入如图315所示的数据,并将其保存为num_csv.csv(也可以不更改文件扩展名,文件内的数据使用逗号分隔即可)。


接下来,使用pandas.read_csv方法进行读取即可,代码如下: 


import pandas as pd



file_name = 'num_csv.csv'

csv_file = pd.read_csv(file_name)

print(csv_file)




读取后的结果如图316所示,可以看到结果中最左边一列的0和1代表行号,这说明函数认为CSV文件中只有两行数据,而第1行的first~fifth不算作数据,仅算作表头。



图315用于测试的CSV数据




图316读取CSV数据的结果




如果需要将第1行作为数据考虑,或者需要读取的CSV文件没有表头,则需要在read_csv方法中指定header=None,即数据中不存在表头行,读取CSV文件的结果如图317所示,代码如下: 


csv_file_wo_header = pd.read_csv(file_name, header=None)

print(csv_file_wo_header)





图317读取不带header的CSV数据



从结果可以看出,指定header=None后,左侧行号变为从0~2,一共3列,原本的表头被认为是第1行数据。除此之外程序自动给数据加上了表头,以数字进行标识。




如果只想取出数据部分,而不需要表头与行号信息,则可以使用DataFrame的values属性进行获取,代码如下: 


csv_file_values = pd.read_csv(file_name).values

print(csv_file_values)




运行结果的csv_file_values类型是NumPy的ndarray,取出数据后可以进一步使用NumPy中的方法对其进行处理。

2. 读取Excel文件

Excel是人们日常生活中最常用的软件之一,Pandas对于Excel文件的读取也提供了十分方便的接口。和CSV文件不同,由于Excel文件中可以存在多张表(Sheet),在读取Excel文件时需要指定读取的表,使用Pandas读取Excel文件的代码如下: 


file_name = 'num_excel.xlsx'

#可以通过Sheet名或者Sheet的索引进行访问('Sheet1' == 0,'Sheet2' == 1)

excel_file = pd.read_excel(file_name, 0)

print(excel_file)




3. 读取JSON文件

Pandas同样可以读取JSON数据,与3.8节中将要提到的JSON模块不同,Pandas读取的JSON数据仍然是DataFrame的形式,所读取的JSON文件可以分为4种格式(orient),第1种是split,其表示将DataFrame中的行号、列号及内容分开存储。JSON中 index的数据为DataFrame中的最左一列的行号索引,以columns的数据为DataFrame中的表头名,以data的数据为DataFrame中的内容; 剩余3种分别是index、records及table,这3种格式在此不进行讲解,详细内容可以参考Pandas官网。 

新建一个num_json.json文件,其内容如图318所示。



图318新建JSON数据示例



读取num_json.json文件中的数据的代码如下: 


file_name = 'num_json.json'

#index -> [index], columns -> [columns], data -> [values]

json_file = pd.read_json(file_name, orient='split')

print(json_file)




运行程序后,可以得到如图319所示的结果。




图319读取JSON文件



3.3.3使用Pandas处理数据

使用Pandas读取数据后,可以使用NumPy中的方法处理DataFrame中的values,也可以直接使用Pandas对DataFrame进行处理,本节将展示几种常见的数据处理方式。

1. 取行数据

本节将说明如何取出DataFrame中的一行。使用DataFrame的loc属性并传入行名称取出对应行,或者使用DataFrame的iloc属性,此时需要传入的是行索引以取出对应行。取出csv_file中的第1行(索引为0)的代码如下: 


print(csv_file.iloc[0])




运行以上程序后,可以得到如图320所示的结果。可以看出其确实取出了DataFrame中的第1行,并且其结果同时输出了列名、行名和数据类型等信息。




2. 取列数据

本节说明如何取出DataFrame中的一列,同样可以使用loc属性和iloc属性,此时对loc的索引格式为loc[:, <column_name>],需要给loc传入两个索引值,前一个表示行索引,后一个则表示列索引。除此之外,还可以直接使用DataFrame[<column_name>]的形式取出列数据,这两种方法的代码如下: 


#方法1

print(csv_file['first'])

#方法2

print(csv_file.loc[:, 'first'])




运行程序后,可以得到如图321所示的结果。



图320取出DataFrame中的行数据




图321取出DataFrame中的列数据




3. 求数据的统计信息

使用Pandas可以方便地得到DataFrame的统计信息,如最大值、最小值、平均值等,下面的程序分别展示了如何取出DataFrame中行的最大值、DataFrame中列的最小值及整个DataFrame中的平均值,代码如下: 


#axis=1表示对行进行操作

print(csv_file.max(axis=1))

#axis=0表示对列进行操作,默认axis为0

print(csv_file.min(axis=0))

#先取出列的平均值,接着求一次列均值的均值即为整个DataFrame的均值

print(csv_file.mean().mean())





图322DataFrame中的

统计信息


运行程序后可以得到如图322所示的结果,能看到0行和1行的最大值分别为5和10,first到fifth这5列的最小值分别为1、2、3、4、5,而整个DataFrame的均值为5.5。

4. 处理缺失值

在数据集中,常常存在缺失数据,对于缺失数据的处理通常有两种方法,一种是直接将含有缺失数据的记录删除; 另一种是将特征值填入缺失位置,通常会在该位置填入该列的平均值或0,或者直接填入字段以示此处空缺/无效。找到DataFrame中的缺失数据并进一步进行处理的代码如下: 


//ch3/test_pandas.py

#插入一条所有数据为NaN的记录

csv_file_with_na = csv_file.reindex([0, 1, 2])

print(csv_file_with_na)

#查看NaN在DataFrame中的位置

print(csv_file_with_na.isna())

#使用每列的平均值填入该列所有NaN的位置

print(csv_file_with_na.fillna(csv_file_with_na.mean(axis=0)))

#在所有NaN的位置填入0

print(csv_file_with_na.fillna(0))

#在所有NaN的位置填入"Missing"字段

print(csv_file_with_na.fillna('Missing'))

#丢弃DataFrame中含有NaN的行

print(csv_file_with_na.dropna(axis=0))

#丢弃DataFrame中含有NaN的列

print(csv_file_with_na.dropna(axis=1))




运行程序后,可以得到如图323所示的结果。从结果可以看出,使用reindex方法后,由于原DataFrame中没有索引为2的行,所以Pandas自动新建了一条所有字段都为NaN的记录,使用了isna方法查看DataFrame中所有NaN的位置,其返回一个bool类型的DataFrame,可以看到除了索引为2的行都为True外,其他记录都为False,说明只有刚才插入的记录是NaN。接下来代码分别使用了fillna方法将各列均值、0或Missing字段对缺失值进行了填充,从图323中可以看出不同填充方案得到的结果。最后代码还展示了如何直接舍弃含有NaN的数据,一共有两种舍弃方式,即舍弃行或者舍弃列。从舍弃行的结果可以看出,索引为2的记录被直接删除,而舍弃列的结果返回了一个空的DataFrame,因为每列都含有NaN,因此所有的数据都被舍弃了。




图323处理DataFrame中的缺失值



本节说明了Pandas的基本用法,从各种类型数据的读取到数据的处理,相比本书介绍的部分,Pandas还有许多数据处理与分析方法,更多信息可以查看Pandas的官网。

3.4scikitlearn

scikitlearn是一个开源的Python机器学习库,在开始说明具体任务之前,先向读者阐述scikitlearn中模型的建立与使用过程。在使用scikitlearn做机器学习的过程中,第1步是准备数据,准备好训练集数据与测试集数据; 接着实例化一个模型的对象(模型即后文中将要介绍的SVM、随机森林等),需要根据具体任务选择适合的模型; 在实例化模型完成之后,直接调用fit函数即可,此函数需要传入训练数据,函数内部自动拟合所传入的数据。在fit完成后,若需要测试,则调用predict函数即可,接着可以使用score函数对预测值与真实值之间的差异进行评估,该函数会返回一个得分以表示差异的大小。

从上述使用框架的过程中可以看出,scikitlearn是一个封装性很强的包,这对于新手而言十分友好,无须自己定义过多函数或写过多代码,直接调用其封装好的函数即可,而整个过程对用户形成了一个黑盒,使用户难以理解算法内部的具体实现,这也是封装过强的弊端。

下面本书就以回归及分类任务为例具体讲解框架中每步的做法。

3.4.1使用scikitlearn进行回归

本节以简单的回归问题说明scikitlearn的用法,以3.4节中所讲的5个步骤依次进行介绍。

1. 准备数据

本节选取了一个图像较为复杂的函数y=xsin(x)+0.1x2cos(x)+x ,使用以下程序生成y在x∈[-10, 10]上的数据,并且x以间隔为0.01取值。为了验证scikitlearn中模型的学习能力,直接使用训练数据进行测试,代码如下: 


//ch3/test_scikit_learn.py

from sklearn.svm import SVR, SVC

import numpy as np

import matplotlib.pyplot as plt



#生成回归任务的数据

def get_regression_data():

start = -10

end = 10

space = 0.01



#自变量从[start, end]中以space为等间距获取

x = np.linspace(start, end, int((end - start) / space))

#根据自变量计算因变量,并给其加上噪声干扰

y = x * np.sin(x) + 0.1 * x ** 2 * np.cos(x) + x + 5 * np.random.randn(*x.shape)



#返回训练数据

return np.reshape(x, [-1, 1]), y



#得到回归数据

x, y = get_regression_data()

#打印数据形状以进行验证

print(x.shape, y.shape)




运行以上程序可以得到命令行的输出: (2000, 1) (2000,),说明训练数据与测试数据已经被正确获取,其中训练数据x的形状中2000表示有2000个训练样本,1表示每个训练样本由1个数构成,而训练标签y的形状表明其是由2000个数组成的标签。

接下来对训练数据进行可视化,让读者对数据有一个直观上的认识,同时也测试生成的数据是否符合要求。使用Matplotlib对数据进行可视化,以蓝色的点进行标识,实现可视化过程的代码如下: 


//ch3/test_scikit_learn.py

#可视化数据

figure, axes = plt.subplots()



#以散点图绘制数据






axes.scatter(x, y, s=1, label='training data')

#以LaTeX风格设置标题

axes.set_title('$y=x sin(x) + 0.1x^2 cos(x) + x$')

axes.legend()

axes.grid()

plt.show()




运行以上程序可以得到如图324所示的函数图像,从图中可以看出数据基本处于一条曲线,该曲线即上面设置的函数,说明训练与测试数据都被正确地生成了。




图324训练数据的图像



2. 实例化模型

本书在这一节选用SVR(Support Vector Regression,支持向量回归),将SVM运用到回归问题上。本节对于原理不做阐述,仅说明scikitlearn的用法。初始化SVR模型的代码如下: 


#初始化分类模型

svr = SVR(kernel='rbf', C=10)




上面的代码表示初始化了一个SVR模型,其kernel参数表示模型选用的核函数,scikitlearn对SVR的核函数有以下常见3种选择: rbf、linear和poly,在此选用rbf作为核函数,C表示误差的惩罚系数,惩罚系数越大,则对训练数据拟合越好,但有可能造成过拟合,其默认值为1,由于训练数据较难拟合,因此本书将C值设置为10,以加强模型的拟合能力,读者可以自行尝试其他值。

3. 使用模型进行拟合

定义好模型后,接下来使用模型拟合训练数据,代码如下: 


#用模型对数据进行拟合

svr_fit = svr.fit(x, y)




使用fit函数对训练数据进行拟合,需要传入数据及其对应的标签。

4. 使用模型进行测试

在模型拟合完数据后,为了测试模型的性能,可以使用predict函数查看其对不同的输入值的预测,测试的代码如下: 


#使用模型进行测试

svr_predict = svr_fit.predict(x)




在predict函数中只用传入训练数据,函数会将预测值返回,此时使用训练数据进行预测,这样方便在之后的可视化过程中查看模型预测与真实值之间的差异。绘制真实值与预测值的代码如下: 


#可视化模型学到的曲线

fig, axes = plt.subplots()

axes.scatter(x, y, s=1, label='training data')

axes.plot(x, svr_predict, lw=2, label='rbf model', color='red')

axes.legend()

axes.grid()

plt.show()




可视化的结果如图325所示,其中深色的曲线是模型预测结果,从图中可以看出,该曲线和原离散数据呈现的图形很相似,说明模型对数据的拟合较好。




图325训练数据的图像



5. 评估模型性能

除了可以使用可视化的方式查看模型拟合情况,还可以使用score方法定量评估模型的好坏,score函数定量地刻画了预测值与真实值之间的差异,其用法的代码如下: 


#评估模型性能

score = svr_fit.score(x, y)

print(score)




score方法需要传入训练数据及其标签,最终在命令行输出的score为0.6428078808326549(读者的score和本书所展示的值可能不同,因为数据的初始化是随机的),说明模型对于64.28%的数据预测正确。由于原数据在空间中较为离散化,过高的score可能会带来过拟合的问题,无论从可视化的结果还是score上来看,该模型的表现可以接受。

3.4.2使用scikitlearn进行分类

本节同样以使用scikitlearn的5个步骤分别说明如何解决分类问题。

1. 准备数据

与回归的数据不同,分类需要给特定的数据指定类标签,为了简便起见,本节使用二维坐标作为分类特征,落在椭圆x21.52+y2=1内的点为一类,类标签以0表示,而落在椭圆x21.52+y2=1与圆x2+y2=4之间的点为另一类,其类标签为1。下面的程序用于生成分类的训练数据,代码如下: 


//ch3/test_scikit_learn.py

#生成分类任务的数据

def get_classification_data():

#数据量

cnt_num = 1000

#计数器

num = 0



#初始化数据与标签的占位符,其中训练数据为平面上的坐标,标签为类别号

x = np.empty(shape=[cnt_num, 2])

y = np.empty(shape=[cnt_num])



while num < cnt_num:

#生成随机的坐标值

rand_x = np.random.rand() * 4 - 2

rand_y = np.random.rand() * 4 - 2



#非法数据,如果超出了圆x^2 + y^2 = 4的范围,则重新生成合法坐标






while rand_x ** 2 + rand_y ** 2 > 4:

rand_x = np.random.rand() * 4 - 2

rand_y = np.random.rand() * 4 - 2



#如果生成的坐标在椭圆x^2 / 1.5^2 + y^2 = 1的范围内,则类标号为0,否则为1

if rand_x ** 2 / 1.5 ** 2 + rand_y ** 2 <= 1:

label = 0

else:

label = 1



#将坐标存入占位符

x[num][0] = rand_x

x[num][1] = rand_y



#将标签存入占位符

y[num] = label



num += 1



#给训练数据添加随机扰动以模拟真实数据

x += 0.3 * np.random.randn(*x.shape)



return x, y



#得到训练数据与标签

x, y = get_classification_data()

#查看数据和标签的形状

print(x.shape, y.shape)




运行上面的程序后,能看到命令行输出(1000, 2) (1000,),表示有1000个训练数据及其对应的标签,其中每个训练数据由两个数(坐标)构成,而标签由1个数字构成。

除此之外,使用Matplotlib以散点图的形式可视化训练数据,由于同时存在不同类别的数据,所以需要先将类标为0的数据和将类标为1的点分开,并以不同的标识绘制,这样会增强图像的直观性,代码如下: 


//ch3/test_scikit_learn.py

#获取标签为0的数据下标

zero_cord = np.where(y == 0)

#获取标签为1的数据下标

one_cord = np.where(y == 1)



#以下标取出标签为0的训练数据

zero_class_cord = x[zero_cord]

#以下标取出标签为1的训练数据

one_class_cord = x[one_cord]



figure, axes = plt.subplots()

#以圆点画出标签为0的训练数据





axes.scatter(zero_class_cord[:, 0], zero_class_cord[:, 1], s=15, marker='o', label='class 0')

#以十字画出标签为1的训练数据

axes.scatter(one_class_cord[:, 0], one_class_cord[:, 1], s=15, marker='+', label='class 1')

axes.grid()

axes.legend()



#分别打印标签为0和1的训练数据的形状

print(zero_class_cord.shape, one_class_cord.shape)

plt.show()




运行以上程序,命令行会输出(388, 2) (612, 2)(读者输出的数据可能不同,因为数据的初始化是随机的),表示在1000个训练样本中,有388个属于类0,有612个属于类1; 同时能看到类似图326所示的结果。




图326分类数据的散点图



2. 实例化模型

本节使用SVM完成对训练数据的分类,代码如下: 


#创建SVM模型

clf = SVC(C=100)





3. 使用模型进行拟合

同样,类似3.4.1节中的第3部分,使用fit函数即能使模型拟合训练数据,代码如下: 


clf.fit(x, y)




4. 使用模型进行测试

本节对分类器的分类边界进行可视化,由于变量(训练数据)可以充斥整个二维空间,因此可以从二维空间中取出足够多的点以覆盖所关心的区域(由于需要将分类数据与分类边界相比较,所以可以将关心的区域设置为训练数据所覆盖的区域),使用得到的模型对关心的区域中的每个点进行分类,以得到其类别号,最终将不同预测的类别号以不同的颜色画出,即可得到模型的分类边界,代码如下: 


//ch3/test_scikit_learn.py

def border_of_classifier(sklearn_cl, x):

#求出所关心范围的最边界值: 最小的x、最小的y、最大的x、最大的y

x_min, y_min = x.min(axis = 0) - 1

x_max, y_max = x.max(axis = 0) + 1



#将[x_min, x_max]和[y_min, y_max]这两个区间分成足够多的点(以0.01为间隔)

x_values, y_values = np.meshgrid(np.arange(x_min, x_max, 0.01), 

np.arange(y_min, y_max, 0.01))



#将上一步分隔的x与y值使用np.stack两两组成一个坐标点,覆盖整个关心的区域

mesh_grid = np.stack((x_values.ravel(), y_values.ravel()), axis=-1)



#使用训练好的模型对于上一步得到的每个点进行分类,得到对应的分类结果

mesh_output = sklearn_cl.predict(mesh_grid)



#改变分类输出的形状,使其与坐标点的形状相同(颜色与坐标一一对应)

mesh_output = mesh_output.reshape(x_values.shape)



fig, axes = plt.subplots()



#根据分类结果从 cmap 中选择颜色进行填充(为了使图像清晰,此处选用binary配色)

axes.pcolormesh(x_values, y_values, mesh_output, cmap='binary')



#将原始训练数据绘制出来

axes.scatter(zero_class_cord[:, 0], 

zero_class_cord[:, 1], s=15, marker='o', label='class 0')

axes.scatter(one_class_cord[:, 0], 

one_class_cord[:, 1], s=15, marker='+', label='class 1')

axes.legend()

axes.grid()



plt.show()



#绘制分类器的边界,传入已训练好的分类器,以及训练数据(为了得到关心的区域范围)

border_of_classifier(clf, x)




运行上面的程序可以得到类似图327所示的结果(由于训练数据的随机性,因此读者的模型的分类边界与图327可能会不完全一致)。




图327训练数据与模型分类边界



5. 评估模型性能

与3.4.1节第5部分类似,使用score函数评估模型即可,代码如下: 


#评估模型性能

score = clf.score(x, y)

print(score)




运行以上代码,可以得到输出0.859(读者得到的结果可能与此不同),说明分类器对85.9%的数据进行了正确分类,而从可视化结果可以看出来,剩余14.1%未被正确分类的数据很有可能是噪声数据(那些存在于两类数据交叉部分的点)。基于这个准确率,可以接受此分类器。

本节以简单的回归与分类问题作为实例,讲解了scikitlearn的基本用法,还有许多别的模型和应用值得读者进一步探究,由于本书不涉及过多的机器学习知识,在此不进行讲解,更多信息可以参考scikitlearn官网。

3.5collections

collections模块在Python标准数据类型的基础上极大地扩展了一些特殊用途的数据类型,包括namedtuple、deque、ChainMap、Counter、OrderedDict、defaultdict、UserDict、UserList和UserString,本节将对其中几种常用的数据类型进行介绍。

3.5.1namedtuple

namedtuple是collections中的一个重要的数据结构,从名称可以看出,它其实是一个元组类型的数据结构,但同时元组内的每个元素还有其对应的名称,这使它也具有类似字典的特性,如下代码展示了使用namedtuple表示四通道图像中每个通道值: 


//ch3/test_collections.py

from collections import namedtuple



#创建一个表示四通道图像的namedtuple

Channel = namedtuple('ImageChannels', field_names=['R', 'G', 'B', 'A'])

ch = Channel(R=127, G=200, B=255, A=100)

#获取四通道中的R通道值

print(ch[0], ch.R)




创建namedtuple时,第1个参数为类型名(typename),类似于class的名称,表示所创建的namedtuple名称,第2个参数表示namedtuple中的属性名,可以传入一个str的列表或者用空格分隔的字符串,为了提高代码的可读性,在如上代码中采用了传入字符串列表的形式对namedtuple中的变量进行定义。从namedtuple中获取属性值则可以采取元组或者类似字典的形式,如上代码所示。

从以上示例可以看出,使用namedtuple相比使用元组而言更加灵活。获取元组中的元素只能使用下标的形式,这对于代码阅读而言十分不直观,同时由于字典类型无法hash,无法被添加进入集合,因此使用namedtuple能很好地解决这些问题。

3.5.2Counter

顾名思义,Counter是一个计数器的数据类型,其能方便地辅助计数相关应用的实现,本质是dict的一个子类,使用Counter进行计数的时候,其会返回一个dict,key为元素,value为该元素出现的次数,如下代码以不同的方式对字符串中的不同字符进行了计数: 


//ch3/test_collections.py

from collections import Counter

#待计数的字符串

s = 'abbcdd'

#直接传入字符串进行计数

counter1 = Counter(s)

#传入列表进行计数

counter2 = Counter(list(s))

#传入元组进行计数

counter3 = Counter(tuple(s))

#传入字典进行计数

counter4 = Counter({'a': 1, 'b': 2, 'c': 1, 'd': 2})

print(counter1, counter2, counter3, counter4)




以上代码分别以直接传入字符串、列表/元组、字典计数器的方式创建了具有相同结果的Counter。

3.5.3OrderedDict

OrderedDict可以使读取字典中数据的顺序与写入数据的顺序相同,使用方法的代码如下: 


//ch3/test_collections.py

from collections import OrderedDict

#创建OrderedDict对象

od = OrderedDict()

#向OrderedDict中存放值

od['A'] = 'a'

od['B'] = 'b'

od['C'] = 'c'

#读取OrderedDict中的值

for k, v in od.items():

print(k, v)




运行如上代码可以看出,打印出来的元素顺序与插入数据时的顺序一致。在Python 3.5及之前,标准数据类型dict的存储和数据插入顺序并不一致,因此在Python 3.5及其之前需要使用OrderedDict保证读取数据的有序性。从Python 3.6开始,标准数据类型dict的读取顺序和写入顺序已经保持一致,读者可以根据自身的应用场景使用字典的顺序特性。

3.5.4defaultdict

使用Python标准数据类型dict时,如果访问的key不在字典中,则程序会抛出异常KeyError,此时需要使用逻辑判断该key是否存在于字典或者使用字典的get方法为不存在的键赋予默认值,代码如下: 


//ch3/test_collections.py

#创建只包含一个元素的字典

d = {'ip': '127.0.0.1'}

#当尝试访问一个字典中不存在的元素时会抛出KeyError

#port = d['port']

#在使用之前需要判断字典中是否有键

port = d['port'] if 'port' in d else None

#使用get方法赋予默认值

port = d.get('port')




虽然通过以上两种方法能够避免程序出现异常,但是无疑都增加了编程的成本,此时可以使用collections模块中的defaultdict来创建带有默认值的字典,如果此时字典中不存在某键,则会直接返回默认值,如下代码为字典中不存在的键返回默认值80: 


//ch3/test_collections.py

from collections import defaultdict

#创建一个默认值为80的defaultdict

dd = defaultdict(lambda: 80)

dd['ip'] = '127.0.0.1'

#返回80

port = dd['port']




使用defaultdict一方面简化了开发者对于异常的处理,另一方面由于程序会默认返回值而不抛出异常,因此使用不慎会为程序引入额外的调试成本,这种行为可能是开发人员不希望的,读者需要根据自身的需要酌情使用。

3.6typing

由于Python是动态语言,其对于类型定义没有严格的检查,编程方式十分灵活,然而便捷的同时也带来了另一个问题,这便是在编程过程中代码提示较少,在阅读他人的代码或后期进行代码维护时会增加成本,因此,在Python 3.5及其以后的版本中,原生Python内置了typing模块,用于辅助Python编程时的类型检查,可以很方便地用于集成开发环境。

在Python中,对于参数或者变量的类型,使用“:  [类型]”的形式编写,而函数返回值的类型则在函数签名后使用“> [类型]”即可,代码如下: 


//ch3/test_typing.py

a: str = 2

#期望接受一种类型为str的参数, 没有返回值

def print_var(v: str) -> None:

print(v)




从代码中可以看出,虽然变量a的值为整型值,但是在进行类型标注时可以任意进行标注,并且在编程过程中以开发人员标注的类型为准,除此之外以上代码还定义了一个期望接受一种类型为字符串的参数,并且由于函数体内只进行了打印操作,没有返回值,因此在函数头使用“> None”进行表示。

在程序运行时,Python并不强制开发人员标注函数和变量的类型,使用typing的主要作用: ①类型检查,防止程序运行时出现参数或返回值类型不一致的情形; ②作为开发文档的附加说明,方便调用。本节将分不同的数据类型(Python标准数据类型、扩展类型等)为读者介绍使用typing的代码写法。

3.6.1标准数据类型标识

Python 3中有6种基本的数据类型,分别是Number、String、List、Tuple、Set和Dictionary,在Number中又包含int、float、bool和complex,下面就分别介绍这几种数据类型在typing中的使用方法。

对于Number和String类型的标识比较简单,直接使用类型即可,代码如下: 


//ch3/test_typing.py

#整型变量

int_var: int = 1

#浮点型变量

float_var: float = 1.0

#布尔型变量

bool_var: bool = True






#复数型变量

complex_var: complex = 1 + 2j

#字符串变量

str_var: str = '1'

#一个整型变量和一个浮点型变量相加并返回

def func_with_type(i: int, f: float, b: bool, c: complex, s: str) -> float:

return i + f




对于Number和String类型变量的标识只需读者理解类型标识的基本写法,并未用到typing模块,而在对List、Tuple、Set和Dictionary类型的变量进行标识时,则需要进一步标识出这些组合类型中数据的类型,例如对于List类型变量,需要使用如下代码进行标识: 


//ch3/test_typing.py

from typing import List

#将标识元素为整型值的列表

list_var: List[int] = [1, 2, 3, 4]




可以看出,在进行类型定义时,需要在“[]”中表明列表中元素的类型。类似地,在对Tuple、Set及Dictionary进行类型定义时,可以使用的代码如下: 


//ch3/test_typing.py

from typing import Tuple, Set, Dict

#含有4个不同类型元素的元组

tuple_var: Tuple[int, str, float, bool] = [1, '2', '3.0', False]

#元素为整型变量的集合

set_var: Set[int] = {1, 2, 3, 4}

#键为字符串且值为整型值的字典

dict_var: Dict[str, int] = {'1': 1, '2': 2, '3': 3}




在Python 3.9及其以后的版本中,Python中标准的组合类型也开始支持“[]”进行类型标识,因此更加推荐使用以下方式进行类型标识: 


//ch3/test_typing.py

#将标识元素为整型值的列表

list_var2: list[int] = [1, 2, 3, 4]

#含有4个不同类型元素的元组

tuple_var2: tuple[int, str, float, bool] = [1, '2', '3.0', False]

#元素为整型变量的集合

set_var2: set[int] = {1, 2, 3, 4}

#键为字符串且值为整型值的字典

dict_var2: dict[str, int] = {'1': 1, '2': 2, '3': 3}




当然,不同类型标识也可以进行组合使用,例如下面的代码标识了变量combined_var是一个内部元素同时包含List、Tuple和Dictionary的元组: 


//ch3/test_typing.py

combined_var: tuple[list[int], tuple[int, str, float, bool], dict[str, int]]




不难看出,每次进行类型标识的时候都需要重新写一次类型的定义,因此Python提供了类型别名的写法,如下代码所示,分别使用了A、B、C 3个变量代表不同的复合数据类型,这样做的好处是能增强代码的可读性,其次也能很方便地复用已经定义过的类型。


//ch3/test_typing.py

#自定义类型别名

A = list[int]

B = tuple[int, str, float, bool]

C = dict[str, int]

combined_var2: tuple[A, B, C] = ([1, ], (1, '2', 3., True), {'1': 1})





对于List、Set等可以存储不同类型变量的复合数据结构而言,则需要使用Union进行标识,从Python 3.10开始,Union可以使用“|”进行标识,下面的代码表示变量为包含整型值或字符串的列表: 


//ch3/test_typing.py

from typing import Union

int_str_var: list[Union[int, str]] = ['a', 2, 'b', 4]

#Python 3.10及以后

int_str_var2: list[int | str] = ['a', 2, 'b', 4]




3.6.2collections中的数据类型标识

类似地,typing模块也支持对collections中的数据类型进行标识,下面将分别进行介绍。在3.5节中已经向读者介绍了collections模块中常用的几种数据结构,本节就以3.5节中的数据结构为例说明collections中的数据类型标识。

使用typing中的NamedTuple为collections中的namedtuple进行类型标识的时候并不用在注解内,而是使用如下代码进行声明: 


//ch3/test_typing.py

from typing import NamedTuple

#相当于collections.namedtuple('Address', ['ip', 'port])

class Address(NamedTuple):

ip: str

port: int

address = Address(ip='127.0.0.1', port=80)




可以看到,在typing中对于namedtuple是以类继承的形式进行实现的,在创建namedtuple实例时使用类似创建类对象的方法即可。

对于Counter、OrderedDict和defaultdict则还是以注解的形式进行类型标识即可,代码如下: 


//ch3/test_typing.py

from typing import Counter as TCnt, OrderedDict as TOrdD, DefaultDict as TDD

from collections import Counter, OrderedDict, defaultdict

#由于Counter的value必定为int, 因此只需标识key的类型






counter: TCnt[str] = Counter('aabbccddefg')

#OrderedDict需要标识key和value的类型

od: TOrdD[str, str] = OrderedDict()

#defaultdict需要标识key和value的类型

dd: TDD[str, str] = defaultdict(str)




对于Counter、OrderedDict和defaultdict的类型标识与Python中的标准数据类型标识方法类似,因此在此不再说明。

在Python 3.9及其之后的版本中,collections中的Counter、OrderedDict和defaultdict已原生支持,代码如下: 


//ch3/test_typing.py

#Python 3.9之后

counter2: Counter[str] = Counter('aabbccddefg')

od2: OrderedDict[str, str] = OrderedDict()

dd2: defaultdict[str, str] = defaultdict()




3.6.3其他常用标识

本节将为读者介绍一些在typing模块中常用的类型标识。

Callable表示当前的变量或参数是一个可调用对象,例如在以下的代码中函数期望传入一个函数对象: 


//ch3/test_typing.py

from typing import Callable

#传入的函数参数接收一个整型值作为参数并且返回值为str

def wrapper1(func: Callable[[int], str]):

return func(0)

#传入的函数参数接收任意的可变参数, 无返回值

def wrapper2(func: Callable[..., None]):

func()




Callable使用列表的形式接收函数的入参类型,并在其后跟随返回值类型。

当传入的参数类型可以为任意时,可以使用Any进行标识,在Python中无法进行类型推断的变量同样也被默认认为是Any类型,在此不使用代码进行说明。

当一个函数从不终止或总会抛出异常时,可以使用NoReturn进行标识,代码如下: 


//ch3/test_typing.py

from typing import NoReturn

#包含死循环的函数

def func_while() -> NoReturn:

from time import sleep

while True:

sleep(1)

#必定会抛出异常的函数

def func_exc(num: int) -> NoReturn:

raise ValueError(f'Bad Value: {num}')




读者需要区分返回值为NoReturn和None的区别,NoReturn表示函数不会终止或者不会返回,而None表示函数无返回值。

Optional表示当前的类型标识是可选的,代码如下: 


//ch3/test_typing.py

from typing import Optional

#期望传入一种类型为整型或浮点型的参数, 允许传入None

def func_with_optional_param(num: Optional[Union[int, float]]):

if num is None:

raise ValueError(f'Unexpected value: {num}')

print(num)




Literal表示变量或参数只能为指定的字面值,如下代码指定了打开文件的模式只能采用['r', 'rb', 'w', 'wb']中的值: 


//ch3/test_typing.py

from typing import Literal

MODE = Literal['r', 'rb', 'w', 'wb']

#打开文件

def open_helper(file: str, mode: MODE) -> str:

with open(file, mode, encoding='utf8'):

pass

return ''




3.7argparse

在Python中,可以使用argparse模块方便地对命令行参数进行处理。编程时,通过命令行传入的不同参数改变程序的执行逻辑,极大地增加了程序的灵活性,因此本节将对argparse模块进行简要介绍。

3.7.1argparse的使用框架

argparse模块能处理指定的命令行参数与位置命令行参数(根据传入参数的位置进行识别),其整体使用流程如下: 

首先使用ArgumentParser方法创建一个解析器parser(此时parser中的参数列表为空),在创建过程中可以为ArgumentParser方法传入定制化的属性,如对该parser的描述等。

创建完parser后,接下来需要为parser添加参数,一般使用add_argument方法,该方法需要指定参数名,同时add_argument方法含有许多可选的属性,如参数目标数据类型、默认值,目标参数指定动作action等。读者可以将一个添加完参数的parser理解成一个参数的集合,该集合包含了所有即将从命令行接受的参数。对于add_argument方法的探究是本节的重点,将在3.7.2节详细说明。

为parser添加完目标参数后,使用parse_args方法将命令行中传入的参数转换为argparse中的Namespace对象(表示参数读取完毕),此时可以使用args.[变量名]的形式访问由命令行传入的参数。

通过以上3个步骤,即可方便地解析来自命令行的参数。值得注意的是,使用add_argument方法为parser添加参数时,如何正确地设计参数的类型、属性及动作是至关重要的,有时错误的设计会给编码带来不小的麻烦。

3.7.2使用argparse解析命令行参数

本节只对add_argument方法进行探讨,若读者想进一步学习ArgumentParser方法的运用,则可以参考argparse的帮助文档。

1. 解析字符串类型的参数

parser从命令行接受的参数的默认类型是字符串,因此直接按照3.7.1节所讲的使用流程接受参数即可,下面的程序说明了这一过程,为创建的parser添加了一个名为vvv(vvv)的参数,并且此参数的简写形式为v(v),默认值为string,代码如下: 


//ch3/test_argparse.py

import argparse



def parse_str():

#创建parser

parser = argparse.ArgumentParser()

#为parser添加一个名为vvv(简称v)的参数,其默认值为string

parser.add_argument('-v', '--vvv', default='string')

#解析参数

args = parser.parse_args()

return args



args = parse_str()

#打印Namespace

print(args)

#打印接受的参数及其类型

print(args.vvv, type(args.vvv))




在控制台使用命令python test_argparse.py v argparse_is_good(或python test_argparse.py vvv argparse_is_good)可以得到如图328(a)所示的结果,能看到args中只有一个名为vvv的参数,它的值恰好是从命令行传入的"argparse_is_good"字符串。同时该参数类型为str。如果直接使用命令python test_argparse.py(不传入任何参数),则会得到如图328(b)所示的结果,可以看到此时vvv参数的值为默认值string。



图328使用argparse解析字符串参数



2. 解析int类型的参数

与3.7.2节的第1部分类似,在使用add_argument时,为type参数传入目标类型即可,下面的程序说明了如何将type指定为int,以解析整型参数,代码如下: 


#为parser添加一个名为iii(简称i)的参数,其默认值为0,将传入的类型限制为整型

parser.add_argument('-i', '--iii', default=0, type=int)




将type指定为int后,程序会尝试将从命令行传入的参数转换为int型,如果转换失败(如使用命令python test_argparse.py i argparse_is_good),则程序会报错终止。有意思的是,default值的类型可以与指定的类型无关,因为只有当命令行未传入参数时,才会使用default值(尝试将default值转换为type类型),因此,如果将上述程序的default改为string,而使用正确传入整型数的命令时,则程序仍能正常执行。除了可以使用int作为type外,对于float也采用类似的处理方式,而type为bool则采用其他的处理方法,有兴趣的读者可以自行尝试将type指定为bool的解析结果(因为本质是将字符转换为bool值,因此会发现无论传入什么值其结果都为True,除非将default置为False并不传入任何参数)。

3. 解析bool类型的参数

由于bool仅有两个值,即True或False,因此argparse处理bool的过程中不需要传入任何值,仅以是否写出该参数进行判别。例如定义了一个名为b的参数,仅当命令中写出了参数b时,该值才为True(或False),当没写时为False(或True),而无须显式地传入True或False。这一点和使用if语句判别bool值十分相似,如下面的程序,使用b==True进行判断是多此一举的,直接使用b本身即可,代码如下: 


b = True

if b == True:

…

if b:

…




对于bool型参数的处理,需要用到add_argument中的action参数,将其指定为store_true(或store_false)表示命令中写了该参数就将其置为True(False),解析bool型的参数的代码如下: 


#添加一个名为bbb(简称b)的参数,其默认值为False,若命令写出--bbb(-b),则值为True

parser.add_argument('-b', '--bbb', default=False, action='store_true')

#添加一个名为ppp(简称p)的参数,其默认值为True,若命令写出--ppp(-p),则值为False

parser.add_argument('-p', '--ppp', default=True, action='store_false')




4. 解析list类型参数

将命令行传入的参数返回为一个list有多种方法,本节介绍其中常用的两种。下面就分别对这两种方法进行介绍。

第1种是将add_argument方法的action指定为append(列表的追加),这种用法适合命令中多次重复使用相同参数传值的情况。假设现已为parser添加了名为eee的参数并将action指定为append,此时使用命令python test_argparse.py eee 1 eee 2则会得到参数eee为['1', '2'],这种方法的缺点是需要多次传入同名参数,不方便使用。

第2种更为便捷的方法是指定add_argument方法中的nargs参数,将这个参数指定为“+”“?”或“*”,分别表示传入1个或多个参数、0个或1个参数及0个或多个参数(同正则表达式的规则一致),并将传入的参数转换为list。例如将nargs指定为“+”并且变量名为eee的整型变量时,使用python test_argparse.py eee 1 2后,直接可以得到名为eee值为[1, 2]的参数。

下面的程序分别说明了以上两种解析列表参数的方法,第1种方法得到的结果为['1', '2'],而第2种方法将type指定为int,结果为[1, 2],代码如下: 


#添加一个名为eee(简称e)的参数,若多次使用--eee(-e),则结果以列表的append形式连接

parser.add_argument('-e', '--eee', action='append')

#添加一个名为lll(简称l)的参数,将传入的参数返回为一个list

parser.add_argument('-l', '--lll', nargs='+', type=int)




argparse还有更多高级用法,如打开指定文件等。

3.8JSON

JSON的全称为JavaScript Object Notation,是一种轻量级的数据交换格式,其使用键值对的形式存储与交换数据(与Python中的字典相同,不过Python中的字符串可以使用单引号或双引号表示,而JSON中仅能使用双引号),其键是无序的,仅支持由键访问数据,而其值是可以有序的,使用有序列表(数组)进行存储。

在Python中,使用JSON模块可以轻松地完成JSON数据的存储与读取。在Python中,JSON支持直接以JSON格式处理Python字典,也支持处理类JSON格式的字符串。值得注意的一点是,使用JSON持久化字典数据时,仅支持Python中的内置数据类型,除此以外的类型需要进行转换,如键值对中存在NumPy中的数据类型(常常会持久化NumPy数组),需要先将其转换为Python中的基本类型才能继续持久化。下面以JSON数据的写入与读取来分别介绍这两种处理方式。

3.8.1使用JSON模块写入数据

在JSON中,写入数据使用dump方法,需要为其传入待存储的字典数据及对应的文件指针。除此之外,可以使用dumps(dump+string)方法将Python字典数据转换为字符串,dump与dumps用法的代码如下: 


//ch3/test_json.py

import json







#初始化Python字典

py_dict = {'message': 'json is brilliant!', 'version': 1.14, 'info': 'python dict'}



#使用dump方法向文件写入Python字典

with open('py_dict.json', 'w', encoding='utf8') as f:

json.dump(py_dict, f)



#使用dumps(dump+string)将字典值转换为对应字符串

dict2str = json.dumps(py_dict)

print(dict2str)




运行以上程序后,能发现代码目录下多了一个py_dict.json文件,其内容即定义的py_dict字典中的值,不同的是,在持久化为JSON文件时会将原字典中的格式自动重整为JSON的标准格式。与此同时,控制台打印的dict2str结果也正是py_dict转换为JSON格式字符串的结果。

3.8.2使用JSON模块读取数据

本节将说明如何读取JSON文件。与持久化数据时所用的dump与dumps这一对“孪生兄弟”类似,读取JSON文件时也有对应的load与loads(load+string)方法: load方法从JSON文件中将持久化的内容读取到Python字典中,而loads则直接从类JSON字符串中获取数据,这两种数据读取的方法的代码如下: 


//ch3/test_json.py

#打开并读取JSON文件

with open('py_dict.json', 'r', encoding='utf8') as f:

load_json_file = json.load(f)



#初始化一个JSON格式的字符串

json_like_str = r'{"message": "json is brilliant!", "version": 1.14, "info": "json-like string"}'

#从字符串中读取数据

load_json_str = json.loads(json_like_str)



#打印从文件中读取的数据

print(load_json_file)

#打印从字符串读取的数据

print(load_json_str)




运行程序后,能看到控制台分别打印出来自文件与字符串的内容,并且它们都是Python中的字典类型,说明读取的内容已经从字符串正确加载并转换为字典类型。

3.9TALib

TALib的全称为Technical Analysis Library,从其名称可以看出这是一个“技术分析”库,其中包含了像是ADX、MACD、RSI这种技术指标,还包括K线信号的模式识别,下面将分别对技术指标和模式识别进行说明。

3.9.1技术指标

本节将为读者简单地介绍几种TALib中常用的技术指标及其使用方法。首先在路径code/ch3/下准备CSV数据202001.csv,其数据内容如图329所示,可以看出数据包含以日为单位的基金数据(单位净值、日增长率、复权净值、复权净值增长率、累计净值、累计收益率、同类型排名、总排名、同类型排名百分比)。




图329202001.csv中的数据示例



使用Pandas模块读取CSV数据,代码如下: 


//ch3/test_talib.py

import pandas as pd



pd.set_option('display.max_rows', None)



#读取CSV数据

data = pd.read_csv('202001.csv')

print(data)

#获取复权净值

adjust_val= data.loc[:, 'adjust_val']




接下来使用最常用的指标SMA(Simple Moving Average,简单移动平均),其计算如式(31)所示。


SMAki=1k∑ij=i-k+1xj(31)


其中,下标i表示第i个简单移动平均值,上标k表示周期为k,因此SMA用于简单计算原数据中包含第i个元素在内的前k个元素的平均值,使用TALib计算SMA的方法,代码如下: 


//ch3/test_talib.py

import talib

#计算周期为5天的复权净值的移动平均值

sma = talib.SMA(adjust_val, timeperiod=5)

print(sma)





图330计算复权净值

SMA值的结果


运行以上代码,可以得到收盘价的SMA值的计算结果,如图330所示。




从图330中可以看出,前4个SMA值为NaN,这是因为代码中计算SMA的周期值为5,因此在计算前4个复权净值的SMA时数据不足,因此返回值为NaN,而第5个值的计算方式为15(1+1+0.9997+1.0013+1.0013)=1.00046,其他的SMA值的计算以此类推。

接下来再介绍一个常用的指标MACD(Moving Average Convergence/Divergence,异同移动平均线),其需要计算一个快速平均线(指数平均线,典型周期为12)和一个慢速平均线(指数平均线,典型周期为26),并计算两者之间的差值作为信号的依据,平均线的计算方法如式(32)所示。



EMAfastm=EMAfastm-1×M-1M+xm×1M
EMAslown=EMAslown-1×N-1N+xn×1N(32)


式中的M、N表示计算EMA的周期,m、n表示序列中第m、第n个元素的EMA值。在TALib中,如果计算EMA的所需元素不够,则其直接使用算术平均值代替计算。得到快速和慢速平均线后,使用式(33)计算DIF值: 


DIFm=EMAfastm-EMAslowm(33)



对DIF同样使用EMA计算指数移动平均值即可得到DEA(MACD值),如式(34)所示。


DEAn=DEAn-1×N-1N+DIFm×1N(34)


分别得到DIF和DEA的值之后,再使用式(35)计算MACD值: 


MACD=DIFm-DEAn(35)


得到的MACD值即为行情软件中MACD指标的红/绿柱所表示的值。在TALib中计算MACD值的方法,代码如下: 


//ch3/test_talib.py

#使用定义计算MACD

ema_fast = talib.EMA(adjust_val, timeperiod=3)

ema_slow = talib.EMA(adjust_val, timeperiod=5)

dif = ema_fast - ema_slow

dea = talib.EMA(dif, timeperiod=2)






macd_hist = (dif - dea)

print(macd_hist)



#直接使用talib计算MACD

macd, macd_signal, macd_hist = \

talib.MACD(adjust_val, fastperiod=3, slowperiod=5, signalperiod=2)

print(macd_hist)




如上代码中展示了如何使用定义与TALib计算MACD值,运行代码可以发现两者的计算结果一致。

3.9.2模式识别

TALib中除了能计算各指标值以外,还能针对K线的排列进行模式识别,例如乌云盖顶、三只乌鸦等K线形态。由于基金数据不符合OHLC的格式,因此TALib中的模式识别不适用于基金数据。例如需要识别OHLC数据中的“乌云盖顶”模式,可以使用以下代码实现: 


res = talib.CDLDARKCLOUDCOVER(opens, highs, lows, closes, penetration=0.5)




更多有关TALib模式识别的内容可以参见其文档。

3.10AKShare

AKShare 是一个功能十分强大的财经数据获取开源Python包,它能够提供股票、期货、债券、期权、外汇、货币、现货、利率、基金、指数等数据,股票包括A股、港股和美股等数据,提供实时与历史行情数据,同时针对股票还提供市场的评价信息和年报等基本面数据。对于期货、期权和外汇等品种,AKShare也提供了类似的数据。

AKShare同时提供了许多有趣的数据,包括中国宏观杠杆率、CPI和PPI报告等宏观数据、不同国家的宏观数据、奥运奖牌、空气质量等。使用者可以将AKShare作为一个大的数据集市,其中不仅提供了金融数据,也能为其他领域提供数据分析应用的数据。

AKShare从相对权威的财经数据网站获取原始数据并进行加工并返回,使用AKShare时的Python版本最好在3.8.5以上。由于原始的财经网站数据格式与接口可能经常发生变化,因此推荐将AKShare升级到最新版本进行使用。

下面以公募基金相关数据获取为例说明AKShare的使用方法。

3.10.1获取基金基础信息

在AKShare中,获取基金的基础信息使用fund_name_em方法,这种方法从天天基金网获取基金的基础信息并返回一个5列的DataFrame,每列信息分别为基金代码、拼音缩写、基金简称、基金类型和拼音全称,其中基金代码和基金类型是最重要的信息,基金的简称与拼音等对实际的分析意义不大。

使用如下的代码完成基金基础信息的获取: 


//ch3/test_akshare.py

import akshare as ak



#获取当前时刻所有基金的基础数据

fund_infos = ak.fund_name_em()

print(fund_infos)




执行代码后,可以得到如图331所示的数据。




图331使用AKShare获取基金基本信息的结果



从图331可以看出本书执行代码的时候一共获取了19743只不同类型的基金。 

3.10.2获取基金历史行情

不同类型的基金在数据组织结构上存在不同,本节以开放式基金为例讲解如何获取基金历史行情。在AKShare中,使用fund_open_fund_info_em方法获取开放式基金的历史行情,这种方法接收两个参数,分别是基金代码fund与指标名称indicator,其中indicator的可选值为下列值之一: 单位净值走势、累计净值走势、累计收益率走势、同类排名走势、同类排名百分比、分红送配详情、拆分详情,不同的指标值的返回值字段有所不同,下面将以代码为202001的基金为例进行讲解。

1. 获取单位净值走势

使用fund_open_fund_info_em(fund, indicator='单位净值走势')获取开放式基金的单位净值走势,可以得到如图332所示的结果。可以看出返回的DataFrame数据不仅包括每日的净值,还包括日增长率的数据。 




日增长率的计算方式如式(36)所示,读者可以自行进行验算。


growth_ratei=unit_vali-unit_vali-1unit_vali-1×100(36)




通过fund_open_fund_info_em方法得到的数据大多是以包含日期的及其相应指标数据的DataFrame。

2. 获取累计净值走势

使用fund_open_fund_info_em(fund, indicator='累计净值走势')获取开放式基金的累计净值走势,返回的数据如图333所示,累计净值走势的数据只有两列,分别为日期及当日该基金的累计净值。



图332使用AKShare获取基金的

历史单位净值




图333使用AKShare获取基金的历史

累计净值走势




由于返回的数据是DataFrame,因此此时可以直接使用plot方法进行绘图,代码如下: 


//ch3/test_akshare.py

#获取累计净值走势

import matplotlib.pyplot as plt



plt.rcParams['font.sans-serif'] = ['SimHei']

fund_cum_val = ak.fund_open_fund_info_em(fund_symbol, indicator='累计净值走势')

fund_cum_val.plot()

plt.show()




运行以上代码可以得到如图334所示的绘图结果。




图334绘制历史累计净值走势图



3. 获取累计收益率走势

使用fund_open_fund_info_em(fund, indicator='累计收益率走势')获取开放式基金的累计收益率走势,得到如图335所示的结果。




图335使用AKShare获取基金的历史累计收益率走势



需要注意的是,返回的累计收益率数据是近半年的,在进行数据拼接的时候需要进行平滑。

4. 获取同类排名走势

使用fund_open_fund_info_em(fund, indicator='同类排名走势')获取开放式基金的同类排名走势,得到如图336所示的结果。




图336使用AKShare获取基金的历史同类排名走势



在返回的数据中,“同类型排名每日近三月排名”表示该基金在同类型基金中的具体名次,“总排名每日近三月排名”表示该基金所在的同类型总基金数量。

5. 获取同类排名百分比

使用fund_open_fund_info_em(fund, indicator='同类排名百分比')获取开放式基金的同类排名百分比,得到如图337所示的结果。




图337使用AKShare获取基金的历史同类排名百分比



在返回的数据中,“同类型排名每日近3月收益排名百分比” 的值表示在同类型的基金中,当前基金近三月收益超越的百分比数,例如对于20130104的数据而言,表示基金202001的近三月收益优于40.87%的同类型基金。百分比数可以由3.10.2节中第4部分的同类排名走势计算得到,使用1-(同类型排名/总排名)即可得到,读者可以自行验证。

6. 获取分红送配详情

使用fund_open_fund_info_em(fund, indicator='分红送配详情')获取开放式基金的分红送配详情,得到如图338所示的结果。




图338使用AKShare获取基金的历史分红送配详情



返回的结果一共分为5列,分别为年份、权益登记日、除息日、每份分红和分红发放日,其中最关键的信息是每份分红与分红发放日,对于返回结果中的每份分红是以文字的形式展现的,需要进一步地进行解析处理。

7. 获取拆分详情

使用fund_open_fund_info_em(fund, indicator='拆分详情')获取开放式基金的拆分详情,由于202001没有历史的拆分数据,因此使用000277可以得到如图339所示的结果。




图339使用AKShare获取基金的历史拆分详情



3.11Tushare

Tushare是一个免费提供各类数据助力行业和量化研究的大数据开放社区,其拥有股票、基金、期货、数字货币等市场行情数据,同时也包括公司财务、基金经理等基本面数据,相较于需要收费的Wind、RQData等服务,Tushare是一个对于新手而言较为友好的量化数据获取方式,API的调用使用积分制,其官网首页如图340所示。




图340Tushare官网首页



首先需要在官网注册一个Tushare的账号,登录之后在个人主页能够查看接口TOKEN,如图341所示。




图341获取Tushare的接口TOKEN



获取TOKEN后,可以使用如下代码测试是否能够正常使用接口: 


//ch3/test_tushare.py

import tushare as ts



#读者的Tushare接口TOKEN

TOKEN = '*******************************************************'

#需要读取行情的标的代码

TS_CODE = '165509.SZ'



#使用TOKEN初始化API

pro = ts.pro_api(TOKEN)

#读取标的的日线数据

fund_info = pro.fund_basic()

print(fund_info)




运行以上代码,可以得到如图342所示的结果,可以看到能够正常读取到基金的基础数据,其中包含的字段有基金代码、基金名称、基金公司、基金托管人、基金类型、成立日期。




图342使用Tushare读取基金的基础数据



同样,使用Tushare也可以获取基金的净值信息,代码如下: 


//ch3/test_tushare.py

#读取基金的净值数据

fund_val = pro.fund_nav(ts_code=TS_CODE)

print(fund_val)




运行代码可以得到如图343所示的结果,可以看到Tushare返回的结果中同时包含单位净值与累计净值数据,相较于AKShare已经将数据对齐返回。




图343使用Tushare读取基金的净值数据



Tushare的API功能众多,更多的用法参见Tushare数据接口说明。

3.12PyPortfolioOpt

PyPortfolioOpt是一个开源的投资组合优化Python库,它可以方便地完成有效前沿的求解、BlackLitterman资产配置模型等,同时可以使用PyPortfolioOpt完成各种期望回报的计算。下面以3.9节中使用的202001.csv文件为例,介绍PyPortfolioOpt的使用。

使用PyPortfolioOpt计算基金复权净值的日收益率,可以使用如下代码实现: 


//ch3/test_pyportfolioopt.py

import pandas as pd

from pypfopt import expected_returns



#读取CSV数据

data = pd.read_csv('202001.csv')

#从复权净值计算收益率

returns = expected_returns.returns_from_prices(data['adjust_val'])

print(returns)




运行代码的结果如图344所示。




图344使用PyPortfolioOpt计算基金复权净值的收益率



也可以使用PyPortfolioOpt计算基于复权净值的年化收益率,代码如下: 


//ch3/test_pyportfolioopt.py

#计算年化收益率

mu = expected_returns.mean_historical_return(data['cum_val'])

print(mu, (1 + returns).prod() ** (252 / returns.count()) - 1)




PyPortfolioOpt的mean_historical_return方法首先将传入的净值转换为收益率序列,将收益率序列进行复利计算后进行年化转换,从而得到最终的结果,在如上代码的最后print函数中展示了手动计算的过程。mean_historical_return函数有许多可选的入参,上面的代码只是展示了默认参数的计算方法,更多用法可以参考官方文档。

由于PyPortfolioOpt模型的使用需要较强的理论知识,因此更多的原理与使用方法将在第6章中介绍。

3.13empyrical

empyrical是一个金融风险指标开源Python库,通过empyrical读者可以方便地计算常见的金融风险指标,例如最大回撤、夏普比率等。使用empyrical计算最大回撤的代码如下: 


//ch3/test_empyrical.py

import empyrical

import pandas as pd

from pypfopt import expected_returns



#读取CSV数据

data = pd.read_csv('202001.csv')

#由净值计算得到收益率

returns = expected_returns.returns_from_prices(data['cum_val'])

#计算收益率的最大回撤

md = empyrical.max_drawdown(returns)

print(md)




结合3.12节中通过PyPortfolioOpt计算收益率序列,将该序列传入empyrical的max_drawdown方法可以得到序列中的最大回撤,输出值为-0.3014693651233726,说明在传入的收益率序列值中发生的最大亏损约为30%。表31中列出了empyrical提供的主要金融风险指标。


表31empyrical提供的主要金融风险指标


API指 标 名 称API指 标 名 称




alphaAlpha系数
annual_return年收益率
annual_volatility年波动率
betaBeta系数
calmar_ratio卡玛比率
capture捕获比率
conditional_value_at_risk条件风险价值
down_alpha_beta下行的Alpha和Beta系数
down_capture下行捕获比率
downside_risk下行风险





max_drawdown最大回撤
omega_ratioOmega比率
sharpe_ratio夏普比率
sortino_ratio索提诺比率
stability_of_timeseries序列稳定性
tail_ratio尾部比率
up_alpha_beta上行的Alpha和Beta系数
up_capture上行捕获比率
up_down_capture上下行捕获比率比值
value_at_risk风险价值



表31中列出的指标在本节不进行详细解释,更多指标相关原理与使用可以参考4.5节。

3.14Orange

Orange是一个基于Python的数据挖掘和机器学习平台,由于其不依赖过多的代码就能完成数据分析的数据流,所以可以使用Orange快速验证模型和想法。Orange的官网如图345所示。




图345Orange的官网首页



从Orange的官网介绍不难看出,其是一个方便的数据挖掘的可视化工具,安装Orange有多种方式,其下载页面如图346所示,可以通过直接下载安装包进行本地安装,也可以通过conda、pip或者源码安装。为了方便起见,建议读者直接下载安装包进行安装。




图346Orange的下载页面



3.14.1Orange中的示例

安装完Orange后,启动后可以看到如图347所示的界面,可以通过单击Help→Example Workflows来查看Orange内置的部分工作流的示例,如图347所示,创建一个Orange中主成分分析(Principal Components Analysis,PCA)的工作流。




图347Orange的主界面



对于工作流中的每个构件都可以双击打开、查看并调整其属性,在如图348所示的主成分分析工作流中,可以看到整个流的起始节点为File节点,说明数据输入是以文件的形式进行输入的,在示例中采用的是brownselected数据集,其包含186个数据,其中每个数据由79个特征组成,数据总共可以分为3类。



图348Orange中的主成分分析示例



双击File节点,可以看到数据集的相关信息,例如各数据列的数据类型及其属性(特征、标签等),File组件支持多种格式的数据,例如CSV、Excel、H5等数据,其也支持自动检测数据源格式,如图349所示。




图349Orange的主成分分析工作流图



类似地,双击PCA组件则能看到PCA的相关设置,例如降维后的维数等,每次为PCA进行不同的设置时,在勾选Apply Automatically选项之后都会自动生效。经过PCA降维处理后的数据流向了两个节点,分别是Scatter Plot和Data Table,其中Scatter Plot会以散点图的形式绘制出降维后的数据,如图350所示,而Data Table则会以表格的形式展示降维后的数据,如图351所示。



图350Orange绘制的散点图




图351Orange展示的表格数据


从图350不难看出,由于降维维数默认为2,因此绘制散点图时的x轴和y轴分别对应着PC1和PC2这两个降维后的生成数据,从绘制的散点图来看,将原本包含79维的特征降到二维后,其在平面内也是可分的,因此降维效果十分显著。


使用Data Table展示数据则允许用户查看所有降维后的数据,相较于散点图的展示形式,使用表格能够展示更多数据的具体值。不难发现,Orange可以将上一个节点处理的数据结果输入若干不同的下游节点,极大地方便了用户验证自己的思路。

3.14.2创建自己的工作流

本节将说明如何使用Orange创建自己的工作流,如果目前想验证使用ARIMA模型对基金复权净值的时间序列预测是否有效,则应如何使用Orange进行快速验证呢?最终总体的工作流图如图352所示。



图352使用Orange完成ARIMA模型对时间序列的预测


总体来看,工作流大致分为4部分: 数据读取、数据预处理、模型预测、结果可视化,数据读取的组件使用的是CSV File Import,读取的文件为3.9节中使用的202001.csv,接着使用Select Columns组件从源数据中选取并指定数据分析中忽略的列、特征值列及目标值列。在进行时间序列分析之前,需要将输入数据通过As Timeseries组件将数据转换为时间序列的数据,至此已经可以获取时间序列数据。在此之后,如图353所示,延伸出了7条不同分支,其中每个分支都是对时间序列数据的一种分析尝试,例如查看时间序列数据的周期图、自相关图、格兰杰因果关系检验、原数据一阶差分的ARIMA模型预测结果等,详细的原理在此不进行说明。



图353Orange中时间序列相关组件


与时间序列数据处理相关的组件位于左侧的工具箱中,如图353所示,从图中可以看出其包含雅虎财经的数据源组件(Yahoo Finance),以及可以对时间序列进行内插值的组件(Interpolate)等,在确定需要使用的组件后,可以直接单击组件或将组件拖入右侧的画布中,再使用箭头将不同的组件连接起来,从而得到完整的数据流图。



读者可以在安装Orange之后打开ch3/time_series.ows文件,自行尝试使用Orange进行测试。

3.15Optunity

Optunity是一个包含用于超参数调整的各种优化器的库。无论是监督还是非监督学习方法,超参数调优都是必须解决的问题。

超参数的优化问题的目标函数通常是非凸的、非光滑的且难以直接求解其极值点的。Optunity则提供了一种数值优化的方法,以此进行优化问题的求解,Optunity由Python编写,不过其也能很方便地集成于R或MATLAB中。

Optunity需要待优化的函数返回一个数值类型的值,并支持最大化或最小化目标函数值,在指定了优化算法后能够从优化器中获得最终最优参数的结果及优化过程的中间结果,十分直观与方便。

本节以常用的粒子群(Particle Swarm)优化算法,简要说明Optunity的使用。以优化函数y=(sin(x)+2)×x2为例,可以先用Matplotlib绘制出该函数的图像,代码如下: 


//ch3/test_optunity.py

import optunity

import numpy as np

import matplotlib.pyplot as plt





def func(x):

""" 待优化的函数 """

return (np.sin(x) + 2) * x ** 2



x_range = [-100, 100]

xs = list(range(*x_range))

ys = [func(i) for i in xs]



#绘制待优化函数的图像

plt.plot(xs, ys)

plt.show()




运行以上程序得到的函数图像如图354所示。




图354待优化函数图像



从函数图像不难发现,其最小值应该位于x=0附近,接下来使用Optunity寻找极值: 


//ch3/test_optunity.py

opt = optunity.minimize(

func, num_evals=500, solver_name='particle swarm', x=x_range

)

opt_params, details, suggestion = opt

print(opt_params)

print(details)

print(suggestion)




如上代码所示,由于搜寻的是目标函数的最小值,所以使用了optunity.minimize方法,类似地,在寻找最大值的时候可以使用optunity.maximize方法,其中num_evals表示优化过程中允许调用优化目标函数的最大次数,在指定了优化器方法solver_name后,需要传入优化目标函数的参数值的搜寻范围,如代码中将参数x的搜寻范围指定为[-100,100]。运行以上程序可以观察到其打印的opt_params结果为{'x':  -0.0025152026389108073},是一个近似x=0的结果。

得益于Optunity框架的灵活性,其只需用户传入一个可以返回数值类型的待优化目标函数,并指定该函数的输入参数的取值范围,因此在进行量化交易的回测时,可以编写函数返回收益率或回撤值作为待优化参数,对该函数进行最大值或最小值优化。

3.16Optuna

类似于3.15节中的Optunity,Optuna也是一个参数优化工具,其支持自定义剪枝函数等,并且支持对于不同类型取值的初始化,例如枚举值、整型值、浮点型值等都有不同的参数值初始化取值方法。下面的代码说明了如何使用Optuna对数值型的函数进行优化: 


//ch3/test_optuna.py

import optuna

import numpy as np



def func(x):

""" 待优化的函数 """

return (np.sin(x) + 2) * x ** 2



def objective(trial):






x_range = trial.suggest_uniform('x', -100, 100)

return func(x_range)



study = optuna.create_study()

study.optimize(objective, n_trials=500)

print(study.best_params)




代码中选用的待优化目标函数与3.15节中一样,在Optuna中,需要先定义一个学习任务study,在创建study时可以为其传入direction参数以表示优化方向(最小化minimize或最大化maximize),默认值为minimize。

创建学习任务后,再向该任务中添加优化目标objective,需要在目标中使用suggest相关的方法(如代码中的suggest_uniform则是以均匀分布生成随机数)为目标函数生成初始值,再将这些初始值传入目标函数,对得到的目标值进行评价,由于在上述代码中目标函数的评价值即为函数值,因此无须再进行额外的评价过程。

在调用optimize方法时,需要将n_trials指定为实验次数,完成优化后,打印study.best_params则是优化后的最优参数值。除此之外,还可以打印best_value、best_trial等,分别表示最优参数下的最优函数值及最优的一次实验相关信息等。

在进行量化回测方面的相关优化时,其通常只涉及对于数值的优化,因此Optuna与Optunity在使用成本上相似,读者可以根据自身习惯进行选取。

3.17小结

本章介绍了常用于投研的Python库和工具,包括数据处理与可视化、Python编程辅助包、金融数据获取与分析的工具等几大类。读者可以根据自身情况学习与使用这些Python工具,为后面的章节打下基础。