Source code for skcriteria.agg.ervd

#!/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
# Copyright (c) 2022-2025 QuatroPe
# All rights reserved.

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

"""Implementation of ERVD method."""

# =============================================================================
# IMPORTS
# =============================================================================

from ..utils import hidden

with hidden():
    import functools

    import numpy as np
    from scipy.spatial import distance

    from ._agg_base import RankResult, SKCDecisionMakerABC
    from ..core import Objective
    from ..utils import doc_inherit, rank

# =============================================================================
# ERVD
# =============================================================================


def _value_function(matrix, reference_points, alpha, lambd, objectives):
    """Value function for ERVD."""
    delta = matrix - reference_points  # Calculate the difference only one time

    maximize_mask = np.broadcast_to(objectives == Objective.MAX, matrix.shape)

    result = np.empty_like(matrix, dtype=float)

    # MAX objectives
    # masks
    gains_min = (delta < 0) & ~maximize_mask
    losses_min = ~gains_min & ~maximize_mask
    gains_max = (delta > 0) & maximize_mask
    losses_max = ~gains_max & maximize_mask

    # apply the value function
    result[gains_max] = delta[gains_max] ** alpha
    result[losses_max] = -lambd * ((-delta[losses_max]) ** alpha)

    # MIN objectives
    result[gains_min] = (-delta[gains_min]) ** alpha
    result[losses_min] = -lambd * (delta[losses_min] ** alpha)

    return result


w_minkowski = functools.partial(distance.minkowski, p=1)


[docs] def ervd( matrix, objectives, weights, reference_points, alpha, lambd, metric, w_metric, **kwargs, ): """Execute ERVD without any validation.""" # apply the value function based on the maximize_mask value_matrix = _value_function( matrix, reference_points, alpha, lambd, objectives ) # create the ideal and the anti ideal arrays ideal = np.max(value_matrix, axis=0) anti_ideal = np.min(value_matrix, axis=0) # calculate distances weight_or_none = weights if w_metric else None s_plus = distance.cdist( value_matrix, [ideal], metric=metric, w=weight_or_none, **kwargs ).flatten() s_minus = distance.cdist( value_matrix, [anti_ideal], metric=metric, w=weight_or_none, **kwargs ).flatten() # relative closeness similarity = s_minus / (s_plus + s_minus) return ( rank.rank_values(similarity, reverse=True), similarity, ideal, anti_ideal, s_plus, s_minus, )
[docs] class ERVD(SKCDecisionMakerABC): """ Election based on Relative Value Distances (ERVD) decision-making method. This method integrates an s-shape value function, departing from the traditional expected utility function, to more accurately capture risk-averse and risk-seeking behaviors. ERVD builds upon the foundational principles of the TOPSIS method, extending its capabilities by incorporating concepts from prospect theory to refine the assessment of alternatives based on their relative distances from ideal and anti-ideal solutions. Parameters ---------- lambda_value: float, default=2.25 Represents the attenuation factor of the losse. alpha_value: float, default=0.88 Diminishing sensitivity parameters. metric: str or callable, default='minkowski' The distance metric to be used for calculating distances between alternatives and ideal/anti-ideal points. It can be a string representing a metric name from `scipy.spatial.distance` or a custom callable function that computes distances. w_metric: bool, default=True Whether to use weights in the distance metric calculation. If True, the weights will be applied to the alternatives when calculating distances. If False, the distances will be calculated without weights. References ---------- :cite:p:`shyur2015multiple` """ _skcriteria_parameters = [ "lambda_value", "alpha_value", "metric", "w_metric", ] def __init__( self, *, lambda_value=2.25, alpha_value=0.88, metric=w_minkowski, w_metric=True, ): if not callable(metric) and metric not in distance._METRICS_NAMES: metrics = ", ".join(f"'{m}'" for m in distance._METRICS_NAMES) raise ValueError( f"Invalid metric '{metric}'. Plese choose from: {metrics}" ) self._metric = metric self._lambd = lambda_value self._alpha = alpha_value self._w_metric = w_metric @property def alpha_value(self): """Diminishing sensitivity parameter.""" return self._alpha @property def lambda_value(self): """Attenuation factor of the losses.""" return self._lambd @property def metric(self): """Which distance metric will be used.""" return self._metric @property def w_metric(self): """Whether to use weights in the metric.""" return self._w_metric @doc_inherit(SKCDecisionMakerABC._make_result) def _make_result(self, alternatives, values, extra): return RankResult( "ERVD", alternatives=alternatives, values=values, extra=extra ) @doc_inherit(SKCDecisionMakerABC._evaluate_data) def _evaluate_data( self, matrix, objectives, weights, reference_points, **kwargs ): rank, similarity, ideal, anti_ideal, s_plus, s_minus = ervd( matrix, objectives, weights, reference_points, self.alpha_value, self.lambda_value, self.metric, self.w_metric, ) return rank, { "similarity": similarity, "ideal": ideal, "anti_ideal": anti_ideal, "s_plus": s_plus, "s_minus": s_minus, } def _validate_reference_points(self, reference_points, matrix): if reference_points is None: raise ValueError( "Reference points must be provided for ERVD evaluation." ) if len(reference_points) != matrix.shape[1]: raise ValueError( "Reference points must match the number of criteria in " "the decision matrix." )
[docs] def evaluate(self, dm, *, reference_points=None): """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. reference_points: array-like, optional Reference points for each criterion. Returns ------- :py:class:`skcriteria.data.RankResult` Ranking. """ data = dm.to_dict() self._validate_reference_points(reference_points, data["matrix"]) result_data, extra = self._evaluate_data( **data, reference_points=reference_points ) alternatives = data["alternatives"] result = self._make_result( alternatives=alternatives, values=result_data, extra=extra ) return result