Source code for pyanp.pairwise

'''
Group pairwise object and calculations.  See :py:mod:`pyanp.priority` for
all methods of calculating priorities from a pairwise comparison matrix
in addition to inconsistency calculations.
'''

import numpy as np
import pandas as pd
from pyanp.priority import incon_std
from pyanp.general import islist, matrix_as_df
from pyanp.prioritizer import Prioritizer, PriorityType
from pyanp.priority import pri_eigen
from copy import deepcopy
import re

[docs]class Pairwise(Prioritizer): ''' Creates a new group pairwise comparison object. :param alts: The list alternatives (things you are comparing) to start with. Should be a list-like object of strings. :param users: The users to start the group pairwise comparison object with. It should be a list-like object of strings. :param demographic_cols: The names of the demographic columns to start the group pairwise comparison object with. It should be a list-like object of strings. ''' def __init__(self, alts=None, users=None, demographic_cols = None): if alts is None: alts = [] if users is None: users = [] if demographic_cols is None: demographic_cols = ['Name', 'Age'] self.alts = alts all_cols = demographic_cols + ['Matrix'] #if 'Name' not in demographic_cols: # all_cols = ['Name'] + all_cols self.df = pd.DataFrame(data = None, columns=all_cols) self.priority_calc = pri_eigen def __deepcopy__(self, memodict={}): rval = Pairwise() rval.alts = deepcopy(self.alts) rval.df = self.df.copy() rval.priority_calc = self.priority_calc rval.df['Matrix'] = [deepcopy(mat) for mat in self.df['Matrix']] return rval
[docs] def is_user(self, user_name:str)->bool: ''' Checks if a user exists in this group pairwise comparison object. :param user_name: The name of the user to look for :return: True/False ''' return user_name in self.df.index
[docs] def is_alt(self, alt_name:str)->bool: ''' Checks if an alternative (a thing you are pairwise comparing) exists in this group pairwise comparison object. :param alt_name: The name of the alternative to check for. :return: True/False ''' return alt_name in self.alts
[docs] def nalts(self)->int: ''' :return: The number of alternatives (things you are pairwise comparing) in this group pairwise comparison object. ''' return len(self.alts)
def _blank_pairwise(self): ''' Creates a blank pairwise comparison for the right number of alts ''' nalts = self.nalts() return np.identity(nalts)
[docs] def add_user(self, user_name:str)->None: ''' Adds a user to this group pairwise comparison object. :param user_name: The name of the user to add :return: Nothing :raises ValueError: If the user already existed. ''' if self.is_user(user_name): raise ValueError("User "+user_name+" already existed") ncols = len(self.df.columns) data = [None]*(ncols-1)+[self._blank_pairwise()] self.df.loc[user_name] = data
[docs] def add_alt(self, alt_name:str, ignore_existing=False)->None: ''' Adds an alternative (thing you are pairwise comparing) to this group pairwise comparison object. :param alt_name: The name of the alternative to add. :return: Nothing :raises ValueError: If the alternative already esisted. ''' if self.is_alt(alt_name): if ignore_existing: return else: raise ValueError("Alt "+alt_name+" already existed") self.alts.append(alt_name) for user in self.df.index: mat = self.matrix(user) self.df.loc[user, "Matrix"] = add_place(mat)
[docs] def matrix(self, user_name=None, createUnknownUser:bool=True, as_df=False)->np.ndarray: ''' Gets the pairwise comparison for a user or group of users. :param user_name: The name/names of the user/users to get the comparisons of. If None, that means to get the group average for all users. If it is a string, that means get the pairwise comparison matrix of that user. If it is a list-like of strings, we get the group average matrix for all of those users. :param createUnknownUser: If True and the user_name did not exist, we should create that user. Otherwise throw an error if we request for a non-existant user. :param as_df: If True return as pandas.DataFrame with index/column names as the alt names, otherwise return numpy.ndarray :return: The numpy array of the pairwise comparisons, or DataFrame if as_df is True :raises ValueError: If createUnknownUser=False and we request for a single non-existant user. ''' if user_name is None: user_name = self.usernames() if isinstance(user_name, (str, int, float)): # We are just doing a single user if not self.is_user(user_name): if createUnknownUser: self.add_user(user_name) else: raise ValueError("No such user " + user_name) rval = self.df.loc[user_name, "Matrix"] else: mats = [self.df.loc[user, 'Matrix'] for user in user_name] if len(mats) == 0: return np.identity(self.nalts(), dtype=float) rval = geom_avg_mats(mats) if as_df: return matrix_as_df(rval, self.alt_names()) else: return rval
[docs] def incon_std(self, user_name=None)->float: ''' Calculates the standard Saaty pairwise comparison inconsistency for a user or group of users. :param user_name: The name/names of the users to get the inconsistency of. If None, we get the inconsistency of the group average matrix. If it is a string, we get the inconsistency of that user. If it is a list of users, we get the inconsistency of the group average for that list of users. :return: The Saaty inconsistency score. ''' matrix = self.matrix(user_name) return incon_std(matrix)
[docs] def alt_index(self, alt_name_or_index)->int: ''' Find the index (integer location) of the given alternative in the pairwise comparison matrices. :param alt_name_or_index: If this is an integer, we simply return that integer. Otherwise we look up the index of the alternative name in the list of alternatives in this object. :return: The index that alternative has in the pairwise comparison matrices. ''' if isinstance(alt_name_or_index, (int)): return alt_name_or_index if alt_name_or_index not in self.alts: raise ValueError("No such alt "+alt_name_or_index) return self.alts.index(alt_name_or_index)
[docs] def vote_series(self, votes:pd.Series, row, col, createUnknownUser:bool=True)->None: ''' Changes a single pairwise value for a series of users. :param votes: Series whose index is usernames and values are their votes. :param row: The integer or string name of the row to compare at. :param col: The integer or string name of the column to compare at. :param createUnknownUser: If True and a username does not exist in this object, we will create it first, then do the comparison. Otherwise it throws an exception for unknown users. :return: Nothing :raises ValueError: If the user does not exist and createUnknownUsers is False. ''' for uname, val in votes.iteritems(): self.vote(uname, row, col, val, createUnknownUser=createUnknownUser)
[docs] def vote_matrix(self, user_name:str, val=np.ndarray, createUnknownUser:bool=True): ''' Sets the vote matrix for a user :param user_name: :param val: :return: ''' mat = self.matrix(user_name, createUnknownUser=createUnknownUser) mat[:,:] = val
[docs] def vote(self, user_name:str, row, col, val:float=0, createUnknownUser:bool=True)->None: ''' Changes a single pairwise value for a single user. :param user_name: The string name of the user whose pairwise comparison vote you wish to change. :param row: The integer or string name of the row to compare at. :param col: The integer or string name of the column to compare at. :param val: The new pairwise comparison value :param createUnknownUser: If True and user_name does not exist in this object, we will create it first, then do the comparison. Otherwise it throws an exception for unknown users. :return: Nothing :raises ValueError: If the user does not exist and createUnknownUsers is False. ''' mat = self.matrix(user_name, createUnknownUser=createUnknownUser) row = self.alt_index(row) col = self.alt_index(col) if row == col: # Cannot vote self at all raise ValueError("row cannot equal column when voting") mat[row, col] = val if val == 0: mat[col, row] = 0 else: mat[col, row] = 1.0/val
[docs] def unvote(self, user_name:str, row, col, createUnknownUser:bool=True)->None: ''' Unsets a pairwise comparison :param user_name: The string name of the user whose pairwise comparison vote you wish to unset. :param row: The integer or string name of the row to compare at. :param col: The integer or string name of the column to compare at. :param createUnknownUser: If True and user_name does not exist in this object, we will create it first, then do the unset operation. Otherwise it throws an exception for unknown users. :return: Nothing :raises ValueError: If the user does not exist and createUnknownUsers is False. ''' self.vote(user_name, row, col, val=0)
[docs] def usernames(self): ''' :return: A list of the users in this group pairwise comparison object. ''' return list(self.df.index)
[docs] def priority(self, username=None, ptype:PriorityType=None): ''' Calculates the resulting priority for the given user / users. :param user_name: The name/names of the users to calculate the priority of. If None, we get the priority of the group average matrix. If it is a string, we get the priority of that user. If it is a list of users, we get the priority of the group average for that list of users. :param ptype: How should we normalize the resulting priorities (if at all). :return: A pandas.Series whose indices are the alternative names and whose values are the priorities of those alternatives. ''' mat = self.matrix(username) rval = self.priority_calc(mat) return pd.Series(data=rval, index=self.alts)
def _repr_html(self, tab="\t"): rval = tab+"<ul>\n" for user in self.usernames(): mat = self.matrix(user) matstr = tab+"\t"+str(mat) matstr = re.sub("\n", "\n"+tab+"\t", matstr) rval += tab+"\t"+"<li>"+str(user)+"\n"+matstr+"\n" rval += tab+"</ul>" return rval
[docs] def data_names(self, append_to=None, post_pend=""): ''' ''' alt_names = self.alt_names() nalts = len(alt_names) if append_to is None: append_to = [] for alt1pos in range(nalts): for alt2pos in range(alt1pos+1, nalts): append_to.append(alt_names[alt1pos]+" vs "+alt_names[alt2pos]+" "+post_pend) return append_to
[docs] def alt_names(self): ''' :return: List of string alt names ''' return deepcopy(self.alts)
[docs]def add_place(mat): ''' Adds a row and column to the end of a matrix, and makes the last entry 1, rest of the added entries are zeroes :param mat: The matrix to add an entry to. :return: New matrix ''' if mat is None: return np.array([[1]]) nrows = len(mat) if nrows == 0: return np.array([[1]]) ncols = len(mat[0]) rval = np.hstack([mat, [[0]]*nrows]) rval = np.vstack([rval, [0]*(ncols+1)]) rval[nrows,ncols]=1 return rval
[docs]def geom_avg_mats(mats)->np.ndarray: ''' Calculates the geometric average of the given matrices. :param mats: A list-like object of numpy arrays :return: A numpy array that is the geometric average ''' if len(mats) <= 0: raise ValueError('Need more then 0 matrices') nrows, ncols = mats[0].shape rval = np.zeros([nrows, ncols]) for r in range(nrows): for c in range(ncols): rval[r,c]=1 nonzerocount=0 for mat in mats: val = mat[r,c] if val != 0: rval[r,c] *= val nonzerocount += 1 if nonzerocount > 0: rval[r,c] = rval[r,c] ** (1.0/nonzerocount) else: rval[r,c] = 0 return rval