11

How to round floats to integers while preserving their sum? has the below answer written in pseudocode, which rounds a vector to integer values such that the sum of the elements in unchanged and the roundoff error is minimized. I'd like to implement this efficiently (i.e. vectorized if possible) in R.

For example, rounding these numbers yields a different total:

set.seed(1)
(v <- 10 * runif(4))
# [1] 2.655087 3.721239 5.728534 9.082078
(v <- c(v, 25 - sum(v)))
# [1] 2.655087 3.721239 5.728534 9.082078 3.813063
sum(v)
# [1] 25
sum(round(v))
# [1] 26

Copying pseudocode from answer for reference

// Temp array with same length as fn.
tempArr = Array(fn.length)

// Calculate the expected sum.
arraySum = sum(fn)

lowerSum = 0
-- Populate temp array.
for i = 1 to fn.lengthf
    tempArr[i] = { result: floor(fn[i]),              // Lower bound
                   difference: fn[i] - floor(fn[i]),  // Roundoff error
                   index: i }                         // Original index

    // Calculate the lower sum
    lowerSum = lowerSum + tempArr[i] + lowerBound
end for

// Sort the temp array on the roundoff error
sort(tempArr, "difference")

// Now arraySum - lowerSum gives us the difference between sums of these
// arrays. tempArr is ordered in such a way that the numbers closest to the
// next one are at the top.
difference = arraySum - lowerSum

// Add 1 to those most likely to round up to the next number so that
// the difference is nullified.
for i = (tempArr.length - difference + 1) to tempArr.length
    tempArr.result = tempArr.result + 1
end for

// Optionally sort the array based on the original index.
array(sort, "index")
Community
  • 1
  • 1
Max Ghenis
  • 14,783
  • 16
  • 84
  • 132

3 Answers3

26

In an even simpler form, I would say this algorithm is:

  1. Start with everything rounded down
  2. Round up the numbers with the highest fractional parts until the desired sum is reached.

This can be implemented in a vectorized way in R by:

  1. Round down with floor
  2. Order numbers by their fractional parts (using order)
  3. Use tail to grab the indices of the elements with the k largest fractional parts, where k is the amount that we need to increase the sum to reach our target value
  4. Increment the output value in each of these indices by 1

In code:

smart.round <- function(x) {
  y <- floor(x)
  indices <- tail(order(x-y), round(sum(x)) - sum(y))
  y[indices] <- y[indices] + 1
  y
}
v
# [1] 2.655087 3.721239 5.728534 9.082078 3.813063
sum(v)
# [1] 25
smart.round(v)
# [1] 2 4 6 9 4
sum(smart.round(v))
# [1] 25
josliber
  • 43,891
  • 12
  • 98
  • 133
11

Thanks for this useful function! Just to add to the answer, if rounding to the specified number of decimal places, the function can be modified:

smart.round <- function(x, digits = 0) {
  up <- 10 ^ digits
  x <- x * up
  y <- floor(x)
  indices <- tail(order(x-y), round(sum(x)) - sum(y))
  y[indices] <- y[indices] + 1
  y / up
}
Mikhail
  • 153
  • 2
  • 8
  • amazing great. do you know why results are different if I use map instead? v <- c( 0.9472164, 71.5330771, 27.5197066) lapply(v,smart.round) map(v,smart.round) – titeuf Oct 02 '20 at 14:22
4

Running total and diff based approach is much faster compared to smartRound by @josliber:

diffRound <- function(x) { 
  diff(c(0, round(cumsum(x)))) 
}

Here how results compare on 1m records (see details here: Running Rounding):

res <- microbenchmark(
  "diff(dww)" = x$diff.rounded <- diffRound(x$numbers) ,
  "smart(josliber)"= x$smart.rounded <- smartRound(x$numbers),
  times = 100
)

benchmark of column rounding methods

Unit: milliseconds
expr            min       lq        mean       median     uq       max       neval
diff(dww)       38.79636  59.70858  100.6581   95.4304    128.226  240.3088   100
smart(josliber) 466.06067 719.22723 966.6007   1106.2781  1177.523 1439.9360  100
Community
  • 1
  • 1
Bulat
  • 6,869
  • 1
  • 29
  • 52
  • 3
    While this solution is faster than mine and preserves the desired sum, it unfortunately does not minimize the roundoff error as requested in the question. For instance, with `v <- c(2.655087, 3.721239, 5.728534, 9.082078, 3.813063)`, the roundoff error of `smart.round`, `sum(abs(smart.round(v)-v))`, returns 1.474329 while the roundoff error of `diffRound`, `sum(abs(diffRound(v)-v))`, returns 1.606633. – josliber May 01 '16 at 19:48
  • that is true, the overall roundoff error is not minimised by diff method. I am not sure if it is a strong requirement of the question though. – Bulat May 01 '16 at 19:59
  • 1
    Original question had this clause: "Given that those four conditions are satisfied, an algorithm that minimizes the rounding variance (sum((in[i] - fn[i])^2)) is preferable, but it's not a big deal." – Bulat May 01 '16 at 20:00
  • I think you're referring to the linked question (which was asked by somebody else); all versions of the question here have asked for the rounding that preserves the rounded sum and minimizes the roundoff error. – josliber May 01 '16 at 20:40
  • Agree with josliber, so keeping his as the solution. This is an interesting approach too though, thanks Bulat. – Max Ghenis May 02 '16 at 16:32