source: orange/Orange/OrangeWidgets/Data/OWDiscretize.py @ 11096:cf7d2ae9d22b

Revision 11096:cf7d2ae9d22b, 45.5 KB checked in by Ales Erjavec <ales.erjavec@…>, 19 months ago (diff)

Added new svg icons for the widgets/categories.

Line 
1"""
2<name>Discretize</name>
3<description>Discretization of continuous attributes.</description>
4<icon>icons/Discretize.svg</icon>
5<contact>Ales Erjavec (ales.erjavec(@at@)fri.uni-lj.si)</contact>
6<priority>2100</priority>
7"""
8
9import orange
10from OWWidget import *
11from OWGraph import *
12import OWGUI
13
14
15NAME = "Discretize"
16DESCRIPTION = "Discretization of continuous attributes."
17
18MAINTAINER = "Ales Erjavec"
19MAINTAINER_EMAIL = "ales.erjavec < at > fri.uni-lj.si"
20
21CATEGORY = "Data"
22PRIORITY = 2100
23ICON = "icons/Discretize.svg"
24
25HELP = "docs/html/data/discretize.html"
26
27INPUTS = [("Data", orange.ExampleTable, "setData")]
28OUTPUTS = [("Data", orange.ExampleTable,)]
29
30WIDGET_CLASS = "OWDiscretize"
31
32
33def frange(low, up, steps):
34    inc=(up-low)/steps
35    return [low+i*inc for i in range(steps)]
36
37class DiscGraph(OWGraph):
38    def __init__(self, master, *args):
39        OWGraph.__init__(self, *args)
40        self.master=master
41
42        self.rugKeys = []
43        self.cutLineKeys = []
44        self.cutMarkerKeys = []
45        self.probCurveKey = None
46        self.baseCurveKey = None
47        self.lookaheadCurveKey = None
48
49        self.setAxisScale(QwtPlot.yRight, 0.0, 1.0, 0.0)
50        self.setYLaxisTitle("Split gain")
51        self.setXaxisTitle("Attribute value")
52        self.setYRaxisTitle("Class probability")
53        self.setShowYRaxisTitle(1)
54        self.setShowYLaxisTitle(1)
55        self.setShowXaxisTitle(1)
56        self.enableYLaxis(1)
57        self.enableYRaxis(1)
58
59        self.resolution=50
60        #self.setCursor(Qt.ArrowCursor)
61        #self.canvas().setCursor(Qt.ArrowCursor)
62
63        self.data = self.attr = self.contingency = None
64        self.minVal = self.maxVal = 0
65        self.curCutPoints=[]
66
67
68    def computeAddedScore(self, spoints):
69        candidateSplits = [x for x in frange(self.minVal, self.maxVal, self.resolution) if x not in spoints]
70        idisc = orange.IntervalDiscretizer(points = [-99999] + spoints)
71        var = idisc.constructVariable(self.data.domain[self.master.continuousIndices[self.master.selectedAttr]])
72        measure = self.master.measures[self.master.measure][1]
73        score=[]
74        chisq = self.master.measure == 2
75        for cut in candidateSplits:
76            idisc.points = spoints + [cut]
77            idisc.points.sort()
78            score.append(measure(var, self.data))
79
80        return candidateSplits, score
81
82
83    def invalidateBaseScore(self):
84        self.baseCurveX = self.baseCurveY = None
85
86
87    def computeLookaheadScore(self, split):
88        if self.data and self.data.domain.classVar:
89            self.lookaheadCurveX, self.lookaheadCurveY = self.computeAddedScore(list(self.curCutPoints) + [split])
90        else:
91            self.lookaheadCurveX = self.lookaheadCurveY = None
92
93
94    def clearAll(self):
95        for rug in self.rugKeys:
96            rug.detach()
97        self.rugKeys = []
98
99        if self.baseCurveKey:
100            self.baseCurveKey.detach()
101            self.baseCurveKey = None
102
103        if self.lookaheadCurveKey:
104            self.lookaheadCurveKey.detach()
105            self.lookaheadCurveKey = None
106
107        if self.probCurveKey:
108            self.probCurveKey.detach()
109            self.probCurveKey = None
110
111        for c in self.cutLineKeys:
112            c.detach()
113        self.cutLineKeys = []
114
115        for m in self.cutMarkerKeys:
116            m.detach()
117        self.cutMarkerKeys = []
118
119        self.replot()
120
121
122    def setData(self, attr, data):
123        self.clearAll()
124        self.attr, self.data = attr, data
125        self.curCutPoints = []
126
127        if not data or not attr:
128            self.snapDecimals = 1
129            self.probDist = None
130            return
131
132        if data.domain.classVar:
133            self.contingency = orange.ContingencyAttrClass(attr, data)
134            try:
135                self.condProb = orange.ConditionalProbabilityEstimatorConstructor_loess(
136                   self.contingency,
137                   nPoints=50)
138            except:
139                self.condProb = None
140            self.probDist = None
141            attrValues = self.contingency.keys()
142        else:
143            self.condProb = self.contingency = None
144            self.probDist = orange.Distribution(attr, data)
145            attrValues = self.probDist.keys()
146
147        if attrValues:
148            self.minVal, self.maxVal = min(attrValues), max(attrValues)
149        else:
150            self.minVal, self.maxVal = 0, 1
151        mdist = self.maxVal - self.minVal
152        if mdist > 1e-30:
153            self.snapDecimals = -int(math.ceil(math.log(mdist, 10)) -2)
154        else:
155            self.snapDecimals = 1
156
157        self.baseCurveX = None
158
159        self.plotRug(True)
160        self.plotProbCurve(True)
161        self.plotCutLines(True)
162
163        self.updateLayout()
164        self.replot()
165
166
167    def plotRug(self, noUpdate = False):
168        for rug in self.rugKeys:
169            rug.detach()
170        self.rugKeys = []
171
172        if self.master.showRug:
173            targetClass = self.master.targetClass
174
175            if self.contingency:
176                freqhigh = [(val, freq[targetClass]) for val, freq in self.contingency.items() if freq[targetClass] > 1e-6]
177                freqlow = [(val, freq.abs - freq[targetClass]) for val, freq in self.contingency.items()]
178                freqlow = [f for f in freqlow if f[1] > 1e-6]
179            elif self.probDist:
180                freqhigh = []
181                freqlow = self.probDist.items()
182            else:
183                return
184
185            if freqhigh:
186                maxf = max([f[1] for f in freqhigh])
187                if freqlow:
188                    maxf = max(maxf, max([f[1] for f in freqlow]))
189            elif freqlow:
190                maxf = max([f[1] for f in freqlow])
191            else:
192                return
193
194            freqfac = maxf > 1e-6 and .1 / maxf or 1
195
196            for val, freq in freqhigh:
197                c = self.addCurve("", Qt.gray, Qt.gray, 1, style = QwtPlotCurve.Lines, symbol = QwtSymbol.NoSymbol, xData = [val, val], yData = [1.0, 1.0 - max(.02, freqfac * freq)], autoScale = 1)
198                c.setYAxis(QwtPlot.yRight)
199                self.rugKeys.append(c)
200
201            for val, freq in freqlow:
202                c = self.addCurve("", Qt.gray, Qt.gray, 1, style = QwtPlotCurve.Lines, symbol = QwtSymbol.NoSymbol, xData = [val, val], yData = [0.04, 0.04 + max(.02, freqfac * freq)], autoScale = 1)
203                c.setYAxis(QwtPlot.yRight)
204                self.rugKeys.append(c)
205
206        if not noUpdate:
207            self.replot()
208
209
210    def plotBaseCurve(self, noUpdate = False):
211        if self.baseCurveKey:
212            self.baseCurveKey.detach()
213            self.baseCurveKey = None
214
215        if self.master.showBaseLine and self.master.resetIndividuals and self.data and self.data.domain.classVar and self.attr:
216            if not self.baseCurveX:
217                self.baseCurveX, self.baseCurveY = self.computeAddedScore(list(self.curCutPoints))
218
219            #self.setAxisOptions(QwtPlot.yLeft, self.master.measure == 3 and QwtAutoScale.Inverted or QwtAutoScale.None)
220            self.axisScaleEngine(QwtPlot.yLeft).setAttributes(self.master.measure == 3 and QwtScaleEngine.Inverted or QwtScaleEngine.NoAttribute)
221            #self.axisScaleEngine(QwtPlot.yLeft).setAttribute(QwtScaleEngine.Inverted, self.master.measure == 3)
222            self.baseCurveKey = self.addCurve("", Qt.black, Qt.black, 1, style = QwtPlotCurve.Lines, symbol = QwtSymbol.NoSymbol, xData = self.baseCurveX, yData = self.baseCurveY, lineWidth = 2, autoScale = 1)
223            self.baseCurveKey.setYAxis(QwtPlot.yLeft)
224
225        if not noUpdate:
226            self.replot()
227
228
229    def plotLookaheadCurve(self, noUpdate = False):
230        if self.lookaheadCurveKey:
231            self.lookaheadCurveKey.detach()
232            self.lookaheadCurveKey = None
233
234        if self.lookaheadCurveX and self.master.showLookaheadLine:
235            #self.setAxisOptions(QwtPlot.yLeft, self.master.measure == 3 and QwtAutoScale.Inverted or QwtAutoScale.None)
236            self.axisScaleEngine(QwtPlot.yLeft).setAttributes(self.master.measure == 3 and QwtScaleEngine.Inverted or QwtScaleEngine.NoAttribute)
237            self.lookaheadCurveKey = self.addCurve("", Qt.black, Qt.black, 1, style = QwtPlotCurve.Lines, symbol = QwtSymbol.NoSymbol, xData = self.lookaheadCurveX, yData = self.lookaheadCurveY, lineWidth = 1, autoScale = 1)
238            self.lookaheadCurveKey.setYAxis(QwtPlot.yLeft)
239            #self.lookaheadCurveKey.setVisible(1)
240
241        if not noUpdate:
242            self.replot()
243
244
245    def plotProbCurve(self, noUpdate = False):
246        if self.probCurveKey:
247            self.probCurveKey.detach()
248            self.probCurveKey = None
249
250        if self.contingency and self.condProb and self.master.showTargetClassProb:
251            xData = self.contingency.keys()[1:-1]
252            self.probCurveKey = self.addCurve("", Qt.gray, Qt.gray, 1, style = QwtPlotCurve.Lines, symbol = QwtSymbol.NoSymbol, xData = xData, yData = [self.condProb(x)[self.master.targetClass] for x in xData], lineWidth = 2, autoScale = 1)
253            self.probCurveKey.setYAxis(QwtPlot.yRight)
254
255        if not noUpdate:
256            self.replot()
257
258
259    def plotCutLines(self, noUpdate = False):
260        attr = self.data.domain[self.master.continuousIndices[self.master.selectedAttr]]
261        for c in self.cutLineKeys:
262            c.detach()
263        self.cutLineKeys = []
264
265        for m in self.cutMarkerKeys:
266            m.detach()
267        self.cutMarkerKeys = []
268
269        for cut in self.curCutPoints:
270            c = self.addCurve("", Qt.blue, Qt.blue, 1, style = QwtPlotCurve.Steps, symbol = QwtSymbol.NoSymbol, xData = [cut, cut], yData = [.9, 0.1], autoScale = 1)
271            c.setYAxis(QwtPlot.yRight)
272            self.cutLineKeys.append(c)
273
274            m = self.addMarker(str(attr(cut)), cut, .9, Qt.AlignCenter | Qt.AlignTop, bold=1)
275            m.setYAxis(QwtPlot.yRight)
276            self.cutMarkerKeys.append(m)
277        if not noUpdate:
278            self.replot()
279
280    def getCutCurve(self, cut):
281        ccc = self.transform(QwtPlot.xBottom, cut)
282        for i, c in enumerate(self.curCutPoints):
283            cc = self.transform(QwtPlot.xBottom, c)
284            if abs(cc-ccc)<3:
285                self.cutLineKeys[i].curveInd = i
286                return self.cutLineKeys[i]
287        return None
288
289
290    def setSplits(self, splits):
291        if self.data:
292            self.curCutPoints = splits
293
294            self.baseCurveX = None
295            self.plotBaseCurve()
296            self.plotCutLines()
297
298
299    def addCutPoint(self, cut):
300        self.curCutPoints.append(cut)
301        c = self.addCurve("", Qt.blue, Qt.blue, 1, style = QwtPlotCurve.Steps, symbol = QwtSymbol.NoSymbol, xData = [cut, cut], yData = [1.0, 0.015], autoScale = 1)
302        c.setYAxis(QwtPlot.yRight)
303        self.cutLineKeys.append(c)
304        c.curveInd = len(self.cutLineKeys) - 1
305        return c
306
307
308    def mousePressEvent(self, e):
309        if not self.data:
310            return
311
312        self.mouseCurrentlyPressed = 1
313
314        canvasPos = self.canvas().mapFrom(self, e.pos())
315        cut = self.invTransform(QwtPlot.xBottom, canvasPos.x())
316        curve = self.getCutCurve(cut)
317        if not curve and self.master.snap:
318            curve = self.getCutCurve(round(cut, self.snapDecimals))
319
320        if curve:
321            if e.button() == Qt.RightButton:
322                self.curCutPoints.pop(curve.curveInd)
323                self.plotCutLines(True)
324            else:
325                cut = self.curCutPoints.pop(curve.curveInd)
326                self.plotCutLines(True)
327                self.selectedCutPoint=self.addCutPoint(cut)
328        else:
329            self.selectedCutPoint=self.addCutPoint(cut)
330            self.plotCutLines(True)
331
332        self.baseCurveX = None
333        self.plotBaseCurve()
334        self.master.synchronizeIf()
335
336
337    def mouseMoveEvent(self, e):
338        if not self.data:
339            return
340
341        canvasPos = self.canvas().mapFrom(self, e.pos())
342
343        if self.mouseCurrentlyPressed:
344            if self.selectedCutPoint:
345                canvasPos = self.canvas().mapFrom(self, e.pos())
346                pos = self.invTransform(QwtPlot.xBottom, canvasPos.x())
347                if self.master.snap:
348                    pos = round(pos, self.snapDecimals)
349
350                if self.curCutPoints[self.selectedCutPoint.curveInd]==pos:
351                    return
352                if pos > self.maxVal or pos < self.minVal:
353                    self.curCutPoints.pop(self.selectedCutPoint.curveInd)
354                    self.baseCurveX = None
355                    self.plotCutLines(True)
356                    self.mouseCurrentlyPressed = 0
357                    return
358
359                self.curCutPoints[self.selectedCutPoint.curveInd] = pos
360                self.selectedCutPoint.setData([pos, pos], [.9, 0.1])
361
362                self.computeLookaheadScore(pos)
363                self.plotLookaheadCurve()
364                self.replot()
365
366                self.master.synchronizeIf()
367
368
369        elif self.getCutCurve(self.invTransform(QwtPlot.xBottom, canvasPos.x())):
370            self.canvas().setCursor(Qt.SizeHorCursor)
371        else:
372            self.canvas().setCursor(Qt.ArrowCursor)
373
374
375    def mouseReleaseEvent(self, e):
376        if not self.data:
377            return
378
379        self.mouseCurrentlyPressed = 0
380        self.selectedCutPoint = None
381        self.baseCurveX = None
382        self.plotBaseCurve()
383        self.plotCutLines(True)
384        self.master.synchronizeIf()
385        if self.lookaheadCurveKey and self.lookaheadCurveKey:
386            self.lookaheadCurveKey.setVisible(0)
387        self.replot()
388
389
390    def targetClassChanged(self):
391        self.plotRug()
392        self.plotProbCurve()
393
394
395class CustomListItemDelegate(QItemDelegate):
396    def paint(self, painter, option, index):
397        item = self.parent().itemFromIndex(index)
398        item.setText(item.name + item.master.indiLabels[item.labelIdx])
399        QItemDelegate.paint(self, painter, option, index)
400
401
402class ListItemWithLabel(QListWidgetItem):
403    def __init__(self, icon, name, labelIdx, master):
404        QListWidgetItem.__init__(self, icon, name)
405        self.name = name
406        self.master = master
407        self.labelIdx = labelIdx
408
409#    def paint(self, painter):
410#        btext = str(self.text())
411#        self.setText(btext + self.master.indiLabels[self.labelIdx])
412#        QListWidgetItem.paint(self, painter)
413#        self.setText(btext)
414
415
416class OWDiscretize(OWWidget):
417    settingsList=["autoApply", "measure", "showBaseLine", "showLookaheadLine", "showTargetClassProb", "showRug", "snap", "autoSynchronize", "resetIndividuals"]
418    contextHandlers = {"": PerfectDomainContextHandler("", ["targetClass", "discretization", "classDiscretization",
419                                                     "indiDiscretization", "intervals", "classIntervals", "indiIntervals",
420                                                     "outputOriginalClass", "indiData", "indiLabels", "resetIndividuals",
421                                                     "selectedAttr", "customSplits", "customClassSplits"])}
422
423    callbackDeposit=[]
424
425    D_N_METHODS = 5
426    D_LEAVE, D_ENTROPY, D_FREQUENCY, D_WIDTH, D_REMOVE = range(5)
427    D_NEED_N_INTERVALS = [2, 3]
428
429    def __init__(self, parent=None, signalManager=None, name="Interactive Discretization"):
430        OWWidget.__init__(self, parent, signalManager, name)
431        self.showBaseLine=1
432        self.showLookaheadLine=1
433        self.showTargetClassProb=1
434        self.showRug=0
435        self.snap=1
436        self.measure=0
437        self.targetClass=0
438        self.discretization = self.classDiscretization = self.indiDiscretization = 1
439        self.intervals = self.classIntervals = self.indiIntervals = 3
440        self.outputOriginalClass = True
441        self.indiData = []
442        self.indiLabels = []
443        self.resetIndividuals = 0
444        self.customClassSplits = ""
445
446        self.selectedAttr = 0
447        self.customSplits = ["", "", ""]
448        self.autoApply = True
449        self.dataChanged = False
450        self.autoSynchronize = True
451        self.pointsChanged = False
452
453        self.customLineEdits = []
454        self.needsDiscrete = []
455
456        self.data = self.originalData = None
457
458        self.loadSettings()
459
460        self.inputs=[("Data", ExampleTable, self.setData)]
461        self.outputs=[("Data", ExampleTable)]
462        self.measures=[("Information gain", orange.MeasureAttribute_info()),
463                       #("Gain ratio", orange.MeasureAttribute_gainRatio),
464                       ("Gini", orange.MeasureAttribute_gini()),
465                       ("chi-square", orange.MeasureAttribute_chiSquare()),
466                       ("chi-square prob.", orange.MeasureAttribute_chiSquare(computeProbabilities=1)),
467                       ("Relevance", orange.MeasureAttribute_relevance()),
468                       ("ReliefF", orange.MeasureAttribute_relief())]
469        self.discretizationMethods=["Leave continuous", "Entropy-MDL discretization", "Equal-frequency discretization", "Equal-width discretization", "Remove continuous attributes"]
470        self.classDiscretizationMethods=["Equal-frequency discretization", "Equal-width discretization"]
471        self.indiDiscretizationMethods=["Default", "Leave continuous", "Entropy-MDL discretization", "Equal-frequency discretization", "Equal-width discretization", "Remove attribute"]
472
473        self.mainHBox =  OWGUI.widgetBox(self.mainArea, orientation=0)
474
475        vbox = self.controlArea
476        box = OWGUI.radioButtonsInBox(vbox, self, "discretization", self.discretizationMethods[:-1], "Default discretization", callback=[self.clearLineEditFocus, self.defaultMethodChanged])
477        self.needsDiscrete.append(box.buttons[1])
478        box.setSizePolicy(QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed))
479        indent = OWGUI.checkButtonOffsetHint(self.needsDiscrete[-1])
480        self.interBox = OWGUI.widgetBox(OWGUI.indentedBox(box, sep=indent))
481        OWGUI.widgetLabel(self.interBox, "Number of intervals (for equal width/frequency)")
482        OWGUI.separator(self.interBox, height=4)
483        self.intervalSlider=OWGUI.hSlider(OWGUI.indentedBox(self.interBox), self, "intervals", None, 2, 10, callback=[self.clearLineEditFocus, self.defaultMethodChanged])
484        OWGUI.appendRadioButton(box, self, "discretization", self.discretizationMethods[-1])
485        OWGUI.separator(vbox)
486
487        ribg = OWGUI.radioButtonsInBox(vbox, self, "resetIndividuals", ["Use default discretization for all attributes", "Explore and set individual discretizations"], "Individual attribute treatment", callback = self.setAllIndividuals)
488        ll = QWidget(ribg)
489        ll.setFixedHeight(1)
490        OWGUI.widgetLabel(ribg, "Set discretization of all attributes to")
491        hcustbox = OWGUI.widgetBox(OWGUI.indentedBox(ribg), 0, 0)
492        for c in range(1, 4):
493            OWGUI.appendRadioButton(ribg, self, "resetIndividuals", "Custom %i" % c, insertInto = hcustbox)
494
495        OWGUI.separator(vbox)
496
497        box = self.classDiscBox = OWGUI.radioButtonsInBox(vbox, self, "classDiscretization", self.classDiscretizationMethods, "Class discretization", callback=[self.clearLineEditFocus, self.classMethodChanged])
498        cinterBox = OWGUI.widgetBox(box)
499        self.intervalSlider=OWGUI.hSlider(OWGUI.indentedBox(cinterBox, sep=indent), self, "classIntervals", None, 2, 10, callback=[self.clearLineEditFocus, self.classMethodChanged], label="Number of intervals")
500        hbox = OWGUI.widgetBox(box, orientation = 0)
501        OWGUI.appendRadioButton(box, self, "discretization", "Custom" + "  ", insertInto = hbox)
502        self.classCustomLineEdit = OWGUI.lineEdit(hbox, self, "customClassSplits", callback = self.classCustomChanged, focusInCallback = self.classCustomSelected)
503#        Can't validate - need to allow spaces
504        box.setSizePolicy(QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed))
505        OWGUI.separator(box)
506        self.classIntervalsLabel = OWGUI.widgetLabel(box, "Current splits: ")
507        OWGUI.separator(box)
508        OWGUI.checkBox(box, self, "outputOriginalClass", "Output original class", callback = self.commitIf)
509        OWGUI.widgetLabel(box, "("+"Widget always uses discretized class internally."+")")
510
511        OWGUI.separator(vbox)
512        #OWGUI.rubber(vbox)
513
514        box = OWGUI.widgetBox(vbox, "Commit")
515        applyButton = OWGUI.button(box, self, "Commit", callback = self.commit, default=True)
516        autoApplyCB = OWGUI.checkBox(box, self, "autoApply", "Commit automatically", callback=[self.clearLineEditFocus])
517        OWGUI.setStopper(self, applyButton, autoApplyCB, "dataChanged", self.commit)
518        OWGUI.rubber(vbox)
519
520        #self.mainSeparator = OWGUI.separator(self.mainHBox, width=25)        # space between control and main area
521        self.mainIABox =  OWGUI.widgetBox(self.mainHBox, "Individual attribute settings")
522        self.mainBox = OWGUI.widgetBox(self.mainIABox, orientation=0)
523        OWGUI.separator(self.mainIABox)#, height=30)
524        graphBox = OWGUI.widgetBox(self.mainIABox, "", orientation=0)
525       
526       
527#        self.needsDiscrete.append(graphBox)
528        graphOptBox = OWGUI.widgetBox(graphBox)
529        OWGUI.separator(graphBox, width=10)
530       
531        graphGraphBox = OWGUI.widgetBox(graphBox)
532        self.graph = DiscGraph(self, graphGraphBox)
533        graphGraphBox.layout().addWidget(self.graph)
534        reportButton2 = OWGUI.button(graphGraphBox, self, "Report Graph", callback = self.reportGraph, debuggingEnabled=0)
535
536        #graphOptBox.layout().setSpacing(4)
537        box = OWGUI.widgetBox(graphOptBox, "Split gain measure", addSpace=True)
538        self.measureCombo=OWGUI.comboBox(box, self, "measure", orientation=0, items=[e[0] for e in self.measures], callback=[self.clearLineEditFocus, self.graph.invalidateBaseScore, self.graph.plotBaseCurve])
539        OWGUI.checkBox(box, self, "showBaseLine", "Show discretization gain", callback=[self.clearLineEditFocus, self.graph.plotBaseCurve])
540        OWGUI.checkBox(box, self, "showLookaheadLine", "Show lookahead gain", callback=self.clearLineEditFocus)
541        self.needsDiscrete.append(box)
542
543        box = OWGUI.widgetBox(graphOptBox, "Target class", addSpace=True)
544        self.targetCombo=OWGUI.comboBox(box, self, "targetClass", orientation=0, callback=[self.clearLineEditFocus, self.graph.targetClassChanged])
545        stc = OWGUI.checkBox(box, self, "showTargetClassProb", "Show target class probability", callback=[self.clearLineEditFocus, self.graph.plotProbCurve])
546        OWGUI.checkBox(box, self, "showRug", "Show rug (may be slow)", callback=[self.clearLineEditFocus, self.graph.plotRug])
547        self.needsDiscrete.extend([self.targetCombo, stc])
548
549        box = OWGUI.widgetBox(graphOptBox, "Editing", addSpace=True)
550        OWGUI.checkBox(box, self, "snap", "Snap to grid", callback=[self.clearLineEditFocus])
551        syncCB = OWGUI.checkBox(box, self, "autoSynchronize", "Apply on the fly", callback=self.clearLineEditFocus)
552        syncButton = OWGUI.button(box, self, "Apply", callback = self.synchronizePressed)
553        OWGUI.setStopper(self, syncButton, syncCB, "pointsChanged", self.synchronize)
554        OWGUI.rubber(graphOptBox)
555
556        self.attrList = OWGUI.listBox(self.mainBox, self, callback = self.individualSelected)
557        self.attrList.setItemDelegate(CustomListItemDelegate(self.attrList))
558        self.attrList.setFixedWidth(300)
559
560        self.defaultMethodChanged()
561
562        OWGUI.separator(self.mainBox, width=10)
563        box = OWGUI.radioButtonsInBox(OWGUI.widgetBox(self.mainBox), self, "indiDiscretization", [], callback=[self.clearLineEditFocus, self.indiMethodChanged])
564        #hbbox = OWGUI.widgetBox(box)
565        #hbbox.layout().setSpacing(4)
566        for meth in self.indiDiscretizationMethods[:-1]:
567            OWGUI.appendRadioButton(box, self, "indiDiscretization", meth)
568        self.needsDiscrete.append(box.buttons[2])
569        self.indiInterBox = OWGUI.indentedBox(box, sep=indent, orientation = "horizontal")
570        OWGUI.widgetLabel(self.indiInterBox, "Num. of intervals: ")
571        self.indiIntervalSlider = OWGUI.hSlider(self.indiInterBox, self, "indiIntervals", None, 2, 10, callback=[self.clearLineEditFocus, self.indiMethodChanged], width = 100)
572        OWGUI.rubber(self.indiInterBox) 
573        OWGUI.appendRadioButton(box, self, "indiDiscretization", self.indiDiscretizationMethods[-1])
574        #OWGUI.rubber(hbbox)
575        #OWGUI.separator(box)
576        #hbbox = OWGUI.widgetBox(box)
577        for i in range(3):
578            hbox = OWGUI.widgetBox(box, orientation = "horizontal")
579            OWGUI.appendRadioButton(box, self, "indiDiscretization", "Custom %i" % (i+1) + " ", insertInto = hbox)
580            le = OWGUI.lineEdit(hbox, self, "", callback = lambda w=i: self.customChanged(w), focusInCallback = lambda w=i: self.customSelected(w))
581            le.setFixedWidth(110)
582            self.customLineEdits.append(le)
583            OWGUI.toolButton(hbox, self, "CC", width=30, callback = lambda w=i: self.copyToCustom(w))
584            OWGUI.rubber(hbox)
585        OWGUI.rubber(box)
586
587        #self.controlArea.setFixedWidth(0)
588
589        self.contAttrIcon =  self.createAttributeIconDict()[orange.VarTypes.Continuous]
590       
591        self.setAllIndividuals()
592
593
594
595    def setData(self, data=None):
596        self.closeContext()
597
598        self.indiData = []
599        self.attrList.clear()
600        for le in self.customLineEdits:
601            le.clear()
602        self.indiDiscretization = 0
603
604        self.originalData = data
605        haveClass = bool(data and data.domain.classVar)
606        continuousClass = haveClass and data.domain.classVar.varType == orange.VarTypes.Continuous
607
608        self.data = self.originalData
609        if continuousClass:
610            if not self.discretizeClass():
611                self.data = self.discClassData = None
612                self.warning(0)
613                self.error(0, "Cannot discretize the class")
614        else:
615            self.data = self.originalData
616            self.discClassData = None
617
618        for c in self.needsDiscrete:
619            c.setVisible(haveClass)
620
621        if self.data:
622            domain = self.data.domain
623            self.continuousIndices = [i for i, attr in enumerate(domain.attributes) if attr.varType == orange.VarTypes.Continuous]
624            if not self.continuousIndices:
625                self.data = None
626
627        self.classDiscBox.setEnabled(not data or continuousClass)
628        if self.data:
629            for i, attr in enumerate(domain.attributes):
630                if attr.varType == orange.VarTypes.Continuous:
631                    self.attrList.addItem(ListItemWithLabel(self.contAttrIcon, attr.name, self.attrList.count(), self))
632                    self.indiData.append([0, 4, "", "", ""])
633                else:
634                    self.indiData.append(None)
635
636            self.fillClassCombo()
637            self.indiLabels = [""] * self.attrList.count()
638
639            self.graph.setData(None, self.data)
640            self.selectedAttr = 0
641            self.openContext("", data)
642#            if self.classDiscretization == 2:
643#                self.discretizeClass()
644
645            # Prevent entropy discretization with non-discrete class
646            if not haveClass:
647                if self.discretization == self.D_ENTROPY:
648                    self.discretization = self.D_FREQUENCY
649                # Say I'm overcautious if you will, but you haven't seen as much as I did :)
650                if not haveClass:
651                    if self.indiDiscretization-1 == self.D_ENTROPY:
652                        self.indiDiscretization = 0
653                    for indiData in self.indiData:
654                        if indiData and indiData[0] == self.D_ENTROPY:
655                            indiData[0] = 0
656
657            self.computeDiscretizers()
658            self.attrList.setCurrentItem(self.attrList.item(self.selectedAttr))
659        else:
660            self.targetCombo.clear()
661            self.graph.setData(None, None)
662
663#        self.graph.setData(self.data)
664
665        self.makeConsistent()
666
667        # this should be here because 'resetIndividuals' is a context setting
668        self.showHideIndividual()
669
670        self.commit()
671
672
673    def fillClassCombo(self):
674        self.targetCombo.clear()
675
676        if not self.data or not self.data.domain.classVar:
677            return
678
679        domain = self.data.domain
680        for v in domain.classVar.values:
681            self.targetCombo.addItem(str(v))
682        if self.targetClass<len(domain.classVar.values):
683            self.targetCombo.setCurrentIndex(self.targetClass)
684        else:
685            self.targetCombo.setCurrentIndex(0)
686            self.targetClass=0
687
688    def classChanged(self):
689        self.fillClassCombo()
690        self.computeDiscretizers()
691
692
693    def clearLineEditFocus(self):
694        if self.data:
695            df = self.indiDiscretization
696            for le in self.customLineEdits:
697                if le.hasFocus():
698                    le.clearFocus()
699            self.indiDiscretization = self.indiData[self.continuousIndices[self.selectedAttr]][0] = df
700            if self.classCustomLineEdit.hasFocus():
701                self.classCustomLineEdit.clearFocus()
702
703
704
705    def individualSelected(self):
706        if not self.data:
707            return
708
709        if self.attrList.selectedItems() == []: return
710        self.selectedAttr = self.attrList.row(self.attrList.selectedItems()[0])
711        attrIndex = self.continuousIndices[self.selectedAttr]
712        attr = self.data.domain[attrIndex]
713        indiData = self.indiData[attrIndex]
714
715        self.customSplits = indiData[2:]
716        for le, cs in zip(self.customLineEdits, self.customSplits):
717            le.setText(" ".join(cs))
718
719        self.indiDiscretization, self.indiIntervals = indiData[:2]
720        self.indiInterBox.setEnabled(self.indiDiscretization-1 in self.D_NEED_N_INTERVALS)
721
722        self.graph.setData(attr, self.data)
723        if hasattr(self, "discretizers"):
724            self.graph.setSplits(self.discretizers[attrIndex] and self.discretizers[attrIndex].getValueFrom.transformer.points or [])
725        else:
726            self.graph.plotBaseCurve(False)
727
728
729    def computeDiscretizers(self):
730        self.discretizers = []
731
732        if not self.data:
733            return
734
735        self.discretizers = [None] * len(self.data.domain)
736        for i, idx in enumerate(self.continuousIndices):
737            self.computeDiscretizer(i, idx)
738
739        self.commitIf()
740
741
742    def makeConsistent(self):
743        self.interBox.setEnabled(self.discretization in self.D_NEED_N_INTERVALS)
744        self.indiInterBox.setEnabled(self.indiDiscretization-1 in self.D_NEED_N_INTERVALS)
745
746
747    def defaultMethodChanged(self):
748        self.interBox.setEnabled(self.discretization in self.D_NEED_N_INTERVALS)
749
750        if not self.data:
751            return
752
753        for i, idx in enumerate(self.continuousIndices):
754            self.computeDiscretizer(i, idx, True)
755
756        self.commitIf()
757
758    def classMethodChanged(self):
759        if not self.data:
760            return
761
762        self.discretizeClass()
763        self.classChanged()
764        attrIndex = self.continuousIndices[self.selectedAttr]
765        self.graph.setData(self.data.domain[attrIndex], self.data)
766        self.graph.setSplits(self.discretizers[attrIndex] and self.discretizers[attrIndex].getValueFrom.transformer.points or [])
767        if self.targetClass > len(self.data.domain.classVar.values):
768            self.targetClass = len(self.data.domain.classVar.values)-1
769
770
771    def indiMethodChanged(self, dontSetACustom=False):
772        if self.data:
773            i, idx = self.selectedAttr, self.continuousIndices[self.selectedAttr]
774            self.indiData[idx][0] = self.indiDiscretization
775            self.indiData[idx][1] = self.indiIntervals
776
777            self.indiInterBox.setEnabled(self.indiDiscretization-1 in self.D_NEED_N_INTERVALS)
778            if self.indiDiscretization and self.indiDiscretization - self.D_N_METHODS != self.resetIndividuals - 1:
779                self.resetIndividuals = 1
780
781            if not self.data:
782                return
783
784            which = self.indiDiscretization - self.D_N_METHODS - 1
785            if not dontSetACustom and which >= 0 and not self.customSplits[which]:
786                attr = self.data.domain[idx]
787                splitsTxt = self.indiData[idx][2+which] = [str(attr(x)) for x in self.graph.curCutPoints]
788                self.customSplits[which] = splitsTxt # " ".join(splitsTxt)
789                self.customLineEdits[which].setText(" ".join(splitsTxt))
790                self.computeDiscretizer(i, idx)
791            else:
792                self.computeDiscretizer(i, idx)
793
794            self.commitIf()
795
796
797    def customSelected(self, which):
798        if self.data and self.indiDiscretization != self.D_N_METHODS + which + 1: # added 1 - we need it, right?
799            self.indiDiscretization = self.D_N_METHODS + which + 1
800            idx = self.continuousIndices[self.selectedAttr]
801            attr = self.data.domain[idx]
802            self.indiMethodChanged()
803
804
805    def showHideIndividual(self):
806        if not self.resetIndividuals:
807                self.mainArea.hide()
808        elif self.mainArea.isHidden():
809            self.graph.plotBaseCurve()
810            self.mainArea.show()
811        qApp.processEvents()
812        QTimer.singleShot(0, self.adjustSize)
813
814    def setAllIndividuals(self):
815        self.showHideIndividual()
816
817        if not self.data:
818            return
819
820        self.clearLineEditFocus()
821        method = self.resetIndividuals
822        if method == 1:
823            return
824        if method:
825            method += self.D_N_METHODS - 1
826        for i, idx in enumerate(self.continuousIndices):
827            if self.indiData[idx][0] != method:
828                self.indiData[idx][0] = method
829                if i == self.selectedAttr:
830                    self.indiDiscretization = method
831                    self.indiMethodChanged(True) # don't set a custom
832                    if method:
833                        self.computeDiscretizer(i, idx)
834                else:
835                    self.computeDiscretizer(i, idx)
836
837        self.attrList.reset()
838        self.commitIf()
839
840
841    def customChanged(self, which):
842        if not self.data:
843            return
844
845        idx = self.continuousIndices[self.selectedAttr]
846        le = self.customLineEdits[which]
847
848        content = str(le.text()).replace(":", " ").replace(",", " ").split()
849        content = dict.fromkeys(content).keys()  # remove duplicates (except 8.0, 8.000 ...)
850        try:
851            content.sort(lambda x, y:cmp(float(x), float(y)))
852        except:
853            content = str(le.text())
854
855        le.setText(" ".join(content))
856        self.customSplits[which] = content
857        self.indiData[idx][which+2] = content
858
859        self.indiData[idx][0] = self.indiDiscretization = which + self.D_N_METHODS + 1
860
861        self.computeDiscretizer(self.selectedAttr, self.continuousIndices[self.selectedAttr])
862        self.commitIf()
863
864
865    def copyToCustom(self, which):
866        self.clearLineEditFocus()
867        if not self.data:
868            return
869
870        idx = self.continuousIndices[self.selectedAttr]
871
872        if self.indiDiscretization >= self.D_N_METHODS + 1:
873            splits = self.customSplits[self.indiDiscretization - self.D_N_METHODS - 1]
874            try:
875                valid = bool([float(i) for i in self.customSplits[which]].split())
876            except:
877                valid = False
878        else:
879            valid = False
880
881        if not valid:
882            attr = self.data.domain[idx]
883            splits = list(self.discretizers[idx] and self.discretizers[idx].getValueFrom.transformer.points or [])
884            splits = [str(attr(i)) for i in splits]
885
886        self.indiData[idx][2+which] = self.customSplits[which] = splits
887        self.customLineEdits[which].setText(" ".join(splits))
888#        self.customSelected(which)
889
890
891    # This weird construction of the list is needed for easier translation into other languages
892    shortDiscNames = [""] + [" (%s)" % x for x in ("leave continuous", "entropy", "equal frequency", "equal width", "removed")] + [(" ("+"custom %i"+")") % x for x in range(1, 4)]
893    # This one is used for reports
894    shortDiscNamesUnpar = ("", "leave continuous", "entropy", "equal frequency", "equal width", "removed", "custom", "custom", "custom")
895
896    def computeDiscretizer(self, i, idx, onlyDefaults=False):
897        attr = self.data.domain[idx]
898        indiData = self.indiData[idx]
899
900        discType, intervals = indiData[:2]
901        discName = self.shortDiscNames[discType]
902
903        defaultUsed = not discType
904
905        if defaultUsed:
906            discType = self.discretization+1
907            intervals = self.intervals
908
909        if discType >= self.D_N_METHODS + 1:
910
911            try:
912                customs = [float(r) for r in indiData[discType-self.D_N_METHODS+1]]
913            except:
914                customs = []
915
916            if not customs:
917                discType = self.discretization+1
918                intervals = self.intervals
919                discName = "%s ->%s)" % (self.shortDiscNames[indiData[0]][:-1], self.shortDiscNames[discType][2:-1])
920                defaultUsed = True
921
922        if onlyDefaults and not defaultUsed:
923            return
924
925        discType -= 1
926        try:
927            if discType == self.D_LEAVE: # leave continuous
928                discretizer = None
929            elif discType == self.D_ENTROPY:
930                discretizer = orange.EntropyDiscretization(attr, self.data)
931            elif discType == self.D_FREQUENCY:
932                discretizer = orange.EquiNDiscretization(attr, self.data, numberOfIntervals = intervals)
933            elif discType == self.D_WIDTH:
934                discretizer = orange.EquiDistDiscretization(attr, self.data, numberOfIntervals = intervals)
935            elif discType == self.D_REMOVE:
936                discretizer = False
937            else:
938                discretizer = orange.IntervalDiscretizer(points = customs).constructVariable(attr)
939        except:
940            discretizer = False
941
942
943        self.discretizers[idx] = discretizer
944
945        if discType == self.D_LEAVE:
946            discInts = ""
947        elif discType == self.D_REMOVE:
948            discInts = ""
949        elif not discretizer:
950            discInts = ": "+"<can't discretize>"
951        else:
952            points = discretizer.getValueFrom.transformer.points
953            discInts = points and (": " + ", ".join([str(attr(x)) for x in points])) or ": "+"<removed>"
954        self.indiLabels[i] = discInts + discName
955        self.attrList.reset()
956
957        if i == self.selectedAttr:
958            self.graph.setSplits(discretizer and discretizer.getValueFrom.transformer.points or [])
959
960
961
962    def discretizeClass(self):
963        if self.originalData:
964            discType = self.classDiscretization
965            classVar = self.originalData.domain.classVar
966
967            if discType == 2:
968                try:
969                    content = self.customClassSplits.replace(":", " ").replace(",", " ").replace("-", " ").split()
970                    customs = dict.fromkeys([float(x) for x in content]).keys()  # remove duplicates (except 8.0, 8.000 ...)
971                    customs.sort()
972                except:
973                    customs = []
974
975                if not customs:
976                    discType = 0
977
978            try:
979                if discType == 0:
980                    discretizer = orange.EquiNDiscretization(classVar, self.originalData, numberOfIntervals = self.classIntervals)
981                elif discType == 1:
982                    discretizer = orange.EquiDistDiscretization(classVar, self.originalData, numberOfIntervals = self.classIntervals)
983                else:
984                    discretizer = orange.IntervalDiscretizer(points = customs).constructVariable(classVar)
985
986                self.discClassData = orange.ExampleTable(orange.Domain(self.originalData.domain.attributes, discretizer), self.originalData)
987                if self.data:
988                    self.data = self.discClassData
989                # else, the data has no continuous attributes other then the class
990
991                self.classIntervalsLabel.setText("Current splits: " + ", ".join([str(classVar(x)) for x in discretizer.getValueFrom.transformer.points]))
992                self.error(0)
993                self.warning(0)
994                return True
995            except:
996                if self.data:
997                    self.warning(0, "Cannot discretize the class; using previous class")
998                else:
999                    self.error(0, "Cannot discretize the class")
1000                self.classIntervalsLabel.setText("")
1001                return False
1002
1003
1004    def classCustomChanged(self):
1005        self.classMethodChanged()
1006
1007    def classCustomSelected(self):
1008        if self.classDiscretization != 2: # prevent a cycle (this function called by setFocus at its end)
1009            self.classDiscretization = 2
1010            self.classMethodChanged()
1011            self.classCustomLineEdit.setFocus()
1012
1013    def discretize(self):
1014        if not self.data:
1015            return
1016
1017
1018    def synchronizeIf(self):
1019        if self.autoSynchronize:
1020            self.synchronize()
1021        else:
1022            self.pointsChanged = True
1023
1024    def synchronizePressed(self):
1025        self.clearLineEditFocus()
1026        self.synchronize()
1027
1028    def synchronize(self):
1029        if not self.data:
1030            return
1031
1032        slot = self.indiDiscretization - self.D_N_METHODS - 1
1033        if slot < 0:
1034            for slot in range(3):
1035                if not self.customLineEdits[slot].text():
1036                    break
1037            else:
1038                slot = 0
1039            self.indiDiscretization = slot + self.D_N_METHODS + 1
1040
1041        idx = self.continuousIndices[self.selectedAttr]
1042        attr = self.data.domain[idx]
1043        cp = list(self.graph.curCutPoints)
1044        cp.sort()
1045        splits = [str(attr(i)) for i in cp]
1046        splitsTxt = " ".join(splits)
1047        self.indiData[idx][0] = self.indiDiscretization
1048        self.indiData[idx][2+slot] = self.customSplits[slot] = splits
1049        self.customLineEdits[slot].setText(splitsTxt)
1050
1051        discretizer = orange.IntervalDiscretizer(points = cp).constructVariable(attr)
1052        self.discretizers[idx] = discretizer
1053
1054        self.indiLabels[self.selectedAttr] = ": " + splitsTxt + self.shortDiscNames[self.indiDiscretization]
1055        self.attrList.reset()
1056
1057        self.pointsChanged = False
1058        self.commitIf()
1059
1060
1061    def commitIf(self):
1062        if self.autoApply:
1063            self.commit()
1064        else:
1065            self.dataChanged = True
1066
1067    def commit(self):
1068        self.clearLineEditFocus()
1069
1070        if self.data:
1071            newattrs=[]
1072            for attr, disc in zip(self.data.domain.attributes, self.discretizers):
1073                if disc:
1074                    if disc.getValueFrom.transformer.points:
1075                        newattrs.append(disc)
1076                elif disc == None:  # can also be False -> remove
1077                    newattrs.append(attr)
1078
1079            if self.data.domain.classVar:
1080                if self.outputOriginalClass:
1081                    newdomain = orange.Domain(newattrs, self.originalData.domain.classVar)
1082                else:
1083                    newdomain = orange.Domain(newattrs, self.data.domain.classVar)
1084            else:
1085                newdomain = orange.Domain(newattrs, None)
1086
1087            newdata = orange.ExampleTable(newdomain, self.originalData)
1088
1089        elif self.discClassData and self.outputOriginalClass:
1090            newdata = self.discClassData
1091
1092        elif self.originalData and not (self.originalData.domain.classVar and self.originalData.domain.classVar.varType == orange.VarTypes.Continuous and not self.discClassData):  # no continuous attributes...
1093            newdata = self.originalData
1094        else:
1095            newdata = None
1096
1097        self.send("Data", newdata)
1098        dataChanged = False
1099
1100
1101    def sendReport(self):
1102        self.reportData(self.data, "Input data")
1103       
1104        settings = [("Default method", self.shortDiscNamesUnpar[self.discretization+1])]
1105        if 3 <= self.discretization <= 4:
1106            settings.append(("Number of intervals", str(self.intervals)))
1107        self.reportSettings("Settings", settings)
1108       
1109        attrs = []
1110        if self.data:
1111            for i, (attr, disc) in enumerate(zip(self.data.domain.attributes, self.discretizers)):
1112                if disc:
1113                    discType, intervals = self.indiData[i][:2]
1114                    cutpoints = ", ".join(str(attr(x)) for x in disc.getValueFrom.transformer.points)
1115                    if not cutpoints:
1116                        attrs.append((attr.name, "removed"))
1117                    elif not discType:
1118                        attrs.append((attr.name, cutpoints))
1119                    else:
1120                        attrs.append((attr.name, "%s (%s)" % (cutpoints, self.shortDiscNamesUnpar[discType])))
1121                elif disc == None:
1122                    if attr.varType == orange.VarTypes.Continuous:
1123                        attrs.append((attr.name, "left continuous"))
1124                    else:
1125                        attrs.append((attr.name, "already discrete"))
1126            classVar = self.data.domain.classVar
1127            if classVar:
1128                if classVar.varType == orange.VarTypes.Continuous:
1129                    attrs.append(("Class ('%s')" % classVar.name, "%s (%s)" % (self.classIntervalsLabel,
1130                                  ["equal frequency", "equal width", "custom"][self.classDiscretization])))
1131                    attrs.append(("Output discretized class", OWGUI.YesNo[self.outputOriginalClass]))
1132        self.reportSettings("Attributes", attrs)
1133
1134
1135    def reportGraph(self):
1136        try:
1137            attrName = self.data.domain[self.continuousIndices[self.selectedAttr]].name
1138        except:
1139            return
1140        self.reportSettings("Discretization Graph", 
1141                            [("Attribute", attrName),
1142                             ("Gain measure", self.measures[self.measure][0]),
1143                             ("Target class", self.data.domain.classVar.values[self.targetClass])])
1144        self.reportRaw("<br/>")
1145        self.reportImage(self.graph.saveToFileDirect, QSize(400, 300))
1146        self.finishReport()
1147
1148               
1149import sys
1150if __name__=="__main__":
1151    app=QApplication(sys.argv)
1152    w=OWDiscretize()
1153    w.show()
1154#    d=orange.ExampleTable("../../doc/datasets/bridges.tab")
1155#    d=orange.ExampleTable("../../doc/datasets/auto-mpg.tab")
1156    d = orange.ExampleTable("../../doc/datasets/iris.tab")
1157#    d = orange.ExampleTable(r"E:\Development\Orange Datasets\UCI\iris.tab")
1158    w.setData(d)
1159    #w.setData(None)
1160    #w.setData(d)
1161    app.exec_()
1162    w.saveSettings()
Note: See TracBrowser for help on using the repository browser.