Source code for mapof.elections.objects.ApprovalElection

import logging
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
from mapof.elections.objects.Microscope import Microscope


[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 set_microscope( self, radius: float = None, alpha: float = 0.1, s=30, object_type=None, double_gradient=False, color='blue', marker='o', annotate: bool = False ): """Print a map of the election (i.e., microscope) using matplotlib's OO API.""" if object_type is None: object_type = self.object_type fig, ax = plt.subplots(figsize=(6.4, 6.4)) X = [elem[0] for elem in self.coordinates[object_type]] Y = [elem[1] for elem in self.coordinates[object_type]] start = False if start: ax.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]) ax.scatter(X[i], Y[i], color=[0, y, x], s=s, alpha=alpha) else: ax.scatter(X, Y, color=color, s=s, alpha=alpha, marker=marker) if annotate: for i in range(len(X)): ax.annotate(i, (X[i], Y[i]), color='black') avg_x = np.mean(X) avg_y = np.mean(Y) if radius: ax.set_xlim([avg_x - radius, avg_x + radius]) ax.set_ylim([avg_y - radius, avg_y + radius]) ax.axis('off') plt.close(fig) self.microscope = Microscope(fig, ax, self.experiment_id, self.label, object_type) return self.microscope