Skip to content

bulanik.sezgi

sezgi

sezgi bridge: nonlinear and fuzzy BWM, and an ANFIS trainer, as sezgi problems.

Needs the sezgi extra (pip install "bulanik[sezgi]"). sezgi is a metaheuristic optimisation library; here its problems are minimised without derivatives.

sezgi has no single-objective constraint API, so every constraint is a penalty inside evaluate (minimised). The default algorithm everywhere is sezgi.DifferentialEvolution(pop_size=40, variant="jde"); pass any other sezgi algorithm as algorithm=. Runs are deterministic for a fixed seed and budget (the number of objective evaluations).

The two consistency_ratio fields differ

In the BWMResult returned by BWMProblem.solve the consistency_ratio is Rezaei's (2015) ratio \(\xi / CI(a_{BW})\), where \(CI\) is bwm_consistency_index of the best-over-worst comparison \(a_{BW}\). It lies in \([0, 1]\) at the optimum and is analogous to (not interchangeable with) AHP's consistency ratio. When \(a_{BW} = 1\) the index is 0: the ratio is then 0.0 if \(\xi \le 10^{-6}\) (consistent) and None otherwise, with an explanation in extras["consistency_note"]. FuzzyBWMProblem.solve reports \(k / CI\) with Guo & Zhao's fuzzy_bwm_consistency_index, which is never 0. By contrast, bwm_linear in bulanik.lumix stores the linear model's optimum \(\xi^L\) itself under the same field name. Read operators["consistency"] ("rezaei2015_ci", "guo_zhao2017_ci" or "xi_L") before comparing results.

Examples:

>>> from bulanik.sezgi import BWMProblem
>>> problem = BWMProblem({"price": 1, "quality": 2, "speed": 8},
...                      {"price": 8, "quality": 4, "speed": 1})
>>> r = problem.solve(budget=3000, seed=1)
>>> {k: round(v, 2) for k, v in r.weights.items()}
{'price': 0.62, 'quality': 0.31, 'speed': 0.08}

BWMProblem

BWMProblem(best_to_others: Mapping[str, float], others_to_worst: Mapping[str, float], criteria: Sequence[str] | None = None, *, best: str | None = None, worst: str | None = None, penalty: float = 10.0)

Bases: Problem

Rezaei's (2015, Omega 53:49-57) nonlinear best-worst method.

With best criterion \(B\), worst criterion \(W\) and comparisons \(a_{Bj}\), \(a_{jW}\) the model is

\[\min \xi \quad \text{s.t.} \quad \left|\frac{w_B}{w_j} - a_{Bj}\right| \le \xi\ (j \ne B),\; \left|\frac{w_j}{w_W} - a_{jW}\right| \le \xi\ (j \ne W),\; \textstyle\sum_j w_j = 1.\]

evaluate(x) is the largest of those deviations plus penalty * |sum(x) - 1|; the ratios are scale-invariant, so the penalty only fixes the scale. Search space: Float(1e-6, 1, n).

solve().consistency_ratio is \(\xi / CI\), not \(\xi^L\)

Here consistency_ratio is Rezaei's (2015) ratio \(\xi / CI(a_{BW})\), a normalised ratio, analogous to (not interchangeable with) AHP's consistency ratio; Rezaei's acceptance thresholds differ. It is not the quantity bwm_linear reports under the same field name, which is the linear model's \(\xi^L\) itself. For \(a_{BW} = 1\) the index \(CI\) is 0, so the ratio is 0.0 when \(\xi \le 10^{-6}\) and None (with a note in extras["consistency_note"]) otherwise.

Attributes:

Name Type Description
criteria

Criterion names in order.

best

The best criterion.

worst

The worst criterion.

best_to_others

Validated best-to-others comparisons by criterion.

others_to_worst

Validated others-to-worst comparisons by criterion.

penalty

Weight of the sum-to-one penalty.

Parameters:

Name Type Description Default
best_to_others Mapping[str, float]

Comparison of the best criterion over each criterion.

required
others_to_worst Mapping[str, float]

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 as the unique one with comparison 1 when None; pass it when several criteria tie at 1.

None
worst str | None

The worst criterion, inferred likewise.

None
penalty float

Weight of the penalty for violating the sum-to-one constraint (positive).

10.0

Raises:

Type Description
ValueError

For invalid comparison vectors (see resolve_best_worst) or a non-positive penalty.

Examples:

>>> from bulanik.sezgi import BWMProblem
>>> problem = BWMProblem({"a": 1, "b": 2, "c": 8}, {"a": 8, "b": 4, "c": 1})
>>> problem.best, problem.worst
('a', 'c')
>>> round(problem.deviation([8, 4, 1]), 6)   # perfectly consistent, any scale
0.0
>>> round(problem.deviation([0.6, 0.3, 0.1]), 6)
2.0
References

Rezaei (2015), Omega 53, 49-57.

deviation

deviation(x: Sequence[float]) -> float

The BWM objective \(\xi\) at weights x, without the penalty.

Parameters:

Name Type Description Default
x Sequence[float]

One positive weight per criterion, in criteria order (need not sum to 1).

required

Returns:

Type Description
float

The largest deviation \(\max(|w_B/w_j - a_{Bj}|, |w_j/w_W - a_{jW}|)\).

Raises:

Type Description
ValueError

If x has the wrong length or a component is not a finite positive number.

solve

solve(algorithm: Any = None, *, budget: int = 20000, seed: int = 0) -> BWMResult

Minimise with sezgi and return weights rescaled to sum to 1.

consistency_ratio is \(\xi /\) bwm_consistency_index\((a_{BW})\), with \(\xi\) recomputed at the rescaled weights. For \(a_{BW} = 1\) that index is 0: the ratio is then 0.0 when \(\xi \le 10^{-6}\) (consistent) and None otherwise, with extras["consistency_note"] saying why. This differs from bwm_linear, whose consistency_ratio is \(\xi^L\) itself. extras also holds evals_used and best_f.

Parameters:

Name Type Description Default
algorithm Any

A sezgi algorithm instance; None selects DifferentialEvolution(pop_size=40, variant="jde").

None
budget int

Number of objective evaluations (integer, at least 1).

20000
seed int

Random seed, making the run reproducible.

0

Returns:

Type Description
BWMResult

A BWMResult with

BWMResult

method == "bwm_nonlinear" and

BWMResult

operators["consistency"] == "rezaei2015_ci".

Raises:

Type Description
ValueError

If budget is not a positive integer.

Examples:

>>> from bulanik.sezgi import BWMProblem
>>> problem = BWMProblem({"a": 1, "b": 2, "c": 8}, {"a": 8, "b": 4, "c": 1})
>>> r = problem.solve(budget=3000, seed=1)
>>> r.ranking, r.xi < 0.01, r.consistency_ratio < 0.01
(('a', 'b', 'c'), True, True)

With \(a_{BW} = 1\) the consistency index is 0, so an inconsistent set of comparisons has no ratio:

>>> p = BWMProblem({"a": 1, "b": 1, "c": 3}, {"a": 1, "b": 1, "c": 2},
...                best="a", worst="b")
>>> r = p.solve(budget=2000, seed=1)
>>> r.consistency_ratio is None
True
>>> r.extras["consistency_note"]
"the comparisons are inconsistent (xi = 1.00213) but Rezaei's consistency index is 0 for a_BW = 1, so no consistency ratio exists"

FuzzyBWMProblem

FuzzyBWMProblem(best_to_others: Mapping[str, TFN], others_to_worst: Mapping[str, TFN], criteria: Sequence[str] | None = None, *, best: str | None = None, worst: str | None = None, penalty: float = 10.0)

Bases: Problem

Guo & Zhao's (2017, Knowledge-Based Systems 121:23-31) fuzzy best-worst method.

The weights are triangular numbers \(\tilde w_j = (l_j, m_j, u_j)\). With fuzzy comparisons \(\tilde a_{Bj} = (l_{Bj}, m_{Bj}, u_{Bj})\), the model is \(\min k\) subject to, for \(j \ne B\),

\[|l_B - l_{Bj} u_j| \le k u_j,\quad |m_B - m_{Bj} m_j| \le k m_j,\quad |u_B - u_{Bj} l_j| \le k l_j\]

(since \(\tilde w_B / \tilde w_j = (l_B/u_j, m_B/m_j, u_B/l_j)\)), the same rows for \(\tilde w_j / \tilde w_W\) with \(\tilde a_{jW}\) for \(j \ne W\), the normalisation \(\sum_j R(\tilde w_j) = 1\) with the graded mean \(R = (l + 4m + u)/6\), and \(l \le m \le u\). evaluate is the largest |row| / denominator plus penalty * (|sum_j R_j - 1| + sum_j [max(0, l_j - m_j) + max(0, m_j - u_j)]), where both ordering terms are summed over the criteria. Search space: Float(1e-6, 1, 3n), \((l, m, u)\) per criterion.

Comparisons are TFN with \(l > 0\) and \(m \ge 1\), e.g. from FUZZY_BWM_TFN; best and worst are found by their TFN(1, 1, 1) comparison.

Attributes:

Name Type Description
criteria

Criterion names in order.

best

The best criterion.

worst

The worst criterion.

best_to_others

Fuzzy best-to-others comparisons by criterion.

others_to_worst

Fuzzy others-to-worst comparisons by criterion.

penalty

Weight of the normalisation and ordering penalty.

Parameters:

Name Type Description Default
best_to_others Mapping[str, TFN]

Fuzzy comparison of the best criterion over each one.

required
others_to_worst Mapping[str, TFN]

Fuzzy comparison of each criterion over the worst.

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 TFN(1, 1, 1) when None.

None
worst str | None

The worst criterion; inferred likewise.

None
penalty float

Penalty weight (positive).

10.0

Raises:

Type Description
TypeError

If a comparison is not a TFN.

ValueError

If a TFN has \(l \le 0\) or \(m < 1\), the vectors are invalid (see resolve_best_worst), or penalty is not positive.

Examples:

>>> from bulanik.scales import FUZZY_BWM_TFN as F
>>> from bulanik.sezgi import FuzzyBWMProblem
>>> problem = FuzzyBWMProblem({"a": F["EI"], "b": F["WI"], "c": F["AI"]},
...                           {"a": F["AI"], "b": F["FI"], "c": F["EI"]})
>>> problem.best, problem.worst, problem.space().__class__.__name__
('a', 'c', 'Float')
References

Guo & Zhao (2017), Knowledge-Based Systems 121, 23-31.

deviation

deviation(x: Sequence[float]) -> float

The fuzzy BWM objective \(k\) at x, without the penalty.

Parameters:

Name Type Description Default
x Sequence[float]

The triples \((l, m, u)\) of all criteria, flattened in criteria order (length \(3n\), positive components).

required

Returns:

Type Description
float

The largest row deviation.

Raises:

Type Description
ValueError

If x has the wrong length or a non-positive component.

solve

solve(algorithm: Any = None, *, budget: int = 100000, seed: int = 0) -> BWMResult

Minimise with sezgi; return fuzzy weights rescaled so \(\sum R = 1\).

Each best triple is sorted so that \(l \le m \le u\) (a no-op unless the search ended a hair inside the ordering penalty) and every triple is scaled by \(1 / \sum R\), which leaves \(k\) unchanged. xi is \(k\) recomputed at the returned weights, weights are the graded means \(R(\tilde w_j)\), fuzzy_weights the TFNs and consistency_ratio is \(k /\) fuzzy_bwm_consistency_index\((\tilde a_{BW})\): a ratio to the index, like BWMProblem's and unlike bwm_linear's, which is \(\xi^L\) itself. The fuzzy index is never 0, so the ratio is always a number.

Parameters:

Name Type Description Default
algorithm Any

A sezgi algorithm instance; None selects DifferentialEvolution(pop_size=40, variant="jde").

None
budget int

Number of objective evaluations (integer, at least 1).

100000
seed int

Random seed, making the run reproducible.

0

Returns:

Type Description
BWMResult

A BWMResult with

BWMResult

method == "fuzzy_bwm" and fuzzy_weights set.

Raises:

Type Description
ValueError

If budget is not a positive integer.

Examples:

>>> from bulanik.scales import FUZZY_BWM_TFN as F
>>> from bulanik.sezgi import FuzzyBWMProblem
>>> problem = FuzzyBWMProblem({"a": F["EI"], "b": F["WI"], "c": F["AI"]},
...                           {"a": F["AI"], "b": F["FI"], "c": F["EI"]})
>>> r = problem.solve(budget=20000, seed=1)
>>> r.ranking, {k: round(v, 2) for k, v in r.weights.items()}
(('a', 'b', 'c'), {'a': 0.51, 'b': 0.35, 'c': 0.14})
>>> round(sum(r.weights.values()), 6)
1.0
>>> r.operators["consistency"]
'guo_zhao2017_ci'

ANFISProblem

ANFISProblem(anfis: ANFIS, X: Sequence[Sequence[float]], y: Sequence[float], *, coef_bound: float = 10.0)

Bases: Problem

Training RMSE of an ANFIS as a sezgi problem.

The space is Float(0, 1, D) over all \(D\) parameters; decode maps each \(z\) affinely to its parameter's bounds (span = hi - lo of its input): gaussian \(c\) in [lo, hi], \(\sigma\) in [span/100, span]; bell \(a\) in [span/100, span], \(b\) in [0.5, 5], \(c\) in [lo, hi]; consequents in [-coef_bound, coef_bound]. evaluate sets the decoded parameters on anfis (this instance is the working copy and is mutated) and returns the RMSE, or inf when no rule fires or the output overflows. The ANFIS needs its ranges (pass ranges= when building it; train_anfis infers them from the data when it has none).

Attributes:

Name Type Description
anfis

The working ANFIS whose parameters evaluate overwrites.

coef_bound

Bound of the consequent coefficients.

Parameters:

Name Type Description Default
anfis ANFIS

An ANFIS with input ranges.

required
X Sequence[Sequence[float]]

Training inputs, one row per sample.

required
y Sequence[float]

Training targets.

required
coef_bound float

Consequent coefficients are searched in [-coef_bound, coef_bound] (positive).

10.0

Raises:

Type Description
TypeError

If anfis is not an ANFIS.

RuntimeError

If the ANFIS has no input ranges.

ValueError

If coef_bound is not positive or X, y are invalid.

Examples:

>>> from bulanik.anfis import ANFIS
>>> from bulanik.sezgi import ANFISProblem
>>> X = [[i / 10] for i in range(11)]
>>> y = [2 * x[0] + 1 for x in X]
>>> problem = ANFISProblem(ANFIS(1, 2, ranges=[(0, 1)]), X, y)
>>> problem.dim
8
>>> 0.0 <= problem.evaluate([0.5] * problem.dim) < 10
True

dim property

dim: int

Number of parameters \(D\) (the length of z in decode).

decode

decode(z: Sequence[float]) -> list[float]

Map z in \([0, 1]^D\) to the ANFIS parameter vector.

Parameters:

Name Type Description Default
z Sequence[float]

\(D\) numbers in [0, 1].

required

Returns:

Type Description
list[float]

The parameters in the order of

list[float]

param_layout, each between its

list[float]

bounds.

Raises:

Type Description
ValueError

If z has the wrong length or a component outside [0, 1].

Examples:

>>> from bulanik.anfis import ANFIS
>>> from bulanik.sezgi import ANFISProblem
>>> X = [[0.0], [1.0]]
>>> p = ANFISProblem(ANFIS(1, 1, ranges=[(0, 1)]), X, [0.0, 1.0], coef_bound=2)
>>> p.decode([0.0] * p.dim)
[0.0, 0.01, -2.0, -2.0]
>>> p.decode([1.0] * p.dim)
[1.0, 1.0, 2.0, 2.0]

ANFISTrainResult dataclass

ANFISTrainResult(anfis: ANFIS, rmse: float, evals_used: int)

Outcome of train_anfis.

Attributes:

Name Type Description
anfis ANFIS

The trained copy of the ANFIS.

rmse float

Its training root-mean-square error.

evals_used int

Number of objective evaluations sezgi used.

train_anfis

train_anfis(anfis: ANFIS, X: Sequence[Sequence[float]], y: Sequence[float], *, algorithm: Any = None, budget: int = 20000, seed: int = 0, coef_bound: float = 10.0) -> ANFISTrainResult

Train a deep copy of anfis with sezgi; anfis itself is untouched.

An ANFIS built without ranges gets them from the min and max of each column of X on the copy, as ANFIS.fit does (a constant column raises ValueError). Use this derivative-free trainer instead of fit when you want a global search over all membership and consequent parameters.

Parameters:

Name Type Description Default
anfis ANFIS

The ANFIS whose structure is trained (not modified).

required
X Sequence[Sequence[float]]

Training inputs, one row per sample.

required
y Sequence[float]

Training targets.

required
algorithm Any

A sezgi algorithm instance; None selects DifferentialEvolution(pop_size=40, variant="jde").

None
budget int

Number of objective evaluations (integer, at least 1).

20000
seed int

Random seed, making the run reproducible.

0
coef_bound float

Consequent coefficients are searched in [-coef_bound, coef_bound] (positive).

10.0

Returns:

Type Description
ANFISTrainResult

Raises:

Type Description
TypeError

If anfis is not an ANFIS.

ValueError

If the data or budget or coef_bound is invalid, or a column of X is constant and ranges must be inferred.

RuntimeError

If no finite RMSE is found.

Examples:

>>> from bulanik.anfis import ANFIS
>>> from bulanik.sezgi import train_anfis
>>> X = [[i / 10] for i in range(11)]
>>> y = [2 * x[0] + 1 for x in X]
>>> model = ANFIS(1, 2)
>>> res = train_anfis(model, X, y, budget=2000, seed=0)
>>> res.rmse < 0.1, res.evals_used, model.ranges is None
(True, 2000, True)