Reproducing browser rounding in R
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:
V8
package.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.
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 ,
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"
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));
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
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
# 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")
If you see mistakes or want to suggest changes, please create an issue on the source repository.
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 ...".
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} }