積み上げ棒グラフの考え方整理しよう!ライブラリ「Bokeh」で積み上げ棒グラフに数値を入れる

データアナリティクス事業本部@札幌の佐藤です。

Bokehライブラリの積み上げ棒グラフは頭の中でしっかりイメージをつけないとうまく実装できないなと思うことが多々あります。
私もちょいちょい描画した結果がおかしくなっていることがあり、自分の中での整理も含めて積み上げ棒グラフについて書いていきたいと思います。

Bokehのバージョンは1.4.0です。

積み上げ棒グラフのイメージ

こんな感じのグラフができます。

積み上げ棒グラフは hbar_stack()(vbar_stack())で実装します。

実装する上で大切なのが、Y軸とX軸に何を置くのかということを意識することです。
当たり前の話ではありますが、実装していくにあたってどっちがどっちなのか分からなくなることがあります。
この後説明しますが、実装するにあたり行列変換したり描画するけど、明示的に実装しないというところもあるためです。

データは以下のようなものを用意して年別の売上高を出すようなイメージにします。
ヘッダの食べ物は昔やっていた『アイカツフレンズ!』のコラボカフェのメニューです。
数値は適当なので、コラボカフェの実際とは全く異なります。

year トマトバジルチーズのスペシャルサンドイッチ ベリーパフェ チョコミントマカロン風ケーキ わたあめ
2012 10 8
2013 11 8 3 0
2014 14 11 9 4
2015 15 13 13 4
2016 18 13 11 3
2017 28 15 12 2
2018 15 10 8 3
2019 17 13

実際に描画する

import pandas as pd
# bokeh系のライブラリ
from bokeh.plotting import figure, show
from bokeh.io import output_notebook
from bokeh.models import ColumnDataSource, FactorRange
from bokeh.core.properties import value
from bokeh.palettes import RdYlGn

# jupyter notebookで出力させるのでoutput_notebook()を記載
output_notebook()

df = pd.read_csv("XXXX/sales.tsv", engine='python', encoding="utf-8", sep="\t", header=0)
# キャスト
df.iloc[:, [1,2,3,4]]= df.fillna("0").iloc[:, [1,2,3,4]].astype("int")

DataFrameとして処理します。空白を0に置き換えて、数値型にキャストしておきます。

DataFrameをそのまま使用しないほうがよい

ここからが積み上げ棒グラフのポイントのひとつとなりますが、このままDataFrameを使用してもでは上手く作成することができません。
DataFrameをそののまま使用して積み上げ棒グラフを作成することは可能ですが、グラフを逆順にしたいなど場合DataFrameのindexを逆にしても行うことができません。

menu=["トマトバジルチーズのスペシャルサンドイッチ", "ベリーパフェ", "チョコミントマカロン風ケーキ", "わたあめ"]

p = figure(plot_width=1000,
           plot_height=500, 
           title="ペンギンカフェ年間売上高")

p.hbar_stack(stackers=menu, 
             y='year', 
             height=0.9, 
             color=RdYlGn[4], 
             source=ColumnDataSource(df),
             legend_label=menu)

p.xaxis.axis_label = '売上高(十万円)'

p.yaxis.ticker = df["year"] # Y軸をDataFrameの年度にする

show(p)

ぱっと見はちゃんとできている風になっているので、これでOKなケースもあると思います。 このケースはY軸が数値とみなしているのでうまくいっていますが、数値ではない場合は figure() で明示的にしてしていないので上手くいきません。

じゃあ figure() に明示的に指定することで逆順にできるのかですが、明示的にしてもうまくいきません。

years = ["2012","2013","2014","2015","2016","2017","2018","2019"]

p = figure(y_range=years, # 明示的に指定
           plot_width=1000,
           plot_height=500, 
           title="ペンギンカフェ年間売上高")

p.hbar_stack(stackers=menu, 
             y='year', 
             height=0.9, 
             color=RdYlGn[4], 
             source=ColumnDataSource(df),
             legend_label=menu)

p.xaxis.axis_label = '売上高(十万円)'

p.yaxis.ticker = df["year"] # Y軸をDataFrameの年度にする

show(p)

積み上げ棒グラフどこ行った……。

積み上げ棒グラフ用の辞書型変数を作成するのがベター

ベターな実装はDataFrameを加工して、積み上げ棒グラフ用の辞書型変数を作成してしまうことです。
その際にまず以下のように行列変換します。

# 行列変換
dfs = df.T
# figure()に指定する用の年を設定
years = [str(i) for i in dfs.values[0]]

# X軸に表示順のListを定義
menu = ["トマトバジルチーズのスペシャルサンドイッチ", "ベリーパフェ", "チョコミントマカロン風ケーキ", "わたあめ"]

# 値を持たせた辞書型変数を定義
data = {'years' : years,
        'トマトバジルチーズのスペシャルサンドイッチ'   : dfs.values[1],
        'ベリーパフェ'   : dfs.values[2],
        'チョコミントマカロン風ケーキ'   : dfs.values[3],
        'わたあめ'   : dfs.values[4]
       }

行列変換すると以下のような形になります。

0 1 2 3 4 5 6 7
year 2012 2013 2014 2015 2016 2017 2018 2019
トマトバジルチーズのスペシャルサンドイッチ 10 11 14 15 18 28 15 17
ベリーパフェ 8 8 11 13 13 15 10 13
チョコミントマカロン風ケーキ 0 3 9 13 11 12 8 0
わたあめ 0 0 4 4 3 2 3 0

意識するのは描画されるときに、0列~7列の列の単位で出力されること、Y軸はyear、X軸は「トマトバジルチーズのスペシャルサンドイッチ」「ベリーパフェ」「チョコミントマカロン風ケーキ」「わたあめ」の積み上げであるということです。

これを意識して、figure() に指定する用の年の変数、X軸用の表示順の変数を用意します。
data 変数のkeyとと上記ふたつで定義した値が異なっている場合、描画時におかしくなるので注意です。

p = figure(y_range=years, 
           plot_width=1000,
           plot_height=500, 
           title="ペンギンカフェ年間売上高")

p.hbar_stack(stackers=menu, 
             y='years', 
             height=0.9, 
             color=RdYlGn[4], 
             source=ColumnDataSource(data=data),
             legend_label=menu)


p.xaxis.axis_label = '売上高(十万円)'   

show(p)

実装自体はこれで終わりですが、どうせなら数値もグラフ内にあったほうが良いですよね。

グラフ内に数値を入れる

p = figure(y_range=years, 
           plot_width=1000,
           plot_height=500, 
           title="ペンギンカフェ年間売上高")

p.hbar_stack(stackers=menu, 
             y='years', 
             height=0.9, 
             color=RdYlGn[4], 
             source=ColumnDataSource(data=data),
             legend_label=menu)


p.text(x=[i/2.5 for i in data['トマトバジルチーズのスペシャルサンドイッチ']], 
       y=years, 
       text=[i if i != 0 else None for i in data['トマトバジルチーズのスペシャルサンドイッチ']], 
       y_offset=10,
       text_font_size="10pt")

p.text(x=[i+j/2.5 for i,j in zip(data['トマトバジルチーズのスペシャルサンドイッチ'],data['ベリーパフェ'])], 
       y=years, text=[i if i != 0 else None for i in data['ベリーパフェ']], 
       y_offset=10,
       text_font_size="10pt")

p.text(x=[i+j+x/2.5 for i,j,x in zip(data['トマトバジルチーズのスペシャルサンドイッチ'],data['ベリーパフェ'],data['チョコミントマカロン風ケーキ'])], 
       y=years, 
       text=[i if i != 0 else None for i in data['チョコミントマカロン風ケーキ']], 
       y_offset=10,
       text_font_size="10pt")

p.text(x=[i+j+x+y/2.5 for i,j,x,y in zip(data['トマトバジルチーズのスペシャルサンドイッチ'],data['ベリーパフェ'],data['チョコミントマカロン風ケーキ'],data['わたあめ'])], 
       y=years, 
       text=[i if i != 0 else None for i in data['わたあめ']], 
       y_offset=10,
       text_font_size="10pt")


p.xaxis.axis_label = '売上高(十万円)'


show(p)

text()を使用することでグラフ内に数値をセットできます。
X軸Y軸の座標をセットしてあげるのことが必要なので、それぞれセットさせます。

今回は積み上げるのが4つだったので x の書き方をべた書きしていますが要素数が多い場合は考慮してあげたほうが良いです。
0を積み上げ棒グラフに表示させたくなかったので、0の場合は None にすることで表示させないようにしています。

また座標の調整として、x_offsety_offset を使用できますが、これはあくまで微調整用と考えてください。
上記だと2.5で除算していますが、これを x_offset でやろうとした場合、単純にX軸の末端(一番値の大きい数字)から座標を加減算するだけですので、値によって表示場所がバラバラになります。

p = figure(y_range=years, 
           plot_width=1000,
           plot_height=500, 
           title="ペンギンカフェ年間売上高")

p.hbar_stack(stackers=menu, 
             y='years', 
             height=0.9, 
             color=RdYlGn[4], 
             source=ColumnDataSource(data=data),
             legend_label=menu)

p.text(x=[i for i in data['トマトバジルチーズのスペシャルサンドイッチ']], 
       y=years, 
       text=[i if i != 0 else None for i in data['トマトバジルチーズのスペシャルサンドイッチ']], 
       x_offset=-150, # textでの減算はやめて、x_offsetで調整する
       y_offset=10,
       text_font_size="10pt")

p.xaxis.axis_label = '売上高(十万円)'

show(p)

最後に

積み上げ棒グラフは辞書型変数の作成で戸惑うところがあるとはお思いますが、それさえできてしまえば簡単に実装することができます。 是非実装してみてください。

参考

Handling Categorical Data