diff --git a/CMakeLists.txt b/CMakeLists.txt index 81cafe0..a7e99a6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -201,6 +201,7 @@ add_library(qe_multiwinner_methods src/multiwinner/quotas.cc src/multiwinner/qbuck.cc # src/multiwinner/qpq.cc + src/multiwinner/randballots.cc src/multiwinner/shuntsstv.cc src/multiwinner/stv.cc src/stats/multiwinner/mwstats.cc) diff --git a/src/main/multiwinner.cc b/src/main/multiwinner.cc index 7cd6f11..4799880 100644 --- a/src/main/multiwinner.cc +++ b/src/main/multiwinner.cc @@ -2,12 +2,6 @@ // Multiwinner hacks. // Segment properly later. TODO. -// Also TODO aggregate ballots in the case that multiple voters vote in the -// same way. Sketch of how to do this: Make map from orderings to int, -// map[this ordering]++ for all ballots, then adjust weights accordingly. -// Remember to delete the first and second keys of the map properly, or it'll -// leak. - // TODO: Write normalized array contents to files so that we can make graphs // based on them, etc. // Wishlist: Candidate ordering by opinions. FindBest/FindWorst instead of @@ -19,10 +13,6 @@ #include "singlewinner/elimination/elimination.h" #include "singlewinner/stats/cardinal.h" -/*#include "tools.cc" -#include "mwstats.cc" -#include "condorcet/methods.cc"*/ - #include "stats/multiwinner/mwstats.h" #include "stats/multiwinner/vse.h" @@ -40,6 +30,8 @@ #include "multiwinner/compat_qbuck.h" #include "multiwinner/dhwl.h" #include "multiwinner/stv.h" + +#include "multiwinner/randballots.h" //#include "multiwinner/qpq.cc" #include "hack/msvc_random.h" @@ -232,68 +224,6 @@ list random_council(size_t num_candidates, size_t council_size) { return (toRet); } -// TODO: Handle case with weighted ballots: 1000 A > B, 1 B > C should have -// only 1/1000 chance of picking B > C, not 1/2. -// Beware: this method is not cloneproof. -list random_ballot(int num_ballots, const election_t & ballots, - size_t num_candidates, size_t council_size, size_t accept_number) { - - // Read off the first min(ordering size, accept_number) candidates. - // Then permute randomly and pick the first council_size of these. - // Inspired by LeGrand's 3OPT minmax approval scheme. - - - // Pick the random ballot - size_t counter = 0, rand_ballot = floor(drand48() * num_ballots); - - election_t::const_iterator pos; - - for (pos = ballots.begin(); pos != ballots.end() && counter < - rand_ballot; ++pos) { - ++counter; - } - - // Now pos is a random ballot. - size_t requested = min(accept_number, pos->contents.size()); - vector accepted(requested, 0); - vector taken(num_candidates, false); // incomplete ballots - - counter = 0; - - for (ordering::const_iterator opos = pos->contents.begin(); - opos != pos->contents.end() && counter < requested; - ++opos) { - assert(opos->get_candidate_num() < num_candidates); - taken[opos->get_candidate_num()] = true; - accepted[counter++] = opos->get_candidate_num(); - } - - // Fill with random if the voter was too indecisive. - while (accepted.size() < max(accept_number, council_size)) { - int proposed_candidate = floor(drand48() * num_candidates); - if (taken[proposed_candidate]) { - continue; - } - accepted.push_back(proposed_candidate); - taken[proposed_candidate] = true; - } - - // Randomly shuffle - random_shuffle(accepted.begin(), accepted.end()); - - // .. and load to the list we're going to return. - - list toRet; - - for (counter = 0; counter < council_size; ++counter) { - assert(accepted[counter] < num_candidates); - toRet.push_back(accepted[counter]); - } - - return (toRet); -} - - double get_error(const list & council, int num_candidates, size_t council_size, const vector > & population_profiles, @@ -306,13 +236,16 @@ double get_error(const list & council, vector seen(num_candidates, false); for (list::const_iterator pos = council.begin(); pos != council.end(); ++pos) { + assert(0 <= *pos && *pos < num_candidates); assert(!seen[*pos]); + seen[*pos] = true; } - return (errm(whole_pop_profile, get_quantized_opinion_profile( - council, population_profiles))); + return errm(whole_pop_profile, + get_quantized_opinion_profile( + council, population_profiles)); } /* Majoritarian utility (VSE) @@ -376,6 +309,35 @@ double get_council_utility(const std::vector & utilities, return sum_utils / (double)council.size(); } +// Gets the expected utility from a random dictator procedure. Note: this +// assumes that every voter ranks at least as many candidates as the +// council size. + +double get_random_dictator_utility(const std::vector & utilities, + const election_t & election, size_t council_size) { + + double total_weight = 0; + double total_utility = 0; + + for (const ballot_group & ballot: election) { + size_t cands_picked = 0; + + assert(ballot.contents.size() >= council_size); + + for (auto pos = ballot.contents.begin(); + cands_picked < council_size && pos != ballot.contents.end(); + ++pos, ++cands_picked) { + + size_t candidate = pos->get_candidate_num(); + + total_weight += ballot.get_weight(); + total_utility += ballot.get_weight() * utilities[candidate]; + } + } + + return total_utility / total_weight; +} + //// void get_limits(int num_candidates, int council_size, @@ -383,16 +345,22 @@ void get_limits(int num_candidates, int council_size, const vector & whole_pop_profile, int tries, double & worst, double & average, double & best) { + // TODO: use exhaustive enumeration if the number of + // different combinations is smaller than the "tries" + // limit. + + // log combinations can be calculated as + // lgamma(N+1) - (lgamma(r+1)+lgamma(N-r+1)), + // it's better to use logarithms because the numbers + // may become too large. + worst = -INFINITY; average = 0; best = INFINITY; - double d_worst = 0; - int counter; for (counter = 0; counter < tries; ++counter) { - double this_error = get_error(random_council(num_candidates, council_size), num_candidates, council_size, population_profiles, @@ -406,56 +374,48 @@ void get_limits(int num_candidates, int council_size, } average += this_error; - - // Exponential average. - d_worst = d_worst * 0.5 + this_error * 0.5; - - // Try to normalize the noise in getting best and worst by - // breaking after a certain error level. - // Didn't work very well. - // if (fabs(this_error - d_worst) < 1e-6) break; - } average /= (double)(counter); } // n * tries instead of plain n(as we would have with a vector of ballots. -// TODO: Fix somehow. -double random_ballot(const election_t & ballots, - int num_candidates, int council_size, int accept_number, +// TODO: Just calculate the expected value by letting every voter be +// a dictator. + +double random_dictator(const election_t & election, + size_t num_candidates, size_t council_size, const vector > & population_profiles, - const vector & whole_pop_profile, int tries) { + const vector & whole_pop_profile) { - int num_ballots = population_profiles.size(); + double total_weight = 0; + double total_disprop = 0; - double sum = 0; + for (const ballot_group & ballot: election) { + size_t cands_picked = 0; - for (int counter = 0; counter < tries; ++counter) - sum += get_error(random_ballot(num_ballots, ballots, - num_candidates, council_size, - accept_number), num_candidates, - council_size, population_profiles, - whole_pop_profile); + assert(ballot.contents.size() >= council_size); + std::list dictatorial_council; - return (sum / (double)tries); -} + for (auto pos = ballot.contents.begin(); + cands_picked < council_size && pos != ballot.contents.end(); + ++pos, ++cands_picked) { + size_t candidate = pos->get_candidate_num(); -// Election method normalization info + dictatorial_council.push_back(candidate); + } + total_disprop += ballot.get_weight() * get_error( + dictatorial_council, num_candidates, council_size, + population_profiles, whole_pop_profile); -/*class multiwinner_stats { - public: - multiwinner_method * method; - double sum_scores; - double sum_normalized_scores; + total_weight += ballot.get_weight(); + } - multiwinner_stats(multiwinner_method * method_in) { - method = method_in; - sum_scores = 0; - sum_normalized_scores = 0; - } -};*/ + return total_disprop / total_weight; +} + +// Election method normalization info string padded(string a, int maxlen) { int len = max(1, maxlen - (int)a.size()); @@ -463,24 +423,17 @@ string padded(string a, int maxlen) { return (a + string(len, ' ')); } -string display_stats(double stat_sum, double rounds_so_far, - double current_unnorm_result, double current_norm_result, +string display_stats(const VSE & disprop, const VSE & utility, string name) { - string toRet = padded(dtos(stat_sum/rounds_so_far), 12) + padded(name, - 30) + "round: " + padded(dtos(current_norm_result), - 9) + " (unnorm: " + padded(dtos(current_unnorm_result), 9) +")"; + std::string toRet = padded(dtos(disprop.get(), 5), 8) + " " + + padded(dtos(utility.get(), 5), 7) + " " + + s_padded(name, 32) + "round: " + s_padded(dtos( + disprop.get_this_round(), 4), 7); - return (toRet); + return toRet; } -/*string display_stats(const multiwinner_stats & in, double rounds_so_far, - double current_unnorm_result, double current_norm_result) { - return (display_stats(in.sum_normalized_scores, rounds_so_far, - current_unnorm_result, current_norm_result, - in.method->name())); -}*/ - // QnD void print_std_pref(const election_t & f) { @@ -524,21 +477,7 @@ void set_best(multiwinner_stats & meta, double minimum, double maximum, meta.add_result(minimum, gathered_scores[0], maximum); } -int main(int argc, char * * argv) { - - // Used for inlining. - int maxnum = 0; - bool run_forever = false; - if (argc < 2) { - std::cerr << "No max match number specified, running forever." << endl; - run_forever = true; - maxnum = 1; - } else { - maxnum = stoi(argv[1]); - } - - assert(maxnum > 0); - +std::vector get_multiwinner_methods() { // Set up some majoritarian election methods and their stats. // Condorcet methods should probably have their own arrays. vector e_methods; // WARNING: Leak. TODO, fix. @@ -546,6 +485,10 @@ int main(int argc, char * * argv) { vector > condorcet; vector > other_methods; + // Set the random ballot method's seed to 1 for reproducibility. + e_methods.push_back(multiwinner_stats(std::make_shared + (1))); + // All are PT_WHOLE for now. positional_methods.push_back(std::make_shared(PT_WHOLE)); positional_methods.push_back(std::make_shared(PT_WHOLE)); @@ -706,49 +649,47 @@ int main(int argc, char * * argv) { multiwinner_stats qpqmetam("Best of QPQ(Meta, multiround)"), qpqmetas( "Best of QPQ(Meta, sequential)");*/ + return e_methods; +} - ///////////////////////////////////////////////////////////////////////// - /// Done adding voting methods. - ///////////////////////////////////////////////////////////////////////// - - // I'm going to resize to one just to make debugging easier. - /*e_methods.clear(); - e_methods.push_back(multiwinner_stats(std::make_shared()));*/ +int main(int argc, char * * argv) { - int number = 0; + // Used for inlining. + int maxnum = 0; + bool run_forever = false; + if (argc < 2) { + std::cerr << "No max match number specified, running forever." << endl; + run_forever = true; + maxnum = 1; + } else { + maxnum = stoi(argv[1]); + } - // This should be turned into a proper multiwinner method. So should - // "worst of n" and "best of n" for that matter. - double sum_random = 0; + assert(maxnum > 0); - election_t ballots; + vector e_methods = get_multiwinner_methods(); - double sum_rbal = 0; + ///////////////////////////////////////////////////////////////////////// + /// Done adding voting methods. + ///////////////////////////////////////////////////////////////////////// - double sum_worst = 0; - double sum_best = 0; + election_t ballots; // Very quick and dirty majoritarian VSE data. - std::map method_utility; - double random_utility = 0; - double best_utility = 0; - double cur_random_utility = 0, cur_best_utility = 0; - size_t utility_count = 0; + std::map method_utility; + + int number = 0; - // DONE: Reproduce in secpr to see where we get different result. - // IRV sounds way too low. It's not a glitch! It may be an effect of - // the ballots assumptions or the council distributions, though.. + double cur_best_utility = 0, cur_random_utility = 0; - number = 0; - int idx; // 55 to 56 + double sum_rbal = 0, sum_random = 0; - // 26 is example of Meek being worse than "ordinary" STV. Very strange. - // 63 is another. Round 148 is an example where tiebreaking is needed - // because at some point all remaining tie for quota. + // I probably need to make mwstats independent of the election + // method or something... using a map might not be such a bad idea. + // then this could also do all the Pareto stuff I'd like to do. + VSE random_dictator_disprop, random_dictator_maj; - // 149 - // 156 crashed BTR-STV. No more. - for (idx = 0; idx < maxnum || run_forever; ++idx) { + for (int idx = 0; idx < 100+maxnum && run_forever; ++idx) { srandom(idx); // So we can replay in the case of bugs. srand(idx); @@ -778,7 +719,6 @@ int main(int argc, char * * argv) { vector pop_opinion_profile(opinions, 0); - // TODO: Find functional way of doing this for (int counter = 0; counter < opinions; ++counter) { pop_opinion_profile[counter] = drand48(); } @@ -816,74 +756,45 @@ int main(int argc, char * * argv) { council_size); cur_random_utility = get_random_utilities(utilities); - best_utility += cur_best_utility; - random_utility += cur_random_utility; - ++utility_count; - - std::cout << "VSE: random: " << cur_random_utility << ", best: " << - cur_best_utility << std::endl; - std::cout << "Total: random: " << random_utility/utility_count << - ", best: " << best_utility/utility_count << "\n"; - - // Get the disproportionality of random councils, which is needed - // to anchor the proportionality of random candidate for the VSE- - // like disproportionality measure. - - // We could also run this through for_each_combination. TODO later. + // Get the best, random (expected), and worst council + // disproportionality. The best and random are needed to anchor + // the VSE-like disproportionality scale. These are all + // approximate except for small councils. - double random_disprop = 0; - size_t max_disprop_iters = 1000; + double cur_worst_disprop, cur_mean_disprop, cur_best_disprop; - for (size_t i = 0; i < max_disprop_iters; ++i) { - std::list random_draw = random_council(num_candidates, - council_size); - - random_disprop += get_error(random_draw, - num_candidates, council_size, - population_profiles, - pop_opinion_profile); - } + get_limits(num_candidates, council_size, population_profiles, + pop_opinion_profile, 75000, cur_worst_disprop, + cur_mean_disprop, cur_best_disprop); // The stuff below should use proper multiwinner_stats objects. // TODO!!! - random_disprop /= (double)max_disprop_iters; - std::cout << "Random candidate (raw): " << random_disprop << "\n"; - - double worst, average, best; - get_limits(num_candidates, council_size, population_profiles, - pop_opinion_profile, 75000, worst, average, - best); + std::cout << "Random candidate (raw): " << cur_mean_disprop << "\n"; - sum_worst += worst; - sum_best += best; + cout << "Worst: " << cur_worst_disprop << ", Average: " + << cur_mean_disprop << ", Best: " << cur_best_disprop << endl; - cout << "Worst: " << worst << ", Average: " << average << - ", Best: " << best << endl; + sum_random += cur_mean_disprop; - sum_random += random_disprop; + /*cout << display_stats(0, number, cur_mean_disprop, + 0, "-- Random candidates --") << endl;*/ - cout << display_stats(0, number, random_disprop, - 0, "-- Random candidates --") << endl; + double rdic_value = random_dictator(ballots, num_candidates, + council_size, population_profiles, pop_opinion_profile); - // Create ballots consistent with the population's preferences. - // (Yuck, this is ugly...) + double rdic_utility = get_random_dictator_utility(utilities, + ballots, council_size); - // Not good - double rbal_value = random_ballot(ballots, num_candidates, - council_size, num_candidates / 4, - population_profiles, pop_opinion_profile, - 10000); + random_dictator_disprop.add_result(cur_mean_disprop, + rdic_value, cur_best_disprop); + random_dictator_maj.add_result(cur_random_utility, + rdic_utility, cur_best_utility); - sum_rbal += rbal_value; + sum_rbal += rdic_value; - double random_ballot_vse = (sum_rbal - sum_random) / - (sum_best - sum_random); - double this_round_rb_vse = (rbal_value - random_disprop) / - (best - random_disprop); - - cout << display_stats(random_ballot_vse, 1, rbal_value, - this_round_rb_vse, "-- Random ballot (0.25) --") << endl; + cout << display_stats(random_dictator_disprop, + random_dictator_maj, "-- Random dictator --") << endl; double error; @@ -913,43 +824,20 @@ int main(int argc, char * * argv) { population_profiles, pop_opinion_profile); - e_methods[counter].add_result(rbal_value, error, best); + e_methods[counter].add_result(rdic_value, error, cur_best_disprop); // VSE update double this_method_utility = get_council_utility(utilities, council); - if (method_utility.find(e_methods[counter].get_name()) == - method_utility.end()) { - method_utility[e_methods[counter].get_name()] = this_method_utility; - } else { - method_utility[e_methods[counter].get_name()] += this_method_utility; - } - - double total_utility = method_utility[e_methods[counter].get_name()]; - double maj_VSE = (total_utility - random_utility) / (best_utility - - random_utility); + method_utility[e_methods[counter].get_name()].add_result( + cur_random_utility, this_method_utility, cur_best_utility); // Print it all out. - cout << e_methods[counter].display_stats(maj_VSE) << endl; + cout << e_methods[counter].display_stats( + method_utility[e_methods[counter].get_name()].get()) << endl; } - - // Meta - /*set_best(qpqmetas, best, worst, e_methods, "QPQ", "sequential"); - cout << qpqmetas.display_stats() << endl; - set_best(qpqmetam, best, worst, e_methods, "QPQ", "multiround"); - cout << qpqmetam.display_stats() << endl;*/ } - - /*cout << "Borda: " << error << ", norm: " << renorm(best, worst, error, - 0.0, 1.0) << endl; - - error = get_error(majoritarian_council(council_size, num_candidates, - ballots, &hare), population_profiles, - pop_opinion_profile); - - cout << "Other: " << error << ", norm: " << renorm(best, worst, error, - 0.0, 1.0) << endl;*/ } diff --git a/src/multiwinner/randballots.cc b/src/multiwinner/randballots.cc new file mode 100644 index 0000000..9d1ee96 --- /dev/null +++ b/src/multiwinner/randballots.cc @@ -0,0 +1,77 @@ +#include "randballots.h" + +#include + +#include + +std::list random_ballots::get_council(int council_size, + int num_candidates, const election_t & ballots) const { + + // https://stackoverflow.com/a/38158887 + std::vector > + shuffled_ballots(ballots.cbegin(), ballots.cend()); + + std::random_shuffle(shuffled_ballots.begin(), + shuffled_ballots.end(), random_gen); + + std::vector hopefuls(num_candidates, true); + + bool elected_a_candidate; + size_t num_elected = 0; + std::list elected_candidates; + + do { + elected_a_candidate = false; + + for (const ballot_group & ballot: shuffled_ballots) { + if (num_elected >= (size_t)council_size) { + continue; + } + + if (ballot.get_weight() != 1) { + throw std::invalid_argument("Random ballots: " + "weighted ballots are not supported."); + } + std::vector winners = ordering_tools::get_winners( + ballot.contents, hopefuls); + + std::random_shuffle(winners.begin(), + winners.end(), random_gen); + + bool elected_from_ballot = false; + + for (size_t i = 0; i < winners.size() + && !elected_from_ballot; ++i) { + + if (!hopefuls[winners[i]]) { + continue; + } + + hopefuls[winners[i]] = false; + elected_candidates.push_back(winners[i]); + elected_from_ballot = true; + elected_a_candidate = true; + ++num_elected; + } + } + } while (elected_a_candidate && num_elected < (size_t)council_size); + + if (num_elected == (size_t)council_size) { + return elected_candidates; + } + + // Everybody's first preference has been accepted, but we still + // haven't filled up the council. Just fill randomly. + + for (size_t i = 0; i < (size_t)num_candidates + && num_elected < (size_t)council_size; ++i) { + + if (hopefuls[i]) { + hopefuls[i] = false; + elected_candidates.push_back(i); + ++num_elected; + } + } + + return elected_candidates; +} \ No newline at end of file diff --git a/src/multiwinner/randballots.h b/src/multiwinner/randballots.h new file mode 100644 index 0000000..ddbbc1e --- /dev/null +++ b/src/multiwinner/randballots.h @@ -0,0 +1,35 @@ +#pragma once + +#include "random/random.h" +#include "methods.h" +#include + +// "Random ballots": shuffle the ballots, then elect a random first preference from +// each voter until the council is full or we've exhausted the ballots. If it's the +// latter, do it again but ignore the already chosen candidates. + +// Note that this is different from random dictator, which elects a slate of a +// random voter's preferences. + +// I don't really think there's a reason to pass general coordinate generators +// into this method because I don't see how QMC would help. Maybe if this is run +// tons of times? + +class random_ballots : public multiwinner_method { + private: + mutable rng random_gen; + + public: + std::list get_council(int council_size, + int num_candidates, const election_t & ballots) const; + + std::string name() const { + return "Random ballots"; + } + + void set_seed(rseed_t seed) { + random_gen.s_rand(seed); + } + + random_ballots(rseed_t seed) : random_gen(seed) {} +}; \ No newline at end of file diff --git a/src/stats/multiwinner/mwstats.cc b/src/stats/multiwinner/mwstats.cc index 1100adc..011e3d2 100644 --- a/src/stats/multiwinner/mwstats.cc +++ b/src/stats/multiwinner/mwstats.cc @@ -72,14 +72,6 @@ double multiwinner_stats::get_average(bool normalized) const { } } -/*double multiwinner_stats::get_median(bool normalized) { - if (normalized) { - return (get_median(normalized_scores)); - } else { - return (get_median(scores)); - } -}*/ - double multiwinner_stats::get_last(bool normalized) const { assert(!scores.empty()); @@ -92,14 +84,8 @@ double multiwinner_stats::get_last(bool normalized) const { } } -std::string multiwinner_stats::display_stats(double additional_stat) -const { - - /*std::string name; - - if (mw_method == NULL) - name = "N/A"; - else name = mw_method->name();*/ +std::string multiwinner_stats::display_stats( + double additional_stat) const { std::string additional_stat_str; if (isfinite(additional_stat)) { diff --git a/src/stats/multiwinner/vse.h b/src/stats/multiwinner/vse.h index c88890f..7ab4f56 100644 --- a/src/stats/multiwinner/vse.h +++ b/src/stats/multiwinner/vse.h @@ -40,6 +40,10 @@ class VSE { (last_best - last_random); } + double get_this_round_raw() const { + return last_method; + } + size_t get_rounds() const { return rounds; } diff --git a/src/tools/ballot_tools.cc b/src/tools/ballot_tools.cc index 16a8c78..ef4ccdb 100644 --- a/src/tools/ballot_tools.cc +++ b/src/tools/ballot_tools.cc @@ -194,6 +194,29 @@ std::vector ordering_tools::get_winners(const ordering & in) { return (winners); } +std::vector ordering_tools::get_winners(const ordering & in, + const std::vector & hopefuls) { + + std::vector winners; + + ordering::const_iterator start = in.begin(); + + while (start != in.end() && !hopefuls[start->get_candidate_num()]) { + ++start; + } + + if (start == in.end()) { + return winners; + } + + for (ordering::const_iterator pos = in.begin(); pos != in.end() && + pos->get_score() == in.begin()->get_score(); ++pos) { + winners.push_back(pos->get_candidate_num()); + } + + return winners; +} + bool ordering_tools::is_winner(const ordering & in, int candidate_num) { diff --git a/src/tools/ballot_tools.h b/src/tools/ballot_tools.h index 5f4f42b..f1a6046 100644 --- a/src/tools/ballot_tools.h +++ b/src/tools/ballot_tools.h @@ -37,6 +37,8 @@ class ordering_tools { static bool has_multiple_winners(const ordering & in); static bool has_multiple_losers(const ordering & in); static std::vector get_winners(const ordering & in); + static std::vector get_winners(const ordering & in, + const std::vector & hopefuls); static bool is_winner(const ordering & in, int candidate_num); // Checks if the ordering has any explicitly equal-ranked