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

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

Changes in headers, widget descriptions text.

Line 
1import OWGUI
2import Orange
3import pkg_resources
4from collections import namedtuple
5from functools import partial
6from OWWidget import *
7from Orange.feature import scoring
8from Orange.classification import svm
9from Orange.ensemble import forest
10
11NAME = "Rank"
12DESCRIPTION = "Ranks and filters data features by their relevance."
13LONG_DESCRIPTION = ""
14ICON = "icons/Rank.svg"
15PRIORITY = 1102
16AUTHOR = "Janez Demsar"
17AUTHOR_EMAIL = "janez.demsar(@at@)fri.uni-lj.si"
18INPUTS = [("Data", Orange.data.Table, "setData")]
19OUTPUTS = [("Reduced Data", Orange.data.Table, Default + Single)]
20
21
22def is_discrete(var):
23    return isinstance(var, Orange.feature.Discrete)
24
25
26def is_continuous(var):
27    return isinstance(var, Orange.feature.Continuous)
28
29
30def is_class_discrete(data):
31    return is_discrete(data.domain.class_var)
32
33
34def is_class_continuous(data):
35    return is_continuous(data.domain.class_var)
36
37
38def table(shape, fill=None):
39    """ Return a 2D table with shape filed with ``fill``
40    """
41    return [[fill for j in range(shape[1])] for i in range(shape[0])]
42
43
44MEASURE_PARAMS = {
45    scoring.Relief: [
46        {"name": "k",
47         "type": int,
48         "display_name": "Neighbours",
49         "range": (1, 20),
50         "default": 10,
51         "doc": "Number of neighbors to consider."},
52        {"name":"m",
53         "type": int,
54         "display_name": "Examples",
55         "range": (20, 100),
56         "default": 20,
57         "doc": ""}
58        ],
59    forest.ScoreFeature: [
60        {"name": "trees",
61         "type": int,
62         "display_name": "Num. of trees",
63         "range": (20, 100),
64         "default": 100,
65         "doc": "Number of trees in the random forest."}
66        ]
67    }
68
69
70_score_meta = namedtuple(
71    "_score_meta",
72    ["name",
73     "shortname",
74     "score",
75     "params",
76     "supports_regression",
77     "supports_classification",
78     "handles_discrete",
79     "handles_continuous"]
80)
81
82
83class score_meta(_score_meta):
84    # Add sensible defaults to __new__
85    def __new__(cls, name, shortname, score, params=None,
86                supports_regression=True, supports_classification=True,
87                handles_continuous=True, handles_discrete=True):
88        return _score_meta.__new__(
89            cls, name, shortname, score, params,
90            supports_regression, supports_classification,
91            handles_discrete, handles_continuous
92        )
93
94
95# Default scores.
96SCORES = [
97    score_meta(
98        "ReliefF", "ReliefF", scoring.Relief,
99        params=MEASURE_PARAMS[scoring.Relief],
100        handles_continuous=True,
101        handles_discrete=True),
102    score_meta(
103        "Information Gain", "Inf. gain", scoring.InfoGain,
104        params=None,
105        supports_regression=False,
106        supports_classification=True,
107        handles_continuous=False,
108        handles_discrete=True),
109    score_meta(
110        "Gain Ratio", "Gain Ratio", scoring.GainRatio,
111        params=None,
112        supports_regression=False,
113        handles_continuous=False,
114        handles_discrete=True),
115    score_meta(
116        "Gini Gain", "Gini", scoring.Gini,
117        params=None,
118        supports_regression=False,
119        supports_classification=True,
120        handles_continuous=False),
121    score_meta(
122        "Log Odds Ratio", "log OR", Orange.core.MeasureAttribute_logOddsRatio,
123        params=None,
124        supports_regression=False,
125        handles_continuous=False),
126    score_meta(
127        "MSE", "MSE", scoring.MSE,
128        params=None,
129        supports_classification=False,
130        handles_continuous=False),
131    score_meta(
132        "Linear SVM Weights", "SVM weight", svm.ScoreSVMWeights,
133        params=None),
134    score_meta(
135        "Random Forests", "RF", forest.ScoreFeature,
136        params=MEASURE_PARAMS[forest.ScoreFeature]),
137]
138
139_DEFAULT_SELECTED = set(m.name for m in SCORES[:6])
140
141
142class MethodParameter(object):
143    def __init__(self, name="", type=None, display_name="Parameter",
144                 range=None, default=None, doc=""):
145        self.name = name
146        self.type = type
147        self.display_name = display_name
148        self.range = range
149        self.default = default
150        self.doc = doc
151
152
153def measure_parameters(measure):
154    return [MethodParameter(**args) for args in (measure.params or [])]
155
156
157def param_attr_name(measure, param):
158    """Name of the OWRank widget's member where the parameter is stored.
159    """
160    return "param_" + measure.__name__ + "_" + param.name
161
162
163def drop_exceptions(iterable, exceptions=(Exception,)):
164    iterable = iter(iterable)
165    while True:
166        try:
167            yield next(iterable)
168        except StopIteration:
169            raise
170        except BaseException as ex:
171            if not isinstance(ex, exceptions):
172                raise
173
174
175def load_ep_drop_exceptions(entry_point):
176    for ep in pkg_resources.iter_entry_points(entry_point):
177        try:
178            yield ep.load()
179        except Exception:
180            log = logging.getLogger(__name__)
181            log.debug("", exc_info=True)
182
183
184def all_measures():
185    iter_ep = load_ep_drop_exceptions("orange.widgets.feature_score")
186    scores = [m for m in iter_ep if isinstance(m, score_meta)]
187    return SCORES + scores
188
189
190class OWRank(OWWidget):
191    settingsList = [
192        "nDecimals", "nIntervals", "sortBy", "nSelected",
193        "selectMethod", "autoApply", "showDistributions",
194        "distColorRgb", "selectedMeasures"
195    ]
196
197    def __init__(self, parent=None, signalManager=None):
198        OWWidget.__init__(self, parent, signalManager, "Rank")
199
200        self.inputs = [("Data", ExampleTable, self.setData)]
201        self.outputs = [("Reduced Data", ExampleTable, Default + Single)]
202
203        self.nDecimals = 3
204        self.nIntervals = 4
205        self.sortBy = 2
206        self.selectMethod = 2
207        self.nSelected = 5
208        self.autoApply = True
209        self.showDistributions = 1
210        self.distColorRgb = (220, 220, 220, 255)
211        self.distColor = QColor(*self.distColorRgb)
212
213        self.all_measures = all_measures()
214
215        self.selectedMeasures = dict(
216            [(name, True) for name in _DEFAULT_SELECTED] +
217            [(m.name, False)
218             for m in self.all_measures[len(_DEFAULT_SELECTED):]]
219        )
220
221        self.data = None
222
223        self.methodParamAttrs = []
224        for m in self.all_measures:
225            params = measure_parameters(m)
226            for p in params:
227                name_mangled = param_attr_name(m.score, p)
228                setattr(self, name_mangled, p.default)
229                self.methodParamAttrs.append(name_mangled)
230
231        self.settingsList = self.settingsList + self.methodParamAttrs
232
233        self.loadSettings()
234
235        self.discMeasures = [m for m in self.all_measures
236                             if m.supports_classification]
237        self.contMeasures = [m for m in self.all_measures
238                             if m.supports_regression]
239
240        self.stackedLayout = QStackedLayout()
241        self.stackedLayout.setContentsMargins(0, 0, 0, 0)
242        self.stackedWidget = OWGUI.widgetBox(self.controlArea, margin=0,
243                                             orientation=self.stackedLayout,
244                                             addSpace=True)
245
246        # Discrete class scoring
247        discreteBox = OWGUI.widgetBox(self.stackedWidget, "Scoring",
248                                      addSpace=False,
249                                      addToLayout=False)
250        self.stackedLayout.addWidget(discreteBox)
251
252        # Continuous class scoring
253        continuousBox = OWGUI.widgetBox(self.stackedWidget, "Scoring",
254                                        addSpace=False,
255                                        addToLayout=False)
256        self.stackedLayout.addWidget(continuousBox)
257
258        def measure_control(container, measure):
259            """Construct UI control for `measure` (measure_meta instance).
260            """
261            name = measure.name
262            params = measure_parameters(measure)
263            if params:
264                hbox = OWGUI.widgetBox(container, orientation="horizontal")
265                OWGUI.checkBox(hbox, self.selectedMeasures, name, name,
266                               callback=partial(self.measuresSelectionChanged,
267                                                measure),
268                               tooltip="Enable " + name)
269
270                smallWidget = OWGUI.SmallWidgetLabel(
271                    hbox, pixmap=1, box=name + " Parameters",
272                    tooltip="Show " + name + "Parameters")
273
274                for param in params:
275                    OWGUI.spin(smallWidget.widget, self,
276                               param_attr_name(measure.score, param),
277                               param.range[0], param.range[-1],
278                               label=param.display_name,
279                               tooltip=param.doc,
280                               callback=partial(
281                                   self.measureParamChanged, measure, param),
282                               callbackOnReturn=True)
283
284                OWGUI.button(smallWidget.widget, self, "Load defaults",
285                             callback=partial(self.loadMeasureDefaults,
286                                              measure))
287            else:
288                OWGUI.checkBox(container, self.selectedMeasures, name, name,
289                               callback=partial(self.measuresSelectionChanged,
290                                                measure),
291                               tooltip="Enable " + name)
292
293        for measure in self.all_measures:
294            if measure.supports_classification:
295                measure_control(discreteBox, measure)
296
297            if measure.supports_regression:
298                measure_control(continuousBox, measure)
299
300        OWGUI.comboBox(
301            discreteBox, self, "sortBy", label="Sort by  ",
302            items=["No Sorting", "Attribute Name", "Number of Values"] +
303                  [m.name for m in self.discMeasures],
304            orientation=0, valueType=int,
305            callback=self.sortingChanged)
306
307        OWGUI.comboBox(
308            continuousBox, self, "sortBy", label="Sort by  ",
309            items=["No Sorting", "Attribute Name", "Number of Values"] +
310                  [m.name for m in self.contMeasures],
311            orientation=0, valueType=int,
312            callback=self.sortingChanged)
313
314        box = OWGUI.widgetBox(self.controlArea, "Discretization",
315                              addSpace=True)
316        OWGUI.spin(box, self, "nIntervals", 2, 20,
317                   label="Intervals: ",
318                   orientation=0,
319                   tooltip="Disctetization for measures which cannot score "
320                           "continuous attributes.",
321                   callback=self.discretizationChanged,
322                   callbackOnReturn=True)
323
324        box = OWGUI.widgetBox(self.controlArea, "Precision", addSpace=True)
325        OWGUI.spin(box, self, "nDecimals", 1, 6, label="No. of decimals: ",
326                   orientation=0, callback=self.decimalsChanged)
327
328        box = OWGUI.widgetBox(self.controlArea, "Score bars",
329                              orientation="horizontal", addSpace=True)
330        self.cbShowDistributions = OWGUI.checkBox(
331            box, self, "showDistributions", 'Enable',
332            callback=self.showDistributionsChanged
333        )
334
335        OWGUI.rubber(box)
336        box = OWGUI.widgetBox(box, orientation="horizontal")
337        wl = OWGUI.widgetLabel(box, "Color: ")
338        OWGUI.separator(box)
339        self.colButton = OWGUI.toolButton(
340            box, self, callback=self.changeColor, width=20, height=20,
341            debuggingEnabled=0)
342
343        self.cbShowDistributions.disables.extend([wl, self.colButton])
344        self.cbShowDistributions.makeConsistent()
345
346        selMethBox = OWGUI.widgetBox(self.controlArea, "Select attributes",
347                                     addSpace=True)
348        self.clearButton = OWGUI.button(selMethBox, self, "Clear",
349                                        callback=self.clearSelection)
350        self.clearButton.setDisabled(True)
351
352        buttonGrid = QGridLayout()
353        selMethRadio = OWGUI.radioButtonsInBox(
354            selMethBox, self, "selectMethod", [],
355            callback=self.selectMethodChanged)
356
357        b1 = OWGUI.appendRadioButton(
358            selMethRadio, self, "selectMethod", "All",
359            insertInto=selMethRadio,
360            callback=self.selectMethodChanged,
361            addToLayout=False)
362
363        b2 = OWGUI.appendRadioButton(
364            selMethRadio, self, "selectMethod", "Manual",
365            insertInto=selMethRadio,
366            callback=self.selectMethodChanged,
367            addToLayout=False)
368
369        b3 = OWGUI.appendRadioButton(
370            selMethRadio, self, "selectMethod", "Best ranked",
371            insertInto=selMethRadio,
372            callback=self.selectMethodChanged,
373            addToLayout=False)
374
375        spin = OWGUI.spin(OWGUI.widgetBox(selMethRadio, addToLayout=False),
376                          self, "nSelected", 1, 100, orientation=0,
377                          callback=self.nSelectedChanged)
378        buttonGrid.addWidget(b1, 0, 0)
379        buttonGrid.addWidget(b2, 1, 0)
380        buttonGrid.addWidget(b3, 2, 0)
381        buttonGrid.addWidget(spin, 2, 1)
382        selMethRadio.layout().addLayout(buttonGrid)
383        OWGUI.separator(selMethBox)
384
385        applyButton = OWGUI.button(
386            selMethBox, self, "Commit", callback=self.apply, default=True)
387        autoApplyCB = OWGUI.checkBox(
388            selMethBox, self, "autoApply", "Commit automatically")
389        OWGUI.setStopper(
390            self, applyButton, autoApplyCB, "dataChanged", self.apply)
391
392        OWGUI.rubber(self.controlArea)
393
394        # Discrete and continuous table views are stacked
395        self.ranksViewStack = QStackedLayout()
396        self.mainArea.layout().addLayout(self.ranksViewStack)
397
398        self.discRanksView = QTableView()
399        self.ranksViewStack.addWidget(self.discRanksView)
400        self.discRanksView.setSelectionBehavior(QTableView.SelectRows)
401        self.discRanksView.setSelectionMode(QTableView.MultiSelection)
402        self.discRanksView.setSortingEnabled(True)
403
404        self.discRanksModel = QStandardItemModel(self)
405        self.discRanksModel.setHorizontalHeaderLabels(
406            ["Attribute", "#"] + [m.shortname for m in self.discMeasures]
407        )
408
409        self.discRanksProxyModel = MySortProxyModel(self)
410        self.discRanksProxyModel.setSourceModel(self.discRanksModel)
411        self.discRanksView.setModel(self.discRanksProxyModel)
412
413        self.discRanksView.setColumnWidth(1, 20)
414        self.discRanksView.sortByColumn(2, Qt.DescendingOrder)
415        self.connect(self.discRanksView.selectionModel(),
416                     SIGNAL("selectionChanged(QItemSelection, QItemSelection)"),
417                     self.onSelectionChanged)
418        self.connect(self.discRanksView,
419                     SIGNAL("pressed(const QModelIndex &)"),
420                     self.onSelectItem)
421        self.connect(self.discRanksView.horizontalHeader(),
422                     SIGNAL("sectionClicked(int)"),
423                     self.headerClick)
424
425        self.contRanksView = QTableView()
426        self.ranksViewStack.addWidget(self.contRanksView)
427        self.contRanksView.setSelectionBehavior(QTableView.SelectRows)
428        self.contRanksView.setSelectionMode(QTableView.MultiSelection)
429        self.contRanksView.setSortingEnabled(True)
430
431        self.contRanksModel = QStandardItemModel(self)
432        self.contRanksModel.setHorizontalHeaderLabels(
433            ["Attribute", "#"] + [m.shortname for m in self.contMeasures]
434        )
435
436        self.contRanksProxyModel = MySortProxyModel(self)
437        self.contRanksProxyModel.setSourceModel(self.contRanksModel)
438        self.contRanksView.setModel(self.contRanksProxyModel)
439
440        self.discRanksView.setColumnWidth(1, 20)
441        self.contRanksView.sortByColumn(2, Qt.DescendingOrder)
442        self.connect(self.contRanksView.selectionModel(),
443                     SIGNAL("selectionChanged(QItemSelection, QItemSelection)"),
444                     self.onSelectionChanged)
445        self.connect(self.contRanksView,
446                     SIGNAL("pressed(const QModelIndex &)"),
447                     self.onSelectItem)
448        self.connect(self.contRanksView.horizontalHeader(),
449                     SIGNAL("sectionClicked(int)"),
450                     self.headerClick)
451
452        # Switch the current view to Discrete
453        self.switchRanksMode(0)
454        self.resetInternals()
455        self.updateDelegates()
456        self.updateVisibleScoreColumns()
457
458        self.resize(690, 500)
459        self.updateColor()
460
461        self.measure_scores = table((len(self.measures), 0), None)
462
463    def switchRanksMode(self, index):
464        """
465        Switch between discrete/continuous mode
466        """
467        self.ranksViewStack.setCurrentIndex(index)
468        self.stackedLayout.setCurrentIndex(index)
469
470        if index == 0:
471            self.ranksView = self.discRanksView
472            self.ranksModel = self.discRanksModel
473            self.ranksProxyModel = self.discRanksProxyModel
474            self.measures = self.discMeasures
475        else:
476            self.ranksView = self.contRanksView
477            self.ranksModel = self.contRanksModel
478            self.ranksProxyModel = self.contRanksProxyModel
479            self.measures = self.contMeasures
480
481        self.updateVisibleScoreColumns()
482
483    def setData(self, data):
484        self.error(0)
485        self.resetInternals()
486        self.data = self.isDataWithClass(data) and data or None
487        if self.data:
488            attrs = self.data.domain.attributes
489            self.usefulAttributes = \
490                [attr for attr in attrs
491                 if is_discrete(attr) or is_continuous(attr)]
492
493            if is_class_continuous(self.data):
494                self.switchRanksMode(1)
495            elif is_class_discrete(self.data):
496                self.switchRanksMode(0)
497            else:
498                # String or other.
499                self.error(0, "Cannot handle class variable type %r" %
500                           type(self.data.domain.class_var).__name__)
501
502            self.ranksModel.setRowCount(len(attrs))
503            for i, a in enumerate(attrs):
504                if is_discrete(a):
505                    v = len(a.values)
506                else:
507                    v = "C"
508                item = PyStandardItem()
509                item.setData(QVariant(v), Qt.DisplayRole)
510                self.ranksModel.setItem(i, 1, item)
511                item = PyStandardItem(a.name)
512                item.setData(QVariant(i), OWGUI.SortOrderRole)
513                self.ranksModel.setItem(i, 0, item)
514
515            self.ranksView.resizeColumnToContents(1)
516
517            self.measure_scores = table((len(self.measures),
518                                         len(attrs)), None)
519            self.updateScores()
520            if is_class_discrete(self.data):
521                self.setLogORTitle()
522            self.ranksView.setSortingEnabled(self.sortBy > 0)
523
524        self.applyIf()
525
526    def updateScores(self, measuresMask=None):
527        """
528        Update the current computed scores.
529
530        If `measuresMask` is given it must be an list of bool values
531        indicating what measures should be recomputed.
532
533        """
534        if not self.data:
535            return
536
537        measures = self.measures
538        # Invalidate all warnings
539        self.warning(range(max(len(self.discMeasures),
540                               len(self.contMeasures))))
541
542        if measuresMask is None:
543            # Update all selected measures
544            measuresMask = [self.selectedMeasures.get(m.name)
545                            for m in measures]
546
547        for measure_index, (meas, mask) in enumerate(zip(measures, measuresMask)):
548            if not mask:
549                continue
550
551            params = measure_parameters(meas)
552            estimator = meas.score()
553            if params:
554                for p in params:
555                    setattr(estimator, p.name,
556                            getattr(self, param_attr_name(meas.score, p)))
557
558            if not meas.handles_continuous:
559                data = self.getDiscretizedData()
560                attr_map = data.attrDict
561                data = self.data
562            else:
563                attr_map, data = {}, self.data
564
565            attr_scores = []
566            for i, attr in enumerate(data.domain.attributes):
567                attr = attr_map.get(attr, attr)
568                s = None
569                if attr is not None:
570                    try:
571                        s = estimator(attr, data)
572                    except Exception, ex:
573                        self.warning(measure_index, "Error evaluating %r: %r" %
574                                     (meas.name, str(ex)))
575                    if meas.name == "Log Odds Ratio" and s is not None:
576                        # Hardcoded values returned by log odds measure
577                        if s == -999999:
578                            attr = u"-\u221E"
579                        elif s == 999999:
580                            attr = u"\u221E"
581                        else:
582                            attr = attr.values[1]
583                        s = ("%%.%df" % self.nDecimals + " (%s)") % (s, attr)
584                attr_scores.append(s)
585            self.measure_scores[measure_index] = attr_scores
586
587        self.updateRankModel(measuresMask)
588        self.ranksProxyModel.invalidate()
589
590        if self.selectMethod in [0, 2]:
591            self.autoSelection()
592
593    def updateRankModel(self, measuresMask=None):
594        """
595        Update the rankModel.
596        """
597        values = []
598        for i, scores in enumerate(self.measure_scores):
599            values_one = []
600            for j, s in enumerate(scores):
601                if isinstance(s, float):
602                    values_one.append(s)
603                else:
604                    values_one.append(None)
605                item = self.ranksModel.item(j, i + 2)
606                if not item:
607                    item = PyStandardItem()
608                    self.ranksModel.setItem(j, i + 2, item)
609                item.setData(QVariant(s), Qt.DisplayRole)
610            values.append(values_one)
611
612        for i, vals in enumerate(values):
613            valid_vals = [v for v in vals if v is not None]
614            if valid_vals:
615                vmin, vmax = min(valid_vals), max(valid_vals)
616                for j, v in enumerate(vals):
617                    if v is not None:
618                        # Set the bar ratio role for i-th measure.
619                        ratio = float((v - vmin) / ((vmax - vmin) or 1))
620                        item = self.ranksModel.item(j, i + 2)
621                        if self.showDistributions:
622                            item.setData(QVariant(ratio), OWGUI.BarRatioRole)
623                        else:
624                            item.setData(QVariant(), OWGUI.BarRatioRole)
625
626        self.ranksView.resizeColumnsToContents()
627        self.ranksView.setColumnWidth(1, 20)
628        self.ranksView.resizeRowsToContents()
629
630    def showDistributionsChanged(self):
631        # This should be handled by the delegates only (must always set the BarRatioRole
632        self.updateRankModel()
633        # Need to update the selection
634        self.autoSelection()
635
636    def changeColor(self):
637        color = QColorDialog.getColor(self.distColor, self)
638        if color.isValid():
639            self.distColorRgb = color.getRgb()
640            self.updateColor()
641
642    def updateColor(self):
643        self.distColor = QColor(*self.distColorRgb)
644        w = self.colButton.width() - 8
645        h = self.colButton.height() - 8
646        pixmap = QPixmap(w, h)
647        painter = QPainter()
648        painter.begin(pixmap)
649        painter.fillRect(0, 0, w, h, QBrush(self.distColor))
650        painter.end()
651        self.colButton.setIcon(QIcon(pixmap))
652        self.updateDelegates()
653
654    def resetInternals(self):
655        self.data = None
656        self.discretizedData = None
657        self.attributeOrder = []
658        self.selected = []
659        self.measured = {}
660        self.usefulAttributes = []
661        self.dataChanged = False
662        self.lastSentAttrs = None
663        self.ranksModel.setRowCount(0)
664
665    def onSelectionChanged(self, *args):
666        """
667        Called when the ranks view selection changes.
668        """
669        selected = self.selectedAttrs()
670        self.clearButton.setEnabled(bool(selected))
671        self.applyIf()
672
673    def onSelectItem(self, index):
674        """
675        Called when the user selects/unselects an item in the table view.
676        """
677        self.selectMethod = 1  # Manual
678        self.clearButton.setEnabled(bool(self.selectedAttrs()))
679        self.applyIf()
680
681    def clearSelection(self):
682        self.ranksView.selectionModel().clear()
683
684    def selectMethodChanged(self):
685        if self.selectMethod in [0, 2]:
686            self.autoSelection()
687
688    def nSelectedChanged(self):
689        self.selectMethod = 2
690        self.selectMethodChanged()
691
692    def getDiscretizedData(self):
693        if not self.discretizedData:
694            discretizer = Orange.feature.discretization.EqualFreq(
695                numberOfIntervals=self.nIntervals)
696            contAttrs = [attr for attr in self.data.domain.attributes
697                         if is_continuous(attr)]
698            at = []
699            attrDict = {}
700            for attri in contAttrs:
701                try:
702                    nattr = discretizer(attri, self.data)
703                    at.append(nattr)
704                    attrDict[attri] = nattr
705                except:
706                    pass
707            domain = Orange.data.Domain(at, self.data.domain.class_var)
708            self.discretizedData = self.data.translate(domain)
709            self.discretizedData.setattr("attrDict", attrDict)
710        return self.discretizedData
711
712    def discretizationChanged(self):
713        self.discretizedData = None
714        self.updateScores([not m.handles_continuous for m in self.measures])
715        self.autoSelection()
716
717    def measureParamChanged(self, measure, param=None):
718        index = self.measures.index(measure)
719        mask = [i == index for i, _ in enumerate(self.measures)]
720        self.updateScores(mask)
721
722    def loadMeasureDefaults(self, measure):
723        params = measure_parameters(measure)
724        for i, p in enumerate(params):
725            setattr(self, param_attr_name(measure.score, p), p.default)
726        self.measureParamChanged(measure)
727
728    def autoSelection(self):
729        selModel = self.ranksView.selectionModel()
730        rowCount = self.ranksModel.rowCount()
731        columnCount = self.ranksModel.columnCount()
732        model = self.ranksProxyModel
733        if self.selectMethod == 0:
734            selection = QItemSelection(
735                model.index(0, 0),
736                model.index(rowCount - 1, columnCount - 1)
737            )
738            selModel.select(selection, QItemSelectionModel.ClearAndSelect)
739        if self.selectMethod == 2:
740            nSelected = min(self.nSelected, rowCount)
741            selection = QItemSelection(
742                model.index(0, 0),
743                model.index(nSelected - 1, columnCount - 1)
744            )
745            selModel.select(selection, QItemSelectionModel.ClearAndSelect)
746
747    def headerClick(self, index):
748        self.sortBy = index + 1
749        if not self.ranksView.isSortingEnabled():
750            # The sorting is disabled ("No sorting|" selected by user)
751            self.sortingChanged()
752
753        if index > 1 and self.selectMethod == 2:
754            # Reselect the top ranked attributes
755            self.autoSelection()
756        self.sortBy = index + 1
757        return
758
759    def sortingChanged(self):
760        """
761        Sorting was changed by user (through the Sort By combo box.)
762        """
763        self.updateSorting()
764        self.autoSelection()
765
766    def updateSorting(self):
767        """
768        Update the sorting of the model/view.
769        """
770        self.ranksProxyModel.invalidate()
771        if self.sortBy == 0:
772            self.ranksProxyModel.setSortRole(OWGUI.SortOrderRole)
773            self.ranksProxyModel.sort(0, Qt.DescendingOrder)
774            self.ranksView.setSortingEnabled(False)
775
776        else:
777            self.ranksProxyModel.setSortRole(Qt.DisplayRole)
778            self.ranksView.sortByColumn(self.sortBy - 1, Qt.DescendingOrder)
779            self.ranksView.setSortingEnabled(True)
780
781    def setLogORTitle(self):
782        var = self.data.domain.classVar
783        if len(var.values) == 2:
784            title = "log OR (for %r)" % var.values[1][:10]
785        else:
786            title = "log OR"
787
788        index = [m.name for m in self.discMeasures].index("Log Odds Ratio")
789
790        item = PyStandardItem(title)
791        self.ranksModel.setHorizontalHeaderItem(index + 2, item)
792
793    def measuresSelectionChanged(self, measure=None):
794        """Measure selection has changed. Update column visibility.
795        """
796        if measure is None:
797            # Update all scores
798            measuresMask = None
799        else:
800            # Update scores for shown column if they are not yet computed.
801            shown = self.selectedMeasures.get(measure.name, False)
802            index = self.measures.index(measure)
803            if all(s is None for s in self.measure_scores[index]) and shown:
804                measuresMask = [m == measure for m in self.measures]
805            else:
806                measuresMask = [False] * len(self.measures)
807        self.updateScores(measuresMask)
808
809        self.updateVisibleScoreColumns()
810
811    def updateVisibleScoreColumns(self):
812        """
813        Update the visible columns of the scores view.
814        """
815        for i, measure in enumerate(self.measures):
816            shown = self.selectedMeasures.get(measure.name)
817            self.ranksView.setColumnHidden(i + 2, not shown)
818
819    def sortByColumn(self, col):
820        if col < 2:
821            self.sortBy = 1 + col
822        else:
823            self.sortBy = 3 + self.selectedMeasures[col - 2]
824        self.sortingChanged()
825
826    def decimalsChanged(self):
827        self.updateDelegates()
828        self.ranksView.resizeColumnsToContents()
829
830    def updateDelegates(self):
831        self.contRanksView.setItemDelegate(
832            OWGUI.ColoredBarItemDelegate(
833                self,
834                decimals=self.nDecimals,
835                color=self.distColor)
836        )
837
838        self.discRanksView.setItemDelegate(
839            OWGUI.ColoredBarItemDelegate(
840                self,
841                decimals=self.nDecimals,
842                color=self.distColor)
843        )
844
845    def sendReport(self):
846        self.reportData(self.data)
847        self.reportRaw(OWReport.reportTable(self.ranksView))
848
849    def applyIf(self):
850        if self.autoApply:
851            self.apply()
852        else:
853            self.dataChanged = True
854
855    def apply(self):
856        selected = self.selectedAttrs()
857        if not self.data or not selected:
858            self.send("Reduced Data", None)
859        else:
860            domain = Orange.data.Domain(selected, self.data.domain.classVar)
861            domain.addmetas(self.data.domain.getmetas())
862            data = Orange.data.Table(domain, self.data)
863            self.send("Reduced Data", data)
864        self.dataChanged = False
865
866    def selectedAttrs(self):
867        if self.data:
868            inds = self.ranksView.selectionModel().selectedRows(0)
869            source = self.ranksProxyModel.mapToSource
870            inds = map(source, inds)
871            inds = [ind.row() for ind in inds]
872            return [self.data.domain.attributes[i] for i in inds]
873        else:
874            return []
875
876
877class PyStandardItem(QStandardItem):
878    """A StandardItem subclass for python objects.
879    """
880    def __init__(self, *args):
881        QStandardItem.__init__(self, *args)
882        self.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
883
884    def __lt__(self, other):
885        my = self.data(Qt.DisplayRole).toPyObject()
886        other = other.data(Qt.DisplayRole).toPyObject()
887        if my is None:
888            return True
889        return my < other
890
891
892class MySortProxyModel(QSortFilterProxyModel):
893    def headerData(self, section, orientation, role):
894        """ Don't map headers.
895        """
896        source = self.sourceModel()
897        return source.headerData(section, orientation, role)
898
899    def lessThan(self, left, right):
900        role = self.sortRole()
901        left = left.data(role).toPyObject()
902        right = right.data(role).toPyObject()
903        return left < right
904
905
906if __name__ == "__main__":
907    a = QApplication(sys.argv)
908    ow = OWRank()
909    ow.setData(Orange.data.Table("wine.tab"))
910    ow.setData(Orange.data.Table("zoo.tab"))
911    ow.setData(Orange.data.Table("servo.tab"))
912    ow.setData(Orange.data.Table("iris.tab"))
913#    ow.setData(orange.ExampleTable("auto-mpg.tab"))
914    ow.show()
915    a.exec_()
916    ow.saveSettings()
Note: See TracBrowser for help on using the repository browser.