BigQuery上のデータを使って省メモリでバスケット分析
今回はBigQuery上のデータから同時に購入される商品について分析を行うバスケット分析を行ってみたので、その実装方法をまとめたいと思います。データサイズが大きく使用するメモリを削減した実装を行います。
目次
はじめに
今回はpythonのmlxtendライブラリを用いてBigQueryに保存されたテーブルのデータをバスケット分析しようとしたところ、インターネット上にある多くの方法ではメモリ不足となってしまいました。そこで一部の工程を手で作ることで使用するメモリの削減を行ったので、そのやり方についてまとめたいと思います。なお、Google Cloud のプロジェクトの認証部分についてはブラウザのある環境を前提としています。
バスケット分析の各指標について
よく使われる指標については以下のようなものが挙げられます。mlxtendではantecedentsによって、consequentsがどの程度購入されるようになるかという形で出力します。
- antecedents: 前提とする商品(群)。
- consequents: 今考えている商品(群)。
- support値: その購入の仕方をした顧客の全顧客に対する割合。
- confidence値: antecedentsを購入したときにconsequentsを購入する条件付き確率。つまり、antecedentsを購入した際に、どれだけの割合がconsequentsを購入するか。
- lift値: confidenceの値をconsequentsを購入する顧客の割合で割った値。antecedentsと一緒にconsequentsを購入した顧客の割合と、単純に全顧客に対するconsequentsを購入する顧客の割合の比なので、antecedentsによってconsequentsがどの程度買われるようになったかを示す。
今回使用したデータ
今回は購入履歴の情報を使いました。各行1つの商品が購入された時の情報を表している状態です。
ここでは以下のようなデータがBigQuery上にあり、これを使用したとしましょう。実際には取引先キーには取引相手を一意に特定できるような、商品名は商品を決定できるような項目です。
上記の表では同時に複数の商品が購入された場合、商品ごとに別の行に分けられます。また、受注先が同じであっても日時が異なる場合には、同時に購入されているわけではありません。したがって、今回の例では取引先キーと受注日時が同じであるようなすべてのデータを統合した上で、1つのデータとして再構築する必要があります。
つまり、イメージとしては上記の表を下記のようなものに書き換えてから考えることになります。
モジュールのimport
使うモジュールはmlxtend、pandas、google.cloudです。
import mlxtend
from mlxtend.preprocessing import TransactionEncoder
from mlxtend.frequent_patterns import apriori
from mlxtend.frequent_patterns import association_rules
from google.cloud import bigquery
import pandas as pd
gcloud CLIのインストール(Local環境の場合)
Google Cloudのgcloud CLIから自分のOSにあったもののインストーラーをダウンロードして、インストーラーに従って進みましょう。
その後、使用するテーブルがあるプロジェクトを以下の手順で認証しましょう。
1. `gcloud init`コマンドを使用して初期化する。はじめての場合defaultという設定ができ、その後、ブラウザに飛んで認証を求められるので認証を行う。
2. 複数のGoogle Cloud ProjectがそのGoogle Accountに紐づいている場合には選択が求められるので、使用したいデータのあるプロジェクトを選択する。
BigQueryからデータの読み込み
bq_client = bigquery.Client()
query ='Your Query'
base_df = bq_client.query(query).to_dataframe()
base_df['sales'] = base_df['単価'] * base_df['受注数']
threshold = base_df['sales'].quantile(0.85)
processed_df = base_df[base_df['sales'] >= threshold].reset_index(drop=True)
Your QueryにはBigQueryからデータをロードするクエリを書きましょう。ここではデータ数が膨大なので売上を表すsales列を作成し、その上位15%のみを使用しています。この辺りはクエリで削減しても良いですし、使うデータのサイズに応じて変更しましょう。
時に購入された商品をまとめる
del base_df
tmp_df = (
processed_df.groupby(["取引先キー", "受注日時", "商品名"], dropna=False)["受注数"]
.sum()
.reset_index()
)
del processed_df
grouped_df = (
tmp_df.groupby(["取引先キー", "受注日時"])["商品名"]
.apply(list)
.reset_index()
)
unique_products_group = tmp_df['商品名'].unique()
del tmp_df
tmp_dfは全く同じ扱いをされるべき行が複数できてしまっている場合に、それをまとめて1つの行にしたものです。その後、先に示したように取引先キーと受注日時ごとに商品をまとめてリストとし、1行にしたものがgrouped_dfになります。それから、この後使う用に全ての商品データの名前をリストとして取得しておきます。
バスケット分析において受注数自体は意味を持たず、その商品が購入されたか否かの情報のみが重要となってきます。そのため、grouped_dfでは受注数の情報を落としています。なお、今回の実装では受注数が0とならないことを前提としています。もし、そのようなケースが考えられる場合には別途、tmp_dfに対して前処理を行う、applyで与える関数をカスタムするといった処理を行ってください。
また、データサイズが大きい場合には変数の解放をプログラム中で行わないとメモリオーバーを引き起こしてしまうことがあるので、使用しない変数はすぐに解放しましょう。
mlxtend用のone-hotのテーブル作成
product_columns = {
product: grouped_df["商品名"].apply(lambda x: product in x)
for product in unique_products
}
product_df = pd.DataFrame(product_columns)
del grouped_df, product_columns
mlxtendは列に商品名を、行に各顧客をとり、その顧客がその商品を購入したかを真偽値として持つような行列を入力として用います。ここではその行列(product_df)の作成を行います。
商品名ごとの列を作り、Lamda式を使って各注文でその商品が購入されているかどうかを真偽値で埋めていきます。これをデータフレームに変えたものがproduct_dfです。
バスケット分析の結果出力
freq_items = apriori(
product_df,
min_support=0.01,
use_colnames=True,
max_len=2,
low_memory=True,
verbose=False,
)
df_rules = association_rules(
freq_items,
metric="confidence",
min_threshold=0.0,
)
先の工程で作成したデータフレームを除く、apriori関数のそれぞれの引数について軽く説明します。
- min_support(必須): support値の最小値を規定します。support値が小さいということはその購入パターンの出現頻度が小さいということになります。この値を定めることでノイズを除去します。
- use_colnames(必須): Trueの場合、引数として取った行列の列成分の値を出力されるデータに使用します。特段の理由がなければTrueでいいでしょう。
- max_len(必須): 同時に考慮する商品の最大数です。2以上の整数値で、2の場合1つの商品に対して別の1つの商品がどれだけ一緒に買われるかのみを表しますが、3以上にすると複数の商品が買われた場合に、別の商品や商品群がどれだけ買われやすいかという多対多の関係まで見れます。
- low_memory: これをTrueにすると、メモリ消費を抑えられる代わりに実行時間が長くなります。
- verbose: 実行中にステータスメッセージを出力するかどうかを制御します。
association_rules関数はapriori関数の出力結果をdfにしているイメージです。metricで評価基準としたい統計値を選び、thresholdでその下限値を決めます。
出力の補正
def convert_frozenset(item):
if isinstance(item, frozenset):
return ', '.join(item)
return item
association_rulesの出力するデータフレームにおいて、antecedentsとconsequentsを表す列はfrozensetと呼ばれるオブジェクトになっています。これにより、そのまま保存等すると読み取りにくいフォーマットとなってしまいますので、あらかじめ文字列に修正しておきましょう。
終わりに
今回はBigQuery上のデータをバスケット分析する方法をまとめました。データ数が多くメモリに乗らない場合は検討してみてください。