Rounding in R: Common Data Wrangling Frustrations and Workarounds in R, Julia, and Python

Reading time:
time
min
By:
Deepansh Khurana
April 25, 2023

If you’ve played with data for long enough, you’re sure to run into some popular dead-ends. There are many of these, from misspelled location names and addresses to placeholder values entering the data pipeline because of a bug. One of the most frustrating things, if not the prime pain point of peril, is rounding numbers. More often than not, you do not get what you expect, regardless of what language you are working on. The good thing about a team like ours is that we share not only our wins but also things that make us pull our hair out. This blog post is inspired by one such discussion. <blockquote>Tired of validating data? <a href="https://appsilon.com/automated-r-data-quality-reporting/" target="_blank" rel="noopener">Automate it! and generate reports in R and Shiny</a>.</blockquote> <h2>Hey, Does This Look Right To You? The Rounding Issue in Programming 🤔</h2> Most things start with an innocent bug, or at least, what looks like one. And that is where this story begins as well. <code>round(0.5)</code> If you enter the above in the R Console, unless you know all the context already, you would expect the standard mathematical procedure. The result should be 1, you’d tell yourself, and when you hit Return, the display will flash. <code>&gt;&gt; 0</code> But how? Surely, there is something wrong here. To check, let’s try another example. <code>round(1.5) &gt; 2</code> And if you look closely, both of those numbers share a common property. They are even numbers. That is the first thing you learn when you learn about rounding in R, even if you learn about it the hard way: <b>R rounds to Even.</b> <h2>But Why Round to Even in R? Who Gains From This? 🤷</h2> Tl;dr: You gain and it’s the only reasonable and deterministic way. As it turns out, a lot of things. Rounding to Even is rooted in a standard called IEC 60559. The standard dictates that you round to the nearest even number. So, round(0.5) becomes 0 and even round(-1.5) becomes -2. The standard, however, is not agnostic to the operating system and representation error, which is where the second problem comes in but we will get to that. First, we must try to understand the reasoning behind this standard. For that, let’s go to a piece of recent history and quote, Greg Snow and his famous explanation from 2008. <blockquote><i>“The logic behind the round to even rule is that we are trying to represent an underlying continuous value and if x comes from a truly continuous distribution, then the probability that x==2.5 is 0 and the 2.5 was probably already rounded once from any values between 2.45 and 2.54999999999999..., if we use the round up on 0.5 rule that we learned in grade school, then the double rounding means that values between 2.45 and 2.50 will all round to 3 (having been rounded first to 2.5). This will tend to bias estimates upwards. To remove the bias we need to either go back to before the rounding to 2.5 (which is often impossible to impractical), or just round up half the time and round down half the time (or better would be to round proportional to how likely we are to see values below or above 2.5 rounded to 2.5, but that will be close to 50/50 for most underlying distributions). The stochastic approach would be to have the round function randomly choose which way to round, but deterministic types are not comfortable with that, so "round to even" was chosen (round to odd should work about the same) as a consistent rule that rounds up and down about 50/50.</i> <i>If you are dealing with data where 2.5 is likely to represent an exact value (money for example), then you may do better by multiplying all values by 10 or 100 and working in integers, then converting back only for the final printing. Note that 2.50000001 rounds to 3, so if you keep more digits of accuracy until the final printing, then rounding will go in the expected direction, or you can add 0.000000001 (or other small number) to your values just before rounding, but that can bias your estimates upwards.” </i>(<a href="https://stat.ethz.ch/pipermail/r-help/2008-June/164927.html" target="_blank" rel="nofollow noopener">source</a>)</blockquote> Maybe let’s take a look at a hands-on experiment. We wanted to show you the effect of different rounding methods in R/python/javascript, but <b>they don’t even implement</b> methods different from Round to Even! <blockquote>Using the RStudio IDE? <a href="https://appsilon.com/rstudio-shortcuts-and-tips/" target="_blank" rel="noopener">Maximize your productivity with our favorite shortcuts and tips</a>!</blockquote> Fortunately <a href="https://julialang.org/" target="_blank" rel="nofollow noopener">julia</a> implements different rounding methods and we can play with them. <h3>The Experiment 🧪</h3> Let’s take a large vector of thousand random numbers from 0 to 1. Then let’s round each number in this vector to 1 decimal place in three different ways, Round to Even (the default), RoundUp and RoundDown. Note that RoundUp is equivalent to our school technique of rounding. Finally, we’ll compare which mean is closer to the mean of the original vector. <pre><code> using Random, Statistics x = rand(MersenneTwister(0), 1_000) y1 = round.(x, digits=1) y2 = round.(x, RoundUp, digits=1) y3 = round.(x, RoundDown, digits=1)</code></pre> <h3>The Results 🔍</h3> So what are the means? <pre><code> mean(x), mean(y1), mean(y2), mean(y3) <br>(0.5006018120380458, 0.5012000000000001, 0.5496999999999999, 0.44970000000000004) </code></pre> We see that the mean of the vector after rounding to even is <b>much</b> closer to the mean of the original value, while rounding up or down results in the mean being <b>10% off</b>. Rounding to even is a way to deal with rounding ties in a deterministic manner (i.e. without randomness) that proved to be the most simple and reliable, even though it might be weird at first. <h3>But That’s Not The End Of It! 😢</h3> There’s another issue why R works this way besides the round to even rule. There is another devil at play here and that’s the<b> finite floating-point precision.</b> Hold on, that’s a lot of words. Okay, let’s take it one at a time. R only stores values till about 53 binary or about 22 floating points. In other words, anything after that digit is lost and is not accounted for. While this is not a problem for a number as small as 0.5, it proves to be a big hassle when the numbers are more precise, which simply means there are more digits after the decimal point. This is not a problem specific to R, overall, but the limits above are specific to it. There is also an infamous <a href="https://cran.r-project.org/doc/FAQ/R-FAQ.html#Why-doesn_0027t-R-think-these-numbers-are-equal_003f">R FAQ question</a> dedicated to it. The following quote is the key point in that answer. <i>All other numbers are internally rounded to (typically) 53 binary digits accuracy. As a result, two floating point numbers will not reliably be equal unless they have been computed by the same algorithm, and not always even then.</i> So, overall, unless two numbers are processed in the exact same way, it is impossible to say with good confidence how R will equate them. But, you may be wondering, how does that apply to rounding? When you have a number that exceeds the decimal places of 22, you would see a representation of it that is untrue since the precision is truncated. For example: <pre><code> &gt; num &lt;- 2.499999999999999999999 &gt; num [1] 2.5 &gt; round(num) [1] 2 </code></pre> Here, when we output num, the precision is lost since the digits exceed 22. However, if we reduce the number of 9s, the precision is retained. It is also connected to the infamous problem (when working in binary): <pre><code> &gt; 0.1 + 0.2 == 0.3 [1] FALSE </code></pre> (or when working in decimal): <pre><code> x = ⅓  = 0.33333, 3*x = 0.99999, 3x =0.99999 ≠1, </code></pre> <h3>floor() and ceiling() 🪜</h3> While they are great alternatives, floor() and ceiling() are often not preferred since they round to a whole number at all times. Often, the use-case is to keep some decimal places intact. When we round, we are often looking to reduce precision while keeping a representation of the digits we are letting go of intact. These functions do not preserve that.  <h3>Why Not Truncate? ✂️</h3> Of course, truncating is an option but if we truncate 1.25 and 1.21 to one decimal place, both would be 1.2, and that would not be a correct representation either. Also, if you look at it, truncate is just round-down for positive numbers and round-up for negative numbers. We’ve seen it’s biased. <h2>Okay, I’ll Just Use Python for My Rounding Needs 🐍</h2> This is all a bit too much, isn’t it? But then, life is rarely as simple when you boil it down to the brass tacks. Python is not devoid of its issues as well. Nor is any language, it’s the IEEE 754 standard 🙂. However as written in the standard, the procedure is not hardware/implementation agnostic.  It might be funny, but python build-in rounding procedure works differently than the one in numpy: <pre><code> In [1]: import numpy as np <br>In [2]: np.round(0.15, 1) Out[2]: 0.2 <br>In [3]: round(0.15, 1) Out[3]: 0.1 </code></pre> Is this a problem? Usually it’s not, but occasionally it might be. Of course you can find implementation details <a href="https://numpy.org/doc/stable/reference/generated/numpy.around.html" target="_blank" rel="nofollow noopener">in the documentation</a>. <h2>Well, Let’s Go To JavaScript Then for All Things Math☕</h2> JavaScript has the Math.round() method to achieve rounding of decimals. It also has the Math.ceil() and Math.floor() methods. Math.round() method rounds to the nearest integer.  If the fractional part of the number is greater than or equal to .5, the argument is rounded to the next higher integer. If the fractional part of the number is less than .5, the argument is rounded to the next lower integer. <img class="aligncenter size-full wp-image-18973" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01b53d29d4bcb968441c2_rounding-issues-in-data-wrangling-example.webp" alt="rounding issues in data wrangling javascript example" width="248" height="306" /> To round off to a specific number of digits, the common solution is to divide the number by 10^x and then multiply the result by 10^x where x is the number of digits to round off to. <img class="aligncenter size-full wp-image-18971" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01b55e64b1cc59a129365_math_round-error-in-programming-example.webp" alt="multiplication error in javascript" width="656" height="306" /> JavaScript seems to be more consistent with true rounding according to arithmetic principles. <h2>What Should I Do Then? I Need Logical Rounding in R! 😭</h2> First of all, <b>never use float-point numbers to represent money-like numbers</b> in computer memory. Either use the dedicated Decimal type if your language supports one (like in <a href="https://docs.python.org/3/library/decimal.html#module-decimal" target="_blank" rel="nofollow noopener">python</a> or <a href="https://docs.oracle.com/cd/E57014_01/wls/WLAPI/weblogic/wtc/jatmi/Decimal.html" target="_blank" rel="nofollow noopener">java)</a> or convert money-float to integer by multiplying by some factor of 10 and avoid floats in general in those cases. With quantities that you usually use floats for, this shouldn’t be the issue. If it is, don’t use floats 🙂. If, in case, you are looking for a function to emulate the true, logical rounding in R, you can go with this alternative we found on <a href="https://stackoverflow.com/questions/12688717/round-up-from-5" target="_blank" rel="nofollow noopener">StackOverflow</a>. <pre><code> true_round &lt;- function(number, digits) {  posneg &lt;- sign(number)  number &lt;- abs(number) * 10^digits  number &lt;- number + 0.5 + sqrt(.Machine$double.eps)  number &lt;- trunc(number)  number &lt;- number / 10 ^ digits  number * posneg } </code></pre> Another solution that could be adopted from javascript is to multiply the number and divide the result by 10^x where x is the number of digits to round off to. This is not perfect and does not give desired results always, but for some cases this might work. <img class="aligncenter size-full wp-image-18977" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01b5587a667cfae8cabe4_rounding-issues-in-r-programming-example.webp" alt="rounding issues in r programming example" width="540" height="260" /> Note how in the first 2 examples the results are different, but in the last 2 examples the results are consistent. When the requirement is to check for equality between decimals up to x decimal digits. Then we can also simply use the difference and add a threshold to it. And do this without any rounding offs. So: <b>If max(abs(y - x)) &gt; threshold</b> then x and y are not equal. For example: <img class="aligncenter size-full wp-image-18975" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01b57f072657ad682c21d_rounding-issues-in-python-programming-example.webp" alt="rounding issues in python programming example" width="540" height="152" /> <h2>Rounding Out R, Julia and Python - Is It Over Yet? 🙏</h2> Yes, and to conclude, in this article, we dove into the shenanigans of rounding in R and other languages. Overall, things are messier than they appear on the surface. The decisions made to make a language work a certain way are bound to produce bad outcomes for certain use-cases. But the good thing about software, if it doesn’t work for you, there is always a way, or at least, some wiggle room to workaround.  Most languages have something wonky going on within them when it comes to rounding numbers and it is important to keep all this in mind so that when you face a perplexing number the next time during your analysis, you immediately know the usual suspect. 🔍 To round it all up (pun intended), stay sharp. It’s not the end of the world yet – it’s just an imprecise number, which may or may cause it someday. <blockquote>Shiny app running slow? Don't fret, maybe you were given a <a href="https://appsilon.com/performant-r-shiny-apps-with-database-indexing-normalization/" target="_blank" rel="noopener">tough start with a slow database</a>.</blockquote>

Have questions or insights?

Engage with experts, share ideas and take your data journey to the next level!
Explore Possibilities

Share Your Data Goals with Us

From advanced analytics to platform development and pharma consulting, we craft solutions tailored to your needs.

Talk to our Experts
r
python
statistics