123 votes

Définir le point central d'une carte de couleurs dans matplotlib

Je veux définir le point central d'une carte de couleurs, c'est-à-dire que mes données vont de -5 à 10 et je veux que zéro soit le point central. Je pense que la façon de le faire est de sous-classer normalize et d'utiliser la norme, mais je n'ai pas trouvé d'exemple et ce n'est pas clair pour moi, ce que je dois mettre en œuvre exactement ?

0 votes

C'est ce qu'on appelle une carte en couleurs "divergente" ou "bipolaire", où le point central de la carte est important et où les données vont au-dessus et au-dessous de ce point. sandia.gov/~kmorel/documents/ColorMaps

3 votes

Toutes les réponses dans ce fil de discussion semblent plutôt compliquées. La solution facile à utiliser est présentée dans cette excellente réponse qui, entre-temps, a également été intégré dans la documentation de matplotlib, section Normalisation personnalisée : Deux gammes linéaires .

97voto

Paul H Points 5612

Je sais que c'est un peu tard, mais je viens de passer par ce processus et j'ai trouvé une solution qui est peut-être moins robuste que de sous-classer normalize, mais beaucoup plus simple. J'ai pensé qu'il serait bon de la 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 :

enter image description here

1 votes

Merci beaucoup pour votre contribution géniale ! Cependant, le code n'était pas capable de les deux recadrer et déplacer la même carte de couleurs, et vos instructions étaient un peu imprécises et trompeuses. J'ai maintenant corrigé cela et j'ai pris la liberté de modifier votre message. De plus, je l'ai inclus dans une de mes bibliothèques personnelles et je vous ai ajouté en tant qu'auteur. J'espère que cela ne vous dérange pas.

0 votes

@TheChymera la carte des couleurs dans le coin inférieur droit a été recadrée et recentrée. N'hésitez pas à l'utiliser comme bon vous semble.

0 votes

Oui, c'est le cas, malheureusement, cela ne semble qu'approximativement correct comme coïncidence. Si start y stop ne sont pas 0 et 1 respectivement, après avoir fait reg_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, ce qui fait que la remise à l'échelle n'a aucun sens lorsque vous recadrez. Aussi, start doit être compris entre 0 et 0,5 et stop de 0,5 à 1, et non les deux de 0 à 1 comme vous l'indiquez.

50voto

macKaiver Points 397

Notez que dans la version 3.2+ de matplotlib, l'option TwoSlopeNorm a été ajoutée. Je pense qu'elle couvre votre cas d'utilisation. Elle peut être utilisée comme ceci :

from matplotlib import colors
divnorm=colors.TwoSlopeNorm(vmin=-5., vcenter=0., vmax=10)
pcolormesh(your_data, cmap="coolwarm", norm=divnorm)

Dans matplotlib 3.1, la classe s'appelait Norme divergente .

28voto

Joe Kington Points 68089

Le plus simple est d'utiliser l'option vmin y vmax arguments pour imshow (en supposant que vous travaillez avec des données d'image) plutôt que de sous-classer l'option 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()

enter image description here

1 votes

Est-il possible de mettre à jour l'exemple en une courbe gaussienne pour que nous puissions mieux voir la gradation de la couleur ?

3 votes

Je n'aime pas cette solution, car elle n'utilise pas toute la gamme dynamique des couleurs disponibles. De plus, j'aimerais avoir un exemple de normalisation pour construire une normalisation de type symlog.

2 votes

@tillsten - Je suis confus, alors... Vous ne pouvez pas utiliser toute la gamme dynamique de la barre de couleur si vous voulez 0 au milieu, non ? Vous voulez une échelle non-linéaire alors ? Une échelle pour les valeurs supérieures à 0, une autre pour les valeurs inférieures ? Dans ce cas, oui, vous aurez besoin de sous-classer Normalize . J'ajouterai un exemple tout à l'heure (en supposant que quelqu'un d'autre ne me devance pas...).

24voto

tillsten Points 1861

Voici une solution qui consiste à sous-classer Normalize. 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

0 votes

Est-il possible d'utiliser cette classe en plus de l'échelle log ou sym-log sans devoir créer d'autres sous-classes ? Mon cas d'utilisation actuel utilise déjà "norm=SymLogNorm(linthresh=1)".

0 votes

Parfait, c'est exactement ce que je cherchais. Peut-être devriez-vous ajouter une photo pour montrer la différence ? Ici le point médian est centré dans la barre, contrairement aux autres normalisateurs de points médians où le point médian peut être déplacé vers les extrémités.

16voto

icemtel Points 323

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 : pic-1

Le même exemple avec uniquement des données positives vals = np.array([[1., 3], [6, 10]])

pic-2

Propriétés :

  • Le point médian reçoit la couleur du milieu.
  • Les plages supérieures et inférieures sont redimensionnées par la même transformation linéaire.
  • Seules les couleurs qui apparaissent sur l'image sont affichées dans la barre de couleurs.
  • Il semble que cela fonctionne bien même si vmin est plus grande que midpoint (je n'ai cependant pas testé tous les cas limites).

Cette solution est inspirée d'une classe du même nom de l'entreprise cette page

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X