1. 程式人生 > >Apriori演算法--關聯規則挖掘

Apriori演算法--關聯規則挖掘

介紹

Apriori演算法是一個經典的資料探勘演算法,Apriori的單詞的意思是"先驗的",說明這個演算法是具有先驗性質的,就是說要通過上一次的結果推匯出下一次的結果,這個如何體現將會在下面的分析中會慢慢的體現出來。Apriori演算法的用處是挖掘頻繁項集的,頻繁項集粗俗的理解就是找出經常出現的組合,然後根據這些組合最終推出我們的關聯規則。

Apriori演算法原理

Apriori演算法是一種逐層搜尋的迭代式演算法,其中k項集用於挖掘(k+1)項集,這是依靠他的先驗性質的:

頻繁項集的所有非空子集一定是也是頻繁的。

通過這個性質可以對候選集進行剪枝。用k項集如何生成(k+1)項集呢,這個是演算法裡面最難也是最核心的部分。

通過2個步驟

1、連線步,將頻繁項自己與自己進行連線運算。

2、剪枝步,去除候選集項中的不符合要求的候選項,不符合要求指的是這個候選項的子集並非都是頻繁項,要遵守上文提到的先驗性質。

3、通過1,2步驟還不夠,在後面還要根據支援度計數篩選掉不滿足最小支援度數的候選集。

演算法例項

首先是測試資料:

交易ID

商品ID列表

T100

I1I2I5

T200

I2I4

T300

I2I3

T400

I1I2I4

T500

I1I3

T600

I2I3

T700

I1I3

T800

I1I2I3I5

T900

I1I2I3

演算法的步驟圖:


最後我們可以看到頻繁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演算法的缺點

此演算法的的應用非常廣泛,但是他在運算的過程中會產生大量的侯選集,而且在匹配的時候要進行整個資料庫的掃描,因為要做支援度計數的統計操作,在小規模的資料上操作還不會有大問題,如果是大型的資料庫上呢,他的效率還是有待提高的。