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.

RevLine 
[11748]1import OWGUI
2import Orange
3import pkg_resources
[11684]4from collections import namedtuple
5from functools import partial
[8042]6from OWWidget import *
[11685]7from Orange.feature import scoring
8from Orange.classification import svm
9from Orange.ensemble import forest
[11684]10
[11748]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
[8875]21
[11685]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
[8042]29
[8251]30def is_class_discrete(data):
[11685]31    return is_discrete(data.domain.class_var)
32
[8251]33
34def is_class_continuous(data):
[11685]35    return is_continuous(data.domain.class_var)
36
[8251]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])]
[8875]42
43
[11684]44MEASURE_PARAMS = {
[11685]45    scoring.Relief: [
[11684]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        ],
[11685]59    forest.ScoreFeature: [
[11684]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    }
[8875]68
69
[11684]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)
[8875]81
82
[11684]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        )
[8875]93
[11684]94
95# Default scores.
96SCORES = [
97    score_meta(
[11685]98        "ReliefF", "ReliefF", scoring.Relief,
99        params=MEASURE_PARAMS[scoring.Relief],
[11684]100        handles_continuous=True,
101        handles_discrete=True),
102    score_meta(
[11685]103        "Information Gain", "Inf. gain", scoring.InfoGain,
[11684]104        params=None,
105        supports_regression=False,
106        supports_classification=True,
107        handles_continuous=False,
108        handles_discrete=True),
109    score_meta(
[11685]110        "Gain Ratio", "Gain Ratio", scoring.GainRatio,
[11684]111        params=None,
112        supports_regression=False,
113        handles_continuous=False,
114        handles_discrete=True),
115    score_meta(
[11685]116        "Gini Gain", "Gini", scoring.Gini,
[11684]117        params=None,
118        supports_regression=False,
119        supports_classification=True,
120        handles_continuous=False),
121    score_meta(
[11685]122        "Log Odds Ratio", "log OR", Orange.core.MeasureAttribute_logOddsRatio,
[11684]123        params=None,
124        supports_regression=False,
125        handles_continuous=False),
126    score_meta(
[11685]127        "MSE", "MSE", scoring.MSE,
[11684]128        params=None,
129        supports_classification=False,
130        handles_continuous=False),
131    score_meta(
[11685]132        "Linear SVM Weights", "SVM weight", svm.ScoreSVMWeights,
[11684]133        params=None),
134    score_meta(
[11685]135        "Random Forests", "RF", forest.ScoreFeature,
136        params=MEASURE_PARAMS[forest.ScoreFeature]),
[11684]137]
138
139_DEFAULT_SELECTED = set(m.name for m in SCORES[:6])
[8875]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
[11666]151
[8875]152
153def measure_parameters(measure):
[11684]154    return [MethodParameter(**args) for args in (measure.params or [])]
155
[8875]156
157def param_attr_name(measure, param):
[11684]158    """Name of the OWRank widget's member where the parameter is stored.
[8875]159    """
160    return "param_" + measure.__name__ + "_" + param.name
[11684]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
[8042]190class OWRank(OWWidget):
[11684]191    settingsList = [
192        "nDecimals", "nIntervals", "sortBy", "nSelected",
193        "selectMethod", "autoApply", "showDistributions",
[11729]194        "distColorRgb", "selectedMeasures"
[11684]195    ]
[8042]196
[11684]197    def __init__(self, parent=None, signalManager=None):
[8042]198        OWWidget.__init__(self, parent, signalManager, "Rank")
199
[9546]200        self.inputs = [("Data", ExampleTable, self.setData)]
201        self.outputs = [("Reduced Data", ExampleTable, Default + Single)]
[8042]202
203        self.nDecimals = 3
204        self.nIntervals = 4
[8251]205        self.sortBy = 2
[8042]206        self.selectMethod = 2
207        self.nSelected = 5
208        self.autoApply = True
209        self.showDistributions = 1
[11684]210        self.distColorRgb = (220, 220, 220, 255)
[8042]211        self.distColor = QColor(*self.distColorRgb)
[11684]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
[8875]221        self.data = None
[11684]222
[8875]223        self.methodParamAttrs = []
[11684]224        for m in self.all_measures:
225            params = measure_parameters(m)
[8875]226            for p in params:
[11684]227                name_mangled = param_attr_name(m.score, p)
228                setattr(self, name_mangled, p.default)
229                self.methodParamAttrs.append(name_mangled)
230
[8875]231        self.settingsList = self.settingsList + self.methodParamAttrs
[8042]232
[11684]233        self.loadSettings()
[8042]234
[11684]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
[8251]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)
[11684]245
[8251]246        # Discrete class scoring
[8875]247        discreteBox = OWGUI.widgetBox(self.stackedWidget, "Scoring",
248                                      addSpace=False,
249                                      addToLayout=False)
250        self.stackedLayout.addWidget(discreteBox)
[11684]251
[8875]252        # Continuous class scoring
253        continuousBox = OWGUI.widgetBox(self.stackedWidget, "Scoring",
254                                        addSpace=False,
255                                        addToLayout=False)
256        self.stackedLayout.addWidget(continuousBox)
[11684]257
258        def measure_control(container, measure):
259            """Construct UI control for `measure` (measure_meta instance).
[8875]260            """
[11684]261            name = measure.name
[8875]262            params = measure_parameters(measure)
263            if params:
[11684]264                hbox = OWGUI.widgetBox(container, orientation="horizontal")
[8875]265                OWGUI.checkBox(hbox, self.selectedMeasures, name, name,
[11684]266                               callback=partial(self.measuresSelectionChanged,
267                                                measure),
[8875]268                               tooltip="Enable " + name)
[11684]269
270                smallWidget = OWGUI.SmallWidgetLabel(
271                    hbox, pixmap=1, box=name + " Parameters",
272                    tooltip="Show " + name + "Parameters")
273
[8875]274                for param in params:
[11684]275                    OWGUI.spin(smallWidget.widget, self,
276                               param_attr_name(measure.score, param),
[8875]277                               param.range[0], param.range[-1],
[11684]278                               label=param.display_name,
[8875]279                               tooltip=param.doc,
[11684]280                               callback=partial(
[11685]281                                   self.measureParamChanged, measure, param),
[8875]282                               callbackOnReturn=True)
[11684]283
[8875]284                OWGUI.button(smallWidget.widget, self, "Load defaults",
[11684]285                             callback=partial(self.loadMeasureDefaults,
286                                              measure))
[8042]287            else:
[8875]288                OWGUI.checkBox(container, self.selectedMeasures, name, name,
[11684]289                               callback=partial(self.measuresSelectionChanged,
290                                                measure),
[8875]291                               tooltip="Enable " + name)
[11684]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
[11685]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)
[8042]313
[8251]314        box = OWGUI.widgetBox(self.controlArea, "Discretization",
315                              addSpace=True)
316        OWGUI.spin(box, self, "nIntervals", 2, 20,
317                   label="Intervals: ",
318                   orientation=0,
[11685]319                   tooltip="Disctetization for measures which cannot score "
320                           "continuous attributes.",
[8251]321                   callback=self.discretizationChanged,
322                   callbackOnReturn=True)
[8042]323
324        box = OWGUI.widgetBox(self.controlArea, "Precision", addSpace=True)
[8251]325        OWGUI.spin(box, self, "nDecimals", 1, 6, label="No. of decimals: ",
326                   orientation=0, callback=self.decimalsChanged)
[8042]327
[8251]328        box = OWGUI.widgetBox(self.controlArea, "Score bars",
329                              orientation="horizontal", addSpace=True)
[11685]330        self.cbShowDistributions = OWGUI.checkBox(
331            box, self, "showDistributions", 'Enable',
332            callback=self.showDistributionsChanged
333        )
334
[8042]335        OWGUI.rubber(box)
336        box = OWGUI.widgetBox(box, orientation="horizontal")
337        wl = OWGUI.widgetLabel(box, "Color: ")
338        OWGUI.separator(box)
[11685]339        self.colButton = OWGUI.toolButton(
340            box, self, callback=self.changeColor, width=20, height=20,
341            debuggingEnabled=0)
342
[8042]343        self.cbShowDistributions.disables.extend([wl, self.colButton])
344        self.cbShowDistributions.makeConsistent()
345
[11685]346        selMethBox = OWGUI.widgetBox(self.controlArea, "Select attributes",
347                                     addSpace=True)
348        self.clearButton = OWGUI.button(selMethBox, self, "Clear",
349                                        callback=self.clearSelection)
[8042]350        self.clearButton.setDisabled(True)
[11685]351
[8042]352        buttonGrid = QGridLayout()
[11685]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)
[8042]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
[11685]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)
[8042]391
392        OWGUI.rubber(self.controlArea)
[11685]393
[8251]394        # Discrete and continuous table views are stacked
395        self.ranksViewStack = QStackedLayout()
396        self.mainArea.layout().addLayout(self.ranksViewStack)
[11685]397
[8251]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)
[11685]403
[8251]404        self.discRanksModel = QStandardItemModel(self)
[11684]405        self.discRanksModel.setHorizontalHeaderLabels(
406            ["Attribute", "#"] + [m.shortname for m in self.discMeasures]
407        )
[11685]408
[8251]409        self.discRanksProxyModel = MySortProxyModel(self)
410        self.discRanksProxyModel.setSourceModel(self.discRanksModel)
411        self.discRanksView.setModel(self.discRanksProxyModel)
[11685]412
[8251]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)
[11685]424
[8251]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)
[11685]430
[8251]431        self.contRanksModel = QStandardItemModel(self)
[11684]432        self.contRanksModel.setHorizontalHeaderLabels(
433            ["Attribute", "#"] + [m.shortname for m in self.contMeasures]
434        )
[11685]435
[8251]436        self.contRanksProxyModel = MySortProxyModel(self)
437        self.contRanksProxyModel.setSourceModel(self.contRanksModel)
438        self.contRanksView.setModel(self.contRanksProxyModel)
[11685]439
[8251]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)
[11685]451
[8251]452        # Switch the current view to Discrete
453        self.switchRanksMode(0)
454        self.resetInternals()
455        self.updateDelegates()
[8875]456        self.updateVisibleScoreColumns()
[8042]457
[11685]458        self.resize(690, 500)
[8042]459        self.updateColor()
[11685]460
[9313]461        self.measure_scores = table((len(self.measures), 0), None)
[8042]462
[8251]463    def switchRanksMode(self, index):
[11685]464        """
465        Switch between discrete/continuous mode
[8251]466        """
467        self.ranksViewStack.setCurrentIndex(index)
468        self.stackedLayout.setCurrentIndex(index)
[11684]469
[8251]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
[11684]480
[8875]481        self.updateVisibleScoreColumns()
[11685]482
[8251]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
[11685]489            self.usefulAttributes = \
490                [attr for attr in attrs
491                 if is_discrete(attr) or is_continuous(attr)]
492
[8251]493            if is_class_continuous(self.data):
494                self.switchRanksMode(1)
495            elif is_class_discrete(self.data):
496                self.switchRanksMode(0)
[11685]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
[8251]502            self.ranksModel.setRowCount(len(attrs))
503            for i, a in enumerate(attrs):
[11685]504                if is_discrete(a):
[8251]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)
[11685]514
[8251]515            self.ranksView.resizeColumnToContents(1)
[11685]516
[8251]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)
[11685]523
[8251]524        self.applyIf()
525
526    def updateScores(self, measuresMask=None):
[11685]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        """
[8251]534        if not self.data:
535            return
[11685]536
[8251]537        measures = self.measures
[11684]538        # Invalidate all warnings
539        self.warning(range(max(len(self.discMeasures),
540                               len(self.contMeasures))))
541
[8251]542        if measuresMask is None:
[8875]543            # Update all selected measures
[11684]544            measuresMask = [self.selectedMeasures.get(m.name)
545                            for m in measures]
546
547        for measure_index, (meas, mask) in enumerate(zip(measures, measuresMask)):
[8251]548            if not mask:
549                continue
[11684]550
551            params = measure_parameters(meas)
552            estimator = meas.score()
[8875]553            if params:
554                for p in params:
555                    setattr(estimator, p.name,
[11684]556                            getattr(self, param_attr_name(meas.score, p)))
557
558            if not meas.handles_continuous:
[8251]559                data = self.getDiscretizedData()
560                attr_map = data.attrDict
561                data = self.data
562            else:
563                attr_map, data = {}, self.data
[11684]564
[8251]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:
[8875]571                        s = estimator(attr, data)
[8251]572                    except Exception, ex:
[11685]573                        self.warning(measure_index, "Error evaluating %r: %r" %
574                                     (meas.name, str(ex)))
[11684]575                    if meas.name == "Log Odds Ratio" and s is not None:
[11685]576                        # Hardcoded values returned by log odds measure
[8251]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
[11685]586
[8875]587        self.updateRankModel(measuresMask)
[8251]588        self.ranksProxyModel.invalidate()
[11685]589
[8251]590        if self.selectMethod in [0, 2]:
591            self.autoSelection()
[11685]592
[8251]593    def updateRankModel(self, measuresMask=None):
[11685]594        """
595        Update the rankModel.
[8251]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()
[11685]608                    self.ranksModel.setItem(j, i + 2, item)
[8251]609                item.setData(QVariant(s), Qt.DisplayRole)
610            values.append(values_one)
[11685]611
[8251]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.
[8875]619                        ratio = float((v - vmin) / ((vmax - vmin) or 1))
[11685]620                        item = self.ranksModel.item(j, i + 2)
[8251]621                        if self.showDistributions:
[11685]622                            item.setData(QVariant(ratio), OWGUI.BarRatioRole)
[8251]623                        else:
[11685]624                            item.setData(QVariant(), OWGUI.BarRatioRole)
625
[8251]626        self.ranksView.resizeColumnsToContents()
627        self.ranksView.setColumnWidth(1, 20)
628        self.ranksView.resizeRowsToContents()
[11685]629
630    def showDistributionsChanged(self):
[8875]631        # This should be handled by the delegates only (must always set the BarRatioRole
[8251]632        self.updateRankModel()
633        # Need to update the selection
634        self.autoSelection()
[8042]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)
[11685]644        w = self.colButton.width() - 8
645        h = self.colButton.height() - 8
[8042]646        pixmap = QPixmap(w, h)
647        painter = QPainter()
648        painter.begin(pixmap)
[11685]649        painter.fillRect(0, 0, w, h, QBrush(self.distColor))
[8042]650        painter.end()
651        self.colButton.setIcon(QIcon(pixmap))
[8251]652        self.updateDelegates()
[8042]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
[8251]663        self.ranksModel.setRowCount(0)
[8042]664
[8251]665    def onSelectionChanged(self, *args):
[11685]666        """
667        Called when the ranks view selection changes.
[8251]668        """
669        selected = self.selectedAttrs()
670        self.clearButton.setEnabled(bool(selected))
671        self.applyIf()
[11685]672
[8251]673    def onSelectItem(self, index):
674        """
[11685]675        Called when the user selects/unselects an item in the table view.
676        """
677        self.selectMethod = 1  # Manual
[8251]678        self.clearButton.setEnabled(bool(self.selectedAttrs()))
679        self.applyIf()
[8042]680
681    def clearSelection(self):
[8251]682        self.ranksView.selectionModel().clear()
[8042]683
684    def selectMethodChanged(self):
[8251]685        if self.selectMethod in [0, 2]:
686            self.autoSelection()
[8042]687
688    def nSelectedChanged(self):
689        self.selectMethod = 2
690        self.selectMethodChanged()
691
[8251]692    def getDiscretizedData(self):
693        if not self.discretizedData:
[11685]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)]
[8251]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
[11685]707            domain = Orange.data.Domain(at, self.data.domain.class_var)
708            self.discretizedData = self.data.translate(domain)
[8251]709            self.discretizedData.setattr("attrDict", attrDict)
710        return self.discretizedData
[11684]711
[8042]712    def discretizationChanged(self):
713        self.discretizedData = None
[11684]714        self.updateScores([not m.handles_continuous for m in self.measures])
[8251]715        self.autoSelection()
[11684]716
717    def measureParamChanged(self, measure, param=None):
718        index = self.measures.index(measure)
[8875]719        mask = [i == index for i, _ in enumerate(self.measures)]
720        self.updateScores(mask)
[11685]721
[11684]722    def loadMeasureDefaults(self, measure):
[8875]723        params = measure_parameters(measure)
724        for i, p in enumerate(params):
[11684]725            setattr(self, param_attr_name(measure.score, p), p.default)
726        self.measureParamChanged(measure)
[11685]727
[8251]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:
[11685]734            selection = QItemSelection(
735                model.index(0, 0),
736                model.index(rowCount - 1, columnCount - 1)
737            )
[8251]738            selModel.select(selection, QItemSelectionModel.ClearAndSelect)
739        if self.selectMethod == 2:
740            nSelected = min(self.nSelected, rowCount)
[11685]741            selection = QItemSelection(
742                model.index(0, 0),
743                model.index(nSelected - 1, columnCount - 1)
744            )
[8251]745            selModel.select(selection, QItemSelectionModel.ClearAndSelect)
[8042]746
747    def headerClick(self, index):
[8251]748        self.sortBy = index + 1
749        if not self.ranksView.isSortingEnabled():
750            # The sorting is disabled ("No sorting|" selected by user)
751            self.sortingChanged()
[11685]752
[8251]753        if index > 1 and self.selectMethod == 2:
754            # Reselect the top ranked attributes
755            self.autoSelection()
756        self.sortBy = index + 1
757        return
[8042]758
759    def sortingChanged(self):
[11685]760        """
761        Sorting was changed by user (through the Sort By combo box.)
[8251]762        """
763        self.updateSorting()
764        self.autoSelection()
[11685]765
[8251]766    def updateSorting(self):
[11685]767        """
768        Update the sorting of the model/view.
[8251]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)
[11685]775
[8251]776        else:
777            self.ranksProxyModel.setSortRole(Qt.DisplayRole)
778            self.ranksView.sortByColumn(self.sortBy - 1, Qt.DescendingOrder)
779            self.ranksView.setSortingEnabled(True)
[8042]780
781    def setLogORTitle(self):
[11684]782        var = self.data.domain.classVar
[8251]783        if len(var.values) == 2:
784            title = "log OR (for %r)" % var.values[1][:10]
785        else:
786            title = "log OR"
[11685]787
[11684]788        index = [m.name for m in self.discMeasures].index("Log Odds Ratio")
[8042]789
[11684]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.
[8251]795        """
[11684]796        if measure is None:
[8875]797            # Update all scores
798            measuresMask = None
799        else:
800            # Update scores for shown column if they are not yet computed.
[11684]801            shown = self.selectedMeasures.get(measure.name, False)
802            index = self.measures.index(measure)
[8875]803            if all(s is None for s in self.measure_scores[index]) and shown:
[11684]804                measuresMask = [m == measure for m in self.measures]
[8875]805            else:
806                measuresMask = [False] * len(self.measures)
807        self.updateScores(measuresMask)
[11685]808
[8875]809        self.updateVisibleScoreColumns()
[11685]810
[8875]811    def updateVisibleScoreColumns(self):
[11685]812        """
813        Update the visible columns of the scores view.
[8875]814        """
815        for i, measure in enumerate(self.measures):
[11684]816            shown = self.selectedMeasures.get(measure.name)
[8251]817            self.ranksView.setColumnHidden(i + 2, not shown)
[8042]818
819    def sortByColumn(self, col):
820        if col < 2:
821            self.sortBy = 1 + col
822        else:
[11685]823            self.sortBy = 3 + self.selectedMeasures[col - 2]
[8042]824        self.sortingChanged()
825
826    def decimalsChanged(self):
[8251]827        self.updateDelegates()
828        self.ranksView.resizeColumnsToContents()
[11685]829
[8251]830    def updateDelegates(self):
[11685]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
[8042]845    def sendReport(self):
846        self.reportData(self.data)
[9269]847        self.reportRaw(OWReport.reportTable(self.ranksView))
[8042]848
849    def applyIf(self):
850        if self.autoApply:
851            self.apply()
852        else:
853            self.dataChanged = True
854
855    def apply(self):
[8251]856        selected = self.selectedAttrs()
857        if not self.data or not selected:
[9546]858            self.send("Reduced Data", None)
[8042]859        else:
[11685]860            domain = Orange.data.Domain(selected, self.data.domain.classVar)
[8251]861            domain.addmetas(self.data.domain.getmetas())
[11685]862            data = Orange.data.Table(domain, self.data)
[9546]863            self.send("Reduced Data", data)
[8251]864        self.dataChanged = False
[11685]865
[8251]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:
[11685]874            return []
[8042]875
876
[8251]877class PyStandardItem(QStandardItem):
[11685]878    """A StandardItem subclass for python objects.
[8251]879    """
880    def __init__(self, *args):
881        QStandardItem.__init__(self, *args)
[11685]882        self.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
883
[8251]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
[11685]891
[8251]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)
[11685]898
[8251]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
[8042]904
[11685]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"))
[8251]913#    ow.setData(orange.ExampleTable("auto-mpg.tab"))
[8042]914    ow.show()
915    a.exec_()
916    ow.saveSettings()
Note: See TracBrowser for help on using the repository browser.