アソシエーション分析をやってみた

1. はじめに

この記事では、Aidemyさんの「データ分析コース」を受講した際に作成した、最終成果物を載せています。

1-1. 受講のきっかけ・目的

受講のきっかけ・目的は、以下のような感じです。
・きっかけ
Pythonの勉強を進めていく中で、不明点を質問できる人がほしかった。
(元々独学でPythonしていたので、ハードルは低かったです)
・目的
仕事で顧客の購買分析をしたかった。それを自分で書いたプログラムで行いたかった。

正直なところ、購買分析なんてものは会社が買っているどこかのアプリでできるししているのが現状ですが、個人的には「どんな仕組みでやっているのか」を知ることは大事だし、やっぱり何より興味があったのでやってみました。

1-2. 掲載内容について

この記事に書いてある内容については、こちらのブログで紹介してあります。ただ私はどうしてもアソシエーション分析に関心があったため、Aidemyさんでの受講で学習した知識をもとに、期間ごとに分けて分析を行うなどしています。

2. 前準備

2-1. 実行環境

・JupyterLab 1.2.6
・Python 3.7.6

2-2. 使用データ

自社データは使えないので、UCIデータセットを使用しています。こちらのHPを参考に他を探したのですが、残念ながら他にはなさそうです、、、
今回のデータは小売店の仕入れのデータです。

3. 実装

その前に。最終目的はこんな感じです。
季節ごとに購入品の関連性の特徴を見ることで
例えばこうやったら売れるんじゃないか?を考えてみる。

分析の流れとしては以下の通りです。
3-1. データの読み込み
3-2. データの確認(欠損値、統計量)
 3-2-1. データの中身全体を確認
 3-2-2. 欠損値有無やデータタイプの確認
 3-2-3. 統計量の確認
3-3. データの抽出
 3-3-1. キャンセルされたオーダーの削除
 3-3-2. 国の絞り込み
 3-3-3. 売上金額による期間の絞り込み
3-4. データの変形
3-5. 分析
 3-5-1. 支持度によるデータの抽出
 3-5-2. 確信度orリフト値によるデータの抽出
 3-5-3. チューニング
3-6. 最後に

3-1. データの読み込み

# 必要なライブラリのインポート
import numpy as np
import pandas as pd

#データの読み込み 
df = pd.read_excel('https://archive.ics.uci.edu/ml/machine-learning-databases/00352/Online%20Retail.xlsx')

3-2. データの確認(欠損値、統計量)

3-2-1. データの中身全体を確認

まずデータの中身を確認します。

# 表の先頭5行を表示
display(df.head())
df.head()実行結果

列は7項目からできており、日本語訳するとこんな感じです。
・InvoiceNo:伝票番号
・StockCode:商品番号
・Description:商品の詳細
・Quantity:購入数
・InvoiceDate:伝票発行日
・UnitPrice:単価
・Country:国

3-2-2. 欠損値有無やデータタイプの確認

次に欠損値の有無等を確認します。

# データの情報表示
df.info()
df.info()実行結果

「df.isnull().sum()」でも欠損値は確認できますが、今回はどんな形式のデータがあるかとか、全体的なことも確認したかったので「df.info()」で確認してみました。
RangeIndexの部分が541909 entriesとなっていますが、2列目のDescription(540455 non-null)と6行目のCustomerID(406829 non-null)は数が不足しているので欠損値があるとわかります。ただ今回はこの列のデータは使用しないため対応不要です。

3-2-3. 統計量の確認

次に各列のデータがどんなもの(期間、種類等)から成っているかを確認します。

# 統計量を表示
display(df.describe(exclude='number'))
df.describe(exclude='number')実行結果

「exclude='number'」で数値以外の項目についてデータ確認しました。知りたかったことは以下の通りです。
・データ数
 精度に影響するので→'InvoceNo', 'StockCode'から把握
・期間
 季節による購買傾向変化の可能性があるので→'InvoiceDate'から把握
・国の数
 国による購買傾向変化の可能性があるので→'Country'から把握

これらのことから、全体のデータ数は55,000くらいで、期間は1年間、国は38か国くらいあるけど再頻出のもので50,000となり全体の90%くらいを占めている(国の偏りが大きい)ということがわかります。
国に偏りがるので、次にどんな国がどのくらいあるのかを見てみます。

# 国の数と種類をカウント
print(df['Country'].value_counts().head(10))
df['Country'].value_counts().head(10)実行結果

結果は多い国10ヵ国分に絞っています。こう見るとUnited Kingdomが一番多いみたいですね。
ここでUnited Kingdomに絞りたいところですが、このデータの中にはキャンセルされたオーダーも含まれているらしい(別のサイトなどで調べた)ので、先にそちらの処理をしてから最終的なデータ量を確認した後、国も絞っていきたいと思います。

3-3. データの抽出

3-3-1. キャンセルされたオーダーの削除

キャンセルされたオーダーは、’InvoiceNo'の最初の一文字に「C」がついているものです。

# 念のためdfをdf2にコピー
df2 = df.copy()

# lambda関数で、最初の一文字(str(x)[0])を列'InvoiceType'として抽出
df2['InvoiceType'] = df2['InvoiceNo'].map(lambda x: str(x)[0])
display(df2.head())
最初の一文字抽出後の結果

表示結果から、一番右の列に’InvoiceType'が追加されていることがわかります。「5」が新規オーダー、「C」がキャンセルされたオーダーなので新規オーダーのみを抽出します。

# 新規オーダーのみ抽出
df2 = df[df2['InvoiceType']=='5']

# 国の数と種類をカウント
print(df2['Country'].value_counts().head(10))
絞り込み後のデータ量

United Kingdomのデータ量新規オーダー抽出前と比較すると、495478から487619に減少しましたが、相変わらず多数を占めていることがわかります。
なのでこのまま、United Kingdomのデータに絞ります。

3-3-2. 国の絞り込み

# 国の絞り込み
df3 = df2[df2['Country']=='United Kingdom']

# データ数の確認
print(df3.shape[0])

print(df3.shape[0])でデータ数を確認すると487619となっており、狙った通りにきちんと絞り込めていることも確認できます。

3-3-3. 売上金額による期間の絞り込み

次に、季節ごとによって購入するものも違うと思うので、売上金額の多い箇所に絞ってデータを分析しようと思います。
まずは売上金額を算出し、それを新しく作った'TotalPrice'の列に記入します。

# 合計金額を列'TotalPrice'に表示
df3['TotalPrice'] = df3.UnitPrice*df3.Quantity

# 結果の表示
display(df3.head())
'TotalPrice'追加後のデータ

次に期間ごとに売上金額に差があるかを、グラフ描写することで確認します。

# 必要なライブラリのインポート
import matplotlib.pyplot as plt

# グラフの描写
x_TotalP = df3['InvoiceDate']
y_TotalP = df3['TotalPrice']
plt.plot(x_TotalP, y_TotalP)
月ごとの売上金額のグラフ

グラフより、3か所で特に売上金額が高くなっている箇所があるのがわかります。そこで1-3月、5-7月、10-12月の3つの期間についてそれぞれ分析を進めていくことにしました。

まず、各期間でのデータを抽出します。

# 必要なライブラリのインポート
import datetime as dt

# 各期間をそれぞれ別のデータフレームとして抽出
df3_M1M3 = df3[(df3['InvoiceDate'] >= dt.datetime(2011,1,1,0,0)) & (df3['InvoiceDate'] < dt.datetime(2011,3,1,0,0))]
df3_M5M7 = df3[(df3['InvoiceDate'] >= dt.datetime(2011,5,1,0,0)) & (df3['InvoiceDate'] < dt.datetime(2011,8,1,0,0))]
df3_M10M12 = df3[(df3['InvoiceDate'] >= dt.datetime(2011,10,1,0,0)) & (df3['InvoiceDate'] < dt.datetime(2012,1,1,0,0))]

次に、先程仮説立てしたように、本当に各期間で購入物が変わってくるのかを確認しておこうと思います。そこで売上総額の大きいものから上位10アイテムに絞って、購入金額の占める割合を見てみます。

dfs = [df3_M1M3, df3_M5M7, df3_M10M12]

# 各期間に対してアイテムごとに合計金額を算出し上位10アイテムをグラフ描写
for df in dfs:
    df3_grouped = df.groupby('Description')['TotalPrice'].sum()
    top_five_df3 = pd.DataFrame(df3_grouped.sort_values(ascending=False)[0:10])
    plt.figure().patch.set_facecolor('White')
    plt.pie(top_five_df3,counterclock=False,startangle=90,autopct='%.1f%%',pctdistance=0.7)
    plt.legend(top_five_df3.index, fontsize=12,bbox_to_anchor=(0.9, 1))
1-3月(M1M3)の購入物グラフ
5-7月(M5M7)の購入物グラフ
10-12月(M10M12)の購入物グラフ

1-3月は友達同士でお家パーティーでもするのでしょうか。セラミックのジャーや、大きなバッグなどがあります。
5-7月も、1-3月と似た感じでパーティー用の旗やティーポットなどがあります。
10-12月は小鳥のペーパークラフトやクリスマス用のキット、保温できるポットなんかがあって冬っぽい仕入れ内容になっています。
1-3月と5-7月の特徴が見えにくいですが、少なくとも10-12月では仕入れ内容も変わっていそうです。このまま分析を進めます。

3-4. データの変形

次に、アソシエーション分析を進めていくためにデータを変形していきます。

df_name = ['df3_M1M3_deform', 'df3_M5M7_deform', 'df3_M10M12_deform']

i=0
for df in dfs:
    # 'InvoiceNo'と'StockCode'をキーにして商品の合計数を集計
    w1 = df.groupby(['InvoiceNo', 'StockCode'])['Quantity'].sum()

    # 発注番号を行として、どのアイテムを購入しているかわかるように変形
    # 購入していないアイテムについては0で埋める
    w2 = w1.unstack().reset_index().fillna(0).set_index('InvoiceNo')

    # 集計後のデータにdf_nameの名前をつけておく
    display(w2.head(5))
    df_name[i] = w2
    i+=1

次に、「買った」か「買っていない」かをわかるようにするために
・発注あり(値が0以外)⇒True
・発注なし(値が0)⇒False
に変更します。

# apply()で行列全体に対して「0以外ならTrue」「0ならFalse」を適用
i=0
for i in range(3):
    df_name[i] = df_name[i].apply(lambda x:x>0)
    display(df_name[i].head())
M1M3の実行結果

各'InvoiceNo'に対してそれぞれのアイテムを購入しているかどうかがわかるようになりました。

次に、やっとアソシエーション分析をしていきます。

3-5. 分析

まず必要なライブラリの読み込みを行います。

# 必要なライブラリのインポート
!pip install mlxtend
from mlxtend.frequent_patterns import apriori
from mlxtend.frequent_patterns import association_rules

3-5-1. 支持度によるデータの抽出

まず、支持度による下限値の絞り込みをします。
支持度は「注目している商品を購入した顧客の比率」であり、図のカラー部分のことです。

支持度のイメージ図
freq_items = ['freq_items_M1M3', 'freq_items_M5M7', 'freq_items_M10M12']

i=0
j=0
for j in range(3):
    # 支持度の下限値をmin_support=0.06として設定
    freq_item = apriori(df_name[i], min_support=0.06, use_colnames=True)
    display(freq_item.sort_values('support', ascending = False).head(10))
    print(freq_item.shape[0])

    # 集計後のデータにfreq_itemsの名前をつけておく
    freq_items[j] = freq_item
    i+=1
    j+=1
実行結果(左からM1M3, M5M7, M10M12)

上に結果をまとめて実行結果を表しています。こんな感じで各支持度とアイテムが出てきました。左下の数字は、各期間で支持度0.06以上のアイテムがいくつあるかです。
次にこれらについて確信度とリフト値を算出し、抽出します。

3-5-2. 確信度orリフト値によるデータの抽出

次に、確信度(信頼度)またはリフト値の高いものを抽出します。
先ほど支持度で下限値を決めましたが、確信度は「Aを買った人がBも買う確率」、リフト値は「何もしなくてもBを買う人の確率に対して、Aと一緒ならBを一緒に買う人の確率」を表しています。

確信度とリフト値のイメージ図
a_rules = ['a_rule_M1M3', 'a_rule_M5M7', 'a_rule_M10M12']

k=0
for k in range(3):
    # 今回はリフト値を指定
    # min_threshold=1とし、リフト値が1以上で指定
    a_rule = association_rules(freq_items[k], metric='lift',min_threshold=1)

    # リフト値の高いものからソート
    a_rule = a_rule.sort_values('lift', ascending=False).reset_index(drop=True)
    display(a_rule)
    print(a_rule.shape[0])

    # 集計後のデータにa_rulesの名前をつけておく
    a_rules[k] = a_rule
実行結果(上からM1M3, M5M7, M10M12)

この結果から、M1M3、M5M7は値が出ていないのがわかります。リフト値を指定した時点での抽出できたアイテム数が少なかったのでしょうか。
取り合えずこの結果から、M10M12の値を用いて関係グラフを作成してみました。関係グラフの作成のしかたは理解が難しく長くなってしまうので、ここでは結果のみ示しています。(*詳細はこちらを参考にしてください)

Support=0.06の時のM10M12の関係グラフ

22086と22910はアイテムを表しています。各アイテムは下記のものを示しています。
・22086:PAPER CHAIN KIT 50'S CHRISTMAS
・22910:PAPER CHAIN KIT VINTAGE CHRISTMAS
みんなこれでクリスマスの飾り付けをするんですね。確かに、品ぞろえをよくするためにも一緒に仕入れた方がよさそうです。

でも分析の結果としてはなんだかおもしろくないので、次に支持度を変えてみました。

3-5-3. チューニング

長くなってしまうので、各期間で支持度を変えたものについて関係グラフをまとめて示します。

Support=0.05, 0.04, 0.03の時の各期間の関係グラフ

図中の番号は、次に示す表中の番号に対応しています。


Support=0.05, 0.04, 0.03の時の各期間の購入物

表中の黄字は、Support=0.05→0.04に変化させることで新しく追加されたアイテムです。また、Support=0.04で赤字になっているアイテムは、関係グラフ中の赤枠の部分のアイテムで、複数のものと同時に買われている(おそらく人気商品)ものです。

このように、支持度を調整することで見えなかった関係が見えてきました。Support=0.05の情報だけでは2商品購入時の割引等の戦略しか考えられませんが、Support=0.04の情報も加えると、人気商品とそうでない商品の同時購入を促進することで、新規トライアルの獲得や販売アイテムの拡大に繋げることもできそうです。
今回の分析では購入金額が大きくなっている3期間について分析しましたが、その他の期間についても分析することで、見えなかった新しい関連性が見えてくると思います。国ごとに比較してもおもしろそうです。

3-6. 最後に

アソシエーション分析を自分の手を動かして行うことで、どんな情報から商品購買における関連性を見出しているのかをより理解することができました。個人的には、よくある分析ツールは使えれば基本的にOKで、その詳細な仕組みを理解することは無駄だと思います。ただ、大体の方法を理解することは、「考えられる人間になる」ことに繋がると思うので今回分析をしてみて理解を深められたのはとてもよかったと思います。
Pythonについてはまだまだ初心者で、ちょっとかじったくらいですが、日常の定型業務効率化や新しい観点からのデータ分析実現に向けて、今後も学習を継続していきたいです。
最後までお読みいただきありがとうございました。


この記事が気に入ったらサポートをしてみませんか?