Skip to content

pandas round

Cut a column of long decimals down to the precision you actually meant.

Have you ever printed a table of averages and gotten 2.807692307692308 where you wanted 2.81? That is what round is for. Hand it a number of decimal places, and it gives you back a new data frame or series in which every floating-point value has been rounded to that many places. Columns that are not numbers come through untouched, so you can call it on a whole frame without first picking out the numeric columns.

There is one thing to understand before anything else, and it explains most of the trouble people have with this method: round changes your data. It is not a display setting. The rounded frame it returns holds different numbers than the one you started with, and every calculation you do afterward uses those different numbers. Sometimes that is exactly what you want. Often it is not.

Official documentation: DataFrame.round and Series.round

The arguments that earn their keep

There is really only one argument, decimals, and it takes three shapes:

df.round(2)                                # two places, every numeric column
df.round({'goals': 2, 'capacity': -3})     # a different precision per column
df.round(pd.Series({'goals': 2}))          # the same idea, as a series
df.round(-3)                               # to the nearest thousand

The integer is the form everybody knows. The dictionary is the form everybody should know, because real tables mix quantities that deserve different precision, and one number cannot serve them all. Keys that match no column are ignored without complaint, and columns you leave out of the dictionary are not rounded at all — which is a feature, since it lets you touch two columns in a twenty-column frame and leave the rest alone.

Negative decimals run the other way, rounding to tens, hundreds, thousands. This is the argument I see used least and wish were used most: when a number is a rough estimate, saying so with round(-3) is more honest than printing six digits you do not believe.

DataFrame.round also accepts *args and **kwargs. They are there for NumPy compatibility and have no effect, so do not go looking for a hidden option.

A worked example, on real data

Bamboo Weekly #172 used Josh Fjelstul's World Cup repository, which has a file per topic — matches, stadiums, players, goals. Two of those files together can tell us how the tournament has changed: how many goals a match produces, and how big the stadiums are.

import pandas as pd

base = 'https://raw.githubusercontent.com/jfjelstul/worldcup/master/data-csv/'

matches = pd.read_csv(base + 'matches.csv')
stadiums = pd.read_csv(base + 'stadiums.csv')

df = (
    matches
    .merge(stadiums[['stadium_id', 'stadium_capacity']], on='stadium_id')
    .assign(goals=pd.col('home_team_score') + pd.col('away_team_score'))
    .groupby('tournament_name')
    .agg(matches=('match_id', 'count'),
         goals_per_match=('goals', 'mean'),
         mean_capacity=('stadium_capacity', 'mean'))
    .tail(6)
)
                             matches  goals_per_match  mean_capacity
tournament_name
2011 FIFA Women's World Cup       32         2.687500   34500.000000
2014 FIFA Men's World Cup         64         2.671875   67093.750000
2015 FIFA Women's World Cup       52         2.807692   41230.769231
2018 FIFA Men's World Cup         64         2.640625   48015.625000
2019 FIFA Women's World Cup       52         2.807692   31192.307692
2022 FIFA Men's World Cup         64         2.687500   55031.250000

Every one of those digits after the decimal point is noise. The obvious fix is df.round(2), and it does help — but look at what it does to the capacities:

                             matches  goals_per_match  mean_capacity
tournament_name
2011 FIFA Women's World Cup       32             2.69       34500.00
2014 FIFA Men's World Cup         64             2.67       67093.75
2015 FIFA Women's World Cup       52             2.81       41230.77
2018 FIFA Men's World Cup         64             2.64       48015.62
2019 FIFA Women's World Cup       52             2.81       31192.31
2022 FIFA Men's World Cup         64             2.69       55031.25

Two decimal places is right for goals per match and absurd for an average stadium. Nobody cares about three quarters of a seat. These two columns want different treatment, which is precisely what the dictionary form is for:

df.round({'goals_per_match': 2, 'mean_capacity': -3})
                             matches  goals_per_match  mean_capacity
tournament_name
2011 FIFA Women's World Cup       32             2.69        34000.0
2014 FIFA Men's World Cup         64             2.67        67000.0
2015 FIFA Women's World Cup       52             2.81        41000.0
2018 FIFA Men's World Cup         64             2.64        48000.0
2019 FIFA Women's World Cup       52             2.81        31000.0
2022 FIFA Men's World Cup         64             2.69        55000.0

Now the table says what I actually know: about 2.7 goals a match, in stadiums holding roughly fifty thousand people. Notice that matches was never mentioned in the dictionary and so was never touched, and that df itself is unchanged — round returned a new frame, and the original still has all of its digits.

Four mistakes people make

Half does not round up. This is the one that generates the bug reports:

pd.Series([0.5, 1.5, 2.5, 3.5, 4.5]).round()
0    0.0
1    2.0
2    2.0
3    4.0
4    4.0
dtype: float64

Two and a half rounded down to 2, and a half rounded down to 0, while three and a half rounded up to 4. Pandas rounds half to even, also called banker's rounding, and so does Python itself: round(2.5) is 2 while round(3.5) is 4. The reasoning is that always rounding ties upward biases a long column of numbers upward, while sending ties to whichever neighbor is even cancels out over many rows. It is in the real data above, too — the 2018 men's average of 48015.625 became 48015.62, not 48015.63. If this is new to you, I have written it up at Python round and banker's rounding and recorded a short video, Why does round(2.5) == 2 in Python?.

Floating point means you may not get the value you asked for. Rounding is exact arithmetic performed on numbers that are not exact:

pd.Series([2.675, 1.005, 8.835]).round(2)
0    2.68
1    1.00
2    8.84
dtype: float64

1.005 rounded to 1.00, because the value stored in memory is 1.00499999999999989342 — a hair below the number you typed. Stranger still, Pandas and Python disagree on the first one: round(2.675, 2) in plain Python gives 2.67, while Pandas gives 2.68. Neither is wrong. Python inspects the stored value, which is 2.67499999999999982236, and rounds down. Pandas goes through NumPy, which multiplies by 100 first — and 2.675 * 100 lands on exactly 267.5, which then rounds half to even, up to 268. If you need decimal arithmetic to behave the way it does on paper, floats are the wrong storage and Python's decimal module is the right one.

Rounding when you meant formatting. .round(2) throws away information; formatting only changes what you see. For money that difference is the whole ballgame. If the goal is a readable table, format it — outside a notebook, an f-string or map with a format method attached to it does the job:

df['mean_capacity'].map('{:,.0f}'.format)
tournament_name
2011 FIFA Women's World Cup    34,500
2014 FIFA Men's World Cup      67,094
2015 FIFA Women's World Cup    41,231
2018 FIFA Men's World Cup      48,016
2019 FIFA Women's World Cup    31,192
2022 FIFA Men's World Cup      55,031
Name: mean_capacity, dtype: str

In a Jupyter notebook, df.style.format({'goals_per_match': '{:.2f}', 'mean_capacity': '{:,.0f}'}) renders the whole table that way at once, with thousands separators and all. Either route leaves the numbers alone: the Styler's .data['mean_capacity'].sum() is still 277063.70192307694, where the same sum after .round(-3) is 276000.0. My rule is to round when the extra digits are false precision that should not survive into later calculations, and to format when they are merely ugly.

Rounding partway through, and letting the error pile up. Round early and every subsequent sum carries the error of every rounded row. Here is the share of World Cup matches held in each host country:

pct = matches['country_name'].value_counts(normalize=True).mul(100)

pct.sum()            # 100.0
pct.round(0).sum()   # 102.0

Rounding twenty percentages to whole numbers produced a set of shares that adds up to 102 percent. Round at the end of the chain, not in the middle; and if the rounded parts must still total the whole, you will have to apportion the remainder yourself.

Where it shows up in Bamboo Weekly

Twelve Bamboo Weekly solutions call .round() on real data, and a thirteenth reaches for Python's built-in round inside a map. Four worth reading:

Bamboo Weekly #172: World Cup is the source of the data above. Splitting matches into morning, afternoon and night and taking the mean of total_goals gives long decimals, so the chain ends .mean().round(2) — 2.25 goals in the morning against 2.94 at night.

Bamboo Weekly #45: Netflix shows the cosmetic case at its most useful. Mean hours viewed per release year came back as 8.637500e+06 and friends; a single .round(2) turned the whole series back into numbers a person can read.

Bamboo Weekly #46: Pedestrians rounds a single value rather than a column: .value_counts(normalize=True).drop(0).sum().round(2) gives 0.2, meaning that 20 percent of accidents in the data involve a pedestrian. round is a method on the scalar as well as on the series, so it chains at the end either way.

Bamboo Weekly #175: Inflation ends a groupby with .sort_values().round(2) before plotting, which is the pattern I use most: sort, round, then hand the result to Plotly.

Practice it

Work through a .round() exercise, with instant feedback and no signup required: practice.lernerpython.com/bamboo-weekly/round/

Go deeper

If the floating-point half of this page was the surprising part, the underlying story is worth an hour of your time — I wrote about it years ago in Fun with floats, and it explains a great deal of otherwise inexplicable Pandas output. The other page that pairs with this one is astype, for when what you want is not fewer decimal places but a different dtype entirely.

More Pandas videos on Python and Pandas with Reuven Lerner.

Bamboo Weekly is the practice. If you want the structured version — full courses with downloadable Jupyter notebooks, plus live Pandas office hours when you get stuck — that is LernerPython+Data. A paid Bamboo Weekly subscription is included with it.

See it on real data

Below are the 13 Bamboo Weekly exercises that use round on real-world data — try each one, then study the worked solution.

Part of the Pandas Methods Index. See also practice by skill.