Source code for pyanp.priority

'''
All pairwise matrix to priority vector calculations

@author: Dr. Bill Adams
'''
import numpy as np
from pyanp.general import get_matrix

######################################################
#### Priority Vector Calculations                 ####
######################################################

[docs]def geom_avg(vals)->float: """ Compute the geometric average of a list of values. :param vals: A list-like of numbers :return: The geometric average of the values. """ rval=1.0 count = 0 for val in vals: val = vals[count] if val != 0: rval *= val count+=1 if count != 0: rval = pow(rval, 1.0/count) return(rval)
[docs]def geom_avg_mat(mat, coeffs = None)->np.ndarray: ''' Computes the geometric average of the columns of a matrix. :param mat: Must be an numpy.array of shape [nRows, nCols] :param coeffs: If not None, it is a list like object with nColsOfMat elements. We multiply column 0 of mat by coeffs[0], column 1 of mat by coeffs[1], etc and then do the geometric average of the columns. Essentially this weights the columns. :return: An np.array of dimension [nRowsOfMat], i.e. a vector. that is the weighted geometric average of the columns of the matrix mat. ''' size = mat.shape[0] rval = np.ones([size]) # Normalize the coeffs if they exist if np.any(coeffs): coeffs = size*np.array(coeffs)/sum(coeffs) for row in range(size): if np.any(coeffs): theRow = mat[row,:] ** np.array(coeffs) else: theRow = mat[row,:] rval[row] = geom_avg(theRow) return(rval)
[docs]def pri_expeigen(mat, error = 1e-10): """ Calculates priorities using exponential (aka multiplicative) eigenvector :param mat: An numpy.array of shape [size, size] of pairwise comparisions. :param error=1e-10: The convergence error term :return numpy.array: The resulting exponential eigenvector as a numpy.array of shape [size] """ size = mat.shape[0] vec = np.ones([size]) diff = 1 count=0 while diff >= error and count < 100: nextv = geom_avg_mat(mat, vec) nextv = nextv/max(nextv) diff = max(abs(nextv - vec)) vec = nextv count+=1 return(vec/sum(vec))
[docs]def pri_llsm(mat): ''' Calculates the priorities using the geometric mean method, aka Log Least Squares Method (LLSM). :param mat: An numpy.array of dimension [size,size] :return numpy.array: The resulting llsm priority vector as a numpy.array of shape [size] ''' rval = geom_avg_mat(mat) rval = rval / sum(rval) return(rval)
# LLSM is the same geometric average, so let's make a synonym pri_geomavg = pri_llsm
[docs]def harker_fix(mat:np.ndarray)->np.ndarray: """ Performs Harker's fix on the numpy matrix mat. It returns a copy with the fix. The function does not change the matrix mat. :param mat: A square numpy. :return: A copy of mat with Harker's fix applied to it """ nrows = mat.shape[0] ncols = mat.shape[1] rval = mat.copy() for row in range(nrows): val = 1 for col in range(ncols): if col != row and mat[row,col]==0: val+=1 rval[row,row]=val return(rval)
[docs]def pri_eigen(mat:np.ndarray, error:float = 1e-10, use_harker:bool = False, return_eigenval:bool=False): ''' Calculates the largest eigen vector of a matrix. :param mat: A square numpy array. :param use_harker=False: Should we apply Harker's fix before computing? :param return_eigenval=False: If True it returns only the eigenvalue, otherwise only returns the eigenvector. :return numpy.array: The largest eigenvector that is the normalized (sum to 1) largest eigenvector as a numpy.array of shape [size] if return_eigenval=False, otherwise returns the eigenvalue as a number. ''' if mat is None or mat.shape==(0,0): # Eigen vector of the empty matrix is [] return np.array([]) if use_harker: mat = harker_fix(mat) size = mat.shape[0] #Create our return value vec = np.ones([size]) diff = 1 while diff > error: nextv = np.matmul(mat, vec) nextv = nextv/sum(nextv) diff = max(abs(nextv - vec)) vec = nextv if return_eigenval: nextv = np.matmul(mat, vec) return sum(nextv) else: return(vec)
[docs]def inconsistency_divisor(mat_or_size)->float: ''' Calculates the inconsistency divisor for a matrix, or the size of a matrix. The inconsistency divisor is what you divide (eigenvalue - size) by to get the inconsistency. :param mat_or_size: Either a pairwise matrix, or simply the size of the pairwise matrix (which is what determines the inconsistency divisor). :return: The inconsistency divisor ''' size = size_array_like(mat_or_size) t=size - 1 if size<=0: return 1 elif size==1: return 1 elif size==2: return 1 elif size==3: return .52 * t elif size==4: return .89 * t elif size==5: return 1.12 * t elif size==6: return 1.25 * t elif size==7: return 1.35 * t elif size==8: return 1.40 * t elif size==9: return 1.45 * t elif size==10: return 1.49 * t elif size==11: return 1.51 * t elif size==12: return 1.54 * t elif size==13: return 1.56 * t elif size==14: return 1.57 * t elif size==15: return 1.58 * t else: return 1.98 * (1 - (size - 1) / (size * (size - 1) / 2))
[docs]def incon_std(mat:np.ndarray, error:float = 1e-10, use_harker:bool = True)->float: ''' Calculates the inconsistency of a pairwise matrix using the standard Saaty AHP/ANP theoretic formula. :param mat: A numpy.array of shape [size,size] of pairwise comparisons. :param error: The error to use for the pri_eigen calculation :param use_harker: Should we apply Harker's fix before the calculation? :return: The inconsistency. ''' size = mat.shape[0] largest_eigen_val = pri_eigen(mat, error, use_harker, return_eigenval=True) return (largest_eigen_val-size)/inconsistency_divisor(mat)
[docs]def incon_gci(mat:np.ndarray, use_harker:bool = True, only_count_nonzero=True)->float: ''' Calculates the inconsistency of a pairwise matrix using the GCI formula from :param mat: A numpy.array of shape [size,size] of pairwise comparisons. :param use_harker: Should we apply Harker's fix before the calculation? :param only_count_nonzero: Should we only average nonzero comparisons (this is not the official defn of gci) :return: The inconsistency. ''' size = mat.shape[0] if use_harker: mat = harker_fix(mat) privec = pri_llsm(mat) primat = ratio_mat(privec) return mat_gci(mat, primat, only_count_nonzero=only_count_nonzero)
[docs]def mat_gci(mat1:np.ndarray, mat2:np.ndarray, only_count_nonzero=True)->float: ''' Calculates the GCI of two matrices :param mat1: :param mat2: :return: ''' size = mat1.shape[0] rval = 0 count_nonzero = 0 for row in range(size): for col in range(row+1, size): if (mat1[row,col] != 0) and (mat2[row,col] != 0): rval += np.log(mat1[row,col] / mat2[row,col]) ** 2 count_nonzero += 1 if only_count_nonzero: # We only average the nonzero values. This is different from the # reference paper, but it makes more sense to me if count_nonzero > 0: return rval / count_nonzero else: # There are no non-zero values, return 0 return 0 else: # We want to use the official formula from the paper # so be it if size > 2: return 2 / ((size-1)*(size-2)) * rval else: return 0
######################################################### ### Priority Error Calculations ######## #########################################################
[docs]def prerr_euclidratio(pwmat, privec): ''' Calculates the euclidean distance error between the pairwise matrix and the ratio matrix of a priority vector. This calculates using the following formula .. math:: \\sqrt{ \\sum_{i,j} \\left(\\frac{pwmat[i, j] - privec[i]}{privec[j]} \\right)^2} :param pwmat: A numpy.array of shape [size, size] of pairwise comparisons. :param privec: A numpy.array of share [size] of the priority vector to compare this pairwise matrix to. :return: The error/distance between the ratio matrix and the matrix. ''' rval = 0 diffsum = 0 count = 0 size = pwmat.shape[0] for i in range(0, size): for j in range(0, size): if (i != j) and (pwmat[i, j] != 0): diffsum += (pwmat[i, j] - privec[i] / privec[j]) ** 2 count += 1 if count == 0: return 0 else: return diffsum ** (1.0 / 2)
[docs]def prerr_ratio_avg(pwmat, privec): ''' Calculates priority error using the arithmetic average of ratio distance of pwmat from the ratio matrix of privec It averages: .. math:: ratio\_greater\_1(pwmat[i, j], (privec[i]/privec[j])) - 1 where .. math:: ratio\_greater\_1(a,b) = \\begin{cases} 1 & \\hbox{ if a or b = 0 } \\\\ max(a/b, b/a) & \\hbox{otherwise} \\end{cases}. :param pwmat: A numpy.array of shape [size, size] of pairwise comparisons :param privec: A numpy.array of shape [size] of priortiy vector :return: The ratio average priority vector ''' diffsum = 0 count = 0 size = pwmat.shape[0] rmat = ratio_mat(privec) for i in range(0, size): for j in range(0, size): if (pwmat[i, j] >= 1) and (i != j): ratio = ratio_greater_1(pwmat[i, j], rmat[i, j]) score = ratio - 1 diffsum += score count += 1 # print("ratio={} diffprod={}".format(ratio, diffprod)) if count == 0: return 0 else: return diffsum * (1.0 / count)
[docs]def ratio_greater_1(a, b): ''' The ratio of a to b (or b to a) that is larger than or equal to 1. :param a: A numerical value for the ratio calculation :param b: Another numerical value :return: 1 if a or b is 0, otherwise max(a/b, b/a) ''' if (a == 0) or (b == 0): return 1 else: return max(a/b, b/a)
[docs]def prerr_ratio_prod(pwmat, privec): ''' Calculates priority error using the geometric average of ratios of pwmat and the ratio matrix of privec, the formula is: .. math:: \\sqrt[n(n-1)/2]{\\prod_{i=1, j=i+1} ratio\_greater\_1(pwmat[i, j], privec[i]/privec[j])} :param pwmat: A numpy.array of shape [size, size] of pairwise comparisons :param privec: A numpy.array of shape [size] of priortiy vector :return: The calculated error ''' diffprod = 1 count = 0 size = pwmat.shape[0] for i in range(0, size): for j in range(0, size): if (pwmat[i, j] >= 1) and (i != j): ratio = ratio_greater_1(pwmat[i, j], privec[i]/privec[j]) diffprod *= ratio count += 1 #print("ratio={} diffprod={}".format(ratio, diffprod)) #print(diffprod) if count == 0: return 0 else: return diffprod ** (1.0 / count)
###################################################### ### Creating a pairwise matrix from simpler data ##### ######################################################
[docs]def ratio_mat(pv)->np.ndarray: ''' Returns the ratio matrix of a vector :param pv: An array-like object with len(pv)=size :return: A numpy.array of shape [size, size] of the ratios ''' size = len(pv) rval = np.identity(n=size) for row in range(size): for col in range(size): if pv[col] != 0: rval[row, col] = pv[row] / pv[col] return rval
[docs]def utmrowlist_to_npmatrix(list_of_votes)->np.ndarray: ''' Convert a list of values to a pairwise matrix, assuming the list is the upper triangular part only :param list_of_votes: An array like, the first elements are the top row of the UTM part of the matrix. Then it goes to the second row, etc. :return: A numpy.array of the full pairwise comparison matrix. ''' N = len(list_of_votes) # Find dims n_float = (1 + np.sqrt(1+8*N))/2 # This should be an integer for it to work n = int(n_float) if (np.abs(n-n_float) > 1e-10): raise ValueError("The list is the wrong size for a utm") rval = np.identity(n) pos = 0 for row in range(n): for col in range(row+1, n): val = list_of_votes[pos] rval[row, col] = val if val != 0: rval[col, row] = 1/val pos += 1 return rval
###################################################### ### Helper functions ################################ ######################################################
[docs]def size_array_like(mat_or_size): ''' Returns the size of an array like or integer :param mat_or_size: Either an integer (specifying the size) or an array-like or numpy.array of shape [size, size]. If array-like list of lists, we only use len(mat_or_size), we do not check that the array-like is actually square. :return: The parameter if it was an integer, len(mat_or_size) if param is a list, or mat_or_size.shape[0] if mat_or_size is a numpy.ndarray ''' if isinstance(mat_or_size, (int)): return mat_or_size elif isinstance(mat_or_size, (np.ndarray)): return mat_or_size.shape[0] elif isinstance(mat_or_size, (tuple, list)): return len(mat_or_size) else: raise ValueError("Unable to get size")