カテゴリー

当サイトはアフィリエイトプログラムによる収益を得ています〈景品表示法に基づく表記です)

go

【plotly&heatmap】go.Heatmapで2次元配列をマップ化

2021年7月10日

こんな人にオススメ

以前、matplotlib.pyplotpltplt.imshowで2次元配列をマップ化したけど、plotlyでは一体どうやって書くの?

ということで、今回はplotlyで2次元配列をヒートマットでマップ化する。以前、pltでもマップ化したのでその続き。plotlyを使用することで各マス目のデータをホバーとして閲覧したり拡大や縮小などが簡単なのがメリット。

以前のpltのマップについては以下参照。

【python&imshow】plt.imshowで2次元配列をマップ化

続きを見る

ちなみに似たようなグラフで2次元ヒストグラム(plotlyだと2dHistogram)があるが今回はこれは扱わない。

python環境は以下。

  • Python 3.9.4
  • plotly 4.14.3
  • plotly-orca 3.4.2
  • numpy 1.20.3

運営者のメガネです。YouTubeTwitterInstagramも運営中。自己紹介お問い合わせページあります。

運営者メガネ

2Dヒストグラム

2Dヒストグラムはイメージとしては、ある範囲のxと、ある範囲のyの両方の範囲に果てはまる数が何個あるかを2次元のマップとして表したものだ。上の画像だと、x0から4の範囲 かつ y10から14の範囲であるデータは黄色と水色が重なっている合計8データだ。これを今回で言えば合計6種類の範囲で行うことで2Dヒストグラムが完成する。

上の画像の右側の2Dヒストグラムのマップを作成することができるコードを以下に示す。

import plotly.graph_objects as go
import plotly.io as pio
# 2dhistogramの挙動を調べる

x = [i for i in range(10)] + [i for i in reversed(range(10))]
y = [i for i in range(10, 20 + 1)] + [i for i in reversed(range(10, 20 + 1))]
print(x)
# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
print(y)
# [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10]

def histogram1():
    fig = go.Figure(go.Histogram2d(
        x=x,
        y=y
    ))
    fig.show()

    pio.write_html(fig, '2dhistogram1.html')
    pio.write_image(fig, '2dhistogram1.png')

histogram1()

他のレパートリーについては以下に載せる。

importとかの下準備と使用するデータ

まずは必要なimport関連やデータの作成。

使用するモジュールのimport

import plotly.graph_objects as go
import plotly.io as pio
import plotly.figure_factory as ff
import numpy as np

今回もいつものplotlyと同じimportが並ぶが、ffが新規。ffは最後に行う各マス目の値を簡単に表示させる用のもの。

使用するデータ

np.random.seed(seed=1)
arr = np.random.randint(1, 100, 36).astype("float")
# print(arr)
# # [38. 13. 73. 10. 76.  6. 80. 65. 17.  2. 77. 72.  7. 26. 51. 21. 19. 85.
# #  12. 29. 30. 15. 51. 69. 88. 88. 95. 97. 87. 14. 10.  8. 64. 62. 23. 58.]
arr2d = arr.reshape(4, -1)
# print(arr2d)
# # [[38. 13. 73. 10. 76.  6. 80. 65. 17.]
# #  [ 2. 77. 72.  7. 26. 51. 21. 19. 85.]
# #  [12. 29. 30. 15. 51. 69. 88. 88. 95.]
# #  [97. 87. 14. 10.  8. 64. 62. 23. 58.]]
arr2d[0][3] = np.nan
arr2d[1][2] = np.nan
arr2d[3][4] = np.nan
# print(arr2d)
# # [[38. 13. 73. nan 76.  6. 80. 65. 17.]
# #  [ 2. 77. nan  7. 26. 51. 21. 19. 85.]
# #  [12. 29. 30. 15. 51. 69. 88. 88. 95.]
# #  [97. 87. 14. 10. nan 64. 62. 23. 58.]]
# print(arr2d.shape)
# # (4, 9)

今回も以前と同様、使用するデータは乱数の4行9列の合計36データ。乱数はnp.random.seed(seed=1)で1番の乱数に固定。そのうちの3データをNaNに変更して、非数として扱うこととした。

非数を使用することでマップでのNaNの挙動を確認する。

2次元配列をヒートマップ化


まずは簡単にヒートマップを作成。使用する関数はgo.Heatmapで、zに2次元配列を入れるとヒートマップの高さ方向(色分けのこと)のデータを加えることが可能。

NaNは色付けはされず、背景の水色っぽい灰色っぽい色になっている。また、plotlyではデフォルトでグリッドが引かれているので、うっすらそのグリッドを確認することが可能。

def simple_heatmap():
    """2次元データを入れただけのヒートマップを作成
    """

    plot = []
    d = go.Heatmap(
        z=arr2d,  # 2次元配列を使用
    )
    plot.append(d)

    layout = go.Layout(
        title='heatmap',
        xaxis=dict(title='x'),
        yaxis=dict(title='y'),
    )

    fig = go.Figure(
        data=plot,
        layout=layout,
    )
    fig.show()

    pio.write_html(fig, 'simple_heatmap.html')
    pio.write_image(fig, 'simple_heatmap.png')

# 作成されるマップのyはpltとは上下逆であることに注意
simple_heatmap()

また、作成されるマップのy方向はpltとは異なりデフォルトでは上下逆となるので注意。上下逆にしたかったら、layoutyaxisに引数としてautorange='reversed'を入れると可能。

ヒートマップのパラメータを設定


ヒートマップには色々な引数が存在しており、この引数を使用することで見た目を変更することが可能。

def heatmap_params():
    """レイアウトの一部のパラメータを設定したヒートマップを作成
    """

    plot = []
    d = go.Heatmap(
        name='heatmap',  # 凡例とホバーで表示されるらしいけど出ない
        z=arr2d,
        x=(1, 2),  # 横軸ラベルに表示する値、1刻み
        y=(10, 20),  # 縦軸ラベルに表示する値、10刻み
        opacity=0.5,  # マップの透明度を0.5に
        showlegend=True,  # 凡例を表示
        colorbar=dict(
            len=0.8,  # カラーバーの長さを0.8に(デフォルトは1)
            outlinecolor='black',  # カラーバーの枠線の色
            outlinewidth=2,  # カラーバーの枠線の太さ
            bordercolor='gray',  # カラーバーとラベルを含むカラーバー自体の枠線の色
            borderwidth=1,  # カラーバーとラベルを含むカラーバー自体の枠線の太さ
            title=dict(
                text='bar',
                side='right',  # カラーバーのタイトルをつける位置(デフォルトはtop)
            ),
        ),
        colorscale='jet',  # カラーバーの色合いをjetに変更
        zmin=-5,  # カラーバーの最小値
        zmax=55,  # カラーバーの最大値
    )
    plot.append(d)

    layout = go.Layout(
        title='heatmap',
        xaxis=dict(title='x'),
        yaxis=dict(title='y'),
    )

    fig = go.Figure(
        data=plot,
        layout=layout,
    )
    fig.show()

    pio.write_html(fig, 'heatmap_params.html')
    pio.write_image(fig, 'heatmap_params.png')

heatmap_params()

x, yをそれぞれ設定すると、横軸の値を変更することが可能。今回はx(1, 2)としているので1刻みで表示されるが、y(10, 20)で10刻みなので表示も10刻みになる。なお、(10,)のように要素を1つだけにすると1刻みにとなり10, 11, ...と表記される。

また、カラーバーの最大値よりも大きい値をとるzの色は最大値と同じになる。これは最小値も同様。

マップの配色を変更

def heatmap_colorscale(colorscale: str):
    """カラースケールを色々変更したヒートマップを作成
    <https://plotly.com/python/builtin-colorscales/>
    """

    plot = []
    d = go.Heatmap(
        name='heatmap',  # 凡例とホバーで表示されるらしいけど出ない
        z=arr2d,
        colorscale=colorscale,
    )
    plot.append(d)

    layout = go.Layout(
        title=colorscale,
        xaxis=dict(title='x'),
        yaxis=dict(title='y'),
    )

    fig = go.Figure(
        data=plot,
        layout=layout,
    )
    fig.show()

    pio.write_html(fig, f"heatmap_{colorscale}.html")
    pio.write_image(fig, f"heatmap_{colorscale}.png")

# pltで使えてplotlyで使えないカラースケールは似たものを使用
for colorscale in ['gray', 'jet', 'hsv', 'oxy', 'plasma', 'Plotly3', 'RdBu']:
    heatmap_colorscale(
        colorscale=colorscale,
    )

マップの配色については先ほども行ったように、colorscaleで設定する。colorscaleに指定できるのはjetの他にも[[0, 'green'], [0.5, 'red'], [1.0, 'rgb(0, 0, 255)']]のように各カラーバーの位置と色を指定することでも可能。

カラースケール一覧を確認するには、カラースケールに該当しない色の名称(arayとかkkkkとか)を入れることでエラー文として確認することができる。もしくはplotlyのサイト「Built-in Continuous Color Scales in Python」から確認することも可能。

上のコードで示したカラースケール以外については以下のようにマップができる。

NaNの可視化


pltの時はNaNの位置は簡単に色を変えることができた。しかし、plotlyの場合ではNaNを検出して色を変更するという機能はないようだ(これから追加されるかもしれないが)。

なので、ここでは一旦NaN-1に変換し、0を灰色としてそこから値が大きくなるにつれてレインボーを形成するようなカラーバーを自作し対応。

def heatmap_nan():
    """NaNだけ灰色に色を変更したヒートマップを作成
    """

    new_arr2d = arr2d.copy()
    new_arr2d[np.isnan(new_arr2d)] = -1  # NaNを無限に変換

    plot = []
    d = go.Heatmap(
        z=new_arr2d,
        colorscale=[
            [0, 'gray'],  # NaNに該当する値を灰色にして区別する
            [0.01, 'purple'],  # 0が灰色でそれ以降を手動でレインボーに設定
            [0.25, 'blue'],
            [0.5, 'green'],
            [0.75, 'orange'],
            [1, 'red'],
        ],
        hovertemplate='x: %{x}<br>' + 'y: %{y}<br>' + 'z: %{z}<br>'
        + 'nan: %{text}<br>'  # zの値の表記を変更
        + "<extra></extra>",
        # ホバーのzの表記変更で使う値
        # np.nanでもfloat('nan')でもNoneでも表記はnullになる
        text=np.where(new_arr2d == -1, np.nan, new_arr2d)
        # text=np.where(new_arr2d == -1, None, new_arr2d),
    )
    plot.append(d)

    layout = go.Layout(
        title='include NaN',
        xaxis=dict(title='x'),
        yaxis=dict(title='y'),
    )

    fig = go.Figure(
        data=plot,
        layout=layout,
    )
    fig.show()

    pio.write_html(fig, 'heatmap_nan.html')
    pio.write_image(fig, 'heatmap_nan.png')

heatmap_nan()

また、このままだとホバーした時にzの値が-1となってしまうので、ホバーの値はHeatmapの引数textで-1NaNに変更しhovertemplateを使用することで対応。しかし、NaNとは表記されずnullとなる。

float('nan')でもNoneでもnullと表記されNaN表記でないところが気持ち悪いがここは現状維持で止まっておく。

カラーバーを水平にしたい(けどできない)


横長のマップの場合、カラーバーは左ではなく下に置く方がキレイに見えることがある。しかし、plotlyではそもそもカラーバーを下に置くことができないようだ。そこで、ここでは単純にカラーバーを下に置いてみた。

def heatmap_horizontal():
    """カラーバーを水平にすることはできないので、擬似的に水平に設置
    ただし、色の並びは垂直と同じなので実用性はない
    """

    plot = []
    d = go.Heatmap(
        z=arr2d,
        colorbar=dict(
            x=-0.01, xanchor='left',
            y=-0.05, yanchor='top',
            thickness=1800,  # カラーバーの横の長さは長く
            len=0.08,  # カラーバーの縦の長さは短く
        ),
    )
    plot.append(d)

    layout = go.Layout(
        title='heatmap',
        xaxis=dict(title='x'),
        yaxis=dict(title='y'),
    )

    fig = go.Figure(
        data=plot,
        layout=layout,
    )
    fig.show()

    pio.write_html(fig, 'heatmap_horizontal.html')
    pio.write_image(fig, 'heatmap_horizontal.png')

heatmap_horizontal()

しかし、いくら下に置いたからって元々は縦長のカラーバーなので、グラデーションの具合は縦のまま。全然実用的ではない。

マップの軸目盛を数値・文字に変更


軸目盛を変更できることが可能なのは既に上に書いたので、ここでは一定の法則ではない配列を軸ラベルにしたときの挙動について解説する。

y(10, 20, 50, 100)なので等間隔だが、x(1, 2, 4, 7, 11, 16, 22, 29)なので等間隔ではない。で、plotly的にはなるべくこの配列に近いように軸目盛を変更してくれるが、中途半端な値になっている。

原因はよくわからんが、初めの1, 21, 2.25になっているのでのちも崩れている。なので、下手に非等間隔の配列を指定するのは避けた方が良いだろう。

def heatmap_ticks(x: tuple, y: tuple, save_name: str):
    """軸の値を強制変更した時のヒートマップを作成

    Parameters
    ----------
    x : tuple
        横軸ラベルとして使用する配列
    y : tuple
        縦軸ラベルとして使用する配列
    save_name : str
        ファイルを個別に認識するためのファイル名の一部名称
    """

    plot = []
    d = go.Heatmap(
        name='heatmap',
        z=arr2d,
        x=x,
        y=y,
    )
    plot.append(d)

    layout = go.Layout(
        title='heatmap',
        xaxis=dict(title='x'),
        yaxis=dict(title='y'),
    )

    fig = go.Figure(
        data=plot,
        layout=layout,
    )
    fig.show()

    pio.write_html(fig, f"heatmap_ticks_{save_name}.html")
    pio.write_image(fig, f"heatmap_ticks_{save_name}.png")

heatmap_ticks(
    x=(1, 2, 4, 7, 11, 16, 22, 29),
    y=(10, 20, 50, 100),
    save_name='num',
)

また、Noneを配列中に入れるとその行(or列)は消える。さらに軸目盛のラベルにはlistdictなどを指定することができるが、dictを使用すると[object Object]となっておかしくなる。


# Noneの部分はマス目が消える
heatmap_ticks(
    x=(10, 'A', 'a', 'aaa', [1, 2], (3, 4), None, True, {10: 20}),
    y=(10, 20, 50, 100),
    save_name='moji',
)

さらに、横軸だけではなく縦軸にもNoneを適用すると、縦軸のどこにNoneを置いたかによっても挙動が大きく異なる。xに加えたyNoneNoneを置いた位置の隣の値は表示対象外になるっぽい。

# yのNoneはNoneを置いた位置の隣の値は表示対象外になるっぽい
tpl = (
    (None, 20, 50, 100),  # 上2つの領域が対象
    (10, None, 50, 100),  # 一番上が対象
    (10, 20, None, 100),  # 一番下が対象
    (10, 20, 50, None),  # 下2つが対象
)
for num, y in enumerate(tpl):
    heatmap_ticks(
        x=(10, None, 'a', 'aaa', None, None, None, True, None),
        y=y,
        save_name=f"None{num}",
    )

マス目を正方形に


これまでのヒートマップのマス目はデフォルトでの長方形型のマス目だった。ここではこれを正方形に整形する。正方形にしないと美しくないし、横軸、縦軸ともに等間隔で並んでいる際の勘違いや誤解を減らすことができる。

方法はとてもシンプルでgo.layoutの引数にscaleanchor='x'を追加すればいい。意味合いとしてはyのマス目のスケールをxに合わせるという意味で、両軸のスケールが合えばもちろんマス目は正方形になる。

def heatmap_square():
    """マス目を正方形にしたヒートマップを作成
    """

    plot = []
    d = go.Heatmap(
        z=arr2d,
    )
    plot.append(d)

    layout = go.Layout(
        title='heatmap',
        xaxis=dict(title='x'),
        yaxis=dict(
            title='y',
            scaleanchor='x',  # マス目をxと同じスケールにする
        ),
    )

    fig = go.Figure(
        data=plot,
        layout=layout,
    )
    fig.show()

    pio.write_html(fig, 'heatmap_square.html')
    pio.write_image(fig, 'heatmap_square.png')

heatmap_square()

巨大データを扱う

データによったら1000×1000のデータを扱うことがあるかもしれない。実際、執筆者は2000×2000程度のデータを研究として使用していた。これはPCのスペック的な面が影響を及ぼしているのかもしれないが、体感5000×5000データでマップの表示すらされなくなった。

また、Google Colaboratoryだと2000×2000データでランタイムの切断になった。したがって、データ数が多い場合はplotlyを使用せずにpltimshowなどを使用して軽量化を図る方がいいかもしれない。

# PCのスペックとかにもよるかもしれんが5000×5000データくらいだと表示すらされない
num = 350  # 1辺のデータ数
arr_l = np.arange(num ** 2)
arr2d_l = arr_l.reshape(num, -1)

print(arr2d_l.shape)
# (350, 350)

def heatmap_large():
    """データ数の多いヒートマップを作成
    """

    plot = []
    d = go.Heatmap(
        z=arr2d_l,
        colorscale='twilight',
    )
    plot.append(d)

    layout = go.Layout(
        title='heatmap large',
        xaxis=dict(title='x',),
        yaxis=dict(title='y',),
    )

    fig = go.Figure(
        data=plot,
        layout=layout,
    )
    fig.show()

    pio.write_html(fig, 'heatmap_large.html')
    pio.write_image(fig, 'heatmap_large.png')

heatmap_large()

各マス目に値を出力


最後にヒートマップの各マス目にそのマス目の値を出力する。使用するモジュールはffff.create_annotated_heatmapとすることでマス目に値を入れることができる。

def annotation_heatmap():
    """zの値をマス目に表示
    ただし、横軸・縦軸のラベルはつかない
    """
    # ヒートマップにしたい配列
    z = [
        [1, 20, 30],
        [20, 1, 60],
        [30, 60, 1]
    ]
    fig = ff.create_annotated_heatmap(
        z=z
    )
    fig.show()

    pio.write_html(fig, 'annotation_heatmap.html')
    pio.write_image(fig, 'annotation_heatmap.png')

annotation_heatmap()

ただ、この場合だと横・縦軸の目盛が消えているので追記する必要がある。

目盛の追記


目盛の追記はff.create_annotated_heatmapの引数xyにそれぞれ配列を入れてあげることで設定可能。

def annotation_xy_heatmap():
    """zの値をマス目に表示
    横軸・縦軸のラベルをつける
    """
    # ヒートマップにしたい配列
    z = [
        [1, 20, 30],
        [20, 1, 60],
        [30, 60, 1]
    ]
    fig = ff.create_annotated_heatmap(
        z=z,
        x=(1, 2, 3),
        y=(0, 1, 2),
    )
    fig.show()

    pio.write_html(fig, 'annotation_xy_heatmap.html')
    pio.write_image(fig, 'annotation_xy_heatmap.png')

annotation_xy_heatmap()

なお、x, yは1刻みなので10刻みや100刻みの配列を入れると目盛のラベルが多くなりすぎる。


def annotation_large_heatmap():
    """zの値をマス目に表示
    横軸・縦軸のラベルをつける
    ラベルの値は1刻みで作らないver.
    """
    # ヒートマップにしたい配列
    z = [
        [1, 20, 30],
        [20, 1, 60],
        [30, 60, 1]
    ]
    fig = ff.create_annotated_heatmap(
        z=z,
        x=(100, 200, 300),
        y=(10, 20, 30),
    )
    fig.show()

    pio.write_html(fig, 'annotation_large_heatmap.html')
    pio.write_image(fig, 'annotation_large_heatmap.png')

annotation_large_heatmap()

したがって、xaxisで目盛の位置と表記を設定する。tickvalsで自作の目盛を入れる位置を、ticktextで自作の目盛自体を設定する。


def annotation_large_heatmap2():
    """zの値をマス目に表示
    横軸・縦軸のラベルをつける
    ラベルの値は1刻みで作らない&刻みを合わせるver.
    """
    # ヒートマップにしたい配列
    z = [
        [1, 20, 30],
        [20, 1, 60],
        [30, 60, 1]
    ]
    fig = ff.create_annotated_heatmap(
        z=z,
        x=(100, 200, 300),
        y=('A', 'c', 'DDD'),
    )
    fig.update_xaxes(
        tickvals=[100, 200, 300],
        ticktext=['100', '200', '300']
    )

    fig.show()

    pio.write_html(fig, 'annotation_large_heatmap2.html')
    pio.write_image(fig, 'annotation_large_heatmap2.png')

annotation_large_heatmap2()

まだまだ発展途上

今回はplotlyのヒートマップを使用した2次元配列のマップ化を解説した。pltに比べてplotlyだとヒートマップでできることが格段に減るので、ホバーを使いたいとき以外は基本的にはpltで賄う方がいいのかもしれない。

関連記事

【python&imshow】plt.imshowで2次元配列をマップ化

続きを見る

【plotly&3D】goで3Dグラフを作成

続きを見る

【plotly&go.Scatter】plotlyの散布図グラフの描き方

続きを見る

【plotly&ボタン】plotlyのupdatemenusにbuttonsを追加

続きを見る

ガジェット

2023/9/18

【デスクツアー2022下半期】モノは少なく、でも効率的に Desk Updating #0

今回はガジェットブロガーなのにデスク環境を構築していない執筆者の ...

ライフハック

2023/9/16

【Audible vs YouTube Premium】耳で聴く音声学習コンテンツを比較

ワイヤレスイヤホンが普及し耳で学習することへのハードルが格段に下 ...

完全ワイヤレスイヤホン(TWS)

2023/9/18

【SENNHEISER MOMENTUM True Wireless 3レビュー】全てが整ったイヤホン

今回は高音質・高機能なSENNHEISERのフラグシップ完全ワイヤレスイヤホン「SENNH ...

ライフハック

2023/3/11

【YouTube Premiumとは】メリットしかないから全員入れ

今回はYouTube Premiumを実際に使ってみてどうなのか、どんなメリット/デメリット ...

マウス

2023/8/17

【Logicool MX ERGOレビュー】疲れない作業効率重視トラックボールマウス

こんな人におすすめ トラックボールマウスの王道Logicool MX ERGOが気になるけどऩ ...

ベストバイ

2023/9/18

【ベストバイ2022】今年買って良かったモノのトップ10

2022年ベストバイ この1年を振り返って執筆者は何を買ったのか。ガジェッ& ...

スマホ

2023/1/15

【楽天モバイル×povo2.0の併用】月1,000円の保険付きデュアルSIM運用

こんな人におすすめ 楽天モバイルとpovo2.0のデュアルSIM運用って実際のとこ ...

マウス

2023/9/16

【Logicool MX ERGO vs MX Master 3】ERGOをメインにした決定的な理由

こんな疑問・お悩みを持っている人におすすめ 執筆者はLogicoolのハイエンӠ ...

macOSアプリケーション

2022/9/30

【Chrome拡張機能】便利で効率的に作業できるおすすめの拡張機能を18個紹介する

こんな人におすすめ Chromeの拡張機能を入れたいけど、調べても同じような ...

macOSアプリケーション

2023/5/3

【Automator活用術】Macで生産性を上げる作業の自動化術

今回はMacに標準でインストールされているアプリ「Automator」を使ってできる ...

Pythonを学びたいけど独学できる時間なんてない人へのすゝめ

執筆者は大学の研究室・大学院にて独学でPythonを習得した。

でも社会人になったら独学で行うには時間も体力もなくて大変だ。

時間がない社会人だからこそプロの教えを乞うのが効率的。

ここでは色んなタイプに合ったプログラミングスクールの紹介をする。

  • この記事を書いた人

メガネ

Webエンジニア駆け出し。独学のPythonで天文学系の大学院を修了。常時金欠のガジェット好きでM2 Pro MacBook Pro(30万円) x Galaxy S22 Ultra(17万円)使いの狂人。自己紹介と半生→変わって楽しいの繰り返しレビュー依頼など→お問い合わせ運営者情報、TwitterX@m_ten_pa、 YouTube@megatenpa、 Threads@megatenpa

-go
-, , ,