Grouping array elements and calculating means for each group is a common operation in signal processing, data downsampling, and time series analysis. NumPy's reshape method provides an efficient, loop-free solution.
The Reshape Trick
Transform the 1D array into a 2D matrix with N columns, then calculate the mean across each row:
import numpy as np
arr = np.array([10, 20, 30, 40, 50, 60])
n = 3
averages = arr.reshape(-1, n).mean(axis=1)
print(averages) # [20. 50.]Step-by-Step Breakdown
import numpy as np
arr = np.array([10, 20, 30, 40, 50, 60])
n = 3
reshaped = arr.reshape(-1, n)
print("Reshaped:")
print(reshaped)
averages = reshaped.mean(axis=1)
print(f"\nAverages: {averages}") # [20. 50.]Handling Non-Divisible Arrays
When the array length isn't evenly divisible by N, you must decide how to handle the remainder.
Option 1: Trim Excess Elements
Discard elements that don't form a complete group:
import numpy as np
arr = np.array([1, 2, 3, 4, 5, 6, 7]) # 7 elements
n = 3
num_complete_groups = len(arr) // n
trim_length = num_complete_groups * n
trimmed = arr[:trim_length]
result = trimmed.reshape(-1, n).mean(axis=1)
print(f"Trimmed array: {trimmed}") # [1 2 3 4 5 6]
print(f"Averages: {result}") # [2. 5.]Option 2: Pad with NaN
Include remaining elements by padding with NaN and using nanmean:
import numpy as np
arr = np.array([1, 2, 3, 4, 5, 6, 7])
n = 3
remainder = len(arr) % n
if remainder != 0:
pad_size = n - remainder
arr_padded = np.pad(arr.astype(float), (0, pad_size),
constant_values=np.nan)
else:
arr_padded = arr.astype(float)
result = np.nanmean(arr_padded.reshape(-1, n), axis=1)
print(f"Padded array: {arr_padded}") # Padded array: [ 1. 2. 3. 4. 5. 6. 7. nan nan]
print(f"Averages: {result}") # Averages: [2. 5. 7.]Option 3: Pad with Zeros
If zeros are meaningful in your context:
import numpy as np
arr = np.array([1, 2, 3, 4, 5, 6, 7])
n = 3
remainder = len(arr) % n
if remainder != 0:
pad_size = n - remainder
arr_padded = np.pad(arr, (0, pad_size), constant_values=0)
else:
arr_padded = arr
result = arr_padded.reshape(-1, n).mean(axis=1)
print(f"Padded array: {arr_padded}") # Padded array: [1 2 3 4 5 6 7 0 0]
print(f"Averages: {result}") # Averages: [2. 5. 2.33333333]Reusable Function
Wrap the logic in a flexible function:
import numpy as np
def average_every_n(arr, n, handle_remainder='trim'):
"""
Average every n consecutive elements.
Parameters:
arr: Input array
n: Group size
handle_remainder: 'trim', 'pad_nan', or 'pad_zero'
"""
arr = np.asarray(arr)
remainder = len(arr) % n
if remainder == 0:
return arr.reshape(-1, n).mean(axis=1)
if handle_remainder == 'trim':
trim_length = (len(arr) // n) * n
return arr[:trim_length].reshape(-1, n).mean(axis=1)
elif handle_remainder == 'pad_nan':
pad_size = n - remainder
padded = np.pad(arr.astype(float), (0, pad_size),
constant_values=np.nan)
return np.nanmean(padded.reshape(-1, n), axis=1)
elif handle_remainder == 'pad_zero':
pad_size = n - remainder
padded = np.pad(arr, (0, pad_size), constant_values=0)
return padded.reshape(-1, n).mean(axis=1)
arr = np.array([1, 2, 3, 4, 5, 6, 7])
print(average_every_n(arr, 3, 'trim')) # [2. 5.]
print(average_every_n(arr, 3, 'pad_nan')) # [2. 5. 7.]
print(average_every_n(arr, 3, 'pad_zero')) # [2. 5. 2.33333333]Other Aggregations
The same reshape pattern works with other operations:
import numpy as np
arr = np.array([10, 20, 30, 40, 50, 60])
n = 3
reshaped = arr.reshape(-1, n)
sums = reshaped.sum(axis=1)
print(f"Sums: {sums}") # [60 150]
maxes = reshaped.max(axis=1)
print(f"Maxes: {maxes}") # [30 60]
mins = reshaped.min(axis=1)
print(f"Mins: {mins}") # [10 40]
stds = reshaped.std(axis=1)
print(f"Stds: {stds}") # [8.16... 8.16...]Output:
Sums: [ 60 150]
Maxes: [30 60]
Mins: [10 40]
Stds: [8.16496581 8.16496581]2D Array: Average Every N Rows
Extend the concept to average groups of rows:
import numpy as np
matrix = np.array([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
[10, 11, 12],
[13, 14, 15],
[16, 17, 18]
])
n = 2 # Average every 2 rows
result = matrix.reshape(-1, n, matrix.shape[1]).mean(axis=1)
print(result)Output:
[[ 2.5 3.5 4.5]
[ 8.5 9.5 10.5]
[14.5 15.5 16.5]]Practical Example: Signal Downsampling
import numpy as np
np.random.seed(42)
signal = np.sin(np.linspace(0, 4*np.pi, 1000)) + np.random.normal(0, 0.1, 1000)
downsampling_factor = 10
downsampled = signal.reshape(-1, downsampling_factor).mean(axis=1)
print(f"Original length: {len(signal)}") # 1000
print(f"Downsampled length: {len(downsampled)}") # 100Output:
Original length: 1000
Downsampled length: 100Performance Comparison
The reshape method is significantly faster than Python loops:
import numpy as np
import timeit
arr = np.random.rand(1_000_000)
n = 100
def numpy_method():
return arr.reshape(-1, n).mean(axis=1)
def loop_method():
return np.array([arr[i:i+n].mean() for i in range(0, len(arr), n)])
numpy_time = timeit.timeit(numpy_method, number=100)
loop_time = timeit.timeit(loop_method, number=100)
print(f"NumPy reshape: {numpy_time:.4f}s")
print(f"Python loop: {loop_time:.4f}s")
print(f"Speedup: {loop_time/numpy_time:.1f}x")Output:
NumPy reshape: 0.0469s
Python loop: 4.3649s
Speedup: 93.1xSummary
| Step | Operation | Purpose |
|---|---|---|
| 1 | arr.reshape(-1, n) |
Group into n-element rows |
| 2 | .mean(axis=1) |
Average each group |
| 3 | Trim or pad (if needed) | Handle non-divisible lengths |
Use reshape(-1, n).mean(axis=1) for fast, vectorized binned averages. This pattern extends to other aggregations like sum(), max(), min(), and std(), making it a versatile tool for data downsampling and grouped statistics.