
Image by Editor | Midjourney
When working with large datasets in data science and machine learning projects, memory management becomes a crucial concern. Efficient memory usage can significantly impact both the speed and scalability of your applications.
In this article, we’ll explore how to handle large arrays efficiently using NumPy, a foundational library for numerical computing in Python. Let’s dive in!
Why Memory Optimization Matters
Memory optimization refers to managing system resources carefully to minimize memory usage while maintaining peak performance. In Python, standard lists are relatively inefficient because they store each element as a separate object. In contrast, NumPy arrays store data in contiguous memory blocks with fixed data types, making them far more memory-efficient for handling large volumes of data.
By understanding NumPy’s memory model and leveraging smart techniques, you can optimize your code to work faster and at a larger scale.
How NumPy Stores Data
NumPy arrays are homogeneous, meaning every element shares the same data type and occupies the same amount of memory. This uniformity allows for efficient storage and fast access, unlike Python lists, where elements can vary in size and require more memory lookups.
NumPy also offers two memory layouts:
- C-order (Row-major): Data is stored row by row, ideal for row-wise operations (default in NumPy)
- Fortran-order (Column-major): Data is stored column by column, useful for column-wise operations or compatibility with Fortran-based tools
Choosing the right memory layout based on your workload can improve performance and memory access patterns.
Strategies for Handling Large Arrays
Handling large arrays requires a number of considerations beyond the norm. Here we list several strategies for this very scenario.
1. Use In-Place Operations Whenever Possible
Normally, when you perform operations like addition, multiplication, or transformation on NumPy arrays, new arrays are created — consuming extra memory temporarily. To avoid this, use in-place operations that modify the existing array without allocating new memory:
import numpy as np arr = np.array([1, 2, 3, 4]) arr += 5 # In-place addition, no new array created arr *= 2 # In-place multiplication
2. Choose the Right Data Types
Selecting an appropriate data type (dtype) is one of the easiest ways to reduce memory usage.
For example:
import numpy as np
# Create the same array with different dtypes
array_float64 = np.ones(1_000_000, dtype=np.float64)
array_int8 = np.ones(1_000_000, dtype=np.int8)
print(f"float64 array size: {array_float64.nbytes / 1e6:.2f} MB")
print(f"int8 array size: {array_int8.nbytes / 1e6:.2f} MB")
Choosing a smaller data type, like float32 instead of float64, or int8 when dealing with small integers, can dramatically cut down memory consumption.
3. Choose the Right Data Types
In NumPy, a view is a new array object that shares the same data buffer with the original array, while a copy creates a separate new array in memory.
import numpy as np # Original large array original_array = np.arange(10_000) # Create a view view_array = original_array[1000:2000] # Create a copy copy_array = original_array[1000:2000].copy() # Modify the view -- affects the original view_array[0] = 9999 print(original_array[1000]) # 9999 # Modify the copy -- does NOT affect the original copy_array[0] = 1111 print(original_array[1000]) # Still 9999
Modifying view_array also affects original_array, whereas changes to copy_array do not. Using views when possible saves memory by avoiding unnecessary duplication.
4. Chunk Your Computations (Batch Processing)
When working with very large arrays or datasets that can’t fit into RAM, you can process them in smaller batches or chunks instead of all at once.
Like, for instance:
import numpy as np
# Simulate a large array
large_array = np.arange(10_000_000, dtype=np.float32)
chunk_size = 1_000_000 # 1 million elements at a time
for start in range(0, large_array.shape[0], chunk_size):
end = start + chunk_size
chunk = large_array[start:end]
mean_value = np.mean(chunk) # Example processing
print(f"Chunk {start//chunk_size + 1}: Mean = {mean_value:.2f}")
This technique minimizes memory usage by loading and processing only small portions at a time, very useful for model training, data cleaning, or transformation pipelines.
5. Take Advantage of Broadcasting
Broadcasting allows NumPy to perform operations on arrays of different shapes without creating large temporary arrays.
import numpy as np # Small array array = np.array([10, 20, 30]) # Broadcast a scalar multiplication result = array * 5 print(result) # [50, 100, 150]
By reusing existing memory structures during operations, broadcasting helps minimize memory overhead.
6. Reduce Array Size with np.squeeze and np.compress
You can further optimize memory usage by reducing unnecessary dimensions or filtering data:
import numpy as np # Remove singleton dimensions array = np.array([[[1], [2], [3]]]) squeezed_array = np.squeeze(array) print(squeezed_array.shape) # (3,) # Filter array with a condition data = np.array([10, 20, 30, 40, 50]) condition = [0, 1, 0, 1, 0] filtered_data = np.compress(condition, data) print(filtered_data) # [20 40]
squeeze() eliminates dimensions of size one, while compress() creates smaller, filtered arrays, both saving memory.
7. Work with Large Data Using Memory Mapping (np.memmap)
When your datasets are too large to fit into RAM, memory mapping offers a solution. np.memmap lets you work with data stored on disk as if it were a NumPy array in memory.
import numpy as np # Create a memory-mapped array filename = 'large_array.dat' data = np.memmap(filename, dtype='float32', mode='w+', shape=(5_000_000,)) # Write some data data[1000:1010] = np.arange(10, dtype=np.float32) # Read from disk without loading all small_slice = data[1000:1010] print(small_slice) # Always flush changes! data.flush()
With memmap, you load only the necessary parts of the dataset into memory, keeping memory usage low while handling massive files.
Conclusions
Optimizing memory usage is critical when working with large arrays in data science projects. By carefully following the previously stated strategies, you can handle your numpy arrays in a more efficient way and get the most out of your local resources.
Mastering these techniques allows you to scale your projects further without being bottlenecked by memory limits — a vital skill for any serious data scientist or machine learning engineer.
As an additional step, you can check out this excellent tutorial about How to efficiently work with very large NumPy arrays (Google Colab). This hands-on notebook walks you through additional best practices like memory-mapping, efficient data types, and advanced tricks to scale your workflows.
