@jfruan
        
        2017-04-14T00:50:58.000000Z
        字数 15958
        阅读 1824
    DM
在关联规则挖掘领域最经典的算法法是Apriori,其致命的缺点是需要多次扫描事务数据库。于是人们提出了各种裁剪(prune)数据集的方法以减少I/O开支,韩嘉炜老师的FP-Tree算法就是其中非常高效的一种。
举个例子,设事务数据库为:
A E F G
A F G
A B E F G
E F G
每一行为一个事务,事务由若干个互不相同的项目构成,任意几个项目的组合称为一个模式。 
上例中一共有4个事务。
模式{A,F,G}的支持数为3,支持度为3/4。支持数大于阈值minSuport的模式称为频繁模式(Frequent Patten)。
{F,G}的支持度数为4,支持度为4/4。
{A}的支持度数为3,支持度为3/4。
{F,G}=>{A}的置信度为:{A,F,G}的支持度数 除以 {F,G}的支持度数,即3/4
{A}=>{F,G}的置信度为:{A,F,G}的支持度数 除以 {A}的支持度数,即3/3
强关联规则挖掘是在满足一定支持度的情况下寻找置信度达到阈值的所有模式。
算法描述:
输入:事务集合 List<List<String>> transactions输出:频繁模式集合及相应的频数 Map<List<String>,Integer> FrequentPattens初始化 PostModel=[],CPB=transactionsvoid FPGrowth(List<List<String>> CPB,List<String> PostModel){if CPB为空:return统计CPB中每一个项目的计数,把计数小于最小支持数minSuport的删除掉,对于CPB中的每一条事务按项目计数降序排列。由CPB构建FP-Tree,FP-Tree中包含了表头项headers,每一个header都指向了一个链表HeaderLinkList,链表中的每个元素都是FP-Tree上的一个节点,且节点名称与header.name相同。for header in headers:newPostModel=header.name+PostModel把<newPostModel, header.count>加到FrequentPattens中。newCPB=[]for TreeNode in HeaderLinkList:得到从FP-Tree的根节点到TreeNode的全路径path,把path作为一个事务添加到newCPB中,要重复添加TreeNode.count次。FPGrowth(newCPB,newPostModel)
算法的核心是FPGrowth函数,这是一个递归函数。CPB的全称是Conditional Pattern Base(条件模式基),我们可以把CPB理解为算法在不同阶段的事务集合。PostModel称为后缀模式,它是一个List。后文会详细讲CPB和PostModel是如何生成的,初始时令PostModel为空,令CPB就是原始的事务集合。
下面我们举个例子来详细讲解FPGrowth函数的完整实现。
事务数据库如下,一行表示一条购物记录:
牛奶,鸡蛋,面包,薯片
鸡蛋,爆米花,薯片,啤酒
鸡蛋,面包,薯片
牛奶,鸡蛋,面包,爆米花,薯片,啤酒
牛奶,面包,啤酒
鸡蛋,面包,啤酒
牛奶,面包,薯片
牛奶,鸡蛋,面包,黄油,薯片
牛奶,鸡蛋,黄油,薯片
令minSuport=3,统计每一个项目出现的次数,把次数低于minSuport的项目删除掉,剩下的项目按出现的次数降序排列,得到F1: 
对于每一条事务,按照F1中的顺序重新排序,不在F1中的被删除掉。这样整个事务集合变为:
薯片,鸡蛋,面包,牛奶
薯片,鸡蛋,啤酒
薯片,鸡蛋,面包
薯片,鸡蛋,面包,牛奶,啤酒
面包,牛奶,啤酒
鸡蛋,面包,啤酒
薯片,面包,牛奶
薯片,鸡蛋,面包,牛奶
薯片,鸡蛋,牛奶
上面的事务集合即为当前的CPB,当前的PostModel依然为空。由CPB构建FP-Tree的步骤如下。 
插入第一条事务(薯片,鸡蛋,面包,牛奶)之后

插入第二条事务(薯片,鸡蛋,啤酒) 

插入第三条记录(面包,牛奶,啤酒) 

估计你也知道怎么插了,最终生成的FP-Tree是: 

上图中左边的那一叫做表头项,树中相同名称的节点要链接起来,链表的第一个元素就是表头项里的元素。不论是表头项节点还是FP-Tree中有节点,它们至少有2个属性:name和count。
现在我们已进行完算法描述的第10行。go on
遍历表头项中的每一项,我们拿“牛奶:6”为例。
新的PostModel为“表头项+老的PostModel”,现在由于老的PostModel还是空list,所以新的PostModel为:[牛奶]。新的PostModel就是一条频繁模式,它的支持数即为表头项的count:6,所以此处可以输出一条频繁模式<[牛奶], 6>
从表头项“牛奶”开始,找到FP-Tree中所有的“牛奶”节点,然后找到从树的根节点到“牛奶”节点的路径。得到4条路径:
薯片:7,鸡蛋:6,牛奶:1
薯片:7,鸡蛋:6,面包:4,牛奶:3
薯片:7,面包:1,牛奶:1
面包:1,牛奶:1
对于每一条路径上的节点,其count都设置为牛奶的count:
薯片:1,鸡蛋:1,牛奶:1
薯片:3,鸡蛋:3,面包:3,牛奶:3
薯片:1,面包:1,牛奶:1
面包:1,牛奶:1
因为每一项末尾都是牛奶,可以把牛奶去掉,得到新的CPB:
薯片:1,鸡蛋:1
薯片:3,鸡蛋:3,面包:3
薯片:1,面包:1
面包:1
然后递归调用FPGrowth(新的CPB,新的PostModel),当发现新有CPB为空时递归就可以退出了。

树上只有一条路径{A-B-C},在保证A-B-C这种顺序的前提下,这三个节点的所有组合是:A,B,C,AB,AC,BC,ABC。每一种组合与postModel拼接形成一条频繁模式,模式的支持数即为表头项的计数(单枝的情况下所有表头项和所有树节点的计数都是相同的)。
import java.util.List;public class StrongAssociationRule {public List<String> condition;public String result;public int support;public double confidence;}
TreeNode.java
import java.util.ArrayList;import java.util.List;/*** @Description: FP树的节点* @Author orisun* @Date Jun 23, 2016*/class TreeNode {/*** 节点名称**/private String name;/*** 频数**/private int count;private TreeNode parent;private List<TreeNode> children;/*** 下一个节点(由表头项维护的那个链表)**/private TreeNode nextHomonym;/*** 末节点(由表头项维护的那个链表)**/private TreeNode tail;@Overridepublic String toString() {return name;}public TreeNode() {}public TreeNode(String name) {this.name = name;}public String getName() {return this.name;}public void setName(String name) {this.name = name;}public int getCount() {return this.count;}public void setCount(int count) {this.count = count;}public TreeNode getParent() {return this.parent;}public void setParent(TreeNode parent) {this.parent = parent;}public List<TreeNode> getChildren() {return this.children;}public void addChild(TreeNode child) {if (getChildren() == null) {List<TreeNode> list = new ArrayList<TreeNode>();list.add(child);setChildren(list);} else {getChildren().add(child);}}public TreeNode findChild(String name) {List<TreeNode> children = getChildren();if (children != null) {for (TreeNode child : children) {if (child.getName().equals(name)) {return child;}}}return null;}public void setChildren(List<TreeNode> children) {this.children = children;}public void printChildrenName() {List<TreeNode> children = getChildren();if (children != null) {for (TreeNode child : children)System.out.print(child.getName() + " ");} elseSystem.out.print("null");}public TreeNode getNextHomonym() {return this.nextHomonym;}public void setNextHomonym(TreeNode nextHomonym) {this.nextHomonym = nextHomonym;}public void countIncrement(int n) {this.count += n;}public TreeNode getTail() {return tail;}public void setTail(TreeNode tail) {this.tail = tail;}}
FPTree.java
import java.io.BufferedReader;import java.io.BufferedWriter;import java.io.FileReader;import java.io.FileWriter;import java.io.IOException;import java.text.DecimalFormat;import java.util.ArrayList;import java.util.Collections;import java.util.Comparator;import java.util.HashMap;import java.util.HashSet;import java.util.LinkedList;import java.util.List;import java.util.Map;import java.util.Map.Entry;import java.util.Set;/*** @Description: FPTree强关联规则挖掘算法* @Author orisun* @Date Jun 23, 2016*/public class FPTree {/*** 频繁模式的最小支持数**/private int minSuport;/*** 关联规则的最小置信度**/private double confident;/*** 事务项的总数**/private int totalSize;/*** 存储每个频繁项及其对应的计数**/private Map<List<String>, Integer> frequentMap = new HashMap<List<String>, Integer>();/*** 关联规则中,哪些项可作为被推导的结果,默认情况下所有项都可以作为被推导的结果**/private Set<String> decideAttr = null;public int getMinSuport() {return this.minSuport;}/*** 设置最小支持数** @param minSuport*/public void setMinSuport(int minSuport) {this.minSuport = minSuport;}public double getConfident() {return confident;}/*** 设置最小置信度** @param confident*/public void setConfident(double confident) {this.confident = confident;}/*** 设置决策属性。如果要调用{@linkplain #readTransRocords(String[])},需要在调用{@code readTransRocords}* 之后再调用{@code setDecideAttr}** @param decideAttr*/public void setDecideAttr(Set<String> decideAttr) {this.decideAttr = decideAttr;}/*** 获取频繁项集** @return* @Description:*/public Map<List<String>, Integer> getFrequentItems() {return frequentMap;}public int getTotalSize() {return totalSize;}/*** 根据一条频繁模式得到若干关联规则** @param list* @return*/private List<StrongAssociationRule> getRules(List<String> list) {List<StrongAssociationRule> rect = new LinkedList<StrongAssociationRule>();if (list.size() > 1) {for (int i = 0; i < list.size(); i++) {String result = list.get(i);if (decideAttr.contains(result)) {List<String> condition = new ArrayList<String>();condition.addAll(list.subList(0, i));condition.addAll(list.subList(i + 1, list.size()));StrongAssociationRule rule = new StrongAssociationRule();rule.condition = condition;rule.result = result;rect.add(rule);}}}return rect;}/*** 从若干个文件中读入Transaction Record,同时把所有项设置为decideAttr** @param filenames* @return* @Description:*/public List<List<String>> readTransRocords(String[] filenames) {Set<String> set = new HashSet<String>();List<List<String>> transaction = null;if (filenames.length > 0) {transaction = new LinkedList<List<String>>();for (String filename : filenames) {try {FileReader fr = new FileReader(filename);BufferedReader br = new BufferedReader(fr);try {String line = null;// 一项事务占一行while ((line = br.readLine()) != null) {if (line.trim().length() > 0) {// 每个item之间用","分隔String[] str = line.split(",");//每一项事务中的重复项需要排重Set<String> record = new HashSet<String>();for (String w : str) {record.add(w);set.add(w);}List<String> rl = new ArrayList<String>();rl.addAll(record);transaction.add(rl);}}} finally {br.close();}} catch (IOException ex) {System.out.println("Read transaction records failed." + ex.getMessage());System.exit(1);}}}this.setDecideAttr(set);return transaction;}/*** 生成一个序列的各种子序列。(序列是有顺序的)** @param residualPath* @param results*/private void combine(LinkedList<TreeNode> residualPath, List<List<TreeNode>> results) {if (residualPath.size() > 0) {//如果residualPath太长,则会有太多的组合,内存会被耗尽的TreeNode head = residualPath.poll();List<List<TreeNode>> newResults = new ArrayList<List<TreeNode>>();for (List<TreeNode> list : results) {List<TreeNode> listCopy = new ArrayList<TreeNode>(list);newResults.add(listCopy);}for (List<TreeNode> newPath : newResults) {newPath.add(head);}results.addAll(newResults);List<TreeNode> list = new ArrayList<TreeNode>();list.add(head);results.add(list);combine(residualPath, results);}}private boolean isSingleBranch(TreeNode root) {boolean rect = true;while (root.getChildren() != null) {if (root.getChildren().size() > 1) {rect = false;break;}root = root.getChildren().get(0);}return rect;}/*** 计算事务集中每一项的频数** @param transRecords* @return*/private Map<String, Integer> getFrequency(List<List<String>> transRecords) {Map<String, Integer> rect = new HashMap<String, Integer>();for (List<String> record : transRecords) {for (String item : record) {Integer cnt = rect.get(item);if (cnt == null) {cnt = new Integer(0);}rect.put(item, ++cnt);}}return rect;}/*** 根据事务集合构建FPTree** @param transRecords* @Description:*/public void buildFPTree(List<List<String>> transRecords) {totalSize = transRecords.size();//计算每项的频数final Map<String, Integer> freqMap = getFrequency(transRecords);//先把频繁1项集添加到频繁模式中// for (Entry<String, Integer> entry : freqMap.entrySet()) {// String name = entry.getKey();// int cnt = entry.getValue();// if (cnt >= minSuport) {// List<String> rule = new ArrayList<String>();// rule.add(name);// frequentMap.put(rule, cnt);// }// }//每条事务中的项按F1排序for (List<String> transRecord : transRecords) {Collections.sort(transRecord, new Comparator<String>() {@Overridepublic int compare(String o1, String o2) {return freqMap.get(o2) - freqMap.get(o1);}});}FPGrowth(transRecords, null);}/*** FP树递归生长,从而得到所有的频繁模式** @param cpb 条件模式基* @param postModel 后缀模式*/private void FPGrowth(List<List<String>> cpb, LinkedList<String> postModel) {// System.out.println("CPB is");// for (List<String> records : cpb) {// System.out.println(records);// }// System.out.println("PostPattern is " + postPattern);Map<String, Integer> freqMap = getFrequency(cpb);Map<String, TreeNode> headers = new HashMap<String, TreeNode>();for (Entry<String, Integer> entry : freqMap.entrySet()) {String name = entry.getKey();int cnt = entry.getValue();//每一次递归时都有可能出现一部分模式的频数低于阈值if (cnt >= minSuport) {TreeNode node = new TreeNode(name);node.setCount(cnt);headers.put(name, node);}}TreeNode treeRoot = buildSubTree(cpb, freqMap, headers);//如果只剩下虚根节点,则递归结束if ((treeRoot.getChildren() == null) || (treeRoot.getChildren().size() == 0)) {return;}//如果树是单枝的,则直接把“路径的各种组合+后缀模式”添加到频繁模式集中。这个技巧是可选的,即跳过此步进入下一轮//递归也可以得到正确的结果if (isSingleBranch(treeRoot)) {LinkedList<TreeNode> path = new LinkedList<TreeNode>();TreeNode currNode = treeRoot;while (currNode.getChildren() != null) {currNode = currNode.getChildren().get(0);path.add(currNode);}//调用combine时path不宜过长,否则会OutOfMemoryif (path.size() <= 20) {List<List<TreeNode>> results = new ArrayList<List<TreeNode>>();combine(path, results);for (List<TreeNode> list : results) {int cnt = 0;List<String> rule = new ArrayList<String>();for (TreeNode node : list) {rule.add(node.getName());cnt = node.getCount();//cnt最FPTree叶节点的计数}if (postModel != null) {rule.addAll(postModel);}frequentMap.put(rule, cnt);}return;} else {System.err.println("length of path is too long: " + path.size());}}for (TreeNode header : headers.values()) {List<String> rule = new ArrayList<String>();rule.add(header.getName());if (postModel != null) {rule.addAll(postModel);}//表头项+后缀模式 构成一条频繁模式(频繁模式内部也是按照F1排序的),频繁度为表头项的计数frequentMap.put(rule, header.getCount());//新的后缀模式:表头项+上一次的后缀模式(注意保持顺序,始终按F1的顺序排列)LinkedList<String> newPostPattern = new LinkedList<String>();newPostPattern.add(header.getName());if (postModel != null) {newPostPattern.addAll(postModel);}//新的条件模式基List<List<String>> newCPB = new LinkedList<List<String>>();TreeNode nextNode = header;while ((nextNode = nextNode.getNextHomonym()) != null) {int counter = nextNode.getCount();//获得从虚根节点(不包括虚根节点)到当前节点(不包括当前节点)的路径,即一条条件模式基。注意保持顺序:你节点//在前,子节点在后,即始终保持频率高的在前LinkedList<String> path = new LinkedList<String>();TreeNode parent = nextNode;while ((parent = parent.getParent()).getName() != null) {//虚根节点的name为nullpath.push(parent.getName());//往表头插入}//事务要重复添加counter次while (counter-- > 0) {newCPB.add(path);}}FPGrowth(newCPB, newPostPattern);}}/*** 把所有事务插入到一个FP树当中** @param transRecords* @param F1* @return*/private TreeNode buildSubTree(List<List<String>> transRecords,final Map<String, Integer> freqMap,final Map<String, TreeNode> headers) {TreeNode root = new TreeNode();//虚根节点for (List<String> transRecord : transRecords) {LinkedList<String> record = new LinkedList<String>(transRecord);TreeNode subTreeRoot = root;TreeNode tmpRoot = null;if (root.getChildren() != null) {//延已有的分支,令各节点计数加1while (!record.isEmpty()&& (tmpRoot = subTreeRoot.findChild(record.peek())) != null) {tmpRoot.countIncrement(1);subTreeRoot = tmpRoot;record.poll();}}//长出新的节点addNodes(subTreeRoot, record, headers);}return root;}/*** 往特定的节点下插入一串后代节点,同时维护表头项到同名节点的链表指针** @param ancestor* @param record* @param headers*/private void addNodes(TreeNode ancestor, LinkedList<String> record,final Map<String, TreeNode> headers) {while (!record.isEmpty()) {String item = (String) record.poll();//单个项的出现频数必须大于最小支持数,否则不允许插入FP树。达到最小支持度的项都在headers中。每一次递归根据条件//模式基本建立新的FPTree时,把要把频数低于minSuport的排除在外,这也正是FPTree比穷举法快的真正原因if (headers.containsKey(item)) {TreeNode leafnode = new TreeNode(item);leafnode.setCount(1);leafnode.setParent(ancestor);ancestor.addChild(leafnode);TreeNode header = headers.get(item);TreeNode tail = header.getTail();if (tail != null) {tail.setNextHomonym(leafnode);} else {header.setNextHomonym(leafnode);}header.setTail(leafnode);addNodes(leafnode, record, headers);}// else {// System.err.println(item + " is not F1");// }}}/*** 获取所有的强规则** @return*/public List<StrongAssociationRule> getAssociateRule() {assert totalSize > 0;List<StrongAssociationRule> rect = new ArrayList<StrongAssociationRule>();//遍历所有频繁模式for (Entry<List<String>, Integer> entry : frequentMap.entrySet()) {List<String> items = entry.getKey();int count1 = entry.getValue();//一条频繁模式可以生成很多关联规则List<StrongAssociationRule> rules = getRules(items);//计算每一条关联规则的支持度和置信度for (StrongAssociationRule rule : rules) {if (frequentMap.containsKey(rule.condition)) {int count2 = frequentMap.get(rule.condition);double confidence = 1.0 * count1 / count2;if (confidence >= this.confident) {rule.support = count1;rule.confidence = confidence;rect.add(rule);}} else {System.err.println(rule.condition + " is not a frequent pattern, however "+ items + " is a frequent pattern");}}}return rect;}public static void main(String[] args) throws IOException {String infile = "trolley.txt";FPTree fpTree = new FPTree();fpTree.setConfident(0.6);fpTree.setMinSuport(3);if (args.length >= 2) {double confidence = Double.parseDouble(args[0]);int suport = Integer.parseInt(args[1]);fpTree.setConfident(confidence);fpTree.setMinSuport(suport);}List<List<String>> trans = fpTree.readTransRocords(new String[]{infile});Set<String> decideAttr = new HashSet<String>();decideAttr.add("鸡蛋");decideAttr.add("面包");fpTree.setDecideAttr(decideAttr);long begin = System.currentTimeMillis();fpTree.buildFPTree(trans);long end = System.currentTimeMillis();System.out.println("buildFPTree use time " + (end - begin));Map<List<String>, Integer> pattens = fpTree.getFrequentItems();String outfile = "pattens.txt";BufferedWriter bw = new BufferedWriter(new FileWriter(outfile));System.out.println("模式\t频数");bw.write("模式\t频数");bw.newLine();for (Entry<List<String>, Integer> entry : pattens.entrySet()) {System.out.println(entry.getKey() + "\t" + entry.getValue());bw.write(joinList(entry.getKey()) + "\t" + entry.getValue());bw.newLine();}bw.close();System.out.println();List<StrongAssociationRule> rules = fpTree.getAssociateRule();outfile = "rule.txt";bw = new BufferedWriter(new FileWriter(outfile));System.out.println("条件\t结果\t支持度\t置信度");bw.write("条件\t结果\t支持度\t置信度");bw.newLine();DecimalFormat dfm = new DecimalFormat("#.##");for (StrongAssociationRule rule : rules) {System.out.println(rule.condition + "->" + rule.result + "\t" + dfm.format(rule.support)+ "\t" + dfm.format(rule.confidence));bw.write(rule.condition + "->" + rule.result + "\t" + dfm.format(rule.support) + "\t"+ dfm.format(rule.confidence));bw.newLine();}bw.close();}private static String joinList(List<String> list) {if (list == null || list.size() == 0) {return "";}StringBuilder sb = new StringBuilder();for (String ele : list) {sb.append(ele);sb.append(",");}//把最后一个逗号去掉return sb.substring(0, sb.length() - 1);}}
输入trolley.txt
牛奶,鸡蛋,面包,薯片鸡蛋,爆米花,薯片,啤酒鸡蛋,面包,薯片牛奶,鸡蛋,面包,爆米花,薯片,啤酒牛奶,面包,啤酒鸡蛋,面包,啤酒牛奶,面包,薯片牛奶,鸡蛋,面包,黄油,薯片牛奶,鸡蛋,黄油,薯片
输出pattens.txt
模式 频数面包,啤酒 3鸡蛋,牛奶 4面包,薯片 5薯片,鸡蛋 6啤酒 4薯片 7面包,薯片,鸡蛋,牛奶 3鸡蛋,啤酒 3面包,牛奶 5薯片,鸡蛋,牛奶 4面包,鸡蛋,牛奶 3面包 7牛奶 6面包,薯片,鸡蛋 4薯片,牛奶 5鸡蛋 7面包,鸡蛋 5面包,薯片,牛奶 4
输出rule.txt
条件 结果 支持度 置信度[啤酒]->面包 3 0.75[牛奶]->鸡蛋 4 0.67[薯片]->面包 5 0.71[薯片]->鸡蛋 6 0.86[薯片, 鸡蛋, 牛奶]->面包 3 0.75[面包, 薯片, 牛奶]->鸡蛋 3 0.75[啤酒]->鸡蛋 3 0.75[牛奶]->面包 5 0.83[薯片, 牛奶]->鸡蛋 4 0.8[鸡蛋, 牛奶]->面包 3 0.75[面包, 牛奶]->鸡蛋 3 0.6[薯片, 鸡蛋]->面包 4 0.67[面包, 薯片]->鸡蛋 4 0.8[鸡蛋]->面包 5 0.71[面包]->鸡蛋 5 0.71[薯片, 牛奶]->面包 4 0.8