この内容は金明哲さんの「テキストアナリティクスの基礎と実践」のRでの実装をpythonで書き換えながら読んでいくものです。
提供されているデータは、すでに形態素解析されてある程度集計されたデータとなります。
説明が不十分であること、参考書通りの解析ができているわけではないことはご了承ください。
詳しくは本を読んでいただければと思います。
前回
テキストのクラスタリング
6. アソシエーション分析による共起分析
6.1 アソシエーション分析
アソシエーション分析は、POS(販売時点情報)データ解析のために開発された方法である。
以下のPOSデータをイメージした架空のデータを示す。
このようなデータをマーケット・バスケット・トランザクションと呼び、各行を1つのトランザクション、あるいはバスケットと呼ぶ。
アソシエーション分析は、バスケットの中の商品間の関連性について分析を行う方法である。
トランザクションデータから、頻出するアイテムの組み合わせの規則性を漏れなく抽出し、興味深い結果を探し出すことを主な目的とする。
Aprioriなどのアソシエーション分析のアルゴリズムには、ルールを返すものと、頻出アイテムセットを返すものがある。
import pandas as pd
d1 = pd.DataFrame({'TID':[1,2,3,4,5],
'アイテム集合':[{'パン','牛乳','ハム','果物'},
{'パン','オムツ','ビール','ハム'},
{'ソーセージ','ビール','オムツ'},
{'弁当','ビール','オムツ','タバコ'},
{'弁当','ビール','オレンジジュース','果物'}]})
d1
6.2 アソシエーション・ルール
アソシエーション・ルールとは
トランザクションデータベースに頻出するアイテム間の組み合わせの規則をアソシエーション・ルールと呼ぶ。
「商品Aを買うと商品Bも買う」というように、項目間に同時性や関連性があるルールを相関ルールと呼び、「{A}⇒{B}」と表す。
この「⇒」の左辺を条件部(antecedent:LHS)、右辺を結論部(consequent:RHS)と呼ぶ。
アソシエーション・ルールを検出するもっとも広く知られているアルゴリズムは、Apriori(アプリオリ)である。
ルールの評価指標
アソシエーション・ルールを検出する際、何らかの評価指標が必要である。
多く用いられている指標としては支持度、確信度、リフトがある。
ルールX⇒Yの支持度は、アイテム集合XとYを同時に含むトランザクションが全体の中に占める割合である。
$$
supp(X⇒Y)=\frac{XとYを同時に含む件数}{全体の件数}
$$
確信度は、アイテム集合XとYを含むルールの件数が条件Xを含む件数の中に占める割合である。
$$
conf(X⇒Y)=\frac{supp(X⇒Y)}{supp(X)}=\frac{XとYを同時に含む件数}{Xを含む件数}
$$
リフトは、確信度を結論部の支持度で割った値である。
$$
lift(X⇒Y)=\frac{conf(X⇒Y)}{supp(X)}=\frac{確信度}{Yを含む件数\ /\ 全体の件数}
$$
例えば、X={オムツ}、Y={ビール}とすると、Xを含むのは3件、Yを含むのは4件、XとYを同時に含むのは3件、総件数は5件である。
ルールX⇒Yの支持度は、確信度、リフトはそれぞれ、
$supp(X⇒Y)=3\ /\ 5=0.6$、$conf(X⇒Y)=3\ /\ 3=1$、$lift(X⇒Y)=1\ /\ (4\ /\ 5)=1.25$である。
データ形式と操作
データ形式と変換
トランザクション形式の場合、TID(Transaction ID)ごとにアイテム数もアイテム名もさまざまである。
トランザクションデータは次に表す表のように、バスケットにあるアイテムは1、ないもの0として行列形式で表すことができる。
import numpy as np
d1_b = pd.DataFrame(d1['アイテム集合'].astype('str').str.replace('[{}\']', ''), columns=['アイテム集合'])
d1_b=d1_b['アイテム集合'].str.split('[{,}]', expand=True)
d1_b['TID'] = np.arange(1,6)
d1_b = d1_b.melt(id_vars='TID', value_name='アイテム')
pd.crosstab(d1_b['TID'], d1_b['アイテム'])
各アイテムとトランザクションの番号との関係を示す。
pd.DataFrame(d1_b.groupby("アイテム")["TID"].apply(tuple)).reset_index()
テキストデータの準備と操作
短い文を語や文節に分割し、その語や文節のバスケットの中のアイテムと見なすことで、語や文節の共起関係のルールを抽出することができる。
仮想的なアンケートの自由回答文10個のデータを使用する。
data1 = [["学費", "下げ", "講義", "充実", "はかっ", "欲しい", "適当", "授業", "いる", "思わ", "れる", "先生", "かなり", "居る"],
["学費", "もう少し", "安く", "欲しい"],
["休み", "期間", "多い", "割", "学費", "高い", "何", "使わ", "いる", "はっきりし", "欲しい"],
["授業", "担当", "教員", "生徒", "選ば", "欲しい"],
["学費", "削減", "あと", "ロッカー"],
["個人", "ロッカー", "作っ", "下さい", "自動車", "通学", "認め"],
["学費", "軽減"],
["学費", "もっと", "安く", "欲しい"],
["クーラー", "つけ", "欲しい"],
["学費", "安く", "下さい"]]
dataset = pd.DataFrame({'Items':data1}, index=np.arange(10))
df = dataset['Items']
単語の出現回数を可視化する。
import collections
import matplotlib.pyplot as plt
import japanize_matplotlib
word_list = []
for w in data1:
word_list += w
#単語の数カウント
c = collections.Counter(word_list)
count_data = pd.DataFrame({'word':dict(c).keys(),
'count':dict(c).values()})
plt.figure(figsize=(12,4))
plt.bar(x=count_data['word'], height=count_data['count']);
plt.xticks(rotation=90);
plt.ylabel('頻度')
ルールの抽出
相関ルールを抽出するためaprioriを用いる。
支持度がもっとも高いアイテムは{学費}で、支持度がもっとも高いルールは「{安く}→{学費}」である。
共起関係について分析しようとするときには、語の前後関係が示されるネットワーク分析よりメリットがない。
しかし、アンケート調査結果の分析を行うとき、自由回答文とアンケート調査の他の項目を選択した情報とをリンク付けして
分析することができるため、アソシエーション分析を用いたほうが便利である。
実行には時間がかかるため注意する。
(今回の10個のデータでも数分かかった。)
from mlxtend.preprocessing import TransactionEncoder
from mlxtend.frequent_patterns import apriori, association_rules
te = TransactionEncoder()
te_ary = te.fit(df).transform(df)
df2 = pd.DataFrame(te_ary, columns=te.columns_)
freq_items1 = apriori(df2, min_support=0.1, use_colnames=True)
freq_items1.sort_values(['support'], ascending=False).reset_index(drop=True)
a_rules1 = association_rules(freq_items1, metric='confidence', min_threshold=1)
a_rules1 = a_rules1.sort_values(['support','leverage'], ascending=False).reset_index(drop=True)
a_rules1.loc[:10,:]
ルールの右辺(結論部)が「安く」という語になっているルールのみを抽出した結果を次に示す。
data2 = a_rules1[a_rules1['consequents']=={'安く'}].reset_index(drop=True)
data2
ルールの視覚化
ルールを構成する語は「もう少し」「もっと」「学費」「欲しい」「下さい」「安く」であり、語からルールに向かって矢印が示された場合、これらの語はルールの左辺(条件部)の構成要素であることを示す。
例えば、rule9には「もっと」「学費」「欲しい」という3つの語から矢印が出ており、さらにrule9から「安く」に矢印が向かっている。
これらから、rule9のルールは{もっと、学費、欲しい}→{安く}になる。
ant = data2['antecedents'].astype('str').str.replace('[(frozenset)\{\}\']', '').str.split('[{,}]', expand=True)
ant['ID'] = ['rule'+str(i+1) for i in np.arange(9)]
ant = ant.melt(id_vars='ID', value_name='item')[['ID', 'item']]
ant= ant[~ant['item'].isna()]
ant[['ID', 'item']] = ant[['item', 'ID']]
ant['ID'] = ant['ID'].str.replace(' ', '')
ant['item'] = ant['item'].str.replace(' ', '')
ant[:10]
con = data2['consequents'].astype('str').str.replace('[(frozenset)\{\}\']', '').str.split('[{,}]', expand=True).replace(' ','')
con['ID'] = ['rule'+str(i+1) for i in np.arange(9)]
con = con.melt(id_vars='ID', value_name='item')[['ID', 'item']]
con= con[~con['item'].isna()]
plot_data = pd.concat([con, ant]).reset_index(drop=True)
import networkx as nx
import matplotlib.pyplot as plt
import japanize_matplotlib
# Graphオブジェクトの作成
G = nx.Graph()
G2 = nx.DiGraph(G)
# nodeデータの追加
v = np.unique([plot_data.ID]+[plot_data.item])
G2.add_nodes_from(v)
# edgeデータの追加
G2.add_edges_from([(plot_data.loc[i, 'ID'], plot_data.loc[i, 'item']) for i in range(len(plot_data))])
c = ['lightgreen' if 'rule' in s else 'lightblue' for s in v]
e_c = ['green' if 'rule' in u else 'blue' for u,v,w in G2.edges(data=True)]
n=len(v)
pos = {}
for i in np.arange(n):
pos.update({v[i]: (0.8*np.sin(2*np.pi/n*i), 0.8*np.cos(2*np.pi/n*i))})
plt.figure(figsize=(8,8))
# ネットワークの可視化
nx.draw(G2, with_labels = True, node_size = 2000, node_color=c,edge_color=e_c, pos=pos, font_family='MS Gothic')
plt.show()
頻出共起の抽出
頻出共起の抽出アルゴリズムeclat
検索アルゴリズムには大きく分けて、幅優先検索と深さ優先検索に分けることができる。
Aprioriは幅優先検索タイプに属する。
深さ優先検索タイプのアルゴリズムにeclatというアルゴリズムがある。
eclatは、最小支持度の減少による性能の悪化がAprioriより少ないが、頻出アイテムが多いときには性能が悪くなる可能性がある。
アルゴリズムeclatの例
eclatによって返される結果は、相関ルールではなく、頻出する語の組み合わせの集合{X,Y}である。
pyECLATというパッケージを使用する場合、次のようなデータを作成する。
data_tr = dataset['Items'].astype(str).str.split(',', expand=True)
data_tr = data_tr.apply(lambda x: x.str.replace('[\[\]\ \']',''))
data_tr
eclatの実行をする。
from pyECLAT import ECLAT
# 引数情報
min_support = 0.01 # 出力する結果のsupprt最小値
min_n_products = 2 # 最小の商品組み合わせ数
#max_length = max([len(x) for x in dataset]) # 出力するアソシエーション・ルール数
max_length = 3 # 出力するアソシエーション・ルール数
# create an instance of eclat
my_eclat = ECLAT(data=data_tr, verbose=True)
# Eclat
rule_indices, rule_supports = my_eclat.fit(min_support=min_support,
min_combination=min_n_products,
max_combination=max_length
)
頻度および支持度でソートした結果を示す。
score_sorted = sorted(rule_indices.items(), key=lambda x:x[1], reverse=True)
score_sorted[:10]
[('学費 & 下さい', [9]),
('下さい & 安く', [9]),
('学費 & 下さい & 安く', [9]),
('つけ & クーラー', [8]),
('つけ & 欲しい', [8]),
('クーラー & 欲しい', [8]),
('つけ & クーラー & 欲しい', [8]),
('もっと & 学費', [7]),
('もっと & 安く', [7]),
('もっと & 欲しい', [7])]
score_sorted = sorted(rule_supports.items(), key=lambda x:x[1], reverse=True)
score_sorted[:10]
[('学費 & 欲しい', 0.4),
('学費 & 安く', 0.3),
('学費 & いる', 0.2),
('授業 & 欲しい', 0.2),
('安く & 欲しい', 0.2),
('いる & 欲しい', 0.2),
('学費 & 安く & 欲しい', 0.2),
('学費 & いる & 欲しい', 0.2),
('何 & 使わ', 0.1),
('何 & 割', 0.1)]
次回
テキストの分類分析
参考書