商品購入ログを使って商品のクラスタリング 商品購入ログから商品のクラスタリングをしたい。 Pythonの最強ライブラリscikit-learnを使って行う。
最もメジャーなK-means 疎行列に強いらしいSpectralClustering 階層型クラスタリングのウォード法 同一データに対して、上記3種のクラスタリングを行う。
1. 環境構築 Anacondaが既にインストールされている前提。 入ってなければ以下を参考にして入れる。
Anacondaのインストール
Anacondaのインストールこのブログ内でよく「Anacondaがインストールされている想定」と書くが一度もインストールの方法を書いていないのでここに書く。非…
以下のように「data」という名前の環境を作り、必要なライブラリをインストール。
1 2 3 4 5 conda create -n data python=3.9 conda activate data conda install -c anaconda pandas conda install -c conda-forge matplotlib conda install -c anaconda scikit-learn
終わり。
2. 使用データ 手元にあった下記の購入履歴を使用。 ストラクチャーデッキを参考にしている。
yugioh_log.csv
時刻 購入者 商品 0:19:15 城之内 クリッター 1:16:21 遊戯 ブラック・マジシャン・ガール 2:06:38 海馬 融合 2:34:36 遊戯 ブラック・マジシャン 2:57:13 海馬 融合解除 4:04:33 マリク サイクロン 4:38:45 パンドラ 死のマジック・ボックス 4:48:40 海馬 リビングデッドの呼び声 5:18:28 城之内 ハリケーン 6:33:19 遊戯 死者蘇生 8:46:46 遊戯 融合 9:40:23 遊戯 ブラック・ホール 9:40:30 ペガサス 死者蘇生 10:26:40 ペガサス 融合 10:54:43 ペガサス 大嵐 11:33:02 ペガサス ハリケーン 14:19:39 マリク 聖なるバリア −ミラーフォース− 15:44:19 海馬 ブラック・ホール 15:44:37 城之内 融合 16:29:03 ペガサス クリッター 17:05:46 マリク 死者蘇生 17:09:09 海馬 死者蘇生 20:26:48 城之内 リビングデッドの呼び声 21:13:05 海馬 青眼の白龍 21:41:42 城之内 サイクロン 21:49:30 ペガサス 融合解除 21:52:27 遊戯 聖なるバリア −ミラーフォース− 21:59:26 マリク 融合 22:15:42 パンドラ ブラック・マジシャン 23:14:51 遊戯 融合解除 23:17:19 遊戯 サイクロン 23:23:05 遊戯 クリッター 23:55:36 海馬 クリッター 23:56:50 遊戯 死のマジック・ボックス 23:55:56 海馬 青眼の白龍
3. 実験 さっきの購入履歴を使用してPythonによるクラスタリング実験を行う。
3.1. クロス集計 はじめにpandasによってデータを取得。 その後pandasの関数で「商品」と「購入者」のクロス集計を行う。
クロス集計とは、今回の例でいうと「商品」を縦軸、「購入者」を横軸として、 各商品の行に対して、それを購入した人の列に「購入した数」が与えられる集計方法。
今回は購入したかどうかだけを見るためにcross = cross.mask(cross > 0, 1)
として、「1以上」を全部「1」にしている。 これをしないと特定のひとりが大量に何かを買った時にノイズになる(かもしれない)。
1 2 3 4 5 6 7 8 9 10 import pandas as pd #### CSV読み込み loadcsv = "yugioh_log.csv" df = pd.read_csv(loadcsv, encoding="shift-jis") # ← CSVの文字コードがutf8なら encoding="utf-8" #### クロス集計 cross = pd.crosstab(df["商品"], df["購入者"]) cross = cross.mask(cross > 0, 1) # 2回以上でも1にする # cross.to_csv("yugioh_log_cross.csv", encoding="shift-jis") #CSV出力 utf8だとEXCELが文字化けするのでshift-jis
こんな感じになる↓
商品 パンドラ ペガサス マリク 城之内 海馬 遊戯 クリッター 0 1 0 1 1 1 サイクロン 0 0 1 1 0 1 ハリケーン 0 1 0 1 0 0 ブラック・ホール 0 0 0 0 1 1 ブラック・マジシャン 1 0 0 0 0 1 ブラック・マジシャン・ガール 0 0 0 0 0 1 リビングデッドの呼び声 0 0 0 1 1 0 大嵐 0 1 0 0 0 0 死のマジック・ボックス 1 0 0 0 0 1 死者蘇生 0 1 1 0 1 1 聖なるバリア −ミラーフォース− 0 0 1 0 0 1 融合 0 1 1 1 1 1 融合解除 0 1 0 0 1 1 青眼の白龍 0 0 0 0 1 0
3.2. クラスタリング(K-means) K-meansによるクラスタリングを行う。 scikit-learnの関数にさっきのクロス集計した表を入れたら良い。
クラス数を自分で決める必要がある、とりあえず「5」とする。
1 2 3 4 5 6 7 8 9 #### クラスタリング(K-means) from sklearn import cluster # クラスタリング実行 kmeans = cluster.KMeans(n_clusters=5) predict = kmeans.fit_predict(cross) # クラスタリング結果表示 new_class = pd.DataFrame({"class":predict}, index=cross.index) print(new_class.groupby("class").groups)
結果にはランダム性があるのでやるたびにちょっと変わる。 こんな感じになる↓
1 2 3 4 5 0: ['ブラック・ホール', 'ブラック・マジシャン', 'ブラック・マジシャン・ガール', '死のマジック・ボックス'], 1: ['クリッター', '死者蘇生', '融合', '融合解除'], 2: ['ハリケーン', '大嵐'], 3: ['サイクロン', '聖なるバリア −ミラーフォース−'], 4: ['リビングデッドの呼び声', '青眼の白龍']
「ブラック・マジシャン」, 「ブラック・マジシャン・ガール」, 「死のマジック・ボックス」が同じクラスなのはそれっぽい。 「融合」と「融合解除’」が一緒なのもそれっぽい。
少ないデータのわりによくできている。
3.3. クラスタリング(SpectralClustering) 続いてSpectralClustering。 この手法は疎行列(0ばかりの行列)のクラスタリングに向いているらしい。 今回の表はそこそこ疎行列っぽいので試してみる。
これもクラスを「5」とする。
1 2 3 4 5 6 7 8 9 10 11 #### クラスタリング(SpectralClustering) from scipy.sparse import csr_matrix # クラスタリング実行 csr = csr_matrix(cross) # データ型をスパースマトリクスに変換 spectral_clustering = cluster.SpectralClustering(n_clusters=5, affinity='nearest_neighbors', n_init=100, assign_labels='discretize') predict = spectral_clustering.fit_predict(csr) # クラスタリング結果表示 new_class = pd.DataFrame({"class":predict}, index=cross.index) new_class.to_csv("yugioh_log_cluster.csv", encoding="shift-jis") print(new_class.groupby("class").groups)
K-meansと同様に結果にランダム性がある。 こんな感じになる↓
1 2 3 4 5 0: ['大嵐', '青眼の白龍'], 1: ['ブラック・ホール', 'ブラック・マジシャン', 'ブラック・マジシャン・ガール', '死のマジック・ボックス'], 2: ['クリッター', 'ハリケーン', 'リビングデッドの呼び声'], 3: ['サイクロン'], 4: ['死者蘇生', '聖なるバリア −ミラーフォース−', '融合', '融合解除']
さっきのK-meansと大体同じ感じにクラスタリングされている。 そもそものデータが少なくて差が出づらい、もっと巨大なデータなら差が現れると思う。
3.4. クラスタリング(ウォード法) 続いてウォード法。 これは階層型クラスタリングと呼ばれる種類の手法。
クラスタリングの様子が樹形図として視覚的に観察できて面白い。 実行時間が長いので大きなデータに対して行うと後悔する。
これはクラスの分け方がさっきまでの非階層型クラスタリングとは異なる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #### クラスタリング(ウォード法) from scipy.cluster.hierarchy import linkage, dendrogram, fcluster # クラスタリング実行 linkage_result = linkage(cross, method='ward', metric='euclidean') threshold = max(linkage_result[:, 2])*0.5 # クラス分けの閾値 MAXの半分とする clustered = fcluster(linkage_result, t=threshold, criterion='distance') # クラスタリング結果表示 new_class = pd.DataFrame({'class':clustered}, index=cross.index) print(new_class.groupby("class").groups) # クラスタリングの樹形図表示 import matplotlib.pyplot as plt plt.rcParams["font.family"] = "Yu Gothic" plt.rcParams["font.size"] = 6 # rcParamsではフォントサイズは最小6までしか小さくできない plt.figure(num=None, figsize=(16, 9), dpi=150) # さらにフォントを小さくしたい場合はfigsizeを大きくして相対的に小さくする plt.subplots_adjust(left=0.2) # 文字が左にはみ出るので調整 dendrogram(linkage_result, labels=cross.index, color_threshold=threshold, orientation="right") plt.savefig("yugioh_log_dendrogram.png")
クラス分けの結果はこんな感じ↓
1 2 3 4 5 6 1: ['サイクロン', '聖なるバリア −ミラーフォース−'], 2: ['ブラック・マジシャン', '死のマジック・ボックス'], 3: ['ブラック・ホール', 'ブラック・マジシャン・ガール'], 4: ['クリッター', '死者蘇生', '融合', '融合解除'], 5: ['ハリケーン', '大嵐'], 6: ['リビングデッドの呼び声', '青眼の白龍']
樹形図はこんな感じ↓
トーナメントみたいに繋がってる相手が一番特徴の近い相手。 繋がってる時の横軸が長いほど特徴が離れている。
この横軸を縦に切ったときに繋がっている者同士を同じクラスとする。 今回は最大値の半分であるおよそ1.5で切った。 文字が小さくて見づらいが、もし2.0にしていたら「ブラック・マジシャン」と「ブラック・マジシャン・ガール」が同じクラスになっていたことが分かる。
3.5. 結果 今回はデータが小さいこともあり、どれも似たような結果となった。 実際には多分手法による良し悪しが出てくる。
これを応用すれば、たとえばデッキを横軸、使用カードを縦軸としたクロス集計によってカードのクラスタリングなどもできる。 購入履歴にとどまらず色々なところで使えそう。
4. コード 全部まとまった状態のコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 import pandas as pd from sklearn import cluster from scipy.sparse import csr_matrix from scipy.cluster.hierarchy import linkage, dendrogram, fcluster import matplotlib.pyplot as plt #### CSV読み込み loadcsv = "yugioh_log.csv" df = pd.read_csv(loadcsv, encoding="shift-jis") # ← CSVの文字コードがutf8なら encoding="utf-8" #### 1. クロス集計 cross = pd.crosstab(df["商品"], df["購入者"]) cross = cross.mask(cross > 0, 1) # 2回以上でも1にする cross.to_csv("yugioh_log_cross.csv", encoding="shift-jis") #CSV出力 utf8だとEXCELが文字化けするのでshift-jis #### 2. クラスタリング(K-means) # クラスタリング実行 kmeans = cluster.KMeans(n_clusters=5) predict = kmeans.fit_predict(cross) # クラスタリング結果表示 new_class = pd.DataFrame({"class":predict}, index=cross.index) print(new_class.groupby("class").groups) #### 3. クラスタリング(SpectralClustering) # クラスタリング実行 csr = csr_matrix(cross) # データ型をスパースマトリクスに変換 spectral_clustering = cluster.SpectralClustering(n_clusters=5, affinity='nearest_neighbors', n_init=100, assign_labels='discretize') predict = spectral_clustering.fit_predict(csr) # クラスタリング結果表示 new_class = pd.DataFrame({"class":predict}, index=cross.index) new_class.to_csv("yugioh_log_cluster.csv", encoding="shift-jis") print(new_class.groupby("class").groups) #### 4. クラスタリング(ウォード法) # クラスタリング実行 linkage_result = linkage(cross, method='ward', metric='euclidean') threshold = max(linkage_result[:, 2])*0.5 # クラス分けの閾値 MAXの半分とする clustered = fcluster(linkage_result, t=threshold, criterion='distance') # クラスタリング結果表示 new_class = pd.DataFrame({'class':clustered}, index=cross.index) print(new_class.groupby("class").groups) # クラスタリングの詳細表示 plt.rcParams["font.family"] = "Yu Gothic" plt.rcParams["font.size"] = 6 # rcParamsではフォントサイズは最小6までしか小さくできない plt.figure(num=None, figsize=(16, 9), dpi=150) # さらにフォントを小さくしたい場合はfigsizeを大きくして相対的に小さくする plt.subplots_adjust(left=0.2) # 文字が左にはみ出るので調整 dendrogram(linkage_result, labels=cross.index, color_threshold=threshold, orientation="right") plt.savefig("yugioh_log_dendrogram.png")