source: orange/orange/OrangeWidgets/Data/OWDiscretize.py @ 9546:2b6cc6f397fe

Revision 9546:2b6cc6f397fe, 45.1 KB checked in by ales_erjavec <ales.erjavec@…>, 2 years ago (diff)

Renamed widget channel names in line with the new naming rules/convention.
Added backwards compatibility in orngDoc loadDocument to enable loading of schemas saved before the change.

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