こんな人にオススメ
以前、matplotlib.pyplot
、plt
のplt.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
2Dヒストグラム
2Dヒストグラムはイメージとしては、ある範囲のx
と、ある範囲のy
の両方の範囲に果てはまる数が何個あるかを2次元のマップとして表したものだ。上の画像だと、x
が0
から4
の範囲 かつ y
が10
から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
とは異なりデフォルトでは上下逆となるので注意。上下逆にしたかったら、layout
のyaxis
に引数として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で-1
をNaN
に変更し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
, 2
が1
, 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列)は消える。さらに軸目盛のラベルにはlist
やdict
などを指定することができるが、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
に加えたy
のNone
はNone
を置いた位置の隣の値は表示対象外になるっぽい。
# 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
を使用せずにplt
のimshow
などを使用して軽量化を図る方がいいかもしれない。
# 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()
各マス目に値を出力
最後にヒートマップの各マス目にそのマス目の値を出力する。使用するモジュールはff
でff.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
の引数x
とy
にそれぞれ配列を入れてあげることで設定可能。
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を追加
続きを見る