Cheating with rounded percentages and floating-point numbers

A picture of Batman dressed in a dark-blueish suit with his tongue pointing out of his mouth as if he's concentrating on something complicated. In the background are a lot of buildings in different shades of colour and some stars in the sky. On top of the whole picture are shiny white maths symbols.

My friends are mean sometimes, but as they were making fun of my maths skills, they also pointed to a valid error on the website. On a specific page of Comics-Outmash, a list of percentages are displayed. They looked totally legit, except that once you added them all up, the total was a little bit more than 100%. This is easy to fix if you’re not afraid of massaging the numbers.

The votes for the next month's title, totaling an underwhelming 94%
The votes for the next month's title, totaling an underwhelming 94%

On Comics-Outmash, members can suggest titles for the following month’s book club edition. After a while, those same members can vote for at least one title they like. The title with the most vote gets picked. Additionally, once a title has been made available, users vote on it depending on how much they liked it. In both cases, we display percentages and in both cases, we ran into that issue:

The votes on a single title which somehow totals 101%
The votes on a single title which somehow totals 101%

There was multiple ways to address this:

  • Present percentages with 2 decimal points. Even if it doesn’t solve the issue (and probably makes the information look worse), it might deter the people from doing maths.
  • Remove percentages altogether and just present the number of votes
  • Find a proper method to address this issue, which is the method I chose

The Quota Method

There are a lot of very smart people on the internet, so it didn’t take long for me to discover the Quota Method.

The quota or divide-and-rank methods make up a category of apportionment rules, i.e. algorithms for allocating seats in a legislative body among multiple groups (e.g. parties or federal states). The quota methods begin by calculating an entitlement (basic number of seats) for each party, by dividing their vote totals by an electoral quota (a fixed number of votes needed to win a seat, as a unit). Then, leftover seats, if any are allocated by rounding up the apportionment for some parties.

This mention seats and political bodies, but it actually applies to our situation as well: we have 100 seats to distribute. The idea is to keep doing the same maths we’re doing but then:

  1. keep the whole part of each number, add them all
  2. subtract the sum from 100 which gives you the “leftover seats”
  3. assign one leftover seat to the numbers with the higher fractional part until we run out

It’s a little confusing to explain, but let’s take a look at an example where we have 4 titles and very precise numbers.

Title Percentage
Batman 13.626332
Flash 47.989636
Captain Marvel 9.596008
Snow Piercer 28.788024
Total 100

Unfortunately, once we round those numbers, the total is now 101:

Title Percentage
Batman 14
Flash 48
Captain Marvel 10
Snow Piercer 29
Total 101

Using the quota method, we can apply the steps we mentioned earlier by splitting and sorting the numbers:

Title Percentage Whole Part Fractional part
Flash 47.989636 47 0.989636
Snow Piercer 28.788024 28 0.788024
Batman 13.626332 13 0.626332
Captain Marvel 9.596008 9 0.596008
Total 100 97 -

The total of the whole part of each numbers is 97, which means that we have 3 extra seats to assign to the numbers with the higher fractional part, that’s Flash, Snow Piercer and Batman, so we add 1 to each and the total is back to 100.

Title Percentage
Flash 48
Snow Piercer 29
Batman 14
Captain Marvel 9
Total 100

Kotlin Implementation

As I mentioned in a previous post, Comics Outmash is written in Kotlin, so here is my implementation. It’s very possible that there is a shorter version, but I like the idea of being able to understand what it’s doing in 6 months:

data class AdjustedItem<T>(
    val item: T,
    val roundedDownValue: Int,
    val decimalPart: Double,
)

fun <T> adjustResults(list: List<T>, accessor: (T) -> Double): List<Pair<T, Int>> {
    val roundedDown = list
        .map {
            val value = accessor(it)
            val roundedVersion = floor(value).toInt()

            AdjustedItem(
                item = it,
                roundedDownValue = roundedVersion,
                decimalPart = value - roundedVersion,
            )
        }.sortedByDescending { it.decimalPart }

    val roundedDownTotal = roundedDown.sumOf { it.roundedDownValue }
    val leftover = 100 - roundedDownTotal

    val lhs = roundedDown.take(leftover).map { it.item to it.roundedDownValue + 1 }
    val rhs = roundedDown.drop(leftover).map { it.item to it.roundedDownValue }

    return lhs + rhs
}

Conclusion

The StackOverflow for maths nerds is a wild place that I’m not accustomed to, but digging into this problen was actually quite fun. Hopefully this will be helpful to someone.