Source code for mapof.elections.objects.ApprovalElection

import logging
import os
from abc import ABC
from collections import Counter

import numpy as np
from mapof.core.distances import hamming
from matplotlib import pyplot as plt

import mapof.elections.persistence.election_exports as exports
import mapof.elections.persistence.election_imports as imports
from mapof.elections.cultures import generate_approval_votes
from mapof.elections.cultures.params import update_params_approval
from mapof.elections.objects.Election import Election


[docs] class ApprovalElection(Election, ABC): """ Approval Election class. """ def __init__(self, experiment_id=None, election_id=None, culture_id=None, num_candidates=None, fast_import=False, params=None, **kwargs): super().__init__(experiment_id=experiment_id, election_id=election_id, culture_id=culture_id, num_candidates=num_candidates, instance_type='approval', fast_import=fast_import, params=params, **kwargs) self.approvalwise_vector = [] self.reverse_approvals = [] self.candidatelikeness_original_vectors = [] if self.is_imported and self.experiment_id is not None and not fast_import: self.import_approval_election() self.try_updating_params()
[docs] def import_approval_election(self) -> None: """ Imports approval elections from a file. Returns ------- None """ try: ( self.votes, self.num_voters, self.num_candidates, self.params, self.culture_id, self.num_options, self.quantities, self.distinct_votes, ) = imports.import_approval_election( experiment_id=self.experiment_id, election_id=self.election_id, is_shifted=self.is_shifted ) except Exception: logging.warning(f'Could not import instance {self.election_id}.')
def try_updating_params(self): if self.culture_id is not None: self.params = update_params_approval( self.params, self.culture_id, )
[docs] def votes_to_approvalwise_vector(self) -> None: """ Converts votes to approvalwise vectors. Returns ------- None """ approvalwise_vector = np.zeros([self.num_candidates]) for vote in self.votes: for c in vote: approvalwise_vector[c] += 1 approvalwise_vector = approvalwise_vector / self.num_voters self.approvalwise_vector = np.sort(approvalwise_vector)
[docs] def compute_reverse_approvals(self) -> None: """ Computes the reverse approvals. Returns ------- None """ self.reverse_approvals = [set(i for i, vote in enumerate(self.votes) if c in vote) for c in range(self.num_candidates)]
[docs] def get_reverse_approvals(self) -> list[set]: """ Returns the reverse approvals. Additionally, if they are not computed, it computes them. Returns ------- list[set] The reverse approvals. """ if self.reverse_approvals is None or self.reverse_approvals == []: self.compute_reverse_approvals() return self.reverse_approvals
[docs] def prepare_instance(self, is_exported=False, is_aggregated=True) -> None: """ Prepares all the instances within the experiment. Parameters ---------- is_exported : bool is_aggregated : bool Returns ------- None """ self.votes = generate_approval_votes(culture_id=self.culture_id, num_candidates=self.num_candidates, num_voters=self.num_voters, params=self.params) if not self.is_pseudo: c = Counter(map(tuple, self.votes)) counted_votes = [[count, list(row)] for row, count in c.items()] counted_votes = sorted(counted_votes, reverse=True) self.quantities = [a[0] for a in counted_votes] self.distinct_votes = [a[1] for a in counted_votes] self.num_options = len(counted_votes) else: self.quantities = [self.num_voters] self.num_options = 1 if is_exported: exports.export_election_within_experiment(self, is_aggregated=is_aggregated)
def _compute_distances_between_votes(self, distance_id: str = 'hamming') -> np.ndarray: """ Computes distances between the votes. Parameters ---------- distance_id : str Name of the distance. Returns ------- np.ndarray Distances. """ distances = np.zeros([self.num_voters, self.num_voters]) for v1 in range(self.num_voters): for v2 in range(self.num_voters): if distance_id == 'hamming': distances[v1][v2] = hamming(self.votes[v1], self.votes[v2]) elif distance_id == 'jaccard': if len(self.votes[v1].union(self.votes[v2])) == 0: distances[v1][v2] = 1 else: distances[v1][v2] = 1 - len( self.votes[v1].intersection(self.votes[v2])) / len( self.votes[v1].union(self.votes[v2])) self.distances['vote'] = distances if self.is_exported: exports.export_distances(self, object_type='vote') return distances def _compute_distances_between_candidates(self, distance_id='hamming') -> np.ndarray: """ Computes distances between the candidates. Parameters ---------- distance_id : str Name of the distance. Returns ------- np.ndarray Distances. """ self.compute_reverse_approvals() distances = np.zeros([self.num_candidates, self.num_candidates]) for c1 in range(self.num_candidates): for c2 in range(self.num_candidates): if distance_id == 'hamming': distances[c1][c2] = hamming(self.reverse_approvals[c1], self.reverse_approvals[c2]) elif distance_id == 'jaccard': if len(self.reverse_approvals[c1].union(self.reverse_approvals[c2])) == 0: distances[c1][c2] = 1 else: distances[c1][c2] = 1 - len( self.reverse_approvals[c1].intersection(self.reverse_approvals[c2])) \ / len( self.reverse_approvals[c1].union(self.reverse_approvals[c2])) self.distances['candidate'] = distances if self.is_exported: exports.export_distances(self, object_type='candidate') return distances def get_candidatelikeness_original_vectors(self, is_recomputed=False): if self.candidatelikeness_original_vectors is not None \ and len(self.candidatelikeness_original_vectors) > 0 \ and not is_recomputed: return self.candidatelikeness_original_vectors return self._voted_to_candidatelikeness_original_vectors() def _voted_to_candidatelikeness_original_vectors(self): """ Converts votes to candidate-likeness vectors """ matrix = np.zeros([self.num_candidates, self.num_candidates]) for c_1 in range(self.num_candidates): for c_2 in range(self.num_candidates): for vote in self.votes: if (c_1 in vote and c_2 not in vote) or (c_1 not in vote and c_2 in vote): matrix[c_1][c_2] += 1 candidatelikeness_original_vectors = matrix / self.num_voters self.candidatelikeness_original_vectors = candidatelikeness_original_vectors return candidatelikeness_original_vectors
[docs] def compute_distances(self, object_type=None, distance_id: str = 'hamming') -> np.ndarray: """ Computes distances between the votes or candidates. """ if object_type is None: object_type = self.object_type if object_type == 'vote': return self._compute_distances_between_votes(distance_id=distance_id) elif object_type == 'candidate': return self._compute_distances_between_candidates(distance_id=distance_id)
[docs] def print_map( self, show: bool = True, radius: float = None, name: str = None, alpha: float = 0.1, s=30, object_type=None, double_gradient=False, saveas=None, color='blue', marker='o', annotate: bool = False ): """ Print a map of the election (i.e., microscope). """ if object_type is None: object_type = self.object_type if object_type is None: object_type = self.object_type plt.figure(figsize=(6.4, 6.4)) X = [] Y = [] for elem in self.coordinates[object_type]: X.append(elem[0]) Y.append(elem[1]) start = False if start: plt.scatter(X[0], Y[0], color='sienna', s=1000, alpha=1, marker='X') if double_gradient: for i in range(len(X)): x = float(self.points['voters'][i][0]) y = float(self.points['voters'][i][1]) plt.scatter(X[i], Y[i], color=[0, y, x], s=s, alpha=alpha) else: plt.scatter(X, Y, color=color, s=s, alpha=alpha, marker=marker) if annotate: for i in range(len(X)): plt.annotate(i, (X[i], Y[i]), color='black') avg_x = np.mean(X) avg_y = np.mean(Y) if radius: plt.xlim([avg_x - radius, avg_x + radius]) plt.ylim([avg_y - radius, avg_y + radius]) plt.axis('off') if saveas is not None: if saveas == 'default': saveas = f'{self.label}_{object_type}' file_name = os.path.join(os.getcwd(), "images", name, f'{saveas}.png') plt.savefig(file_name, bbox_inches='tight', dpi=100) if show: plt.show() else: plt.clf() plt.close()