Topic 10 of 12 | π Lecture 10 β ICT582Topic10.pdf
Numerical Processing with NumPy
Creating and manipulating ndarrays β Python's high-performance array library.
π― Learning Objectives
- Explain what NumPy is and why ndarrays are faster than lists
- Create ndarrays using
np.array(),np.zeros(),np.ones(),np.arange(),np.linspace() - Access ndarray properties:
ndim,shape,size,dtype,itemsize - Index and slice ndarrays (1D and 2D)
- Perform element-wise operations and use universal functions (ufuncs)
- Distinguish copy vs view, and use
np.concatenate()
π Key Concepts
What is NumPy?
NumPy = Numerical Python. A third-party package for multi-dimensional array processing.
pip install numpy # install once
import numpy as np # convention: alias 'np'
NumPy is used alongside SciPy and Matplotlib as a MATLAB replacement. Created by Travis Oliphant in 2005 from Numeric and Numarray.
Why ndarray over list?
- Lists are heterogeneous (different types allowed) but slow for large arrays.
- ndarrays are homogeneous (all elements same type) and stored in one continuous memory block.
- Processing speed can be 50Γ faster than lists for large numerical data.
- NumPy also provides many convenient functions for multi-dimensional array operations.
Creating ndarrays
import numpy as np
# From a list or tuple
a = np.array([1, 2, 3, 4]) # 1D
b = np.array([[1,2,3],[4,5,6]]) # 2D (2Γ3)
# Special constructors
np.zeros((2, 4)) # 2Γ4 filled with 0.0
np.ones((3, 2)) # 3Γ2 filled with 1.0
np.empty((3, 5)) # 3Γ5 with uninitialized values
# arange (like range, returns ndarray)
np.arange(5) # [0 1 2 3 4]
np.arange(2, 8) # [2 3 4 5 6 7]
np.arange(1, 7, 2) # [1 3 5]
np.arange(12).reshape(3, 4) # 3Γ4 matrix
# linspace β n evenly spaced values
np.linspace(0, 3, 9) # 9 values from 0 to 3
np.linspace(0, np.pi, 10) # 10 values 0 to Ο
Ndarray Properties
a = np.array([[[1,2,3,4],
[5,6,7,8],
[9,0,1,2]],
[[9,8,7,6],
[5,4,3,2],
[1,0,9,8]]])
print(a.ndim) # 3 (number of dimensions)
print(a.shape) # (2, 3, 4) (size of each dimension)
print(a.size) # 24 (total elements)
print(a.dtype) # int64 (element type)
print(a.itemsize) # 8 (bytes per element)
| Property | Meaning |
|---|---|
ndim | Number of dimensions (axes) |
shape | Tuple: size along each axis |
size | Total number of elements |
dtype | Data type of elements (e.g. int64, float64) |
itemsize | Bytes per element |
Dimensions and Shapes Visualised
| Code | ndim | shape | size |
|---|---|---|---|
np.array(3) | 0 | () | 1 |
np.array([2,1,3,4]) | 1 | (4,) | 4 |
np.array([[2,1,3,4],[1,3,5,7],[4,6,1,3]]) | 2 | (3,4) | 12 |
Element Type (dtype)
Ndarrays are homogeneous β all elements must be the same type. If you mix int and float, all become float.
a = np.array([2, 3.1, 4, 5])
print(a) # [2. 3.1 4. 5. ] (all float)
# Specify dtype explicitly
b = np.array([2, 4, 5, 10], dtype=np.int16)
c = np.arange(1, 5, dtype=float) # [1. 2. 3. 4.]
Indexing Ndarrays
a = np.array([1, 2, 3, 4])
print(a[2]) # 3 (positive index)
print(a[-2]) # 3 (negative index)
# 2D indexing: [row, col]
b = np.array([[1,2,3,4],
[5,6,7,8]])
print(b[1, 2]) # 7 (row 1, col 2)
print(b[0, -1]) # 4 (row 0, last col)
Slicing Ndarrays
Same syntax as lists: [start:stop:step]. Stop is exclusive.
a = np.array([1,2,3,4,5,6,7,8])
print(a[1:5]) # [2 3 4 5]
print(a[:5:2]) # [1 3 5]
# 2D slicing
c = np.arange(12).reshape(3, 4)
# [[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]]
print(c[1, :]) # [4 5 6 7] (row 1)
print(c[:, 2]) # [2 6 10] (col 2)
print(c[0:2, 1:3]) # [[1 2][5 6]]
Copy vs View
- A slice of an ndarray creates a view (no copy). Modifying the slice modifies the original.
- Use
.copy()to get an independent copy.
a = np.array([1, 2, 3, 4])
b = a[1:3] # view β shares memory with a
b[0] = 99
print(a) # [1 99 3 4] (a was also changed!)
c = a[1:3].copy() # independent copy
c[0] = 0
print(a) # [1 99 3 4] (a unchanged)
Element-wise Operations
Arithmetic operations apply to every element β no loop needed.
a = np.array([1, 2, 3, 4])
print(a + 10) # [11 12 13 14]
print(a * 2) # [ 2 4 6 8]
print(a ** 2) # [ 1 4 9 16]
b = np.array([10, 20, 30, 40])
print(a + b) # [11 22 33 44]
print(a * b) # [10 40 90 160]
Universal Functions (ufuncs)
NumPy provides element-wise mathematical functions called ufuncs:
a = np.array([1, 4, 9, 16])
print(np.sqrt(a)) # [1. 2. 3. 4.]
print(np.exp(a)) # e^1, e^4, ...
print(np.log(a)) # natural log
print(np.sin(a)) # sine of each
print(np.abs(a)) # absolute value
Matrix Multiplication
A = np.array([[1,2],[3,4]])
B = np.array([[5,6],[7,8]])
print(A * B) # Element-wise: [[ 5 12][21 32]]
print(A @ B) # Matrix multiplication: [[19 22][43 50]]
# Or: np.matmul(A, B)
Concatenate Arrays
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
c = np.concatenate([a, b]) # [1 2 3 4 5 6]
β οΈ Exam Focus
- Know why ndarrays are faster than lists: homogeneous, contiguous memory, 50Γ speed.
- Know ndarray properties:
ndim,shape,size,dtype,itemsizeand what they return. arangevslinspace: arange = fixed step; linspace = fixed number of values.- Slicing creates a view (not a copy) β modifying a slice modifies the original. Use
.copy()for independence. - Element-wise:
a * bmultiplies corresponding elements;a @ bis matrix multiplication.
β Common Mistakes
- Confusing element-wise multiplication
A * Bwith matrix multiplicationA @ B. - Not knowing that slicing an ndarray creates a view β modifying it changes the original.
- Mixing types in an array expecting them to stay separate β all become float if there's a mix.
- Thinking
np.zeros((3,4))needs a 2-argument call β it takes a tuple(3,4), not two arguments.
β‘ Quick Recap
- NumPy = Numerical Python. Install with
pip install numpy. Import asnp. - Ndarray: homogeneous, contiguous memory, 50Γ faster than list for numerical ops.
- Create:
np.array(),np.zeros(),np.ones(),np.arange(),np.linspace(). - Properties:
ndim(dims),shape(sizes),size(total),dtype(type). - Indexing:
a[i]for 1D,a[i,j]for 2D. Slicing same as list. - Slice = view (shared memory). Use
.copy()for an independent copy. - Element-wise:
a + b,a * b. Matrix mult:A @ B.