Quickstart¶
Five short recipes. Every code block runs when the site is built, so the output
under each one is real. Recipe 5 (the lumix bridge) and the plot in recipe 1
(bulanik.plot) need their extras; everything else needs nothing
but bulanik.
1. Fuzzy numbers¶
Numbers are immutable, validated values. Operators call the default
("algebraic") operator family; add, mul and friends take an ops= name.
from bulanik import IFN, TFN
a, b = TFN(1, 2, 4), TFN(2, 3, 5)
print(a + b) # componentwise sum of the triangles
print(a * b) # min/max of the corner products, product of the peaks
print(a.alpha_cut(0.5)) # the interval where membership >= 0.5
print(round(a.score(), 4)) # crisp score for ranking (centroid by default)
x, y = IFN(0.6, 0.3), IFN(0.5, 0.4) # intuitionistic: membership + non-membership <= 1
print(x + y) # algebraic operators (default)
print(x.add(y, ops="einstein")) # Einstein operators
print(x.score(), x.score("normalized"))
try:
IFN(0.8, 0.5) # 0.8 + 0.5 > 1
except ValueError as error:
print("ValueError:", error)
TFN(l=3.0, m=5.0, u=9.0)
TFN(l=2.0, m=6.0, u=20.0)
(1.5, 3.0)
2.3333
IFN(mu=0.8, nu=0.12)
IFN(mu=0.8461538461538461, nu=0.08450704225352113)
0.3 0.65
ValueError: need mu^1 + nu^1 <= 1, got mu=0.8, nu=0.5 (sum 1.3)
A membership plot of two triangles, drawn with bulanik.plot (the
plot extra) at build time in both themes; the one matching your colour scheme
is shown:
2. Aggregate expert opinions¶
Experts write words; a LinguisticScale turns
them into fuzzy numbers; aggregate_experts
combines the [expert][alternative][criterion] table cell by cell.
from bulanik.aggregate import aggregate_experts, wa, wg
from bulanik.scales import EXAMPLE_IFN_5
words = [
[["H", "M"], ["L", "VL"]], # expert 1: two alternatives, two criteria
[["VH", "M"], ["M", "H"]], # expert 2
]
ratings = EXAMPLE_IFN_5.convert(words)
combined = aggregate_experts(ratings, [0.6, 0.4], method="wa")
for row in combined:
print([f"({n.mu:.3f}, {n.nu:.3f})" for n in row])
a, b = ratings[0][0][0], ratings[1][0][0]
for name, mean in [("wa", wa([a, b], [0.5, 0.5])), ("wg", wg([a, b], [0.5, 0.5]))]:
print(f"{name}: ({mean.mu:.4f}, {mean.nu:.4f})") # weighted average, weighted geometric mean
['(0.807, 0.115)', '(0.500, 0.400)']
['(0.388, 0.510)', '(0.420, 0.459)']
wa: (0.8268, 0.1000)
wg: (0.7937, 0.1282)
3. A Mamdani fuzzy controller¶
Rules are plain text. evaluate returns the defuzzified outputs; explain shows
why.
from bulanik.inference import MamdaniFIS
from bulanik.sets import LinguisticVariable, triangular
service = LinguisticVariable("service", (0, 10), {
"poor": triangular(0, 0, 5), "average": triangular(0, 5, 10), "good": triangular(5, 10, 10)})
food = LinguisticVariable("food", (0, 10), {
"rancid": triangular(0, 0, 5), "delicious": triangular(5, 10, 10)})
tip = LinguisticVariable("tip", (0, 30), {
"cheap": triangular(0, 5, 10), "average": triangular(10, 15, 20),
"generous": triangular(20, 25, 30)})
fis = MamdaniFIS([service, food], [tip], [
"IF service IS poor OR food IS rancid THEN tip IS cheap",
"IF service IS average THEN tip IS average",
"IF service IS good OR food IS delicious THEN tip IS generous",
])
inputs = {"service": 3, "food": 8}
print(f"tip = {fis.evaluate(inputs)['tip']:.2f}")
for activation in fis.explain(inputs).activations:
print(f" fired {activation.strength:.2f}: {activation.rule}")
tip = 15.86
fired 0.40: IF service IS poor OR food IS rancid THEN tip IS cheap
fired 0.60: IF service IS average THEN tip IS average
fired 0.60: IF service IS good OR food IS delicious THEN tip IS generous
4. Fuzzy TOPSIS¶
Triangular ratings and triangular weights, ranked with Chen's (2000) fuzzy TOPSIS.
from bulanik import TFN
from bulanik.mcdm import fuzzy_topsis
from bulanik.scales import EXAMPLE_TFN_7 as scale
criteria = ["quality", "service", "price"]
ratings = scale.convert([
["G", "VG", "F"], # alternative A on each criterion
["F", "G", "G"], # B
["MP", "F", "VG"], # C
])
weights = [TFN(0.7, 0.9, 1.0), TFN(0.5, 0.7, 0.9), TFN(0.3, 0.5, 0.7)]
result = fuzzy_topsis(ratings, weights, ["benefit", "benefit", "cost"],
alternatives=["A", "B", "C"], criteria=criteria)
print(result.ranking)
for name in result.ranking:
print(f" {name}: closeness {result.scores[name]:.4f}")
print(result.operators) # the names of the methods that were used
('A', 'B', 'C')
A: closeness 0.5910
B: closeness 0.4265
C: closeness 0.2921
{'normalization': 'linear_scale_chen2000', 'distance': 'vertex'}
5. Best-worst weights, then a ranking¶
The best-worst method (BWM) asks for two short comparison vectors. It needs a
solver, so it lives in the bridges: bwm_linear
(the lumix extra) solves it as a linear program. The result feeds straight
into topsis.
from bulanik.lumix import bwm_linear
from bulanik.mcdm import topsis
# quality is the best criterion, price the worst
best_to_others = {"quality": 1, "service": 2, "price": 4}
others_to_worst = {"quality": 4, "service": 3, "price": 1}
bwm = bwm_linear(best_to_others, others_to_worst)
print({k: round(v, 4) for k, v in bwm.weights.items()}, f"xi = {bwm.xi:.4f}")
data = [[8, 7, 120], [7, 5, 100], [9, 6, 110]] # rows: A, B, C
result = topsis(data, bwm, ["benefit", "benefit", "cost"],
alternatives=["A", "B", "C"], criteria=list(bwm.weights))
print(result.ranking)
print({k: round(v, 4) for k, v in result.scores.items()})
{'quality': 0.5625, 'service': 0.3125, 'price': 0.125} xi = 0.0625
('C', 'A', 'B')
{'A': 0.629, 'B': 0.1153, 'C': 0.7389}
Next¶
- Conventions: what the library promises about immutability, errors, tolerances and names.
- Choosing a method: which number type, inference system and MCDM method fits your problem.
- The user guide and the 24 examples.