tkherox blog

データサイエンスおよびソフトウェア開発、たまに育児についての話を書いています

flowWeaverでSankey Diagramの可視化

今回は可視化の記事についてです.

Sankey Diagramとは

SanKey Diagramとは各プロセス間の流量を表現する可視化パターンです.矢印の向きでプロセスの向きを表して,その矢印の太さで流れの量を表しています.活用シーンとしては,ユーザ導線などを視覚的にみたい時などに利用されます.よく見かける例としてはGoogle Analyticsのユーザーフローで示されるようなサイト内でのユーザーの動きを可視化する際などに使われたりしています。
ちなみに以下の「データビジュアライゼーションのデザインパターン20」 の書籍内でもSankey Diagramの解説が載っているので,体系的に可視化について学ばれたい方は以下の書籍を読んでみることをお勧めします.

floWeaver

早速ですが今回はこのSankey DiagramをPythonを用いて可視化していきます.
私たちがPythonでSankey Diagramを可視化する際に利用するライブラリの選択肢としては,主にmatplotlibPlotlyfloWeaver があります.それぞれについて簡単に説明しておきます.
まず,matplotlibはデータ分析を扱っている人であれば誰でも一度は触ったことがある言わずと知れた可視化ライブラリです.こちらのライブラリでも実はSankey Diagramを扱うことができます.しかし、個人的には可視化された図が可読性が決して良いものではないためあまり好みではありません.そのため今回は本記事では扱わないこととします.
続いて,Plotlyです.Plotly は多様な可視化パターンに対応しながらインタラクティブな図を作成することができるツールです.Plotly を普段から慣れている方はこちらの Plotly を用いたSankey Diagramを利用することで可読性の高い図を学習コストも少なく生成できるため非常に良いかと思います.
そして、最後に floWeaver ですが、こちらはSankey Diagramの可視化に特化した可視化ライブラリになります.これまでの2つと比べると機能面で1つの可視化しかできないのかと思うかもしれませんが,Sankey Diagramの可視化に特化しているため,細かなチューニングが行えるのと,なんといっても可視化された図の可読性の高さが非常に高い点で扱う価値があると思います.
3つのライブラリをご紹介しましたが,こちらの記事ではあまり見慣れないという点で floWeaver を用いてSankey Diagramの実装を以降では扱っていきます

floweaver.readthedocs.io

実行環境

実行環境は以下となります.

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G9028

$ python -V
Python 3.9.1

インストール

floWeaver のインストールはpipにて簡単に行えます.

$ pip install floweaver

加えてJupyterで floWeaver を利用する際には ipysankeywidget も必要になるため,別途インストールしてJupyterの拡張機能にてipysankeywidgetを有効化しておきます.

$ pip install ipysankeywidget
$ jupyter nbextension enable --py --sys-prefix ipysankeywidget

floWeaverの実装方法

さて,本題の floWeaver による可視化です.
今回は公開されているデータセットではなく,自作した擬似データを用いてSankey Diagramを可視化していきます.ユーザの年代別に特定の商品がどれくらいの数量で購買されたのかを示すSankey Diagramを可視化していきます.

データ

まずは自作のデータを作成していきます.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib
from floweaver import *

np.random.seed(42)

size = 40
generations = np.array(["10代", "20代", "30代", "40代", "50代", "60代", "70代"])
channels = np.array(["メルマガ", "マスメディア", "Web広告", "SNS"])
products = np.array(["商品A", "商品B", "商品C", "商品D", "商品E"])

# 擬似データ作成
data = {
    "source": list(np.random.choice(generations, size=size)),
    "target": list(np.random.choice(products, size=size)),
    "channels": list(np.random.choice(channels, size=size)),
    "value": np.random.randint(0, 1000, size),
}

# データフレーム変換(重複除外)
df = pd.DataFrame(data).drop_duplicates(subset=['source', 'target', "channels"])

# typeカラムの追加
df['type'] = 'Not Recommendation'
df.loc[df.target.isin(['商品A', "商品B"]), "type"]  = "Recommendation"

生成されたデータフレームについて少し解説すると,sourceカラムには年代,targetカラムは商品,channelsカラムは媒体・チャネル,valueカラムは購買数,typeカラムはお勧めフラグを示したデータとなっています. 実際に生成されたデータの一部は以下のようになりました.

生成されたデータ(一部抜粋)

可視化

続いて生成されたデータを用いて floWaeverによるSankey Diagramの可視化を実装していきましょう.

# サイズの指定
size = dict(width=570, height=300)

# 可視化時に表示するノードを定義
nodes = {
    "generations": ProcessGroup(["10代", "20代", "30代", "40代", "50代", "60代", "70代"]),
    "products": ProcessGroup(["商品A", "商品B", "商品C", "商品D", "商品E"]),
}

# 表示する順番の定義
ordering = [
    ["generations"],
    ["products"],
]

# コネクションの定義
bundles = [
    Bundle('generations', 'products'),
]

# ノードの中で項目を分けるためのパーティション定義
partition_generations = Partition.Simple(
    "process",
    ["10代", "20代", "30代", "40代", "50代", "60代", "70代"]
)
partition_products = Partition.Simple(
    "process",
    ["商品A", "商品B", "商品C", "商品D", "商品E"]
)

# 各ノードにパーティションを適応
nodes["generations"].partition = partition_generations
nodes["products"].partition = partition_products

# 可視化実行
sdd = SankeyDefinition(nodes, bundles, ordering)
weave(sdd, df).to_widget(**size)

上記のコードにより以下の図が作成されます.
各年代から各商品に対してどれくらいの購買があったのかを接続する線と線の太さで表現できていますね.ちなみに floWeaver ではカラムをキー名として流入元,流入先,線の太さを指定しており,デフォルトでは当該キー名が sourcetargetvalueとなっているため,今回もデフォルトの値を用いるように floWeaver に合わせてデータフレームを定義しています.

次に特定の商品の色を変更してハイライトしてみましょう.
今回はお勧めフラグが定義されている商品Aと商品Bについてハイライトしていきます.

size = dict(width=570, height=300)

# 可視化時に表示するノードを定義
nodes = {
    "generations": ProcessGroup(["10代", "20代", "30代", "40代", "50代", "60代", "70代"]),
    "products": ProcessGroup(["商品A", "商品B", "商品C", "商品D", "商品E"]),
}

# 表示する順番の定義
ordering = [
    ["generations"],
    ["products"],
]

# コネクションの定義
bundles = [
    Bundle('generations', 'products'),
]

# ノードの中で項目を分けるためのパーティション定義
partition_generations = Partition.Simple(
    "process",
    ["10代", "20代", "30代", "40代", "50代", "60代", "70代"]
)
partition_products = Partition.Simple(
    "process",
    ["商品A", "商品B", "商品C", "商品D", "商品E"]
)
partition_type = Partition.Simple(
    "type",
    ["Recommendation", "Not Recommendation"]
)

# 各ノードにパーティションを適応
nodes["generations"].partition = partition_generations
nodes["products"].partition = partition_products

# 可視化(paletteにて色の指定)
sdd = SankeyDefinition(nodes, bundles, ordering, flow_partition=partition_type)
weave( sdd, df, palette={"Recommendation": "darkmagenta", "Not Recommendation": "darkgray"}).to_widget(**size)

ハイライトする場合にはSankyeDifinitionクラスのインスタンスを生成する際のパラメータのflow_partitionにてPartitionクラスのインスタンスを指定し,その後のweave関数内のpaletteパラメータで色の指定を行います.
今回は商品Aと商品Bに該当するリンクがハイライトされており,特定の接続と流量の関係が把握しやすくなっていますね.

加えて,Sankey Diagramの接続点を増やしてみます.
生成したデータフレームにはchannelsカラムにて購買時にユーザ接点となったチャネルを定義しています.これを使って「ユーザ年代 - チャネル - 商品」の流入量を可視化してみましょう.

size = dict(width=570, height=300)

# 可視化時に表示するノードを定義
nodes = {
    "generations": ProcessGroup(["10代", "20代", "30代", "40代", "50代", "60代", "70代"]),
    "products": ProcessGroup(["商品A", "商品B", "商品C", "商品D", "商品E"]),
}

# ノードの中で項目を分けるためのパーティション定義
partition_generations = Partition.Simple( 
    "process",
    ["10代", "20代", "30代", "40代", "50代", "60代", "70代"]
)
partition_products = Partition.Simple( 
    "process", 
    ["商品A", "商品B", "商品C", "商品D", "商品E"]
)
partition_type = Partition.Simple( 
    "type",
    ["Recommendation", "Not Recommendation"]
)
partitiol_channels = Partition.Simple( 
    "channels",
    ["メルマガ", "マスメディア", "Web広告", "SNS"]
)

# 各ノードにパーティションを適応
nodes["generations"].partition = partition_generations
nodes["products"].partition = partition_products
nodes['channels'] = Waypoint(partitiol_channels)

# 表示する順番の定義
ordering = [
    ['generations'],
    ['channels'],
    ['products'],
]

# コネクションの定義(waypointを指定)
bundles = [
    Bundle('generations', 'products', waypoints=['channels']),
]

# 可視化(paletteにて色の指定)
sdd = SankeyDefinition(nodes, bundles, ordering, flow_partition=partition_type)
weave( sdd, df, palette={"Recommendation": "darkmagenta", "Not Recommendation": "darkgray"}).to_widget(**size)

以下の図のように年代と商品の流入量の可視化の中にチャネルの中継点を加えて可視化できていることが見てとれます.このように中継点を加えていくことでより複雑な項目関係と流量を示すことができます.また,ハイライトも同時に適応可能なのでそれぞれのノード間の中で特異なデータの傾向を明らかにする際などに効果を発揮するかと思います.

最後に,ノードの項目を集約する方法を記載していきます.
中継点を増やした先ほどのSankey Diagramでは接続する線が多くなって見辛くなってしまっています.ここでは,世代ノードの数をヤング世代(10代〜20代)とミドル世代(30代〜50代),シニア世代(60代〜70代)の3つに集約して図を見やすくしてみましょう.

size = dict(width=570, height=300)

# 可視化時に表示するノードを定義
nodes = {
    "generations": ProcessGroup(["10代", "20代", "30代", "40代", "50代", "60代", "70代"]),
    "products": ProcessGroup(["商品A", "商品B", "商品C", "商品D", "商品E"]),
}

# パーティション定義にてグルーピングを指定
partition_group_generations = Partition.Simple(
    "process", 
    [ ("ヤング世代", ["10代", "20代"]), ("ミドル世代", ["30代", "40代", "50代"]), ("シニア世代", ["60代", "70代"])]
)
partition_generations = Partition.Simple( 
    "process",
    ["10代", "20代", "30代", "40代", "50代", "60代", "70代"]
)
partition_products = Partition.Simple( 
    "process", 
    ["商品A", "商品B", "商品C", "商品D", "商品E"]
)
partition_type = Partition.Simple( 
    "type",
    ["Recommendation", "Not Recommendation"]
)
partitiol_channels = Partition.Simple( 
    "channels", 
    ["メルマガ", "マスメディア", "Web広告", "SNS"]
)

# 各ノードにパーティションを適応
nodes['generations'].partition = partition_group_generations
nodes["products"].partition = partition_products
nodes['channels'] = Waypoint(partitiol_channels)

# 表示する順番の定義
ordering = [
    ['generations'],
    ['channels'],
    ['products'],
]

# コネクションの定義(waypointを指定)
bundles = [
    Bundle('generations', 'products', waypoints=['channels']),
]

# 可視化(paletteにて色の指定)
sdd = SankeyDefinition(nodes, bundles, ordering, flow_partition=partition_type)
weave( sdd, df, palette={"Recommendation": "darkmagenta", "Not Recommendation": "darkgray"}).to_widget(**size)

年代のノードの項目がヤング世代,ミドル世代,シニア世代の3つに集約されましたね.このようにノードの項目が多くなると非常に込み入った図になる場合もあるので,ノードの項目を集約してあげると可読性が向上するため,適切な状況に応じて活用すると良いです.

まとめ

今回はSankey Diagramの可視化を floWeaver というPythonライブラリを用いて行ってみました.こういったあまり利用頻度の高くない可視化パターンを適切な状況でレポーティングに活用できるようになると,周りからの見方も変わるかもしれないですね.これを機に頭の隅に新しい可視化パターンとしてストックしておいて,是非適切な場で活用してもらえると良いかと思います.

参考資料