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

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

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

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