From b3e76b8a26d782f75a604db1cb4ee2a591e9b10b Mon Sep 17 00:00:00 2001 From: Jonathan Cooper Date: Fri, 18 Aug 2023 09:05:07 +0100 Subject: [PATCH 1/4] Reflect changed question name in live form --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d2e54a9..498dd95 100644 --- a/README.md +++ b/README.md @@ -13,5 +13,5 @@ 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 ``` From 383cddf494855e11912e47ffccef1715e404f43f Mon Sep 17 00:00:00 2001 From: Jonathan Cooper Date: Fri, 18 Aug 2023 09:05:30 +0100 Subject: [PATCH 2/4] Print some more diagnostic info --- ranked_vote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ranked_vote.py b/ranked_vote.py index 40df88b..459c4f3 100644 --- a/ranked_vote.py +++ b/ranked_vote.py @@ -30,7 +30,7 @@ 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) From 16dd945ecc00945144bc679a88bfaadab165d4eb Mon Sep 17 00:00:00 2001 From: Jonathan Cooper Date: Mon, 17 Aug 2026 10:52:24 +0100 Subject: [PATCH 3/4] Add quick implementation of token mapping In case of accidentally sending out two sets of voting tokens! --- ranked_vote.py | 4 +++- tests/map.csv | 4 ++++ tests/test_voting.py | 7 +++++++ utils.py | 8 +++++++- 4 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 tests/map.csv diff --git a/ranked_vote.py b/ranked_vote.py index 739c5de..894caaf 100644 --- a/ranked_vote.py +++ b/ranked_vote.py @@ -26,9 +26,11 @@ 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) 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 = [] From 180f3e226cf12f2c581f1f2ce207e287fa29cea7 Mon Sep 17 00:00:00 2001 From: Jonathan Cooper Date: Mon, 24 Aug 2026 10:49:59 +0100 Subject: [PATCH 4/4] Add instructions for using the new option --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 498dd95..fce4394 100644 --- a/README.md +++ b/README.md @@ -15,3 +15,14 @@ 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 "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 +```