Skip to content

Example 24: Ranking on fuzzy numbers

Two experts' intuitionistic ratings of four suppliers go through rank_fuzzy, score-first with crisp CODAS, WASPAS and CoCoSo, and score-last with their fuzzy versions.

Source

examples/24_fuzzy_ranking.py
"""Score first or score last: CODAS, WASPAS and CoCoSo on an intuitionistic matrix.

Two experts rate four suppliers on price (a cost), quality and delivery with
intuitionistic numbers (mu, nu). `rank_fuzzy` aggregates them and then either

- scores every aggregated number first and ranks the crisp matrix with the
  crisp method (`method="codas"`, `"waspas"`, `"cocoso"`), or
- hands the aggregated *fuzzy* matrix to the fuzzy method
  (`method="fuzzy_codas"`, `"fuzzy_waspas"`, `"fuzzy_cocoso"`), which weights,
  sums and multiplies the numbers with intuitionistic arithmetic and scores
  only at the end.

The crisp ratio methods need positive entries, so every run uses
`score="normalized"`, (1 + mu - nu) / 2. The fuzzy WASPAS is run both ways of
combining its weighted sum and weighted product: as numbers
(`combine="fuzzy"`) and as scores (`combine="scores"`).
"""

from bulanik import IFN
from bulanik.mcdm import DecisionMatrix, rank_fuzzy

alternatives = ["Atlas", "Borealis", "Cedar", "Delta"]
criteria = ["price", "quality", "delivery"]
types = ["cost", "benefit", "benefit"]
weights = {"price": 0.40, "quality": 0.35, "delivery": 0.25}

# ratings [expert][alternative][criterion]; for price a high mu means "expensive"
expert1 = [
    [IFN(0.60, 0.30), IFN(0.70, 0.20), IFN(0.60, 0.30)],
    [IFN(0.40, 0.50), IFN(0.60, 0.30), IFN(0.40, 0.50)],
    [IFN(0.50, 0.40), IFN(0.80, 0.10), IFN(0.50, 0.40)],
    [IFN(0.70, 0.20), IFN(0.50, 0.40), IFN(0.80, 0.10)],
]
expert2 = [
    [IFN(0.50, 0.40), IFN(0.70, 0.20), IFN(0.70, 0.20)],
    [IFN(0.40, 0.40), IFN(0.50, 0.40), IFN(0.50, 0.40)],
    [IFN(0.60, 0.30), IFN(0.70, 0.20), IFN(0.40, 0.50)],
    [IFN(0.80, 0.10), IFN(0.60, 0.30), IFN(0.70, 0.20)],
]
matrix = DecisionMatrix(alternatives, criteria, [expert1, expert2])

runs = {
    "codas (score first)": dict(method="codas"),
    "fuzzy_codas": dict(method="fuzzy_codas"),
    "waspas (score first)": dict(method="waspas"),
    "fuzzy_waspas, fuzzy": dict(method="fuzzy_waspas", combine="fuzzy"),
    "fuzzy_waspas, scores": dict(method="fuzzy_waspas", combine="scores"),
    "cocoso (score first)": dict(method="cocoso"),
    "fuzzy_cocoso": dict(method="fuzzy_cocoso"),
}
results = {name: rank_fuzzy(matrix, weights, types, score="normalized", **kw)
           for name, kw in runs.items()}

print("fuzzy ranking table (1 = best)")
print(f"{'run':<22}" + "".join(f"{a:>10}" for a in alternatives))
for name, r in results.items():
    position = {a: i + 1 for i, a in enumerate(r.ranking)}
    print(f"{name:<22}" + "".join(f"{position[a]:>10}" for a in alternatives))

print()
print("scores (larger is better; compare within a method, not across methods)")
for name, r in results.items():
    print(f"{name:<22}" + "".join(f"{r.scores[a]:>10.4f}" for a in alternatives))

fw = results["fuzzy_waspas, fuzzy"]
print()
print("fuzzy_waspas keeps the numbers: Q1 (weighted sum) and Q2 (weighted product) of Cedar")
q1, q2 = fw.extras["Q1"]["Cedar"], fw.extras["Q2"]["Cedar"]
print(f"  Q1 = IFN({q1.mu:.4f}, {q1.nu:.4f}), Q2 = IFN({q2.mu:.4f}, {q2.nu:.4f})")

assert all(sorted(r.ranking) == sorted(alternatives) for r in results.values())

Output

Run when this page is built:

fuzzy ranking table (1 = best)
run                        Atlas  Borealis     Cedar     Delta
codas (score first)            2         1         3         4
fuzzy_codas                    3         4         1         2
waspas (score first)           1         3         2         4
fuzzy_waspas, fuzzy            1         3         2         4
fuzzy_waspas, scores           1         3         2         4
cocoso (score first)           1         3         2         4
fuzzy_cocoso                   1         3         2         4

scores (larger is better; compare within a method, not across methods)
codas (score first)       0.1357    0.1816    0.0572   -0.3746
fuzzy_codas              -0.0124   -0.0220    0.0398   -0.0055
waspas (score first)      0.8584    0.8099    0.8150    0.7397
fuzzy_waspas, fuzzy       0.6020    0.5464    0.5793    0.4910
fuzzy_waspas, scores      0.6009    0.5464    0.5775    0.4853
cocoso (score first)      3.4985    1.6353    2.7110    1.3283
fuzzy_cocoso              2.1570    1.9680    2.0701    1.7275

fuzzy_waspas keeps the numbers: Q1 (weighted sum) and Q2 (weighted product) of Cedar
  Q1 = IFN(0.5565, 0.3253), Q2 = IFN(0.4864, 0.4075)

What to notice

  • WASPAS and CoCoSo give the same order score-first and score-last here. CODAS does not: score-first it picks Borealis, score-last Cedar. The crisp method turns a cost into a ratio of scores and measures distances on the scored matrix; fuzzy_codas takes the complement of the cost rating and measures distances between the weighted numbers.
  • combine="fuzzy" and combine="scores" differ from the third decimal on, because a fuzzy mix of two close numbers scores close to the mix of their scores. They agree on the order in this example, though they need not in general.
  • Scores from different methods live on different scales, so compare the ranks across methods and the scores only within one method.
  • extras["Q1"] and extras["Q2"] keep the weighted sum and the weighted product as IFN numbers.

Go further

Run it yourself: python examples/24_fuzzy_ranking.py.