Pythonで購入履歴から商品のクラスタリング(3パターン)

Pythonで購入履歴から商品のクラスタリング(3パターン)

商品購入ログを使って商品のクラスタリング

商品購入ログから商品のクラスタリングをしたい。
Pythonの最強ライブラリscikit-learnを使って行う。

  • 最もメジャーなK-means
  • 疎行列に強いらしいSpectralClustering
  • 階層型クラスタリングのウォード法

同一データに対して、上記3種のクラスタリングを行う。

1. 環境構築

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

こんな感じになる↓

商品パンドラペガサスマリク城之内海馬遊戯
クリッター010111
サイクロン001101
ハリケーン010100
ブラック・ホール000011
ブラック・マジシャン100001
ブラック・マジシャン・ガール000001
リビングデッドの呼び声000110
大嵐010000
死のマジック・ボックス100001
死者蘇生011011
聖なるバリア −ミラーフォース−001001
融合011111
融合解除010011
青眼の白龍000010

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")

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×