分級聚類——部落格分類 (畫出分級聚類樹狀圖)
《集體智慧程式設計》的第三章——發現組群 下面的測試資料可以在網上下載
通過分級聚類的方式將資料一層一層的聚類,最終聚類為一個大的物件。畫了一個樣例圖如下:
其中將A、B、C、D、E五個物件進行層級聚類,最終的聚類步驟上面已經標出(1,2,3,4)。
原理:迴圈遍歷所有物件,利用演算法計算物件點之間的距離,每次將最近的兩個物件聚為一類,直到得到最終的結果。
其實在程式碼中是定義了一個類,每個物件點都通過類來記錄它的重要資訊,然後儲存到一個列表中。while迴圈中巢狀兩層迴圈,每次找到兩個最近距離的物件點,將其聚類,然後計算兩者的關係,同樣使用上面的類來儲存資訊(記錄了左右節點),然後去除那兩個物件點,並加入兩者聚類後的新類。重複以上步驟,直到len(列表)等於1,表示獲得最終的層級聚類了。
下面直接上程式碼(程式碼很簡單,註釋很詳細):
樣例部分資料如下:
行為(部落格名);列為(關鍵字);中間資料為(出現次數)
#讀取文字並返回內容 將部落格資訊儲存到的文字 def readfile(filename): lines = [line for line in open(filename)] #第一行是列標題 colnames = lines[0].strip().split('\t')[1:] rownames = [] data = [] for line in lines[1:]: p = line.strip().split('\t') #每行的第一列是列名 rownames.append(p[0]) #剩餘部分就是該行對應的資料 data.append([float(x) for x in p[1:]]) return rownames,colnames,data #皮爾遜相關度求距離 from math import sqrt def pearson(v1,v2): #簡單求和 sum1 = sum(v1) sum2 = sum(v2) #求平方和 sum1Sq = sum([pow(v,2) for v in v1]) sum2Sq = sum([pow(v,2) for v in v2]) #求乘積和 pSum = sum([v1[i]*v2[i] for i in range(len(v1))]) #計算r (Pearson sum) num = pSum - (sum1*sum2/len(v1)) den = sqrt((sum1Sq - pow(sum1,2)/len(v1))*(sum2Sq - pow(sum2,2)/len(v1))) if(den == 0): return 0 return 1.0 - num/den #儲存每一個葉節點或聚類節點的屬性 class bicluster(object): def __init__(self,vec,left=None,right=None,distance=0.0,id=None): self.left=left self.right = right self.vec = vec self.id = id self.distance = distance #迴圈層級聚類,返回聚類完成的最終的一個大聚類 def hcluster(rows,distance=pearson): distances = {} currentclustid = -1 #最開始的聚類就是資料集中的行 clust = [bicluster(rows[i],id=i) for i in range(len(rows))] while(len(clust)>1): lowestpair = (0,1) closest = distance(clust[0].vec,clust[1].vec) #遍歷每一個配對,尋找最小距離 for i in range(len(clust)): for j in range(i+1,len(clust)): #利用distances來快取距離的計算值 if(clust[i].id,clust[j].id) not in distances: distances[(clust[i].id,clust[j].id)] = distance(clust[i].vec,clust[j].vec) d = distances[(clust[i].id,clust[j].id)] if(d<closest): closest = d lowestpair = (i,j) #計算兩個聚類的平均值 mergevec = [(clust[lowestpair[0]].vec[i] + clust[lowestpair[1]].vec[i])/2.0 for i in range(len(clust[0].vec))] #建立新的類 這裡雖然下面將兩個類刪除,但是這兩個都已經儲存到了新的類中。left=左點的所有資訊 想象一下雖然原始類刪除了,但合併的類越來越大,包含了前面合併的所有資訊 newcluster =bicluster(mergevec,left=clust[lowestpair[0]],right=clust[lowestpair[1]],distance=closest,id=currentclustid) #不在原始集合中的聚類,其id為負 currentclustid -=1 del clust[lowestpair[1]] del clust[lowestpair[0]] clust.append(newcluster) return clust[0] #呼叫函式 blognames,words,data = readfile('blogdata.txt') clust = hcluster(data) #簡單打印出聚類圖 def printclust(clust,labels=None,n=0): #列印正確,遞迴列印,從最終的這個層級開始遍歷樹打印出所有的id為正的原始部落格名,id為負則為合併的層級分支,並非原始資料 #利用縮排來建立層級佈局 for i in range(n): if(i<n-1): print('*',end='') else: print('*') if(clust.id<0): #負數標記代表這是一個分支 print(clust.id) print('#') else: #正數標記代表這是一個葉節點 if(labels == None): print(clust.id) else: print(clust.id) print(labels[clust.id]) #現在開始列印右側分支和左側分支 if(clust.left != None): printclust(clust.left,labels=labels,n=n+1) if(clust.right != None): printclust(clust.right,labels=labels,n=n+1) # printclust(clust,labels=blognames)
以上程式碼較多,但不是很難,基本都有註釋。最後一個函式是無關緊要的用來較為直觀的檢視聚類結果。
下面我們使用PIL畫圖,畫出聚類圖:
from PIL import Image,ImageDraw #繪製樹圖 #確定聚類的高度 def getheight(clust): #這是一個葉節點嗎?若是,則返回高度為1 if(clust.left == None and clust.right == None): return 1 #否則,遞迴求出 高度為每個分支的高度之和 return getheight(clust.left) + getheight(clust.right) # print(getheight(clust)) #99 #根節點的總體誤差 def getdepth(clust): #一個葉節點的距離是0.0 if(clust.left == None and clust.right ==None): return 0 #一個枝節點的距離等於左右兩側分支中距離較大者加上該枝節點自身的距離 return max(getdepth(clust.left),getdepth(clust.right)) + clust.distance #確定總的大圖,呼叫函式畫圖 def drawdendrogram(clust,labels,jpeg='clusters.jpg'): #高度和寬度 h = getheight(clust)*20 w = 1200 depth = getdepth(clust) #由於寬度是固定的,因此我們需要對距離值做響應的調整 scaling = float(w-150)/depth #新建一個白色背景的圖片 img = Image.new('RGB',(w,h),(255,255,255)) #建立對img操作的物件,類似於遊標 draw = ImageDraw.Draw(img) draw.line((0,h/2,10,h/2),fill=(255,0,0)) #(x1,y1,x2,y2) 以(x1,y1)為起點,(x2,y2)為終點畫一條線 顏色可以自己補充,有很多種定義方式fill是一種 'yellow' #畫第一個節點 drawnode(draw,clust,10,(h/2),scaling,labels) img.save(jpeg,'JPEG') #通過遞迴,使用傳入的引數進行畫圖 畫節點 def drawnode(draw,clust,x,y,scaling,labels): #畫圖物件,層級聚類物件,(x,y)起點,縮放因子,標籤 #小於0,表示為分支節點,所以畫線,再遞迴傳入分支節點的左右節點 if(clust.id<0): h1 = getheight(clust.left)*20 h2 = getheight(clust.right)*20 top = y-(h1+h2)/2 bottom = y+(h1+h2)/2 #線的長度 ll = clust.distance*scaling #聚類到其子節點的垂直線 draw.line((x,top+h1/2,x,bottom-h2/2),fill=(255,0,0)) #連線左側節點的水平線 draw.line((x,top+h1/2,x+ll,top+h1/2),fill=(255,0,0)) #連線右側節點的水平線 draw.line((x,bottom-h2/2,x+ll,bottom-h2/2),fill=(255,0,0)) #呼叫右側繪製左右節點 drawnode(draw,clust.left,x+ll,top+h1/2,scaling,labels) drawnode(draw,clust.right,x+ll,bottom-h2/2,scaling,labels) #大於0,表示為葉節點,所以直接畫圖就行 else: #如果這是一個葉節點,則繪製節點的標籤 draw.text((x+5,y-7),labels[clust.id],(0,0,0)) drawdendrogram(clust,blognames,jpeg='blogclust.jpg')
傳入上面的結果到drawdendrogram函式中,獲得jpg部落格聚類圖:
下面一個格外的思考,上面是將部落格進行聚類,我們可不可以同時對行和列(關鍵字)進行聚類呢?這樣我們就知道哪些關鍵字是容易劃分在一起的。類似於當我們做市場調研時對消費者群體進行分組很有意義,有助於我們摸清消費者的統計資訊和產品狀況,還可能有助與確定哪些商品是可以捆綁銷售的。
下面利用一個簡單的函式將資料進行轉置,就可以使用我們上面的聚類函數了:
#轉置資料區矩陣,將單詞轉換為行,聚類行(單詞)
def rotatematrix(data):
newdata = []
for i in range(len(data[0])):
newrow = [data[j][i] for j in range(len(data))]
newdata.append(newrow)
return newdata
rdata = rotatematrix(data)
wordclust = hcluster(rdata)
drawdendrogram(wordclust,labels=words,jpeg='wordclust.jpg')
對於關鍵字的聚類我們也使用drawdendrogram函式繪製了圖:
上面繪製的圖簡直就是藝術,真的很美,但是在其中做上下線條時,我有點沒懂作者的方法,我想的方法是比較簡單的。不過作者的方法畫出來的圖真的很美。
以上便是 層級聚類(部落格聚類) 的全部內容。