決策樹——繪圖的全過程

以書上例子爲基礎(按照整個程序的調用順序總結):

首先列出樹的數據,兩組樹的數據組成的列表,分別是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 )


這條程序是終點的座標:x軸座標爲:前一個點X座標 + (1 + 當前節點下的葉子節點總數) * 0.5 * 葉間距,其中0.5 * 葉間距就是半頁距,可以理解爲:  前一個位置x座標 + (1+頁節點個數) * 半頁距       y軸座標是當前的plotTree.yOff,用書上3.6的圖爲例,第一次是要確定樹根的位置,x座標是  -0.5*葉距 + (1+3)*半頁距,y軸座標是1,每下降一層,y軸座標就減一層,樹根不算入層數



下面調用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([])






















發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章