source: orange/Orange/OrangeWidgets/Data/OWRank.py @ 11685:7609e7a0753c

Revision 11685:7609e7a0753c, 32.7 KB checked in by Ales Erjavec <ales.erjavec@…>, 8 months ago (diff)

Rank widget code style fixes.

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