source: orange/orange/OrangeWidgets/Associate/OWAssociationRulesViewer.py @ 8042:ffcb93bc9028

Revision 8042:ffcb93bc9028, 20.7 KB checked in by markotoplak, 3 years ago (diff)

Hierarchical clustering: also catch RuntimeError when importing matplotlib (or the documentation could not be built on server).

Line 
1"""
2<name>Association Rules Filter</name>
3<description>Association rules filter and viewer.</description>
4<icon>icons/AssociationRulesViewer.png</icon>
5<contact>Janez Demsar (janez.demsar(@at@)fri.uni-lj.si)</contact>
6<priority>200</priority>
7"""
8import orange, sys
9from OWWidget import *
10import OWGUI
11
12class AssociationRulesViewerScene(QGraphicsScene):
13    def __init__(self, master, widget):
14        QGraphicsScene.__init__(self, widget)
15        self.master = master
16        self.rect = None
17        self.mousePressed = False
18        self.unselect()
19        self.draw()
20
21    def unselect(self):
22        if self.rect:
23            self.rect.hide()
24
25
26    def draw(self):
27        master = self.master
28        nc, nr, cw, ch, ig = master.numcols, master.numrows, master.cellwidth, master.cellheight, master.ingrid
29        scmin, scmax, srmin, srmax = master.sel_colmin, master.sel_colmax, master.sel_rowmin, master.sel_rowmax
30
31        self.setSceneRect(0, 0, nc * cw +1, nr * ch +1)
32
33        for a in self.items():
34            self.removeItem(a)
35
36        maxcount = max([max([len(cell) for cell in row]) for row in master.ingrid])
37        maxcount = float(max(10, maxcount))
38
39        pens = [QPen(QColor(200,200,200), 1), QPen(QColor(200,200,255), 1)]
40        brushes = [QBrush(QColor(255, 255, 255)), QBrush(QColor(250, 250, 255))]
41        self.cells = []
42        for x in range(nc):
43            selx = x >= scmin and x <= scmax
44            for y in range(nr):
45                sel = selx and y >= srmin and y <= srmax
46                cell = QGraphicsRectItem(x*cw, y*ch, cw+1, ch+1, None, self)
47                cell.setZValue(0)
48                cell.setPen(pens[sel])
49                if not ig[y][x]:
50                    cell.setBrush(brushes[sel])
51                else:
52                    if sel:
53                        color = 220 - 220 * len(ig[y][x]) / maxcount
54                        cell.setBrush(QBrush(QColor(color, color, 255)))
55                    else:
56                        color = 255 - 235 * len(ig[y][x]) / maxcount
57                        cell.setBrush(QBrush(QColor(color-20, color-20, color)))
58                cell.show()
59
60        self.rect = QGraphicsRectItem(scmin*cw, srmin*ch, (scmax-scmin+1)*cw, (srmax-srmin+1)*ch, None, self)
61        self.rect.setPen(QPen(QColor(128, 128, 255), 2))
62        self.rect.setZValue(1)
63        self.rect.hide()
64
65        self.update()
66        self.master.shownSupport.setText('%3i%% - %3i%%' % (int(master.supp_min*100), int(master.supp_max*100)))
67        self.master.shownConfidence.setText('%3i%% - %3i%%' % (int(master.conf_min*100), int(master.conf_max*100)))
68        self.master.shownRules.setText('%3i' % sum([sum([len(cell) for cell in row]) for row in master.ingrid]))
69
70    def updateSelectionRect(self):
71        master = self.master
72        self.rect.setRect(master.sel_colmin*master.cellwidth,
73                          master.sel_rowmin*master.cellheight,
74                          (master.sel_colmax-master.sel_colmin+1)*master.cellwidth,
75                          (master.sel_rowmax-master.sel_rowmin+1)*master.cellheight)
76        self.update()
77
78    def mousePressEvent(self, ev):
79        self.sel_startX = int(ev.scenePos().x())
80        self.sel_startY = int(ev.scenePos().y())
81        master = self.master
82        master.sel_colmin = master.sel_colmax = self.sel_startX / master.cellwidth
83        master.sel_rowmin = master.sel_rowmax = self.sel_startY / master.cellheight
84        self.rect.show()
85        self.updateSelectionRect()
86        self.mousePressed = True
87
88    def mouseMoveEvent(self, ev):
89        if self.mousePressed:
90            self.sel_endX = int(ev.scenePos().x())
91            self.sel_endY = int(ev.scenePos().y())
92            t = self.sel_startX /self.master.cellwidth, self.sel_endX /self.master.cellwidth
93            self.master.sel_colmin, self.master.sel_colmax = min(t), max(t)
94            t = self.sel_startY /self.master.cellheight, self.sel_endY /self.master.cellheight
95            self.master.sel_rowmin, self.master.sel_rowmax = min(t), max(t)
96
97            self.master.sel_colmin = max(self.master.sel_colmin, 0)
98            self.master.sel_rowmin = max(self.master.sel_rowmin, 0)
99            self.master.sel_colmax = min(self.master.sel_colmax, self.master.numcols-1)
100            self.master.sel_rowmax = min(self.master.sel_rowmax, self.master.numrows-1)
101
102            self.updateSelectionRect()
103
104    def mouseReleaseEvent(self, ev):
105        self.master.updateRuleList()
106        self.master.sendIfAuto()
107        self.mousePressed = False
108
109
110class AssociationRulesViewerView(QGraphicsView):
111    def __init__(self, master, scene, widget):
112        QGraphicsView.__init__(self, scene, widget)
113        self.master = master
114        self.scene = scene
115        self.setFixedSize(365, 365)
116        self.selecting = False
117        self.update()
118
119    def contentsMousePressEvent(self, ev):
120        self.sel_startX = ev.pos().x()
121        self.sel_startY = ev.pos().y()
122        master = self.master
123        master.sel_colmin = master.sel_colmax = self.sel_startX / master.cellwidth
124        master.sel_rowmin = master.sel_rowmax = self.sel_startY / master.cellheight
125        self.scene().draw()
126        master.updateRuleList()
127
128    def contentsMouseMoveEvent(self, ev):
129        self.sel_endX = ev.pos().x()
130        self.sel_endY = ev.pos().y()
131        t = self.sel_startX /self.master.cellwidth, self.sel_endX /self.master.cellwidth
132        self.master.sel_colmin, self.master.sel_colmax = min(t), max(t)
133        t = self.sel_startY /self.master.cellheight, self.sel_endY /self.master.cellheight
134        self.master.sel_rowmin, self.master.sel_rowmax = min(t), max(t)
135
136        self.master.sel_colmin = max(self.master.sel_colmin, 0)
137        self.master.sel_rowmin = max(self.master.sel_rowmin, 0)
138        self.master.sel_colmax = min(self.master.sel_colmax, self.master.numcols-1)
139        self.master.sel_rowmax = min(self.master.sel_rowmax, self.master.numrows-1)
140
141        self.scene.draw()
142        self.master.updateRuleList()
143
144    def contentsMouseReleaseEvent(self, ev):
145        self.master.sendIfAuto()
146
147
148class OWAssociationRulesViewer(OWWidget):
149    measures = [("Support",    "Supp", "support"),
150                ("Confidence", "Conf", "confidence"),
151                ("Lift",       "Lift", "lift"),
152                ("Leverage",   "Lev",  "leverage"),
153                ("Strength",   "Strg", "strength"),
154                ("Coverage",   "Cov",  "coverage")]
155
156    settingsList = ["autoSend", "sortedBy"] + [vn[2] for vn in measures]
157
158    def __init__(self, parent=None, signalManager = None):
159        OWWidget.__init__(self, parent, signalManager, "AssociationRulesViewer", wantMainArea=0, noReport=True)
160
161        self.inputs = [("Association Rules", orange.AssociationRules, self.arules)]
162        self.outputs = [("Association Rules", orange.AssociationRules)]
163
164        self.supp_min, self.supp_max = self.conf_min, self.conf_max = 0., 1.
165        self.numcols = self.numrows = 20
166        self.showBars = True
167
168        self.cellwidth = self.cellheight = 18
169
170        for m in self.measures:
171            setattr(self, m[2], False)
172        self.support = self.confidence = True
173        self.sortedBy = 0
174        self.autoSend = True
175
176        self.loadSettings()
177
178        self.rules = None
179        self.selectedRules = []
180        self.noZoomButton()
181        self.mainArea = OWGUI.widgetBox(self.topWidgetPart, orientation = "horizontal", sizePolicy = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding), margin = 0)
182
183        mainLeft = OWGUI.widgetBox(self.mainArea, "Filter")
184        self.mainArea.layout().setAlignment(mainLeft, Qt.AlignLeft | Qt.AlignTop)
185        OWGUI.separator(self.mainArea, 16, 0)
186        mainRight = OWGUI.widgetBox(self.mainArea, "Rules")
187        mainRight.setSizePolicy(QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding))
188        mainLeft.setSizePolicy(QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum))
189
190        infoGrid = QGridLayout()
191        info = OWGUI.widgetBox(mainLeft, orientation = infoGrid)
192        infoGrid.addWidget(OWGUI.widgetLabel(info, "Shown", addToLayout = 0), 1, 0)
193        infoGrid.addWidget(OWGUI.widgetLabel(info, "Selected", addToLayout = 0), 2, 0)
194        infoGrid.addWidget(OWGUI.widgetLabel(info, "Support (H)", addToLayout = 0), 0, 1)
195        infoGrid.addWidget(OWGUI.widgetLabel(info, "Confidence (V)", addToLayout = 0), 0, 2)
196        infoGrid.addWidget(OWGUI.widgetLabel(info, "# Rules", addToLayout = 0), 0, 3)
197
198        self.shownSupport = OWGUI.widgetLabel(info, " ", addToLayout = 0)
199        infoGrid.addWidget(self.shownSupport, 1, 1)
200        self.shownConfidence = OWGUI.widgetLabel(info, " ", addToLayout = 0)
201        infoGrid.addWidget(self.shownConfidence, 1, 2)
202        self.shownRules = OWGUI.widgetLabel(info, " ", addToLayout = 0)
203        infoGrid.addWidget(self.shownRules, 1, 3)
204
205        self.selSupport = OWGUI.widgetLabel(info, " ", addToLayout = 0)
206        infoGrid.addWidget(self.selSupport, 2, 1)
207        self.selConfidence = OWGUI.widgetLabel(info, " ", addToLayout = 0)
208        infoGrid.addWidget(self.selConfidence, 2, 2)
209        self.selRules = OWGUI.widgetLabel(info, " ", addToLayout = 0)
210        infoGrid.addWidget(self.selRules, 2, 3)
211
212        OWGUI.separator(mainLeft, 0, 4)
213        self.ruleScene = AssociationRulesViewerScene(self, mainLeft)
214        self.sceneView = AssociationRulesViewerView(self, self.ruleScene, mainLeft)
215        mainLeft.layout().addWidget(self.sceneView)
216
217        boxb = OWGUI.widgetBox(mainLeft, box=None, orientation="horizontal")
218        OWGUI.button(boxb, self, 'Zoom', callback = self.zoomButton)
219        OWGUI.button(boxb, self, 'Show All', callback = self.showAllButton)
220        OWGUI.button(boxb, self, 'No Zoom', callback = self.noZoomButton)
221        OWGUI.separator(boxb, 16, 8)
222        OWGUI.button(boxb, self, 'Unselect', callback = self.unselect)
223
224        self.grid = QGridLayout()
225        rightUpRight = OWGUI.widgetBox(mainRight, orientation = self.grid)
226        for i, m in enumerate(self.measures):
227            cb = OWGUI.checkBox(rightUpRight, self, m[2], m[0], callback = self.showHideColumns, addToLayout = 0)
228            self.grid.addWidget(cb, i % 2, i / 2)
229
230        OWGUI.separator(mainRight, 0, 4)
231       
232        trules = self.trules = QTableWidget(0, 0, mainRight)
233        mainRight.layout().addWidget(trules)
234        trules.verticalHeader().hide()
235        trules.setSelectionMode(QTableWidget.NoSelection)
236        trules.setColumnCount(len(self.measures)+3)
237
238        header = trules.horizontalHeader()
239        trules.setHorizontalHeaderLabels([m[1] for m in self.measures]+["Antecedent", "->", "Consequent"])
240        trules.horizontalHeaderItem(len(self.measures)).setTextAlignment(Qt.AlignRight)
241        trules.horizontalHeaderItem(len(self.measures)+2).setTextAlignment(Qt.AlignLeft)
242        #trules.setItemDelegate(OWGUI.TableBarItem(self, trules))
243        trules.normalizers = []
244
245        bottomGrid = QGridLayout()
246        bottom = OWGUI.widgetBox(mainRight, orientation = bottomGrid)
247
248        self.reportButton = OWGUI.button(bottom, self, "&Report", self.reportAndFinish, addToLayout=0, debuggingEnabled=0)
249#        self.saveButton = OWGUI.button(bottom, self, "Save Rules", callback = self.saveRules, addToLayout=0)
250        commitButton = OWGUI.button(bottom, self, "Send Rules", callback = self.sendRules, addToLayout=0, default=True)
251        autoSend = OWGUI.checkBox(bottom, self, "autoSend", "Send rules automatically", disables=[(-1, commitButton)], addToLayout=0)
252        autoSend.makeConsistent()
253
254        bottomGrid.addWidget(self.reportButton, 1, 0)
255#        bottomGrid.addWidget(self.saveButton, 1, 1)
256        bottomGrid.addWidget(autoSend, 0, 1)
257        bottomGrid.addWidget(commitButton, 1, 1)
258
259        self.controlArea.setFixedSize(0, 0)
260        self.resize(1000, 380)
261
262    def sendReport(self):
263        if not self.rules:
264            return 
265        self.reportSettings("Rules statistics",
266                            [("Total number of rules", len(self.rules)),
267                             ("Support", "%i%% - %i%%" % (self.supp_allmin*100, self.supp_allmax*100)),
268                             ("Confidence", "%i%% - %i%%" % (self.conf_allmin*100, self.conf_allmax*100)),
269                             ("Number of rules in the graph", self.shownRules.text()),
270                             ("Support", self.shownSupport.text()),
271                             ("Confidence", self.shownConfidence.text()),
272                             ("Selected rules", self.selRules.text()),
273                             ("Support", self.selSupport.text()),
274                             ("Confidence", self.selConfidence.text()),
275                            ])
276
277        self.reportSection("Rule matrix")
278        self.reportRaw("<p>Note: columns correspond to support, rows correspond to confidence</p><br/><center>")
279        buffer = QPixmap(self.ruleScene.width(), self.ruleScene.height())
280        painter = QPainter(buffer)
281        painter.fillRect(buffer.rect(), QBrush(QColor(255, 255, 255)))
282        self.ruleScene.render(painter)
283        painter.end()
284        self.reportImage(lambda filename: buffer.save(filename, os.path.splitext(filename)[1][1:]))
285        self.reportRaw("</center><br/>")
286       
287        self.reportSection("Selected rules")
288        self.reportRaw(OWReport.reportTable(self.trules))
289
290
291    def checkScale(self):
292        if self.supp_min == self.supp_max:
293            self.supp_max += 0.01
294        if self.conf_max == self.conf_min:
295            self.conf_max += 0.01
296        self.suppInCell = (self.supp_max - self.supp_min) / self.numcols
297        self.confInCell = (self.conf_max - self.conf_min) / self.numrows
298
299
300    def unselect(self):
301        self.sel_colmin = self.sel_colmax = self.sel_rowmin = self.sel_rowmax = -1
302        self.selectedRules = sum(sum(self.ingrid, []), [])
303
304        self.displayRules()
305        if hasattr(self, "selConfidence"):
306            self.updateConfSupp()
307
308        if hasattr(self, "ruleScene"):
309            self.ruleScene.unselect()
310            self.ruleScene.draw()
311
312        self.sendIfAuto()
313
314
315    def updateConfSupp(self):
316        if self.sel_colmin >= 0:
317            smin, cmin = self.coordToSuppConf(self.sel_colmin, self.sel_rowmin)
318            smax, cmax = self.coordToSuppConf(self.sel_colmax+1, self.sel_rowmax+1)
319        else:
320            smin, cmin = self.supp_min, self.conf_min
321            smax, cmax = self.supp_max, self.conf_max
322
323        self.selConfidence.setText("%3i%% - %3i%%" % (round(100*cmin), round(100*cmax)))
324        self.selSupport.setText("%3i%% - %3i%%" % (round(100*smin), round(100*smax)))
325        self.selRules.setText("%3i" % len(self.selectedRules))
326
327
328    def updateRuleList(self):
329        self.selectedRules = sum(sum((row[self.sel_colmin : self.sel_colmax+1] for row in self.ingrid[self.sel_rowmin : self.sel_rowmax+1]), []), [])
330        self.displayRules()
331        self.updateConfSupp()
332#        self.saveButton.setEnabled(len(self.selectedRules) > 0)
333
334
335    def displayRules(self):
336        if hasattr(self, "trules"):
337            trules = self.trules
338            trules.setRowCount(len(self.selectedRules))
339
340            rulecol = len(self.measures)
341            trules.normalizers = []
342            self.trules.setUpdatesEnabled(False)
343            self.trules.setSortingEnabled(False)
344            try:
345                self.progressBarInit()
346                progressStep = 100./(rulecol+1)
347                for col, m in enumerate(self.measures):
348                    mname = m[2]
349                    values = [getattr(rule, mname) for rule in self.selectedRules]
350                    if m[1] in ["Supp", "Conf", "Cov"]:
351                        trules.normalizers.append((1, 1))
352                    elif values:
353                        mi, ma = min(values), max(values)
354                        div = ma - mi
355                        trules.normalizers.append((ma, div or 1))
356                    else:
357                        trules.normalizers.append((0, 1))
358                    for row, meas in enumerate(values):
359                        trules.setItem(row, col, QTableWidgetItem(%.3f  " % meas))
360                    self.progressBarAdvance(progressStep)
361   
362                for row, rule in enumerate(self.selectedRules):
363                    left, right = [QTableWidgetItem(x) for x in str(rule).replace(" ", "  ").replace("=>", ">").replace("=<=", "<=").split(" -> ", 1)]
364                    trules.setItem(row, rulecol, left)
365                    trules.item(row, rulecol).setTextAlignment(Qt.AlignRight)
366                    trules.setItem(row, rulecol+2, right)
367            finally:
368                self.progressBarFinished()
369
370            self.trules.resizeColumnsToContents()
371            self.trules.resizeRowsToContents()
372           
373            self.showHideColumns()
374            self.trules.setSortingEnabled(True)
375            self.trules.setUpdatesEnabled(True)
376
377
378
379    def showHideColumns(self):
380        for i, m in enumerate(self.measures):
381            (getattr(self, m[2]) and self.trules.showColumn or self.trules.hideColumn)(i)
382
383
384#    def saveRules(self):
385#        fileName = QFileDialog.getSaveFileName(self, "Save Rules", "myRules.txt", "Textfiles (*.txt)" );
386#        if not fileName.isNull() :
387#            f = open(str(fileName), 'w')
388#            if self.selectedRules:
389#                toWrite = [m for m in self.measures if getattr(self, m[2])]
390#                if toWrite:
391#                    f.write("\t".join([m[1] for m in toWrite]) + "\n")
392#                for rule in self.selectedRules:
393#                    f.write("\t".join(["%.3f" % getattr(rule, m[2]) for m in toWrite] + [`rule`.replace(" ", "  ")]) + "\n")
394
395
396    def setIngrid(self):
397        smin, sic, cmin, cic = self.supp_min, self.suppInCell, self.conf_min, self.confInCell
398        self.ingrid = [[[] for x in range(self.numcols)] for y in range(self.numrows)]
399        if self.rules:
400            for r in self.rules:
401                self.ingrid[min(self.numrows-1, int((r.confidence - cmin) / cic))][min(self.numcols-1, int((r.support - smin) / sic))].append(r)
402
403
404    def coordToSuppConf(self, col, row):
405        return self.supp_min + col * self.suppInCell, self.conf_min + row * self.confInCell
406
407
408    def zoomButton(self):
409        if self.sel_rowmin >= 0:
410            # have to compute both at ones!
411            self.supp_min, self.conf_min, self.supp_max, self.conf_max = self.coordToSuppConf(self.sel_colmin, self.sel_rowmin) + self.coordToSuppConf(self.sel_colmax+1, self.sel_rowmax+1)
412            self.checkScale()
413
414            smin, sic, cmin, cic = self.supp_min, self.suppInCell, self.conf_min, self.confInCell
415            newingrid = [[[] for x in range(self.numcols)] for y in range(self.numrows)]
416            for row in self.ingrid[self.sel_rowmin : self.sel_rowmax+1]:
417                for cell in row[self.sel_colmin : self.sel_colmax+1]:
418                    for rule in cell:
419                        inrow = (rule.confidence - cmin) / cic
420                        if inrow >= 0 and inrow < self.numrows + 1e-3:
421                            incol = (rule.support - smin) / sic
422                            if incol >= 0 and incol < self.numcols + 1e-3:
423                                newingrid[min(int(inrow), self.numrows-1)][min(int(incol), self.numcols-1)].append(rule)
424            self.ingrid = newingrid
425
426            self.unselect()
427            self.ruleScene.draw()
428            self.sendIfAuto()
429
430
431    def rezoom(self, smi, sma, cmi, cma):
432        self.supp_min, self.supp_max, self.conf_min, self.conf_max = smi, sma, cmi, cma
433        self.checkScale() # to set the inCell
434        self.setIngrid()
435        self.unselect()
436        if hasattr(self, "ruleScene"):
437            self.ruleScene.draw()
438        self.sendIfAuto()
439
440    def showAllButton(self):
441        self.rezoom(self.supp_allmin, self.supp_allmax, self.conf_allmin, self.conf_allmax)
442
443    def noZoomButton(self):
444        self.rezoom(0., 1., 0., 1.)
445
446    def sendIfAuto(self):
447        if self.autoSend:
448            self.sendRules()
449
450    def sendRules(self):
451        self.send("Association Rules", orange.AssociationRules(self.selectedRules))
452
453    def arules(self,rules):
454        self.rules = rules
455        if self.rules:
456            supps = [rule.support for rule in self.rules]
457            self.supp_min = min(supps)
458            self.supp_max = max(supps)
459            del supps
460
461            confs = [rule.confidence for rule in self.rules]
462            self.conf_min = min(confs)
463            self.conf_max = max(confs)
464            del confs
465
466            self.checkScale()
467        else:
468            self.supp_min, self.supp_max = self.conf_min, self.conf_max = 0., 1.
469
470        self.supp_allmin, self.supp_allmax, self.conf_allmin, self.conf_allmax = self.supp_min, self.supp_max, self.conf_min, self.conf_max
471        self.rezoom(self.supp_allmin, self.supp_allmax, self.conf_allmin, self.conf_allmax)
472
473
474
475if __name__=="__main__":
476    a=QApplication(sys.argv)
477    ow=OWAssociationRulesViewer()
478
479    dataset = orange.ExampleTable('../../doc/datasets/car.tab')
480    rules=orange.AssociationRulesInducer(dataset, minSupport = 0.3, maxItemSets=15000)
481    ow.arules(rules)
482
483    ow.show()
484    a.exec_()
485    ow.saveSettings()
Note: See TracBrowser for help on using the repository browser.