Apriori算法--关联规则挖掘

来源:互联网 发布:数据结构设计 编辑:程序博客网 时间:2024/05/17 01:21

我的数据挖掘算法代码:https://github.com/linyiqun/DataMiningAlgorithm

介绍

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 5T2 2 4T3 2 3T4 1 2 4T5 1 3T6 2 3T7 1 3T8 1 2 3 5T9 1 2 3
频繁项类:

/** * 频繁项集 *  * @author lyq *  */public class FrequentItem implements Comparable<FrequentItem>{// 频繁项集的集合IDprivate 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;}@Overridepublic int compareTo(FrequentItem o) {// TODO Auto-generated method stubreturn 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;// 每个事务中的商品IDprivate 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 {// 支持度计数加1tempItem = 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<>();// 不符合要求的idArrayList<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算法的缺点

此算法的的应用非常广泛,但是他在运算的过程中会产生大量的侯选集,而且在匹配的时候要进行整个数据库的扫描,因为要做支持度计数的统计操作,在小规模的数据上操作还不会有大问题,如果是大型的数据库上呢,他的效率还是有待提高的。

6 1