The following objects are masked from 'package:stats':
chisq.test, fisher.test

Attaching package: 'dplyr'

The following objects are masked from 'package:stats':
filter, lag

The following objects are masked from 'package:base':
intersect, setdiff, setequal, union

round from R base

The round() function in Base R will round to the nearest whole number and ‘rounding to the even number’ when equidistant, meaning that exactly 12.5 rounds to the integer 12. However, this is dependent on OS services and on representation error (since e.g. 0.15 is not represented exactly, the rounding rule applies to the represented number and not to the printed number, and so round(0.15, 1) could be either 0.1 or 0.2).

round(1:9/10+0.05,1)

[1] 0.2 0.2 0.3 0.4 0.6 0.7 0.8 0.9 1.0

round_half_up from package janitor

Note that the janitor package in R contains a function round_half_up() that rounds away from zero. In this case it rounds to the nearest whole number and ‘away from zero’ or ‘rounding up’ when equidistant, meaning that exactly 12.5 rounds to the integer 13.

If using the janitor package in R, and the function round_half_up(), the results would be the same with the exception of rounding 1.2345 to 3 decimal places where a result of 1.235 would be obtained instead of 1.234. However, in some rare cases, round_half_up() does not return result as expected. There are two kinds of cases for it. 1. Round down for positive decimal like 0.xx5.

round_half_up(524288.1255, digits =3)

[1] 524288.1

The cause is that when the decimal is stored in binary, the value usually does not exactly the same with the original number. In the example above, 524288.1255 is stored as a value a little less than the original value. Then round_half_up() rounds it down.

options(digits=22)524288.1255

[1] 524288.1254999999655411

In round_half_up(), a small decimal sqrt(.Machine$double.eps) is added before rounding. It avoids some incorrect rounding due to the stored numeric value is a little less than the original value, but does not cover all conditions.

round_half_up <-function (x, digits =0) { posneg <-sign(x) z <-abs(x) *10^digits z <- z +0.5+sqrt(.Machine$double.eps) z <-trunc(z) z <- z/10^digits z * posneg}

More examples can be found from the code below. It creates numeric values containing different digit numbers of integer part and decimal part, and all ending with 5 for rounding.

options(digits=15) #set digit number to display int1 <-c(0,2^(1:19)) #create values of integer partround_digits <-1:7#define values of rounding digitsdec1 <-2^(-round_digits)+10^(-round_digits-1)*5#create values of decimal partdf1 <-cross_join(tibble(int1),tibble(dec1,round_digits)) |>mutate(num1=int1+dec1) #combine integer part and decimal partdf1 |>mutate(rounded_num=round_half_up(num1,round_digits)) |>#round the numbersfilter(rounded_num<num1) |>#incorrect if rounded result is less than the original numberprint.data.frame()

6 of 140 numbers have incorrect results. Most of them are big numbers or long decimals to round.

Round up for positive decimal like 0.4999….

options(digits=16)round_half_up(1.4999999851,0)

[1] 2

It occurs when the number is smaller than but so closed to 0.xx5. As described in point 1 above, in round_half_up(), a small decimal sqrt(.Machine$double.eps) is added before rounding, which causes a number bigger than 0.xx5 to be rounded. It occurs only when the decimal is long, so round_half_up() is still reliable.
And the added decimal sqrt(.Machine$double.eps) is necessary. Without it, or even replace it to a smaller decimal, there will be more incorrect results under point 1, as the example below. Some of them are common, e.g. rounding 16.1255 to 3 decimals.

#a new function to round away from zero, by replacing sqrt(.Machine$double.eps) in round_half_up to a smaller numberround_half_up_test <-function (x, digits =0){ posneg <-sign(x) z <-abs(x) *10^digits z <- z +0.5+ .Machine$double.eps *100 z <-trunc(z) z <- z/10^digits z * posneg}options(digits=15) #set digit number to display df1 |>mutate(rounded_num=round_half_up_test(num1,round_digits)) |>#round the numbersfilter(rounded_num<num1) |>#incorrect if rounded result is less than the original numberprint.data.frame()

https://stackoverflow.com/a/12688836 discussed multiple algorithms to round away from zero, including the one implemented in round_half_up(). Below is another algorithm modified from it.

Like round_half_up(), it also contains the two kinds of incorrect results. And like round_half_up(), a small decimal is added to make 0.xx5 round up. The parameter eps is provided to let user decide which small decimal to add.

To avoid the rounding issue totally, the only way is to increase precision, e.g. using package Rmpfr. It will need CPU resource. And it’s not always necessary considering the accuracy of current functions.

Conclusion

So far, round_half_up() from package janitor is still one of the best solutions to round away from zero, though users may meet incorrect results in rare cases when the numbers are big or the decimal is long.

options(digits =7) #This just returns the number of displayed digits back to the default