3.2在Python中使用Matplotlib注解绘制树形图
3.2.1 Matplotlib注解
程序清单3-5 使用文本注解绘制树节点:
想要理解书上的代码,我花费了好久,才看懂。不是因为算法有多复杂,而是其中涉及的matplotlib的大量函数,需要进行查阅。这里附上相关的博客和文档。
http://blog.csdn.net/panda1234lee/article/details/52311593
Python--matplotlib绘图可视化知识点整理
http://blog.csdn.net/helunqu2017/article/details/78659490
matplotlib命令与格式:标题(title),标注(annotate),文字说明(text)
http://blog.csdn.net/u012704941/article/details/73604625
机器学习实战决策树plotTree函数完全解析
https://matplotlib.org/gallery.html#lines_bars_and_markers
官方文档
首先来看关于全局变量的定义。
def fun1():
fun1.a=3
def fun2():
fun1.a=0
fun2()
print(fun1.a)
fun1()
print(fun1.a)
输出结果是:0
3
>>>
fun1.a是全局变量。
接下来看关于annotate和subplot函数的用法。
subplot是绘制子图的。subplot(a,b,c)a是行数,b是列数,c是第几个
import numpy as np
import matplotlib.pyplot as plt
x = np.arange(-10, 11,1)#产生-10,-9...10的List
print(x)
y = x * x
plt.subplot(221)
plt.plot(x, y)
# 添加注释
# 第一个参数是注释的内容
# xy设置箭头尖的坐标
# xytext设置注释内容显示的起始位置
# arrowprops 用来设置箭头
# facecolor 设置箭头的颜色
# headlength 箭头的头的长度
# headwidth 箭头的宽度
# width 箭身的宽度
plt.annotate("This is a zhushi", xy = (0, 1), xytext = (-4, 50),\
arrowprops = dict(facecolor = "r", headlength = 5, headwidth = 10, width = 5))
# 可以通过设置xy和xytext中坐标的值来设置箭身是否倾斜
plt.subplot(223)#绘制2行2列的第三个图形
plt.scatter(x,y)
plt.show()这里注意arange和range的区别,arange()是numpy中的
函数说明:arange([start,] stop[, step,], dtype=None)根据start与stop指定的范围以及step设定的步长,
生成一个 ndarray。
range()函数
- 函数说明: range(start, stop[, step]) -> range object,根据start与stop指定的范围以及step设定的步长,生成一个序列。
参数含义:start:计数从start开始。默认是从0开始。例如range(5)等价于range(0, 5);
end:技术到end结束,但不包括end.例如:range(0, 5) 是[0, 1, 2, 3, 4]没有5
scan:每次跳跃的间距,默认为1。例如:range(0, 5) 等价于 range(0, 5, 1)
函数返回的是一个range object
import matplotlib.pyplot as plt
#定义决策树决策结果的属性 sawtooth 波浪方框 fc表示字体颜色的深浅 0.1~0.9 依次变浅
decisionNode = dict(boxstyle = 'sawtooth',fc = '0.8')
#定义决策树的叶子节点的属性 round4 矩形方框
leafNode = dict(boxstyle ="round4",fc="0.8")
#定义决策树箭头属性
arrow_args = dict(arrowstyle ="<-")
#nodeTxt为要显示的文本,centerPt为文本的中心点,箭头所在的点,
#parentPt为指向文本的点,nodeType给标题增加外框,ha是Horizontal alignment 水平对齐
#va是垂直对齐va和ha指定对其方式,可以是top,bottom,center或者left,right,center,
def plotNode(nodeTxt,centerPt,parentPt,nodeType,duiqi):
createPlot.ax1.annotate(nodeTxt,xy=parentPt,\
xytext=centerPt,va="top",ha=duiqi,bbox= nodeType,\
arrowprops=arrow_args)
#创建绘图
def createPlot():
fig = plt.figure(1,facecolor='white') #定义一个画布 背景为白色
fig.clf() #清空画布
#createPlot.ax1为全局变量,绘制图像的句柄
#frameon 是否绘制坐标轴矩形
createPlot.ax1 = plt.subplot(111,frameon=False)
#绘制节点
plotNode('decisionNode\nafafafa',(0.5,0.1),(0.1,0.5),decisionNode,"left")
plotNode('leafNode\nfafafa',(0.8,0.1),(0.3,0.8),leafNode,"center")
plt.show()
createPlot()
到这里,已经大致明白了如何绘制基本的带箭头的图形,和书上代码的意思。接下来就是绘制树了。
程序清单3-6 获取叶节点的数目和树的层数
def getNumLeafs(mytree):#获得叶子节点的数量
numLeafs=0;
keylist=list(mytree.keys())#这是因为python3改变了dict.keys,返回的是dict_keys对象,支持iterable
#但不支持indexable,我们可以将其明确的转化成list,则此项功能在3中应这样实现:
firstStr=keylist[0]#当前树的根节点
secondDict=mytree[firstStr]
for key in secondDict.keys():
if type(secondDict[key]).__name__=='dict':
numLeafs+=getNumLeafs(secondDict[key])
#{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
#firstStr='no surfing',secondDict.keys()有0,1
else: numLeafs+=1
return numLeafs从当前树的根节点开始出发,获得对应的键值,就是下一字典树。然后遍历该字典树的键,如果键的属性还是字典,证明不是叶子节点,递归下去,否则叶子结点数目+1.
这里是关于__name__的理解测试:'''x={"a":123,"b":{2:"q"}}
type(x['b'])
<class 'dict'>
type(x['a'])
<class 'int'>
type(x['b']).__name__ 注意是两个_ _
'dict'
type(x['a']).__name__
'int'
type(x['b'])=='dict'
False
type(x['b'])==dict
True'''
获取深度:DFS
def getTreeDepth(mytree):
maxDepth=0;
keylist=list(mytree.keys())
firstStr=keylist[0]#根节点
secondDict=mytree[firstStr]
for key in secondDict.keys():
if type(secondDict[key]).__name__=='dict':
thisDepth=1+getTreeDepth(secondDict[key])
else:thisDepth=1
if thisDepth>maxDepth:
maxDepth=thisDepth
return maxDepth提供数据:def retrieveTree(i):
listOfTrees = [
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}},
{'no surfacing': {0: 'no', 1: {'flippers': {0: {'head': {0: 'no', 1: 'yes'}}, 1: 'no'}}}}
]
return listOfTrees[i]程序清单3-7 plotTree
def plotNode(nodeTxt,centerPt,parentPt,nodeType):createPlot.ax1.annotate(nodeTxt,xy=parentPt,\ xytext=centerPt,va="top",ha="center",bbox= nodeType,\ arrowprops=arrow_args)#绘制节点和相应的箭头def PlotMidText(contrPt,parentPt,txtString):#绘制父节点和子节点的中间信息 xMid=(contrPt[0]+parentPt[0])/2.0# yMid=(contrPt[1]+parentPt[1])/2.0 createPlot.ax1.text(xMid,yMid,txtString)def plotTree(myTree,parentPt,nodeTxt):#当前树,父节点,父节点和当前节点箭头的txt,父节点的键 numLeafs=getNumLeafs(myTree)#获取当前叶子节点数量 depth=getTreeDepth(myTree) keylist=list(myTree.keys()) firstStr=keylist[0]#当前节点 contrPt=(plotTree.xoff+(1.0+numLeafs)/(2.0*plotTree.totalW),plotTree.yoff) PlotMidText(contrPt,parentPt,nodeTxt) plotNode(firstStr,contrPt,parentPt,decisionNode) secondDict=myTree[firstStr]#下一个字典,继续绘制节点 plotTree.yoff=plotTree.yoff-1.0/plotTree.totalD for key in secondDict.keys(): if type(secondDict[key]).__name__=='dict': plotTree(secondDict[key],contrPt,str(key)) else: plotTree.xoff=plotTree.xoff+1.0/plotTree.totalW#更新当前叶节点x坐标 plotNode(secondDict[key],(plotTree.xoff,plotTree.yoff),contrPt,leafNode) PlotMidText((plotTree.xoff,plotTree.yoff),contrPt,str(key)) plotTree.yoff=plotTree.yoff+1.0/plotTree.totalD
参考:http://blog.csdn.net/u012704941/article/details/73604625
先导:这里说一下为什么说一个递归树的绘制为什么会是很难懂,这里不就是利用递归函数来绘图么,就如递归计算树的深度、叶子节点一样,问题不是递归的思路,而是这本书中一些坐标的起始取值、以及在计算节点坐标所作的处理,而且在树中对这部分并没有取讲述,所以在看这段代码的时候可能大体思路明白但是具体的细节却知之甚少,所以本篇主要是对其中书中提及甚少的作详细的讲述,当然代码的整体思路也不会放过的
准备:这里说一下具体绘制的时候是利用自定义plotNode函数来绘制,这个函数一次绘制的是一个箭头和一个节点,如下图:
思路:这里绘图,作者选取了一个很聪明的方式,并不会因为树的节点的增减和深度的增减而导致绘制出来的图形出现问题,当然不能太密集。这里利用整棵树的叶子节点数作为份数将整个x轴的长度进行平均切分,利用树的深度作为份数将y轴长度作平均切分,并利用plotTree.xOff作为最近绘制的一个叶子节点的x坐标,当再一次绘制叶子节点坐标的时候才会plotTree.xOff才会发生改变;用plotTree.yOff作为当前绘制的深度,plotTree.yOff是在每递归一层就会减一份(上边所说的按份平均切分),其他时候是利用这两个坐标点去计算非叶子节点,这两个参数其实就可以确定一个点坐标,这个坐标确定的时候就是绘制节点的时候
整体算法的递归思路倒是很容易理解:
每一次都分三个步骤:
(1)绘制自身
(2)判断子节点非叶子节点,递归
(3)判断子节点为叶子节点,绘制
上边代码中红色部分如此处理原理:
首先由于整个画布根据叶子节点数和深度进行平均切分,并且x轴的总长度为1,即如同下图:
1、其中方形为非叶子节点的位置,@是叶子节点的位置,因此每份即上图的一个表格的长度应该为1/plotTree.totalW,但是叶子节点的位置应该为@所在位置,则在开始的时候plotTree.xOff的赋值为-0.5/plotTree.totalW,即意为开始x位置为第一个表格左边的半个表格距离位置,这样作的好处为:在以后确定@位置时候可以直接加整数倍的1/plotTree.totalW,
2、对于plotTree函数中的红色部分即如下:
cntrPt = (plotTree.xOff + (1.0 + float(numLeafs))/2.0/plotTree.totalW, plotTree.yOff)plotTree.xOff即为最近绘制的一个叶子节点的x坐标,在确定当前节点位置时每次只需确定当前节点有几个叶子节点,因此其叶子节点所占的总距离就确定了即为float(numLeafs)/plotTree.totalW*1(因为总长度为1),因此当前节点的位置即为其所有叶子节点所占距离的中间即一半为float(numLeafs)/2.0/plotTree.totalW*1,但是由于开始plotTree.xOff赋值并非从0开始,而是左移了半个表格,因此还需加上半个表格距离即为1/2/plotTree.totalW*1,则加起来便为(1.0 + float(numLeafs))/2.0/plotTree.totalW*1,因此偏移量确定,则x位置变为plotTree.xOff + (1.0 + float(numLeafs))/2.0/plotTree.totalW
3、对于plotTree函数参数赋值为(0.5, 1.0)
因为开始的根节点并不用划线,因此父节点和当前节点的位置需要重合,利用2中的确定当前节点的位置便为(0.5, 1.0)
总结:利用这样的逐渐增加x的坐标,以及逐渐降低y的坐标能能够很好的将树的叶子节点数和深度考虑进去,因此图的逻辑比例就很好的确定了,这样不用去关心输出图形的大小,一旦图形发生变化,函数会重新绘制,但是假如利用像素为单位来绘制图形,这样缩放图形就比较有难度了

