中央揃え帯グラフ

Design of Diverging Stacked Bar Charts for Likert Scales and Other Applications という論文で、次のようなグラフが提案されています。ここで1〜5は、例えば1は「反対」、2は「やや反対」、3は「どちらでもない」、4は「やや賛成」、5は「賛成」のような、いわゆるLikert(リッカート)尺度の値です。

中央揃え帯グラフ

この “diverging stacked bar chart” は、直訳すれば「発散型積み上げ棒グラフ」となりますが、“stacked bar chart” は日本では「帯グラフ」と呼ぶのが一般的ですし、両側に伸びるという意味の “diverging” を「発散型」と訳してもわかりにくいので、ここでは「中央揃え帯グラフ」と呼んでみました。特に中央が「どちらでもない」を表すLikert尺度の可視化に向いています。

import numpy as np
import pandas as pd
from collections import Counter
import matplotlib.pyplot as plt
from matplotlib.ticker import MaxNLocator

# データの例
x = [3]*200 + [4]*600 + [5]*200
y = [1]*50 + [2]*100 + [3]*150 + [4]*290 + [5]*410

df = pd.DataFrame([Counter(x), Counter(y)], index=["x", "y"])
df = df.fillna(0).astype(int)
df = df.reindex(columns=range(min(x+y), max(x+y)+1), fill_value=0)
df = df[::-1] 

n = len(df.columns)

def get_offset(row):
    mid_idx = n // 2
    if n % 2 == 1:
        return -(row.iloc[:mid_idx].sum() + row.iloc[mid_idx] / 2)
    else:
        return -row.iloc[:mid_idx].sum()

offsets = df.apply(get_offset, axis=1)

fig, ax = plt.subplots(figsize=(6.4, 2))
cmap = plt.get_cmap("coolwarm_r")
colors = cmap(np.linspace(0, 1, n))
y_pos = np.arange(len(df.index))

for i, col in enumerate(df.columns):
    widths = df[col]
    lefts = offsets + df.iloc[:, :i].sum(axis=1)
    ax.barh(y_pos, widths, left=lefts, color=colors[i], edgecolor="black", label=col)

ax.set_yticks(y_pos)
ax.set_yticklabels(df.index)
ax.axvline(0, color='gray', linewidth=1, zorder=-1)
ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left')

min_x = offsets.min()
max_x = (offsets + df.sum(axis=1)).max()
ax.set_xlim(1.05 * min_x - 0.05 * max_x, 1.05 * max_x - 0.05 * min_x)

# 目盛を整数に限る
ax.xaxis.set_major_locator(MaxNLocator(integer=True))

# 見やすい位置に移動
plt.tight_layout()
plt.show()

ここでは帯の長さを数にしましたが、割合にすることもできます。

「賛成」側を暖色系にするか寒色系にするかは悩ましいところです。ここでは元論文に合わせて「賛成」側を寒色系にしました。賛成と反対、どっちが暖色系?もご覧ください。逆の色にするには "coolwarm_r""coolwarm" にします。