からっぽのしょこ

読んだら書く!書いたら読む!同じ事は二度調べ(たく)ない

斜線入りのヒートマップの作成

はじめに

 ヒートマップの任意の場所に斜線を描画します。Matplotlibライブラリを利用したヒートマップの作図については「https://www.anarchive-beta.com/entry/2021/10/01/100107」を参照してください。

斜線入りのヒートマップの作成

 matplotlibライブラリのAxes.pcolor()を用いて、任意の場所に斜線を入れたヒートマップを作図します。

 利用するライブラリを読み込みます。

# 利用するモジュール
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import numpy as np


 まずは、行数Rと列数Cを指定して、作図に利用する配列xを作成します。

# 行数と列数を指定
R = 5
C = 7

# 配列を作成
x = np.arange(R * C).reshape([R, C])
print(x)
print(x.shape)
[[ 0  1  2  3  4  5  6]
 [ 7  8  9 10 11 12 13]
 [14 15 16 17 18 19 20]
 [21 22 23 24 25 26 27]
 [28 29 30 31 32 33 34]]
(5, 7)

 この例では、グラデーションが分かりやすいように、値が1ずつ大きくなる2次元配列を作成します。
 RCも作図に使います。

 Axes.pcolor()hatch引数を指定すると斜線を入れられます。

# ヒートマップを作成
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(8, 6)) # 図の設定
ax.pcolor(x, hatch='x', edgecolor='blue') # 斜線入りヒートマップ
ax.set_xticks(np.arange(C) + 0.5) # x軸目盛の位置
ax.set_xticklabels(np.arange(C))  # x軸目盛のラベル
ax.set_yticks(np.arange(R) + 0.5) # y軸目盛の位置
ax.set_yticklabels(np.arange(R))  # y軸目盛のラベル
ax.invert_yaxis() # y軸を反転
ax.set_aspect('equal', adjustable='box') # アスペクト比
plt.show() # 図を表示

f:id:anemptyarchive:20211001074437p:plain
ヒートマップ:全面斜線

 ここでの目的は、一部のタイルのみに斜線を入れることです。

・配列のマスキング

 斜線の描画用に、任意の要素をマスクした配列を作成します。

 表示しない要素をマスクした配列x_maskを作成します。

# 表示するインデックスを指定
r = 2
c = 2

# 元の配列と同じ形状の配列を作成
x_mask = np.zeros_like(x)

# マスクしない要素を指定
x_mask[r, c] = 1
#x_mask[r, :] = 1
#x_mask[:, c] = 1

# 指定した要素以外をマスク
x_mask = np.ma.masked_where(x_mask != 1, x)
print(x_mask)
[[-- -- -- -- -- -- --]
 [-- -- -- -- -- -- --]
 [-- -- 16 -- -- -- --]
 [-- -- -- -- -- -- --]
 [-- -- -- -- -- -- --]]

 rc列目の要素以外がマスクされました。
 マスクについては、NumPyの公式ドキュメント「The numpy.ma module — NumPy v1.21 Manual」を参照してください。masked_***()というマスク用の関数は他にも実装されています。用途に応じて使い分けてください。

 マスクされていない要素のみ描画されます。

# ヒートマップを作成
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(8, 6)) # 図の設定
ax.pcolor(x_mask) # ヒートマップ
ax.set_xticks(np.arange(C) + 0.5) # x軸目盛の位置
ax.set_xticklabels(np.arange(C))  # x軸目盛のラベル
ax.set_yticks(np.arange(R) + 0.5) # y軸目盛の位置
ax.set_yticklabels(np.arange(R))  # y軸目盛のラベル
ax.invert_yaxis() # y軸を反転
ax.set_aspect('equal', adjustable='box') # アスペクト比
plt.show() # 図を表示

f:id:anemptyarchive:20211001074458p:plain
ヒートマップ:任意の要素


・斜線入りヒートマップの作図

 任意の場所に斜線を描画します。

 元の配列xとhatch用の配列x_maskのヒートマップを重ねて作図します。

# ヒートマップを作成
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(8, 6)) # 図の設定
ax.pcolor(x) # ヒートマップ
ax.pcolor(x_mask, hatch='x', edgecolor='blue', linewidth=2) # 斜線入りヒートマップ
ax.set_xticks(np.arange(C) + 0.5) # x軸目盛の位置
ax.set_xticklabels(np.arange(C))  # x軸目盛のラベル
ax.set_yticks(np.arange(R) + 0.5) # y軸目盛の位置
ax.set_yticklabels(np.arange(R))  # y軸目盛のラベル
ax.invert_yaxis() # y軸を反転
ax.set_aspect('equal', adjustable='box') # アスペクト比
plt.show() # 図を表示

f:id:anemptyarchive:20211001074554p:plain
ヒートマップ:一部斜線(未完)

 x_maskのヒートマップに関して、x_maskの最小値と最大値から色付けが決まるため、xx_maskで色が対応していません。

 そこで、x_maskのグラデーションの最小値と最大値をxに対応させます。

# ヒートマップを作成
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(8, 6)) # 図の設定
ax.pcolor(x) # ヒートマップ
ax.pcolor(x_mask, hatch='x', edgecolor='blue', linewidth=2, 
          vmin=np.min(x), vmax=np.max(x)) # 斜線入りヒートマップ
ax.set_xticks(np.arange(C) + 0.5) # x軸目盛の位置
ax.set_xticklabels(np.arange(C))  # x軸目盛のラベル
ax.set_yticks(np.arange(R) + 0.5) # y軸目盛の位置
ax.set_yticklabels(np.arange(R))  # y軸目盛のラベル
ax.invert_yaxis() # y軸を反転
ax.set_aspect('equal', adjustable='box') # アスペクト比
plt.show() # 図を表示

f:id:anemptyarchive:20211001074619p:plain
ヒートマップ:一部斜線

 vmin, vmax引数でグラデーションの最小値と最大値を設定します。

 以上で任意の場所に斜線を入れられました。

 続いて、条件に応じて斜線を入れます。

 値をランダムに生成して、指定した値より大きい場合だけマスクします。

# 値をランダムに生成
x = np.random.rand(R, C)
print(np.round(x, 1))

# 0.5より大きい要素をマスク
x_mask = np.ma.masked_where(x > 0.5, x)
print(np.round(x_mask, 1))
[[0.5 0.1 0.9 0.7 0.4 0.2 0.1]
 [0.3 0.5 1.  0.6 0.5 0.7 0.8]
 [0.7 0.2 0.1 0.4 0.5 0.4 0. ]
 [0.  0.7 0.9 0.1 0.2 0.8 0.1]
 [0.2 0.9 0.5 0.4 0.7 0.5 0.1]]
[[0.5 0.1 -- -- 0.4 0.2 0.1]
 [0.3 -- -- -- -- -- --]
 [-- 0.2 0.1 0.4 -- 0.4 0.0]
 [0.0 -- -- 0.1 0.2 -- 0.1]
 [0.2 -- 0.5 0.4 -- 0.5 0.1]]

 0から1の一様乱数に従い配列xの値を生成します。
 0.5より大きい要素をマスクした配列x_maskを作成します。

 マスクしていない要素(0.5以下の要素)に斜線を入れます。

# ヒートマップを作成
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(9, 6)) # 図の設定
ax.pcolor(x) # ヒートマップ
c = ax.pcolor(x_mask, hatch='x', edgecolor='blue', 
              vmin=np.min(x), vmax=np.max(x)) # 斜線入りヒートマップ
ax.set_xticks(np.arange(C) + 0.5) # x軸目盛の位置
ax.set_xticklabels(np.arange(C))  # x軸目盛のラベル
ax.set_yticks(np.arange(R) + 0.5) # y軸目盛の位置
ax.set_yticklabels(np.arange(R))  # y軸目盛のラベル
ax.invert_yaxis() # y軸を反転
ax.set_aspect('equal', adjustable='box') # アスペクト比
fig.colorbar(c, ax=ax) # カラーバー
plt.show() # 図を表示

f:id:anemptyarchive:20211001074637p:plain
ヒートマップ:条件に従って斜線

 マスクしていない要素に斜線が入ります。つまり、np.ma.masked_where()の条件を満たす要素がそのまま表示されます。

・hatchの設定

 hatch引数に指定できる記号と模様を確認します。

 全てのパターンを描画してみます。

・作図コード(クリックで展開)

# hatch用の記号を設定
pattern_lt = ['/', '\\', '|', '-', '+', 'x', 'o', 'O', '.', '*']

# 行数と列数を指定
R, C = 5, 7

# 配列を作成
a = np.ones((R, C))

# ヒートマップを作成
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(10, 4)) # 図の設定
#ax.pcolor(x, cmap='Blues') # ヒートマップ

# 記号ごとに斜線を描画
for i in range(len(pattern_lt)):
    # マスクした配列を作成
    a_mask = np.zeros_like(a)
    a_mask[i // C, i % C] = 1
    a_mask = np.ma.masked_where(a_mask != 1, a)
    
    # 斜線を描画
    ax.pcolor(a_mask, cmap='Blues', hatch=pattern_lt[i], edgecolor='turquoise') # 斜線入りヒートマップ
ax.set_xticks(np.arange(C) + 0.5) # x軸目盛の位置
ax.set_xticklabels(np.arange(C))  # x軸目盛のラベル
ax.set_yticks(np.arange(R) + 0.5) # y軸目盛の位置
ax.set_yticklabels(np.arange(R))  # y軸目盛のラベル
ax.set_title('hatch=' + str(pattern_lt)) # タイトル
ax.invert_yaxis() # y軸を反転
ax.set_aspect('equal', adjustable='box') # アスペクト比
plt.show() # 図を表示

f:id:anemptyarchive:20211001074707p:plain
hatchの設定

 \が2つになっているのはエスケープキーのためです。
 hatchに指定できる記号(模様)については、Matplotlibの公式ドキュメント「matplotlib.collections — Matplotlib 3.4.3 documentation」を参照してください。

 記号の数を増やすと、描画される斜線等の密度が高くなります。

f:id:anemptyarchive:20211001074750p:plain
hatchの設定


・アニメーションの作成

 斜線入りのタイルの位置を変更するアニメーションを作成します。

 斜線入りのタイルを1つずつ移動します。

・作図コード(クリックで展開)

# 行数と列数を指定
R, C = 5, 7

# 配列を作成
x = np.arange(R * C).reshape([R, C])

# hatch用の記号を設定
pattern_lt = ['/', '\\', '|', '-', '+', 'x', 'o', 'O', '.', '*']

# 図の設定
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(8, 6))

# 作図処理を関数として定義
def update(i):
    # 前フレームのグラフを初期化
    plt.cla()
    
    # i番目以外の要素をマスクした配列を作成
    x_mask = np.zeros_like(x)
    x_mask[i // C, i % C] = 1
    x_mask = np.ma.masked_where(x_mask != 1, x)
    
    # i回目のヒートマップを作成
    ax.pcolor(x) # ヒートマップ
    ax.pcolor(x_mask, hatch=pattern_lt[i % len(pattern_lt)], edgecolor='blue', linewidth=2, 
              vmin=np.min(x), vmax=np.max(x)) # 斜線入りヒートマップ
    ax.set_xticks(np.arange(C) + 0.5) # x軸目盛の位置
    ax.set_xticklabels(np.arange(C))  # x軸目盛のラベル
    ax.set_yticks(np.arange(R) + 0.5) # y軸目盛の位置
    ax.set_yticklabels(np.arange(R))  # y軸目盛のラベル
    ax.set_title('i=' + str(i) + ', hatch=' + pattern_lt[i % len(pattern_lt)]) # タイトル
    ax.invert_yaxis() # y軸を反転
    ax.set_aspect('equal', adjustable='box') # アスペクト比

# gif画像を作成
hatch_anime = FuncAnimation(fig, update, frames=R * C, interval=200)

# gif画像を保存
hatch_anime.save('hatch.gif')

 pattern_lt[i % len(pattern_lt)]の部分は普通に記号を1つ指定してください。

f:id:anemptyarchive:20211001074826g:plain
斜線入りヒートマップ:斜線の移動


 斜線を入れる条件を0.1ずつ大きくします。

・作図コード(クリックで展開)

# 行数と列数を指定
R, C = 5, 7

# 値をランダムに生成
x = np.random.rand(R * C).reshape([R, C])

# 図の設定
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(9, 6))

# フラグをFalseに設定
cbar_flg = False

# 作図処理を関数として定義
def update(i):
    global cbar_flg
    # 前フレームのグラフを初期化
    plt.cla()
    
    # 0.i以上の要素をマスクした配列を作成
    x_mask = np.ma.masked_where(x >= i * 0.1, x)
    
    # i回目のヒートマップを作成
    c = ax.pcolor(x) # ヒートマップ
    ax.pcolor(x_mask, hatch='x', edgecolor='blue', vmin=np.min(x), vmax=np.max(x)) # 斜線入りヒートマップ
    ax.set_xticks(np.arange(C) + 0.5) # x軸目盛の位置
    ax.set_xticklabels(np.arange(C))  # x軸目盛のラベル
    ax.set_yticks(np.arange(R) + 0.5) # y軸目盛の位置
    ax.set_yticklabels(np.arange(R))  # y軸目盛のラベル
    ax.set_title('i=' + str(i) + ', x<' + str(np.round(i * 0.1, 1))) # タイトル
    ax.invert_yaxis() # y軸を反転
    ax.set_aspect('equal', adjustable='box') # アスペクト比
    
    # 初回のみカラーバーを実行
    if not cbar_flg:
        cbar_flg = True # フラグをTrueに変更
        fig.colorbar(c, ax=ax) # カラーバー

# gif画像を作成
hatch_anime = FuncAnimation(fig, update, frames=11, interval=500)

# gif画像を保存
hatch_anime.save('random.gif')

f:id:anemptyarchive:20211001074911g:plain
斜線入りヒートマップ:条件の変更

 カラーバーは初期化されないようなので(?)、フレームの回数分表示されてしまいます。
 そこで、cbar_flgを切り替えて初回のみfig.colorbar()を実行します。glovalって何?これがないとカラーバーが2つ表示されるのはなぜ?。

おわりに

 思いの外苦労したので、メモしておきます。
 ところで、hatchってなんて訳すの?斜線?陰影?個人的には網掛けと呼びたいんだけど、meshとどう違うんだ。

 先日公開されたカバーが最高なのでみんな聴いて。