Je veux définir le point central d'une palette de couleurs, c'est-à-dire que mes données vont de -5 à 10, je veux que zéro soit le milieu. Je pense que la façon de le faire est de sous-classer la normalisation et d'utiliser la norme, mais je n'ai trouvé aucun exemple et je ne sais pas exactement ce que je dois implémenter.
python
matplotlib
tillsten
la source
la source
Réponses:
Notez que dans matplotlib version 3.1, la classe DivergingNorm a été ajoutée. Je pense que cela couvre votre cas d'utilisation. Il peut être utilisé comme ceci:
from matplotlib import colors colors.DivergingNorm(vmin=-4000., vcenter=0., vmax=10000)
Dans matplotlib 3.2, la classe a été renommée TwoSlopesNorm
la source
norm
fait la normalisation de votre image.norms
aller de pair avec les couleurs.TwoSlopeNorm
: matplotlib.org/3.2.0/api/_as_gen/…Je sais que c'est tard dans le jeu, mais je viens de passer par ce processus et j'ai trouvé une solution qui peut-être moins robuste que le sous-classement normaliser, mais beaucoup plus simple. J'ai pensé que ce serait bien de le partager ici pour la postérité.
La fonction
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
Un exemple
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([])
Résultats de l'exemple:
la source
start
etstop
ne sont pas respectivement 0 et 1, après celareg_index = np.linspace(start, stop, 257)
, vous ne pouvez plus supposer que la valeur 129 est le point médian de la cmap d'origine, par conséquent, la remise à l'échelle entière n'a aucun sens chaque fois que vous recadrez. En outre,start
devrait être de 0 à 0,5 etstop
de 0,5 à 1, pas les deux de 0 à 1 comme vous le demandez.midpoint
des données est égal à 0 ou 1. Voir ma réponse ci-dessous pour une solution simple à ce problème.Voici une solution sous-classant Normaliser. Pour l'utiliser
norm = MidPointNorm(midpoint=3) imshow(X, norm=norm)
Voici la classe:
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
la source
Il est plus facile d'utiliser simplement les arguments
vmin
etvmax
pourimshow
(en supposant que vous travaillez avec des données d'image) plutôt que de sous-classermatplotlib.colors.Normalize
.Par exemple
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()
la source
Normalize
. J'ajouterai un exemple dans un instant (en supposant que quelqu'un d'autre ne me bat pas dessus ...).vmax=abs(Z).max(), vmin=-abs(Z).max()
Ici, je crée une sous-classe de
Normalize
suivi d'un exemple minimal.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()
Résultat:
Le même exemple avec seulement des données positives
vals = np.array([[1., 3], [6, 10]])
Propriétés:
vmin
est plus grand quemidpoint
(n'a pas testé tous les cas de bord cependant).Cette solution est inspirée d'une classe du même nom de cette page
la source
def __call__
)normalized_min
etnormalized_max
sont pris comme des entiers. Mettez-les simplement comme 0,0. Aussi, pour obtenir la sortie correcte de votre figure, j'ai dû utiliservals = sp.array([[-5.0, 0.0], [5.0, 10.0]])
. Merci pour la réponse, en tout cas!Je ne sais pas si vous cherchez toujours une réponse. Pour moi, essayer de sous-classer
Normalize
a échoué. Je me suis donc concentré sur la création manuelle d'un nouvel ensemble de données, de graduations et d'étiquettes de graduation pour obtenir l'effet que je pense que vous visez.J'ai trouvé le
scale
module dans matplotlib qui a une classe utilisée pour transformer les tracés de lignes par les règles 'syslog', donc je l'utilise pour transformer les données. Ensuite, je redimensionne les données pour qu'elles passent de 0 à 1 (ceNormalize
qui fait généralement), mais je redimensionne les nombres positifs différemment des nombres négatifs. C'est parce que votre vmax et vmin peuvent ne pas être les mêmes, donc .5 -> 1 peut couvrir une plage positive plus grande que 0,5 -> 0, la plage négative le fait. Il était plus facile pour moi de créer une routine pour calculer les valeurs de graduation et d'étiquette.Vous trouverez ci-dessous le code et un exemple de figure.
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')
N'hésitez pas à ajuster les "constantes" (par exemple
VMAX
) en haut du script pour confirmer qu'il se comporte bien.la source
J'utilisais l'excellente réponse de Paul H, mais j'ai rencontré un problème car certaines de mes données allaient de négatives à positives, tandis que d'autres ensembles allaient de 0 à positif ou de négatif à 0; dans les deux cas, je voulais que 0 soit coloré en blanc (le point médian de la palette de couleurs que j'utilise). Avec l'implémentation existante, si votre
midpoint
valeur est égale à 1 ou 0, les mappages d'origine n'étaient pas écrasés. Vous pouvez le voir dans l'image suivante: La 3ème colonne semble correcte, mais la zone bleu foncé dans la 2ème colonne et la zone rouge foncé dans les colonnes restantes sont toutes supposées être blanches (leurs valeurs de données sont en fait 0). Utiliser mon correctif me donne: Ma fonction est essentiellement la même que celle de Paul H, avec mes modifications au début de lafor
boucle: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: J'ai rencontré à nouveau un problème similaire lorsque certaines de mes données allaient d'une petite valeur positive à une valeur positive plus grande, où les valeurs très faibles étaient colorées en rouge au lieu de blanc. Je l'ai corrigé en ajoutant une ligne
Edit #2
dans le code ci-dessus.la source
Si cela ne vous dérange pas de calculer le rapport entre vmin, vmax et zéro, il s'agit d'une jolie carte linéaire de base allant du bleu au blanc au rouge, qui définit le blanc en fonction du rapport
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)
Le format cdict est assez simple: les lignes sont des points dans le dégradé qui est créé: la première entrée est la valeur x (le rapport le long du dégradé de 0 à 1), la seconde est la valeur finale du segment précédent, et la troisième est la valeur de départ du segment suivant - si vous voulez des dégradés lisses, les deux derniers sont toujours les mêmes. Consultez la documentation pour plus de détails.
la source
LinearSegmentedColormap.from_list()
tuples(val,color)
et de les passer sous forme de liste à l'color
argument de cette méthode oùval0=0<val1<...<valN==1
.J'ai eu un problème similaire, mais je voulais que la valeur la plus élevée soit entièrement rouge et coupe les faibles valeurs de bleu, ce qui donne l'impression que le bas de la barre de couleurs a été coupé. Cela a fonctionné pour moi (inclut la transparence facultative):
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()
la source