janitor round_half_up() in R: Round Numbers Away From Zero
The janitor round_half_up() function rounds a numeric vector using conventional half-up rounding so that exact midpoints like 2.5 always go away from zero, instead of R's default banker's rounding that sends 2.5 to 2. It is the small utility you reach for when audit, regulatory, or survey results require the rounding rule that most people expect intuitively.
round_half_up(2.5) # 3, not 2 round_half_up(c(0.5, 1.5, 2.5, 3.5)) # 1 2 3 4 round_half_up(-2.5) # -3, away from zero round_half_up(0.125, digits = 2) # 0.13 round_half_up(c(1.2345, 5.6789), digits = 3)# 1.235 5.679 round_half_up(c(NA, 1.5, 2.5)) # NA 2 3 mtcars$mpg |> round_half_up(digits = 1) # vectorised over a column
Need explanation? Read on for examples and pitfalls.
What round_half_up() does in one sentence
round_half_up() rounds a numeric vector so exact midpoints move away from zero, matching the convention taught in school. Internally it shifts the value by 0.5 plus a tiny epsilon, truncates, then restores the original sign, which sidesteps the floating-point edge cases that trip up naive implementations.
The contrast with base R is the whole point. round(2.5) returns 2 (IEC 60559 banker's rounding). round_half_up(2.5) returns 3, restoring the rule a non-statistician expects.
Syntax
round_half_up() takes a numeric vector and an integer digits argument, and returns a numeric vector of the same length. No data frame plumbing, no tidyselect, no extra arguments.
The full signature is short:
round_half_up(x, digits = 0)
x is a numeric vector. digits is the number of decimal places to keep, defaulting to 0 (integer rounding). The function preserves NA values, vectorises naturally, and works inside dplyr::mutate(), sapply(), or any other vectorised context. There is no na.rm, no rounding mode toggle, no second positional argument to worry about.
Common patterns
Five patterns cover almost every reason to reach for round_half_up(). Run them in order; each block builds on the one before.
1. Round to an integer
The default call rounds to the nearest integer, sending each .5 away from zero. This is the most common use: percentages reported as whole numbers, head counts, age in years.
Every midpoint in x rounded up under round_half_up(). Base R round() sent each to the nearest even number, which surprises readers who expected 1, 2, 3, 4, 5.
2. Keep decimals with the digits argument
Pass digits to keep N decimal places. The same half-up rule applies at that scale.
The difference shows up wherever the third decimal is exactly 5. In a financial report, the half-up result matches the rule an auditor expects.
round_half_up() whenever the rounded numbers feed a report a non-technical reader will check. Survey percentages, dollar amounts, age groups, and grade boundaries all benefit from the conventional rule. Save banker's rounding for places where accumulated bias actually matters, like long simulation chains.3. Negative numbers round away from zero
For negatives, half-up means "away from zero", not "toward positive infinity". This matches the intuitive symmetry: 2.5 to 3 mirrors -2.5 to -3.
A naive floor(x + 0.5) implementation gets negatives wrong: it sends -0.5 to 0 and -1.5 to -1 by rounding toward positive infinity. round_half_up() uses sign(x) and abs(x) so the rule stays symmetric.
floor(x + 0.5) shortcut and assume it matches round_half_up(). It works on positive midpoints, then quietly disagrees on negative ones. The janitor implementation handles this case for you; the time you save by importing the package is the time you do not spend debugging an audit reconciliation.4. Use it inside dplyr pipelines
round_half_up() is just a vector function, so it composes with mutate() and across(). This is the natural way to apply it to selected columns of a data frame.
The lambda \(v) round_half_up(v, digits = 1) is the pattern: wrap the call so across() can pass the column. Without the lambda the second argument never reaches the function.
5. Handle NA values without surprises
NA propagates cleanly; no extra argument needed. This matters in real survey data where missing responses are common.
If you want NAs dropped before rounding, do it explicitly with na.omit() or [complete.cases(.)]. The function intentionally does not silently remove them.
Compare with alternatives
Base R, janitor, and the formatting helpers each round in a different way. Pick by what the downstream consumer expects.
| Approach | Rounds | Best for |
|---|---|---|
janitor::round_half_up() |
Half away from zero | Reports, audits, surveys, anything a non-statistician reviews |
base::round() |
Half to even (banker's) | Long simulations, anywhere cumulative bias matters |
janitor::round_half_to_even() |
Half to even, vector form | Explicit banker's rounding when you want the name visible |
janitor::adorn_rounding() |
Configurable, data frame input | Tabyl chains and adorn_* pipelines |
base::signif() |
Significant digits, not decimals | Scientific notation, very small or very large values |
formatC() / sprintf() |
Returns a character string | Display only, when no further math happens |
round_half_up() when the rounded values will be read; choose round() when they will be re-aggregated. Conventional rounding is correct for the reader; banker's rounding is correct for the math. Most reports want the first, most simulations want the second, and the only real mistake is using one when you should have used the other.Common pitfalls
Pitfall 1: assuming round_half_up() works on a data frame. It does not. Passing a data frame returns an error or unexpected coercion. Use mutate(across(...)) for a data frame, or use janitor::adorn_rounding() for a tabyl chain.
Pitfall 2: assuming round_half_up() defeats floating-point error. A value like 0.1 + 0.2 is not literally 0.3 in memory, so round_half_up(0.15, digits = 1) can still return 0.1 in extreme cases. The function adds sqrt(.Machine$double.eps) to mitigate this, but it is not a guarantee.
Pitfall 3: confusing round_half_up() with ceiling(). Ceiling always rounds toward positive infinity; half-up rounds the .5 cases only. ceiling(2.1) is 3, but round_half_up(2.1) is 2.
Try it yourself
Try it: Take the vector c(-1.5, -0.5, 0.5, 1.5, 2.5) and round it with round_half_up(). Compare the result with base round(). Save the half-up result to ex_rounded.
Click to reveal solution
Explanation: round_half_up() rounds each midpoint away from zero, so -1.5 goes to -2 and 0.5 goes to 1. Base R round() uses banker's rounding, sending 0.5 to 0 and 1.5 to 2 (toward the even integer). The two rules agree on -1.5 and 2.5 here only by coincidence.
Related janitor functions
round_half_up() is one piece of the janitor rounding family. Each helper sits at a slightly different abstraction level.
round_half_to_even(): vector-level banker's rounding, the explicit twin ofround_half_up()adorn_rounding(): data frame rounding for tabyl chains, with aroundingmode argumentadorn_pct_formatting(): format proportions as percent strings instead of numericstabyl(): build the frequency table thatadorn_rounding()polishessignif_half_up(): half-up rounding by significant digits, niche but availableclean_names(): standardise column names before any numeric cleanup
See the janitor reference on tidyverse.org for the source and full argument list.
FAQ
What is the difference between round_half_up() and base R round()?
round_half_up() rounds every exact midpoint away from zero, so 2.5 becomes 3 and -2.5 becomes -3. Base R round() uses IEC 60559 banker's rounding, which sends midpoints to the nearest even integer: 2.5 becomes 2 and 3.5 becomes 4. Both are defensible, but only round_half_up() matches the convention most readers learned in school. Switch when the rounded numbers will end up in a report, regulatory filing, or audit.
Does round_half_up() work on negative numbers?
Yes, and the behaviour is symmetric. Negative midpoints round away from zero: -0.5 becomes -1, -1.5 becomes -2, -2.5 becomes -3. This is what most people expect when they say "round half up" out loud, even though the literal phrase suggests rounding toward positive infinity. The janitor implementation uses sign(x) and abs(x) internally to keep the rule consistent regardless of sign.
Can I apply round_half_up() to a data frame column inside dplyr?
Yes. Wrap the call in a lambda when using across(): mutate(across(c(col1, col2), \(v) round_half_up(v, digits = 2))). The lambda is needed so the digits argument reaches the function. For a single column, mutate(col1 = round_half_up(col1, digits = 2)) is shorter.
Why does round_half_up(0.15, digits = 1) sometimes return 0.1?
Floating-point storage. The value 0.15 cannot be represented exactly in binary, so it lives in memory as something like 0.14999999999999997, which rounds down. round_half_up() adds sqrt(.Machine$double.eps) to mitigate this for typical inputs, and it succeeds for almost all real-world values, but a few pathological cases slip through. If exactness on tiny decimals matters, round at the data entry stage or use a fixed-point package like Rmpfr.
Is there a half-up version of signif()?
Yes, janitor ships signif_half_up() for half-up rounding by significant digits rather than decimal places. Reach for it when the audience expects "3 significant figures, half up" instead of "2 decimal places, half up", a common request in chemistry and engineering reports.