Round in numpy to Nearest Step
Asked Answered
C

3

10

I would like to know how I can round a number in numpy to an upper or lower threshold which is function of predefined step size. Hopefully stated in a clearer way, if I have the number 123 and a step size equal to 50, I need to round 123 to the closest of either 150 or 100, in this case 100. I came out with function below which does the work but I wonder if there is a better, more succint, way to do this.

Thanks in advance,

Paolo

def getRoundedThresholdv1(a, MinClip):
    import numpy as np
    import math
    digits = int(math.log10(MinClip))+1
    b = np.round(a, -digits)
    if b > a:  # rounded-up
        c = b - MinClip
        UpLow = np.array((b,c))
    else:  # rounded-down
        c = b + MinClip
        UpLow = np.array((c,b))
    AbsDelta = np.abs(a - UpLow)
    return UpLow[AbsDelta.argmin()]




getRoundedThresholdv1(143, 50)
Cameron answered 22/10, 2011 at 11:23 Comment(2)
your code doesn't work, for example getRoundedThresholdv1(143, 50) returns 50 instead of 150Festinate
it works now - needed int(math.log10(MinClip))+1 instead of int(math.log10(a))+1 - thanks for pointing this outCameron
F
20

The solution by pb360 is much better, using the second argument of builtin round in python3.

I think you don't need numpy:

def getRoundedThresholdv1(a, MinClip):
    return round(float(a) / MinClip) * MinClip

here a is a single number, if you want to vectorize this function you only need to replace round with np.round and float(a) with np.array(a, dtype=float)

Festinate answered 22/10, 2011 at 11:34 Comment(7)
Nice answer: though of course, if you care much about exactly which way halfway cases are rounded, life gets a lot more difficult.Sipe
@MarkDickinson: well, now it round to the nearest, for example getRoundedThresholdv1(150, 100): round to 200 because 150 is closer to 100 than to 100 as 0.5 is close to 1 than to 0 (round algebra). If you want to change the logic you can try with ceil or floorFestinate
true, for a simple example. But if you try to use this to round to multiples of 0.1 (for example), you're going to run into issues with numerical errors pushing the computed result a tiny amount one side or the other of the true result, so that the output from halfway cases becomes essentially unpredictable.Sipe
Additionally, there are differences between the way that numpy's round and Python's round treat halfway cases. For an example, try round(2.5) with numpy, Python 2.x, and Python 3.xSipe
@MarkDickinson: Oh my God! "Numpy rounds to the nearest even value"Festinate
@MarkDickinson: solutions? numpy.rint seems the sameFestinate
WARNING this solution broke for me with running getRoundedThresholdv1(a=13.200000000000001, MinClip=0.0001) returns 13.200000000000001Luxembourg
L
8

Summary: This is a correct way to do it, the top answer has cases that do not work:

def round_step_size(quantity: Union[float, Decimal], step_size: Union[float, Decimal]) -> float:
    """Rounds a given quantity to a specific step size
    :param quantity: required
    :param step_size: required
    :return: decimal
    """
    precision: int = int(round(-math.log(step_size, 10), 0))
    return float(round(quantity, precision))

My reputation is too low to post a comment on the top answer from Ruggero Turra and point out the issue. However it has cases which did not work for example:

def getRoundedThresholdv1(a, MinClip):
    return round(float(a) / MinClip) * MinClip

getRoundedThresholdv1(quantity=13.200000000000001, step_size=0.0001)

Returns 13.200000000000001 right back whether using numpy or the standard library round. I didn't even find this by stress testing the function. It just came up when using it in production code and spat an error.

Note full credit for this answer comes out of an open source github repo which is not mine found here

Luxembourg answered 22/6, 2021 at 20:12 Comment(1)
This will work only if the step size is some variant of 10^<integer> (ex: 0.01, 0.1, 1, 10, ...). For example, round_step_size(0.46, 0.2) = 0.5 when I would expect 0.4.Rosin
O
2

Note that round() in Ruggero Turra his answer rounds to the nearest even integer. Meaning:

a= 0.5
round(a)

Out: 0

Which may not be what you expect.

In case you want 'classical' rounding, you can use this function, which supports both scalars and Numpy arrays:

import Numpy as np

def getRoundedThresholdv1(a, MinClip):
    scaled = a/MinClip
    return np.where(scaled % 1 >= 0.5, np.ceil(scaled), np.floor(scaled))*MinClip

Alternatively, you could use Numpy's method digitize. It requires you to define the array of your steps. digitize will kind of ceil your value to the next step. So in order to round in a 'classical' way we need an intermediate step.

You can use this:

import Numpy as np
    
def getRoundedThresholdv1(a, MinClipBins):
    intermediate = (MinClipBins[1:] + MinClipBins[:-1])/2
    return MinClipBins[np.digitize(a, intermediate)]

You can then call it like:

bins = np.array([0,  50, 100, 150])
test1 = getRoundedThresholdv1(74, bins)
test2 = getRoundedThresholdv1(125, bins)

Which gives:

test1 = 50
test2 = 150 
Organometallic answered 19/3, 2021 at 15:10 Comment(2)
In your code referencing numpy's digitize function, you use "np.discritize" instead of "np.digitize". If you meant "np.discritize", I could not find it on numpy's v1.26 web manual.Rosin
@Rosin Thank you, that was an typo indeed. Fixed it now.Organometallic

© 2022 - 2024 — McMap. All rights reserved.