Apriori演算法--關聯規則挖掘
介紹
Apriori演算法是一個經典的資料探勘演算法,Apriori的單詞的意思是"先驗的",說明這個演算法是具有先驗性質的,就是說要通過上一次的結果推匯出下一次的結果,這個如何體現將會在下面的分析中會慢慢的體現出來。Apriori演算法的用處是挖掘頻繁項集的,頻繁項集粗俗的理解就是找出經常出現的組合,然後根據這些組合最終推出我們的關聯規則。
Apriori演算法原理
Apriori演算法是一種逐層搜尋的迭代式演算法,其中k項集用於挖掘(k+1)項集,這是依靠他的先驗性質的:
頻繁項集的所有非空子集一定是也是頻繁的。
通過這個性質可以對候選集進行剪枝。用k項集如何生成(k+1)項集呢,這個是演算法裡面最難也是最核心的部分。
通過2個步驟
1、連線步,將頻繁項自己與自己進行連線運算。
2、剪枝步,去除候選集項中的不符合要求的候選項,不符合要求指的是這個候選項的子集並非都是頻繁項,要遵守上文提到的先驗性質。
3、通過1,2步驟還不夠,在後面還要根據支援度計數篩選掉不滿足最小支援度數的候選集。
演算法例項
首先是測試資料:
交易ID |
商品ID列表 |
T100 |
I1,I2,I5 |
T200 |
I2,I4 |
T300 |
I2,I3 |
T400 |
I1,I2,I4 |
T500 |
I1,I3 |
T600 |
I2,I3 |
T700 |
I1,I3 |
T800 |
I1,I2,I3,I5 |
T900 |
I1,I2,I3 |
最後我們可以看到頻繁3項集的結果為{1, 2, 3}和{1, 2, 5},然後我們去後者{1, 2, 5}作為頻繁項集來生產他的關聯規則,但是在這之前得先知道一些概念,怎麼樣才能夠成為一條關聯規則,關有頻繁項集還是不夠的。
關聯規則
confidence(置信度)
confidence的中文意思為自信的,在這裡其實表示的是一種條件概率,當在A條件下,B發生的概率就可以表示為confidence(A->B)=p(B|A),意為在A的情況下,推出B的概率。那麼關聯規則與有什麼關係呢,請繼續往下看。最小置信度閾值
按照字面上的意思就是限制置信度值的一個限制條件嘛,這個很好理解。
強規則
強規則就是指的是置信度滿足最小置信度(就是>=最小置信度)的推斷就是一個強規則,也就是文中所說的關聯規則了。這個在下面的程式中會有所體現。
演算法的程式碼實現
我自己寫的演算法實現可能會讓你有點晦澀難懂,不過重在理解演算法的整個思路即可,尤其是連線步和剪枝步是最難點所在,可能還存在bug。
輸入資料:
T1 1 2 5
T2 2 4
T3 2 3
T4 1 2 4
T5 1 3
T6 2 3
T7 1 3
T8 1 2 3 5
T9 1 2 3
頻繁項類:
/**
* 頻繁項集
*
* @author lyq
*
*/
public class FrequentItem implements Comparable<FrequentItem>{
// 頻繁項集的集合ID
private String[] idArray;
// 頻繁項集的支援度計數
private int count;
//頻繁項集的長度,1項集或是2項集,亦或是3項集
private int length;
public FrequentItem(String[] idArray, int count){
this.idArray = idArray;
this.count = count;
length = idArray.length;
}
public String[] getIdArray() {
return idArray;
}
public void setIdArray(String[] idArray) {
this.idArray = idArray;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
@Override
public int compareTo(FrequentItem o) {
// TODO Auto-generated method stub
return this.getIdArray()[0].compareTo(o.getIdArray()[0]);
}
}
主程式類:
package DataMining_Apriori;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* apriori演算法工具類
*
* @author lyq
*
*/
public class AprioriTool {
// 最小支援度計數
private int minSupportCount;
// 測試資料檔案地址
private String filePath;
// 每個事務中的商品ID
private ArrayList<String[]> totalGoodsIDs;
// 過程中計算出來的所有頻繁項集列表
private ArrayList<FrequentItem> resultItem;
// 過程中計算出來頻繁項集的ID集合
private ArrayList<String[]> resultItemID;
public AprioriTool(String filePath, int minSupportCount) {
this.filePath = filePath;
this.minSupportCount = minSupportCount;
readDataFile();
}
/**
* 從檔案中讀取資料
*/
private void readDataFile() {
File file = new File(filePath);
ArrayList<String[]> dataArray = new ArrayList<String[]>();
try {
BufferedReader in = new BufferedReader(new FileReader(file));
String str;
String[] tempArray;
while ((str = in.readLine()) != null) {
tempArray = str.split(" ");
dataArray.add(tempArray);
}
in.close();
} catch (IOException e) {
e.getStackTrace();
}
String[] temp = null;
totalGoodsIDs = new ArrayList<>();
for (String[] array : dataArray) {
temp = new String[array.length - 1];
System.arraycopy(array, 1, temp, 0, array.length - 1);
// 將事務ID加入列表吧中
totalGoodsIDs.add(temp);
}
}
/**
* 判讀字元陣列array2是否包含於陣列array1中
*
* @param array1
* @param array2
* @return
*/
public boolean iSStrContain(String[] array1, String[] array2) {
if (array1 == null || array2 == null) {
return false;
}
boolean iSContain = false;
for (String s : array2) {
// 新的字母比較時,重新初始化變數
iSContain = false;
// 判讀array2中每個字元,只要包括在array1中 ,就算包含
for (String s2 : array1) {
if (s.equals(s2)) {
iSContain = true;
break;
}
}
// 如果已經判斷出不包含了,則直接中斷迴圈
if (!iSContain) {
break;
}
}
return iSContain;
}
/**
* 項集進行連線運算
*/
private void computeLink() {
// 連線計算的終止數,k項集必須算到k-1子項集為止
int endNum = 0;
// 當前已經進行連線運算到幾項集,開始時就是1項集
int currentNum = 1;
// 商品,1頻繁項集對映圖
HashMap<String, FrequentItem> itemMap = new HashMap<>();
FrequentItem tempItem;
// 初始列表
ArrayList<FrequentItem> list = new ArrayList<>();
// 經過連線運算後產生的結果項集
resultItem = new ArrayList<>();
resultItemID = new ArrayList<>();
// 商品ID的種類
ArrayList<String> idType = new ArrayList<>();
for (String[] a : totalGoodsIDs) {
for (String s : a) {
if (!idType.contains(s)) {
tempItem = new FrequentItem(new String[] { s }, 1);
idType.add(s);
resultItemID.add(new String[] { s });
} else {
// 支援度計數加1
tempItem = itemMap.get(s);
tempItem.setCount(tempItem.getCount() + 1);
}
itemMap.put(s, tempItem);
}
}
// 將初始頻繁項集轉入到列表中,以便繼續做連線運算
for (Map.Entry entry : itemMap.entrySet()) {
list.add((FrequentItem) entry.getValue());
}
// 按照商品ID進行排序,否則連線計算結果將會不一致,將會減少
Collections.sort(list);
resultItem.addAll(list);
String[] array1;
String[] array2;
String[] resultArray;
ArrayList<String> tempIds;
ArrayList<String[]> resultContainer;
// 總共要算到endNum項集
endNum = list.size() - 1;
while (currentNum < endNum) {
resultContainer = new ArrayList<>();
for (int i = 0; i < list.size() - 1; i++) {
tempItem = list.get(i);
array1 = tempItem.getIdArray();
for (int j = i + 1; j < list.size(); j++) {
tempIds = new ArrayList<>();
array2 = list.get(j).getIdArray();
for (int k = 0; k < array1.length; k++) {
// 如果對應位置上的值相等的時候,只取其中一個值,做了一個連線刪除操作
if (array1[k].equals(array2[k])) {
tempIds.add(array1[k]);
} else {
tempIds.add(array1[k]);
tempIds.add(array2[k]);
}
}
resultArray = new String[tempIds.size()];
tempIds.toArray(resultArray);
boolean isContain = false;
// 過濾不符合條件的的ID陣列,包括重複的和長度不符合要求的
if (resultArray.length == (array1.length + 1)) {
isContain = isIDArrayContains(resultContainer,
resultArray);
if (!isContain) {
resultContainer.add(resultArray);
}
}
}
}
// 做頻繁項集的剪枝處理,必須保證新的頻繁項集的子項集也必須是頻繁項集
list = cutItem(resultContainer);
currentNum++;
}
// 輸出頻繁項集
for (int k = 1; k <= currentNum; k++) {
System.out.println("頻繁" + k + "項集:");
for (FrequentItem i : resultItem) {
if (i.getLength() == k) {
System.out.print("{");
for (String t : i.getIdArray()) {
System.out.print(t + ",");
}
System.out.print("},");
}
}
System.out.println();
}
}
/**
* 判斷列表結果中是否已經包含此陣列
*
* @param container
* ID陣列容器
* @param array
* 待比較陣列
* @return
*/
private boolean isIDArrayContains(ArrayList<String[]> container,
String[] array) {
boolean isContain = true;
if (container.size() == 0) {
isContain = false;
return isContain;
}
for (String[] s : container) {
// 比較的視乎必須保證長度一樣
if (s.length != array.length) {
continue;
}
isContain = true;
for (int i = 0; i < s.length; i++) {
// 只要有一個id不等,就算不相等
if (s[i] != array[i]) {
isContain = false;
break;
}
}
// 如果已經判斷是包含在容器中時,直接退出
if (isContain) {
break;
}
}
return isContain;
}
/**
* 對頻繁項集做剪枝步驟,必須保證新的頻繁項集的子項集也必須是頻繁項集
*/
private ArrayList<FrequentItem> cutItem(ArrayList<String[]> resultIds) {
String[] temp;
// 忽略的索引位置,以此構建子集
int igNoreIndex = 0;
FrequentItem tempItem;
// 剪枝生成新的頻繁項集
ArrayList<FrequentItem> newItem = new ArrayList<>();
// 不符合要求的id
ArrayList<String[]> deleteIdArray = new ArrayList<>();
// 子項集是否也為頻繁子項集
boolean isContain = true;
for (String[] array : resultIds) {
// 列舉出其中的一個個的子項集,判斷存在於頻繁項集列表中
temp = new String[array.length - 1];
for (igNoreIndex = 0; igNoreIndex < array.length; igNoreIndex++) {
isContain = true;
for (int j = 0, k = 0; j < array.length; j++) {
if (j != igNoreIndex) {
temp[k] = array[j];
k++;
}
}
if (!isIDArrayContains(resultItemID, temp)) {
isContain = false;
break;
}
}
if (!isContain) {
deleteIdArray.add(array);
}
}
// 移除不符合條件的ID組合
resultIds.removeAll(deleteIdArray);
// 移除支援度計數不夠的id集合
int tempCount = 0;
for (String[] array : resultIds) {
tempCount = 0;
for (String[] array2 : totalGoodsIDs) {
if (isStrArrayContain(array2, array)) {
tempCount++;
}
}
// 如果支援度計數大於等於最小最小支援度計數則生成新的頻繁項集,並加入結果集中
if (tempCount >= minSupportCount) {
tempItem = new FrequentItem(array, tempCount);
newItem.add(tempItem);
resultItemID.add(array);
resultItem.add(tempItem);
}
}
return newItem;
}
/**
* 陣列array2是否包含於array1中,不需要完全一樣
*
* @param array1
* @param array2
* @return
*/
private boolean isStrArrayContain(String[] array1, String[] array2) {
boolean isContain = true;
for (String s2 : array2) {
isContain = false;
for (String s1 : array1) {
// 只要s2字元存在於array1中,這個字元就算包含在array1中
if (s2.equals(s1)) {
isContain = true;
break;
}
}
// 一旦發現不包含的字元,則array2陣列不包含於array1中
if (!isContain) {
break;
}
}
return isContain;
}
/**
* 根據產生的頻繁項集輸出關聯規則
*
* @param minConf
* 最小置信度閾值
*/
public void printAttachRule(double minConf) {
// 進行連線和剪枝操作
computeLink();
int count1 = 0;
int count2 = 0;
ArrayList<String> childGroup1;
ArrayList<String> childGroup2;
String[] group1;
String[] group2;
// 以最後一個頻繁項集做關聯規則的輸出
String[] array = resultItem.get(resultItem.size() - 1).getIdArray();
// 子集總數,計算的時候除去自身和空集
int totalNum = (int) Math.pow(2, array.length);
String[] temp;
// 二進位制陣列,用來代表各個子集
int[] binaryArray;
// 除去頭和尾部
for (int i = 1; i < totalNum - 1; i++) {
binaryArray = new int[array.length];
numToBinaryArray(binaryArray, i);
childGroup1 = new ArrayList<>();
childGroup2 = new ArrayList<>();
count1 = 0;
count2 = 0;
// 按照二進位制位關係取出子集
for (int j = 0; j < binaryArray.length; j++) {
if (binaryArray[j] == 1) {
childGroup1.add(array[j]);
} else {
childGroup2.add(array[j]);
}
}
group1 = new String[childGroup1.size()];
group2 = new String[childGroup2.size()];
childGroup1.toArray(group1);
childGroup2.toArray(group2);
for (String[] a : totalGoodsIDs) {
if (isStrArrayContain(a, group1)) {
count1++;
// 在group1的條件下,統計group2的事件發生次數
if (isStrArrayContain(a, group2)) {
count2++;
}
}
}
// {A}-->{B}的意思為在A的情況下發生B的概率
System.out.print("{");
for (String s : group1) {
System.out.print(s + ", ");
}
System.out.print("}-->");
System.out.print("{");
for (String s : group2) {
System.out.print(s + ", ");
}
System.out.print(MessageFormat.format(
"},confidence(置信度):{0}/{1}={2}", count2, count1, count2
* 1.0 / count1));
if (count2 * 1.0 / count1 < minConf) {
// 不符合要求,不是強規則
System.out.println("由於此規則置信度未達到最小置信度的要求,不是強規則");
} else {
System.out.println("為強規則");
}
}
}
/**
* 數字轉為二進位制形式
*
* @param binaryArray
* 轉化後的二進位制陣列形式
* @param num
* 待轉化數字
*/
private void numToBinaryArray(int[] binaryArray, int num) {
int index = 0;
while (num != 0) {
binaryArray[index] = num % 2;
index++;
num /= 2;
}
}
}
呼叫類:
/**
* apriori關聯規則挖掘演算法呼叫類
* @author lyq
*
*/
public class Client {
public static void main(String[] args){
String filePath = "C:\\Users\\lyq\\Desktop\\icon\\testInput.txt";
AprioriTool tool = new AprioriTool(filePath, 2);
tool.printAttachRule(0.7);
}
}
輸出的結果:
頻繁1項集:
{1,},{2,},{3,},{4,},{5,},
頻繁2項集:
{1,2,},{1,3,},{1,5,},{2,3,},{2,4,},{2,5,},
頻繁3項集:
{1,2,3,},{1,2,5,},
頻繁4項集:
{1, }-->{2, 5, },confidence(置信度):2/6=0.333由於此規則置信度未達到最小置信度的要求,不是強規則
{2, }-->{1, 5, },confidence(置信度):2/7=0.286由於此規則置信度未達到最小置信度的要求,不是強規則
{1, 2, }-->{5, },confidence(置信度):2/4=0.5由於此規則置信度未達到最小置信度的要求,不是強規則
{5, }-->{1, 2, },confidence(置信度):2/2=1為強規則
{1, 5, }-->{2, },confidence(置信度):2/2=1為強規則
{2, 5, }-->{1, },confidence(置信度):2/2=1為強規則
程式演算法的問題和技巧
在實現Apiori演算法的時候,碰到的一些問題和待優化的點特別要提一下:
1、首先程式的執行效率不高,裡面有大量的for巢狀迴圈疊加上迴圈,當然這有本身演算法的原因(連線運算所致)還有我的各個的方法選擇,很多一部分用來比較字串陣列。
2、這個是我覺得會是程式的一個漏洞,當生成的候選項集加入resultItemId時,會出現{1, 2, 3}和{3, 2, 1}會被當成不同的侯選集,未做順序的判斷。
3、程式的除錯過程中由於未按照從小到大的排序,導致,生成的候選集與真實值不一致的情況,所以這裡必須在頻繁1項集的時候就應該是有序的。
4、在輸出關聯規則的時候,用到了數字轉二進位制陣列的形式,輸出他的各個非空子集,然後最出關聯規則的判斷。
Apriori演算法的缺點
此演算法的的應用非常廣泛,但是他在運算的過程中會產生大量的侯選集,而且在匹配的時候要進行整個資料庫的掃描,因為要做支援度計數的統計操作,在小規模的資料上操作還不會有大問題,如果是大型的資料庫上呢,他的效率還是有待提高的。