NumPy - np.round(), np.floor(), np.ceil()

• NumPy's rounding functions operate element-wise on arrays and return arrays of the same shape, making them significantly faster than Python's built-in functions for bulk operations

Key Insights

• NumPy’s rounding functions operate element-wise on arrays and return arrays of the same shape, making them significantly faster than Python’s built-in functions for bulk operations • np.round() uses banker’s rounding (round half to even) by default, which differs from Python’s built-in round() and reduces cumulative rounding bias in statistical calculations • Understanding the decimals parameter and out parameter enables in-place operations and precise control over rounding precision for both integers and floating-point numbers

Understanding NumPy Rounding Functions

NumPy provides three core functions for rounding operations: np.round(), np.floor(), and np.ceil(). Each serves a distinct purpose in numerical computing. While np.round() rounds to the nearest integer or specified decimal place, np.floor() always rounds down and np.ceil() always rounds up toward positive infinity.

import numpy as np

arr = np.array([1.2, 2.5, 3.7, -1.2, -2.5, -3.7])

print("Original:", arr)
print("Round:", np.round(arr))
print("Floor:", np.floor(arr))
print("Ceil:", np.ceil(arr))

Output:

Original: [ 1.2  2.5  3.7 -1.2 -2.5 -3.7]
Round: [ 1.  2.  4. -1. -2. -4.]
Floor: [ 1.  2.  3. -2. -3. -4.]
Ceil: [ 2.  3.  4. -1. -2. -3.]

np.round() and Banker’s Rounding

The np.round() function implements “round half to even” or banker’s rounding. When a value is exactly halfway between two integers, it rounds to the nearest even number. This behavior minimizes systematic bias in repeated rounding operations.

import numpy as np

# Demonstrating banker's rounding
halfway_values = np.array([0.5, 1.5, 2.5, 3.5, 4.5, 5.5])
rounded = np.round(halfway_values)

print("Values:", halfway_values)
print("Rounded:", rounded)
print("All even:", np.all(rounded % 2 == 0))

Output:

Values: [0.5 1.5 2.5 3.5 4.5 5.5]
Rounded: [0. 2. 2. 4. 4. 6.]
All even: True

The decimals parameter controls precision. Positive values round to decimal places, negative values round to positions left of the decimal point:

import numpy as np

values = np.array([123.456, 789.012, 345.678])

print("Original:", values)
print("2 decimals:", np.round(values, decimals=2))
print("1 decimal:", np.round(values, decimals=1))
print("Nearest 10:", np.round(values, decimals=-1))
print("Nearest 100:", np.round(values, decimals=-2))

Output:

Original: [123.456 789.012 345.678]
2 decimals: [123.46 789.01 345.68]
1 decimal: [123.5 789.  345.7]
Nearest 10: [120. 790. 350.]
Nearest 100: [100. 800. 300.]

np.floor() for Downward Rounding

np.floor() returns the largest integer less than or equal to each element. This is particularly useful for binning operations, index calculations, and when you need consistent downward rounding regardless of sign.

import numpy as np

# Practical example: Binning continuous data
data = np.array([0.1, 0.9, 1.2, 2.8, 3.1, 4.7, 5.5])
bin_size = 1.0

# Create bin indices
bin_indices = np.floor(data / bin_size).astype(int)
print("Data:", data)
print("Bin indices:", bin_indices)

# Count items per bin
unique, counts = np.unique(bin_indices, return_counts=True)
print("\nBin counts:")
for bin_id, count in zip(unique, counts):
    print(f"  Bin {bin_id}: {count} items")

Output:

Data: [0.1 0.9 1.2 2.8 3.1 4.7 5.5]
Bin indices: [0 0 1 2 3 4 5]

Bin counts:
  Bin 0: 2 items
  Bin 1: 1 items
  Bin 2: 1 items
  Bin 3: 1 items
  Bin 4: 1 items
  Bin 5: 1 items

Negative numbers demonstrate floor’s behavior:

import numpy as np

negatives = np.array([-0.1, -0.9, -1.1, -1.9, -2.5])
print("Values:", negatives)
print("Floor:", np.floor(negatives))
print("Int cast:", negatives.astype(int))  # Different from floor!

Output:

Values: [-0.1 -0.9 -1.1 -1.9 -2.5]
Floor: [-1. -1. -2. -2. -3.]
Int cast: [ 0  0 -1 -1 -2]

np.ceil() for Upward Rounding

np.ceil() returns the smallest integer greater than or equal to each element. This is essential for resource allocation problems where you need to ensure sufficient capacity.

import numpy as np

# Practical example: Calculate required containers
items_per_container = 50
item_counts = np.array([23, 50, 51, 100, 127, 200])

containers_needed = np.ceil(item_counts / items_per_container).astype(int)

print("Items:", item_counts)
print("Containers needed:", containers_needed)
print("Total containers:", containers_needed.sum())

Output:

Items: [ 23  50  51 100 127 200]
Containers needed: [1 1 2 2 3 4]
Total containers: 13

Combining ceil with other operations for pagination:

import numpy as np

def calculate_pagination(total_items, page_size):
    """Calculate pagination metrics for multiple datasets."""
    total_items = np.array(total_items)
    total_pages = np.ceil(total_items / page_size).astype(int)
    
    return {
        'total_items': total_items,
        'page_size': page_size,
        'total_pages': total_pages,
        'last_page_items': total_items - (total_pages - 1) * page_size
    }

results = calculate_pagination([100, 157, 200, 250], page_size=25)
for key, value in results.items():
    print(f"{key}: {value}")

Output:

total_items: [100 157 200 250]
page_size: 25
total_pages: [4 7 8 10]
last_page_items: [25  7 25 25]

Performance and In-Place Operations

NumPy rounding functions support the out parameter for in-place operations, reducing memory allocation overhead:

import numpy as np
import time

# Performance comparison
size = 10_000_000
data = np.random.randn(size)

# Method 1: Create new array
start = time.perf_counter()
result1 = np.round(data, decimals=2)
time1 = time.perf_counter() - start

# Method 2: In-place operation
data_copy = data.copy()
start = time.perf_counter()
np.round(data_copy, decimals=2, out=data_copy)
time2 = time.perf_counter() - start

print(f"New array: {time1:.4f} seconds")
print(f"In-place: {time2:.4f} seconds")
print(f"Speedup: {time1/time2:.2f}x")

Multi-Dimensional Arrays and Broadcasting

All rounding functions work seamlessly with multi-dimensional arrays:

import numpy as np

# Financial data: prices across different products and time periods
prices = np.array([
    [10.234, 15.678, 20.123],
    [11.456, 16.789, 21.234],
    [12.567, 17.890, 22.345]
])

print("Original prices:")
print(prices)

print("\nRounded to cents:")
print(np.round(prices, decimals=2))

# Calculate price ranges (floor to ceil)
price_ranges = np.ceil(prices) - np.floor(prices)
print("\nPrice ranges:")
print(price_ranges)

# Round to nearest $5 for pricing tiers
pricing_tiers = np.round(prices / 5) * 5
print("\nPricing tiers ($5 increments):")
print(pricing_tiers)

Output:

Original prices:
[[10.234 15.678 20.123]
 [11.456 16.789 21.234]
 [12.567 17.89  22.345]]

Rounded to cents:
[[10.23 15.68 20.12]
 [11.46 16.79 21.23]
 [12.57 17.89 22.35]]

Price ranges:
[[1. 1. 1.]
 [1. 1. 1.]
 [1. 1. 1.]]

Pricing tiers ($5 increments):
[[10. 15. 20.]
 [10. 15. 20.]
 [15. 20. 20.]]

Edge Cases and Data Type Considerations

Rounding functions preserve array shape but may change data types:

import numpy as np

# Integer arrays remain integers with floor/ceil
int_arr = np.array([1, 2, 3], dtype=np.int32)
print("Integer floor:", np.floor(int_arr).dtype)  # float64

# Explicitly maintain integer type
result = np.floor(int_arr).astype(np.int32)
print("Cast back:", result.dtype)

# Handling NaN and Inf
special_values = np.array([np.nan, np.inf, -np.inf, 1.5])
print("\nSpecial values:", special_values)
print("Round:", np.round(special_values))
print("Floor:", np.floor(special_values))
print("Ceil:", np.ceil(special_values))

These functions form the foundation of numerical processing in NumPy. Use np.round() for general-purpose rounding with statistical properties, np.floor() for binning and indexing operations, and np.ceil() for capacity planning and resource allocation. Understanding their behavior with negative numbers, multi-dimensional arrays, and edge cases ensures robust numerical code.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.