/** * Copyright (C) 2001-2017 by RapidMiner and the contributors * * Complete list of developers available at our web site: * * http://rapidminer.com * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. */ package com.rapidminer.gui.graphs; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.swing.BorderFactory; import javax.swing.JComponent; import org.apache.commons.collections15.Factory; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.gui.viewer.AssociationRuleFilter; import com.rapidminer.gui.viewer.AssociationRuleFilterListener; import com.rapidminer.operator.learner.associations.AssociationRule; import com.rapidminer.operator.learner.associations.AssociationRules; import com.rapidminer.operator.learner.associations.Item; import com.rapidminer.tools.Tools; import edu.uci.ics.jung.graph.DirectedSparseGraph; import edu.uci.ics.jung.graph.Graph; /** * Creates a graph model for a set of association rules. * * @author Ingo Mierswa */ public class AssociationRulesGraphCreator extends GraphCreatorAdaptor implements AssociationRuleFilterListener { private final Factory<String> edgeFactory = new Factory<String>() { int i = 0; @Override public String create() { return "E" + i++; } }; private final AssociationRules rules; private final Map<String, String> toolTipInfos = new HashMap<>(); private final List<String> nodeList = new LinkedList<>(); private final List<String> edgeList = new LinkedList<>(); private final Map<String, List<String>> asPremise = new HashMap<>(); private final Map<String, List<String>> asConclusion = new HashMap<>(); private final AssociationRuleFilter filter; private DirectedSparseGraph<String, String> graph; private GraphViewer<String, String> viewer; public AssociationRulesGraphCreator(AssociationRules rules) { this.rules = rules; this.filter = new AssociationRuleFilter(rules); this.filter.setBorder(BorderFactory.createTitledBorder("Filter")); this.filter.addAssociationRuleFilterListener(this); } @Override public Graph<String, String> createGraph() { graph = new DirectedSparseGraph<String, String>(); boolean[] allFilter = new boolean[rules.getNumberOfRules()]; for (int i = 0; i < allFilter.length; i++) { allFilter[i] = true; } addRuleNodes(allFilter); return graph; } private void addRuleNodes(boolean[] filter) { Iterator<String> e = edgeList.iterator(); while (e.hasNext()) { graph.removeEdge(e.next()); } Iterator<String> n = nodeList.iterator(); while (n.hasNext()) { graph.removeVertex(n.next()); } edgeList.clear(); nodeList.clear(); toolTipInfos.clear(); asPremise.clear(); asConclusion.clear(); int ruleIndex = 1; for (int r = 0; r < rules.getNumberOfRules(); r++) { if (filter[r]) { AssociationRule rule = rules.getRule(r); // define conjunction node String conjunctionNode = "Rule " + ruleIndex + " (" + Tools.formatNumber(rule.getTotalSupport()) + " / " + Tools.formatNumber(rule.getConfidence()) + ")"; toolTipInfos.put(conjunctionNode, createTooltip(rule, ruleIndex)); nodeList.add(conjunctionNode); // add premise nodes Iterator<Item> p = rule.getPremiseItems(); while (p.hasNext()) { Item premiseItem = p.next(); String edgeId = edgeFactory.create(); edgeList.add(edgeId); nodeList.add(premiseItem.toString()); graph.addEdge(edgeId, premiseItem.toString(), conjunctionNode); List<String> premiseList = asPremise.get(premiseItem.toString()); if (premiseList == null) { premiseList = new LinkedList<String>(); asPremise.put(premiseItem.toString(), premiseList); } premiseList.add("Rule " + ruleIndex); } // add conclusion nodes Iterator<Item> c = rule.getConclusionItems(); while (c.hasNext()) { Item conclusionItem = c.next(); String edgeId = edgeFactory.create(); edgeList.add(edgeId); nodeList.add(conclusionItem.toString()); graph.addEdge(edgeId, conjunctionNode, conclusionItem.toString()); List<String> conclusionList = asConclusion.get(conclusionItem.toString()); if (conclusionList == null) { conclusionList = new LinkedList<String>(); asConclusion.put(conclusionItem.toString(), conclusionList); } conclusionList.add("Rule " + ruleIndex); } } ruleIndex++; } } /** Returns true for rule nodes. */ @Override public boolean isBold(String id) { return toolTipInfos.get(id) == null; } /** Returns true for rule nodes. */ @Override public boolean isLeaf(String id) { return toolTipInfos.get(id) != null; } /** Returns null. */ @Override public String getVertexToolTip(String id) { String toolTip = toolTipInfos.get(id); if (toolTip != null) { return toolTip; } else { return createTooltip(id); } } @Override public String getEdgeName(String id) { return null; } @Override public String getVertexName(String id) { return id; } /** * Returns the label offset. In most case, using -1 is just fine (default offset). Some tree * like graphs might prefer to use 0 since they manage the offset themself. */ @Override public int getLabelOffset() { return -1; } /** Returns false. */ @Override public boolean showEdgeLabelsDefault() { return false; } /** Returns false. */ @Override public boolean showVertexLabelsDefault() { return true; } /** Returns the shape of the edges. */ @Override public int getEdgeShape() { return EDGE_SHAPE_QUAD_CURVE; } @Override public Object getObject(String id) { return id; } @Override public int getNumberOfOptionComponents() { return 1; } @SuppressWarnings("unchecked") @Override public JComponent getOptionComponent(final GraphViewer<?, ?> viewer, int index) { if (index == 0) { this.viewer = (GraphViewer<String, String>) viewer; return filter; } else { return null; } } @Override public void setFilter(boolean[] filter) { addRuleNodes(filter); viewer.updateLayout(); } /** * Create the tooltip for a rule. * * @param id * the id of a rule for which to create the tooltip * @return the HTML-formatted tooltip string */ private String createTooltip(String id) { StringBuilder sb = new StringBuilder(); sb.append( "<html><div style=\"font-size: 10px; font-family: 'Open Sans'\"><p style=\"font-size: 110%; text-align: center; font-family: 'Open Sans Semibold'\"><b>" + id + "</b><hr NOSHADE style=\"color: '#000000'; width: 95%; \"/></p><br/>"); sb.append("Premise: " + (asPremise.get(id) == null || asPremise.get(id).size() == 0 ? "-" : formatDependencies(asPremise.get(id))) + "<br/>"); sb.append("Conclusion: " + (asConclusion.get(id) == null || asConclusion.get(id).size() == 0 ? "-" : formatDependencies(asConclusion.get(id)))); sb.append("</div></html>"); return sb.toString(); } /** * Create the tooltip for a rule. * * @param rule * the rule for which to create the tooltip * @param ruleIndex * the index of the rule * @return the HTML-formatted tooltip string */ private String createTooltip(AssociationRule rule, int ruleIndex) { StringBuilder sb = new StringBuilder(); sb.append("<html><div style=\"font-size: 10px; font-family: 'Open Sans'\">"); sb.append("<p style=\"font-size: 110%; text-align: center; font-family: 'Open Sans Semibold'\"><b>Rule " + ruleIndex + "</b><hr NOSHADE style=\"color: '#000000'; width: 95%; \"/></p>"); sb.append(SwingTools.transformToolTipText( formatDependencies(rule.getPremiseItems()) + " \u2794 " + formatDependencies(rule.getConclusionItems()), false, 200, false, false) + "<br/>"); sb.append("Support: " + Tools.formatNumber(rule.getTotalSupport(), 2) + "<br/>"); sb.append("Confidence: " + Tools.formatNumber(rule.getConfidence(), 2) + "<br/>"); sb.append("Lift: " + Tools.formatNumber(rule.getLift(), 2) + "<br/>"); sb.append("Gain: " + Tools.formatNumber(rule.getGain(), 2) + "<br/>"); sb.append("Conviction: " + Tools.formatNumber(rule.getConviction(), 2) + "<br/>"); sb.append("Laplace: " + Tools.formatNumber(rule.getLaplace(), 2) + "<br/>"); sb.append("Ps: " + Tools.formatNumber(rule.getPs(), 2)); sb.append("</div></html>"); return sb.toString(); } /** * Displays the rule premise/conclusion list contents nicely formatted for humans. * * @param dependencyList * the list of premises/conclusions of the rule to format * @return the formatted string */ private static String formatDependencies(Iterator<Item> dependencyIterator) { StringBuilder sb = new StringBuilder(); while (dependencyIterator.hasNext()) { sb.append("<i>"); sb.append(dependencyIterator.next()); sb.append("</i>"); if (dependencyIterator.hasNext()) { sb.append(',').append(' '); } } return sb.toString(); } /** * Displays the rule premise/conclusion list contents nicely formatted for humans. * * @param dependencyList * the list of premises/conclusions of the rule to format * @return the formatted string */ private static String formatDependencies(Collection<String> dependencyList) { StringBuilder sb = new StringBuilder(); int size = dependencyList.size(); for (String item : dependencyList) { sb.append("<i>"); sb.append(item); sb.append("</i>"); if (--size > 0) { sb.append(',').append(' '); } } return sb.toString(); } }