Drum Corona International: Building a Constant Sum Voting System

June 6, 2020
drum-corona-international

In my previous post, I wrote about how I plan on simulating the 2020 DCI season, as there won’t be a real DCI season. In short, I made some changes to my normal DCI forecast model that will allow us to have corps from different years compete against each other in a simulated season.

I’d like this to be something the entire drum corps community can engage with, so I plan on having a vote to determine which shows are included in the simulation. There is a minimum data requirement for me to be able to include a show in the vote 1, but we also need to have a voting system in place before we can hold the vote.

How the Vote Will Work

There are hundreds of shows (I’m using show to mean a corps and year pairing, like “Blue Devils 2010”) that we could include, and the simulated season will consist of at least two dozen of them. In this context, it doesn’t make much sense to give everyone one vote because they’ll have a vested interest in getting more than one show into the simulation. A popular alternative is ranked choice voting, but filling out a ranked choice ballot will be time consuming, potentially confusing, and it requires that everyone give a rank to every show - even the ones they don’t want to see in the simulation.

The solution that I like best is constant sum voting. In it, voters will get some number of votes (probably 50 or 100) they can distribute across any number of the shows, however they’d like. If they really want to see one show in the simulation, they can put all of their votes towards just one show. Or, if they don’t feel as strongly, they can split their votes among 10 shows. I like constant sum voting because it provides a mechanism for voters to indicate their preferences and how strong those preferences are. Ranked choice voting doesn’t allow that level of granularity 2.

I first came across the constant sum voting mechanism when I was investigating Qualtrics to see if I could hold the vote without writing any code. Unfortunately, a Qualtrics license costs way more than I am willing to pay, as does a SurveyMonkey license, and Google Forms (the free alternative) doesn’t support constant sum voting. I guess you get only what you’re willing to pay for. This means I have to build the system myself. The rest of this post walks though the process of building a prototype of this type of vote to prove its feasibility.

How It’s Made

Building this prototype isn’t too hard with Rstudio’s shiny package. I use Shiny to build all of the interactives on my site, because it is flexible enough to do everything I need while being easy enough to learn that I don’t have to sink a ton of time into it. As an added bonus, I don’t have to learn JavaScript!

A shiny app contains two components - a user interface, which lays out the various elements that a user can interact with, and a server, which does all the computation.

User Interface

The user interface is simple. It consists of three panels:

  1. Instructions and other information
  2. The ballot
  3. The submit button and then a message for the voter that the submission was successful

There are two major things this proof of concept needs to accomplish. First, it needs to be able to accommodate any number of options because I don’t know what the choices on the ballot will be. Second, we want the user interface to help the voter by letting them know, at any given time, how many votes they have left and whether they can submit.

The first problem is about code generation. Rather than typing out each element manually, it’d be nice to have a master list that we can edit without having to look at the code. Code generation is a common problem, so this pretty easy to tackle. We can create a function that reads a list of options from a csv file and builds the code as a string. We can then run the function when the app is being rendered and tell R to evaluate the resulting string as though it were normal code, using the eval function.

# This function builds a UI from a choices.csv document
buildChoiceUI <- function(filename, numberOfVotes) {
    choiceData = read.csv("choices.csv", stringsAsFactors=F)
    nChoices = nrow(choiceData)
    defaultVotes = 0
    
    choiceOrder = sample(choiceData$ChoiceID)
    
    UIstring = "tagList("
    sumString = "sum(c("
    uiID = vector(mode="character", length=nChoices)
    for (i in 1:nChoices) {
        index = choiceOrder[i]
        inputVarName = paste("choice", i, sep="")
        choiceLabel = choiceData$Name[index]
        
        
        UIstring = paste(UIstring, 
            " numericInput(inputId='",inputVarName, "'",
            ", label='", choiceLabel, "'",
            ", value=", defaultVotes, 
            ",min=0, max=",numberOfVotes,
            ", step=1)",
            sep = ""
        )
        
        sumString = paste(sumString,
            "input$", inputVarName,
            sep = ""
        )
        
        if (i < nChoices) {
            UIstring = paste(UIstring, ",")
            sumString = paste(sumString, ",")
        }
        
        uiID[index] = inputVarName
    }
    UIstring = paste(UIstring, ")")
    sumString = paste(sumString, "))")
    
    # Add the inputID to the table so we can reference when saving votes
    choiceData$uiID = uiID
    
    return(list(UIstring, choiceData, sumString))
}

This function has a trick up its sleeve. In voting and polling where the user is asked to pick from of several options, the results will be biased towards the options presented first. We don’t want to give an unfair advantage to any shows on the ballot, so the function randomizes the order of presentation for each voter. This keeps the final results bias-free 3.

The next thing to tackle is having the user interface help the voter by providing some information. Because this requires some behind-the-scenes calculations, we shift the responsibility from the UI to the server.

Server

The server provides real-time feedback to the user via an observe function. An observer will figure out which user inputs it depends on, and automatically execute the code it contains whenever one of those input changes. Any time a vote is entered, altered, or removed, this function will run.

numericObserver <- observe({
    shinyjs::hide("submit")

    #print(eval(parse(text=totalVoteString)))
    totalVotesCast = eval(parse(text=totalVoteString))
    #print(totalVotesCast)

    # Handle the text message first
    if (!is.na(totalVotesCast)) { # This is a failsafe if the UI hasn't completely rendered, or a 
        if (totalVotesCast < votesAllowed & !reactVals$hasSubmit) {
            votesLeft = votesAllowed - totalVotesCast
            output$submitmessage = renderText(paste(
                "You still have", votesLeft, "votes left to cast before you can submit."
            ))
        } else if (totalVotesCast > votesAllowed & !reactVals$hasSubmit) {
            votesOver = totalVotesCast - votesAllowed
            output$submitmessage = renderText(paste(
                "You've entered", votesOver, "votes too many! You must remove them before you can submit."
            ))
        } else if (totalVotesCast == votesAllowed) {
            #print(reactVals$hasSubmit)
            if (!reactVals$hasSubmit) {
                output$submitmessage = renderText(
                    "You have entered the correct number of votes. Hit SUBMIT to vote now!"
                )
                shinyjs::show("submit")
            } else {
                output$submitmessage = renderText(
                    "Thank you for voting! Your results have been recorded."
                )
            }
        }
    }
})

There’s a lot indentation to track here, but this is what the code actually does:

  1. The first thing the observer does is hide the submit button to make sure the user doesn’t accidentally submit their vote with errors, like having the wrong number of votes.
  2. Any time an input changes, it runs code to sum the votes that have been entered so far 4 and compares that to the total number of votes they should cast.
  1. The observer tells the UI to render a message for the voter, letting them know how many votes they have left to cast if they’ve entered too few, or how many votes to take away if they’ve entered too many. This provides clarity to the voter and helps them submit a correct ballot.

  2. Once the number of votes is correct, it unhides the submit button so the user can cast their vote. Once they do, the server flips the hasSubmit boolean to true so the observer knows the vote has been recorded.

  3. When the observer sees the ballot has been recorded, it takes the submit button away for good, so that they can’t spam votes by clicking over and over again.

This series of steps is run every time the user changes their ballot, which means they will be getting real-time feedback as they fill it out. The key to all of this is that the function executes quickly, so that the user (hopefully) doesn’t notice it at all. They should get a smooth experience from start to finish as the user interface tells them how to proceed at any given time, and then helps to make sure they don’t submit extra ballots.

The last thing to cover is how the server records the vote itself. There are lots of ways that we could store the votes - we could use a database, link the application with a Google Drive file or folder, or even try to keep all the votes in memory. Each of these options, in general, is a sensible solution. So the best choice for our use case is the one which satisfies our needs.

Given that it’s already June when I’m writing this, we need something that will be fast, simple, and robust to my silly mistakes. Database connections can be fickle and require us to be very deliberate about handling concurrency, in case multiple voters are casting a vote simultaneously. Google Drive has the same problem, and either one requires nontrivial setup. Holding the votes in memory is fine, but if the power is ever cut to the server, even if it’s not for very long, we lose every vote. Ultimately, I think the best solution is to write a small csv file to disk for each vote. In R, writing this file is a 1-liner.

When someone submits a vote, a few things happen. First, the server logs the time that is was hit, down to the millisecond. It uses this to build a name for the csv which will be unique to each vote, using the convention vote_YYYY_MM_DD_HH_MM_SS_mmm.csv 5. In this, the upper case letters refer to the time unit which matches their letter (year, month, day, hour, minute, and second), and the lowercase m is the number of milliseconds.

The server then writes a table to that csv file with the following columns:

  1. The ID number of each choice
  2. The choice name (for example, one may be “Glassmen 2010”)
  3. The order in which each choice was presented (for later analysis if we want)
  4. The votes the choice received

All we have to do to tabulate the final vote is read each output file and add up the votes. Each of these files will be very small (probably no more than 5 KB), which means we won’t have to worry about eating up a ton of disk space on the server and they’ll be easy to back up. This approach is quick, simple to understand, and robust - both in terms of handling concurrent votes and making it harder for me to screw up on accident.

What’s Next

Fundamentally, that’s all there is to hosting a constant sum vote. I have posted this code to github if you want to take a look, and I have this simple app up and running on my server to give a sense for what the user interface looks and feels like. I don’t record these votes though, so don’t get too excited.

The next task is to determine the choices for the ballot itself, so that we can pick the competitors of the inaugural (and hopefully only) season of Drum Corona International!


Support

I hope you are enjoying Drum Corona International! People who do open-source projects like this often ask for donations to help cover expenses (like server costs) but luckily I’ve got that covered. Instead, if you’re enjoying any of my projects, please consider donating to the Michigan Drum Corps Scholarship Fund.

The Michigan Drum Corps Scholarship Fund is a 501(c)3 nonprofit that I cofounded to help support members of the Michigan band community who are interested in DCI. The organization offers scholarships to Michigan students marching or auditioning with any DCI drum corps. The economic implications of COVID-19 are broad, and I think scholarships programs like this are especially important for keeping drum corps accessible to everyone. Every dollar helps!



  1. I’m working on this right now, and my next blog post will probably be about all the considerations and all the work required to come up with a good list of potential competitors.↩︎

  2. If you’re in the mood for a fun thought experiment, think about the ramifications this voting mechanism would have on political primaries here in the United States.↩︎

  3. Whether or not I use this in the actual vote is yet to be determined. A list of years that is presented in a random order might make things look unintuitive and confusing to the voter, which may prevent them from voting at all!↩︎

  4. Astute readers may have noticed that this code was automatically generated by the same function that builds the user interface from the list of choices.↩︎

  5. Technically, this naming convention isn’t guaranteed to be unique to each voter. If two people submit at the exact same millisecond, then we’ll have a problem on our hands. This is extremely unlikely, so while this solution isn’t perfect, it’s good enough.↩︎

Drum Corona International Week in Review: July 11

July 11, 2020
drum-corona-international drum-corona-week-in-review

Drum Corona Spotlight: California Nostalgia

July 10, 2020
drum-corona-international drum-corona-spotlight

Drum Corona Spotlight: North of the Border

July 7, 2020
drum-corona-international drum-corona-spotlight