關聯分析又稱關聯挖掘,就是在交易數據、關系數據或其他信息載體中,查找存在於項目集合或對象集合之間的頻繁模式、關聯、相關性或因果結構。關聯分析的一個典型例子是購物籃分析。通過發現顧客放入購物籃中不同商品之間的聯系,分析顧客的購買習慣。比如,67%的顧客在購買尿布的同時也會購買啤酒。通過了解哪些商品頻繁地被顧客同時購買,可以幫助零售商制定營銷策略。關聯分析也可以應用於其他領域,如生物信息學、醫療診斷、網頁挖掘和科學數據分析等。
1. 問題定義
圖1 購物籃數據的二元表示
圖1表示顧客的購物籃數據,其中每一行是每位顧客的購物記錄,對應一個事務,而每一列對應一個項。令I={i1, i2, ... , id}是購物籃數據中所有項的集合,而T={t1, t2, ... , tN}是所有事務的集合。每個事務ti包含的項集都是I的子集。在關聯分析中,包含0個或多個項的集合被稱為項集(itemset)。所謂的關聯規則是指形如X→Y的表達式,其中X和Y是不相交的項集。在關聯分析中,有兩個重要的概念——支持度(support)和置信度(confidence)。支持度確定規則可以用於給定數據集的頻繁程度,而置信度確定Y在包含X的事務中出現的頻繁程度。支持度(s)和置信度(c)這兩種度量的形式定義如下:
公式1
其中,N是事務的總數。關聯規則的支持度很低,說明該規則只是偶然出現,沒有多大意義。另一方面,置信度可以度量通過關聯規則進行推理的可靠性。因此,大多數關聯分析算法采用的策略是:
(1)頻繁項集產生:其目標是發現滿足最小支持度阈值的所有項集,這些項集稱作頻繁項集。
(2)規則的產生:其目標是從上一步發現的頻繁項集中提取所有高置信度的規則,這些規則稱作強規則。
2. 構建FP-tree
FP-growth算法通過構建FP-tree來壓縮事務數據庫中的信息,從而更加有效地產生頻繁項集。FP-tree其實是一棵前綴樹,按支持度降序排列,支持度越高的頻繁項離根節點越近,從而使得更多的頻繁項可以共享前綴。
圖2 事務型數據庫
圖2表示用於購物籃分析的事務型數據庫。其中,a,b,...,p分別表示客戶購買的物品。首先,對該事務型數據庫進行一次掃描,計算每一行記錄中各種物品的支持度,然後按照支持度降序排列,僅保留頻繁項集,剔除那些低於支持度阈值的項,這裡支持度阈值取3,從而得到<(f:4),(c:4),(a:3),(b:3),(m:3,(p:3)>(由於支持度計算公式中的N是不變的,所以僅需要比較公式中的分子)。圖2中的第3列展示了排序後的結果。
FP-tree的根節點為null,不表示任何項。接下來,對事務型數據庫進行第二次掃描,從而開始構建FP-tree:
第一條記錄<f,c,a,m,p>對應於FP-tree中的第一條分支<(f:1),(c:1),(a:1),(m:1),(p:1)>:
圖3 第一條記錄
由於第二條記錄<f,c,a,b,m>與第一條記錄有相同的前綴<f,c,a>,因此<f,c,a>的支持度分別加一,同時在(a:2)節點下添加節點(b:1),(m:1)。所以,FP-tree中的第二條分支是<(f:2),(c:2),(a:2),(h:1),(m:1)>:
圖4 第二條記錄
第三條記錄<f,b>與前兩條記錄相比,只有一個共同前綴<f>,因此,只需要在(f:3)下添加節點<b:1>:
圖5 第三條記錄
第四條記錄<c,b,p>與之前所有記錄都沒有共同前綴,因此在根節點下添加節點(c:1),(b:1),(p:1):
圖6 第四條記錄
類似地,將第五條記錄<f,c,a,m,p>作為FP-tree的一個分支,更新相關節點的支持度:
圖7 第五條記錄
為了便於對整棵樹進行遍歷,建立一張項的頭表(an item header table)。這張表的第一列是按照降序排列的頻繁項。第二列是指向該頻繁項在FP-tree中節點位置的指針。FP-tree中每一個節點還有一個指針,用於指向相同名稱的節點:
圖8 FP-tree
綜上,FP-tree的節點可以定義為:
1 2 3 4 5 6 7 8 9 10 11class
TreeNode {
private
:
String name;
// 節點名稱
int
count;
// 支持度計數
TreeNode *parent;
// 父節點
Vector<TreeNode *> children;
// 子節點
TreeNode *nextHomonym;
// 指向同名節點
...
}
3. 從FP-tree中挖掘頻繁模式(Frequent Patterns)
我們從頭表的底部開始挖掘FP-tree中的頻繁模式。在FP-tree中以p結尾的節點鏈共有兩條,分別是<(f:4),(c:3),(a:3),(m:2),(p:2)>和<(c:1),(b:1),(p:1)>。其中,第一條節點鏈表表示客戶購買的物品清單<f,c,a,m,p>在數據庫中共出現了兩次。需要注意到是,盡管<f,c,a>在第一條節點鏈中出現了3次,單個物品<f>出現了4次,但是它們與p一起出現只有2次,所以在條件FP-tree中將<(f:4),(c:3),(a:3),(m:2),(p:2)>記為<(f:2),(c:2),(a:2),(m:2),(p:2)>。同理,第二條節點鏈表示客戶購買的物品清單<c,b,p>在數據庫中只出現了一次。我們將p的前綴節點鏈<(f:2),(c:2),(a:2),(m:2)>和<(c:1),(b:1)>稱為p的條件模式基(conditional pattern base)。我們將p的條件模式基作為新的事務數據庫,每一行存儲p的一個前綴節點鏈,根據第二節中構建FP-tree的過程,計算每一行記錄中各種物品的支持度,然後按照支持度降序排列,僅保留頻繁項集,剔除那些低於支持度阈值的項,建立一棵新的FP-tree,這棵樹被稱之為p的條件FP-tree:
圖9 p的條件FP-tree
從圖9可以看到p的條件FP-tree中滿足支持度阈值的只剩下一個節點(c:3),所以以p結尾的頻繁項集有(p:3),(cp:3)。由於c的條件模式基為空,所以不需要構建c的條件FP-tree。
在FP-tree中以m結尾的節點鏈共有兩條,分別是<(f:4),(c:3),(a:3),(m:2)>和<(f:4),(c:3),(a:3),(b:1),(m:1)>。所以m的條件模式基是<(f:2),(c:2),(a:2)>和<(f:1),(c:1),(a:1),(b:1)>。我們將m的條件模式基作為新的事務數據庫,每一行存儲m的一個前綴節點鏈,計算每一行記錄中各種物品的支持度,然後按照支持度降序排列,僅保留頻繁項集,剔除那些低於支持度阈值的項,建立m的條件FP-tree:
圖10 m的條件FP-tree
與p不同,m的條件FP-tree中有3個節點,所以需要多次遞歸地挖掘頻繁項集mine(<(f:3),(c:3),(a:3)|(m:3)>)。按照<(a:3),(c:3),(f:3)>的順序遞歸調用mine(<(f:3),(c:3)|a,m>),mine(<(f:3)|c,m>),mine(null|f,m)。由於(m:3)滿足支持度阈值要求,所以以m結尾的頻繁項集有{(m:3)}。
圖11 節點(a,m)的條件FP-tree
從圖11可以看出,節點(a,m)的條件FP-tree有2個節點,需要進���步遞歸調用mine(<(f:3)|c,a,m>)和mine(<null|f,a,m>)。進一步遞歸mine(<(f:3)|c,a,m>)生成mine(<null|f,c,a,m>)。因此,以(a,m)結尾的頻繁項集有{(am:3),(fam:3),(cam:3),(fcam:3)}。
圖 12 節點(c,m)的條件FP-tree
從圖12可以看出,節點(c,m)的條件FP-tree只有1個節點,所以只需要遞歸調用mine(<null|f,c,m>)。因此,以(c,m)結尾的頻繁項集有{(cm:3),(fcm:3)}。同理,以(f,m)結尾的頻繁項集有{(fm:3)}。
在FP-tree中以b結尾的節點鏈共有三條,分別是<(f:4),(c:3),(a:3),(b:1)>,<(f:4),(b:1)>和<(c:1),(b:1)>。由於節點b的條件模式基<(f:1),(c:1),(a:1)>,<(f:1)>和<(c:1)>都不滿足支持度阈值,所以不需要再遞歸。因此,以b結尾的頻繁項集只有(b:3)。
同理可得,以a結尾的頻繁項集{(fa:3),(ca:3),(fca:3),(a:3)},以c結尾的頻繁項集{(fc:3),(c:4)},以f結尾的頻繁項集{(f:4)}。
4. 算法實現
聲明FP-tree節點:
class TreeNode { //Constructors-Destructors public: TreeNode(); TreeNode(string); ~TreeNode(); //Member variables private: string nodeName; int supportCount; TreeNode *parentNode; vector<TreeNode *> childNodeList; TreeNode *nextHomonymNode; //Member functions public: string getName(); void setName(string); int getSupportCount() const; void setSupportCount(int); TreeNode* getParentNode() const; void setParentNode(TreeNode*); vector<TreeNode*> getChildNodeList() const; void addChild(TreeNode*); TreeNode* findChildNode(string) const; void setChildren(vector<TreeNode*>); void printChildrenNames() const; TreeNode* getNextHomonym() const; void setNextHomonym(TreeNode *nextHomonym); void countIncrement(int); };
構建HeaderTable:
//HeaderTable存儲事務數據庫的數據 vector<TreeNode*> FPTree::buildHeaderTable(vector<vector<string>> transRecords) { vector<TreeNode*> F1; //存儲滿足支持度阈值的節點,並按照支持度降序排列,支持度相等的情況下按照字母順序排序,所以構建的FP-tree與論文有所不同,但是最終生成的頻繁項集是一樣的 if (transRecords.size() > 0) { map<string, TreeNode*> mp; //calculate supportCount of every transRecords for (vector<string> record : transRecords) { for (string item : record) { //if item not in map, put item into map and set supportCount one if (mp.find(item) == mp.end()) { TreeNode *node = new TreeNode(item); node->setSupportCount(1); mp.insert(map<string, TreeNode*>::value_type(item, node)); } //if item in map, supportCount plus one else { mp.find(item)->second->countIncrement(1); } } } //put TreeNodes whose supportCount greater than minSupportThreshold into vector F1 for (auto iterator = mp.begin(); iterator != mp.end(); iterator++) { if (iterator->second->getSupportCount() >= minSupportThreshold) { //cout << "iterator->second = " << iterator->second->getSupportCount() << endl; F1.push_back(iterator->second); } } //sort vector F1 by supportCount sort(F1.begin(), F1.end(), sortBySupportCount); } return F1; }
構建FP-tree:
TreeNode* FPTree::buildTree(vector<vector<string>> transRecords, vector<TreeNode*> F1) { TreeNode *root = new TreeNode(); //根節點root for (vector<string> transRecord : transRecords) { //拷貝transRecord到record vector<string> record; for (auto iter = transRecord.begin(); iter != transRecord.end(); iter++) { record.push_back(*iter); } record = sortedByF1(record, F1); //根據F1中存儲的頻繁項集,將record按照支持度降序排列,並且僅保留頻繁項集,剔除那些低於支持度阈值的項
//順序比較record中的節點和FP-tree中的節點,如果record中的節點已經存在於FP-tree中,將該節點的支持度加一,繼續比較下一個節點,否則調用addNodes來添加剩余節點到FP-tree中 TreeNode *subTreeRoot = root; TreeNode *tmpRoot = nullptr; if (!root->getChildNodeList().empty()) { while (!record.empty() && (tmpRoot = subTreeRoot->findChildNode(*(record.begin()))) != nullptr) { tmpRoot->countIncrement(1); subTreeRoot = tmpRoot; record.erase(record.begin()); } } addNodes(subTreeRoot, &record, F1); } return root; }
添加節點:
void FPTree::addNodes(TreeNode *ancestor, vector<string> *record, vector<TreeNode*> F1) { if (!record->empty()) { while (!record->empty()) { string item = *(record->begin()); record->erase(record->begin()); TreeNode *leafNode = new TreeNode(item); leafNode->setSupportCount(1); leafNode->setParentNode(ancestor); ancestor->addChild(leafNode); for (TreeNode *f1 : F1) { if (f1->getName() == item) { while (f1->getNextHomonym() != NULL) { f1 = f1->getNextHomonym(); } f1->setNextHomonym(leafNode); break; } } addNodes(leafNode, record, F1); } } }
sortedByF1:
vector<string> FPTree::sortedByF1(vector<string> transRecord, vector<TreeNode*> F1) { //如果item是頻繁項,則一定對應於F1中的序號,按照該序號對item進行排序,存儲到rest中 map<string, int> mp; for (string item : transRecord) { for (int i = 0; i < F1.size(); i++) { TreeNode *tNode = F1[i]; if (tNode->getName() == item) { mp.insert(map<string, int>::value_type(item, i)); } } } vector<pair<string, int>> vec; for (auto iterator = mp.begin(); iterator != mp.end(); iterator++) { vec.push_back(make_pair(iterator->first, iterator->second)); } sort(vec.begin(), vec.end(), sortByF1); vector<string> rest; for (auto iterator = vec.begin(); iterator != vec.end(); iterator++) { rest.push_back((*iterator).first); } return rest; }
遞歸調用FP-Growth挖掘頻繁項:
//postPattern存儲後綴,比如從HeaderTable中的p節點開始挖掘頻繁項時,postPattern為p void FPTree::FPGrowth(vector<vector<string>> transRecords, vector<string> postPattern) { vector<TreeNode*> headerTable = buildHeaderTable(transRecords); //構建headerTable TreeNode *treeRoot = buildTree(transRecords, headerTable); //構建FP-tree //遞歸退出條件:根節點沒有孩子節點 if (treeRoot->getChildNodeList().size() == 0) { return; } //輸出頻繁項集 if (!postPattern.empty()) { for (TreeNode *header : headerTable) { cout << header->getSupportCount() << ends << header->getName() << ends; for (string str : postPattern) { cout << str << ends; } cout << endl; } } //遍歷headerTable for (TreeNode *header : headerTable) { vector<string> newPostPattern; newPostPattern.push_back(header->getName()); //存儲原先的後綴 if (!postPattern.empty()) { for (string str : postPattern) { newPostPattern.push_back(str); } } //newTransRecords存儲前綴節點鏈 vector<vector<string>> newTransRecords; TreeNode *backNode = header->getNextHomonym(); //通過getNextHomonym遍歷同名節點,通過getParentNode獲取前綴節點鏈 while (backNode != nullptr) { int supportCount = backNode->getSupportCount(); vector<string> preNodes; TreeNode *parent = backNode;
while ((parent = parent->getParentNode())->getName().length() != 0) { preNodes.push_back(parent->getName()); } while (supportCount-- > 0) { newTransRecords.push_back(preNodes); } backNode = backNode->getNextHomonym();
} FPGrowth(newTransRecords, newPostPattern); //遞歸構建條件FP-tree } }
5. 討論
在韓家炜教授提出FP-growth算法之前,關聯分析普遍采用Apriori及其變形算法。但是,Apriori及其變形算法需要多次掃描數據庫,並需要生成指數級的候選項集,性能並不理想。FP-growth算法提出利用了高效的數據結構FP-tree,不再需要多次掃描數據庫,同時也不再需要生成大量的候選項。
對於單路徑的FP-tree其實不需要遞歸,通過排列組合可以直接生成。韓家炜教授在其論文中提到了針對單路徑的優化算法。論文中也提到了面對大數據時,如何調整FP-growth算法使之適應數據量。
6. 參考資料
[1] Mining Frequent Patterns without Candidate Generation. Jiawei Han, Jian Pei, and Yiwen Yin. Data Mining and Knowledge Discovery. Volume 8 Issue 1. January 2004. [PDF]
[2] Frequent Pattern 挖掘之二(FP Growth算法). yfx416. Software Engineer in NRC. 2011. [Link]
[3] FP-Tree算法的實現. Orisun. 華夏35度. 2011. [Link]