# -*- coding: utf-8 -*- """ Fuzzy sets and aggregation utils """ # # Author: Soren A. Davidsen <sorend@gmail.com> # import numpy as np from collections.abc import Sequence import numbers from scipy.optimize import minimize def helper_np_array(X): if isinstance(X, (np.ndarray, np.generic)): return X elif isinstance(X, Sequence): return np.array(X) elif isinstance(X, numbers.Number): return np.array([X]) else: raise ValueError("unsupported type for building np.array: %s" % (type(X),)) class ZadehNegatedSet: def __init__(self, s): self.s = s def __call__(self, X): return 1.0 - self.s(X) def __str__(self): return "Not(%s)" % (str(self.s),) class TriangularSet: def __init__(self, a, b, c): self.a = a self.b = b self.c = c def __call__(self, X): X = helper_np_array(X) y = np.zeros(X.shape) # allocate output (y) left = (self.a < X) & (X < self.b) # find where to apply left right = (self.b < X) & (X < self.c) # find where to apply right y[left] = (X[left] - self.a) / (self.b - self.a) y[X == self.b] = 1.0 # at top y[right] = (self.c - X[right]) / (self.c - self.b) return y def __str__(self): return "Δ(%.2f %.2f %.2f)" % (self.a, self.b, self.c) def __repr__(self): return str(self) class TrapezoidalSet: def __init__(self, a, b, c, d): self.a = a self.b = b self.c = c self.d = d def __call__(self, X): X = helper_np_array(X) y = np.zeros(X.shape) left = (self.a < X) & (X < self.b) center = (self.b <= X) & (X <= self.c) right = (self.c < X) & (X < self.d) y[left] = (X[left] - self.a) / (self.b - self.a) y[center] = 1.0 y[right] = (self.d - X[right]) / (self.d - self.c) return y def __str__(self): return "T(%.2f %.2f %.2f %.2f)" % (self.a, self.b, self.c, self.d) class PiSet: def __init__(self, r, a=None, b=None, p=None, q=None, m=2.0): if a is not None: self.a = a self.p = (r + a) / 2.0 # between r and a elif p is not None: self.p = p self.a = r - (2.0 * (r - p)) # one "p" extra. else: raise ValueError("please specify a or p") if b is not None: self.b = b self.q = (r + b) / 2.0 elif q is not None: self.q = q self.b = r + (2.0 * (q - r)) else: raise ValueError("please specify b or q") # if a >= r or r >= b: # raise ValueError("please ensure a < r < b, got: a=%f, r=%f b=%f" % (self.a, self.r, self.b)) self.r = r self.m = m self.S = (2 ** (m - 1.0)) self.r_a = self.r - self.a self.b_r = self.b - self.r def __call__(self, X): X = helper_np_array(X) y = np.zeros(X.shape) l1 = (self.a < X) & (X <= self.p) # left lower l2 = (self.p < X) & (X <= self.r) # left upper r1 = (self.r < X) & (X <= self.q) # right upper r2 = (self.q < X) & (X <= self.b) # right lower y[l1] = self.S * (((X[l1] - self.a) / (self.r_a)) ** self.m) y[l2] = 1.0 - (self.S * (((self.r - X[l2]) / (self.r_a)) ** self.m)) y[r1] = 1.0 - (self.S * (((X[r1] - self.r) / (self.b_r)) ** self.m)) y[r2] = self.S * (((self.b - X[r2]) / (self.b_r)) ** self.m) return y def __str__(self): return "π(p=%.2f r=%.2f q=%.2f)" % (self.p, self.r, self.q) def __repr__(self): return str(self) def prod(X, axis=-1): """Product along dimension 0 or 1 depending on array or matrix""" return np.multiply.reduce(X, axis) def mean(X, axis=-1): return np.nanmean(X, axis) def min(X, axis=-1): return np.nanmin(X, axis) def max(X, axis=-1): return np.nanmax(X, axis) def lukasiewicz_i(X): return np.maximum(0.0, X[:, 0] + X[:, 1] - 1) def lukasiewicz_u(X): return np.minimum(1.0, X[:, 0] + X[:, 1]) def einstein_i(X): a, b = X[:, 0], X[:, 1] return (a * b) / (2.0 - (a + b - (a * b))) def einstein_u(X): a, b = X[:, 0], X[:, 1] return (a + b) / (1.0 + (a * b)) def algebraic_sum(X, axis=-1): return 1.0 - prod(1.0 - X, axis) def min_max_normalize(X): nmin, nmax = np.nanmin(X), np.nanmax(X) return (X - nmin) / (nmax - nmin) def p_normalize(X, axis=None): """Normalize values as probabilities (sums to one) Parameters: ----------- X : the numpy array to normalize axis : None does not deal with axes (default), 0: probas by row-sums, 1: probas by column-sums """ assert axis in [None, 0, 1], "Only axes None, 0 and 1 is supported" def handle_all_zeros(a): b = np.sum(X, dtype='float') if b > 0.0: return a / b else: return np.ones(a.shape) / a.size def handle_zero_rows(a): b = np.sum(a, axis=0, dtype='float') f = (b == 0) y = np.array(a, copy=True) y[:,f] = 1 b[f] = y.shape[0] return y / b if axis == 0: return handle_zero_rows(X) elif axis == 1: return handle_zero_rows(X.T).T else: return handle_all_zeros(X) def dispersion(w): return -np.sum(w[w > 0.0] * np.log(w[w > 0.0])) # filter 0 as 0 * -inf is undef in NumPy def ndispersion(w): return dispersion(w) / np.log(len(w)) def yager_orness(w): """ The orness is a measure of how "or-like" a given weight vector is for use in OWA. orness(w) = 1/(n-1) * sum( (n-i)*w ) """ n = len(w) return np.sum(np.arange(n - 1, -1, -1) * w) / (n - 1.0) def yager_andness(w): """ Yager's andness is 1.0 - Yager's orness for a given weight vector. """ return 1.0 - yager_orness(w) def weights_mapping(w): s = np.e ** w return s / np.sum(s) class OWA(object): """ Order weighted averaging operator. The order weighted averaging operator aggregates vector of a1, ..., an using a a permutation b1, ... bn for which b1 >= b2 => ... >= bn and a weight vector w, for which that w = w1, ..., wn in [0, 1] and sum w = 1 Averaging is done with weighted mean: sum(b*w) Parameters: ----------- v : The weights """ def __init__(self, v): self.v = v self.v_ = v[::-1] # save the inverse so we don't need to reverse np.sort self.lv = len(v) def __call__(self, X, axis=-1): if X.shape[axis] != self.lv: raise ValueError("len(X) != len(v)") b = np.sort(X, axis) # creates permutation return self.sorted_mean(b, axis) def sorted_mean(self, X, axis=-1): """Use for pre-sorted X""" return np.sum(X * self.v_, axis) def __str__(self): return "OWA(" + " ".join([ "%.4f" % (x,) for x in self.v]) + ")" def __repr__(self): return str(self) def andness(self): return yager_andness(self.v) def orness(self): return yager_orness(self.v) def disp(self): return dispersion(self.v) def ndisp(self): return ndispersion(self.v) class GOWA(OWA): """ Generalized order weighted averaging operator. The generalized order weighted averaging operator aggregates a vector of a1, ..., an using a permutation b1, ..., bn for which b1 >= b2 => ... => bn where b1 is the largest value in a, and which has an related weight vector w = w1, ..., wn in [0, 1] and sum w = 1. Averaging is used using the power-mean: sum(w*b^p)^(1/p), where p is the power parameter. """ def __init__(self, p, v): """ Constructs GOWA operator. Parameters: ----------- p : power parameter. v : weights. """ super(GOWA, self).__init__(v) self.p = p self.inv_p = (1.0 / p) def sorted_mean(self, X, axis=-1): return np.sum((X ** self.p) * self.v_, axis) ** self.inv_p def __str__(self): return ("GOWA(%f, " % (self.p,)) + " ".join([ "%.4f" % (x,) for x in self.v]) + ")" def gowa(p, *w): """Create Generalized OWA (GOWA) operator from weights""" w = np.array(w, copy=False).ravel() return GOWA(p, w) def owa(*w): """Create OWA operator from weights""" w = np.array(w, copy=False).ravel() return OWA(w) def meowa(n, orness, **kwargs): """ Maximize dispersion at a specified orness level. """ if 0.0 > orness or orness > 1.0: raise ValueError("orness must be in [0, 1]") if n < 2: raise ValueError("n must be > 1") def negdisp(v): return -dispersion(v) # we want to maximize, but scipy want to minimize def constraint_has_orness(v): return yager_orness(v) - orness def constraint_has_sum(v): return np.sum(v) - 1.0 return _minimize_owa(negdisp, (constraint_has_orness, constraint_has_sum), n, **kwargs) def sampling_owa_orness(x, d, **kwargs): """ Maximize orness of an owa operator given a given result data point. """ n = len(x) if n < 2: raise ValueError("n must be > 1") s_ = np.sort(x)[::-1] def negorness(v): return -yager_orness(v) def constraint_has_output_d(v): return np.sum(s_ * v) - d def constraint_has_sum(v): return np.sum(v) - 1.0 return _minimize_owa(negorness, (constraint_has_sum, constraint_has_output_d), n, **kwargs) def sampling_owa_ndisp(x, d, **kwargs): """ Maximize dispersion of an owa operator given a given result data point. """ n = len(x) if n < 2: raise ValueError("n must be > 1") s_ = np.sort(x)[::-1] def negndisp(v): return -ndispersion(v) def constraint_has_output_d(v): return np.sum(s_ * v) - d def constraint_has_sum(v): return np.sum(v) - 1.0 return _minimize_owa(negndisp, (constraint_has_sum, constraint_has_output_d), n, **kwargs) def mvowa(n, orness, **kwargs): """ Maximum variability order weighted aggregation. Construct aggregation with fixed orness but maximized variance. [Fuller and Majlender, 2003] """ if 0.0 > orness or orness > 1.0: raise ValueError("orness must be in [0, 1]") if n < 2: raise ValueError("n must be > 1") def variance(v): n = len(v) return (np.sum(v * v) - (1 - (n * n))) / n def constraint_has_orness(v): return yager_orness(v) - orness def constraint_has_sum(v): return np.sum(v) - 1.0 return _minimize_owa(variance, (constraint_has_orness, constraint_has_sum), n, **kwargs) def _minimize_owa(minfunc, constraints, n, **kwargs): bounds = tuple([ (0, 1) for x in range(n) ]) # this is actually the third constraint, but common. initial = np.ones(n) / n constraints_ = tuple([ {"fun": c, "type": "eq"} for c in constraints ]) res = minimize(minfunc, initial, bounds=bounds, options=kwargs, constraints=constraints_) if res.success: return OWA(res.x) else: raise ValueError("Could not optimize weights: " + res.message) class AndnessDirectedAveraging: def __init__(self, p): self.p = p self.tnorm = p <= 0.5 self.alpha = (1.0 - p) / p if self.tnorm else p / (1.0 - p) def __call__(self, X, axis=-1): X = np.array(X, copy=False) if self.tnorm: return (np.sum(X ** self.alpha, axis) / X.shape[axis]) ** (1.0 / self.alpha) else: return 1.0 - ((np.sum((1.0 - X) ** (1.0 / self.alpha), axis) / X.shape[axis]) ** self.alpha) def aa(p): assert 0 < p and p < 1 return AndnessDirectedAveraging(p)