以書上例子爲基礎(按照整個程序的調用順序總結):
首先列出樹的數據,兩組樹的數據組成的列表,分別是listOfTrees[0]以及listOfTrees[1]:
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]
開始繪圖,繪圖函數:
def createPlot(inTree):
fig = plt.figure(1, facecolor='white')
fig.clf()
axprops = dict(xticks=[], yticks=[]) #去除座標軸顯示,也可以選擇顯示哪些點,如plt.xticks([5,6]),或者ax1.set_xticks([5,6])
createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)
plotTree.totalW = float(getNumLeafs(inTree)) #plotTree是個函數,函數是對象可隨時添加公共屬性,totalW就是加入的一個屬性,plotTree.totalW爲葉節點的總數
plotTree.totalD = float(getTreeDepth(inTree)) #plotTree.totalD樹的深度,這兩個是不變的全局變量,就表示整個樹的深度和寬度
plotTree.xOff = -0.5 / plotTree.totalW
plotTree.yOff = 1.0
plotTree( inTree, (0.5, 1.0), '' )
plt.show()
下面開始逐條分析上述函數及其對應的調用函數:
1.
fig = plt.figure(1, facecolor='white')
創建一個畫布1,考慮到默認變量的全局性,必須指定1,方便下面的操作也在畫布1上進行,底色設置爲白色
2.
fig.clf()
清除之前的畫布上的圖像,這個跟內存有關係
3.
axprops = dict(xticks=[], yticks=[])
不顯示座標軸,有待解決
4.
createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)
createPlot是定義的函數名,是一個對象,只要是對象就可以定義公共屬性,createPlot.ax1中的.ax1就是一個公共屬性5.
plotTree.totalW = float(getNumLeafs(inTree))
plotTree是定義的函數名,是一個對象,只要是對象就可以定義公共屬性,createPlot.ax1中的.ax1就是一個公共屬性,相當於是一個變量名,只要定義過的函數,都可以函數名.xxx作爲變量名,這條程序調用了getNumLeafs函數,函數如下:
def getNumLeafs(myTree): #獲取葉節點數目
numLeafs = 0
firstStr = list( myTree.keys() )[0] #把樹轉換成關鍵字列表,此時列表中只有一個關鍵字,因爲是第一個分支點
secondDict = myTree[firstStr] #獲取關鍵字(第一個問題)下的內容,至少有一個回答和一個結果,所以內容至少是{0:1}這樣的形式
for key in secondDict.keys(): #遍尋第一個問題的所有回答,即第一個關鍵字下的字典的關鍵字
if type(secondDict[key]).__name__ == 'dict': #判斷下一級是不是還是字典, .__name__作用是將類型名稱變爲str
numLeafs += getNumLeafs(secondDict[key]) #葉節點的數目等於所有最後一級的總數目
else: #比如第一個問題有2個分支,1個分支到底了+1,另一個分支又分出2個分支,2個分支都到底了,+2,一共是3
numLeafs += 1 #按程序步驟是,第一個關鍵字不符合if,+1,第二個關鍵字進入getNumLeafs(secondDict[key]),兩個分支都不符合if,return2
return numLeafs #即getNumLeafs(secondDict[key])是2,最後結果是3
函數注意點:
myTree.keys()在Python3中不是list,而是dict_keys,需要用函list()轉換成列表才能用位置標號取用
6.
plotTree.totalD = float(getTreeDepth(inTree))
這條程序調用了getTreeDepth函數,函數如下:
def getTreeDepth(myTree): #獲取樹的層數
maxDepth = 0
firstStr = list( myTree.keys() )[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: #key尋遍所有關鍵字,第一個關鍵字可能層數只有1,但是第二個可能是2層
maxDepth = thisDepth #加入一個比較,取層數最多的那個就是樹的層數
return maxDepth
7.
plotTree.xOff = -0.5 / plotTree.totalW
plotTree.yOff = 1.0
plotTree( inTree, (0.5, 1.0), '' )
難點分析參考下面這篇文章:
http://blog.csdn.net/qq_25974431/article/details/79083628
plotTree( inTree, (0.5, 1.0), '' )
調用時給的3個數據是,起始的完整樹的數據,根的座標(一定是在0.5,1.0位置),以及一個空的字符串,因爲第一次畫圖實際上起點是(0.5,1.0),終點也是(0.5,1.0),在繪製樹形圖中,父級是起點,子級是終點,而樹根自己到自己不需要字符,所以第三個參數給了空字符串plotTree函數:
def plotTree(myTree, parentPt, nodeTxt):
numLeafs = getNumLeafs(myTree) #獲取當前節點下的葉節點總個數,後面遞歸myTree會變化
depth = getTreeDepth(myTree) #獲取當前樹的深度
firstStr = list( myTree.keys() )[0] #第一個問題,即獲取樹根
cntrPt = ( plotTree.xOff + (1.0 + float(numLeafs)) / 2.0 / plotTree.totalW, plotTree.yOff )
#x軸的座標由前一個位置確定當前位置,第一次是由初始位置確定
plotMidText(cntrPt, parentPt, nodeTxt) #父子之間加文本
plotNode(firstStr, cntrPt, parentPt, decisionNode) #指定文本內容,終點,起點,文本框類型
secondDict = myTree[firstStr] #提取字典下一層內容
plotTree.yOff = plotTree.yOff - 1.0 / plotTree.totalD #樹的深度往下走一級,樹的深度不計算樹根,y軸被分爲plotTree.totalD,每層高度1.0 / plotTree.totalD
for key in secondDict.keys():
if type( secondDict[key] ).__name__ == 'dict':
plotTree( secondDict[key], cntrPt, str(key) ) #遞歸,繪製下一層
else:
plotTree.xOff = plotTree.xOff + 1.0 / plotTree.totalW #如果不是字典,那肯定是一個節點,這個節點的x座標位置距離上一個節點1.0 / plotTree.totalW
plotNode( secondDict[key], (plotTree.xOff, plotTree.yOff), cntrPt, leafNode )
plotMidText( (plotTree.xOff, plotTree.yOff), cntrPt, str(key) )
plotTree.yOff = plotTree.yOff + 1.0 / plotTree.totalD
理解:
cntrPt = ( plotTree.xOff + (1.0 + float(numLeafs)) / 2.0 / plotTree.totalW, plotTree.yOff )
下面調用plotMidText函數:
plotMidText(cntrPt, parentPt, nodeTxt)
def plotMidText(cntrPt, parentPt, txtString): #起始點終止點之間的中點加文本
#書上的寫法囉嗦,(2,2)和(4,4)的重點直接(4+2)*0.5就行了,不用寫成2+(4-2)*0.5
xMid = ( parentPt[0] + cntrPt[0] ) / 2.0 #這樣寫更方便
yMid = ( parentPt[1] + cntrPt[1] ) / 2.0
fig = plt.figure(1)
ax1 = fig.add_subplot(111,frameon=False)
ax1.set_xticks([])
ax1.set_yticks([])
ax1.text(xMid, yMid, txtString) #在(xMid,yMid)位置加上文本內容txtString
#text()作用:將文本放置在軸域的任意位置
在程序最前面定義的幾個變量
decisionNode = dict( boxstyle = 'sawtooth', fc = '0.8' ) #boxstyle爲文本框類型,sawtooth爲鋸齒形
leafNode = dict( boxstyle = 'round4', fc = '0.8' ) #round4爲長方圓形,fc是邊框線粗細
arrow_args = dict( arrowstyle = '<-' ) #arrowstyle爲箭頭的樣式
下面調用plotNode函數:
plotNode(firstStr, cntrPt, parentPt, decisionNode) #樹根是非葉節點,所以用decisionNode
def plotNode(nodeTxt, centerpt, parentPt, nodeType):
fig = plt.figure(1)
ax1 = fig.add_subplot(111, frameon=False)
ax1.set_xticks([])
ax1.set_yticks([])
ax1.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction', xytext=centerpt,\
textcoords='axes fraction', va='center', ha='center', bbox=nodeType,\
arrowprops=arrow_args)
annotate括號中,nodeTxt是文本內容,xy是起點,xytext是終點,bbox是文本框類型,arrowprops是箭頭類型,調用完後相當於完成了對樹根的繪製
繪製完樹根後下面進行數據的更新:
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], cntrPt, str(key) ) #遞歸,繪製下一層
else:
plotTree.xOff = plotTree.xOff + 1.0 / plotTree.totalW #如果不是字典,那肯定是一個節點,這個節點的x座標位置距離上一個節點1.0 / plotTree.totalW
plotNode( secondDict[key], (plotTree.xOff, plotTree.yOff), cntrPt, leafNode )
plotMidText( (plotTree.xOff, plotTree.yOff), cntrPt, str(key) )
判斷如果是字典的話,secondDict爲第二層字典( 樹至少是2個字典,{ 問題1:{ 0:1, 1:0 } } )
plotTree( secondDict[key], cntrPt, str(key) ) #遞歸,繪製下一層
遞歸時,之前的cntrPt終點被當作起點,即從樹根出發的意思,str(key)則是要加在父子級之間的文本,key就是對樹根的回答,要轉化爲字符串
plotTree.xOff = plotTree.xOff + 1.0 / plotTree.totalW
plotNode( secondDict[key], (plotTree.xOff, plotTree.yOff), cntrPt, leafNode )
plotMidText( (plotTree.xOff, plotTree.yOff), cntrPt, str(key) )
如果不是字典,那這個回答下就是一個葉子節點,這個葉子節點的座標距離上一個節點座標的距離是一個葉間距,就是 (1/ 葉節點個數)這麼大的距離,因爲這個繪製的是第一個葉子節點的X座標,上一個座標是預先設定好的-0.5*頁間距
然後設置樣式,繪製該節點,並在父子級中點加入回答文本
最後加
plotTree.yOff = plotTree.yOff + 1.0 / plotTree.totalD #作用是y軸座標回到上一層位置
語句位置需要注意,是在循環外面,作用以2張圖的對比來說明
畫這張圖的整個過程應該是第一個for循環畫出第一層,第一層的第二個關鍵字下還是字典,所以是節點,遞歸畫出第二層,先尋到關鍵字head,head關鍵字下的內容還是字典,所以遞歸進入第三層,遍尋後畫出2個葉子節點,這個時候,在第二次遞歸後樹的y軸已經到0了,如果在第二次遞歸的最後y軸不反回上一層,就會出現上圖中右側的情況,在第二次遞歸畫完兩個節點後,y軸返回上一層,然後進入第一次遞歸中尋到關鍵字no,是一個葉子節點,此時該次遞歸中兩個key遍尋完了,在第一次遞歸中執行最後一條語句,y軸再返回上一層,第一次遞歸也結束了,此時非遞歸的2個key也遍尋完了,最後再執行一次非遞歸中的plotTree.yOff
= plotTree.yOff + 1.0 / plotTree.totalD,此時y的位置返回到1
最後如果要顯示沒有座標沒有邊框的圖,程序中每個定義圖紙的地方都要如此寫:
fig = plt.figure(1)
ax1 = fig.add_subplot(111, frameon=False)
ax1.set_xticks([])
ax1.set_yticks([])