Apa itu round di python?

It’s the era of big data, and every day more and more business are trying to leverage their data to make informed decisions. Many businesses are turning to Python’s powerful data science ecosystem to analyze their data, as evidenced by Python’s rising popularity in the data science realm.

One thing every data science practitioner must keep in mind is how a dataset may be biased. Drawing conclusions from biased data can lead to costly mistakes.

There are many ways bias can creep into a dataset. If you’ve studied some statistics, you’re probably familiar with terms like reporting bias, selection bias and sampling bias. There is another type of bias that plays an important role when you are dealing with numeric data: rounding bias.

In this article, you will learn:

  • Why the way you round numbers is important
  • How to round a number according to various rounding strategies, and how to implement each method in pure Python
  • How rounding affects data, and which rounding strategy minimizes this effect
  • How to round numbers in NumPy arrays and Pandas DataFrames
  • When to apply different rounding strategies

Take the Quiz: Test your knowledge with our interactive “Rounding Numbers in Python” quiz. Upon completion you will receive a score so you can track your learning progress over time:

Take the Quiz »

This article is not a treatise on numeric precision in computing, although we will touch briefly on the subject. Only a familiarity with the fundamentals of Python is necessary, and the math involved here should feel comfortable to anyone familiar with the equivalent of high school algebra.

Let’s start by looking at Python’s built-in rounding mechanism.

Python’s Built-in def truncate(n, decimals=0): multiplier = 10 ** decimals return int(n * multiplier) / multiplier 9 Function

Python has a built-in

def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier
9 function that takes two numeric arguments,
>>> truncate(12.5)
12.0

>>> truncate(-5.963, 1)
-5.9

>>> truncate(1.625, 2)
1.62
1 and
>>> truncate(12.5)
12.0

>>> truncate(-5.963, 1)
-5.9

>>> truncate(1.625, 2)
1.62
2, and returns the number
>>> truncate(12.5)
12.0

>>> truncate(-5.963, 1)
-5.9

>>> truncate(1.625, 2)
1.62
1 rounded to
>>> truncate(12.5)
12.0

>>> truncate(-5.963, 1)
-5.9

>>> truncate(1.625, 2)
1.62
2. The
>>> truncate(12.5)
12.0

>>> truncate(-5.963, 1)
-5.9

>>> truncate(1.625, 2)
1.62
2 argument defaults to zero, so leaving it out results in a number rounded to an integer. As you’ll see,
def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier
9 may not work quite as you expect.

The way most people are taught to round a number goes something like this:

Round the number

>>> truncate(12.5)
12.0

>>> truncate(-5.963, 1)
-5.9

>>> truncate(1.625, 2)
1.62
1 to
>>> truncate(12.5)
12.0

>>> truncate(-5.963, 1)
-5.9

>>> truncate(1.625, 2)
1.62
8 decimal places by first shifting the decimal point in
>>> truncate(12.5)
12.0

>>> truncate(-5.963, 1)
-5.9

>>> truncate(1.625, 2)
1.62
1 by
>>> truncate(12.5)
12.0

>>> truncate(-5.963, 1)
-5.9

>>> truncate(1.625, 2)
1.62
8 places by multiplying
>>> truncate(12.5)
12.0

>>> truncate(-5.963, 1)
-5.9

>>> truncate(1.625, 2)
1.62
1 by 10ᵖ (10 raised to the
>>> truncate(12.5)
12.0

>>> truncate(-5.963, 1)
-5.9

>>> truncate(1.625, 2)
1.62
8th power) to get a new number
>>> truncate(125.6, -1)
120.0

>>> truncate(-1374.25, -3)
-1000.0
3.

Then look at the digit

>>> truncate(125.6, -1)
120.0

>>> truncate(-1374.25, -3)
-1000.0
4 in the first decimal place of
>>> truncate(125.6, -1)
120.0

>>> truncate(-1374.25, -3)
-1000.0
3. If
>>> truncate(125.6, -1)
120.0

>>> truncate(-1374.25, -3)
-1000.0
4 is less than 5, round
>>> truncate(125.6, -1)
120.0

>>> truncate(-1374.25, -3)
-1000.0
3 down to the nearest integer. Otherwise, round
>>> truncate(125.6, -1)
120.0

>>> truncate(-1374.25, -3)
-1000.0
3 up.

Finally, shift the decimal point back

>>> truncate(12.5)
12.0

>>> truncate(-5.963, 1)
-5.9

>>> truncate(1.625, 2)
1.62
8 places by dividing
>>> truncate(125.6, -1)
120.0

>>> truncate(-1374.25, -3)
-1000.0
3 by 10ᵖ.

It’s a straightforward algorithm! For example, the number

>>> import math

>>> math.ceil(1.2)
2

>>> math.ceil(2)
2

>>> math.ceil(-0.5)
0
1 rounded to the nearest whole number is
>>> import math

>>> math.ceil(1.2)
2

>>> math.ceil(2)
2

>>> math.ceil(-0.5)
0
2. The number
>>> import math

>>> math.ceil(1.2)
2

>>> math.ceil(2)
2

>>> math.ceil(-0.5)
0
3 rounded to one decimal place is
>>> import math

>>> math.ceil(1.2)
2

>>> math.ceil(2)
2

>>> math.ceil(-0.5)
0
4.

Now open up an interpreter session and round

>>> import math

>>> math.ceil(1.2)
2

>>> math.ceil(2)
2

>>> math.ceil(-0.5)
0
1 to the nearest whole number using Python’s built-in function:

>>>

>>> round(2.5)
2

Gasp!

How does

def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier
9 handle the number
>>> import math

>>> math.ceil(1.2)
2

>>> math.ceil(2)
2

>>> math.ceil(-0.5)
0
8?

>>>

>>> round(1.5)
2

So,

def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier
9 rounds
>>> import math

>>> math.ceil(1.2)
2

>>> math.ceil(2)
2

>>> math.ceil(-0.5)
0
8 up to
>>> round(1.5)
2
01, and
>>> import math

>>> math.ceil(1.2)
2

>>> math.ceil(2)
2

>>> math.ceil(-0.5)
0
1 down to
>>> round(1.5)
2
01!

Before you go raising an issue on the Python bug tracker, let me assure you that

>>> round(1.5)
2
04 is supposed to return
>>> round(1.5)
2
01. There is a good reason why
def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier
9 behaves the way it does.

In this article, you’ll learn that there are more ways to round a number than you might expect, each with unique advantages and disadvantages.

def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier
9 behaves according to a particular rounding strategy—which may or may not be the one you need for a given situation.

You might be wondering, “Can the way I round numbers really have that much of an impact?” Let’s take a look at just how extreme the effects of rounding can be.

Remove ads

How Much Impact Can Rounding Have?

Suppose you have an incredibly lucky day and find $100 on the ground. Rather than spending all your money at once, you decide to play it smart and invest your money by buying some shares of different stocks.

The value of a stock depends on supply and demand. The more people there are who want to buy a stock, the more value that stock has, and vice versa. In high volume stock markets, the value of a particular stock can fluctuate on a second-by-second basis.

Let’s run a little experiment. We’ll pretend the overall value of the stocks you purchased fluctuates by some small random number each second, say between $0.05 and -$0.05. This fluctuation may not necessarily be a nice value with only two decimal places. For example, the overall value may increase by $0.031286 one second and decrease the next second by $0.028476.

You don’t want to keep track of your value to the fifth or sixth decimal place, so you decide to chop everything off after the third decimal place. In rounding jargon, this is called truncating the number to the third decimal place. There’s some error to be expected here, but by keeping three decimal places, this error couldn’t be substantial. Right?

To run our experiment using Python, let’s start by writing a

>>> round(1.5)
2
08 function that truncates a number to three decimal places:

>>>

>>> def truncate(n):
...     return int(n * 1000) / 1000

The

>>> round(1.5)
2
08 function works by first shifting the decimal point in the number
>>> truncate(12.5)
12.0

>>> truncate(-5.963, 1)
-5.9

>>> truncate(1.625, 2)
1.62
1 three places to the right by multiplying
>>> truncate(12.5)
12.0

>>> truncate(-5.963, 1)
-5.9

>>> truncate(1.625, 2)
1.62
1 by
>>> round(1.5)
2
12. The integer part of this new number is taken with
>>> round(1.5)
2
13. Finally, the decimal point is shifted three places back to the left by dividing
>>> truncate(12.5)
12.0

>>> truncate(-5.963, 1)
-5.9

>>> truncate(1.625, 2)
1.62
1 by
>>> round(1.5)
2
12.

Next, let’s define the initial parameters of the simulation. You’ll need two variables: one to keep track of the actual value of your stocks after the simulation is complete and one for the value of your stocks after you’ve been truncating to three decimal places at each step.

Start by initializing these variables to

>>> round(1.5)
2
16:

>>>

>>> actual_value, truncated_value = 100, 100

Now let’s run the simulation for 1,000,000 seconds (approximately 11.5 days). For each second, generate a random value between

>>> round(1.5)
2
17 and
>>> round(1.5)
2
18 with the
>>> round(1.5)
2
19 function in the
>>> round(1.5)
2
20 module, and then update
>>> round(1.5)
2
21 and
>>> round(1.5)
2
22:

>>>

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239

The meat of the simulation takes place in the

>>> round(1.5)
2
23 loop, which loops over the
>>> round(1.5)
2
24 of numbers between
>>> round(1.5)
2
25 and
>>> round(1.5)
2
26. The value taken from
>>> round(1.5)
2
27 at each step is stored in the variable
>>> round(1.5)
2
28, which we use here because we don’t actually need this value inside of the loop.

At each step of the loop, a new random number between

>>> round(1.5)
2
17 and
>>> round(1.5)
2
18 is generated using
>>> round(1.5)
2
31 and assigned to the variable
>>> round(1.5)
2
32. The new value of your investment is calculated by adding
>>> round(1.5)
2
32 to
>>> round(1.5)
2
34, and the truncated total is calculated by adding
>>> round(1.5)
2
32 to
>>> round(1.5)
2
36 and then truncating this value with
>>> round(1.5)
2
08.

As you can see by inspecting the

>>> round(1.5)
2
34 variable after running the loop, you only lost about $3.55. However, if you’d been looking at
>>> round(1.5)
2
36, you’d have thought that you’d lost almost all of your money!

Note: In the above example, the

>>> round(1.5)
2
40 function is used to seed the pseudo-random number generator so that you can reproduce the output shown here.

To learn more about randomness in Python, check out Real Python’s Generating Random Data in Python (Guide).

Ignoring for the moment that

def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier
9 doesn’t behave quite as you expect, let’s try re-running the simulation. We’ll use
def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier
9 this time to round to three decimal places at each step, and
>>> round(1.5)
2
43 the simulation again to get the same results as before:

>>>

>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258

What a difference!

Shocking as it may seem, this exact error caused quite a stir in the early 1980s when the system designed for recording the value of the Vancouver Stock Exchange truncated the overall index value to three decimal places instead of rounding. Rounding errors have swayed elections and even resulted in the loss of life.

How you round numbers is important, and as a responsible developer and software designer, you need to know what the common issues are and how to deal with them. Let’s dive in and investigate what the different rounding methods are and how you can implement each one in pure Python.

Remove ads

A Menagerie of Methods

There are a plethora of rounding strategies, each with advantages and disadvantages. In this section, you’ll learn about some of the most common techniques, and how they can influence your data.

Truncation

The simplest, albeit crudest, method for rounding a number is to truncate the number to a given number of digits. When you truncate a number, you replace each digit after a given position with 0. Here are some examples:

ValueTruncated ToResult12.345Tens place1012.345Ones place1212.345Tenths place12.312.345Hundredths place12.34

You’ve already seen one way to implement this in the

>>> round(1.5)
2
08 function from the section. In that function, the input number was truncated to three decimal places by:

  • Multiplying the number by
    >>> round(1.5)
    2
    
    12 to shift the decimal point three places to the right
  • Taking the integer part of that new number with
    >>> round(1.5)
    2
    
    13
  • Shifting the decimal place three places back to the left by dividing by
    >>> round(1.5)
    2
    
    12

You can generalize this process by replacing

>>> round(1.5)
2
12 with the number 10ᵖ (
>>> round(1.5)
2
49 raised to the pth power), where p is the number of decimal places to truncate to:

def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier

In this version of

>>> round(1.5)
2
08, the second argument defaults to
>>> round(1.5)
2
25 so that if no second argument is passed to the function, then
>>> round(1.5)
2
08 returns the integer part of whatever number is passed to it.

The

>>> round(1.5)
2
08 function works well for both positive and negative numbers:

>>>

>>> truncate(12.5)
12.0

>>> truncate(-5.963, 1)
-5.9

>>> truncate(1.625, 2)
1.62

You can even pass a negative number to

>>> round(1.5)
2
54 to truncate to digits to the left of the decimal point:

>>>

>>> truncate(125.6, -1)
120.0

>>> truncate(-1374.25, -3)
-1000.0

When you truncate a positive number, you are rounding it down. Likewise, truncating a negative number rounds that number up. In a sense, truncation is a combination of rounding methods depending on the sign of the number you are rounding.

Let’s take a look at each of these rounding methods individually, starting with rounding up.

Rounding Up

The second rounding strategy we’ll look at is called “rounding up.” This strategy always rounds a number up to a specified number of digits. The following table summarizes this strategy:

ValueRound Up ToResult12.345Tens place2012.345Ones place1312.345Tenths place12.412.345Hundredths place12.35

To implement the “rounding up” strategy in Python, we’ll use the function from the

>>> round(1.5)
2
56 module.

The

>>> round(1.5)
2
55 function gets its name from the term “ceiling,” which is used in mathematics to describe the nearest integer that is greater than or equal to a given number.

Every number that is not an integer lies between two consecutive integers. For example, the number

>>> round(1.5)
2
58 lies in the interval between
>>> round(1.5)
2
59 and
>>> round(1.5)
2
01. The “ceiling” is the greater of the two endpoints of the interval. The lesser of the two endpoints in called the “floor.” Thus, the ceiling of
>>> round(1.5)
2
58 is
>>> round(1.5)
2
01, and the floor of
>>> round(1.5)
2
58 is
>>> round(1.5)
2
59.

In mathematics, a special function called the ceiling function maps every number to its ceiling. To allow the ceiling function to accept integers, the ceiling of an integer is defined to be the integer itself. So the ceiling of the number

>>> round(1.5)
2
01 is
>>> round(1.5)
2
01.

In Python,

>>> round(1.5)
2
67 implements the ceiling function and always returns the nearest integer that is greater than or equal to its input:

>>>

>>> import math

>>> math.ceil(1.2)
2

>>> math.ceil(2)
2

>>> math.ceil(-0.5)
0

Notice that the ceiling of

>>> round(1.5)
2
68 is
>>> round(1.5)
2
25, not
>>> round(1.5)
2
70. This makes sense because
>>> round(1.5)
2
25 is the nearest integer to
>>> round(1.5)
2
68 that is greater than or equal to
>>> round(1.5)
2
68.

Let’s write a function called

>>> round(1.5)
2
74 that implements the “rounding up” strategy:

>>> round(1.5)
2
0

You may notice that

>>> round(1.5)
2
74 looks a lot like
>>> round(1.5)
2
08. First, the decimal point in
>>> truncate(12.5)
12.0

>>> truncate(-5.963, 1)
-5.9

>>> truncate(1.625, 2)
1.62
1 is shifted the correct number of places to the right by multiplying
>>> truncate(12.5)
12.0

>>> truncate(-5.963, 1)
-5.9

>>> truncate(1.625, 2)
1.62
1 by
>>> round(1.5)
2
79. This new value is rounded up to the nearest integer using
>>> round(1.5)
2
67, and then the decimal point is shifted back to the left by dividing by
>>> round(1.5)
2
79.

This pattern of shifting the decimal point, applying some rounding method to round to an integer, and then shifting the decimal point back will come up over and over again as we investigate more rounding methods. This is, after all, the mental algorithm we humans use to round numbers by hand.

Let’s look at how well

>>> round(1.5)
2
74 works for different inputs:

>>>

>>> round(1.5)
2
1

Just like

>>> round(1.5)
2
08, you can pass a negative value to
>>> round(1.5)
2
54:

>>>

>>> round(1.5)
2
2

When you pass a negative number to

>>> round(1.5)
2
54, the number in the first argument of
>>> round(1.5)
2
74 is rounded to the correct number of digits to the left of the decimal point.

Take a guess at what

>>> round(1.5)
2
87 returns:

>>>

>>> round(1.5)
2
3

Is

>>> round(1.5)
2
88 what you expected?

If you examine the logic used in defining

>>> round(1.5)
2
74—in particular, the way the
>>> round(1.5)
2
67 function works—then it makes sense that
>>> round(1.5)
2
87 returns
>>> round(1.5)
2
88. However, some people naturally expect symmetry around zero when rounding numbers, so that if
>>> import math

>>> math.ceil(1.2)
2

>>> math.ceil(2)
2

>>> math.ceil(-0.5)
0
8 gets rounded up to
>>> round(1.5)
2
01, then
>>> round(1.5)
2
95 should get rounded up to
>>> round(1.5)
2
96.

Let’s establish some terminology. For our purposes, we’ll use the terms “round up” and “round down” according to the following diagram:

Apa itu round di python?
Round up to the right and down to the left. (Image: David Amos)

Rounding up always rounds a number to the right on the number line, and rounding down always rounds a number to the left on the number line.

Remove ads

Rounding Down

The counterpart to “rounding up” is the “rounding down” strategy, which always rounds a number down to a specified number of digits. Here are some examples illustrating this strategy:

ValueRounded Down ToResult12.345Tens place1012.345Ones place1212.345Tenths place12.312.345Hundredths place12.34

To implement the “rounding down” strategy in Python, we can follow the same algorithm we used for both

>>> round(1.5)
2
97 and
>>> round(1.5)
2
74. First shift the decimal point, then round to an integer, and finally shift the decimal point back.

In

>>> round(1.5)
2
74, we used
>>> round(1.5)
2
67 to round up to the ceiling of the number after shifting the decimal point. For the “rounding down” strategy, though, we need to round to the floor of the number after shifting the decimal point.

Lucky for us, the

>>> round(1.5)
2
56 module has a function that returns the floor of its input:

>>>

>>> round(1.5)
2
4

Here’s the definition of

>>> def truncate(n):
...     return int(n * 1000) / 1000
03:

>>> round(1.5)
2
5

That looks just like

>>> round(1.5)
2
74, except
>>> round(1.5)
2
67 has been replaced with
>>> def truncate(n):
...     return int(n * 1000) / 1000
06.

You can test

>>> def truncate(n):
...     return int(n * 1000) / 1000
03 on a few different values:

>>>

>>> round(1.5)
2
6

The effects of

>>> round(1.5)
2
74 and
>>> def truncate(n):
...     return int(n * 1000) / 1000
03 can be pretty extreme. By rounding the numbers in a large dataset up or down, you could potentially remove a ton of precision and drastically alter computations made from the data.

Before we discuss any more rounding strategies, let’s stop and take a moment to talk about how rounding can make your data biased.

Interlude: Rounding Bias

You’ve now seen three rounding methods:

>>> round(1.5)
2
08,
>>> round(1.5)
2
74, and
>>> def truncate(n):
...     return int(n * 1000) / 1000
03. All three of these techniques are rather crude when it comes to preserving a reasonable amount of precision for a given number.

There is one important difference between

>>> round(1.5)
2
08 and
>>> round(1.5)
2
74 and
>>> def truncate(n):
...     return int(n * 1000) / 1000
03 that highlights an important aspect of rounding: symmetry around zero.

Recall that

>>> round(1.5)
2
74 isn’t symmetric around zero. In mathematical terms, a function f(x) is symmetric around zero if, for any value of x, f(x) + f(-x) = 0. For example,
>>> def truncate(n):
...     return int(n * 1000) / 1000
17 returns
>>> round(1.5)
2
01, but
>>> round(1.5)
2
87 returns
>>> round(1.5)
2
70. The
>>> def truncate(n):
...     return int(n * 1000) / 1000
03 function isn’t symmetric around 0, either.

On the other hand, the

>>> round(1.5)
2
08 function is symmetric around zero. This is because, after shifting the decimal point to the right,
>>> round(1.5)
2
08 chops off the remaining digits. When the initial value is positive, this amounts to rounding the number down. Negative numbers are rounded up. So,
>>> def truncate(n):
...     return int(n * 1000) / 1000
24 returns
>>> round(1.5)
2
59, and
>>> def truncate(n):
...     return int(n * 1000) / 1000
26 returns
>>> round(1.5)
2
70.

The concept of symmetry introduces the notion of rounding bias, which describes how rounding affects numeric data in a dataset.

The “rounding up” strategy has a round towards positive infinity bias, because the value is always rounded up in the direction of positive infinity. Likewise, the “rounding down” strategy has a round towards negative infinity bias.

The “truncation” strategy exhibits a round towards negative infinity bias on positive values and a round towards positive infinity for negative values. Rounding functions with this behavior are said to have a round towards zero bias, in general.

Let’s see how this works in practice. Consider the following list of floats:

>>>

>>> round(1.5)
2
7

Let’s compute the mean value of the values in

>>> def truncate(n):
...     return int(n * 1000) / 1000
28 using the function:

>>>

>>> round(1.5)
2
8

Now apply each of

>>> round(1.5)
2
74,
>>> def truncate(n):
...     return int(n * 1000) / 1000
03, and
>>> round(1.5)
2
08 in a list comprehension to round each number in
>>> def truncate(n):
...     return int(n * 1000) / 1000
28 to one decimal place and calculate the new mean:

>>>

>>> round(1.5)
2
9

After every number in

>>> def truncate(n):
...     return int(n * 1000) / 1000
28 is rounded up, the new mean is about
>>> def truncate(n):
...     return int(n * 1000) / 1000
35, which is greater than the actual mean of about
>>> def truncate(n):
...     return int(n * 1000) / 1000
36. Rounding down shifts the mean downwards to about
>>> def truncate(n):
...     return int(n * 1000) / 1000
37. The mean of the truncated values is about
>>> def truncate(n):
...     return int(n * 1000) / 1000
38 and is the closest to the actual mean.

This example does not imply that you should always truncate when you need to round individual values while preserving a mean value as closely as possible. The

>>> def truncate(n):
...     return int(n * 1000) / 1000
28 list contains an equal number of positive and negative values. The
>>> round(1.5)
2
08 function would behave just like
>>> round(1.5)
2
74 on a list of all positive values, and just like
>>> def truncate(n):
...     return int(n * 1000) / 1000
03 on a list of all negative values.

What this example does illustrate is the effect rounding bias has on values computed from data that has been rounded. You will need to keep these effects in mind when drawing conclusions from data that has been rounded.

Typically, when rounding, you are interested in rounding to the nearest number with some specified precision, instead of just rounding everything up or down.

For example, if someone asks you to round the numbers

>>> def truncate(n):
...     return int(n * 1000) / 1000
43 and
>>> def truncate(n):
...     return int(n * 1000) / 1000
44 to one decimal place, you would probably respond quickly with
>>> round(1.5)
2
58 and
>>> def truncate(n):
...     return int(n * 1000) / 1000
46. The
>>> round(1.5)
2
08,
>>> round(1.5)
2
74, and
>>> def truncate(n):
...     return int(n * 1000) / 1000
03 functions don’t do anything like this.

What about the number

>>> def truncate(n):
...     return int(n * 1000) / 1000
50? You probably immediately think to round this to
>>> def truncate(n):
...     return int(n * 1000) / 1000
46, but in reality,
>>> def truncate(n):
...     return int(n * 1000) / 1000
50 is equidistant from
>>> round(1.5)
2
58 and
>>> def truncate(n):
...     return int(n * 1000) / 1000
46. In a sense,
>>> round(1.5)
2
58 and
>>> def truncate(n):
...     return int(n * 1000) / 1000
46 are both the nearest numbers to
>>> def truncate(n):
...     return int(n * 1000) / 1000
50 with single decimal place precision. The number
>>> def truncate(n):
...     return int(n * 1000) / 1000
50 is called a tie with respect to
>>> round(1.5)
2
58 and
>>> def truncate(n):
...     return int(n * 1000) / 1000
46. In cases like this, you must assign a tiebreaker.

The way that most people are taught break ties is by rounding to the greater of the two possible numbers.

Remove ads

Rounding Half Up

The “rounding half up” strategy rounds every number to the nearest number with the specified precision, and breaks ties by rounding up. Here are some examples:

ValueRound Half Up ToResult13.825Tens place1013.825Ones place1413.825Tenths place13.813.825Hundredths place13.83

To implement the “rounding half up” strategy in Python, you start as usual by shifting the decimal point to the right by the desired number of places. At this point, though, you need a way to determine if the digit just after the shifted decimal point is less than or greater than or equal to

>>> def truncate(n):
...     return int(n * 1000) / 1000
61.

One way to do this is to add

>>> def truncate(n):
...     return int(n * 1000) / 1000
62 to the shifted value and then round down with
>>> def truncate(n):
...     return int(n * 1000) / 1000
06. This works because:

  • If the digit in the first decimal place of the shifted value is less than five, then adding

    >>> def truncate(n):
    ...     return int(n * 1000) / 1000
    
    62 won’t change the integer part of the shifted value, so the floor is equal to the integer part.

  • If the first digit after the decimal place is greater than or equal to

    >>> def truncate(n):
    ...     return int(n * 1000) / 1000
    
    61, then adding
    >>> def truncate(n):
    ...     return int(n * 1000) / 1000
    
    62 will increase the integer part of the shifted value by
    >>> round(1.5)
    2
    
    59, so the floor is equal to this larger integer.

Here’s what this looks like in Python:

>>> def truncate(n):
...     return int(n * 1000) / 1000
0

Notice that

>>> def truncate(n):
...     return int(n * 1000) / 1000
68 looks a lot like
>>> def truncate(n):
...     return int(n * 1000) / 1000
03. This might be somewhat counter-intuitive, but internally
>>> def truncate(n):
...     return int(n * 1000) / 1000
68 only rounds down. The trick is to add the
>>> def truncate(n):
...     return int(n * 1000) / 1000
62 after shifting the decimal point so that the result of rounding down matches the expected value.

Let’s test

>>> def truncate(n):
...     return int(n * 1000) / 1000
68 on a couple of values to see that it works:

>>>

>>> def truncate(n):
...     return int(n * 1000) / 1000
1

Since

>>> def truncate(n):
...     return int(n * 1000) / 1000
68 always breaks ties by rounding to the greater of the two possible values, negative values like
>>> round(1.5)
2
95 round to
>>> round(1.5)
2
70, not to
>>> round(1.5)
2
96:

>>>

>>> def truncate(n):
...     return int(n * 1000) / 1000
2

Great! You can now finally get that result that the built-in

def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier
9 function denied to you:

>>>

>>> def truncate(n):
...     return int(n * 1000) / 1000
3

Before you get too excited though, let’s see what happens when you try and round

>>> def truncate(n):
...     return int(n * 1000) / 1000
78 to
>>> round(1.5)
2
01 decimal places:

>>>

>>> def truncate(n):
...     return int(n * 1000) / 1000
4

Wait. We just discussed how ties get rounded to the greater of the two possible values.

>>> def truncate(n):
...     return int(n * 1000) / 1000
78 is smack in the middle of
>>> def truncate(n):
...     return int(n * 1000) / 1000
81 and
>>> def truncate(n):
...     return int(n * 1000) / 1000
82. Since
>>> def truncate(n):
...     return int(n * 1000) / 1000
81 is the greater of these two,
>>> def truncate(n):
...     return int(n * 1000) / 1000
84 should return
>>> def truncate(n):
...     return int(n * 1000) / 1000
81. But instead, we got
>>> def truncate(n):
...     return int(n * 1000) / 1000
82.

Is there a bug in the

>>> def truncate(n):
...     return int(n * 1000) / 1000
68 function?

When

>>> def truncate(n):
...     return int(n * 1000) / 1000
68 rounds
>>> def truncate(n):
...     return int(n * 1000) / 1000
78 to two decimal places, the first thing it does is multiply
>>> def truncate(n):
...     return int(n * 1000) / 1000
78 by
>>> round(1.5)
2
16. Let’s make sure this works as expected:

>>>

>>> def truncate(n):
...     return int(n * 1000) / 1000
5

Well… that’s wrong! But it does explain why

>>> def truncate(n):
...     return int(n * 1000) / 1000
84 returns -1.23. Let’s continue the
>>> def truncate(n):
...     return int(n * 1000) / 1000
68 algorithm step-by-step, utilizing
>>> round(1.5)
2
28 in the REPL to recall the last value output at each step:

>>>

>>> def truncate(n):
...     return int(n * 1000) / 1000
6

Even though

>>> def truncate(n):
...     return int(n * 1000) / 1000
95 is really close to
>>> def truncate(n):
...     return int(n * 1000) / 1000
96, the nearest integer that is less than or equal to it is
>>> def truncate(n):
...     return int(n * 1000) / 1000
97. When the decimal point is shifted back to the left, the final value is
>>> def truncate(n):
...     return int(n * 1000) / 1000
82.

Well, now you know how

>>> def truncate(n):
...     return int(n * 1000) / 1000
84 returns
>>> def truncate(n):
...     return int(n * 1000) / 1000
82 even though there is no logical error, but why does Python say that
>>> actual_value, truncated_value = 100, 100
01 is
>>> actual_value, truncated_value = 100, 100
02? Is there a bug in Python?

Aside: In a Python interpreter session, type the following:

>>>

>>> def truncate(n):
...     return int(n * 1000) / 1000
7

Seeing this for the first time can be pretty shocking, but this is a classic example of floating-point representation error. It has nothing to do with Python. The error has to do with how machines store floating-point numbers in memory.

Most modern computers store floating-point numbers as binary decimals with 53-bit precision. Only numbers that have finite binary decimal representations that can be expressed in 53 bits are stored as an exact value. Not every number has a finite binary decimal representation.

For example, the decimal number

>>> actual_value, truncated_value = 100, 100
03 has a finite decimal representation, but infinite binary representation. Just like the fraction 1/3 can only be represented in decimal as the infinitely repeating decimal
>>> actual_value, truncated_value = 100, 100
04, the fraction
>>> actual_value, truncated_value = 100, 100
05 can only be expressed in binary as the infinitely repeating decimal
>>> actual_value, truncated_value = 100, 100
06.

A value with an infinite binary representation is rounded to an approximate value to be stored in memory. The method that most machines use to round is determined according to the IEEE-754 standard, which specifies rounding to the nearest representable binary fraction.

The Python docs have a section called Floating Point Arithmetic: Issues and Limitations which has this to say about the number 0.1:

On most machines, if Python were to print the true decimal value of the binary approximation stored for

>>> actual_value, truncated_value = 100, 100
03, it would have to display

>>>

>>> def truncate(n):
...     return int(n * 1000) / 1000
8

That is more digits than most people find useful, so Python keeps the number of digits manageable by displaying a rounded value instead

>>>

>>> def truncate(n):
...     return int(n * 1000) / 1000
9

Just remember, even though the printed result looks like the exact value of

>>> actual_value, truncated_value = 100, 100
05, the actual stored value is the nearest representable binary fraction. (Source)

For a more in-depth treatise on floating-point arithmetic, check out David Goldberg’s article What Every Computer Scientist Should Know About Floating-Point Arithmetic, originally published in the journal ACM Computing Surveys, Vol. 23, No. 1, March 1991.

The fact that Python says that

>>> actual_value, truncated_value = 100, 100
01 is
>>> actual_value, truncated_value = 100, 100
02 is an artifact of floating-point representation error. You might be asking yourself, “Okay, but is there a way to fix this?” A better question to ask yourself is “Do I need to fix this?”

Floating-point numbers do not have exact precision, and therefore should not be used in situations where precision is paramount. For applications where the exact precision is necessary, you can use the

>>> actual_value, truncated_value = 100, 100
11 class from Python’s
>>> actual_value, truncated_value = 100, 100
12 module. You’ll learn more about the
>>> actual_value, truncated_value = 100, 100
11 class below.

If you have determined that Python’s standard

>>> actual_value, truncated_value = 100, 100
14 class is sufficient for your application, some occasional errors in
>>> def truncate(n):
...     return int(n * 1000) / 1000
68 due to floating-point representation error shouldn’t be a concern.

Now that you’ve gotten a taste of how machines round numbers in memory, let’s continue our discussion on rounding strategies by looking at another way to break a tie.

Remove ads

Rounding Half Down

The “rounding half down” strategy rounds to the nearest number with the desired precision, just like the “rounding half up” method, except that it breaks ties by rounding to the lesser of the two numbers. Here are some examples:

ValueRound Half Down ToResult13.825Tens place1013.825Ones place1413.825Tenths place13.813.825Hundredths place13.82

You can implement the “rounding half down” strategy in Python by replacing

>>> def truncate(n):
...     return int(n * 1000) / 1000
06 in the
>>> def truncate(n):
...     return int(n * 1000) / 1000
68 function with
>>> round(1.5)
2
67 and subtracting
>>> def truncate(n):
...     return int(n * 1000) / 1000
62 instead of adding:

>>> actual_value, truncated_value = 100, 100
0

Let’s check

>>> actual_value, truncated_value = 100, 100
20 against a few test cases:

>>>

>>> actual_value, truncated_value = 100, 100
1

Both

>>> def truncate(n):
...     return int(n * 1000) / 1000
68 and
>>> actual_value, truncated_value = 100, 100
20 have no bias in general. However, rounding data with lots of ties does introduce a bias. For an extreme example, consider the following list of numbers:

>>>

>>> actual_value, truncated_value = 100, 100
2

Let’s compute the mean of these numbers:

>>>

>>> actual_value, truncated_value = 100, 100
3

Next, compute the mean on the data after rounding to one decimal place with

>>> def truncate(n):
...     return int(n * 1000) / 1000
68 and
>>> actual_value, truncated_value = 100, 100
20:

>>>

>>> actual_value, truncated_value = 100, 100
4

Every number in

>>> def truncate(n):
...     return int(n * 1000) / 1000
28 is a tie with respect to rounding to one decimal place. The
>>> def truncate(n):
...     return int(n * 1000) / 1000
68 function introduces a round towards positive infinity bias, and
>>> actual_value, truncated_value = 100, 100
20 introduces a round towards negative infinity bias.

The remaining rounding strategies we’ll discuss all attempt to mitigate these biases in different ways.

Rounding Half Away From Zero

If you examine

>>> def truncate(n):
...     return int(n * 1000) / 1000
68 and
>>> actual_value, truncated_value = 100, 100
20 closely, you’ll notice that neither of these functions is symmetric around zero:

>>>

>>> actual_value, truncated_value = 100, 100
5

One way to introduce symmetry is to always round a tie away from zero. The following table illustrates how this works:

ValueRound Half Away From Zero ToResult15.25Tens place2015.25Ones place1515.25Tenths place15.3-15.25Tens place-20-15.25Ones place-15-15.25Tenths place-15.3

To implement the “rounding half away from zero” strategy on a number

>>> truncate(12.5)
12.0

>>> truncate(-5.963, 1)
-5.9

>>> truncate(1.625, 2)
1.62
1, you start as usual by shifting the decimal point to the right a given number of places. Then you look at the digit
>>> truncate(125.6, -1)
120.0

>>> truncate(-1374.25, -3)
-1000.0
4 immediately to the right of the decimal place in this new number. At this point, there are four cases to consider:

  1. If
    >>> truncate(12.5)
    12.0
    
    >>> truncate(-5.963, 1)
    -5.9
    
    >>> truncate(1.625, 2)
    1.62
    
    1 is positive and
    >>> actual_value, truncated_value = 100, 100
    
    33, round up
  2. If
    >>> truncate(12.5)
    12.0
    
    >>> truncate(-5.963, 1)
    -5.9
    
    >>> truncate(1.625, 2)
    1.62
    
    1 is positive and
    >>> actual_value, truncated_value = 100, 100
    
    35, round down
  3. If
    >>> truncate(12.5)
    12.0
    
    >>> truncate(-5.963, 1)
    -5.9
    
    >>> truncate(1.625, 2)
    1.62
    
    1 is negative and
    >>> actual_value, truncated_value = 100, 100
    
    33, round down
  4. If
    >>> truncate(12.5)
    12.0
    
    >>> truncate(-5.963, 1)
    -5.9
    
    >>> truncate(1.625, 2)
    1.62
    
    1 is negative and
    >>> actual_value, truncated_value = 100, 100
    
    35, round up

After rounding according to one of the above four rules, you then shift the decimal place back to the left.

Given a number

>>> truncate(12.5)
12.0

>>> truncate(-5.963, 1)
-5.9

>>> truncate(1.625, 2)
1.62
1 and a value for
>>> round(1.5)
2
54, you could implement this in Python by using
>>> def truncate(n):
...     return int(n * 1000) / 1000
68 and
>>> actual_value, truncated_value = 100, 100
20:

>>> actual_value, truncated_value = 100, 100
6

That’s easy enough, but there’s actually a simpler way!

If you first take the absolute value of

>>> truncate(12.5)
12.0

>>> truncate(-5.963, 1)
-5.9

>>> truncate(1.625, 2)
1.62
1 using Python’s built-in function, you can just use
>>> def truncate(n):
...     return int(n * 1000) / 1000
68 to round the number. Then all you need to do is give the rounded number the same sign as
>>> truncate(12.5)
12.0

>>> truncate(-5.963, 1)
-5.9

>>> truncate(1.625, 2)
1.62
1. One way to do this is using the function.

>>> actual_value, truncated_value = 100, 100
48 takes two numbers
>>> actual_value, truncated_value = 100, 100
50 and
>>> actual_value, truncated_value = 100, 100
51 and returns
>>> actual_value, truncated_value = 100, 100
50 with the sign of
>>> actual_value, truncated_value = 100, 100
51:

>>>

>>> actual_value, truncated_value = 100, 100
7

Notice that

>>> actual_value, truncated_value = 100, 100
48 returns a
>>> actual_value, truncated_value = 100, 100
14, even though both of its arguments were integers.

Using

>>> actual_value, truncated_value = 100, 100
45,
>>> def truncate(n):
...     return int(n * 1000) / 1000
68 and
>>> actual_value, truncated_value = 100, 100
48, you can implement the “rounding half away from zero” strategy in just two lines of Python:

>>> actual_value, truncated_value = 100, 100
8

In

>>> actual_value, truncated_value = 100, 100
59, the absolute value of
>>> truncate(12.5)
12.0

>>> truncate(-5.963, 1)
-5.9

>>> truncate(1.625, 2)
1.62
1 is rounded to
>>> round(1.5)
2
54 decimal places using
>>> def truncate(n):
...     return int(n * 1000) / 1000
68 and this result is assigned to the variable
>>> actual_value, truncated_value = 100, 100
63. Then the original sign of
>>> truncate(12.5)
12.0

>>> truncate(-5.963, 1)
-5.9

>>> truncate(1.625, 2)
1.62
1 is applied to
>>> actual_value, truncated_value = 100, 100
63 using
>>> actual_value, truncated_value = 100, 100
48, and this final value with the correct sign is returned by the function.

Checking

>>> actual_value, truncated_value = 100, 100
59 on a few different values shows that the function behaves as expected:

>>>

>>> actual_value, truncated_value = 100, 100
9

The

>>> actual_value, truncated_value = 100, 100
59 function rounds numbers the way most people tend to round numbers in everyday life. Besides being the most familiar rounding function you’ve seen so far,
>>> actual_value, truncated_value = 100, 100
59 also eliminates rounding bias well in datasets that have an equal number of positive and negative ties.

Let’s check how well

>>> actual_value, truncated_value = 100, 100
59 mitigates rounding bias in the example from the previous section:

>>>

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
0

The mean value of the numbers in

>>> def truncate(n):
...     return int(n * 1000) / 1000
28 is preserved almost exactly when you round each number in
>>> def truncate(n):
...     return int(n * 1000) / 1000
28 to one decimal place with
>>> actual_value, truncated_value = 100, 100
59!

However,

>>> actual_value, truncated_value = 100, 100
59 will exhibit a rounding bias when you round every number in datasets with only positive ties, only negative ties, or more ties of one sign than the other. Bias is only mitigated well if there are a similar number of positive and negative ties in the dataset.

How do you handle situations where the number of positive and negative ties are drastically different? The answer to this question brings us full circle to the function that deceived us at the beginning of this article: Python’s built-in function.

Remove ads

Rounding Half To Even

One way to mitigate rounding bias when rounding values in a dataset is to round ties to the nearest even number at the desired precision. Here are some examples of how to do that:

ValueRound Half To Even ToResult15.255Tens place2015.255Ones place1515.255Tenths place15.315.255Hundredths place15.26

The “rounding half to even strategy” is the strategy used by Python’s built-in

def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier
9 function and is the . This strategy works under the assumption that the probabilities of a tie in a dataset being rounded down or rounded up are equal. In practice, this is usually the case.

Now you know why

>>> round(1.5)
2
04 returns
>>> round(1.5)
2
01. It’s not a mistake. It is a conscious design decision based on solid recommendations.

To prove to yourself that

def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier
9 really does round to even, try it on a few different values:

>>>

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
1

The

def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier
9 function is nearly free from bias, but it isn’t perfect. For example, rounding bias can still be introduced if the majority of the ties in your dataset round up to even instead of rounding down. Strategies that mitigate bias even better than “rounding half to even” , but they are somewhat obscure and only necessary in extreme circumstances.

Finally,

def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier
9 suffers from the same hiccups that you saw in
>>> def truncate(n):
...     return int(n * 1000) / 1000
68 thanks to floating-point representation error:

>>>

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
2

You shouldn’t be concerned with these occasional errors if floating-point precision is sufficient for your application.

When precision is paramount, you should use Python’s

>>> actual_value, truncated_value = 100, 100
11 class.

The >>> actual_value, truncated_value = 100, 100 11 Class

Python’s decimal module is one of those “batteries-included” features of the language that you might not be aware of if you’re new to Python. The guiding principle of the

>>> actual_value, truncated_value = 100, 100
12 module can be found in the documentation:

Decimal “is based on a floating-point model which was designed with people in mind, and necessarily has a paramount guiding principle – computers must provide an arithmetic that works in the same way as the arithmetic that people learn at school.” – excerpt from the decimal arithmetic specification. (Source)

The benefits of the

>>> actual_value, truncated_value = 100, 100
12 module include:

  • Exact decimal representation:
    >>> actual_value, truncated_value = 100, 100
    
    03 is actually
    >>> actual_value, truncated_value = 100, 100
    
    03, and
    >>> actual_value, truncated_value = 100, 100
    
    89 returns
    >>> round(1.5)
    2
    
    25, as you’d expect.
  • Preservation of significant digits: When you add
    >>> actual_value, truncated_value = 100, 100
    
    91 and
    >>> actual_value, truncated_value = 100, 100
    
    92, the result is
    >>> actual_value, truncated_value = 100, 100
    
    93 with the trailing zero maintained to indicate significance.
  • User-alterable precision: The default precision of the
    >>> actual_value, truncated_value = 100, 100
    
    12 module is twenty-eight digits, but this value can be altered by the user to match the problem at hand.

Let’s explore how rounding works in the

>>> actual_value, truncated_value = 100, 100
12 module. Start by typing the following into a Python REPL:

>>>

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
3

>>> actual_value, truncated_value = 100, 100
96 returns a
>>> actual_value, truncated_value = 100, 100
97 object representing the default context of the
>>> actual_value, truncated_value = 100, 100
12 module. The context includes the default precision and the default rounding strategy, among other things.

As you can see in the example above, the default rounding strategy for the

>>> actual_value, truncated_value = 100, 100
12 module is
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
00. This aligns with the built-in
def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier
9 function and should be the preferred rounding strategy for most purposes.

Let’s declare a number using the

>>> actual_value, truncated_value = 100, 100
12 module’s
>>> actual_value, truncated_value = 100, 100
11 class. To do so, create a new
>>> actual_value, truncated_value = 100, 100
11 instance by passing a
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
05 containing the desired value:

>>>

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
4

Note: It is possible to create a

>>> actual_value, truncated_value = 100, 100
11 instance from a floating-point number, but doing so introduces floating-point representation error right off the bat. For example, check out what happens when you create a
>>> actual_value, truncated_value = 100, 100
11 instance from the floating-point number
>>> actual_value, truncated_value = 100, 100
03:

>>>

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
5

In order to maintain exact precision, you must create

>>> actual_value, truncated_value = 100, 100
11 instances from strings containing the decimal numbers you need.

Just for fun, let’s test the assertion that

>>> actual_value, truncated_value = 100, 100
11 maintains exact decimal representation:

>>>

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
6

Ahhh. That’s satisfying, isn’t it?

Rounding a

>>> actual_value, truncated_value = 100, 100
11 is done with the
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
12 method:

>>>

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
7

Okay, that probably looks a little funky, so let’s break that down. The

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
13 argument in
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
12 determines the number of decimal places to round the number. Since
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
15 has one decimal place, the number
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
16 rounds to a single decimal place. The default rounding strategy is “rounding half to even,” so the result is
>>> import math

>>> math.ceil(1.2)
2

>>> math.ceil(2)
2

>>> math.ceil(-0.5)
0
4.

Recall that the

def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier
9 function, which also uses the “rounding half to even strategy,” failed to round
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
19 to two decimal places correctly. Instead of
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
20,
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
21 returns
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
22. Thanks to the
>>> actual_value, truncated_value = 100, 100
12 modules exact decimal representation, you won’t have this issue with the
>>> actual_value, truncated_value = 100, 100
11 class:

>>>

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
8

Another benefit of the

>>> actual_value, truncated_value = 100, 100
12 module is that rounding after performing arithmetic is taken care of automatically, and significant digits are preserved. To see this in action, let’s change the default precision from twenty-eight digits to two, and then add the numbers
>>> def truncate(n):
...     return int(n * 1000) / 1000
43 and
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
27:

>>>

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
9

To change the precision, you call

>>> actual_value, truncated_value = 100, 100
96 and set the
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
29 attribute. If setting the attribute on a function call looks odd to you, you can do this because
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
30 returns a special
>>> actual_value, truncated_value = 100, 100
97 object that represents the current internal context containing the default parameters used by the
>>> actual_value, truncated_value = 100, 100
12 module.

The exact value of

>>> def truncate(n):
...     return int(n * 1000) / 1000
43 plus
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
27 is
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
35. Since the precision is now two digits, and the rounding strategy is set to the default of “rounding half to even,” the value
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
35 is automatically rounded to
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
37.

To change the default rounding strategy, you can set the

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
38 property to any one of several . The following table summarizes these flags and which rounding strategy they implement:

FlagRounding Strategy

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
39Rounding up
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
40Rounding down
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
41Truncation
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
42Rounding away from zero
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
43Rounding half away from zero
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
44Rounding half towards zero
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
45Rounding half to even
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
46Rounding up and rounding towards zero

The first thing to notice is that the naming scheme used by the

>>> actual_value, truncated_value = 100, 100
12 module differs from what we agreed to earlier in the article. For example,
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
42 implements the “rounding away from zero” strategy, which actually rounds negative numbers down.

Secondly, some of the rounding strategies mentioned in the table may look unfamiliar since we haven’t discussed them. You’ve already seen how

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
45 works, so let’s take a look at each of the others in action.

The

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
39 strategy works just like the
>>> round(1.5)
2
74 function we defined earlier:

>>>

>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
0

Notice that the results of

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
39 are not symmetric around zero.

The

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
40 strategy works just like our
>>> def truncate(n):
...     return int(n * 1000) / 1000
03 function:

>>>

>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
1

Like

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
39, the
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
40 strategy is not symmetric around zero.

The

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
41 and
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
42 strategies have somewhat deceptive names. Both
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
59 and
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
60 are symmetric around zero:

>>>

>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
2

The

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
41 strategy rounds numbers towards zero, just like the
>>> round(1.5)
2
08 function. On the other hand,
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
42 rounds everything away from zero. This is a clear break from the terminology we agreed to earlier in the article, so keep that in mind when you are working with the
>>> actual_value, truncated_value = 100, 100
12 module.

There are three strategies in the

>>> actual_value, truncated_value = 100, 100
12 module that allow for more nuanced rounding. The
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
43 method rounds everything to the nearest number and breaks ties by rounding away from zero:

>>>

>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
3

Notice that

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
43 works just like our
>>> actual_value, truncated_value = 100, 100
59 and not like
>>> def truncate(n):
...     return int(n * 1000) / 1000
68.

There is also a

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
44 strategy that breaks ties by rounding towards zero:

>>>

>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
4

The final rounding strategy available in the

>>> actual_value, truncated_value = 100, 100
12 module is very different from anything we have seen so far:

>>>

>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
5

In the above examples, it looks as if

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
46 rounds everything towards zero. In fact, this is exactly how
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
46 works, unless the result of rounding ends in a
>>> round(1.5)
2
25 or
>>> def truncate(n):
...     return int(n * 1000) / 1000
61. In that case, the number gets rounded away from zero:

>>>

>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
6

In the first example, the number

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
76 is first rounded towards zero in the second decimal place, producing
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
77. Since
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
77 does not end in a
>>> round(1.5)
2
25 or a
>>> def truncate(n):
...     return int(n * 1000) / 1000
61, it is left as is. On the other hand,
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
81 is rounded towards zero in the second decimal place, resulting in the number
>>> import math

>>> math.ceil(1.2)
2

>>> math.ceil(2)
2

>>> math.ceil(-0.5)
0
8. This ends in a
>>> def truncate(n):
...     return int(n * 1000) / 1000
61, so the first decimal place is then rounded away from zero to
>>> import math

>>> math.ceil(1.2)
2

>>> math.ceil(2)
2

>>> math.ceil(-0.5)
0
4.

In this section, we have only focused on the rounding aspects of the

>>> actual_value, truncated_value = 100, 100
12 module. There are a large number of other features that make
>>> actual_value, truncated_value = 100, 100
12 an excellent choice for applications where the standard floating-point precision is inadequate, such as banking and some problems in scientific computing.

For more information on

>>> actual_value, truncated_value = 100, 100
11, check out the in the Python docs.

Next, let’s turn our attention to two staples of Python’s scientific computing and data science stacks: NumPy and Pandas.

Remove ads

Rounding NumPy Arrays

In the domains of data science and scientific computing, you often store your data as a NumPy

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
88. One of NumPy’s most powerful features is its use of vectorization and broadcasting to apply operations to an entire array at once instead of one element at a time.

Let’s generate some data by creating a 3×4 NumPy array of pseudo-random numbers:

>>>

>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
7

First, we seed the

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
89 module so that you can easily reproduce the output. Then a 3×4 NumPy array of floating-point numbers is created with
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
90.

Note: You’ll need to

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
91 before typing the above code into your REPL if you don’t already have NumPy in your environment. If you installed Python with Anaconda, you’re already set!

If you haven’t used NumPy before, you can get a quick introduction in the section of Brad Solomon’s Look Ma, No For-Loops: Array Programming With NumPy here at Real Python.

For more information on NumPy’s random module, check out the section of Brad’s Generating Random Data in Python (Guide).

To round all of the values in the

>>> def truncate(n):
...     return int(n * 1000) / 1000
28 array, you can pass
>>> def truncate(n):
...     return int(n * 1000) / 1000
28 as the argument to the function. The desired number of decimal places is set with the
>>> round(1.5)
2
54 keyword argument. The round half to even strategy is used, just like Python’s built-in
def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier
9 function.

For example, the following rounds all of the values in

>>> def truncate(n):
...     return int(n * 1000) / 1000
28 to three decimal places:

>>>

>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
8

>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
94 is at the mercy of floating-point representation error, just like
def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier
9 is.

For example, the value in the third row of the first column in the

>>> def truncate(n):
...     return int(n * 1000) / 1000
28 array is
>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
01. When you round this to three decimal places using the “rounding half to even” strategy, you expect the value to be
>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
02. But you can see in the output from
>>> import random
>>> random.seed(100)

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     truncated_value = truncate(truncated_value + randn)
...

>>> actual_value
96.45273913513529

>>> truncated_value
0.239
94 that the value is rounded to
>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
04. However, the value
>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
05 in the first row of the second column rounds correctly to
>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
06.

If you need to round the data in your array to integers, NumPy offers several options:

  • >>> random.seed(100)
    >>> actual_value, rounded_value = 100, 100
    
    >>> for _ in range(1000000):
    ...     randn = random.uniform(-0.05, 0.05)
    ...     actual_value = actual_value + randn
    ...     rounded_value = round(rounded_value + randn, 3)
    ...
    
    >>> actual_value
    96.45273913513529
    
    >>> rounded_value
    96.258
    
    07
  • >>> random.seed(100)
    >>> actual_value, rounded_value = 100, 100
    
    >>> for _ in range(1000000):
    ...     randn = random.uniform(-0.05, 0.05)
    ...     actual_value = actual_value + randn
    ...     rounded_value = round(rounded_value + randn, 3)
    ...
    
    >>> actual_value
    96.45273913513529
    
    >>> rounded_value
    96.258
    
    08
  • >>> random.seed(100)
    >>> actual_value, rounded_value = 100, 100
    
    >>> for _ in range(1000000):
    ...     randn = random.uniform(-0.05, 0.05)
    ...     actual_value = actual_value + randn
    ...     rounded_value = round(rounded_value + randn, 3)
    ...
    
    >>> actual_value
    96.45273913513529
    
    >>> rounded_value
    96.258
    
    09
  • >>> random.seed(100)
    >>> actual_value, rounded_value = 100, 100
    
    >>> for _ in range(1000000):
    ...     randn = random.uniform(-0.05, 0.05)
    ...     actual_value = actual_value + randn
    ...     rounded_value = round(rounded_value + randn, 3)
    ...
    
    >>> actual_value
    96.45273913513529
    
    >>> rounded_value
    96.258
    
    10

The

>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
11 function rounds every value in the array to the nearest integer greater than or equal to the original value:

>>>

>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
9

Hey, we discovered a new number! Negative zero!

Actually, the IEEE-754 standard requires the implementation of both a positive and negative zero. What possible use is there for something like this? Wikipedia knows the answer:

Informally, one may use the notation “

>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
12” for a negative value that was rounded to zero. This notation may be useful when a negative sign is significant; for example, when tabulating Celsius temperatures, where a negative sign means below freezing. ()

To round every value down to the nearest integer, use

>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
13:

>>>

def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier
0

You can also truncate each value to its integer component with

>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
14:

>>>

def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier
1

Finally, to round to the nearest integer using the “rounding half to even” strategy, use

>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
15:

>>>

def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier
2

You might have noticed that a lot of the rounding strategies we discussed earlier are missing here. For the vast majority of situations, the

>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
16 function is all you need. If you need to implement another strategy, such as
>>> def truncate(n):
...     return int(n * 1000) / 1000
68, you can do so with a simple modification:

def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier
3

Thanks to NumPy’s vectorized operations, this works just as you expect:

>>>

def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier
4

Now that you’re a NumPy rounding master, let’s take a look at Python’s other data science heavy-weight: the Pandas library.

Remove ads

Rounding Pandas >>> random.seed(100) >>> actual_value, rounded_value = 100, 100 >>> for _ in range(1000000): ... randn = random.uniform(-0.05, 0.05) ... actual_value = actual_value + randn ... rounded_value = round(rounded_value + randn, 3) ... >>> actual_value 96.45273913513529 >>> rounded_value 96.258 18 and >>> random.seed(100) >>> actual_value, rounded_value = 100, 100 >>> for _ in range(1000000): ... randn = random.uniform(-0.05, 0.05) ... actual_value = actual_value + randn ... rounded_value = round(rounded_value + randn, 3) ... >>> actual_value 96.45273913513529 >>> rounded_value 96.258 19

The Pandas library has become a staple for data scientists and data analysts who work in Python. In the words of Real Python’s own Joe Wyndham:

Pandas is a game-changer for data science and analytics, particularly if you came to Python because you were searching for something more powerful than Excel and VBA. (Source)

Note: Before you continue, you’ll need to

>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
20 if you don’t already have it in your environment. As was the case for NumPy, if you installed Python with Anaconda, you should be ready to go!

The two main Pandas data structures are the

>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
19, which in very loose terms works sort of like an Excel spreadsheet, and the
>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
18, which you can think of as a column in a spreadsheet. Both
>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
18 and
>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
19 objects can also be rounded efficiently using the
>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
25 and
>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
26 methods:

>>>

def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier
5

The

>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
26 method can also accept a dictionary or a
>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
18, to specify a different precision for each column. For instance, the following examples show how to round the first column of
>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
29 to one decimal place, the second to two, and the third to three decimal places:

>>>

def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier
6

If you need more rounding flexibility, you can apply NumPy’s

>>> def truncate(n):
...     return int(n * 1000) / 1000
02,
>>> round(1.5)
2
55, and
>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
32 functions to Pandas
>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
18 and
>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
19 objects:

>>>

def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier
7

The modified

>>> def truncate(n):
...     return int(n * 1000) / 1000
68 function from the previous section will also work here:

>>>

def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier
8

Congratulations, you’re well on your way to rounding mastery! You now know that there are more ways to round a number than there are taco combinations. (Well… maybe not!) You can implement numerous rounding strategies in pure Python, and you have sharpened your skills on rounding NumPy arrays and Pandas

>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
18 and
>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
19 objects.

There’s just one more step: knowing when to apply the right strategy.

Applications and Best Practices

The last stretch on your road to rounding virtuosity is understanding when to apply your newfound knowledge. In this section, you’ll learn some best practices to make sure you round your numbers the right way.

Store More and Round Late

When you deal with large sets of data, storage can be an issue. In most relational databases, each column in a table is designed to store a specific data type, and numeric data types are often assigned precision to help conserve memory.

For example, a temperature sensor may report the temperature in a long-running industrial oven every ten seconds accurate to eight decimal places. The readings from this are used to detect abnormal fluctuations in temperature that could indicate the failure of a heating element or some other component. So, there might be a Python script running that compares each incoming reading to the last to check for large fluctuations.

The readings from this sensor are also stored in a SQL database so that the daily average temperature inside the oven can be computed each day at midnight. The manufacturer of the heating element inside the oven recommends replacing the component whenever the daily average temperature drops

>>> random.seed(100)
>>> actual_value, rounded_value = 100, 100

>>> for _ in range(1000000):
...     randn = random.uniform(-0.05, 0.05)
...     actual_value = actual_value + randn
...     rounded_value = round(rounded_value + randn, 3)
...

>>> actual_value
96.45273913513529

>>> rounded_value
96.258
38 degrees below normal.

For this calculation, you only need three decimal places of precision. But you know from the incident at the Vancouver Stock Exchange that removing too much precision can drastically affect your calculation.

If you have the space available, you should store the data at full precision. If storage is an issue, a good rule of thumb is to store at least two or three more decimal places of precision than you need for your calculation.

Finally, when you compute the daily average temperature, you should calculate it to the full precision available and round the final answer.

Remove ads

Obey Local Currency Regulations

When you order a cup of coffee for $2.40 at the coffee shop, the merchant typically adds a required tax. The amount of that tax depends a lot on where you are geographically, but for the sake of argument, let’s say it’s 6%. The tax to be added comes out to $0.144. Should you round this up to $0.15 or down to $0.14? The answer probably depends on the regulations set forth by the local government!

Situations like this can also arise when you are converting one currency to another. In 1999, the European Commission on Economical and Financial Affairs codified the use of the “rounding half away from zero” strategy when converting currencies to the Euro, but other currencies may have adopted different regulations.

Another scenario, “Swedish rounding”, occurs when the minimum unit of currency at the accounting level in a country is smaller than the lowest unit of physical currency. For example, if a cup of coffee costs $2.54 after tax, but there are no 1-cent coins in circulation, what do you do? The buyer won’t have the exact amount, and the merchant can’t make exact change.

How situations like this are handled is typically determined by a country’s government. You can find a list of rounding methods used by various countries on Wikipedia.

If you are designing software for calculating currencies, you should always check the local laws and regulations in your users’ locations.

When In Doubt, Round Ties To Even

When you are rounding numbers in large datasets that are used in complex computations, the primary concern is limiting the growth of the error due to rounding.

Of all the methods we’ve discussed in this article, the “rounding half to even” strategy minimizes rounding bias the best. Fortunately, Python, NumPy, and Pandas all default to this strategy, so by using the built-in rounding functions you’re already well protected!

Summary

Whew! What a journey this has been!

In this article, you learned that:

  • There are various rounding strategies, which you now know how to implement in pure Python.

  • Every rounding strategy inherently introduces a rounding bias, and the “rounding half to even” strategy mitigates this bias well, most of the time.

  • The way in which computers store floating-point numbers in memory naturally introduces a subtle rounding error, but you learned how to work around this with the

    >>> actual_value, truncated_value = 100, 100
    
    12 module in Python’s standard library.

  • You can round NumPy arrays and Pandas

    >>> random.seed(100)
    >>> actual_value, rounded_value = 100, 100
    
    >>> for _ in range(1000000):
    ...     randn = random.uniform(-0.05, 0.05)
    ...     actual_value = actual_value + randn
    ...     rounded_value = round(rounded_value + randn, 3)
    ...
    
    >>> actual_value
    96.45273913513529
    
    >>> rounded_value
    96.258
    
    18 and
    >>> random.seed(100)
    >>> actual_value, rounded_value = 100, 100
    
    >>> for _ in range(1000000):
    ...     randn = random.uniform(-0.05, 0.05)
    ...     actual_value = actual_value + randn
    ...     rounded_value = round(rounded_value + randn, 3)
    ...
    
    >>> actual_value
    96.45273913513529
    
    >>> rounded_value
    96.258
    
    19 objects.

  • There are best practices for rounding with real-world data.

Take the Quiz: Test your knowledge with our interactive “Rounding Numbers in Python” quiz. Upon completion you will receive a score so you can track your learning progress over time:

Take the Quiz »

If you are interested in learning more and digging into the nitty-gritty details of everything we’ve covered, the links below should keep you busy for quite a while.

At the very least, if you’ve enjoyed this article and learned something new from it, pass it on to a friend or team member! Be sure to share your thoughts with us in the comments. We’d love to hear some of your own rounding-related battle stories!

Happy Pythoning!

Additional Resources

Rounding strategies and bias:

  • Rounding, Wikipedia
  • Rounding Numbers without Adding a Bias, from ZipCPU

Floating-point and decimal specifications:

  • IEEE-754, Wikipedia
  • IBM’s General Decimal Arithmetic Specification

Interesting Reads:

  • What Every Computer Scientist Should Know About Floating-Point Arithmetic, David Goldberg, ACM Computing Surveys, March 1991
  • Floating Point Arithmetic: Issues and Limitations, from python.org
  • Why Python’s Integer Division Floors, by Guido van Rossum

Mark as Completed

🐍 Python Tricks 💌

Get a short & sweet Python Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Apa itu round di python?

Send Me Python Tricks »

About David Amos

Apa itu round di python?
Apa itu round di python?

David is a writer, programmer, and mathematician passionate about exploring mathematics through code.

» More about David


Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Apa itu round di python?

Adriana

Apa itu round di python?

Geir Arne

Apa itu round di python?

Joanna

Master Real-World Python Skills With Unlimited Access to Real Python

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

Master Real-World Python Skills
With Unlimited Access to Real Python

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Rate this article:

Tweet Share Share Email

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students. and get answers to common questions in our support portal.

Apa fungsi def di python?

3. Sintaks Fungsi Penggunaan fungsi dalam python memiliki sintaks umum sebagai pedoman. Secara umum sintaks fungsi terdiri menjadi beberapa bagian berikut: kata kunci def, sebagai tanda bahwa blok kode program tersebut merupakan fungsi. nama fungsi, nama fungsi yang dibuat oleh programmer.

Apa itu eval di python?

Fungsi eval() di Python Fungsi eval() digunakan untuk memparsing atau menguraikan ekspresi string yang diteruskan ke fungsi ini dan menjalankannya sebagai ekspresi python (kode) di dalam program.

Apa itu pow dalam python?

math.pow() adalah fungsi library Python bawaan yang mengembalikan nilai x ke pangkat y (x²). Untuk menghitung pangkat dalam Python, kita bisa menggunakan fungsi math.pow(). Fungsi library ini digunakan untuk menghitung pangkat angka sehingga dapat membuat penghitungan pangkat lebih mudah.

Apa fungsi dari float?

Float merupakan salah satu dari bentuk tipe data pada pemrograman. Tipe data sangat penting dipahami jika kamu masuk ke dunia pemrograman komputasi karena merupakan sebuah atribut yang nantinya digunakan sebagai alat untuk memberi tahu pada sistem komputer tentang cara menafsir nilai dari data tersebut.