Source code for pabutools.analysis.mesanalytics

from __future__ import annotations

from pabutools.utils import Numeric
from pabutools.election.instance import Instance, Project
from pabutools.election.profile import Profile
from pabutools.rules.budgetallocation import BudgetAllocation, AllocationDetails
from pabutools.rules.mes.mes_rule import method_of_equal_shares


[docs] class ProjectLoss(Project): """ Class used to represent the projects and how much budget they lost due to other projects being picked. This extends the :py:class:`~pabutools.election.instance.Project` and thus represents the project itself. Parameters ---------- project: :py:class:`~pabutools.election.instance.Project` Project for which analytics is calculated. supporters_budget: :py:class:`~pabutools.utils.Numeric` The collective budget of the project supporters when project was considered by a rule. budget_lost: dict[:py:class:`~pabutools.election.instance.Project`, :py:class:`~pabutools.utils.Numeric`] Describes the amount of budget project supporters spent on other projects prior to this projects' consideration. Attributes ---------- supporters_budget: :py:class:`~pabutools.utils.Numeric` The collective budget of the project supporters when project was considered by rule. budget_lost: dict[:py:class:`~pabutools.election.instance.Project`, :py:class:`~pabutools.utils.Numeric`] Describes the amount of budget project supporters spent on other projects prior to this projects' consideration. """ def __init__( self, project: Project, supporters_budget: Numeric, budget_lost: dict[Project, Numeric], ): Project.__init__( self, project.name, project.cost, project.categories, project.targets ) self.supporters_budget: Numeric = supporters_budget self.budget_lost: dict[Project, Numeric] = budget_lost
[docs] def total_budget_lost(self) -> Numeric: """ Returns the total budget spent by project supporters on prior projects. Returns ------- :py:class:`~pabutools.utils.Numeric` The total budget spent. """ return sum(self.budget_lost.values())
def __str__(self): return f"ProjectLoss[{self.name}, {float(self.supporters_budget)}, {self.total_budget_lost()}]" def __repr__(self): return f"ProjectLoss[{self.name}, {float(self.supporters_budget)}, {self.total_budget_lost()}]"
[docs] def calculate_project_loss( allocation_details: AllocationDetails, verbose: bool = False, ) -> list[ProjectLoss]: """ Returns a list of :py:class:`~pabutools.analysis.mesanalytics.ProjectLoss` objects for the projects. Parameters ---------- allocation_details: :py:class:`~pabutools.rules.budgetallocation.AllocationDetails` The details of the budget allocation considered. verbose: bool, optional (De)Activate the display of additional information. Defaults to `False`. Returns ------- list[:py:class:`~pabutools.analysis.mesanalytics.ProjectLoss`] List of :py:class:`~pabutools.analysis.mesanalytics.ProjectLoss` objects. """ if not hasattr(allocation_details, "iterations"): raise ValueError( "Provided budget allocation details do not support calculating project loss. The allocation_details " "should have an 'iterations', and 'voter_multiplicity' attributes." ) if len(allocation_details.iterations) == 0: if verbose: print(f"Rule did not have any iterations, returning empty list") return [] project_losses = [] voter_count = len(allocation_details.iterations[0].voters_budget) voter_multiplicity = allocation_details.voter_multiplicity iterations_count = len(allocation_details.iterations) voter_spendings: dict[int, list[tuple[Project, Numeric]]] = {} for idx in range(voter_count): voter_spendings[idx] = [] for idx, iteration in enumerate(allocation_details.iterations): project_losses.append( _create_project_loss( iteration.selected_project, iteration.voters_budget, voter_spendings, voter_multiplicity, verbose, ) ) for supporter_idx in iteration.selected_project.supporter_indices: voter_spendings[supporter_idx].append( ( iteration.selected_project, iteration.voters_budget[supporter_idx] - iteration.voters_budget_after_selection[supporter_idx], ) ) for project_detail in iteration: if project_detail.discarded or ( idx == iterations_count - 1 and project_detail.project != iteration.selected_project ): project_losses.append( _create_project_loss( project_detail.project, iteration.voters_budget_after_selection, voter_spendings, voter_multiplicity, verbose, ) ) return project_losses
[docs] def calculate_effective_supports( instance: Instance, profile: Profile, allocation: BudgetAllocation, mes_params: dict | None = None, final_budget: Numeric | None = None, ) -> dict[Project, int]: """ Returns a dictionary of :py:class:`~pabutools.election.instance.project` and their effective support in a given instance, profile and mes election. Effective support for a project is an analytical metric which allows to measure the ratio of initial budget received to minimal budget required to win. Effective support is represented in percentages. Parameters ---------- instance: :py:class:`~pabutools.election.instance.Instance` The instance. profile: :py:class:`~pabutools.election.profile.profile.AbstractProfile` The profile. allocation: :py:class:`~pabutools.rules.budgetallocation.BudgetAllocation` Resulting allocation of the above instance & profile. mes_params: dict, optional Dictionary of additional parameters that are passed as keyword arguments to the MES rule. Defaults to None. final_budget: Numeric, optional Numeric value of the final budget which will replace the instance budget. Allows for simulating exhaustive MES. Returns ------- dict[:py:class:`~pabutools.election.instance.Project`, int] Dictionary of pairs (:py:class:`~pabutools.election.instance.Project`, effective support). """ if mes_params is None: mes_params = {} effective_supports: dict[Project, int] = {} if final_budget: instance.budget_limit = final_budget for project in instance: effective_supports[project] = calculate_effective_support( instance, profile, project, project in allocation, mes_params ) return effective_supports
[docs] def calculate_effective_support( instance: Instance, profile: Profile, project: Project, was_picked: bool, mes_params: dict | None = None, ) -> int: """ Calculates the effective support of a given project in a given instance, profile and mes election. Effective support for a project is an analytical metric which allows to measure the ratio of initial budget received to minimal budget required to win. Effective support is represented in percentages. Parameters ---------- instance: :py:class:`~pabutools.election.instance.Instance` The instance. profile : :py:class:`~pabutools.election.profile.profile.AbstractProfile` The profile. project: :py:class:`~pabutools.election.instance.Project` Project for which effective support is calculated. Must be a part of the instance. was_picked: bool Whether the considerd project was picked as a winner in the allocation. mes_params: dict, optional Dictionary of additional parameters that are passed as keyword arguments to the MES rule. Defaults to `{}`. Returns ------- int The effective support value in percentages for project. """ if project not in instance: raise RuntimeError("Provided instance does not match the allocation details") if mes_params is None: mes_params = {} mes_params["analytics"] = True mes_params["skipped_project"] = project mes_params["resoluteness"] = True details = method_of_equal_shares(instance, profile, **mes_params).details effective_support = details.skipped_project_eff_support if was_picked: effective_support = max(effective_support, 100) return effective_support
def _create_project_loss( project: Project, current_voters_budget: list[Numeric], voter_spendings: dict[int, list[tuple[Project, Numeric]]], voter_multiplicity: list[int], verbose: bool = False, ) -> ProjectLoss: if verbose: print(f"Considering: {project.name}") budget_lost = {} for idx in project.supporter_indices: spending = voter_spendings[idx] for prev_project, spent in spending: if prev_project not in budget_lost.keys(): budget_lost[prev_project] = 0 budget_lost[prev_project] = ( budget_lost[prev_project] + spent * voter_multiplicity[idx] ) return ProjectLoss( project, sum( current_voters_budget[i] * voter_multiplicity[i] for i in project.supporter_indices ), budget_lost, )