Averaging consecutive groups of elements is essential for downsampling signals, reducing data resolution, and creating binned statistics. NumPy's reshape method provides an efficient, vectorized solution without Python loops.
The Reshape Trick
Transform the 1D array into a 2D matrix where each row contains k consecutive elements, then compute the mean of each row:
import numpy as np
arr = np.array([10, 20, 30, 40, 50, 60])
k = 3
result = arr.reshape(-1, k).mean(axis=1)
print(result) # [20. 50.]Step-by-Step Breakdown
import numpy as np
arr = np.array([10, 20, 30, 40, 50, 60])
k = 3
reshaped = arr.reshape(-1, k)
print("Reshaped:")
print(reshaped)
averages = reshaped.mean(axis=1)
print(f"\nAverages: {averages}") # [20. 50.]Output:
Reshaped:
[[10 20 30]
[40 50 60]]
Averages: [20. 50.]Handling Non-Divisible Arrays
When the array length isn't evenly divisible by k, you need a strategy for the remaining elements.
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, 8])
k = 3
remainder = len(arr) % k
if remainder:
arr_trimmed = arr[:-remainder]
else:
arr_trimmed = arr
result = arr_trimmed.reshape(-1, k).mean(axis=1)
print(f"Trimmed array: {arr_trimmed}") # Trimmed array: [1 2 3 4 5 6]
print(f"Averages: {result}") # Averages: [2. 5.]Option 2: Pad with NaN and Use nanmean
Include all elements by padding with NaN:
import numpy as np
arr = np.array([1, 2, 3, 4, 5, 6, 7, 8])
k = 3
remainder = len(arr) % k
if remainder:
pad_size = k - 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, k), axis=1)
print(f"Padded: {arr_padded}") # Padded: [ 1. 2. 3. 4. 5. 6. 7. 8. nan]
print(f"Averages: {result}") # Averages: [2. 5. 7.5]Option 3: Pad with Zeros
When zeros are meaningful in your context:
import numpy as np
arr = np.array([1, 2, 3, 4, 5, 6, 7, 8])
k = 3
remainder = len(arr) % k
if remainder:
pad_size = k - remainder
arr_padded = np.pad(arr, (0, pad_size), constant_values=0)
else:
arr_padded = arr
result = arr_padded.reshape(-1, k).mean(axis=1)
print(f"Padded: {arr_padded}") # Padded: [1 2 3 4 5 6 7 8 0]
print(f"Averages: {result}") # Averages: [2. 5. 5.]Handling Missing Data (NaN Values)
When your array already contains NaN values, use np.nanmean():
import numpy as np
arr = np.array([1.0, 2.0, np.nan, 4.0, 5.0, 6.0])
k = 3
result = np.nanmean(arr.reshape(-1, k), axis=1)
print(result) # [1.5 5. ]Reusable Function
Wrap the logic in a flexible function:
import numpy as np
def average_consecutive(arr, k, handle_remainder='trim'):
"""
Average every k consecutive elements.
Parameters:
arr: Input array
k: Group size
handle_remainder: 'trim', 'pad_nan', or 'pad_zero'
Returns:
Array of group averages
"""
arr = np.asarray(arr, dtype=float)
remainder = len(arr) % k
if remainder == 0:
return arr.reshape(-1, k).mean(axis=1)
if handle_remainder == 'trim':
return arr[:-remainder].reshape(-1, k).mean(axis=1)
elif handle_remainder == 'pad_nan':
pad_size = k - remainder
padded = np.pad(arr, (0, pad_size), constant_values=np.nan)
return np.nanmean(padded.reshape(-1, k), axis=1)
elif handle_remainder == 'pad_zero':
pad_size = k - remainder
padded = np.pad(arr, (0, pad_size), constant_values=0)
return padded.reshape(-1, k).mean(axis=1)
raise ValueError(f"Unknown remainder handling: {handle_remainder}")
arr = np.array([1, 2, 3, 4, 5, 6, 7, 8])
print(average_consecutive(arr, 3, 'trim')) # [2. 5.]
print(average_consecutive(arr, 3, 'pad_nan')) # [2. 5. 7.5]
print(average_consecutive(arr, 3, 'pad_zero')) # [2. 5. 5.]Other Aggregations
The same reshape pattern works with other reduction operations:
import numpy as np
arr = np.array([10, 20, 30, 40, 50, 60])
k = 3
reshaped = arr.reshape(-1, k)
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.16496581 8.16496581]
medians = np.median(reshaped, axis=1)
print(f"Medians: {medians}") # [20. 50.]Output:
Sums: [ 60 150]
Maxes: [30 60]
Mins: [10 40]
Stds: [8.16496581 8.16496581]
Medians: [20. 50.]2D Arrays: Average Consecutive Rows
Extend the concept to average groups of rows in a matrix:
import numpy as np
matrix = np.arange(24).reshape(6, 4)
print("Original:")
print(matrix)
k = 2 # Average every 2 rows
result = matrix.reshape(-1, k, matrix.shape[1]).mean(axis=1)
print("\nAveraged (every 2 rows):")
print(result)Output:
Original:
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]
[12 13 14 15]
[16 17 18 19]
[20 21 22 23]]
Averaged (every 2 rows):
[[ 2. 3. 4. 5.]
[10. 11. 12. 13.]
[18. 19. 20. 21.]]Practical Example: Signal Downsampling
import numpy as np
np.random.seed(42)
time = np.linspace(0, 1, 1000)
signal = np.sin(2 * np.pi * 5 * time) + np.random.normal(0, 0.1, 1000)
k = 10
downsampled = signal.reshape(-1, k).mean(axis=1)
downsampled_time = time[::k]
print(f"Original: {len(signal)} samples") # 1000
print(f"Downsampled: {len(downsampled)} samples") # 100Performance Comparison
The reshape method significantly outperforms Python loops:
import numpy as np
import timeit
arr = np.random.rand(1_000_000)
k = 100
def numpy_method():
return arr.reshape(-1, k).mean(axis=1)
def loop_method():
return np.array([arr[i:i+k].mean() for i in range(0, len(arr), k)])
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.1483s
Python loop: 12.2428s
Speedup: 82.5xSummary
| Step | Code | Purpose |
|---|---|---|
| Group elements | arr.reshape(-1, k) |
Create rows of k elements |
| Calculate average | .mean(axis=1) |
Average each group |
| Handle remainder | Trim or pad | Deal with non-divisible lengths |
| Handle NaN | np.nanmean() |
Ignore missing values |
Use reshape(-1, k).mean(axis=1) for efficient, vectorized averaging of consecutive subarrays. This pattern extends naturally to other aggregations like sum(), max(), min(), and std(), making it a versatile tool for signal processing and data downsampling.