【Python】複数のヒートマップで同じ色を同じ値にする

PythonのTopに戻る

結論:カラーマップだけでなく正規化範囲もそろえる

複数のヒートマップで同じ色を同じ数値に対応させたいなら、同じNormalizeと同じカラーマップを各画像へ渡す。cmap="viridis"だけをそろえても、図ごとに最小値と最大値を自動設定している限り色の意味は一致しない。条件Aでは4、条件Bでは40が同じ黄色になる、といった見落としが起こり得る。

色への変換は、値を正規化してからカラーマップの色へ対応させる2段階で考えると分かりやすい。比較したいのが絶対的な信号強度なら共通範囲を使う。一方、各試料の内部パターンだけを見る目的で個別に正規化することもある。その場合は強度の絶対比較には使えないことを図注に書き、各図のカラーバーで数値範囲を示したい。

そのまま動かせる例

ここでは条件Aの配列を10倍したものを条件Bとする。上段はそれぞれに自動スケールを使い、下段は0〜40の範囲を共有する。上段の色模様は同じでも数値は10倍違う。下段ではAが暗くなり、絶対強度の違いが見える。データは説明用の6画素なので、大きな測定ファイルを用意する必要はない。

NumPyとMatplotlibを読み込める環境で、例全体をスクリプトとして実行する。現在の作業フォルダーへshared-colors.pngを保存するため、同名ファイルのないフォルダーを使おう。下段のカラーバーは2枚のAxesをまとめて対象にし、比較図の片方だけを狭くしないようにしている。

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import Normalize

low = np.array([[0., 1., 2.], [2., 3., 4.]])
high = low * 10
norm = Normalize(vmin=0, vmax=40)
fig, axs = plt.subplots(2, 2, figsize=(8, 6), layout="constrained")
try:
    for col, (name, data) in enumerate([("A", low), ("B", high)]):
        local = axs[0, col].imshow(data, cmap="viridis", origin="lower")
        axs[0, col].set_title(f"{name}: separate scale")
        fig.colorbar(local, ax=axs[0, col], label="Signal (a.u.)")
        shared = axs[1, col].imshow(data, cmap="viridis", norm=norm,
                                    origin="lower")
        axs[1, col].set_title(f"{name}: common 0..40")
    for ax in axs.flat:
        ax.set(xlabel="Column", ylabel="Row", xticks=[0, 1, 2], yticks=[0, 1])
    fig.colorbar(shared, ax=axs[1, :].tolist(), label="Signal (a.u.)")
    a, b = [ax.images[0] for ax in axs[1]]
    assert a.norm is b.norm
    np.testing.assert_allclose(a.to_rgba(4), b.to_rgba(4))
    print("A range:", (float(low.min()), float(low.max())))
    print("B range:", (float(high.min()), float(high.max())))
    print("same value, same RGBA:", bool(np.allclose(a.to_rgba(4), b.to_rgba(4))))
    print("normalized 4 and 40:", norm([4, 40]).tolist())
    fig.savefig("shared-colors.png", dpi=160)
finally:
    plt.close(fig)
print("saved: shared-colors.png")

実行結果

A range: (0.0, 4.0)
B range: (0.0, 40.0)
same value, same RGBA: True
normalized 4 and 40: [0.1, 1.0]
saved: shared-colors.png
上段は別々の色範囲、下段は共通の0から40で描いた2条件のヒートマップ
上段は同じ色模様でも数値が10倍違う。下段では共通のカラーバーで絶対強度を比較できる。

共通の色の意味を確かめる

出力ではAが0〜4、Bが0〜40と分かる。共通の正規化では4が0.1、40が1.0へ写される。同じ値4を両方の画像に通したRGBAも一致する。見た目の色が似ているかを目で確認するだけでなく、変換そのものを一致させている点が重要である。比較に使う図の色範囲を、データごとの偶然の最小値・最大値へ任せず管理できる。

一つのnormオブジェクトを共有すると、その範囲を変更した際にも一貫した正規化を保ちやすい。ただしカラーマップまで自動的に同じになると考えないこと。この例では両方に同じcmapを明示している。カラーバーは渡された画像の変換を表示するもので、別の画像の設定を勝手に修正する機能ではない。

凡例と欠測の色も統一する

比較図ではカラーバーへ量の名前と単位を付け、画素の縦横も同じ座標規約にそろえたい。欠測があるときは通常の最低値と異なる色を使い、その意味も説明する。欠測を0へ置き換えて共通範囲を求めると、値のない場所が弱い信号に見えてしまう。表示補間を変えると境界が滑らかになることもあるが、新たな観測値が増えたわけではない。定量比較では同じ補間設定を使い、必要なら各セルの数値や要約統計も合わせて確認しよう。

範囲の決め方と比較できる条件

共通範囲には、全比較データの最小値と最大値、装置の測定範囲、事前に決めた基準値などを使える。時期をまたいで図を比較する場合は、その都度データ全体から求め直すと色の意味が変わる。比較対象を追加しても同じ読み方を維持したいなら、範囲を固定して記録しておこう。有限値が一つもないデータや、全値が同じデータの扱いも別途決める。

一部の極端な値が範囲を広げると、他の違いが見えにくくなる。外れ値を除いた範囲や分位点を使う場合は、表示範囲外の値があることを説明し、カラーバーの延長表示などで区別する。都合のよい範囲だけを選んで差を強調しないようにしたい。対数正規化なら0以下、差分を発散色で描くなら中心値という追加条件も入る。

同じ色変換でも、測定単位や露光時間、背景補正、面積あたりか総量かが異なれば直接比較できるとは限らない。表示の統一は比較の必要条件の一つである。またnormを与えるときは、同じ呼び出しへvmin・vmaxも重ねて指定せず、範囲をnorm側へまとめる。値の意味と表示設定を一緒に残しておくと再作図時に迷いにくい。

確認環境と参考資料

例はLinux・CPython 3.12.14・NumPy 2.3.5・Matplotlib 3.10.8(Agg)で実行した。掲載した出力はこの環境での結果である。公式資料のstable版や最新版は更新されるため、手元のバージョンと対応する仕様も確認してほしい。

関連項目:画像の上下反転と半画素ずれを直す

PythonのTopに戻る