Learn NumPy from the ground up through mental models, practical examples, array operations, broadcasting, aggregation, and linear algebra.
NumPy is one of the most important libraries in Python for numerical computing. It provides an efficient way to store, organize, and perform mathematical operations on large collections of numbers.
If you are learning Machine Learning, NumPy is especially important because data, features, weights, predictions, and many mathematical operations are represented using arrays.
In this tutorial, we’ll build an understanding of NumPy step by step. We’ll start with the idea of arrays, learn how to work with their shapes and values, and gradually move toward broadcasting, aggregation, and matrix multiplication.
The goal is not just to memorize NumPy syntax. It’s to understand how NumPy organizes data and why its operations work.
1. Why Do We Need NumPy?
Python already provides lists for storing multiple values.
For example:
numbers = [10, 20, 30, 40, 50] We can access individual elements, loop through them, and perform calculations. However, numerical computing often requires operations on thousands or millions of values, and regular Python loops become inefficient and inconvenient.
Consider adding 10 to every number.
Using a Python list:
numbers = [10, 20, 30, 40, 50] result = [] for number in numbers: result.append(number + 10) print(result) # [20, 30, 40, 50, 60] Using NumPy:
import numpy as np numbers = np.array([10, 20, 30, 40, 50]) result = numbers + 10 print(result) # [20 30 40 50 60] NumPy performs the same operation directly on the entire array. This is possible because NumPy supports vectorized operations: applying an operation to an entire array without writing an explicit Python loop.
Why NumPy?
- Performance: Many NumPy operations are implemented in optimized, compiled code.
- Convenience: Mathematical operations can be performed directly on entire arrays.
- Multidimensional data: NumPy supports vectors, matrices, and higher-dimensional arrays.
- Ecosystem: NumPy is a foundation for libraries such as Pandas, SciPy, and scikit-learn.
Importing NumPy
import numpy as np The alias np is the widely used convention in Python.
2. Understanding NumPy Arrays
The central object in NumPy is the ndarray, short for N-dimensional array. Think of an array as a container that organizes values into dimensions. A dimension describes an axis along which data is organized.
2.1 One-Dimensional Array: A Vector
A one-dimensional array is a sequence of values.
a1 = np.array([1, 2, 3]) Visualize it as:
[1, 2, 3] It has one dimension because the values are arranged along a single axis.
a1.ndim # 1 a1.shape # (3,) What does (3,) mean? It means the array has one dimension containing three elements. Notice that it is not (1, 3):
-
(3,)is a one-dimensional array. -
(1, 3)is a two-dimensional array with one row and three columns.
This distinction becomes important when working with Machine Learning models.
2.2 Two-Dimensional Array: A Matrix
A two-dimensional array organizes values into rows and columns.
a2 = np.array([ [1, 2, 3], [4, 5, 6] ]) Visualize it:
Columns 0 1 2 ┌─────────── Row 0 │ 1 2 3 Row 1 │ 4 5 6 Check its properties:
a2.ndim # 2 a2.shape # (2, 3) For a two-dimensional array, shape = (rows, columns). So (2, 3) means 2 rows, 3 columns, and 6 total elements.
2.3 Three-Dimensional Array: A Tensor
A three-dimensional array can be understood as multiple matrices stacked together.
a3 = np.array([ [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ], [ [10, 11, 12], [13, 14, 15], [16, 17, 18] ] ]) Visualize it as two matrices:
Matrix 0 Matrix 1 1 2 3 10 11 12 4 5 6 13 14 15 7 8 9 16 17 18 a3.ndim # 3 a3.shape # (2, 3, 3) Read the shape from left to right: 2 matrices, 3 rows in each matrix, 3 columns in each row.
2.4 Higher-Dimensional Arrays
NumPy supports arrays with more than three dimensions.
a4 = np.random.randint(0, 10, size=(2, 3, 4, 5)) a4.shape # (2, 3, 4, 5) a4.ndim # 4 Don’t try to memorize a special meaning for every dimension count. Instead, learn to read the shape one dimension at a time.
Understanding Dimensions at a Glance
| Array | Shape | Meaning |
|---|---|---|
| 1D | (3,) | 3 values |
| 2D | (2, 3) | 2 rows, 3 columns |
| 3D | (2, 3, 3) | 2 matrices, each 3 × 3 |
| 4D | (2, 3, 4, 5) | 4 dimensions with those sizes |
Mental model: An array’s shape tells you how its values are organized. Its number of dimensions tells you how many axes it has.
3. Understanding Array Attributes
Every NumPy array provides attributes that describe its structure and data. These four are particularly important:
| Attribute | Meaning |
|---|---|
.shape | Size of the array along each dimension |
.ndim | Number of dimensions |
.size | Total number of elements |
.dtype | Data type of the elements |
Example:
a = np.array([ [1, 2, 3], [4, 5, 6] ]) print(a.shape) # (2, 3) print(a.ndim) # 2 print(a.size) # 6 print(a.dtype) # int64 Why Does dtype Matter?
Unlike a regular Python list, a NumPy array generally stores its elements using a single, consistent data type.
a = np.array([1, 2, 3]) print(a.dtype) # int64 (commonly) b = np.array([1.5, 2.5, 3.5]) print(b.dtype) # float64 (commonly) If we mix integers and floating-point values, NumPy converts everything to floats so the array can use one data type:
c = np.array([1, 2.5, 3]) print(c) # [1. 2.5 3. ] print(c.dtype) # float64 Small integer types need extra care. They wrap around silently when a result is too large:
x = np.array([250], dtype=np.uint8) x + 10 # array([4], dtype=uint8) <- overflow wrapped around, no error This matters in practice because images are often stored as uint8. Convert with .astype(...) before doing arithmetic:
(x.astype(np.int64) + 10) # array([260]) A Useful Habit
Whenever you encounter an unfamiliar array, inspect these four attributes:
print('Shape:', a.shape) print('Dimensions:', a.ndim) print('Elements:', a.size) print('Data type:', a.dtype) Understanding these properties will help you debug many Machine Learning problems, especially shape mismatches.
4. Creating NumPy Arrays
NumPy provides several functions for creating arrays without manually typing every value.
4.1 Creating Arrays from Python Lists
The most common way to create an array is with np.array().
a = np.array([1, 2, 3]) b = np.array([ [1, 2], [3, 4] ]) You can create arrays from lists or tuples.
4.2 Creating Arrays Filled with Zeros or Ones
| Function | Description |
|---|---|
np.zeros(shape) | Creates an array filled with zeros |
np.ones(shape) | Creates an array filled with ones |
zeros = np.zeros((2, 3)) print(zeros) # [[0. 0. 0.] # [0. 0. 0.]] ones = np.ones((2, 3)) print(ones) # [[1. 1. 1.] # [1. 1. 1.]] 4.3 Creating a Sequence of Numbers
np.arange() creates a sequence of numbers.
a = np.arange(0, 10, 2) print(a) # [0 2 4 6 8] Its arguments are np.arange(start, stop, step):
-
start: where the sequence begins. -
stop: where the sequence ends, excluded. -
step: the difference between consecutive values.
np.arange(1, 6) # [1 2 3 4 5] When you want a specific number of points instead of a step size, use np.linspace(). Its end point is included:
np.linspace(0, 1, 5) # [0. 0.25 0.5 0.75 1. ] 4.4 Creating Random Arrays
NumPy provides functions for generating random numbers.
# Random integers from 0 to 9 random_ints = np.random.randint(0, 10, size=(3, 4)) # Random floating-point numbers between 0 and 1 random_floats = np.random.random((2, 3)) The upper limit in randint() is exclusive, so np.random.randint(0, 10, size=5) generates five integers from 0 through 9.
4.5 Reproducible Random Numbers
Computers usually generate pseudo-random numbers using algorithms. The values appear random, but they come from a deterministic process. A random seed controls the starting point of that process.
np.random.seed(42) print(np.random.randint(0, 10, size=5)) # [6 3 7 4 6] Running the same code again with the same seed produces the same sequence. This is useful in Machine Learning because it makes experiments reproducible.
Modern alternative: NumPy also provides the recommended default_rng() interface.
rng = np.random.default_rng(42) random_ints = rng.integers(0, 10, size=5) random_floats = rng.random((2, 3)) You can reuse rng to generate more random values.
Watch out: the same seed gives different numbers in the old (np.random.seed) and new (default_rng) interfaces. If your output doesn’t match a tutorial’s, check which one it uses.
Remember: use a fixed seed when you want repeatable experiments. Leave it out when you want different random samples on each run.
5. Accessing Array Data: Indexing and Slicing
Creating arrays is only the beginning. We also need to access specific values or select parts of an array.
- Indexing: accessing individual elements.
- Slicing: selecting a range or portion of an array.
5.1 Indexing
NumPy uses zero-based indexing, just like Python lists.
a = np.array([10, 20, 30, 40]) print(a[0]) # 10 print(a[2]) # 30 Negative indices count from the end:
print(a[-1]) # 40 print(a[-2]) # 30 Indexing a 2D Array
For a two-dimensional array, use array[row_index, column_index].
a = np.array([ [10, 20, 30], [40, 50, 60] ]) print(a[0, 1]) # 20 print(a[1, 2]) # 60 Read a[0, 1] as: select row 0, then select column 1.
Indexing a 3D Array
For a three-dimensional array, you need three indices, one per axis.
print(a3[0, 1, 2]) # 6 Read it dimension by dimension:
-
0: select the first matrix. -
1: select its second row. -
2: select its third column.
The result is 6.
5.2 Slicing
Slicing selects a range of elements. The general syntax is array[start:stop:step]:
-
start: starting index (included). -
stop: ending index (excluded). -
step: how many positions to move each time.
a = np.array([10, 20, 30, 40, 50]) print(a[1:4]) # [20 30 40] Index 1 is included, but index 4 is excluded. You can also leave out values:
a[:3] # [10 20 30] a[2:] # [30 40 50] a[:] # [10 20 30 40 50] a[::2] # [10 30 50] a[::-1] # [50 40 30 20 10] 5.3 Multidimensional Slicing
For multidimensional arrays, each dimension gets its own slice.
a = np.array([ [1, 2, 3], [4, 5, 6], [7, 8, 9] ]) print(a[:2, :2]) # [[1 2] # [4 5]] Read it dimension by dimension: :2 selects the first two rows, and :2 selects the first two columns.
a[:, 1] # [2 5 8] This selects every row but only column 1. For a 3D array, a3[:2, :2, :2] selects the first two elements along each of the three dimensions.
Important: read multidimensional slicing one dimension at a time. It is much easier than trying to understand the entire expression at once.
5.4 Views vs. Copies
A basic slice does not copy the data. It returns a view that shares memory with the original array.
a = np.array([10, 20, 30, 40]) b = a[1:3] b[0] = 999 print(a) # [ 10 999 30 40] <- the original changed! If you need an independent copy, ask for one explicitly:
b = a[1:3].copy() The rule of thumb: basic slicing always returns a view, while boolean and fancy indexing (such as a[a > 2] or a[[0, 2]]) return copies. When in doubt, call .copy().
5.5 Finding Unique Values
np.unique() returns the unique values in an array.
arr = np.array([1, 2, 2, 3, 3, 3]) print(np.unique(arr)) # [1 2 3] This is useful when exploring datasets and checking which distinct values appear in a feature.
6. Array Operations and Broadcasting
One of NumPy’s biggest advantages is that we can perform mathematical operations directly on entire arrays.
6.1 Element-wise Arithmetic
NumPy performs arithmetic operations element by element when the array shapes are compatible.
a = np.array([1, 2, 3]) b = np.array([4, 5, 6]) a + b # [5 7 9] a - b # [-3 -3 -3] a * b # [4 10 18] a / b # [0.25 0.4 0.5 ] a ** 2 # [1 4 9] a % 2 # [1 0 1] Notice what happens with multiplication:
a * b # [1*4, 2*5, 3*6] # [4, 10, 18] This is element-wise multiplication. It is not matrix multiplication.
Other Arithmetic Operations
a // b # [0 0 0] b // a # [4 2 2] // performs floor division, returning the floor of each division result.
6.2 Universal Functions (ufuncs)
NumPy provides optimized mathematical functions called universal functions, or ufuncs. They operate element by element on arrays.
a = np.array([1, 2, 3]) np.add(a, 5) # [6 7 8] np.sqrt(a) # [1. 1.41421356 1.73205081] np.exp(a) # [ 2.71828183 7.3890561 20.08553692] np.log(a) # [0. 0.69314718 1.09861229] You can use these functions instead of writing loops to perform mathematical operations on every element.
6.3 Broadcasting
Broadcasting allows NumPy to perform operations on arrays with different shapes, as long as their dimensions are compatible. Think of it as NumPy logically expanding a smaller array so that it can work with a larger one. The values are not necessarily copied or physically repeated in memory.
a = np.array([1, 2, 3]) print(a + 5) # [6 7 8] Conceptually, NumPy treats the scalar 5 as if it were [5, 5, 5], then performs element-wise addition:
[1, 2, 3] [5, 5, 5] ----------- [6, 7, 8] Broadcasting with Matrices
Now consider a matrix and a one-dimensional array.
a = np.array([ [1, 2, 3], [4, 5, 6] ]) # shape: (2, 3) b = np.array([10, 20, 30]) # shape: (3,) print(a + b) # [[11 22 33] # [14 25 36]] NumPy conceptually applies b to each row:
A: B: Result: [ 1 2 3 ] [10 20 30] [11 22 33] [ 4 5 6 ] [10 20 30] [14 25 36] The Broadcasting Golden Rule
Compare the shapes from right to left. Two dimensions are compatible when:
- They are equal, or
- At least one of them is
1.
If one shape has fewer dimensions, imagine adding dimensions of size 1 to its left until both shapes have the same number of dimensions.
| Shape A | Shape B | Compatible? | Reason |
|---|---|---|---|
(2, 3) | (2, 3) | Yes | Both dimensions match |
(2, 3) | (1, 3) | Yes | 1 can broadcast to 2 |
(2, 3) | (3,) | Yes | Shapes align as (2, 3) and (1, 3) |
(2, 3) | (2, 1) | Yes | 1 can broadcast to 3 |
(2, 3) | (2, 4) | No | 3 and 4 do not match |
(2, 3) | (2,) | No | Aligns as (1, 2), and 3 vs 2 do not match |
An Incompatible Example
a = np.ones((2, 3)) b = np.ones((2, 4)) a + b # ValueError: operands could not be broadcast together Compare from right to left:
A: (2, 3) B: (2, 4) ↑ 3 vs 4 <- neither equal nor 1 Remember: Compare dimensions from right to left. Equal dimensions or a dimension of 1 are compatible.
7. Aggregation
Aggregation means combining multiple values into a summary. For example, instead of looking at every student’s score individually, we might calculate the average score of the class.