Я рисую несколько географиков с помощью cartopy. Использование нормализации двух наклонов и цветовой карты «RdBu_r» для построения поля температуры воздуха.
color_map = 'RdBu_r'
fig = plt.figure(figsize=(16, 12))
ax = plt.axes(projection=prj)
norm = TwoSlopeNorm(vmin=np.min(data), vcenter=273.15, vmax=np.max(data))
filled_contour = ax.contourf(data['longitude'], data['latitude'], data, norm=norm, levels=15, transform=ccrs.PlateCarree(), cmap=color_map, extend='both')
isotherm_0 = ax.contour(data['longitude'], data['latitude'], data, levels=[273.15], colors='green', transform=ccrs.PlateCarree(), linewidths=0.5, linestyles='dashed')
Я выполняю нормализацию, чтобы покрыть отрицательные температуры синими оттенками, а положительные температуры красными оттенками. Если набор данных содержит широкий диапазон отрицательных и положительных температур, географик выглядит в порядке:
Но если будет много положительных температур и мало слегка отрицательных, то карта не будет построена так, как ожидалось: На изображении видно, как слегка отрицательные температуры (минимум 272,6236 К) окрашены в темно-синий оттенок (крайнее синее значение для карты цветов), а не в светло-голубой, как для температур чуть ниже нуля.
Когда я рисую географический график, увеличенный до этой области отрицательных температур, я также получаю красивое изображение с правильным распределением цветовых оттенков: Почему цвета прорисованы неправильно, как на Рисунке 2? Как мне этого избежать?
ОБНОВЛЯТЬ: Поскольку в моем проекте нет таких естественных экстремальных температур, я взял фиксированный массив и применил к нему цветовую карту:
# Compute min and max of data
min_val, max_val = np.min(data), np.max(data)
# Define the center (K or C)
center = 273.15 if min_val>150 else 0
# Define the temperature range for colormapping
levels = np.arange(-80, 81, 1) if center==0 else np.arange(193.15, 354.15, 1)
color_map = 'RdBu_r'
filled_contour = ax.contourf(data['lon'], data['lat'], data, levels=levels, transform=ccrs.PlateCarree(), cmap=color_map, extend='both')
В результате цвета хорошо распределены, но в районе 0 градусов цвета слишком слабые (почти белые):
Поэтому я добавил разные варианты цветовой карты: «Красные» только для положительных температур, «Синие» для отрицательных и «RdBu_r» для обеих. И здесь я определил соответствующую нормализацию:
# Determine normalization and colormap
if min_val >= center:
norm = Normalize(vmin=center, vmax=max_val)
color_map = 'Reds'
elif max_val < center:
norm = Normalize(vmin=min_val, vmax=center)
color_map = 'Blues'
else:
norm = TwoSlopeNorm(vmin=min_val, vcenter=center, vmax=max_val)
color_map = 'RdBu_r'
Затем рисование с помощью norm=norm
дало мне следующее:
С точки зрения цветов это лучше, хотя мне нужно обрезать цветовую полосу. На этом изображении есть синий цвет для отрицательных температур, но он не очень репрезентативен: он слишком яркий для температур чуть ниже нуля.
Наконец, чтобы обрезать цветовую полосу, я добавил следующее:
filled_contour.set_clim(min_val-5, max_val+6)
Что дало мне это: И хотя цветовая полоса здесь не была обрезана, цвета здесь распределены еще более естественно: светло-голубой для негатива (но не слишком белый) и приятный градиент для позитива. Тем не менее, мне нужно как-то обрезать цветовую полосу...
Другая проблема возникла с различными вариантами цветовой карты, например, только для положительных температур. Цветовая карта (для диапазона значений -80, ..., 80) распределяется неправильно, начиная с vmin=0:
norm = Normalize(vmin=center, vmax=max_val)
color_map = 'Reds'
Думаю, мне придется определить другой диапазон уровней для положительных температур (например, np.arange(0, 81, 1)). Однако мне все это кажется быстрым и грязным обходным путем. Мне бы хотелось, чтобы для этого объекта существовал какой-нибудь умный автоматический способ. Эта возня с цветом занимает гораздо больше времени, чем работа с данными и т. д., никогда бы не подумал, что это настолько сложно...
@Timeless Да, это работает, но таким образом он одинаково отображает цветовую карту на весь диапазон значений, таким образом окрашивая положительные температуры также синими оттенками. Как я уже говорил, для температур около нуля (273,15 К) я хочу, чтобы отрицательные температуры были окрашены синими оттенками, а положительные температуры - красными оттенками. Хотя я использую метод Normalize() для случаев, когда температуры только положительные (цветовая карта «Красные») или только отрицательные (цветовая карта «Синий»).
В основном это связано с фиксированным количеством ячеек в аргументе levels
функции contourf
.
Согласно документации , int levels
обычно генерирует равноотстоящие друг от друга контурные линии между минимумом и максимумом данных (иногда это может привести к меньшему количеству ячеек, чтобы сделать значения разделения более приятными на цветовой панели. Например, ваш второй пример имеет только 11 уровней , меньше 15 вы указали). Если ваши данные имеют слишком большой максимум (), то небольшие значения ниже 273,15 попадут в один контейнер, поэтому будет только один цвет.
Одним из простых решений является увеличение количества уровней. Скажем, если мы хотим, чтобы на стороне было как минимум 3 цвета с относительно меньшими диапазонами, тогда установите levels=(np.ptp(data)/(np.min((273.15-np.min(data), np.max(data)-273.15))/3)).astype(int)
в ax.contourf
. Но будьте осторожны: это может привести к слишком большому количеству уровней, когда максимум или минимум будут слишком близки к 273,15.
Вы также можете попробовать указать уровни вручную, используя фиксированный массив и фиксированный TwoSlopeNorm
.
Спасибо, я воспользовался вашим советом и дополнил свой вопрос новой информацией.
Для достижения своей цели я использовал фиксированный массив и фиксированную цветовую карту.
Я получил цветовую карту «RdBu_r» и применил ее к температурному диапазону [-50, +50], чтобы получить:
cm = {
-50: "#053061",
-49: "#073466",
...
49: "#750421",
50: "#6c011f" }
temperature_range = list(cm.keys()) #range of values for plotting
temperature_range = temperature_range[::step] #if _step_-degree gradation is required
colors = list(cm.values())
# if I want the colormap to fit the real given range of values
# instead of a general [-50, +50] range, I use the following
# temperature_range. It must be equal on both sides of zero
# since I use binary colormap, where middle part is always zero degrees:
lim = np.ceil(np.max(np.abs(dataset)))
temperature_range = np.arange(-lim, lim+1, step)
custom_cmap = ListedColormap(colors, name = "rdbu_colormap")
norm = BoundaryNorm(temperature_range, custom_cmap.N)
filled_contour = ax.contourf(lons, lats, data, norm=norm, levels=temperature_range,
transform=ccrs.PlateCarree(), cmap=custom_cmap, extend='both')
cbar = plt.colorbar(filled_contour, ax=ax, orientation='horizontal', pad=0.05, aspect=50)
norm = plt.Normalize(vmin=np.min(data), vmax=np.max(data))
?