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 normaliser les sous-classes et d'utiliser la norme, mais je n'ai trouvé aucun exemple et ce n'est pas clair pour moi, ce que je dois implémenter exactement.
Voici une solution sous-classant Normaliser. Pour l'utiliser
norm = MidPointNorm(midpoint=3)
imshow(X, norm=norm)
Voici la classe:
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 * (val - 0.5)
if val < 0:
return val*abs(vmin-midpoint) + midpoint
else:
return val*abs(vmax-midpoint) + midpoint
Je sais que nous sommes en retard dans le jeu, mais je viens de passer à travers ce processus et de proposer une solution qui est peut-être moins robuste que normaliser les sous-classes, mais beaucoup plus simple. Je pensais que ce serait bien de le partager ici pour la postérité.
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
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([])
Il est plus simple d'utiliser simplement les arguments vmin
et vmax
pour imshow
(en supposant que vous travaillez avec des données d'image) plutôt que de sous-classer matplotlib.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()
Je ne sais pas si vous cherchez toujours une réponse. Pour moi, essayer de sous-classe Normalize
a échoué. Je me suis donc concentré sur la création manuelle d'un nouvel ensemble de données, de graduations et de libellés pour obtenir l'effet que je pense que vous visez.
J'ai trouvé le module scale
dans matplotlib qui a une classe utilisée pour transformer les tracés linéaires selon les règles 'syslog', aussi je l'utilise pour transformer les données. Ensuite, j'échelle les données de manière à ce qu'elles passent de 0 à 1 (ce que Normalize
fait généralement), mais j'échelle les nombres positifs différemment des nombres négatifs. Ceci est dû au fait que vos vmax et vmin peuvent ne pas être identiques, de sorte que .5 -> 1 peut couvrir une plage positive supérieure à .5 -> 0, la plage négative le fait. Il était plus facile pour moi de créer une routine pour calculer les valeurs de tick et d’étiquette.
Vous trouverez ci-dessous le code et un exemple.
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.
Cette solution est inspirée d'une classe du même nom de cette page
Ici, je crée une sous-classe de Normalize
suivie d'un exemple minimal.
import scipy as sp
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 sp.ma.masked_array(sp.interp(value, x, y))
vals = sp.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()
Et le même exemple avec uniquement des données positives vals = sp.array([[1., 3], [6, 10]])
Pour résumer, cette norme a les propriétés suivantes:
vmin
est plus grand que midpoint
(cependant, il n'a pas testé tous les cas Edge).J'utilisais l'excellente réponse de Paul H, mais je me suis heurté à 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égatives à 0; Dans les deux cas, je voulais que 0 soit coloré en blanc (le point central de la palette de couleurs que j'utilise). Avec l'implémentation existante, si votre valeur midpoint
est égale à 1 ou à 0, les mappages d'origine n'étaient pas remplacés. Vous pouvez voir cela 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 la boucle for
:
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 https://stackoverflow.com/questions/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é un problème similaire une fois de plus lorsque certaines de mes données allaient d'une petite valeur positive à une valeur positive plus grande, où les très basses valeurs étaient colorées en rouge au lieu de blanc. Je l'ai corrigé en ajoutant la ligne Edit #2
dans le code ci-dessus.
Si vous voulez bien calculer le rapport entre vmin, vmax et zéro, voici une carte linéaire assez basique allant du bleu au blanc au rouge, qui définit le blanc selon le 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 du dégradé 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 pour le segment suivant - si vous voulez des dégradés lisses, les deux derniers sont toujours les mêmes. Voir la documentation pour plus de détails.
Notez que dans la version 3.1 de matplotlib, 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)
J'avais un problème similaire, mais je voulais que la valeur la plus élevée soit entièrement rouge et que les valeurs basses soient coupées en 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()