Half Integers

Author

Emanuele Messori

Published

January 12, 2024

Rounding does matter

How do we do that? There are many ways. Here we focus on the most common rounding, specified by the round() function in R, round() and numpy.round() in Python.How do we do this? There are many ways. Here we focus on the most common rounding, specified by the round() function in R, round() and numpy.round() in Python.

In R

half_integers <- seq(0.5, 10.5, 1)
cat(paste(half_integers, "=", round(half_integers)), sep = "\n")
0.5 = 0
1.5 = 2
2.5 = 2
3.5 = 4
4.5 = 4
5.5 = 6
6.5 = 6
7.5 = 8
8.5 = 8
9.5 = 10
10.5 = 10

From R documentation :

round rounds the values in its first argument to the specified number of decimal places (default 0). See ‘Details’ about “round to even” when rounding off a 5.

Note that for rounding off a 5, the IEC 60559 standard (see also ‘IEEE 754’) is expected to be used, ‘go to the even digit’. Therefore round(0.5) is 0 and round(-1.5) is -2. However, this is dependent on OS services and on representation error (since e.g. 0.15 is not represented exactly, the rounding rule applies to the represented number and not to the printed number, and so round(0.15, 1) could be either 0.1 or 0.2).

Interesting things happen when rounding off to the first decimal :

little_floats <- seq(0.05, 1, 0.05)
cat(
    paste(little_floats, 
          "-->",
          round(little_floats,1)),
  sep = "\n")
0.05 --> 0
0.1 --> 0.1
0.15 --> 0.2
0.2 --> 0.2
0.25 --> 0.2
0.3 --> 0.3
0.35 --> 0.4
0.4 --> 0.4
0.45 --> 0.4
0.5 --> 0.5
0.55 --> 0.6
0.6 --> 0.6
0.65 --> 0.7
0.7 --> 0.7
0.75 --> 0.8
0.8 --> 0.8
0.85 --> 0.9
0.9 --> 0.9
0.95 --> 1
1 --> 1

But:

li <- c(0.05,0.10,0.15,0.2,0.25)
cat(
  paste(li, 
        "-->",
        round(li, 1)
  ),
  sep = "\n"
)
0.05 --> 0
0.1 --> 0.1
0.15 --> 0.1
0.2 --> 0.2
0.25 --> 0.2

In Python

import numpy as np
half_integers = np.arange(0.5, 11.5, 1)
half_integers
array([ 0.5,  1.5,  2.5,  3.5,  4.5,  5.5,  6.5,  7.5,  8.5,  9.5, 10.5])
hi = half_integers.round()
for (i,n) in zip(half_integers, hi):
  print(f"{i} --> {n}")
0.5 --> 0.0
1.5 --> 2.0
2.5 --> 2.0
3.5 --> 4.0
4.5 --> 4.0
5.5 --> 6.0
6.5 --> 6.0
7.5 --> 8.0
8.5 --> 8.0
9.5 --> 10.0
10.5 --> 10.0

And, slightly more laconic Python doc :

round(number, ndigits=None)1

Round a number to a given precision in decimal digits.
 
The return value is an integer if ndigits is omitted or None.  Otherwise
the return value has the same type as the number.  ndigits may be negative.

Rounding off to the first decimal unit :

lf = np.arange(0.15, 1, 0.05)
for (i,n) in zip(lf, lf.round(1)):
  print(f"{i} --> {n}")
0.15 --> 0.2
0.2 --> 0.2
0.25 --> 0.2
0.30000000000000004 --> 0.3
0.3500000000000001 --> 0.4
0.40000000000000013 --> 0.4
0.45000000000000007 --> 0.5
0.5000000000000001 --> 0.5
0.5500000000000002 --> 0.6
0.6000000000000002 --> 0.6
0.6500000000000002 --> 0.7
0.7000000000000002 --> 0.7
0.7500000000000002 --> 0.8
0.8000000000000003 --> 0.8
0.8500000000000002 --> 0.9
0.9000000000000002 --> 0.9
0.9500000000000003 --> 1.0

Due to the limitations of floating-point arithmetic, the output of the np.arange() function may not be exact 2. :)

Finally :

round(0.15,1)
0.1
np.round(0.15, 1)
0.2
[1] "Python version : 3.8"
R version 4.3.2 (2023-10-31 ucrt)
Platform: x86_64-w64-mingw32/x64 (64-bit)
Running under: Windows 11 x64 (build 22000)

Matrix products: default


locale:
[1] LC_COLLATE=French_France.utf8  LC_CTYPE=French_France.utf8   
[3] LC_MONETARY=French_France.utf8 LC_NUMERIC=C                  
[5] LC_TIME=French_France.utf8    

time zone: Europe/Paris
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

loaded via a namespace (and not attached):
 [1] digest_0.6.33     fastmap_1.1.1     xfun_0.41         Matrix_1.6-1.1   
 [5] lattice_0.21-9    reticulate_1.34.0 rappdirs_0.3.3    knitr_1.45       
 [9] htmltools_0.5.7   png_0.1-8         rmarkdown_2.25    cli_3.6.1        
[13] grid_4.3.2        withr_2.5.2       compiler_4.3.2    rprojroot_2.0.4  
[17] here_1.0.1        rstudioapi_0.15.0 tools_4.3.2       evaluate_0.23    
[21] Rcpp_1.0.11       yaml_2.3.7        rlang_1.1.2       jsonlite_1.8.7   
[25] htmlwidgets_1.6.2

Footnotes

  1. To be fair, i used the round() or around() method in numpy, for which there is a more extensive documentation :

    around(a, decimals=0, out=None)

    Evenly round to the given number of decimals.
     
    Parameters
    ----------
    a : array_like
        Input data.
    decimals : int, optional
        Number of decimal places to round to (default: 0).  If
        decimals is negative, it specifies the number of positions to
        the left of the decimal point.
    out : ndarray, optional
        Alternative output array in which to place the result. It must have
        the same shape as the expected output, but the type of the output
        values will be cast if necessary. See :ref:`ufuncs-output-type` for more
        details.
     
    Returns
    -------
    rounded_array : ndarray
        An array of the same type as `a`, containing the rounded values.
        Unless `out` was specified, a new array is created.  A reference to
        the result is returned.
     
        The real and imaginary parts of complex numbers are rounded
        separately.  The result of rounding a float is a float.
     
    See Also
    --------
    ndarray.round : equivalent method
    ceil, fix, floor, rint, trunc
     
     
    Notes
    -----
    `~numpy.round` is often used as an alias for `~numpy.around`.
     
    For values exactly halfway between rounded decimal values, NumPy
    rounds to the nearest even value. Thus 1.5 and 2.5 round to 2.0,
    -0.5 and 0.5 round to 0.0, etc.
     
    ``np.around`` uses a fast but sometimes inexact algorithm to round
    floating-point datatypes. For positive `decimals` it is equivalent to
    ``np.true_divide(np.rint(a * 10**decimals), 10**decimals)``, which has
    error due to the inexact representation of decimal fractions in the IEEE
    floating point standard [1]_ and errors introduced when scaling by powers
    of ten. For instance, note the extra “1” in the following:
     
        >>> np.round(56294995342131.5, 3)
        56294995342131.51
     
    If your goal is to print such values with a fixed number of decimals, it is
    preferable to use numpy’s float printing routines to limit the number of
    printed decimals:
     
        >>> np.format_float_positional(56294995342131.5, precision=3)
        ‘56294995342131.5’
     
    The float printing routines use an accurate but much more computationally
    demanding algorithm to compute the number of digits after the decimal
    point.
     
    Alternatively, Python’s builtin `round` function uses a more accurate
    but slower algorithm for 64-bit floating point values:
     
        >>> round(56294995342131.5, 3)
        56294995342131.5
        >>> np.round(16.055, 2), round(16.055, 2)  # equals 16.0549999999999997
        (16.06, 16.05)
     
     
    References
    ----------
    .. [1] “Lecture Notes on the Status of IEEE 754”, William Kahan,
           https://people.eecs.berkeley.edu/~wkahan/ieee754status/IEEE754.PDF

    ↩︎