Source code for compsoc.evaluate

"""
Evaluation functions
"""
from typing import List, Tuple, Callable

from compsoc.profile import Profile
from compsoc.voter_model import get_profile_from_model, generate_distorted_from_normal_profile
from compsoc.voting_rules.borda import borda_rule
from compsoc.voting_rules.borda_gamma import get_borda_gamma
from compsoc.voting_rules.copeland import copeland_rule
from compsoc.voting_rules.dowdall import dowdall_rule
from compsoc.voting_rules.simpson import simpson_rule


[docs] def voter_subjective_utility_for_elected_candidate(elected: List[int], vote: Tuple[int], topn: int) -> tuple: """ Calculates the subjective utility of an individual voter for an elected candidate. :param elected: List of elected candidates. :type elected: List[int] :param vote: A tuple containing a voter's ranked candidates. :type vote: Tuple[int] :param topn: The number of top candidates to consider for utility calculation. :type topn: int :return: A tuple containing utility for the top candidate and total utility for top n candidates. :rtype: tuple """ # Gain, based on original vote (utility) and elected candidate # Given a particular vote structure (ranking), return its utility # knowing the elected candidate num_candidates = len(elected) utility_increments = [(num_candidates - i) / (num_candidates * 1.0) for i in range(num_candidates)] my_best = vote[0] # utility for the top only utility_for_top = utility_increments[elected.index(my_best)] # Utility for my top n candidate total_utility = 0.0 for i in range(min(topn, len(vote))): total_utility += utility_increments[elected.index(vote[i])] return utility_for_top, total_utility
[docs] def get_rule_utility(profile: Profile, rule: Callable[[Profile, int], any], topn: int, verbose=False): """ Calculates the total utility and "top n" utility for a given rule. :param profile: The voting profile. :type profile: Profile :param rule: The voting rule function. :type rule: Callable[[int], int],# | float], :param topn: The number of top candidates to consider for utility calculation. :type topn: int :param verbose: Print additional information if True, defaults to False. :type verbose: bool, optional :return: A dictionary containing the total utility for the top candidate and the total utility for top n candidates. :rtype: dict[str, float] """ rule_name = rule.__name__ ranking = profile.ranking(rule) elected_candidates = [c[0] for c in ranking] if verbose: print(f"Ranking based on '{rule_name}' gives {ranking} with winners {elected_candidates}") print("======================================================================") total_u, total_u_n = 0., 0. if verbose: print("Counts \t Ballot \t Utility of first") for pair in profile.pairs: # Utility of the ballot given elected_candidates, multipled by its counts u, u_n = voter_subjective_utility_for_elected_candidate(elected_candidates, pair[1], topn=topn) if verbose: print(f"{pair[0]} \t {pair[1]} \t {u}") total_u += pair[0] * u total_u_n += pair[0] * u_n if verbose: print("Total : ", total_u) return {"top": total_u, "topn": total_u_n}
[docs] def evaluate_voting_rules(num_candidates: int, num_voters: int, topn: int, voters_model: str, distortion_ratio: float = 0.0, verbose: bool = False ) -> dict[str, dict[str, float]]: """ Evaluates various voting rules and returns a dictionary with the results. :param num_candidates: The number of candidates. :type num_candidates: int :param num_voters: The number of voters. :type num_voters: int :param topn: The number of top candidates to consider for utility calculation. :type topn: int :param voters_model: The model used to generate the voter profiles. :type voters_model: str :param distortion_ratio: The distortion rate, defaults to 0.0. :type distortion_ratio: int, optional :param verbose: Print additional information if True, defaults to False. :type verbose: bool, optional :return: A dictionary containing the results for each voting rule. :rtype: dict[str, dict[str, float]] """ profile = get_profile_from_model(num_candidates, num_voters, voters_model) profile.distort(distortion_ratio) if verbose: print(profile.pairs) borda_rule.__name__ = "Borda" copeland_rule.__name__ = "Copeland" dowdall_rule.__name__ = "Dowdall" simpson_rule.__name__ = "Simpson" rules = [borda_rule, copeland_rule, dowdall_rule, simpson_rule] # Adding some extra Borda variants, with decay parameter for gamma in [1.0, 0.99, 0.75, 0.6, 0.25, 0.01]: gamma_rule = get_borda_gamma(gamma) gamma_rule.__name__ = f"Borda Gamma({gamma})" rules.append(gamma_rule) result = {} for rule in rules: result[rule.__name__] = get_rule_utility(profile, rule, topn, verbose) return result