Chapter 03 · Section I · 24 min read
NumPy: working with arrays of numbers
NumPy turns a Python list of numbers into something a thousand times faster to operate on. Every numerical library in the AI ecosystem — pandas, scikit-learn, PyTorch — is built on top of it.
In the last chapter you held numbers in plain Python lists. For five or ten of them that is fine. For five million — the size of a dataset of every Khalti transaction in a year, or every weather reading across Nepal in a day — plain Python lists become painfully slow. NumPy is the library every working numerical Python program reaches for the moment lists stop being fast enough. Everything you do for the rest of the track sits on top of it.
Installing NumPy
If you have not already, install it in your project. In a terminal, from your project folder:
pip install numpy
In a notebook you can do the same from a cell by prefixing the line with !:
!pip install numpy
You only do this once per project. (We will properly understand why once per project, and what “project” means, in Chapter 4.)
To check it works, in a notebook cell:
import numpy as np
print(np.__version__)
import numpy as np is the universal convention. Almost every NumPy tutorial, library, and book uses np as the short name. Follow the crowd.
Your first array
An array is a NumPy version of a list. You make one from a Python list:
import numpy as np
temps = np.array([22.4, 24.1, 19.8, 21.7, 23.0])
print(temps)
This prints:
[22.4 24.1 19.8 21.7 23. ]
It looks like a list with no commas. That is roughly what it is — but it is also much faster to do maths on, because internally NumPy stores the numbers in one tight block of memory and uses optimised C code to operate on them.
A few useful built-ins:
temps.mean() # 22.2 — the average
temps.max() # 24.1
temps.min() # 19.8
temps.sum() # 111.0
len(temps) # 5
temps.shape # (5,) — the dimensions of the array
shape is one of the most important NumPy concepts. We will get to two-dimensional arrays in a moment, and once we do, knowing the shape becomes essential.
The big idea: vectorisation
In plain Python, if you want to convert a list of Celsius temperatures to Fahrenheit, you write a loop:
celsius = [22.4, 24.1, 19.8, 21.7, 23.0]
fahrenheit = []
for c in celsius:
fahrenheit.append(c * 9 / 5 + 32)
In NumPy, you write what you mean:
celsius = np.array([22.4, 24.1, 19.8, 21.7, 23.0])
fahrenheit = celsius * 9 / 5 + 32
One line. No loop. NumPy applies the operation to every element at once, and does it in fast compiled code. This is called vectorisation, and it is the single biggest reason NumPy exists. Once you internalise it, you write very different code.
A few more examples:
temps + 5 # add 5 to every temperature
temps * 2 # double every temperature
temps > 22 # array of True/False — is each temp above 22?
temps[temps > 22] # only the temperatures above 22
The last line is a small revelation. NumPy lets you index by condition — give it an array of booleans and it returns the elements where the condition is True. This pattern, called boolean masking, is everywhere in real data work.
Two dimensions: a small matrix
Real data is rarely just a row. It is rows and columns — a list of weather stations, each with seven days of readings; a list of Khalti accounts, each with a year of monthly balances. NumPy handles this with a 2D array:
readings = np.array([
[22.4, 24.1, 19.8, 21.7, 23.0], # Kathmandu
[28.6, 29.1, 27.4, 28.9, 28.3], # Birgunj
[15.2, 16.4, 14.0, 15.8, 15.1], # Jomsom
])
print(readings.shape) # (3, 5) — 3 rows, 5 columns
print(readings.mean()) # average of everything
print(readings.mean(axis=1)) # average per row → per city
print(readings.mean(axis=0)) # average per column → per day
shape tells you the number of rows and columns. axis=1 says “collapse the columns, give me one value per row”. axis=0 says “collapse the rows, give me one value per column”. axis is a small idea you will get wrong the first three or four times you use it; that is normal.
You can also index into specific rows and columns:
readings[0] # first row — all Kathmandu readings
readings[0, 2] # row 0, column 2 — Kathmandu on day 3
readings[:, 2] # column 2 — every city's day-3 reading
The colon means “all of them, along this axis”.
Common operations you will write a hundred times
A short list, all of which you should try in a notebook before the quiz:
np.zeros(5) # array of five zeros
np.ones((3, 4)) # 3×4 array of ones
np.arange(0, 10, 2) # [0, 2, 4, 6, 8]
np.linspace(0, 1, 11) # [0.0, 0.1, 0.2, ..., 1.0] — 11 evenly spaced
np.random.rand(5) # 5 random floats between 0 and 1
np.arange is the NumPy version of Python’s range. np.linspace is invaluable when you need a fixed number of evenly spaced points (e.g., for plotting). np.random.rand is your way of cheaply generating fake data when you are testing something.
Check your understanding
Quick check
—What does this code print? import numpy as np a = np.array([10, 20, 30, 40, 50]) print((a > 25).sum())
Quick check
—A 2D array has shape (4, 6). Which of the following is true?
What comes next
NumPy is the engine. pandas, in the next section, is the friendlier interface for the most common case: tables with named columns. Pandas DataFrames are built directly on top of NumPy arrays — every speed advantage you just met carries forward, with the added convenience of names instead of column indices. After that, matplotlib lets you finally see what your numbers are doing.