Skip to content

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.

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. "ahp".

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. {"weights": "eigenvector"}.

consistency_ratio float | None

Saaty's consistency ratio \(CR = CI / RI\) for ahp and fuzzy_ahp; None for methods without one.

fuzzy_weights dict[str, FuzzyNumber] | None

The fuzzy weights before defuzzification (fuzzy_ahp only), else None.

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. "topsis".

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. {"normalization": "vector"}.

extras dict[str, Any]

Per-alternative intermediate values keyed by name, e.g. d_plus / d_minus for TOPSIS; each maps alternative to value.

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

"bwm_linear", "bwm_nonlinear" or "fuzzy_bwm".

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. bwm_linear stores the linear model's \(\xi^L\) itself; BWMProblem and FuzzyBWMProblem store \(\xi / CI\) (and None when \(CI = 0\) and the comparisons are inconsistent). Check operators["consistency"] before comparing.

best str

The best (most important) reference criterion.

worst str

The worst (least important) reference criterion.

operators dict[str, str]

Operator choices used, including "consistency".

fuzzy_weights dict[str, FuzzyNumber] | None

Fuzzy weights (FuzzyBWMProblem only), else None.

extras dict[str, Any]

Solver details (e.g. evals_used, best_f, or the LumiX model).

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

"dematel".

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 relation > 1e-12 * max(prominence), in input order.

effects tuple[str, ...]

The other factors (a zero net relation counts as an effect).

threshold float

Cut-off used for relations.

relations tuple[tuple[str, str, float], ...]

(from, to, t_ij) for every off-diagonal \(t_{ij}\) above the threshold, sorted row-major (by from then to in input order).

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; C1..Cn by default.

None

Returns:

Type Description
WeightingResult
WeightingResult

consistency_ratio set.

Raises:

Type Description
TypeError

If an entry is a bool or str instead of a number.

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), criteria has the wrong length or duplicates, or power iteration does not converge.

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 TFN with \(l > 0\), reciprocal (\(\tilde a_{ji} = 1/\tilde a_{ij}\), to relative tolerance 1e-9), of order at most 10.

required
criteria Sequence[str] | None

Criterion names; C1..Cn by default.

None
defuzzification str

Score name of TFN used to defuzzify ("centroid", "graded_mean" or "mean").

'centroid'

Returns:

Type Description
WeightingResult
WeightingResult

fuzzy_weights and consistency_ratio.

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 defuzzification is unknown.

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; C1..Cn by default.

None
aggregation str

"wg" (default) or "wa".

'wg'
ops str

Operator set of the number kind.

'algebraic'
score str | None

Score name of the kind; None is its default. Orthopair and interval-valued kinds default to the signed "difference" score, which is not a ratio scale: whenever every row score happens to be positive it is used as one, with no error, and the weights can then differ a lot from those of score="normalized". Use "normalized" for these kinds.

None
consistency_matrix Sequence[Sequence[float]] | None

Optional crisp matrix valid for ahp whose CR is reported.

None

Returns:

Type Description
WeightingResult

A WeightingResult whose

WeightingResult

fuzzy_weights hold the aggregated row numbers. operators["consistency"]

WeightingResult

names how the consistency ratio was obtained: "saaty_ri_on_scores",

WeightingResult

"saaty_ri_on_given_matrix" (with consistency_matrix) or "none".

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, aggregation, ops or score is unknown, a row score is not positive, or consistency_matrix is invalid.

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 IVQROFN numbers of one kind and rung, of order 2 to 10.

required
criteria Sequence[str] | None

Criterion names; C1..Cn by default.

None
method str | None

"multiplicative" or "possibility"; None picks "possibility" for \(q = 1\) and "multiplicative" otherwise.

None
consistency_matrix Sequence[Sequence[float]] | None

Optional crisp matrix valid for ahp whose CR is reported.

None

Returns:

Type Description
WeightingResult

A WeightingResult. extras

WeightingResult

holds scored_matrix, consistency_ratio and consistency (how the

WeightingResult

ratio was obtained: "saaty_ri_on_given_matrix" with consistency_matrix,

WeightingResult

"saaty_ri_on_scores" when the scored matrix is positive, else "none"

WeightingResult

and a consistency_ratio of None), and for the

WeightingResult

multiplicative method the \(n \times n\) lists d_lower, d_upper,

WeightingResult

s_lower, s_upper, h, t; for the possibility method

WeightingResult

interval_weights (a list of (lower, upper)) and possibility

WeightingResult

(\(p_{ij}\)).

Raises:

Type Description
TypeError

If an entry is not an IVQROFN or the kinds or rungs are mixed.

ValueError

If the matrix is not square, has fewer than 2 or more than 10 rows, method is unknown, "possibility" is asked for \(q \ne 1\), or consistency_matrix is invalid.

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 for the kind's default. A given name must be a known score name even when every input is crisp (it is then unused).

None

Returns:

Type Description
WeightingResult
WeightingResult

operators["score"] is the resolved score, or "crisp".

Raises:

Type Description
TypeError

If a comparative importance is a bool or str instead of a number.

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 score is unknown (for fuzzy numbers a negative score can be avoided with score="normalized").

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; C1..Cn by default.

None

Returns:

Type Description
WeightingResult

Raises:

Type Description
TypeError

If a matrix entry is a bool or str instead of a number.

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 TFN.

required
criteria Sequence[str]

The \(n\) criterion names, in the order that was judged.

required
score str | None

How to score fuzzy weights: a TFN score name ("graded_mean" by default). A given name must be a known score name even when every input is crisp (it is then unused).

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 relative_importance.

None

Returns:

Type Description
WeightingResult
WeightingResult

fuzzy_weights is set for TFN input. operators["score"] is the

WeightingResult

resolved score name, or "crisp"; operators["inverse"] is "none" or

WeightingResult

"mean_of_forward_and_inverse".

Raises:

Type Description
TypeError

If a judgement is a bool, a str or a fuzzy number that is not a TFN.

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 relative_importance and s_inverse), a judgement is not finite, some \(s_j\) or \(s'_j\) is not positive (a crisp \(s_j \le 0\), or a TFN with \(s^l \le 0\)), some \(k_j\) or \(k'_j\) is not positive (a crisp \(s_j \ge 2\), or a TFN with \(s^u \ge 2\)), or score is unknown.

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; C1..Cn by default.

None
criteria_types Sequence[str] | None

"benefit" or "cost" per criterion; all benefit by default.

None

Returns:

Type Description
WeightingResult
WeightingResult

extras `{"std": {c: sigma}, "correlation": {c: {c2: rho}},

WeightingResult

"information": {c: C}}.stdandinformation` are 0 for a constant

WeightingResult

column, which is absent from correlation.

Raises:

Type Description
TypeError

If a matrix entry is a bool or str.

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

\[C_i = \frac{D_i^-}{D_i^+ + D_i^-} \in [0, 1],\]

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 WeightingResult / BWMResult. They must sum to 1 unless normalize_weights is set.

required
criteria_types Sequence[str]

"benefit" (larger is better) or "cost" (smaller is better) for each criterion.

required
alternatives Sequence[str] | None

Alternative names; A1..Am by default.

None
criteria Sequence[str] | None

Criterion names; if None they are the keys of a mapping or result weights (in order), else C1..Cn.

None
normalize_weights bool

Rescale non-negative weights to sum to 1 instead of requiring it.

False

Returns:

Type Description
RankingResult

A RankingResult with

RankingResult

operators == {"normalization": "vector", "distance": "euclidean"}.

Raises:

Type Description
TypeError

If a matrix entry or a weight is a bool or str instead of a number.

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 "benefit" or "cost", or the weights are negative or (without normalize_weights) do not sum to 1.

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

\[S_i = \sum_j w_j \frac{|f_j^* - x_{ij}|}{|f_j^* - f_j^-|}, \qquad R_i = \max_j w_j \frac{|f_j^* - x_{ij}|}{|f_j^* - f_j^-|},\]

(criteria with \(f_j^* = f_j^-\) are skipped) and the compromise index is

\[Q_i = v\,\frac{S_i - S^*}{S^- - S^*} + (1 - v)\,\frac{R_i - R^*}{R^- - R^*},\]

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 WeightingResult / BWMResult. They must sum to 1 unless normalize_weights is set.

required
criteria_types Sequence[str]

"benefit" (larger is better) or "cost" (smaller is better) for each criterion.

required
alternatives Sequence[str] | None

Alternative names; A1..Am by default.

None
criteria Sequence[str] | None

Criterion names; if None they are the keys of a mapping or result weights (in order), else C1..Cn.

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 with

RankingResult

operators == {"normalization": "linear_range", "v": "0.5"} (the

RankingResult

value of v as text).

Raises:

Type Description
TypeError

If a matrix entry or a weight is a bool or str instead of a number.

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 "benefit" or "cost", or the weights are negative or (without normalize_weights) do not sum to 1, or v is outside [0, 1] or not finite.

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):

\[Q_i = \lambda \sum_j w_j \bar x_{ij} + (1 - \lambda) \prod_j \bar x_{ij}^{\,w_j},\]

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 WeightingResult / BWMResult. They must sum to 1 unless normalize_weights is set.

required
criteria_types Sequence[str]

"benefit" (larger is better) or "cost" (smaller is better) for each criterion.

required
alternatives Sequence[str] | None

Alternative names; A1..Am by default.

None
criteria Sequence[str] | None

Criterion names; if None they are the keys of a mapping or result weights (in order), else C1..Cn.

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 with

RankingResult

operators == {"normalization": "linear_max", "lambda": "0.5"} (the

RankingResult

value of lam as text).

Raises:

Type Description
TypeError

If a matrix entry or a weight is a bool or str instead of a number.

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 "benefit" or "cost", or the weights are negative or (without normalize_weights) do not sum to 1, an entry is not strictly positive (for fuzzy scores try score="normalized"), or lam is outside [0, 1].

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

\[PDA_{ij} = \frac{\max(0, x_{ij} - \bar x_j)}{\bar x_j}, \qquad NDA_{ij} = \frac{\max(0, \bar x_j - x_{ij})}{\bar x_j},\]

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 WeightingResult / BWMResult. They must sum to 1 unless normalize_weights is set.

required
criteria_types Sequence[str]

"benefit" (larger is better) or "cost" (smaller is better) for each criterion.

required
alternatives Sequence[str] | None

Alternative names; A1..Am by default.

None
criteria Sequence[str] | None

Criterion names; if None they are the keys of a mapping or result weights (in order), else C1..Cn.

None
normalize_weights bool

Rescale non-negative weights to sum to 1 instead of requiring it.

False

Returns:

Type Description
RankingResult

A RankingResult with

RankingResult

operators == {"normalization": "average_distance"}.

Raises:

Type Description
TypeError

If a matrix entry or a weight is a bool or str instead of a number.

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 "benefit" or "cost", or the weights are negative or (without normalize_weights) do not sum to 1, or a column average is not positive.

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

\[E_i = \sqrt{\sum_j (r_{ij} - ns_j)^2}, \qquad T_i = \sum_j |r_{ij} - ns_j|.\]

The relative assessment is

\[h_{ik} = (E_i - E_k) + \psi(E_i - E_k)\,(T_i - T_k), \qquad \psi(x) = \begin{cases} 1 & |x| \ge \tau \\ 0 & |x| < \tau \end{cases}\]

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 WeightingResult / BWMResult. They must sum to 1 unless normalize_weights is set.

required
criteria_types Sequence[str]

"benefit" (larger is better) or "cost" (smaller is better) for each criterion.

required
alternatives Sequence[str] | None

Alternative names; A1..Am by default.

None
criteria Sequence[str] | None

Criterion names; if None they are the keys of a mapping or result weights (in order), else C1..Cn.

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" (taxicab term when \(|\Delta E| \ge \tau\), as printed) or "tables" (when \(|\Delta E| < \tau\), as the paper's tables); see above.

'equation'

Returns:

Type Description
RankingResult

A RankingResult with

RankingResult

operators == {"normalization": "linear_max", "tau": "0.02", "psi": "equation"}

RankingResult

(the value of tau as text).

Raises:

Type Description
TypeError

If a matrix entry, a weight or tau is a bool or str instead of a number.

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 "benefit" or "cost", the weights are negative or (without normalize_weights) do not sum to 1, an entry is negative, a cost column has a zero, a benefit column is all zeros (for fuzzy scores try score="normalized"), a column spans so many orders of magnitude that an entry underflows, tau is outside [0, 1] or not finite, or psi is not a valid name.

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

\[Q_i = S^+_i + \frac{\sum_k S^-_k}{S^-_i \sum_k 1 / S^-_k},\]

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 WeightingResult / BWMResult. They must sum to 1 unless normalize_weights is set.

required
criteria_types Sequence[str]

"benefit" (larger is better) or "cost" (smaller is better) for each criterion.

required
alternatives Sequence[str] | None

Alternative names; A1..Am by default.

None
criteria Sequence[str] | None

Criterion names; if None they are the keys of a mapping or result weights (in order), else C1..Cn.

None
normalize_weights bool

Rescale non-negative weights to sum to 1 instead of requiring it.

False

Returns:

Type Description
RankingResult

A RankingResult with

RankingResult

operators == {"normalization": "sum"}.

Raises:

Type Description
TypeError

If a matrix entry or a weight is a bool or str instead of a number.

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 "benefit" or "cost", the weights are negative or (without normalize_weights) do not sum to 1, an entry is negative, a column sum is not positive, or an alternative has \(S^-_i = 0\) while a cost criterion carries weight (the error names it).

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

\[f(K^-_i) = \frac{K^+_i}{K^+_i + K^-_i}, \qquad f(K^+_i) = \frac{K^-_i}{K^+_i + K^-_i},\]

and the score, larger is better, is

\[f(K_i) = \frac{K^+_i + K^-_i}{1 + \frac{1 - f(K^+_i)}{f(K^+_i)} + \frac{1 - f(K^-_i)}{f(K^-_i)}}.\]

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 with

RankingResult

operators == {"normalization": "ideal_ratio"}.

Raises:

Type Description
TypeError

If a matrix entry or a weight is a bool or str instead of a number.

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 "benefit" or "cost", the weights are negative or (without normalize_weights) do not sum to 1, an entry is not strictly positive (for fuzzy scores try score="normalized"), or the ratios are so extreme that \(S_{AAI}\) underflows to 0.

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

\[S_i = \sum_j w_j r_{ij}, \qquad P_i = \sum_{j:\, w_j > 0} r_{ij}^{\,w_j},\]

(a criterion with weight 0 is left out of both). Three appraisal scores combine them,

\[k_{ia} = \frac{P_i + S_i}{\sum_i (P_i + S_i)}, \qquad k_{ib} = \frac{S_i}{\min_i S_i} + \frac{P_i}{\min_i P_i}, \qquad k_{ic} = \frac{\lambda S_i + (1 - \lambda) P_i}{\lambda \max_i S_i + (1 - \lambda) \max_i P_i},\]

and the score, larger is better, is

\[k_i = (k_{ia}\, k_{ib}\, k_{ic})^{1/3} + \tfrac13 (k_{ia} + k_{ib} + k_{ic}).\]

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 WeightingResult / BWMResult. They must sum to 1 unless normalize_weights is set.

required
criteria_types Sequence[str]

"benefit" (larger is better) or "cost" (smaller is better) for each criterion.

required
alternatives Sequence[str] | None

Alternative names; A1..Am by default.

None
criteria Sequence[str] | None

Criterion names; if None they are the keys of a mapping or result weights (in order), else C1..Cn.

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 with

RankingResult

operators == {"normalization": "min_max", "lambda": "0.5"} (the value

RankingResult

of lam as text).

Raises:

Type Description
TypeError

If a matrix entry, a weight or lam is a bool or str instead of a number.

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 "benefit" or "cost", the weights are negative or (without normalize_weights) do not sum to 1, lam is outside [0, 1] or not finite, or an alternative is worst on every weighted criterion (the error names it).

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:

\[u^{sd}_i = \sum_{\Omega_{\max}} w_j r_{ij} - \sum_{\Omega_{\min}} w_j r_{ij}, \qquad u^{pd}_i = \prod_{\Omega_{\max}} r_{ij}^{w_j} - \prod_{\Omega_{\min}} r_{ij}^{w_j},\]
\[u^{sr}_i = \frac{\sum_{\Omega_{\max}} w_j r_{ij}}{\sum_{\Omega_{\min}} w_j r_{ij}}, \qquad u^{pr}_i = \frac{\prod_{\Omega_{\max}} r_{ij}^{w_j}}{\prod_{\Omega_{\min}} r_{ij}^{w_j}}.\]

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 with

RankingResult

operators == {"normalization": "linear_max", "product": "power"}.

Raises:

Type Description
TypeError

If a matrix entry or a weight is a bool or str instead of a number.

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 "benefit" or "cost", the weights are negative or (without normalize_weights) do not sum to 1, an entry is not strictly positive (for fuzzy scores try score="normalized"), product is not a valid name, or the product ratio overflows.

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

\[c_{kl} = \sum_{j \in C_{kl}} w_j, \qquad d_{kl} = \frac{\max_{j \in D_{kl}} |v_{kj} - v_{lj}|}{\max_j |v_{kj} - v_{lj}|},\]

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

\[e_{kl} = [c_{kl} \ge \bar c]\,[d_{kl} \le \bar d],\]

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,

\[C_k = \sum_{l \ne k} c_{kl} - \sum_{l \ne k} c_{lk}, \qquad D_k = \sum_{l \ne k} d_{kl} - \sum_{l \ne k} d_{lk},\]

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,

\[S_k = C_k - D_k,\]

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 WeightingResult / BWMResult. They must sum to 1 unless normalize_weights is set.

required
criteria_types Sequence[str]

"benefit" (larger is better) or "cost" (smaller is better) for each criterion.

required
alternatives Sequence[str] | None

Alternative names; A1..Am by default.

None
criteria Sequence[str] | None

Criterion names; if None they are the keys of a mapping or result weights (in order), else C1..Cn.

None
normalize_weights bool

Rescale non-negative weights to sum to 1 instead of requiring it.

False
cost str

"reverse" or "inverse"; see above.

'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 with

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 bool or str instead of a number.

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 "benefit" or "cost", the weights are negative or (without normalize_weights) do not sum to 1, a threshold is outside [0, 1] or not finite, cost is not a valid name, or (cost="inverse") a cost column has an entry that is not strictly positive.

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.

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][alternative][criterion].

expert_weights tuple[float, ...] | None

Expert weights summing to 1, or None for equal weights.

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 LinguisticScale that maps each term to a fuzzy number.

required
alternatives Sequence[str]

Alternative names.

required
criteria Sequence[str]

Criterion names.

required
experts_terms Sequence[Sequence[Sequence[str]]]

Term names indexed [expert][alternative][criterion].

required
expert_weights Sequence[float] | None

Expert weights summing to 1, or None for equal.

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 WeightingResult or BWMResult).

required
criteria_types Sequence[str]

"benefit" or "cost" for each criterion.

required
method str

"topsis", "vikor", "waspas", "edas", "codas", "copras", "marcos", "cocoso", "wisp", "electre", "fuzzy_codas", "fuzzy_waspas" or "fuzzy_cocoso".

'topsis'
aggregation str

"wa" or "wg".

'wa'
ops str

Operator set of the fuzzy number kind ("algebraic" for TFN).

'algebraic'
score str | None

Score name for defuzzification, or None for the kind's default. The ratio methods (waspas, codas, copras, marcos, cocoso, wisp) need strictly positive scores; the orthopair kinds default to a signed score, so pass "normalized" for them. Kinds without that score (for example IVNeutrosophicFN) get a "use a positive-valued score" hint instead. For the fuzzy methods the name goes to the method unchanged; with None, fuzzy_cocoso uses "normalized" when the kind has it (else the kind's default) and fuzzy_codas / fuzzy_waspas the kind's default.

None
**method_kwargs Any

Passed to the crisp method, e.g. v= for VIKOR, lam= for WASPAS and CoCoSo, tau= and psi= for CODAS, product= for WISP, c_threshold= and d_threshold= for ELECTRE, normalize_weights= (and the same keywords for the fuzzy methods).

{}

Returns:

Type Description
RankingResult

Raises:

Type Description
ValueError

If method, aggregation, ops or score is unknown, or the crisp (or fuzzy) method rejects its input.

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 TFN ratings. Benefit ratings need \(l \ge 0\) and a positive upper bound somewhere in the column; cost ratings need \(l > 0\).

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 WeightingResult / BWMResult.

required
criteria_types Sequence[str]

"benefit" or "cost" for each criterion.

required
alternatives Sequence[str] | None

Alternative names; A1..Am by default.

None
criteria Sequence[str] | None

Criterion names; if None, the keys (in order) of a mapping or result weights, else C1..Cn. They appear in error messages and select the mapping entries.

None

Returns:

Type Description
RankingResult

A RankingResult with

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\),

\[E_i = \sqrt{\sum_j d_E(\tilde u_{ij}, \tilde{ns}_j)^2}, \qquad T_i = \sum_j d_H(\tilde u_{ij}, \tilde{ns}_j).\]

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),

\[h_{ik} = (E_i - E_k) + \psi(E_i - E_k)\,(T_i - T_k), \qquad H_i = \sum_k h_{ik},\]

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 euclidean and hamming distances.

required
weights _Weights

Crisp criteria weights: a sequence in criteria order, a mapping by criterion name, or a WeightingResult / BWMResult. They must sum to 1 unless normalize_weights is set.

required
criteria_types Sequence[str]

"benefit" or "cost" for each criterion.

required
alternatives Sequence[str] | None

Alternative names; A1..Am by default.

None
criteria Sequence[str] | None

Criterion names; if None they are the keys of a mapping or result weights (in order), else C1..Cn.

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" or "einstein".

'algebraic'
score str | None

Score that picks the negative-ideal numbers; None for the kind's default.

None
tau float

Threshold of \(\psi\) in [0, 1].

0.02
psi str

"equation" or "tables", as for crisp CODAS.

'equation'

Returns:

Type Description
RankingResult

A RankingResult with

RankingResult

operators == {"ops": ..., "score": ..., "tau": "0.02", "psi": "equation"}.

Raises:

Type Description
TypeError

If an entry is not a fuzzy number, the kinds (or rungs) differ, a cost criterion meets a kind without complement(), the kind lacks a Euclidean or Hamming distance, or a weight or tau is a bool or str.

ValueError

If the matrix is empty or ragged, names, types or weights do not match (as for the crisp methods), ops, score or psi is unknown, tau is outside [0, 1], or the kind cannot scale a rating.

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

\[\tilde Q^{(1)}_i = \bigoplus_j w_j \tilde x_{ij}, \qquad \tilde Q^{(2)}_i = \bigotimes_j \tilde x_{ij}^{\,w_j},\]

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's scale and add, 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 WeightingResult / BWMResult. They must sum to 1 unless normalize_weights is set.

required
criteria_types Sequence[str]

"benefit" or "cost" for each criterion.

required
alternatives Sequence[str] | None

Alternative names; A1..Am by default.

None
criteria Sequence[str] | None

Criterion names; if None they are the keys of a mapping or result weights (in order), else C1..Cn.

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" or "einstein".

'algebraic'
score str | None

Score of the final numbers; None for the kind's default.

None
lam float

Balance \(\lambda \in [0, 1]\) between the WSM and the WPM.

0.5
combine str

"fuzzy" (combine the numbers, then score) or "scores" (score, then combine); see above.

'fuzzy'

Returns:

Type Description
RankingResult

A RankingResult with

RankingResult

operators == {"ops": ..., "score": ..., "lambda": "0.5", "combine": "fuzzy"}.

Raises:

Type Description
TypeError

If an entry is not a fuzzy number, the kinds (or rungs) differ, a cost criterion meets a kind without complement(), or a weight or lam is a bool or str.

ValueError

If the matrix is empty or ragged, names, types or weights do not match (as for the crisp methods), ops, score or combine is unknown, lam is outside [0, 1], or the kind rejects an operation (e.g. wg of a triangular number with a negative point).

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

\[\tilde S_i = \bigoplus_j w_j \tilde x_{ij}, \qquad \tilde P_i = \bigotimes_j \tilde x_{ij}^{\,w_j}\]

(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),

\[k_{ia} = \frac{P_i + S_i}{\sum_i (P_i + S_i)}, \qquad k_{ib} = \frac{S_i}{\min_i S_i} + \frac{P_i}{\min_i P_i}, \qquad k_{ic} = \frac{\lambda S_i + (1 - \lambda) P_i}{\lambda \max_i S_i + (1 - \lambda) \max_i P_i},\]
\[k_i = (k_{ia}\, k_{ib}\, k_{ic})^{1/3} + \tfrac13 (k_{ia} + k_{ib} + k_{ic}),\]

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 WeightingResult / BWMResult. They must sum to 1 unless normalize_weights is set.

required
criteria_types Sequence[str]

"benefit" or "cost" for each criterion.

required
alternatives Sequence[str] | None

Alternative names; A1..Am by default.

None
criteria Sequence[str] | None

Criterion names; if None they are the keys of a mapping or result weights (in order), else C1..Cn.

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" or "einstein".

'algebraic'
score str | None

A non-negative score; None for "normalized" where the kind has it, else the kind's default.

None
lam float

Balance \(\lambda \in [0, 1]\) in \(k_{ic}\).

0.5

Returns:

Type Description
RankingResult

A RankingResult with

RankingResult

operators == {"ops": ..., "score": ..., "lambda": "0.5"}.

Raises:

Type Description
TypeError

If an entry is not a fuzzy number, the kinds (or rungs) differ, a cost criterion meets a kind without complement(), or a weight or lam is a bool or str.

ValueError

If the matrix is empty or ragged, names, types or weights do not match (as for the crisp methods), ops or score is unknown, lam is outside [0, 1], a score is negative, or an alternative's \(S_i\) or \(P_i\) is 0 (the error names it).

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 TFN per expert, all of the same order.

required

Returns:

Type Description
list[list[float]]

The averaged crisp matrix.

Raises:

Type Description
TypeError

If an entry is not a TFN.

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; F1..Fn by default.

None
defuzzification str | None

For crisp input only None or "crisp" are valid (anything else raises). For fuzzy input, "cfcs" (triangular numbers only; the default for them) or a score name of the kind (the kind's default score otherwise).

None
normalization str

"max_row_col" or "max_row".

'max_row_col'
threshold float | None

Cut-off for relations; the mean of all \(n^2\) entries of \(T\) when None.

None
weighting str

"sqrt" or "prominence".

'sqrt'

Returns:

Type Description
DEMATELResult

A DEMATELResult; its

DEMATELResult

relations are sorted row-major.

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, factors is wrong, or \(I - N\) is singular or ill-conditioned (the normalised matrix has spectral radius 1, or numerically 1 so that the largest total relation exceeds \(10^8\) times the largest normalised direct relation; e.g. equal or nearly equal row and column sums).

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 best_to_others.

None
best str | None

The best criterion. Inferred from the vector when None.

None
worst str | None

The worst criterion. Inferred from the vector when None.

None
one Any

The "equally important" value, 1.0 for crisp comparisons and TFN(1, 1, 1) for fuzzy ones.

1.0

Returns:

Type Description
tuple[list[str], str, str]

A tuple (criteria, best, worst); criteria is a list.

Raises:

Type Description
TypeError

If a comparison value is a bool or str instead of a number.

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\),

\[CI(a) = \frac{(1 + 2a) - \sqrt{8a + 1}}{2}.\]

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 a_bw is not finite or is below 1.

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\),

\[CI(u) = \frac{(1 + 2u) + \sqrt{8u + 1}}{2},\]

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 a_bw is not a TFN.

ValueError

If a_bw.u is below 1.

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.