⏱️ 45 min

NumPy Fundamentals

Master array operations and numerical computing with NumPy

Introduction to NumPy

NumPy (Numerical Python) is the fundamental package for scientific computing in Python. It provides support for large, multi-dimensional arrays and matrices, along with a collection of mathematical functions to operate on these arrays efficiently.

Why NumPy?

- **Fast**: Written in C, NumPy operations are much faster than Python lists - **Memory Efficient**: Uses contiguous memory blocks - **Convenient**: Rich set of mathematical functions - **Foundation**: Base for pandas, scikit-learn, TensorFlow, and PyTorch

Creating NumPy Arrays

Let's start by creating arrays in different ways:

python
import numpy as np

# From Python list
arr1 = np.array([1, 2, 3, 4, 5])
print(f"1D Array: {arr1}")
print(f"Shape: {arr1.shape}, Dtype: {arr1.dtype}")

# 2D Array (Matrix)
arr2 = np.array([[1, 2, 3], [4, 5, 6]])
print(f"\n2D Array:\n{arr2}")
print(f"Shape: {arr2.shape}")

# Zeros and Ones
zeros = np.zeros((3, 4))
ones = np.ones((2, 3))
print(f"\nZeros:\n{zeros}")

# Range of values
range_arr = np.arange(0, 10, 2)  # Start, stop, step
print(f"\nRange: {range_arr}")

# Linearly spaced values
linspace = np.linspace(0, 1, 5)  # Start, stop, count
print(f"Linspace: {linspace}")

# Random arrays
random_arr = np.random.rand(3, 3)  # Uniform [0, 1)
random_normal = np.random.randn(3, 3)  # Standard normal
print(f"\nRandom:\n{random_arr}")
Output:
1D Array: [1 2 3 4 5]
Shape: (5,), Dtype: int64

2D Array:
[[1 2 3]
 [4 5 6]]
Shape: (2, 3)

Zeros:
[[0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]]

Range: [0 2 4 6 8]
Linspace: [0.   0.25 0.5  0.75 1.  ]

Random:
[[0.5488 0.7152 0.6028]
 [0.5449 0.4237 0.6459]
 [0.4376 0.8918 0.9637]]

Array Operations

NumPy supports vectorized operations - operations applied to entire arrays at once:

python
import numpy as np

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

# Element-wise operations
print(f"Addition: {a + b}")
print(f"Multiplication: {a * b}")
print(f"Power: {a ** 2}")
print(f"Square root: {np.sqrt(a)}")

# Comparison
print(f"\nGreater than 2: {a > 2}")
print(f"Values > 2: {a[a > 2]}")

# Aggregate functions
arr = np.array([[1, 2, 3], [4, 5, 6]])
print(f"\nSum: {np.sum(arr)}")
print(f"Mean: {np.mean(arr)}")
print(f"Max: {np.max(arr)}")
print(f"Min: {np.min(arr)}")

# Along axis
print(f"Sum by column: {np.sum(arr, axis=0)}")
print(f"Sum by row: {np.sum(arr, axis=1)}")
Output:
Addition: [ 6  8 10 12]
Multiplication: [ 5 12 21 32]
Power: [ 1  4  9 16]
Square root: [1.         1.41421356 1.73205081 2.        ]

Greater than 2: [False False  True  True]
Values > 2: [3 4]

Sum: 21
Mean: 3.5
Max: 6
Min: 1
Sum by column: [5 7 9]
Sum by row: [ 6 15]

Array Indexing and Slicing

Access and manipulate array elements:

python
import numpy as np

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

# Indexing
print(f"First element: {arr[0]}")
print(f"Last element: {arr[-1]}")

# Slicing [start:stop:step]
print(f"First 5: {arr[:5]}")
print(f"Last 3: {arr[-3:]}")
print(f"Every 2nd: {arr[::2]}")
print(f"Reverse: {arr[::-1]}")

# 2D indexing
arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(f"\n2D Array:\n{arr2d}")
print(f"Element at [1, 2]: {arr2d[1, 2]}")
print(f"First row: {arr2d[0, :]}")
print(f"Last column: {arr2d[:, -1]}")

# Boolean indexing
mask = arr2d > 5
print(f"\nMask:\n{mask}")
print(f"Values > 5: {arr2d[mask]}")
Output:
First element: 0
Last element: 9
First 5: [0 1 2 3 4]
Last 3: [7 8 9]
Every 2nd: [0 2 4 6 8]
Reverse: [9 8 7 6 5 4 3 2 1 0]

2D Array:
[[1 2 3]
 [4 5 6]
 [7 8 9]]
Element at [1, 2]: 6
First row: [1 2 3]
Last column: [3 6 9]

Mask:
[[False False False]
 [False False  True]
 [ True  True  True]]
Values > 5: [6 7 8 9]

Reshaping Arrays

Change array dimensions while preserving data:

python
import numpy as np

arr = np.arange(12)
print(f"Original: {arr}")
print(f"Shape: {arr.shape}")

# Reshape
reshaped = arr.reshape(3, 4)
print(f"\nReshaped (3x4):\n{reshaped}")

# Transpose
transposed = reshaped.T
print(f"\nTransposed (4x3):\n{transposed}")

# Flatten
flattened = reshaped.flatten()
print(f"\nFlattened: {flattened}")

# Add/remove dimensions
expanded = arr[np.newaxis, :]  # Add row dimension
print(f"\nExpanded shape: {expanded.shape}")

squeezed = expanded.squeeze()  # Remove size-1 dimensions
print(f"Squeezed shape: {squeezed.shape}")
Output:
Original: [ 0  1  2  3  4  5  6  7  8  9 10 11]
Shape: (12,)

Reshaped (3x4):
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]

Transposed (4x3):
[[ 0  4  8]
 [ 1  5  9]
 [ 2  6 10]
 [ 3  7 11]]

Flattened: [ 0  1  2  3  4  5  6  7  8  9 10 11]

Expanded shape: (1, 12)
Squeezed shape: (12,)

Matrix Operations

Linear algebra operations essential for machine learning:

python
import numpy as np

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

print(f"Matrix A:\n{A}")
print(f"\nMatrix B:\n{B}")

# Matrix multiplication
C = np.dot(A, B)  # or A @ B
print(f"\nA @ B:\n{C}")

# Element-wise multiplication
element_wise = A * B
print(f"\nElement-wise A * B:\n{element_wise}")

# Transpose
print(f"\nA.T:\n{A.T}")

# Determinant
det = np.linalg.det(A)
print(f"\nDeterminant of A: {det}")

# Inverse
inv = np.linalg.inv(A)
print(f"\nInverse of A:\n{inv}")

# Verify: A @ A^-1 = I
identity = A @ inv
print(f"\nA @ A^-1 (should be I):\n{identity}")
Output:
Matrix A:
[[1 2]
 [3 4]]

Matrix B:
[[5 6]
 [7 8]]

A @ B:
[[19 22]
 [43 50]]

Element-wise A * B:
[[ 5 12]
 [21 32]]

A.T:
[[1 3]
 [2 4]]

Determinant of A: -2.0

Inverse of A:
[[-2.   1. ]
 [ 1.5 -0.5]]

A @ A^-1 (should be I):
[[1.0000000e+00 0.0000000e+00]
 [8.8817842e-16 1.0000000e+00]]

Practice Exercise

Try these exercises to test your understanding:

python
# Exercise 1: Create a 5x5 matrix with values ranging from 0 to 24
# Your code here:
matrix = np.arange(25).reshape(5, 5)

# Exercise 2: Extract all values from matrix that are divisible by 3
# Your code here:
divisible_by_3 = matrix[matrix % 3 == 0]

# Exercise 3: Create a 3x3 identity matrix
# Your code here:
identity = np.eye(3)

# Exercise 4: Calculate mean of each row in the matrix
# Your code here:
row_means = np.mean(matrix, axis=1)

# Exercise 5: Normalize the matrix (subtract mean, divide by std)
# Your code here:
normalized = (matrix - np.mean(matrix)) / np.std(matrix)

print("Check your answers below!")
Output:
Matrix:
[[ 0  1  2  3  4]
 [ 5  6  7  8  9]
 [10 11 12 13 14]
 [15 16 17 18 19]
 [20 21 22 23 24]]

Divisible by 3: [ 0  3  6  9 12 15 18 21 24]

Identity:
[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]

Row means: [ 2.  7. 12. 17. 22.]

Normalized matrix (first row):
[-1.61 -1.48 -1.35 -1.22 -1.08]
Sharan Initiatives - Making a Difference Together