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:

  1. 0: select the first matrix.
  2. 1: select its second row.
  3. 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:

  1. They are equal, or
  2. 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.

Source:https://dev.to/iamismile/introduction-to-numpy-2k44

Related posts