Skip to content

Project 2: World of Elections

Dukes Vote logo
credit: Dukes Vote

Remember to Vote!

We have a major election this year in the United States. The student vote is the decisive factor in many election races. Review the 2024 Election Guide and FAQ from the Madison Center for Civic Engagement. You can check your voter registration status via that page.

Objectives

  • Write conditionals to make decisions in a program.
  • Work with lists and loops to iterate over data sets.
  • Use relational and logical operators in expressions.
  • Design one or more methods to simplify the code.
  • Write unit tests that cover 100% of the program.
  • Read and process file input of different types.

Background

Election season is upon us, and many people don't realize just how involved computers, algorithms, and network security are in this process. While we won't get into all the details here, you might check out the wikipedia article on electronic voting. Also of interest is the humanitarian open source software project Ushahidi which develops crowd sourcing tools and began in response to post-election violence in Kenya in 2008.

But on with our own election tools! For this assignment we will implement and compare several types of election systems used here in the U.S and also across the world. You will implement several ballot counting approaches as outlined below based on 4 candidate elections.

You will implement two typical vote counting methods (popular and majority) then an electoral system counting and finally a ranked choice voting (RCV) aka instant runoff voting (IRV) system. All elections will read votes from a single delimited value file in this format: voterID, State/Ward/District for electoral voting, A, B, C, D where A,B,C,D are candidates and the numbers represent 1–4 first through last (4th) choice.

Type# method signature Description
1. Popular Vote def count_popular(vote_counts): that returns an int index of the winning candidate -1 if invalid or -2 for a tie the candidate with the highest number of votes wins. A tie results in an invalid election (return -2 in that case). For all other invalid elections return -1.
2. Absolute majority def count_majority(vote_counts): returns an int index of the winning candidate, or -1 if invalid, -2 if a tie, -3 if not majority. the candidate with the highest number of votes wins, but only if the number of votes is more than 50% of all votes cast in the election. If the highest number of votes is not a majority, the election should return -3 in that case or -2 if a tie or -1 if invalid in any other case.
3. Electoral Vote def electoral_votes(votes_per_state, allocation_per_state): index of the winning candidate, or return -3 in that case or -2 if a tie or -1 if invalid in any other case. This takes 2 parameters a) the votes per state dictionary which has the number of votes each candidate received per state and b) the allocation per state a dictionary of how electoral votes are mapped across key states of interest or all states depending on the scope of your election. For this assignment, if the state vote is invalid, exclude the state from the election. The electoral_votes function return -3 in that case or -2 if a tie or -1 if invalid in any other case.
4. Ranked Choice/Instant Runoff Voting def irv(ballot, candidates): returns an int for the winning candidate index value or -1 if an invalid election. In IRV elections, multiple rounds take place. At the start of each round, if any candidate secures a majority (over 50%) in first-place (first choice) votes, that candidate wins. If no candidate has a majority, the candidate with the fewest first-place votes is eliminated, and those votes are reassigned to each voter’s next preferred candidate. This process repeats until one candidate achieves a majority, as illustrated in the example in Part C below.

Provided Data and Code

Start with the following source files:

Data files: - these should be put in a CS149/data directory in VSCode or in the pa2/data folder for Thonny. Your tests should all reference these files using the path data/name_of_file (Ex: data/raw_ballot_5.csv).

Part B:

Part C:

Overall setup

Within VSCode or Thonny, create a new folder/directory for this project and call it pa2. Download the provided code files into this folder for the appropriate submission.

Create Test Cases First

It might seem out of order, but the first step in this PA is to construct tests that verify the correct operation of your (future) code. This technique is known as Test Driven Development (TDD).

You should create test data files/cases for various types of vote ties. Remember you don't need that many votes to test things - try some short test files of only 2–3 votes per candidate (say 8–12 lines total) that will test various scenarios of ties and invalid elections.

test_election_b.py is provided as a starting point. DO NOT start working on election_b.py file until you have completed the creation of some tests.

Part B

test_vote_file_processor_b.py

You must write unit tests for test_vote_file_processor_b.py. You should write several smaller tests to handle distinct cases. The provided file contains two testing functions to get you started.

You should properly document your test_vote_file_processor_b.py file at the top. Docstrings for test functions are generally not required.

Important

Your tests must use only the data files provided above. No other data files will be present when you submit your tests for grading.

vote_file_processor_b.py

The vote_file_processor_b.py file will contain a single method:

read_first_choice(ballot_file, delimiter)

Take in two String arguments for the ballot_file (filename of the file) and delimiter (i.e. ", "). Read the first choice counts from a raw ballot file, separating the items based on the passed delimiter. You should return None if the file does not exist or there are other issues with the file name. Otherwise you should return a list of the candidates' first choice counts.

Sample file:

voterID State A B C D
Voter0 LA 4 1 2 3
Voter1 AZ 3 4 1 2
Voter2 MO 3 2 4 1
Voter3 MN 2 3 1 4
Voter4 AZ 2 1 4 3

Sample return: [0, 2, 2, 1]

In the sample file above, for instance, Voter0’s first choice is candidate B, with candidate C as their second choice, followed by candidates D and A.

Candidate A appears as 0 voters first choice, Candidate B appears as 2 voters first choice, Candidate C appears as 2 voters first choice and Candidate D appears as 1 voters first choice.

test_election_b.py

The test_election_b tests will test these two functions in election_b.py.

You should write tests that cover your entire election_b.py file. See the Coverage Lab for details on how to do coverage testing.

election_b.py

You now need to write your own election program which contains the 2 functions you tested in Part B. As you work on the assignment, you may find it useful to reuse existing methods and/or implement additional methods to simplify your code. Feel free to add new methods to election_b.py (or in part C, election_c.py), but do NOT change the design of any of the provided functions. Duplicating logic/code within your methods (which are clearly performed by existing methods) may result in point deductions.

count_popular(vote_counts)

This should take a single parameter of a list of vote counts for each candidate 0-3 (A-D) in our four candidate election. You must find the candidate with the most votes and return that candidate's index number (0-3) or return -1 if there is an invalid election, or -2 if there is a tie.

count_majority(vote_counts)

This should take a single parameter of a list of vote counts for each candidate 0-3 (A-D) in our four candidate election. You will find the candidate with the majority of votes (more than 50% is considered a majority for our elections) and return that candidate's index number (0-3) or -1 if there is an invalid election, or -2 if there is a tie, or -3 if there is no majority.

Part C

In part C you may import the functions from Part B, but for submission you do not need to submit your part B files to Gradescope as we will provide our solutions for these.

test_vote_file_processor_c.py

You must write unit tests for test_vote_file_processor_c.py. You should write several smaller tests to handle distinct cases. The provided file contains two testing functions to get you started.

You should properly document your test_vote_file_processor_c.py file at the top. Docstrings for test functions are generally not required.

Important

Your tests must use only the data files provided above. No other data files will be present when you submit your tests for grading.

vote_file_processor_c.py

The vote_file_processor_c.py file will contain 3 methods:

read_first_choice_per_state(ballot_file, delimiter)

Take in two String arguments - the ballot_file (filename of the file) and delimiter (e.g. ", "). Read the first choice counts from a raw ballot file where the items are separated based on the passed delimiter.

In the US, national electoral voting system - there are 538 electoral votes, and a candidate needs a majority of 270 electoral votes to win the presidency. Most states use a "winner-takes-all" system, meaning the candidate who wins the popular vote in a state typically receives all of that state's electoral votes, with exceptions in Maine and Nebraska, which use a proportional system.

In this function, you should build and return a dictionary object that represents the candidates' first choice counts per state, where the keys are the states and the values are the counts. This dictionary will be used in your electoral_votes function in election_c.py.

Based on the sample input file:

voterID State A B C D
Voter0 LA 4 1 2 3
Voter1 AZ 3 4 1 2
Voter2 MO 3 2 4 1
Voter3 MN 2 3 1 4
Voter4 AZ 2 1 4 3

Your dictionary should look like this:

{'LA': [0, 1, 0, 0], 'AZ': [0, 1, 1, 0],
 'MO': [0, 0, 0, 1], 'MN': [0, 0, 1, 0]}
The dictionary represents the total number of first choices per candidate for all the voters in a state.

read_complete_ballot(ballot_file, delimiter)

Take in two String arguments - the ballot_file (filename of the file) and delimiter (e.g. ", "). Read the complete ballot from a raw ballot file where the items are separated based on the passed delimiter.

For the sample input file:

voterID State A B C D
Voter0 LA 4 1 2 3
Voter1 AZ 3 4 1 2
Voter2 MO 3 2 4 1
Voter3 MN 2 3 1 4
Voter4 AZ 2 1 4 3

your dictionary should look like this:

{'Voter0': [1, 2, 3, 0],
 'Voter1': [2, 3, 0, 1],
 'Voter2': [3, 1, 0, 2],
 'Voter3': [2, 0, 1, 3],
 'Voter4': [1, 0, 3, 2]}
The dictionary structure has the voters as the keys and a list of choices as the values.

Imagine that the candidate choices are represented by numbers:

A = 0
B = 1
C = 2
D = 3

Notice that the structure of the input file is that the positions in the file represent the candidate choices and the values represent ranks.

For example, in the file,Voter0 has the list of numbers 4, 1, 2, 3 which means that candidate A (position 0) is their 4th ranked candidate, candidate B (position 1) is their 1st ranked, candidate C (2) is their 2nd ranked and candidate D (3) is their 3rd ranked candidate.

In the result dictionary, the positions represent ranks and the values represent choices. For Voter0, in the first position (representing rank 1), we find the number 1 (representing candidate B), in the second position (rank 2), we find 2 (candidate C), in the third position (rank 3), there is a 3 (for candidate D), and in the fourth position (rank 4) there is a 0 (candidate A).

Verify this transformation by looking at Voter1. The list [2, 3, 0, 1] means candidate 2 (C) was ranked 1st, candidate 3 (D) was ranked 2nd, candidate 0 (A) was ranked 3rd, and candidate 1 (B) was ranked 4th.

The result dictionary will be used in your irv function in election_c.py.

read_electoral_college(electoral_file)

This function will read the electoral votes from a JSON file. It should return None if the file does not exist.

test_election_c.py

The test_election_c tests will test the functions in election_c.py.

You may want to use the global variables:

A = 0
B = 1
C = 2
D = 3
to map candidate indexes to candidate letters A, B, C, D.

election_c.py

You now need to write your own election program which contains the 2 methods you tested in Part C.

electoral_votes(votes_per_state, allocation_per_state)

This will find the overall winner of the state-wide popular vote. This takes 2 parameters a) the votes per state dictionary which has the number of votes each candidate received per state and b) the allocation per state a dictionary of how electoral votes are mapped across key states of interest or all states depending on the scope of your election. For this assignment, if the state vote is invalid or a tie, exclude the state from the election. Assume all states use a winner-takes-all method, which means the candidate who wins the state takes all of the electoral votes allocated to that state.

Sample input:

voterID State A B C D
Voter0 LA 4 1 2 3
Voter1 AZ 3 4 1 2
Voter2 MO 3 2 4 1
Voter3 MN 2 3 1 4
Voter4 AZ 2 1 4 3

Sample return: -2

Note 1: There is a tie in the electoral votes nationwide. Note 2: The sample input and output here demonstrate the entire process, including both the reading and electoral vote steps.

irv(ballot, candidates)

Finds the overall winner using instant runoff voting explained below. This takes 2 parameters a) ballot - a dictionary of voter preferences over candidates b) candidates, a set of candidate ids. This will return an int of the winner.

Sample input file:

voterID State A B C D
Voter0 LA 4 1 2 3
Voter1 AZ 3 4 1 2
Voter2 MO 3 2 4 1
Voter3 MN 2 3 1 4
Voter4 AZ 2 1 4 3

Sample return: 1

Note: Candidate 2 is Bob Barker in the election driver

Sample dictionary:

 ballot1 = {"v0":  [B, A, C, D],
                   "v1":  [B, D, C, A],
                   "v2":  [B, D, C, A],
                   "v3":  [B, C, D, A],
                   "v4":  [C, D, A, B],
                   "v5":  [A, C, B, D],
                 }

The return from this should be: 1

Instant Runoff Voting (IRV) / Rank Voting explanation

Instant Runoff Voting Reference

Example 1

Consider the preference table below, in which we have four different candidates, called A, B, C, and D here for simplicity.

voterid First choice Second choice Third choice Fourth choice
"v0" B A C D
"v1" B A C D
"v2" B D C A
"v3" C D B A
"v4" C D A B
"v5" B A C D

Round 1:

  • Count the votes for each candidate as the first choice
    • A is 0, B is 4, C is 2, and D is 0
  • There are total of 0 + 4 + 2 + 0 = 6 votes. A majority would be 4 votes. B has a majority, so the winner should be B.

Example 2

Consider the preference table below, in which a company’s advertising team is voting on four different candidates, called A, B, C, and D here for simplicity.

voterid First choice Second choice Third choice Fourth choice
"v0" A C B D
"v1" B A C D
"v2" B D C A
"v3" C D B A
"v4" C D A B
"v5" A C B D

Round 1:

  • Count the votes for each candidate as the first choice
    • A is 2, B is 2, C is 2, and D is 0
  • There are total of 2 + 2 + 2 + 0 = 6 votes. A majority would be 4 votes. No one yet has a majority, so we proceed to elimination rounds.
  • Candidate D has the fewest first-place votes, so we remove that choice
voterid First choice Second choice Third choice Fourth choice
"v0" A C B
"v1" B A C
"v2" B C A
"v3" C B A
"v4" C A B
"v5" A C B
  • We then shift everyone’s choices up to fill the gaps.
voterid First choice Second choice Third choice
"v0" A C B
"v1" B A C
"v2" B C A
"v3" C B A
"v4" C A B
"v5" A C B

Round 2:

  • Count the votes for each candidate as the first choice
    • A is 2, B is 2, C is 2
  • There are total of 2 + 2 + 2 = 6 votes. A majority would be 4 votes. No one yet has a majority, so we proceed to elimination rounds.
  • Candidates A, B, and C are with the same votes, but C is the last (larger index), so we remove that C
voterid First choice Second choice
"v0" A B
"v1" B A
"v2" B A
"v3" B A
"v4" A B
"v5" A B

Round 3:

  • Count the votes for each candidate as the first choice
    • A is 3, B is 3
  • There are total of 3 + 3 = 6 votes. A majority would be 4 votes. No one yet has a majority, so we proceed to elimination rounds.
  • Candidates A, and B are with the same votes, but B is the last (larger index), so we remove that B
voterid First choice
"v0" A
"v1" A
"v2" A
"v3" A
"v4" A
"v5" A

Round 4:

  • Count the votes for each candidate as the first choice
    • A is 6.
  • There are total of 6 votes. A majority would be 4 votes. So that A is the winner.

Summary of Submissions:

Part A (10 points)

1) Readiness Quiz (10 points) in Canvas.

Before the deadline for Part A you should read this document carefully, then look at the starter code provided above. Once you have a clear understanding of the expectations for this assignment, complete the readiness quiz in Canvas (10 points). The grading for this quiz will be all or nothing: your score on the quiz will be 0 if you miss any questions.

If you do not successfully complete the readiness quiz, you cannot receive any credit for Part B or Part C.

Part B (30 points)

Submit the following files to the corresponding Gradescope assignments:

  • PA2B Tests
    • test_election_b.py
    • test_vote_file_processor_b.py
  • PA2B Code
    • election_b.py
    • vote_file_processor_b.py

Be sure to test your code carefully for correctness AND style before submitting to gradescope.

You are limited to 15 submissions for Part B.

Part C (60 points)

Submit the following files to the corresponding Gradescope assignments:

  • PA2C Tests
    • test_election_c.py
    • test_vote_file_processor_c.py
  • PA2C Code
    • election_c.py
    • vote_file_processor_c.py

Before uploading your submission to Gradescope, be sure to complete the following steps:

  • Test your solution carefully with your own tests.
  • Make sure that election_driver.py works as expected with your finished code.
  • Review the course style guide to ensure that your code meets the style requirements.
  • Run flake8 and eliminate all warnings.
  • Review and update comments as needed.

You are limited to 10 submissions for Part C.

Acknowledgments

This assignment was based on a version created by Chris Mayfield and Dee Weikle. The Wikipedia article, and this assignment page, are licensed under the Creative Commons Attribution-Share-Alike License 3.0.