Add pabutools.recommendation: a recommendation system for participatory budgeting - #72
Add pabutools.recommendation: a recommendation system for participatory budgeting#72roeiyanku wants to merge 17 commits into
Conversation
Implements the function headers (with doctests and empty bodies) and the pytest test suite for the algorithms in "A Recommendation System for Participatory Budgeting" (Leibiker & Talmon, 2023), integrating with the pabutools data structures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Represent three-state partial ballots with pabutools' CardinalBallot (+1 approve / -1 disapprove / 0 hidden) instead of a new ballot type - Split model training into a separate, ballot-agnostic module (pb_model_training.py); ML libs are an optional "recommendation" extra imported lazily - Reuse pabutools where the primitive exists; keep empty stubs for this stage - Samplers return the exposed set E_v as set[Project]; random_setup drops the unused ballots argument; remove symmetric_distance (per instructor) - Use the paper's vocabulary throughout (Learning/Target Voters, full/partial ballot, ideal instance, A_v/D_v/E_v/H_v) and update the unit tests Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The predict_by_classification/matrix_factorization/factorization_machines bodies still called the train_* functions; that is real code, not an empty implementation. Remove those calls (and the now-unused pb_model_training import) so every function body is an empty stub and all tests fail, as the assignment requires. The predict/train relationship stays documented in the docstrings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the APPROVAL/DISAPPROVAL/HIDDEN module constants with a comment documenting the +1/-1/0 convention, so the file contains only headers, empty implementations, and documentation - no concrete code. Update the tests to use the literal scores instead of importing the constants. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove the lazy import guards (try/except ImportError for xgboost/surprise/ lightfm) from the train_* bodies - that was real code. Each body is now just `return None # Empty implementation`. The optional-dependency behaviour stays documented in the docstrings for the later implementation stage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Partial-ballot layer: build/decode the +1/-1/0 CardinalBallot convention - Definitions 2.1/2.2 (approval scores via pabutools, consensus = |2*score-n|) - Greedy approval by raw score with lexicographic tie-breaking (Section 2.2.5) - Sampling modules: random, offline popularity/consensus/controversiality (top/bottom-k of the sigma/gamma orderings), online adaptive-controversial - Prediction: LV-majority, XGBoost per-project classification, and matrix-factorization / factorization-machines via a shared NumPy SVD core (scikit-surprise and lightfm wheels do not build on this Windows + NumPy 2 environment; the substitution is documented in the docstrings) - Full pipeline (sampling -> prediction -> greedy) and the FA score (Def 5.1) - INFO/DEBUG logging narrating every algorithm; helper functions with doctests All 39 pytest tests and 102 doctests pass. Validated end-to-end on the paper's Bielany 2020 Pabulib dataset, reproducing the paper's qualitative findings.
- greedy_approval now delegates to greedy_utilitarian_welfare with Cost_Sat: marginal satisfaction under Cost_Sat is score(p)*cost(p), so dividing by the cost ranks by the raw approval score - exactly the paper's greedy approval (verified equivalent, incl. tie-breaking, on 30 random instances) - Delete approval_scores: Definition 2.1 is exactly the library's profile.approval_scores(); callers use it directly - Delete most_popular_projects: the sigma ordering is inlined into its only remaining caller, offline_popularity - Drop the TestApprovalScores class (the library tests its own method) 35 tests + doctests pass.
…tation
- partial_ballot: one dict-union expression instead of three loops
- reveal_ballot: single partial_ballot call on E&full, E-full, P-E
- predict_by_majority and _approve_hidden_by_score: the completion is now
literally A_v | {p in H_v : condition(p)} as in the paper
- predict_by_classification: per-project votes as a dict comprehension,
numpy import hoisted to the module level
- online_adaptive_controversial: min over set(instance)-exposed directly
- recommend: the sampling->prediction step is one list comprehension
Per-item DEBUG logs are folded into the per-function INFO summaries.
35 tests + doctests pass.
…trix Full-implementation stage of "A Recommendation System for Participatory Budgeting" (Leibiker & Talmon 2023), experiment side (Sections 3, 5, 6): - split_lv_tv: Step 1, partition the ideal profile into Learning/Target Voters by the paper's sample-degree and LV-degree knobs (Section 3.0.1). - run_pipeline (renamed from recommend): one cell of the design matrix; setup and predict are now required keyword arguments. - exposed_sets: one interface over the five Section 3.1 setups. - run_all_experiments: the full Section 6 treatment matrix (setups x predictors x sample degrees x LV degrees, n_repeat random splits averaged), reporting FA and inline Symmetric Distance per cell. - classification_metrics: Section 5.1 precision/recall/F1 over the hidden projects, dependency-free. - Remove predict_by_majority (not one of the paper's predictors) and its tests; PREDICTORS is now exactly classification/MF/FM. - fractional_allocation_score accepts any Iterable[Project], so the BudgetAllocation from greedy_approval can be passed as is. - Fix the [recommendation] extra: add scikit-learn (XGBClassifier needs xgboost's sklearn API), drop unused scikit-surprise/lightfm whose wheels do not build on Windows + NumPy 2. - Add the paper link to every file header per the submission rules. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A structured two-camp electorate (60% majority approving {p1,p2,p3},
40% minority approving {p4,p5,p6}) fixes the real winning bundle; a
third of the voters become Target Voters with only k=2 of 6 projects
exposed. Every combination of the 5 sampling setups and 3 prediction
modules must reconstruct the exact real bundle (FA = 1.0, SD = 0) -
the paper's own success criterion (Section 2.4 / 5.2). 15 new cases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Running "python pb_recommendation.py" now shows, like the course examples: the doctest summary, the full DEBUG/INFO log trail of one end-to-end pipeline run on a two-camp electorate (split -> sampling -> per-voter prediction -> greedy approval -> FA score), and a validation failure demo. exposed_sets validates its inputs: an unknown setup name or a k outside [0, m] raises a ValueError naming the problem (was a raw KeyError before). Covered by doctests and two pytest cases; also fix the stale "recommend:" log prefix left from the run_pipeline rename. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The module's __main__ goes back to running only the doctests. The ValueError validation in exposed_sets (unknown setup / k out of range) stays, with its doctests and pytest coverage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
run_pipeline now logs when it starts (setup, predictor, k, LV/TV counts) and when all TV ballots are completed, so the log trail reads chronologically. run_all_experiments announces the sweep dimensions and the ground-truth bundle up front, logs every repeat's FA/SD at DEBUG, and labels the per-cell INFO line as a mean over the repeats. The iterable parameters are materialised as tuples first, so passing generators no longer exhausts them before the sweep. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Updated test cases by removing example1 fixture and several tests related to greedy approval and other functionalities. Adjusted edge case descriptions and cleaned up unused code.
…ry budgeting Implements "A Recommendation System for Participatory Budgeting" by Gil Leibiker and Nimrod Talmon (AAMAS 2023), https://optlearnmas23.github.io/files/p17.pdf The module tackles the paper's information overload problem: instead of asking every voter about every project, it queries each voter for a partial ballot, estimates the votes it did not ask for, and runs the voting rule on the completion. Contents: * the five sampling setups of Section 3.1, plus next_adaptive_question so the online adaptive setup can drive a live process rather than only a simulation * the three prediction modules of Section 2.1: binary classification with XGBoost, matrix factorization, and factorization machines * the LV/TV partiality arithmetic of Section 3.0.1, greedy approval, elect for running a real process, and the evaluation metrics of Section 5 * 43 tests in tests/test_recommendation.py, plus doctests throughout The machine-learning libraries are an optional "recommendation" extra and are imported lazily, so the module imports and every non-ML algorithm works without them; tests that need to fit a model skip rather than fail. Two points where the paper is under-specified or inconsistent are documented in the docstrings with the reading implemented: Section 2.1.1 never defines what a training row is, and the Section 5.2.1 example disagrees with its own definition of symmetric distance.
|
Hey :) Thank you for your contribution. I don't fully understand what the code offers, could you explain to me how a user would use these functions and what they would provide? |
Every other module in the library has a narrative usage page alongside its API reference; this one only had the reference. The page walks through the two ways the module is used - running a process from partial ballots with elect, and measuring the loss on a known profile with run_experiment - with runnable examples and cross-references into the reference pages. Building the docs to check it also surfaced two markup errors in the module's own docstrings, fixed here: * |LV| and |TV| are substitution syntax in reStructuredText, so the cardinality notation in plan_sampling and split_lv_tv raised "Undefined substitution referenced" and never rendered; they are literals now. * the cross-reference to greedy_utilitarian_welfare pointed at the re-export path rather than pabutools.rules.greedywelfare, where Sphinx documents it.
|
Thank you for the quick response! I'll explain below and also add an rst file. In short: it lets a PB election run without asking every voter about every project. You ask each person about a handful, estimate the votes you never asked for, and the usual voting rule takes it from there. (For context, this came out of #58, which Erel opened for our Research Algorithms course.) There are two flows. Running an actual PB process where you cannot ask everyone about everything. You hold the ballots of the people who did fill one in completely, you choose which projects to put to everyone else, collect their partial answers, and get an outcome: asked = offline_controversiality(instance, lv_profile, k=2) {Library, Shade} <- the two projects the complete ballots most disagree aboutanswers = { elect(instance, lv_profile, answers, predictor="classification") BudgetAllocation([Garden, Shade])So a user provides the instance, the ballots collected in full, and one partial ballot per remaining voter. They get back a BudgetAllocation, the same as from any other rule. The prediction only fills in projects a voter was never asked about; anything she actually answered is kept as given. Evaluating the idea instead. real = set(greedy_approval(instance, profile)) fractional_allocation_score(real, predicted, instance.budget_limit) # 0.75 run_experiment is a simulation: Everything else is the individual steps of those two flows, for anyone who wants to assemble them differently. I pushed a usage page, docs-source/source/usage/recommendation.rst. It covers the above with runnable examples. Building the docs to check it also turned up two markup errors in my own docstrings (|LV| is substitution syntax in reStructuredText, and one :py:func: target was wrong); both are fixed in the same commit. Happy to restructure any of it to fit your conventions. |
The module mixed running the system with measuring it. The Section 5 metrics and the Section 6 treatment matrix are analysis of an outcome, which is what pabutools.analysis is for, so they move to a new pabutools/analysis/recommendationanalytics.py: * classification_metrics (Section 5.1) and fractional_allocation_score (Definition 5.1) are pure functions over sets of projects - the latter compares any two bundles and is not recommendation-specific at all * run_all_experiments, SAMPLE_DEGREES and LV_DEGREES: a benchmark harness whose entire output is those metrics, with no role in running a process The line is now that pabutools.recommendation runs the system and pabutools.analysis measures how well it did. run_pipeline and run_experiment stay behind: they are simulations, but they return bundles rather than scores, and moving them would drag complete_ballots along with them. Imports run analysis -> recommendation, the same direction mesanalytics already uses to reach pabutools.rules; nothing in recommendation imports analysis, so there is no cycle. The optional ML libraries stay lazily imported inside the functions that need them, so pulling recommendation into pabutools.analysis's __init__ does not make them hard dependencies. Also replace the two "if __name__ == '__main__': doctest.testmod()" blocks with tests/test_doctests.py. They were the only thing running the modules' doctests - there is no pytest configuration in the project - so deleting them outright would have quietly dropped 49 tests. The new module collects them as an ordinary parameterized TestCase rather than through a load_tests hook, because pytest does not implement that protocol and would collect none of them; the examples that fit a model skip when their optional library is absent, as the equivalent unit tests already do. Full suite: 280 passed, 9 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Following review feedback, I moved the evaluation code to a more appropriate place in the library.
Imports run One related cleanup: the two Full suite: 280 passed, 9 skipped. |
|
Hey!Just wanted to follow up on the PR I submitted a couple of weeks ago. Whenever you get a chance, I’d really appreciate a review. Let me know if there’s anything you’d like me to change or add. Thanks! |
|
Hi! It doesn't feel to me like this material belongs to a library like pabutools, since it doesn't seem to be generally useful. Edit: I see that Simon in the issue discussion seems to have indicated this would be welcome, it's his call. Generally, I think it isn't good form to let Claude write any text like PR descriptions or comments that you expect other people to read. It's better to write yourself to explain what you're doing and why you think it is a good addition. |
|
Thank you for the constructive criticism. If @Simon-Rey thinks there's much to cut down that's not a problem, I'll edit as necessary. In general the pr is meant to give the user the tools, based on machine learning algorithms, to predict the results of the full ballots, as most voters in real life will leave many of the projects unanswered. |
|
I think my initial concern (and from what I understand also that of Dominik) is that I don't really think that users of the package will use that piece of code. The functionalities offered by a package need to be general, solve problem that several people will want to look into. Right now it all feels too specific for me to really believe that others will get advantage from it. There are thousands of lines of code that all seem to me to have been AI generated (no problem with that, but it does not give me great confidence) and so I want to be a bit careful before introducing that to the package as they will need to be maintained at some point. Overall, I don't know what to do with all of this and since Dominik also raised concerns I am tempted to close without merging. |
Implements "A Recommendation System for Participatory Budgeting" by Gil
Leibiker and Nimrod Talmon (AAMAS 2023):
https://optlearnmas23.github.io/files/p17.pdf
The module addresses the paper's information overload problem — the cognitive
burden of judging every project on a long ballot. Instead of asking every voter
about every project, it queries each voter for a partial ballot, estimates the
votes it did not ask for, and runs the voting rule on the completion.
Contents
pabutools.recommendationruns the system:and controversiality, and the online adaptive setup.
next_adaptive_questionexposes a single step of the adaptive loop so it can drive a live process,
not only a simulation.
matrix factorization, and factorization machines.
plan_sampling,split_lv_tv),greedy approval, and
electfor running a real process where no groundtruth exists.
pabutools.analysis.recommendationanalyticsmeasures how well it did:classification_metrics(precision, recall andF1 over the projects a voter was never asked about) and
fractional_allocation_score(Definition 5.1).run_all_experiments, the treatment matrix of §6 — every setup x predictorx sample degree x LV degree, averaged over repeated random splits.
Tests: 40 in
tests/test_recommendation.pyand 3 intests/test_analysis.py,plus 49 doctests collected by
tests/test_doctests.py.Dependencies
The machine-learning libraries are an optional
recommendationextra and areimported lazily inside the functions that need them, so the module imports and
every non-ML algorithm works without them. Tests that fit a model skip rather
than fail when the library is absent, which keeps CI green on the standard
.[dev]install.Notes on the paper
Two points are under-specified or inconsistent; both are documented in the
docstrings alongside the reading that was implemented:
but never defines what a training row is or which features it carries.
SD({1,2,3}, {1,3,4}) = 1 where the definition yields 2. The first and third
examples agree with the definition, which is what is implemented.