Changeset 8875:333d9cc40d6a in orange


Ignore:
Timestamp:
09/01/11 12:12:31 (3 years ago)
Author:
ales_erjavec <ales.erjavec@…>
Branch:
default
Convert:
a14ee62577e1da2da3f88213362a966ebc514638
Message:

Added ScoreEarthImportance, SVMLinearWeights and randomForests attribute measures.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • orange/OrangeWidgets/Data/OWRank.py

    r8251 r8875  
    77""" 
    88from OWWidget import * 
     9from PyQt4 import QtCore, QtGui 
     10 
     11if not hasattr(QtCore, "Signal"): 
     12    QtCore.Signal = QtCore.pyqtSignal 
     13     
     14if not hasattr(QtCore, "Slot"): 
     15    QtCore.Signal = QtCore.pyqtSlot 
     16     
    917import OWGUI 
    1018import orange 
     19 
     20from functools import partial 
    1121 
    1222def _toPyObject(variant): 
     
    3747    return [[fill for j in range(shape[1])] for i in range(shape[0])] 
    3848  
     49from Orange.regression.earth import ScoreEarthImportance 
     50from orngSVM import MeasureAttribute_SVMWeights 
     51from orngEnsemble import MeasureAttribute_randomForests 
     52 
     53MEASURE_PARAMS = {ScoreEarthImportance: \ 
     54                    [{"name": "degree",  
     55                      "type": int, 
     56                      "display_name": "Max. term degree", 
     57                      "range": range(1, 4), 
     58                      "default": 2, 
     59                      "doc": "Maximum degree of terms included in the model."  
     60                     }, 
     61                     {"name": "t", 
     62                      "type": int, 
     63                      "display_name": "Num. models.", 
     64                      "range": range(1, 21), 
     65                      "default": 10, 
     66                      "doc": "Number of models to train for feature scoring." 
     67                      }, 
     68#                     {"name": "score_what", 
     69#                      "type": int, 
     70#                      "display_name": "Score what", 
     71#                      "range": range(0, 3), 
     72#                      "display_role": ["Num. Subsets", "RSS", "GCV"] 
     73#                      "default": 2, 
     74#                      "doc": ""} 
     75                     ], 
     76                  orange.MeasureAttribute_relief: \ 
     77                     [{"name": "k", 
     78                       "type": int, 
     79                       "display_name": "Neighbours", 
     80                       "range": range(1, 21), 
     81                       "default": 10, 
     82                       "doc": "Number of neighbors to consider."}, 
     83                      {"name":"m", 
     84                       "type": int, 
     85                       "display_name": "Examples", 
     86                       "range": range(20, 101), 
     87                       "default": 20, 
     88                       "doc": ""} 
     89                      ], 
     90                  MeasureAttribute_randomForests:\ 
     91                     [{"name": "trees", 
     92                       "type": int, 
     93                       "display_name": "Num. of trees", 
     94                       "range": range(20, 101), 
     95                       "default": 100, 
     96                       "doc": "Number of trees in the random forest."} 
     97                      ] 
     98                  } 
     99 
     100 
     101MEASURES = [("ReliefF", "ReliefF", orange.MeasureAttribute_relief), 
     102            ("Information Gain", "Inf. gain", orange.MeasureAttribute_info), 
     103            ("Gain Ratio", "Gain Ratio", orange.MeasureAttribute_gainRatio), 
     104            ("Gini Gain", "Gini", orange.MeasureAttribute_gini), 
     105            ("Log Odds Ratio", "log OR", orange.MeasureAttribute_logOddsRatio), 
     106            ("MSE", "MSE", orange.MeasureAttribute_MSE), 
     107            ("Earth Importance", "Earth imp.", ScoreEarthImportance), 
     108            ("Linear SVM Weights", "SVM weight", MeasureAttribute_SVMWeights), 
     109            ("Random Forests", "RF", MeasureAttribute_randomForests), 
     110            ] 
     111 
     112MEASURES_HANDLES_CONTINUOUS = {"ReliefF": True, 
     113                               "Earth Importance": True, 
     114                               "Linear SVM Weights": True, 
     115                               "Random Forests": True, 
     116                               } 
     117 
     118MEASURES_SUPPORTS_REGRESSION = {"ReliefF": True, 
     119                                "MSE": True, 
     120                                "Earth Importance": True, 
     121                                "Random Forests": True, 
     122                                } 
     123 
     124MEASURES_SUPPORTS_CLASSIFICATION = {"MSE": False, 
     125                                    "Random Forests": True, 
     126                                    } 
     127 
     128MEASURES_DEFAULT_SELECTED = dict([(mname, True) for mname, _, _ in MEASURES[:6]] + \ 
     129                                 [(mname, False) for mname, _, _ in MEASURES[6:]]) # The Earth imp. and SVM are not selected by default 
     130 
     131 
     132class MethodParameter(object): 
     133    def __init__(self, name="", type=None, display_name="Parameter", 
     134                 range=None, default=None, doc=""): 
     135        self.name = name 
     136        self.type = type 
     137        self.display_name = display_name 
     138        self.range = range 
     139        self.default = default 
     140        self.doc = doc 
     141     
     142def supports_classification(name): 
     143    return MEASURES_SUPPORTS_CLASSIFICATION.get(name, True) 
     144 
     145def supports_regression(name): 
     146    return MEASURES_SUPPORTS_REGRESSION.get(name, False) 
     147 
     148def handles_continuous(name): 
     149    return MEASURES_HANDLES_CONTINUOUS.get(name, False) 
     150 
     151def measure_parameters(measure): 
     152    return [MethodParameter(**args) for args in MEASURE_PARAMS.get(measure, [])] 
     153 
     154def param_attr_name(measure, param): 
     155    """ Name of the OWRank widget where the parameter is stored.  
     156    """ 
     157    return "param_" + measure.__name__ + "_" + param.name 
     158         
    39159class OWRank(OWWidget): 
    40     settingsList =  ["nDecimals", "reliefK", "reliefN", "nIntervals", "sortBy", "nSelected", "selectMethod", "autoApply", "showDistributions", "distColorRgb"] 
    41     discMeasures          = ["ReliefF", "Information Gain", "Gain Ratio", "Gini Gain", "Log Odds Ratio"] 
    42     discMeasuresShort     = ["ReliefF", "Inf. gain", "Gain ratio", "Gini", "log OR"] 
    43     discMeasuresAttrs     = ["computeReliefF", "computeInfoGain", "computeGainRatio", "computeGini", "computeLogOdds"] 
    44     discEstimators        = [orange.MeasureAttribute_relief, orange.MeasureAttribute_info, orange.MeasureAttribute_gainRatio, orange.MeasureAttribute_gini, orange.MeasureAttribute_logOddsRatio] 
    45     discHandlesContinuous = [True, False, False, False, False] 
    46      
    47     contMeasures      = ["ReliefF", "MSE"] 
    48     contMeasuresShort = ["ReliefF", "MSE"] 
    49     contMeasuresAttrs = ["computeReliefFCont", "computeMSECont"] 
    50     contEstimators    = [orange.MeasureAttribute_relief, orange.MeasureAttribute_MSE] 
    51     contHandlesContinuous   = [True, False] 
     160    settingsList =  ["nDecimals", "nIntervals", "sortBy", "nSelected", "selectMethod", "autoApply", "showDistributions", "distColorRgb"] 
    52161 
    53162    def __init__(self,parent=None, signalManager = None): 
     
    57166        self.outputs = [("Reduced Example Table", ExampleTable, Default + Single)] 
    58167 
    59         self.settingsList += self.discMeasuresAttrs + self.contMeasuresAttrs 
    60         self.logORIdx = self.discMeasuresShort.index("log OR") 
    61  
    62168        self.nDecimals = 3 
    63         self.reliefK = 10 
    64         self.reliefN = 20 
    65169        self.nIntervals = 4 
    66170        self.sortBy = 2 
     
    72176        self.distColor = QColor(*self.distColorRgb) 
    73177        self.minmax = {} 
    74          
     178        self.selectedMeasures = dict(MEASURES_DEFAULT_SELECTED) 
    75179        self.data = None 
    76  
    77         for meas in self.discMeasuresAttrs + self.contMeasuresAttrs: 
    78             setattr(self, meas, True) 
    79  
    80         self.loadSettings() 
     180         
     181#        self.measure_parameters = AttributeDict() 
     182#        self.measure_parameters = {} 
     183         
     184        self.methodParamAttrs = [] 
     185        for _, _, m in MEASURES: 
     186            params = measure_parameters(m) or [] 
     187            for p in params: 
     188                setattr(self, param_attr_name(m, p), p.default) 
     189                self.methodParamAttrs.append(param_attr_name(m, p)) 
     190        self.settingsList = self.settingsList + self.methodParamAttrs 
     191         
     192        self.loadSettings()  
    81193 
    82194        labelWidth = 80 
    83  
     195         
     196        self.discMeasures = [name for name, short, _ in MEASURES \ 
     197                             if supports_classification(name)] 
     198         
     199        self.contMeasures = [name for name, short, _ in MEASURES \ 
     200                             if supports_regression(name)] 
     201         
     202        self.discMeasuresShort = [short for name, short, _ in MEASURES \ 
     203                                  if supports_classification(name)] 
     204         
     205        self.contMeasuresShort = [short for name, short, _ in MEASURES \ 
     206                                  if supports_regression(name)] 
     207         
     208        self.discEstimators = [measure for name, _, measure in MEASURES \ 
     209                               if supports_classification(name)] 
     210         
     211        self.contEstimators = [measure for name, _, measure in MEASURES \ 
     212                               if supports_regression(name)] 
     213         
     214        self.discHandlesContinuous = map(handles_continuous, self.discMeasures) 
     215        self.contHandlesContinuous = map(handles_continuous, self.contMeasures) 
     216 
     217        # The stacked layout for Classification/Regression measures 
     218#        self.stackedWidget = OWGUI.widgetBox(self.controlArea, margin=0, 
     219#                                             addSpace=True) 
     220         
    84221        self.stackedLayout = QStackedLayout() 
    85222        self.stackedLayout.setContentsMargins(0, 0, 0, 0) 
     
    87224                                             orientation=self.stackedLayout, 
    88225                                             addSpace=True) 
     226#        self.stackedWidget.layout().addLayout(self.stackedLayout) 
    89227        # Discrete class scoring 
    90         box = OWGUI.widgetBox(self.stackedWidget, "Scoring", 
    91                               addSpace=False, 
    92                               addToLayout=False) 
    93         self.stackedLayout.addWidget(box) 
    94          
    95         for meas, valueName in zip(self.discMeasures, self.discMeasuresAttrs): 
    96             if valueName == "computeReliefF": 
    97                 hbox = OWGUI.widgetBox(box, orientation = "horizontal") 
    98                 OWGUI.checkBox(hbox, self, valueName, meas, callback=self.measuresChanged) 
    99                 hbox.layout().addSpacing(5) 
    100                 smallWidget = OWGUI.SmallWidgetLabel(hbox, pixmap = 1, box = "ReliefF Parameters", tooltip = "Show ReliefF parameters") 
    101                 OWGUI.spin(smallWidget.widget, self, "reliefK", 1, 20, label="Neighbours", labelWidth=labelWidth, orientation=0, callback=self.reliefChanged, callbackOnReturn = True) 
    102                 OWGUI.spin(smallWidget.widget, self, "reliefN", 20, 100, label="Examples", labelWidth=labelWidth, orientation=0, callback=self.reliefChanged, callbackOnReturn = True) 
    103                 OWGUI.button(smallWidget.widget, self, "Load defaults", callback = self.loadReliefDefaults) 
    104                 OWGUI.rubber(hbox) 
     228        discreteBox = OWGUI.widgetBox(self.stackedWidget, "Scoring", 
     229                                      addSpace=False, 
     230                                      addToLayout=False) 
     231        self.stackedLayout.addWidget(discreteBox) 
     232         
     233        # Continuous class scoring 
     234        continuousBox = OWGUI.widgetBox(self.stackedWidget, "Scoring", 
     235                                        addSpace=False, 
     236                                        addToLayout=False) 
     237        self.stackedLayout.addWidget(continuousBox) 
     238         
     239        def measure_control(container, name, measure): 
     240            """ Construct UI control for measure. 
     241            """ 
     242            params = measure_parameters(measure) 
     243            if params: 
     244                hbox = OWGUI.widgetBox(container, orientation = "horizontal") 
     245                OWGUI.checkBox(hbox, self.selectedMeasures, name, name, 
     246                               callback=partial(self.measuresSelectionChanged, name), 
     247                               tooltip="Enable " + name) 
     248                smallWidget = OWGUI.SmallWidgetLabel(hbox, pixmap=1, box=name + " Parameters", 
     249                                                     tooltip="Show " + name + "Parameters") 
     250                for param in params: 
     251                    OWGUI.spin(smallWidget.widget, self, param_attr_name(measure, param), 
     252                               param.range[0], param.range[-1], 
     253                               label=param.display_name,  
     254                               tooltip=param.doc, 
     255                               callback=partial(self.measureParamChanged, name, param), 
     256                               callbackOnReturn=True) 
     257                 
     258                OWGUI.button(smallWidget.widget, self, "Load defaults", 
     259                             callback=partial(self.loadMeasureDefaults, name)) 
    105260            else: 
    106                 OWGUI.checkBox(box, self, valueName, meas, callback=self.measuresChanged) 
    107                  
    108         OWGUI.comboBox(box, self, "sortBy", label = "Sort by"+"  ", 
    109                        items = ["No Sorting", "Attribute Name", "Number of Values"] + self.discMeasures, 
    110                        orientation=0, valueType = int, callback=self.sortingChanged) 
    111                  
    112         # Continuous class scoring 
    113         box = OWGUI.widgetBox(self.stackedWidget, "Scoring", 
    114                               addSpace=False, 
    115                               addToLayout=False) 
    116         self.stackedLayout.addWidget(box) 
    117         for meas, valueName in zip(self.contMeasures, self.contMeasuresAttrs): 
    118             if valueName == "computeReliefFCont": 
    119                 hbox = OWGUI.widgetBox(box, orientation = "horizontal") 
    120                 OWGUI.checkBox(hbox, self, valueName, meas, callback=self.measuresChanged) 
    121                 hbox.layout().addSpacing(5) 
    122                 smallWidget = OWGUI.SmallWidgetLabel(hbox, pixmap = 1, box = "ReliefF Parameters", tooltip = "Show ReliefF parameters") 
    123                 OWGUI.spin(smallWidget.widget, self, "reliefK", 1, 20, label="Neighbours", labelWidth=labelWidth, orientation=0, callback=self.reliefChanged, callbackOnReturn = True) 
    124                 OWGUI.spin(smallWidget.widget, self, "reliefN", 20, 100, label="Examples", labelWidth=labelWidth, orientation=0, callback=self.reliefChanged, callbackOnReturn = True) 
    125                 OWGUI.button(smallWidget.widget, self, "Load defaults", callback = self.loadReliefDefaults) 
    126                 OWGUI.rubber(hbox) 
    127             else: 
    128                 OWGUI.checkBox(box, self, valueName, meas, callback=self.measuresChanged) 
    129                  
    130         OWGUI.comboBox(box, self, "sortBy", label = "Sort by"+"  ", 
    131                        items = ["No Sorting", "Attribute Name", "Number of Values"] + self.contMeasures, 
    132                        orientation=0, valueType = int, callback=self.sortingChanged) 
     261                OWGUI.checkBox(container, self.selectedMeasures, name, name, 
     262                               callback=partial(self.measuresSelectionChanged, name), 
     263                               tooltip="Enable " + name) 
     264         
     265        for name, short_name, measure in MEASURES: 
     266            if supports_classification(name): 
     267                measure_control(discreteBox, name, measure) 
     268                     
     269            if supports_regression(name): 
     270                measure_control(continuousBox, name, measure) 
     271         
     272         
     273        OWGUI.comboBox(discreteBox, self, "sortBy", label = "Sort by"+"  ", 
     274                       items = ["No Sorting", "Attribute Name", "Number of Values"] + \ 
     275                               [name for name in self.discMeasures], 
     276                       orientation=0, valueType = int, 
     277                       callback=self.sortingChanged) 
     278         
     279        OWGUI.comboBox(continuousBox, self, "sortBy", label = "Sort by"+"  ", 
     280                       items = ["No Sorting", "Attribute Name", "Number of Values"] + \ 
     281                               [name for name in self.contMeasures], 
     282                       orientation=0, valueType = int, 
     283                       callback=self.sortingChanged) 
    133284 
    134285        box = OWGUI.widgetBox(self.controlArea, "Discretization", 
     
    137288                   label="Intervals: ", 
    138289                   orientation=0, 
    139                    tooltip="Disctetization for measures which cannot score continuous attributes", 
     290                   tooltip="Disctetization for measures which cannot score continuous attributes.", 
    140291                   callback=self.discretizationChanged, 
    141292                   callbackOnReturn=True) 
     
    244395        self.resetInternals() 
    245396        self.updateDelegates() 
     397        self.updateVisibleScoreColumns() 
    246398 
    247399#        self.connect(self.table.horizontalHeader(), SIGNAL("sectionClicked(int)"), self.headerClick) 
     
    263415            self.handlesContinuous = self.discHandlesContinuous 
    264416            self.estimators = self.discEstimators 
    265             self.measuresAttrs = self.discMeasuresAttrs 
    266417        else: 
    267418            self.ranksView = self.contRanksView 
     
    271422            self.handlesContinuous = self.contHandlesContinuous 
    272423            self.estimators = self.contEstimators 
    273             self.measuresAttrs = self.contMeasuresAttrs 
     424             
     425        self.updateVisibleScoreColumns() 
    274426             
    275427    def setData(self, data): 
     
    310462                self.setLogORTitle() 
    311463            self.ranksView.setSortingEnabled(self.sortBy > 0) 
    312             self.ranksView 
    313464             
    314465        self.applyIf() 
     
    329480         
    330481        if measuresMask is None: 
    331             measuresMask = [True] * len(measures) 
    332         for measure_index, (est, meas, handles, mask) in enumerate(zip( 
    333                 estimators, measures, handlesContinous, measuresMask)): 
     482            # Update all selected measures 
     483            measuresMask = [self.selectedMeasures.get(m) for m in measures] 
     484         
     485        for measure_index, (est, meas, mask) in enumerate(zip( 
     486                estimators, measures, measuresMask)): 
    334487            if not mask: 
    335488                continue 
    336             if meas == "ReliefF": 
    337                 est = est() 
    338                 est.k = self.reliefK 
    339                 est.m = self.reliefN 
    340             else: 
    341                 est = est() 
     489            handles = MEASURES_HANDLES_CONTINUOUS.get(meas, False) 
     490            params = measure_parameters(est) 
     491            estimator = est() 
     492            if params: 
     493                for p in params: 
     494                    setattr(estimator, p.name, 
     495                            getattr(self, param_attr_name(est, p))) 
     496                     
    342497            if not handles: 
    343498                data = self.getDiscretizedData() 
     
    352507                if attr is not None: 
    353508                    try: 
    354                         s = est(attr, data) 
     509                        s = estimator(attr, data) 
    355510                    except Exception, ex: 
    356511                        self.warning(measure_index, "Error evaluating %r: %r" % (meas, str(ex))) 
     
    367522            self.measure_scores[measure_index] = attr_scores 
    368523         
    369         self.updateRankModel() 
     524        self.updateRankModel(measuresMask) 
    370525        self.ranksProxyModel.invalidate() 
    371526         
     
    384539                else: 
    385540                    values_one.append(None) 
    386                  
    387 #                if s is None: 
    388 #                    s = "NA" 
    389541                item = self.ranksModel.item(j, i + 2) 
    390542                if not item: 
     
    401553                    if v is not None: 
    402554                        # Set the bar ratio role for i-th measure. 
    403                         ratio = (v - vmin) / ((vmax - vmin) or 1) 
     555                        ratio = float((v - vmin) / ((vmax - vmin) or 1)) 
    404556                        if self.showDistributions: 
    405557                            self.ranksModel.item(j, i + 2).setData(QVariant(ratio), OWGUI.BarRatioRole) 
     
    412564             
    413565    def cbShowDistributions(self): 
     566        # This should be handled by the delegates only (must always set the BarRatioRole 
    414567        self.updateRankModel() 
    415568        # Need to update the selection 
     
    446599 
    447600    def onSelectionChanged(self, *args): 
    448         """ Called when the ranks vire selection changes. 
     601        """ Called when the ranks view selection changes. 
    449602        """ 
    450603        selected = self.selectedAttrs() 
    451604        self.clearButton.setEnabled(bool(selected)) 
    452 #        # Change the selectionMethod to manual if necessary. 
    453 #        if self.selectMethod == 0 and len(selected) != self.ranksModel.rowCount(): 
    454 #            self.selectMethod = 1 
    455 #        elif self.selectMethod == 2: 
    456 #            inds = self.ranksView.selectionModel().selectedRows(0) 
    457 #            rows = [ind.row() for ind in inds] 
    458 #            if set(rows) != set(range(self.nSelected)): 
    459 #                self.selectMethod = 1 
    460                  
    461605        self.applyIf() 
    462606         
     
    500644        self.updateScores([not b for b in self.handlesContinuous]) 
    501645        self.autoSelection() 
    502  
    503     def reliefChanged(self): 
    504         self.updateScores([m == "ReliefF" for m in self.measures]) 
    505         self.autoSelection() 
    506  
    507     def loadReliefDefaults(self): 
    508         self.reliefK = 5 
    509         self.reliefN = 20 
    510         self.reliefChanged() 
     646         
     647    def measureParamChanged(self, name, param=None): 
     648        index = self.measures.index(name) 
     649        measure = self.estimators[index] 
     650        mask = [i == index for i, _ in enumerate(self.measures)] 
     651        self.updateScores(mask) 
     652     
     653    def loadMeasureDefaults(self, name): 
     654        index = self.measures.index(name) 
     655        measure = self.estimators[index] 
     656        params = measure_parameters(measure) 
     657        for i, p in enumerate(params): 
     658            setattr(self, param_attr_name(measure, p), p.default) 
     659        self.measureParamChanged(name) 
    511660         
    512661    def autoSelection(self): 
     
    561710 
    562711    def setLogORTitle(self): 
    563         var =self.data.domain.classVar  
     712        var =self.data.domain.classVar     
    564713        if len(var.values) == 2: 
    565714            title = "log OR (for %r)" % var.values[1][:10] 
    566715        else: 
    567716            title = "log OR" 
    568         item = PyStandardItem(title) 
    569         self.ranksModel.setHorizontalHeaderItem(self.ranksModel.columnCount() - 1, item) 
    570         return  
    571  
    572     def measuresChanged(self): 
     717        if "Log Odds Ratio" in self.discEstimators: 
     718            index = self.discMeasures.index("Log Odds Ratio") 
     719            item = PyStandardItem(title) 
     720            self.ranksModel.setHorizontalHeaderItem(index + 2, item) 
     721 
     722    def measuresSelectionChanged(self, name=None): 
    573723        """ Measure selection has changed. Update column visibility. 
    574724        """ 
    575         for i, valName in enumerate(self.measuresAttrs): 
    576             shown = getattr(self, valName, True) 
     725        if name is None: 
     726            # Update all scores 
     727            measuresMask = None 
     728        else: 
     729            # Update scores for shown column if they are not yet computed. 
     730            shown = self.selectedMeasures.get(name, False) 
     731            index = self.measures.index(name) 
     732            if all(s is None for s in self.measure_scores[index]) and shown: 
     733                measuresMask = [n == name for n in self.measures] 
     734            else: 
     735                measuresMask = [False] * len(self.measures) 
     736        self.updateScores(measuresMask) 
     737         
     738        self.updateVisibleScoreColumns() 
     739             
     740    def updateVisibleScoreColumns(self): 
     741        """ Update the visible columns of the scores view. 
     742        """ 
     743        for i, measure in enumerate(self.measures): 
     744            shown = self.selectedMeasures.get(measure) 
    577745            self.ranksView.setColumnHidden(i + 2, not shown) 
    578746 
     
    625793            return [self.data.domain.attributes[i] for i in inds] 
    626794        else: 
    627             return [] 
     795            return []     
    628796 
    629797class RankItemDelegate(QStyledItemDelegate): 
     
    729897        left = left.data(role).toPyObject() 
    730898        right = right.data(role).toPyObject() 
    731 #        print left, right 
    732899        return left < right 
    733900 
     
    737904    ow.setData(orange.ExampleTable("wine.tab")) 
    738905    ow.setData(orange.ExampleTable("zoo.tab")) 
    739 #    ow.setData(orange.ExampleTable("servo.tab")) 
     906    ow.setData(orange.ExampleTable("servo.tab")) 
     907    ow.setData(orange.ExampleTable("iris.tab")) 
    740908#    ow.setData(orange.ExampleTable("auto-mpg.tab")) 
    741909    ow.show() 
Note: See TracChangeset for help on using the changeset viewer.