在 imshow 的边缘添加条形图,使条形图与单元格对齐

Adding bar plots at the margins of imshow, keeping bars aligned to cells

我正在尝试在 imshow 图的顶部添加一个条形图,在右侧添加另一个条形图,条形图与 imshow“单元格”对齐。

我已经尝试过使用本例 adding histograms at the margins of a scatterplot) 中使用的方法和 make_axes_locatable.

我得到的结果如图所示。有两个问题我无法解决:

  1. imshow 图的实际大小小于我绘制它的轴的大小,因为我想保持矩阵纵横比,所以实际图将严格包含在轴
  2. 即使这不是问题(见上图),条形图也不会与 imshow 单元格对齐。

这是我的代码

# from mpl_toolkits.axes_grid1 import make_axes_locatable

plt.style.use('dark_background')

m = np.random.rand(25, 200)

# definitions for the axes
left, width = 0.1, 0.65
bottom, height = 0.1, 0.65
spacing = 0.005

rect0 = [left, bottom, width, height]
rect1 = [left, bottom + height + spacing, width, 0.2]
rect2 = [left + width + spacing, bottom, 0.2, height]

# start with a rectangular Figure
fig = plt.figure(figsize=(20, 8))

ax0 = plt.axes(rect0)
ax0.tick_params(direction='in', top=True, right=True)
ax1 = plt.axes(rect1)
ax1.tick_params(direction='in', labelbottom=False)
ax2 = plt.axes(rect2)
ax2.tick_params(direction='in', labelleft=False)

ax0.matshow(m, norm=matplotlib.colors.LogNorm())

# divider = make_axes_locatable(ax)
# cax = divider.append_axes('right', size='95%', pad=0)
ax1.bar(np.arange(m.shape[1]), np.apply_along_axis(scipy.stats.entropy, 0, m))

# divider = make_axes_locatable(ax)
# cax = divider.append_axes('bottom', size='95%', pad=0)
ax2.barh(np.arange(m.shape[0]), np.apply_along_axis(scipy.stats.entropy, 1, m), orientation='horizontal')
plt.savefig('/data/l989o/a/so.png')
plt.style.use('default')

编辑 尝试向绘图添加细节,如轴标签或色标,我发现一般情况可能更复杂。我添加了用于添加其他绘图元素的更一般情况的代码以及代码。

请注意,我注意到我必须反转右侧的条形图,因为在使用 orientation=horizontal 时,条形图的顺序与图像的其中一行相反。

# from mpl_toolkits.axes_grid1 import make_axes_locatable
import functools

plt.style.use('dark_background')
m = np.random.rand(58, 226) * 20
# definitions for the axes
left, width = 0.1, 0.65
bottom, height = 0.1, 0.65
spacing = 0.005

rect0 = [left, bottom, width, height]
rect1 = [left, bottom + height + spacing, width, 0.2]
rect2 = [left + width + spacing, bottom, 0.2, height]

# start with a rectangular Figure
fig = plt.figure(figsize=(20, 8))

ax0 = plt.axes(rect0)
ax0.tick_params(direction='in', top=True, right=True)
ax1 = plt.axes(rect1)
ax1.tick_params(direction='in', labelbottom=False)
ax2 = plt.axes(rect2)
ax2.tick_params(direction='in', labelleft=False)

t = 10
n = 2
cmap = matplotlib.colors.LinearSegmentedColormap.from_list(None, plt.cm.Set1(range(0, n)), n)
im = ax0.imshow(m > t, cmap=cmap)
ax0.set_xlabel('image')
ax0.set_ylabel('cluster label')

divider = make_axes_locatable(ax0)
cax = divider.append_axes('left', size='1%', pad=1)
cbar = fig.colorbar(im, ticks=range(n), cax=cax)
# cbar.set_lim(-0.5, n - 0.5)
cbar.ax.tick_params(length=0)
cbar.set_ticks([0.25, 0.75])
cbar.set_ticklabels([f'<= {t}', f'> {t}'])
cbar.ax.set_title('# cells')

# divider = make_axes_locatable(ax)
# cax = divider.append_axes('right', size='95%', pad=0)
def sum_treshold(v, threshold):
    return np.sum(v > threshold)
ax1.bar(np.arange(m.shape[1]), np.apply_along_axis(functools.partial(sum_treshold, threshold=t), 0, m))
ax1.set_xlim([0, m.shape[1]])

# divider = make_axes_locatable(ax)
# cax = divider.append_axes('bottom', size='95%', pad=0)
ax2.barh(np.arange(m.shape[0])[::-1], np.apply_along_axis(functools.partial(sum_treshold, threshold=t), 1, m), orientation='horizontal')
ax2.set_ylim([0, m.shape[0]])
plt.savefig('/data/l989o/a/so.png')
plt.style.use('default')

编辑 2 这是最终输出的示例。为了获得这一点,我进行了疯狂的二进制搜索并设置了硬编码坐标(这当然只适用于我拥有的特定数据矩阵,而不适用于一般情况)。

我不确定这是否会为您提供您想要的确切布局,但这里的一些内容可能会有所帮助。

此答案使用 gridspec 定义子图的相对比率,使用 inset_axestransform 来添加颜色条。 @Marc 的回答 是一个很好的简单示例,说明如何使用 gridspec,如果那部分令人困惑的话。

import matplotlib.pyplot as plt
%matplotlib inline
import matplotlib.gridspec as gridspec
import matplotlib.colors as mcolors
import numpy as np

m = np.random.rand(58, 226) * 20

fig = plt.figure(figsize=(20,8), constrained_layout=True)
gs = fig.add_gridspec(2, 3)
ax1 = fig.add_subplot(gs[0, 0:2])
ax2 = fig.add_subplot(gs[1, 0:2]) 
## can add these if you need to share axes:, sharex = ax1, sharey = ax1)
ax3 = fig.add_subplot(gs[:, -1])

ax1.bar(np.arange(m.shape[1]), np.arange(m.shape[1]))
vals = ax2.imshow(np.random.random((20,10)), cmap='rainbow', aspect='auto') 
## aspect = 'auto' follows the established gridpec space
## default for imshow is equal axis
ax3.barh(np.arange(m.shape[1]), np.arange(m.shape[1]))

cbax2=ax2.inset_axes([1.05,0,0.03,1], transform=ax2.transAxes)
## the inset axes inputs are x,y,width,height
## the transform "anchors" these relative to the ax2 axis
## so here we are saying start at 5% past the ax2 width; start at the bottom of ax2 (y=0);
### make the inset axis 3% as wide as the ax2 axis; and make it 100% as tall as the ax2 axis
cbar2=fig.colorbar(vals, cax=cbax2, format = '%1.2g', orientation='vertical')

根据评论更新: 这是否更接近您需要的答案?

import matplotlib.pyplot as plt
%matplotlib inline
import matplotlib.gridspec as gridspec
import matplotlib.colors as mcolors
import numpy as np
import functools

def sum_treshold(v, threshold):
    return np.sum(v > threshold)

m = np.random.rand(58, 226) * 20
t = 10
n = 2
cmap = mcolors.LinearSegmentedColormap.from_list(None, plt.cm.Set1(range(0, n)), n)

fig = plt.figure(figsize=(20,8), constrained_layout=True)
gs = fig.add_gridspec(2, 3)
ax1 = fig.add_subplot(gs[0, 0:2])
ax2 = fig.add_subplot(gs[1, 0:2], sharex = ax1, sharey = ax1)
ax3 = fig.add_subplot(gs[1, -1], sharey = ax1)

ax1.bar(np.arange(m.shape[1]), 
        np.apply_along_axis(functools.partial(sum_treshold, threshold=t), 0, m))
ax1.set_xlim([0, m.shape[1]])
im = ax2.imshow(m > t, cmap=cmap)
ax3.barh(np.arange(m.shape[0])[::-1], 
         np.apply_along_axis(functools.partial(sum_treshold, threshold=t), 1, m), 
         orientation='horizontal')
ax3.set_ylim([0, m.shape[0]])


cbax2=ax2.inset_axes([-0.10,0,0.03,1], transform=ax2.transAxes)
cbar2=fig.colorbar(im, cax=cbax2, format = '%1.2g', orientation='vertical')

由于您已经计算了轴的所有尺寸,因此您可以调整它们以遵循图像纵横比施加的限制。您需要通过更改 height 或更改图形高度来更改 ax0 的高度。如果宽高比在其他方面是错误的,则需要对宽度做类似的事情。

要添加颜色条,您需要从一开始就保留一些space,或者将其放在右上角的空白处。

这是一个示例,现在包括 space 用于左侧的颜色栏,底部移动到绘图的中心。现在子图之间的间距相等。

import matplotlib.pyplot as plt
import matplotlib
import numpy as np

plt.style.use('dark_background')

m = np.random.randn(25, 200).cumsum(axis=0).cumsum(axis=1)
m -= m.min()
m *= 20 / m.max()

# definitions for the axes
left, width = 0.1, 0.65
bottom, height = 0.1, 0.65
spacing = 0.005
width_2 = 0.2
height_1 = 0.2

fig_width, fig_height = 20, 8
aspect_m = m.shape[0] / m.shape[1]
aspect_rect = fig_height * height / (fig_width * width)
if aspect_m < aspect_rect:  # either reduce the fig_height, or reduce adapt rectangle height
    new_height = aspect_m * (fig_width * width) / fig_height
    # optionally increase height_1 and/or increase bottom
    bottom += (height - new_height) / 2
    height = new_height
else:  # similar for the width
    width = fig_height * height / fig_width / aspect_m
    # optionally increase width_2 and/or increase left
rect0 = [left, bottom, width, height]
rect1 = [left, bottom + height + spacing, width, height_1]
rect2 = [left + width + spacing * fig_height / fig_width, bottom, width_2, height]
rectcbar = [left - 0.06, bottom, 0.01, height]

fig = plt.figure(figsize=(fig_width, fig_height))

ax0 = plt.axes(rect0)
ax0.tick_params(direction='in', top=True, right=True)
ax1 = plt.axes(rect1)
ax1.tick_params(direction='in', labelbottom=False)
ax2 = plt.axes(rect2)
ax2.tick_params(direction='in', labelleft=False)
cbarax = plt.axes(rectcbar)

t = 10
cmap = matplotlib.colors.ListedColormap(['red', 'dodgerblue'])
im = ax0.imshow(m > t, cmap=cmap, origin='lower')
ax0.set_xlabel('image')
ax0.set_ylabel('cluster label')

cbar = fig.colorbar(im, cax=cbarax)
cbar.ax.tick_params(length=0)
cbar.set_ticks([0.25, 0.75])
cbar.ax.set_yticklabels([f'≤ {t}', f'> {t}'])
cbar.ax.set_title('# cells')

ax1.bar(np.arange(m.shape[1]), np.sum(m > t, axis=0))

ax2.barh(np.arange(m.shape[0]), np.sum(m > t, axis=1))

ax0.set_aspect('equal')
ax1.get_shared_x_axes().join(ax1, ax0)
ax2.get_shared_y_axes().join(ax2, ax0)
plt.show()