pythoncoordinatesrounding

rounding coordinates to centre of grid square


Im currently trying to collect some weather data from an API, and to reduce the amount of API calls im trying to batch the calls on 0.5degree longitude and latitude chunks due to its resolution. I had this code

def round_to_grid_center(coordinate,grid_spacing=0.5 ):
        offset = grid_spacing / 2
        return round(((coordinate - offset) / grid_spacing)) * grid_spacing + offset

but this function rounded values at 0.5 down to 0.25 instead of up to 0.75, incorrect rounding for entry 5 so i added this fix. It works for me, but I'm sure there is a better more efficient method to round the coordinates to their closest grid square centre. Please let me know!

def round_to_grid_center(coordinate,grid_spacing=0.5 ):
    #temp fix for round down error
    if ((coordinate % 0.5)== 0 and (coordinate % 1 )!= 0):
        offset = grid_spacing / 2
        return round(((coordinate + 0.01 - offset) / grid_spacing)) * grid_spacing + offset
    else:
        offset = grid_spacing / 2
        return round(((coordinate - offset) / grid_spacing)) * grid_spacing + offset

Output rounding entry 5 correctly for batch


Solution

  • Given: round half to even

    The round() function uses "round half to even" rounding mode, as mentioned in the Built-in Types doc, section Numeric types (emphasis by me):

    Operation Result
    round(x[, n]) x rounded to n digits, rounding half to even. If n is omitted, it defaults to 0.

    "Rounding half to even" means that a floating point number with a decimal part of .5 is rounded towards the closest even integer rather than the closest greater integer. For example, both round(1.5) and round(2.5) will produce 2. In entry 5 (coordinate=38.5, grid_spacing=0.5, offset=0.25), you will consequently get round((38.5-0.25)/0.5)) = round(76.5) = 76, and thus a rounded-down result for the part of your calculation before spacing and offset correction.

    The Wikipedia article on rounding provides as motivation for this rounding mode:

    This function minimizes the expected error when summing over rounded figures, even when the inputs are mostly positive or mostly negative, provided they are neither mostly even nor mostly odd.

    If one needs further convincing that this rounding mode makes sense, one might want to have a look at the very detailed answer to this question ("rounding half to even" is called "banker's rounding" there).

    Required: round half up

    In any case, what you want is round half up instead. You can follow the answers to this question for potential solutions, e.g. rather than using round(x), you could use int(x + .5) or float(Decimal(x).to_integral_value(rounding=ROUND_HALF_UP)).

    Altogether, this could look as follows:

    from decimal import Decimal, ROUND_HALF_UP
    
    values = [(33.87, 151.21), (33.85, 151.22), ( 38.75, 149.85),
              (35.15, 150.85), (38.50, 149.87), (-38.50, 149.95)]
    
    def round_to_grid_center(coordinate, grid_spacing=0.5):
        offset = grid_spacing / 2
        return round((coordinate - offset) / grid_spacing) * grid_spacing + offset
        
    def round_with_int(coordinate, grid_spacing=0.5):
        offset = grid_spacing / 2
        return int(.5 + ((coordinate - offset) / grid_spacing)) * grid_spacing + offset
    
    def round_with_decimal(coordinate, grid_spacing=0.5):
        offset = grid_spacing / 2
        return float(Decimal((coordinate - offset) / grid_spacing).to_integral_value(rounding=ROUND_HALF_UP)) * grid_spacing + offset
    
    for round_current in [round_to_grid_center, round_with_int, round_with_decimal]:
        print(f"\n{round_current.__name__}():")
        for i, (v1, v2) in enumerate(values):
            print(f"{i+1}: {v1}→{round_current(v1)}, {v2}→{round_current(v2)}")
    

    Which prints:

    round_to_grid_center():
    1: 33.87→33.75, 151.21→151.25
    2: 33.85→33.75, 151.22→151.25
    3: 38.75→38.75, 149.85→149.75
    4: 35.15→35.25, 150.85→150.75
    5: 38.5→38.25, 149.87→149.75
    6: -38.5→-38.75, 149.95→149.75
    
    round_with_int():
    1: 33.87→33.75, 151.21→151.25
    2: 33.85→33.75, 151.22→151.25
    3: 38.75→38.75, 149.85→149.75
    4: 35.15→35.25, 150.85→150.75
    5: 38.5→38.75, 149.87→149.75
    6: -38.5→-38.25, 149.95→149.75
    
    round_with_decimal():
    1: 33.87→33.75, 151.21→151.25
    2: 33.85→33.75, 151.22→151.25
    3: 38.75→38.75, 149.85→149.75
    4: 35.15→35.25, 150.85→150.75
    5: 38.5→38.75, 149.87→149.75
    6: -38.5→-38.75, 149.95→149.75
    

    Note how the values differ for negative numbers though (which I included as entry 6): with int(), "up" means "towards positive infinity"; with Decimal, "up" means "away from zero".

    Last but not least – be aware of numerical imprecision in floating point representation and arithmetic: depending on the size of coordinate and the value of grid_spacing, round-off error may lead to unexpected results.