source: orange/orange/OrangeWidgets/Visualize Qt/OWDiscretizeQt.py @ 9546:2b6cc6f397fe

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