Add pabutools.recommendation: a recommendation system for participatory budgeting - #72
Add pabutools.recommendation: a recommendation system for participatory budgeting#72roeiyanku wants to merge 16 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. |
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
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,
electfor running a real process where no ground truthexists, and the evaluation metrics of §5.
tests/test_recommendation.py, plus doctests throughout.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.