Cheating with rounded percentages and floating-point numbers
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.
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:
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:
- keep the whole part of each number, add them all
- subtract the sum from 100 which gives you the “leftover seats”
- 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.