Quero definir o ponto médio de um mapa de cores, ou seja, meus dados vão de -5 a 10, quero que zero seja o meio. Acho que a maneira de fazer isso é normalizar a subclasse e usar a norma, mas não encontrei nenhum exemplo e não está claro para mim o que exatamente devo implementar.
python
matplotlib
Tiltstênio
fonte
fonte
Respostas:
Observe que no matplotlib versão 3.1 a classe DivergingNorm foi adicionada. Acho que cobre seu caso de uso. Ele pode ser usado assim:
from matplotlib import colors colors.DivergingNorm(vmin=-4000., vcenter=0., vmax=10000)
No matplotlib 3.2, a classe foi renomeada para TwoSlopesNorm
fonte
norm
faz a normalização para sua imagem.norms
ande de mãos dadas com os mapas de cores.TwoSlopeNorm
: matplotlib.org/3.2.0/api/_as_gen/…Sei que é tarde para o jogo, mas acabei de passar por esse processo e descobri uma solução que talvez seja menos robusta do que normalizar a criação de subclasses, mas muito mais simples. Achei que seria bom compartilhar aqui para a posteridade.
A função
import numpy as np import matplotlib import matplotlib.pyplot as plt from mpl_toolkits.axes_grid1 import AxesGrid def shiftedColorMap(cmap, start=0, midpoint=0.5, stop=1.0, name='shiftedcmap'): ''' Function to offset the "center" of a colormap. Useful for data with a negative min and positive max and you want the middle of the colormap's dynamic range to be at zero. Input ----- cmap : The matplotlib colormap to be altered start : Offset from lowest point in the colormap's range. Defaults to 0.0 (no lower offset). Should be between 0.0 and `midpoint`. midpoint : The new center of the colormap. Defaults to 0.5 (no shift). Should be between 0.0 and 1.0. In general, this should be 1 - vmax / (vmax + abs(vmin)) For example if your data range from -15.0 to +5.0 and you want the center of the colormap at 0.0, `midpoint` should be set to 1 - 5/(5 + 15)) or 0.75 stop : Offset from highest point in the colormap's range. Defaults to 1.0 (no upper offset). Should be between `midpoint` and 1.0. ''' cdict = { 'red': [], 'green': [], 'blue': [], 'alpha': [] } # regular index to compute the colors reg_index = np.linspace(start, stop, 257) # shifted index to match the data shift_index = np.hstack([ np.linspace(0.0, midpoint, 128, endpoint=False), np.linspace(midpoint, 1.0, 129, endpoint=True) ]) for ri, si in zip(reg_index, shift_index): r, g, b, a = cmap(ri) cdict['red'].append((si, r, r)) cdict['green'].append((si, g, g)) cdict['blue'].append((si, b, b)) cdict['alpha'].append((si, a, a)) newcmap = matplotlib.colors.LinearSegmentedColormap(name, cdict) plt.register_cmap(cmap=newcmap) return newcmap
Um exemplo
biased_data = np.random.random_integers(low=-15, high=5, size=(37,37)) orig_cmap = matplotlib.cm.coolwarm shifted_cmap = shiftedColorMap(orig_cmap, midpoint=0.75, name='shifted') shrunk_cmap = shiftedColorMap(orig_cmap, start=0.15, midpoint=0.75, stop=0.85, name='shrunk') fig = plt.figure(figsize=(6,6)) grid = AxesGrid(fig, 111, nrows_ncols=(2, 2), axes_pad=0.5, label_mode="1", share_all=True, cbar_location="right", cbar_mode="each", cbar_size="7%", cbar_pad="2%") # normal cmap im0 = grid[0].imshow(biased_data, interpolation="none", cmap=orig_cmap) grid.cbar_axes[0].colorbar(im0) grid[0].set_title('Default behavior (hard to see bias)', fontsize=8) im1 = grid[1].imshow(biased_data, interpolation="none", cmap=orig_cmap, vmax=15, vmin=-15) grid.cbar_axes[1].colorbar(im1) grid[1].set_title('Centered zero manually,\nbut lost upper end of dynamic range', fontsize=8) im2 = grid[2].imshow(biased_data, interpolation="none", cmap=shifted_cmap) grid.cbar_axes[2].colorbar(im2) grid[2].set_title('Recentered cmap with function', fontsize=8) im3 = grid[3].imshow(biased_data, interpolation="none", cmap=shrunk_cmap) grid.cbar_axes[3].colorbar(im3) grid[3].set_title('Recentered cmap with function\nand shrunk range', fontsize=8) for ax in grid: ax.set_yticks([]) ax.set_xticks([])
Resultados do exemplo:
fonte
start
estop
não forem 0 e 1 respectivamente, depois de fazer issoreg_index = np.linspace(start, stop, 257)
, você não pode mais assumir que o valor 129 é o ponto médio do cmap original, portanto, todo o reescalonamento não faz sentido sempre que você corta. Além disso,start
deve ser de 0 a 0,5 estop
de 0,5 a 1, não ambos de 0 a 1 como você instrui.midpoint
dos dados for igual a 0 ou 1. Veja minha resposta abaixo para uma solução simples para esse problema.Aqui está uma solução de subclasse Normalize. Para usá-lo
norm = MidPointNorm(midpoint=3) imshow(X, norm=norm)
Aqui está a aula:
import numpy as np from numpy import ma from matplotlib import cbook from matplotlib.colors import Normalize class MidPointNorm(Normalize): def __init__(self, midpoint=0, vmin=None, vmax=None, clip=False): Normalize.__init__(self,vmin, vmax, clip) self.midpoint = midpoint def __call__(self, value, clip=None): if clip is None: clip = self.clip result, is_scalar = self.process_value(value) self.autoscale_None(result) vmin, vmax, midpoint = self.vmin, self.vmax, self.midpoint if not (vmin < midpoint < vmax): raise ValueError("midpoint must be between maxvalue and minvalue.") elif vmin == vmax: result.fill(0) # Or should it be all masked? Or 0.5? elif vmin > vmax: raise ValueError("maxvalue must be bigger than minvalue") else: vmin = float(vmin) vmax = float(vmax) if clip: mask = ma.getmask(result) result = ma.array(np.clip(result.filled(vmax), vmin, vmax), mask=mask) # ma division is very slow; we can take a shortcut resdat = result.data #First scale to -1 to 1 range, than to from 0 to 1. resdat -= midpoint resdat[resdat>0] /= abs(vmax - midpoint) resdat[resdat<0] /= abs(vmin - midpoint) resdat /= 2. resdat += 0.5 result = ma.array(resdat, mask=result.mask, copy=False) if is_scalar: result = result[0] return result def inverse(self, value): if not self.scaled(): raise ValueError("Not invertible until scaled") vmin, vmax, midpoint = self.vmin, self.vmax, self.midpoint if cbook.iterable(value): val = ma.asarray(value) val = 2 * (val-0.5) val[val>0] *= abs(vmax - midpoint) val[val<0] *= abs(vmin - midpoint) val += midpoint return val else: val = 2 * (value - 0.5) if val < 0: return val*abs(vmin-midpoint) + midpoint else: return val*abs(vmax-midpoint) + midpoint
fonte
É mais fácil apenas usar os argumentos
vmin
evmax
paraimshow
(assumindo que você está trabalhando com dados de imagem) em vez de criar subclassesmatplotlib.colors.Normalize
.Por exemplo
import numpy as np import matplotlib.pyplot as plt data = np.random.random((10,10)) # Make the data range from about -5 to 10 data = 10 / 0.75 * (data - 0.25) plt.imshow(data, vmin=-10, vmax=10) plt.colorbar() plt.show()
fonte
Normalize
. Vou adicionar um exemplo daqui a pouco (supondo que outra pessoa não chegue antes de mim ...).vmax=abs(Z).max(), vmin=-abs(Z).max()
Aqui eu crio uma subclasse de
Normalize
seguido por um exemplo mínimo.import numpy as np import matplotlib as mpl import matplotlib.pyplot as plt class MidpointNormalize(mpl.colors.Normalize): def __init__(self, vmin, vmax, midpoint=0, clip=False): self.midpoint = midpoint mpl.colors.Normalize.__init__(self, vmin, vmax, clip) def __call__(self, value, clip=None): normalized_min = max(0, 1 / 2 * (1 - abs((self.midpoint - self.vmin) / (self.midpoint - self.vmax)))) normalized_max = min(1, 1 / 2 * (1 + abs((self.vmax - self.midpoint) / (self.midpoint - self.vmin)))) normalized_mid = 0.5 x, y = [self.vmin, self.midpoint, self.vmax], [normalized_min, normalized_mid, normalized_max] return np.ma.masked_array(np.interp(value, x, y)) vals = np.array([[-5., 0], [5, 10]]) vmin = vals.min() vmax = vals.max() norm = MidpointNormalize(vmin=vmin, vmax=vmax, midpoint=0) cmap = 'RdBu_r' plt.imshow(vals, cmap=cmap, norm=norm) plt.colorbar() plt.show()
Resultado:
O mesmo exemplo apenas com dados positivos
vals = np.array([[1., 3], [6, 10]])
Propriedades:
vmin
for maior do quemidpoint
(não testei todos os casos extremos).Esta solução é inspirada em uma classe com o mesmo nome desta página
fonte
def __call__
)normalized_min
enormalized_max
são considerados inteiros. Basta colocá-los como 0,0. Além disso, para obter a saída correta de sua figura, tive que usarvals = sp.array([[-5.0, 0.0], [5.0, 10.0]])
. Obrigado pela resposta, de qualquer maneira!Não tenho certeza se você ainda está procurando uma resposta. Para mim, tentar criar uma subclasse
Normalize
não teve sucesso. Então, me concentrei em criar manualmente um novo conjunto de dados, ticks e tick-labels para obter o efeito que acho que você deseja.Eu encontrei o
scale
módulo em matplotlib que tem uma classe usada para transformar plotagens de linha pelas regras 'syslog', então eu uso isso para transformar os dados. Em seguida, dimensiono os dados para que vão de 0 a 1 (o queNormalize
geralmente acontece), mas dimensiono os números positivos de maneira diferente dos números negativos. Isso ocorre porque seus vmax e vmin podem não ser os mesmos, então 0,5 -> 1 pode cobrir um intervalo positivo maior do que 0,5 -> 0, o intervalo negativo cobre. Foi mais fácil para mim criar uma rotina para calcular os valores de escala e rótulo.Abaixo está o código e uma figura de exemplo.
import numpy as np import matplotlib.pyplot as plt import matplotlib.mpl as mpl import matplotlib.scale as scale NDATA = 50 VMAX=10 VMIN=-5 LINTHRESH=1e-4 def makeTickLables(vmin,vmax,linthresh): """ make two lists, one for the tick positions, and one for the labels at those positions. The number and placement of positive labels is different from the negative labels. """ nvpos = int(np.log10(vmax))-int(np.log10(linthresh)) nvneg = int(np.log10(np.abs(vmin)))-int(np.log10(linthresh))+1 ticks = [] labels = [] lavmin = (np.log10(np.abs(vmin))) lvmax = (np.log10(np.abs(vmax))) llinthres = int(np.log10(linthresh)) # f(x) = mx+b # f(llinthres) = .5 # f(lavmin) = 0 m = .5/float(llinthres-lavmin) b = (.5-llinthres*m-lavmin*m)/2 for itick in range(nvneg): labels.append(-1*float(pow(10,itick+llinthres))) ticks.append((b+(itick+llinthres)*m)) # add vmin tick labels.append(vmin) ticks.append(b+(lavmin)*m) # f(x) = mx+b # f(llinthres) = .5 # f(lvmax) = 1 m = .5/float(lvmax-llinthres) b = m*(lvmax-2*llinthres) for itick in range(1,nvpos): labels.append(float(pow(10,itick+llinthres))) ticks.append((b+(itick+llinthres)*m)) # add vmax tick labels.append(vmax) ticks.append(b+(lvmax)*m) return ticks,labels data = (VMAX-VMIN)*np.random.random((NDATA,NDATA))+VMIN # define a scaler object that can transform to 'symlog' scaler = scale.SymmetricalLogScale.SymmetricalLogTransform(10,LINTHRESH) datas = scaler.transform(data) # scale datas so that 0 is at .5 # so two seperate scales, one for positive and one for negative data2 = np.where(np.greater(data,0), .75+.25*datas/np.log10(VMAX), .25+.25*(datas)/np.log10(np.abs(VMIN)) ) ticks,labels=makeTickLables(VMIN,VMAX,LINTHRESH) cmap = mpl.cm.jet fig = plt.figure() ax = fig.add_subplot(111) im = ax.imshow(data2,cmap=cmap,vmin=0,vmax=1) cbar = plt.colorbar(im,ticks=ticks) cbar.ax.set_yticklabels(labels) fig.savefig('twoscales.png')
Sinta-se à vontade para ajustar as "constantes" (por exemplo
VMAX
) na parte superior do script para confirmar se ele se comporta bem.fonte
Eu estava usando a excelente resposta de Paul H, mas tive um problema porque alguns dos meus dados variaram de negativo a positivo, enquanto outros conjuntos variaram de 0 a positivo ou de negativo a 0; em ambos os casos, eu queria que 0 fosse colorido como branco (o ponto médio do mapa de cores que estou usando). Com a implementação existente, se seu
midpoint
valor for igual a 1 ou 0, os mapeamentos originais não foram substituídos. Você pode ver isso na imagem a seguir: A 3ª coluna parece correta, mas a área azul escura na 2ª coluna e a área vermelha escura nas colunas restantes devem ser todas brancas (seus valores de dados são, na verdade, 0). Usar minha correção me dá: Minha função é essencialmente a mesma de Paul H, com minhas edições no início dofor
loop:def shiftedColorMap(cmap, min_val, max_val, name): '''Function to offset the "center" of a colormap. Useful for data with a negative min and positive max and you want the middle of the colormap's dynamic range to be at zero. Adapted from /programming/7404116/defining-the-midpoint-of-a-colormap-in-matplotlib Input ----- cmap : The matplotlib colormap to be altered. start : Offset from lowest point in the colormap's range. Defaults to 0.0 (no lower ofset). Should be between 0.0 and `midpoint`. midpoint : The new center of the colormap. Defaults to 0.5 (no shift). Should be between 0.0 and 1.0. In general, this should be 1 - vmax/(vmax + abs(vmin)) For example if your data range from -15.0 to +5.0 and you want the center of the colormap at 0.0, `midpoint` should be set to 1 - 5/(5 + 15)) or 0.75 stop : Offset from highets point in the colormap's range. Defaults to 1.0 (no upper ofset). Should be between `midpoint` and 1.0.''' epsilon = 0.001 start, stop = 0.0, 1.0 min_val, max_val = min(0.0, min_val), max(0.0, max_val) # Edit #2 midpoint = 1.0 - max_val/(max_val + abs(min_val)) cdict = {'red': [], 'green': [], 'blue': [], 'alpha': []} # regular index to compute the colors reg_index = np.linspace(start, stop, 257) # shifted index to match the data shift_index = np.hstack([np.linspace(0.0, midpoint, 128, endpoint=False), np.linspace(midpoint, 1.0, 129, endpoint=True)]) for ri, si in zip(reg_index, shift_index): if abs(si - midpoint) < epsilon: r, g, b, a = cmap(0.5) # 0.5 = original midpoint. else: r, g, b, a = cmap(ri) cdict['red'].append((si, r, r)) cdict['green'].append((si, g, g)) cdict['blue'].append((si, b, b)) cdict['alpha'].append((si, a, a)) newcmap = matplotlib.colors.LinearSegmentedColormap(name, cdict) plt.register_cmap(cmap=newcmap) return newcmap
EDIT: Eu tive um problema semelhante mais uma vez quando alguns dos meus dados variaram de um valor positivo pequeno a um valor positivo maior, onde os valores muito baixos estavam sendo coloridos em vermelho em vez de branco. Eu consertei adicionando uma linha
Edit #2
no código acima.fonte
Se você não se importa em calcular a proporção entre vmin, vmax e zero, este é um mapa linear bastante básico de azul para branco e vermelho, que define o branco de acordo com a proporção
z
:def colormap(z): """custom colourmap for map plots""" cdict1 = {'red': ((0.0, 0.0, 0.0), (z, 1.0, 1.0), (1.0, 1.0, 1.0)), 'green': ((0.0, 0.0, 0.0), (z, 1.0, 1.0), (1.0, 0.0, 0.0)), 'blue': ((0.0, 1.0, 1.0), (z, 1.0, 1.0), (1.0, 0.0, 0.0)) } return LinearSegmentedColormap('BlueRed1', cdict1)
O formato cdict é bastante simples: as linhas são pontos no gradiente que é criado: a primeira entrada é o valor x (a proporção ao longo do gradiente de 0 a 1), o segundo é o valor final para o segmento anterior, e o terceiro é o valor inicial para o próximo segmento - se você quiser gradientes suaves, os dois últimos são sempre os mesmos. Veja a documentação para mais detalhes.
fonte
LinearSegmentedColormap.from_list()
tuplas(val,color)
e passá-las como lista para ocolor
argumento deste método ondeval0=0<val1<...<valN==1
.Tive um problema semelhante, mas queria que o valor mais alto fosse totalmente vermelho e eliminasse os valores baixos de azul, fazendo com que parecesse essencialmente que a parte inferior da barra de cores foi cortada. Isso funcionou para mim (inclui transparência opcional):
def shift_zero_bwr_colormap(z: float, transparent: bool = True): """shifted bwr colormap""" if (z < 0) or (z > 1): raise ValueError('z must be between 0 and 1') cdict1 = {'red': ((0.0, max(-2*z+1, 0), max(-2*z+1, 0)), (z, 1.0, 1.0), (1.0, 1.0, 1.0)), 'green': ((0.0, max(-2*z+1, 0), max(-2*z+1, 0)), (z, 1.0, 1.0), (1.0, max(2*z-1,0), max(2*z-1,0))), 'blue': ((0.0, 1.0, 1.0), (z, 1.0, 1.0), (1.0, max(2*z-1,0), max(2*z-1,0))), } if transparent: cdict1['alpha'] = ((0.0, 1-max(-2*z+1, 0), 1-max(-2*z+1, 0)), (z, 0.0, 0.0), (1.0, 1-max(2*z-1,0), 1-max(2*z-1,0))) return LinearSegmentedColormap('shifted_rwb', cdict1) cmap = shift_zero_bwr_colormap(.3) x = np.arange(0, np.pi, 0.1) y = np.arange(0, 2*np.pi, 0.1) X, Y = np.meshgrid(x, y) Z = np.cos(X) * np.sin(Y) * 5 + 5 plt.plot([0, 10*np.pi], [0, 20*np.pi], color='c', lw=20, zorder=-3) plt.imshow(Z, interpolation='nearest', origin='lower', cmap=cmap) plt.colorbar()
fonte