Source code for skcriteria.agg.rim

#!/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
# =============================================================================

"""RIM reference ideal method."""

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

from ..utils import hidden

with hidden():

    import numpy as np
    import warnings

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


# =============================================================================
# RIM
# =============================================================================


def _rim_normalize(value, value_range, ref_ideal):
    """
    Normalize a value based on the ideal reference interval and valid range.

    Based on the paper's normalization direction.
    The normalization returns 1.0 if the value is inside
    the reference interval, and decays toward 0.0 when it
    deviates from the ideal, according to its relative distance
    to the boundaries of the valid range.
    """
    range_min, range_max = value_range
    ideal_min, ideal_max = ref_ideal

    if ideal_min <= value <= ideal_max:
        return 1.0
    elif range_min != ideal_min and range_min <= value < ideal_min:
        return 1 - min(abs(value - ideal_min), abs(value - ideal_max)) / abs(
            range_min - ideal_min
        )
    elif ideal_max != range_max and ideal_max < value <= range_max:
        return 1 - min(abs(value - ideal_min), abs(value - ideal_max)) / abs(
            ideal_max - range_max
        )
    raise ValueError(
        f"Value {value} outside normalization range ({range_min}, {range_max})"
    )


def _rim(matrix, weights, ref_ideals, ranges):

    # Normalize the valuation matrix X
    # using the reference ideal and ranges
    # in the sense of the papers RIM
    norm_matrix = np.empty_like(matrix, dtype=float)
    for i in range(matrix.shape[0]):
        for j in range(matrix.shape[1]):
            norm_matrix[i, j] = _rim_normalize(
                matrix[i, j],
                ranges[j],
                ref_ideals[j],
            )

    # Calculate the weighted normalized matrix
    weighted_matrix = norm_matrix * weights

    # Calculate the variation to the normalized
    # reference ideal for each alternative
    i_plus = np.linalg.norm(
        weighted_matrix - weights, axis=1
    )  # distance to ideal
    i_minus = np.linalg.norm(weighted_matrix, axis=1)  # distance to origin

    # Calculate the relative index of each alternative
    relative_index = i_minus / (i_plus + i_minus)
    ranking = rank.rank_values(relative_index, reverse=True)

    return ranking, {
        "score": relative_index,
        "norm_matrix": norm_matrix,
        "weighted_matrix": weighted_matrix,
        "i_plus": i_plus,
        "i_minus": i_minus,
    }


[docs] class RIM(SKCDecisionMakerABC): """ Reference Ideal Method (RIM) for multi-criteria decision analysis. RIM ranks alternatives based on their similarity to a user-defined *reference ideal region* instead of relying on classical ideal and anti-ideal points. This method considers intervals as ideals, allowing more flexible and realistic preference modeling. The method normalizes the decision matrix values with respect to the ideal intervals and the valid ranges of each criterion. Alternatives closer to the ideal intervals receive higher scores. Parameters ---------- ref_ideals : list of tuple Specifies the ideal reference intervals for each criterion. Each tuple should be of the form (ideal_min, ideal_max). If not provided, the default ideal value for the criteria depends on the desired objectives; if it is to be maximized, the highest value within the matrix of that criterion will be set as the ideal value, and for the criteria to be minimized, the minimum value will be used (which generates intervals of length zero). ranges : list of tuple List of tuples specifying the min and max bounds of each criterion Each tuple should be of the form (range_min, range_max). If not provided, they are calculated from the maximum and minimum values of the decision matrix per criterion. References ---------- :cite:p:`cables2016rim` """ _skcriteria_parameters = [] @doc_inherit(SKCDecisionMakerABC._evaluate_data) def _evaluate_data(self, matrix, weights, ref_ideals, ranges, **kwargs): ranking, method_extra = _rim(matrix, weights, ref_ideals, ranges) extra = {"ref_ideals": ref_ideals, "ranges": ranges, **method_extra} return ranking, extra @doc_inherit(SKCDecisionMakerABC._make_result) def _make_result(self, alternatives, values, extra): return RankResult( "RIM", alternatives=alternatives, values=values, extra=extra, ) def _validate_ranges(self, matrix, ref_ideals, ranges): """Validates the consistency and format of ref_ideals and ranges.""" n_criteria = matrix.shape[1] if len(ref_ideals) != n_criteria: raise ValueError( "ref_ideals length must match number of criteria." ) if len(ranges) != n_criteria: raise ValueError("Ranges length must match number of criteria.") if not all( isinstance(ideal, tuple) and len(ideal) == 2 for ideal in ref_ideals ): raise TypeError( "Each ref_ideal must be a tuple or list of length 2." ) if not all(isinstance(rng, tuple) and len(rng) == 2 for rng in ranges): raise TypeError("Each range must be a tuple or list of length 2.") for i, (ideal, valid_range) in enumerate(zip(ref_ideals, ranges)): if not (valid_range[0] <= ideal[0] <= ideal[1] <= valid_range[1]): raise ValueError( f"{ideal} must be within ranges[{i}] = {valid_range}" )
[docs] def evaluate(self, dm, *, ref_ideals=None, ranges=None): """Validate the decision matrix and calculate a ranking. If `ref_ideals` or `ranges` are not provided, default values will be automatically inferred: - If `ref_ideals` is None, an interval of length zero is created using the column-wise maximum (for MAX objectives) or minimum (for MIN objectives) from the decision matrix. - If `ranges` is None, the valid range for each criterion is set to the minimum and maximum values of the corresponding column. Parameters ---------- dm : DecisionMatrix Decision matrix to evaluate. ref_ideals : array-like of tuple, optional Reference ideal intervals (per criterion), where each tuple defines (ideal_min, ideal_max). If None, a degenerate interval is used based on the objectives. ranges : array-like of tuple, optional Ranges (min, max) for each criterion. If None, calculated from column-wise min and max values. Warnings -------- UserWarning If `ref_ideals` or `ranges` are not provided, default values are inferred from the decision matrix. Returns ------- :py:class:`skcriteria.data.RankResult` Ranking. """ df_ranges = dm.matrix.agg(["min", "max"]) if ref_ideals is None: where_max = np.equal(dm.objectives, Objective.MAX) ideals = np.where( where_max, df_ranges.loc["max"], df_ranges.loc["min"] ) ref_ideals = ref_ideals = list( map(tuple, np.stack([ideals, ideals], axis=1)) ) warnings.warn( "No `ref_ideals` were provided. Using default values based " "on the objectives of the decision matrix." "For MAX objectives, the column maximum is used; " "for MIN objectives, the minimum is used. " "This produces reference intervals of length zero." ) if ranges is None: ranges = list(map(tuple, df_ranges.T.to_numpy())) warnings.warn( "No `ranges` were provided. " "Using the minimum and maximum values of each column" "in the decision matrix as default bounds." ) self._validate_ranges(dm.matrix.to_numpy(), ref_ideals, ranges) return self._evaluate_dm(dm, ref_ideals=ref_ideals, ranges=ranges)