bulanik.mcdm¶
Multi-criteria decision making. The package re-exports the weighting and
ranking functions and the result types; they are defined in submodules,
rendered one after another below: results,
weighting, ranking,
scoring, outranking,
fuzzy, ranking on fuzzy numbers in
fuzzy_ranking, the influence analysis in
dematel and the best-worst helpers in
bwm. The solvers for the best-worst method itself live in
the bridges bulanik.lumix and bulanik.sezgi.
mcdm ¶
Multi-criteria decision making: weighting and ranking methods.
The package has two kinds of methods and three result containers.
- Weighting (criteria weights):
ahp,fuzzy_ahp,ahp_fuzzy_numbers,ahp_interval_valued,swara,piprecia,entropyandcriticreturn aWeightingResult. The best-worst method needs an optimiser, so it lives in the bridges (bulanik.lumix,bulanik.sezgi) and returns aBWMResult. -
Ranking (alternatives):
topsis,vikor,waspasandedaswork on a crisp matrix, as do the scoring methodscodas,copras,marcos,cocosoandwisp, and the outranking methodelectre;rank_fuzzyfirst aggregates expert fuzzy ratings from aDecisionMatrixand scores them;fuzzy_topsisis Chen's (2000) method on triangular numbers; andfuzzy_codas,fuzzy_waspasandfuzzy_cocosocompute on an aggregated matrix of fuzzy numbers and score only at the end. All return aRankingResult. -
Influence analysis:
dematel(crisp, or fuzzy with CFCS defuzzification) returns aDEMATELResultwhoseas_weightsfeeds the ranking methods.
Every result records the operators it used in its operators mapping. The
ranking methods take the criteria weights as a sequence, a mapping by criterion
name, or a weighting result directly.
Examples:
>>> from bulanik.mcdm import ahp, topsis
>>> w = ahp([[1, 3], [1/3, 1]], ["cost", "quality"])
>>> round(w.weights["cost"], 2), w.ranking
(0.75, ('cost', 'quality'))
>>> r = topsis([[250, 16], [200, 12], [300, 32]], w, ["cost", "benefit"],
... alternatives=["A", "B", "C"])
>>> r.ranking
('B', 'C', 'A')
Results¶
results ¶
Result containers shared by the MCDM methods.
Three frozen dataclasses: WeightingResult
(criteria weights), RankingResult
(alternative scores) and BWMResult (best-worst
weights). Each carries a ranking tuple (best first) and an operators mapping
that names the operator choices behind the numbers, so a result is
self-describing.
WeightingResult
dataclass
¶
WeightingResult(method: str, criteria: tuple[str, ...], weights: dict[str, float], ranking: tuple[str, ...], operators: dict[str, str], consistency_ratio: float | None = None, fuzzy_weights: dict[str, FuzzyNumber] | None = None, extras: dict[str, Any] = dict())
Criteria weights from a weighting method (ranking is best first).
Attributes:
| Name | Type | Description |
|---|---|---|
method |
str
|
Method name, e.g. |
criteria |
tuple[str, ...]
|
Criterion names in input order. |
weights |
dict[str, float]
|
Weight of each criterion; they sum to 1. |
ranking |
tuple[str, ...]
|
Criterion names sorted by weight, largest first (ties keep input order). |
operators |
dict[str, str]
|
Operator choices used, e.g. |
consistency_ratio |
float | None
|
|
fuzzy_weights |
dict[str, FuzzyNumber] | None
|
The fuzzy weights before defuzzification
( |
extras |
dict[str, Any]
|
Method-specific additional values. |
Examples:
>>> from bulanik.mcdm import swara
>>> r = swara([0.3], ["a", "b"])
>>> r.ranking, round(r.weights["a"], 4)
(('a', 'b'), 0.5652)
>>> r.consistency_ratio is None
True
RankingResult
dataclass
¶
RankingResult(method: str, alternatives: tuple[str, ...], scores: dict[str, float], ranking: tuple[str, ...], operators: dict[str, str], extras: dict[str, Any] = dict())
Alternative scores and their ranking (ranking is best first).
"Best" depends on the method: a larger score is better for TOPSIS, WASPAS
and EDAS, a smaller one for VIKOR (whose ranking is already ordered
accordingly). Use ranking rather than sorting scores yourself.
Attributes:
| Name | Type | Description |
|---|---|---|
method |
str
|
Method name, e.g. |
alternatives |
tuple[str, ...]
|
Alternative names in input order. |
scores |
dict[str, float]
|
Score of each alternative. |
ranking |
tuple[str, ...]
|
Alternative names, best first (ties keep input order). |
operators |
dict[str, str]
|
Operator choices used, e.g. |
extras |
dict[str, Any]
|
Per-alternative intermediate values keyed by name, e.g.
|
Examples:
>>> from bulanik.mcdm import vikor
>>> m = [[250, 16, 12, 5], [200, 16, 8, 3], [300, 32, 16, 4], [275, 32, 8, 4]]
>>> r = vikor(m, [0.3, 0.3, 0.2, 0.2], ["cost", "benefit", "benefit", "benefit"])
>>> r.ranking, sorted(r.extras)
(('A4', 'A3', 'A1', 'A2'), ['R', 'S'])
BWMResult
dataclass
¶
BWMResult(method: str, criteria: tuple[str, ...], weights: dict[str, float], ranking: tuple[str, ...], xi: float, consistency_ratio: float | None, best: str, worst: str, operators: dict[str, str], fuzzy_weights: dict[str, FuzzyNumber] | None = None, extras: dict[str, Any] = dict())
Best-worst method weights (ranking is best first).
Produced by bwm_linear,
BWMProblem and
FuzzyBWMProblem.
Attributes:
| Name | Type | Description |
|---|---|---|
method |
str
|
|
criteria |
tuple[str, ...]
|
Criterion names in input order. |
weights |
dict[str, float]
|
Weight of each criterion; they sum to 1. |
ranking |
tuple[str, ...]
|
Criterion names sorted by weight, largest first. |
xi |
float
|
Optimal objective of the BWM model, the largest deviation between the comparisons and the weight ratios. |
consistency_ratio |
float | None
|
Not the same quantity in every producer.
|
best |
str
|
The best (most important) reference criterion. |
worst |
str
|
The worst (least important) reference criterion. |
operators |
dict[str, str]
|
Operator choices used, including |
fuzzy_weights |
dict[str, FuzzyNumber] | None
|
Fuzzy weights ( |
extras |
dict[str, Any]
|
Solver details (e.g. |
DEMATELResult
dataclass
¶
DEMATELResult(method: str, factors: tuple[str, ...], direct: tuple[tuple[float, ...], ...], normalized: tuple[tuple[float, ...], ...], total: tuple[tuple[float, ...], ...], d: dict[str, float], r: dict[str, float], prominence: dict[str, float], relation: dict[str, float], causes: tuple[str, ...], effects: tuple[str, ...], threshold: float, relations: tuple[tuple[str, str, float], ...], weights: dict[str, float], ranking: tuple[str, ...], operators: dict[str, str], extras: dict[str, Any] = dict())
DEMATEL influence analysis: total-relation matrix, cause/effect groups and weights.
Produced by dematel. ranking lists the
factors by weight, largest first (ties keep input order).
Attributes:
| Name | Type | Description |
|---|---|---|
method |
str
|
|
factors |
tuple[str, ...]
|
Factor names in input order. |
direct |
tuple[tuple[float, ...], ...]
|
Crisp direct-relation matrix (after defuzzification). |
normalized |
tuple[tuple[float, ...], ...]
|
Normalised matrix \(N = A/s\). |
total |
tuple[tuple[float, ...], ...]
|
Total-relation matrix \(T = N(I - N)^{-1}\). |
d |
dict[str, float]
|
Row sums of \(T\) (influence given), by factor. |
r |
dict[str, float]
|
Column sums of \(T\) (influence received), by factor. |
prominence |
dict[str, float]
|
\(D + R\), by factor. |
relation |
dict[str, float]
|
\(D - R\), by factor. |
causes |
tuple[str, ...]
|
Factors with |
effects |
tuple[str, ...]
|
The other factors (a zero net relation counts as an effect). |
threshold |
float
|
Cut-off used for |
relations |
tuple[tuple[str, str, float], ...]
|
|
weights |
dict[str, float]
|
Factor weights summing to 1. |
ranking |
tuple[str, ...]
|
Factor names by weight, best first. |
operators |
dict[str, str]
|
Operator choices used. |
extras |
dict[str, Any]
|
Method-specific additional values. |
as_weights ¶
as_weights() -> WeightingResult
The factor weights as a WeightingResult.
Usable as the criteria weights of every ranking method.
Weighting¶
weighting ¶
Criteria weighting: AHP, Buckley fuzzy AHP, SWARA, PIPRECIA, entropy and CRITIC.
Six methods that turn judgements or data into criteria weights summing to 1,
each returning a WeightingResult:
| Method | Input | Kind |
|---|---|---|
ahp |
crisp pairwise comparison matrix | subjective |
fuzzy_ahp |
matrix of triangular numbers | subjective |
swara |
comparative importances of an ordered list | subjective |
piprecia |
relative importances of a list in any order (crisp or TFN) | subjective |
entropy |
decision matrix | objective |
critic |
decision matrix | objective |
The AHP-based methods report Saaty's consistency ratio \(CR = CI / RI\) with \(CI = (\lambda_{\max} - n) / (n - 1)\) and the random index \(RI\) of Saaty's table for the matrix order \(n\):
| \(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 |
so matrices larger than 10 are rejected, and for \(n \le 2\) the ratio is 0.
The best-worst method is in bulanik.lumix and
bulanik.sezgi.
References
Saaty (1980), The Analytic Hierarchy Process; Buckley (1985), Fuzzy Sets and Systems 17(3), 233-247; Kersuliene et al. (2010), Journal of Business Economics and Management 11(2), 243-258; Diakoulaki, Mavrotas & Papayannakis (1995), Computers & Operations Research 22(7), 763-770; Stanujkic et al. (2017), Romanian Journal of Economic Forecasting 20(4), 116-133; Stevic et al. (2018), Symmetry 10(11), 586.
ahp ¶
ahp(matrix: Sequence[Sequence[float]], criteria: Sequence[str] | None = None) -> WeightingResult
Saaty's AHP: principal-eigenvector weights and consistency ratio.
The weights are the principal eigenvector of the reciprocal comparison matrix \(A\) (\(a_{ij}\) = importance of criterion \(i\) over \(j\), \(a_{ji} = 1/a_{ij}\)), normalised to sum 1 (power iteration). The consistency ratio is \(CR = \max(0, (\lambda_{\max} - n)/(n - 1)) / RI_n\) with Saaty's random index (see the module table); it is 0 for \(n \le 2\). A ratio below about 0.1 is conventionally acceptable.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrix
|
Sequence[Sequence[float]]
|
Square, positive, reciprocal matrix with ones on the diagonal, of order at most 10. |
required |
criteria
|
Sequence[str] | None
|
Criterion names; |
None
|
Returns:
| Type | Description |
|---|---|
WeightingResult
|
A |
WeightingResult
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If an entry is a |
ValueError
|
If the matrix is not square, is larger than 10, has an
entry that is not finite and positive, has a diagonal other than 1
or is not reciprocal (to relative tolerance 1e-9), |
Examples:
>>> from bulanik.mcdm import ahp
>>> r = ahp([[1, 3, 5], [1/3, 1, 2], [1/5, 1/2, 1]], ["cost", "quality", "speed"])
>>> {k: round(v, 3) for k, v in r.weights.items()}
{'cost': 0.648, 'quality': 0.23, 'speed': 0.122}
>>> round(r.consistency_ratio, 3)
0.003
>>> r.ranking
('cost', 'quality', 'speed')
References
Saaty (1980), The Analytic Hierarchy Process, McGraw-Hill.
fuzzy_ahp ¶
fuzzy_ahp(matrix: Sequence[Sequence[TFN]], criteria: Sequence[str] | None = None, *, defuzzification: str = 'centroid') -> WeightingResult
Buckley's fuzzy AHP (geometric mean of rows) with TFN judgements.
For each row \(i\) the fuzzy geometric mean \(\tilde r_i = (\prod_j
\tilde a_{ij})^{1/n}\) is taken componentwise; the fuzzy weight is
\(\tilde w_i = \tilde r_i \otimes (\sum_k \tilde r_k)^{-1}\). Each \(\tilde
w_i\) is defuzzified with defuzzification and the crisp values are
normalised to sum 1. The consistency ratio is Saaty's, computed on the
defuzzified matrix (see the module table).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrix
|
Sequence[Sequence[TFN]]
|
Square matrix of |
required |
criteria
|
Sequence[str] | None
|
Criterion names; |
None
|
defuzzification
|
str
|
Score name of |
'centroid'
|
Returns:
| Type | Description |
|---|---|
WeightingResult
|
A |
WeightingResult
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If an entry is not a TFN. |
ValueError
|
If the matrix is not square or is larger than 10, an entry
has \(l \le 0\), the matrix is not reciprocal, the names are wrong or
|
Examples:
>>> from bulanik import TFN
>>> from bulanik.mcdm import fuzzy_ahp
>>> m = [[TFN(1, 1, 1), TFN(2, 3, 4)], [TFN(1/4, 1/3, 1/2), TFN(1, 1, 1)]]
>>> r = fuzzy_ahp(m, ["a", "b"])
>>> {k: round(v, 3) for k, v in r.weights.items()}
{'a': 0.742, 'b': 0.258}
>>> sorted(r.fuzzy_weights)
['a', 'b']
References
Buckley (1985), Fuzzy Sets and Systems 17(3), 233-247.
ahp_fuzzy_numbers ¶
ahp_fuzzy_numbers(matrix: Sequence[Sequence[FuzzyNumber]], criteria: Sequence[str] | None = None, *, aggregation: str = 'wg', ops: str = 'algebraic', score: str | None = None, consistency_matrix: Sequence[Sequence[float]] | None = None) -> WeightingResult
AHP over a comparison matrix of any one fuzzy number kind.
Row \(i\) is aggregated with equal weights \(1/n\) by
wg (the fuzzy geometric mean \(\tilde r_i =
(\bigotimes_j \tilde a_{ij})^{1/n}\), default) or
wa, using the operator set ops of the number
kind. The row number is then scored, \(s_i = \mathrm{score}(\tilde r_i)\),
and the weights are \(w_i = s_i / \sum_k s_k\).
This differs from fuzzy_ahp (Buckley,
triangular numbers only): Buckley first normalises fuzzily,
\(\tilde w_i = \tilde r_i \otimes (\sum_k \tilde r_k)^{-1}\), and defuzzifies
afterwards, while here the row number is defuzzified first and the crisp
scores are normalised. The generic form needs no fuzzy division, so it works
for interval-valued, orthopair, neutrosophic and interval type-2 numbers.
The consistency ratio is Saaty's. With consistency_matrix (typically the
crisp Saaty values of the linguistic terms) it is that matrix's CR
("saaty_ri_on_given_matrix"); otherwise, if every entry's score is
positive, it is computed on the matrix of entry scores
("saaty_ri_on_scores"); otherwise it is None ("none").
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrix
|
Sequence[Sequence[FuzzyNumber]]
|
Square matrix of fuzzy numbers of one kind (and rung), of order at most 10. |
required |
criteria
|
Sequence[str] | None
|
Criterion names; |
None
|
aggregation
|
str
|
|
'wg'
|
ops
|
str
|
Operator set of the number kind. |
'algebraic'
|
score
|
str | None
|
Score name of the kind; |
None
|
consistency_matrix
|
Sequence[Sequence[float]] | None
|
Optional crisp matrix valid for
|
None
|
Returns:
| Type | Description |
|---|---|
WeightingResult
|
A |
WeightingResult
|
|
WeightingResult
|
names how the consistency ratio was obtained: |
WeightingResult
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If an entry is not a fuzzy number or the kinds are mixed. |
ValueError
|
If the matrix is not square or larger than 10, |
Examples:
>>> from bulanik import TFN
>>> from bulanik.mcdm import ahp_fuzzy_numbers
>>> m = [[TFN(1, 1, 1), TFN(2, 3, 4)], [TFN(1/4, 1/3, 1/2), TFN(1, 1, 1)]]
>>> r = ahp_fuzzy_numbers(m, ["a", "b"])
>>> {k: round(v, 3) for k, v in r.weights.items()}
{'a': 0.743, 'b': 0.257}
References
Saaty (1980), The Analytic Hierarchy Process, McGraw-Hill. Kahraman, Oztaysi, Ucal Sari and Turanoglu (2014), Knowledge-Based Systems 59, 48-57, doi:10.1016/j.knosys.2014.02.001 (interval type-2 AHP with geometric-mean rows and the DTraT score).
ahp_interval_valued ¶
ahp_interval_valued(matrix: Sequence[Sequence[IVQROFN]], criteria: Sequence[str] | None = None, *, method: str | None = None, consistency_matrix: Sequence[Sequence[float]] | None = None) -> WeightingResult
Interval-valued AHP over a comparison matrix of one IVQROFN kind.
The matrix holds linguistic judgments written as interval-valued orthopair
numbers \(([\mu^L, \mu^U], [\nu^L, \nu^U])\) of one rung \(q\) (IVIFN,
IVPFN, IVFFN or a general IVQROFN). Both triangles are filled by the
decision maker; reciprocity is not required and the diagonal is taken as
given, so the scale's "equal" term may differ between scales.
method="multiplicative" (any \(q\); default for \(q \ge 2\)). With
\(d^L_{ij} = (\mu^L_{ij})^q - (\nu^U_{ij})^q\) and
\(d^U_{ij} = (\mu^U_{ij})^q - (\nu^L_{ij})^q\) the interval multiplicative
entries are \(s = 1000^{d/q}\) (so \(s^L\), \(s^U\) from \(d^L\), \(d^U\)), the
hesitancy factor is
\(h_{ij} = 1 - ((\mu^U_{ij})^q - (\mu^L_{ij})^q) - ((\nu^U_{ij})^q - (\nu^L_{ij})^q)\),
and \(t_{ij} = \tfrac{s^L_{ij} + s^U_{ij}}{2}\,h_{ij}\). The weights are
\(w_i = \sum_j t_{ij} / \sum_{i,j} t_{ij}\).
method="possibility" (IVIFN, \(q = 1\) only; default for \(q = 1\)). The
interval exponential matrix is \(a^-_{ij} = e^{\mu^L_{ij} - \nu^U_{ij}}\),
\(a^+_{ij} = e^{\mu^U_{ij} - \nu^L_{ij}}\). The interval weight of criterion
\(i\) is built from the row sums,
\(\tilde w_i = [w^-_i, w^+_i] = [\sum_j a^-_{ij} / \sum_{i,j} a^+_{ij},\
\sum_j a^+_{ij} / \sum_{i,j} a^-_{ij}]\). The possibility degree is
\(p_{ij} = \dfrac{\max(0, w^+_j - w^-_i) - \max(0, w^-_j - w^+_i)}
{(w^+_i - w^-_i) + (w^+_j - w^-_j)}\). Here \(p_{ij}\) is the degree to which
\(\tilde w_j \ge \tilde w_i\), which is why the weight of \(j\) sums over \(i\):
\(w_j = \sum_i p_{ij} + n/2 - 1\), normalised to sum 1. When both intervals
are single points, \(p_{ij}\) is 0.5 if they are equal and otherwise 1 or 0
by their order.
The consistency ratio is Saaty's. With consistency_matrix it is that
matrix's CR; otherwise, if every entry's default score is positive, it is
computed on the matrix of entry scores; otherwise it is None. The value is
also in extras["consistency_ratio"]. Saaty's CR assumes a reciprocal
matrix, so when the judgments are not reciprocal (both triangles filled
independently) the ratio on the scored matrix is indicative only.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrix
|
Sequence[Sequence[IVQROFN]]
|
Square matrix of |
required |
criteria
|
Sequence[str] | None
|
Criterion names; |
None
|
method
|
str | None
|
|
None
|
consistency_matrix
|
Sequence[Sequence[float]] | None
|
Optional crisp matrix valid for
|
None
|
Returns:
| Type | Description |
|---|---|
WeightingResult
|
A |
WeightingResult
|
holds |
WeightingResult
|
ratio was obtained: |
WeightingResult
|
|
WeightingResult
|
and a |
WeightingResult
|
multiplicative method the \(n \times n\) lists |
WeightingResult
|
|
WeightingResult
|
|
WeightingResult
|
(\(p_{ij}\)). |
Raises:
| Type | Description |
|---|---|
TypeError
|
If an entry is not an |
ValueError
|
If the matrix is not square, has fewer than 2 or more than
10 rows, |
Examples:
>>> from bulanik import IVIFN
>>> from bulanik.mcdm import ahp_interval_valued
>>> e, h, l = IVIFN(.5, .5, .5, .5), IVIFN(.5, .6, .25, .4), IVIFN(.25, .4, .5, .6)
>>> r = ahp_interval_valued([[e, h], [l, e]], ["a", "b"])
>>> {k: round(v, 3) for k, v in r.weights.items()}
{'a': 0.724, 'b': 0.276}
References
Karasan, Ilbahar and Kahraman (2019), Soft Computing 23(21) (Pythagorean fuzzy AHP with the multiplicative interval matrix), doi:10.1007/s00500-018-3649-0. Alkan and Kahraman (2023), Informatica 34(1), 1-33 (Fermatean fuzzy AHP, \(q = 3\)), doi:10.15388/22-INFOR493. The form for a general \(q\) is this library's generalisation. Cebi and Ilbahar (2018), International Journal of the Analytic Hierarchy Process 10(2), doi:10.13033/ijahp.v10i2.549 (interval-valued intuitionistic fuzzy AHP) and Bolturk and Kahraman (2018), same journal, 10(3), doi:10.13033/ijahp.v10i3.545 (the neutrosophic counterpart) give the interval weights, the possibility degree and \(w = \sum p + n/2 - 1\). Both use powers of 10 where this function uses \(e\), which changes the weights.
swara ¶
swara(comparative_importance: Sequence[float | FuzzyNumber], criteria: Sequence[str], *, score: str | None = None) -> WeightingResult
SWARA (Kersuliene et al. 2010); criteria are listed most important first.
With the comparative importance \(s_j\) of criterion \(j\) relative to the one before it, \(q_1 = 1\) and \(q_j = q_{j-1} / (s_j + 1)\); the weights are \(w_j = q_j / \sum_k q_k\).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
comparative_importance
|
Sequence[float | FuzzyNumber]
|
\(n - 1\) values \(s_2, \dots, s_n\), all crisp (non-negative) or all fuzzy. |
required |
criteria
|
Sequence[str]
|
The \(n\) criterion names, most important first. |
required |
score
|
str | None
|
How to score fuzzy importances: a score name of their kind, or
|
None
|
Returns:
| Type | Description |
|---|---|
WeightingResult
|
|
WeightingResult
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If a comparative importance is a |
ValueError
|
If there are no criteria or duplicate names, the number of
importances is not \(n - 1\), crisp and fuzzy are mixed, a score is
negative or |
Examples:
>>> from bulanik.mcdm import swara
>>> r = swara([0.3, 0.2], ["a", "b", "c"])
>>> {k: round(v, 3) for k, v in r.weights.items()}
{'a': 0.415, 'b': 0.319, 'c': 0.266}
References
Kersuliene, Zavadskas & Turskis (2010), Journal of Business Economics and Management 11(2), 243-258.
entropy ¶
entropy(matrix: Sequence[Sequence[float]], criteria: Sequence[str] | None = None) -> WeightingResult
Shannon-entropy objective weights from a decision matrix.
With \(p_{ij} = x_{ij} / \sum_i x_{ij}\) and \(m\) alternatives, the entropy of criterion \(j\) is \(e_j = -\sum_i p_{ij}\ln p_{ij} / \ln m\) (with \(0 \ln 0 = 0\)), the divergence is \(d_j = 1 - e_j\) and the weight is \(w_j = d_j / \sum_k d_k\). A criterion whose values differ more between alternatives gets more weight.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrix
|
Sequence[Sequence[float]]
|
Alternatives by criteria, with non-negative entries. |
required |
criteria
|
Sequence[str] | None
|
Criterion names; |
None
|
Returns:
| Type | Description |
|---|---|
WeightingResult
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If a matrix entry is a |
ValueError
|
If the matrix is invalid, has fewer than 2 alternatives, a negative entry, a column that sums to zero, or no criterion discriminates between the alternatives. |
Examples:
>>> from bulanik.mcdm import entropy
>>> r = entropy([[1, 9], [2, 8], [3, 7]], ["a", "b"])
>>> {k: round(v, 3) for k, v in r.weights.items()}
{'a': 0.944, 'b': 0.056}
References
Shannon (1948), Bell System Technical Journal 27, 379-423 and 623-656; the entropy procedure for attribute weights is described in Hwang & Yoon (1981); see also Zeleny (1982), Multiple Criteria Decision Making.
piprecia ¶
piprecia(relative_importance: Sequence[float | TFN], criteria: Sequence[str], *, score: str | None = None, s_inverse: Sequence[float | TFN] | None = None) -> WeightingResult
PIPRECIA (Stanujkic et al. 2017) and its fully fuzzy form (Stevic et al. 2018).
The criteria are listed in any order (unlike SWARA they need not be sorted). The judgement \(s_j\) for \(j \ge 2\) compares criterion \(j\) with the one before it: \(s_j > 1\) if \(C_j\) is more important, \(s_j = 1\) if equal, \(s_j < 1\) if less. The valid domain is \(0 < s_j < 2\): the sources' scales (Stanujkic et al. 2017; Dalic et al. 2020) give \(s_j\) as a positive ratio of importances below 2 (a value near 2 makes \(k_j\) near 0), and \(k_j = 2 - s_j\) must stay positive. Zero and negative \(s_j\) are meaningless as a ratio and are rejected, although the formula would still produce numbers. Then \(k_1 = 1\), \(k_j = 2 - s_j\), \(q_1 = 1\), \(q_j = q_{j-1} / k_j\) and \(w_j = q_j / \sum_k q_k\).
With TFN judgements the method is Stevic et al.'s
fully fuzzy PIPRECIA with the usual triangular arithmetic: \(\tilde k_j =
(2, 2, 2) - \tilde s_j = (2 - s^u, 2 - s^m, 2 - s^l)\), \(\tilde q_j =
\tilde q_{j-1} \oslash \tilde k_j\) (so \(q^l_j = q^l_{j-1} / k^u_j\) and \(q^u_j
= q^u_{j-1} / k^l_j\)) and \(\tilde w_j = \tilde q_j \oslash \sum_k \tilde q_k\).
The crisp weights are the fuzzy weights scored with score and normalised to
sum 1. The source defuzzifies with the graded mean \((l + 4m + u) / 6\), which is
the default; its fuzzy scales are designed so that no \(\tilde k_j\) contains 0.
Inverse pass. With s_inverse the method follows Stevic et al. (2018) and
Dalic et al. (2020): the same criteria are judged again from the penultimate one
backwards. \(s'_j\) compares criterion \(j\) with criterion \(j + 1\) (\(j = 1, \dots,
n - 1\); so s_inverse has \(n - 1\) entries in the order of criteria, not
reversed). Then \(k'_n = 1\), \(k'_j = 2 - s'_j\), \(q'_n = 1\), \(q'_j = q'_{j+1} /
k'_j\) and \(w'_j = q'_j / \sum_k q'_k\) (fuzzy for TFN as above). Each of \(w_j\) and
\(w'_j\) is scored with score (columns I and II of Table 6 in Dalic et al.) and
the final weight is their mean, \((w_j + w'_j) / 2\). The source does not
renormalise, and for fuzzy input that mean need not sum to 1 (the wide inverse
weights score high); weights are renormalised to sum 1, which leaves their
ranking unchanged. extras then holds forward and inverse (the scored
vectors before averaging, as printed in the source, not renormalised for TFN
input) and, for TFN input, fuzzy_inverse; fuzzy_weights stays the forward
pass.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
relative_importance
|
Sequence[float | TFN]
|
\(n - 1\) values \(s_2, \dots, s_n\), all crisp numbers
or all |
required |
criteria
|
Sequence[str]
|
The \(n\) criterion names, in the order that was judged. |
required |
score
|
str | None
|
How to score fuzzy weights: a |
None
|
s_inverse
|
Sequence[float | TFN] | None
|
Optional inverse judgements \(s'_1, \dots, s'_{n-1}\) (see
above), of the same kind (crisp or TFN) as |
None
|
Returns:
| Type | Description |
|---|---|
WeightingResult
|
|
WeightingResult
|
|
WeightingResult
|
resolved score name, or |
WeightingResult
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If a judgement is a |
ValueError
|
If there are no criteria or duplicate names, the number of
judgements (or inverse judgements) is not \(n - 1\), crisp numbers and
TFN are mixed (also between |
Examples:
>>> from bulanik.mcdm import piprecia
>>> r = piprecia([1.2, 0.9], ["a", "b", "c"])
>>> {k: round(v, 3) for k, v in r.weights.items()}
{'a': 0.295, 'b': 0.369, 'c': 0.336}
>>> from bulanik import TFN
>>> f = piprecia([TFN(1.1, 1.15, 1.2)], ["a", "b"])
>>> w = f.fuzzy_weights["b"]
>>> round(w.l, 3), round(w.m, 3), round(w.u, 3)
(0.494, 0.541, 0.592)
>>> r = piprecia([1.2, 0.9], ["a", "b", "c"], s_inverse=[1.1, 0.7])
>>> {k: round(v, 3) for k, v in r.weights.items()}
{'a': 0.31, 'b': 0.33, 'c': 0.36}
References
Stanujkic, Zavadskas, Karabasevic, Smarandache & Turskis (2017), Romanian Journal of Economic Forecasting 20(4), 116-133 (no DOI). Stevic, Stjepanovic, Bozickovic, Das & Stanujkic (2018), Symmetry 10(11), 586, doi:10.3390/sym10110586; the inverse pass as restated with a worked example in Dalic, Stevic, Karamasa & Puska (2020), Decision Making: Applications in Management and Engineering 3(1), 80-95, doi:10.31181/dmame2003114d.
critic ¶
critic(matrix: Sequence[Sequence[float]], criteria: Sequence[str] | None = None, *, criteria_types: Sequence[str] | None = None) -> WeightingResult
CRITIC objective weights (Diakoulaki, Mavrotas & Papayannakis 1995).
Each column is scaled to \([0, 1]\) with the best value at 1, \(r_{ij} = (x_{ij} - x^-_j) / (x^+_j - x^-_j)\), where \(x^+_j\) is the largest value of a benefit criterion and the smallest of a cost criterion. With the population standard deviation \(\sigma_j\) of \(r_{\cdot j}\) and the Pearson correlation \(\rho_{jk}\) of two scaled columns, the information carried by criterion \(j\) is \(C_j = \sigma_j \sum_k (1 - \rho_{jk})\) and \(w_j = C_j / \sum_k C_k\). A criterion gets weight for varying a lot (large \(\sigma_j\)) and for conflicting with the others (low \(\rho_{jk}\)). The weights do not depend on whether \(\sigma_j\) uses \(m\) or \(m - 1\) in the denominator, because the factor is the same for every column and cancels in \(w_j\); population is used. They are also unchanged by a positive scaling and a shift of any column (for a cost column the same holds).
A constant column has no scale and no correlation: it gets weight 0 and is left out of the other columns' correlation sums, so adding one changes no other weight.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrix
|
Sequence[Sequence[float]]
|
Alternatives by criteria, finite numbers. |
required |
criteria
|
Sequence[str] | None
|
Criterion names; |
None
|
criteria_types
|
Sequence[str] | None
|
|
None
|
Returns:
| Type | Description |
|---|---|
WeightingResult
|
A |
WeightingResult
|
|
WeightingResult
|
"information": {c: C}} |
WeightingResult
|
column, which is absent from |
Raises:
| Type | Description |
|---|---|
TypeError
|
If a matrix entry is a |
ValueError
|
If the matrix is invalid, has fewer than 2 alternatives, the names or types are wrong, no criterion discriminates between the alternatives (all columns constant), or two or more informative columns are all perfectly positively correlated, so every \(C_j\) is 0 (e.g. 2 alternatives and no conflict). |
Examples:
>>> from bulanik.mcdm import critic
>>> r = critic([[7, 9], [8, 7], [9, 8]], ["a", "b"], criteria_types=["benefit", "cost"])
>>> {k: round(v, 3) for k, v in r.weights.items()}
{'a': 0.5, 'b': 0.5}
>>> round(r.extras["correlation"]["a"]["b"], 3)
0.5
References
Diakoulaki, Mavrotas & Papayannakis (1995), Computers & Operations Research 22(7), 763-770, doi:10.1016/0305-0548(94)00059-H.
Ranking¶
ranking ¶
Crisp ranking methods: TOPSIS, VIKOR, WASPAS and EDAS.
Four classical methods that rank alternatives from a crisp decision matrix
(rows are alternatives, columns are criteria), a vector of criteria weights and
the type of each criterion ("benefit" or "cost"). All have the same
signature apart from one method parameter, accept the weights as a sequence, a
mapping by criterion name or a
WeightingResult /
BWMResult, and return a
RankingResult. Higher scores are better
for TOPSIS, WASPAS and EDAS; for VIKOR lower is better, and the ranking
of every result is already ordered best first.
References
Hwang & Yoon (1981); Opricovic & Tzeng (2004), European Journal of Operational Research 156(2), 445-455; Zavadskas et al. (2012), Elektronika ir Elektrotechnika 122(6), 3-6; Keshavarz Ghorabaee et al. (2015), Informatica 26(3), 435-451.
topsis ¶
topsis(matrix: Sequence[Sequence[float]], weights: Sequence[float] | Mapping[str, float] | WeightingResult | BWMResult, criteria_types: Sequence[str], *, alternatives: Sequence[str] | None = None, criteria: Sequence[str] | None = None, normalize_weights: bool = False) -> RankingResult
TOPSIS (Hwang & Yoon 1981) with vector normalisation, Euclidean distance.
Each column is divided by its Euclidean norm and multiplied by the weight, \(v_{ij} = w_j x_{ij} / \sqrt{\sum_i x_{ij}^2}\). With the ideal point \(v_j^+\) (column maximum for a benefit, minimum for a cost) and the anti-ideal \(v_j^-\), the distances \(D_i^{\pm}\) are Euclidean and the score is the relative closeness
larger is better (0.5 if both distances are 0, i.e. all alternatives are
equal). A column of zeros contributes 0. extras holds d_plus and
d_minus.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrix
|
Sequence[Sequence[float]]
|
Alternatives by criteria (rows by columns), finite numbers. |
required |
weights
|
Sequence[float] | Mapping[str, float] | WeightingResult | BWMResult
|
Criteria weights: a sequence in criteria order, a mapping by
criterion name (naming every criterion and no other), or a
|
required |
criteria_types
|
Sequence[str]
|
|
required |
alternatives
|
Sequence[str] | None
|
Alternative names; |
None
|
criteria
|
Sequence[str] | None
|
Criterion names; if |
None
|
normalize_weights
|
bool
|
Rescale non-negative weights to sum to 1 instead of requiring it. |
False
|
Returns:
| Type | Description |
|---|---|
RankingResult
|
A |
RankingResult
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If a matrix entry or a weight is a |
ValueError
|
If the matrix is empty, ragged or non-finite, the counts of
names, types or weights do not match, names repeat, a mapping or
result misses a criterion or names an unknown one, a type is not
|
Examples:
>>> from bulanik.mcdm import topsis
>>> r = topsis([[250, 16], [200, 12], [300, 32]], [0.5, 0.5],
... ["cost", "benefit"], alternatives=["A", "B", "C"])
>>> r.ranking
('C', 'B', 'A')
>>> {k: round(v, 3) for k, v in r.scores.items()}
{'A': 0.262, 'B': 0.301, 'C': 0.699}
References
Hwang & Yoon (1981), Multiple Attribute Decision Making, Springer.
vikor ¶
vikor(matrix: Sequence[Sequence[float]], weights: Sequence[float] | Mapping[str, float] | WeightingResult | BWMResult, criteria_types: Sequence[str], *, alternatives: Sequence[str] | None = None, criteria: Sequence[str] | None = None, normalize_weights: bool = False, v: float = 0.5) -> RankingResult
VIKOR (Opricovic & Tzeng 2004); v is the weight of group utility.
For each criterion the best \(f_j^*\) and worst \(f_j^-\) values are the column maximum and minimum for a benefit (swapped for a cost). The group utility and individual regret are
(criteria with \(f_j^* = f_j^-\) are skipped) and the compromise index is
with \(S^* = \min S\), \(S^- = \max S\) (likewise \(R\); a term is 0 when its
range is 0). Lower \(Q\) is better: ranking lists the smallest score
first and the scores are \(Q\), not a goodness measure. extras holds S
and R. The compromise-acceptability conditions C1/C2 are not checked.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrix
|
Sequence[Sequence[float]]
|
Alternatives by criteria (rows by columns), finite numbers. |
required |
weights
|
Sequence[float] | Mapping[str, float] | WeightingResult | BWMResult
|
Criteria weights: a sequence in criteria order, a mapping by
criterion name (naming every criterion and no other), or a
|
required |
criteria_types
|
Sequence[str]
|
|
required |
alternatives
|
Sequence[str] | None
|
Alternative names; |
None
|
criteria
|
Sequence[str] | None
|
Criterion names; if |
None
|
normalize_weights
|
bool
|
Rescale non-negative weights to sum to 1 instead of requiring it. |
False
|
v
|
float
|
Weight of the group utility strategy in [0, 1]; \(1 - v\) weights the individual regret. 0.5 is the usual consensus. |
0.5
|
Returns:
| Type | Description |
|---|---|
RankingResult
|
A |
RankingResult
|
|
RankingResult
|
value of |
Raises:
| Type | Description |
|---|---|
TypeError
|
If a matrix entry or a weight is a |
ValueError
|
If the matrix is empty, ragged or non-finite, the counts of
names, types or weights do not match, names repeat, a mapping or
result misses a criterion or names an unknown one, a type is not
|
Examples:
>>> from bulanik.mcdm import vikor
>>> prices = [[250, 16, 12, 5], [200, 16, 8, 3], [300, 32, 16, 4], [275, 32, 8, 4]]
>>> r = vikor(prices, [0.3, 0.3, 0.2, 0.2], ["cost", "benefit", "benefit", "benefit"],
... alternatives=["A", "B", "C", "D"])
>>> r.ranking
('D', 'C', 'A', 'B')
>>> {k: round(v, 3) for k, v in r.scores.items()}
{'A': 0.75, 'B': 1.0, 'C': 0.5, 'D': 0.208}
References
Opricovic & Tzeng (2004), European Journal of Operational Research 156(2), 445-455.
waspas ¶
waspas(matrix: Sequence[Sequence[float]], weights: Sequence[float] | Mapping[str, float] | WeightingResult | BWMResult, criteria_types: Sequence[str], *, alternatives: Sequence[str] | None = None, criteria: Sequence[str] | None = None, normalize_weights: bool = False, lam: float = 0.5) -> RankingResult
WASPAS (Zavadskas et al. 2012): lam * WSM + (1 - lam) * WPM.
Entries are normalised linearly, \(\bar x_{ij} = x_{ij} / \max_i x_{ij}\) for
a benefit and \(\min_i x_{ij} / x_{ij}\) for a cost. The weighted-sum and
weighted-product scores are combined with the parameter \(\lambda\)
(lam):
larger is better. \(\lambda = 1\) is pure WSM, \(\lambda = 0\) pure WPM.
extras holds wsm and wpm.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrix
|
Sequence[Sequence[float]]
|
Alternatives by criteria (rows by columns), finite numbers. |
required |
weights
|
Sequence[float] | Mapping[str, float] | WeightingResult | BWMResult
|
Criteria weights: a sequence in criteria order, a mapping by
criterion name (naming every criterion and no other), or a
|
required |
criteria_types
|
Sequence[str]
|
|
required |
alternatives
|
Sequence[str] | None
|
Alternative names; |
None
|
criteria
|
Sequence[str] | None
|
Criterion names; if |
None
|
normalize_weights
|
bool
|
Rescale non-negative weights to sum to 1 instead of requiring it. |
False
|
lam
|
float
|
The combination parameter \(\lambda\) in [0, 1] (0.5 by default). |
0.5
|
Returns:
| Type | Description |
|---|---|
RankingResult
|
A |
RankingResult
|
|
RankingResult
|
value of |
Raises:
| Type | Description |
|---|---|
TypeError
|
If a matrix entry or a weight is a |
ValueError
|
If the matrix is empty, ragged or non-finite, the counts of
names, types or weights do not match, names repeat, a mapping or
result misses a criterion or names an unknown one, a type is not
|
Examples:
>>> from bulanik.mcdm import waspas
>>> r = waspas([[250, 16], [200, 12], [300, 32]], [0.5, 0.5],
... ["cost", "benefit"], alternatives=["A", "B", "C"], lam=0.5)
>>> r.ranking
('C', 'B', 'A')
>>> {k: round(v, 3) for k, v in r.scores.items()}
{'A': 0.641, 'B': 0.65, 'C': 0.825}
References
Zavadskas, Turskis, Antucheviciene & Zakarevicius (2012), Elektronika ir Elektrotechnika 122(6), 3-6.
edas ¶
edas(matrix: Sequence[Sequence[float]], weights: Sequence[float] | Mapping[str, float] | WeightingResult | BWMResult, criteria_types: Sequence[str], *, alternatives: Sequence[str] | None = None, criteria: Sequence[str] | None = None, normalize_weights: bool = False) -> RankingResult
EDAS (Keshavarz Ghorabaee et al. 2015): distance from the average solution.
With the column average \(\bar x_j\), the positive and negative distances from average (PDA and NDA) of a benefit criterion are
swapped for a cost criterion. The weighted sums \(SP_i = \sum_j w_j PDA_{ij}\)
and \(SN_i = \sum_j w_j NDA_{ij}\) are normalised, \(NSP_i = SP_i / \max SP\)
and \(NSN_i = 1 - SN_i / \max SN\) (0 and 1 when the maximum is 0), and the
appraisal score is \((NSP_i + NSN_i) / 2\) in [0, 1], larger is better.
extras holds SP, SN, NSP and NSN.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrix
|
Sequence[Sequence[float]]
|
Alternatives by criteria (rows by columns), finite numbers. |
required |
weights
|
Sequence[float] | Mapping[str, float] | WeightingResult | BWMResult
|
Criteria weights: a sequence in criteria order, a mapping by
criterion name (naming every criterion and no other), or a
|
required |
criteria_types
|
Sequence[str]
|
|
required |
alternatives
|
Sequence[str] | None
|
Alternative names; |
None
|
criteria
|
Sequence[str] | None
|
Criterion names; if |
None
|
normalize_weights
|
bool
|
Rescale non-negative weights to sum to 1 instead of requiring it. |
False
|
Returns:
| Type | Description |
|---|---|
RankingResult
|
A |
RankingResult
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If a matrix entry or a weight is a |
ValueError
|
If the matrix is empty, ragged or non-finite, the counts of
names, types or weights do not match, names repeat, a mapping or
result misses a criterion or names an unknown one, a type is not
|
Examples:
>>> from bulanik.mcdm import edas
>>> r = edas([[250, 16], [200, 12], [300, 32]], [0.5, 0.5],
... ["cost", "benefit"], alternatives=["A", "B", "C"])
>>> r.ranking
('C', 'A', 'B')
>>> {k: round(v, 3) for k, v in r.scores.items()}
{'A': 0.25, 'B': 0.167, 'C': 0.75}
References
Keshavarz Ghorabaee, Zavadskas, Olfat & Turskis (2015), Informatica 26(3), 435-451.
Scoring¶
scoring ¶
Scoring ranking methods: CODAS, COPRAS, MARCOS, CoCoSo, WISP.
Methods that score each alternative from a crisp decision matrix (rows are
alternatives, columns are criteria), a vector of criteria weights and the type
of each criterion ("benefit" or "cost"). They share the signature and the
weight forms of the classical methods in bulanik.mcdm.ranking
and return a RankingResult whose
ranking is ordered best first. Higher scores are better throughout.
The methods are codas,
copras, marcos,
cocoso and wisp.
References
Keshavarz Ghorabaee, Zavadskas, Turskis & Antucheviciene (2016), Economic Computation and Economic Cybernetics Studies and Research 50(3), 25-44; Zavadskas, Kaklauskas & Sarka (1994), Technological and Economic Development of Economy 1(3), 131-139; Stevic, Pamucar, Puska & Chatterjee (2020), Computers & Industrial Engineering 140, 106231; Yazdani, Zarate, Zavadskas & Turskis (2019), Management Decision 57(9), 2501-2519; Stanujkic, Popovic, Karabasevic, Meidute-Kavaliauskiene & Ulutas (2023), IEEE Transactions on Engineering Management 70(5), 1933-1944.
codas ¶
codas(matrix: Sequence[Sequence[float]], weights: Sequence[float] | Mapping[str, float] | WeightingResult | BWMResult, criteria_types: Sequence[str], *, alternatives: Sequence[str] | None = None, criteria: Sequence[str] | None = None, normalize_weights: bool = False, tau: float = 0.02, psi: str = 'equation') -> RankingResult
CODAS (Keshavarz Ghorabaee et al. 2016): distances from the negative-ideal point.
Entries are normalised linearly, \(n_{ij} = x_{ij} / \max_i x_{ij}\) for a benefit and \(\min_i x_{ij} / x_{ij}\) for a cost, and weighted, \(r_{ij} = w_j n_{ij}\). The negative-ideal point is \(ns_j = \min_i r_{ij}\); the Euclidean (primary) and taxicab (secondary) distances from it are
The relative assessment is
and the score is \(H_i = \sum_k h_{ik}\), larger is better (the scores sum to
0, so they can be negative). extras holds E and T.
Two conventions for \(\psi\). The paper is internally inconsistent. Its Eq. (10), printed above, adds the taxicab term when the Euclidean distances differ by at least \(\tau\), although the text says the taxicab distance is the tie-breaker "if the difference ... is less than \(\tau\)". The paper's own Tables 4 and 8 (Examples 1 and 2) follow the text, i.e. the opposite branch, and Chen & Gou (2021) criticise the printed form. Both are available:
psi="equation"(default): Eq. (10) as printed, the taxicab term counts when \(|E_i - E_k| \ge \tau\). It is the form printed in the paper.psi="tables": the convention of the paper's tables, the taxicab term counts when \(|E_i - E_k| < \tau\). Use it to reproduce the paper's printed numbers (Example 1 gives \(H = 0.1865, 0.6304, 0.3357, \dots\)).
Use "tables" to match a paper that follows the original tables and the
default to match one that follows the equation; the distances \(E\), \(T\) are
the same either way. The choice is recorded in operators["psi"].
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrix
|
Sequence[Sequence[float]]
|
Alternatives by criteria (rows by columns), finite numbers. A benefit column needs entries \(\ge 0\) with a maximum \(> 0\) (a zero normalises to 0); a cost column needs entries \(> 0\) (since \(\min_i x_{ij} / x_{ij}\) is undefined at 0). |
required |
weights
|
Sequence[float] | Mapping[str, float] | WeightingResult | BWMResult
|
Criteria weights: a sequence in criteria order, a mapping by
criterion name (naming every criterion and no other), or a
|
required |
criteria_types
|
Sequence[str]
|
|
required |
alternatives
|
Sequence[str] | None
|
Alternative names; |
None
|
criteria
|
Sequence[str] | None
|
Criterion names; if |
None
|
normalize_weights
|
bool
|
Rescale non-negative weights to sum to 1 instead of requiring it. |
False
|
tau
|
float
|
Threshold of \(\psi\) in [0, 1]; the paper suggests 0.01 to 0.05 and uses 0.02. |
0.02
|
psi
|
str
|
|
'equation'
|
Returns:
| Type | Description |
|---|---|
RankingResult
|
A |
RankingResult
|
|
RankingResult
|
(the value of |
Raises:
| Type | Description |
|---|---|
TypeError
|
If a matrix entry, a weight or |
ValueError
|
If the matrix is empty, ragged or non-finite, the counts of
names, types or weights do not match, names repeat, a mapping or
result misses a criterion or names an unknown one, a type is not
|
Examples:
>>> from bulanik.mcdm import codas
>>> r = codas([[250, 16], [200, 12], [300, 32]], [0.5, 0.5],
... ["cost", "benefit"], alternatives=["A", "B", "C"])
>>> r.ranking
('C', 'B', 'A')
>>> {k: round(v, 3) for k, v in r.scores.items()}
{'A': -0.517, 'B': -0.179, 'C': 0.696}
References
Keshavarz Ghorabaee, Zavadskas, Turskis & Antucheviciene (2016), A new combinative distance-based assessment (CODAS) method for multi-criteria decision-making, Economic Computation and Economic Cybernetics Studies and Research 50(3), 25-44. Chen & Gou (2021), Computational and Applied Mathematics 41(1), 11, doi:10.1007/s40314-021-01568-6, on the threshold function.
copras ¶
copras(matrix: Sequence[Sequence[float]], weights: Sequence[float] | Mapping[str, float] | WeightingResult | BWMResult, criteria_types: Sequence[str], *, alternatives: Sequence[str] | None = None, criteria: Sequence[str] | None = None, normalize_weights: bool = False) -> RankingResult
COPRAS (Zavadskas, Kaklauskas & Sarka 1994): complex proportional assessment.
Each column is divided by its sum and weighted, \(d_{ij} = w_j x_{ij} / \sum_i x_{ij}\). Benefit and cost criteria are summed separately, \(S^+_i = \sum_{j \in \text{benefit}} d_{ij}\) and \(S^-_i = \sum_{j \in \text{cost}} d_{ij}\), and the relative significance is
larger is better (just \(S^+_i\) without a cost criterion). scores holds
\(Q\); extras holds S_plus, S_minus and the utility degree
utility \(= 100\,Q_i / \max Q\) in percent. A cost criterion with weight 0
is ignored, so a model whose cost criteria all have weight 0 is scored as
having none.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrix
|
Sequence[Sequence[float]]
|
Alternatives by criteria (rows by columns), non-negative finite numbers with a positive sum in every column. |
required |
weights
|
Sequence[float] | Mapping[str, float] | WeightingResult | BWMResult
|
Criteria weights: a sequence in criteria order, a mapping by
criterion name (naming every criterion and no other), or a
|
required |
criteria_types
|
Sequence[str]
|
|
required |
alternatives
|
Sequence[str] | None
|
Alternative names; |
None
|
criteria
|
Sequence[str] | None
|
Criterion names; if |
None
|
normalize_weights
|
bool
|
Rescale non-negative weights to sum to 1 instead of requiring it. |
False
|
Returns:
| Type | Description |
|---|---|
RankingResult
|
A |
RankingResult
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If a matrix entry or a weight is a |
ValueError
|
If the matrix is empty, ragged or non-finite, the counts of
names, types or weights do not match, names repeat, a mapping or
result misses a criterion or names an unknown one, a type is not
|
Examples:
>>> from bulanik.mcdm import copras
>>> r = copras([[250, 16], [200, 12], [300, 32]], [0.5, 0.5],
... ["cost", "benefit"], alternatives=["A", "B", "C"])
>>> r.ranking
('C', 'B', 'A')
>>> {k: round(v, 3) for k, v in r.scores.items()}
{'A': 0.295, 'B': 0.303, 'C': 0.402}
References
Zavadskas & Kaklauskas (1996), Determination of an efficient contractor by using the new method of multicriteria assessment, in Langford & Retik (eds.), The Organization and Management of Construction: Shaping Theory and Practice, Vol. 2, 94-104; Zavadskas, Kaklauskas & Sarka (1994), The new method of multicriteria complex proportional assessment of projects, Technological and Economic Development of Economy 1(3), 131-139. The \(Q\) formula is Eq. (8) of Razavi Hajiagha, Hashemi & Zavadskas (2013), Technological and Economic Development of Economy 19(1), 22-37, doi:10.3846/20294913.2012.762953.
marcos ¶
marcos(matrix: Sequence[Sequence[float]], weights: Sequence[float] | Mapping[str, float] | WeightingResult | BWMResult, criteria_types: Sequence[str], *, alternatives: Sequence[str] | None = None, criteria: Sequence[str] | None = None, normalize_weights: bool = False) -> RankingResult
MARCOS (Stevic et al. 2020): measurement of alternatives and ranking according to compromise solution.
The matrix is extended with the ideal solution AI (best value per criterion: the maximum of a benefit, the minimum of a cost) and the anti-ideal AAI (the worst value). Every entry is divided by the ideal value, \(n_{ij} = x_{ij} / x_{AI,j}\) for a benefit and \(x_{AI,j} / x_{ij}\) for a cost, and weighted, \(v_{ij} = w_j n_{ij}\). With \(S_i = \sum_j v_{ij}\) (also for the AAI and AI rows) the utility degrees are \(K^-_i = S_i / S_{AAI}\) and \(K^+_i = S_i / S_{AI}\). The utility functions are
and the score, larger is better, is
extras holds K_minus, K_plus and S. Because \(f(K^-_i)\) and
\(f(K^+_i)\) are the same for every alternative (they depend only on
\(S_{AAI} / S_{AI}\)), the ranking equals the ranking by \(S_i\).
The entries must be strictly positive: a non-positive entry has no
meaningful ratio to the ideal.
Args:
matrix: Alternatives by criteria (rows by columns), strictly positive finite numbers.
weights: Criteria weights: a sequence in criteria order, a mapping by
criterion name (naming every criterion and no other), or a
WeightingResult /
BWMResult. They must sum to 1
unless normalize_weights is set.
criteria_types: "benefit" (larger is better) or "cost" (smaller is
better) for each criterion.
alternatives: Alternative names; A1..Am by default.
criteria: Criterion names; if None they are the keys of a mapping or
result weights (in order), else C1..Cn.
normalize_weights: Rescale non-negative weights to sum to 1 instead of
requiring it.
Returns:
| Type | Description |
|---|---|
RankingResult
|
A |
RankingResult
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If a matrix entry or a weight is a |
ValueError
|
If the matrix is empty, ragged or non-finite, the counts of
names, types or weights do not match, names repeat, a mapping or
result misses a criterion or names an unknown one, a type is not
|
Examples:
>>> from bulanik.mcdm import marcos
>>> r = marcos([[250, 16], [200, 12], [300, 32]], [0.5, 0.5],
... ["cost", "benefit"], alternatives=["A", "B", "C"])
>>> r.ranking
('C', 'B', 'A')
>>> {k: round(v, 3) for k, v in r.scores.items()}
{'A': 0.552, 'B': 0.583, 'C': 0.707}
References
Stevic, Pamucar, Puska & Chatterjee (2020), Sustainable supplier selection in healthcare industries using a new MCDM method: Measurement of alternatives and ranking according to COmpromise solution (MARCOS), Computers & Industrial Engineering 140, 106231, doi:10.1016/j.cie.2019.106231. The equations follow the restatement in Ulutas, Karabasevic, Popovic, Stanujkic, Nguyen & Karakoy (2020), Mathematics 8(10), 1672, Eqs. (18)-(28), doi:10.3390/math8101672.
cocoso ¶
cocoso(matrix: Sequence[Sequence[float]], weights: Sequence[float] | Mapping[str, float] | WeightingResult | BWMResult, criteria_types: Sequence[str], *, alternatives: Sequence[str] | None = None, criteria: Sequence[str] | None = None, normalize_weights: bool = False, lam: float = 0.5) -> RankingResult
CoCoSo (Yazdani et al. 2019): combined compromise solution.
Entries are scaled to \([0, 1]\) by min-max normalisation, \(r_{ij} = (x_{ij} - \min_i x_{ij}) / (\max_i x_{ij} - \min_i x_{ij})\) for a benefit and \((\max_i x_{ij} - x_{ij}) / (\max_i x_{ij} - \min_i x_{ij})\) for a cost; a constant column gets \(r_{ij} = 0\) for every alternative, which adds nothing to either sum below. The weighted-sum and weighted-power comparability sequences are
(a criterion with weight 0 is left out of both). Three appraisal scores combine them,
and the score, larger is better, is
extras holds S, P, k_a, k_b and k_c. Entries may be any finite
numbers. If some alternative is worst on every weighted criterion then
\(\min S = \min P = 0\) and \(k_{ib}\) is undefined (Rasoanaivo et al. 2024
report this failure of the method); a ValueError names that alternative.
This also happens when all alternatives are identical, and it
always happens when only one criterion carries weight (a single effective
criterion): the worst alternative on it is worst on every effective one, so
CoCoSo cannot rank such a model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrix
|
Sequence[Sequence[float]]
|
Alternatives by criteria (rows by columns), finite numbers. |
required |
weights
|
Sequence[float] | Mapping[str, float] | WeightingResult | BWMResult
|
Criteria weights: a sequence in criteria order, a mapping by
criterion name (naming every criterion and no other), or a
|
required |
criteria_types
|
Sequence[str]
|
|
required |
alternatives
|
Sequence[str] | None
|
Alternative names; |
None
|
criteria
|
Sequence[str] | None
|
Criterion names; if |
None
|
normalize_weights
|
bool
|
Rescale non-negative weights to sum to 1 instead of requiring it. |
False
|
lam
|
float
|
Balance \(\lambda \in [0, 1]\) between the weighted sum and the weighted power in \(k_{ic}\); the paper suggests 0.5. |
0.5
|
Returns:
| Type | Description |
|---|---|
RankingResult
|
A |
RankingResult
|
|
RankingResult
|
of |
Raises:
| Type | Description |
|---|---|
TypeError
|
If a matrix entry, a weight or |
ValueError
|
If the matrix is empty, ragged or non-finite, the counts of
names, types or weights do not match, names repeat, a mapping or
result misses a criterion or names an unknown one, a type is not
|
Examples:
>>> from bulanik.mcdm import cocoso
>>> r = cocoso([[7, 8], [8, 7], [9, 9]], [0.6, 0.4], ["benefit", "cost"],
... alternatives=["A", "B", "C"])
>>> r.ranking
('B', 'C', 'A')
>>> {k: round(v, 3) for k, v in r.scores.items()}
{'A': 1.408, 'B': 3.788, 'C': 2.758}
References
Yazdani, Zarate, Zavadskas & Turskis (2019), A combined compromise solution (CoCoSo) method for multi-criteria decision-making problems, Management Decision 57(9), 2501-2519, doi:10.1108/MD-05-2017-0458. On the zero-division failure: Rasoanaivo, Rakotonirainy & Zarate (2024), Combined compromise for ideal solution (CoCoFISo), Expert Systems with Applications 251, 124079, doi:10.1016/j.eswa.2024.124079.
wisp ¶
wisp(matrix: Sequence[Sequence[float]], weights: Sequence[float] | Mapping[str, float] | WeightingResult | BWMResult, criteria_types: Sequence[str], *, alternatives: Sequence[str] | None = None, criteria: Sequence[str] | None = None, normalize_weights: bool = False, product: str = 'power') -> RankingResult
WISP (Stanujkic et al. 2023): integrated simple weighted sum product method.
Entries are divided by the column maximum for every criterion, \(r_{ij} = x_{ij} / \max_i x_{ij}\). With \(\Omega_{\max}\) the benefit and \(\Omega_{\min}\) the cost criteria (those with positive weight), four utility measures combine the weighted sums and weighted products:
Each is recalculated, \(\bar u_i = (1 + u_i) / (1 + \max_k u_k)\), and the
score, larger is better, is \(u_i = \tfrac14 (\bar u^{sd}_i + \bar u^{pd}_i +
\bar u^{sr}_i + \bar u^{pr}_i)\). extras holds the four measures u_sd,
u_pd, u_sr, u_pr and their recalculated forms ubar_sd, ubar_pd,
ubar_sr, ubar_pr.
Only benefit or only cost criteria. For benefit criteria only, the paper (Stanujkic 2023, Sec. 2.3.1) uses two measures, the weighted sum and product, and averages their recalculated values. The convention here reproduces exactly that: in a difference an empty group contributes 0 (sum or product); in a ratio an empty denominator group is replaced by 1, and an empty numerator is 0 for sums and 1 for products. For benefit criteria only, \(u^{sd} = u^{sr}\) and \(u^{pd} = u^{pr}\), so the mean of the four is the paper's mean of two. For cost criteria only the paper is silent; the same convention gives \(u^{sd} = -\sum w r\), \(u^{pd} = -\prod r^{w}\), \(u^{sr} = 0\) and \(u^{pr} = 1 / \prod r^{w}\). A criterion with weight 0 is ignored.
Degenerate matrices. A measure with \(1 + \max_k u_k = 0\) (every alternative has \(r = 1\) on cost criteria only, so \(u^{sd} = u^{pd} = -1\) for all) is \(0/0\) and all alternatives are equal on it, so \(\bar u = 1\) for every alternative. A constant matrix, or a single alternative, therefore gives equal scores.
Two conventions for the products. The cited formulas write the weighted
product of the normalised ratings; the worked example of the CC-BY
restatement (Stanujkic 2023, Table 7) evaluates it as \(\prod_j r_{ij} w_j\)
(multiplication, values around \(10^{-7}\)) rather than \(\prod_j r_{ij}^{w_j}\),
which is the weighted product model and equals the WASPAS product in
the same article's Table 5. Both are available: product="power"
(default) uses \(r_{ij}^{w_j}\); product="times" uses \(r_{ij} w_j\) and
reproduces Table 7. The choice is recorded in operators["product"].
Args:
matrix: Alternatives by criteria (rows by columns), strictly positive finite numbers.
weights: Criteria weights: a sequence in criteria order, a mapping by
criterion name (naming every criterion and no other), or a
WeightingResult /
BWMResult. They must sum to 1
unless normalize_weights is set.
criteria_types: "benefit" (larger is better) or "cost" (smaller is
better) for each criterion.
alternatives: Alternative names; A1..Am by default.
criteria: Criterion names; if None they are the keys of a mapping or
result weights (in order), else C1..Cn.
normalize_weights: Rescale non-negative weights to sum to 1 instead of
requiring it.
product: "power" (\(r_{ij}^{w_j}\)) or "times" (\(r_{ij} w_j\)); see above.
Returns:
| Type | Description |
|---|---|
RankingResult
|
A |
RankingResult
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If a matrix entry or a weight is a |
ValueError
|
If the matrix is empty, ragged or non-finite, the counts of
names, types or weights do not match, names repeat, a mapping or
result misses a criterion or names an unknown one, a type is not
|
Examples:
>>> from bulanik.mcdm import wisp
>>> r = wisp([[250, 16], [200, 12], [300, 32]], [0.5, 0.5],
... ["cost", "benefit"], alternatives=["A", "B", "C"])
>>> r.ranking
('C', 'A', 'B')
>>> {k: round(v, 3) for k, v in r.scores.items()}
{'A': 0.829, 'B': 0.827, 'C': 1.0}
References
Stanujkic, Popovic, Karabasevic, Meidute-Kavaliauskiene & Ulutas (2023), An integrated simple weighted sum product method - WISP, IEEE Transactions on Engineering Management 70(5), 1933-1944 (online 2021), doi:10.1109/TEM.2021.3075783. Equations (16)-(31) as restated in M. Stanujkic (2023), Comparative analysis of MULTIMOORA, WASPAS and WISP methods: the case of candidate selection, Journal of Process Management and New Technologies 11(3-4), 79-88, doi:10.5937/jpmnt11-47703.
Outranking¶
outranking ¶
Outranking ranking methods: ELECTRE I with net concordance and discordance.
Methods that compare alternatives pair by pair instead of scoring each one
alone. electre takes a crisp decision
matrix (rows are alternatives, columns are criteria), a vector of criteria
weights and the type of each criterion ("benefit" or "cost"), shares the
signature and the weight forms of the methods in
bulanik.mcdm.ranking and returns a
RankingResult whose ranking is ordered
best first.
References
Roy (1968), Revue francaise d'informatique et de recherche operationnelle 2(8), 57-75; Hwang & Yoon (1981), Multiple Attribute Decision Making, Springer; van Delft & Nijkamp (1977), Multi-criteria Analysis and Regional Decision-making, Martinus Nijhoff.
electre ¶
electre(matrix: Sequence[Sequence[float]], weights: Sequence[float] | Mapping[str, float] | WeightingResult | BWMResult, criteria_types: Sequence[str], *, alternatives: Sequence[str] | None = None, criteria: Sequence[str] | None = None, normalize_weights: bool = False, cost: str = 'reverse', c_threshold: float | None = None, d_threshold: float | None = None) -> RankingResult
ELECTRE I (Roy 1968) with a complete ranking from net concordance and discordance.
The matrix is vector-normalised and weighted as in TOPSIS, \(v_{ij} = w_j x_{ij} / \sqrt{\sum_i x_{ij}^2}\) (a zero column gives 0). For an ordered pair of alternatives \(k \ne l\) the concordance set is \(C_{kl} = \{j : x_{kj} \ge x_{lj}\}\) for a benefit criterion (\(x_{kj} \le x_{lj}\) for a cost one) and the discordance set \(D_{kl}\) is its complement. The concordance and discordance indices are
with \(d_{kl} = 0\) when \(D_{kl}\) is empty or the denominator is 0 (identical alternatives). The denominator is the largest weighted difference of the pair over all criteria, as in Hwang & Yoon (1981), not a per-criterion range. The thresholds \(\bar c\) and \(\bar d\) are the means of \(c_{kl}\) and \(d_{kl}\) over all pairs \(k \ne l\) unless given. Alternative \(k\) outranks \(l\) when
Two entries of a criterion are tied, and concordant both ways, when they differ by at most \(10^{-12}\) times the larger magnitude (so scaling a column cannot create or break a tie by one ulp). The thresholds are applied with a slack of \(10^{-12}\) so that a pair sitting on a threshold in exact arithmetic is not lost to rounding.
The outranking relation is partial. For a complete ranking the net indices of van Delft & Nijkamp (1977) are used,
where a large \(C_k\) and a small \(D_k\) are good. Sources rank by \(C_k\) (descending) and by \(D_k\) (ascending) and pick the alternative that is best on both; here the two are combined into one score so that a full ranking always exists,
larger is better. The net indices do not depend on the thresholds, so
neither do scores and ranking; the thresholds only shape outranks.
extras holds net_concordance, net_discordance, concordance and
discordance (each {alternative: {other: value}}) and outranks
({alternative: tuple of the alternatives it outranks}).
The criterion types are the user's call: whether a criterion is a benefit or a cost is not inferred from the data, and the published example in the tests assumes its types.
Cost criteria. Two treatments are in use, selected by cost:
"reverse"(default, Hwang & Yoon): a cost column is kept and the comparison reversed (\(\le\) instead of \(\ge\))."inverse"(e.g. Cakin & Ozdemir 2013): a cost column is replaced by \(1/x_{ij}\) before the vector norm and then compared like a benefit. It needs \(x_{ij} > 0\) in every cost column. The concordance indices are the same; the discordance indices (and so the scores) differ.
The choice is recorded in operators["cost"].
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrix
|
Sequence[Sequence[float]]
|
Alternatives by criteria (rows by columns), finite numbers; at least two rows. |
required |
weights
|
Sequence[float] | Mapping[str, float] | WeightingResult | BWMResult
|
Criteria weights: a sequence in criteria order, a mapping by
criterion name (naming every criterion and no other), or a
|
required |
criteria_types
|
Sequence[str]
|
|
required |
alternatives
|
Sequence[str] | None
|
Alternative names; |
None
|
criteria
|
Sequence[str] | None
|
Criterion names; if |
None
|
normalize_weights
|
bool
|
Rescale non-negative weights to sum to 1 instead of requiring it. |
False
|
cost
|
str
|
|
'reverse'
|
c_threshold
|
float | None
|
Concordance threshold in [0, 1]; the mean of \(c_{kl}\) by default. |
None
|
d_threshold
|
float | None
|
Discordance threshold in [0, 1]; the mean of \(d_{kl}\) by default. |
None
|
Returns:
| Type | Description |
|---|---|
RankingResult
|
A |
RankingResult
|
`operators == {"normalization": "vector", "cost": "reverse", "score": |
RankingResult
|
"net_concordance_minus_net_discordance", "c_threshold": "mean", |
RankingResult
|
"d_threshold": "mean"}` (a given threshold appears as the text of its value). |
Raises:
| Type | Description |
|---|---|
TypeError
|
If a matrix entry, a weight or a threshold is a |
ValueError
|
If the matrix is empty, ragged or non-finite or has fewer
than two rows, the counts of names, types or weights do not match,
names repeat, a mapping or result misses a criterion or names an
unknown one, a type is not |
Examples:
>>> from bulanik.mcdm import electre
>>> r = electre([[7, 9], [8, 7], [9, 8]], [0.6, 0.4], ["benefit", "cost"],
... alternatives=["A", "B", "C"])
>>> r.ranking
('C', 'B', 'A')
>>> r.extras["outranks"]
{'A': (), 'B': ('A',), 'C': ('A',)}
>>> {k: round(v, 3) for k, v in r.extras["net_concordance"].items()}
{'A': -2.0, 'B': 0.8, 'C': 1.2}
>>> {k: round(v, 3) for k, v in r.extras["net_discordance"].items()}
{'A': 2.0, 'B': -0.667, 'C': -1.333}
References
Roy (1968), Classement et choix en presence de points de vue multiples (la methode ELECTRE), Revue francaise d'informatique et de recherche operationnelle 2(8), 57-75, doi:10.1051/ro/196802V100571. Hwang & Yoon (1981), Multiple Attribute Decision Making: Methods and Applications, Springer, pp. 115-127. van Delft & Nijkamp (1977), Multi-criteria Analysis and Regional Decision-making, Martinus Nijhoff. Net indices as applied by Cakin & Ozdemir (2013), Afyon Kocatepe Universitesi IIBF Dergisi 15(2), 339-361.
Fuzzy ranking¶
fuzzy ¶
Fuzzy ranking: expert decision matrices, rank_fuzzy and Chen's fuzzy TOPSIS.
Two routes from fuzzy ratings to a ranking.
rank_fuzzytakes aDecisionMatrixof expert ratings (any fuzzy number kind), aggregates the experts, scores each fuzzy number to a crisp value and hands the crisp matrix to a classical method (topsis,vikor,waspas,edas,codas,copras,marcos,cocoso,wisporelectre). Withmethod="fuzzy_codas","fuzzy_waspas"or"fuzzy_cocoso"it skips the scoring and hands the aggregated fuzzy matrix tofuzzy_codas,fuzzy_waspasorfuzzy_cocoso.fuzzy_topsiskeeps the numbers fuzzy throughout, following Chen (2000), for triangular numbers.
References
Chen (2000), Fuzzy Sets and Systems 114(1), 1-9.
DecisionMatrix
dataclass
¶
DecisionMatrix(alternatives: tuple[str, ...], criteria: tuple[str, ...], experts: tuple[tuple[tuple[FuzzyNumber, ...], ...], ...], expert_weights: tuple[float, ...] | None = None)
Expert ratings [expert][alternative][criterion] of one fuzzy number kind.
The input of rank_fuzzy. Construction
validates the shape and that all ratings are fuzzy numbers of the same
kind, and normalises the containers to tuples.
Attributes:
| Name | Type | Description |
|---|---|---|
alternatives |
tuple[str, ...]
|
Alternative names (unique, at least one). |
criteria |
tuple[str, ...]
|
Criterion names (unique, at least one). |
experts |
tuple[tuple[tuple[FuzzyNumber, ...], ...], ...]
|
Ratings indexed |
expert_weights |
tuple[float, ...] | None
|
Expert weights summing to 1, or |
Raises:
| Type | Description |
|---|---|
ValueError
|
If there are no experts, alternatives or criteria, names repeat, an expert's table has the wrong shape, or the expert weights are invalid (negative, wrong count, not summing to 1). |
TypeError
|
If a rating is not a fuzzy number or the kinds differ. |
Examples:
>>> from bulanik import TFN
>>> from bulanik.mcdm import DecisionMatrix
>>> dm = DecisionMatrix(
... alternatives=["A", "B"], criteria=["price"],
... experts=[[[TFN(1, 2, 3)], [TFN(2, 3, 4)]]])
>>> dm.expert_weights is None
True
>>> dm.experts[0][1][0]
TFN(l=2.0, m=3.0, u=4.0)
from_terms
classmethod
¶
from_terms(scale: LinguisticScale, alternatives: Sequence[str], criteria: Sequence[str], experts_terms: Sequence[Sequence[Sequence[str]]], expert_weights: Sequence[float] | None = None) -> 'DecisionMatrix'
Build from linguistic terms [expert][alternative][criterion].
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scale
|
LinguisticScale
|
The |
required |
alternatives
|
Sequence[str]
|
Alternative names. |
required |
criteria
|
Sequence[str]
|
Criterion names. |
required |
experts_terms
|
Sequence[Sequence[Sequence[str]]]
|
Term names indexed |
required |
expert_weights
|
Sequence[float] | None
|
Expert weights summing to 1, or |
None
|
Returns:
| Type | Description |
|---|---|
'DecisionMatrix'
|
The decision matrix of converted ratings. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If a term is not in the scale. |
ValueError
|
As for the constructor. |
Examples:
>>> from bulanik import TFN
>>> from bulanik.mcdm import DecisionMatrix
>>> from bulanik.scales import LinguisticScale
>>> scale = LinguisticScale("demo", {"L": TFN(0, 0, 5), "H": TFN(5, 10, 10)})
>>> dm = DecisionMatrix.from_terms(scale, ["A", "B"], ["price"],
... [[["L"], ["H"]], [["H"], ["H"]]])
>>> dm.experts[0][0][0]
TFN(l=0.0, m=0.0, u=5.0)
rank_fuzzy ¶
rank_fuzzy(matrix: DecisionMatrix, weights: Any, criteria_types: Sequence[str], *, method: str = 'topsis', aggregation: str = 'wa', ops: str = 'algebraic', score: str | None = None, **method_kwargs: Any) -> RankingResult
Aggregate experts, score the fuzzy numbers, then rank with a crisp method.
The expert ratings are combined cell by cell with
aggregate_experts (weighted
average "wa" or weighted geometric mean "wg"), each aggregated number
is converted to a crisp value with its kind's score, and the resulting
matrix is ranked by method with the crisp function of the same name
(so VIKOR scores are still lower-is-better). The returned operators are
those of the crisp method plus aggregation, ops and score. ELECTRE
already uses the key score for its own ranking score, which is kept as
ranking_score.
The methods "fuzzy_codas", "fuzzy_waspas" and "fuzzy_cocoso" are
different: the aggregated fuzzy matrix goes straight to
fuzzy_codas,
fuzzy_waspas or
fuzzy_cocoso with ops and
score (no scoring first; score=None leaves the choice to the method),
and operators are the method's plus aggregation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrix
|
DecisionMatrix
|
The expert ratings. |
required |
weights
|
Any
|
Criteria weights in any form the crisp methods accept (a
sequence, a mapping, a
|
required |
criteria_types
|
Sequence[str]
|
|
required |
method
|
str
|
|
'topsis'
|
aggregation
|
str
|
|
'wa'
|
ops
|
str
|
Operator set of the fuzzy number kind ( |
'algebraic'
|
score
|
str | None
|
Score name for defuzzification, or |
None
|
**method_kwargs
|
Any
|
Passed to the crisp method, e.g. |
{}
|
Returns:
| Type | Description |
|---|---|
RankingResult
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
TypeError
|
If a fuzzy method rejects the number kind (see its Raises). |
Examples:
>>> from bulanik import TFN
>>> from bulanik.mcdm import DecisionMatrix, rank_fuzzy
>>> from bulanik.scales import LinguisticScale
>>> scale = LinguisticScale("demo", {"L": TFN(0, 0, 5), "M": TFN(3, 5, 7), "H": TFN(5, 10, 10)})
>>> dm = DecisionMatrix.from_terms(
... scale, ["A", "B"], ["price", "quality"],
... [[["L", "H"], ["M", "M"]], [["M", "H"], ["H", "L"]]])
>>> r = rank_fuzzy(dm, {"price": 0.4, "quality": 0.6}, ["cost", "benefit"])
>>> r.ranking
('A', 'B')
>>> r.operators["aggregation"], r.operators["score"]
('wa', 'centroid')
>>> rank_fuzzy(dm, [0.4, 0.6], ["cost", "benefit"], method="waspas", lam=0.7).ranking
('A', 'B')
>>> from bulanik.scales import EXAMPLE_IFN_5
>>> ifn = DecisionMatrix.from_terms(EXAMPLE_IFN_5, ["A", "B"], ["price", "quality"],
... [[["L", "H"], ["M", "M"]]])
>>> r = rank_fuzzy(ifn, [0.4, 0.6], ["cost", "benefit"], method="fuzzy_cocoso")
>>> r.ranking, r.operators["score"]
(('A', 'B'), 'normalized')
fuzzy_topsis ¶
fuzzy_topsis(matrix: Sequence[Sequence[TFN]], weights: Sequence[TFN | float] | Mapping[str, TFN | float] | WeightingResult | BWMResult, criteria_types: Sequence[str], *, alternatives: Sequence[str] | None = None, criteria: Sequence[str] | None = None) -> RankingResult
Fuzzy TOPSIS of Chen (2000) on triangular fuzzy numbers.
Ratings are normalised per criterion to \([0, 1]\): for a benefit,
\(\tilde r_{ij} = (l_{ij}, m_{ij}, u_{ij}) / c_j^*\) with \(c_j^* = \max_i
u_{ij}\); for a cost, \(\tilde r_{ij} = (a_j^-/u_{ij}, a_j^-/m_{ij},
a_j^-/l_{ij})\) with \(a_j^- = \min_i l_{ij}\). They are multiplied by the
weights (componentwise, \(\tilde v_{ij} = \tilde r_{ij}\otimes\tilde w_j\)).
The fuzzy positive and negative ideal solutions are fixed at
\((1,1,1)\) and \((0,0,0)\), the distance between triangular numbers is the
vertex distance \(d(\tilde a, \tilde b) = \sqrt{\tfrac13 \sum (a_k -
b_k)^2}\), and the score is the closeness coefficient
\(CC_i = d_i^- / (d_i^* + d_i^-)\) with \(d_i^* = \sum_j d(\tilde v_{ij},
(1,1,1))\) and \(d_i^- = \sum_j d(\tilde v_{ij}, (0,0,0))\), larger is
better. extras holds d_star and d_minus.
The weights must lie in [0, 1] (TFN or crisp). Because the ideal solutions are (1,1,1) and (0,0,0), a weight above 1 would push the weighted ratings past the "ideal"; normalise a 0-10 weight by dividing by 10. Weights need not sum to 1.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrix
|
Sequence[Sequence[TFN]]
|
Alternatives by criteria of |
required |
weights
|
Sequence[TFN | float] | Mapping[str, TFN | float] | WeightingResult | BWMResult
|
Weights in \([0, 1]\): a sequence in criteria order, a mapping by
criterion name (naming every criterion and no others, as for the
crisp methods) or a
|
required |
criteria_types
|
Sequence[str]
|
|
required |
alternatives
|
Sequence[str] | None
|
Alternative names; |
None
|
criteria
|
Sequence[str] | None
|
Criterion names; if |
None
|
Returns:
| Type | Description |
|---|---|
RankingResult
|
A |
RankingResult
|
`operators == {"normalization": "linear_scale_chen2000", |
RankingResult
|
"distance": "vertex"}`. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If a rating is not a TFN. |
ValueError
|
If the matrix is empty or ragged, names or weights do not match, a weight lies outside \([0, 1]\), or a rating violates the sign requirements above. |
Examples:
>>> from bulanik import TFN
>>> from bulanik.mcdm import fuzzy_topsis
>>> m = [[TFN(5, 7, 9), TFN(3, 5, 7)],
... [TFN(7, 9, 9), TFN(1, 3, 5)],
... [TFN(3, 5, 7), TFN(7, 9, 9)]]
>>> r = fuzzy_topsis(m, {"quality": TFN(.5, .7, .9), "price": 0.3},
... ["benefit", "cost"], alternatives=["X", "Y", "Z"])
>>> r.ranking
('Y', 'X', 'Z')
>>> {k: round(v, 3) for k, v in r.scores.items()}
{'X': 0.329, 'Y': 0.413, 'Z': 0.243}
>>> fuzzy_topsis(m, [5, 0.3], ["benefit", "cost"])
Traceback (most recent call last):
...
ValueError: weight of criterion 'C1' must lie in [0, 1] for fuzzy TOPSIS ...
References
Chen (2000), Fuzzy Sets and Systems 114(1), 1-9.
Ranking on fuzzy numbers¶
fuzzy_ranking ¶
Ranking on fuzzy numbers: CODAS, WASPAS and CoCoSo computed on the numbers themselves.
rank_fuzzy with a crisp method scores every
fuzzy rating first and then ranks the crisp matrix. The three methods here keep
the ratings fuzzy: the weighted matrix, the negative-ideal solution, the
weighted sums and the weighted products are built with the number kind's own
operators (scale, add, mul, power, through
wa and wg where they
apply), and the numbers are scored only at the end, as the published
intuitionistic, Pythagorean and neutrosophic variants of these methods do.
fuzzy_codas: distances of the weighted numbers from the negative-ideal numbers.fuzzy_waspas: the weighted sum and the weighted product combined as numbers, then scored (or scored, then combined).fuzzy_cocoso: the weighted sum and the weighted product scored, then CoCoSo's three appraisal scores.
Each takes an already aggregated matrix (alternatives by criteria) of one
number kind, crisp criteria weights in the forms the crisp methods accept,
criteria_types, the operator set ops and a score name, and returns a
RankingResult whose ranking is best
first. A cost criterion is turned into a benefit one with the kind's
complement() (for an orthopair number \((\mu, \nu) \mapsto (\nu, \mu)\)), which
exists for the orthopair, interval-valued orthopair, picture, spherical,
neutrosophic and interval neutrosophic kinds; triangular, trapezoidal and
interval type-2 numbers have none, so they can only be used with benefit
criteria here (or through rank_fuzzy).
References
Rani, Ali, Krishankumar, Mishra, Cavallaro & Ravichandran (2021), Energies 14(15), 4594 (SVN-CoCoSo).
fuzzy_codas ¶
fuzzy_codas(matrix: Sequence[Sequence[Any]], weights: _Weights, criteria_types: Sequence[str], *, alternatives: Sequence[str] | None = None, criteria: Sequence[str] | None = None, normalize_weights: bool = False, ops: str = 'algebraic', score: str | None = None, tau: float = 0.02, psi: str = 'equation') -> RankingResult
CODAS on fuzzy numbers: distances of the weighted numbers from the negative-ideal numbers.
Cost criteria are first replaced by their complements. Each rating is
weighted with the kind's scalar multiple, \(\tilde u_{ij} = w_j \tilde
x_{ij}\) (scale; for an intuitionistic number \((1 - (1 - \mu)^{w_j},
\nu^{w_j})\)). The negative-ideal number of criterion \(j\), \(\tilde{ns}_j\),
is the \(\tilde u_{ij}\) with the lowest score in its column (the first
one on a tie). With the kind's normalised Euclidean and Hamming distances
\(d_E\) and \(d_H\),
For an intuitionistic number the method defines the distances as
\(d_E(a, b)^2 = \tfrac12[(\mu_a - \mu_b)^2 + (\nu_a - \nu_b)^2 +
(\pi_a - \pi_b)^2]\) and \(d_H(a, b) = \tfrac12(|\mu_a - \mu_b| + |\nu_a -
\nu_b| + |\pi_a - \pi_b|)\) (the kind's "euclidean" and "hamming"
distances), and \(E_i\) and \(T_i\) are the sums of Altın et al. (2026),
Eqs. (15)-(16), with these distances (see also Szmidt & Kacprzyk 2000 on
distances between intuitionistic fuzzy sets). The relative assessment and the score are those
of crisp codas (shared code),
larger is better; psi selects the convention of \(\psi\) exactly as there
("equation", the default, adds the taxicab term when
\(|E_i - E_k| \ge \tau\), as in Altın et al. (2026), Eq. (19)).
extras holds E and T (by alternative) and negative_ideal, the
numbers \(\tilde{ns}_j\) by criterion. Picture numbers fail to scale
by almost any weight below 1 (see PictureFN),
so fuzzy_codas raises a ValueError naming the criterion and the weight for
them; use fuzzy_waspas,
fuzzy_cocoso or
rank_fuzzy(method="codas", score="normalized") instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrix
|
Sequence[Sequence[Any]]
|
Alternatives by criteria of fuzzy numbers of one kind (and
rung) with |
required |
weights
|
_Weights
|
Crisp criteria weights: a sequence in criteria order, a
mapping by criterion name, or a
|
required |
criteria_types
|
Sequence[str]
|
|
required |
alternatives
|
Sequence[str] | None
|
Alternative names; |
None
|
criteria
|
Sequence[str] | None
|
Criterion names; if |
None
|
normalize_weights
|
bool
|
Rescale non-negative weights to sum to 1 instead of requiring it. |
False
|
ops
|
str
|
Operator set of the kind, e.g. |
'algebraic'
|
score
|
str | None
|
Score that picks the negative-ideal numbers; |
None
|
tau
|
float
|
Threshold of \(\psi\) in [0, 1]. |
0.02
|
psi
|
str
|
|
'equation'
|
Returns:
| Type | Description |
|---|---|
RankingResult
|
A |
RankingResult
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If an entry is not a fuzzy number, the kinds (or rungs)
differ, a cost criterion meets a kind without |
ValueError
|
If the matrix is empty or ragged, names, types or weights
do not match (as for the crisp methods), |
Examples:
>>> from bulanik import IFN
>>> from bulanik.mcdm import fuzzy_codas
>>> m = [[IFN(0.6, 0.3), IFN(0.4, 0.5)], [IFN(0.5, 0.4), IFN(0.7, 0.2)]]
>>> r = fuzzy_codas(m, [0.6, 0.4], ["benefit", "benefit"])
>>> r.ranking
('A2', 'A1')
>>> {k: round(v, 4) for k, v in r.scores.items()}
{'A1': -0.2708, 'A2': 0.2708}
References
Keshavarz Ghorabaee, Zavadskas, Turskis & Antucheviciene (2016), Economic Computation and Economic Cybernetics Studies and Research 50(3), 25-44 (crisp CODAS). See also: Szmidt & Kacprzyk (2000), Distances between intuitionistic fuzzy sets, Fuzzy Sets and Systems 114(3), 505-518, doi:10.1016/S0165-0114(98)00244-9; Bolturk (2018), Pythagorean fuzzy CODAS and its application to supplier selection in a manufacturing firm, Journal of Enterprise Information Management 31(4), 550-564, doi:10.1108/JEIM-01-2018-0020.
fuzzy_waspas ¶
fuzzy_waspas(matrix: Sequence[Sequence[Any]], weights: _Weights, criteria_types: Sequence[str], *, alternatives: Sequence[str] | None = None, criteria: Sequence[str] | None = None, normalize_weights: bool = False, ops: str = 'algebraic', score: str | None = None, lam: float = 0.5, combine: str = 'fuzzy') -> RankingResult
WASPAS on fuzzy numbers: weighted sum and weighted product combined as numbers, then scored.
Cost criteria are first replaced by their complements. With the kind's operators, the weighted sum (WSM) and the weighted product (WPM) of each alternative are the numbers
i.e. wa and wg of the
row (for an intuitionistic number \(\tilde Q^{(1)} = (1 - \prod_j (1 -
\mu_j)^{w_j}, \prod_j \nu_j^{w_j})\)). combine selects how they meet
\(\lambda\); larger \(Q_i\) is better either way.
combine="fuzzy"(default): they are combined as numbers, with the kind'sscaleandadd, and the combination is scored once,
$\(\tilde Q_i = \lambda \tilde Q^{(1)}_i \oplus (1 - \lambda) \tilde Q^{(2)}_i, \qquad Q_i = S(\tilde Q_i).\)$
Interval-valued intuitionistic numbers can use
score="defuzzified". It is computed as wa of
\((\tilde Q^{(1)}_i, \tilde Q^{(2)}_i)\) with weights
\((\lambda, 1 - \lambda)\), which is that scale and add (a weight of
0 drops its term, so \(\lambda = 1\) gives the WSM alone and
\(\lambda = 0\) the WPM alone).
- combine="scores": the two numbers are scored first and the scores
combined, \(Q_i = \lambda S(\tilde Q^{(1)}_i) + (1 - \lambda)
S(\tilde Q^{(2)}_i)\), the form of crisp WASPAS.
extras holds the numbers Q1 and Q2, the scores Q1_score and
Q2_score, and (for combine="fuzzy") the combined number Q.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrix
|
Sequence[Sequence[Any]]
|
Alternatives by criteria of fuzzy numbers of one kind (and rung). |
required |
weights
|
_Weights
|
Crisp criteria weights: a sequence in criteria order, a
mapping by criterion name, or a
|
required |
criteria_types
|
Sequence[str]
|
|
required |
alternatives
|
Sequence[str] | None
|
Alternative names; |
None
|
criteria
|
Sequence[str] | None
|
Criterion names; if |
None
|
normalize_weights
|
bool
|
Rescale non-negative weights to sum to 1 instead of requiring it. |
False
|
ops
|
str
|
Operator set of the kind, e.g. |
'algebraic'
|
score
|
str | None
|
Score of the final numbers; |
None
|
lam
|
float
|
Balance \(\lambda \in [0, 1]\) between the WSM and the WPM. |
0.5
|
combine
|
str
|
|
'fuzzy'
|
Returns:
| Type | Description |
|---|---|
RankingResult
|
A |
RankingResult
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If an entry is not a fuzzy number, the kinds (or rungs)
differ, a cost criterion meets a kind without |
ValueError
|
If the matrix is empty or ragged, names, types or weights
do not match (as for the crisp methods), |
Examples:
>>> from bulanik import IFN
>>> from bulanik.mcdm import fuzzy_waspas
>>> m = [[IFN(0.6, 0.3), IFN(0.4, 0.5)], [IFN(0.5, 0.4), IFN(0.7, 0.2)]]
>>> r = fuzzy_waspas(m, [0.6, 0.4], ["benefit", "benefit"])
>>> r.ranking
('A2', 'A1')
>>> {k: round(v, 4) for k, v in r.scores.items()}
{'A1': 0.142, 'A2': 0.2676}
>>> s = fuzzy_waspas(m, [0.6, 0.4], ["benefit", "benefit"], combine="scores")
>>> {k: round(v, 4) for k, v in s.scores.items()}
{'A1': 0.1418, 'A2': 0.2672}
References
Zavadskas, Turskis, Antucheviciene & Zakarevicius (2012), Elektronika ir Elektrotechnika 122(6), 3-6 (crisp WASPAS). See also: Ilbahar & Kahraman (2018), Retail store performance measurement using a novel interval-valued Pythagorean fuzzy WASPAS method, Journal of Intelligent & Fuzzy Systems 35(3), 3835-3846, doi:10.3233/JIFS-18730.
fuzzy_cocoso ¶
fuzzy_cocoso(matrix: Sequence[Sequence[Any]], weights: _Weights, criteria_types: Sequence[str], *, alternatives: Sequence[str] | None = None, criteria: Sequence[str] | None = None, normalize_weights: bool = False, ops: str = 'algebraic', score: str | None = None, lam: float = 0.5) -> RankingResult
CoCoSo on fuzzy numbers: weighted sum and weighted product as numbers, scored, then compromised.
Cost criteria are first replaced by their complements. With the kind's operators the weighted-sum and weighted-product sequences are the numbers
(wa and wg of the row),
with no normalisation step: \(\tilde P_i\) is the weighted geometric
product, not a fuzzy sum of powers. This follows the form of the
neutrosophic CoCoSo of Rani et al. (2021), Eqs. (11)-(12). Their
scores \(S_i = S(\tilde S_i)\) and \(P_i = S(\tilde P_i)\) enter crisp
CoCoSo's appraisal scores (shared code with
cocoso),
larger is better. The ratios need non-negative scores: the default
score is the kind's "normalized" one where it has it (orthopair,
interval-valued orthopair, picture, spherical: values in [0, 1]), and the
kind's default otherwise; a negative score raises ValueError. If an
alternative's \(S_i\) or \(P_i\) is 0, \(k_{ib}\) divides by zero and a
ValueError names the alternative. Because \(\tilde P_i\) is a weighted
product, a single intuitionistic \((0, 1)\) rating is enough to make \(P_i\)
zero, even if the alternative is good on the other criteria. extras holds the numbers S and P, their
scores S_score and P_score, and k_a, k_b, k_c.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrix
|
Sequence[Sequence[Any]]
|
Alternatives by criteria of fuzzy numbers of one kind (and rung). |
required |
weights
|
_Weights
|
Crisp criteria weights: a sequence in criteria order, a
mapping by criterion name, or a
|
required |
criteria_types
|
Sequence[str]
|
|
required |
alternatives
|
Sequence[str] | None
|
Alternative names; |
None
|
criteria
|
Sequence[str] | None
|
Criterion names; if |
None
|
normalize_weights
|
bool
|
Rescale non-negative weights to sum to 1 instead of requiring it. |
False
|
ops
|
str
|
Operator set of the kind, e.g. |
'algebraic'
|
score
|
str | None
|
A non-negative score; |
None
|
lam
|
float
|
Balance \(\lambda \in [0, 1]\) in \(k_{ic}\). |
0.5
|
Returns:
| Type | Description |
|---|---|
RankingResult
|
A |
RankingResult
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If an entry is not a fuzzy number, the kinds (or rungs)
differ, a cost criterion meets a kind without |
ValueError
|
If the matrix is empty or ragged, names, types or weights
do not match (as for the crisp methods), |
Examples:
>>> from bulanik import IFN
>>> from bulanik.mcdm import fuzzy_cocoso
>>> m = [[IFN(0.6, 0.3), IFN(0.4, 0.5)], [IFN(0.5, 0.4), IFN(0.7, 0.2)]]
>>> r = fuzzy_cocoso(m, [0.6, 0.4], ["benefit", "benefit"])
>>> r.ranking, r.operators["score"]
(('A2', 'A1'), 'normalized')
>>> {k: round(v, 4) for k, v in r.scores.items()}
{'A1': 2.0738, 'A2': 2.3016}
References
Rani, Ali, Krishankumar, Mishra, Cavallaro & Ravichandran (2021), An integrated single-valued neutrosophic combined compromise solution methodology for renewable energy resource selection problem, Energies 14(15), 4594, doi:10.3390/en14154594, Eqs. (7) and (11)-(16). Yazdani, Zarate, Zavadskas & Turskis (2019), Management Decision 57(9), 2501-2519, doi:10.1108/MD-05-2017-0458 (crisp CoCoSo). See also: Liu, Rani & Mishra (2021), A novel Pythagorean fuzzy combined compromise solution framework for the assessment of medical waste treatment technology, Journal of Cleaner Production 292, 126047, doi:10.1016/j.jclepro.2021.126047.
DEMATEL¶
dematel ¶
DEMATEL: crisp and fuzzy (CFCS) influence analysis.
dematel takes a direct-relation matrix \(A\)
(how strongly factor \(i\) influences factor \(j\)) and returns the total-relation
matrix \(T = N(I - N)^{-1}\) with \(N = A/s\), the prominence \(D + R\) and relation
\(D - R\) of each factor (row sums \(D\) and column sums \(R\) of \(T\)), the cause and
effect groups and factor weights.
Fuzzy entries are defuzzified first. For triangular numbers the default is the
CFCS (converting fuzzy data into crisp scores) procedure of Opricovic & Tzeng
(2003), see cfcs; any other kind uses a score of
the kind.
References
Gabus & Fontela (1972), World Problems, an Invitation to Further Thought within the Framework of DEMATEL, Battelle Geneva Research Centre. Opricovic & Tzeng (2003), International Journal of Uncertainty, Fuzziness and Knowledge-Based Systems 11(5), 635-652 (CFCS). Wu & Lee (2007), Expert Systems with Applications 32(2), 499-507 (fuzzy DEMATEL with CFCS).
cfcs ¶
cfcs(matrices: Sequence[Sequence[Sequence[TFN]]]) -> list[list[float]]
Crisp values from one or more experts' triangular-number matrices (CFCS).
For cell \((i, j)\) with expert ratings \((l^k, m^k, r^k)\), \(k = 1..K\), let \(\min = \min_k l^k\), \(\Delta = \max_k r^k - \min\) and, for each expert, \(x_l = (l^k - \min)/\Delta\) (likewise \(x_m\), \(x_r\)), \(x^{ls} = x_m/(1 + x_m - x_l)\), \(x^{rs} = x_r/(1 + x_r - x_m)\), \(x = [x^{ls}(1 - x^{ls}) + x^{rs}x^{rs}]/(1 - x^{ls} + x^{rs})\) and \(z^k = \min + x\Delta\). The cell value is the mean of \(z^k\).
Scope. As in Opricovic & Tzeng (2003), the minimum and maximum are taken per cell across the experts, not over the whole matrix; each expert's crisp value is computed first and the experts are averaged after the CFCS step. With a single expert the cell's own \(l\) and \(r\) are used. If \(\Delta = 0\) then \(z = \min\).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrices
|
Sequence[Sequence[Sequence[TFN]]]
|
One square matrix of |
required |
Returns:
| Type | Description |
|---|---|
list[list[float]]
|
The averaged crisp matrix. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If an entry is not a |
ValueError
|
If there is no expert or the matrices are not square and equal. |
Examples:
>>> from bulanik import TFN
>>> z = TFN(0, 0, 0)
>>> round(cfcs([[[z, TFN(1, 2, 4)], [z, z]]])[0][1], 4)
2.2167
dematel ¶
dematel(matrix: Sequence[Sequence[float | FuzzyNumber]], factors: Sequence[str] | None = None, *, defuzzification: str | None = None, normalization: str = 'max_row_col', threshold: float | None = None, weighting: str = 'sqrt') -> DEMATELResult
DEMATEL on a crisp or fuzzy direct-relation matrix.
\(N = A/s\) with \(s = \max(\max_i\sum_j a_{ij}, \max_j\sum_i a_{ij})\)
("max_row_col") or \(s = \max_i\sum_j a_{ij}\) ("max_row"), then
\(T = N(I - N)^{-1}\); \(D\) and \(R\) are the row and column sums of \(T\). The
diagonal of \(A\) is used as given.
Weights: "sqrt" gives \(w_i \propto \sqrt{(D_i + R_i)^2 + (D_i - R_i)^2}\),
the length of the factor's vector in the cause-effect plane (a common
convention in DEMATEL-based weighting, not a formula of the original
method); "prominence" gives \(w_i \propto D_i + R_i\).
A factor is a cause when \(D_i - R_i > 10^{-12}\max_k(D_k + R_k)\) and an effect otherwise, so rounding noise around an exact zero net relation counts as an effect (as an exact zero does).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrix
|
Sequence[Sequence[float | FuzzyNumber]]
|
Square matrix, order at least 2, of finite non-negative numbers or of fuzzy numbers of one kind. |
required |
factors
|
Sequence[str] | None
|
Factor names; |
None
|
defuzzification
|
str | None
|
For crisp input only |
None
|
normalization
|
str
|
|
'max_row_col'
|
threshold
|
float | None
|
Cut-off for |
None
|
weighting
|
str
|
|
'sqrt'
|
Returns:
| Type | Description |
|---|---|
DEMATELResult
|
A |
DEMATELResult
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If entries are not numbers, or crisp and fuzzy are mixed. |
ValueError
|
If the matrix is not square or smaller than 2, has a
negative or non-finite entry (after defuzzification; a signed
default score suggests a positive-valued one), is all zero, an option
is unknown, |
Examples:
>>> from bulanik.mcdm import dematel
>>> r = dematel([[0, 2], [1, 0]])
>>> r.causes, r.effects, r.relation
(('F1',), ('F2',), {'F1': 1.0, 'F2': -1.0})
Best-worst helpers¶
bwm ¶
Best-worst method (BWM) helpers shared by the LumiX and sezgi bridges.
The best-worst method (Rezaei 2015) weights \(n\) criteria from two vectors of
pairwise comparisons: the best-to-others vector \(a_{Bj}\) (how much more
important the best criterion \(B\) is than \(j\)) and the others-to-worst vector
\(a_{jW}\) (how much more important \(j\) is than the worst criterion \(W\)), on a
1-9 scale. The weights solve a small optimisation problem, which needs a
solver, so the models live in bulanik.lumix (linear) and
bulanik.sezgi (nonlinear and fuzzy). This module holds the
solver-free parts: input validation and the consistency indices.
References
Rezaei (2015), Omega 53, 49-57; Guo & Zhao (2017), Knowledge-Based Systems 121, 23-31.
resolve_best_worst ¶
resolve_best_worst(best_to_others: Mapping[str, Any], others_to_worst: Mapping[str, Any], criteria: Sequence[str] | None = None, best: str | None = None, worst: str | None = None, *, one: Any = 1.0) -> tuple[list[str], str, str]
Validate BWM comparison vectors and resolve the best and worst criteria.
Both mappings must have exactly the keys of criteria. If best (worst)
is not given it is the unique key whose best-to-others (others-to-worst)
value equals one. The comparison of best over worst must agree in both
vectors: \(a_{BW}\) = best_to_others[worst] = others_to_worst[best].
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
best_to_others
|
Mapping[str, Any]
|
Comparison of the best criterion over each criterion. |
required |
others_to_worst
|
Mapping[str, Any]
|
Comparison of each criterion over the worst criterion. |
required |
criteria
|
Sequence[str] | None
|
Criterion names, fixing the order. Defaults to the order of
|
None
|
best
|
str | None
|
The best criterion. Inferred from the vector when |
None
|
worst
|
str | None
|
The worst criterion. Inferred from the vector when |
None
|
one
|
Any
|
The "equally important" value, |
1.0
|
Returns:
| Type | Description |
|---|---|
tuple[list[str], str, str]
|
A tuple |
Raises:
| Type | Description |
|---|---|
TypeError
|
If a comparison value is a |
ValueError
|
If criteria are duplicated or fewer than two, a vector's keys differ from the criteria, a crisp value is not finite or is below 1, the best or worst is unknown, cannot be inferred or is ambiguous, best equals worst, or the two \(a_{BW}\) values differ. |
Examples:
>>> from bulanik.mcdm.bwm import resolve_best_worst
>>> resolve_best_worst({"a": 1, "b": 2, "c": 8}, {"a": 8, "b": 4, "c": 1})
(['a', 'b', 'c'], 'a', 'c')
bwm_consistency_index ¶
bwm_consistency_index(a_bw: float) -> float
Rezaei's (2015) consistency index for a best-over-worst comparison.
The index is the largest \(\xi\) the model can reach for the given \(a = a_{BW}\): the root of \(\xi^2 - (1 + 2a)\xi + (a^2 - a) = 0\) that is at most \(a\),
It is 0, 0.44, 1.00, 1.63, 2.30, 3.00, 3.73, 4.47 and 5.23 for
\(a = 1, \dots, 9\). The consistency ratio of a solution is \(\xi / CI(a_{BW})\)
(BWMProblem); it does not exist for \(a = 1\).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
a_bw
|
float
|
The comparison of the best over the worst criterion, at least 1. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The index, never negative. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> from bulanik.mcdm.bwm import bwm_consistency_index
>>> [round(bwm_consistency_index(a), 2) for a in (1, 5, 9)]
[0.0, 2.3, 5.23]
References
Rezaei (2015), Omega 53, 49-57.
fuzzy_bwm_consistency_index ¶
fuzzy_bwm_consistency_index(a_bw: TFN) -> float
Guo & Zhao's (2017) consistency index for a fuzzy best-over-worst comparison.
With \(u\) the upper bound of \(a_{BW}\) it is the larger root of \(\xi^2 - (1 + 2u)\xi + (u^2 - u) = 0\),
which gives 3.00, 3.80, 5.29, 6.69 and 8.04 for the terms EI, WI, FI, VI and
AI of FUZZY_BWM_TFN. Unlike the crisp index
it is never 0.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
a_bw
|
TFN
|
The fuzzy comparison of the best over the worst criterion. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The index. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
Examples:
>>> from bulanik import TFN
>>> from bulanik.mcdm.bwm import fuzzy_bwm_consistency_index
>>> round(fuzzy_bwm_consistency_index(TFN(1, 1, 1)), 2)
3.0
References
Guo & Zhao (2017), Knowledge-Based Systems 121, 23-31.