source: orange/orange/OrangeWidgets/Evaluate/OWLiftCurve.py @ 9505:4b798678cd3d

Revision 9505:4b798678cd3d, 17.5 KB checked in by matija <matija.polajnar@…>, 2 years ago (diff)

Merge in the (heavily modified) MLC code from GSOC 2011 (modules, documentation, evaluation code, regression test). Widgets will be merged in a little bit later, which will finally close ticket #992.

Line 
1"""
2<name>Lift Curve</name>
3<description>Displays a lift curve based on evaluation of classifiers.</description>
4<contact>Tomaz Curk</contact>
5<icon>icons/LiftCurve.png</icon>
6<priority>1020</priority>
7"""
8from OWColorPalette import ColorPixmap
9from OWWidget import *
10from OWGraph import *
11from OWGUI import *
12from OWROC import *
13
14import orngStat, orngEval
15import statc, math
16
17class singleClassLiftCurveGraph(singleClassROCgraph):
18    def __init__(self, parent = None, name = None, title = ""):
19        singleClassROCgraph.__init__(self, parent, name)
20
21        self.enableYRaxis(1)
22        self.setXaxisTitle("P Rate")
23        self.setAxisAutoScale(QwtPlot.yRight)
24        self.setAxisAutoScale(QwtPlot.yLeft)
25        self.setYLaxisTitle("TP")
26        self.setShowYRaxisTitle(1)
27        self.setYRaxisTitle("Cost")
28
29        self.setShowMainTitle(1)
30        self.setMainTitle(title)
31        self.averagingMethod = 'merge'
32
33    def computeCurve(self, res, classIndex=-1, keepConcavities=1):
34        return orngStat.computeLiftCurve(res, classIndex)
35
36    def setNumberOfClassifiersIterationsAndClassifierColors(self, classifierNames, iterationsNum, classifierColor):
37        singleClassROCgraph.setNumberOfClassifiersIterationsAndClassifierColors(self, classifierNames, iterationsNum, classifierColor)
38        self.performanceLineCKey.setYAxis(QwtPlot.yRight)
39        self.performanceLineCKey.setSymbol(QwtSymbol())
40
41    def setTestSetData(self, splitByIterations, targetClass):
42        self.splitByIterations = splitByIterations
43        ## generate the "base" unmodified Lift curves
44        self.targetClass = targetClass
45        iteration = 0
46
47        for isplit in splitByIterations:
48            # unmodified Lift curve
49            P, N, curves = self.computeCurve(isplit, self.targetClass)
50            self.setIterationCurves(iteration, curves)
51            iteration += 1
52
53    ## the lift curve is the average curve from the selected test sets
54    ## no other average curves here
55    def calcAverageCurves(self):
56        ##
57        ## self.averagingMethod == 'merge':
58        mergedIterations = orngEval.ExperimentResults(1, self.splitByIterations[0].classifierNames, self.splitByIterations[0].classValues, self.splitByIterations[0].weights, classifiers=self.splitByIterations[0].classifiers, loaded=self.splitByIterations[0].loaded)
59        i = 0
60        for show, isplit in zip(self.showIterations, self.splitByIterations):
61            if show:
62                for te in isplit.results:
63                    mergedIterations.results.append( te )
64            i += 1
65        self.mergedConvexHullData = []
66        if len(mergedIterations.results) > 0:
67            self.P, self.N, curves = self.computeCurve(mergedIterations, self.targetClass, 1)
68            _, _, convexCurves = self.computeCurve(mergedIterations, self.targetClass, 0)
69            classifier = 0
70            for c in curves:
71                x = [px for (px, py, pf) in c]
72                y = [py for (px, py, pf) in c]
73                curve = self.mergedCKeys[classifier]
74                curve.setData(x, y)
75                classifier += 1
76            classifier = 0
77            for c in convexCurves:
78                self.mergedConvexHullData.append(c) ## put all points of all curves into one big array
79                x = [px for (px, py, pf) in c]
80                y = [py for (px, py, pf) in c]
81                curve = self.mergedConvexCKeys[classifier]
82                curve.setData(x, y)
83                classifier += 1
84
85            self.diagonalCKey.setData([0.0, 1.0], [0.0, self.P])
86        else:
87            for c in range(len(self.mergedCKeys)):
88                self.mergedCKeys[c].setData([], [])
89                self.mergedConvexCKeys[c].setData([], [])
90
91    ## always set to 'merge' mode
92    def setAveragingMethod(self, m):
93        self.averagingMethod = 'merge'
94        self.updateCurveDisplay()
95
96    ## performance line
97    def calcUpdatePerformanceLine(self):
98        ## now draw the closest line to the curve
99        b = (self.averagingMethod == 'merge') and self.showPerformanceLine
100        self.removeMarkers()
101        costx = []
102        costy = []
103
104        firstGlobalMinP = 1
105        globalMinCost = 0
106        globalMinCostPoints = []
107
108        for (x, TP, fp) in self.hullCurveDataForPerfLine:
109            first = 1
110            minc = 0
111            localMinCostPoints = []
112            for (cNum, (threshold, FPrate)) in fp:
113                if TP > self.P:
114                    import warnings
115                    warnings.warn("The sky is falling!!")
116                cost = self.pvalue*(1.0 - TP/(self.P or 1))*self.FNcost + (1.0 - self.pvalue)*FPrate*self.FPcost
117                if first or cost < minc:
118                    first = 0
119                    minc = cost
120                    localMinCostPoints = [ (x, minc, threshold, cNum) ]
121                else:
122                    if cost == minc:
123                        localMinCostPoints.append( (x, minc, threshold, cNum) )
124
125            if firstGlobalMinP or minc < globalMinCost:
126                firstGlobalMinP = 0
127                globalMinCost = minc
128                globalMinCostPoints = [l for l in localMinCostPoints]
129            else:
130                if minc == globalMinCost:
131                    globalMinCostPoints.extend(localMinCostPoints)
132
133            costx.append(x)
134            costy.append(minc)
135
136        if self.performanceLineCKey: #self.curve(self.performanceLineCKey):
137            self.performanceLineCKey.setData(costx, costy)
138            self.performanceLineCKey.setVisible(b)
139        self.replot()
140#        self.update()
141
142        nOnMinc = {}
143        for (x, minc, threshold, cNum) in globalMinCostPoints:
144            s = "c:%.1f, th:%1.3f %s" % (minc, threshold, self.classifierNames[cNum])
145            marker = self.addMarker(s, 0, 0)
146            marker.setAxis(QwtPlot.xBottom, QwtPlot.yRight)
147            onYCn = nOnMinc.get(str(x), 0)
148
149            lminc = self.invTransform(QwtPlot.yLeft, self.transform(QwtPlot.yRight, minc)) ## ugly
150            if onYCn > 0:
151                lminc = lminc - onYCn*0.05
152                nOnMinc[str(x)] = nOnMinc[str(x)] + 1
153                marker.setSymbol(QwtSymbol())
154            else:
155                nOnMinc[str(x)] = 1
156                marker.setSymbol(self.performanceLineSymbol)
157
158            lminc = self.invTransform(QwtPlot.yRight, self.transform(QwtPlot.yLeft, lminc)) ## ugly ugly
159
160            marker.setXValue(x)
161            marker.setYValue(lminc)
162            if x >= 0.90:
163                marker.setLabelAlignment(Qt.AlignLeft)
164            else:
165                marker.setLabelAlignment(Qt.AlignRight)
166
167            marker.setVisible(b)
168
169    def setPointWidth(self, v):
170        self.performanceLineSymbol.setSize(v, v)
171        for marker in [item for item in self.itemList() if isinstance(item, QwtPlotMarker)]:
172            marker.setSymbol(self.performanceLineSymbol)
173        self.replot()
174#        self.update()
175
176class OWLiftCurve(OWROC):
177    settingsList = ["PointWidth", "CurveWidth", "ShowDiagonal",
178                    "ConvexHullCurveWidth", "HullColor", "ShowConvexHull", "ShowConvexCurves", "EnablePerformance"]
179    contextHandlers = {"": EvaluationResultsContextHandler("", "targetClass", "selectedClassifiers")}
180
181    def __init__(self, parent=None, signalManager = None):
182        OWWidget.__init__(self, parent, signalManager, "Lift Curve Analysis", 1)
183
184        # inputs
185        self.inputs=[("Evaluation Results", orngTest.ExperimentResults, self.results, Default)]
186
187        # default settings
188        self.PointWidth = 7
189        self.CurveWidth = 3
190        self.ConvexCurveWidth = 1
191        self.ShowDiagonal = TRUE
192        self.ConvexHullCurveWidth = 3
193        self.HullColor = str(QColor(Qt.yellow).name())
194        self.ShowConvexHull = TRUE
195        self.ShowConvexCurves = FALSE
196        self.EnablePerformance = TRUE
197        self.classifiers = []
198        self.selectedClassifiers = []
199
200        #load settings
201        self.loadSettings()
202
203### Moved here to override the saved settings since the controls do not exist any more
204        self.CurveWidth = 3
205        self.ConvexCurveWidth = 1
206        self.ConvexHullCurveWidth = 3
207
208        # temp variables
209        self.dres = None
210        self.classifierColor = None
211        self.numberOfClasses  = 0
212        self.targetClass = None
213        self.numberOfClassifiers = 0
214        self.numberOfIterations = 0
215        self.graphs = []
216        self.maxp = 1000
217        self.defaultPerfLinePValues = []
218
219        # performance analysis (temporary values
220        self.FPcost = 500.0
221        self.FNcost = 500.0
222        self.pvalue = 50.0 ##0.400
223
224        # list of values (remember for each class)
225        self.FPcostList = []
226        self.FNcostList = []
227        self.pvalueList = []
228
229        # GUI
230        #self.grid.expand(3, 3)
231        import sip
232        sip.delete(self.mainArea.layout())
233        self.graphsGridLayoutQGL = QGridLayout(self.mainArea)
234        self.mainArea.setLayout(self.graphsGridLayoutQGL)
235
236        # save each ROC graph in separate file
237        self.connect(self.graphButton, SIGNAL("clicked()"), self.saveToFile)
238
239        ## general tab
240        self.tabs = OWGUI.tabWidget(self.controlArea)
241        self.generalTab = OWGUI.createTabPage(self.tabs, "General")
242
243       
244        ## target class
245        self.classCombo = OWGUI.comboBox(self.generalTab, self, 'targetClass', box='Target class', items=[], callback=self.target)
246        OWGUI.separator(self.generalTab)
247
248        ## classifiers selection (classifiersQLB)
249        self.classifiersQVGB = OWGUI.widgetBox(self.generalTab, "Classifiers", addSpace=True)
250        self.classifiersQLB = OWGUI.listBox(self.classifiersQVGB, self, "selectedClassifiers", selectionMode = QListWidget.MultiSelection, callback = self.classifiersSelectionChange)
251        self.unselectAllClassifiersQLB = OWGUI.button(self.classifiersQVGB, self, "(Un)select all", callback = self.SUAclassifiersQLB)
252##        OWGUI.checkBox(self.classifiersQVGB, self, 'ShowConvexHull', 'Show convex lift hull', tooltip='', callback=self.setShowConvexHull)
253##        OWGUI.checkBox(self.classifiersQVGB, self, 'ShowDiagonal', 'Show diagonal', tooltip='', callback=self.setShowDiagonal)
254
255        # show Lift Curve convex hull
256        OWGUI.checkBox(self.generalTab, self, 'ShowConvexHull', 'Show lift convex hull', tooltip='', callback=self.setShowConvexHull)
257               
258
259        # performance analysis
260        self.performanceTab = OWGUI.createTabPage(self.tabs, "Analysis")
261        self.performanceTabCosts = OWGUI.widgetBox(self.performanceTab)
262        OWGUI.checkBox(self.performanceTabCosts, self, 'EnablePerformance', 'Show cost function', tooltip='', callback=self.setShowPerformanceAnalysis)
263
264        ## FP and FN cost ranges
265        mincost = 1; maxcost = 1000; stepcost = 5;
266        self.maxpsum = 100; self.minp = 1; self.maxp = self.maxpsum - self.minp ## need it also in self.pvaluesUpdated
267        stepp = 1.0
268
269        OWGUI.widgetLabel(self.performanceTabCosts, "False positive cost")
270        OWGUI.hSlider(OWGUI.indentedBox(self.performanceTabCosts), self, 'FPcost', minValue=mincost, maxValue=maxcost, step=stepcost, callback=self.costsChanged, ticks=50)
271        OWGUI.widgetLabel(self.performanceTabCosts, "False negative cost")
272        OWGUI.hSlider(OWGUI.indentedBox(self.performanceTabCosts), self, 'FNcost', minValue=mincost, maxValue=maxcost, step=stepcost, callback=self.costsChanged, ticks=50)
273
274        OWGUI.widgetLabel(self.performanceTabCosts, "Prior target class probability [%]")
275        ptc = OWGUI.indentedBox(self.performanceTabCosts)
276        OWGUI.hSlider(ptc, self, 'pvalue', minValue=self.minp, maxValue=self.maxp, step=stepp, callback=self.pvaluesUpdated, ticks=5, labelFormat="%2.1f")
277        OWGUI.separator(ptc)
278        OWGUI.button(ptc, self, 'Compute from data', self.setDefaultPValues) ## reset p values to default
279
280
281        ## test set selection (testSetsQLB)
282        self.testSetsQVGB = OWGUI.widgetBox(self.performanceTab, "Test sets")
283        self.testSetsQLB = OWGUI.listBox(self.testSetsQVGB, self, selectionMode = QListWidget.MultiSelection, callback = self.testSetsSelectionChange)
284        self.unselectAllTestSetsQLB = OWGUI.button(self.testSetsQVGB, self, "(Un)select all", callback = self.SUAtestSetsQLB)
285
286        # settings tab
287        self.settingsTab = OWGUI.createTabPage(self.tabs, "Settings")
288        OWGUI.hSlider(self.settingsTab, self, 'PointWidth', box='Point width', minValue=0, maxValue=9, step=1, callback=self.setPointWidth, ticks=1)
289        OWGUI.hSlider(self.settingsTab, self, 'CurveWidth', box='Lift curve width', minValue=1, maxValue=5, step=1, callback=self.setCurveWidth, ticks=1)
290        OWGUI.hSlider(self.settingsTab, self, 'ConvexHullCurveWidth', box='Lift curve convex hull', minValue=2, maxValue=9, step=1, callback=self.setConvexHullCurveWidth, ticks=1)
291        OWGUI.checkBox(self.settingsTab, self, 'ShowDiagonal', 'Show diagonal', tooltip='', callback=self.setShowDiagonal)
292        OWGUI.rubber(self.settingsTab)
293##        self.SettingsTab.addStretch(100)
294
295#        OWGUI.rubber(self.controlArea)
296        self.resize(770, 530)
297
298    def sendReport(self):
299        # need to reimport - Qt provides something stupid instead
300        from __builtin__ import hex
301        self.reportSettings("Settings",
302                            [("Classifiers", ", ".join('<font color="#%s">%s</font>' % ("".join(("0"+hex(x)[2:])[-2:] for x in self.classifierColor[cNum].getRgb()[:3]), str(item.text()))
303                                                        for cNum, item in enumerate(self.classifiersQLB.item(i) for i in range(self.classifiersQLB.count()))
304                                                          if item.isSelected())),
305                             ("Target class", self.classCombo.itemText(self.targetClass)
306                                              if self.targetClass is not None else
307                                              "N/A"),
308                             ("Costs", "FP=%i, FN=%i" % (self.FPcost, self.FNcost)),
309                             ("Prior target class probability", "%i%%" % self.pvalue)
310                            ])
311        if self.targetClass is not None:
312            self.reportRaw("<br/>")
313            self.reportImage(self.graphs[self.targetClass].saveToFileDirect, QSize(500, 400))
314
315
316    def calcAllClassGraphs(self):
317        for (cl, g) in enumerate(self.graphs):
318            g.setNumberOfClassifiersIterationsAndClassifierColors(self.dres.classifierNames, self.numberOfIterations, self.classifierColor)
319            g.setTestSetData(self.dresSplitByIterations, cl)
320            g.setShowConvexHull(self.ShowConvexHull)
321            g.setShowPerformanceLine(self.EnablePerformance)
322
323            ## user settings
324            g.setPointWidth(self.PointWidth)
325            g.setCurveWidth(self.CurveWidth)
326            g.setShowDiagonal(self.ShowDiagonal)
327            g.setConvexHullCurveWidth(self.ConvexHullCurveWidth)
328            g.setHullColor(QColor(self.HullColor))
329
330    def results(self, dres):
331        self.closeContext()
332
333        self.FPcostList = []
334        self.FNcostList = []
335        self.pvalueList = []
336
337        self.classCombo.clear()
338        self.removeGraphs()
339        self.testSetsQLB.clear()
340        self.classifiersQLB.clear()
341
342        self.dres = dres
343
344        if not dres:
345            self.targetClass = None
346            self.openContext("", dres)
347            return
348
349        self.defaultPerfLinePValues = []
350        if self.dres <> None:
351            ## classQLB
352            self.numberOfClasses = len(self.dres.classValues)
353            self.graphs = []
354
355            for i in range(self.numberOfClasses):
356                self.FPcostList.append( 500)
357                self.FNcostList.append( 500)
358                graph = singleClassLiftCurveGraph(self.mainArea, "", "Predicted class: " + self.dres.classValues[i])
359                self.graphs.append( graph )
360                self.classCombo.addItem(self.dres.classValues[i])
361
362            ## classifiersQLB
363            self.classifierColor = []
364            self.numberOfClassifiers = self.dres.numberOfLearners
365            if self.numberOfClassifiers > 1:
366                allCforHSV = self.numberOfClassifiers - 1
367            else:
368                allCforHSV = self.numberOfClassifiers
369            for i in range(self.numberOfClassifiers):
370                newColor = QColor()
371                newColor.setHsv(i*255/allCforHSV, 255, 255)
372                self.classifierColor.append( newColor )
373
374            ## testSetsQLB
375            self.dresSplitByIterations = orngStat.splitByIterations(self.dres)
376            self.numberOfIterations = len(self.dresSplitByIterations)
377
378            self.calcAllClassGraphs()
379
380            ## classifiersQLB
381            for i in range(self.numberOfClassifiers):
382                newColor = self.classifierColor[i]
383                self.classifiersQLB.addItem(QListWidgetItem(ColorPixmap(newColor), self.dres.classifierNames[i]))
384            self.classifiersQLB.selectAll()
385
386            ## testSetsQLB
387            self.testSetsQLB.addItems([str(i) for i in range(self.numberOfIterations)])
388            self.testSetsQLB.selectAll()
389
390            ## calculate default pvalues
391            reminder = self.maxp
392            for f in orngStat.classProbabilitiesFromRes(self.dres):
393                v = int(round(f * self.maxp))
394                reminder -= v
395                if reminder < 0:
396                    v = v+reminder
397                self.defaultPerfLinePValues.append(v)
398                self.pvalueList.append( v)
399
400            self.targetClass = 0 ## select first target
401            self.target()
402        else:
403            self.classifierColor = None
404        self.openContext("", self.dres)
405        self.performanceTabCosts.setEnabled(1)
406        self.setDefaultPValues()
407
408if __name__ == "__main__":
409    a = QApplication(sys.argv)
410    owdm = OWLiftCurve()
411    owdm.show()
412    a.exec_()
413
414
Note: See TracBrowser for help on using the repository browser.