Matplotlib discreta colorbar

94

Estou tentando fazer uma barra de cores discreta para um gráfico de dispersão em matplotlib

Eu tenho meus dados x, y e para cada ponto um valor de tag inteiro que desejo ser representado com uma cor única, por exemplo

plt.scatter(x, y, c=tag)

normalmente, a tag será um número inteiro de 0-20, mas o intervalo exato pode mudar

até agora, usei apenas as configurações padrão, por exemplo

plt.colorbar()

o que dá uma gama contínua de cores. Idealmente, eu gostaria de um conjunto de n cores discretas (n = 20 neste exemplo). Ainda melhor seria obter um valor de tag de 0 para produzir uma cor cinza e 1-20 para ser colorido.

Eu encontrei alguns scripts de 'livro de receitas', mas eles são muito complicados e não consigo pensar que sejam a maneira certa de resolver um problema aparentemente simples

bph
fonte
1
faz este ou esta ajuda?
Francesco Montesano
obrigado pelos links, mas o segundo exemplo é o que quero dizer sobre meios extremamente complicados de realizar uma tarefa (aparentemente) trivial - o primeiro link é útil
bph
1
Achei este link muito útil para discretizar um mapa de cores existente: gist.github.com/jakevdp/91077b0cae40f8f8244a
BallpointBen

Respostas:

91

Você pode criar uma barra de cores discreta personalizada com bastante facilidade usando um BoundaryNorm como normalizador para sua dispersão. A parte peculiar (no meu método) é fazer 0 aparecer como cinza.

Para imagens, costumo usar cmap.set_bad () e converter meus dados em um array mascarado numpy. Seria muito mais fácil tornar 0 em cinza, mas não consegui fazer isso funcionar com o scatter ou o cmap personalizado.

Como alternativa, você pode fazer seu próprio cmap do zero ou ler um existente e substituir apenas algumas entradas específicas.

import numpy as np
import matplotlib as mpl
import matplotlib.pylab as plt

fig, ax = plt.subplots(1, 1, figsize=(6, 6))  # setup the plot

x = np.random.rand(20)  # define the data
y = np.random.rand(20)  # define the data
tag = np.random.randint(0, 20, 20)
tag[10:12] = 0  # make sure there are some 0 values to show up as grey

cmap = plt.cm.jet  # define the colormap
# extract all colors from the .jet map
cmaplist = [cmap(i) for i in range(cmap.N)]
# force the first color entry to be grey
cmaplist[0] = (.5, .5, .5, 1.0)

# create the new map
cmap = mpl.colors.LinearSegmentedColormap.from_list(
    'Custom cmap', cmaplist, cmap.N)

# define the bins and normalize
bounds = np.linspace(0, 20, 21)
norm = mpl.colors.BoundaryNorm(bounds, cmap.N)

# make the scatter
scat = ax.scatter(x, y, c=tag, s=np.random.randint(100, 500, 20),
                  cmap=cmap, norm=norm)

# create a second axes for the colorbar
ax2 = fig.add_axes([0.95, 0.1, 0.03, 0.8])
cb = plt.colorbar.ColorbarBase(ax2, cmap=cmap, norm=norm,
    spacing='proportional', ticks=bounds, boundaries=bounds, format='%1i')

ax.set_title('Well defined discrete colors')
ax2.set_ylabel('Very custom cbar [-]', size=12)

insira a descrição da imagem aqui

Eu pessoalmente acho que com 20 cores diferentes é um pouco difícil de ler o valor específico, mas isso depende de você, é claro.

Rutger Kassies
fonte
Não tenho certeza se isso é permitido, mas você poderia olhar minha pergunta aqui ?
vwos
6
plt.colorbar.ColorbarBaselança Erro. Usompl.colorbar.ColorbarBase
zeeshan khan
Obrigado por esta resposta, sinto falta do doc. Tentei transpor para roses de vento de percentis e tive um bug com mapeamento de cores. É um caso de uso diferente, mas pode sugerir que está N-1em cmap = mpl.colors.LinearSegmentedColormap.from_list('Custom cmap', cmaplist, cmap.N-1). Caso contrário, as cores não são distribuídas igualmente nas caixas e você tem um problema de barreira de cerca.
jlandercy
1
Aqui está o código para reproduzir um mapeamento igualmente distribuído:q=np.arange(0.0, 1.01, 0.1) cmap = mpl.cm.get_cmap('jet') cmaplist = [cmap(x) for x in q] cmap = mpl.colors.LinearSegmentedColormap.from_list('Custom cmap', cmaplist, len(q)-1) norm = mpl.colors.BoundaryNorm(q, cmap.N)
jlandercy
Não tenho certeza sobre o N-1, talvez você esteja certo, mas não posso replicar com meu exemplo. Você pode evitar o LinearSegmentedColormap(e seu Nargumento) usando um ListedColormap. Os documentos melhoraram muito desde 13, consulte por exemplo: matplotlib.org/3.1.1/tutorials/colors/…
Rutger Kassies
62

Você pode seguir este exemplo :

#!/usr/bin/env python
"""
Use a pcolor or imshow with a custom colormap to make a contour plot.

Since this example was initially written, a proper contour routine was
added to matplotlib - see contour_demo.py and
http://matplotlib.sf.net/matplotlib.pylab.html#-contour.
"""

from pylab import *


delta = 0.01
x = arange(-3.0, 3.0, delta)
y = arange(-3.0, 3.0, delta)
X,Y = meshgrid(x, y)
Z1 = bivariate_normal(X, Y, 1.0, 1.0, 0.0, 0.0)
Z2 = bivariate_normal(X, Y, 1.5, 0.5, 1, 1)
Z = Z2 - Z1 # difference of Gaussians

cmap = cm.get_cmap('PiYG', 11)    # 11 discrete colors

im = imshow(Z, cmap=cmap, interpolation='bilinear',
            vmax=abs(Z).max(), vmin=-abs(Z).max())
axis('off')
colorbar()

show()

que produz a seguinte imagem:

poormans_contour

David Zwicker
fonte
14
cmap = cm.get_cmap ('jet', 20) então scatter (x, y, c = tags, cmap = cmap) me leva até lá - é muito difícil encontrar documentação útil para matplotlib
bph
O link parece estar quebrado, FYI.
Quinn Culver
42

As respostas acima são boas, exceto pelo fato de não terem uma marcação adequada na barra de cores. Gosto de ter marcações no meio da cor para que o mapeamento de número -> cor seja mais claro. Você pode resolver esse problema alterando os limites da chamada matshow:

import matplotlib.pyplot as plt
import numpy as np

def discrete_matshow(data):
    #get discrete colormap
    cmap = plt.get_cmap('RdBu', np.max(data)-np.min(data)+1)
    # set limits .5 outside true range
    mat = plt.matshow(data,cmap=cmap,vmin = np.min(data)-.5, vmax = np.max(data)+.5)
    #tell the colorbar to tick at integers
    cax = plt.colorbar(mat, ticks=np.arange(np.min(data),np.max(data)+1))

#generate data
a=np.random.randint(1, 9, size=(10, 10))
discrete_matshow(a)

exemplo de barra de cores discreta

ben.dichter
fonte
1
Concordo que colocar a marca no meio da cor correspondente é muito útil ao olhar para dados discretos. Seu segundo método está correto. No entanto, seu primeiro método é, em geral, errado : você está rotulando os ticks com valores que são inconsistentes com sua colocação na barra de cores. set_ticklabels(...)só deve ser usado para controlar a formatação da etiqueta (por exemplo, número decimal, etc.). Se os dados forem realmente discretos, você pode não notar nenhum problema. Se houver ruído no sistema (por exemplo, 2 -> 1,9), essa rotulagem inconsistente resultará em uma barra de cores enganosa e incorreta.
E. Davis
E., acho que você está certo de que alterar os limites é a solução superior, então removi o outro - embora nenhum dos dois lidaria bem com o "ruído". Alguns ajustes seriam necessários para lidar com dados contínuos.
ben.dichter
37

Para definir valores acima ou abaixo da faixa do mapa de cores, você vai querer usar os métodos set_overe set_underdo mapa de cores. Se você quiser sinalizar um valor específico, mascare-o (ou seja, crie uma matriz mascarada) e use o set_badmétodo. (Dê uma olhada na documentação da classe de mapa de cores base: http://matplotlib.org/api/colors_api.html#matplotlib.colors.Colormap )

Parece que você quer algo assim:

import matplotlib.pyplot as plt
import numpy as np

# Generate some data
x, y, z = np.random.random((3, 30))
z = z * 20 + 0.1

# Set some values in z to 0...
z[:5] = 0

cmap = plt.get_cmap('jet', 20)
cmap.set_under('gray')

fig, ax = plt.subplots()
cax = ax.scatter(x, y, c=z, s=100, cmap=cmap, vmin=0.1, vmax=z.max())
fig.colorbar(cax, extend='min')

plt.show()

insira a descrição da imagem aqui

Joe Kington
fonte
isso é muito bom - tentei usar set_under, mas não incluí o vmin, então não acho que ele estava fazendo nada
bph
8

Este tópico já está bem coberto, mas eu queria adicionar algo mais específico: eu queria ter certeza de que um determinado valor seria mapeado para aquela cor (não para qualquer cor).

Não é complicado mas como demorou algum tempo, poderá ajudar outras pessoas a não perderem tanto tempo como eu :)

import matplotlib
from matplotlib.colors import ListedColormap

# Let's design a dummy land use field
A = np.reshape([7,2,13,7,2,2], (2,3))
vals = np.unique(A)

# Let's also design our color mapping: 1s should be plotted in blue, 2s in red, etc...
col_dict={1:"blue",
          2:"red",
          13:"orange",
          7:"green"}

# We create a colormar from our list of colors
cm = ListedColormap([col_dict[x] for x in col_dict.keys()])

# Let's also define the description of each category : 1 (blue) is Sea; 2 (red) is burnt, etc... Order should be respected here ! Or using another dict maybe could help.
labels = np.array(["Sea","City","Sand","Forest"])
len_lab = len(labels)

# prepare normalizer
## Prepare bins for the normalizer
norm_bins = np.sort([*col_dict.keys()]) + 0.5
norm_bins = np.insert(norm_bins, 0, np.min(norm_bins) - 1.0)
print(norm_bins)
## Make normalizer and formatter
norm = matplotlib.colors.BoundaryNorm(norm_bins, len_lab, clip=True)
fmt = matplotlib.ticker.FuncFormatter(lambda x, pos: labels[norm(x)])

# Plot our figure
fig,ax = plt.subplots()
im = ax.imshow(A, cmap=cm, norm=norm)

diff = norm_bins[1:] - norm_bins[:-1]
tickz = norm_bins[:-1] + diff / 2
cb = fig.colorbar(im, format=fmt, ticks=tickz)
fig.savefig("example_landuse.png")
plt.show()

insira a descrição da imagem aqui

Enzoupi
fonte
Estava tentando replicar isso, porém o código não é executado porque 'tmp' está indefinido. Também não está claro o que 'pos' está na função lambda. Obrigado!
George Liu
@GeorgeLiu Certamente você foi escrito! Eu cometi um erro de copiar / colar e agora está corrigido! O snippet de código agora está em execução! Em relação a posnão tenho a certeza do porque está aqui mas é pedido pelo FuncFormatter () ... Talvez outra pessoa possa nos esclarecer sobre isso!
Enzoupi,
7

Tenho investigado essas idéias e aqui estão meus cinco centavos. Ele evita chamar BoundaryNorme também especificar normcomo um argumento para scattere colorbar. No entanto, não encontrei maneira de eliminar a longa chamada para matplotlib.colors.LinearSegmentedColormap.from_list.

Alguns antecedentes são que matplotlib fornece os chamados mapas de cores qualitativos, destinados ao uso com dados discretos. Set1, por exemplo, tem 9 cores facilmente distinguíveis e tab20pode ser usado para 20 cores. Com esses mapas, pode ser natural usar suas primeiras n cores para colorir gráficos de dispersão com n categorias, como faz o exemplo a seguir. O exemplo também produz uma barra de cores com n cores discretas adequadamente rotuladas.

import matplotlib, numpy as np, matplotlib.pyplot as plt
n = 5
from_list = matplotlib.colors.LinearSegmentedColormap.from_list
cm = from_list(None, plt.cm.Set1(range(0,n)), n)
x = np.arange(99)
y = x % 11
z = x % n
plt.scatter(x, y, c=z, cmap=cm)
plt.clim(-0.5, n-0.5)
cb = plt.colorbar(ticks=range(0,n), label='Group')
cb.ax.tick_params(length=0)

que produz a imagem abaixo. O nna chamada para Set1especifica as primeiras ncores desse mapa de cores e o último nna chamada para from_list especifica a construção de um mapa com ncores (o padrão é 256). Para definir cmcomo o mapa de cores padrão com plt.set_cmap, achei necessário dar um nome e registrá-lo, a saber:

cm = from_list('Set15', plt.cm.Set1(range(0,n)), n)
plt.cm.register_cmap(None, cm)
plt.set_cmap(cm)
...
plt.scatter(x, y, c=z)

gráfico de dispersão com cores discretas

Kristjan Jonasson
fonte
1

Acho que você gostaria de dar uma olhada em colors.ListedColormap para gerar seu mapa de cores ou, se precisar apenas de um mapa de cores estático, estou trabalhando em um aplicativo que pode ajudar.

ChrisC
fonte
que parece legal, possivelmente exagero para minhas necessidades - você poderia sugerir uma maneira de marcar um valor de cinza em um mapa de cores existente? para que 0 valores saiam cinza e os outros saiam como cores?
bph de
@Hiett que tal gerar um array RGB color_list com base em seus valores y e passar isso para ListedColormap? Você pode marcar um valor com color_list [y == value_to_tag] = gray_color.
ChrisC