Skip to content

Weighting methods

A weighting method turns judgements or data into criteria weights that sum to 1. bulanik has four, all returning a WeightingResult, and a fifth, the best-worst method, that needs an optimiser and so lives in the LumiX and sezgi bridges.

Method Function Input Kind
AHP ahp crisp pairwise comparison matrix subjective
Fuzzy AHP fuzzy_ahp matrix of triangular numbers subjective
SWARA swara comparative importances of an ordered list subjective
Entropy entropy decision matrix (alternatives by criteria) objective
BWM bwm_linear, BWMProblem, FuzzyBWMProblem best-to-others and others-to-worst vectors subjective

Subjective methods encode what the decision makers think; objective ones read the weights off the data. Every result carries weights (a dict), ranking (criteria, largest weight first), operators (the choices behind the numbers) and, for the AHP-based methods, consistency_ratio. Pass a result straight to a ranking method as its weights.

Three panels of horizontal bars for price, quality and delivery: AHP weights 0.540, 0.297, 0.163 with CR 0.008; SWARA weights 0.419, 0.349, 0.233; entropy weights with delivery largest at 0.597 Three panels of horizontal bars for price, quality and delivery: AHP weights 0.540, 0.297, 0.163 with CR 0.008; SWARA weights 0.419, 0.349, 0.233; entropy weights with delivery largest at 0.597
Three methods, three answers for the same three criteria, drawn by plot.weights. AHP and SWARA are judgement-based and agree on the order; entropy ignores judgement and rewards the criterion whose values differ most between the four suppliers, here delivery. The AHP panel shows its consistency ratio in the corner.

AHP

Saaty's analytic hierarchy process asks for a square matrix \(A\) in which \(a_{ij}\) says how many times more important criterion \(i\) is than \(j\) (the usual scale is 1 to 9), with \(a_{ji} = 1/a_{ij}\) and ones on the diagonal. The weights are the principal eigenvector of \(A\) normalised to sum 1; bulanik finds it by power iteration.

The matrix is rarely perfectly consistent (\(a_{ij} a_{jk} = a_{ik}\)), so AHP also reports how far it is from consistent. With the principal eigenvalue \(\lambda_{\max}\) and order \(n\),

\[ CI = \frac{\lambda_{\max} - n}{n - 1}, \qquad CR = \frac{\max(0, CI)}{RI_n}, \]

where \(RI_n\) is Saaty's random index, the average \(CI\) of random reciprocal matrices of order \(n\). The code uses this table:

\(n\) 1 2 3 4 5 6 7 8 9 10
\(RI\) 0 0 0.58 0.90 1.12 1.24 1.32 1.41 1.45 1.49

Hence matrices larger than 10 are rejected (there is no random index beyond 10), and for \(n \le 2\) the ratio is 0 (every 1x1 and 2x2 reciprocal matrix is consistent). A ratio below about 0.1 is conventionally acceptable.

from bulanik.mcdm import ahp

names = ["cost", "quality", "speed"]
r = ahp([[1, 3, 5], [1/3, 1, 2], [1/5, 1/2, 1]], names)
print({k: round(v, 3) for k, v in r.weights.items()})
print(r.ranking, round(r.consistency_ratio, 3), r.operators)

# cost beats quality 3:1 and quality beats speed 3:1, but speed beats cost 3:1: a cycle
cyclic = ahp([[1, 3, 1/3], [1/3, 1, 3], [3, 1/3, 1]], names)
print({k: round(v, 3) for k, v in cyclic.weights.items()}, round(cyclic.consistency_ratio, 3))
{'cost': 0.648, 'quality': 0.23, 'speed': 0.122}
('cost', 'quality', 'speed') 0.003 {'weights': 'eigenvector', 'consistency': 'saaty_ri'}
{'cost': 0.333, 'quality': 0.333, 'speed': 0.333} 1.149

The cyclic matrix has equal weights and a ratio far above 0.1: AHP will always give you some weights, so check consistency_ratio before trusting them.

Fuzzy AHP

fuzzy_ahp takes the same matrix of triangular numbers (TFN), so each judgement can carry its own uncertainty. It follows Buckley's (1985) geometric-mean method:

  1. The fuzzy geometric mean of row \(i\) is \(\tilde r_i = (\prod_j \tilde a_{ij})^{1/n}\), taken componentwise on \((l, m, u)\).
  2. The fuzzy weight is \(\tilde w_i = \tilde r_i \otimes (\sum_k \tilde r_k)^{-1}\).
  3. Each \(\tilde w_i\) is defuzzified with defuzzification (a TFN score name: "centroid", the default, "graded_mean" or "mean") and the crisp values are normalised to sum 1.

The result keeps the fuzzy weights in fuzzy_weights. The consistency ratio is Saaty's, computed on the defuzzified matrix with the table above. The matrix must be reciprocal in the fuzzy sense, \(\tilde a_{ji} = 1/\tilde a_{ij}\) (relative tolerance \(10^{-9}\)), with every lower bound positive. SAATY_TFN and LinguisticScale.reciprocal build such entries from the words of the 1-9 scale.

from bulanik import TFN
from bulanik.mcdm import fuzzy_ahp
from bulanik.scales import SAATY_TFN

one = SAATY_TFN["1"]
three, five = SAATY_TFN["3"], SAATY_TFN["5"]
matrix = [
    [one,                        three,                      five],
    [SAATY_TFN.reciprocal("3"),  one,                        three],
    [SAATY_TFN.reciprocal("5"),  SAATY_TFN.reciprocal("3"),  one],
]
r = fuzzy_ahp(matrix, ["cost", "quality", "speed"])
print({k: round(v, 3) for k, v in r.weights.items()})
print("CR:", round(r.consistency_ratio, 3), r.operators)
print("cost, fuzzy:", r.fuzzy_weights["cost"])
g = fuzzy_ahp(matrix, ["cost", "quality", "speed"], defuzzification="graded_mean")
print({k: round(v, 3) for k, v in g.weights.items()})
{'cost': 0.629, 'quality': 0.263, 'speed': 0.107}
CR: 0.082 {'weights': 'buckley_geometric_mean', 'defuzzification': 'centroid', 'consistency': 'saaty_ri'}
cost, fuzzy: TFN(l=0.43062425834913853, m=0.636985571744757, u=0.9185188009830095)
{'cost': 0.633, 'quality': 0.261, 'speed': 0.106}

SWARA

Step-wise weight assessment ratio analysis (Kersuliene et al. 2010) needs no matrix: rank the criteria from most to least important, then for each criterion say how much less important it is than the one before it. With these comparative importances \(s_2, \dots, s_n\) the code computes

\[ q_1 = 1, \qquad q_j = \frac{q_{j-1}}{s_j + 1}, \qquad w_j = \frac{q_j}{\sum_k q_k}. \]

An \(s_j\) of 0 means "as important as the previous one", and 0.3 means "30% less important". The names are given most important first and there are \(n - 1\) values. The importances may be crisp (non-negative) or all fuzzy (any fuzzy number kind); fuzzy ones are scored first, with score= or the kind's default, and operators["score"] records which. There is no consistency ratio.

from bulanik import TFN
from bulanik.mcdm import swara

r = swara([0.3, 0.2], ["a", "b", "c"])
print({k: round(v, 3) for k, v in r.weights.items()}, r.operators)

fuzzy = swara([TFN(0.2, 0.3, 0.4), TFN(0.1, 0.2, 0.3)], ["a", "b", "c"])
print({k: round(v, 3) for k, v in fuzzy.weights.items()}, fuzzy.operators)
print(swara([TFN(0.2, 0.3, 0.4)], ["a", "b"], score="mean").operators)
{'a': 0.415, 'b': 0.319, 'c': 0.266} {'score': 'crisp'}
{'a': 0.415, 'b': 0.319, 'c': 0.266} {'score': 'centroid'}
{'score': 'mean'}

Entropy

The entropy method is objective: it uses only the decision matrix. With \(p_{ij} = x_{ij} / \sum_i x_{ij}\) for \(m\) alternatives, criterion \(j\) has entropy and divergence

\[ e_j = -\frac{1}{\ln m} \sum_i p_{ij} \ln p_{ij}, \qquad d_j = 1 - e_j, \qquad w_j = \frac{d_j}{\sum_k d_k}, \]

with \(0 \ln 0 = 0\). A criterion on which the alternatives are all alike has entropy near 1, carries little information and gets little weight; one on which they differ a lot gets more. Entropy says nothing about what matters to you, only about what discriminates, so it is often combined with, not substituted for, a subjective method.

from bulanik.mcdm import entropy

r = entropy([[1, 9], [2, 8], [3, 7]], ["a", "b"])
print({k: round(v, 3) for k, v in r.weights.items()})   # a (1, 2, 3) differs more, relatively, than b (9, 8, 7)
print(r.operators, r.consistency_ratio)
{'a': 0.944, 'b': 0.056}
{'weights': 'shannon_entropy'} None

Best-worst method

The best-worst method (BWM, Rezaei 2015) asks for only \(2n - 3\) comparisons: the best criterion \(B\) against every other (\(a_{Bj}\)) and every other against the worst criterion \(W\) (\(a_{jW}\)), on a 1 to 9 scale. The weights are those that best reproduce the comparisons, which is an optimisation problem, solved by the LumiX bridge (linear form) or the sezgi bridge (nonlinear and fuzzy forms). Its input checks and two consistency indices are solver-free, so they live in bulanik.mcdm.bwm:

  • bwm_consistency_index(a_bw) is Rezaei's largest attainable deviation \(\xi\) for a best-over-worst comparison \(a = a_{BW}\), \(CI(a) = \bigl((1 + 2a) - \sqrt{8a + 1}\bigr)/2\), and
  • fuzzy_bwm_consistency_index(a_bw) is Guo and Zhao's for a triangular \(\tilde a_{BW}\) with upper bound \(u\), \(CI(u) = \bigl((1 + 2u) + \sqrt{8u + 1}\bigr)/2\) (the larger root, so it is never 0).
from bulanik.mcdm.bwm import bwm_consistency_index, fuzzy_bwm_consistency_index
from bulanik.scales import FUZZY_BWM_TFN

print("a_BW  ", *range(1, 10), sep="\t")
print("CI(a) ", *[f"{bwm_consistency_index(a):.2f}" for a in range(1, 10)], sep="\t")
print()
for term in FUZZY_BWM_TFN.terms:
    tfn = FUZZY_BWM_TFN[term]
    print(f"{term}  {str(tfn):<44} CI = {fuzzy_bwm_consistency_index(tfn):.2f}")
a_BW   1 2 3 4 5 6 7 8 9
CI(a)  0.00 0.44 1.00 1.63 2.30 3.00 3.73 4.47 5.23

EI  TFN(l=1.0, m=1.0, u=1.0)                     CI = 3.00
WI  TFN(l=0.6666666666666666, m=1.0, u=1.5)      CI = 3.80
FI  TFN(l=1.5, m=2.0, u=2.5)                     CI = 5.29
VI  TFN(l=2.5, m=3.0, u=3.5)                     CI = 6.69
AI  TFN(l=3.5, m=4.0, u=4.5)                     CI = 8.04

The consistency ratio of a BWM result is \(\xi / CI\), which is why the ratio does not exist for \(a_{BW} = 1\) in the crisp model (see the sezgi bridge).

Pitfalls

AHP matrices must be exactly reciprocal. The check is a relative tolerance of \(10^{-9}\) on \(a_{ij} a_{ji} = 1\), the diagonal must be 1, entries must be positive and the order at most 10. Write 1/3, not 0.33.

A fuzzy AHP entry must be a TFN with \(l > 0\). Plain numbers and other number kinds are rejected, and so is a \(\tilde a_{ji}\) that is not the reciprocal of \(\tilde a_{ij}\).

SWARA wants the criteria sorted by you. The function does not sort: the first name is taken as the most important. With fuzzy importances a negative score is an error; use score="normalized" if the default score can go negative.

Entropy needs variation. Fewer than 2 alternatives, a negative entry, a column that sums to 0, or a matrix whose columns are all constant is an error (no criterion discriminates).

A small consistency_ratio is a necessary check, not proof. It says the judgements do not contradict each other, not that they are right.

from bulanik import TFN
from bulanik.mcdm import ahp, entropy, fuzzy_ahp, swara

def show(make):
    try:
        print(make())
    except Exception as error:
        print(f"{type(error).__name__}: {error}")

show(lambda: ahp([[1, 3], [0.33, 1]]))                       # not reciprocal
show(lambda: ahp([[2, 1], [1, 2]]))                          # diagonal is not 1
show(lambda: ahp([[1, 0], [0, 1]]))                          # not positive
show(lambda: ahp([[1.0] * 11 for _ in range(11)]))           # no random index beyond 10
show(lambda: ahp([[1, 3], [1/3, 1]], ["a", "a"]))            # duplicate names
show(lambda: fuzzy_ahp([[1, 3], [1/3, 1]]))                  # not TFN entries
show(lambda: fuzzy_ahp([[TFN(1, 1, 1), TFN(2, 3, 4)], [TFN(1, 1, 1), TFN(1, 1, 1)]]))  # not reciprocal
show(lambda: swara([0.3], ["a", "b", "c"]))                  # needs n - 1 = 2 values
show(lambda: swara([0.3, TFN(0.1, 0.2, 0.3)], ["a", "b", "c"]))   # crisp and fuzzy mixed
show(lambda: swara([-0.5], ["a", "b"]))
show(lambda: entropy([[1, 2], [1, 2]]))                      # no criterion discriminates
show(lambda: entropy([[1, 2]]))                              # one alternative
ValueError: AHP matrix must be reciprocal: a[0][1]*a[1][0] != 1
ValueError: AHP diagonal must be 1
ValueError: AHP entries must be positive
ValueError: AHP supports at most 10 criteria (no random index beyond 10), got 11
ValueError: criteria names must be unique
TypeError: fuzzy AHP needs TFN entries, got int
ValueError: fuzzy AHP matrix must be reciprocal: a[1][0] != 1/a[0][1]
ValueError: expected 2 comparative importances for 3 criteria, got 1
ValueError: SWARA comparative importances must be all crisp or all fuzzy, not a mix
ValueError: comparative importance must be >= 0, got -0.5; for fuzzy inputs try score="normalized"
ValueError: no criterion discriminates between the alternatives
ValueError: entropy weighting needs at least 2 alternatives

See also

  • More weighting methods for CRITIC, PIPRECIA, ahp_fuzzy_numbers and DEMATEL.
  • Ranking: pass a result straight to topsis, vikor, waspas, edas.
  • LumiX bridge and sezgi bridge: the best-worst method.
  • Aggregation to combine several experts' judgements before weighting, Sets and scales for SAATY_TFN and friends.
  • API: bulanik.mcdm, bulanik.plot.weights.
  • Examples: 03 fuzzy AHP, 04 SWARA and TOPSIS.
  • Saaty (1980), The Analytic Hierarchy Process, McGraw-Hill; Buckley (1985), "Fuzzy hierarchical analysis", Fuzzy Sets and Systems 17(3); Kersuliene, Zavadskas and Turskis (2010), "Selection of rational dispute resolution method by applying new step-wise weight assessment ratio analysis (SWARA)", Journal of Business Economics and Management 11(2); Shannon (1948), "A mathematical theory of communication", Bell System Technical Journal 27; Hwang and Yoon (1981), Multiple Attribute Decision Making, Springer (the entropy procedure; see also Zeleny 1982); Rezaei (2015), "Best-worst multi-criteria decision-making method", Omega 53; Guo and Zhao (2017), "Fuzzy best-worst multi-criteria decision-making method and its applications", Knowledge-Based Systems 121.