スポンサーリンク

Pythonを使って行動ログの可視化 Sankey Diagram

広告

 今やデータを残しておくことは簡単になってきており、様々なデータが日々蓄積されています。その中でも、何かしらのアクションのログを残しておいて、いつか役に立たせようと思っている方は多いかと思います。例えば、Webページの遷移ログや位置情報を使った行動ログなどです。ただ実際は、ログが膨大になり複雑さが増すことで、代表値だけの集計で終えているといった方も多いかと感じます。確かに、日々のアクセス数や平均購入額のまるっとした代表値だけでもわかることは多々ありますが、宝の持ち腐れな感も否めません。刀を持っているのにやさで戦っている感じです。やはり、行動ログがあるのならば、行動同士の関連性や、キーとなる行動につながっている行動を知りたいところです。そこで今回は、行動ログを使って行動全体の可視化をしてみたいと思います。そのために、『Sankey Diagram』を使います。Sankey Diagramの説明はこちら-Wikipediaをご覧ください。

 早速、Sankey Diagramを見てみます。以下で今回最終的に作成する図を示しています。この図を見ても何の遷移なのかはよくわかりませんが、行動の繋がりの全体感と各行動の関連性を見ることができます。

f:id:dskomei:20180419224939p:plain:w550

 上の図は何の行動遷移なのかというと今回のために自作した適当なデータであり、ランダムな行動選択の結果できたものなので、行動間に何かしらの関係を持っているわけではありません。またSankey Diagramの描き方自体は既に様々な文献で述べられており今更取り上げることでもないですが、生の行動ログから加工してSakey Diagramで使えるようなデータにする部分の処理も一緒に述べられている文献はあまりないように思います。そこで今回は、生の行動ログに近いデータを自作し、そのデータを使ってSankey Diagramを描くまでの処理を一通りやってみたいと思います。今回のコードこちら-GitHubにあります。

 本エントリの流れは以下のとおりです。

  1. 行動ログの作成
  2. 作成した行動ログの加工
  3. Sankey Diagramを描く


行動ログの作成

 行動ログは様々な種類のものがありますが、今回は「Webページの遷移から購入に至るまで」というテーマでデータを作ってみようと思います。しかし、確率を細かく設定するなどの大それたことはしません。ランダム様に全てお任せします。美しさとはランダムであります!
 
 イメージとしては、TOP画面から、5つのページ(page1~page5)への遷移と「like」・「purchase」ボタンの押下ができ、どこからでも他のページへの遷移やボタンの押下が可能なものとします。若干無理な設定ですが。ここで、行動遷移に到達してほしいゴールがあるとします。今回はそれを「purchase」(購入)ボタンとしています。つまり、どのページ・ボタンから購入に至っているかに着目するという裏設定があります。「purchase」ボタンを押さずに行動がどこかで終わっているユーザは離脱したとみなします。

 今回作成した行動ログの一例を以下に示します。以下のデータにおいて、idをユーザIDとして考えると、ユーザIDが0の人はTOP画面から「page1」に遷移し、すぐに離脱していると想像できます。

f:id:dskomei:20180418235527p:plain:w550

 上記のデータを作成するためのコードは以下のとおりです。行動遷移数をランダムにし、その行動遷移数に応じてページリストとボタンリストからランダムに行動を選択しています。

from pathlib import Path
import numpy as np
import datetime
import pandas as pd


"""

行動ログの作成

"""

action_data_dir_path = Path('./action_data')
if not action_data_dir_path.exists():
    action_data_dir_path.mkdir(parents=True)

## 行動の定義
start_action = 'top'
pages = ['page1','page2', 'page3', 'page4', 'page5']
buttons = ['like','purchase']

## ページ遷移の重み設定
## ページ5よりページ1のほうが5倍出現しやすい
pages_weights = np.array([5, 3, 2, 1.5, 1])
pages_weights = pages_weights / pages_weights.sum()


n_id = 100                         # id数
max_action = 12                    # 遷移行動数の最大
action_datas = pd.DataFrame()
for id in range(n_id):

    # 遷移行動数の決定
    n_action = np.random.randint(1, max_action)
    now = datetime.datetime.now()

    if n_action - 1 >0:
        # 遷移行動数をページ遷移とボタンで分配
        n_page = n_action - 1 - np.random.randint(0, n_action-1)
        # page_weightsの確率でページ遷移リストの中からn_page個ランダムに選択
        pages_ = np.random.choice(pages, n_page, replace=True, p=pages_weights)
        # 残りの行動遷移数をボタンリストの中からランダムに選択
        buttons_ = np.random.choice(buttons, n_action-n_page-1, replace=True)
        actions = np.random.choice(np.r_[pages_, buttons_], n_action-1, replace=False)
        # topと結合
        actions = np.r_[np.array([start_action]), actions]

        # 行動ログっぽくするため時間も付与
        dates = [(now + datetime.timedelta(seconds=i)).strftime('%Y-%m-%d %H:%M:%S') for i in range(1, n_action + 1)]
    else:
        actions = [start_action]
        dates = [now.strftime('%Y-%m-%d %H:%M:%S')]

    data = pd.DataFrame(np.array([[id]*n_action,
                                  actions,
                                  dates]).T, columns=['id', 'action_name', 'date'])
    action_datas = pd.concat([action_datas, data], axis=0)

action_datas.to_csv(action_data_dir_path.joinpath('action_data.tsv'),
                    index=False, sep='\t', encoding='utf-8')


行動ログの加工

 先程行動ログを作成したので、行動ログをSankey Diagramで扱える形に変換していきたいと思います。Sankey Diagramで扱うための最終型とは、連結している行動列とその行動列の行われた回数がセットになっているデータです。整形プロセスのイメージは以下をご覧ください。

f:id:dskomei:20180419221202p:plain:w550

 ただし今回のテーマとして、「購入につながる行動を見ること」としているので、購入にたどり着いた場合はそこで行動遷移を区切り、次の行動を行動遷移の新たな基点とします。また、購入に至らなかったユーザがどの行動で離脱したかもわかるように、ユーザの最終行動にend項目を追加しています。それ踏まえた行動ログの加工の流れは以下の図のとおりです。

f:id:dskomei:20180419221608p:plain:w550

 上記の行動ログの加工のコードをいかに記載します。

from pathlib import Path
import pandas as pd
import numpy as np

"""
行動ログをSankey Diagramで扱えるような行動列に加工する

"""

action_dir_path = Path('./action_data')
result_dir_path = Path('./result')
if not result_dir_path.exists():
    result_dir_path.mkdir(parents=True)


data = pd.read_csv(action_dir_path.joinpath('action_data.tsv'),
                   encoding='utf-8',
                   sep='\t')

ids = sorted(np.unique(data['id']))
start_point = 'top'
end_points = ['purchase']

# action_from : 行動の基点
# action_to : action_fromに繋がる行動
# num_layer : 行動遷移番号
datas = [['id', 'action_from', 'action_to', 'num_layer', 'flag_end']]

for id in ids:
    tmp = data.ix[data['id'] == id, :].copy()
    end_index = len(tmp)
    action_names = tmp['action_name'].values

    i = 0
    num_layer = 0
    #  行動がTOP画面か否かで場合分け
    if end_index > 1:
        while i < end_index:
            # 終端になったらaciton_toをendとする
            if i == end_index-1:
                list_ = [id, action_names[i], 'end', num_layer, 1]
                num_layer += 1
            else:
                # 次の行動がtop画面になっている場合、action_toをendとする
                if action_names[i+1] == start_point:
                    list_ = [id, action_names[i], 'end', num_layer, 1]
                    num_layer = 0
                else:
                    list_ = [id, action_names[i], action_names[i + 1], num_layer, 0]
                    num_layer += 1
                    # 指定したゴールの行動がある場合、その行動がaction_fromにならないように飛ばす
                    if action_names[i+1] in end_points:
                        i += 1
                        num_layer = 0
            i += 1
            datas.append(list_)
    else:
        list_ = [id, action_names[0], 'end', 0, 1]
        id += 1
        datas.append(list_)


datas = pd.DataFrame(datas)
datas.to_csv(result_dir_path.joinpath('action_sequence.tsv'),
             index=False,
             sep='\t',
             encoding='utf-8',
             header=False)

 あとは、action_fromとaction_toでグルーピングして、アクション数を数えます。

from pathlib import Path
import pandas as pd


result_dir_path = Path('./result')
master_dir_path = Path('./original_data')


action_sequence_data = pd.read_csv(result_dir_path.joinpath('action_sequence.tsv'),
                                   sep='\t', encoding='utf-8')

action_sequence_data_sum = action_sequence_data.groupby(['action_from', 'action_to']).count()
action_sequence_data_sum = action_sequence_data_sum.reset_index()
action_sequence_data_sum = action_sequence_data_sum.ix[:, 0:3]
action_sequence_data_sum.columns = ['action_from', 'action_to', 'value']

action_sequence_data_sum.to_csv(result_dir_path.joinpath('sankey_data.tsv'), sep='\t', encoding='utf-8', index=False)


Sankey Diagramを描く

 Sankey Diagramを描くために『ipysankeywidget』モジュールを使います。このモジュールはipython上で動くため、ipythonのモジュールも必要です。こちらに詳しく書かれています。『ipysankeywidget』はpipで簡単に入れることができますが、以下のようなおまじないも必要です。

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

 それでは、Sankey Diagramを書いてみます。以下の図のようになりました。TOP画面からはpage1への行動が一番多く、購入に一番つながっているのはlikeボタンです。どの画面からもバランスよく離脱しています。*これは適当なデータです。

f:id:dskomei:20180418232848p:plain:w550

 それではSankey Diagramを描くコードを以下に記載します。以下のコードはipython上で動かしています。

from pathlib import Path
import pandas as pd
from ipysankeywidget import SankeyWidget
from ipywidgets import Layout

master_dir_path = Path('./master_data')
result_dir_path = Path('./result')
image_dir_path = Path('./image')

if not image_dir_path.exists():
    image_dir_path.mkdir(parents=True)

data = pd.read_csv(result_dir_path.joinpath('sankey_data.tsv'), encoding='utf-8', sep='\t')

##  sorceにaction_from,targetにaction_to, valueにアクション行動回数の
##  辞書を作っている
links = []
for i in range(len(data)):
    data_ = data.iloc[i]
    links.append({'source':data_['action_from'], 'target':data_['action_to'], 'value':data_['value']})

##  先にレイアウトをしてしている
##  これにより画像サイズや余白を決めている
layout = Layout(width='1000', height='700')
def sankey(margin_top=10, **value):
    return SankeyWidget(layout=layout,
                       marings=dict(top=margin_top, bottom=0, left=40, right=160),
                       **value)

##  Sankey Diagramを描く
s = sankey(links=links)

##  Sankey Diagramを指定したパスに保存
s.auto_save_png(image_dir_path.joinpath('action_sankey.png').__str__())

 以上の流れでSankey Diagramを描くことができます。実際に描く部分のコード自体は短いため、データの加工の部分がより際立っていると思います。

 またSankeyWidgetには様々なオプションがあります。詳しくはこちら-Sankey Diagram Examplesを御覧ください。今回は様々なオプションの中でも、行動のグループ化を試してみたいと思います。Webページによっては広告のページやお知らせのページなどページのカテゴリがあります。そこで、page1とpage2を広告、page3とpage4をお知らせとして、グループ指定して描画してみます。以下が結果です。少し見づらいですが、グループでくくられていることがわかります。

f:id:dskomei:20180419224939p:plain:w550

 グループ指定するためには、オプションとして、グループにする行動のid、title、グループ群を入れなければいけません。以下のようにしてグループ化しました。

##  titleにグループ名、nodesに指定したグループ群を入れる
groups = []
i = 0
group_dicts =  {'AD':['page1', 'page2'],
              'INFO':['page3', 'page4']}
for i, group_name in enumerate(group_dicts.keys()):
    groups.append({'id':'G'+str(i), 
                   'title':group_name, 
                   'nodes':group_dicts[group_name]})

## グループ引数で指定
s = sankey(links=links, groups=groups)

s.auto_save_png(image_dir_path.joinpath('action_sankey_groups.png').__str__())


参考文献

 行動の可視化という点ではネットワークと言う視点も大事です。ネットワークにおける分析ではこちらの本が参考になりました。

ネットワーク分析