MCDM

Normalize, weigh and score decision matrices with multiple-criteria decision-making (MCDM) methods.

The functions in this module turn a decision matrix into a ranking. Each row of the matrix is an alternative, such as an area to sample, and each column is a criterion. Ranking takes three steps:

  1. normalize rescales every criterion to [0, 1].
  2. weigh computes the importance of each criterion. You can also set the weights yourself.
  3. score gives each alternative a score.

Optimizer.get_rank runs the three steps. Every step takes its method as a name, such as LINEAR2 or TOPSIS, in any case.

The functions use these parameter names throughout:

This module adapts code from mcdm, Copyright (c) 2020-2022 Dimitrios-Georgios Akestoridis, released under the MIT License.

Input checks

Each method checks its inputs first and raises ValueError if they do not suit it.


source

is_normalized_matrix

def is_normalized_matrix(
    z_matrix:<built-in function array>, # Matrix to check
)->bool:

True if every element of z_matrix is in [0, 1].


source

is_normalized_vector

def is_normalized_vector(
    w_vector:list, # Weights to check
)->bool:

True if no weight in w_vector is negative and the weights sum to 1.


source

check_scoring_input

def check_scoring_input(
    z_matrix:<built-in function array>, # Normalized decision matrix
    w_vector:<built-in function array>, # Weight of each criterion
    is_benefit_z:list, # Benefit (`True`) or cost (`False`) flag of each criterion
    s_method:str, # Scoring method
):

Raise ValueError if the inputs do not suit scoring method s_method.


source

check_weighting_input

def check_weighting_input(
    z_matrix:<built-in function array>, # Normalized decision matrix
    c_method:str, # Correlation method, used by `CRITIC` and `VIC`
    w_method:str, # Weighting method
):

Raise ValueError if the inputs do not suit weighting method w_method.


source

check_normalization_input

def check_normalization_input(
    x_matrix:<built-in function array>, # Decision matrix
    is_benefit_x:list, # Benefit (`True`) or cost (`False`) flag of each criterion
    n_method:str, # Normalization method, or `None` for a matrix already in [0, 1]
):

Raise ValueError if the inputs do not suit normalization method n_method.

Normalization

Normalization rescales each criterion so that criteria measured in different units can be compared. LINEAR1 and LINEAR2 also turn every cost criterion into a benefit criterion.


source

linear1

def linear1(
    x_matrix:<built-in function array>, # Decision matrix
    is_benefit_x:list, # Benefit (`True`) or cost (`False`) flag of each criterion
)->tuple: # The normalized matrix and the new benefit flags, `(z_matrix, is_benefit_z)`

Divide each benefit criterion by its maximum, and the minimum of each cost criterion by its values. Every criterion becomes a benefit criterion.


source

linear2

def linear2(
    x_matrix:<built-in function array>, # Decision matrix
    is_benefit_x:list, # Benefit (`True`) or cost (`False`) flag of each criterion
)->tuple: # The normalized matrix and the new benefit flags, `(z_matrix, is_benefit_z)`

Rescale each criterion to [0, 1] between its minimum and maximum, reversed for cost criteria. Every criterion becomes a benefit criterion.


source

linear3

def linear3(
    x_matrix:<built-in function array>, # Decision matrix
    is_benefit_x:list, # Benefit (`True`) or cost (`False`) flag of each criterion
)->tuple: # The normalized matrix and the new benefit flags, `(z_matrix, is_benefit_z)`

Divide each criterion by its sum. Benefit and cost flags are unchanged.


source

vector

def vector(
    x_matrix:<built-in function array>, # Decision matrix
    is_benefit_x:list, # Benefit (`True`) or cost (`False`) flag of each criterion
)->tuple: # The normalized matrix and the new benefit flags, `(z_matrix, is_benefit_z)`

Divide each criterion by its Euclidean norm. Benefit and cost flags are unchanged.


source

normalize

def normalize(
    x_matrix:<built-in function array>, # Decision matrix
    is_benefit_x:list, # Benefit (`True`) or cost (`False`) flag of each criterion
    n_method:str, # `LINEAR1`, `LINEAR2`, `LINEAR3`, `VECTOR`, or `None` to keep a matrix already in [0, 1]
)->tuple: # The normalized matrix and the new benefit flags, `(z_matrix, is_benefit_z)`

Normalize x_matrix with n_method.

The examples below use three areas and three criteria: the highest measured value, the number of measurements and a prior estimate. The number of measurements is a cost criterion, because areas with fewer measurements need more sampling:

x = np.array([[0.8, 4, 0.5],
              [0.3, 2, 0.9],
              [0.6, 8, 0.2]])
is_benefit_x = [True, False, True]

LINEAR2 maps the lowest value of each criterion to 0 and the highest to 1. It reverses the scale of the cost criterion. Area 2 has the most measurements and gets 0:

z, is_benefit_z = normalize(x, is_benefit_x, 'LINEAR2')
test_eq(is_benefit_z, [True, True, True])
test_close(z[:, 1], [2/3, 1, 0])
z
array([[1.        , 0.66666667, 0.42857143],
       [0.        , 1.        , 1.        ],
       [0.6       , 0.        , 0.        ]])

Missing values stay NaN. Each normalizer computes its column statistics from the values that are present. A missing value does not change how the rest of its column is normalized:

x_gap = np.array([[2.,  10.],
                  [4., np.nan],
                  [8.,   5.]])
z_gap, _ = normalize(x_gap, [True, False], 'LINEAR1')
test_close(z_gap[[0, 2]], [[0.25, 0.5], [1., 1.]])
test_eq(np.isnan(z_gap[1, 1]), True)
z_gap
array([[0.25, 0.5 ],
       [0.5 ,  nan],
       [1.  , 1.  ]])

Correlation

The CRITIC and VIC weighting methods measure how much criteria overlap by correlating the columns of the decision matrix.


source

abspearson

def abspearson(
    z_matrix:<built-in function array>, # Normalized decision matrix
)->numpy.ndarray:

Absolute Pearson correlation between every pair of columns of z_matrix.


source

dcor

def dcor(
    z_matrix:<built-in function array>, # Normalized decision matrix
)->numpy.ndarray:

Distance correlation between every pair of columns of z_matrix.


source

squared_dcov_matrix

def squared_dcov_matrix(
    z_matrix:<built-in function array>, # Normalized decision matrix
)->numpy.ndarray:

Squared distance covariance between every pair of columns of z_matrix.


source

dist_matrix

def dist_matrix(
    z_vector:<built-in function array>, # One column of a decision matrix
)->numpy.ndarray:

Absolute difference between every pair of elements of z_vector.


source

lin_func

def lin_func(
    dmatrix, # Distance matrix
):

Double-centre dmatrix: subtract its row and column means, then add its grand mean.


source

squared_dcov

def squared_dcov(
    j_func, # Double-centred distance matrix of column j
    l_func, # Double-centred distance matrix of column l
):

Squared distance covariance of two columns.


source

squared_dcor

def squared_dcor(
    jl_dcov2, # Squared distance covariance of columns j and l
    j_dvar2, # Squared distance variance of column j
    l_dvar2, # Squared distance variance of column l
):

Squared distance correlation of two columns.


source

pearson

def pearson(
    z_matrix:<built-in function array>, # Normalized decision matrix
)->numpy.ndarray:

Pearson correlation between every pair of columns of z_matrix.


source

correlate

def correlate(
    z_matrix:<built-in function array>, # Normalized decision matrix
    c_method:str, # `PEARSON`, `ABSPEARSON` or `DCOR`
)->numpy.ndarray:

Correlation between every pair of columns of z_matrix, computed with c_method.

Weighting

Weighting methods derive the importance of each criterion from the data. Every method returns weights that sum to 1.


source

em

def em(
    z_matrix:<built-in function array>, # Normalized decision matrix whose columns each sum to 1, as `LINEAR3` produces
)->numpy.ndarray:

Criterion weights from the Entropy Measure (EM) method.


source

mw

def mw(
    z_matrix:<built-in function array>, # Normalized decision matrix
)->numpy.ndarray:

Equal weights for every criterion, from the Mean Weights (MW) method.


source

sd

def sd(
    z_matrix:<built-in function array>, # Normalized decision matrix
)->numpy.ndarray:

Criterion weights proportional to the standard deviation of each column, from the Standard Deviation (SD) method.


source

vic

def vic(
    z_matrix:<built-in function array>, # Normalized decision matrix
    c_method:str='dCor', # `ABSPEARSON` or `DCOR`. `None` means `DCOR`
)->numpy.ndarray:

Criterion weights from the Variability and Interdependencies of Criteria (VIC) method.


source

critic

def critic(
    z_matrix:<built-in function array>, # Normalized decision matrix
    c_method:str='Pearson', # `PEARSON`, `ABSPEARSON` or `DCOR`. `None` means `PEARSON`
)->numpy.ndarray:

Criterion weights from the Criteria Importance Through Intercriteria Correlation (CRITIC) method.


source

weigh

def weigh(
    z_matrix:<built-in function array>, # Normalized decision matrix
    w_method:str, # `MW`, `EM`, `SD`, `CRITIC` or `VIC`
    c_method:str=None, # Correlation method for `CRITIC` and `VIC`
)->numpy.ndarray:

Criterion weights of z_matrix, computed with w_method.

MW gives every criterion the same weight. CRITIC gives more weight to criteria that vary more and correlate less with the others. Here it gives the first criterion almost half of the total weight:

test_close(weigh(z, 'MW'), [1/3, 1/3, 1/3])
w = weigh(z, 'CRITIC', 'PEARSON')
test_close(w.sum(), 1)
w
array([0.4931945 , 0.23714979, 0.26965571])

Scoring

Scoring methods combine the normalized criteria and their weights into one score per alternative.


source

topsis

def topsis(
    z_matrix:<built-in function array>, # Normalized decision matrix
    w_vector:list, # Weight of each criterion
    is_benefit_z:list, # Benefit (`True`) or cost (`False`) flag of each criterion
)->tuple: # The scores and `True`, as `(s_vector, desc_order)`

Score each alternative by its relative closeness to the ideal alternative, from 0 to 1, with the Technique for Order Preference by Similarity to Ideal Solution (TOPSIS). Higher is better.


source

cp

def cp(
    z_matrix:<built-in function array>, # Normalized decision matrix
    w_vector:list, # Weight of each criterion
    is_benefit_z:list, # Benefit (`True`) or cost (`False`) flag of each criterion
)->tuple: # The scores and `False`, as `(s_vector, desc_order)`

Score each alternative by its Euclidean distance to the ideal alternative, with Compromise Programming (CP). Lower is better.


source

score

def score(
    z_matrix:<built-in function array>, # Normalized decision matrix
    is_benefit_z:list, # Benefit (`True`) or cost (`False`) flag of each criterion
    w_vector:list, # Weight of each criterion
    s_method:str, # `CP` or `TOPSIS`
)->tuple: # The scores, and `True` if higher scores rank first, as `(s_vector, desc_order)`

Score each alternative of z_matrix with s_method.

score returns the scores and desc_order. desc_order is True when higher scores rank first. TOPSIS ranks area 0 first, then area 1, then area 2:

s_topsis, desc_order = score(z, is_benefit_z, w, 'TOPSIS')
test_eq(desc_order, True)
test_eq(np.argsort(-s_topsis), [0, 1, 2])
s_topsis
array([0.75394406, 0.42133458, 0.41935961])

CP also ranks area 0 first, but it ranks area 2 above area 1. Lower CP scores rank first:

s_cp, desc_order = score(z, is_benefit_z, w, 'CP')
test_eq(desc_order, False)
test_eq(np.argsort(s_cp), [0, 2, 1])
s_cp
array([0.17318287, 0.4931945 , 0.40972278])