FP_growth算法是韩家炜老师在2000年提出的关联分析算法,该算法和Apriori算法最大的不同有两点:
第一,不产生候选集,第二,只需要两次遍历数据库,大大提高了效率,用31646条测试记录,最小支持度是2%,
用Apriori算法要半个小时但是用FP_growth算法只要6分钟就可以了,效率非常明显。
它的核心是FP_tree,一种树型数据结构,特点是尽量把相同元素用一个节点表示,这样就大大减少了空间,和birch算法有类似的思想。还是以如下数据为例。
每一行表示一条交易,共有9行,既9笔交易,左边表示交易ID,右边表示商品名称。最小支持度是22%,那么每件商品至少要出现9*22%=2次才算频繁。第一次扫描数据库,统计每件商品出现的次数,按次数对各个商品递减排序,有:。然后第二次扫描数据库,在每条交易中按此种顺序给商品排序,如果有某个商品出现的次数小于阈值2,则删除该商品,有:
剩下的就是构造FP_tree了,这是核心,树的每个节点的结构体如下:
//FP-tree的存储结构
typedef struct CSNode{
//商品编号
int item;
//次数
int count;
//父节点,孩子节点,兄弟节点
CSNode *parent,*firstchild,*nextsibling;
//相同商品的前驱,后继节点,方便将相同商品的节点连接起来,根节点的直接孩子节点的这两个指针都是空
CSNode *pre,*next;
}*CSTree;
其中item,*firstchild,*nextsibling是树这个结构体常用的属性。count记录商品item出现的次数,*parent是为了方便从叶子节点逆向访问根节点而设置的。*pre,*next的注释已经很清楚了。构造树的原则是:将每条记录看做一个从根节点到叶子节点的路径,如果某个商品在节点中已经存在了,则对应count计数器加1,相当于所有的前缀都要加1,如果不存在则在该条记录的后面商品开辟一条新的路径。下面一条一条记录演示怎么构造FP_tree。
第三次访问数据库,构造FP_tree。第一条记录:I2,I1,I5,有:
父节点没有表示出来,根节点是空节点。2:1表示商品2出现了1次,其他表示类推。左边的数组按照商品顺序递减排列,保存了各个商品的当前指针,目的是为了在后面找到相同的后缀,将相同的商品用单项箭头虚线连起来,实际是双向链表链接的,并且将此时的节点商品1和节点商品5保存为商品1和商品5的当前指针,而对于商品2,商品3,商品4的当前指针还在左边的数组中保存。注意根节点的直接孩子不用连起来,后面会讲理由。第二条记录:I2,I4,有:
该记录和第一条记录共用前缀I2,所以商品2的次数要加1,而商品4则作为商品2的一个新孩子节点,这里没有把兄弟节点画出来。并且左边商品4要指向该节点,此时商品4的当前指针指向节点商品4。第三条记录:I2,I3,类似,结果是:
当添加完商品4后,商品4的当前指针要指向新的节点商品4,此时两条红色的虚线就把以商品4为后缀的节点连起来了。第5条记录:I1,I3,有:
商品1由于和根节点的所有直接子孩子(这里只有商品2这个子孩子)不同,因此要另外开辟一条路径。商品3的当前指针要指向新的节点商品3,如图中的黄色虚线所指,到这里体现了构造FP_tree的一般性了。再把剩下的记录都加进来,最终的FP_tree是:
这颗FP_tree最大程度的把相同的商品放在用同一个节点保存,最大限度的节省了空间。剩下的工作就是挖掘这颗FP_tree了。
挖掘的目的是找出FP_tree的各个路径中相同的集合,有两中方式,方式一,从根节点朝叶子节点顺着遍历树,方式二,从叶子节点朝根节点逆着遍历树。想想方式一挺麻烦的,幸亏我们设置了*parent指针,通过它就可以很方便的用方式二。我们从商品出现次数由少到多的顺序开始遍历树,先从商品5开始,由于有*pre,*next指针分方便将所有以商品5做为叶子节点的路径全找出来,然后再根据*parent指针找到父节点,根节点是空不用找。以I5做元素的条件模式基是:{(I2 I1:1),(I2 I1 I3:1)}。后面的1表示出现商品I2,I1,I5同时出现的次数。现在解释为什么:根节点的直接孩子不用*pre,*next指针连起来,因为假如连起来的话,那么以它为后缀时,将没有前缀,也就是说它的频繁项集是1,这在大多数情况下没意义。由它构造出条件FP_tree,注意由于开始按照商品名称排序了,那么条件模式基中的每一项也会按照这种方式排序。如果条件模式基中某项A是另外一项B的子集那么在算B时,要将A出现的次数加上,实现这个功能最简单明了的方法就是一一匹配,假如条件模式基共有N项,则时间复杂度是N的平方,若先按照条件模式基的长度递增排序得到:{(I2 I1:1),(I2 I1 I3:1)},排序的时间复杂度是N*log(N),那么只有可能是长度短的项是长度长的项的子集,此时总匹配次数是:N-1 + N-2 + ,,, + 1 = N*(N-1)/2,和前面的排序时间加起来是:N*log(N) + N*(N-1)/2当N大于时4时,该值小于N的平方。在实际中N一般会大于4。最终我们得到以I5作为后缀的频繁项集是:{I2 I5:2},{I1 I5:2},{I2 I1 I5:2}他们出现的次数都大于等于最小支持度。类似可以得到其它后缀的频繁项集。
FP_growth算法不产生候选序列,并且只需要3次遍历数据库,对比Apriori算法而言有了很大的改进。其实想想这也符合历史发展的规律,Apriori在1993年才提出来的,那是数据挖掘才刚起步,而到2000年时,已经有了一定的发展,FP_growth是站在Apriori的肩膀上发明的,这种现象具有普遍性。
FP—growth代码实现部分
主程序部分

1 package DataMining_FPTree; 2 /** 3 * FPTree频繁模式树算法 4 * 一个使用的这个算法的用例是输入一个单词或者单词的一部分,搜索引擎就会自动 补全查询词项,通过查看互联网上的用词来找出经常在一块出现的词对(使用Aporior算法也是找出经常出现的词对,这两种方法都是无监督学习),这需要一种发现频繁集的方法 5 * @author clj 6 * 7 */ 8 public class Client { 9 public static void main(String[] args){ 10 //这里使用的是输入文件的绝对路径 11 String filePath="E:\code\data mining\DataMining_FPTree\src\DataMining_FPTree\testInput.txt"; 12 //最小支持度阈值 13 int minSupportCount = 2; 14 //构造函数 15 FPTreeTool tool = new FPTreeTool(filePath, minSupportCount); 16 //调用构建树 17 tool.startBuildingTree(); 18 } 19 }
树节点的数据结构

1 package DataMining_FPTree; 2 3 import java.util.ArrayList; 4 5 /** 6 * FP树节点 7 * 这里使用Comparable的原因是因为每个项集要进行排序 8 * 按照节点的count来排序 9 * @author clj 10 * 11 */ 12 public class TreeNode implements Comparable<TreeNode>, Cloneable{ 13 // 节点类别名称 14 private String name; 15 // 计数数量 16 private Integer count; 17 // 父亲节点,这个节点的用法是根据给定叶子节点上溯到整棵树,这时就需要指向父节点 18 private TreeNode parentNode; 19 // 孩子节点,可以为多个 20 private ArrayList<TreeNode> childNodes; 21 22 public TreeNode(String name, int count){ 23 this.name = name; 24 this.count = count; 25 } 26 27 public String getName() { 28 return name; 29 } 30 31 public void setName(String name) { 32 this.name = name; 33 } 34 35 public Integer getCount() { 36 return count; 37 } 38 39 public void setCount(Integer count) { 40 this.count = count; 41 } 42 43 public TreeNode getParentNode() { 44 return parentNode; 45 } 46 47 public void setParentNode(TreeNode parentNode) { 48 this.parentNode = parentNode; 49 } 50 51 public ArrayList<TreeNode> getChildNodes() {//孩子节点可能不止一个,所以需要用list来保存 52 return childNodes; 53 } 54 55 public void setChildNodes(ArrayList<TreeNode> childNodes) { 56 this.childNodes = childNodes; 57 } 58 59 @Override 60 public int compareTo(TreeNode o) { 61 // TODO Auto-generated method stub 62 return o.getCount().compareTo(this.getCount()); 63 } 64 65 @Override 66 protected Object clone() throws CloneNotSupportedException {//如果想重写父类的方法,比如toString()方法的话,在方法前面加上@Override 系统可以帮你检查方法的正确性, 67 // TODO Auto-generated method stub 68 //因为对象内部有引用,需要采用深拷贝,这里就相当于是一个深度优先搜索,这里的clone相当于没有用 69 //System.out.println("The name="+this.getName()); 70 71 TreeNode node = (TreeNode)super.clone(); 72 if(this.getParentNode() != null){ 73 node.setParentNode((TreeNode) this.getParentNode().clone()); 74 } 75 76 if(this.getChildNodes() != null){ 77 node.setChildNodes((ArrayList<TreeNode>) this.getChildNodes().clone()); 78 } 79 80 return node; 81 } 82 83 }
程序的主要部分FPTreeTool

1 package DataMining_FPTree; 2 3 import java.io.BufferedReader; 4 import java.io.File; 5 import java.io.FileReader; 6 import java.io.IOException; 7 import java.util.ArrayList; 8 import java.util.Collections; 9 import java.util.HashMap; 10 import java.util.Iterator; 11 import java.util.Map; 12 import java.util.Map.Entry; 13 14 /** 15 * FPTree算法工具类 16 * 与Apriori算法不同的是FP树需要将非频繁项移除并且重排序 17 * @author clj 18 * 19 */ 20 public class FPTreeTool { 21 // 输入数据文件位置 22 private String filePath; 23 // 最小支持度阈值 24 private int minSupportCount; 25 // 所有事物ID记录 26 private ArrayList<String[]> totalGoodsID; 27 // 各个ID的统计数目映射表项,计数用于排序使用,用于项集 28 private HashMap<String, Integer> itemCountMap; 29 //后面的成员方法中并没有重新定义成员变量,所以成员函数中可以改变的成员变量的值 30 31 public FPTreeTool(String filePath, int minSupportCount) { 32 this.filePath = filePath; 33 this.minSupportCount = minSupportCount; 34 readDataFile(); 35 } 36 37 /** 38 * 从文件中读取数据,至此还没有对数据进行排序 39 */ 40 private void readDataFile() { 41 File file = new File(filePath); 42 ArrayList<String[]> dataArray = new ArrayList<String[]>(); 43 44 try { 45 BufferedReader in = new BufferedReader(new FileReader(file));//这一句话相当于新建了两个对象 46 String str; 47 String[] tempArray; 48 while ((str = in.readLine()) != null) { 49 tempArray = str.split(" "); 50 dataArray.add(tempArray); 51 } 52 in.close(); 53 } catch (IOException e) { 54 e.getStackTrace(); 55 } 56 57 String[] temp; 58 int count = 0; 59 itemCountMap = new HashMap<>();//之所以使用会使用hashMap的形式是因为后面会更改key所对应的value的值,时间复杂度小 60 totalGoodsID = new ArrayList<>();//totalGoodsId只需要将其保存在矩阵中 61 for (String[] a : dataArray) { 62 temp = new String[a.length - 1]; 63 System.arraycopy(a, 1, temp, 0, a.length - 1);//和Apriori算法一样第一个保存的是第几笔记录 64 totalGoodsID.add(temp); 65 for (String s : temp) { 66 if (!itemCountMap.containsKey(s)) { 67 count = 1; 68 } else { 69 count = ((int) itemCountMap.get(s)); 70 // 支持度计数加1 71 count++; 72 } 73 // 更新表项,如果有key s,则直接更新,否则创建 74 itemCountMap.put(s, count); 75 } 76 } 77 System.out.println("name="+itemCountMap.keySet()+" count="+itemCountMap.values()); 78 79 } 80 81 /** 82 * 根据事务记录构造FP树 83 * 当suffixPattern不为空的时候,建立的就是条件FP树 84 * surffixPatter是后缀模式 85 */ 86 private void buildFPTree(ArrayList<String> suffixPattern, 87 ArrayList<ArrayList<TreeNode>> transctionList) { 88 89 90 // 设置一个空根节点 91 TreeNode rootNode = new TreeNode(null, 0); 92 int count = 0; 93 // 节点是否存在 94 boolean isExist = false; 95 ArrayList<TreeNode> childNodes; 96 ArrayList<TreeNode> pathList; 97 // 相同类型节点链表,用于构造的新的FP树 98 HashMap<String, ArrayList<TreeNode>> linkedNode = new HashMap<>();//每个节点的LinkNode 99 HashMap<String, Integer> countNode = new HashMap<>(); 100 // 根据事务记录,一步步构建FP树,逐个读入事务记录,并把每个事务映射到FP树中的一条路径中 101 for (ArrayList<TreeNode> array : transctionList) { 102 TreeNode searchedNode;//TreeNode节点中每个项集中应该是只有一个元素 103 pathList = new ArrayList<>();//在构建的时候,将读入的每个项集添加到一条已经存在的路径中 104 /* 105 System.out.print("array="); 106 for(int i=0;i<array.size();i++) 107 System.out.print(" "+array.get(i).getName()); 108 System.out.println(); 109 */ 110 for (TreeNode node : array) {//array保存的是FP中的一条路径 111 pathList.add(node);//pathList开始为空,在事务中读到一个节点就把它放到pathList中 112 //System.out.println("正在处理的节点node="+node.getName()+" count="+node.getCount()); 113 //System.out.println("before keySets="+countNode.keySet()+"count="+countNode.values()); 114 nodeCounted(node, countNode);//countNode是一个HashMap类型,初始时为一个空的HashMap,在读事务过程中依次进行修改 115 //System.out.println("after keySets="+countNode.keySet()+"count="+countNode.values()); 116 /*System.out.print("pathList="); 117 for(int i=0;i<pathList.size();i++) 118 System.out.print(" "+pathList.get(i).getName()); 119 System.out.println(); 120 */ 121 searchedNode = searchNode(rootNode, pathList);//这里只是查找,不会影响count的变化 122 childNodes = searchedNode.getChildNodes(); 123 124 if (childNodes == null) {//如果正好找到路径中的结尾,则直接加入到结尾 125 //System.out.println("找到了对应的叶节点,在叶节点下存储"); 126 childNodes = new ArrayList<>(); 127 childNodes.add(node); 128 searchedNode.setChildNodes(childNodes); 129 node.setParentNode(searchedNode); 130 nodeAddToLinkedList(node, linkedNode); 131 } else { 132 isExist = false; 133 for (TreeNode node2 : childNodes) { 134 // 如果找到名称相同,则更新支持度计数 135 //System.out.println("##############"); 136 if (node.getName().equals(node2.getName())) { 137 //System.out.println("在父节点下找到了对应的节点"); 138 count = node2.getCount() + node.getCount(); 139 node2.setCount(count); 140 // 标识已找到节点位置 141 isExist = true; 142 break; 143 } 144 } 145 146 if (!isExist) { 147 // 如果没有找到,需添加子节点 148 //System.out.println("&没有在父节点下找到了对应的节点"); 149 childNodes.add(node); 150 node.setParentNode(searchedNode); 151 nodeAddToLinkedList(node, linkedNode); 152 } 153 } 154 //System.out.println("countNode.key="+countNode.keySet()+" value="+countNode.values()); 155 156 /*Iterator<Entry<String, ArrayList<TreeNode>>> it = linkedNode.entrySet().iterator(); 157 while( it.hasNext()) 158 { 159 Map.Entry<String, ArrayList<TreeNode>> entry = it.next(); 160 String key = entry.getKey(); 161 ArrayList<TreeNode> values=(ArrayList<TreeNode>)entry.getValue(); 162 for(TreeNode value:values) 163 { 164 System.out.print(" linkedNode.name="+value.getName()+" LinkedNode.count="+value.getCount()); 165 } 166 System.out.println(); 167 //TreeNode tempNode= entry.getValue().get(i); 168 }*/ 169 170 171 } 172 } 173 174 // 如果FP树已经是单条路径,则输出此时的频繁模式 175 if(suffixPattern!=null) 176 { 177 System.out.println("suffixPattern.size="+suffixPattern.size()); 178 for(int i=0;i<suffixPattern.size();i++) 179 System.out.print(suffixPattern.get(i)+" "); 180 System.out.println(); 181 } 182 else 183 System.out.println("suffixPattern.size=0"); 184 if (isSinglePath(rootNode)) { 185 System.out.println("issinglePath-------"); 186 printFrequentPattern(suffixPattern, rootNode); 187 188 } else { 189 ArrayList<ArrayList<TreeNode>> tList; 190 ArrayList<String> sPattern; 191 if (suffixPattern == null) { 192 sPattern = new ArrayList<>(); 193 } else { 194 // 进行一个拷贝,避免互相引用的影响 195 sPattern = (ArrayList<String>) suffixPattern.clone(); 196 } 197 198 // 利用节点链表构造新的事务 199 for (Map.Entry entry : countNode.entrySet()) { 200 // 添加到后缀模式中 201 sPattern.add((String) entry.getKey()); 202 System.out.println("entry.key="+entry.getKey()+" entry.value="+entry.getValue()); 203 204 //获取到了条件模式机,作为新的事务 205 tList = getTransactionList((String) entry.getKey(), linkedNode); 206 207 System.out.print("[后缀模式]:{"); 208 for(String s: sPattern){ 209 System.out.print(s + ", "); 210 } 211 System.out.print("}, 此时的条件模式基:"); 212 for(ArrayList<TreeNode> tnList: tList){ 213 System.out.print("{"); 214 for(TreeNode n: tnList){ 215 System.out.print(n.getName() + ", "); 216 } 217 System.out.print("}, "); 218 } 219 System.out.println(); 220 // 递归构造FP树 221 buildFPTree(sPattern, tList); 222 // 再次移除此项,构造不同的后缀模式,防止对后面造成干扰 223 sPattern.remove((String) entry.getKey()); 224 } 225 } 226 } 227 228 /** 229 * 将节点加入到同类型节点的链表中 230 * 231 * @param node 232 * 待加入节点 233 * @param linkedList 234 * 链表图 235 */ 236 private void nodeAddToLinkedList(TreeNode node, 237 HashMap<String, ArrayList<TreeNode>> linkedList) { 238 String name = node.getName(); 239 ArrayList<TreeNode> list; 240 241 if (linkedList.containsKey(name)) { 242 list = linkedList.get(name); 243 // 将node添加到此队列末尾 244 list.add(node); 245 } else { 246 list = new ArrayList<>(); 247 list.add(node); 248 linkedList.put(name, list); 249 } 250 } 251 252 /** 253 * 根据链表构造出新的事务,根据name,得到以name为尾的各记录 254 * 255 * @param name 256 * 节点名称 257 * @param linkedList 258 * 链表 259 * @return 260 */ 261 private ArrayList<ArrayList<TreeNode>> getTransactionList(String name, 262 HashMap<String, ArrayList<TreeNode>> linkedList) { 263 ArrayList<ArrayList<TreeNode>> tList = new ArrayList<>(); 264 ArrayList<TreeNode> targetNode = linkedList.get(name); 265 ArrayList<TreeNode> singleTansaction; 266 TreeNode temp; 267 System.out.println("#getTransaction中name="+name); 268 for (TreeNode node : targetNode) { 269 singleTansaction = new ArrayList<>(); 270 271 temp = node; 272 while (temp.getParentNode().getName() != null) { 273 System.out.println("temp.name="+temp.getName()+" count="+temp.getCount()); 274 temp = temp.getParentNode(); 275 276 singleTansaction.add(new TreeNode(temp.getName(), 1)); 277 } 278 System.out.println("temp.name="+temp.getName()+" count="+temp.getCount()); 279 System.out.println("singleTansaction="); 280 for(int i=0;i<singleTansaction.size();i++) 281 { 282 System.out.println("("+singleTansaction.get(i).getName()+","+singleTansaction.get(i).getCount()+")"); 283 } 284 System.out.println(); 285 // 按照支持度计数得反转一下 286 Collections.reverse(singleTansaction); 287 288 289 for (TreeNode node2 : singleTansaction) { 290 // 支持度计数调成与模式后缀一样 291 node2.setCount(node.getCount()); 292 } 293 System.out.println("##singleTansaction="); 294 for(int i=0;i<singleTansaction.size();i++) 295 { 296 System.out.println("("+singleTansaction.get(i).getName()+","+singleTansaction.get(i).getCount()+")"); 297 } 298 System.out.println(); 299 300 if (singleTansaction.size() > 0) { 301 tList.add(singleTansaction); 302 } 303 } 304 305 return tList; 306 } 307 308 /** 309 * 节点计数 310 * 311 * @param node 312 * 待加入节点 313 * @param nodeCount 314 * 计数映射图 315 */ 316 private void nodeCounted(TreeNode node, HashMap<String, Integer> nodeCount) { 317 int count = 0; 318 String name = node.getName(); 319 320 if (nodeCount.containsKey(name)) { 321 count = nodeCount.get(name); 322 count++; 323 } else { 324 count = 1; 325 } 326 327 nodeCount.put(name, count); 328 } 329 330 /** 331 * 显示决策树 332 * 333 * @param node 334 * 待显示的节点 335 * @param blankNum 336 * 行空格符,用于显示树型结构 337 */ 338 private void showFPTree(TreeNode node, int blankNum) { 339 System.out.println("¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥显示FPTree"); 340 for (int i = 0; i < blankNum; i++) { 341 System.out.print(" "); 342 } 343 System.out.print("--"); 344 System.out.print("--"); 345 346 if (node.getChildNodes() == null) {//叶子节点 347 System.out.print("["); 348 System.out.print("I" + node.getName() + ":" + node.getCount()); 349 System.out.print("]"); 350 } else { 351 // 递归显示子节点 352 System.out.print("【" + node.getName() + "】"); 353 for (TreeNode childNode : node.getChildNodes()) { 354 showFPTree(childNode, 2 * blankNum); 355 } 356 } 357 358 } 359 360 /** 361 * 待插入节点的抵达位置节点,从根节点开始向下寻找待插入节点的位置,返回待插入节点的父节点 362 * 363 * @param root 364 * @param list 365 * @return 366 */ 367 private TreeNode searchNode(TreeNode node, ArrayList<TreeNode> list) { 368 ArrayList<TreeNode> pathList = new ArrayList<>(); 369 TreeNode tempNode = null; 370 TreeNode firstNode = list.get(0); 371 boolean isExist = false; 372 // 重新转一遍,避免出现同一引用 373 for (TreeNode node2 : list) { 374 pathList.add(node2); 375 } 376 //System.out.println("待插入的节点:name="+node.getName()+" count="+node.getCount()); 377 /*for(int i=0;i<list.size();i++) 378 System.out.print(" ("+list.get(i).getName()+","+list.get(i).getCount()+")"); 379 System.out.println();*/ 380 // 如果没有孩子节点,则直接返回,在此节点下添加子节点,查找已构建树中的叶子节点 381 if (node.getChildNodes() == null) { 382 //System.out.println("此节点为叶子节点,为返回的节点,node.name="+node.getName()+" count="+node.getCount()); 383 return node; 384 } 385 386 for (TreeNode n : node.getChildNodes()) { 387 if (n.getName().equals(firstNode.getName()) && list.size() == 1) {//list中只有一个元素,即路径中的第一个元素 388 tempNode = node; 389 isExist = true; 390 //System.out.println("第一个元素恰好为要查找的节点,且节点长度为1"); 391 break; 392 } else if (n.getName().equals(firstNode.getName())) { 393 // 还没有找到最后的位置,继续找,在查找的过程中时是正好匹配,从路径中消除 394 //System.out.println("#第一个元素恰好为要查找的节点,且节点长度不为1"); 395 pathList.remove(firstNode); 396 tempNode = searchNode(n, pathList);//使用递归的形式去查询子节点 397 //System.out.println("¥¥¥返回节点:tempNode.name="+tempNode.getName()+" count="+tempNode.getCount()); 398 return tempNode; 399 } 400 } 401 402 // 如果没有找到,则新添加到孩子节点中 403 if (!isExist) { 404 //System.out.println("没有找到"); 405 tempNode = node; 406 } 407 //System.out.println("@@@@返回节点:node.name="+tempNode.getName()+" count="+tempNode.getCount()); 408 return tempNode; 409 } 410 411 /** 412 * 判断目前构造的FP树是否是单条路径的 413 * 414 * @param rootNode 415 * 当前FP树的根节点 416 * @return 417 */ 418 private boolean isSinglePath(TreeNode rootNode) { 419 // 默认是单条路径 420 boolean isSinglePath = true; 421 ArrayList<TreeNode> childList; 422 TreeNode node; 423 node = rootNode; 424 //是使用循环而不是递归判断是否是单条路径 425 while (node.getChildNodes() != null) { 426 childList = node.getChildNodes(); 427 if (childList.size() == 1) { 428 node = childList.get(0); 429 } else { 430 isSinglePath = false; 431 break; 432 } 433 } 434 435 return isSinglePath; 436 } 437 438 /** 439 * 开始构建FP树 440 */ 441 public void startBuildingTree() { 442 ArrayList<TreeNode> singleTransaction;//单条事务 443 ArrayList<ArrayList<TreeNode>> transactionList = new ArrayList<>();//事务总链 444 TreeNode tempNode; 445 int count = 0; 446 447 for (String[] idArray : totalGoodsID) { 448 singleTransaction = new ArrayList<>(); 449 for (String id : idArray) { 450 count = itemCountMap.get(id); 451 tempNode = new TreeNode(id, count); 452 singleTransaction.add(tempNode); 453 } 454 455 // 根据支持度数的多少进行排序 456 Collections.sort(singleTransaction); 457 458 /*System.out.println("singleTansaction as following:"); 459 for(int i=0;i<singleTransaction.size();i++) 460 System.out.print("("+singleTransaction.get(i).getName()+","+singleTransaction.get(i).getCount()+")"); 461 System.out.println();*/ 462 463 for (TreeNode node : singleTransaction) { 464 // 支持度计数重新归为1,将事务路径节点的count设置为1 465 node.setCount(1); 466 } 467 /*System.out.println("singleTansaction"); 468 for(int i=0;i<singleTransaction.size();i++) 469 System.out.print("***("+singleTransaction.get(i).getName()+","+singleTransaction.get(i).getCount()+")"); 470 System.out.println();*/ 471 transactionList.add(singleTransaction); 472 } 473 for(int i=0;i<transactionList.size();i++) 474 { 475 ArrayList<TreeNode> singleTransaction1=new ArrayList<>(); 476 singleTransaction1=transactionList.get(i); 477 for(int j=0;j<singleTransaction1.size();j++) 478 { 479 System.out.print("("+singleTransaction1.get(j).getName()+","+singleTransaction1.get(j).getCount()+")"); 480 } 481 System.out.println(); 482 483 } 484 buildFPTree(null, transactionList); 485 } 486 487 /** 488 * 输出此单条路径下的频繁模式 489 * 490 * @param suffixPattern 491 * 后缀模式 492 * @param rootNode 493 * 单条路径FP树根节点 494 */ 495 private void printFrequentPattern(ArrayList<String> suffixPattern, 496 TreeNode rootNode) { 497 ArrayList<String> idArray = new ArrayList<>(); 498 TreeNode temp; 499 temp = rootNode; 500 // 用于输出组合模式 501 int length = 0; 502 int num = 0; 503 int[] binaryArray; 504 505 while (temp.getChildNodes() != null) { 506 temp = temp.getChildNodes().get(0); 507 508 // 筛选支持度系数大于最小阈值的值,P(A)>P(AB),若P(A)<阈值,则删除这个节点即不添加到里面 509 if (temp.getCount() >= minSupportCount) { 510 idArray.add(temp.getName()); 511 } 512 } 513 514 length = idArray.size(); 515 num = (int) Math.pow(2, length); 516 for (int i = 0; i < num; i++) { 517 binaryArray = new int[length]; 518 numToBinaryArray(binaryArray, i); 519 520 // 如果后缀模式只有1个,不能输出自身 521 if (suffixPattern.size() == 1 && i == 0) { 522 continue; 523 } 524 525 System.out.print("频繁模式:{【后缀模式:"); 526 // 先输出固有的后缀模式 527 if (suffixPattern.size() > 1 528 || (suffixPattern.size() == 1 && idArray.size() > 0)) { 529 for (String s : suffixPattern) { 530 System.out.print(s + ", "); 531 } 532 } 533 System.out.print("】"); 534 // 输出路径上的组合模式 535 for (int j = 0; j < length; j++) { 536 if (binaryArray[j] == 1) { 537 System.out.print(idArray.get(j) + ", "); 538 } 539 } 540 System.out.println("}"); 541 } 542 } 543 544 /** 545 * 数字转为二进制形式 546 * 547 * @param binaryArray 548 * 转化后的二进制数组形式 549 * @param num 550 * 待转化数字 551 */ 552 private void numToBinaryArray(int[] binaryArray, int num) { 553 int index = 0; 554 while (num != 0) { 555 binaryArray[index] = num % 2; 556 index++; 557 num /= 2; 558 } 559 } 560 561 }
readDataFile从文件中读取数据,
buildFPTree(ArrayList<String> suffixPattern,ArrayList<ArrayList<TreeNode>> transctionList)构建FP树(包括FP条件树),当suffixpatter不为空的时候构建的就是FP条件树,
nodeAddToLinkedList(TreeNode node,HashMap<String, ArrayList<TreeNode>> linkedList),和邻接表类似,某个Node在树中出现的位置保存在linkedList中
private ArrayList<ArrayList<TreeNode>> getTransactionList(String name,HashMap<String, ArrayList<TreeNode>> linkedList)得到还有name节点的交易记录
nodeCounted(TreeNode node, HashMap<String, Integer> nodeCount)因为最后交易记录是以节点的计数多少进行排序的,这一个记录node在所有记录中出现的次数,同一条事务其实是没有先后顺序的,为了把树尽可能的减小才这样进行排序的
showFPTree(TreeNode node, int blankNum) 展示树
private TreeNode searchNode(TreeNode node, ArrayList<TreeNode> list) 要插入的节点在树中应该插入到哪个节点的下面呢,这里返回的是待插入节点的父节点
printFrequentPattern(ArrayList<String> suffixPattern,TreeNode rootNode)输出单条路径下的频繁模式,
常见的频繁项集挖掘算法有两类,一类是Apriori算法,另一类是FPGrowth。Apriori通过不断的构造候选集、筛选候选集挖掘出频繁项集,需要多次扫描原始数据,当原始数据较大时,磁盘I/O次数太多,效率比较低下。FPGrowth算法则只需扫描原始数据两遍,通过FP-tree数据结构对原始数据进行压缩,效率较高。
也许有人会问?如果这个数据库足够大,以至于构造的FP树大到无法完全保存在内存中,这该如何是好.这的确是个问题. Han Jiawei在论文中也给出了一种思路,就是通过将原来的大的数据库分区成几个小的数据库(这种小的数据库称之为投射数据库),对这几个小的数据库分别进行FP Growth算法.
还是拿上面的例子来说事,我们把包含p的所有数据库记录都单独存成一个数据库,我们称之为p-投射数据库,类似的m,b,a,c,f我们都可以生成相应的投射数据库,这些投射数据库构成的FP树相对而言大小就小得多,完全可以放在内存里.
在现代数据挖掘任务中,数据量越来越大,因此并行化的需求越来越大,上面提出的问题也越来越迫切.下一篇博客,博主将分析一下,FP Growth如何在MapReduce的框架下并行化.
[1]Mining Frequent Patterns without Candidate Gen