Я хочу установить среднюю точку цветовой карты, то есть мои данные идут от -5 до 10, я хочу, чтобы ноль был серединой. Я думаю, что способ сделать это - создать подклассы normalize и использовать норму, но я не нашел ни одного примера, и мне непонятно, что именно я должен реализовать.
python
matplotlib
Tillsten
источник
источник
Ответы:
Обратите внимание, что в matplotlib версии 3.1 был добавлен класс DivergingNorm . Я думаю, это касается вашего варианта использования. Его можно использовать так:
from matplotlib import colors colors.DivergingNorm(vmin=-4000., vcenter=0., vmax=10000)
В matplotlib 3.2 класс был переименован в TwoSlopesNorm
источник
norm
делает нормализацию для вашего образа.norms
идут рука об руку с палитрой.TwoSlopeNorm
: matplotlib.org/3.2.0/api/_as_gen/…Я знаю, что это поздно для игры, но я просто прошел через этот процесс и придумал решение, которое, возможно, менее надежно, чем подкласс нормализации, но гораздо проще. Я подумал, что было бы хорошо поделиться этим здесь для потомков.
Функция
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([])
Результаты примера:
источник
start
иstop
не равны 0 и 1 соответственно, после того, как вы это сделаетеreg_index = np.linspace(start, stop, 257)
, вы больше не сможете предполагать, что значение 129 является средней точкой исходного cmap, поэтому полное изменение масштаба не имеет смысла при кадрировании. Кроме того,start
должно быть от 0 до 0,5 иstop
от 0,5 до 1, а не от 0 до 1 одновременно, как вы указываете.midpoint
данные равны 0 или 1. См. Мой ответ ниже, где описано простое решение этой проблемы.Вот подкласс решения Normalize. Использовать это
norm = MidPointNorm(midpoint=3) imshow(X, norm=norm)
Вот класс:
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
источник
Это проще всего просто использовать
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()
источник
Normalize
. Я добавлю пример чуть позже (при условии, что кто-то другой меня не опередит ...).vmax=abs(Z).max(), vmin=-abs(Z).max()
Здесь я создаю подкласс, за
Normalize
которым следует минимальный пример.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()
Результат:
Тот же пример только с положительными данными
vals = np.array([[1., 3], [6, 10]])
Свойства:
vmin
больше, чемmidpoint
(хотя не проверял все крайние случаи).Это решение вдохновлено классом с таким же именем на этой странице.
источник
def __call__
)normalized_min
иnormalized_max
принимаются как целые числа. Просто поставьте их как 0,0. Также, чтобы получить правильный вывод вашей фигуры, мне пришлось использоватьvals = sp.array([[-5.0, 0.0], [5.0, 10.0]])
. В любом случае спасибо за ответ!Не уверен, что вы все еще ищете ответ. Для меня попытка создания подкласса
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
) в верхней части скрипта, чтобы убедиться, что он ведет себя хорошо.источник
Я использовал отличный ответ от Paul H, но столкнулся с проблемой, потому что некоторые из моих данных варьировались от отрицательных до положительных, а другие наборы варьировались от 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'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 #2
в код выше.источник
Если вы не возражаете против определения соотношения между vmin, vmax и нулем, это довольно простая линейная карта от синего к белому и к красному, которая устанавливает белый цвет в соответствии с соотношением
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), вторая - конечное значение для предыдущего сегмента и третий - начальное значение для следующего сегмента - если вы хотите плавные градиенты, последние два всегда одинаковы. Подробнее см. В документации .
источник
LinearSegmentedColormap.from_list()
кортежах(val,color)
и передать их как списокcolor
аргументу этого метода whereval0=0<val1<...<valN==1
.У меня была аналогичная проблема, но я хотел, чтобы максимальное значение было полностью красным, а низкие значения синего отсекали, чтобы это выглядело по существу так, как будто нижняя часть шкалы цветов была обрезана. Это сработало для меня (включая дополнительную прозрачность):
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()
источник