Python中用于自定义SciPy树形图聚类颜色的群集(link_color_func?)

19

我想使用一个以字典形式表示的颜色映射(即{leaf: color})来为我的聚类结果着色。

我尝试按照https://joernhees.de/blog/2015/08/26/scipy-hierarchical-clustering-and-dendrogram-tutorial/的教程进行,但是出现了混乱的颜色。默认的绘图效果还不错,我只是想分配不同的颜色。我看到有一个link_color_func,但是当我尝试使用我的颜色映射(D_leaf_color字典)时,由于它不是函数,因此出现了错误。我创建了D_leaf_color来自定义与特定聚类关联的叶子的颜色。在我的实际数据集中,这些颜色具有特定意义,因此我不想使用任意的颜色分配。

我不想使用color_threshold,因为在我的实际数据中,我有更多的聚类,SciPy会重复使用颜色,因此出现了这个问题...

如何使用我的叶子颜色字典自定义树状图聚类的颜色?

我在GitHub上提出了一个问题https://github.com/scipy/scipy/issues/6346,其中进一步阐述了在解释SciPy的层次聚类树状图输出?(也许发现了一个bug...)中着色叶子的方法,但是我仍然无法弄清楚如何实际执行以下操作:(i)使用树状图输出重构我的树状图,并使用指定的颜色字典,或者(ii)重新格式化我的D_leaf_color字典以适用于link_color_func参数。

# Init
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns; sns.set()

# Load data
from sklearn.datasets import load_diabetes

# Clustering
from scipy.cluster.hierarchy import dendrogram, fcluster, leaves_list
from scipy.spatial import distance
from fastcluster import linkage # You can use SciPy one too

%matplotlib inline

# Dataset
A_data = load_diabetes().data
DF_diabetes = pd.DataFrame(A_data, columns = ["attr_%d" % j for j in range(A_data.shape[1])])

# Absolute value of correlation matrix, then subtract from 1 for disimilarity
DF_dism = 1 - np.abs(DF_diabetes.corr())

# Compute average linkage
A_dist = distance.squareform(DF_dism.as_matrix())
Z = linkage(A_dist,method="average")

# Color mapping
D_leaf_colors = {"attr_1": "#808080", # Unclustered gray

                 "attr_4": "#B061FF", # Cluster 1 indigo
                 "attr_5": "#B061FF",
                 "attr_2": "#B061FF",
                 "attr_8": "#B061FF",
                 "attr_6": "#B061FF",
                 "attr_7": "#B061FF",

                 "attr_0": "#61ffff", # Cluster 2 cyan
                 "attr_3": "#61ffff",
                 "attr_9": "#61ffff",
                 }

# Dendrogram
# To get this dendrogram coloring below  `color_threshold=0.7`
D = dendrogram(Z=Z, labels=DF_dism.index, color_threshold=None, leaf_font_size=12, leaf_rotation=45, link_color_func=D_leaf_colors)
# TypeError: 'dict' object is not callable

输入图像描述

我还尝试了如何获取由scipy.cluster.hierarchy生成的树状图的子树


我无法从您的描述中确定您希望总体上得到什么样的树状图(即,对于任意叶子颜色字典)。据我所知,仅仅指定叶子的颜色是没有意义的,因为您无法保证给出相同颜色的叶子会在树状图中靠近彼此。树状图中着色的不是叶子,而是聚类之间的链接。您是否以某种方式从聚类中生成了leaf_colors?如果是这样,难道您不能从聚类中生成链接颜色吗? - BrenBarn
这是正确的,但我创建叶子颜色字典的方式是使用fcluster获得实际聚类。 - O.rka
但是你不能使用类似的逻辑来获取链接并根据这些链接指定颜色吗?你不能仅基于fcluster获得颜色,因为fcluster仅返回平面聚类,并且丢弃有关较低级别聚类的信息。您需要完整的链接结构。 - BrenBarn
fcluster 中获得一个长度为 n 的数组,其中 n 是我正在进行聚类的样本数量。该数组的每个索引都有簇编号。我同时迭代该数组和原始标签,将样本分配到簇中。 - O.rka
1
但是,你是否看到树状图包含的信息比这多得多?树状图不仅指示单个平面聚类集合。它显示了每个聚类何时与其他聚类合并的完整“历史记录”。每个拱形代表两个聚类的合并,因此无论您提供什么颜色信息,都必须提供有关聚类对的信息,而不仅仅是单个“根”聚类或单个叶节点。如果您只关心最终聚类,则甚至可能根本不需要使用树状图。 - BrenBarn
是的,这是真的。我想将它与树状图配对以进行可视化。对我来说,从叶标签开始向后推导出从距离截止生成的聚类最容易可视化。我理解算法的工作原理,但要从一开始就使用链接而不是标签,我需要重新做很多包装。事后看来,我应该从那种方式开始。我使用了Ulrich的帮助将叶子颜色字典转换为链接颜色字典。 - O.rka
4个回答

15

这里有一个解决方案,它使用linkage() 的返回矩阵Z(在文档中早先描述,但有点隐藏)和link_color_func:

# see question for code prior to "color mapping"

# Color mapping
dflt_col = "#808080"   # Unclustered gray
D_leaf_colors = {"attr_1": dflt_col,

                 "attr_4": "#B061FF", # Cluster 1 indigo
                 "attr_5": "#B061FF",
                 "attr_2": "#B061FF",
                 "attr_8": "#B061FF",
                 "attr_6": "#B061FF",
                 "attr_7": "#B061FF",

                 "attr_0": "#61ffff", # Cluster 2 cyan
                 "attr_3": "#61ffff",
                 "attr_9": "#61ffff",
                 }

# notes:
# * rows in Z correspond to "inverted U" links that connect clusters
# * rows are ordered by increasing distance
# * if the colors of the connected clusters match, use that color for link
link_cols = {}
for i, i12 in enumerate(Z[:,:2].astype(int)):
  c1, c2 = (link_cols[x] if x > len(Z) else D_leaf_colors["attr_%d"%x]
    for x in i12)
  link_cols[i+1+len(Z)] = c1 if c1 == c2 else dflt_col

# Dendrogram
D = dendrogram(Z=Z, labels=DF_dism.index, color_threshold=None,
  leaf_font_size=12, leaf_rotation=45, link_color_func=lambda x: link_cols[x])

以下是输出结果: 树状图


1
奇怪。x 应该只访问 link_cols 中已经设置的键。你能打印出 Z[:,:2].astype(int) 吗? - Ulrich Stern
1
D_leaf_colors 是否可能导致关键错误? - Ulrich Stern
1
让我们在聊天中继续这个讨论 - Ulrich Stern
1
你有描述这个 bug 的链接吗? - Ulrich Stern
1
你的代码正确地将“压缩型”(“n选2”)距离矩阵传递给了linkage()函数。请参考此答案 - Ulrich Stern
显示剩余5条评论

7

应用自定义颜色映射到聚类分支的两行代码:

import matplotlib as mpl
from matplotlib.pyplot import cm
from scipy.cluster import hierarchy

cmap = cm.rainbow(np.linspace(0, 1, 10))
hierarchy.set_link_color_palette([mpl.colors.rgb2hex(rgb[:3]) for rgb in cmap])

您可以将rainbow替换为任何cmap,并将10更改为您想要的聚类数。


似乎O.rka在将他在字典中定义的颜色应用到图表中遇到了问题。也许你可以调整你的最小工作示例来向他展示如何实现这一点。 - NOhs
没错,我在寻找如何应用自定义色图,但是找不到简单的解决方案。希望这对于正在寻找那个特别的东西的人有所帮助 ;) - alelouis

0

这个答案有所帮助,但将其转化为更一般的情况并不容易 - 这里是一个运行scipy凝聚聚类并绘制相应树状图的函数,使用自定义颜色,针对给定距离阈值:

def rgb_hex(color):
    '''converts a (r,g,b) color (either 0-1 or 0-255) to its hex representation.
    for ambiguous pure combinations of 0s and 1s e,g, (0,0,1), (1/1/1) is assumed.'''
    message='color must be an iterable of length 3.'
    assert hasattr(color, '__iter__'), message
    assert len(color)==3, message
    if all([(c<=1)&(c>=0) for c in color]): color=[int(round(c*255)) for c in color] # in case provided rgb is 0-1
    color=tuple(color)
    return '#%02x%02x%02x' % color

def get_cluster_colors(n_clusters, my_set_of_20_rgb_colors, alpha=0.8, alpha_outliers=0.05):
    cluster_colors = my_set_of_20_rgb_colors
    cluster_colors = [c+[alpha] for c in cluster_colors]
    outlier_color = [0,0,0,alpha_outliers]
    return [cluster_colors[i%19] for i in range(n_clusters)] + [outlier_color]

def cluster_and_plot_dendrogram(X, threshold, method='ward', metric='euclidean', default_color='black'):

    # perform hierarchical clustering
    Z              = hierarchy.linkage(X, method=method, metric=metric)

    # get cluster labels
    labels         = hierarchy.fcluster(Z, threshold, criterion='distance') - 1
    labels_str     = [f"cluster #{l}: n={c}\n" for (l,c) in zip(*np.unique(labels, return_counts=True))]
    n_clusters     = len(labels_str)

    cluster_colors = [rgb_hex(c[:-1]) for c in get_cluster_colors(n_clusters, alpha=0.8, alpha_outliers=0.05)]
    cluster_colors_array = [cluster_colors[l] for l in labels]
    link_cols = {}
    for i, i12 in enumerate(Z[:,:2].astype(int)):
        c1, c2 = (link_cols[x] if x > len(Z) else cluster_colors_array[x] for x in i12)
        link_cols[i+1+len(Z)] = c1 if c1 == c2 else 'k'

    # plot dendrogram with colored clusters
    fig = plt.figure(figsize=(12, 5))
    plt.title('Hierarchical Clustering Dendrogram')
    plt.xlabel('Data points')
    plt.ylabel('Distance')

    # plot dendrogram based on clustering results
    hierarchy.dendrogram(
        Z,

        labels = labels,

        color_threshold=threshold,

        truncate_mode = 'level',
        p = 5,
        show_leaf_counts = True,
        leaf_rotation=90,
        leaf_font_size=10,
        show_contracted=False,

        link_color_func=lambda x: link_cols[x],
        above_threshold_color=default_color,
        distance_sort='descending',
        ax=plt.gca()
    )
    plt.axhline(threshold, color='k')
    for i, s in enumerate(labels_str):
        plt.text(0.8, 0.95-i*0.04, s,
                transform=plt.gca().transAxes,
                va='top', color=cluster_colors[i])
    
    fig.patch.set_facecolor('white')

    return labels # 0 indexed

这将返回聚类标签,并生成如下图所示的绘图:enter image description here

希望这能帮助未来的某个人。


这个很好,但出于某种原因它没有保持单例和给定聚类中单例数量之间的区别。由于某种原因,在样本大小为1的多个聚类中输出相同的索引。 - TunaFishLies

-1
我找到了一个不太正式的解决方案,需要使用颜色阈值(但如果不使用它,颜色不会与OP中呈现的颜色相同),但这可能会让你走向解决方案。然而,你可能没有足够的信息来知道如何设置颜色调色板顺序。
# Init
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns; sns.set()

# Load data
from sklearn.datasets import load_diabetes

# Clustering
from scipy.cluster.hierarchy import dendrogram, fcluster, leaves_list, set_link_color_palette
from scipy.spatial import distance
from fastcluster import linkage # You can use SciPy one too

%matplotlib inline
# Dataset
A_data = load_diabetes().data
DF_diabetes = pd.DataFrame(A_data, columns = ["attr_%d" % j for j in range(A_data.shape[1])])

# Absolute value of correlation matrix, then subtract from 1 for disimilarity
DF_dism = 1 - np.abs(DF_diabetes.corr())

# Compute average linkage
A_dist = distance.squareform(DF_dism.as_matrix())
Z = linkage(A_dist,method="average")

# Color mapping dict not relevant in this case
# Dendrogram
# To get this dendrogram coloring below  `color_threshold=0.7`
#Change the color palette, I did not include the grey, which is used above the threshold
set_link_color_palette(["#B061FF", "#61ffff"])
D = dendrogram(Z=Z, labels=DF_dism.index, color_threshold=.7, leaf_font_size=12, leaf_rotation=45, 
               above_threshold_color="grey")

结果:

enter image description here


你运行了这个答案吗?link_color_func=getcolor 抛出了一个 KeyError - wflynny
是的,我刚刚弄明白了。已经纠正了。索引从10到18。可能对应于属性1到9,属性的映射不正确,但这就是解决方案... - rll
这不正确。索引 n + 1n + n(这里是10到18)对应于联接矩阵 Z 中的聚类。 - wflynny
1
至少我现在更好地理解了这个问题 :) - rll

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接