Aggregation¶
Several experts rate the same thing, or one decision has several criteria, and
you want one fuzzy number back. bulanik offers three functions in
bulanik.aggregate:
wa, the weighted average \(\sum_i w_i A_i\);wg, the weighted geometric mean \(\prod_i A_i^{w_i}\);aggregate_experts, which applies one of them to a stack of expert rating tables, cell by cell.
They work for every number type, because they are built from the type's own
scale, add, power and mul. The result has the type of the inputs.
Weighted average and weighted geometric mean¶
wa scales each number by its weight and folds the results with add; wg raises
each to its weight with power and folds with mul. What that means depends on
the type:
| Type | wa |
wg |
|---|---|---|
TFN, TrFN |
componentwise weighted mean of the points | componentwise weighted geometric mean (points \(\ge 0\)) |
IFN |
IFWA: \(\bigl(1 - \prod (1 - \mu_i)^{w_i},\; \prod \nu_i^{w_i}\bigr)\) | IFWG: \(\bigl(\prod \mu_i^{w_i},\; 1 - \prod (1 - \nu_i)^{w_i}\bigr)\) |
PFN, FFN, QROFN |
the same, on the \(q\)-th powers, then the \(q\)-th root | the same |
PictureFN |
\(\bigl(1 - \prod (1 - \mu_i)^{w_i},\; \prod \eta_i^{w_i},\; \prod \nu_i^{w_i}\bigr)\) (closed form) | \(\bigl(\prod (\mu_i + \eta_i)^{w_i} - \prod \eta_i^{w_i},\; \prod \eta_i^{w_i},\; 1 - \prod (1 - \nu_i)^{w_i}\bigr)\) (closed form) |
SphericalFN, NeutrosophicFN |
built from that type's scale and add |
built from power and mul |
IVQROFN, IVIFN, IVPFN, IVFFN |
the orthopair rule on the lower pair and on the upper pair separately | the same |
IVNeutrosophicFN |
the neutrosophic rule on the lower and on the upper bounds | the same |
IT2TrFN |
componentwise weighted mean of the four points of the upper and of the lower trapezoid; the heights take the min |
componentwise weighted geometric mean (points \(\ge 0\)) |
The interval-valued kinds (see Interval-valued numbers and
Interval type-2 trapezoids) aggregate the same way, so wa and wg and
aggregate_experts accept them, and so does rank_fuzzy:
from bulanik import IT2TrFN, IVIFN
from bulanik.aggregate import wa, wg
x = [IVIFN(0.4, 0.5, 0.3, 0.4), IVIFN(0.5, 0.6, 0.2, 0.3)]
r = wa(x, [0.7, 0.3])
print(tuple(round(v, 4) for v in (r.mu_l, r.mu_u, r.nu_l, r.nu_u)))
low = IT2TrFN((0, 0.1, 0.1, 0.3, 1, 1), (0.05, 0.1, 0.1, 0.2, 0.9, 0.9))
high = IT2TrFN((0.5, 0.7, 0.7, 0.9, 1, 1), (0.55, 0.7, 0.7, 0.85, 0.9, 0.9))
print(tuple(round(v, 4) for v in wa([low, high]).upper))
print(tuple(round(v, 4) for v in wg([low, high]).upper))
(0.4319, 0.5324, 0.2656, 0.3669)
(0.25, 0.4, 0.4, 0.6, 1.0, 1.0)
(0.0, 0.2646, 0.2646, 0.5196, 1.0, 1.0)
The picture forms are closed because scaling a picture number by a weight below 1 can leave its constraint set (see Three-part numbers). Weights must be non-negative; inputs with weight 0 are dropped.
ops= selects the operator family of the type ("einstein" for orthopair
numbers) and weights=None means equal weights.
from bulanik import IFN, PFN, TFN, PictureFN
from bulanik.aggregate import wa, wg
t = [TFN(1, 2, 4), TFN(2, 3, 5), TFN(0, 1, 2)]
print(wa(t)) # equal weights
print(wa(t, [0.5, 0.3, 0.2]))
print(wg(t[:2], [0.5, 0.5]))
x = [IFN(0.6, 0.3), IFN(0.5, 0.4)]
for name, fn in (("wa", wa), ("wg", wg)):
r = fn(x, [0.7, 0.3])
print(name, f"({r.mu:.4f}, {r.nu:.4f})")
r = wa(x, [0.7, 0.3], ops="einstein")
print("wa einstein", f"({r.mu:.4f}, {r.nu:.4f})")
p = wa([PFN(0.7, 0.5), PFN(0.6, 0.6)]) # same rung required
print(p)
print([round(v, 4) for v in wa([PictureFN(0.5, 0.2, 0.1), PictureFN(0.4, 0.3, 0.2)]).parts])
TFN(l=1.0, m=1.9999999999999998, u=3.6666666666666665)
TFN(l=1.1, m=2.1, u=3.9)
TFN(l=1.4142135623730951, m=2.4494897427831783, u=4.47213595499958)
wa (0.5723, 0.3270)
wg (0.5681, 0.3316)
wa einstein (0.5717, 0.3276)
PFN(mu=0.6547409607750014, nu=0.5477225575051662)
[0.4523, 0.2449, 0.1414]
Weights¶
Weights must sum to 1 (within \(10^{-9}\), then silently rescaled to exactly 1).
Pass normalize=True to give raw importances instead, say [3, 2, 1]; the
weights are then divided by their sum.
from bulanik import TFN
from bulanik.aggregate import wa
a, b = TFN(0, 2, 4), TFN(2, 4, 6)
print(wa([a, b], [3, 1], normalize=True)) # same as [0.75, 0.25]
print(wa([a, b], [0.75, 0.25]))
print(wa([a, b], [1.0, 0.0])) # a weight of 0 drops the input
TFN(l=0.5, m=2.5, u=4.5)
TFN(l=0.5, m=2.5, u=4.5)
TFN(l=0.0, m=2.0, u=4.0)
Aggregating experts¶
aggregate_experts(ratings, weights) takes ratings indexed
[expert][alternative][criterion] (a list of equally shaped matrices of fuzzy
numbers) and returns the [alternative][criterion] matrix. method="wa" is the
default; "wg" gives the geometric version. The weights are one per expert.
The typical flow is words → numbers → one table:
from bulanik.aggregate import aggregate_experts
from bulanik.scales import EXAMPLE_TFN_7 as scale
words = [
[["G", "F"], ["MG", "VG"]], # expert 1: 2 alternatives x 2 criteria
[["VG", "MP"], ["F", "G"]], # expert 2
[["G", "F"], ["G", "G"]], # expert 3
]
ratings = scale.convert(words) # same nesting, now TFNs
weights = [0.5, 0.3, 0.2] # expert 1 counts the most
table = aggregate_experts(ratings, weights)
for row in table:
print([f"({n.l:.2f}, {n.m:.2f}, {n.u:.2f})" for n in row])
geo = aggregate_experts(ratings, weights, method="wg")
print(geo[0][0])
['(7.60, 9.30, 10.00)', '(2.40, 4.40, 6.40)']
['(4.80, 6.80, 8.60)', '(8.00, 9.50, 10.00)']
TFN(l=7.54816492585302, m=9.289016976853711, u=10.0)
The aggregated table is what fuzzy_topsis and the
other MCDM functions consume. See Fuzzy TOPSIS.
Pitfalls¶
Types must match, rungs included. Mixing TFN and IFN, or IFN and PFN,
raises TypeError. Convert your scale once so that every expert uses the same
type.
Weights are validated. They must be non-negative and finite, one per value,
and sum to 1 unless you pass normalize=True.
Geometric means need non-negative triangles. wg of a triangle with a
negative lowest point fails, because power does.
Expert tables must have the same shape. Every expert needs the same number of alternatives and criteria, and the weights count experts, not criteria.
Empty input. There is nothing to average.
from bulanik import IFN, PFN, TFN
from bulanik.aggregate import aggregate_experts, wa, wg
def show(make):
try:
print(make())
except Exception as error:
print(f"{type(error).__name__}: {error}")
show(lambda: wa([TFN(1, 2, 3), IFN(0.5, 0.4)]))
show(lambda: wa([IFN(0.5, 0.4), PFN(0.5, 0.4)]))
show(lambda: wa([TFN(1, 2, 3), TFN(2, 3, 4)], [0.5, 0.2])) # sums to 0.7
show(lambda: wa([TFN(1, 2, 3), TFN(2, 3, 4)], [0.5, 0.25, 0.25])) # three weights, two values
show(lambda: wa([TFN(1, 2, 3), TFN(2, 3, 4)], [1.5, -0.5]))
show(lambda: wa([TFN(1, 2, 3), TFN(2, 3, 4)], [0, 0], normalize=True))
show(lambda: wg([TFN(-1, 2, 3), TFN(2, 3, 4)]))
show(lambda: wa([]))
show(lambda: wa([1, 2]))
show(lambda: wa([IFN(0.5, 0.4)], ops="hamacher"))
show(lambda: aggregate_experts([[[TFN(1, 2, 3)]], [[TFN(1, 2, 3), TFN(1, 2, 3)]]]))
show(lambda: aggregate_experts([[[TFN(1, 2, 3)]]], method="median"))
show(lambda: aggregate_experts([]))
TypeError: cannot combine TFN with IFN(q=1)
TypeError: cannot combine IFN(q=1) with PFN(q=2)
ValueError: weights must sum to 1, got 0.7
ValueError: expected 2 weights, got 3
ValueError: weights must be non-negative
ValueError: weights must not all be zero
ValueError: power 0.5 needs the lowest point >= 0, got -1.0
ValueError: cannot aggregate an empty list of values
TypeError: cannot aggregate int; need fuzzy numbers
ValueError: unknown ops 'hamacher' for IFN; valid: algebraic, einstein
ValueError: expert rating matrices must all have the same shape
ValueError: unknown method 'median' for aggregate_experts; valid: wa, wg
ValueError: need at least one expert
See also¶
- Orthopair numbers and Three-part numbers for what the operators do to each type; Interval-valued numbers and Interval type-2 trapezoids for the v0.2 kinds.
- Sets and scales for
LinguisticScale.convert. - Weighting for computing the criterion weights, and Fuzzy TOPSIS for ranking the aggregated table.
- API:
wa,wg,aggregate_experts. - Examples: 02 scale and experts and 05 intuitionistic pipeline.