Source code for skcriteria.madm.electre

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

"""ELimination Et Choix Traduisant la REalité - ELECTRE.

ELECTRE is a family of multi-criteria decision analysis methods
that originated in Europe in the mid-1960s. The acronym ELECTRE stands for:
ELimination Et Choix Traduisant la REalité (ELimination and Choice Expressing
REality).

Usually the ELECTRE Methods are used to discard some alternatives to the
problem, which are unacceptable. After that we can use another MCDA to select
the best one. The Advantage of using the Electre Methods before is that we
can apply another MCDA with a restricted set of alternatives saving much time.

"""

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

import numpy as np

from ..core import KernelResult, Objective, SKCDecisionMakerABC
from ..utils import doc_inherit

# =============================================================================
# CONCORDANCE
# =============================================================================


def _conc_row(row, matrix, matrix_objectives, matrix_weights):
    difference = row - matrix
    outrank = (
        (matrix_objectives == Objective.MAX.value) & (difference >= 0)
    ) | ((matrix_objectives == Objective.MIN.value) & (difference <= 0))
    filter_weights = matrix_weights * outrank.astype(int)
    new_row = np.sum(filter_weights, axis=1)
    return new_row


[docs]def concordance(matrix, objectives, weights): """Calculate the concordance matrix.""" matrix_len = len(matrix) matrix_objectives = np.tile(objectives, (matrix_len, 1)) matrix_weights = np.tile(weights, (matrix_len, 1)) matrix_concordance = np.empty((matrix_len, matrix_len), dtype=float) for idx, row in enumerate(matrix): new_row = _conc_row(row, matrix, matrix_objectives, matrix_weights) matrix_concordance[idx] = new_row np.fill_diagonal(matrix_concordance, np.nan) return matrix_concordance
# ============================================================================= # DISCORDANCE # ============================================================================= def _disc_row(row, mtx, matrix_objectives, max_range): difference = mtx - row worsts = ( (matrix_objectives == Objective.MAX.value) & (difference > 0) ) | ((matrix_objectives == Objective.MIN.value) & (difference < 0)) filter_difference = np.abs(difference * worsts) delta = filter_difference / max_range new_row = np.max(delta, axis=1) return new_row
[docs]def discordance(matrix, objectives): """Calculate the discordance matrix.""" matrix_len = len(matrix) matrix_objectives = np.tile(objectives, (matrix_len, 1)) max_range = (np.max(matrix, axis=0) - np.min(matrix, axis=0)).max() matrix_discordance = np.empty((matrix_len, matrix_len), dtype=float) for idx, row in enumerate(matrix): matrix_discordance[idx] = _disc_row( row, matrix, matrix_objectives, max_range ) np.fill_diagonal(matrix_discordance, np.nan) return matrix_discordance
# ============================================================================= # ELECTRE 1 # =============================================================================
[docs]def electre1(matrix, objectives, weights, p=0.65, q=0.35): """Execute ELECTRE1 without any validation.""" # get the concordance and discordance info matrix_concordance = concordance(matrix, objectives, weights) matrix_discordance = discordance(matrix, objectives) with np.errstate(invalid="ignore"): outrank = (matrix_concordance >= p) & (matrix_discordance <= q) kernel = ~outrank.any(axis=0) return kernel, outrank, matrix_concordance, matrix_discordance
[docs]class ELECTRE1(SKCDecisionMakerABC): """Find a the kernel solution through ELECTRE-1. The ELECTRE I model find the kernel solution in a situation where true criteria and restricted outranking relations are given. That is, ELECTRE I cannot derive the ranking of alternatives but the kernel set. In ELECTRE I, two indices called the concordance index and the discordance index are used to measure the relations between objects Parameters ---------- p : float, optional (default=0.65) Concordance threshold. Threshold of how much one alternative is at least as good as another to be significative. q : float, optional (default=0.35) Discordance threshold. Threshold of how much the degree one alternative is strictly preferred to another to be significative. References ---------- :cite:p:`roy1990outranking` :cite:p:`roy1968classement` :cite:p:`tzeng2011multiple` """ def __init__(self, p=0.65, q=0.35): self.p = p self.q = q @property def p(self): """Concordance threshold.""" return self._p @p.setter def p(self, p): self._p = float(p) @property def q(self): """Discordance threshold.""" return self._q @q.setter def q(self, q): self._q = float(q) @doc_inherit(SKCDecisionMakerABC._evaluate_data) def _evaluate_data(self, matrix, objectives, weights, **kwargs): kernel, outrank, matrix_concordance, matrix_discordance = electre1( matrix, objectives, weights, self.p, self.q ) return kernel, { "outrank": outrank, "matrix_concordance": matrix_concordance, "matrix_discordance": matrix_discordance, } @doc_inherit(SKCDecisionMakerABC._make_result) def _make_result(self, alternatives, values, extra): return KernelResult( "ELECTRE1", alternatives=alternatives, values=values, extra=extra )