matplotlibでカラーマップの中点を定義する


87

カラーマップの中間点を設定したい、つまりデータが-5から10になり、ゼロを中間にしたい。それを行う方法は、正規化をサブクラス化して標準を使用することだと思いますが、例が見つからず、正確に何を実装する必要があるのか​​がわかりません。


これは「発散」または「バイポーラ」カラーマップと呼ばれ、マップの中心点が重要であり、データはこの点の上下に移動します。sandia.gov/~kmorel/documents/ColorMaps
endolith

3
このスレッドのすべての答えはかなり複雑に見えます。使いやすいソリューションは、この優れた回答に示されています。この回答は、その間にmatplotlibのドキュメントの「カスタム正規化:2つの線形範囲」セクションにも含まれています
ImportanceOfBeingErnest

回答:


14

matplotlibバージョン3.1では、DivergingNormクラスが追加されていることに注意してください。それはあなたのユースケースをカバーしていると思います。これは次のように使用できます。

from matplotlib import colors
colors.DivergingNorm(vmin=-4000., vcenter=0., vmax=10000)

matplotlib 3.2では、クラスの名前がTwoSlopesNormに変更されました


これは面白そうに見えますが、プロットする前にデータを変換するためにこれを使用する必要があるようです。カラーバーの凡例は、元のデータではなく、変換されたデータに関連します。
BLI

3
@bliはそうではありません。normあなたのイメージのための正規化を行います。normsカラーマップと連携してください。
ポール

1
:それを交換する方法として、うるさくこれはなしドキュメントと3.2で廃止されmatplotlib.org/3.2.0/api/_as_gen/...
daknowles

1
ええ、ドキュメントは不明確です。私はそれがに名前が変更されていると思いますTwoSlopeNormmatplotlib.org/3.2.0/api/_as_gen/...
macKaiver

91

これがゲームに遅れていることはわかっていますが、私はこのプロセスを経て、サブクラス化の正規化よりも堅牢ではないが、はるかに単純なソリューションを思いつきました。後世のためにここで共有するのが良いと思いました。

関数

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([])

例の結果:

ここに画像の説明を入力してください


あなたの素晴らしい貢献に感謝します!ただし、コードは同じカラーマップのトリミングとシフトの両方を行うことができず、指示は少し不正確で誤解を招くものでした。私は今それを修正し、あなたの投稿を編集する自由を取りました。また、私はそれを私の個人的なライブラリの1つに含め、著者としてあなたを追加しました。気を悪くしないで下さい。
TheChymera 2014

@TheChymeraは、右下隅のカラーマップがトリミングされ、再センタリングされています。必要に応じて、これを自由に使用してください。
ポールH

はい、それはあります、悲しいことにそれは偶然としてほぼ正しいように見えるだけです。startstopがそれぞれ0と1でない場合、実行した後reg_index = np.linspace(start, stop, 257)、値129が元のcmapの中点であると想定できなくなるため、トリミングするたびに再スケーリング全体が意味をなしません。また、指示どおりに0から1の両方ではなくstart、0から0.5およびstop0.5から1にする必要があります。
TheChymera 2014

@TheChymera私はあなたのバージョンを試し、それについて2つの考えを持っていました。1)作成したインデックスはすべて長さ257のようですが、matplotlibではデフォルトで256になっていると思いますか?2)私のデータ範囲が-1から1000であると仮定すると、それはポジティブによって支配されているため、より多くのレベル/レイヤーがポジティブブランチに移動する必要があります。しかし、あなたの関数はネガティブとポジティブの両方に128レベルを与えるので、レベルを不均一に分割する方が「公平」だと思います。
ジェイソン

これは優れた解決策ですがmidpoint、データのが0または1に等しい場合は失敗します。この問題の簡単な修正については、以下の私の回答を参照してください。
DaveTheScientist 2017年

22

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

より多くのサブクラスを作成することなく、ログまたはsym-logスケーリングに加えてこのクラスを使用することは可能ですか?私の現在のユースケースでは、すでに「norm = SymLogNorm(linthresh = 1)」を使用しています
AnnanFay 2016年

完璧です、これはまさに私が探していたものです。違いを示すために写真を追加する必要があるかもしれませんか?ここでは、中点を端に向かってドラッグできる他の中点ノーマライザーとは対照的に、中点はバーの中央に配置されています。
豪華な2018

18

サブクラス化するよりも、(画像データを操作していると仮定して)vminvmax引数を使用するのが最も簡単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()

ここに画像の説明を入力してください


1
色のグラデーションがよく見えるように、例をガウス曲線に更新することはできますか?
Dat Chu

3
利用可能な色のダイナミックレンジ全体を使用していないため、このソリューションは好きではありません。また、symlogのような正規化を構築するための正規化の例を示したいと思います。
2011

2
@ tillsten-混乱しているのですが...中央に0が必要な場合は、カラーバーのダイナミックレンジ全体を使用することはできません。では、非線形スケールが必要ですか?0より大きい値用に1つのスケール、下の値用に1つのスケール?その場合、ええ、サブクラス化する必要がありますNormalize。少しだけ例を追加します(他の誰かが私を打ち負かさないと仮定します...)。
ジョーキントン2011

@ジョー:あなたは正しいです、それは線形ではありません(より正確には、2つの線形部分)。vmin / vmaxを使用すると、-5より小さい値のcolorangeは使用されません(これは一部のアプリケーションでは意味がありますが、私のものではありません)。
2011

2
Zの一般的なデータの場合:vmax=abs(Z).max(), vmin=-abs(Z).max()
endolith 2013年

12

ここでは、のサブクラスと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()

結果: pic-1

ポジティブデータのみの同じ例 vals = np.array([[1., 3], [6, 10]])

pic-2

プロパティ:

  • 中点は中間色になります。
  • 上限と下限の範囲は、同じ線形変換によって再スケーリングされます。
  • 写真に表示されている色のみがカラーバーに表示されます。
  • vminがより大きい場合でも正常に動作するようですmidpoint(ただし、すべてのエッジケースをテストしていません)。

このソリューションは、このページの同じ名前のクラスに触発されています


3
そのシンプルさによるベストアンサー。他の答えは、あなたがすでにスーパーエキスパートになろうとしているMatplotlibエキスパートである場合にのみ最適です。ほとんどのmatplotlibの回答を求める人は、犬や家族の家に帰るために何かを成し遂げようとしているだけです。彼らにとって、この回答が最適です。
sapo_cosmico

この解決策は確かに最善のようですが、機能しません!テストスクリプトを実行したところ、結果は完全に異なります(青い四角のみが含まれ、赤は含まれていません)。@icemtel、確認していただけますか?(上のインデントの問題点の横def __call__
フィリペ

わかりました、私は問題を見つけました:計算の数normalized_minnormalized_maxは整数として取られます。それらを0.0と入力するだけです。また、あなたの図の正しい出力を得るために、私はを使わなければなりませんでしたvals = sp.array([[-5.0, 0.0], [5.0, 10.0]]) 。とにかく、答えてくれてありがとう!
フィリペ

こんにちは@Filipe私のマシンでは問題を再現できません(Python 3.7、matplotlib 2.2.3、新しいバージョンでも同じだと思います)。どのバージョンがありますか?とにかく、float型の配列を作成する小さな編集を行い、インデントの問題を修正しました。それを指摘してくれてありがとう
icemtel

うーん..python3で試したところ、動作します。しかし、私はpython2.7を使用しています。修正してくれてありがとう。使い方はとても簡単です!:)
フィリペ

5

あなたがまだ答えを探しているかどうかわからない。私にとって、サブクラス化Normalizeを試みることは失敗しました。そこで、私はあなたが目指していると思う効果を得るために、新しいデータセット、ティック、ティックラベルを手動で作成することに焦点を合わせました。

scale'syslog'ルールによってラインプロットを変換するために使用されるクラスを持つmatplotlibのモジュールを見つけたので、それを使用してデータを変換します。次に、データを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 = 10、vmin = -5およびlinthresh = 1e-4

VMAXスクリプトの上部にある「定数」(例)を自由に調整して、正常に動作することを確認してください。


以下に示すように、あなたの提案に感謝します、私はサブクラス化に成功しました。ただし、コードはティックラベルを正しく作成するのに非常に役立ちます。
2011年

4

Paul Hの優れた回答を使用していましたが、データの一部が負から正の範囲であり、他のセットが0から正または負から0の範囲であったため、問題が発生しました。どちらの場合も、0を白(使用しているカラーマップの中点)として色付けしたかったのです。既存の実装でmidpointは、値が1または0に等しい場合、元のマッピングは上書きされていませんでした。次の図で確認できます 編集前のグラフ 。3番目の列は正しいように見えますが、2番目の列の濃い青色の領域と残りの列の濃い赤色の領域はすべて白であると想定されています(データ値は実際には0です)。私の修正を使用すると、次のようになります。 編集後のグラフ 私の関数は基本的にPaul Hの関数と同じですが、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上記のコードに行を追加して修正しました。


これは良さそうに見えますが、引数はPaul Hの回答(およびコメント)から変更されたようです...回答に呼び出し例を追加できますか?
フィリペ

1

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までのグラデーションに沿った比率)、2番目は前のセグメントの終了値、そして3番目は次のセグメントの開始値です。滑らかなグラデーションが必要な場合、後の2つは常に同じです。詳細については、ドキュメント参照してください


1
LinearSegmentedColormap.from_list()タプル内で指定し、(val,color)それらをリストとしてcolorこのメソッドの引数に渡す オプションもありますval0=0<val1<...<valN==1
maurizio 2016

0

同様の問題がありましたが、最も高い値を完全な赤にし、低い値の青を切り取って、カラーバーの下部が切り取られたように見せたかったのです。これは私のために働きました(オプションの透明度を含みます):

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()
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.