如何在不使用 scipy 的情况下在 matplotlib 中绘制树状图?

How to draw dendrogram in matplotlib without using scipy?

我想在不使用 scipy 的情况下使用 matplotlib 绘制树状图。一个类似的问题has been posted here; however, the marked solution suggests using scipy and the links in the other answers suggesting using ETE do not work. Using this example,我已经验证了我自己的方法(即不是scipy方法)使用单链接准则应用凝聚层次聚类的准确性。

使用上面链接的相同示例,我有必要的参数来创建我自己的树状图。原 distance_matrix 由:

 .. DISTANCE MATRIX (SHAPE=(6, 6)):
[[  0 662 877 255 412 996]
 [662   0 295 468 268 400]
 [877 295   0 754 564   0]
 [255 468 754   0 219 869]
 [412 268 564 219   0 669]
 [996 400   0 869 669   0]]

使用了 distance_matrix 的掩码数组,这样上面的对角线条目就不会被算作最小值。原来distance_matrix的掩码由:

给出
 .. MASKED (BEFORE) DISTANCE MATRIX (SHAPE=(6, 6)):
[[-- 662 877 255 412 996]
 [662 -- 295 468 268 400]
 [877 295 -- 754 564 0]
 [255 468 754 -- 219 869]
 [412 268 564 219 -- 669]
 [996 400 0 869 669 --]]

distance_matrix 在算法的每次迭代中就地更改。算法完成后,distance_matrix 由以下公式给出:

 .. MASKED (AFTER) DISTANCE MATRIX (SHAPE=(1, 1)):
[[--]]

级别(每次合并的最小距离)由以下公式给出:

 .. 5 LEVELS:
[138, 219, 255, 268, 295]

我们还可以在每次迭代时查看合并数据点的索引;这些索引对应于原始的 distance_matrix,因为减少维度具有改变索引位置的效果。这些指数由:

 .. 5x2 LOCATIONS:
[(2, 5), (3, 4), (0, 3), (0, 1), (0, 2)]

根据这些索引,树状图的 xticklabels 的顺序按时间顺序给出:

.. 6 XTICKLABELS
[2 5 3 4 0 1]

关于链接示例,

0 = BA
1 = FI 
2 = MI 
3 = NA 
4 = RM 
5 = TO

使用这些参数,我想生成一个如下所示的树状图(借用链接示例):

我尝试使用 matplotlib 复制此树状图的尝试如下:

fig, ax = plt.subplots()
for loc, level in zip(locations, levels):
    x = np.array(loc)
    y = level * np.ones(x.size)
    ax.step(x, y, where='mid')
    ax.set_xticks(xticklabels)
    # ax.set_xticklabels(xticklabels)
    plt.show()
    plt.close(fig)

我上面的尝试产生了下图:

我意识到我必须重新排序 xticklabels 以便第一个合并点出现在右边缘,随后的每个合并点都向左移动;这样做必然意味着调整连接线的宽度。另外,我使用 ax.step 而不是 ax.bar 这样线条看起来更有条理(与到处都是矩形条相反);我唯一能想到的就是使用 ax.axhlineax.axvline 绘制水平线和垂直线。我希望有一种更简单的方法来完成我想要的。有使用 matplotlib 的直接方法吗?

虽然肯定更容易依赖 scipy,但我会这样做 "manually",即没有它:

import matplotlib.pyplot as plt
import numpy as np

def mk_fork(x0,x1,y0,y1,new_level):
    points=[[x0,x0,x1,x1],[y0,new_level,new_level,y1]]
    connector=[(x0+x1)/2.,new_level]
    return (points),connector

levels=[138, 219, 255, 268, 295]
locations=[(2, 5), (3, 4), (0, 3), (0, 1), (0, 2)]
label_map={
    0:{'label':'BA','xpos':0,'ypos':0},
    1:{'label':'FI','xpos':3,'ypos':0},
    2:{'label':'MI','xpos':4,'ypos':0},
    3:{'label':'NA','xpos':1,'ypos':0},
    4:{'label':'RM','xpos':2,'ypos':0},
    5:{'label':'TO','xpos':5,'ypos':0},
}

fig,ax=plt.subplots()

for i,(new_level,(loc0,loc1)) in enumerate(zip(levels,locations)):

    print('step {0}:\t connecting ({1},{2}) at level {3}'.format(i, loc0, loc1, new_level ))

    x0,y0=label_map[loc0]['xpos'],label_map[loc0]['ypos']
    x1,y1=label_map[loc1]['xpos'],label_map[loc1]['ypos']

    print('\t points are: {0}:({2},{3}) and {1}:({4},{5})'.format(loc0,loc1,x0,y0,x1,y1))

    p,c=mk_fork(x0,x1,y0,y1,new_level)

    ax.plot(*p)
    ax.scatter(*c)

    print('\t connector is at:{0}'.format(c))


    label_map[loc0]['xpos']=c[0]
    label_map[loc0]['ypos']=c[1]
    label_map[loc0]['label']='{0}/{1}'.format(label_map[loc0]['label'],label_map[loc1]['label'])
    print('\t updating label_map[{0}]:{1}'.format(loc0,label_map[loc0]))

    ax.text(*c,label_map[loc0]['label'])

_xticks=np.arange(0,6,1)
_xticklabels=['BA','NA','RM','FI','MI','TO']

ax.set_xticks(_xticks)
ax.set_xticklabels(_xticklabels)

ax.set_ylim(0,1.05*np.max(levels))

plt.show()

这主要依赖于创建字典 label_map,它将原始 "locations"(即 (2,5))映射到 "xtick order"(即 (4,5)) .在每个步骤 i 中使用 mk_fork() 创建了一个 "fork",其中 returns points(随后在线图中连接)以及 connector 点,然后将其作为 'xpos','ypos' 的新值存储在 label_map.

我添加了多个 print() 语句来强调每一步发生的事情,并添加了一个 .text() 来突出每个 "connector".

的位置

结果: