T.DOGG
T.DOGG

Reputation: 223

How to round a number to a chosen integer

In Denmark we have an odd grading system that goes as follows. [-3,00,02,4,7,10,12] Our assignment is to take a vector with different decimal numbers, and round it to the nearest valid grade. Here is our code so far.

import numpy as np

def roundGrade(grades):    
    if (-5<grades<-1.5):
        gradesRounded = -3
    elif (-1.5<=grades<1.5):
        gradesRounded = 00
    elif (1.5<=grades<3):
        gradesRounded = 2
    elif (3<=grades<5.5):
        gradesRounded = 4
    elif (5.5<=grades<8.5):
        gradesRounded = 7
    elif (8.5<=grades<11):
        gradesRounded = 10
    elif (11<=grades<15):
        gradesRounded = 12
    return gradesRounded

print(roundGrade(np.array[-2.1,6.3,8.9,9]))

Our console doesn't seem to like this and retuns: TypeError: builtin_function_or_method' object is not subscriptable

All help is appreciated, and if you have a smarter method you are welcome to put us in our place.

Upvotes: 18

Views: 3778

Answers (10)

user3483203
user3483203

Reputation: 51165

I would recommend using numpy.digitize here, as it allows you to easily vectorize this operation.

Setup

bins = np.array([-5, -1.5, 1.5, 3, 5.5, 8.5, 11, 15])
outputs = np.array([-3,  0,  2,  4,  7, 10, 12])

grades = np.array([-2.1,  6.3,  8.9,  9. ])

Using numpy.digitize:

outputs[np.digitize(grades, bins)-1]

array([-3,  7, 10, 10])

Even though other answers have shown how to use np.vectorize, digitize will still provide you with a large performance increase when binning values:

_v_round_grade = np.vectorize(roundGrade)
grades = np.random.randint(-4, 14, 10000)

In [496]: %timeit _v_round_grade(grades)
5.64 ms ± 24.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [497]: %timeit outputs[np.digitize(grades, bins)-1]
210 µs ± 567 ns per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Validation

>>> np.array_equal(_v_round_grade(grades), outputs[np.digitize(grades, bins)-1])
True

Using this builtin digitize function is over 25x faster than simply vectorizing your own function.

Upvotes: 1

miradulo
miradulo

Reputation: 29690

You could simply take the minimum distance from each grade to each grade group, like so. This assumes you actually want to round to the nearest grade from your grade group, which your current code doesn't do exactly.

grade_groups = [-3,0,2,4,7,10,12]
sample_grades = [-2.1,6.3,8.9,9]
grouped = [min(grade_groups,key=lambda x:abs(grade-x)) for grade in sample_grades]
print(grouped)

Outputs:

[-3, 7, 10, 10]

Note that even after fixing your error your approach won't yet work because roundGrade expects a single number as a parameter. As shown by juanapa you could vectorize your function.

Upvotes: 18

Isaac Greene
Isaac Greene

Reputation: 161

You are getting that error because of the extra parenthesis in the line

print(roundGrade(np.array([-2.1,6.3,8.9,9])))

is should be

print(roundGrade(np.array[-2.1,6.3,8.9,9]))

Upvotes: 1

kezzos
kezzos

Reputation: 3221

I think you might be able to do it in a one liner using only lists:

l = [-2.1,6.3,8.9,9]
b = [-3,0,02,4,7,10,12]

a = [b[min(enumerate([abs(j - item) for j in b]), key=lambda p:p[1])[0]] for item in l]
>>> [-3, 7, 10, 10]

You can break this down as:

min(enumerate([abs(j - item) for j in b]), key=lambda p:p[1])[0]  # Find the index of the nearest grade boundary
[b[...] for item in l]  # Get the associated grade boundary for all the items in the original list

Upvotes: 2

Neo
Neo

Reputation: 3786

def roundGrade(grade):
    d = {x:abs(x - grade) for x in [-3,00,02,4,7,10,12]}
    return min(d, key=d.get)
print list(map(roundGrade, [-2.1, 6.3, 8.9, 9]))

It is less efficient (I think) but is much smaller code
It creates a dictionary where the key is a round grade and the value is the difference between the round grade and the given grade
Then it finds the minimum value (smallest difference) and returns the key (the rounded value
Then it just uses map to apply this function over every item in the list

Upvotes: 2

Mikea
Mikea

Reputation: 33

I would round by calculating which valid grade it is closest to and returning that one:

import numpy as np
def roundGrade(grade):
    validGrades = np.array([-3,0,2,4,7,10,12])
    return validGrades[np.argmin((grade-validGrades)**2)]

This of course only allows you to pass a single grade at a time, but you can just loop over your array of decimals, either outside the function or inside it to make it array compatible.

Upvotes: 2

juanpa.arrivillaga
juanpa.arrivillaga

Reputation: 95927

You are getting that error because when you print, you are using incorrect syntax:

print(roundGrade(np.array[-2.1,6.3,8.9,9]))

needs to be

print(roundGrade(np.array([-2.1,6.3,8.9,9])))

Notice the extra parentheses: np.array(<whatever>)

However, this won't work, since your function expects a single number. Fortunately, numpy provides a function which can fix that for you:

In [15]: roundGrade = np.vectorize(roundGrade)

In [16]: roundGrade(np.array([-2.1,6.3,8.9,9]))
Out[16]: array([-3,  7, 10, 10])

http://docs.scipy.org/doc/numpy-1.10.1/reference/generated/numpy.vectorize.html

Upvotes: 25

Ma0
Ma0

Reputation: 15204

That's how i would do it:

def roundGrade(grades_in):
    grades_out = []
    for grades in grades_in:
        if grades < -5 or grades > 15:
            gradesRounded = '??'
            print('Grade input out of range ({:})!'.format(grades))
        if (-5<grades<-1.5):
            gradesRounded = '-3'
        elif (-1.5<=grades<1.5):
            gradesRounded = '00'
        elif (1.5<=grades<3):
            gradesRounded = '2'
        elif (3<=grades<5.5):
            gradesRounded = '4'
        elif (5.5<=grades<8.5):
            gradesRounded = '7'
        elif (8.5<=grades<11):
            gradesRounded = '10'
        elif (11<=grades<15):
            gradesRounded = '12'
        grades_out.append(gradesRounded)
    return grades_out

 grades_in = [-7, -2.1, 0.1, 6.3, 8.9, 9]
 print(roundGrade(grades_in))  #prints: ['??', '-3', '00', '7', '10', '10']

Takes a list and returns one. Handles out-of-range input and list items returned are strings instead of integers to add that fancy "00" and not "0".

Upvotes: 1

Aviad
Aviad

Reputation: 349

As for the exception, numpy array is a function , and needed to be called with () So your method call should be like:

print(roundGrade(np.array([-2.1,6.3,8.9,9])))

and not:

print(roundGrade(np.array[-2.1,6.3,8.9,9]))

As for the function itself, either use grades.any() or grades.all() to test the entire elements, otherwise the comparison is not defined.

Upvotes: 0

Yotam Salmon
Yotam Salmon

Reputation: 2411

Well, even without testing your code, I can see some problems here.

Your function, roundGrade, takes a number and returns a number, but when you call it, you provide an array to it. Assuming your indentation is ok and the call is not inside the function, I would do something like that:

def roundGrade(grades):

    if (-5<grades<-1.5):
        gradesRounded = -3
    elif (-1.5<=grades<1.5):
        gradesRounded = 00
    elif (1.5<=grades<3):
        gradesRounded = 2
    elif (3<=grades<5.5):
        gradesRounded = 4
    elif (5.5<=grades<8.5):
        gradesRounded = 7
    elif (8.5<=grades<11):
        gradesRounded = 10
    elif (11<=grades<15):
        gradesRounded = 12
    return gradesRounded

#print(roundGrade(np.array[-2.1,6.3,8.9,9]))
# Here, I assume you want to round each of the grades in the array. If I'm wrong, comment, please!

for i in [-2.1, 6.3, 8.9, 9]:
    print roundGrade(i)

Calling the method and providing an array is not ok, while calling that method with each of the elements is ok, because the method is supposed to receive a number and not an array.

Upvotes: 1

Related Questions