【matplotlib】棒グラフを積み上げて表示する【Python】
積み上げ式の棒グラフを作成する方法。
カテゴリー別に棒グラフを並べたい場合は、seabornでグラフを作成する際にhueを指定すれば、簡単にhue別に並べたグラフを作成できる。
一方、グラフを縦に積み上げたい場合、hue毎に並べて表示するような簡単な方法ではできない。
シンプルな積み上げ棒グラフは割と簡単に作成できるが、例えばhueで横並びにしつつ縦にも積上げるような棒グラフの作成は、専用の機能がないので少し面倒。しかし、matplotlibの勉強にはなる。
基本的な積み上げ棒グラフの作成
hueなどでカテゴリー別の棒グラフなどを作成しない、基本的な棒グラフを縦に積み上げていく。
データ準備
以下のようなデータを対象としてグラフを作成する。
1import pandas as pd
2# データフレームの作成
3data = pd.DataFrame({
4 '月': ['1月', '2月', '3月', '4月', '5月'],
5 'hue1': [20, 30, 40, 50, 60],
6 'hue2': [15, 25, 35, 45, 55],
7 'hue3': [10, 20, 30, 40, 50]
8})
9print(data)
10# 月 hue1 hue2 hue3
11# 0 1月 20 15 10
12# 1 2月 30 25 20
13# 2 3月 40 35 30
14# 3 4月 50 45 40
15# 4 5月 60 55 50
日付列と各月のデータ3種類(hue1, hue2, hue3)が並んでいるので、月毎に積み上げる。
簡単な積み上げ棒グラフの場合、matplotlibでもseabornでもどちらでもできる。
matplotlibを使った積上げ棒グラフ
積上げ棒グラフを作成する場合、matplotlibを使った方がやりやすい。
hue1、hue2、hue3の各列の棒グラフを作成する。plt.bar()を3回やる感じ。
棒グラフを作成する際に、bottom_dataという引数を指定することで、棒グラフのy軸の開始位置を設定できる。
hue2、hue3の棒グラフ作成時にはbottom_dataを指定することで棒グラフを積み上げていくことができる。
1import matplotlib.pyplot as plt
2
3# 積み上げ棒グラフの作成
4plt.bar(data['月'], data['hue1'], label='hue1')
5# 下から2段目以降はbottomで各月の棒グラフが始まる位置を指定する。
6# 下から2段目は一番下のグラフの上なのでbottomにhue1列の値を指定する。
7plt.bar(data['月'], data['hue2'], bottom=data['hue1'], label='hue2')
8# 下から3番目は下から1番目、2番目のグラフの上にあるので、bottomにhu1、hu2列を加算した値を指定する。
9plt.bar(data['月'], data['hue3'], bottom=data['hue1']+data['hue2'], label='hue3')
10title = 'シンプルな積み上げ棒グラフ_matplotlib使用'
11plt.title(title)
12# 凡例の表示
13plt.legend()
14# グラフの表示
15plt.show()
16
多少地味だが、カテゴリー別に横に並べつつ、縦にも積上げるような棒グラフを作成したい場合、この基本を理解しておく必要がある。
seabornを使った積上げ棒グラフ
seabornを使う場合も基本的にはmatplotlibを使う場合と同じ。
sns.barplot()ではbottomを使うことになる。
1import seaborn as sns
2import matplotlib.pyplot as plt
3
4# 最初のカテゴリーのグラフを作成
5sns.barplot(x=data['月'], y=data['hue1'], color='blue', label='hue1')
6
7# 2つ目のカテゴリーのグラフを作成
8bottom_hue = data['hue1']
9sns.barplot(x=data['月'], y=data['hue2'], color='orange', label='hue2', bottom=bottom_hue)
10
11# 3つ目のカテゴリーのグラフを作成
12bottom_hue += data['hue2']
13sns.barplot(x=data['月'], y=data['hue3'], color='green', label='hue3', bottom=bottom_hue)
14title = 'シンプルな積み上げ棒グラフ_seaborn使用'
15plt.title(title)
16# 凡例の表示
17plt.legend()
18
19# グラフの表示
20plt.show()
21
これぐらいのシンプルなグラフならseabornでも描画可能。
hue別に横に並べて積み上げ棒グラフを作成する
データ準備
seabornのtipsデータを使う。
グラフ作成用にgroupbyで集約する。
x, yはそれぞれx軸、y軸の値。
hueは横に並べる対象のカテゴリー列(seabornのhueと同じ扱いをする対象列)。
stackは縦に積上げる対象のカテゴリー列。
x, hue, stackをキーにyを集約する。
1import matplotlib.pyplot as plt
2import seaborn as sns
3import numpy as np
4import pandas as pd
5
6# データの読み込み
7df_tips = sns.load_dataset("tips")
8
9# 変数の定義
10x = "day"
11y = "tip"
12hue = "sex"
13stack = "smoker"
14# observedを指定しないとFutureWarningが出るが、TrueでもFalseでもグラフ作成には影響しない
15data = df_tips.groupby([x, hue, stack])[y].sum().reset_index()
16print(data)
17# day sex smoker tip
18# 0 Thur Male Yes 30.58
19# 1 Thur Male No 58.83
20# 2 Thur Female Yes 20.93
21# 3 Thur Female No 61.49
22# 4 Fri Male Yes 21.93
23# 5 Fri Male No 5.00
24# 6 Fri Female Yes 18.78
25# 7 Fri Female No 6.25
26# 8 Sat Male Yes 77.74
27# 9 Sat Male No 104.21
28# 10 Sat Female Yes 43.03
29# 11 Sat Female No 35.42
30# 12 Sun Male Yes 52.82
31# 13 Sun Male No 133.96
32# 14 Sun Female Yes 14.00
33# 15 Sun Female No 46.61
34
x軸はday(曜日)、hueにsex(性別)を指定し、smokerのYes、Noを積み上げる。
hue別に横に並べつつ縦に積上げる棒グラフを作成
hueで横に並べるがseabornの便利な機能は使えないので、matpliotlibの機能だけで、地道に位置を設定しながらグラフを作成していく。
コード全体とグラフを記載してから、個別に説明していく。
コード全体とグラフ
1# プロットのサイズを設定
2plt.figure(figsize=(8, 4.5))
3
4# 各x値のユニークな値を取得
5x_values = data[x].unique()
6
7# hueとstackのユニークな値を取得
8hue_values = data[hue].unique()
9stack_values = data[stack].unique()
10
11# バーの幅を設定。hueの数によって変更。
12bar_width = round(1 / (len(hue_values) + 1), 2)
13print(f"{bar_width=}")
14# bar_width=0.33
15
16# バーの色を設定。hueとstackの組み合わせ毎に色を塗り分ける。
17n_colors = len(hue_values) * len(stack_values)
18colors = sns.color_palette("husl", n_colors)
19color_mapping = {}
20for idx, (hue_value, stack_value) in enumerate(itertools.product(hue_values, stack_values)):
21 color_mapping[(hue_value, stack_value)] = colors[idx]
22print(color_mapping.keys())
23# dict_keys([('Male', 'Yes'), ('Male', 'No'), ('Female', 'Yes'), ('Female', 'No')])
24
25# バーをプロット
26for i, x_value in enumerate(x_values):
27 for j, hue_value in enumerate(hue_values):
28 _bottom = 0 # bottomの初期値を設定
29 for k, stack_value in enumerate(stack_values):
30 # 対応するデータを取得
31 _data = data[(data[x] == x_value) & (data[hue] == hue_value) & (data[stack] == stack_value)]
32 # バーのx位置を計算
33 # iは1, 2, 3...と各xの中心を表す。len(hue_values)/2はhueの中心を表す。
34 # (j - len(hue_values)/2 + 0.5)で各hueがhueの中心からどれだけ離れるかを表す(最終的にはbar_widthをかけた分だけの距離をxから離れることになる)
35 # +0.5は各hueのグラフがxグループ内で真ん中に表示されるように調整するための数値。iが1刻みなので、その半分の0.5となる。
36 _x = i + (j - len(hue_values)/2 + 0.5) * bar_width
37 # バーをプロット
38 color = color_mapping[(hue_value, stack_value)]
39 plt.bar(_x, _data[y].values, bar_width, bottom=_bottom, label=f'{hue_value}, {stack_value}', color=color)
40 # bottomの値を更新
41 _bottom += _data[y].values
42
43# 凡例を表示
44handles, labels = plt.gca().get_legend_handles_labels()
45new_labels, new_handles = [], []
46# デフォルトでは凡例に重複があるため、重複を取り除く。
47for handle, label in zip(handles, labels):
48 if label not in new_labels:
49 new_labels.append(label)
50 new_handles.append(handle)
51plt.legend(new_handles, new_labels, title=f'{hue}, {stack}')
52
53# x軸のラベルを設定
54plt.xticks(np.arange(len(x_values)), x_values)
55title = f"hueで横に並べてstackで縦に積み上げる棒グラフ"
56plt.title(title)
57# レイアウトを調整
58plt.tight_layout()
59
60# プロットを表示
61plt.show()
62
x軸に曜日があり、曜日ごとに、Maleが左側、Femaleが右側になってグラフが並んでいる。
更に、MaleとFemaleで下側がSmokerのYesが、上側がSmokerのNoとなるようにグラフが積み重なっている。
(この形のグラフは何を主張したいのか、何を見せたいのかが分かりにくくなるので、あまり作成することはないが・・・)
ここからコードを分解して少し説明する。
バーの幅などの設定
1# プロットのサイズを設定
2plt.figure(figsize=(8, 4.5))
3
4# 各x値のユニークな値を取得
5x_values = data[x].unique()
6
7# hueとstackのユニークな値を取得
8hue_values = data[hue].unique()
9stack_values = data[stack].unique()
10
11# バーの幅を設定。hueの数によって変更。
12bar_width = round(1 / (len(hue_values) + 1), 2)
13print(f"{bar_width=}")
14# bar_width=0.33
x_values, hue_values, stack_valuesはグラフ描画時にforでループすることになる。
棒グラフのバーの幅は、hueのユニーク値数が増えると、その分、バーの幅を小さくする必要がある。でないと、隣のxの棒グラフが重なってしまう。
凡例の色の設定
1# バーの色を設定。hueとstackの組み合わせ毎に色を塗り分ける。
2n_colors = len(hue_values) * len(stack_values)
3colors = sns.color_palette("husl", n_colors)
4color_mapping = {}
5for idx, (hue_value, stack_value) in enumerate(itertools.product(hue_values, stack_values)):
6 color_mapping[(hue_value, stack_value)] = colors[idx]
7print(color_mapping.keys())
8# dict_keys([('Male', 'Yes'), ('Male', 'No'), ('Female', 'Yes'), ('Female', 'No')])
hueとstackのユニーク値の組み合わせの数だけ、グラフの領域が必要になるので、色の設定を行う。
itertools.product()でhueとstackのユニーク値の組み合わせを作り、各組合せをキーとして色の辞書を作成する。
各hueとstackののタプルをキー値とするのは、棒グラフ描画時にhueとstackでループするので、そのまま使えるため。
積上げ棒グラフの描画
グラフ描画のメインとなる部分。
1# バーをプロット
2for i, x_value in enumerate(x_values):
3 for j, hue_value in enumerate(hue_values):
4 _bottom = 0 # bottomの初期値を設定
5 for k, stack_value in enumerate(stack_values):
6 # 対応するデータを取得
7 _data = data[(data[x] == x_value) & (data[hue] == hue_value) & (data[stack] == stack_value)]
8 # バーのx位置を計算
9 # iは1, 2, 3...と各xの中心を表す。len(hue_values)/2はhueの中心を表す。
10 # (j - len(hue_values)/2 + 0.5)で各hueがhueの中心からどれだけ離れるかを表す(最終的にはbar_widthをかけた分だけの距離をxから離れることになる)
11 # +0.5は各hueのグラフがxグループ内で真ん中に表示されるように調整するための数値。iが1刻みなので、その半分の0.5となる。
12 _x = i + (j - len(hue_values)/2 + 0.5) * bar_width
13 # バーをプロット
14 color = color_mapping[(hue_value, stack_value)]
15 plt.bar(_x, _data[y].values, bar_width, bottom=_bottom, label=f'{hue_value}, {stack_value}', color=color)
16 # bottomの値を更新
17 _bottom += _data[y].values
18
x, hue, stackの順にループする。
xとhueが決まると、バーのx軸の位置を決められる。そこからstackごとに高さを決めていく。
_data = data[(data[x] == x_value) & (data[hue] == hue_value) & (data[stack] == stack_value)]
で対応するデータを取得する。
_x = i + (j - len(hue_values)/2 + 0.5) * bar_width
で、x軸の位置を決める。
color = color_mapping[(hue_value, stack_value)]
で、あらかじめ作成したカラーマップからバーの色を取得する。
plt.bar(_x, _data[y].values, bar_width, bottom=_bottom, label=f'{hue_value}, {stack_value}', color=color)
でバーを作成する。凡例の表示に使うlabelも忘れずに設定しておく。
最後に次のstackのためにbottomを更新する。
凡例の表示
1# 凡例を表示
2handles, labels = plt.gca().get_legend_handles_labels()
3new_labels, new_handles = [], []
4# デフォルトでは凡例に重複があるため、重複を取り除く。
5for handle, label in zip(handles, labels):
6 if label not in new_labels:
7 new_labels.append(label)
8 new_handles.append(handle)
9plt.legend(new_handles, new_labels, title=f'{hue}, {stack}')
10
バーの描画時にlabelを設定したが、そのまま何もせずに凡例を表示すると、同じ凡例が繰り返し表示されてしまうため、重複を削除する必要がある。
handlesとlabelsを取得してからループさせ、新規のlabelであれば残すだけ。
その他のグラフの設定
1# x軸のラベルを設定
2plt.xticks(np.arange(len(x_values)), x_values)
3title = f"hueで横に並べてstackで縦に積み上げる棒グラフ"
4plt.title(title)
5# レイアウトを調整
6plt.tight_layout()
7# プロットを表示
8plt.show()
ここの処理は他のグラフでもよく行う処理で、軸のラベルやグラフタイトルなどを設定しているだけ。
グラフ作成を関数化する
1回書くだけならべた書きでも良いが、複数回使うとなると、毎回コピペして少し修正するとかでも、面倒になってくるのである程度関数化しておくと便利。
(そもそもこの形のグラフを作成する機会がほとんどないが・・・)
hueで横並びにしてstackで積上げ棒グラフを作成する関数
1import matplotlib.pyplot as plt
2import seaborn as sns
3import numpy as np
4import pandas as pd
5
6def stacked_grouped_barplot(df, x, y, hue, stack, title="") -> plt.Axes:
7 data = df.groupby([x, hue, stack], observed=False)[y].sum().reset_index()
8
9 # プロットのサイズを設定
10 fig, ax = plt.subplots(figsize=(8, 4.5))
11
12 # 各x値のユニークな値を取得
13 x_values = data[x].unique()
14
15 # hueとstackのユニークな値を取得
16 hue_values = data[hue].unique()
17 stack_values = data[stack].unique()
18
19 # バーの幅を設定。hueの数によって変更。
20 bar_width = round(1 / (len(hue_values) + 1), 2)
21 print(f"{bar_width=}")
22
23 # バーの色を設定。hueとstackの組み合わせ毎に色を塗り分ける。
24 n_colors = len(hue_values) * len(stack_values)
25 colors = sns.color_palette("husl", n_colors)
26 color_mapping = {}
27 for idx, (hue_value, stack_value) in enumerate(itertools.product(hue_values, stack_values)):
28 color_mapping[(hue_value, stack_value)] = colors[idx]
29 print(color_mapping.keys())
30
31 # バーをプロット
32 for i, x_value in enumerate(x_values):
33 for j, hue_value in enumerate(hue_values):
34 _bottom = 0 # bottomの初期値を設定
35 for k, stack_value in enumerate(stack_values):
36 # 対応するデータを取得
37 _data = data[(data[x] == x_value) & (data[hue] == hue_value) & (data[stack] == stack_value)]
38 # バーのx位置を計算
39 # iは1, 2, 3...と各xの中心を表す。len(hue_values)/2はhueの中心を表す。
40 # (j - len(hue_values)/2 + 0.5)で各hueがhueの中心からどれだけ離れるかを表す(最終的にはbar_widthをかけた分だけの距離をxから離れることになる)
41 # +0.5は各hueのグラフがxグループ内で真ん中に表示されるように調整するための数値。iが1刻みなので、その半分の0.5となる。
42 _x = i + (j - len(hue_values)/2 + 0.5) * bar_width
43 # バーをプロット
44 color = color_mapping[(hue_value, stack_value)]
45 ax.bar(_x, _data[y].values, bar_width, bottom=_bottom, label=f'{hue_value}, {stack_value}', color=color)
46 # bottomの値を更新
47 _bottom += _data[y].values
48
49 # 凡例を表示
50 handles, labels = ax.get_legend_handles_labels()
51 print(f"{labels=}")
52
53 new_labels, new_handles = [], []
54 # デフォルトでは凡例に重複があるため、重複を取り除く。
55 for handle, label in zip(handles, labels):
56 if label not in new_labels:
57 new_labels.append(label)
58 new_handles.append(handle)
59 ax.legend(new_handles, new_labels, title=f'{hue}, {stack}')
60 _handles, _labels = ax.get_legend_handles_labels()
61 print(f"{_labels=}")
62
63 # x軸のラベルを設定
64 ax.set_xticks(np.arange(len(x_values)))
65 ax.set_xticklabels(x_values)
66 ax.set_xlabel(x)
67 ax.set_ylabel(y)
68 # title = f"hueで横に並べてstackで縦に積み上げる棒グラフ"
69 ax.set_title(title)
70 # レイアウトを調整
71 plt.tight_layout()
72
73 return ax
74
引数にDataFrame, x, y, hue, stackを指定するとplt.Axesを返してくれるようにしている。
基本的な処理の流れは上の方で説明したものと同じだが、pltよりもAxesの方が関数外での処理を色々追加しやすいのでAxesを返すようにしている。
そのため、全体的にplt.ではなくax.の書き方にしている。
関数を使ってグラフを書いてみる
以下のようにDataFrameやx、y、hue、stackを指定することでグラフを作成できる。
1# データの読み込み
2df_tips = sns.load_dataset("tips")
3
4# 変数の定義
5x = "day"
6y = "tip"
7hue = "sex"
8stack = "smoker"
9title = "stacked_grouped_barplotで作成したグラフ"
10
11ax = stacked_grouped_barplot(df=df_tips, x=x, y=y, hue=hue, stack=stack, title=title)
12plt.show()
13
凡例をグラフの外側に表示する
上記の関数だと凡例の位置はautoになっているので、凡例の種類が多いと、グラフに重なってしまい非常に見にくくなってしまう。
凡例が重なるグラフの例
1# データの読み込み
2df_tips = sns.load_dataset("tips")
3
4# 変数の定義
5x = "day"
6y = "tip"
7hue = "size"
8stack = "smoker"
9title = "判例が重なって見にくいグラフ"
10
11ax = stacked_grouped_barplot(df=df_tips, x=x, y=y, hue=hue, stack=stack, title=title)
12# ax.set_xticklabels(ax.get_xticklabels(), rotation=90)
13plt.show()
14
凡例の重複を削除する関数
ax.legend()で凡例を位置設定しなおす場合、位置だけを設定することはできず、凡例のハンドラーとラベルも同時に設定する必要がある。
そのため、重複を削除したラベルを作成する必要がある。
1def get_unique_legend_items(ax):
2 handles, labels = ax.get_legend_handles_labels()
3 new_labels, new_handles = [], []
4 for handle, label in zip(handles, labels):
5 if label not in new_labels:
6 new_labels.append(label)
7 new_handles.append(handle)
8 return new_handles, new_labels
9
凡例の位置を変えたグラフ作成
stacked_grouped_barplotで作成したaxに対して、すでに設定済みの凡例を削除し、新たな凡例を設定する。
その際に、凡例をグラフ外に表示するようにする。
1# データの読み込み
2df_tips = sns.load_dataset("tips")
3
4# 変数の定義
5x = "day"
6y = "tip"
7stack = "smoker"
8hue = "size"
9title = "凡例を外に表示させたグラフ"
10
11ax = stacked_grouped_barplot(df=df_tips, x=x, y=y, hue=hue, stack=stack, title=title)
12ax.get_legend().remove() # 元の凡例を削除
13unique_handles, unique_labels = get_unique_legend_items(ax) # 重複を削除したハンドルとラベルを取得
14ax.legend(unique_handles, unique_labels, loc="upper left", bbox_to_anchor=(1, 1), title=f'{hue}, {stack}') # 新しい凡例を設定
15plt.tight_layout()
16# グラフを表示
17plt.show()
18
(こんな面倒なことはせずに、凡例の位置を引数で指定できるようにすれば良いのにとも思う)
まとめ
今回はコードが長いのでコードの記載は割愛。ポイントは以下の通り。
- 積上げ棒グラフを作成する場合はplt.bar()の引数にbottomを指定する
- hueで横並びにしつつ縦にも積上げたい場合は、各バーのx軸の位置を決めてからbottomを決めてバーをプロットする