Sinaplot(シーナプロット)

Sinaplotは、箱ひげ図に代わる統計グラフの一種で、バイオリンプロットの中に個々のデータ点を(水平位置はランダムに)プロットするものです(Sidiropoulos et al, SinaPlot: An Enhanced Chart for Simple and Truthful Representation of Single Observations Over Multiple Classes, 2018)。Claus O. Wilke の Fundamentals of Data Visualization (O'Reilly, 2019) p.87 脚注によれば “The name sina plot is meant to honor Sina Hadi Sohi, a student at the University of Copenhagen, Denmark, who wrote the first version of the code that researchers at the university used to make such plots (Frederik O. Bagger, personal communication).” とのことで、論文第2著者の Sina(シーナ)さんに因むようです。

Sinaplot: パーマーペンギンの体重分布

上はパーマーペンギンの体重分布のSinaplotです(バイオリンプロットを薄く重ね書きしています)。

オリジナルの著者たちによる sinaplot パッケージがR用に作られています。Rのggplot2でもsinaplotが描けます。

後で例を挙げますが、Python用にはRのggplot2を模した plotnine パッケージでsinaplotが描けます。Seabornを拡張した seaborn_sinaplot パッケージもありましたが、最新のSeabornでは使えなくなっています(Seaborn 0.12.2までなら動きますが、新しめのNumPyで使うには2箇所の np.boolbool に書き換える必要があります)。

このページの図を描いたのは、独自実装の次のコードです(ChatGPT-4oに手伝ってもらいました):

import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import gaussian_kde

def sinaplot(x, y, data, violin=True, max_width=0.8, title=None, show_grid=False):
    data = data.dropna(subset=[x, y])
    categories = np.sort(data[x].unique())

    fig, ax = plt.subplots()

    def offset(category_index, data, scale=1.0):
        return np.array([category_index] * len(data)) + (scale * data)

    density_max = 0
    for category in categories:
        values = data[data[x] == category][y].values
        n = len(values)
        kde = gaussian_kde(values)
        density_max = max(density_max, n * kde(values).max())

    for i, category in enumerate(categories):
        values = data[data[x] == category][y].values
        n = len(values)
        kde = gaussian_kde(values)
        if violin:
            value_range = np.linspace(values.min(), values.max(), 50)
            density = kde(value_range)
            density = n * density / density_max * max_width / 2
            ax.fill_betweenx(value_range, offset(i, density), offset(i, -density),
                             color="#E0E0E0", alpha=0.8)
        jitter = n * (np.random.random(n) * 2 - 1) * kde(values) / density_max * max_width / 2
        ax.scatter(offset(i, jitter), values, color="black", s=10)

    ax.set_xticks(range(len(categories)))
    ax.set_xticklabels(categories)
    ax.set_xlim(-0.8, len(categories)-0.2)
    ax.set_xlabel(x)
    ax.set_ylabel(y)
    if title:
        ax.set_title(title)
    ax.grid(show_grid)
    plt.show()

パーマーペンギンのデータを使ってSinaplotを描きます:

import seaborn as sns

penguins = sns.load_dataset("penguins")
sinaplot(x="species", y="body_mass_g", data=penguins)

薄いバイオリンが邪魔なら violin=False オプションを付けます。

実験用にランダムデータを生成して描きます:

import pandas as pd

rng = np.random.default_rng()
d = {
    "name": ["Aaa"] * 50 + ["Bbb"] * 20 + ["Ccc"] * 30,
    "value": np.concatenate((rng.normal(10, 3, 50),
                             rng.normal(5, 1, 20),
                             rng.normal(7, 5, 30)))
}
df = pd.DataFrame(data=d)
sinaplot(x="name", y="value", data=df, violin=False, title="Sinaplot Example")

上にも書きましたが、Rのggplot2を模した plotnine パッケージの geom_sina を使ってもsinaplotが描けます。

import seaborn as sns
from plotnine import ggplot, aes, geom_sina

penguins = sns.load_dataset("penguins")

p = (ggplot(penguins, aes(x='species', y='body_mass_g'))
     + geom_sina())

p.show()
Sinaplot: パーマーペンギンの体重分布

この実装では、ご覧のように、群ごとの点の密度が一定ではないようです。