Sankey Diagram という可視化方法

Sankey Diagram という可視化方法

こんにちは

grasys でデータ分析をしている t.watanabe です!

みなさんは「web サイトのお客さんの移動ログ」や「ゲーム内のユーザさんの行動ログ」から「商品を買ってくれるお客さんがどのような行動をしているか」など考えることはありませんか?

ただ、人やモノの移動をどうやって可視化すべきか迷うときはないでしょうか?

そのようなときに使いやすい Sankey Diagram を紹介したいと思います

Sankey Diagram とは?

なにかしらの遷移の可視化に特化した図表です

python のライブラリの plotly で表示することを前提とします

流量を線の太さで表現したサンキーダイアグラムの例

こちらのグラフは単純な Sankey Diagram です

plotly のイメージを引用しました

各青い矩形が遷移のターニングポイント(node)で帯の幅が遷移数、帯の結びつきが遷移を表しています

では、Sankey Diagram を描いてみましょう

Sankey Diagram を描いてみる

まずは環境設定からです

今回は後々のデータ分析などをできる環境まで作りますので jupyter notebook に描くことを目指します

Windows の場合

Plain text
powershell -c "irm https://astral.sh/uv/install.ps1 | more"

MacOS, Linux の場合

Plain text
curl -LsSf https://astral.sh/uv/install.sh | less

uv を入れたら作業ディレクトリに移動し

Plain text
uv init .
uv add plotly jupyter
uv sync
uv run jupyter notebook --allow-root --port=8888 --ip=0.0.0.0 --NotebookApp.token=''

と打ち込み jupyter を起動します

ブラウザで

Plain text
http://localhost:8888

にアクセスしてください

Sankey Diagram を描いてみるの画面

このような画面がでれば成功です(最初はホワイトテーマなので好みのものに変えてください)

Plain text
右クリック/New Notebook/

で notebook を作成し、セルに下記のコードをコピー・アンド・ペーストしてください

Plain text
import plotly.graph_objects as go

fig = go.Figure(data=[go.Sankey(
    node = dict(
      pad = 15,
      thickness = 20,
      line = dict(color = "black", width = 0.5),
      label = ["A1", "A2", "B1", "B2", "C1", "C2"],
      color = "blue"
    ),
    link = dict(
      source = [0, 1, 0, 2, 3, 3], 
      target = [2, 3, 3, 4, 4, 5],
      value = [8, 4, 2, 8, 4, 2]
  )
)])

fig.update_layout(title_text="Basic Sankey Diagram", font_size=10)
fig.show()

実行すると冒頭の Sankey Diagram が表示されます

実データに適応する前に一つずつ確認します

Plain text
import plotly.graph_objects as go

ここは plotly の Sankey Diagram を呼び出します

Plain text
fig = go.Figure(data=[go.Sankey(
   ...
)])

Sankey Diagram をつくります

次はオプションです

Plain text
    node = dict(
      pad = 15,
      thickness = 20,
      line = dict(color = "black", width = 0.5),
      label = ["A1", "A2", "B1", "B2", "C1", "C2"],
      color = "blue"
    ),

ポイントとなる “ノード” の設定を作ります

Plain text
      pad = 15,
      thickness = 20,

これは表示時の空白やサイズの設定です

Plain text
  line = dict(color = "black", width = 0.5),

帯の設定です

黒い色の 0.5 サイズです

Plain text
  label = ["A1", "A2", "B1", "B2", "C1", "C2"],

ノードのラベルをつけます

あとでこの順序が重要になるので変数か何かにメモしておきましょう

Plain text
  color = "blue"

ノードの色です、青で設定しています

次は帯の設定です

Plain text
    link = dict(
      source = [0, 1, 0, 2, 3, 3], 
      target = [2, 3, 3, 4, 4, 5],
      value = [8, 4, 2, 8, 4, 2]
  )

数字が設定されていますね

なんの数字でしょうか?

そこで先程メモした順番のラベルが登場です

Plain text
label = ["A1", "A2", "B1", "B2", "C1", "C2"],

これですね

左から A1:0, A2: 1, … C2:5 となります

source には遷移元となるラベルに対応する index を入れます

target は遷移先となる index を入れます

Plain text
source = [0, 1, 0, 2, 3, 3], 
target = [2, 3, 3, 4, 4, 5],

source = ['A1', 'A2', 'A1', 'B1', 'B2', 'B2'], 
target = ['B1', 'B2', 'B2', 'C1', 'C1', 'C2'],
ということです
つまり

index[0] が A1 -> B1
index[1] が A2 -> B2
index[2] が A1 -> B2
index[3] が B1 -> C1
index[4] が B2 -> C1
index[5] が B2 -> C2

となります

value はそれぞれの index の帯の幅です

次に細かな表示の調整をします

Plain text
fig.update_layout(title_text="Basic Sankey Diagram", font_size=10)

フォントサイズは 10 で Basic Sankey Diagram というタイトルで設定します

Plain text
fig.show()

これで表示まで行きました

ちょっと、分かりづらいですね

ただ、一回表示までできてしまえばなれるかもしれないです

サンプル描画

最後に実データで表示するサンプルを置いてきます

pandas のデータフレームをユーザごとに行動の順番を stage として定義し A, B, C として並べ、Sankey Diagram に起こします

stage は各ユーザが閲覧した web ページのようなイメージです

例えば下記の図でいうと「サトウ」さんは A -> B の順にページを移動したと読み取れます(図は時系列で表現されており、0番と6番でサトウさんが登場しています)

Plain text
import pandas as pd
df = pd.DataFrame(
    {
        "user": ["サトウ", "タカハシ", "タナカ", "ワタナベ", "タカハシ", "タナカ", "サトウ", "タカハシ", "タナカ", "ワタナベ"],
        "stage": ["A", "A", "A", "A", "B", "B", "B", "C", "C", "C"],
    }
)

サンプル描画の画面

Sankey Diagram では個別の行動ログを全体のボリュームとして集約して描画するため、全体の傾向を掴むのに適しています。

一方で、「特定のユーザーがどう動いたか」というミクロな追跡には不向きなので注意が必要です。

Plain text

def df2sankey_diagram(df):
    import plotly.graph_objects as go
    
    df = pd.concat([df, df.groupby("user").shift(-1).rename(columns={"stage": "next_stage"})], axis=1)
    df = df.dropna()

    channel2index = {k: i for i, k in enumerate(list(set(df["stage"].tolist()) | set(df["next_stage"].tolist())))}
    index2channel = {v: k for k, v in channel2index.items()}

    labels = [x for x, _ in channel2index.items()]
    src = [channel2index[k] for k in df["stage"].tolist() if k in channel2index.keys()]
    dst = [channel2index[k] for k in df["next_stage"].tolist() if k in channel2index.keys()]
    
    fig = go.Figure(data=[go.Sankey(
        node = dict(
            pad = 15,
            thickness = 20,
            line = dict(color="black", width=0.5),
            label = labels,
        ),
        link = dict(
            source = src,
            target = dst,
            value = [1 for _ in src]
        )
    )])
    fig.update_layout(
        title_text="Sankey Diagram による状態遷移", 
        font_size=10, 
        width=2400, 
        height=400,
        legend=dict(
            title="Sample",
            traceorder='normal',
            itemsizing='constant'
        )
    )
    fig.show()

サンプル描画を示すグラフ

結び

色々なグラフがありますが、適切なグラフを用いてわかりやすく伝えることは難しいです

しかしながら、協力していく中で可視化は重要なファクタだと思います!

出典

plotly, Sankey Diagram in Python

Installing uv