Package mvpa :: Package measures :: Module base

[hide private]
[frames] | no frames]

# Source Code for Module mvpa.measures.base

      1  # emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
      2  # vi: set ft=python sts=4 ts=4 sw=4 et:
      3  ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
      4  #
      5  #   See COPYING file distributed along with the PyMVPA package for the
      6  #   copyright and license terms.
      7  #
      8  ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
      9  """Base class for data measures: algorithms that quantify properties of
     10  datasets.
     11
     12  Besides the `DatasetMeasure` base class this module also provides the
     13  (abstract) `FeaturewiseDatasetMeasure` class. The difference between a general
     14  measure and the output of the `FeaturewiseDatasetMeasure` is that the latter
     15  returns a 1d map (one value per feature in the dataset). In contrast there are
     16  no restrictions on the returned value of `DatasetMeasure` except for that it
     17  has to be in some iterable container.
     18
     19  """
     20
     21  __docformat__ = 'restructuredtext'
     22
     23  import numpy as N
     24  import mvpa.support.copy as copy
     25
     26  from mvpa.misc.state import StateVariable, ClassWithCollections
     27  from mvpa.misc.args import group_kwargs
     28  from mvpa.misc.transformers import FirstAxisMean, SecondAxisSumOfAbs
     29  from mvpa.base.dochelpers import enhancedDocString
     30  from mvpa.base import externals, warning
     31  from mvpa.clfs.stats import autoNullDist
     32
     33  if __debug__:
     34      from mvpa.base import debug
     35
     36
     37 -class DatasetMeasure(ClassWithCollections):
     38      """A measure computed from a `Dataset`
     39
     40      All dataset measures support arbitrary transformation of the measure
     41      after it has been computed. Transformation are done by processing the
     42      measure with a functor that is specified via the `transformer` keyword
     43      argument of the constructor. Upon request, the raw measure (before
     44      transformations are applied) is stored in the `raw_results` state variable.
     45
     46      Additionally all dataset measures support the estimation of the
     47      probabilit(y,ies) of a measure under some distribution. Typically this will
     48      be the NULL distribution (no signal), that can be estimated with
     49      permutation tests. If a distribution estimator instance is passed to the
     50      `null_dist` keyword argument of the constructor the respective
     51      probabilities are automatically computed and stored in the `null_prob`
     52      state variable.
     53
     54      .. note::
     55        For developers: All subclasses shall get all necessary parameters via
     56        their constructor, so it is possible to get the same type of measure for
     57        multiple datasets by passing them to the __call__() method successively.
     58      """
     59
     60      raw_results = StateVariable(enabled=False,
     61          doc="Computed results before applying any " +
     62              "transformation algorithm")
     63      null_prob = StateVariable(enabled=True)
     64      """Stores the probability of a measure under the NULL hypothesis"""
     65      null_t = StateVariable(enabled=False)
     66      """Stores the t-score corresponding to null_prob under assumption
     67      of Normal distribution"""
     68
     69 -    def __init__(self, transformer=None, null_dist=None, **kwargs):
     70          """Does nothing special.
     71
     72          :Parameters:
     73            transformer: Functor
     74              This functor is called in `__call__()` to perform a final
     75              processing step on the to be returned dataset measure. If None,
     76              nothing is called
     77            null_dist: instance of distribution estimator
     78              The estimated distribution is used to assign a probability for a
     79              certain value of the computed measure.
     80          """
     81          ClassWithCollections.__init__(self, **kwargs)
     82
     83          self.__transformer = transformer
     84          """Functor to be called in return statement of all subclass __call__()
     85          methods."""
     86          null_dist_ = autoNullDist(null_dist)
     87          if __debug__:
     88              debug('SA', 'Assigning null_dist %s whenever original given was %s'
     89                    % (null_dist_, null_dist))
     90          self.__null_dist = null_dist_
     91
     92
     93      __doc__ = enhancedDocString('DatasetMeasure', locals(), ClassWithCollections)
     94
     95
     96 -    def __call__(self, dataset):
     97          """Compute measure on a given `Dataset`.
     98
     99          Each implementation has to handle a single arguments: the source
    100          dataset.
    101
    102          Returns the computed measure in some iterable (list-like)
    103          container applying transformer if such is defined
    104          """
    105          result = self._call(dataset)
    106          result = self._postcall(dataset, result)
    107          return result
    108
    109
    110 -    def _call(self, dataset):
    111          """Actually compute measure on a given `Dataset`.
    112
    113          Each implementation has to handle a single arguments: the source
    114          dataset.
    115
    116          Returns the computed measure in some iterable (list-like) container.
    117          """
    118          raise NotImplemented
    119
    120
    121 -    def _postcall(self, dataset, result):
    122          """Some postprocessing on the result
    123          """
    124          self.states.raw_results = result
    125          if not self.__transformer is None:
    126              if __debug__:
    127                  debug("SA_", "Applying transformer %s" % self.__transformer)
    128              result = self.__transformer(result)
    129
    130          # estimate the NULL distribution when functor is given
    131          if not self.__null_dist is None:
    132              if __debug__:
    133                  debug("SA_", "Estimating NULL distribution using %s"
    134                        % self.__null_dist)
    135
    136              # we need a matching datameasure instance, but we have to disable
    137              # the estimation of the null distribution in that child to prevent
    138              # infinite looping.
    139              measure = copy.copy(self)
    140              measure.__null_dist = None
    141              self.__null_dist.fit(measure, dataset)
    142
    143              if self.states.isEnabled('null_t'):
    144                  # get probability under NULL hyp, but also request
    145                  # either it belong to the right tail
    146                  null_prob, null_right_tail = \
    147                             self.__null_dist.p(result, return_tails=True)
    148                  self.states.null_prob = null_prob
    149
    150                  externals.exists('scipy', raiseException=True)
    151                  from scipy.stats import norm
    152
    153                  # TODO: following logic should appear in NullDist,
    154                  #       not here
    155                  tail = self.null_dist.tail
    156                  if tail == 'left':
    157                      acdf = N.abs(null_prob)
    158                  elif tail == 'right':
    159                      acdf = 1.0 - N.abs(null_prob)
    160                  elif tail in ['any', 'both']:
    161                      acdf = 1.0 - N.clip(N.abs(null_prob), 0, 0.5)
    162                  else:
    163                      raise RuntimeError, 'Unhandled tail %s' % tail
    164                  # We need to clip to avoid non-informative inf's ;-)
    165                  # that happens due to lack of precision in mantissa
    166                  # which is 11 bits in double. We could clip values
    167                  # around 0 at as low as 1e-100 (correspond to z~=21),
    168                  # but for consistency lets clip at 1e-16 which leads
    169                  # to distinguishable value around p=1 and max z=8.2.
    170                  # Should be sufficient range of z-values ;-)
    171                  clip = 1e-16
    172                  null_t = norm.ppf(N.clip(acdf, clip, 1.0 - clip))
    173                  # assure that we deal with arrays:
    174                  null_t = N.array(null_t, ndmin=1, copy=False)
    175                  null_t[~null_right_tail] *= -1.0 # revert sign for negatives
    176                  self.states.null_t = null_t                 # store
    177              else:
    178                  # get probability of result under NULL hypothesis if available
    179                  # and don't request tail information
    180                  self.null_prob = self.__null_dist.p(result)
    181
    182          return result
    183
    184
    185 -    def __repr__(self, prefixes=[]):
    186          """String representation of DatasetMeasure
    187
    188          Includes only arguments which differ from default ones
    189          """
    190          prefixes = prefixes[:]
    191          if self.__transformer is not None:
    192              prefixes.append("transformer=%s" % self.__transformer)
    193          if self.__null_dist is not None:
    194              prefixes.append("null_dist=%s" % self.__null_dist)
    195          return super(DatasetMeasure, self).__repr__(prefixes=prefixes)
    196
    197 -    def untrain(self):
    198          """'Untraining' Measure
    199
    200          Some derived classes might used classifiers, so we need to
    201          untrain those
    202          """
    203          pass
    204
    205      @property
    206 -    def null_dist(self):
    207          """Return Null Distribution estimator"""
    208          return self.__null_dist
    209
    210      @property
    211 -    def transformer(self):
    212          """Return transformer"""
    213          return self.__transformer
    214
    215
    216 -class FeaturewiseDatasetMeasure(DatasetMeasure):
    217      """A per-feature-measure computed from a `Dataset` (base class).
    218
    219      Should behave like a DatasetMeasure.
    220      """
    221
    222      base_sensitivities = StateVariable(enabled=False,
    223          doc="Stores basic sensitivities if the sensitivity " +
    224              "relies on combining multiple ones")
    225
    226      # XXX should we may be default to combiner=None to avoid
    227      # unexpected results? Also rethink if we need combiner here at
    228      # all... May be combiners should be 'adjoint' with transformer
    229      # YYY in comparison to CombinedSensitivityAnalyzer here default
    230      #     value for combiner is worse than anywhere. From now on,
    231      #     default combiners should be provided "in place", ie
    232      #     in SMLR it makes sense to have SecondAxisMaxOfAbs,
    233      #     in SVM (pair-wise) only for not-binary should be
    234      #     SecondAxisSumOfAbs, though could be Max as well... uff
    235      #   YOH: started to do so, but still have issues... thus
    236      #        reverting back for now
    237      #   MH: Full ack -- voting for no default combiners!
    238 -    def __init__(self, combiner=SecondAxisSumOfAbs, **kwargs): # SecondAxisSumOfAbs
    239          """Initialize
    240
    241          :Parameters:
    242            combiner : Functor
    243              The combiner is only applied if the computed featurewise dataset
    244              measure is more than one-dimensional. This is different from a
    245              `transformer`, which is always applied. By default, the sum of
    246              absolute values along the second axis is computed.
    247          """
    248          DatasetMeasure.__init__(self, **kwargs)
    249
    250          self.__combiner = combiner
    251
    252 -    def __repr__(self, prefixes=None):
    253          if prefixes is None:
    254              prefixes = []
    255          if self.__combiner != SecondAxisSumOfAbs:
    256              prefixes.append("combiner=%s" % self.__combiner)
    257          return \
    258              super(FeaturewiseDatasetMeasure, self).__repr__(prefixes=prefixes)
    259
    260
    261 -    def _call(self, dataset):
    262          """Computes a per-feature-measure on a given `Dataset`.
    263
    264          Behaves like a `DatasetMeasure`, but computes and returns a 1d ndarray
    265          with one value per feature.
    266          """
    267          raise NotImplementedError
    268
    269
    270 -    def _postcall(self, dataset, result):
    271          """Adjusts per-feature-measure for computed `result`
    272
    273
    274          TODO: overlaps in what it does heavily with
    275           CombinedSensitivityAnalyzer, thus this one might make use of
    276           CombinedSensitivityAnalyzer yoh thinks, and here
    277           base_sensitivities doesn't sound appropriate.
    278           MH: There is indeed some overlap, but also significant differences.
    279               This one operates on a single sensana and combines over second
    280               axis, CombinedFeaturewiseDatasetMeasure uses first axis.
    281               Additionally, 'Sensitivity' base class is
    282               FeaturewiseDatasetMeasures which would have to be changed to
    283               CombinedFeaturewiseDatasetMeasure to deal with stuff like
    284               SMLRWeights that return multiple sensitivity values by default.
    285               Not sure if unification of both (and/or removal of functionality
    286               here does not lead to an overall more complicated situation,
    287               without any real gain -- after all this one works ;-)
    288          """
    289          # !!! This is not stupid -- it is intended -- some times we might get
    290          #     scalars as input.
    291          result = N.atleast_1d(result)
    292          result_sq = result.squeeze()
    293          # Assure that we have some iterable (could be a scalar if it
    294          # was just a single value in 1D array)
    295          result_sq = N.atleast_1d(result_sq)
    296
    297          if len(result_sq.shape)>1:
    298              n_base = result.shape[1]
    299              """Number of base sensitivities"""
    300              if self.states.isEnabled('base_sensitivities'):
    301                  b_sensitivities = []
    302                  if not self.states.isKnown('biases'):
    303                      biases = None
    304                  else:
    305                      biases = self.states.biases
    306                      if len(self.states.biases) != n_base:
    307                          warning("Number of biases %d differs from number "
    308                                  "of base sensitivities %d which could happen "
    309                                  "when measure is collided across labels."
    310                                  % (len(self.states.biases), n_base))
    311                  for i in xrange(n_base):
    312                      if not biases is None:
    313                          if n_base > 1 and len(biases) == 1:
    314                              # The same bias for all bases
    315                              bias = biases[0]
    316                          else:
    317                              bias = biases[i]
    318                      else:
    319                          bias = None
    320                      b_sensitivities = StaticDatasetMeasure(
    321                          measure = result[:,i],
    322                          bias = bias)
    323                  self.states.base_sensitivities = b_sensitivities
    324
    325              # After we stored each sensitivity separately,
    326              # we can apply combiner
    327              if self.__combiner is not None:
    328                  result = self.__combiner(result)
    329          else:
    330              # remove bogus dimensions
    331              # XXX we might need to come up with smth better. May be some naive
    332              # combiner? :-)
    333              result = result_sq
    334
    335          # call base class postcall
    336          result = DatasetMeasure._postcall(self, dataset, result)
    337
    338          return result
    339
    340      @property
    341 -    def combiner(self):
    342          """Return combiner"""
    343          return self.__combiner
    344
    345
    346
    347 -class StaticDatasetMeasure(DatasetMeasure):
    348      """A static (assigned) sensitivity measure.
    349
    350      Since implementation is generic it might be per feature or
    351      per whole dataset
    352      """
    353
    354 -    def __init__(self, measure=None, bias=None, *args, **kwargs):
    355          """Initialize.
    356
    357          :Parameters:
    358            measure
    359               actual sensitivity to be returned
    360            bias
    361               optionally available bias
    362          """
    363          DatasetMeasure.__init__(self, *args, **kwargs)
    364          if measure is None:
    365              raise ValueError, "Sensitivity measure has to be provided"
    366          self.__measure = measure
    367          self.__bias = bias
    368
    369 -    def _call(self, dataset):
    370          """Returns assigned sensitivity
    371          """
    372          return self.__measure
    373
    374      #XXX Might need to move into StateVariable?
    375      bias = property(fget=lambda self:self.__bias)
    376
    377
    378
    379  #
    380  # Flavored implementations of FeaturewiseDatasetMeasures
    381
    382 -class Sensitivity(FeaturewiseDatasetMeasure):
    383
    384      _LEGAL_CLFS = []
    385      """If Sensitivity is classifier specific, classes of classifiers
    386      should be listed in the list
    387      """
    388
    389 -    def __init__(self, clf, force_training=True, **kwargs):
    390          """Initialize the analyzer with the classifier it shall use.
    391
    392          :Parameters:
    393            clf : :class:`Classifier`
    394              classifier to use.
    395            force_training : Bool
    396              if classifier was already trained -- do not retrain
    397          """
    398
    399          """Does nothing special."""
    400          FeaturewiseDatasetMeasure.__init__(self, **kwargs)
    401
    402          _LEGAL_CLFS = self._LEGAL_CLFS
    403          if len(_LEGAL_CLFS) > 0:
    404              found = False
    405              for clf_class in _LEGAL_CLFS:
    406                  if isinstance(clf, clf_class):
    407                      found = True
    408                      break
    409              if not found:
    410                  raise ValueError, \
    411                    "Classifier %s has to be of allowed class (%s), but is %s" \
    412                                % (clf, _LEGAL_CLFS, `type(clf)`)
    413
    414          self.__clf = clf
    415          """Classifier used to computed sensitivity"""
    416
    417          self._force_training = force_training
    418          """Either to force it to train"""
    419
    420 -    def __repr__(self, prefixes=None):
    421          if prefixes is None:
    422              prefixes = []
    423          prefixes.append("clf=%s" % repr(self.clf))
    424          if not self._force_training:
    425              prefixes.append("force_training=%s" % self._force_training)
    426          return super(Sensitivity, self).__repr__(prefixes=prefixes)
    427
    428
    429 -    def __call__(self, dataset=None):
    430          """Train classifier on `dataset` and then compute actual sensitivity.
    431
    432          If the classifier is already trained it is possible to extract the
    433          sensitivities without passing a dataset.
    434          """
    435          # local bindings
    436          clf = self.__clf
    437          if not clf.trained or self._force_training:
    438              if dataset is None:
    439                  raise ValueError, \
    440                        "Training classifier to compute sensitivities requires " \
    441                        "a dataset."
    442              if __debug__:
    443                  debug("SA", "Training classifier %s %s" %
    444                        (`clf`,
    445                         {False: "since it wasn't yet trained",
    446                          True:  "although it was trained previousely"}
    447                         [clf.trained]))
    448              clf.train(dataset)
    449
    450          return FeaturewiseDatasetMeasure.__call__(self, dataset)
    451
    452
    453 -    def _setClassifier(self, clf):
    454          self.__clf = clf
    455
    456
    457 -    def untrain(self):
    458          """Untrain corresponding classifier for Sensitivity
    459          """
    460          if self.__clf is not None:
    461              self.__clf.untrain()
    462
    463      @property
    464 -    def feature_ids(self):
    465          """Return feature_ids used by the underlying classifier
    466          """
    467          return self.__clf._getFeatureIds()
    468
    469
    470      clf = property(fget=lambda self:self.__clf,
    471                     fset=_setClassifier)
    472
    473
    474
    475 -class CombinedFeaturewiseDatasetMeasure(FeaturewiseDatasetMeasure):
    476      """Set sensitivity analyzers to be merged into a single output"""
    477
    478      sensitivities = StateVariable(enabled=False,
    479          doc="Sensitivities produced by each analyzer")
    480
    481      # XXX think again about combiners... now we have it in here and as
    482      #     well as in the parent -- FeaturewiseDatasetMeasure
    483      # YYY because we don't use parent's _call. Needs RF
    484 -    def __init__(self, analyzers=None,  # XXX should become actually 'measures'
    485                   combiner=None, #FirstAxisMean,
    486                   **kwargs):
    487          """Initialize CombinedFeaturewiseDatasetMeasure
    488
    489          :Parameters:
    490            analyzers : list or None
    491              List of analyzers to be used. There is no logic to populate
    492              such a list in __call__, so it must be either provided to
    493              the constructor or assigned to .analyzers prior calling
    494          """
    495          if analyzers is None:
    496              analyzers = []
    497
    498          FeaturewiseDatasetMeasure.__init__(self, **kwargs)
    499          self.__analyzers = analyzers
    500          """List of analyzers to use"""
    501
    502          self.__combiner = combiner
    503          """Which functor to use to combine all sensitivities"""
    504
    505
    506 -    def _call(self, dataset):
    507          sensitivities = []
    508          for ind,analyzer in enumerate(self.__analyzers):
    509              if __debug__:
    510                  debug("SA", "Computing sensitivity for SA#%d:%s" %
    511                        (ind, analyzer))
    512              sensitivity = analyzer(dataset)
    513              sensitivities.append(sensitivity)
    514
    515          self.states.sensitivities = sensitivities
    516          if __debug__:
    517              debug("SA",
    518                    "Returning combined using %s sensitivity across %d items" %
    519                    (self.__combiner, len(sensitivities)))
    520
    521          if self.__combiner is not None:
    522              sensitivities = self.__combiner(sensitivities)
    523          else:
    524              # assure that we have an ndarray on output
    525              sensitivities = N.asarray(sensitivities)
    526          return sensitivities
    527
    528
    529 -    def untrain(self):
    530          """Untrain CombinedFDM
    531          """
    532          if self.__analyzers is not None:
    533              for anal in self.__analyzers:
    534                  anal.untrain()
    535
    536 -    def _setAnalyzers(self, analyzers):
    537          """Set the analyzers
    538          """
    539          self.__analyzers = analyzers
    540          """Analyzers to use"""
    541
    542      analyzers = property(fget=lambda x:x.__analyzers,
    543                           fset=_setAnalyzers,
    544                           doc="Used analyzers")
    545
    546
    547  # XXX Why did we come to name everything analyzer? inputs of regular
    548  #     things like CombinedFeaturewiseDatasetMeasure can be simple
    549  #     measures....
    550
    551 -class SplitFeaturewiseDatasetMeasure(FeaturewiseDatasetMeasure):
    552      """Compute measures across splits for a specific analyzer"""
    553
    554      # XXX This beast is created based on code of
    555      #     CombinedFeaturewiseDatasetMeasure, thus another reason to refactor
    556
    557      sensitivities = StateVariable(enabled=False,
    558          doc="Sensitivities produced for each split")
    559
    560      splits = StateVariable(enabled=False, doc=
    561         """Store the actual splits of the data. Can be memory expensive""")
    562
    563 -    def __init__(self, splitter, analyzer,
    564                   insplit_index=0, combiner=None, **kwargs):
    565          """Initialize SplitFeaturewiseDatasetMeasure
    566
    567          :Parameters:
    568            splitter : Splitter
    569              Splitter to use to split the dataset
    570            analyzer : DatasetMeasure
    571              Measure to be used. Could be analyzer as well (XXX)
    572            insplit_index : int
    573              splitter generates tuples of dataset on each iteration
    574              (usually 0th for training, 1st for testing).
    575              On what split index in that tuple to operate.
    576          """
    577
    578          # XXX might want to extend insplit_index to handle 'all', so we store
    579          #     sensitivities for all parts of the splits... not sure if it is needed
    580
    581          # XXX We really think through whole transformer/combiners pipelining
    582
    583          # Here we provide combiner None since if needs to be combined
    584          # within each sensitivity, it better be done within analyzer
    585          FeaturewiseDatasetMeasure.__init__(self, combiner=None, **kwargs)
    586
    587          self.__analyzer = analyzer
    588          """Analyzer to use per split"""
    589
    590          self.__combiner = combiner
    591          """Which functor to use to combine all sensitivities"""
    592
    593          self.__splitter = splitter
    594          """Splitter to be used on the dataset"""
    595
    596          self.__insplit_index = insplit_index
    597
    598
    599 -    def untrain(self):
    600          """Untrain SplitFeaturewiseDatasetMeasure
    601          """
    602          if self.__analyzer is not None:
    603              self.__analyzer.untrain()
    604
    605
    606 -    def _call(self, dataset):
    607          # local bindings
    608          analyzer = self.__analyzer
    609          insplit_index = self.__insplit_index
    610
    611          sensitivities = []
    612          self.states.splits = splits = []
    613          store_splits = self.states.isEnabled("splits")
    614
    615          for ind,split in enumerate(self.__splitter(dataset)):
    616              ds = split[insplit_index]
    617              if __debug__ and "SA" in debug.active:
    618                  debug("SA", "Computing sensitivity for split %d on "
    619                        "dataset %s using %s" % (ind, ds, analyzer))
    620              sensitivity = analyzer(ds)
    621              sensitivities.append(sensitivity)
    622              if store_splits: splits.append(split)
    623
    624          self.states.sensitivities = sensitivities
    625          if __debug__:
    626              debug("SA",
    627                    "Returning sensitivities combined using %s across %d items "
    628                    "generated by splitter %s" %
    629                    (self.__combiner, len(sensitivities), self.__splitter))
    630
    631          if self.__combiner is not None:
    632              sensitivities = self.__combiner(sensitivities)
    633          else:
    634              # assure that we have an ndarray on output
    635              sensitivities = N.asarray(sensitivities)
    636          return sensitivities
    637
    638
    639 -class BoostedClassifierSensitivityAnalyzer(Sensitivity):
    640      """Set sensitivity analyzers to be merged into a single output"""
    641
    642
    643      # XXX we might like to pass parameters also for combined_analyzer
    644      @group_kwargs(prefixes=['slave_'], assign=True)
    645 -    def __init__(self,
    646                   clf,
    647                   analyzer=None,
    648                   combined_analyzer=None,
    649                   slave_kwargs={},
    650                   **kwargs):
    651          """Initialize Sensitivity Analyzer for `BoostedClassifier`
    652
    653          :Parameters:
    654            clf : `BoostedClassifier`
    655              Classifier to be used
    656            analyzer : analyzer
    657              Is used to populate combined_analyzer
    658            slave_*
    659              Arguments to pass to created analyzer if analyzer is None
    660          """
    661          Sensitivity.__init__(self, clf, **kwargs)
    662          if combined_analyzer is None:
    663              # sanitarize kwargs
    664              kwargs.pop('force_training', None)
    665              combined_analyzer = CombinedFeaturewiseDatasetMeasure(**kwargs)
    666          self.__combined_analyzer = combined_analyzer
    667          """Combined analyzer to use"""
    668
    669          if analyzer is not None and len(self._slave_kwargs):
    670              raise ValueError, \
    671                    "Provide either analyzer of slave_* arguments, not both"
    672          self.__analyzer = analyzer
    673          """Analyzer to use for basic classifiers within boosted classifier"""
    674
    675
    676 -    def untrain(self):
    677          """Untrain BoostedClassifierSensitivityAnalyzer
    678          """
    679          if self.__analyzer is not None:
    680              self.__analyzer.untrain()
    681          self.__combined_analyzer.untrain()
    682
    683
    684 -    def _call(self, dataset):
    685          analyzers = []
    686          # create analyzers
    687          for clf in self.clf.clfs:
    688              if self.__analyzer is None:
    689                  analyzer = clf.getSensitivityAnalyzer(**(self._slave_kwargs))
    690                  if analyzer is None:
    691                      raise ValueError, \
    692                            "Wasn't able to figure basic analyzer for clf %s" % \
    693                            `clf`
    694                  if __debug__:
    695                      debug("SA", "Selected analyzer %s for clf %s" % \
    696                            (`analyzer`, `clf`))
    697              else:
    698                  # XXX shallow copy should be enough...
    699                  analyzer = copy.copy(self.__analyzer)
    700
    701              # assign corresponding classifier
    702              analyzer.clf = clf
    703              # if clf was trained already - don't train again
    704              if clf.trained:
    705                  analyzer._force_training = False
    706              analyzers.append(analyzer)
    707
    708          self.__combined_analyzer.analyzers = analyzers
    709
    710          # XXX not sure if we don't want to call directly ._call(dataset) to avoid
    711          # double application of transformers/combiners, after all we are just
    712          # 'proxying' here to combined_analyzer...
    713          # YOH: decided -- lets call ._call
    714          return self.__combined_analyzer._call(dataset)
    715
    716      combined_analyzer = property(fget=lambda x:x.__combined_analyzer)
    717
    718
    719 -class ProxyClassifierSensitivityAnalyzer(Sensitivity):
    720      """Set sensitivity analyzer output just to pass through"""
    721
    722      clf_sensitivities = StateVariable(enabled=False,
    723          doc="Stores sensitivities of the proxied classifier")
    724
    725
    726      @group_kwargs(prefixes=['slave_'], assign=True)
    727 -    def __init__(self,
    728                   clf,
    729                   analyzer=None,
    730                   **kwargs):
    731          """Initialize Sensitivity Analyzer for `BoostedClassifier`
    732          """
    733          Sensitivity.__init__(self, clf, **kwargs)
    734
    735          if analyzer is not None and len(self._slave_kwargs):
    736              raise ValueError, \
    737                    "Provide either analyzer of slave_* arguments, not both"
    738
    739          self.__analyzer = analyzer
    740          """Analyzer to use for basic classifiers within boosted classifier"""
    741
    742
    743 -    def untrain(self):
    744          super(ProxyClassifierSensitivityAnalyzer, self).untrain()
    745          if self.__analyzer is not None:
    746              self.__analyzer.untrain()
    747
    748
    749 -    def _call(self, dataset):
    750          # OPT: local bindings
    751          clfclf = self.clf.clf
    752          analyzer = self.__analyzer
    753
    754          if analyzer is None:
    755              analyzer = clfclf.getSensitivityAnalyzer(
    756                  **(self._slave_kwargs))
    757              if analyzer is None:
    758                  raise ValueError, \
    759                        "Wasn't able to figure basic analyzer for clf %s" % \
    760                        `clfclf`
    761              if __debug__:
    762                  debug("SA", "Selected analyzer %s for clf %s" % \
    763                        (analyzer, clfclf))
    764              # bind to the instance finally
    765              self.__analyzer = analyzer
    766
    767          # TODO "remove" unnecessary things below on each call...
    768          # assign corresponding classifier
    769          analyzer.clf = clfclf
    770
    771          # if clf was trained already - don't train again
    772          if clfclf.trained:
    773              analyzer._force_training = False
    774
    775          result = analyzer._call(dataset)
    776          self.states.clf_sensitivities = result
    777
    778          return result
    779
    780      analyzer = property(fget=lambda x:x.__analyzer)
    781
    782
    783 -class MappedClassifierSensitivityAnalyzer(ProxyClassifierSensitivityAnalyzer):
    784      """Set sensitivity analyzer output be reverse mapped using mapper of the
    785      slave classifier"""
    786
    787 -    def _call(self, dataset):
    788          sens = super(MappedClassifierSensitivityAnalyzer, self)._call(dataset)
    789          # So we have here the case that some sensitivities are given
    790          #  as nfeatures x nclasses, thus we need to take .T for the
    791          #  mapper and revert back afterwards
    792          # devguide's TODO lists this point to 'disguss'
    793          sens_mapped = self.clf.mapper.reverse(sens.T)
    794          return sens_mapped.T
    795
    796
    797 -class FeatureSelectionClassifierSensitivityAnalyzer(ProxyClassifierSensitivityAnalyzer):
    798      """Set sensitivity analyzer output be reverse mapped using mapper of the
    799      slave classifier"""
    800
    801 -    def _call(self, dataset):
    802          sens = super(FeatureSelectionClassifierSensitivityAnalyzer, self)._call(dataset)
    803          # So we have here the case that some sensitivities are given
    804          #  as nfeatures x nclasses, thus we need to take .T for the
    805          #  mapper and revert back afterwards
    806          # devguide's TODO lists this point to 'disguss'
    807          sens_mapped = self.clf.maskclf.mapper.reverse(sens.T)
    808          return sens_mapped.T
    809