Как отрегулировать длины ветвей дендрограммы в matplotlib (как в astrodendro)? [Python]

Вот мой Результирующий график ниже, но я хотел бы, чтобы это выглядело как усеченные дендрограммы в astrodendro, такие как это:

enter image description here

Существует также очень классная дендрограмма из этой статьи, которую я хотел бы воссоздать в matplotlib.

enter image description here

Ниже приведен код для создания набора iris с шумовыми переменными и построения дендрограммы в matplotlib.

Кто-нибудь знает, как либо: (1) обрезать ветки, как в примерах; и/или (2) использовать astrodendro с пользовательской матрицей сцепления и ярлыками?

import pandas as pd
import numpy as np
from sklearn.datasets import load_iris
import astrodendro
from scipy.cluster.hierarchy import dendrogram, linkage
from scipy.spatial import distance

def iris_data(noise=None, palette="hls", desat=1):
    # Iris dataset
    X = pd.DataFrame(load_iris().data,
                     index = [*map(lambda x:f"iris_{x}", range(150))],
                     columns = [*map(lambda x: x.split(" (cm)")[0].replace(" ","_"), load_iris().feature_names)])

    y = pd.Series(load_iris().target,
                           index = X.index,
                           name = "Species")
    c = map_colors(y, mode=1, palette=palette, desat=desat)#y.map(lambda x:{0:"red",1:"green",2:"blue"}[x])

    if noise is not None:
        X_noise = pd.DataFrame(
            np.random.RandomState(0).normal(size=(X.shape[0], noise)),
            index=X_iris.index,
            columns=[*map(lambda x:f"noise_{x}", range(noise))]
        )
        X = pd.concat([X, X_noise], axis=1)
    return (X, y, c)

def dism2linkage(DF_dism, method="ward"):
    """
    Input: A (m x m) dissimalrity Pandas DataFrame object where the diagonal is 0
    Output: Hierarchical clustering encoded as a linkage matrix

    Further reading:
    http://docs.scipy.org/doc/scipy-0.14.0/reference/generated/scipy.cluster.hierarchy.linkage.html
    https://pypi.python.org/pypi/fastcluster
    """
    #Linkage Matrix
    Ar_dist = distance.squareform(DF_dism.as_matrix())
    return linkage(Ar_dist,method=method)


# Get data
X_iris_with_noise, y_iris, c_iris = iris_data(50)
# Get distance matrix
df_dism = 1- X_iris_with_noise.corr().abs()
# Get linkage matrix
Z = dism2linkage(df_dism)

#Create dendrogram
with plt.style.context("seaborn-white"):
    fig, ax = plt.subplots(figsize=(13,3))
    D_dendro = dendrogram(
             Z, 
             labels=df_dism.index,
             color_threshold=3.5,
             count_sort = "ascending",
             #link_color_func=lambda k: colors[k]
             ax=ax
    )
    ax.set_ylabel("Distance")

enter image description here

Ответ 1

Я не уверен, что это действительно представляет собой практический ответ, но он позволяет вам генерировать дендрограммы с усеченными подвесными линиями. Хитрость заключается в том, чтобы генерировать график как нормальный, а затем манипулировать полученным графиком matplotlib для воссоздания строк.

Я не мог заставить ваш пример работать локально, поэтому я только что создал фиктивный набор данных.

from matplotlib import pyplot as plt
from scipy.cluster.hierarchy import dendrogram, linkage
import numpy as np

a = np.random.multivariate_normal([0, 10], [[3, 1], [1, 4]], size=[5,])
b = np.random.multivariate_normal([0, 10], [[3, 1], [1, 4]], size=[5,])
X = np.concatenate((a, b),)

Z = linkage(X, 'ward')

fig = plt.figure()
ax = fig.add_subplot(1,1,1)

dendrogram(Z, ax=ax)

Результирующий график - обычная длинношерстная дендрограмма.

Standard dendrogram image, generated from random data

Теперь за более интересный бит. Дендрограмма состоит из нескольких объектов LineCollection (по одному для каждого цвета). Для обновления строки мы перебираем их, извлекая подробности об их составных путях, изменяющие их, чтобы удалить все строки, идущие к y нуля, а затем воссоздает LineCollection для этих модифицированных путей.

Обновленный путь затем добавляется к осям, а оригинал удаляется.

Одна сложная часть - определить, какую высоту нужно рисовать вместо нуля. Поскольку мы итерируем по каждому пути дендрограмм, мы не знаем, какой момент пришел раньше - мы в принципе понятия не имеем, где мы. Однако мы можем использовать тот факт, что висячие линии висят вертикально. Предполагая, что на одном и том же x нет строк, мы можем искать известные другие значения y для данного x и использовать это в качестве основы для нашего нового y при вычислении. Недостатком является то, что для того, чтобы убедиться, что у нас есть это число, мы должны предварительно сканировать данные.

Примечание. Если вы можете получить висячие линии dendrogram на одном и том же x, вам нужно будет включить y и выполнить поиск ближайшего y выше этого x для этого.

import numpy as np
from matplotlib.path import Path
from matplotlib.collections import LineCollection

fig = plt.figure()
ax = fig.add_subplot(1,1,1)

dendrogram(Z, ax=ax);

for c in ax.collections[:]: # use [:] to get a copy, since we're adding to the same list
    paths = []
    for path in c.get_paths():
        segments = []
        y_at_x = {}
        # Pre-pass over all elements, to find the lowest y value at each x value.
        # we can use this to caculate where to cut our lines.
        for n, seg in enumerate(path.iter_segments()):
            x, y = seg[0]
            # Don't store if the y is zero, or if it higher than the current low.
            if y > 0 and y < y_at_x.get(x, np.inf):
                y_at_x[x] = y

        for n, seg in enumerate(path.iter_segments()):
            x, y = seg[0]

            if y == 0:
                # If we know the last y at this x, use it - 0.5, limit > 0
                y = max(0, y_at_x.get(x, 0) - 0.5)

            segments.append([x,y])

        paths.append(segments)

    lc = LineCollection(paths, colors=c.get_colors())  # Recreate a LineCollection with the same params
    ax.add_collection(lc)
    ax.collections.remove(c) # Remove the original LineCollection

Результирующая дендрограмма выглядит так:

Dendrogram danglies