This is the third and final post in a series covering rounding in .NET. In Part 1, we learned that the Math.Round method’s default rounding algorithm probably isn’t what we would expect, which led to a few more questions about rounding in general. In Part 2, we covered all kinds of different rounding methods and how they can be implemented in C#. Here in Part 3, we’ll wrap things up by looking at some specific rounding scenarios and determining which algorithms are most appropriate in those situations.
As before, to remove a bit of complexity, we’ll assume that “rounding” means “rounding to an integer”. Once you can round to an integer, it’s easy to round to arbitrary precision.
Rounding Individual Values
The rounding operation simply exchanges a number with an approximation of its value. The difference between that approximation and the original value is called rounding error. Rounding error can be difficult to nail down because it depends on both the fraction part of the number and the rounding method used. Fortunately it’s fairly straightforward to establish a range and an average for the rounding error when rounding a single value. Then you can often use those results to establish ranges and averages for rounding multiple values.
The table below contains the rounding error ranges and averages for the rounding methods we’ve been working with. You’ll notice that I’ve included specific cases for positive and negative numbers for the Round Toward Zero and Round Away from Zero methods. If you know something about the numbers you’re rounding, then you can often be more specific about rounding error. For example, suppose you know that a value is the result of dividing an integer by 2 and therefore has a fraction part of 0.0 or 0.5. Then Round Ceiling will always leave the value unchanged or will increase it by exactly 0.5.
|Rounding Method||Rounding Error Range||Mean Rounding Error|
|Round to Nearest (all tie-breaking rules)||[-0.5, 0.5]||0.0|
|Round Ceiling||[0.0, 1.0)||0.5|
|Round Floor||(-1.0, 0.0]||-0.5|
|Round Toward Zero||In General: (-1.0, 1.0)
Positive Numbers: (-1.0, 0.0]
Negative Numbers: [0.0, 1.0)
|In General: 0.0
Positive Numbers: -0.5
Negative Numbers: 0.5
|Round Away from Zero||In General: (-1.0, 1.0)
Positive Numbers: [0.0, 1.0)
Negative Numbers: (-1.0, 0.0]
|In General: 0.0
Positive Numbers: 0.5
Negative Numbers: -0.5
|Round Dither||(-1.0, 1.0)||0.0|
We can reach some useful general conclusions just by examining the rounding error ranges for the various rounding methods. For example, if you’re looking to minimize the overall rounding error for an arbitrary number, then Round to Nearest is the best choice and you probably want to steer clear of Round Dither. And Round Ceiling will increase the value of an arbitrary number by 0.5 on average and will never decrease the value, so count on it shifting your values toward positive infinity.
Cumulative Rounding Error
One of the most famous examples of rounding error is an index that was created in 1982 for the Vancouver Stock Exchange (VSE). The index was assigned a value of 1,000.000 at its inception, but after 22 months the value of the index had fallen to 524.881 with no general downturn in economic activity. Something was wrong. The index value was updated after each stock trade. Rather than recompute the entire index value at each update, the developers who implemented the calculation decided to adjust the value based on the difference in price of the traded stock. After adjusting the index value, they would truncate the result to three decimal places. This truncation caused the index value to lose up to 0.001 points (average 0.0005 points) each time a stock was traded, on average 2,800 times per day. And the rounding error accumulated with every trade. After discovering the error and recomputing the index value (probably using some variation of Round to Nearest), the “true” value was found to be 1098.892.
What happened with the VSE index is an example of cumulative rounding error. When rounding is performed after each step of a long running computation, the result has a cumulative error that is the sum of the rounding error at each step.
Let’s run a simulation of the VSE index with various rounding algorithms. We’ll start with a value of 1,000.000. At each step, we’ll adjust the index value by a delta taken from a random Gaussian distribution with mean 0.000 and standard deviation 0.08333, which should reasonably simulate small stock price fluctuations. Then we’ll run this simulation for 1,232,000 steps (2,800 trades per day x 20 days per month x 22 months). The results are plotted below:
The black line represents several overlapping data sets: no rounding, all of the Round to Nearest methods, and Round Dither. There was some variation among the results for these methods, but they performed similarly. The blue line represents Round Ceiling and Round Away from Zero. The green line represents Round Floor and Round Toward Zero. Clearly Round Ceiling, Round Floor, Round Away from Zero, and Round Toward Zero would be poor choices for this scenario. Round Floor and Round Toward Zero are both equivalent to truncation for the values involved in this simulation, and the results are consistent with what happened in real life.
If we were to extend the simulation beyond 22 months, the Round Ceiling and Round Away from Zero values will continue to grow and the Round Floor values will continue toward negative infinity. Once the cumulative result becomes negative, however, Round Toward Zero will tend to drive the value to zero rather than making it more negative.
Because they have an overall mean rounding error of 0.0, Round to Nearest and Round Dither would be reasonable choices for this scenario. Round to Nearest with any tie-breaking rule would probably be the best choice because its rounding error range is smaller. Again, if your data set has properties you can count on, like every pre-rounding value has a fraction part of 0.0 or 0.5, then you’ll need to pay more attention to this choice.
If we round every value in a set, then we may end up altering characteristics of the set as a whole. We see this in statistical sample sets, where rounding the samples can change the values of statistical measures of the set.
Say we’re looking at the arithmetic mean of a sample set. If we adjust each value by some δ, then the arithmetic mean (μ) will also change by exactly δ:
Suppose we round each value in an arbitrary sample set using Round to Nearest. Each value would change by some δ in the range (-0.5, 0.5), which makes the error range for the arithmetic mean (-0.5, 0.5) as well.
Let’s see an example. We’ll start with a random Gaussian distribution of 1 million values in the interval [0,100], each with a precision of 1/10. We’ll apply each of rounding methods to that set. Take a look at this histogram showing some of the results:
The rounding methods that are not included are duplicates. Rounding caused values to shift, and that caused variation in the number of samples in each interval. How did it affect our statistical measures?
|Mean||Mean % Change||Median||Std Dev||Std Dev % Change|
|Round to Nearest, Round Half Away from Zero||50.055||0.101%||50||16.445||0.017%|
|Round to Nearest, Round Half Toward Zero||49.954||0.100%||50||16.445||0.016%|
|Round to Nearest, Round Half Toward Positive Infinity||50.055||0.101%||50||16.445||0.017%|
|Round to Nearest, Round Half Toward Negative Infinity||49.954||0.100%||50||16.445||0.016%|
|Round to Nearest, Round Half to Even||50.004||0.001%||50||16.446||0.018%|
|Round to Nearest, Round Half to Odd||50.004||0.000%||50||16.445||0.017%|
|Round to Nearest, Round Half Randomly||50.005||0.001%||50||16.446||0.019%|
|Round to Nearest, Round Half Alternatingly||50.005||0.001%||50||16.446||0.018%|
|Round Away from Zero||50.454||0.900%||50||16.445||0.014%|
|Round Toward Zero||49.554||0.900%||50||16.445||0.014%|
Round Ceiling, Round Floor, Round Away from Zero, and Round Toward Zero are probably unacceptable choices for this scenario. They each changed the mean value of our sample set by nearly 1%, which could have significant impact on any statistical results gleaned from the rounded set.
The Round to Nearest methods performed reasonably well, but there’s an inherent bias in half of the tie-breaking rules that shows up in the rounded results. Approximately 10% of the values in our random sample set had a fraction part of 0.5 and thus were subject to the tie-breaking rule. Round Half Away from Zero, Round Half Toward Zero, Round Half Toward Positive Infinity, and Round Half Toward Negative Infinity all had a significantly larger effect on the sample set than the other tie-breaking rules.
Round Dither had essentially no effect on the arithmetic mean of the set, but it changed the standard deviation more than any other method.
For the sample set rounding scenario, four rounding methods appear to be superior: Round to Nearest with Round Half to Even, Round to Nearest with Round Half to Odd, Round to Nearest with Round Half Randomly, and Round to Nearest with Round Half Alternatingly. That makes sense because Round to Nearest has the smallest general rounding error range and these tie-breaking rules are designed to eliminate bias.
Quantization is the process of approximating a continuous range of values (or a very large set of possible discrete values) by a relatively small (usually finite) set of discrete values. This essentially amounts to rounding each sample value to one of the discrete range values. Perhaps the most common use for quantization is in performing analog-to-digital conversion for signals. Audio signals, for instance. The problem is that this process can often change psychometric characteristics of the original signal. Listeners are sensitive to distortion in the signal, so we have to be careful that quantization doesn’t introduce distortion that they will find unacceptable. We need to apply a method of rounding that preserves the essential characteristics of the signal. Let’s look at an example of a smooth analog signal quantized using Round to Nearest:
The approximation looks reasonable, but the stair-step result would sound significantly different than the smoothly increasing curve if this were an audio signal. All of the other rounding methods produce the same kind of stair-step result. Take a look at the results with Round Dither, though:
This doesn’t look all that different from the Round to Nearest stair steps, and it seems like this would be a poor choice with all of those jumps between values. But to the human ear, this is going to sound a lot closer to the original analog signal. For analog signal quantization, Round Dither is the clear choice.
We’ve covered a great deal in this series, but there are other rounding methods and lots of other scenarios to consider. Ultimately you just need to be very careful with rounding. When you round a number, you change its value, and that can have significant effects in your system that can sometimes be difficult to predict.
The best advice is to avoid rounding, or at least round as little as possible. Perform rounding as late as you can in a calculation, and preferably round only the final result. Keep an eye on the context of your problem; look at what values you are rounding and think about the results of rounding those values. Figure out how rounding error will affect your system. And if you care about accuracy, be sure to check your results carefully and impose correctness control (like unit tests) around all critical rounded calculations.
If you’re interested in the code for the example scenarios (which uses the C# rounding implementations from Part 2), you can download the project here. The random Gaussian distributions were easy to generate thanks to Math.NET Iridium, part of the excellent Math.NET Project.
- Part 1: The Mystery of Math.Round
The Math.Round method isn’t as straightforward as one might expect.
- Part 2: Exotic Rounding Algorithms
A menagerie of lesser-known rounding methods, with implementations in C#.
- Part 3: Rounding in Action
Why some rounding algorithms are better than others in certain situations.