diff --git a/README.md b/README.md index d2e54a9..fce4394 100644 --- a/README.md +++ b/README.md @@ -13,5 +13,16 @@ conda activate socrse2023 pip install -r requirements.txt # Run voting -python ranked_vote.py --token_col="Please insert your voting token here" ~/Downloads/SocRSE\ Trustee\ elections\ 2023.csv tokens.txt "Please rank the candidates (displayed in random order)" $num_places +python ranked_vote.py --token_col="Please insert your voting token here" ~/Downloads/SocRSE\ Trustee\ elections\ 2023.csv tokens.txt "Candidate ranking" 7 +``` + +### Token mapping + +In the 2026 election multiple sets of tokens were generated accidentally. +A new `--token_map` argument has been created for such situations. +It allows you to provide a 2-column CSV file, with column headings 'old' and 'new', mapping between valid tokens for each voter. +For instance, you can run: + +```bash +python ranked_vote.py --token_col="Please insert your voting token here" --token_map token_map.csv SocRSE\ Trustee\ elections\ 2026.csv tokens.txt "Candidate ranking" 5 ``` diff --git a/ranked_vote.py b/ranked_vote.py index 6256682..894caaf 100644 --- a/ranked_vote.py +++ b/ranked_vote.py @@ -26,13 +26,15 @@ parser.add_argument("question", type=str, help="The question name on the form") parser.add_argument("seats", type=int, help="Number of seats to elect") parser.add_argument("--token_col", type=str, help="The column in the CSV containing the voting token", default="Voting Token") +parser.add_argument("--token_map", type=Path, default=None, + help="A two column CSV file mapping old to new tokens. Column headings must be 'old' and 'new'") args = parser.parse_args() -votes = parse_google_form(args.ballots, token_col=args.token_col) +votes = parse_google_form(args.ballots, token_col=args.token_col, token_map=args.token_map) valid_tokens = parse_tokens(args.tokens) valid_votes, invalid_votes = filter_valid(votes, valid_tokens) -print(f"Running election for {args.seats} seats") +print(f"Running election for {args.seats} seats with {len(valid_votes)} valid votes ({len(invalid_votes)} invalid)") result = run_stv(valid_votes, args.question, args.seats) print(result) diff --git a/tests/map.csv b/tests/map.csv new file mode 100644 index 0000000..d3c9599 --- /dev/null +++ b/tests/map.csv @@ -0,0 +1,4 @@ +old,new +"jdjfghdj","new1" +"ghghdgn","new2" +"jmhmjm","new3" diff --git a/tests/test_voting.py b/tests/test_voting.py index 5dbc642..5bbb0bf 100644 --- a/tests/test_voting.py +++ b/tests/test_voting.py @@ -30,3 +30,10 @@ def test_stv(simple_file): def test_simple(simple_file): res = count_votes_simple(simple_file["Resolutions"]["Resolution 1"]) assert res == (2, 3, 2, 5) + + +def test_token_map(): + test_dir = Path(__file__).resolve().parent + votes = parse_google_form(test_dir / "votes.csv", "Token", test_dir / "map.csv") + assert (votes.index[:3] == ["new1", "new2", "new3"]).all() + assert votes.index[3] == "hfgdhdfg" diff --git a/utils.py b/utils.py index a7e9c80..94f68fc 100644 --- a/utils.py +++ b/utils.py @@ -1,5 +1,6 @@ import re from pathlib import Path +from typing import Optional import pandas as pd import pyrankvote as rv @@ -22,8 +23,13 @@ def parse_tokens(token_file: Path) -> set[str]: return set(t.strip() for t in tokens.readlines()) -def parse_google_form(csv_file: Path, token_col: str) -> pd.DataFrame: +def parse_google_form(csv_file: Path, token_col: str, token_map: Optional[Path]=None) -> pd.DataFrame: votes = pd.read_csv(csv_file, dtype=str, keep_default_na=False) + if token_map: + token_mapping = pd.read_csv(token_map, dtype=str) + assert (token_mapping.columns == ["old", "new"]).all() + token_mapping = token_mapping.set_index("old").to_dict()["new"] + votes[token_col] = votes[token_col].replace(token_mapping) votes = votes.drop_duplicates(subset=[token_col], keep="last") votes = votes.set_index(token_col).drop(columns=["Timestamp"]) headers = []