R rounding is weird? Try JavaScript!

Reproducing browser rounding in R

Duncan Garmonsway
April 25, 2016

round(0.5) == 0? Eh?

A common source of confusion in R is rounding-to-even (example adapted from ?round):

{r rounding-confusion, echo = TRUE) matrix(c(x1, round(x1)), nrow = 2, byrow = TRUE) #-- IEEE rounding !

This post does five things:

Graph of rounding bias

Here is an unpolished graphical illustration of the bias introduced by rounding halves (0.5, 1.5, etc.) away from zero. The details of the difference are neatly explained in the R Inferno, circle 8.1.52.

JavaScript rounding in the Chrome browser

But when I tried to emulated a website’s behaviour in R, it turned out that Chrome was rounding towards odd numbers after the decimal point (anyone know why?). Try the following in the Chrome Developer Console (ctrl+shift+c in a tab).


// Rounds away from zero
console.log((-1.5).toFixed(0));
console.log((-0.5).toFixed(0));
console.log((0.5).toFixed(0));
console.log((1.5).toFixed(0));

// Rounds to odd
console.log((-0.25).toFixed(1));
console.log((-0.15).toFixed(1));
console.log((-0.05).toFixed(1));
console.log((0.05).toFixed(1));
console.log((0.15).toFixed(1));
console.log((0.25).toFixed(1));

How odd is that? So I adapted a handy MATLAB implementation of rounding-to-odd, and compared it with the other two strategies.

The graph shows that, like rounding-to-even, rounding-to-odd is unbiased, but a snag is that successive rounded operations will never reach zero (see comments on this StackExchange answer):


x <- 77
for(i in 1:10) {
  x <<- round_to_odd(x/2)
  cat(x, ", ")
}
## 39 , 19 , 9 , 5 , 3 , 1 , 1 , 1 , 1 , 1 ,

x <- 77
for(i in 1:10) {
  x <<- round_to_even(x/2)
  cat(x, ", ")
}
## 38 , 19 , 10 , 5 , 2 , 1 , 0 , 0 , 0 , 0 ,

Floating point errors

Using my new round-to-odd function to emulate JavaScript behaviour, I encountered floating point errors. For example, take the number 6.65:


sprintf("%.16f", c(6.65, 7 * 0.95, 7 - 0.35))
## [1] "6.6500000000000004" "6.6499999999999995" "6.6500000000000004"
The tiny differences don’t affect rounding in R:

round_to_odd(c(6.65, 7 * 0.95, 7 - 0.35), 1)
## [1] 6.7 6.7 6.7

But they do affect rounding in JavaScript. Again, paste these into the browser console:


console.log((6.65).toFixed(1));
console.log((7 * 0.95).toFixed(1));
console.log((7 - 0.35).toFixed(1));

Calling JavaScript V8 engine via the V8 package

At this point, I gave up on emulating JavaScript behaviour in R, and resorted to calling JavaScript from R via the V8 package, which uses the V8 JavaScript engine, the same that my browser (Chrome) uses.


library(V8)

# library(V8)
ct <- V8::v8()
roundjs <- function(x, digits) {
  sapply(x, function(y) {ct$get(paste0("Number((", sprintf("%.16f", y), ").toFixed(", digits, "))"))})
}
roundjs(c(6.65, 7 * 0.95, 7 - 0.35), 1)
## [1] 6.7 6.6 6.7

What took me so long

This was a particularly tricky part of a bigger project (see next week’s post).

Most of the time went on finding, testing and correcting the two rounding functions for round-to-odd and round-away-from-zero. I adapted the round-to-odd function from some handy MATLAB implementations of various rounding strategies. Unfortunately, they depended on MATLAB’s built-in round function, which, according to its documentation, rounds away from zero, so I had to find a round-away-from-zero function in R first. Even then, it didn’t work for negatives when I ported it to R, probably due to fundamental language differences:


# Surprising behaviour of
-1.5 %% 2
## [1] 0.5
# Predictable behaviour (but different to MATLAB?)
-1.0 %% 0
## [1] NaN

I also spent quite a while on the graphs of bias, where I befuddled myself by drawing random numbers between 0 to 1 (which is unfair on unbiased functions, because only 0.5 is represented, not 1.5), and by not doing preliminary rounding on the random draws (which meant that 0.5, 1.5, etc., weren’t represented at all).

Finally, my initial V8 function used the V8 package’s own magic for passing values to the V8 engine, but when it didn’t work, I suspected that the values were being passed as a string, and that R was rounding them as part of the conversion. For example:


library(V8)
roundjs <- function(x, digits) {
  ct <- V8::v8()
  ct$source(system.file("js/underscore.js", package="V8")) # Essential for _
  ct$assign("digits", digits)
  xrounded <-
    ct$call("_.forEach",
            x,
            V8::JS("function(item, index, arr) {arr[index] = Number(item.toFixed(digits));}"))
  xrounded
}
roundjs(c(6.65, 7 * 0.95, 7 - 0.35), 1)
## [1] 6.7 6.7 6.7

Code for the graphs


# Compare the two systems
# x1 <- seq(-2.5, 2.5)
# matrix(c(x1,
#          round_to_even(x1),
#          round_to_odd(x1)),
#        nrow = 3, byrow = TRUE)

# Graph the bias of many random draws
N <- 10000
bias<- function(FUN) {
  # Round to one decimal place to ensure 0.5 ever appears.
  # Draw between 0 and 2 to fairly represent both 0.5 and 1.5.
  x <- round(runif(10, min = 0, max = 2), 1)
  mean(FUN(x) - x)
}

bias_to_even <- replicate(N, bias(round_to_even))
bias_away_from_zero <- replicate(N, bias(round_away_from_zero))

limits <- c(-0.5, 0.5)
par(mfrow = c(2, 1))
  hist(bias_to_even, xlim = limits, col = "lightgreen")
  hist(bias_away_from_zero, xlim = limits, col = "lightblue")

# Compare the three systems
# x1 <- seq(-2.5, 2.5)
# matrix(c(x1,
#          round_to_even(x1),
#          round_away_from_zero(x1),
#          round_to_odd(x1)), nrow = 4, byrow = TRUE)

bias_to_odd <- replicate(N, bias(round_to_odd))

limits <- c(-0.5, 0.5)
par(mfrow = c(3, 1))
  hist(bias_to_even, xlim = limits, col = "lightgreen")
  hist(bias_away_from_zero, xlim = limits, col = "lightblue")
  hist(bias_to_odd, xlim = limits, col = "pink")

Corrections

If you see mistakes or want to suggest changes, please create an issue on the source repository.

Reuse

Text and figures are licensed under Creative Commons Attribution CC BY 4.0. Source code is available at https://github.com/nacnudus/duncangarmonsway, unless otherwise noted. The figures that have been reused from other sources don't fall under this license and can be recognized by a note in their caption: "Figure from ...".

Citation

For attribution, please cite this work as

Garmonsway (2016, April 25). Duncan Garmonsway: R rounding is weird?  Try JavaScript!. Retrieved from https://nacnudus.github.io/duncangarmonsway/posts/2016-04-25-rounding/

BibTeX citation

@misc{garmonsway2016r,
  author = {Garmonsway, Duncan},
  title = {Duncan Garmonsway: R rounding is weird?  Try JavaScript!},
  url = {https://nacnudus.github.io/duncangarmonsway/posts/2016-04-25-rounding/},
  year = {2016}
}