Source code for skcriteria.core.methods

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised))
# Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia
# All rights reserved.

# =============================================================================
# DOCS
# =============================================================================

"""Core functionalities of scikit-criteria."""

# =============================================================================
# IMPORTS
# =============================================================================รง

import abc
import inspect

from .data import DecisionMatrix
from ..utils import doc_inherit


# =============================================================================
# BASE DECISION MAKER CLASS
# =============================================================================


_IGNORE_PARAMS = (
    inspect.Parameter.VAR_POSITIONAL,
    inspect.Parameter.VAR_KEYWORD,
)


[docs]class SKCMethodABC(metaclass=abc.ABCMeta): """Base class for all class in scikit-criteria. Notes ----- All estimators should specify: - ``_skcriteria_dm_type``: The type of the decision maker. """ _skcriteria_dm_type = None _skcriteria_parameters = None def __init_subclass__(cls): """Validate if the subclass are well formed.""" decisor_type = cls._skcriteria_dm_type if decisor_type is None: raise TypeError(f"{cls} must redefine '_skcriteria_dm_type'") if ( cls._skcriteria_parameters is None and cls.__init__ is not SKCMethodABC.__init__ ): signature = inspect.signature(cls.__init__) parameters = set() for idx, param_tuple in enumerate(signature.parameters.items()): if idx == 0: # first arugment of a method is the instance continue name, param = param_tuple if param.kind not in _IGNORE_PARAMS: parameters.add(name) cls._skcriteria_parameters = frozenset(parameters) def __repr__(self): """x.__repr__() <==> repr(x).""" cls_name = type(self).__name__ parameters = [] if self._skcriteria_parameters: for pname in sorted(self._skcriteria_parameters): pvalue = getattr(self, pname) parameters.append(f"{pname}={repr(pvalue)}") str_parameters = ", ".join(parameters) return f"{cls_name}({str_parameters})"
# ============================================================================= # SKCTransformer MIXIN # =============================================================================
[docs]class SKCTransformerABC(SKCMethodABC): """Mixin class for all transformer in scikit-criteria.""" _skcriteria_dm_type = "transformer" @abc.abstractmethod def _transform_data(self, **kwargs): """Apply the transformation logic to the decision matrix parameters. Parameters ---------- kwargs: The decision matrix as separated parameters. Returns ------- :py:class:`dict` A dictionary with all the values of the decision matrix transformed. """ raise NotImplementedError()
[docs] def transform(self, dm): """Perform transformation on `dm`. Parameters ---------- dm: :py:class:`skcriteria.data.DecisionMatrix` The decision matrix to transform. Returns ------- :py:class:`skcriteria.data.DecisionMatrix` Transformed decision matrix. """ data = dm.to_dict() transformed_data = self._transform_data(**data) transformed_dm = DecisionMatrix.from_mcda_data(**transformed_data) return transformed_dm
[docs]class SKCMatrixAndWeightTransformerABC(SKCTransformerABC): """Transform weights and matrix together or independently. The Transformer that implements this mixin can be configured to transform `weights`, `matrix` or `both` so only that part of the DecisionMatrix is altered. This mixin require to redefine ``_transform_weights`` and ``_transform_matrix``, instead of ``_transform_data``. """ _TARGET_WEIGHTS = "weights" _TARGET_MATRIX = "matrix" _TARGET_BOTH = "both" def __init__(self, target): self.target = target @property def target(self): """Determine which part of the DecisionMatrix will be transformed.""" return self._target @target.setter def target(self, target): if target not in ( self._TARGET_MATRIX, self._TARGET_WEIGHTS, self._TARGET_BOTH, ): raise ValueError( f"'target' can only be '{self._TARGET_WEIGHTS}', " f"'{self._TARGET_MATRIX}' or '{self._TARGET_BOTH}', " f"found '{target}'" ) self._target = target @abc.abstractmethod def _transform_weights(self, weights): """Execute the transform method over the weights. Parameters ---------- weights: :py:class:`numpy.ndarray` The weights to transform. Returns ------- :py:class:`numpy.ndarray` The transformed weights. """ raise NotImplementedError() @abc.abstractmethod def _transform_matrix(self, matrix): """Execute the transform method over the matrix. Parameters ---------- matrix: :py:class:`numpy.ndarray` The decision matrix to transform Returns ------- :py:class:`numpy.ndarray` The transformed matrix. """ raise NotImplementedError() @doc_inherit(SKCTransformerABC._transform_data) def _transform_data(self, matrix, weights, **kwargs): norm_mtx = matrix norm_weights = weights if self._target in (self._TARGET_MATRIX, self._TARGET_BOTH): norm_mtx = self._transform_matrix(matrix) if self._target in (self._TARGET_WEIGHTS, self._TARGET_BOTH): norm_weights = self._transform_weights(weights) kwargs.update(matrix=norm_mtx, weights=norm_weights, dtypes=None) return kwargs
# ============================================================================= # SK WEIGHTER # =============================================================================
[docs]class SKCWeighterABC(SKCTransformerABC): """Mixin capable of determine the weights of the matrix. This mixin require to redefine ``_weight_matrix``, instead of ``_transform_data``. """ @abc.abstractmethod def _weight_matrix(self, matrix, objectives, weights): """Calculate a new array of weights. Parameters ---------- matrix: :py:class:`numpy.ndarray` The decision matrix to weights. objectives: :py:class:`numpy.ndarray` The objectives in numeric format. weights: :py:class:`numpy.ndarray` The original weights Returns ------- :py:class:`numpy.ndarray` An array of weights. """ raise NotImplementedError() @doc_inherit(SKCTransformerABC._transform_data) def _transform_data(self, matrix, objectives, weights, **kwargs): new_weights = self._weight_matrix( matrix=matrix, objectives=objectives, weights=weights ) kwargs.update( matrix=matrix, objectives=objectives, weights=new_weights ) return kwargs
# ============================================================================= # # =============================================================================
[docs]class SKCDecisionMakerABC(SKCMethodABC): """Mixin class for all decisor based methods in scikit-criteria.""" _skcriteria_dm_type = "decision_maker" @abc.abstractmethod def _evaluate_data(self, **kwargs): raise NotImplementedError() @abc.abstractmethod def _make_result(self, alternatives, values, extra): raise NotImplementedError()
[docs] def evaluate(self, dm): """Validate the dm and calculate and evaluate the alternatives. Parameters ---------- dm: :py:class:`skcriteria.data.DecisionMatrix` Decision matrix on which the ranking will be calculated. Returns ------- :py:class:`skcriteria.data.RankResult` Ranking. """ data = dm.to_dict() result_data, extra = self._evaluate_data(**data) alternatives = data["alternatives"] result = self._make_result( alternatives=alternatives, values=result_data, extra=extra ) return result