Я хочу установить среднюю точку в цветовой палитре, т.е. мои данные идут от -5 до 10, я хочу, чтобы ноль был посередине. Я думаю, что способ сделать это - подклассифицировать нормализацию и использовать норму, но я не нашел никакого примера, и мне непонятно, что именно я должен реализовать.
Определение средней точки цветовой карты в matplotlib
Ответ 1
Обратите внимание, что в matplotlib версии 3.1 был добавлен класс DivergingNorm. Я думаю, что это охватывает ваш вариант использования. Это можно использовать так:
from matplotlib import colors
colors.DivergingNorm(vmin=-4000., vcenter=0., vmax=10000)
Ответ 2
Я знаю, что это поздно для игры, но я только что прошел этот процесс и придумал решение, которое, возможно, менее надежно, чем нормализация подклассов, но намного проще. Я думал, что было бы хорошо поделиться этим здесь для потомков.
Функция
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 dynamic range to be at zero.
Input
-----
cmap : The matplotlib colormap to be altered
start : Offset from lowest point in the colormap 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 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([])
Результаты примера:
Ответ 3
Вот подклассу решения Normalize. Чтобы использовать его
norm = MidPointNorm(midpoint=3)
imshow(X, norm=norm)
Вот класс:
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
Ответ 4
Проще всего просто использовать аргументы vmin
и vmax
для imshow
(при условии, что вы работаете с данными изображения), а не подклассы matplotlib.colors.Normalize
.
например.
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()
Ответ 5
Не уверен, что вы все еще ищете ответ. Для меня попытка подкласса Normalize
была неудачной. Поэтому я сосредоточился на создании нового набора данных, тиков и ярлыков меток, чтобы получить эффект, на который, я думаю, вы стремитесь.
Я нашел модуль scale
в matplotlib, который имеет класс, используемый для преобразования строковых графиков по правилам syslog, поэтому я использую это для преобразования данных. Затем я масштабирую данные так, чтобы они шли от 0 до 1 (что обычно Normalize
), но я масштабирую положительные числа по-разному от отрицательных чисел. Это связано с тем, что ваши vmax и vmin могут быть не одинаковыми, поэтому .5 → 1 может покрывать больший положительный диапазон, чем .5 → 0, отрицательный диапазон. Мне было легче создать рутину для вычисления значений тика и метки.
Ниже приведен код и пример.
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')
Не стесняйтесь настраивать "константы" (например, VMAX
) в верхней части script, чтобы подтвердить, что он ведет себя хорошо.
Ответ 6
Это решение вдохновлено классом с тем же именем с этой страницы
Здесь я создаю подкласс Normalize
последующим минимальным примером.
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()
И тот же пример только с положительными данными vals = sp.array([[1., 3], [6, 10]])
Чтобы подвести итог - эта норма имеет следующие свойства:
- Средняя точка получит средний цвет.
- Верхний и нижний диапазоны будут изменяться одинаково, поэтому при правильной цветовой карте насыщенность цвета будет соответствовать расстоянию от средней точки.
- На цветовой шкале отображаются только те цвета, которые появляются на картинке.
- Кажется, работает нормально, даже если
vmin
больше, чемmidpoint
(хотя не проверял все крайние случаи).
Ответ 7
Я использовал отличный ответ от Пола Х., но столкнулся с проблемой, потому что некоторые из моих данных варьировались от отрицательного до положительного, в то время как другие подходы варьировались от 0 до положительного или от отрицательного до 0; в любом случае я хотел, чтобы 0 было окрашено в белый цвет (средняя точка цветовой карты, которую я использую). В существующей реализации, если значение midpoint
равно 1 или 0, исходные сопоставления не перезаписывались. Вы можете увидеть это на следующем рисунке:
3-й столбец выглядит правильно, но темно-синяя область во 2-м столбце и темно-красная область в остальных столбцах должны быть белыми (их значения данных на самом деле равны 0). Использование моего исправления дает мне:
Моя функция практически такая же, как у Пола Х, с моими правками в начале цикла
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 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 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 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 #2
в коде выше.
Ответ 8
Если вы не против разработки соотношения между vmin, vmax и zero, это довольно простая линейная карта от синего до белого до красного, которая устанавливает белый в соответствии с отношением 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)
Формат cdict довольно прост: строки - это точки в градиенте, который создается: первая запись - это x-значение (отношение вдоль градиента от 0 до 1), второе - конечное значение для предыдущего сегмент, а третий - начальное значение для следующего сегмента - если вы хотите гладкие градиенты, последние два всегда одинаковы. Подробнее см. в документах.
Ответ 9
У меня была аналогичная проблема, но я хотел, чтобы наивысшее значение было полностью красным и отсекало низкие значения синего цвета, что выглядело по существу так, как внизу цветовой панели было отрублено. Это сработало для меня (включая дополнительную прозрачность):
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()