Как нарисовать дендрограмму в matplotlib без использования scipy? - PullRequest
0 голосов
/ 14 мая 2019

Я хотел бы использовать matplotlib для рисования дендрограммы без использования scipy. Подобный вопрос был размещен здесь ; однако отмеченное решение предлагает использовать scipy, а ссылки в других ответах, предлагающих использовать ETE, не работают. Используя этот пример , я проверил точность моего собственного метода (т. Е. Метода не 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

Используя эти параметры, я хотел бы создать дендрограмму, похожую на приведенную ниже (заимствовано из связанного примера):

example dendrogram

Моя попытка повторить эту дендрограмму с помощью 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)

Моя попытка, приведенная выше, приводит к следующей цифре:

attempted dendrogram

Я понимаю, что мне нужно изменить порядок xticklabels так, чтобы первые точки слияния появлялись на правом краю, а каждое последующее слияние сдвигалось влево; это обязательно означает корректировку ширины соединительных линий. Кроме того, я использовал ax.step вместо ax.bar, чтобы линии выглядели более организованными (в отличие от прямоугольных полос везде); единственное, что я могу сделать, - это рисовать горизонтальные и вертикальные линии, используя ax.axhline и ax.axvline. Я надеюсь, что есть более простой способ выполнить то, что я хотел бы. Есть ли прямой подход с использованием matplotlib?

1 Ответ

1 голос
/ 14 мая 2019

Хотя было бы определенно проще положиться на scipy, я бы сделал это «вручную», то есть без него:

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, который отображает исходные "местоположения" (т.е. (2,5)) в "порядок xtick" (т.е. (4,5)). «Форк» создается на каждом шаге i с использованием mk_fork(), который возвращает как points (которые впоследствии соединяются на линейном графике), так и точку connector, которая затем сохраняется как новые значения для 'xpos','ypos' в пределах label_map.

Я добавил несколько операторов print(), чтобы подчеркнуть, что происходит на каждом шаге, и добавил .text(), чтобы выделить местоположение каждого «соединителя».

Результат: a simple dendrogram

...