source: orange/Orange/OrangeWidgets/Data/OWDiscretize.py @ 11748:467f952c108d

Revision 11748:467f952c108d, 45.3 KB checked in by blaz <blaz.zupan@…>, 6 months ago (diff)

Changes in headers, widget descriptions text.

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