Skip to content

Commit 8b0ae06

Browse files
authored
Merge pull request #12 from Kubvv/support-for-multiprofile-analytics
Support for multiprofile MES analytics
2 parents c96d930 + 401c3c5 commit 8b0ae06

File tree

5 files changed

+55
-28
lines changed

5 files changed

+55
-28
lines changed

pabutools/analysis/projectloss.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,13 @@ def calculate_project_loss(
8080
List of :py:class:`~pabutools.analysis.projectloss.ProjectLoss` objects.
8181
8282
"""
83-
if not hasattr(allocation_details, "iterations") or not hasattr(
84-
allocation_details, "initial_budget_per_voter"
83+
if not all(
84+
hasattr(allocation_details, attr)
85+
for attr in ["iterations", "initial_budget_per_voter", "voter_multiplicity"]
8586
):
8687
raise ValueError(
8788
"Provided budget allocation details do not support calculating project loss. The allocation_details "
88-
"should have an 'iterations' and an 'initial_budget_per_voter' attributes."
89+
"should have an 'iterations', 'initial_budget_per_voter' and 'voter_multiplicity' attributes."
8990
)
9091
if len(allocation_details.iterations) == 0:
9192
if verbose:
@@ -94,6 +95,7 @@ def calculate_project_loss(
9495

9596
project_losses = []
9697
voter_count = len(allocation_details.iterations[0].voters_budget)
98+
voter_multiplicity = allocation_details.voter_multiplicity
9799
voter_spendings: dict[int, list[Tuple[Project, Numeric]]] = {}
98100
for idx in range(voter_count):
99101
voter_spendings[idx] = []
@@ -108,15 +110,21 @@ def calculate_project_loss(
108110
f"Considering: {iteration.project.name}, status: {iteration.was_picked}"
109111
)
110112
budget_lost = {}
111-
for spending in [voter_spendings[i] for i in iteration.supporter_indices]:
113+
for idx in iteration.supporter_indices:
114+
spending = voter_spendings[idx]
112115
for project, spent in spending:
113116
if project not in budget_lost.keys():
114117
budget_lost[project] = 0
115-
budget_lost[project] = budget_lost[project] + spent
118+
budget_lost[project] = (
119+
budget_lost[project] + spent * voter_multiplicity[idx]
120+
)
116121
project_losses.append(
117122
ProjectLoss(
118123
iteration.project,
119-
sum(current_voters_budget[i] for i in iteration.supporter_indices),
124+
sum(
125+
current_voters_budget[i] * voter_multiplicity[i]
126+
for i in iteration.supporter_indices
127+
),
120128
budget_lost,
121129
)
122130
)

pabutools/rules/mes/mes_details.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ class MESAllocationDetails(AllocationDetails):
2323
A list of all iterations of a MES rule run. It is progressively populated during a MES rule run.
2424
"""
2525

26-
def __init__(self, initial_budget_per_voter: Numeric):
26+
def __init__(self, initial_budget_per_voter: Numeric, voter_multiplicity: list[int]):
2727
super().__init__()
2828
self.initial_budget_per_voter: Numeric = initial_budget_per_voter
29+
self.voter_multiplicity: list[int] = voter_multiplicity
2930
self.iterations: list[MESIteration] = []
3031

3132
def __str__(self):
@@ -76,10 +77,8 @@ def __init__(
7677
project: Project,
7778
supporter_indices: list[int],
7879
was_picked: bool,
79-
voters_budget=None,
80+
voters_budget: list[int] = [],
8081
):
81-
if voters_budget is None:
82-
voters_budget = []
8382
self.project: Project = project
8483
self.supporter_indices: list[int] = supporter_indices
8584
self.was_picked: bool = was_picked

pabutools/rules/mes/mes_rule.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,7 @@ def method_of_equal_shares_scheme(
564564

565565
budget_allocation = BudgetAllocation(
566566
initial_budget_allocation,
567-
MESAllocationDetails(initial_budget_per_voter) if analytics else None,
567+
MESAllocationDetails(initial_budget_per_voter, [voter.multiplicity for voter in voters]) if analytics else None,
568568
)
569569

570570
previous_outcome: BudgetAllocation | list[BudgetAllocation] = budget_allocation

tests/test_analysis.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ def test_profile_properties(self):
261261
assert median_total_score(instance, card_multi_profile) == 3
262262

263263
def test_project_loss(self):
264-
projects = [Project(chr(ord("a") + idx), 2) for idx in range(6)]
264+
projects = [Project(chr(ord("a") + idx), 4) for idx in range(6)]
265265
supporters = [[0, 1, 2, 4], [2, 3, 4], [0, 2], [0, 1], [4], [5]]
266266
was_picked = [True, True, False, False, False, False]
267267
voters_budget = [
@@ -280,28 +280,28 @@ def test_project_loss(self):
280280
)
281281
for idx in range(len(projects))
282282
]
283-
allocation_details = MESAllocationDetails(initial_budget_per_voter)
283+
allocation_details = MESAllocationDetails(
284+
initial_budget_per_voter,
285+
[2 for _ in range(len(voters_budget[0]))],
286+
)
284287
allocation_details.iterations = iterations
285288

286289
project_losses = calculate_project_loss(allocation_details)
287-
expected_budgets = [
288-
frac(4, 1),
289-
frac(2, 1),
290-
frac(1, 2),
291-
frac(1, 1),
292-
frac(0, 1),
293-
frac(1, 1),
294-
]
290+
expected_budgets = [8, 4, 1, 2, 0, 2]
295291
expected_losses = [
296292
{},
297-
{projects[0]: frac(1, 1)},
298-
{projects[0]: frac(1, 1), projects[1]: frac(1, 2)},
299-
{projects[0]: frac(1, 1)},
300-
{projects[0]: frac(1, 2), projects[1]: frac(1, 2)},
293+
{projects[0]: 2},
294+
{projects[0]: 2, projects[1]: 1},
295+
{projects[0]: 2},
296+
{projects[0]: 1, projects[1]: 1},
301297
{},
302298
]
303299

304300
for idx, project_loss in enumerate(project_losses):
305301
assert project_loss.name == projects[idx].name
306302
assert project_loss.supporters_budget == expected_budgets[idx]
307303
assert project_loss.budget_lost == expected_losses[idx]
304+
305+
# No iterations
306+
project_losses = calculate_project_loss(MESAllocationDetails(1, [1]))
307+
assert project_losses == []

tests/test_rule.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,16 @@ def mes_phragmen(instance, profile, resoluteness=True):
725725
[frac(1, 2), frac(1, 2), frac(1, 2), frac(1, 2), frac(1, 2)],
726726
[frac(1, 2), frac(1, 2), frac(1, 2), frac(1, 2), frac(1, 2)],
727727
),
728+
(
729+
[5, 1, 2, 1, 2],
730+
[0, 1, 2],
731+
[0, 1, 2],
732+
[1, 3],
733+
[frac(1, 2), frac(1, 4), frac(1, 4), frac(1, 4), frac(1, 4)],
734+
[frac(1, 2), frac(1, 4), frac(1, 4), frac(1, 4), frac(1, 4)],
735+
True,
736+
[2, 1, 2, 1, 2, 2],
737+
),
728738
]
729739
)
730740
def test_mes_analytics(
@@ -735,6 +745,8 @@ def test_mes_analytics(
735745
picked_projects_idxs,
736746
expected_third_voter_budget,
737747
expected_fourth_voter_budget,
748+
multiprofile=False,
749+
expected_multiplicity=[1 for _ in range(10)],
738750
):
739751
projects = [Project(chr(ord("a") + idx), costs[idx]) for idx in range(0, 5)]
740752
instance = Instance(projects, budget_limit=5)
@@ -752,21 +764,29 @@ def test_mes_analytics(
752764
ApprovalBallot({projects[4]}),
753765
]
754766
)
767+
if multiprofile:
768+
profile = profile.as_multiprofile()
755769
result = method_of_equal_shares(instance, profile, Cost_Sat, analytics=True)
756770

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

777+
check_voters = [2, 2, 5] if multiprofile else [3, 4, 8]
762778
for idx, anl in enumerate(
763779
sorted(result.details.iterations, key=lambda iter: iter.project.name)
764780
):
765781
assert anl.project.name == projects[idx].name
766782
assert anl.was_picked == (idx in picked_projects_idxs)
767-
assert anl.voters_budget[3] == expected_third_voter_budget[idx]
768-
assert anl.voters_budget[4] == expected_fourth_voter_budget[idx]
769-
assert anl.voters_budget[8] == frac(1, 2)
783+
assert (
784+
anl.voters_budget[check_voters[0]] == expected_third_voter_budget[idx]
785+
)
786+
assert (
787+
anl.voters_budget[check_voters[1]] == expected_fourth_voter_budget[idx]
788+
)
789+
assert anl.voters_budget[check_voters[2]] == frac(1, 2)
770790

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

0 commit comments

Comments
 (0)