Rounding options

library(roundwork)

reround() has a rounding argument that controls how numbers are assumed to have been rounded, and so do unround(), reround_to_fraction(), rounding_bias(), and the many functions in downstream packages such as scrutiny that call them. Specify rounding as any of the strings below.

Which option to choose

For consistency testing, the default is almost always the right choice, and there is usually no need to identify the software behind a paper.

rounding = "up_or_down" counts a value set as consistent if the reported number matches either of the two rounded results. This covers every way of breaking a tie at 5: up, down, to even, away from zero, and toward zero. A value set is therefore never rejected because the wrong tie-breaking rule was assumed. rounding = "ceiling_or_floor" is even more permissive and covers every rounding procedure, including truncation and custom thresholds.

Specifying a single procedure narrows the range of values the test accepts, but does not simply make it stricter. "up" and "down" each admit one half of the default’s ties, so they can only reject what the default accepts. "even" leads to the same results as the default; see the note on rounding to even below. "trunc", "ceiling", "floor", "anti_trunc", and the "*_from" methods with a threshold other than 5 shift the range instead, so they may accept value sets that the default rejects, and vice versa. A single procedure is appropriate if there is positive evidence about how the numbers were produced. Without such evidence, it may lead to false positives or negatives, so in case of doubt, keep the default.

The options

These options return two rounded numbers for each input number. In consistency tests, this means counting a value set as consistent if the reported number matches either of the two rounded reconstructed numbers.

By contrast, these options only return one rounded number per input:

Two arguments modify these procedures:

Finally, five options name a complete tie-breaking procedure, including for negative numbers, so symmetric has nothing left to decide:

"ties_even" is another name for "even". Unlike the other four, it does not correspond to any combination with symmetric, because rounding to even treats negative numbers the same way in either case. It is included because roundTiesToEven is IEEE 754’s default direction.

"ties_up" is "up" with symmetric = FALSE; "ties_away" is "up" with symmetric = TRUE. These names exist because the translation between the two spellings is easy to confuse, and because “up” means away from zero in Excel, Java, and Python but refers to the number line in roundwork. The matching functions are round_ties_up(), round_ties_down(), round_ties_away(), round_ties_zero(), and round_ties_even().

Combining the two spellings with symmetric = TRUE is an error. rounding = "ties_up", symmetric = TRUE looks like a request to round ties away from zero, but "ties_up" already determines the direction. Write rounding = "ties_away" instead, or rounding = "up", symmetric = TRUE.

reround(-2.5, digits = 0, rounding = "ties_up", symmetric = TRUE)
#> Error in `reround()`:
#> ! `symmetric` must not be `TRUE` with `rounding = "ties_up"`.
#> ✖ It is TRUE.
#> ℹ A "ties_*" string names a complete tie-breaking procedure, so it already
#>   determines which way ties go for negative numbers.
#> ℹ For ties away from zero, set `rounding` to "ties_away".

Between them, these five names and "ceiling", "floor", and "trunc" cover every rounding direction IEEE 754 defines:

IEEE 754 roundwork
roundTiesToEven (the standard’s default) "even", "ties_even"
roundTiesToAway "ties_away" ("up" with symmetric = TRUE)
roundTowardPositive "ceiling"
roundTowardNegative "floor"
roundTowardZero "trunc"

The standard has no name for "ties_up", "ties_zero", or "anti_trunc", which are Stata’s, Python’s ROUND_HALF_DOWN, and Excel’s ROUNDUP() respectively.

Negative numbers: symmetric

symmetric matters whenever your data contain negative values: difference scores, z-scores, effect sizes, temperature scales.

By default (symmetric = FALSE), roundwork reads “up” and “down” on the number line, so a negative tie goes up toward zero:

round_up(-2.5)
#> [1] -2
round_down(-2.5)
#> [1] -3

With symmetric = TRUE, a negative number is rounded like its absolute value, so a tie moves away from zero:

round_up(-2.5, symmetric = TRUE)
#> [1] -3
round_down(-2.5, symmetric = TRUE)
#> [1] -2

Most statistical software does the second thing. Excel, SPSS, SAS, and Matlab all round -2.5 to -3; in IEEE 754 terms, this is roundTiesToAway. Stata is the exception, as the table below shows. So if you set rounding to a single direction on data with negative values, you almost always want symmetric = TRUE as well.

If you would rather not have to remember which way round that is, say it directly:

round_ties_away(-2.5)   # Excel, SPSS, SAS, Matlab
#> [1] -3
round_ties_up(-2.5)     # Stata
#> [1] -2

symmetric only affects negative numbers. With "up", "down", and "up_or_down", it only affects ties among them, because the midpoint between two candidates is the same whether or not the sign is removed first. With "up_from" and "down_from" at a threshold other than 5, it affects more than ties, because the point at which these procedures switch direction is not the midpoint:

round_up_from(-4.28, digits = 1, threshold = 9)
#> [1] -4.3
round_up_from(-4.28, digits = 1, threshold = 9, symmetric = TRUE)
#> [1] -4.2

Under the default rounding = "up_or_down", both results are accepted, so symmetric cannot change a test result.

Rounding at 5 in other software

If the part cut off by rounding is exactly 5, how do these programs round, and which roundwork settings reconstruct them?

The rounding and symmetric columns are one way to say it; the "ties_*" equivalent is in brackets. Use one or the other, not both: a "ties_*" string cannot be combined with symmetric = TRUE.

Software Function rounding symmetric Note
Excel ROUND() "up" ("ties_away") TRUE Absolute value is rounded, then the sign is reapplied
Excel ROUNDUP() "anti_trunc" — Away from zero, not ceiling
Excel ROUNDDOWN() "trunc" — Toward zero
Google Sheets ROUND(), ROUNDUP(), ROUNDDOWN() as Excel as Excel Follows Excel
SPSS RND() "up" ("ties_away") TRUE Ties away from zero; the fuzz is user-settable (SET FUZZBITS)
SPSS TRUNC() "trunc" —
SAS ROUND() "up" ("ties_away") TRUE “Rounds up the absolute value and restores the original sign”, with fuzzing
SAS ROUNDE(), ROUNDZ() "even" — ROUNDE() fuzzes, ROUNDZ() does not
Matlab round() "up" ("ties_away") TRUE “Rounds away from zero to the nearest integer with larger magnitude”; a TieBreaker option overrides this
Stata round() "up" ("ties_up") FALSE “x is always rounded up to the larger value \[...\] round(-4.5) is −4”
R (>= 4.0.0) round() "even" — Measures which neighbor is closer on the binary double
Python round() "even" — Own algorithm for inexact ties; can differ from R
Python decimal ROUND_HALF_UP "up" ("ties_away") TRUE In this vocabulary, “up” means away from zero
NumPy np.round() "even" — Own algorithm for inexact ties

Three points in the table require explanation.

Stata is the exception. Its manual is explicit that ties go to the larger value on the number line, so round(4.5) is 5 and round(-4.5) is −4. That is roundwork’s default, symmetric = FALSE. Every other program in the table rounds negative numbers like their absolute values.

“Rounding up” means two different things. Excel’s and Google Sheets’ ROUNDUP() round away from zero: ROUNDUP(-3.14159, 1) is -3.2. That is roundwork’s "anti_trunc", not "ceiling", which would give -3.1; the two coincide for positive numbers only. Likewise, ROUNDDOWN() is "trunc", not "floor". The same pair appears as Java’s RoundingMode.UP and DOWN and as Python’s decimal.ROUND_UP and ROUND_DOWN.

Several programs apply a small tolerance to counter floating-point error, and so does roundwork. SAS’s ROUND() performs “extra computations, called fuzzing, to try to make the result agree with decimal arithmetic”, and SPSS exposes the same idea as a user setting. roundwork’s rounding functions nudge a value by about 1.5e-9, so that round_up(0.145, 2) is 0.15 rather than 0.14. See the Floating-point tolerance section of ?round_up.

Note on rounding to even

rounding = "even" calls base R’s round(). R, Python, and NumPy all round exact ties to even, but each settles inexact cases with its own algorithm, so they can disagree: R rounds 1.05 to 1.0, Python to 1.1. Most decimal fractions have no exact binary representation, so a number that looks like a tie usually is not one, and the result can’t be predicted from the number as printed. See vignette("rounding-in-depth"), section To even.

The consistency tests account for this by treating both bounds as inclusive under "even". These are the same bounds as under "up_or_down", so rounding = "even" gives the same test results as the default. Knowing that a paper’s numbers were computed in R therefore does not make the tests stricter.

A note on roundwork’s names

roundwork’s "up" means rounding up from 5, not rounding up in every case. The latter is what “rounding up” means in everyday language, and what roundwork calls "ceiling". Read "up" and "down" as short for “up from 5” and “down from 5”. The "ties_*" options and round_ties_*() functions name the procedure explicitly.

They are not called round_half_up() because janitor::round_half_up() is roundTiesToAway (janitor::round_half_up(-2.5) is -3), as are Java’s RoundingMode.HALF_UP and Python’s decimal.ROUND_HALF_UP. A roundwork function of the same name that rounded ties toward +Inf would return the opposite result for the same input. "ties_away" is unambiguous.

Where the IEEE 754 standard has a name for a procedure, roundwork offers that name too: round_ties_to_away() is round_ties_away(), round_toward_positive() is round_ceiling(), and so on. See ?ieee-754 for the full mapping.