俺のOneNote

俺のOneNote

データ分析が仕事な人のOneNote愛とか、分析小話とか。

商品購買のアソシエーションルールをネットワークで可視化する

アソシエーション分析の概略

アソシエーション分析は、商品の何と何が一緒に買われやすいのか?を示すための分析手法です。
「アソシエーション分析」とか「アソシエーションルール」とかをググると大量の記事が出てきますので、詳細はそちらの記事を参考にしてください。*1

以下、本記事中で重要になる点だけ記載します。

支持度(support)
総購買ユーザーNのうち、商品Aと商品Bを同時に買っている人の割合です。

f:id:kopaprin:20200504011925p:plain

信頼度(confidence)
商品Aを買った人のうち、商品Bを購入した人の割合です。

f:id:kopaprin:20200504011950p:plain

リフト(lift)

商品Aを買った人のうち商品Bを購入する人の割合(信頼度)が、
そもそも総購買ユーザーNにおける商品Bの購入率より高いか低いか、を表します。

f:id:kopaprin:20200504012034p:plain

一般的には、ある程度の支持度がある購入パターンのみで線引きし、リフト値を見て商品ごとの連関強弱を計ります。

今回の目的は、アソシエーションルールをネットワークで可視化し、商品ごとの連関を分かりやすく表現できるか検証します。
ネットワークは、Pythonのnetworkxを利用し、Tableauで描写することにします。

SQLによるデータ抽出

自身のお仕事にも活かせるよう購買データを使います。
postgresqlが提供してくれているDVDレンタルのサンプルデータを使用することにします。 ただし、バスケット(同レシート内購買)単位での分析は難しいので、ユーザー単位で集計することとします。

サンプルデータとER図はここで確認できます。

www.postgresqltutorial.com

f:id:kopaprin:20200502112927p:plain

これを自身のDB環境に入れます。

以下、SQLで上記指標に必要なA∩B, A, B, Nを集計します。*2

with item as (
    select
        inventory_id
        , title
        , name 
    from

--- 中略------------------------------------------

    inner join(
        select
            tran.title
            , count (distinct customer_id) as "B"
        from
            tran
        group by
            tran.title
    ) as target_table
    on fromto.target_item = target_table.title
    
    ;

※正直汚いSQLです。すみません。
 気になる方は以下GitHubをご確認いただければと思います。

github.com

こんな感じのテーブルに仕上がりました。

f:id:kopaprin:20200504010456p:plain

ここから、support, confidence, liftの算出とネットワーク座標の生成はPythonに渡します。

Python・networkxによる node座標データの作成

ここは、先般のブログどおり、netowrokxでTableauに取り込むテーブルデータを準備します。

kopaprin.hatenadiary.jp

まず、support , confidence , lift を計算します。 さらにAandBの出現UUが少ない組み合わせは除外してしまいましょう。

import pandas as pd
edge_df = pd.read_csv("output.csv")
# 以下、support , confidence , liftを計算します。
edge_df["support"] = edge_df["AandB"] / edge_df["N"]
edge_df["confidence"] = edge_df["AandB"] / edge_df["A"]
edge_df["lift"] = edge_df["confidence"] / ( edge_df["B"] / edge_df["N"] )
# supportが低い組み合わせを除外します。
edge_df = edge_df[edge_df["AandB"]>3]
display(edge_df)
print(edge_df.shape)

本来はconfidenceとliftの両方を考察すべきですが、
一旦liftだけをweightとしてみました。

edge_df_lift = edge_df[["source_item","target_item","confidence"]]
col_name = ["source", "target","weight"]
edge_df_lift.columns = col_name


import networkx as nx

def make_network(df):
  G = nx.from_pandas_edgelist(df,edge_attr=True)
  pos = nx.spring_layout(G)
  edges = G.edges()
  weights = [G[u][v]['weight'] for u,v in edges]
  nx.draw(G, pos, edges=edges, width=weights, node_size=10, node_color="c",with_labels=False)

  node = []
  x = []
  y = []
  for k,v in pos.items():
    node.append(k)
    x.append(v[0])
    y.append(v[1])

  node_df=pd.DataFrame({
      "item":node,
      "X":x,
      "Y":y
  })

  return node_df 

node_df = make_network(edge_df_lift)

・・・一応netowrokxでの画像出力していますが、完全に意味不明な感じになっています。

f:id:kopaprin:20200504020725p:plain

結果が見える戦いをするのはつらいですが、Tableau化まで以前と同じ手順で進めます。

result_df_1 = pd.merge(edge_df_lift,node_df,how="left",left_on="source",right_on="item")
result_df_2 = pd.merge(edge_df_lift,node_df,how="left",left_on="target",right_on="item")
result_df = pd.concat([result_df_1, result_df_2])
result_df["edge_name"] = result_df["source"] + "_" + result_df["target"]

Tableau上での可視化

手順は上にあげた過去記事とおりですので省略します。

なんとなく、lift(weight)が明らかに高い組み合わせもありますが、全体のネットワークの傾向としては解釈が難しいですね。

networkx側でもう少し手を加えるべきなのでしょうか・・・?
そもそも購買データをネットワークグラフにするのが適切ではない・・・?
あるいはサンプルデータだからランダマイズされてるとか・・・?

技術・知識的な未熟さが原因か、そもそもデータの話かは検証する余地がありそうですが、 面白い試みだったかなぁと思います。

*1:Albertの記事が大変分かりやすかったです。

*2:ここではユニークユーザー数ベースの集計値です。もしかしたら単純なトランザクションのほうがよかったかも?