source: orange/Orange/OrangeWidgets/Data/OWRank.py @ 11692:356c325c0efb

Revision 11692:356c325c0efb, 31.9 KB checked in by Ales Erjavec <ales.erjavec@…>, 7 months ago (diff)

Removed 'Earth' code from Orange (moved to 'orangecontrib.earth' package).

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