diff --git a/.gitignore b/.gitignore index dc9e56a3..93dc3b8c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ htmlcov/ .DS_Store dist/ .ipynb_checkpoints - +.venv diff --git a/dist/votekit-1.0.0-py3-none-any.whl b/dist/votekit-1.0.0-py3-none-any.whl new file mode 100644 index 00000000..592b82fd Binary files /dev/null and b/dist/votekit-1.0.0-py3-none-any.whl differ diff --git a/dist/votekit-1.0.0.tar.gz b/dist/votekit-1.0.0.tar.gz new file mode 100644 index 00000000..df814cc0 Binary files /dev/null and b/dist/votekit-1.0.0.tar.gz differ diff --git a/notebooks/.ipynb_checkpoints/load_clean-checkpoint.ipynb b/notebooks/.ipynb_checkpoints/load_clean-checkpoint.ipynb new file mode 100644 index 00000000..b8811e12 --- /dev/null +++ b/notebooks/.ipynb_checkpoints/load_clean-checkpoint.ipynb @@ -0,0 +1,135 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is a tutorial on loading in an election dataset and cleaning ballots.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from votekit.cvr_loaders import load_blt\n", + "import votekit.cleaning as clean" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's first load in our cvr into a preference profile\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# need to make the scottish election data importable\n", + "\n", + "pp, seats = load_blt(\"...\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can clean the ballots from this election.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If we want to remove a candidate, we can call remove_noncands() from the package as shown below.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "cleaned_pp = clean.remove_noncands(pp, [\"Graham HUTCHISON (C)\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also write our own cleaning rule with the helper function clean_profile(). The following example is a cleaning rule truncates the ballot to n-ranks.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from votekit.cleaning import clean_profile\n", + "from votekit.pref_profile import PreferenceProfile\n", + "from votekit.ballot import Ballot\n", + "\n", + "\n", + "def truncate(n: int, pp: PreferenceProfile):\n", + " def truncate_ballot(ballot: Ballot):\n", + " return Ballot(ranking=ballot.ranking[:n], weight=ballot.weight)\n", + "\n", + " pp_clean = clean_profile(pp=pp, clean_ballot_func=truncate_ballot)\n", + " return pp_clean" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "cleaned_pp = truncate(n=3, pp=pp)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Our preference profile is now cleaned, and we can save it as an csv for future use.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "cleaned_pp.to_csv('path/to/save')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.12" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/diff_bg_viz.ipynb b/notebooks/ballot_generator.ipynb similarity index 99% rename from notebooks/diff_bg_viz.ipynb rename to notebooks/ballot_generator.ipynb index 8d24ca63..92355949 100644 --- a/notebooks/diff_bg_viz.ipynb +++ b/notebooks/ballot_generator.ipynb @@ -16,9 +16,7 @@ "outputs": [], "source": [ "import votekit.ballot_generator as bg\n", - "from votekit.plots.profile_plots import plot_summary_stats\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np" + "from votekit.plots.profile_plots import plot_summary_stats" ] }, { diff --git a/notebooks/election_simulation.ipynb b/notebooks/election_simulation.ipynb new file mode 100644 index 00000000..fdd07a6c --- /dev/null +++ b/notebooks/election_simulation.ipynb @@ -0,0 +1,176 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This tutorial shows you how to simulate elections using VoteKit.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "ename": "ModuleNotFoundError", + "evalue": "No module named 'votekit.elections'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[12], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[39m# from votekit.utils import make_ballot\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m \u001b[39mimport\u001b[39;00m \u001b[39mvotekit\u001b[39;00m\u001b[39m.\u001b[39;00m\u001b[39melections\u001b[39;00m\u001b[39m.\u001b[39;00m\u001b[39melection_types\u001b[39;00m \u001b[39mas\u001b[39;00m \u001b[39melections\u001b[39;00m\n\u001b[1;32m 3\u001b[0m \u001b[39mfrom\u001b[39;00m \u001b[39mvotekit\u001b[39;00m\u001b[39m.\u001b[39;00m\u001b[39mpref_profile\u001b[39;00m \u001b[39mimport\u001b[39;00m PreferenceProfile\n\u001b[1;32m 4\u001b[0m \u001b[39mfrom\u001b[39;00m \u001b[39mvotekit\u001b[39;00m\u001b[39m.\u001b[39;00m\u001b[39melections\u001b[39;00m\u001b[39m.\u001b[39;00m\u001b[39mtransfers\u001b[39;00m \u001b[39mimport\u001b[39;00m fractional_transfer\n", + "\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'votekit.elections'" + ] + } + ], + "source": [ + "# from votekit.utils import make_ballot\n", + "import votekit.elections.election_types as elections\n", + "from votekit.pref_profile import PreferenceProfile\n", + "from votekit.elections.transfers import fractional_transfer" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's first build our preference profile with synthetic ballots\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "b1 = make_ballot(ranking=[\"A\", \"D\", \"E\", \"C\", \"B\"], weight=18)\n", + "b2 = make_ballot(ranking=[\"B\", \"E\", \"D\", \"C\", \"A\"], weight=12)\n", + "b3 = make_ballot(ranking=[\"C\", \"B\", \"E\", \"D\", \"A\"], weight=10)\n", + "b4 = make_ballot(ranking=[\"D\", \"C\", \"E\", \"B\", \"A\"], weight=4)\n", + "b5 = make_ballot(ranking=[\"E\", \"B\", \"D\", \"C\", \"A\"], weight=4)\n", + "b6 = make_ballot(ranking=[\"E\", \"C\", \"D\", \"B\", \"A\"], weight=2)\n", + "pp = PreferenceProfile(ballots=[b1, b2, b3, b4, b5, b6])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can define our set of elections to simulate.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "num_seats = 1\n", + "election_borda = elections.Borda(pp, seats=num_seats, score_vector=None)\n", + "election_irv = elections.STV(pp, fractional_transfer, seats=num_seats)\n", + "election_plurality = elections.Plurality(\n", + " pp, seats=num_seats, ballot_ties=False)\n", + "election_seq = elections.SequentialRCV(pp, seats=num_seats)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's run the elections. Running the elections will generate an election state, from which we can get the winners.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "election_state_borda = election_borda.run_election()\n", + "election_state_irv = election_irv.run_election()\n", + "election_state_plurality = election_plurality.run_election()\n", + "election_state_seq = election_seq.run_election()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "outcome_borda = election_state_borda.get_all_winners()\n", + "outcome_irv = election_state_irv.get_all_winners()\n", + "outcome_plurality = election_state_plurality.get_all_winners()\n", + "outcome_seq = election_state_seq.get_all_winners()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We should expect different results for different elections.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(outcome_borda)\n", + "print(outcome_irv)\n", + "print(outcome_plurality)\n", + "print(outcome_seq)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "csci1470 Python 3.9.12", + "language": "python", + "name": "csci1470" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.12" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/load_clean.ipynb b/notebooks/load_clean.ipynb new file mode 100644 index 00000000..b8811e12 --- /dev/null +++ b/notebooks/load_clean.ipynb @@ -0,0 +1,135 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is a tutorial on loading in an election dataset and cleaning ballots.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from votekit.cvr_loaders import load_blt\n", + "import votekit.cleaning as clean" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's first load in our cvr into a preference profile\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# need to make the scottish election data importable\n", + "\n", + "pp, seats = load_blt(\"...\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can clean the ballots from this election.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If we want to remove a candidate, we can call remove_noncands() from the package as shown below.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "cleaned_pp = clean.remove_noncands(pp, [\"Graham HUTCHISON (C)\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also write our own cleaning rule with the helper function clean_profile(). The following example is a cleaning rule truncates the ballot to n-ranks.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from votekit.cleaning import clean_profile\n", + "from votekit.pref_profile import PreferenceProfile\n", + "from votekit.ballot import Ballot\n", + "\n", + "\n", + "def truncate(n: int, pp: PreferenceProfile):\n", + " def truncate_ballot(ballot: Ballot):\n", + " return Ballot(ranking=ballot.ranking[:n], weight=ballot.weight)\n", + "\n", + " pp_clean = clean_profile(pp=pp, clean_ballot_func=truncate_ballot)\n", + " return pp_clean" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "cleaned_pp = truncate(n=3, pp=pp)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Our preference profile is now cleaned, and we can save it as an csv for future use.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "cleaned_pp.to_csv('path/to/save')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.12" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/votekit/graphs/ballot_graph.py b/src/votekit/graphs/ballot_graph.py index d2bdddff..37d15282 100644 --- a/src/votekit/graphs/ballot_graph.py +++ b/src/votekit/graphs/ballot_graph.py @@ -246,7 +246,6 @@ def draw(self, neighborhoods: Optional[dict] = {}, labels: Optional[bool] = Fals i = (list(neighborhoods.keys())).index(center) break elif self.node_data[ballot] != 0 and self.profile: - print(ballot) i = (list(self.cand_num.values())).index(ballot[0]) if "weight" in ballot: diff --git a/src/votekit/utils.py b/src/votekit/utils.py index 40da27f0..75829a2f 100644 --- a/src/votekit/utils.py +++ b/src/votekit/utils.py @@ -1,9 +1,9 @@ from collections import namedtuple from fractions import Fraction -import numpy as np -from typing import Union, Iterable, Optional, Any from itertools import permutations import math +import numpy as np +from typing import Union, Iterable, Optional, Any from .ballot import Ballot from .pref_profile import PreferenceProfile @@ -403,3 +403,11 @@ def fix_ties(ballot: Ballot) -> list[Ballot]: ) return ballots + + +def make_ballot(ranking, weight): + ballot_rank = [] + for cand in ranking: + ballot_rank.append({cand}) + + return Ballot(ranking=ballot_rank, weight=Fraction(weight)) diff --git a/tests/test_e2e.py b/tests/test_e2e.py index fd852704..e69de29b 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -1,113 +0,0 @@ -from pathlib import Path -from fractions import Fraction - -from votekit.cvr_loaders import load_blt -import votekit.cleaning as clean -from votekit.election_state import ElectionState -import votekit.ballot_generator as bg -import votekit.elections.election_types as elections -from votekit.pref_profile import PreferenceProfile -from votekit.ballot import Ballot -from votekit.elections.transfers import fractional_transfer - -# TODO: -# need to do one with visualizations, -# need to test other elections, -# need to test cleaning methods, -# need to add cleaning methods (ballot truncation for ex) -# need to test ballot generation models - - -def test_load_clean_completion(): - """simple example of what a "full" use would look like""" - - # load CVR -> PP representation - BASE_DIR = Path(__file__).resolve().parent - BLT_DIR = BASE_DIR / "data/txt/" - - pp, seats = load_blt(BLT_DIR / "edinburgh17-01_abridged.blt") - print(pp) - - # apply rules to get new PP - cleaned_pp = clean.remove_noncands(pp, ["Graham HUTCHISON (C)"]) - - # write intermediate output for inspection - # cleaned_pp.save("cleaned.cvr") - - # run election using a configured RCV step object - election_borda = elections.Borda(cleaned_pp, 1, score_vector=None) - - outcome_borda = election_borda.run_election() - - assert isinstance(outcome_borda, ElectionState) - - # plot_results(outcome) - - -def test_generate_election_completion(): - number_of_ballots = 100 - candidates = ["W1", "W2", "C1", "C2"] - slate_to_candidate = {"W": ["W1", "W2"], "C": ["C1", "C2"]} - bloc_crossover_rate = {"W": {"C": 0.3}, "C": {"W": 0.4}} - pref_interval_by_bloc = { - "W": {"W1": 0.4, "W2": 0.3, "C1": 0.2, "C2": 0.1}, - "C": {"W1": 0.2, "W2": 0.2, "C1": 0.3, "C2": 0.3}, - } - bloc_voter_prop = {"W": 0.7, "C": 0.3} - - DATA_DIR = "src/votekit/data/" - path = Path(DATA_DIR, "Cambridge_09to17_ballot_types.p") - - ballot_model = bg.CambridgeSampler( - candidates=candidates, - pref_interval_by_bloc=pref_interval_by_bloc, - bloc_voter_prop=bloc_voter_prop, - path=path, - bloc_crossover_rate=bloc_crossover_rate, - slate_to_candidates=slate_to_candidate, - ) - - pp = ballot_model.generate_profile(number_of_ballots=number_of_ballots) - - election_borda = elections.Borda(pp, 1, score_vector=None) - - outcome_borda = election_borda.run_election() - - assert isinstance(outcome_borda, ElectionState) - - -def make_ballot(ranking, weight): - ballot_rank = [] - for cand in ranking: - ballot_rank.append({cand}) - - return Ballot(ranking=ballot_rank, weight=Fraction(weight)) - - -def test_generate_election_diff_res(): - b1 = make_ballot(ranking=["A", "D", "E", "C", "B"], weight=18) - b2 = make_ballot(ranking=["B", "E", "D", "C", "A"], weight=12) - b3 = make_ballot(ranking=["C", "B", "E", "D", "A"], weight=10) - b4 = make_ballot(ranking=["D", "C", "E", "B", "A"], weight=4) - b5 = make_ballot(ranking=["E", "B", "D", "C", "A"], weight=4) - b6 = make_ballot(ranking=["E", "C", "D", "B", "A"], weight=2) - pp = PreferenceProfile(ballots=[b1, b2, b3, b4, b5, b6]) - - election_borda = elections.Borda(pp, 1, score_vector=None) - election_irv = elections.STV(pp, fractional_transfer, 1) - election_plurality = elections.Plurality(pp, seats=1, ballot_ties=False) - election_seq = elections.SequentialRCV(pp, seats=1) - # election_sntv = elections.SNTV(pp, seats=1) - - outcome_borda = election_borda.run_election().get_all_winners() - outcome_irv = election_irv.run_election().get_all_winners() - outcome_plurality = election_plurality.run_election().get_all_winners() - outcome_seq = election_seq.run_election().get_all_winners() - # outcome_sntv = election_sntv.run_election().get_all_winners() - - print(outcome_borda) - print(outcome_irv) - print(outcome_plurality) - print(outcome_seq) - - assert outcome_borda != outcome_irv != outcome_plurality != outcome_seq