Skip to content

Commit

Permalink
Merge pull request #12 from Kubvv/support-for-multiprofile-analytics
Browse files Browse the repository at this point in the history
Support for multiprofile MES analytics
  • Loading branch information
Simon-Rey authored Feb 29, 2024
2 parents c96d930 + 401c3c5 commit 8b0ae06
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 28 deletions.
20 changes: 14 additions & 6 deletions pabutools/analysis/projectloss.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,13 @@ def calculate_project_loss(
List of :py:class:`~pabutools.analysis.projectloss.ProjectLoss` objects.
"""
if not hasattr(allocation_details, "iterations") or not hasattr(
allocation_details, "initial_budget_per_voter"
if not all(
hasattr(allocation_details, attr)
for attr in ["iterations", "initial_budget_per_voter", "voter_multiplicity"]
):
raise ValueError(
"Provided budget allocation details do not support calculating project loss. The allocation_details "
"should have an 'iterations' and an 'initial_budget_per_voter' attributes."
"should have an 'iterations', 'initial_budget_per_voter' and 'voter_multiplicity' attributes."
)
if len(allocation_details.iterations) == 0:
if verbose:
Expand All @@ -94,6 +95,7 @@ def calculate_project_loss(

project_losses = []
voter_count = len(allocation_details.iterations[0].voters_budget)
voter_multiplicity = allocation_details.voter_multiplicity
voter_spendings: dict[int, list[Tuple[Project, Numeric]]] = {}
for idx in range(voter_count):
voter_spendings[idx] = []
Expand All @@ -108,15 +110,21 @@ def calculate_project_loss(
f"Considering: {iteration.project.name}, status: {iteration.was_picked}"
)
budget_lost = {}
for spending in [voter_spendings[i] for i in iteration.supporter_indices]:
for idx in iteration.supporter_indices:
spending = voter_spendings[idx]
for project, spent in spending:
if project not in budget_lost.keys():
budget_lost[project] = 0
budget_lost[project] = budget_lost[project] + spent
budget_lost[project] = (
budget_lost[project] + spent * voter_multiplicity[idx]
)
project_losses.append(
ProjectLoss(
iteration.project,
sum(current_voters_budget[i] for i in iteration.supporter_indices),
sum(
current_voters_budget[i] * voter_multiplicity[i]
for i in iteration.supporter_indices
),
budget_lost,
)
)
Expand Down
7 changes: 3 additions & 4 deletions pabutools/rules/mes/mes_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ class MESAllocationDetails(AllocationDetails):
A list of all iterations of a MES rule run. It is progressively populated during a MES rule run.
"""

def __init__(self, initial_budget_per_voter: Numeric):
def __init__(self, initial_budget_per_voter: Numeric, voter_multiplicity: list[int]):
super().__init__()
self.initial_budget_per_voter: Numeric = initial_budget_per_voter
self.voter_multiplicity: list[int] = voter_multiplicity
self.iterations: list[MESIteration] = []

def __str__(self):
Expand Down Expand Up @@ -76,10 +77,8 @@ def __init__(
project: Project,
supporter_indices: list[int],
was_picked: bool,
voters_budget=None,
voters_budget: list[int] = [],
):
if voters_budget is None:
voters_budget = []
self.project: Project = project
self.supporter_indices: list[int] = supporter_indices
self.was_picked: bool = was_picked
Expand Down
2 changes: 1 addition & 1 deletion pabutools/rules/mes/mes_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,7 @@ def method_of_equal_shares_scheme(

budget_allocation = BudgetAllocation(
initial_budget_allocation,
MESAllocationDetails(initial_budget_per_voter) if analytics else None,
MESAllocationDetails(initial_budget_per_voter, [voter.multiplicity for voter in voters]) if analytics else None,
)

previous_outcome: BudgetAllocation | list[BudgetAllocation] = budget_allocation
Expand Down
28 changes: 14 additions & 14 deletions tests/test_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ def test_profile_properties(self):
assert median_total_score(instance, card_multi_profile) == 3

def test_project_loss(self):
projects = [Project(chr(ord("a") + idx), 2) for idx in range(6)]
projects = [Project(chr(ord("a") + idx), 4) for idx in range(6)]
supporters = [[0, 1, 2, 4], [2, 3, 4], [0, 2], [0, 1], [4], [5]]
was_picked = [True, True, False, False, False, False]
voters_budget = [
Expand All @@ -280,28 +280,28 @@ def test_project_loss(self):
)
for idx in range(len(projects))
]
allocation_details = MESAllocationDetails(initial_budget_per_voter)
allocation_details = MESAllocationDetails(
initial_budget_per_voter,
[2 for _ in range(len(voters_budget[0]))],
)
allocation_details.iterations = iterations

project_losses = calculate_project_loss(allocation_details)
expected_budgets = [
frac(4, 1),
frac(2, 1),
frac(1, 2),
frac(1, 1),
frac(0, 1),
frac(1, 1),
]
expected_budgets = [8, 4, 1, 2, 0, 2]
expected_losses = [
{},
{projects[0]: frac(1, 1)},
{projects[0]: frac(1, 1), projects[1]: frac(1, 2)},
{projects[0]: frac(1, 1)},
{projects[0]: frac(1, 2), projects[1]: frac(1, 2)},
{projects[0]: 2},
{projects[0]: 2, projects[1]: 1},
{projects[0]: 2},
{projects[0]: 1, projects[1]: 1},
{},
]

for idx, project_loss in enumerate(project_losses):
assert project_loss.name == projects[idx].name
assert project_loss.supporters_budget == expected_budgets[idx]
assert project_loss.budget_lost == expected_losses[idx]

# No iterations
project_losses = calculate_project_loss(MESAllocationDetails(1, [1]))
assert project_losses == []
26 changes: 23 additions & 3 deletions tests/test_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,16 @@ def mes_phragmen(instance, profile, resoluteness=True):
[frac(1, 2), frac(1, 2), frac(1, 2), frac(1, 2), frac(1, 2)],
[frac(1, 2), frac(1, 2), frac(1, 2), frac(1, 2), frac(1, 2)],
),
(
[5, 1, 2, 1, 2],
[0, 1, 2],
[0, 1, 2],
[1, 3],
[frac(1, 2), frac(1, 4), frac(1, 4), frac(1, 4), frac(1, 4)],
[frac(1, 2), frac(1, 4), frac(1, 4), frac(1, 4), frac(1, 4)],
True,
[2, 1, 2, 1, 2, 2],
),
]
)
def test_mes_analytics(
Expand All @@ -735,6 +745,8 @@ def test_mes_analytics(
picked_projects_idxs,
expected_third_voter_budget,
expected_fourth_voter_budget,
multiprofile=False,
expected_multiplicity=[1 for _ in range(10)],
):
projects = [Project(chr(ord("a") + idx), costs[idx]) for idx in range(0, 5)]
instance = Instance(projects, budget_limit=5)
Expand All @@ -752,21 +764,29 @@ def test_mes_analytics(
ApprovalBallot({projects[4]}),
]
)
if multiprofile:
profile = profile.as_multiprofile()
result = method_of_equal_shares(instance, profile, Cost_Sat, analytics=True)

assert sorted(list(result), key=lambda proj: proj.name) == [
projects[idx] for idx in picked_projects_idxs
]
assert result.details.initial_budget_per_voter == frac(1, 2)
assert result.details.voter_multiplicity == expected_multiplicity

check_voters = [2, 2, 5] if multiprofile else [3, 4, 8]
for idx, anl in enumerate(
sorted(result.details.iterations, key=lambda iter: iter.project.name)
):
assert anl.project.name == projects[idx].name
assert anl.was_picked == (idx in picked_projects_idxs)
assert anl.voters_budget[3] == expected_third_voter_budget[idx]
assert anl.voters_budget[4] == expected_fourth_voter_budget[idx]
assert anl.voters_budget[8] == frac(1, 2)
assert (
anl.voters_budget[check_voters[0]] == expected_third_voter_budget[idx]
)
assert (
anl.voters_budget[check_voters[1]] == expected_fourth_voter_budget[idx]
)
assert anl.voters_budget[check_voters[2]] == frac(1, 2)

def test_mes_analytics_irresolute(self):
projects = [Project(chr(ord("a") + idx), 3) for idx in range(0, 3)]
Expand Down

0 comments on commit 8b0ae06

Please sign in to comment.