《集体智慧编程》的第三章——发现组群 下面的测试数据可以在网上下载
通过分级聚类的方式将数据一层一层的聚类,最终聚类为一个大的对象。画了一个样例图如下:
其中将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函数绘制了图:
上面绘制的图简直就是艺术,真的很美,但是在其中做上下线条时,我有点没懂作者的方法,我想的方法是比较简单的。不过作者的方法画出来的图真的很美。
以上便是 层级聚类(博客聚类) 的全部内容。