DEV Community

Cover image for Introduction to NumPy
Ismile Hossain
Ismile Hossain

Posted on Edited on

Introduction to NumPy

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]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

Using NumPy:

import numpy as np

numbers = np.array([10, 20, 30, 40, 50])
result = numbers + 10

print(result)
# [20 30 40 50 60]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode

Visualize it as:

[1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

It has one dimension because the values are arranged along a single axis.

a1.ndim
# 1

a1.shape
# (3,)
Enter fullscreen mode Exit fullscreen mode

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]
])
Enter fullscreen mode Exit fullscreen mode

Visualize it:

          Columns
          0   1   2
        ┌───────────
Row 0   │ 1   2   3
Row 1   │ 4   5   6
Enter fullscreen mode Exit fullscreen mode

Check its properties:

a2.ndim
# 2

a2.shape
# (2, 3)
Enter fullscreen mode Exit fullscreen mode

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]
    ]
])
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
a3.ndim
# 3

a3.shape
# (2, 3, 3)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

This matters in practice because images are often stored as uint8. Convert with .astype(...) before doing arithmetic:

(x.astype(np.int64) + 10)
# array([260])
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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]
])
Enter fullscreen mode Exit fullscreen mode

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.]]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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.  ]
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Negative indices count from the end:

print(a[-1])
# 40

print(a[-2])
# 30
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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]]
Enter fullscreen mode Exit fullscreen mode

Read it dimension by dimension: :2 selects the first two rows, and :2 selects the first two columns.

a[:, 1]
# [2 5 8]
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

If you need an independent copy, ask for one explicitly:

b = a[1:3].copy()
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

Notice what happens with multiplication:

a * b
# [1*4, 2*5, 3*6]
# [4, 10, 18]
Enter fullscreen mode Exit fullscreen mode

This is element-wise multiplication. It is not matrix multiplication.

Other Arithmetic Operations

a // b
# [0 0 0]

b // a
# [4 2 2]
Enter fullscreen mode Exit fullscreen mode

// 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]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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]]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Compare from right to left:

A: (2, 3)
B: (2, 4)
      ↑
    3 vs 4   <- neither equal nor 1
Enter fullscreen mode Exit fullscreen mode

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.

arr = np.array([
    [1, 2, 3],
    [4, 5, 6]
])

np.sum(arr)
# 21

np.mean(arr)
# 3.5

np.max(arr)
# 6

np.min(arr)
# 1

np.std(arr)
# Standard deviation

np.var(arr)
# Variance
Enter fullscreen mode Exit fullscreen mode

Common Aggregation Functions

Function Purpose
np.sum() Adds all values
np.mean() Calculates the average
np.max() Finds the maximum value
np.min() Finds the minimum value
np.std() Calculates standard deviation
np.var() Calculates variance

7.1 Mean, Variance, and Standard Deviation

These three concepts are particularly important in statistics and Machine Learning.

  • Mean: The average value. It tells us where the center of the data is.

    Mean = (sum of values) / (number of values)
    
  • Variance: Measures how spread out the values are around the mean (the average squared distance from the mean). A larger variance generally means the values are more spread out.

  • Standard deviation: the square root of variance. It expresses the spread in the same unit as the original data.

Let’s compare two datasets:

high_var = np.array([1, 100, 200, 300, 4000, 5000])
low_var = np.array([2, 4, 6, 8, 10])

print(np.var(high_var))   # very large (in the millions)
print(np.var(low_var))    # 8.0

print(np.std(high_var))   # very large (in the thousands)
print(np.std(low_var))    # about 2.83
Enter fullscreen mode Exit fullscreen mode

The first dataset has much greater variation than the second. Its mean is:

np.mean(high_var)
# about 1600.17
Enter fullscreen mode Exit fullscreen mode

The values are spread widely around that mean.

7.2 Visualizing Variance with Histograms

A histogram helps us understand how values are distributed.

import matplotlib.pyplot as plt

plt.hist(high_var)
plt.title('High Variance')
plt.show()

plt.hist(low_var)
plt.title('Low Variance')
plt.show()
Enter fullscreen mode Exit fullscreen mode

The histogram gives us a visual way to compare how spread out the values are.

7.3 Understanding axis

One of the most confusing NumPy concepts for beginners is axis. Let’s use this matrix:

arr = np.array([
    [1, 2, 3],
    [4, 5, 6]
])

arr.shape
# (2, 3)
Enter fullscreen mode Exit fullscreen mode

It has two axes: axis=0 is the row dimension, and axis=1 is the column dimension. But what does that mean when performing an aggregation? Think of axis as the dimension that NumPy collapses when performing the operation.

Without specifying an axis, NumPy adds every element:

np.sum(arr)
# 21
Enter fullscreen mode Exit fullscreen mode

Using axis=0, NumPy adds values vertically, column by column. The result contains one sum for each column.

np.sum(arr, axis=0)
# [5 7 9]
Enter fullscreen mode Exit fullscreen mode
1 + 4 = 5
2 + 5 = 7
3 + 6 = 9
Enter fullscreen mode Exit fullscreen mode

Using axis=1, NumPy adds values horizontally, row by row. The result contains one sum for each row.

np.sum(arr, axis=1)
# [6 15]
Enter fullscreen mode Exit fullscreen mode
1 + 2 + 3 = 6
4 + 5 + 6 = 15
Enter fullscreen mode Exit fullscreen mode

Visualizing axis

Original array:

          Column 0  Column 1  Column 2
Row 0         1         2         3
Row 1         4         5         6

axis=0: collapse rows (sum vertically)

              1+4       2+5       3+6
Result:       [5,        7,        9]

axis=1: collapse columns (sum horizontally)

              1+2+3     4+5+6
Result:       [6,        15]
Enter fullscreen mode Exit fullscreen mode

A Simple Way to Remember

  • axis=0: move down the rows and combine values in each column.
  • axis=1: move across the columns and combine values in each row.

The same concept works with other aggregation functions:

np.mean(arr, axis=0)
# [2.5 3.5 4.5]

np.max(arr, axis=1)
# [3 6]

np.min(arr, axis=0)
# [1 2 3]
Enter fullscreen mode Exit fullscreen mode

Why is axis important in Machine Learning?

Imagine a dataset where rows represent samples and columns represent features. You may want the average of each feature across all samples:

np.mean(data, axis=0)
Enter fullscreen mode Exit fullscreen mode

Understanding axis will make data preprocessing and numerical operations much easier.


8. Reshaping, Transposing, and Combining Arrays

Sometimes an array contains the correct values, but its shape is not suitable for the operation we want to perform. NumPy provides these tools:

  • reshape(): changes how values are organized.
  • .T: transposes an array by reversing its axes.
  • np.concatenate() and np.stack(): join arrays together.

8.1 Reshaping

Reshaping changes an array’s shape without changing the total number of elements.

a = np.array([1, 2, 3, 4, 5, 6])

print(a.shape)
# (6,)
Enter fullscreen mode Exit fullscreen mode

We can reshape it into two rows and three columns:

b = a.reshape(2, 3)

print(b)
# [[1 2 3]
#  [4 5 6]]

print(b.shape)
# (2, 3)
Enter fullscreen mode Exit fullscreen mode

Or into three rows and two columns, or even a three-dimensional array:

a.reshape(3, 2)
# [[1 2]
#  [3 4]
#  [5 6]]

a.reshape(2, 3, 1)
Enter fullscreen mode Exit fullscreen mode

The Reshaping Golden Rule

The total number of elements must remain the same.

a = np.array([1, 2, 3, 4, 5, 6])

a.reshape(2, 3)  # Valid: 2 × 3 = 6
a.reshape(3, 2)  # Valid: 3 × 2 = 6
a.reshape(2, 4)  # Invalid: 2 × 4 = 8
Enter fullscreen mode Exit fullscreen mode

The last operation raises a ValueError because the requested shape needs eight elements, but the original array contains only six.

You can also use -1 when you want NumPy to infer one dimension:

a.reshape(2, -1)
# [[1 2 3]
#  [4 5 6]]
Enter fullscreen mode Exit fullscreen mode

8.2 Transposing

Transposing changes the order of an array’s axes. For a two-dimensional matrix, .T swaps rows and columns.

a = np.array([
    [1, 2, 3],
    [4, 5, 6]
])

print(a.shape)
# (2, 3)

print(a.T)
# [[1 4]
#  [2 5]
#  [3 6]]

print(a.T.shape)
# (3, 2)
Enter fullscreen mode Exit fullscreen mode
Before:          After:
1  2  3          1  4
4  5  6          2  5
                 3  6
Enter fullscreen mode Exit fullscreen mode

Important: for a 1D NumPy array, .T does not turn it into a column vector. It remains one-dimensional.

a = np.array([1, 2, 3])

a.T.shape
# (3,)
Enter fullscreen mode Exit fullscreen mode

If you need a two-dimensional column vector:

a.reshape(-1, 1)
# [[1]
#  [2]
#  [3]]
Enter fullscreen mode Exit fullscreen mode

Transpose becomes especially useful when working with matrix multiplication and Machine Learning data.

8.3 Combining Arrays

x = np.array([1, 2, 3])
y = np.array([4, 5, 6])

np.concatenate([x, y])
# [1 2 3 4 5 6]      shape: (6,)

np.stack([x, y])
# [[1 2 3]
#  [4 5 6]]          shape: (2, 3)
Enter fullscreen mode Exit fullscreen mode

concatenate joins arrays along an existing axis. stack creates a new axis.


9. Dot Product and Matrix Multiplication

Before learning matrix multiplication, we need to understand the difference between element-wise multiplication and the dot product. These operations are not the same.

9.1 Dot Product of Two Vectors

A dot product takes two vectors, multiplies their corresponding elements, and adds the results together.

[a, b, c] · [x, y, z] = a·x + b·y + c·z
Enter fullscreen mode Exit fullscreen mode
A = np.array([1, 2, 3])
B = np.array([4, 5, 6])

result = np.dot(A, B)

print(result)
# 32
Enter fullscreen mode Exit fullscreen mode

The calculation is:

(1 × 4) + (2 × 5) + (3 × 6)
= 4 + 10 + 18
= 32
Enter fullscreen mode Exit fullscreen mode

9.2 * vs np.dot()

This is a very common beginner mistake.

Element-wise multiplication (*):

a = np.array([1, 2, 3])
b = np.array([4, 5, 6])

print(a * b)
# [ 4 10 18]
Enter fullscreen mode Exit fullscreen mode

Each element is multiplied by its corresponding element.

Dot product (np.dot()):

print(np.dot(a, b))
# 32
Enter fullscreen mode Exit fullscreen mode

The corresponding products are added together.

Operation Result
a * b [4, 10, 18]
np.dot(a, b) 32

Remember:

  • means element-wise multiplication.
  • np.dot(a, b) calculates the dot product for vectors and performs matrix multiplication for two 2D arrays.
  • @ is Python's matrix multiplication operator.

9.3 Matrix Multiplication

Matrix multiplication multiplies the rows of one matrix by the columns of another and adds the products together. Unlike element-wise multiplication, it has a shape compatibility rule. Suppose matrix A has shape (m, n) and matrix B has shape (n, p). Their inner dimensions must match:

(m × n) @ (n × p) = (m × p)
Enter fullscreen mode Exit fullscreen mode

The inner dimensions are n and n, which must be equal. The result has the outer dimensions: m × p.

Example

A = np.array([
    [1, 2, 3],
    [4, 5, 6]
])

B = np.array([
    [7, 8],
    [9, 10],
    [11, 12]
])

result = A @ B

print(result)
# [[ 58  64]
#  [139 154]]
Enter fullscreen mode Exit fullscreen mode

The shapes are:

A.shape
# (2, 3)

B.shape
# (3, 2)
Enter fullscreen mode Exit fullscreen mode
(2, 3) @ (3, 2) = (2, 2)
Enter fullscreen mode Exit fullscreen mode

The inner dimensions match (3 == 3), and the resulting matrix has two rows and two columns.

Why Does Matrix Multiplication Work This Way?

Let’s calculate the first value of the result. Take the first row of A, [1, 2, 3], and the first column of B, [7, 9, 11]. Multiply corresponding elements and add them:

(1 × 7) + (2 × 9) + (3 × 11)
= 7 + 18 + 33
= 58
Enter fullscreen mode Exit fullscreen mode

That gives us the first value in the resulting matrix. The same process is repeated for every row-column combination.

Using .T to Make Shapes Compatible

Sometimes two matrices have the same shape, so they cannot be multiplied directly.

mat1 = np.random.randint(10, size=(5, 3))
mat2 = np.random.randint(10, size=(5, 3))
Enter fullscreen mode Exit fullscreen mode

Both arrays have shape (5, 3). Direct matrix multiplication is invalid:

(5, 3) @ (5, 3)   <- inner dimensions are 3 and 5
Enter fullscreen mode Exit fullscreen mode

We can transpose the second matrix:

result = mat1 @ mat2.T

print(mat2.T.shape)
# (3, 5)

print(result.shape)
# (5, 5)
Enter fullscreen mode Exit fullscreen mode

Now the operation becomes (5, 3) @ (3, 5) = (5, 5). The inner dimensions match.

9.4 Real-World Example: Calculating Daily Revenue

Imagine a shop selling three types of nut butter:

Product Price
Almond Butter $10
Peanut Butter $8
Cashew Butter $12

The shop records how many units of each product it sells every day. Our goal is to calculate the total revenue for each day.

Step 1: Create the sales data

We’ll type the numbers in directly (rather than generating them randomly) so that your output matches the output shown here exactly.

sales_amount = np.array([
    [12, 15, 0],
    [3, 3, 13],
    [6, 8, 0],
    [6, 18, 8],
    [17, 13, 4]
])
Enter fullscreen mode Exit fullscreen mode

Each row represents a day, and each column represents a product.

              Almond  Peanut  Cashew

Monday          12      15       0
Tuesday          3       3      13
Wednesday        6       8       0
Thursday         6      18       8
Friday          17      13       4
Enter fullscreen mode Exit fullscreen mode
sales_amount.shape
# (5, 3)
Enter fullscreen mode Exit fullscreen mode

That means five days and three products.

Step 2: Store the prices

prices = np.array([10, 8, 12])

prices.shape
# (3,)
Enter fullscreen mode Exit fullscreen mode

The order matches the product columns:

[Almond price, Peanut price, Cashew price]
[10,            8,            12]
Enter fullscreen mode Exit fullscreen mode

Step 3: Understand the calculation

For Monday, we sold [12, 15, 0]. The revenue is:

(12 × 10) + (15 × 8) + (0 × 12)
= 120 + 120 + 0
= $240
Enter fullscreen mode Exit fullscreen mode

We need to perform this calculation for all five days.

Step 4: Let NumPy calculate everything

total_sales = sales_amount @ prices

print(total_sales)
# [240 210 124 300 322]

total_sales.shape
# (5,)
Enter fullscreen mode Exit fullscreen mode

The result contains one total for each day.

Why Does This Work?

sales_amount: (5, 3)
prices:       (3,)

Result:       (5,)
Enter fullscreen mode Exit fullscreen mode

This is a matrix-vector multiplication: (5, 3) @ (3,) = (5,). The inner dimensions match, so the operation is valid. NumPy calculates the dot product of each row with the price vector.

This is a practical example of how matrix multiplication can replace repetitive loops. It is also exactly the computation behind many ML models: many rows of data, one shared set of weights, and a weighted sum for each row.


10. Comparison and Boolean Arrays

NumPy allows us to compare array elements directly. The result is a Boolean array containing True or False.

a = np.array([10, 20, 30, 40, 50])

print(a > 25)
# [False False  True  True  True]
Enter fullscreen mode Exit fullscreen mode

Each element is compared with 25. Other comparison operators include:

a < 30
a >= 30
a <= 40
a == 20
a != 20
Enter fullscreen mode Exit fullscreen mode

Filtering Values with Conditions

Boolean arrays are especially useful for selecting values that satisfy a condition.

result = a[a > 25]

print(result)
# [30 40 50]
Enter fullscreen mode Exit fullscreen mode
  1. a > 25 creates a Boolean array.
  2. NumPy uses that Boolean array to select the matching values.
  3. Only values where the condition is True are returned.

Multiple Conditions

You can combine conditions using & (AND), | (OR), and ~ (NOT). Use parentheses around each condition.

result = a[(a > 20) & (a < 50)]

print(result)
# [30 40]
Enter fullscreen mode Exit fullscreen mode

This selects values greater than 20 AND less than 50.

Important: use & and | with NumPy arrays rather than Python’s and and or operators, which raise an error on arrays.


11. Sorting and Finding Positions

NumPy provides functions for sorting values and finding the positions of important elements.

11.1 Sorting

np.sort() returns a sorted copy of an array.

a = np.array([40, 10, 50, 20, 30])

print(np.sort(a))
# [10 20 30 40 50]

print(a)
# [40 10 50 20 30]   <- the original is unchanged
Enter fullscreen mode Exit fullscreen mode

For a two-dimensional array, sorting can be performed along an axis.

a = np.array([
    [3, 1, 2],
    [0, 5, 4]
])

np.sort(a, axis=1)   # sorts each row
# [[1 2 3]
#  [0 4 5]]

np.sort(a, axis=0)   # sorts each column
# [[0 1 2]
#  [3 5 4]]
Enter fullscreen mode Exit fullscreen mode

11.2 Finding the Index of the Maximum or Minimum

np.argmax() returns the index of the maximum value, and np.argmin() returns the index of the minimum value.

a = np.array([10, 50, 20, 40, 30])

np.argmax(a)
# 1

np.argmin(a)
# 0
Enter fullscreen mode Exit fullscreen mode

The maximum value is 50, located at index 1. The minimum value is 10, located at index 0. For multidimensional arrays, you can also specify an axis.

a = np.array([
    [10, 50, 20],
    [40, 30, 60]
])

np.argmax(a, axis=0)
# [1 0 1]

np.argmax(a, axis=1)
# [1 2]
Enter fullscreen mode Exit fullscreen mode

11.3 Getting the Sorted Indices

np.argsort() returns the indices that would sort an array.

a = np.array([40, 10, 30, 20])

indices = np.argsort(a)

print(indices)
# [1 3 2 0]
Enter fullscreen mode Exit fullscreen mode

These indices represent the original positions of the values in ascending order:

Original values:  [40, 10, 30, 20]
Original indices: [ 0,  1,  2,  3]

Sorted values:    [10, 20, 30, 40]
Sorted indices:   [ 1,  3,  2,  0]
Enter fullscreen mode Exit fullscreen mode

This is useful when you want to know the positions of values after sorting without losing their original relationship.


12. Practical Example: Images as NumPy Arrays

NumPy is not only useful for tables and mathematical calculations. It is also widely used in image processing and computer vision. An image is essentially a collection of pixel values.

For a typical RGB image, each pixel contains three color channels: red, green, and blue. In an 8-bit image, each channel has an intensity value between 0 and 255.

Understanding an RGB Image

An RGB image can be represented as a three-dimensional NumPy array with shape (height, width, 3). For example, (480, 640, 3) means:

  • 480 pixels in height.
  • 640 pixels in width.
  • 3 color values for each pixel.

Loading an Image

from matplotlib.image import imread
import matplotlib.pyplot as plt

image = imread('path/to/image.png')

print(type(image))
# <class 'numpy.ndarray'>

print(image.shape)
# (height, width, channels)

print(image.ndim)
# 3

print(image.dtype, image.max())
# float32 1.0    <- for a PNG; a JPEG gives uint8 and a max up to 255

plt.imshow(image)
plt.axis('off')
plt.show()
Enter fullscreen mode Exit fullscreen mode

The exact shape and data type depend on the image and on the library that loads it. With matplotlib’s imread, PNG files come back as floats between 0 and 1, while JPEG files come back as integers between 0 and 255. Always check .dtype and the maximum value before doing math on pixels. Note that some PNGs also include a fourth, transparency channel (RGBA), giving shape (height, width, 4).

Accessing a Pixel

pixel = image[100, 200]

print(pixel)
Enter fullscreen mode Exit fullscreen mode

This accesses the pixel at row 100, column 200, and returns its color-channel values.

red = image[100, 200, 0]
green = image[100, 200, 1]
blue = image[100, 200, 2]
Enter fullscreen mode Exit fullscreen mode

This is how an image becomes numerical data that a computer can process.

The Connection to Machine Learning

Image
  ↓
Pixels
  ↓
Numerical Values
  ↓
NumPy Array
  ↓
Mathematical Operations
  ↓
Machine Learning Model
Enter fullscreen mode Exit fullscreen mode

An image classifier, for example, processes numerical representations of images rather than understanding pictures the way humans do.


13. Why Is NumPy Faster Than Python Lists?

NumPy operations are often much faster than equivalent Python loops, especially with large numerical arrays.

Using a Python loop:

numbers = list(range(100000))

result = []
for number in numbers:
    result.append(number * 2)
Enter fullscreen mode Exit fullscreen mode

Using NumPy:

numbers = np.arange(100000)

result = numbers * 2
Enter fullscreen mode Exit fullscreen mode

The NumPy version is shorter and often faster. There are two important reasons:

  1. Efficient storage. NumPy arrays generally store elements in a compact, homogeneous block of memory. Python lists store references to Python objects, which adds overhead.
  2. Vectorized operations. Many NumPy operations execute optimized compiled code instead of repeatedly running Python-level loops.

Measuring Performance

In Google Colab or Jupyter Notebook, you can use %timeit to compare execution time.

numbers_list = list(range(100000))
numbers_array = np.arange(100000)

%timeit [x * 2 for x in numbers_list]

%timeit numbers_array * 2
Enter fullscreen mode Exit fullscreen mode

The exact results depend on the computer, array size, and operation. NumPy is not automatically faster for every small task. However, for large numerical operations, its vectorized approach is often significantly more efficient.


14. Converting NumPy Arrays to Pandas

NumPy is particularly useful for numerical arrays and mathematical operations. Pandas provides higher-level tools for structured and tabular data, including labeled rows and columns. You can easily convert a NumPy array into a Pandas DataFrame.

import pandas as pd

a = np.array([
    [1, 2.0, 3.3],
    [4, 5.0, 6.5]
])

df = pd.DataFrame(a)

print(df)
Enter fullscreen mode Exit fullscreen mode

Output:

     0    1    2
0  1.0  2.0  3.3
1  4.0  5.0  6.5
Enter fullscreen mode Exit fullscreen mode

You can also provide column and row names:

df = pd.DataFrame(
    a,
    columns=['A', 'B', 'C'],
    index=['row1', 'row2']
)

print(df)
Enter fullscreen mode Exit fullscreen mode
        A    B    C
row1  1.0  2.0  3.3
row2  4.0  5.0  6.5
Enter fullscreen mode Exit fullscreen mode

This is useful when moving from numerical array operations to dataset exploration and preprocessing.

You can also add the revenue totals from the nut butter example as a new column:

weekly_sales = pd.DataFrame(
    sales_amount,
    index=['Mon', 'Tues', 'Wed', 'Thurs', 'Fri'],
    columns=['Almond Butter', 'Peanut Butter', 'Cashew Butter']
)

weekly_sales['Total ($)'] = total_sales
Enter fullscreen mode Exit fullscreen mode
       Almond Butter  Peanut Butter  Cashew Butter  Total ($)
Mon               12             15              0        240
Tues               3              3             13        210
Wed                6              8              0        124
Thurs              6             18              8        300
Fri               17             13              4        322
Enter fullscreen mode Exit fullscreen mode

15. Common Beginner Mistakes

Here are some mistakes worth remembering.

Mistake 1: Confusing a 1D Array with a 2D Row

a = np.array([1, 2, 3])

a.shape
# (3,)
Enter fullscreen mode Exit fullscreen mode

This is a one-dimensional array, not a matrix with one row. To create a 2D row:

a = np.array([[1, 2, 3]])

a.shape
# (1, 3)
Enter fullscreen mode Exit fullscreen mode

Mistake 2: Confusing Element-wise Multiplication with Matrix Multiplication

a * b   # element-wise multiplication

a @ b   # matrix multiplication (dot product for vectors)
Enter fullscreen mode Exit fullscreen mode

These operations have different meanings and shape requirements.

Mistake 3: Ignoring Array Shapes

Before performing an operation, inspect the shapes.

print(a.shape)
print(b.shape)
Enter fullscreen mode Exit fullscreen mode

This is often the fastest way to identify why an operation is failing.

Mistake 4: Misunderstanding Broadcasting

Remember to compare dimensions from right to left. Two dimensions are compatible if they are equal or at least one is 1.

Mistake 5: Confusing axis=0 and axis=1

For a 2D array:

  • axis=0 aggregates vertically, producing results for each column.
  • axis=1 aggregates horizontally, producing results for each row.

Mistake 6: Forgetting That Slicing Returns a View

Basic NumPy slicing returns a view of the original array rather than an independent copy.

a = np.array([10, 20, 30, 40])

b = a[1:3]
b[0] = 999

print(a)
# [ 10 999  30  40]
Enter fullscreen mode Exit fullscreen mode

Changing b also changed the original array. If you need an independent copy, use a[1:3].copy(). Boolean and fancy indexing, by contrast, always return copies.


16. Key Takeaways and Learning Path

You don’t need to memorize every NumPy function immediately. Focus on understanding the fundamental concepts and knowing how to use them.

What to Remember

Priority Concept Important Things to Know
1 Array fundamentals np.array(), dimensions, shapes
2 Array attributes .shape, .ndim, .size, .dtype
3 Array creation np.zeros(), np.ones(), np.arange(), np.linspace(), random functions
4 Indexing and slicing [row, column], start:stop, multidimensional slicing, views vs. copies
5 Array operations +, -, *, /, **, ufuncs
6 Broadcasting Compare dimensions from right to left, np.newaxis
7 Aggregation np.sum(), np.mean(), np.max(), np.min(), axis, keepdims
8 Shape manipulation .reshape(), .T, np.concatenate(), np.stack()
9 Linear algebra Dot product, @, matrix multiplication, shape compatibility
10 Data selection Boolean indexing with & and `
11 Sorting {% raw %}np.sort(), np.argsort(), np.argmax()
12 Practical applications Images, numerical computing, Pandas

How to Practice

For every new concept, follow this learning cycle:

Understand the idea
       ↓
Study a small example
       ↓
Predict the output
       ↓
Run the code
       ↓
Explain why the result happened
       ↓
Try a slightly different example
Enter fullscreen mode Exit fullscreen mode

Don’t just run the examples. Try to predict the shapes and outputs before executing the code.

Final Advice

If you remember only a few things from this tutorial, remember these:

  1. Understand shapes: always know how your data is organized.
  2. Think element by element: most basic NumPy arithmetic operations work element-wise.
  3. Understand broadcasting: different shapes can work together when their dimensions are compatible.
  4. Learn axis: know whether you are combining rows or columns.
  5. Understand matrix multiplication: the inner dimensions must match.
  6. Practice reading code: predict what an operation will return before running it.

These concepts will appear repeatedly when you work with data preprocessing, regression, neural networks, and other Machine Learning algorithms.

The real goal is to become comfortable thinking in arrays, not simply memorizing NumPy functions.


About This Article

This article is based on my personal learning notes from my portfolio site, where I document concepts, examples, and lessons as I learn.

You can find the original notes here: Introduction to NumPy.

A living collection of what I learn through building, curiosity, and exploration.


Top comments (0)